mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-02-15 01:40:15 +01:00
chore: merge master
This commit is contained in:
commit
5e8f247e84
1
.github/ISSUE_TEMPLATE/bug.yaml
vendored
1
.github/ISSUE_TEMPLATE/bug.yaml
vendored
@ -44,6 +44,7 @@ body:
|
|||||||
options:
|
options:
|
||||||
- Docker
|
- Docker
|
||||||
- Debian/PPA
|
- Debian/PPA
|
||||||
|
- Windows Tray App
|
||||||
- Built from source
|
- Built from source
|
||||||
- Other
|
- Other
|
||||||
validations:
|
validations:
|
||||||
|
@ -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,13 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="sm:w-80 w-full relative">
|
<div class="">
|
||||||
<form @submit.prevent="submitSearch">
|
<div class="w-full relative sm:w-80">
|
||||||
<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 @submit.prevent="submitSearch">
|
||||||
</form>
|
<ui-text-input ref="input" v-model="search" :placeholder="$strings.PlaceholderSearch" @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" />
|
||||||
<div class="absolute top-0 right-0 bottom-0 h-full flex items-center px-2 text-gray-400 cursor-pointer" @click="clickClear">
|
</form>
|
||||||
<span v-if="!search" class="material-icons" style="font-size: 1.2rem">search</span>
|
<div class="absolute top-0 right-0 bottom-0 h-full flex items-center px-2 text-gray-400 cursor-pointer" @click="clickClear">
|
||||||
<span v-else class="material-icons" style="font-size: 1.2rem">close</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>
|
||||||
|
</div>
|
||||||
</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 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>
|
||||||
|
@ -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>
|
@ -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) {
|
||||||
|
@ -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>
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
<div class="w-full p-1">
|
<div class="w-full p-1">
|
||||||
<ui-textarea-with-label v-model="newEpisode.subtitle" :label="$strings.LabelSubtitle" :rows="3" />
|
<ui-textarea-with-label v-model="newEpisode.subtitle" :label="$strings.LabelSubtitle" :rows="3" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full p-1 default-style">
|
<div class="w-full p-1">
|
||||||
<ui-rich-text-editor :label="$strings.LabelDescription" v-model="newEpisode.description" />
|
<ui-rich-text-editor :label="$strings.LabelDescription" v-model="newEpisode.description" />
|
||||||
</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>
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -316,6 +316,7 @@ export default {
|
|||||||
reader.rendition = reader.book.renderTo('viewer', {
|
reader.rendition = reader.book.renderTo('viewer', {
|
||||||
width: this.readerWidth,
|
width: this.readerWidth,
|
||||||
height: this.readerHeight * 0.8,
|
height: this.readerHeight * 0.8,
|
||||||
|
allowScriptedContent: true,
|
||||||
spread: 'auto',
|
spread: 'auto',
|
||||||
snap: true,
|
snap: true,
|
||||||
manager: 'continuous',
|
manager: 'continuous',
|
||||||
|
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">
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="default-style">
|
||||||
<p v-if="label" class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': disabled }">
|
<p v-if="label" class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': disabled }">
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</p>
|
</p>
|
||||||
@ -29,31 +29,31 @@ export default {
|
|||||||
config() {
|
config() {
|
||||||
return {
|
return {
|
||||||
toolbar: {
|
toolbar: {
|
||||||
getDefaultHTML: () => ` <div class="trix-button-row">
|
getDefaultHTML: () => `<div class="trix-button-row">
|
||||||
<span class="trix-button-group trix-button-group--text-tools" data-trix-button-group="text-tools">
|
<span class="trix-button-group trix-button-group--text-tools" data-trix-button-group="text-tools">
|
||||||
<button type="button" class="trix-button trix-button--icon trix-button--icon-bold" data-trix-attribute="bold" data-trix-key="b" title="#{lang.bold}" tabindex="-1">#{lang.bold}</button>
|
<button type="button" class="trix-button trix-button--icon trix-button--icon-bold" data-trix-attribute="bold" data-trix-key="b" title="${this.$strings.LabelFontBold}" tabindex="-1">${this.$strings.LabelFontBold}</button>
|
||||||
<button type="button" class="trix-button trix-button--icon trix-button--icon-italic" data-trix-attribute="italic" data-trix-key="i" title="#{lang.italic}" tabindex="-1">#{lang.italic}</button>
|
<button type="button" class="trix-button trix-button--icon trix-button--icon-italic" data-trix-attribute="italic" data-trix-key="i" title="${this.$strings.LabelFontItalic}" tabindex="-1">${this.$strings.LabelFontItalic}</button>
|
||||||
<button type="button" class="trix-button trix-button--icon trix-button--icon-strike" data-trix-attribute="strike" title="#{lang.strike}" tabindex="-1">#{lang.strike}</button>
|
<button type="button" class="trix-button trix-button--icon trix-button--icon-strike" data-trix-attribute="strike" title="${this.$strings.LabelFontStrikethrough}" tabindex="-1">${this.$strings.LabelFontStrikethrough}</button>
|
||||||
<button type="button" class="trix-button trix-button--icon trix-button--icon-link" data-trix-attribute="href" data-trix-action="link" data-trix-key="k" title="#{lang.link}" tabindex="-1">#{lang.link}</button>
|
<button type="button" class="trix-button trix-button--icon trix-button--icon-link" data-trix-attribute="href" data-trix-action="link" data-trix-key="k" title="${this.$strings.LabelTextEditorLink}" tabindex="-1">${this.$strings.LabelTextEditorLink}</button>
|
||||||
</span>
|
</span>
|
||||||
<span class="trix-button-group trix-button-group--block-tools" data-trix-button-group="block-tools">
|
<span class="trix-button-group trix-button-group--block-tools" data-trix-button-group="block-tools">
|
||||||
<button type="button" class="trix-button trix-button--icon trix-button--icon-bullet-list" data-trix-attribute="bullet" title="#{lang.bullets}" tabindex="-1">#{lang.bullets}</button>
|
<button type="button" class="trix-button trix-button--icon trix-button--icon-bullet-list" data-trix-attribute="bullet" title="${this.$strings.LabelTextEditorBulletedList}" tabindex="-1">${this.$strings.LabelTextEditorBulletedList}</button>
|
||||||
<button type="button" class="trix-button trix-button--icon trix-button--icon-number-list" data-trix-attribute="number" title="#{lang.numbers}" tabindex="-1">#{lang.numbers}</button>
|
<button type="button" class="trix-button trix-button--icon trix-button--icon-number-list" data-trix-attribute="number" title="${this.$strings.LabelTextEditorNumberedList}" tabindex="-1">${this.$strings.LabelTextEditorNumberedList}</button>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="trix-button-group-spacer"></span>
|
<span class="trix-button-group-spacer"></span>
|
||||||
<span class="trix-button-group trix-button-group--history-tools" data-trix-button-group="history-tools">
|
<span class="trix-button-group trix-button-group--history-tools" data-trix-button-group="history-tools">
|
||||||
<button type="button" class="trix-button trix-button--icon trix-button--icon-undo" data-trix-action="undo" data-trix-key="z" title="#{lang.undo}" tabindex="-1">#{lang.undo}</button>
|
<button type="button" class="trix-button trix-button--icon trix-button--icon-undo" data-trix-action="undo" data-trix-key="z" title="${this.$strings.LabelUndo}" tabindex="-1">${this.$strings.LabelUndo}</button>
|
||||||
<button type="button" class="trix-button trix-button--icon trix-button--icon-redo" data-trix-action="redo" data-trix-key="shift+z" title="#{lang.redo}" tabindex="-1">#{lang.redo}</button>
|
<button type="button" class="trix-button trix-button--icon trix-button--icon-redo" data-trix-action="redo" data-trix-key="shift+z" title="${this.$strings.LabelRedo}" tabindex="-1">${this.$strings.LabelRedo}</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="trix-dialogs" data-trix-dialogs>
|
<div class="trix-dialogs" data-trix-dialogs>
|
||||||
<div class="trix-dialog trix-dialog--link" data-trix-dialog="href" data-trix-dialog-attribute="href">
|
<div class="trix-dialog trix-dialog--link" data-trix-dialog="href" data-trix-dialog-attribute="href">
|
||||||
<div class="trix-dialog__link-fields">
|
<div class="trix-dialog__link-fields">
|
||||||
<input type="url" name="href" class="trix-input trix-input--dialog" placeholder="#{lang.urlPlaceholder}" aria-label="#{lang.url}" required data-trix-input>
|
<input type="url" name="href" class="trix-input trix-input--dialog" placeholder="" aria-label="URL" required data-trix-input>
|
||||||
<div class="trix-button-group">
|
<div class="trix-button-group">
|
||||||
<input type="button" class="trix-button trix-button--dialog" value="#{lang.link}" data-trix-method="setAttribute">
|
<input type="button" class="trix-button trix-button--dialog" value="${this.$strings.LabelTextEditorLink}" data-trix-method="setAttribute">
|
||||||
<input type="button" class="trix-button trix-button--dialog" value="#{lang.unlink}" data-trix-method="removeAttribute">
|
<input type="button" class="trix-button trix-button--dialog" value="${this.$strings.LabelTextEditorUnlink}" data-trix-method="removeAttribute">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -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,16 +104,16 @@ 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',
|
||||||
sizes: "any"
|
sizes: 'any'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: (process.env.ROUTER_BASE_PATH || '') + '/icon64.png',
|
src: (process.env.ROUTER_BASE_PATH || '') + '/icon192.png',
|
||||||
type: "image/png",
|
type: 'image/png',
|
||||||
sizes: "64x64"
|
sizes: 'any'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
6
client/package-lock.json
generated
6
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",
|
||||||
@ -16976,4 +16976,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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",
|
||||||
@ -36,4 +36,4 @@
|
|||||||
"postcss": "^8.3.6",
|
"postcss": "^8.3.6",
|
||||||
"tailwindcss": "^3.4.1"
|
"tailwindcss": "^3.4.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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.$router.push('/login')
|
|
||||||
|
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')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.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)
|
||||||
@ -113,4 +125,19 @@ export default {
|
|||||||
this.$root.socket.off('author_removed', this.authorRemoved)
|
this.$root.socket.off('author_removed', this.authorRemoved)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</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>
|
@ -1,6 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="authentication-settings">
|
<div id="authentication-settings">
|
||||||
<app-settings-content :header-text="$strings.HeaderAuthentication">
|
<app-settings-content :header-text="$strings.HeaderAuthentication">
|
||||||
|
<div class="w-full border border-white/10 rounded-xl p-4 my-4 bg-primary/25">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<ui-checkbox v-model="showCustomLoginMessage" checkbox-bg="bg" />
|
||||||
|
<p class="text-lg pl-4">Custom Message on Login</p>
|
||||||
|
</div>
|
||||||
|
<transition name="slide">
|
||||||
|
<div v-if="showCustomLoginMessage" class="w-full pt-4">
|
||||||
|
<ui-rich-text-editor v-model="newAuthSettings.authLoginCustomMessage" />
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="w-full border border-white/10 rounded-xl p-4 my-4 bg-primary/25">
|
<div class="w-full border border-white/10 rounded-xl p-4 my-4 bg-primary/25">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<ui-checkbox v-model="enableLocalAuth" checkbox-bg="bg" />
|
<ui-checkbox v-model="enableLocalAuth" checkbox-bg="bg" />
|
||||||
@ -103,6 +115,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
enableLocalAuth: false,
|
enableLocalAuth: false,
|
||||||
enableOpenIDAuth: false,
|
enableOpenIDAuth: false,
|
||||||
|
showCustomLoginMessage: false,
|
||||||
savingSettings: false,
|
savingSettings: false,
|
||||||
newAuthSettings: {}
|
newAuthSettings: {}
|
||||||
}
|
}
|
||||||
@ -193,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,6 +234,10 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.showCustomLoginMessage || !this.newAuthSettings.authLoginCustomMessage?.trim()) {
|
||||||
|
this.newAuthSettings.authLoginCustomMessage = null
|
||||||
|
}
|
||||||
|
|
||||||
this.newAuthSettings.authActiveAuthMethods = []
|
this.newAuthSettings.authActiveAuthMethods = []
|
||||||
if (this.enableLocalAuth) this.newAuthSettings.authActiveAuthMethods.push('local')
|
if (this.enableLocalAuth) this.newAuthSettings.authActiveAuthMethods.push('local')
|
||||||
if (this.enableOpenIDAuth) this.newAuthSettings.authActiveAuthMethods.push('openid')
|
if (this.enableOpenIDAuth) this.newAuthSettings.authActiveAuthMethods.push('openid')
|
||||||
@ -250,6 +267,7 @@ export default {
|
|||||||
}
|
}
|
||||||
this.enableLocalAuth = this.authMethods.includes('local')
|
this.enableLocalAuth = this.authMethods.includes('local')
|
||||||
this.enableOpenIDAuth = this.authMethods.includes('openid')
|
this.enableOpenIDAuth = this.authMethods.includes('openid')
|
||||||
|
this.showCustomLoginMessage = !!this.authSettings.authLoginCustomMessage
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
@ -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>
|
@ -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 []
|
||||||
})
|
})
|
||||||
|
@ -28,6 +28,8 @@
|
|||||||
|
|
||||||
<div class="w-full h-px bg-white bg-opacity-10 my-4" />
|
<div class="w-full h-px bg-white bg-opacity-10 my-4" />
|
||||||
|
|
||||||
|
<p v-if="loginCustomMessage" class="py-2 default-style mb-2" v-html="loginCustomMessage"></p>
|
||||||
|
|
||||||
<p v-if="error" class="text-error text-center py-2">{{ error }}</p>
|
<p v-if="error" class="text-error text-center py-2">{{ error }}</p>
|
||||||
|
|
||||||
<form v-show="login_local" @submit.prevent="submitForm">
|
<form v-show="login_local" @submit.prevent="submitForm">
|
||||||
@ -113,6 +115,9 @@ export default {
|
|||||||
},
|
},
|
||||||
openIDButtonText() {
|
openIDButtonText() {
|
||||||
return this.authFormData?.authOpenIDButtonText || 'Login with OpenId'
|
return this.authFormData?.authOpenIDButtonText || 'Login with OpenId'
|
||||||
|
},
|
||||||
|
loginCustomMessage() {
|
||||||
|
return this.authFormData?.authLoginCustomMessage || null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -14,9 +14,11 @@ const languageCodeMap = {
|
|||||||
'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 +30,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 +97,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 |
@ -99,7 +99,7 @@ export const getters = {
|
|||||||
return `http://localhost:3333${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}&ts=${lastUpdate}${raw ? '&raw=1' : ''}`
|
return `http://localhost:3333${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}&ts=${lastUpdate}${raw ? '&raw=1' : ''}`
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}&ts=${lastUpdate}`
|
return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}&ts=${lastUpdate}${raw ? '&raw=1' : ''}`
|
||||||
},
|
},
|
||||||
getLibraryItemCoverSrcById: (state, getters, rootState, rootGetters) => (libraryItemId, timestamp = null, raw = false) => {
|
getLibraryItemCoverSrcById: (state, getters, rootState, rootGetters) => (libraryItemId, timestamp = null, raw = false) => {
|
||||||
const placeholder = `${rootState.routerBasePath}/book_placeholder.jpg`
|
const placeholder = `${rootState.routerBasePath}/book_placeholder.jpg`
|
||||||
|
@ -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,15 @@
|
|||||||
"ButtonMatchAllAuthors": "Spárovat všechny autory",
|
"ButtonMatchAllAuthors": "Spárovat všechny autory",
|
||||||
"ButtonMatchBooks": "Spárovat Knihy",
|
"ButtonMatchBooks": "Spárovat Knihy",
|
||||||
"ButtonNevermind": "Nevadí",
|
"ButtonNevermind": "Nevadí",
|
||||||
|
"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",
|
||||||
|
"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í",
|
||||||
@ -104,6 +109,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",
|
||||||
@ -281,8 +287,11 @@
|
|||||||
"LabelFinished": "Dokončeno",
|
"LabelFinished": "Dokončeno",
|
||||||
"LabelFolder": "Složka",
|
"LabelFolder": "Složka",
|
||||||
"LabelFolders": "Složky",
|
"LabelFolders": "Složky",
|
||||||
|
"LabelFontBold": "Bold",
|
||||||
"LabelFontFamily": "Rodina písem",
|
"LabelFontFamily": "Rodina písem",
|
||||||
|
"LabelFontItalic": "Italic",
|
||||||
"LabelFontScale": "Měřítko písma",
|
"LabelFontScale": "Měřítko písma",
|
||||||
|
"LabelFontStrikethrough": "Strikethrough",
|
||||||
"LabelFormat": "Formát",
|
"LabelFormat": "Formát",
|
||||||
"LabelGenre": "Žánr",
|
"LabelGenre": "Žánr",
|
||||||
"LabelGenres": "Žánry",
|
"LabelGenres": "Žánry",
|
||||||
@ -387,6 +396,7 @@
|
|||||||
"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)",
|
||||||
@ -403,6 +413,7 @@
|
|||||||
"LabelRecentlyAdded": "Nedávno přidané",
|
"LabelRecentlyAdded": "Nedávno přidané",
|
||||||
"LabelRecentSeries": "Nedávné série",
|
"LabelRecentSeries": "Nedávné série",
|
||||||
"LabelRecommended": "Doporučeno",
|
"LabelRecommended": "Doporučeno",
|
||||||
|
"LabelRedo": "Redo",
|
||||||
"LabelRegion": "Region",
|
"LabelRegion": "Region",
|
||||||
"LabelReleaseDate": "Datum vydání",
|
"LabelReleaseDate": "Datum vydání",
|
||||||
"LabelRemoveCover": "Odstranit obálku",
|
"LabelRemoveCover": "Odstranit obálku",
|
||||||
@ -491,6 +502,10 @@
|
|||||||
"LabelTagsAccessibleToUser": "Značky přístupné uživateli",
|
"LabelTagsAccessibleToUser": "Značky přístupné uživateli",
|
||||||
"LabelTagsNotAccessibleToUser": "Značky nepřístupné uživateli",
|
"LabelTagsNotAccessibleToUser": "Značky nepřístupné uživateli",
|
||||||
"LabelTasks": "Spuštěné Úlohy",
|
"LabelTasks": "Spuštěné Úlohy",
|
||||||
|
"LabelTextEditorBulletedList": "Bulleted list",
|
||||||
|
"LabelTextEditorLink": "Link",
|
||||||
|
"LabelTextEditorNumberedList": "Numbered list",
|
||||||
|
"LabelTextEditorUnlink": "Unlink",
|
||||||
"LabelTheme": "Téma",
|
"LabelTheme": "Téma",
|
||||||
"LabelThemeDark": "Tmavé",
|
"LabelThemeDark": "Tmavé",
|
||||||
"LabelThemeLight": "Světlé",
|
"LabelThemeLight": "Světlé",
|
||||||
@ -516,6 +531,7 @@
|
|||||||
"LabelTracksSingleTrack": "Jedna stopa",
|
"LabelTracksSingleTrack": "Jedna stopa",
|
||||||
"LabelType": "Typ",
|
"LabelType": "Typ",
|
||||||
"LabelUnabridged": "Nezkráceno",
|
"LabelUnabridged": "Nezkráceno",
|
||||||
|
"LabelUndo": "Undo",
|
||||||
"LabelUnknown": "Neznámý",
|
"LabelUnknown": "Neznámý",
|
||||||
"LabelUpdateCover": "Aktualizovat obálku",
|
"LabelUpdateCover": "Aktualizovat obálku",
|
||||||
"LabelUpdateCoverHelp": "Povolit přepsání existujících obálek pro vybrané knihy, pokud je nalezena shoda",
|
"LabelUpdateCoverHelp": "Povolit přepsání existujících obálek pro vybrané knihy, pokud je nalezena shoda",
|
||||||
|
@ -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,15 @@
|
|||||||
"ButtonMatchAllAuthors": "Match alle forfattere",
|
"ButtonMatchAllAuthors": "Match alle forfattere",
|
||||||
"ButtonMatchBooks": "Match bøger",
|
"ButtonMatchBooks": "Match bøger",
|
||||||
"ButtonNevermind": "Glem det",
|
"ButtonNevermind": "Glem det",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
@ -104,6 +109,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",
|
||||||
@ -281,8 +287,11 @@
|
|||||||
"LabelFinished": "Færdig",
|
"LabelFinished": "Færdig",
|
||||||
"LabelFolder": "Mappe",
|
"LabelFolder": "Mappe",
|
||||||
"LabelFolders": "Mapper",
|
"LabelFolders": "Mapper",
|
||||||
|
"LabelFontBold": "Bold",
|
||||||
"LabelFontFamily": "Fontfamilie",
|
"LabelFontFamily": "Fontfamilie",
|
||||||
|
"LabelFontItalic": "Italic",
|
||||||
"LabelFontScale": "Skriftstørrelse",
|
"LabelFontScale": "Skriftstørrelse",
|
||||||
|
"LabelFontStrikethrough": "Strikethrough",
|
||||||
"LabelFormat": "Format",
|
"LabelFormat": "Format",
|
||||||
"LabelGenre": "Genre",
|
"LabelGenre": "Genre",
|
||||||
"LabelGenres": "Genrer",
|
"LabelGenres": "Genrer",
|
||||||
@ -387,6 +396,7 @@
|
|||||||
"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)",
|
||||||
@ -403,6 +413,7 @@
|
|||||||
"LabelRecentlyAdded": "Senest tilføjet",
|
"LabelRecentlyAdded": "Senest tilføjet",
|
||||||
"LabelRecentSeries": "Seneste serie",
|
"LabelRecentSeries": "Seneste serie",
|
||||||
"LabelRecommended": "Anbefalet",
|
"LabelRecommended": "Anbefalet",
|
||||||
|
"LabelRedo": "Redo",
|
||||||
"LabelRegion": "Region",
|
"LabelRegion": "Region",
|
||||||
"LabelReleaseDate": "Udgivelsesdato",
|
"LabelReleaseDate": "Udgivelsesdato",
|
||||||
"LabelRemoveCover": "Fjern omslag",
|
"LabelRemoveCover": "Fjern omslag",
|
||||||
@ -491,6 +502,10 @@
|
|||||||
"LabelTagsAccessibleToUser": "Mærker tilgængelige for bruger",
|
"LabelTagsAccessibleToUser": "Mærker tilgængelige for bruger",
|
||||||
"LabelTagsNotAccessibleToUser": "Mærker ikke tilgængelige for bruger",
|
"LabelTagsNotAccessibleToUser": "Mærker ikke tilgængelige for bruger",
|
||||||
"LabelTasks": "Kører opgaver",
|
"LabelTasks": "Kører opgaver",
|
||||||
|
"LabelTextEditorBulletedList": "Bulleted list",
|
||||||
|
"LabelTextEditorLink": "Link",
|
||||||
|
"LabelTextEditorNumberedList": "Numbered list",
|
||||||
|
"LabelTextEditorUnlink": "Unlink",
|
||||||
"LabelTheme": "Tema",
|
"LabelTheme": "Tema",
|
||||||
"LabelThemeDark": "Mørk",
|
"LabelThemeDark": "Mørk",
|
||||||
"LabelThemeLight": "Lys",
|
"LabelThemeLight": "Lys",
|
||||||
@ -516,6 +531,7 @@
|
|||||||
"LabelTracksSingleTrack": "Enkeltspors",
|
"LabelTracksSingleTrack": "Enkeltspors",
|
||||||
"LabelType": "Type",
|
"LabelType": "Type",
|
||||||
"LabelUnabridged": "Uforkortet",
|
"LabelUnabridged": "Uforkortet",
|
||||||
|
"LabelUndo": "Undo",
|
||||||
"LabelUnknown": "Ukendt",
|
"LabelUnknown": "Ukendt",
|
||||||
"LabelUpdateCover": "Opdater omslag",
|
"LabelUpdateCover": "Opdater omslag",
|
||||||
"LabelUpdateCoverHelp": "Tillad overskrivning af eksisterende omslag for de valgte bøger, når der findes en match",
|
"LabelUpdateCoverHelp": "Tillad overskrivning af eksisterende omslag for de valgte bøger, når der findes en match",
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
"ButtonAuthors": "Autoren",
|
"ButtonAuthors": "Autoren",
|
||||||
"ButtonBrowseForFolder": "Ordnersuche",
|
"ButtonBrowseForFolder": "Ordnersuche",
|
||||||
"ButtonCancel": "Abbrechen",
|
"ButtonCancel": "Abbrechen",
|
||||||
"ButtonCancelEncode": "Abbruch der Verschlüsselung",
|
"ButtonCancelEncode": "Codierung abbrechen",
|
||||||
"ButtonChangeRootPassword": "Hauptpasswort ändern",
|
"ButtonChangeRootPassword": "Hauptpasswort ändern",
|
||||||
"ButtonCheckAndDownloadNewEpisodes": "Überprüfe & lade neue Episoden herunter",
|
"ButtonCheckAndDownloadNewEpisodes": "Überprüfe & lade neue Episoden herunter",
|
||||||
"ButtonChooseAFolder": "Wähle einen Ordner",
|
"ButtonChooseAFolder": "Wähle einen Ordner",
|
||||||
@ -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,19 +43,22 @@
|
|||||||
"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",
|
||||||
|
"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",
|
||||||
"ButtonPurgeAllCache": "Lösche alle Zwischenspeicher",
|
"ButtonPreviousChapter": "Vorheriges Kapitel",
|
||||||
"ButtonPurgeItemsCache": "Lösche Medien-Zwischenspeicher",
|
"ButtonPurgeAllCache": "Cache leeren",
|
||||||
|
"ButtonPurgeItemsCache": "Lösche Medien-Cache",
|
||||||
"ButtonPurgeMediaProgress": "Lösche Hörfortschritte",
|
"ButtonPurgeMediaProgress": "Lösche Hörfortschritte",
|
||||||
"ButtonQueueAddItem": "Zur Warteschlange hinzufügen",
|
"ButtonQueueAddItem": "Zur Warteschlange hinzufügen",
|
||||||
"ButtonQueueRemoveItem": "Aus der Warteschlange entfernen",
|
"ButtonQueueRemoveItem": "Aus der Warteschlange entfernen",
|
||||||
"ButtonQuickMatch": "Schnellabgleich",
|
"ButtonQuickMatch": "Schnellabgleich",
|
||||||
"ButtonRead": "Lese",
|
"ButtonRead": "Lesen",
|
||||||
"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",
|
||||||
@ -70,7 +75,7 @@
|
|||||||
"ButtonScan": "Partial-Scan (nur geänderte/neue Medien)",
|
"ButtonScan": "Partial-Scan (nur geänderte/neue Medien)",
|
||||||
"ButtonScanLibrary": "Bibliothek scannen",
|
"ButtonScanLibrary": "Bibliothek scannen",
|
||||||
"ButtonSearch": "Suchen",
|
"ButtonSearch": "Suchen",
|
||||||
"ButtonSelectFolderPath": "Auswahl Ordnerpfad",
|
"ButtonSelectFolderPath": "Ordnerpfad auswählen",
|
||||||
"ButtonSeries": "Serien",
|
"ButtonSeries": "Serien",
|
||||||
"ButtonSetChaptersFromTracks": "Kapitelerstellung aus Audiodateien",
|
"ButtonSetChaptersFromTracks": "Kapitelerstellung aus Audiodateien",
|
||||||
"ButtonShiftTimes": "Zeitverschiebung",
|
"ButtonShiftTimes": "Zeitverschiebung",
|
||||||
@ -88,7 +93,7 @@
|
|||||||
"ButtonViewAll": "Alles anzeigen",
|
"ButtonViewAll": "Alles anzeigen",
|
||||||
"ButtonYes": "Ja",
|
"ButtonYes": "Ja",
|
||||||
"ErrorUploadFetchMetadataAPI": "Fehler beim Abrufen der Metadaten",
|
"ErrorUploadFetchMetadataAPI": "Fehler beim Abrufen der Metadaten",
|
||||||
"ErrorUploadFetchMetadataNoResults": "Metadaten konnten nicht abgerufen werden. Versuchen Sie den Titel und oder den Autor zu updaten",
|
"ErrorUploadFetchMetadataNoResults": "Metadaten konnten nicht abgerufen werden. Versuche den Titel und oder den Autor zu aktualisieren",
|
||||||
"ErrorUploadLacksTitle": "Es muss ein Titel eingegeben werden",
|
"ErrorUploadLacksTitle": "Es muss ein Titel eingegeben werden",
|
||||||
"HeaderAccount": "Konto",
|
"HeaderAccount": "Konto",
|
||||||
"HeaderAdvanced": "Erweitert",
|
"HeaderAdvanced": "Erweitert",
|
||||||
@ -104,21 +109,22 @@
|
|||||||
"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",
|
||||||
"HeaderEmail": "Email",
|
"HeaderEmail": "Email",
|
||||||
"HeaderEmailSettings": "Email Einstellungen",
|
"HeaderEmailSettings": "Email Einstellungen",
|
||||||
"HeaderEpisodes": "Episoden",
|
"HeaderEpisodes": "Episoden",
|
||||||
"HeaderEreaderDevices": "Ereader Geräte",
|
"HeaderEreaderDevices": "E-Reader Geräte",
|
||||||
"HeaderEreaderSettings": "Ereader Einstellungen",
|
"HeaderEreaderSettings": "E-Reader Einstellungen",
|
||||||
"HeaderFiles": "Dateien",
|
"HeaderFiles": "Dateien",
|
||||||
"HeaderFindChapters": "Kapitel suchen",
|
"HeaderFindChapters": "Kapitel suchen",
|
||||||
"HeaderIgnoredFiles": "Ignorierte Dateien",
|
"HeaderIgnoredFiles": "Ignorierte Dateien",
|
||||||
"HeaderItemFiles": "Medien-Dateien",
|
"HeaderItemFiles": "Medien-Dateien",
|
||||||
"HeaderItemMetadataUtils": "Metadaten",
|
"HeaderItemMetadataUtils": "Metadaten",
|
||||||
"HeaderLastListeningSession": "Letzte Hörsitzung",
|
"HeaderLastListeningSession": "Letzte Hörsitzung",
|
||||||
"HeaderLatestEpisodes": "Letzte Episoden",
|
"HeaderLatestEpisodes": "Neueste Episoden",
|
||||||
"HeaderLibraries": "Bibliotheken",
|
"HeaderLibraries": "Bibliotheken",
|
||||||
"HeaderLibraryFiles": "Alle Dateien",
|
"HeaderLibraryFiles": "Alle Dateien",
|
||||||
"HeaderLibraryStats": "Bibliotheksstatistiken",
|
"HeaderLibraryStats": "Bibliotheksstatistiken",
|
||||||
@ -130,7 +136,7 @@
|
|||||||
"HeaderManageTags": "Tags verwalten",
|
"HeaderManageTags": "Tags verwalten",
|
||||||
"HeaderMapDetails": "Stapelverarbeitung",
|
"HeaderMapDetails": "Stapelverarbeitung",
|
||||||
"HeaderMatch": "Metadaten",
|
"HeaderMatch": "Metadaten",
|
||||||
"HeaderMetadataOrderOfPrecedence": "Metadata order of precedence",
|
"HeaderMetadataOrderOfPrecedence": "Metadaten Rangfolge",
|
||||||
"HeaderMetadataToEmbed": "Einzubettende Metadaten",
|
"HeaderMetadataToEmbed": "Einzubettende Metadaten",
|
||||||
"HeaderNewAccount": "Neues Konto",
|
"HeaderNewAccount": "Neues Konto",
|
||||||
"HeaderNewLibrary": "Neue Bibliothek",
|
"HeaderNewLibrary": "Neue Bibliothek",
|
||||||
@ -138,9 +144,9 @@
|
|||||||
"HeaderOpenIDConnectAuthentication": "OpenID Connect Authentifizierung",
|
"HeaderOpenIDConnectAuthentication": "OpenID Connect Authentifizierung",
|
||||||
"HeaderOpenRSSFeed": "RSS-Feed öffnen",
|
"HeaderOpenRSSFeed": "RSS-Feed öffnen",
|
||||||
"HeaderOtherFiles": "Sonstige Dateien",
|
"HeaderOtherFiles": "Sonstige Dateien",
|
||||||
"HeaderPasswordAuthentication": "Password Authentifizierung",
|
"HeaderPasswordAuthentication": "Passwort Authentifizierung",
|
||||||
"HeaderPermissions": "Berechtigungen",
|
"HeaderPermissions": "Berechtigungen",
|
||||||
"HeaderPlayerQueue": "Spieler Warteschlange",
|
"HeaderPlayerQueue": "Player Warteschlange",
|
||||||
"HeaderPlaylist": "Wiedergabeliste",
|
"HeaderPlaylist": "Wiedergabeliste",
|
||||||
"HeaderPlaylistItems": "Einträge in der Wiedergabeliste",
|
"HeaderPlaylistItems": "Einträge in der Wiedergabeliste",
|
||||||
"HeaderPodcastsToAdd": "Podcasts zum Hinzufügen",
|
"HeaderPodcastsToAdd": "Podcasts zum Hinzufügen",
|
||||||
@ -149,7 +155,7 @@
|
|||||||
"HeaderRemoveEpisodes": "Lösche {0} Episoden",
|
"HeaderRemoveEpisodes": "Lösche {0} Episoden",
|
||||||
"HeaderRSSFeedGeneral": "RSS Details",
|
"HeaderRSSFeedGeneral": "RSS Details",
|
||||||
"HeaderRSSFeedIsOpen": "RSS-Feed ist geöffnet",
|
"HeaderRSSFeedIsOpen": "RSS-Feed ist geöffnet",
|
||||||
"HeaderRSSFeeds": "RSS Feeds",
|
"HeaderRSSFeeds": "RSS-Feeds",
|
||||||
"HeaderSavedMediaProgress": "Gespeicherte Hörfortschritte",
|
"HeaderSavedMediaProgress": "Gespeicherte Hörfortschritte",
|
||||||
"HeaderSchedule": "Zeitplan",
|
"HeaderSchedule": "Zeitplan",
|
||||||
"HeaderScheduleLibraryScans": "Automatische Bibliotheksscans",
|
"HeaderScheduleLibraryScans": "Automatische Bibliotheksscans",
|
||||||
@ -160,7 +166,7 @@
|
|||||||
"HeaderSettingsExperimental": "Experimentelle Funktionen",
|
"HeaderSettingsExperimental": "Experimentelle Funktionen",
|
||||||
"HeaderSettingsGeneral": "Allgemein",
|
"HeaderSettingsGeneral": "Allgemein",
|
||||||
"HeaderSettingsScanner": "Scanner",
|
"HeaderSettingsScanner": "Scanner",
|
||||||
"HeaderSleepTimer": "Einschlaf-Timer",
|
"HeaderSleepTimer": "Sleep-Timer",
|
||||||
"HeaderStatsLargestItems": "Größte Medien",
|
"HeaderStatsLargestItems": "Größte Medien",
|
||||||
"HeaderStatsLongestItems": "Längste Medien (h)",
|
"HeaderStatsLongestItems": "Längste Medien (h)",
|
||||||
"HeaderStatsMinutesListeningChart": "Hörminuten (letzte 7 Tage)",
|
"HeaderStatsMinutesListeningChart": "Hörminuten (letzte 7 Tage)",
|
||||||
@ -191,8 +197,8 @@
|
|||||||
"LabelAll": "Alle",
|
"LabelAll": "Alle",
|
||||||
"LabelAllUsers": "Alle Benutzer",
|
"LabelAllUsers": "Alle Benutzer",
|
||||||
"LabelAllUsersExcludingGuests": "Alle Benutzer außer Gästen",
|
"LabelAllUsersExcludingGuests": "Alle Benutzer außer Gästen",
|
||||||
"LabelAllUsersIncludingGuests": "All Benutzer und Gäste",
|
"LabelAllUsersIncludingGuests": "Alle Benutzer und Gäste",
|
||||||
"LabelAlreadyInYourLibrary": "In der Bibliothek vorhanden",
|
"LabelAlreadyInYourLibrary": "Bereits in der Bibliothek",
|
||||||
"LabelAppend": "Anhängen",
|
"LabelAppend": "Anhängen",
|
||||||
"LabelAuthor": "Autor",
|
"LabelAuthor": "Autor",
|
||||||
"LabelAuthorFirstLast": "Autor (Vorname Nachname)",
|
"LabelAuthorFirstLast": "Autor (Vorname Nachname)",
|
||||||
@ -204,7 +210,7 @@
|
|||||||
"LabelAutoLaunch": "Automatischer Start",
|
"LabelAutoLaunch": "Automatischer Start",
|
||||||
"LabelAutoLaunchDescription": "Automatische Weiterleitung zum Authentifizierungsanbieter beim Navigieren zur Anmeldeseite (manueller Überschreibungspfad <code>/login?autoLaunch=0</code>)",
|
"LabelAutoLaunchDescription": "Automatische Weiterleitung zum Authentifizierungsanbieter beim Navigieren zur Anmeldeseite (manueller Überschreibungspfad <code>/login?autoLaunch=0</code>)",
|
||||||
"LabelAutoRegister": "Automatische Registrierung",
|
"LabelAutoRegister": "Automatische Registrierung",
|
||||||
"LabelAutoRegisterDescription": "Automatische neue Neutzer anlegen nach dem Einloggen",
|
"LabelAutoRegisterDescription": "Automatische neue Neutzer anlegen nach dem Registrieren",
|
||||||
"LabelBackToUser": "Zurück zum Benutzer",
|
"LabelBackToUser": "Zurück zum Benutzer",
|
||||||
"LabelBackupLocation": "Backup-Ort",
|
"LabelBackupLocation": "Backup-Ort",
|
||||||
"LabelBackupsEnableAutomaticBackups": "Automatische Sicherung aktivieren",
|
"LabelBackupsEnableAutomaticBackups": "Automatische Sicherung aktivieren",
|
||||||
@ -212,14 +218,14 @@
|
|||||||
"LabelBackupsMaxBackupSize": "Maximale Sicherungsgröße (in GB)",
|
"LabelBackupsMaxBackupSize": "Maximale Sicherungsgröße (in GB)",
|
||||||
"LabelBackupsMaxBackupSizeHelp": "Zum Schutz vor Fehlkonfigurationen schlagen Sicherungen fehl, wenn sie die konfigurierte Größe überschreiten.",
|
"LabelBackupsMaxBackupSizeHelp": "Zum Schutz vor Fehlkonfigurationen schlagen Sicherungen fehl, wenn sie die konfigurierte Größe überschreiten.",
|
||||||
"LabelBackupsNumberToKeep": "Anzahl der aufzubewahrenden Sicherungen",
|
"LabelBackupsNumberToKeep": "Anzahl der aufzubewahrenden Sicherungen",
|
||||||
"LabelBackupsNumberToKeepHelp": "Es wird immer nur 1 Sicherung auf einmal entfernt. Wenn Sie bereits mehrere Sicherungen als die definierte max. Anzahl haben, sollten Sie diese manuell entfernen.",
|
"LabelBackupsNumberToKeepHelp": "Es wird immer nur 1 Sicherung auf einmal entfernt. Wenn du bereits mehrere Sicherungen als die definierte max. Anzahl hast, solltest du diese manuell entfernen.",
|
||||||
"LabelBitrate": "Bitrate",
|
"LabelBitrate": "Bitrate",
|
||||||
"LabelBooks": "Bücher",
|
"LabelBooks": "Bücher",
|
||||||
"LabelButtonText": "Button Text",
|
"LabelButtonText": "Button Text",
|
||||||
"LabelChangePassword": "Passwort ändern",
|
"LabelChangePassword": "Passwort ändern",
|
||||||
"LabelChannels": "Kanäle",
|
"LabelChannels": "Kanäle",
|
||||||
"LabelChapters": "Kapitel",
|
"LabelChapters": "Kapitel",
|
||||||
"LabelChaptersFound": "gefundene Kapitel",
|
"LabelChaptersFound": "Gefundene Kapitel",
|
||||||
"LabelChapterTitle": "Kapitelüberschrift",
|
"LabelChapterTitle": "Kapitelüberschrift",
|
||||||
"LabelClickForMoreInfo": "Klicken für mehr Informationen",
|
"LabelClickForMoreInfo": "Klicken für mehr Informationen",
|
||||||
"LabelClosePlayer": "Player schließen",
|
"LabelClosePlayer": "Player schließen",
|
||||||
@ -230,7 +236,7 @@
|
|||||||
"LabelComplete": "Vollständig",
|
"LabelComplete": "Vollständig",
|
||||||
"LabelConfirmPassword": "Passwort bestätigen",
|
"LabelConfirmPassword": "Passwort bestätigen",
|
||||||
"LabelContinueListening": "Weiterhören",
|
"LabelContinueListening": "Weiterhören",
|
||||||
"LabelContinueReading": "Lesen fortsetzen",
|
"LabelContinueReading": "Weiterlesen",
|
||||||
"LabelContinueSeries": "Serien fortsetzen",
|
"LabelContinueSeries": "Serien fortsetzen",
|
||||||
"LabelCover": "Titelbild",
|
"LabelCover": "Titelbild",
|
||||||
"LabelCoverImageURL": "URL des Titelbildes",
|
"LabelCoverImageURL": "URL des Titelbildes",
|
||||||
@ -245,7 +251,7 @@
|
|||||||
"LabelDeselectAll": "Alles abwählen",
|
"LabelDeselectAll": "Alles abwählen",
|
||||||
"LabelDevice": "Gerät",
|
"LabelDevice": "Gerät",
|
||||||
"LabelDeviceInfo": "Geräteinformationen",
|
"LabelDeviceInfo": "Geräteinformationen",
|
||||||
"LabelDeviceIsAvailableTo": "Dem Geärt ist es möglich zu ...",
|
"LabelDeviceIsAvailableTo": "Dem Gerät ist es möglich zu ...",
|
||||||
"LabelDirectory": "Verzeichnis",
|
"LabelDirectory": "Verzeichnis",
|
||||||
"LabelDiscFromFilename": "CD aus dem Dateinamen",
|
"LabelDiscFromFilename": "CD aus dem Dateinamen",
|
||||||
"LabelDiscFromMetadata": "CD aus den Metadaten",
|
"LabelDiscFromMetadata": "CD aus den Metadaten",
|
||||||
@ -258,10 +264,10 @@
|
|||||||
"LabelEbooks": "E-Books",
|
"LabelEbooks": "E-Books",
|
||||||
"LabelEdit": "Bearbeiten",
|
"LabelEdit": "Bearbeiten",
|
||||||
"LabelEmail": "Email",
|
"LabelEmail": "Email",
|
||||||
"LabelEmailSettingsFromAddress": "Von Address",
|
"LabelEmailSettingsFromAddress": "Von Adresse",
|
||||||
"LabelEmailSettingsSecure": "Sicherheit",
|
"LabelEmailSettingsSecure": "Sicher",
|
||||||
"LabelEmailSettingsSecureHelp": "Wenn \"true\", verwendet die Verbindung TLS, wenn sie eine Verbindung zum Server herstellt. Bei \"false\" wird TLS verwendet, wenn der Server die STARTTLS-Erweiterung unterstützt. In den meisten Fällen setzen Sie diesen Wert auf \"true\", wenn Sie eine Verbindung zu Port 465 herstellen. Für Port 587 oder 25 behalten Sie den Wert \"false\" bei. (von nodemailer.com/smtp/#authentication)",
|
"LabelEmailSettingsSecureHelp": "Wenn \"an\", verwendet die Verbindung TLS, wenn du eine Verbindung zum Server herstellst. Bei \"aus\" wird TLS verwendet, wenn der Server die STARTTLS-Erweiterung unterstützt. In den meisten Fällen solltest du diesen Wert auf \"an\" schalten, wenn du eine Verbindung zu Port 465 herstellst. Für Port 587 oder 25 behalte den Wert \"aus\" bei. (von nodemailer.com/smtp/#authentication)",
|
||||||
"LabelEmailSettingsTestAddress": "Test Addresse",
|
"LabelEmailSettingsTestAddress": "Test Adresse",
|
||||||
"LabelEmbeddedCover": "Eingebettetes Cover",
|
"LabelEmbeddedCover": "Eingebettetes Cover",
|
||||||
"LabelEnable": "Aktivieren",
|
"LabelEnable": "Aktivieren",
|
||||||
"LabelEnd": "Ende",
|
"LabelEnd": "Ende",
|
||||||
@ -278,17 +284,20 @@
|
|||||||
"LabelFilename": "Dateiname",
|
"LabelFilename": "Dateiname",
|
||||||
"LabelFilterByUser": "Nach Benutzern filtern",
|
"LabelFilterByUser": "Nach Benutzern filtern",
|
||||||
"LabelFindEpisodes": "Episoden suchen",
|
"LabelFindEpisodes": "Episoden suchen",
|
||||||
"LabelFinished": "beendet",
|
"LabelFinished": "Beendet",
|
||||||
"LabelFolder": "Ordner",
|
"LabelFolder": "Ordner",
|
||||||
"LabelFolders": "Verzeichnisse",
|
"LabelFolders": "Verzeichnisse",
|
||||||
|
"LabelFontBold": "Bold",
|
||||||
"LabelFontFamily": "Schriftfamilie",
|
"LabelFontFamily": "Schriftfamilie",
|
||||||
|
"LabelFontItalic": "Italic",
|
||||||
"LabelFontScale": "Schriftgröße",
|
"LabelFontScale": "Schriftgröße",
|
||||||
|
"LabelFontStrikethrough": "Strikethrough",
|
||||||
"LabelFormat": "Format",
|
"LabelFormat": "Format",
|
||||||
"LabelGenre": "Kategorie",
|
"LabelGenre": "Kategorie",
|
||||||
"LabelGenres": "Kategorien",
|
"LabelGenres": "Kategorien",
|
||||||
"LabelHardDeleteFile": "Datei dauerhaft löschen",
|
"LabelHardDeleteFile": "Datei dauerhaft löschen",
|
||||||
"LabelHasEbook": "mit E-Book",
|
"LabelHasEbook": "E-Book verfügbar",
|
||||||
"LabelHasSupplementaryEbook": "mit zusätlichem E-Book",
|
"LabelHasSupplementaryEbook": "Ergänzendes E-Book verfügbar",
|
||||||
"LabelHighestPriority": "Höchste Priorität",
|
"LabelHighestPriority": "Höchste Priorität",
|
||||||
"LabelHost": "Host",
|
"LabelHost": "Host",
|
||||||
"LabelHour": "Stunde",
|
"LabelHour": "Stunde",
|
||||||
@ -311,9 +320,9 @@
|
|||||||
"LabelItem": "Medium",
|
"LabelItem": "Medium",
|
||||||
"LabelLanguage": "Sprache",
|
"LabelLanguage": "Sprache",
|
||||||
"LabelLanguageDefaultServer": "Standard-Server-Sprache",
|
"LabelLanguageDefaultServer": "Standard-Server-Sprache",
|
||||||
"LabelLastBookAdded": "Zuletzt hinzugefügtes Medium",
|
"LabelLastBookAdded": "Zuletzt hinzugefügtes Buch",
|
||||||
"LabelLastBookUpdated": "Zuletzt aktualisiertes Medium",
|
"LabelLastBookUpdated": "Zuletzt aktualisiertes Buch",
|
||||||
"LabelLastSeen": "Zuletzt angesehen",
|
"LabelLastSeen": "Zuletzt gesehen",
|
||||||
"LabelLastTime": "Letztes Mal",
|
"LabelLastTime": "Letztes Mal",
|
||||||
"LabelLastUpdate": "Letzte Aktualisierung",
|
"LabelLastUpdate": "Letzte Aktualisierung",
|
||||||
"LabelLayout": "Layout",
|
"LabelLayout": "Layout",
|
||||||
@ -330,13 +339,13 @@
|
|||||||
"LabelLogLevelDebug": "Fehlersuche",
|
"LabelLogLevelDebug": "Fehlersuche",
|
||||||
"LabelLogLevelInfo": "Informationen",
|
"LabelLogLevelInfo": "Informationen",
|
||||||
"LabelLogLevelWarn": "Warnungen",
|
"LabelLogLevelWarn": "Warnungen",
|
||||||
"LabelLookForNewEpisodesAfterDate": "Suchen nach neuen Episoden nach diesem Datum",
|
"LabelLookForNewEpisodesAfterDate": "Suche nach neuen Episoden nach diesem Datum",
|
||||||
"LabelLowestPriority": "Niedrigste Priorität",
|
"LabelLowestPriority": "Niedrigste Priorität",
|
||||||
"LabelMatchExistingUsersBy": "Zuordnen existierender Benutzer mit",
|
"LabelMatchExistingUsersBy": "Zuordnen existierender Benutzer mit",
|
||||||
"LabelMatchExistingUsersByDescription": "Wird zum Verbinden vorhandener Benutzer verwendet. Sobald die Verbindung hergestellt ist, wird den Benutzern eine eindeutige ID von Ihrem SSO-Anbieter zugeordnet",
|
"LabelMatchExistingUsersByDescription": "Wird zum Verbinden vorhandener Benutzer verwendet. Sobald die Verbindung hergestellt ist, wird den Benutzern eine eindeutige ID vom SSO-Anbieter zugeordnet",
|
||||||
"LabelMediaPlayer": "Mediaplayer",
|
"LabelMediaPlayer": "Mediaplayer",
|
||||||
"LabelMediaType": "Medientyp",
|
"LabelMediaType": "Medientyp",
|
||||||
"LabelMetadataOrderOfPrecedenceDescription": "Eine Höhere Priorität Quelle für Metadaten wird die Metadaten aus eine Quelle mit niedrigerer Priorität überschreiben.",
|
"LabelMetadataOrderOfPrecedenceDescription": "Höher priorisierte Quellen für Metadaten überschreiben Metadaten aus Quellen die niedriger priorisiert sind.",
|
||||||
"LabelMetadataProvider": "Metadatenanbieter",
|
"LabelMetadataProvider": "Metadatenanbieter",
|
||||||
"LabelMetaTag": "Meta Schlagwort",
|
"LabelMetaTag": "Meta Schlagwort",
|
||||||
"LabelMetaTags": "Meta Tags",
|
"LabelMetaTags": "Meta Tags",
|
||||||
@ -344,21 +353,21 @@
|
|||||||
"LabelMissing": "Fehlend",
|
"LabelMissing": "Fehlend",
|
||||||
"LabelMissingParts": "Fehlende Teile",
|
"LabelMissingParts": "Fehlende Teile",
|
||||||
"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 Sie entfernen oder durch zusätzliche URIs für die Integration von Drittanbieter-Apps ergänzen können. 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",
|
||||||
"LabelMoreInfo": "Mehr Info",
|
"LabelMoreInfo": "Mehr Infos",
|
||||||
"LabelName": "Name",
|
"LabelName": "Name",
|
||||||
"LabelNarrator": "Erzähler",
|
"LabelNarrator": "Erzähler",
|
||||||
"LabelNarrators": "Erzähler",
|
"LabelNarrators": "Erzähler",
|
||||||
"LabelNew": "Neu",
|
"LabelNew": "Neu",
|
||||||
"LabelNewestAuthors": "Neuste Autoren",
|
"LabelNewestAuthors": "Neueste Autoren",
|
||||||
"LabelNewestEpisodes": "Neueste Episoden",
|
"LabelNewestEpisodes": "Neueste Episoden",
|
||||||
"LabelNewPassword": "Neues Passwort",
|
"LabelNewPassword": "Neues Passwort",
|
||||||
"LabelNextBackupDate": "Nächstes Sicherungsdatum",
|
"LabelNextBackupDate": "Nächstes Sicherungsdatum",
|
||||||
"LabelNextScheduledRun": "Nächster planmäßiger Durchlauf",
|
"LabelNextScheduledRun": "Nächster planmäßiger Durchlauf",
|
||||||
"LabelNoEpisodesSelected": "Keine Episoden ausgewählt",
|
"LabelNoEpisodesSelected": "Keine Episoden ausgewählt",
|
||||||
"LabelNotes": "Hinweise",
|
"LabelNotes": "Notizen",
|
||||||
"LabelNotFinished": "nicht beendet",
|
"LabelNotFinished": "Nicht beendet",
|
||||||
"LabelNotificationAppriseURL": "Apprise URL(s)",
|
"LabelNotificationAppriseURL": "Apprise URL(s)",
|
||||||
"LabelNotificationAvailableVariables": "Verfügbare Variablen",
|
"LabelNotificationAvailableVariables": "Verfügbare Variablen",
|
||||||
"LabelNotificationBodyTemplate": "Textvorlage",
|
"LabelNotificationBodyTemplate": "Textvorlage",
|
||||||
@ -371,7 +380,7 @@
|
|||||||
"LabelNotStarted": "Nicht begonnen",
|
"LabelNotStarted": "Nicht begonnen",
|
||||||
"LabelNumberOfBooks": "Anzahl der Hörbücher",
|
"LabelNumberOfBooks": "Anzahl der Hörbücher",
|
||||||
"LabelNumberOfEpisodes": "Anzahl der Episoden",
|
"LabelNumberOfEpisodes": "Anzahl der Episoden",
|
||||||
"LabelOpenRSSFeed": "Öffne RSS Feed",
|
"LabelOpenRSSFeed": "Öffne RSS-Feed",
|
||||||
"LabelOverwrite": "Überschreiben",
|
"LabelOverwrite": "Überschreiben",
|
||||||
"LabelPassword": "Passwort",
|
"LabelPassword": "Passwort",
|
||||||
"LabelPath": "Pfad",
|
"LabelPath": "Pfad",
|
||||||
@ -387,22 +396,24 @@
|
|||||||
"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)",
|
||||||
"LabelPreventIndexing": "Verhindere, dass dein Feed von iTunes- und Google-Podcast-Verzeichnissen indiziert wird",
|
"LabelPreventIndexing": "Verhindere, dass dein Feed von iTunes- und Google-Podcast-Verzeichnissen indiziert wird",
|
||||||
"LabelPrimaryEbook": "Haupt-E-Book",
|
"LabelPrimaryEbook": "Primäres E-Book",
|
||||||
"LabelProgress": "Fortschritt",
|
"LabelProgress": "Fortschritt",
|
||||||
"LabelProvider": "Anbieter",
|
"LabelProvider": "Anbieter",
|
||||||
"LabelPubDate": "Veröffentlichungsdatum",
|
"LabelPubDate": "Veröffentlichungsdatum",
|
||||||
"LabelPublisher": "Herausgeber",
|
"LabelPublisher": "Herausgeber",
|
||||||
"LabelPublishYear": "Jahr",
|
"LabelPublishYear": "Jahr",
|
||||||
"LabelRead": "Lesen",
|
"LabelRead": "Lesen",
|
||||||
"LabelReadAgain": "Nocheinmal Lesen",
|
"LabelReadAgain": "Noch einmal Lesen",
|
||||||
"LabelReadEbookWithoutProgress": "E-Book lesen und Fortschritt verwerfen",
|
"LabelReadEbookWithoutProgress": "E-Book lesen und Fortschritt verwerfen",
|
||||||
"LabelRecentlyAdded": "Kürzlich hinzugefügt",
|
"LabelRecentlyAdded": "Kürzlich hinzugefügt",
|
||||||
"LabelRecentSeries": "Aktuelle Serien",
|
"LabelRecentSeries": "Aktuelle Serien",
|
||||||
"LabelRecommended": "Empfohlen",
|
"LabelRecommended": "Empfohlen",
|
||||||
|
"LabelRedo": "Redo",
|
||||||
"LabelRegion": "Region",
|
"LabelRegion": "Region",
|
||||||
"LabelReleaseDate": "Veröffentlichungsdatum",
|
"LabelReleaseDate": "Veröffentlichungsdatum",
|
||||||
"LabelRemoveCover": "Lösche Titelbild",
|
"LabelRemoveCover": "Lösche Titelbild",
|
||||||
@ -414,8 +425,8 @@
|
|||||||
"LabelRSSFeedSlug": "RSS Feed Schlagwort",
|
"LabelRSSFeedSlug": "RSS Feed Schlagwort",
|
||||||
"LabelRSSFeedURL": "RSS Feed URL",
|
"LabelRSSFeedURL": "RSS Feed URL",
|
||||||
"LabelSearchTerm": "Begriff suchen",
|
"LabelSearchTerm": "Begriff suchen",
|
||||||
"LabelSearchTitle": "Titel",
|
"LabelSearchTitle": "Titel suchen",
|
||||||
"LabelSearchTitleOrASIN": "Titel oder ASIN",
|
"LabelSearchTitleOrASIN": "Titel oder ASIN suchen",
|
||||||
"LabelSeason": "Staffel",
|
"LabelSeason": "Staffel",
|
||||||
"LabelSelectAllEpisodes": "Alle Episoden auswählen",
|
"LabelSelectAllEpisodes": "Alle Episoden auswählen",
|
||||||
"LabelSelectEpisodesShowing": "{0} ausgewählte Episoden werden angezeigt",
|
"LabelSelectEpisodesShowing": "{0} ausgewählte Episoden werden angezeigt",
|
||||||
@ -425,10 +436,10 @@
|
|||||||
"LabelSeries": "Serien",
|
"LabelSeries": "Serien",
|
||||||
"LabelSeriesName": "Serienname",
|
"LabelSeriesName": "Serienname",
|
||||||
"LabelSeriesProgress": "Serienfortschritt",
|
"LabelSeriesProgress": "Serienfortschritt",
|
||||||
"LabelSetEbookAsPrimary": "Setzen als Hauptbuch",
|
"LabelSetEbookAsPrimary": "Als Hauptbuch setzen",
|
||||||
"LabelSetEbookAsSupplementary": "Setzen als Ergänzung",
|
"LabelSetEbookAsSupplementary": "Als Ergänzung setzen",
|
||||||
"LabelSettingsAudiobooksOnly": "nur Hörbücher",
|
"LabelSettingsAudiobooksOnly": "Nur Hörbücher",
|
||||||
"LabelSettingsAudiobooksOnlyHelp": "Wenn Sie diese Einstellung aktivieren, werden E-Book-Dateien ignoriert, es sei denn, sie befinden sich in einem Hörbuchordner. In diesem Fall werden sie als zusätzliche E-Books festgelegt",
|
"LabelSettingsAudiobooksOnlyHelp": "Wenn du diese Einstellung aktivierst, werden E-Book-Dateien ignoriert, es sei denn, sie befinden sich in einem Hörbuchordner. In diesem Fall werden sie als zusätzliche E-Books festgelegt",
|
||||||
"LabelSettingsBookshelfViewHelp": "Skeumorphes Design mit Holzeinlegeböden",
|
"LabelSettingsBookshelfViewHelp": "Skeumorphes Design mit Holzeinlegeböden",
|
||||||
"LabelSettingsChromecastSupport": "Chromecastunterstützung",
|
"LabelSettingsChromecastSupport": "Chromecastunterstützung",
|
||||||
"LabelSettingsDateFormat": "Datumsformat",
|
"LabelSettingsDateFormat": "Datumsformat",
|
||||||
@ -439,11 +450,11 @@
|
|||||||
"LabelSettingsEnableWatcherForLibrary": "Ordnerüberwachung für die Bibliothek aktivieren",
|
"LabelSettingsEnableWatcherForLibrary": "Ordnerüberwachung für die Bibliothek aktivieren",
|
||||||
"LabelSettingsEnableWatcherHelp": "Aktiviert das automatische Hinzufügen/Aktualisieren von Elementen, wenn Dateiänderungen erkannt werden. *Erfordert einen Server-Neustart",
|
"LabelSettingsEnableWatcherHelp": "Aktiviert das automatische Hinzufügen/Aktualisieren von Elementen, wenn Dateiänderungen erkannt werden. *Erfordert einen Server-Neustart",
|
||||||
"LabelSettingsExperimentalFeatures": "Experimentelle Funktionen",
|
"LabelSettingsExperimentalFeatures": "Experimentelle Funktionen",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "Funktionen welche sich in der Entwicklung befinden, benötigen Ihr Feedback und Ihre Hilfe beim Testen. Klicken Sie hier, um die Github-Diskussion zu öffnen.",
|
"LabelSettingsExperimentalFeaturesHelp": "Funktionen welche sich in der Entwicklung befinden, benötigen dein Feedback und deine Hilfe beim Testen. Klicke hier, um die Github-Diskussion zu öffnen.",
|
||||||
"LabelSettingsFindCovers": "Suche Titelbilder",
|
"LabelSettingsFindCovers": "Suche Titelbilder",
|
||||||
"LabelSettingsFindCoversHelp": "Wenn Ihr Medium kein eingebettetes Titelbild oder kein Titelbild im Ordner hat, versucht der Scanner, ein Titelbild online zu finden.<br>Hinweis: Dies verlängert die Scandauer",
|
"LabelSettingsFindCoversHelp": "Wenn dein Medium kein eingebettetes Titelbild oder kein Titelbild im Ordner hat, versucht der Scanner, ein Titelbild online zu finden.<br>Hinweis: Dies verlängert die Scandauer",
|
||||||
"LabelSettingsHideSingleBookSeries": "Ausblenden einzelzne Bücher",
|
"LabelSettingsHideSingleBookSeries": "Ausblenden einzelner Bücher",
|
||||||
"LabelSettingsHideSingleBookSeriesHelp": "Serien, die ein einzelnes Buch enthalten, werden in den Regalen der Serienseite und der Startseite ausgeblendet.",
|
"LabelSettingsHideSingleBookSeriesHelp": "Serien, die nur ein einzelnes Buch enthalten, werden auf der Startseite und in der Serienansicht ausgeblendet.",
|
||||||
"LabelSettingsHomePageBookshelfView": "Startseite verwendet die Bücherregalansicht",
|
"LabelSettingsHomePageBookshelfView": "Startseite verwendet die Bücherregalansicht",
|
||||||
"LabelSettingsLibraryBookshelfView": "Bibliothek verwendet die Bücherregalansicht",
|
"LabelSettingsLibraryBookshelfView": "Bibliothek verwendet die Bücherregalansicht",
|
||||||
"LabelSettingsParseSubtitles": "Analysiere Untertitel",
|
"LabelSettingsParseSubtitles": "Analysiere Untertitel",
|
||||||
@ -455,7 +466,7 @@
|
|||||||
"LabelSettingsSortingIgnorePrefixes": "Vorwort/Artikel beim Sortieren ignorieren",
|
"LabelSettingsSortingIgnorePrefixes": "Vorwort/Artikel beim Sortieren ignorieren",
|
||||||
"LabelSettingsSortingIgnorePrefixesHelp": "Beispiel: für den Artikel \"der\" würde der Mediumtitel \"Der Buchtitel\" als \"Buchtitel, Der\" sortiert werden.",
|
"LabelSettingsSortingIgnorePrefixesHelp": "Beispiel: für den Artikel \"der\" würde der Mediumtitel \"Der Buchtitel\" als \"Buchtitel, Der\" sortiert werden.",
|
||||||
"LabelSettingsSquareBookCovers": "Benutze quadratische Titelbilder",
|
"LabelSettingsSquareBookCovers": "Benutze quadratische Titelbilder",
|
||||||
"LabelSettingsSquareBookCoversHelp": "Bevorzugen quadratische Titelbilder gegenüber den Standardtielbildern im Verhältnis 1,6:1",
|
"LabelSettingsSquareBookCoversHelp": "Bevorzuge quadratische Titelbilder gegenüber den Standardtielbildern im Verhältnis 1,6:1",
|
||||||
"LabelSettingsStoreCoversWithItem": "Titelbilder im Medienordner speichern",
|
"LabelSettingsStoreCoversWithItem": "Titelbilder im Medienordner speichern",
|
||||||
"LabelSettingsStoreCoversWithItemHelp": "Standardmäßig werden die Titelbilder in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Titelbilder als jpg Datei in dem gleichen Ordner gespeichert in welchem sich auch das Medium befindet. Es wird immer nur eine Datei mit dem Namen \"cover.jpg\" gespeichert.",
|
"LabelSettingsStoreCoversWithItemHelp": "Standardmäßig werden die Titelbilder in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Titelbilder als jpg Datei in dem gleichen Ordner gespeichert in welchem sich auch das Medium befindet. Es wird immer nur eine Datei mit dem Namen \"cover.jpg\" gespeichert.",
|
||||||
"LabelSettingsStoreMetadataWithItem": "Metadaten als OPF-Datei im Medienordner speichern",
|
"LabelSettingsStoreMetadataWithItem": "Metadaten als OPF-Datei im Medienordner speichern",
|
||||||
@ -463,7 +474,7 @@
|
|||||||
"LabelSettingsTimeFormat": "Zeitformat",
|
"LabelSettingsTimeFormat": "Zeitformat",
|
||||||
"LabelShowAll": "Alles anzeigen",
|
"LabelShowAll": "Alles anzeigen",
|
||||||
"LabelSize": "Größe",
|
"LabelSize": "Größe",
|
||||||
"LabelSleepTimer": "Einschlaf-Timer",
|
"LabelSleepTimer": "Sleep-Timer",
|
||||||
"LabelSlug": "URL Teil",
|
"LabelSlug": "URL Teil",
|
||||||
"LabelStart": "Start",
|
"LabelStart": "Start",
|
||||||
"LabelStarted": "Gestartet",
|
"LabelStarted": "Gestartet",
|
||||||
@ -476,7 +487,7 @@
|
|||||||
"LabelStatsDays": "Tage",
|
"LabelStatsDays": "Tage",
|
||||||
"LabelStatsDaysListened": "Gehörte Tage",
|
"LabelStatsDaysListened": "Gehörte Tage",
|
||||||
"LabelStatsHours": "Stunden",
|
"LabelStatsHours": "Stunden",
|
||||||
"LabelStatsInARow": "nacheinander",
|
"LabelStatsInARow": "Nacheinander",
|
||||||
"LabelStatsItemsFinished": "Gehörte Medien",
|
"LabelStatsItemsFinished": "Gehörte Medien",
|
||||||
"LabelStatsItemsInLibrary": "Bibliothekseinträge",
|
"LabelStatsItemsInLibrary": "Bibliothekseinträge",
|
||||||
"LabelStatsMinutes": "Minuten",
|
"LabelStatsMinutes": "Minuten",
|
||||||
@ -491,9 +502,13 @@
|
|||||||
"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": "Aufzählungsliste",
|
||||||
|
"LabelTextEditorLink": "Link",
|
||||||
|
"LabelTextEditorNumberedList": "nummerierte Liste",
|
||||||
|
"LabelTextEditorUnlink": "entkoppeln",
|
||||||
"LabelTheme": "Theme",
|
"LabelTheme": "Theme",
|
||||||
"LabelThemeDark": "Dark",
|
"LabelThemeDark": "Dunkel",
|
||||||
"LabelThemeLight": "Light",
|
"LabelThemeLight": "Hell",
|
||||||
"LabelTimeBase": "Basiszeit",
|
"LabelTimeBase": "Basiszeit",
|
||||||
"LabelTimeListened": "Gehörte Zeit",
|
"LabelTimeListened": "Gehörte Zeit",
|
||||||
"LabelTimeListenedToday": "Heute gehörte Zeit",
|
"LabelTimeListenedToday": "Heute gehörte Zeit",
|
||||||
@ -503,12 +518,12 @@
|
|||||||
"LabelToolsEmbedMetadata": "Metadaten einbetten",
|
"LabelToolsEmbedMetadata": "Metadaten einbetten",
|
||||||
"LabelToolsEmbedMetadataDescription": "Bettet die Metadaten einschließlich des Titelbildes und der Kapitel in die Audiodatein ein.",
|
"LabelToolsEmbedMetadataDescription": "Bettet die Metadaten einschließlich des Titelbildes und der Kapitel in die Audiodatein ein.",
|
||||||
"LabelToolsMakeM4b": "M4B-Datei erstellen",
|
"LabelToolsMakeM4b": "M4B-Datei erstellen",
|
||||||
"LabelToolsMakeM4bDescription": "Erstellt eine M4B-Datei (Endung \".m4b\") welche mehrere mp3-Dateien in einer einzigen Datei inkl. derer Metadaten (Beschreibung, Titelbild, Kapitel, ....) zusammenfasst. M4B-Datei können darüber hinaus Lesezeichen speichern und mit einem Abspielschutz (Passwort) versehen werden.",
|
"LabelToolsMakeM4bDescription": "Erstellt eine M4B-Datei (Endung \".m4b\") welche mehrere mp3-Dateien in einer einzigen Datei inkl. derer Metadaten (Beschreibung, Titelbild, Kapitel, ...) zusammenfasst. M4B-Datei können darüber hinaus Lesezeichen speichern und mit einem Abspielschutz (Passwort) versehen werden.",
|
||||||
"LabelToolsSplitM4b": "M4B in MP3's aufteilen",
|
"LabelToolsSplitM4b": "M4B in MP3's aufteilen",
|
||||||
"LabelToolsSplitM4bDescription": "Erstellt aus einer mit Metadaten und nach Kapiteln aufgeteilten M4B-Datei seperate MP3's mit eingebetteten Metadaten, Coverbild und Kapiteln.",
|
"LabelToolsSplitM4bDescription": "Erstellt aus einer mit Metadaten und nach Kapiteln aufgeteilten M4B-Datei seperate MP3's mit eingebetteten Metadaten, Coverbild und Kapiteln.",
|
||||||
"LabelTotalDuration": "Gesamtdauer",
|
"LabelTotalDuration": "Gesamtdauer",
|
||||||
"LabelTotalTimeListened": "Gehörte Gesamtzeit",
|
"LabelTotalTimeListened": "Gehörte Gesamtzeit",
|
||||||
"LabelTrackFromFilename": "Titel von Dateiname",
|
"LabelTrackFromFilename": "Titel aus Dateiname",
|
||||||
"LabelTrackFromMetadata": "Titel aus Metadaten",
|
"LabelTrackFromMetadata": "Titel aus Metadaten",
|
||||||
"LabelTracks": "Dateien",
|
"LabelTracks": "Dateien",
|
||||||
"LabelTracksMultiTrack": "Mehrfachdatei",
|
"LabelTracksMultiTrack": "Mehrfachdatei",
|
||||||
@ -516,15 +531,16 @@
|
|||||||
"LabelTracksSingleTrack": "Einzeldatei",
|
"LabelTracksSingleTrack": "Einzeldatei",
|
||||||
"LabelType": "Typ",
|
"LabelType": "Typ",
|
||||||
"LabelUnabridged": "Ungekürzt",
|
"LabelUnabridged": "Ungekürzt",
|
||||||
|
"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",
|
||||||
"LabelUpdatedAt": "Aktualisiert am",
|
"LabelUpdatedAt": "Aktualisiert am",
|
||||||
"LabelUpdateDetails": "Details aktualisieren",
|
"LabelUpdateDetails": "Details aktualisieren",
|
||||||
"LabelUpdateDetailsHelp": "Erlaube das Überschreiben bestehender Details für die ausgewählten Hörbücher wenn eine Übereinstimmung gefunden wird",
|
"LabelUpdateDetailsHelp": "Erlaube das Überschreiben bestehender Details für die ausgewählten Hörbücher, wenn eine Übereinstimmung gefunden wird",
|
||||||
"LabelUploaderDragAndDrop": "Ziehen und Ablegen von Dateien oder Ordnern",
|
"LabelUploaderDragAndDrop": "Ziehen und Ablegen von Dateien oder Ordnern",
|
||||||
"LabelUploaderDropFiles": "Dateien löschen",
|
"LabelUploaderDropFiles": "Dateien löschen",
|
||||||
"LabelUploaderItemFetchMetadataHelp": "Automatisches Abholden von Titel, Author und Serien",
|
"LabelUploaderItemFetchMetadataHelp": "Automatisches Aktualisieren von Titel, Autor und Serie",
|
||||||
"LabelUseChapterTrack": "Kapiteldatei verwenden",
|
"LabelUseChapterTrack": "Kapiteldatei verwenden",
|
||||||
"LabelUseFullTrack": "Gesamte Datei verwenden",
|
"LabelUseFullTrack": "Gesamte Datei verwenden",
|
||||||
"LabelUser": "Benutzer",
|
"LabelUser": "Benutzer",
|
||||||
@ -533,17 +549,17 @@
|
|||||||
"LabelVersion": "Version",
|
"LabelVersion": "Version",
|
||||||
"LabelViewBookmarks": "Lesezeichen anzeigen",
|
"LabelViewBookmarks": "Lesezeichen anzeigen",
|
||||||
"LabelViewChapters": "Kapitel anzeigen",
|
"LabelViewChapters": "Kapitel anzeigen",
|
||||||
"LabelViewQueue": "Spieler-Warteschlange anzeigen",
|
"LabelViewQueue": "Player-Warteschlange anzeigen",
|
||||||
"LabelVolume": "Volumen",
|
"LabelVolume": "Lautstärke",
|
||||||
"LabelWeekdaysToRun": "Wochentage für die Ausführung",
|
"LabelWeekdaysToRun": "Wochentage für die Ausführung",
|
||||||
"LabelYourAudiobookDuration": "Laufzeit Ihres Mediums",
|
"LabelYourAudiobookDuration": "Laufzeit deines Mediums",
|
||||||
"LabelYourBookmarks": "Lesezeichen",
|
"LabelYourBookmarks": "Lesezeichen",
|
||||||
"LabelYourPlaylists": "Eigene Wiedergabelisten",
|
"LabelYourPlaylists": "Eigene Wiedergabelisten",
|
||||||
"LabelYourProgress": "Fortschritt",
|
"LabelYourProgress": "Fortschritt",
|
||||||
"MessageAddToPlayerQueue": "Zur Abspielwarteliste hinzufügen",
|
"MessageAddToPlayerQueue": "Zur Abspielwarteliste hinzufügen",
|
||||||
"MessageAppriseDescription": "Um diese Funktion nutzen zu können, müssen Sie eine Instanz von <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> laufen haben oder eine API verwenden welche dieselbe Anfragen bearbeiten kann. <br />Die Apprise API Url muss der vollständige URL-Pfad sein, an den die Benachrichtigung gesendet werden soll, z.B. wenn Ihre API-Instanz unter <code>http://192.168.1.1:8337</code> läuft, würden Sie <code>http://192.168.1.1:8337/notify</code> eingeben.",
|
"MessageAppriseDescription": "Um diese Funktion nutzen zu können, musst du eine Instanz von <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> laufen haben oder eine API verwenden welche dieselbe Anfragen bearbeiten kann. <br />Die Apprise API Url muss der vollständige URL-Pfad sein, an den die Benachrichtigung gesendet werden soll, z.B. wenn Ihre API-Instanz unter <code>http://192.168.1.1:8337</code> läuft, würdest du <code>http://192.168.1.1:8337/notify</code> eingeben.",
|
||||||
"MessageBackupsDescription": "In einer Sicherung werden Benutzer, Benutzerfortschritte, Details zu den Bibliotheksobjekten, Servereinstellungen und Bilder welche in <code>/metadata/items</code> & <code>/metadata/authors</code> gespeichert sind gespeichert. Sicherungen enthalten keine Dateien welche in den einzelnen Bibliotheksordnern (Medien-Ordnern) gespeichert sind.",
|
"MessageBackupsDescription": "In einer Sicherung werden Benutzer, Benutzerfortschritte, Details zu den Bibliotheksobjekten, Servereinstellungen und Bilder welche in <code>/metadata/items</code> & <code>/metadata/authors</code> gespeichert sind gespeichert. Sicherungen enthalten keine Dateien welche in den einzelnen Bibliotheksordnern (Medien-Ordnern) gespeichert sind.",
|
||||||
"MessageBatchQuickMatchDescription": "Der Schnellabgleich versucht, fehlende Titelbilder und Metadaten für die ausgewählten Artikel hinzuzufügen. Aktivieren Sie die nachstehenden Optionen, damit der Schnellabgleich vorhandene Titelbilder und/oder Metadaten überschreiben kann.",
|
"MessageBatchQuickMatchDescription": "Der Schnellabgleich versucht, fehlende Titelbilder und Metadaten für die ausgewählten Artikel hinzuzufügen. Aktiviere die nachstehenden Optionen, damit der Schnellabgleich vorhandene Titelbilder und/oder Metadaten überschreiben kann.",
|
||||||
"MessageBookshelfNoCollections": "Es wurden noch keine Sammlungen erstellt",
|
"MessageBookshelfNoCollections": "Es wurden noch keine Sammlungen erstellt",
|
||||||
"MessageBookshelfNoResultsForFilter": "Keine Ergebnisse für Filter \"{0}: {1}\"",
|
"MessageBookshelfNoResultsForFilter": "Keine Ergebnisse für Filter \"{0}: {1}\"",
|
||||||
"MessageBookshelfNoRSSFeeds": "Keine RSS-Feeds geöffnet",
|
"MessageBookshelfNoRSSFeeds": "Keine RSS-Feeds geöffnet",
|
||||||
@ -554,57 +570,57 @@
|
|||||||
"MessageChapterErrorStartLtPrev": "Ungültige Kapitelstartzeit: Kapitelanfang < Kapitelanfang vorheriges Kapitel (Kapitelanfang liegt zeitlich vor dem Beginn des vorherigen Kapitels -> Lösung: Kapitelanfang >= Startzeit des vorherigen Kapitels)",
|
"MessageChapterErrorStartLtPrev": "Ungültige Kapitelstartzeit: Kapitelanfang < Kapitelanfang vorheriges Kapitel (Kapitelanfang liegt zeitlich vor dem Beginn des vorherigen Kapitels -> Lösung: Kapitelanfang >= Startzeit des vorherigen Kapitels)",
|
||||||
"MessageChapterStartIsAfter": "Ungültige Kapitelstartzeit: Kapitelanfang > Mediumende (Kapitelanfang liegt nach dem Ende des Mediums)",
|
"MessageChapterStartIsAfter": "Ungültige Kapitelstartzeit: Kapitelanfang > Mediumende (Kapitelanfang liegt nach dem Ende des Mediums)",
|
||||||
"MessageCheckingCron": "Überprüfe Cron...",
|
"MessageCheckingCron": "Überprüfe Cron...",
|
||||||
"MessageConfirmCloseFeed": "Feed wird geschlossen! Sind Sie sicher?",
|
"MessageConfirmCloseFeed": "Feed wird geschlossen! Bist du dir sicher?",
|
||||||
"MessageConfirmDeleteBackup": "Sicherung für {0} wird gelöscht! Sind Sie sicher?",
|
"MessageConfirmDeleteBackup": "Sicherung für {0} wird gelöscht! Bist du dir sicher?",
|
||||||
"MessageConfirmDeleteFile": "Datei wird vom System gelöscht! Sind Sie sicher?",
|
"MessageConfirmDeleteFile": "Datei wird vom System gelöscht! Bist du dir sicher?",
|
||||||
"MessageConfirmDeleteLibrary": "Bibliothek \"{0}\" wird dauerhaft gelöscht! Sind Sie sicher?",
|
"MessageConfirmDeleteLibrary": "Bibliothek \"{0}\" wird dauerhaft gelöscht! Bist du dir sicher?",
|
||||||
"MessageConfirmDeleteLibraryItem": "Bibliothekselement wird aus der Datenbank + Festplatte gelöscht? Sind Sie sicher?",
|
"MessageConfirmDeleteLibraryItem": "Bibliothekselement wird aus der Datenbank + Festplatte gelöscht? Bist du dir sicher?",
|
||||||
"MessageConfirmDeleteLibraryItems": "{0} Bibliothekselemente werden aus der Datenbank + Festplatte gelöscht? Sind Sie sicher?",
|
"MessageConfirmDeleteLibraryItems": "{0} Bibliothekselemente werden aus der Datenbank + Festplatte gelöscht? Bist du dir sicher?",
|
||||||
"MessageConfirmDeleteSession": "Sitzung wird gelöscht! Sind Sie sicher?",
|
"MessageConfirmDeleteSession": "Sitzung wird gelöscht! Bist du dir sicher?",
|
||||||
"MessageConfirmForceReScan": "Scanvorgang erzwingen! Sind Sie sicher?",
|
"MessageConfirmForceReScan": "Scanvorgang erzwingen! Bist du dir sicher?",
|
||||||
"MessageConfirmMarkAllEpisodesFinished": "Alle Episoden werden als abgeschlossen markiert! Sind Sie sicher?",
|
"MessageConfirmMarkAllEpisodesFinished": "Alle Episoden werden als abgeschlossen markiert! Bist du dir sicher?",
|
||||||
"MessageConfirmMarkAllEpisodesNotFinished": "Alle Episoden werden als nicht abgeschlossen markiert! Sind Sie sicher?",
|
"MessageConfirmMarkAllEpisodesNotFinished": "Alle Episoden werden als nicht abgeschlossen markiert! Bist du dir sicher?",
|
||||||
"MessageConfirmMarkSeriesFinished": "Alle Medien dieser Reihe werden als abgeschlossen markiert! Sind Sie sicher?",
|
"MessageConfirmMarkSeriesFinished": "Alle Medien dieser Reihe werden als abgeschlossen markiert! Bist du dir sicher?",
|
||||||
"MessageConfirmMarkSeriesNotFinished": "Alle Medien dieser Reihe werden als nicht abgeschlossen markiert! Sind Sie sicher?",
|
"MessageConfirmMarkSeriesNotFinished": "Alle Medien dieser Reihe werden als nicht abgeschlossen markiert! Bist du dir sicher?",
|
||||||
"MessageConfirmQuickEmbed": "Warnung! Audiodateien werden bei der Schnelleinbettung nicht gesichert! Achten Sie darauf, dass Sie eine Sicherungskopie der Audiodateien besitzen. <br><br>Möchten Sie fortfahren?",
|
"MessageConfirmQuickEmbed": "Warnung! Audiodateien werden bei der Schnelleinbettung nicht gesichert! Achte darauf, dass du eine Sicherungskopie der Audiodateien besitzt. <br><br>Möchtest du fortfahren?",
|
||||||
"MessageConfirmRemoveAllChapters": "Alle Kapitel werden entfernt! Sind Sie sicher?",
|
"MessageConfirmRemoveAllChapters": "Alle Kapitel werden entfernt! Bist du dir sicher?",
|
||||||
"MessageConfirmRemoveAuthor": "Autor \"{0}\" wird enfernt! Sind Sie sicher?",
|
"MessageConfirmRemoveAuthor": "Autor \"{0}\" wird enfernt! Bist du dir sicher?",
|
||||||
"MessageConfirmRemoveCollection": "Sammlung \"{0}\" wird gelöscht! Sind Sie sicher?",
|
"MessageConfirmRemoveCollection": "Sammlung \"{0}\" wird gelöscht! Bist du dir sicher?",
|
||||||
"MessageConfirmRemoveEpisode": "Episode \"{0}\" wird geloscht! Sind Sie sicher?",
|
"MessageConfirmRemoveEpisode": "Episode \"{0}\" wird geloscht! Bist du dir sicher?",
|
||||||
"MessageConfirmRemoveEpisodes": "{0} Episoden werden gelöscht! Sind Sie sicher?",
|
"MessageConfirmRemoveEpisodes": "{0} Episoden werden gelöscht! Bist du dir sicher?",
|
||||||
"MessageConfirmRemoveListeningSessions": "Sind Sie sicher, dass sie {0} Hörsitzungen enfernen möchten?",
|
"MessageConfirmRemoveListeningSessions": "Bist du dir sicher, dass du {0} Hörsitzungen enfernen möchtest?",
|
||||||
"MessageConfirmRemoveNarrator": "Erzähler \"{0}\" wird gelöscht! Sind Sie sicher?",
|
"MessageConfirmRemoveNarrator": "Erzähler \"{0}\" wird gelöscht! Bist du dir sicher?",
|
||||||
"MessageConfirmRemovePlaylist": "Wiedergabeliste \"{0}\" wird entfernt! Sind Sie sicher?",
|
"MessageConfirmRemovePlaylist": "Wiedergabeliste \"{0}\" wird entfernt! Bist du dir sicher?",
|
||||||
"MessageConfirmRenameGenre": "Kategorie \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts werden umbenannt! Sind Sie sicher?",
|
"MessageConfirmRenameGenre": "Kategorie \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts werden umbenannt! Bist du dir sicher?",
|
||||||
"MessageConfirmRenameGenreMergeNote": "Hinweis: Kategorie existiert bereits -> Kategorien werden zusammengelegt.",
|
"MessageConfirmRenameGenreMergeNote": "Hinweis: Kategorie existiert bereits -> Kategorien werden zusammengelegt.",
|
||||||
"MessageConfirmRenameGenreWarning": "Warnung! Ein ähnliche Kategorie mit einem anderen Wortlaut existiert bereits: \"{0}\".",
|
"MessageConfirmRenameGenreWarning": "Warnung! Ein ähnliche Kategorie mit einem anderen Wortlaut existiert bereits: \"{0}\".",
|
||||||
"MessageConfirmRenameTag": "Tag \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts werden umbenannt! Sind Sie sicher?",
|
"MessageConfirmRenameTag": "Tag \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts werden umbenannt! Bist du dir sicher?",
|
||||||
"MessageConfirmRenameTagMergeNote": "Hinweis: Tag existiert bereits -> Tags werden zusammengelegt.",
|
"MessageConfirmRenameTagMergeNote": "Hinweis: Tag existiert bereits -> Tags werden zusammengelegt.",
|
||||||
"MessageConfirmRenameTagWarning": "Warnung! Ein ähnlicher Tag mit einem anderen Wortlaut existiert bereits: \"{0}\".",
|
"MessageConfirmRenameTagWarning": "Warnung! Ein ähnlicher Tag mit einem anderen Wortlaut existiert bereits: \"{0}\".",
|
||||||
"MessageConfirmReScanLibraryItems": "{0} Elemente werden erneut gescannt! Sind Sie sicher?",
|
"MessageConfirmReScanLibraryItems": "{0} Elemente werden erneut gescannt! Bist du dir sicher?",
|
||||||
"MessageConfirmSendEbookToDevice": "{0} E-Book \"{1}\" werden auf das Gerät \"{2}\" gesendet! Sind Sie sicher?",
|
"MessageConfirmSendEbookToDevice": "{0} E-Book \"{1}\" wird auf das Gerät \"{2}\" gesendet! Bist du dir sicher?",
|
||||||
"MessageDownloadingEpisode": "Episode herunterladen",
|
"MessageDownloadingEpisode": "Episode wird heruntergeladen",
|
||||||
"MessageDragFilesIntoTrackOrder": "Verschieben Sie die Dateien in die richtige Reihenfolge",
|
"MessageDragFilesIntoTrackOrder": "Verschiebe die Dateien in die richtige Reihenfolge",
|
||||||
"MessageEmbedFinished": "Einbettung abgeschlossen!",
|
"MessageEmbedFinished": "Einbettung abgeschlossen!",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} Episode(n) in der Warteschlange zum Herunterladen",
|
"MessageEpisodesQueuedForDownload": "{0} Episode(n) in der Warteschlange zum Herunterladen",
|
||||||
"MessageFeedURLWillBe": "Feed-URL wird {0} sein",
|
"MessageFeedURLWillBe": "Feed-URL wird {0} sein",
|
||||||
"MessageFetching": "Abrufen...",
|
"MessageFetching": "Abrufen...",
|
||||||
"MessageForceReScanDescription": "durchsucht alle Dateien neu, wie bei einem frischen Scan. ID3-Tags von Audiodateien, OPF-Dateien und Textdateien werden neu durchsucht.",
|
"MessageForceReScanDescription": "Durchsucht alle Dateien erneut, wie bei einem frischen Scan. ID3-Tags von Audiodateien, OPF-Dateien und Textdateien werden neu durchsucht.",
|
||||||
"MessageImportantNotice": "Wichtiger Hinweis!",
|
"MessageImportantNotice": "Wichtiger Hinweis!",
|
||||||
"MessageInsertChapterBelow": "Kapitel unten einfügen",
|
"MessageInsertChapterBelow": "Kapitel unten einfügen",
|
||||||
"MessageItemsSelected": "{0} ausgewählte Medien",
|
"MessageItemsSelected": "{0} ausgewählte Medien",
|
||||||
"MessageItemsUpdated": "{0} Medien aktualisiert",
|
"MessageItemsUpdated": "{0} Medien aktualisiert",
|
||||||
"MessageJoinUsOn": "Besuchen Sie uns auf",
|
"MessageJoinUsOn": "Besuche uns auf",
|
||||||
"MessageListeningSessionsInTheLastYear": "{0} Ereignisse im letzten Jahr",
|
"MessageListeningSessionsInTheLastYear": "{0} Ereignisse im letzten Jahr",
|
||||||
"MessageLoading": "Laden...",
|
"MessageLoading": "Laden...",
|
||||||
"MessageLoadingFolders": "Lade Ordner...",
|
"MessageLoadingFolders": "Lade Ordner...",
|
||||||
"MessageM4BFailed": "M4B fehlgeschlagen!",
|
"MessageM4BFailed": "M4B fehlgeschlagen!",
|
||||||
"MessageM4BFinished": "M4B beendet!",
|
"MessageM4BFinished": "M4B beendet!",
|
||||||
"MessageMapChapterTitles": "Zuordnen von Kapiteltiteln zu Ihren vorhandenen Medienkapiteln ohne Anpassung der Zeitangaben",
|
"MessageMapChapterTitles": "Zuordnen von Kapiteltiteln zu deinen vorhandenen Medienkapiteln ohne Anpassung der Zeitangaben",
|
||||||
"MessageMarkAllEpisodesFinished": "Alle Episoden als beendet markieren",
|
"MessageMarkAllEpisodesFinished": "Alle Episoden als beendet markieren",
|
||||||
"MessageMarkAllEpisodesNotFinished": "Alle Episoden als ungehört markieren",
|
"MessageMarkAllEpisodesNotFinished": "Alle Episoden als ungehört markieren",
|
||||||
"MessageMarkAsFinished": "Als beendet markieren",
|
"MessageMarkAsFinished": "Als beendet markieren",
|
||||||
"MessageMarkAsNotFinished": "Als nicht abgeschlossen markieren",
|
"MessageMarkAsNotFinished": "Als nicht beendet markieren",
|
||||||
"MessageMatchBooksDescription": "Es wird versucht die Bücher in der Bibliothek mit einem Buch des ausgewählten Suchanbieters abzugleichen um leere Details und das Titelbild auszufüllen. Vorhandene Details werden nicht überschrieben.",
|
"MessageMatchBooksDescription": "Es wird versucht die Bücher in der Bibliothek mit einem Buch des ausgewählten Suchanbieters abzugleichen um leere Details und das Titelbild auszufüllen. Vorhandene Details werden nicht überschrieben.",
|
||||||
"MessageNoAudioTracks": "Keine Audiodateien",
|
"MessageNoAudioTracks": "Keine Audiodateien",
|
||||||
"MessageNoAuthors": "Keine Autoren",
|
"MessageNoAuthors": "Keine Autoren",
|
||||||
@ -637,7 +653,7 @@
|
|||||||
"MessageNoUpdateNecessary": "Keine Aktualisierung erforderlich",
|
"MessageNoUpdateNecessary": "Keine Aktualisierung erforderlich",
|
||||||
"MessageNoUpdatesWereNecessary": "Keine Aktualisierungen waren notwendig",
|
"MessageNoUpdatesWereNecessary": "Keine Aktualisierungen waren notwendig",
|
||||||
"MessageNoUserPlaylists": "Keine Wiedergabelisten vorhanden",
|
"MessageNoUserPlaylists": "Keine Wiedergabelisten vorhanden",
|
||||||
"MessageOr": "oder",
|
"MessageOr": "Oder",
|
||||||
"MessagePauseChapter": "Kapitelwiedergabe pausieren",
|
"MessagePauseChapter": "Kapitelwiedergabe pausieren",
|
||||||
"MessagePlayChapter": "Kapitelanfang anhören",
|
"MessagePlayChapter": "Kapitelanfang anhören",
|
||||||
"MessagePlaylistCreateFromCollection": "Erstelle eine Wiedergabeliste aus der Sammlung",
|
"MessagePlaylistCreateFromCollection": "Erstelle eine Wiedergabeliste aus der Sammlung",
|
||||||
@ -646,11 +662,11 @@
|
|||||||
"MessageRemoveChapter": "Kapitel löschen",
|
"MessageRemoveChapter": "Kapitel löschen",
|
||||||
"MessageRemoveEpisodes": "Entferne {0} Episode(n)",
|
"MessageRemoveEpisodes": "Entferne {0} Episode(n)",
|
||||||
"MessageRemoveFromPlayerQueue": "Aus der Abspielwarteliste löschen",
|
"MessageRemoveFromPlayerQueue": "Aus der Abspielwarteliste löschen",
|
||||||
"MessageRemoveUserWarning": "Benutzer \"{0}\" wird dauerhaft gelöscht! Sind Sie sicher?",
|
"MessageRemoveUserWarning": "Benutzer \"{0}\" wird dauerhaft gelöscht! Bist du dir sicher?",
|
||||||
"MessageReportBugsAndContribute": "Fehler melden, Funktionen anfordern und Beiträge leisten auf",
|
"MessageReportBugsAndContribute": "Fehler melden, Funktionen anfordern und mitwirken",
|
||||||
"MessageResetChaptersConfirm": "Kapitel und vorgenommenen Änderungen werden zurückgesetzt und rückgängig gemacht! Sind Sie sicher?",
|
"MessageResetChaptersConfirm": "Kapitel und vorgenommenen Änderungen werden zurückgesetzt und rückgängig gemacht! Bist du dir sicher?",
|
||||||
"MessageRestoreBackupConfirm": "Sind Sie sicher, dass Sie die Sicherung wiederherstellen wollen, welche am",
|
"MessageRestoreBackupConfirm": "Bist du dir sicher, dass du die Sicherung wiederherstellen willst, welche am",
|
||||||
"MessageRestoreBackupWarning": "Bei der Wiederherstellung einer Sicherung wird die gesamte Datenbank unter /config und die Titelbilder in /metadata/items und /metadata/authors überschrieben.<br /><br />Bei der Sicherung werden keine Dateien in Ihren Bibliotheksordnern verändert. Wenn Sie die Servereinstellungen aktiviert haben, um Cover und Metadaten in Ihren Bibliotheksordnern zu speichern, werden diese nicht gesichert oder überschrieben.<br /><br />Alle Clients, die Ihren Server nutzen, werden automatisch aktualisiert.",
|
"MessageRestoreBackupWarning": "Bei der Wiederherstellung einer Sicherung wird die gesamte Datenbank unter /config und die Titelbilder in /metadata/items und /metadata/authors überschrieben.<br /><br />Bei der Sicherung werden keine Dateien in deinen Bibliotheksordnern verändert. Wenn du die Servereinstellungen aktiviert hast, um Cover und Metadaten in deinen Bibliotheksordnern zu speichern, werden diese nicht gesichert oder überschrieben.<br /><br />Alle Clients, die Ihren Server nutzen, werden automatisch aktualisiert.",
|
||||||
"MessageSearchResultsFor": "Suchergebnisse für",
|
"MessageSearchResultsFor": "Suchergebnisse für",
|
||||||
"MessageSelected": "{0} ausgewählt",
|
"MessageSelected": "{0} ausgewählt",
|
||||||
"MessageServerCouldNotBeReached": "Server kann nicht erreicht werden",
|
"MessageServerCouldNotBeReached": "Server kann nicht erreicht werden",
|
||||||
@ -663,15 +679,15 @@
|
|||||||
"MessageValidCronExpression": "Gültiger Cron-Ausdruck",
|
"MessageValidCronExpression": "Gültiger Cron-Ausdruck",
|
||||||
"MessageWatcherIsDisabledGlobally": "Überwachung ist in den Servereinstellungen global deaktiviert",
|
"MessageWatcherIsDisabledGlobally": "Überwachung ist in den Servereinstellungen global deaktiviert",
|
||||||
"MessageXLibraryIsEmpty": "{0} Bibliothek ist leer!",
|
"MessageXLibraryIsEmpty": "{0} Bibliothek ist leer!",
|
||||||
"MessageYourAudiobookDurationIsLonger": "Die Dauer Ihres Mediums ist länger als die gefundene Dauer",
|
"MessageYourAudiobookDurationIsLonger": "Die Dauer deines Mediums ist länger als die gefundene Dauer",
|
||||||
"MessageYourAudiobookDurationIsShorter": "Die Dauer Ihres Mediums ist kürzer als die gefundene Dauer",
|
"MessageYourAudiobookDurationIsShorter": "Die Dauer deines Mediums ist kürzer als die gefundene Dauer",
|
||||||
"NoteChangeRootPassword": "Der Root-Benutzer (Hauptbenutzer) ist der einzige Benutzer, der ein leeres Passwort haben kann",
|
"NoteChangeRootPassword": "Der Root-Benutzer (Hauptbenutzer) ist der einzige Benutzer, der ein leeres Passwort haben kann",
|
||||||
"NoteChapterEditorTimes": "Hinweis: Die Anfangszeit des ersten Kapitels muss bei 0:00 beginnen und die Anfangszeit des letzten Kapitels darf die Dauer des Mediums nicht überschreiten.",
|
"NoteChapterEditorTimes": "Hinweis: Die Anfangszeit des ersten Kapitels muss bei 0:00 beginnen und die Anfangszeit des letzten Kapitels darf die Dauer des Mediums nicht überschreiten.",
|
||||||
"NoteFolderPicker": "Hinweis: Bereits zugeordnete Ordner werden nicht angezeigt.",
|
"NoteFolderPicker": "Hinweis: Bereits zugeordnete Ordner werden nicht angezeigt.",
|
||||||
"NoteRSSFeedPodcastAppsHttps": "Warnung: Die meisten Podcast-Apps verlangen, dass die URL des RSS-Feeds HTTPS verwendet.",
|
"NoteRSSFeedPodcastAppsHttps": "Warnung: Die meisten Podcast-Apps verlangen, dass die URL des RSS-Feeds HTTPS verwendet.",
|
||||||
"NoteRSSFeedPodcastAppsPubDate": "Warnung: 1 oder mehrere Ihrer Episoden haben kein Veröffentlichungsdatum. Einige Podcast-Apps verlangen dies.",
|
"NoteRSSFeedPodcastAppsPubDate": "Warnung: 1 oder mehrere deiner Episoden haben kein Veröffentlichungsdatum. Einige Podcast-Apps verlangen dies.",
|
||||||
"NoteUploaderFoldersWithMediaFiles": "Ordner mit Mediendateien werden als separate Bibliothekselemente behandelt.",
|
"NoteUploaderFoldersWithMediaFiles": "Ordner mit Mediendateien werden als separate Bibliothekselemente behandelt.",
|
||||||
"NoteUploaderOnlyAudioFiles": "Wenn Sie nur Audiodateien hochladen, wird jede Audiodatei als ein separates Medium behandelt.",
|
"NoteUploaderOnlyAudioFiles": "Wenn du nur Audiodateien hochlädst, wird jede Audiodatei als ein separates Medium behandelt.",
|
||||||
"NoteUploaderUnsupportedFiles": "Nicht unterstützte Dateien werden ignoriert. Bei der Auswahl oder dem Löschen eines Ordners werden andere Dateien, die sich nicht in einem Elementordner befinden, ignoriert.",
|
"NoteUploaderUnsupportedFiles": "Nicht unterstützte Dateien werden ignoriert. Bei der Auswahl oder dem Löschen eines Ordners werden andere Dateien, die sich nicht in einem Elementordner befinden, ignoriert.",
|
||||||
"PlaceholderNewCollection": "Neuer Sammlungsname",
|
"PlaceholderNewCollection": "Neuer Sammlungsname",
|
||||||
"PlaceholderNewFolderPath": "Neuer Ordnerpfad",
|
"PlaceholderNewFolderPath": "Neuer Ordnerpfad",
|
||||||
@ -739,7 +755,7 @@
|
|||||||
"ToastRSSFeedCloseFailed": "RSS-Feed konnte nicht geschlossen werden",
|
"ToastRSSFeedCloseFailed": "RSS-Feed konnte nicht geschlossen werden",
|
||||||
"ToastRSSFeedCloseSuccess": "RSS-Feed geschlossen",
|
"ToastRSSFeedCloseSuccess": "RSS-Feed geschlossen",
|
||||||
"ToastSendEbookToDeviceFailed": "E-Book konnte nicht auf Gerät übertragen werden",
|
"ToastSendEbookToDeviceFailed": "E-Book konnte nicht auf Gerät übertragen werden",
|
||||||
"ToastSendEbookToDeviceSuccess": "E-Book an Gerät senden \"{0}\"",
|
"ToastSendEbookToDeviceSuccess": "E-Book an Gerät \"{0}\" gesendet",
|
||||||
"ToastSeriesUpdateFailed": "Aktualisierung der Serien fehlgeschlagen",
|
"ToastSeriesUpdateFailed": "Aktualisierung der Serien fehlgeschlagen",
|
||||||
"ToastSeriesUpdateSuccess": "Serien aktualisiert",
|
"ToastSeriesUpdateSuccess": "Serien aktualisiert",
|
||||||
"ToastSessionDeleteFailed": "Sitzung konnte nicht gelöscht werden",
|
"ToastSessionDeleteFailed": "Sitzung konnte nicht gelöscht werden",
|
||||||
@ -749,4 +765,4 @@
|
|||||||
"ToastSocketFailedToConnect": "Verbindung zum WebSocket fehlgeschlagen",
|
"ToastSocketFailedToConnect": "Verbindung zum WebSocket fehlgeschlagen",
|
||||||
"ToastUserDeleteFailed": "Benutzer konnte nicht gelöscht werden",
|
"ToastUserDeleteFailed": "Benutzer konnte nicht gelöscht werden",
|
||||||
"ToastUserDeleteSuccess": "Benutzer gelöscht"
|
"ToastUserDeleteSuccess": "Benutzer gelöscht"
|
||||||
}
|
}
|
||||||
|
@ -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,15 @@
|
|||||||
"ButtonMatchAllAuthors": "Match All Authors",
|
"ButtonMatchAllAuthors": "Match All Authors",
|
||||||
"ButtonMatchBooks": "Match Books",
|
"ButtonMatchBooks": "Match Books",
|
||||||
"ButtonNevermind": "Nevermind",
|
"ButtonNevermind": "Nevermind",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
@ -104,6 +109,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",
|
||||||
@ -281,8 +287,11 @@
|
|||||||
"LabelFinished": "Finished",
|
"LabelFinished": "Finished",
|
||||||
"LabelFolder": "Folder",
|
"LabelFolder": "Folder",
|
||||||
"LabelFolders": "Folders",
|
"LabelFolders": "Folders",
|
||||||
|
"LabelFontBold": "Bold",
|
||||||
"LabelFontFamily": "Font family",
|
"LabelFontFamily": "Font family",
|
||||||
|
"LabelFontItalic": "Italic",
|
||||||
"LabelFontScale": "Font scale",
|
"LabelFontScale": "Font scale",
|
||||||
|
"LabelFontStrikethrough": "Strikethrough",
|
||||||
"LabelFormat": "Format",
|
"LabelFormat": "Format",
|
||||||
"LabelGenre": "Genre",
|
"LabelGenre": "Genre",
|
||||||
"LabelGenres": "Genres",
|
"LabelGenres": "Genres",
|
||||||
@ -387,6 +396,7 @@
|
|||||||
"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)",
|
||||||
@ -403,6 +413,7 @@
|
|||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
"LabelRecommended": "Recommended",
|
"LabelRecommended": "Recommended",
|
||||||
|
"LabelRedo": "Redo",
|
||||||
"LabelRegion": "Region",
|
"LabelRegion": "Region",
|
||||||
"LabelReleaseDate": "Release Date",
|
"LabelReleaseDate": "Release Date",
|
||||||
"LabelRemoveCover": "Remove cover",
|
"LabelRemoveCover": "Remove cover",
|
||||||
@ -491,6 +502,10 @@
|
|||||||
"LabelTagsAccessibleToUser": "Tags Accessible to User",
|
"LabelTagsAccessibleToUser": "Tags Accessible to User",
|
||||||
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
||||||
"LabelTasks": "Tasks Running",
|
"LabelTasks": "Tasks Running",
|
||||||
|
"LabelTextEditorBulletedList": "Bulleted list",
|
||||||
|
"LabelTextEditorLink": "Link",
|
||||||
|
"LabelTextEditorNumberedList": "Numbered list",
|
||||||
|
"LabelTextEditorUnlink": "Unlink",
|
||||||
"LabelTheme": "Theme",
|
"LabelTheme": "Theme",
|
||||||
"LabelThemeDark": "Dark",
|
"LabelThemeDark": "Dark",
|
||||||
"LabelThemeLight": "Light",
|
"LabelThemeLight": "Light",
|
||||||
@ -516,6 +531,7 @@
|
|||||||
"LabelTracksSingleTrack": "Single-track",
|
"LabelTracksSingleTrack": "Single-track",
|
||||||
"LabelType": "Type",
|
"LabelType": "Type",
|
||||||
"LabelUnabridged": "Unabridged",
|
"LabelUnabridged": "Unabridged",
|
||||||
|
"LabelUndo": "Undo",
|
||||||
"LabelUnknown": "Unknown",
|
"LabelUnknown": "Unknown",
|
||||||
"LabelUpdateCover": "Update Cover",
|
"LabelUpdateCover": "Update Cover",
|
||||||
"LabelUpdateCoverHelp": "Allow overwriting of existing covers for the selected books when a match is located",
|
"LabelUpdateCoverHelp": "Allow overwriting of existing covers for the selected books when a match is located",
|
||||||
|
@ -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,15 @@
|
|||||||
"ButtonMatchAllAuthors": "Encontrar Todos los Autores",
|
"ButtonMatchAllAuthors": "Encontrar Todos los Autores",
|
||||||
"ButtonMatchBooks": "Encontrar Libros",
|
"ButtonMatchBooks": "Encontrar Libros",
|
||||||
"ButtonNevermind": "Olvidar",
|
"ButtonNevermind": "Olvidar",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
@ -87,15 +92,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 +109,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 +136,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",
|
||||||
@ -187,11 +193,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 +205,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 +221,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 +246,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 +277,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,20 +287,23 @@
|
|||||||
"LabelFinished": "Terminado",
|
"LabelFinished": "Terminado",
|
||||||
"LabelFolder": "Carpeta",
|
"LabelFolder": "Carpeta",
|
||||||
"LabelFolders": "Carpetas",
|
"LabelFolders": "Carpetas",
|
||||||
|
"LabelFontBold": "Negrilla",
|
||||||
"LabelFontFamily": "Familia tipográfica",
|
"LabelFontFamily": "Familia tipográfica",
|
||||||
|
"LabelFontItalic": "Itálica",
|
||||||
"LabelFontScale": "Tamaño de Fuente",
|
"LabelFontScale": "Tamaño de Fuente",
|
||||||
|
"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",
|
||||||
@ -331,20 +340,20 @@
|
|||||||
"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",
|
||||||
"LabelMissingParts": "Partes Ausentes",
|
"LabelMissingParts": "Partes Ausentes",
|
||||||
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
|
"LabelMobileRedirectURIs": "URIs de redirección a móviles permitidos",
|
||||||
"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": "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",
|
||||||
@ -387,6 +396,7 @@
|
|||||||
"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.)",
|
||||||
@ -403,10 +413,11 @@
|
|||||||
"LabelRecentlyAdded": "Agregado Recientemente",
|
"LabelRecentlyAdded": "Agregado Recientemente",
|
||||||
"LabelRecentSeries": "Series Recientes",
|
"LabelRecentSeries": "Series Recientes",
|
||||||
"LabelRecommended": "Recomendados",
|
"LabelRecommended": "Recomendados",
|
||||||
|
"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",
|
||||||
@ -419,7 +430,7 @@
|
|||||||
"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",
|
||||||
@ -491,10 +502,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": "Lista con viñetas",
|
||||||
|
"LabelTextEditorLink": "Enlazar",
|
||||||
|
"LabelTextEditorNumberedList": "Lista numerada",
|
||||||
|
"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",
|
||||||
@ -516,6 +531,7 @@
|
|||||||
"LabelTracksSingleTrack": "Una pista",
|
"LabelTracksSingleTrack": "Una pista",
|
||||||
"LabelType": "Tipo",
|
"LabelType": "Tipo",
|
||||||
"LabelUnabridged": "No Abreviado",
|
"LabelUnabridged": "No Abreviado",
|
||||||
|
"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.",
|
||||||
@ -524,7 +540,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",
|
||||||
@ -558,15 +574,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}\"?",
|
||||||
@ -581,7 +597,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.",
|
||||||
@ -652,7 +668,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}?",
|
||||||
|
@ -32,6 +32,8 @@
|
|||||||
"ButtonHide": "Cacher",
|
"ButtonHide": "Cacher",
|
||||||
"ButtonHome": "Accueil",
|
"ButtonHome": "Accueil",
|
||||||
"ButtonIssues": "Parutions",
|
"ButtonIssues": "Parutions",
|
||||||
|
"ButtonJumpBackward": "Jump Backward",
|
||||||
|
"ButtonJumpForward": "Jump Forward",
|
||||||
"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,15 @@
|
|||||||
"ButtonMatchAllAuthors": "Chercher tous les auteurs",
|
"ButtonMatchAllAuthors": "Chercher tous les auteurs",
|
||||||
"ButtonMatchBooks": "Chercher les livres",
|
"ButtonMatchBooks": "Chercher les livres",
|
||||||
"ButtonNevermind": "Non merci",
|
"ButtonNevermind": "Non merci",
|
||||||
|
"ButtonNextChapter": "Next Chapter",
|
||||||
"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",
|
||||||
|
"ButtonPreviousChapter": "Previous Chapter",
|
||||||
"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",
|
||||||
@ -104,6 +109,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": "Custom Metadata Providers",
|
||||||
"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",
|
||||||
@ -281,8 +287,11 @@
|
|||||||
"LabelFinished": "Terminé le",
|
"LabelFinished": "Terminé le",
|
||||||
"LabelFolder": "Dossier",
|
"LabelFolder": "Dossier",
|
||||||
"LabelFolders": "Dossiers",
|
"LabelFolders": "Dossiers",
|
||||||
|
"LabelFontBold": "Bold",
|
||||||
"LabelFontFamily": "Polices de caractères",
|
"LabelFontFamily": "Polices de caractères",
|
||||||
|
"LabelFontItalic": "Italic",
|
||||||
"LabelFontScale": "Taille de la police de caractère",
|
"LabelFontScale": "Taille de la police de caractère",
|
||||||
|
"LabelFontStrikethrough": "Strikethrough",
|
||||||
"LabelFormat": "Format",
|
"LabelFormat": "Format",
|
||||||
"LabelGenre": "Genre",
|
"LabelGenre": "Genre",
|
||||||
"LabelGenres": "Genres",
|
"LabelGenres": "Genres",
|
||||||
@ -387,6 +396,7 @@
|
|||||||
"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)",
|
||||||
@ -403,6 +413,7 @@
|
|||||||
"LabelRecentlyAdded": "Derniers ajouts",
|
"LabelRecentlyAdded": "Derniers ajouts",
|
||||||
"LabelRecentSeries": "Séries récentes",
|
"LabelRecentSeries": "Séries récentes",
|
||||||
"LabelRecommended": "Recommandé",
|
"LabelRecommended": "Recommandé",
|
||||||
|
"LabelRedo": "Redo",
|
||||||
"LabelRegion": "Région",
|
"LabelRegion": "Région",
|
||||||
"LabelReleaseDate": "Date de parution",
|
"LabelReleaseDate": "Date de parution",
|
||||||
"LabelRemoveCover": "Supprimer la couverture",
|
"LabelRemoveCover": "Supprimer la couverture",
|
||||||
@ -491,6 +502,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",
|
||||||
|
"LabelTextEditorLink": "Link",
|
||||||
|
"LabelTextEditorNumberedList": "Numbered list",
|
||||||
|
"LabelTextEditorUnlink": "Unlink",
|
||||||
"LabelTheme": "Thème",
|
"LabelTheme": "Thème",
|
||||||
"LabelThemeDark": "Sombre",
|
"LabelThemeDark": "Sombre",
|
||||||
"LabelThemeLight": "Clair",
|
"LabelThemeLight": "Clair",
|
||||||
@ -516,6 +531,7 @@
|
|||||||
"LabelTracksSingleTrack": "Piste simple",
|
"LabelTracksSingleTrack": "Piste simple",
|
||||||
"LabelType": "Type",
|
"LabelType": "Type",
|
||||||
"LabelUnabridged": "Version intégrale",
|
"LabelUnabridged": "Version intégrale",
|
||||||
|
"LabelUndo": "Undo",
|
||||||
"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",
|
||||||
|
@ -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,15 @@
|
|||||||
"ButtonMatchAllAuthors": "બધા મેળ ખાતા લેખકો શોધો",
|
"ButtonMatchAllAuthors": "બધા મેળ ખાતા લેખકો શોધો",
|
||||||
"ButtonMatchBooks": "મેળ ખાતી પુસ્તકો શોધો",
|
"ButtonMatchBooks": "મેળ ખાતી પુસ્તકો શોધો",
|
||||||
"ButtonNevermind": "કંઈ વાંધો નહીં",
|
"ButtonNevermind": "કંઈ વાંધો નહીં",
|
||||||
|
"ButtonNextChapter": "Next Chapter",
|
||||||
"ButtonOk": "ઓકે",
|
"ButtonOk": "ઓકે",
|
||||||
"ButtonOpenFeed": "ફીડ ખોલો",
|
"ButtonOpenFeed": "ફીડ ખોલો",
|
||||||
"ButtonOpenManager": "મેનેજર ખોલો",
|
"ButtonOpenManager": "મેનેજર ખોલો",
|
||||||
|
"ButtonPause": "Pause",
|
||||||
"ButtonPlay": "ચલાવો",
|
"ButtonPlay": "ચલાવો",
|
||||||
"ButtonPlaying": "ચલાવી રહ્યું છે",
|
"ButtonPlaying": "ચલાવી રહ્યું છે",
|
||||||
"ButtonPlaylists": "પ્લેલિસ્ટ",
|
"ButtonPlaylists": "પ્લેલિસ્ટ",
|
||||||
|
"ButtonPreviousChapter": "Previous Chapter",
|
||||||
"ButtonPurgeAllCache": "બધો Cache કાઢી નાખો",
|
"ButtonPurgeAllCache": "બધો Cache કાઢી નાખો",
|
||||||
"ButtonPurgeItemsCache": "વસ્તુઓનો Cache કાઢી નાખો",
|
"ButtonPurgeItemsCache": "વસ્તુઓનો Cache કાઢી નાખો",
|
||||||
"ButtonPurgeMediaProgress": "બધું સાંભળ્યું કાઢી નાખો",
|
"ButtonPurgeMediaProgress": "બધું સાંભળ્યું કાઢી નાખો",
|
||||||
@ -104,6 +109,7 @@
|
|||||||
"HeaderCollectionItems": "સંગ્રહ વસ્તુઓ",
|
"HeaderCollectionItems": "સંગ્રહ વસ્તુઓ",
|
||||||
"HeaderCover": "આવરણ",
|
"HeaderCover": "આવરણ",
|
||||||
"HeaderCurrentDownloads": "વર્તમાન ડાઉનલોડ્સ",
|
"HeaderCurrentDownloads": "વર્તમાન ડાઉનલોડ્સ",
|
||||||
|
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
|
||||||
"HeaderDetails": "વિગતો",
|
"HeaderDetails": "વિગતો",
|
||||||
"HeaderDownloadQueue": "ડાઉનલોડ કતાર",
|
"HeaderDownloadQueue": "ડાઉનલોડ કતાર",
|
||||||
"HeaderEbookFiles": "ઇબુક ફાઇલો",
|
"HeaderEbookFiles": "ઇબુક ફાઇલો",
|
||||||
@ -281,8 +287,11 @@
|
|||||||
"LabelFinished": "Finished",
|
"LabelFinished": "Finished",
|
||||||
"LabelFolder": "Folder",
|
"LabelFolder": "Folder",
|
||||||
"LabelFolders": "Folders",
|
"LabelFolders": "Folders",
|
||||||
|
"LabelFontBold": "Bold",
|
||||||
"LabelFontFamily": "ફોન્ટ કુટુંબ",
|
"LabelFontFamily": "ફોન્ટ કુટુંબ",
|
||||||
|
"LabelFontItalic": "Italic",
|
||||||
"LabelFontScale": "Font scale",
|
"LabelFontScale": "Font scale",
|
||||||
|
"LabelFontStrikethrough": "Strikethrough",
|
||||||
"LabelFormat": "Format",
|
"LabelFormat": "Format",
|
||||||
"LabelGenre": "Genre",
|
"LabelGenre": "Genre",
|
||||||
"LabelGenres": "Genres",
|
"LabelGenres": "Genres",
|
||||||
@ -387,6 +396,7 @@
|
|||||||
"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)",
|
||||||
@ -403,6 +413,7 @@
|
|||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
"LabelRecommended": "Recommended",
|
"LabelRecommended": "Recommended",
|
||||||
|
"LabelRedo": "Redo",
|
||||||
"LabelRegion": "Region",
|
"LabelRegion": "Region",
|
||||||
"LabelReleaseDate": "Release Date",
|
"LabelReleaseDate": "Release Date",
|
||||||
"LabelRemoveCover": "Remove cover",
|
"LabelRemoveCover": "Remove cover",
|
||||||
@ -491,6 +502,10 @@
|
|||||||
"LabelTagsAccessibleToUser": "Tags Accessible to User",
|
"LabelTagsAccessibleToUser": "Tags Accessible to User",
|
||||||
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
||||||
"LabelTasks": "Tasks Running",
|
"LabelTasks": "Tasks Running",
|
||||||
|
"LabelTextEditorBulletedList": "Bulleted list",
|
||||||
|
"LabelTextEditorLink": "Link",
|
||||||
|
"LabelTextEditorNumberedList": "Numbered list",
|
||||||
|
"LabelTextEditorUnlink": "Unlink",
|
||||||
"LabelTheme": "Theme",
|
"LabelTheme": "Theme",
|
||||||
"LabelThemeDark": "Dark",
|
"LabelThemeDark": "Dark",
|
||||||
"LabelThemeLight": "Light",
|
"LabelThemeLight": "Light",
|
||||||
@ -516,6 +531,7 @@
|
|||||||
"LabelTracksSingleTrack": "Single-track",
|
"LabelTracksSingleTrack": "Single-track",
|
||||||
"LabelType": "Type",
|
"LabelType": "Type",
|
||||||
"LabelUnabridged": "Unabridged",
|
"LabelUnabridged": "Unabridged",
|
||||||
|
"LabelUndo": "Undo",
|
||||||
"LabelUnknown": "Unknown",
|
"LabelUnknown": "Unknown",
|
||||||
"LabelUpdateCover": "Update Cover",
|
"LabelUpdateCover": "Update Cover",
|
||||||
"LabelUpdateCoverHelp": "Allow overwriting of existing covers for the selected books when a match is located",
|
"LabelUpdateCoverHelp": "Allow overwriting of existing covers for the selected books when a match is located",
|
||||||
|
@ -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,15 @@
|
|||||||
"ButtonMatchAllAuthors": "सभी लेखकों को तलाश करें",
|
"ButtonMatchAllAuthors": "सभी लेखकों को तलाश करें",
|
||||||
"ButtonMatchBooks": "संबंधित पुस्तकों का मिलान करें",
|
"ButtonMatchBooks": "संबंधित पुस्तकों का मिलान करें",
|
||||||
"ButtonNevermind": "कोई बात नहीं",
|
"ButtonNevermind": "कोई बात नहीं",
|
||||||
|
"ButtonNextChapter": "Next Chapter",
|
||||||
"ButtonOk": "ठीक है",
|
"ButtonOk": "ठीक है",
|
||||||
"ButtonOpenFeed": "फ़ीड खोलें",
|
"ButtonOpenFeed": "फ़ीड खोलें",
|
||||||
"ButtonOpenManager": "मैनेजर खोलें",
|
"ButtonOpenManager": "मैनेजर खोलें",
|
||||||
|
"ButtonPause": "Pause",
|
||||||
"ButtonPlay": "चलाएँ",
|
"ButtonPlay": "चलाएँ",
|
||||||
"ButtonPlaying": "चल रही है",
|
"ButtonPlaying": "चल रही है",
|
||||||
"ButtonPlaylists": "प्लेलिस्ट्स",
|
"ButtonPlaylists": "प्लेलिस्ट्स",
|
||||||
|
"ButtonPreviousChapter": "Previous Chapter",
|
||||||
"ButtonPurgeAllCache": "सभी Cache मिटाएं",
|
"ButtonPurgeAllCache": "सभी Cache मिटाएं",
|
||||||
"ButtonPurgeItemsCache": "आइटम Cache मिटाएं",
|
"ButtonPurgeItemsCache": "आइटम Cache मिटाएं",
|
||||||
"ButtonPurgeMediaProgress": "अभी तक सुना हुआ सब हटा दे",
|
"ButtonPurgeMediaProgress": "अभी तक सुना हुआ सब हटा दे",
|
||||||
@ -104,6 +109,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",
|
||||||
@ -281,8 +287,11 @@
|
|||||||
"LabelFinished": "Finished",
|
"LabelFinished": "Finished",
|
||||||
"LabelFolder": "Folder",
|
"LabelFolder": "Folder",
|
||||||
"LabelFolders": "Folders",
|
"LabelFolders": "Folders",
|
||||||
|
"LabelFontBold": "Bold",
|
||||||
"LabelFontFamily": "फुहारा परिवार",
|
"LabelFontFamily": "फुहारा परिवार",
|
||||||
|
"LabelFontItalic": "Italic",
|
||||||
"LabelFontScale": "Font scale",
|
"LabelFontScale": "Font scale",
|
||||||
|
"LabelFontStrikethrough": "Strikethrough",
|
||||||
"LabelFormat": "Format",
|
"LabelFormat": "Format",
|
||||||
"LabelGenre": "Genre",
|
"LabelGenre": "Genre",
|
||||||
"LabelGenres": "Genres",
|
"LabelGenres": "Genres",
|
||||||
@ -387,6 +396,7 @@
|
|||||||
"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)",
|
||||||
@ -403,6 +413,7 @@
|
|||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
"LabelRecommended": "Recommended",
|
"LabelRecommended": "Recommended",
|
||||||
|
"LabelRedo": "Redo",
|
||||||
"LabelRegion": "Region",
|
"LabelRegion": "Region",
|
||||||
"LabelReleaseDate": "Release Date",
|
"LabelReleaseDate": "Release Date",
|
||||||
"LabelRemoveCover": "Remove cover",
|
"LabelRemoveCover": "Remove cover",
|
||||||
@ -491,6 +502,10 @@
|
|||||||
"LabelTagsAccessibleToUser": "Tags Accessible to User",
|
"LabelTagsAccessibleToUser": "Tags Accessible to User",
|
||||||
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
||||||
"LabelTasks": "Tasks Running",
|
"LabelTasks": "Tasks Running",
|
||||||
|
"LabelTextEditorBulletedList": "Bulleted list",
|
||||||
|
"LabelTextEditorLink": "Link",
|
||||||
|
"LabelTextEditorNumberedList": "Numbered list",
|
||||||
|
"LabelTextEditorUnlink": "Unlink",
|
||||||
"LabelTheme": "Theme",
|
"LabelTheme": "Theme",
|
||||||
"LabelThemeDark": "Dark",
|
"LabelThemeDark": "Dark",
|
||||||
"LabelThemeLight": "Light",
|
"LabelThemeLight": "Light",
|
||||||
@ -516,6 +531,7 @@
|
|||||||
"LabelTracksSingleTrack": "Single-track",
|
"LabelTracksSingleTrack": "Single-track",
|
||||||
"LabelType": "Type",
|
"LabelType": "Type",
|
||||||
"LabelUnabridged": "Unabridged",
|
"LabelUnabridged": "Unabridged",
|
||||||
|
"LabelUndo": "Undo",
|
||||||
"LabelUnknown": "Unknown",
|
"LabelUnknown": "Unknown",
|
||||||
"LabelUpdateCover": "Update Cover",
|
"LabelUpdateCover": "Update Cover",
|
||||||
"LabelUpdateCoverHelp": "Allow overwriting of existing covers for the selected books when a match is located",
|
"LabelUpdateCoverHelp": "Allow overwriting of existing covers for the selected books when a match is located",
|
||||||
|
@ -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,15 @@
|
|||||||
"ButtonMatchAllAuthors": "Matchaj sve autore",
|
"ButtonMatchAllAuthors": "Matchaj sve autore",
|
||||||
"ButtonMatchBooks": "Matchaj knjige",
|
"ButtonMatchBooks": "Matchaj knjige",
|
||||||
"ButtonNevermind": "Nije bitno",
|
"ButtonNevermind": "Nije bitno",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
@ -104,6 +109,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",
|
||||||
@ -281,8 +287,11 @@
|
|||||||
"LabelFinished": "Finished",
|
"LabelFinished": "Finished",
|
||||||
"LabelFolder": "Folder",
|
"LabelFolder": "Folder",
|
||||||
"LabelFolders": "Folderi",
|
"LabelFolders": "Folderi",
|
||||||
|
"LabelFontBold": "Bold",
|
||||||
"LabelFontFamily": "Font family",
|
"LabelFontFamily": "Font family",
|
||||||
|
"LabelFontItalic": "Italic",
|
||||||
"LabelFontScale": "Font scale",
|
"LabelFontScale": "Font scale",
|
||||||
|
"LabelFontStrikethrough": "Strikethrough",
|
||||||
"LabelFormat": "Format",
|
"LabelFormat": "Format",
|
||||||
"LabelGenre": "Genre",
|
"LabelGenre": "Genre",
|
||||||
"LabelGenres": "Žanrovi",
|
"LabelGenres": "Žanrovi",
|
||||||
@ -387,6 +396,7 @@
|
|||||||
"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)",
|
||||||
@ -403,6 +413,7 @@
|
|||||||
"LabelRecentlyAdded": "Nedavno dodano",
|
"LabelRecentlyAdded": "Nedavno dodano",
|
||||||
"LabelRecentSeries": "Nedavne serije",
|
"LabelRecentSeries": "Nedavne serije",
|
||||||
"LabelRecommended": "Recommended",
|
"LabelRecommended": "Recommended",
|
||||||
|
"LabelRedo": "Redo",
|
||||||
"LabelRegion": "Regija",
|
"LabelRegion": "Regija",
|
||||||
"LabelReleaseDate": "Datum izlaska",
|
"LabelReleaseDate": "Datum izlaska",
|
||||||
"LabelRemoveCover": "Remove cover",
|
"LabelRemoveCover": "Remove cover",
|
||||||
@ -491,6 +502,10 @@
|
|||||||
"LabelTagsAccessibleToUser": "Tags dostupni korisniku",
|
"LabelTagsAccessibleToUser": "Tags dostupni korisniku",
|
||||||
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
||||||
"LabelTasks": "Tasks Running",
|
"LabelTasks": "Tasks Running",
|
||||||
|
"LabelTextEditorBulletedList": "Bulleted list",
|
||||||
|
"LabelTextEditorLink": "Link",
|
||||||
|
"LabelTextEditorNumberedList": "Numbered list",
|
||||||
|
"LabelTextEditorUnlink": "Unlink",
|
||||||
"LabelTheme": "Theme",
|
"LabelTheme": "Theme",
|
||||||
"LabelThemeDark": "Dark",
|
"LabelThemeDark": "Dark",
|
||||||
"LabelThemeLight": "Light",
|
"LabelThemeLight": "Light",
|
||||||
@ -516,6 +531,7 @@
|
|||||||
"LabelTracksSingleTrack": "Single-track",
|
"LabelTracksSingleTrack": "Single-track",
|
||||||
"LabelType": "Tip",
|
"LabelType": "Tip",
|
||||||
"LabelUnabridged": "Unabridged",
|
"LabelUnabridged": "Unabridged",
|
||||||
|
"LabelUndo": "Undo",
|
||||||
"LabelUnknown": "Nepoznato",
|
"LabelUnknown": "Nepoznato",
|
||||||
"LabelUpdateCover": "Aktualiziraj Cover",
|
"LabelUpdateCover": "Aktualiziraj Cover",
|
||||||
"LabelUpdateCoverHelp": "Dozvoli postavljanje novog covera za odabrane knjige nakon što je match pronađen.",
|
"LabelUpdateCoverHelp": "Dozvoli postavljanje novog covera za odabrane knjige nakon što je match pronađen.",
|
||||||
|
768
client/strings/hu.json
Normal file
768
client/strings/hu.json
Normal file
@ -0,0 +1,768 @@
|
|||||||
|
{
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"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ó",
|
||||||
|
"LabelMissingParts": "Hiányzó részek",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"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,15 @@
|
|||||||
"ButtonMatchAllAuthors": "Aggiungi metadata agli Autori",
|
"ButtonMatchAllAuthors": "Aggiungi metadata agli Autori",
|
||||||
"ButtonMatchBooks": "Aggiungi metadata della Libreria",
|
"ButtonMatchBooks": "Aggiungi metadata della Libreria",
|
||||||
"ButtonNevermind": "Nevermind",
|
"ButtonNevermind": "Nevermind",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
@ -87,15 +92,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 +109,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 +136,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",
|
||||||
@ -199,12 +205,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 +221,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 +277,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",
|
||||||
@ -281,15 +287,18 @@
|
|||||||
"LabelFinished": "Finita",
|
"LabelFinished": "Finita",
|
||||||
"LabelFolder": "Cartella",
|
"LabelFolder": "Cartella",
|
||||||
"LabelFolders": "Cartelle",
|
"LabelFolders": "Cartelle",
|
||||||
|
"LabelFontBold": "Bold",
|
||||||
"LabelFontFamily": "Font family",
|
"LabelFontFamily": "Font family",
|
||||||
|
"LabelFontItalic": "Italic",
|
||||||
"LabelFontScale": "Dimensione Font",
|
"LabelFontScale": "Dimensione Font",
|
||||||
|
"LabelFontStrikethrough": "Strikethrough",
|
||||||
"LabelFormat": "Formato",
|
"LabelFormat": "Formato",
|
||||||
"LabelGenre": "Genere",
|
"LabelGenre": "Genere",
|
||||||
"LabelGenres": "Generi",
|
"LabelGenres": "Generi",
|
||||||
"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",
|
||||||
@ -331,20 +340,20 @@
|
|||||||
"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",
|
"LabelMissingParts": "Parti rimanenti",
|
||||||
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
|
"LabelMobileRedirectURIs": "URI di reindirizzamento mobile consentiti",
|
||||||
"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": "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",
|
||||||
@ -387,6 +396,7 @@
|
|||||||
"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)",
|
||||||
@ -403,6 +413,7 @@
|
|||||||
"LabelRecentlyAdded": "Aggiunti Recentemente",
|
"LabelRecentlyAdded": "Aggiunti Recentemente",
|
||||||
"LabelRecentSeries": "Serie Recenti",
|
"LabelRecentSeries": "Serie Recenti",
|
||||||
"LabelRecommended": "Raccomandati",
|
"LabelRecommended": "Raccomandati",
|
||||||
|
"LabelRedo": "Rifai",
|
||||||
"LabelRegion": "Regione",
|
"LabelRegion": "Regione",
|
||||||
"LabelReleaseDate": "Data Release",
|
"LabelReleaseDate": "Data Release",
|
||||||
"LabelRemoveCover": "Rimuovi cover",
|
"LabelRemoveCover": "Rimuovi cover",
|
||||||
@ -464,7 +475,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",
|
||||||
@ -491,6 +502,10 @@
|
|||||||
"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": "Elenco puntato",
|
||||||
|
"LabelTextEditorLink": "Link",
|
||||||
|
"LabelTextEditorNumberedList": "Elenco Numerato",
|
||||||
|
"LabelTextEditorUnlink": "Unlink",
|
||||||
"LabelTheme": "Tema",
|
"LabelTheme": "Tema",
|
||||||
"LabelThemeDark": "Scuro",
|
"LabelThemeDark": "Scuro",
|
||||||
"LabelThemeLight": "Chiaro",
|
"LabelThemeLight": "Chiaro",
|
||||||
@ -512,10 +527,11 @@
|
|||||||
"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",
|
||||||
|
"LabelUndo": "Undo",
|
||||||
"LabelUnknown": "Sconosciuto",
|
"LabelUnknown": "Sconosciuto",
|
||||||
"LabelUpdateCover": "Aggiornamento Cover",
|
"LabelUpdateCover": "Aggiornamento Cover",
|
||||||
"LabelUpdateCoverHelp": "Consenti la sovrascrittura delle copertine esistenti per i libri selezionati quando viene trovata una corrispondenza",
|
"LabelUpdateCoverHelp": "Consenti la sovrascrittura delle copertine esistenti per i libri selezionati quando viene trovata una corrispondenza",
|
||||||
@ -524,7 +540,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",
|
||||||
@ -572,7 +588,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?",
|
||||||
@ -652,7 +668,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}?",
|
||||||
@ -741,7 +757,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",
|
||||||
@ -749,4 +765,4 @@
|
|||||||
"ToastSocketFailedToConnect": "Socket non riesce a connettersi",
|
"ToastSocketFailedToConnect": "Socket non riesce a connettersi",
|
||||||
"ToastUserDeleteFailed": "Errore eliminazione utente",
|
"ToastUserDeleteFailed": "Errore eliminazione utente",
|
||||||
"ToastUserDeleteSuccess": "Utente eliminato"
|
"ToastUserDeleteSuccess": "Utente eliminato"
|
||||||
}
|
}
|
||||||
|
@ -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,15 @@
|
|||||||
"ButtonMatchAllAuthors": "Pritaikyti visus autorius",
|
"ButtonMatchAllAuthors": "Pritaikyti visus autorius",
|
||||||
"ButtonMatchBooks": "Pritaikyti knygas",
|
"ButtonMatchBooks": "Pritaikyti knygas",
|
||||||
"ButtonNevermind": "Nesvarbu",
|
"ButtonNevermind": "Nesvarbu",
|
||||||
|
"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",
|
||||||
|
"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ą",
|
||||||
@ -104,6 +109,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",
|
||||||
@ -281,8 +287,11 @@
|
|||||||
"LabelFinished": "Baigta",
|
"LabelFinished": "Baigta",
|
||||||
"LabelFolder": "Aplankas",
|
"LabelFolder": "Aplankas",
|
||||||
"LabelFolders": "Aplankai",
|
"LabelFolders": "Aplankai",
|
||||||
|
"LabelFontBold": "Bold",
|
||||||
"LabelFontFamily": "Famiglia di font",
|
"LabelFontFamily": "Famiglia di font",
|
||||||
|
"LabelFontItalic": "Italic",
|
||||||
"LabelFontScale": "Šrifto mastelis",
|
"LabelFontScale": "Šrifto mastelis",
|
||||||
|
"LabelFontStrikethrough": "Strikethrough",
|
||||||
"LabelFormat": "Formatas",
|
"LabelFormat": "Formatas",
|
||||||
"LabelGenre": "Žanras",
|
"LabelGenre": "Žanras",
|
||||||
"LabelGenres": "Žanrai",
|
"LabelGenres": "Žanrai",
|
||||||
@ -387,6 +396,7 @@
|
|||||||
"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)",
|
||||||
@ -403,6 +413,7 @@
|
|||||||
"LabelRecentlyAdded": "Neseniai pridėta",
|
"LabelRecentlyAdded": "Neseniai pridėta",
|
||||||
"LabelRecentSeries": "Naujausios serijos",
|
"LabelRecentSeries": "Naujausios serijos",
|
||||||
"LabelRecommended": "Rekomenduojama",
|
"LabelRecommended": "Rekomenduojama",
|
||||||
|
"LabelRedo": "Redo",
|
||||||
"LabelRegion": "Regionas",
|
"LabelRegion": "Regionas",
|
||||||
"LabelReleaseDate": "Išleidimo data",
|
"LabelReleaseDate": "Išleidimo data",
|
||||||
"LabelRemoveCover": "Pašalinti viršelį",
|
"LabelRemoveCover": "Pašalinti viršelį",
|
||||||
@ -491,6 +502,10 @@
|
|||||||
"LabelTagsAccessibleToUser": "Žymos, pasiekiamos vartotojui",
|
"LabelTagsAccessibleToUser": "Žymos, pasiekiamos vartotojui",
|
||||||
"LabelTagsNotAccessibleToUser": "Žymos, nepasiekiamos vartotojui",
|
"LabelTagsNotAccessibleToUser": "Žymos, nepasiekiamos vartotojui",
|
||||||
"LabelTasks": "Vykdomos užduotys",
|
"LabelTasks": "Vykdomos užduotys",
|
||||||
|
"LabelTextEditorBulletedList": "Bulleted list",
|
||||||
|
"LabelTextEditorLink": "Link",
|
||||||
|
"LabelTextEditorNumberedList": "Numbered list",
|
||||||
|
"LabelTextEditorUnlink": "Unlink",
|
||||||
"LabelTheme": "Tema",
|
"LabelTheme": "Tema",
|
||||||
"LabelThemeDark": "Tamsi",
|
"LabelThemeDark": "Tamsi",
|
||||||
"LabelThemeLight": "Šviesi",
|
"LabelThemeLight": "Šviesi",
|
||||||
@ -516,6 +531,7 @@
|
|||||||
"LabelTracksSingleTrack": "Vienas takelis",
|
"LabelTracksSingleTrack": "Vienas takelis",
|
||||||
"LabelType": "Tipas",
|
"LabelType": "Tipas",
|
||||||
"LabelUnabridged": "Neprikurptas",
|
"LabelUnabridged": "Neprikurptas",
|
||||||
|
"LabelUndo": "Undo",
|
||||||
"LabelUnknown": "Nežinoma",
|
"LabelUnknown": "Nežinoma",
|
||||||
"LabelUpdateCover": "Atnaujinti viršelį",
|
"LabelUpdateCover": "Atnaujinti viršelį",
|
||||||
"LabelUpdateCoverHelp": "Leisti perrašyti esamus viršelius pasirinktoms knygoms, kai yra rasta atitikmenų",
|
"LabelUpdateCoverHelp": "Leisti perrašyti esamus viršelius pasirinktoms knygoms, kai yra rasta atitikmenų",
|
||||||
|
@ -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,15 @@
|
|||||||
"ButtonMatchAllAuthors": "Alle auteurs matchen",
|
"ButtonMatchAllAuthors": "Alle auteurs matchen",
|
||||||
"ButtonMatchBooks": "Alle boeken matchen",
|
"ButtonMatchBooks": "Alle boeken matchen",
|
||||||
"ButtonNevermind": "Laat maar",
|
"ButtonNevermind": "Laat maar",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
@ -104,6 +109,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",
|
||||||
@ -281,8 +287,11 @@
|
|||||||
"LabelFinished": "Voltooid",
|
"LabelFinished": "Voltooid",
|
||||||
"LabelFolder": "Map",
|
"LabelFolder": "Map",
|
||||||
"LabelFolders": "Mappen",
|
"LabelFolders": "Mappen",
|
||||||
|
"LabelFontBold": "Bold",
|
||||||
"LabelFontFamily": "Lettertypefamilie",
|
"LabelFontFamily": "Lettertypefamilie",
|
||||||
|
"LabelFontItalic": "Italic",
|
||||||
"LabelFontScale": "Lettertype schaal",
|
"LabelFontScale": "Lettertype schaal",
|
||||||
|
"LabelFontStrikethrough": "Strikethrough",
|
||||||
"LabelFormat": "Formaat",
|
"LabelFormat": "Formaat",
|
||||||
"LabelGenre": "Genre",
|
"LabelGenre": "Genre",
|
||||||
"LabelGenres": "Genres",
|
"LabelGenres": "Genres",
|
||||||
@ -387,6 +396,7 @@
|
|||||||
"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)",
|
||||||
@ -403,6 +413,7 @@
|
|||||||
"LabelRecentlyAdded": "Recent toegevoegd",
|
"LabelRecentlyAdded": "Recent toegevoegd",
|
||||||
"LabelRecentSeries": "Recente series",
|
"LabelRecentSeries": "Recente series",
|
||||||
"LabelRecommended": "Aangeraden",
|
"LabelRecommended": "Aangeraden",
|
||||||
|
"LabelRedo": "Redo",
|
||||||
"LabelRegion": "Regio",
|
"LabelRegion": "Regio",
|
||||||
"LabelReleaseDate": "Verschijningsdatum",
|
"LabelReleaseDate": "Verschijningsdatum",
|
||||||
"LabelRemoveCover": "Verwijder cover",
|
"LabelRemoveCover": "Verwijder cover",
|
||||||
@ -491,6 +502,10 @@
|
|||||||
"LabelTagsAccessibleToUser": "Tags toegankelijk voor de gebruiker",
|
"LabelTagsAccessibleToUser": "Tags toegankelijk voor de gebruiker",
|
||||||
"LabelTagsNotAccessibleToUser": "Tags niet toegankelijk voor de gebruiker",
|
"LabelTagsNotAccessibleToUser": "Tags niet toegankelijk voor de gebruiker",
|
||||||
"LabelTasks": "Lopende taken",
|
"LabelTasks": "Lopende taken",
|
||||||
|
"LabelTextEditorBulletedList": "Bulleted list",
|
||||||
|
"LabelTextEditorLink": "Link",
|
||||||
|
"LabelTextEditorNumberedList": "Numbered list",
|
||||||
|
"LabelTextEditorUnlink": "Unlink",
|
||||||
"LabelTheme": "Thema",
|
"LabelTheme": "Thema",
|
||||||
"LabelThemeDark": "Donker",
|
"LabelThemeDark": "Donker",
|
||||||
"LabelThemeLight": "Licht",
|
"LabelThemeLight": "Licht",
|
||||||
@ -516,6 +531,7 @@
|
|||||||
"LabelTracksSingleTrack": "Enkele track",
|
"LabelTracksSingleTrack": "Enkele track",
|
||||||
"LabelType": "Type",
|
"LabelType": "Type",
|
||||||
"LabelUnabridged": "Onverkort",
|
"LabelUnabridged": "Onverkort",
|
||||||
|
"LabelUndo": "Undo",
|
||||||
"LabelUnknown": "Onbekend",
|
"LabelUnknown": "Onbekend",
|
||||||
"LabelUpdateCover": "Cover bijwerken",
|
"LabelUpdateCover": "Cover bijwerken",
|
||||||
"LabelUpdateCoverHelp": "Sta overschrijven van bestaande covers toe voor de geselecteerde boeken wanneer een match is gevonden",
|
"LabelUpdateCoverHelp": "Sta overschrijven van bestaande covers toe voor de geselecteerde boeken wanneer een match is gevonden",
|
||||||
|
@ -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,15 @@
|
|||||||
"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",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
@ -104,6 +109,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",
|
||||||
@ -281,8 +287,11 @@
|
|||||||
"LabelFinished": "Fullført",
|
"LabelFinished": "Fullført",
|
||||||
"LabelFolder": "Mappe",
|
"LabelFolder": "Mappe",
|
||||||
"LabelFolders": "Mapper",
|
"LabelFolders": "Mapper",
|
||||||
|
"LabelFontBold": "Bold",
|
||||||
"LabelFontFamily": "Fontfamilie",
|
"LabelFontFamily": "Fontfamilie",
|
||||||
|
"LabelFontItalic": "Italic",
|
||||||
"LabelFontScale": "Font størrelse",
|
"LabelFontScale": "Font størrelse",
|
||||||
|
"LabelFontStrikethrough": "Strikethrough",
|
||||||
"LabelFormat": "Format",
|
"LabelFormat": "Format",
|
||||||
"LabelGenre": "Sjanger",
|
"LabelGenre": "Sjanger",
|
||||||
"LabelGenres": "Sjangers",
|
"LabelGenres": "Sjangers",
|
||||||
@ -387,6 +396,7 @@
|
|||||||
"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)",
|
||||||
@ -403,6 +413,7 @@
|
|||||||
"LabelRecentlyAdded": "Nylig lagt til",
|
"LabelRecentlyAdded": "Nylig lagt til",
|
||||||
"LabelRecentSeries": "Nylige serier",
|
"LabelRecentSeries": "Nylige serier",
|
||||||
"LabelRecommended": "Anbefalte",
|
"LabelRecommended": "Anbefalte",
|
||||||
|
"LabelRedo": "Redo",
|
||||||
"LabelRegion": "Region",
|
"LabelRegion": "Region",
|
||||||
"LabelReleaseDate": "Utgivelsesdato",
|
"LabelReleaseDate": "Utgivelsesdato",
|
||||||
"LabelRemoveCover": "Fjern omslag",
|
"LabelRemoveCover": "Fjern omslag",
|
||||||
@ -491,6 +502,10 @@
|
|||||||
"LabelTagsAccessibleToUser": "Tagger tilgjengelig for bruker",
|
"LabelTagsAccessibleToUser": "Tagger tilgjengelig for bruker",
|
||||||
"LabelTagsNotAccessibleToUser": "Tagger ikke tilgjengelig for bruker",
|
"LabelTagsNotAccessibleToUser": "Tagger ikke tilgjengelig for bruker",
|
||||||
"LabelTasks": "Oppgaver som kjører",
|
"LabelTasks": "Oppgaver som kjører",
|
||||||
|
"LabelTextEditorBulletedList": "Bulleted list",
|
||||||
|
"LabelTextEditorLink": "Link",
|
||||||
|
"LabelTextEditorNumberedList": "Numbered list",
|
||||||
|
"LabelTextEditorUnlink": "Unlink",
|
||||||
"LabelTheme": "Tema",
|
"LabelTheme": "Tema",
|
||||||
"LabelThemeDark": "Mørk",
|
"LabelThemeDark": "Mørk",
|
||||||
"LabelThemeLight": "Lys",
|
"LabelThemeLight": "Lys",
|
||||||
@ -516,6 +531,7 @@
|
|||||||
"LabelTracksSingleTrack": "Enkelspor",
|
"LabelTracksSingleTrack": "Enkelspor",
|
||||||
"LabelType": "Type",
|
"LabelType": "Type",
|
||||||
"LabelUnabridged": "Uavkortet",
|
"LabelUnabridged": "Uavkortet",
|
||||||
|
"LabelUndo": "Undo",
|
||||||
"LabelUnknown": "Ukjent",
|
"LabelUnknown": "Ukjent",
|
||||||
"LabelUpdateCover": "Oppdater omslag",
|
"LabelUpdateCover": "Oppdater omslag",
|
||||||
"LabelUpdateCoverHelp": "Tillat overskriving av eksisterende omslag for de valgte bøkene når en lik bok er funnet",
|
"LabelUpdateCoverHelp": "Tillat overskriving av eksisterende omslag for de valgte bøkene når en lik bok er funnet",
|
||||||
|
@ -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,15 @@
|
|||||||
"ButtonMatchAllAuthors": "Dopasuj wszystkich autorów",
|
"ButtonMatchAllAuthors": "Dopasuj wszystkich autorów",
|
||||||
"ButtonMatchBooks": "Dopasuj książki",
|
"ButtonMatchBooks": "Dopasuj książki",
|
||||||
"ButtonNevermind": "Anuluj",
|
"ButtonNevermind": "Anuluj",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
@ -104,6 +109,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",
|
||||||
@ -281,8 +287,11 @@
|
|||||||
"LabelFinished": "Zakończone",
|
"LabelFinished": "Zakończone",
|
||||||
"LabelFolder": "Folder",
|
"LabelFolder": "Folder",
|
||||||
"LabelFolders": "Foldery",
|
"LabelFolders": "Foldery",
|
||||||
|
"LabelFontBold": "Bold",
|
||||||
"LabelFontFamily": "Rodzina czcionek",
|
"LabelFontFamily": "Rodzina czcionek",
|
||||||
|
"LabelFontItalic": "Italic",
|
||||||
"LabelFontScale": "Font scale",
|
"LabelFontScale": "Font scale",
|
||||||
|
"LabelFontStrikethrough": "Strikethrough",
|
||||||
"LabelFormat": "Format",
|
"LabelFormat": "Format",
|
||||||
"LabelGenre": "Gatunek",
|
"LabelGenre": "Gatunek",
|
||||||
"LabelGenres": "Gatunki",
|
"LabelGenres": "Gatunki",
|
||||||
@ -387,6 +396,7 @@
|
|||||||
"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)",
|
||||||
@ -403,6 +413,7 @@
|
|||||||
"LabelRecentlyAdded": "Niedawno dodany",
|
"LabelRecentlyAdded": "Niedawno dodany",
|
||||||
"LabelRecentSeries": "Ostatnie serie",
|
"LabelRecentSeries": "Ostatnie serie",
|
||||||
"LabelRecommended": "Recommended",
|
"LabelRecommended": "Recommended",
|
||||||
|
"LabelRedo": "Redo",
|
||||||
"LabelRegion": "Region",
|
"LabelRegion": "Region",
|
||||||
"LabelReleaseDate": "Data wydania",
|
"LabelReleaseDate": "Data wydania",
|
||||||
"LabelRemoveCover": "Remove cover",
|
"LabelRemoveCover": "Remove cover",
|
||||||
@ -491,6 +502,10 @@
|
|||||||
"LabelTagsAccessibleToUser": "Tagi dostępne dla użytkownika",
|
"LabelTagsAccessibleToUser": "Tagi dostępne dla użytkownika",
|
||||||
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
||||||
"LabelTasks": "Tasks Running",
|
"LabelTasks": "Tasks Running",
|
||||||
|
"LabelTextEditorBulletedList": "Bulleted list",
|
||||||
|
"LabelTextEditorLink": "Link",
|
||||||
|
"LabelTextEditorNumberedList": "Numbered list",
|
||||||
|
"LabelTextEditorUnlink": "Unlink",
|
||||||
"LabelTheme": "Theme",
|
"LabelTheme": "Theme",
|
||||||
"LabelThemeDark": "Dark",
|
"LabelThemeDark": "Dark",
|
||||||
"LabelThemeLight": "Light",
|
"LabelThemeLight": "Light",
|
||||||
@ -516,6 +531,7 @@
|
|||||||
"LabelTracksSingleTrack": "Single-track",
|
"LabelTracksSingleTrack": "Single-track",
|
||||||
"LabelType": "Typ",
|
"LabelType": "Typ",
|
||||||
"LabelUnabridged": "Unabridged",
|
"LabelUnabridged": "Unabridged",
|
||||||
|
"LabelUndo": "Undo",
|
||||||
"LabelUnknown": "Nieznany",
|
"LabelUnknown": "Nieznany",
|
||||||
"LabelUpdateCover": "Zaktalizuj odkładkę",
|
"LabelUpdateCover": "Zaktalizuj odkładkę",
|
||||||
"LabelUpdateCoverHelp": "Umożliwienie nadpisania istniejących okładek dla wybranych książek w przypadku znalezienia dopasowania",
|
"LabelUpdateCoverHelp": "Umożliwienie nadpisania istniejących okładek dla wybranych książek w przypadku znalezienia dopasowania",
|
||||||
|
768
client/strings/pt-br.json
Normal file
768
client/strings/pt-br.json
Normal file
@ -0,0 +1,768 @@
|
|||||||
|
{
|
||||||
|
"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",
|
||||||
|
"ButtonNextChapter": "Próximo Capítulo",
|
||||||
|
"ButtonOk": "Ok",
|
||||||
|
"ButtonOpenFeed": "Abrir Feed",
|
||||||
|
"ButtonOpenManager": "Abrir Gerenciador",
|
||||||
|
"ButtonPause": "Pausar",
|
||||||
|
"ButtonPlay": "Reproduzir",
|
||||||
|
"ButtonPlaying": "Reproduzindo",
|
||||||
|
"ButtonPlaylists": "Lista de Reprodução",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"LabelMissingParts": "Partes Ausentes",
|
||||||
|
"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",
|
||||||
|
"LabelPhotoPathURL": "Caminho/URL para Foto",
|
||||||
|
"LabelPlaylists": "Listas de Reprodução",
|
||||||
|
"LabelPlayMethod": "Método de Reprodução",
|
||||||
|
"LabelPodcast": "Podcast",
|
||||||
|
"LabelPodcasts": "Podcasts",
|
||||||
|
"LabelPodcastSearchRegion": "Podcast search region",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"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,15 @@
|
|||||||
"ButtonMatchAllAuthors": "Найти всех авторов",
|
"ButtonMatchAllAuthors": "Найти всех авторов",
|
||||||
"ButtonMatchBooks": "Найти книги",
|
"ButtonMatchBooks": "Найти книги",
|
||||||
"ButtonNevermind": "Не важно",
|
"ButtonNevermind": "Не важно",
|
||||||
|
"ButtonNextChapter": "Next Chapter",
|
||||||
"ButtonOk": "Ok",
|
"ButtonOk": "Ok",
|
||||||
"ButtonOpenFeed": "Открыть канал",
|
"ButtonOpenFeed": "Открыть канал",
|
||||||
"ButtonOpenManager": "Открыть менеджер",
|
"ButtonOpenManager": "Открыть менеджер",
|
||||||
|
"ButtonPause": "Pause",
|
||||||
"ButtonPlay": "Слушать",
|
"ButtonPlay": "Слушать",
|
||||||
"ButtonPlaying": "Проигрывается",
|
"ButtonPlaying": "Проигрывается",
|
||||||
"ButtonPlaylists": "Плейлисты",
|
"ButtonPlaylists": "Плейлисты",
|
||||||
|
"ButtonPreviousChapter": "Previous Chapter",
|
||||||
"ButtonPurgeAllCache": "Очистить весь кэш",
|
"ButtonPurgeAllCache": "Очистить весь кэш",
|
||||||
"ButtonPurgeItemsCache": "Очистить кэш элементов",
|
"ButtonPurgeItemsCache": "Очистить кэш элементов",
|
||||||
"ButtonPurgeMediaProgress": "Очистить прогресс медиа",
|
"ButtonPurgeMediaProgress": "Очистить прогресс медиа",
|
||||||
@ -104,6 +109,7 @@
|
|||||||
"HeaderCollectionItems": "Элементы коллекции",
|
"HeaderCollectionItems": "Элементы коллекции",
|
||||||
"HeaderCover": "Обложка",
|
"HeaderCover": "Обложка",
|
||||||
"HeaderCurrentDownloads": "Текущие закачки",
|
"HeaderCurrentDownloads": "Текущие закачки",
|
||||||
|
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
|
||||||
"HeaderDetails": "Подробности",
|
"HeaderDetails": "Подробности",
|
||||||
"HeaderDownloadQueue": "Очередь скачивания",
|
"HeaderDownloadQueue": "Очередь скачивания",
|
||||||
"HeaderEbookFiles": "Файлы e-книг",
|
"HeaderEbookFiles": "Файлы e-книг",
|
||||||
@ -281,8 +287,11 @@
|
|||||||
"LabelFinished": "Закончен",
|
"LabelFinished": "Закончен",
|
||||||
"LabelFolder": "Папка",
|
"LabelFolder": "Папка",
|
||||||
"LabelFolders": "Папки",
|
"LabelFolders": "Папки",
|
||||||
|
"LabelFontBold": "Bold",
|
||||||
"LabelFontFamily": "Семейство шрифтов",
|
"LabelFontFamily": "Семейство шрифтов",
|
||||||
|
"LabelFontItalic": "Italic",
|
||||||
"LabelFontScale": "Масштаб шрифта",
|
"LabelFontScale": "Масштаб шрифта",
|
||||||
|
"LabelFontStrikethrough": "Strikethrough",
|
||||||
"LabelFormat": "Формат",
|
"LabelFormat": "Формат",
|
||||||
"LabelGenre": "Жанр",
|
"LabelGenre": "Жанр",
|
||||||
"LabelGenres": "Жанры",
|
"LabelGenres": "Жанры",
|
||||||
@ -387,6 +396,7 @@
|
|||||||
"LabelPlayMethod": "Метод воспроизведения",
|
"LabelPlayMethod": "Метод воспроизведения",
|
||||||
"LabelPodcast": "Подкаст",
|
"LabelPodcast": "Подкаст",
|
||||||
"LabelPodcasts": "Подкасты",
|
"LabelPodcasts": "Подкасты",
|
||||||
|
"LabelPodcastSearchRegion": "Регион поиска подкастов",
|
||||||
"LabelPodcastType": "Тип подкаста",
|
"LabelPodcastType": "Тип подкаста",
|
||||||
"LabelPort": "Порт",
|
"LabelPort": "Порт",
|
||||||
"LabelPrefixesToIgnore": "Игнорируемые префиксы (без учета регистра)",
|
"LabelPrefixesToIgnore": "Игнорируемые префиксы (без учета регистра)",
|
||||||
@ -403,6 +413,7 @@
|
|||||||
"LabelRecentlyAdded": "Недавно добавленные",
|
"LabelRecentlyAdded": "Недавно добавленные",
|
||||||
"LabelRecentSeries": "Последние серии",
|
"LabelRecentSeries": "Последние серии",
|
||||||
"LabelRecommended": "Рекомендованное",
|
"LabelRecommended": "Рекомендованное",
|
||||||
|
"LabelRedo": "Redo",
|
||||||
"LabelRegion": "Регион",
|
"LabelRegion": "Регион",
|
||||||
"LabelReleaseDate": "Дата выхода",
|
"LabelReleaseDate": "Дата выхода",
|
||||||
"LabelRemoveCover": "Удалить обложку",
|
"LabelRemoveCover": "Удалить обложку",
|
||||||
@ -491,6 +502,10 @@
|
|||||||
"LabelTagsAccessibleToUser": "Теги доступные для пользователя",
|
"LabelTagsAccessibleToUser": "Теги доступные для пользователя",
|
||||||
"LabelTagsNotAccessibleToUser": "Теги не доступные для пользователя",
|
"LabelTagsNotAccessibleToUser": "Теги не доступные для пользователя",
|
||||||
"LabelTasks": "Запущенные задачи",
|
"LabelTasks": "Запущенные задачи",
|
||||||
|
"LabelTextEditorBulletedList": "Bulleted list",
|
||||||
|
"LabelTextEditorLink": "Link",
|
||||||
|
"LabelTextEditorNumberedList": "Numbered list",
|
||||||
|
"LabelTextEditorUnlink": "Unlink",
|
||||||
"LabelTheme": "Тема",
|
"LabelTheme": "Тема",
|
||||||
"LabelThemeDark": "Темная",
|
"LabelThemeDark": "Темная",
|
||||||
"LabelThemeLight": "Светлая",
|
"LabelThemeLight": "Светлая",
|
||||||
@ -516,6 +531,7 @@
|
|||||||
"LabelTracksSingleTrack": "Один трек",
|
"LabelTracksSingleTrack": "Один трек",
|
||||||
"LabelType": "Тип",
|
"LabelType": "Тип",
|
||||||
"LabelUnabridged": "Полное издание",
|
"LabelUnabridged": "Полное издание",
|
||||||
|
"LabelUndo": "Undo",
|
||||||
"LabelUnknown": "Неизвестно",
|
"LabelUnknown": "Неизвестно",
|
||||||
"LabelUpdateCover": "Обновить обложку",
|
"LabelUpdateCover": "Обновить обложку",
|
||||||
"LabelUpdateCoverHelp": "Позволяет перезаписывать существующие обложки для выбранных книг если будут найдены",
|
"LabelUpdateCoverHelp": "Позволяет перезаписывать существующие обложки для выбранных книг если будут найдены",
|
||||||
|
@ -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,15 @@
|
|||||||
"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",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
@ -104,6 +109,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",
|
||||||
@ -281,8 +287,11 @@
|
|||||||
"LabelFinished": "Avslutad",
|
"LabelFinished": "Avslutad",
|
||||||
"LabelFolder": "Mapp",
|
"LabelFolder": "Mapp",
|
||||||
"LabelFolders": "Mappar",
|
"LabelFolders": "Mappar",
|
||||||
|
"LabelFontBold": "Bold",
|
||||||
"LabelFontFamily": "Teckensnittsfamilj",
|
"LabelFontFamily": "Teckensnittsfamilj",
|
||||||
|
"LabelFontItalic": "Italic",
|
||||||
"LabelFontScale": "Teckensnittsskala",
|
"LabelFontScale": "Teckensnittsskala",
|
||||||
|
"LabelFontStrikethrough": "Strikethrough",
|
||||||
"LabelFormat": "Format",
|
"LabelFormat": "Format",
|
||||||
"LabelGenre": "Genre",
|
"LabelGenre": "Genre",
|
||||||
"LabelGenres": "Genrer",
|
"LabelGenres": "Genrer",
|
||||||
@ -387,6 +396,7 @@
|
|||||||
"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)",
|
||||||
@ -403,6 +413,7 @@
|
|||||||
"LabelRecentlyAdded": "Nyligen tillagd",
|
"LabelRecentlyAdded": "Nyligen tillagd",
|
||||||
"LabelRecentSeries": "Senaste serier",
|
"LabelRecentSeries": "Senaste serier",
|
||||||
"LabelRecommended": "Rekommenderad",
|
"LabelRecommended": "Rekommenderad",
|
||||||
|
"LabelRedo": "Redo",
|
||||||
"LabelRegion": "Region",
|
"LabelRegion": "Region",
|
||||||
"LabelReleaseDate": "Utgivningsdatum",
|
"LabelReleaseDate": "Utgivningsdatum",
|
||||||
"LabelRemoveCover": "Ta bort omslag",
|
"LabelRemoveCover": "Ta bort omslag",
|
||||||
@ -491,6 +502,10 @@
|
|||||||
"LabelTagsAccessibleToUser": "Taggar tillgängliga för användaren",
|
"LabelTagsAccessibleToUser": "Taggar tillgängliga för användaren",
|
||||||
"LabelTagsNotAccessibleToUser": "Taggar inte tillgängliga för användaren",
|
"LabelTagsNotAccessibleToUser": "Taggar inte tillgängliga för användaren",
|
||||||
"LabelTasks": "Körande uppgifter",
|
"LabelTasks": "Körande uppgifter",
|
||||||
|
"LabelTextEditorBulletedList": "Bulleted list",
|
||||||
|
"LabelTextEditorLink": "Link",
|
||||||
|
"LabelTextEditorNumberedList": "Numbered list",
|
||||||
|
"LabelTextEditorUnlink": "Unlink",
|
||||||
"LabelTheme": "Tema",
|
"LabelTheme": "Tema",
|
||||||
"LabelThemeDark": "Mörkt",
|
"LabelThemeDark": "Mörkt",
|
||||||
"LabelThemeLight": "Ljust",
|
"LabelThemeLight": "Ljust",
|
||||||
@ -516,6 +531,7 @@
|
|||||||
"LabelTracksSingleTrack": "Enspårigt",
|
"LabelTracksSingleTrack": "Enspårigt",
|
||||||
"LabelType": "Typ",
|
"LabelType": "Typ",
|
||||||
"LabelUnabridged": "Oavkortad",
|
"LabelUnabridged": "Oavkortad",
|
||||||
|
"LabelUndo": "Undo",
|
||||||
"LabelUnknown": "Okänd",
|
"LabelUnknown": "Okänd",
|
||||||
"LabelUpdateCover": "Uppdatera omslag",
|
"LabelUpdateCover": "Uppdatera omslag",
|
||||||
"LabelUpdateCoverHelp": "Tillåt överskrivning av befintliga omslag för de valda böckerna när en matchning hittas",
|
"LabelUpdateCoverHelp": "Tillåt överskrivning av befintliga omslag för de valda böckerna när en matchning hittas",
|
||||||
|
@ -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,15 @@
|
|||||||
"ButtonMatchAllAuthors": "匹配所有作者",
|
"ButtonMatchAllAuthors": "匹配所有作者",
|
||||||
"ButtonMatchBooks": "匹配图书",
|
"ButtonMatchBooks": "匹配图书",
|
||||||
"ButtonNevermind": "没有关系",
|
"ButtonNevermind": "没有关系",
|
||||||
|
"ButtonNextChapter": "Next Chapter",
|
||||||
"ButtonOk": "确定",
|
"ButtonOk": "确定",
|
||||||
"ButtonOpenFeed": "打开源",
|
"ButtonOpenFeed": "打开源",
|
||||||
"ButtonOpenManager": "打开管理器",
|
"ButtonOpenManager": "打开管理器",
|
||||||
|
"ButtonPause": "Pause",
|
||||||
"ButtonPlay": "播放",
|
"ButtonPlay": "播放",
|
||||||
"ButtonPlaying": "正在播放",
|
"ButtonPlaying": "正在播放",
|
||||||
"ButtonPlaylists": "播放列表",
|
"ButtonPlaylists": "播放列表",
|
||||||
|
"ButtonPreviousChapter": "Previous Chapter",
|
||||||
"ButtonPurgeAllCache": "清理所有缓存",
|
"ButtonPurgeAllCache": "清理所有缓存",
|
||||||
"ButtonPurgeItemsCache": "清理项目缓存",
|
"ButtonPurgeItemsCache": "清理项目缓存",
|
||||||
"ButtonPurgeMediaProgress": "清理媒体进度",
|
"ButtonPurgeMediaProgress": "清理媒体进度",
|
||||||
@ -104,6 +109,7 @@
|
|||||||
"HeaderCollectionItems": "收藏项目",
|
"HeaderCollectionItems": "收藏项目",
|
||||||
"HeaderCover": "封面",
|
"HeaderCover": "封面",
|
||||||
"HeaderCurrentDownloads": "当前下载",
|
"HeaderCurrentDownloads": "当前下载",
|
||||||
|
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
|
||||||
"HeaderDetails": "详情",
|
"HeaderDetails": "详情",
|
||||||
"HeaderDownloadQueue": "下载队列",
|
"HeaderDownloadQueue": "下载队列",
|
||||||
"HeaderEbookFiles": "电子书文件",
|
"HeaderEbookFiles": "电子书文件",
|
||||||
@ -281,8 +287,11 @@
|
|||||||
"LabelFinished": "已听完",
|
"LabelFinished": "已听完",
|
||||||
"LabelFolder": "文件夹",
|
"LabelFolder": "文件夹",
|
||||||
"LabelFolders": "文件夹",
|
"LabelFolders": "文件夹",
|
||||||
|
"LabelFontBold": "Bold",
|
||||||
"LabelFontFamily": "字体系列",
|
"LabelFontFamily": "字体系列",
|
||||||
|
"LabelFontItalic": "Italic",
|
||||||
"LabelFontScale": "字体比例",
|
"LabelFontScale": "字体比例",
|
||||||
|
"LabelFontStrikethrough": "Strikethrough",
|
||||||
"LabelFormat": "编码格式",
|
"LabelFormat": "编码格式",
|
||||||
"LabelGenre": "流派",
|
"LabelGenre": "流派",
|
||||||
"LabelGenres": "流派",
|
"LabelGenres": "流派",
|
||||||
@ -387,6 +396,7 @@
|
|||||||
"LabelPlayMethod": "播放方法",
|
"LabelPlayMethod": "播放方法",
|
||||||
"LabelPodcast": "播客",
|
"LabelPodcast": "播客",
|
||||||
"LabelPodcasts": "播客",
|
"LabelPodcasts": "播客",
|
||||||
|
"LabelPodcastSearchRegion": "播客搜索地区",
|
||||||
"LabelPodcastType": "播客类型",
|
"LabelPodcastType": "播客类型",
|
||||||
"LabelPort": "端口",
|
"LabelPort": "端口",
|
||||||
"LabelPrefixesToIgnore": "忽略的前缀 (不区分大小写)",
|
"LabelPrefixesToIgnore": "忽略的前缀 (不区分大小写)",
|
||||||
@ -403,6 +413,7 @@
|
|||||||
"LabelRecentlyAdded": "最近添加",
|
"LabelRecentlyAdded": "最近添加",
|
||||||
"LabelRecentSeries": "最近添加系列",
|
"LabelRecentSeries": "最近添加系列",
|
||||||
"LabelRecommended": "推荐内容",
|
"LabelRecommended": "推荐内容",
|
||||||
|
"LabelRedo": "Redo",
|
||||||
"LabelRegion": "区域",
|
"LabelRegion": "区域",
|
||||||
"LabelReleaseDate": "发布日期",
|
"LabelReleaseDate": "发布日期",
|
||||||
"LabelRemoveCover": "移除封面",
|
"LabelRemoveCover": "移除封面",
|
||||||
@ -491,6 +502,10 @@
|
|||||||
"LabelTagsAccessibleToUser": "用户可访问的标签",
|
"LabelTagsAccessibleToUser": "用户可访问的标签",
|
||||||
"LabelTagsNotAccessibleToUser": "用户无法访问标签",
|
"LabelTagsNotAccessibleToUser": "用户无法访问标签",
|
||||||
"LabelTasks": "正在运行的任务",
|
"LabelTasks": "正在运行的任务",
|
||||||
|
"LabelTextEditorBulletedList": "Bulleted list",
|
||||||
|
"LabelTextEditorLink": "Link",
|
||||||
|
"LabelTextEditorNumberedList": "Numbered list",
|
||||||
|
"LabelTextEditorUnlink": "Unlink",
|
||||||
"LabelTheme": "主题",
|
"LabelTheme": "主题",
|
||||||
"LabelThemeDark": "黑暗",
|
"LabelThemeDark": "黑暗",
|
||||||
"LabelThemeLight": "明亮",
|
"LabelThemeLight": "明亮",
|
||||||
@ -516,6 +531,7 @@
|
|||||||
"LabelTracksSingleTrack": "单轨",
|
"LabelTracksSingleTrack": "单轨",
|
||||||
"LabelType": "类型",
|
"LabelType": "类型",
|
||||||
"LabelUnabridged": "未删节",
|
"LabelUnabridged": "未删节",
|
||||||
|
"LabelUndo": "Undo",
|
||||||
"LabelUnknown": "未知",
|
"LabelUnknown": "未知",
|
||||||
"LabelUpdateCover": "更新封面",
|
"LabelUpdateCover": "更新封面",
|
||||||
"LabelUpdateCoverHelp": "找到匹配项时允许覆盖所选书籍存在的封面",
|
"LabelUpdateCoverHelp": "找到匹配项时允许覆盖所选书籍存在的封面",
|
||||||
|
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",
|
||||||
|
87
readme.md
87
readme.md
@ -241,6 +241,93 @@ subdomain.domain.com {
|
|||||||
reverse_proxy <LOCAL_IP>:<PORT>
|
reverse_proxy <LOCAL_IP>:<PORT>
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
### HAProxy
|
||||||
|
|
||||||
|
Below is a generic HAProxy config, using `audiobookshelf.YOUR_DOMAIN.COM`.
|
||||||
|
|
||||||
|
To use `http2`, `ssl` is needed.
|
||||||
|
|
||||||
|
````make
|
||||||
|
global
|
||||||
|
# ... (your global settings go here)
|
||||||
|
|
||||||
|
defaults
|
||||||
|
mode http
|
||||||
|
# ... (your default settings go here)
|
||||||
|
|
||||||
|
frontend my_frontend
|
||||||
|
# Bind to port 443, enable SSL, and specify the certificate list file
|
||||||
|
bind :443 name :443 ssl crt-list /path/to/cert.crt_list alpn h2,http/1.1
|
||||||
|
mode http
|
||||||
|
|
||||||
|
# Define an ACL for subdomains starting with "audiobookshelf"
|
||||||
|
acl is_audiobookshelf hdr_beg(host) -i audiobookshelf
|
||||||
|
|
||||||
|
# Use the ACL to route traffic to audiobookshelf_backend if the condition is met,
|
||||||
|
# otherwise, use the default_backend
|
||||||
|
use_backend audiobookshelf_backend if is_audiobookshelf
|
||||||
|
default_backend default_backend
|
||||||
|
|
||||||
|
backend audiobookshelf_backend
|
||||||
|
mode http
|
||||||
|
# ... (backend settings for audiobookshelf go here)
|
||||||
|
|
||||||
|
# Define the server for the audiobookshelf backend
|
||||||
|
server audiobookshelf_server 127.0.0.99:13378
|
||||||
|
|
||||||
|
backend default_backend
|
||||||
|
mode http
|
||||||
|
# ... (default backend settings go here)
|
||||||
|
|
||||||
|
# Define the server for the default backend
|
||||||
|
server default_server 127.0.0.123:8081
|
||||||
|
|
||||||
|
````
|
||||||
|
|
||||||
|
### pfSense and HAProxy
|
||||||
|
|
||||||
|
For pfSense the inputs are graphical, and `Health checking` is enabled.
|
||||||
|
|
||||||
|
#### Frontend, Default backend, access control lists and actions
|
||||||
|
|
||||||
|
##### Access Control lists
|
||||||
|
|
||||||
|
| Name | Expression | CS | Not | Value |
|
||||||
|
|:--------------:|:-----------------:|:--:|:---:|:---------------:|
|
||||||
|
| audiobookshelf | Host starts with: | | | audiobookshelf. |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##### Actions
|
||||||
|
|
||||||
|
The `condition acl names` needs to match the name above `audiobookshelf`.
|
||||||
|
|
||||||
|
| Action | Parameters | Condition acl names |
|
||||||
|
|:--------------:|:-----------------:|:---------------:|
|
||||||
|
| `Use Backend` |audiobookshelf | audiobookshelf |
|
||||||
|
|
||||||
|
#### Backend
|
||||||
|
|
||||||
|
|
||||||
|
The `Name` needs to match the `Parameters` above `audiobookshelf`.
|
||||||
|
|
||||||
|
| Name | audiobookshelf |
|
||||||
|
|--------------|-----------------|
|
||||||
|
|
||||||
|
##### Server list:
|
||||||
|
|
||||||
|
| Name | Expression | CS | Not | Value |
|
||||||
|
|:--------------:|:-----------------:|:--:|:---:|:---------------:|
|
||||||
|
| audiobookshelf | Host starts with: | | | audiobookshelf. |
|
||||||
|
|
||||||
|
##### Health checking:
|
||||||
|
|
||||||
|
Health checking is enabled by default. `Http check method` of `OPTIONS` is not supported on Audiobookshelf.
|
||||||
|
If Health check fails, data will not be forwared.
|
||||||
|
Need to do one of following:
|
||||||
|
|
||||||
|
* To disable: Change `Health check method` to `none`.
|
||||||
|
* To make Health checking function: Change `Http check method` to `HEAD` or `GET`.
|
||||||
|
|
||||||
|
|
||||||
# Run from source
|
# Run from source
|
||||||
|
269
server/Auth.js
269
server/Auth.js
@ -8,7 +8,6 @@ const ExtractJwt = require('passport-jwt').ExtractJwt
|
|||||||
const OpenIDClient = require('openid-client')
|
const OpenIDClient = require('openid-client')
|
||||||
const Database = require('./Database')
|
const Database = require('./Database')
|
||||||
const Logger = require('./Logger')
|
const Logger = require('./Logger')
|
||||||
const e = require('express')
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @class Class for handling all the authentication related functionality.
|
* @class Class for handling all the authentication related functionality.
|
||||||
@ -82,7 +81,8 @@ class Auth {
|
|||||||
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,
|
||||||
@ -154,6 +154,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)
|
||||||
}))
|
}))
|
||||||
@ -184,49 +187,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) {
|
const callback = req.query.redirect_uri || req.query.callback
|
||||||
// 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
|
// Additional handling for non-API based authMethod
|
||||||
|
if (!this.isAuthMethodAPIBased(authMethod)) {
|
||||||
|
// Store 'auth_state' if present in the request
|
||||||
if (req.query.state) {
|
if (req.query.state) {
|
||||||
res.cookie('auth_state', req.query.state, {
|
res.cookie('auth_state', req.query.state, { maxAge: TWO_MINUTES, httpOnly: true })
|
||||||
maxAge: 120000, // 2 min
|
|
||||||
httpOnly: true
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const callback = req.query.redirect_uri || req.query.callback
|
// Validate and store the callback URL
|
||||||
|
|
||||||
// check if we are missing a callback parameter - we need one if isRest=false
|
|
||||||
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 })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -240,7 +242,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 {
|
||||||
@ -270,109 +272,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) => {
|
||||||
|
// Get the OIDC client from the strategy
|
||||||
|
// We need to call the client manually, because the strategy does not support forwarding the code challenge
|
||||||
|
// for API or mobile clients
|
||||||
|
const oidcStrategy = passport._strategy('openid-client')
|
||||||
|
const client = oidcStrategy._client
|
||||||
|
const sessionKey = oidcStrategy._key
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// helper function from openid-client
|
const protocol = req.secure || req.get('x-forwarded-proto') === 'https' ? 'https' : 'http'
|
||||||
function pick(object, ...paths) {
|
const hostUrl = new URL(`${protocol}://${req.get('host')}`)
|
||||||
const obj = {}
|
const isMobileFlow = req.query.response_type === 'code' || req.query.redirect_uri || req.query.code_challenge
|
||||||
for (const path of paths) {
|
|
||||||
if (object[path] !== undefined) {
|
// Only allow code flow (for mobile clients)
|
||||||
obj[path] = object[path]
|
if (req.query.response_type && req.query.response_type !== 'code') {
|
||||||
}
|
Logger.debug(`[Auth] OIDC Invalid response_type=${req.query.response_type}`)
|
||||||
}
|
return res.status(400).send('Invalid response_type, only code supported')
|
||||||
return obj
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the OIDC client from the strategy
|
// Generate a state on web flow or if no state supplied
|
||||||
// We need to call the client manually, because the strategy does not support forwarding the code challenge
|
const state = (!isMobileFlow || !req.query.state) ? OpenIDClient.generators.random() : req.query.state
|
||||||
// for API or mobile clients
|
|
||||||
const oidcStrategy = passport._strategy('openid-client')
|
|
||||||
const protocol = (req.secure || req.get('x-forwarded-proto') === 'https') ? 'https' : 'http'
|
|
||||||
|
|
||||||
let mobile_redirect_uri = null
|
// Redirect URL for the SSO provider
|
||||||
|
let redirectUri
|
||||||
// The client wishes a different redirect_uri
|
if (isMobileFlow) {
|
||||||
// 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
|
// Mobile required redirect uri
|
||||||
// where we will handle the redirect to it
|
// If it is in the whitelist, we will save into this.openIdAuthSession and set the redirect uri to /auth/openid/mobile-redirect
|
||||||
if (req.query.redirect_uri) {
|
// where we will handle the redirect to it
|
||||||
// Check if the redirect_uri is in the whitelist
|
if (!req.query.redirect_uri || !isValidRedirectUri(req.query.redirect_uri)) {
|
||||||
if (Database.serverSettings.authOpenIDMobileRedirectURIs.includes(req.query.redirect_uri) ||
|
Logger.debug(`[Auth] Invalid redirect_uri=${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')
|
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 {
|
||||||
oidcStrategy._params.redirect_uri = new URL(`${protocol}://${req.get('host')}/auth/openid/callback`).toString()
|
redirectUri = new URL('/auth/openid/callback', hostUrl).toString()
|
||||||
}
|
|
||||||
|
|
||||||
Logger.debug(`[Auth] Oidc redirect_uri=${oidcStrategy._params.redirect_uri}`)
|
if (req.query.state) {
|
||||||
const client = oidcStrategy._client
|
Logger.debug(`[Auth] Invalid state - not allowed on web openid flow`)
|
||||||
const sessionKey = oidcStrategy._key
|
return res.status(400).send('Invalid state, not allowed on web flow')
|
||||||
|
|
||||||
let code_challenge
|
|
||||||
let code_challenge_method
|
|
||||||
|
|
||||||
// If code_challenge is provided, expect that code_verifier will be handled by the client (mobile app)
|
|
||||||
// The web frontend of ABS does not need to do a PKCE itself, because it never handles the "code" of the oauth flow
|
|
||||||
// and as such will not send a code challenge, we will generate then one
|
|
||||||
if (req.query.code_challenge) {
|
|
||||||
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')
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// If no code_challenge is provided, assume a web application flow and generate one
|
|
||||||
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
|
|
||||||
req.session[sessionKey] = { ...req.session[sessionKey], code_verifier }
|
|
||||||
}
|
}
|
||||||
|
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
|
||||||
@ -454,6 +452,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()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -522,7 +526,46 @@ class Auth {
|
|||||||
if (err) {
|
if (err) {
|
||||||
res.sendStatus(500)
|
res.sendStatus(500)
|
||||||
} else {
|
} else {
|
||||||
res.sendStatus(200)
|
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 {
|
||||||
|
res.sendStatus(200)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -613,7 +656,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
|
||||||
@ -655,7 +698,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) => {
|
||||||
@ -689,8 +732,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,7 +69,12 @@ 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
|
||||||
|
*/
|
||||||
|
async handleLog(level, args) {
|
||||||
const logObj = {
|
const logObj = {
|
||||||
timestamp: this.timestamp,
|
timestamp: this.timestamp,
|
||||||
source: this.source,
|
source: this.source,
|
||||||
@ -71,15 +83,17 @@ class Logger {
|
|||||||
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) {
|
||||||
@ -117,9 +131,15 @@ class Logger {
|
|||||||
this.handleLog(LogLevel.ERROR, args)
|
this.handleLog(LogLevel.ERROR, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
note(...args) {
|
note(...args) {
|
||||||
|
@ -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,11 +124,6 @@ class LibraryItemController {
|
|||||||
const libraryItem = req.libraryItem
|
const libraryItem = req.libraryItem
|
||||||
const mediaPayload = req.body
|
const mediaPayload = req.body
|
||||||
|
|
||||||
// Item has cover and update is removing cover so purge it from cache
|
|
||||||
if (libraryItem.media.coverPath && (mediaPayload.coverPath === '' || mediaPayload.coverPath === null)) {
|
|
||||||
await CacheManager.purgeCoverCache(libraryItem.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Book specific
|
// Book specific
|
||||||
if (libraryItem.isBook) {
|
if (libraryItem.isBook) {
|
||||||
await this.createAuthorsAndSeriesForItemUpdate(mediaPayload, libraryItem.libraryId)
|
await this.createAuthorsAndSeriesForItemUpdate(mediaPayload, libraryItem.libraryId)
|
||||||
|
@ -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', '6'] },
|
||||||
{ name: 'ffprobe', envVariable: 'FFPROBE_PATH' }
|
{ name: 'ffprobe', envVariable: 'FFPROBE_PATH', validVersions: ['5.1', '6'] }
|
||||||
]
|
]
|
||||||
|
|
||||||
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: '6.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 lastLogs = this.currentDailyLog.logs.slice(-5000)
|
const logsDir = Path.join(global.MetadataPath, 'logs')
|
||||||
socket.emit('daily_logs', lastLogs)
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: [
|
||||||
|
@ -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')
|
||||||
}
|
}
|
||||||
|
@ -84,6 +84,24 @@ class Feed {
|
|||||||
return episode.fullPath
|
return episode.fullPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If chapters for an audiobook match the audio tracks then use chapter titles instead of audio file names
|
||||||
|
*
|
||||||
|
* @param {import('../objects/LibraryItem')} libraryItem
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
checkUseChapterTitlesForEpisodes(libraryItem) {
|
||||||
|
const tracks = libraryItem.media.tracks
|
||||||
|
const chapters = libraryItem.media.chapters
|
||||||
|
if (tracks.length !== chapters.length) return false
|
||||||
|
for (let i = 0; i < tracks.length; i++) {
|
||||||
|
if (Math.abs(chapters[i].start - tracks[i].startOffset) >= 1) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
setFromItem(userId, slug, libraryItem, serverAddress, preventIndexing = true, ownerName = null, ownerEmail = null) {
|
setFromItem(userId, slug, libraryItem, serverAddress, preventIndexing = true, ownerName = null, ownerEmail = null) {
|
||||||
const media = libraryItem.media
|
const media = libraryItem.media
|
||||||
const mediaMetadata = media.metadata
|
const mediaMetadata = media.metadata
|
||||||
@ -128,9 +146,10 @@ class Feed {
|
|||||||
this.episodes.push(feedEpisode)
|
this.episodes.push(feedEpisode)
|
||||||
})
|
})
|
||||||
} else { // AUDIOBOOK EPISODES
|
} else { // AUDIOBOOK EPISODES
|
||||||
|
const useChapterTitles = this.checkUseChapterTitlesForEpisodes(libraryItem)
|
||||||
media.tracks.forEach((audioTrack) => {
|
media.tracks.forEach((audioTrack) => {
|
||||||
const feedEpisode = new FeedEpisode()
|
const feedEpisode = new FeedEpisode()
|
||||||
feedEpisode.setFromAudiobookTrack(libraryItem, serverAddress, slug, audioTrack, this.meta)
|
feedEpisode.setFromAudiobookTrack(libraryItem, serverAddress, slug, audioTrack, this.meta, useChapterTitles)
|
||||||
this.episodes.push(feedEpisode)
|
this.episodes.push(feedEpisode)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -168,9 +187,10 @@ class Feed {
|
|||||||
this.episodes.push(feedEpisode)
|
this.episodes.push(feedEpisode)
|
||||||
})
|
})
|
||||||
} else { // AUDIOBOOK EPISODES
|
} else { // AUDIOBOOK EPISODES
|
||||||
|
const useChapterTitles = this.checkUseChapterTitlesForEpisodes(libraryItem)
|
||||||
media.tracks.forEach((audioTrack) => {
|
media.tracks.forEach((audioTrack) => {
|
||||||
const feedEpisode = new FeedEpisode()
|
const feedEpisode = new FeedEpisode()
|
||||||
feedEpisode.setFromAudiobookTrack(libraryItem, this.serverAddress, this.slug, audioTrack, this.meta)
|
feedEpisode.setFromAudiobookTrack(libraryItem, this.serverAddress, this.slug, audioTrack, this.meta, useChapterTitles)
|
||||||
this.episodes.push(feedEpisode)
|
this.episodes.push(feedEpisode)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -214,9 +234,10 @@ class Feed {
|
|||||||
itemsWithTracks.forEach((item, index) => {
|
itemsWithTracks.forEach((item, index) => {
|
||||||
if (item.updatedAt > this.entityUpdatedAt) this.entityUpdatedAt = item.updatedAt
|
if (item.updatedAt > this.entityUpdatedAt) this.entityUpdatedAt = item.updatedAt
|
||||||
|
|
||||||
|
const useChapterTitles = this.checkUseChapterTitlesForEpisodes(item)
|
||||||
item.media.tracks.forEach((audioTrack) => {
|
item.media.tracks.forEach((audioTrack) => {
|
||||||
const feedEpisode = new FeedEpisode()
|
const feedEpisode = new FeedEpisode()
|
||||||
feedEpisode.setFromAudiobookTrack(item, serverAddress, slug, audioTrack, this.meta, index)
|
feedEpisode.setFromAudiobookTrack(item, serverAddress, slug, audioTrack, this.meta, useChapterTitles, index)
|
||||||
this.episodes.push(feedEpisode)
|
this.episodes.push(feedEpisode)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -245,9 +266,10 @@ class Feed {
|
|||||||
itemsWithTracks.forEach((item, index) => {
|
itemsWithTracks.forEach((item, index) => {
|
||||||
if (item.updatedAt > this.entityUpdatedAt) this.entityUpdatedAt = item.updatedAt
|
if (item.updatedAt > this.entityUpdatedAt) this.entityUpdatedAt = item.updatedAt
|
||||||
|
|
||||||
|
const useChapterTitles = this.checkUseChapterTitlesForEpisodes(item)
|
||||||
item.media.tracks.forEach((audioTrack) => {
|
item.media.tracks.forEach((audioTrack) => {
|
||||||
const feedEpisode = new FeedEpisode()
|
const feedEpisode = new FeedEpisode()
|
||||||
feedEpisode.setFromAudiobookTrack(item, this.serverAddress, this.slug, audioTrack, this.meta, index)
|
feedEpisode.setFromAudiobookTrack(item, this.serverAddress, this.slug, audioTrack, this.meta, useChapterTitles, index)
|
||||||
this.episodes.push(feedEpisode)
|
this.episodes.push(feedEpisode)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -295,9 +317,10 @@ class Feed {
|
|||||||
itemsWithTracks.forEach((item, index) => {
|
itemsWithTracks.forEach((item, index) => {
|
||||||
if (item.updatedAt > this.entityUpdatedAt) this.entityUpdatedAt = item.updatedAt
|
if (item.updatedAt > this.entityUpdatedAt) this.entityUpdatedAt = item.updatedAt
|
||||||
|
|
||||||
|
const useChapterTitles = this.checkUseChapterTitlesForEpisodes(item)
|
||||||
item.media.tracks.forEach((audioTrack) => {
|
item.media.tracks.forEach((audioTrack) => {
|
||||||
const feedEpisode = new FeedEpisode()
|
const feedEpisode = new FeedEpisode()
|
||||||
feedEpisode.setFromAudiobookTrack(item, serverAddress, slug, audioTrack, this.meta, index)
|
feedEpisode.setFromAudiobookTrack(item, serverAddress, slug, audioTrack, this.meta, useChapterTitles, index)
|
||||||
this.episodes.push(feedEpisode)
|
this.episodes.push(feedEpisode)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -329,9 +352,10 @@ class Feed {
|
|||||||
itemsWithTracks.forEach((item, index) => {
|
itemsWithTracks.forEach((item, index) => {
|
||||||
if (item.updatedAt > this.entityUpdatedAt) this.entityUpdatedAt = item.updatedAt
|
if (item.updatedAt > this.entityUpdatedAt) this.entityUpdatedAt = item.updatedAt
|
||||||
|
|
||||||
|
const useChapterTitles = this.checkUseChapterTitlesForEpisodes(item)
|
||||||
item.media.tracks.forEach((audioTrack) => {
|
item.media.tracks.forEach((audioTrack) => {
|
||||||
const feedEpisode = new FeedEpisode()
|
const feedEpisode = new FeedEpisode()
|
||||||
feedEpisode.setFromAudiobookTrack(item, this.serverAddress, this.slug, audioTrack, this.meta, index)
|
feedEpisode.setFromAudiobookTrack(item, this.serverAddress, this.slug, audioTrack, this.meta, useChapterTitles, index)
|
||||||
this.episodes.push(feedEpisode)
|
this.episodes.push(feedEpisode)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -97,7 +97,17 @@ class FeedEpisode {
|
|||||||
this.fullPath = episode.audioFile.metadata.path
|
this.fullPath = episode.audioFile.metadata.path
|
||||||
}
|
}
|
||||||
|
|
||||||
setFromAudiobookTrack(libraryItem, serverAddress, slug, audioTrack, meta, additionalOffset = null) {
|
/**
|
||||||
|
*
|
||||||
|
* @param {import('../objects/LibraryItem')} libraryItem
|
||||||
|
* @param {string} serverAddress
|
||||||
|
* @param {string} slug
|
||||||
|
* @param {import('../objects/files/AudioTrack')} audioTrack
|
||||||
|
* @param {Object} meta
|
||||||
|
* @param {boolean} useChapterTitles
|
||||||
|
* @param {number} [additionalOffset]
|
||||||
|
*/
|
||||||
|
setFromAudiobookTrack(libraryItem, serverAddress, slug, audioTrack, meta, useChapterTitles, additionalOffset = null) {
|
||||||
// Example: <pubDate>Fri, 04 Feb 2015 00:00:00 GMT</pubDate>
|
// Example: <pubDate>Fri, 04 Feb 2015 00:00:00 GMT</pubDate>
|
||||||
let timeOffset = isNaN(audioTrack.index) ? 0 : (Number(audioTrack.index) * 1000) // Offset pubdate to ensure correct order
|
let timeOffset = isNaN(audioTrack.index) ? 0 : (Number(audioTrack.index) * 1000) // Offset pubdate to ensure correct order
|
||||||
let episodeId = uuidv4()
|
let episodeId = uuidv4()
|
||||||
@ -119,10 +129,10 @@ class FeedEpisode {
|
|||||||
if (libraryItem.media.tracks.length == 1) { // If audiobook is a single file, use book title instead of chapter/file title
|
if (libraryItem.media.tracks.length == 1) { // If audiobook is a single file, use book title instead of chapter/file title
|
||||||
title = libraryItem.media.metadata.title
|
title = libraryItem.media.metadata.title
|
||||||
} else {
|
} else {
|
||||||
if (libraryItem.media.chapters.length) {
|
if (useChapterTitles) {
|
||||||
// If audio track start and chapter start are within 1 seconds of eachother then use the chapter title
|
// If audio track start and chapter start are within 1 seconds of eachother then use the chapter title
|
||||||
var matchingChapter = libraryItem.media.chapters.find(ch => Math.abs(ch.start - audioTrack.startOffset) < 1)
|
const matchingChapter = libraryItem.media.chapters.find(ch => Math.abs(ch.start - audioTrack.startOffset) < 1)
|
||||||
if (matchingChapter && matchingChapter.title) title = matchingChapter.title
|
if (matchingChapter?.title) title = matchingChapter.title
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ class ServerSettings {
|
|||||||
this.buildNumber = packageJson.buildNumber
|
this.buildNumber = packageJson.buildNumber
|
||||||
|
|
||||||
// Auth settings
|
// Auth settings
|
||||||
// Active auth methodes
|
this.authLoginCustomMessage = null
|
||||||
this.authActiveAuthMethods = ['local']
|
this.authActiveAuthMethods = ['local']
|
||||||
|
|
||||||
// openid settings
|
// openid settings
|
||||||
@ -113,6 +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.8.0
|
||||||
this.authActiveAuthMethods = settings.authActiveAuthMethods || ['local']
|
this.authActiveAuthMethods = settings.authActiveAuthMethods || ['local']
|
||||||
|
|
||||||
this.authOpenIDIssuerURL = settings.authOpenIDIssuerURL || null
|
this.authOpenIDIssuerURL = settings.authOpenIDIssuerURL || null
|
||||||
@ -201,6 +202,7 @@ class ServerSettings {
|
|||||||
logLevel: this.logLevel,
|
logLevel: this.logLevel,
|
||||||
version: this.version,
|
version: this.version,
|
||||||
buildNumber: this.buildNumber,
|
buildNumber: this.buildNumber,
|
||||||
|
authLoginCustomMessage: this.authLoginCustomMessage,
|
||||||
authActiveAuthMethods: this.authActiveAuthMethods,
|
authActiveAuthMethods: this.authActiveAuthMethods,
|
||||||
authOpenIDIssuerURL: this.authOpenIDIssuerURL,
|
authOpenIDIssuerURL: this.authOpenIDIssuerURL,
|
||||||
authOpenIDAuthorizationURL: this.authOpenIDAuthorizationURL,
|
authOpenIDAuthorizationURL: this.authOpenIDAuthorizationURL,
|
||||||
@ -213,7 +215,7 @@ class ServerSettings {
|
|||||||
authOpenIDButtonText: this.authOpenIDButtonText,
|
authOpenIDButtonText: this.authOpenIDButtonText,
|
||||||
authOpenIDAutoLaunch: this.authOpenIDAutoLaunch,
|
authOpenIDAutoLaunch: this.authOpenIDAutoLaunch,
|
||||||
authOpenIDAutoRegister: this.authOpenIDAutoRegister,
|
authOpenIDAutoRegister: this.authOpenIDAutoRegister,
|
||||||
authOpenIDMatchExistingBy: this.authOpenIDMatchExistingBy,
|
authOpenIDMatchExistingBy: this.authOpenIDMatchExistingBy,
|
||||||
authOpenIDMobileRedirectURIs: this.authOpenIDMobileRedirectURIs // Do not return to client
|
authOpenIDMobileRedirectURIs: this.authOpenIDMobileRedirectURIs // Do not return to client
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -246,6 +248,7 @@ class ServerSettings {
|
|||||||
|
|
||||||
get authenticationSettings() {
|
get authenticationSettings() {
|
||||||
return {
|
return {
|
||||||
|
authLoginCustomMessage: this.authLoginCustomMessage,
|
||||||
authActiveAuthMethods: this.authActiveAuthMethods,
|
authActiveAuthMethods: this.authActiveAuthMethods,
|
||||||
authOpenIDIssuerURL: this.authOpenIDIssuerURL,
|
authOpenIDIssuerURL: this.authOpenIDIssuerURL,
|
||||||
authOpenIDAuthorizationURL: this.authOpenIDAuthorizationURL,
|
authOpenIDAuthorizationURL: this.authOpenIDAuthorizationURL,
|
||||||
@ -264,7 +267,9 @@ class ServerSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get authFormData() {
|
get authFormData() {
|
||||||
const clientFormData = {}
|
const clientFormData = {
|
||||||
|
authLoginCustomMessage: this.authLoginCustomMessage
|
||||||
|
}
|
||||||
if (this.authActiveAuthMethods.includes('openid')) {
|
if (this.authActiveAuthMethods.includes('openid')) {
|
||||||
clientFormData.authOpenIDButtonText = this.authOpenIDButtonText
|
clientFormData.authOpenIDButtonText = this.authOpenIDButtonText
|
||||||
clientFormData.authOpenIDAutoLaunch = this.authOpenIDAutoLaunch
|
clientFormData.authOpenIDAutoLaunch = this.authOpenIDAutoLaunch
|
||||||
|
@ -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
|
||||||
|
@ -14,7 +14,7 @@ class AudiobookCovers {
|
|||||||
Logger.error('[AudiobookCovers] Cover search error', error)
|
Logger.error('[AudiobookCovers] Cover search error', error)
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
return items.map(item => ({ cover: item.filename }))
|
return items.map(item => ({ cover: item.versions.png.original }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,14 @@ const { levenshteinDistance } = require('../utils/index')
|
|||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const Throttle = require('p-throttle')
|
const Throttle = require('p-throttle')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef AuthorSearchObj
|
||||||
|
* @property {string} asin
|
||||||
|
* @property {string} description
|
||||||
|
* @property {string} image
|
||||||
|
* @property {string} name
|
||||||
|
*/
|
||||||
|
|
||||||
class Audnexus {
|
class Audnexus {
|
||||||
static _instance = null
|
static _instance = null
|
||||||
|
|
||||||
@ -28,11 +36,19 @@ class Audnexus {
|
|||||||
Audnexus._instance = this
|
Audnexus._instance = this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @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 this._processRequest(this.limiter(() => axios.get(authorRequestUrl)))
|
return this._processRequest(this.limiter(() => axios.get(authorRequestUrl)))
|
||||||
@ -43,6 +59,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}` : ''
|
||||||
@ -58,6 +80,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)
|
||||||
|
|
||||||
@ -70,24 +98,40 @@ class Audnexus {
|
|||||||
} : null
|
} : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @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 authorAsinObjs = await this.authorASINsRequest(name, region)
|
||||||
|
|
||||||
const asins = await this.authorASINsRequest(name, region)
|
let closestMatch = null
|
||||||
const matchingAsin = asins.find(obj => levenshteinDistance(obj.name, name) <= maxLevenshtein)
|
authorAsinObjs.forEach((authorAsinObj) => {
|
||||||
|
authorAsinObj.levenshteinDistance = levenshteinDistance(authorAsinObj.name, name)
|
||||||
|
if (!closestMatch || closestMatch.levenshteinDistance > authorAsinObj.levenshteinDistance) {
|
||||||
|
closestMatch = authorAsinObj
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
if (!matchingAsin) {
|
if (!closestMatch || closestMatch.levenshteinDistance > maxLevenshtein) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const author = await this.authorRequest(matchingAsin.asin)
|
const author = await this.authorRequest(closestMatch.asin)
|
||||||
return author ?
|
if (!author) {
|
||||||
{
|
return null
|
||||||
description: author.description,
|
}
|
||||||
image: author.image || null,
|
|
||||||
asin: author.asin,
|
return {
|
||||||
name: author.name
|
asin: author.asin,
|
||||||
} : null
|
description: author.description,
|
||||||
|
image: author.image || null,
|
||||||
|
name: author.name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getChaptersByASIN(asin, region) {
|
getChaptersByASIN(asin, region) {
|
||||||
@ -124,4 +168,3 @@ class Audnexus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Audnexus
|
module.exports = Audnexus
|
||||||
|
|
||||||
|
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))
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -134,10 +134,13 @@ class LibraryScan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async saveLog() {
|
async saveLog() {
|
||||||
await Logger.logManager.ensureScanLogDir()
|
const scanLogDir = Path.join(global.MetadataPath, 'logs', 'scans')
|
||||||
|
|
||||||
const logDir = Path.join(global.MetadataPath, 'logs', 'scans')
|
if (!(await fs.pathExists(scanLogDir))) {
|
||||||
const outputPath = Path.join(logDir, this.logFilename)
|
await fs.mkdir(scanLogDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputPath = Path.join(scanLogDir, this.logFilename)
|
||||||
const logLines = [JSON.stringify(this.toJSON())]
|
const logLines = [JSON.stringify(this.toJSON())]
|
||||||
this.logs.forEach(l => {
|
this.logs.forEach(l => {
|
||||||
logLines.push(JSON.stringify(l))
|
logLines.push(JSON.stringify(l))
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user