Merge branch 'master' into authorSort

This commit is contained in:
advplyr 2024-03-07 12:26:07 -06:00
commit e50b06183e
109 changed files with 4845 additions and 967 deletions

View File

@ -50,9 +50,8 @@ echo "$controlfile" > dist/debian/DEBIAN/control;
# Package debian # Package debian
pkg -t node18-linux-x64 -o dist/debian/usr/share/audiobookshelf/audiobookshelf . pkg -t node18-linux-x64 -o dist/debian/usr/share/audiobookshelf/audiobookshelf .
fakeroot dpkg-deb --build dist/debian fakeroot dpkg-deb -Zxz --build dist/debian
mv dist/debian.deb "dist/$OUTPUT_FILE" mv dist/debian.deb "dist/$OUTPUT_FILE"
chmod +x "dist/$OUTPUT_FILE"
echo "Finished! Filename: $OUTPUT_FILE" echo "Finished! Filename: $OUTPUT_FILE"

View File

@ -217,36 +217,6 @@ Bookshelf Label
filter: blur(20px); filter: blur(20px);
} }
.episode-subtitle {
word-break: break-word;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
line-height: 16px;
/* fallback */
max-height: 32px;
/* fallback */
-webkit-line-clamp: 2;
/* number of lines to show */
-webkit-box-orient: vertical;
}
.episode-subtitle-long {
word-break: break-word;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
line-height: 16px;
/* fallback */
max-height: 72px;
/* fallback */
-webkit-line-clamp: 6;
/* number of lines to show */
-webkit-box-orient: vertical;
}
/* Padding for toastification toasts in the top right to not cover appbar/toolbar */ /* Padding for toastification toasts in the top right to not cover appbar/toolbar */
.app-bar-and-toolbar .Vue-Toastification__container.top-right { .app-bar-and-toolbar .Vue-Toastification__container.top-right {
padding-top: 104px; padding-top: 104px;

View File

@ -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>

View File

@ -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>

View File

@ -1,5 +1,6 @@
<template> <template>
<div class="sm:w-80 w-full relative"> <div class="">
<div class="w-full relative sm:w-80">
<form @submit.prevent="submitSearch"> <form @submit.prevent="submitSearch">
<ui-text-input ref="input" v-model="search" :placeholder="$strings.PlaceholderSearch" @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" /> <ui-text-input ref="input" v-model="search" :placeholder="$strings.PlaceholderSearch" @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" />
</form> </form>
@ -7,7 +8,8 @@
<span v-if="!search" class="material-icons" style="font-size: 1.2rem">search</span> <span v-if="!search" class="material-icons" style="font-size: 1.2rem">search</span>
<span v-else class="material-icons" style="font-size: 1.2rem">close</span> <span v-else class="material-icons" style="font-size: 1.2rem">close</span>
</div> </div>
<div v-show="showMenu && (lastSearch || isTyping)" class="absolute z-40 -mt-px w-40 sm:w-full bg-bg border border-black-200 shadow-lg rounded-md py-1 px-2 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm globalSearchMenu"> </div>
<div v-show="showMenu && (lastSearch || isTyping)" class="absolute z-40 -mt-px w-full max-w-64 sm:max-w-80 sm:w-80 bg-bg border border-black-200 shadow-lg rounded-md py-1 px-2 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm globalSearchMenu">
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label"> <ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<li v-if="isTyping" class="py-2 px-2"> <li v-if="isTyping" class="py-2 px-2">
<p>{{ $strings.MessageThinking }}</p> <p>{{ $strings.MessageThinking }}</p>

View File

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

View File

@ -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)
} }

View File

@ -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) {

View 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>

View File

@ -49,8 +49,8 @@
</div> </div>
<div v-if="media.coverPath"> <div v-if="media.coverPath">
<p class="text-center text-gray-200">Current</p> <p class="text-center text-gray-200">Current</p>
<a :href="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId, null, true)" target="_blank" class="bg-primary"> <a :href="$store.getters['globals/getLibraryItemCoverSrc'](libraryItem, null, true)" target="_blank" class="bg-primary">
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId, null, true)" :width="100" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrc'](libraryItem, null, true)" :width="100" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</a> </a>
</div> </div>
</div> </div>
@ -328,6 +328,17 @@ export default {
console.error('PersistProvider', error) console.error('PersistProvider', error)
} }
}, },
getDefaultBookProvider() {
let provider = localStorage.getItem('book-provider')
if (!provider) return 'google'
// Validate book provider
if (!this.$store.getters['scanners/checkBookProviderExists'](provider)) {
console.error('Stored book provider does not exist', provider)
localStorage.removeItem('book-provider')
return 'google'
}
return provider
},
getSearchQuery() { getSearchQuery() {
if (this.isPodcast) return `term=${encodeURIComponent(this.searchTitle)}` if (this.isPodcast) return `term=${encodeURIComponent(this.searchTitle)}`
var searchQuery = `provider=${this.provider}&fallbackTitleOnly=1&title=${encodeURIComponent(this.searchTitle)}` var searchQuery = `provider=${this.provider}&fallbackTitleOnly=1&title=${encodeURIComponent(this.searchTitle)}`
@ -434,7 +445,9 @@ export default {
this.searchTitle = this.libraryItem.media.metadata.title this.searchTitle = this.libraryItem.media.metadata.title
this.searchAuthor = this.libraryItem.media.metadata.authorName || '' this.searchAuthor = this.libraryItem.media.metadata.authorName || ''
if (this.isPodcast) this.provider = 'itunes' if (this.isPodcast) this.provider = 'itunes'
else this.provider = localStorage.getItem('book-provider') || 'google' else {
this.provider = this.getDefaultBookProvider()
}
// Prefer using ASIN if set and using audible provider // Prefer using ASIN if set and using audible provider
if (this.provider.startsWith('audible') && this.libraryItem.media.metadata.asin) { if (this.provider.startsWith('audible') && this.libraryItem.media.metadata.asin) {
@ -533,24 +546,11 @@ export default {
// Persist in local storage // Persist in local storage
localStorage.setItem('selectedMatchUsage', JSON.stringify(this.selectedMatchUsage)) localStorage.setItem('selectedMatchUsage', JSON.stringify(this.selectedMatchUsage))
if (Object.keys(updatePayload).length) {
if (updatePayload.metadata.cover) { if (updatePayload.metadata.cover) {
const coverPayload = { updatePayload.url = updatePayload.metadata.cover
url: updatePayload.metadata.cover
}
const success = await this.$axios.$post(`/api/items/${this.libraryItemId}/cover`, coverPayload).catch((error) => {
console.error('Failed to update', error)
return false
})
if (success) {
this.$toast.success(this.$strings.ToastItemCoverUpdateSuccess)
} else {
this.$toast.error(this.$strings.ToastItemCoverUpdateFailed)
}
console.log('Updated cover')
delete updatePayload.metadata.cover delete updatePayload.metadata.cover
} }
if (Object.keys(updatePayload).length) {
const mediaUpdatePayload = updatePayload const mediaUpdatePayload = updatePayload
const updateResult = await this.$axios.$patch(`/api/items/${this.libraryItemId}/media`, mediaUpdatePayload).catch((error) => { const updateResult = await this.$axios.$patch(`/api/items/${this.libraryItemId}/media`, mediaUpdatePayload).catch((error) => {
console.error('Failed to update', error) console.error('Failed to update', error)

View File

@ -20,7 +20,7 @@
</ui-tooltip> </ui-tooltip>
</div> </div>
<div v-if="enableAutoDownloadEpisodes" class="flex items-center py-2"> <div v-if="enableAutoDownloadEpisodes" class="flex items-center py-2">
<ui-text-input ref="maxEpisodesInput" type="number" v-model="newMaxNewEpisodesToDownload" no-spinner :padding-x="1" text-center class="w-10 text-base" @change="updateMaxNewEpisodesToDownload" /> <ui-text-input ref="maxEpisodesToDownloadInput" type="number" v-model="newMaxNewEpisodesToDownload" no-spinner :padding-x="1" text-center class="w-10 text-base" @change="updateMaxNewEpisodesToDownload" />
<ui-tooltip text="Value of 0 sets no max limit. When checking for new episodes this is the max number of episodes that will be downloaded."> <ui-tooltip text="Value of 0 sets no max limit. When checking for new episodes this is the max number of episodes that will be downloaded.">
<p class="pl-4 text-base"> <p class="pl-4 text-base">
Max new episodes to download per check Max new episodes to download per check
@ -129,9 +129,12 @@ export default {
return return
} }
} }
if (this.$refs.maxEpisodesInput && this.$refs.maxEpisodesInput.isFocused) {
if (this.$refs.maxEpisodesInput?.isFocused) {
this.$refs.maxEpisodesInput.blur() this.$refs.maxEpisodesInput.blur()
return }
if (this.$refs.maxEpisodesToDownloadInput?.isFocused) {
this.$refs.maxEpisodesToDownloadInput.blur()
} }
const updatePayload = { const updatePayload = {
@ -140,9 +143,11 @@ export default {
if (this.enableAutoDownloadEpisodes) { if (this.enableAutoDownloadEpisodes) {
updatePayload.autoDownloadSchedule = this.cronExpression updatePayload.autoDownloadSchedule = this.cronExpression
} }
this.newMaxEpisodesToKeep = Number(this.newMaxEpisodesToKeep)
if (this.newMaxEpisodesToKeep !== this.maxEpisodesToKeep) { if (this.newMaxEpisodesToKeep !== this.maxEpisodesToKeep) {
updatePayload.maxEpisodesToKeep = this.newMaxEpisodesToKeep updatePayload.maxEpisodesToKeep = this.newMaxEpisodesToKeep
} }
this.newMaxNewEpisodesToDownload = Number(this.newMaxNewEpisodesToDownload)
if (this.newMaxNewEpisodesToDownload !== this.maxNewEpisodesToDownload) { if (this.newMaxNewEpisodesToDownload !== this.maxNewEpisodesToDownload) {
updatePayload.maxNewEpisodesToDownload = this.newMaxNewEpisodesToDownload updatePayload.maxNewEpisodesToDownload = this.newMaxNewEpisodesToDownload
} }

View File

@ -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() {

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

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

View File

@ -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>

View File

@ -7,9 +7,10 @@
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<p class="hidden md:block text-xl font-semibold">{{ yearInReviewYear }} Year in Review</p> <p class="hidden md:block text-xl font-semibold">{{ $getString('HeaderYearReview', [yearInReviewYear]) }}</p>
<div class="hidden md:block flex-grow" /> <div class="hidden md:block flex-grow" />
<ui-btn class="w-full md:w-auto" @click.stop="clickShowYearInReview">{{ showYearInReview ? 'Hide Year in Review' : 'See Year in Review' }}</ui-btn> <ui-btn class="w-full md:w-auto" @click.stop="clickShowYearInReview">{{ showYearInReview ? $strings.LabelYearReviewHide :
$strings.LabelYearReviewShow }}</ui-btn>
</div> </div>
<!-- your year in review --> <!-- your year in review -->
@ -20,24 +21,27 @@
<!-- previous button --> <!-- previous button -->
<ui-btn small :disabled="!yearInReviewVariant || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant--"> <ui-btn small :disabled="!yearInReviewVariant || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant--">
<span class="material-icons text-lg sm:pr-1 py-px sm:py-0">chevron_left</span> <span class="material-icons text-lg sm:pr-1 py-px sm:py-0">chevron_left</span>
<span class="hidden sm:inline-block pr-2">Previous</span> <span class="hidden sm:inline-block pr-2">{{ $strings.ButtonPrevious }}</span>
</ui-btn> </ui-btn>
<!-- share button --> <!-- share button -->
<ui-btn v-if="showShareButton" small :disabled="processingYearInReview" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReview"> Share </ui-btn> <ui-btn v-if="showShareButton" small :disabled="processingYearInReview" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReview">{{
$strings.ButtonShare }}
</ui-btn>
<div class="flex-grow" /> <div class="flex-grow" />
<p class="hidden sm:block text-lg font-semibold">Your Year in Review ({{ yearInReviewVariant + 1 }})</p> <p class="hidden sm:block text-lg font-semibold">{{ $getString('LabelPersonalYearReview', [yearInReviewVariant + 1]) }}
</p>
<p class="block sm:hidden text-lg font-semibold">{{ yearInReviewVariant + 1 }}</p> <p class="block sm:hidden text-lg font-semibold">{{ yearInReviewVariant + 1 }}</p>
<div class="flex-grow" /> <div class="flex-grow" />
<!-- refresh button --> <!-- refresh button -->
<ui-btn small :disabled="processingYearInReview" class="inline-flex items-center font-semibold mr-1 sm:mr-2" @click="refreshYearInReview"> <ui-btn small :disabled="processingYearInReview" class="inline-flex items-center font-semibold mr-1 sm:mr-2" @click="refreshYearInReview">
<span class="hidden sm:inline-block">Refresh</span> <span class="hidden sm:inline-block">{{ $strings.ButtonRefresh }}</span>
<span class="material-icons sm:!hidden text-lg py-px">refresh</span> <span class="material-icons sm:!hidden text-lg py-px">refresh</span>
</ui-btn> </ui-btn>
<!-- next button --> <!-- next button -->
<ui-btn small :disabled="yearInReviewVariant >= 2 || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant++"> <ui-btn small :disabled="yearInReviewVariant >= 2 || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant++">
<span class="hidden sm:inline-block pl-2">Next</span> <span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span>
<span class="material-icons-outlined text-lg sm:pl-1 py-px sm:py-0">chevron_right</span> <span class="material-icons-outlined text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
</ui-btn> </ui-btn>
</div> </div>
@ -46,7 +50,7 @@
<!-- your year in review short --> <!-- your year in review short -->
<div class="w-full max-w-[800px] mx-auto my-4"> <div class="w-full max-w-[800px] mx-auto my-4">
<!-- share button --> <!-- share button -->
<ui-btn v-if="showShareButton" small :disabled="processingYearInReviewShort" class="inline-flex sm:hidden items-center font-semibold mb-1" @click="shareYearInReviewShort"> Share </ui-btn> <ui-btn v-if="showShareButton" small :disabled="processingYearInReviewShort" class="inline-flex sm:hidden items-center font-semibold mb-1" @click="shareYearInReviewShort">{{ $strings.ButtonShare }}</ui-btn>
<stats-year-in-review-short ref="yearInReviewShort" :year="yearInReviewYear" :processing.sync="processingYearInReviewShort" /> <stats-year-in-review-short ref="yearInReviewShort" :year="yearInReviewYear" :processing.sync="processingYearInReviewShort" />
</div> </div>
@ -56,24 +60,25 @@
<!-- previous button --> <!-- previous button -->
<ui-btn small :disabled="!yearInReviewServerVariant || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant--"> <ui-btn small :disabled="!yearInReviewServerVariant || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant--">
<span class="material-icons text-lg sm:pr-1 py-px sm:py-0">chevron_left</span> <span class="material-icons text-lg sm:pr-1 py-px sm:py-0">chevron_left</span>
<span class="hidden sm:inline-block pr-2">Previous</span> <span class="hidden sm:inline-block pr-2">{{ $strings.ButtonPrevious }}</span>
</ui-btn> </ui-btn>
<!-- share button --> <!-- share button -->
<ui-btn v-if="showShareButton" small :disabled="processingYearInReviewServer" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReviewServer"> Share </ui-btn> <ui-btn v-if="showShareButton" small :disabled="processingYearInReviewServer" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReviewServer">{{ $strings.ButtonShare }}
</ui-btn>
<div class="flex-grow" /> <div class="flex-grow" />
<p class="hidden sm:block text-lg font-semibold">Server Year in Review ({{ yearInReviewServerVariant + 1 }})</p> <p class="hidden sm:block text-lg font-semibold">{{ $getString('LabelServerYearReview', [yearInReviewServerVariant + 1]) }}</p>
<p class="block sm:hidden text-lg font-semibold">{{ yearInReviewServerVariant + 1 }}</p> <p class="block sm:hidden text-lg font-semibold">{{ yearInReviewServerVariant + 1 }}</p>
<div class="flex-grow" /> <div class="flex-grow" />
<!-- refresh button --> <!-- refresh button -->
<ui-btn small :disabled="processingYearInReviewServer" class="inline-flex items-center font-semibold mr-1 sm:mr-2" @click="refreshYearInReviewServer"> <ui-btn small :disabled="processingYearInReviewServer" class="inline-flex items-center font-semibold mr-1 sm:mr-2" @click="refreshYearInReviewServer">
<span class="hidden sm:inline-block">Refresh</span> <span class="hidden sm:inline-block">{{ $strings.ButtonRefresh }}</span>
<span class="material-icons sm:!hidden text-lg py-px">refresh</span> <span class="material-icons sm:!hidden text-lg py-px">refresh</span>
</ui-btn> </ui-btn>
<!-- next button --> <!-- next button -->
<ui-btn small :disabled="yearInReviewServerVariant >= 2 || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant++"> <ui-btn small :disabled="yearInReviewServerVariant >= 2 || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant++">
<span class="hidden sm:inline-block pl-2">Next</span> <span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span>
<span class="material-icons-outlined text-lg sm:pl-1 py-px sm:py-0">chevron_right</span> <span class="material-icons-outlined text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
</ui-btn> </ui-btn>
</div> </div>

View 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>

View File

@ -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">

View File

@ -11,7 +11,7 @@
</div> </div>
{{ item }} {{ item }}
</div> </div>
<input v-show="!readonly" ref="input" v-model="textInput" :disabled="disabled" style="min-width: 40px; width: 40px" class="h-full bg-primary focus:outline-none px-1" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" /> <input v-show="!readonly" ref="input" v-model="textInput" :disabled="disabled" style="min-width: 40px; width: fit-content" class="h-full bg-primary focus:outline-none px-1" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" />
</div> </div>
</form> </form>
@ -110,15 +110,6 @@ export default {
this.typingTimeout = setTimeout(() => { this.typingTimeout = setTimeout(() => {
this.currentSearch = this.textInput this.currentSearch = this.textInput
}, 100) }, 100)
this.setInputWidth()
},
setInputWidth() {
setTimeout(() => {
var value = this.$refs.input.value
var len = value.length * 7 + 24
this.$refs.input.style.width = len + 'px'
this.recalcMenuPos()
}, 50)
}, },
recalcMenuPos() { recalcMenuPos() {
if (!this.menu || !this.$refs.inputWrapper) return if (!this.menu || !this.$refs.inputWrapper) return

View File

@ -14,7 +14,7 @@
<div v-if="showEdit && !disabled" class="rounded-full cursor-pointer w-6 h-6 mx-0.5 bg-bg flex items-center justify-center"> <div v-if="showEdit && !disabled" class="rounded-full cursor-pointer w-6 h-6 mx-0.5 bg-bg flex items-center justify-center">
<span class="material-icons text-white hover:text-success pt-px pr-px" style="font-size: 1.1rem" @click.stop="addItem">add</span> <span class="material-icons text-white hover:text-success pt-px pr-px" style="font-size: 1.1rem" @click.stop="addItem">add</span>
</div> </div>
<input v-show="!readonly" ref="input" v-model="textInput" :disabled="disabled" style="min-width: 40px; width: 40px" class="h-full bg-primary focus:outline-none px-1" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" /> <input v-show="!readonly" ref="input" v-model="textInput" :disabled="disabled" style="min-width: 40px; width: fit-content" class="h-full bg-primary focus:outline-none px-1" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" />
</div> </div>
</form> </form>
@ -127,15 +127,6 @@ export default {
this.typingTimeout = setTimeout(() => { this.typingTimeout = setTimeout(() => {
this.search() this.search()
}, 250) }, 250)
this.setInputWidth()
},
setInputWidth() {
setTimeout(() => {
var value = this.$refs.input.value
var len = value.length * 7 + 24
this.$refs.input.style.width = len + 'px'
this.recalcMenuPos()
}, 50)
}, },
recalcMenuPos() { recalcMenuPos() {
if (!this.menu || !this.$refs.inputWrapper) return if (!this.menu || !this.$refs.inputWrapper) return

View File

@ -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')

View File

@ -29,7 +29,8 @@ module.exports = {
], ],
script: [], script: [],
link: [ link: [
{ rel: 'icon', type: 'image/x-icon', href: (process.env.ROUTER_BASE_PATH || '') + '/favicon.ico' } { rel: 'icon', type: 'image/x-icon', href: (process.env.ROUTER_BASE_PATH || '') + '/favicon.ico' },
{ rel: 'apple-touch-icon', href: (process.env.ROUTER_BASE_PATH || '') + '/ios_icon.png' }
] ]
}, },
@ -95,7 +96,7 @@ module.exports = {
meta: { meta: {
appleStatusBarStyle: 'black', appleStatusBarStyle: 'black',
name: 'Audiobookshelf', name: 'Audiobookshelf',
theme_color: '#373838', theme_color: '#232323',
mobileAppIOS: true, mobileAppIOS: true,
nativeUI: true nativeUI: true
}, },
@ -103,7 +104,7 @@ module.exports = {
name: 'Audiobookshelf', name: 'Audiobookshelf',
short_name: 'Audiobookshelf', short_name: 'Audiobookshelf',
display: 'standalone', display: 'standalone',
background_color: '#373838', background_color: '#232323',
icons: [ icons: [
{ {
src: (process.env.ROUTER_BASE_PATH || '') + '/icon.svg', src: (process.env.ROUTER_BASE_PATH || '') + '/icon.svg',

View File

@ -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",

View File

@ -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",

View File

@ -82,19 +82,33 @@ export default {
this.$setLanguageCode(lang) this.$setLanguageCode(lang)
}, },
logout() { logout() {
var rootSocket = this.$root.socket || {} // Disconnect from socket
const logoutPayload = { if (this.$root.socket) {
socketId: rootSocket.id console.log('Disconnecting from socket', this.$root.socket.id)
this.$root.socket.removeAllListeners()
this.$root.socket.disconnect()
} }
this.$axios.$post('/logout', logoutPayload).catch((error) => {
console.error(error)
})
if (localStorage.getItem('token')) { if (localStorage.getItem('token')) {
localStorage.removeItem('token') localStorage.removeItem('token')
} }
this.$store.commit('libraries/setUserPlaylists', []) this.$store.commit('libraries/setUserPlaylists', [])
this.$store.commit('libraries/setCollections', []) this.$store.commit('libraries/setCollections', [])
this.$axios
.$post('/logout')
.then((logoutPayload) => {
const redirect_url = logoutPayload.redirect_url
if (redirect_url) {
window.location.href = redirect_url
} else {
this.$router.push('/login') this.$router.push('/login')
}
})
.catch((error) => {
console.error(error)
})
}, },
resetForm() { resetForm() {
this.password = null this.password = null

View File

@ -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>

View File

@ -17,7 +17,10 @@
</div> </div>
<p v-if="author.description" class="text-white text-opacity-60 uppercase text-xs mb-2">{{ $strings.LabelDescription }}</p> <p v-if="author.description" class="text-white text-opacity-60 uppercase text-xs mb-2">{{ $strings.LabelDescription }}</p>
<p class="text-white max-w-3xl text-sm leading-5 whitespace-pre-wrap">{{ author.description }}</p> <p ref="description" id="author-description" class="text-white max-w-3xl text-base whitespace-pre-wrap" :class="{ 'show-full': showFullDescription }">{{ author.description }}</p>
<button v-if="isDescriptionClamped" class="py-0.5 flex items-center text-slate-300 hover:text-white" @click="showFullDescription = !showFullDescription">
{{ showFullDescription ? 'Read less' : 'Read more' }} <span class="material-icons text-xl pl-1">{{ showFullDescription ? 'expand_less' : 'expand_more' }}</span>
</button>
</div> </div>
</div> </div>
@ -62,7 +65,10 @@ export default {
} }
}, },
data() { data() {
return {} return {
isDescriptionClamped: false,
showFullDescription: false
}
}, },
computed: { computed: {
streamLibraryItem() { streamLibraryItem() {
@ -82,6 +88,10 @@ export default {
} }
}, },
methods: { methods: {
checkDescriptionClamped() {
if (!this.$refs.description) return
this.isDescriptionClamped = this.$refs.description.scrollHeight > this.$refs.description.clientHeight
},
editAuthor() { editAuthor() {
this.$store.commit('globals/showEditAuthorModal', this.author) this.$store.commit('globals/showEditAuthorModal', this.author)
}, },
@ -93,6 +103,7 @@ export default {
series: this.authorSeries, series: this.authorSeries,
libraryItems: this.libraryItems libraryItems: this.libraryItems
} }
this.$nextTick(this.checkDescriptionClamped)
} }
}, },
authorRemoved(author) { authorRemoved(author) {
@ -104,6 +115,7 @@ export default {
}, },
mounted() { mounted() {
if (!this.author) this.$router.replace('/') if (!this.author) this.$router.replace('/')
this.checkDescriptionClamped()
this.$root.socket.on('author_updated', this.authorUpdated) this.$root.socket.on('author_updated', this.authorUpdated)
this.$root.socket.on('author_removed', this.authorRemoved) this.$root.socket.on('author_removed', this.authorRemoved)
@ -114,3 +126,18 @@ export default {
} }
} }
</script> </script>
<style scoped>
#author-description {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 4;
max-height: 6.25rem;
transition: all 0.3s ease-in-out;
}
#author-description.show-full {
-webkit-line-clamp: unset;
max-height: 999rem;
}
</style>

View File

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

View File

@ -206,7 +206,7 @@ export default {
function isValidRedirectURI(uri) { function isValidRedirectURI(uri) {
// Check for somestring://someother/string // Check for somestring://someother/string
const pattern = new RegExp('^\\w+://[\\w\\.-]+$', 'i') const pattern = new RegExp('^\\w+://[\\w\\.-]+(/[\\w\\./-]*)*$', 'i')
return pattern.test(uri) return pattern.test(uri)
} }

View File

@ -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>

View File

@ -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>

View File

@ -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);
} }

View File

@ -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">

View File

@ -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>

View File

@ -8,11 +8,11 @@
<p v-if="!recentEpisodes.length && !processing" class="text-center text-xl">{{ $strings.MessageNoEpisodes }}</p> <p v-if="!recentEpisodes.length && !processing" class="text-center text-xl">{{ $strings.MessageNoEpisodes }}</p>
<template v-for="(episode, index) in episodesMapped"> <template v-for="(episode, index) in episodesMapped">
<div :key="episode.id" class="flex py-5 cursor-pointer relative" @click.stop="clickEpisode(episode)"> <div :key="episode.id" class="flex py-5 cursor-pointer relative" @click.stop="clickEpisode(episode)">
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](episode.libraryItemId)" :width="96" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" class="hidden md:block" /> <covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](episode.libraryItemId, episode.updatedAt)" :width="96" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" class="hidden md:block" />
<div class="flex-grow pl-4 max-w-2xl"> <div class="flex-grow pl-4 max-w-2xl">
<!-- mobile --> <!-- mobile -->
<div class="flex md:hidden mb-2"> <div class="flex md:hidden mb-2">
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](episode.libraryItemId)" :width="48" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" class="md:hidden" /> <covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](episode.libraryItemId, episode.updatedAt)" :width="48" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" class="md:hidden" />
<div class="flex-grow px-2"> <div class="flex-grow px-2">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex" @click.stop> <div class="flex" @click.stop>
@ -45,7 +45,7 @@
<widgets-podcast-type-indicator :type="episode.episodeType" /> <widgets-podcast-type-indicator :type="episode.episodeType" />
</div> </div>
<p class="text-sm text-gray-200 mb-4 episode-subtitle-long" v-html="episode.subtitle || episode.description" /> <p class="text-sm text-gray-200 mb-4 line-clamp-4" v-html="episode.subtitle || episode.description" />
<div class="flex items-center"> <div class="flex items-center">
<button class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer focus:outline-none" :class="episode.progress && episode.progress.isFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick(episode)"> <button class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer focus:outline-none" :class="episode.progress && episode.progress.isFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick(episode)">

View File

@ -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 []
}) })

View File

@ -10,9 +10,9 @@
<form @submit.prevent="submitServerSetup"> <form @submit.prevent="submitServerSetup">
<p class="text-lg font-semibold mb-2 pl-1 text-center">Create Root User</p> <p class="text-lg font-semibold mb-2 pl-1 text-center">Create Root User</p>
<ui-text-input-with-label v-model="newRoot.username" label="Username" :disabled="processing" class="w-full mb-3 text-sm" /> <ui-text-input-with-label v-model.trim="newRoot.username" label="Username" :disabled="processing" class="w-full mb-3 text-sm" />
<ui-text-input-with-label v-model="newRoot.password" label="Password" type="password" :disabled="processing" class="w-full mb-3 text-sm" /> <ui-text-input-with-label v-model.trim="newRoot.password" label="Password" type="password" :disabled="processing" class="w-full mb-3 text-sm" />
<ui-text-input-with-label v-model="confirmPassword" label="Confirm Password" type="password" :disabled="processing" class="w-full mb-3 text-sm" /> <ui-text-input-with-label v-model.trim="confirmPassword" label="Confirm Password" type="password" :disabled="processing" class="w-full mb-3 text-sm" />
<p class="text-lg font-semibold mt-6 mb-2 pl-1 text-center">Directory Paths</p> <p class="text-lg font-semibold mt-6 mb-2 pl-1 text-center">Directory Paths</p>
<ui-text-input-with-label v-model="ConfigPath" label="Config Path" disabled class="w-full mb-3 text-sm" /> <ui-text-input-with-label v-model="ConfigPath" label="Config Path" disabled class="w-full mb-3 text-sm" />
@ -34,10 +34,10 @@
<form v-show="login_local" @submit.prevent="submitForm"> <form v-show="login_local" @submit.prevent="submitForm">
<label class="text-xs text-gray-300 uppercase">{{ $strings.LabelUsername }}</label> <label class="text-xs text-gray-300 uppercase">{{ $strings.LabelUsername }}</label>
<ui-text-input v-model="username" :disabled="processing" class="mb-3 w-full" /> <ui-text-input v-model.trim="username" :disabled="processing" class="mb-3 w-full" />
<label class="text-xs text-gray-300 uppercase">{{ $strings.LabelPassword }}</label> <label class="text-xs text-gray-300 uppercase">{{ $strings.LabelPassword }}</label>
<ui-text-input v-model="password" type="password" :disabled="processing" class="w-full mb-3" /> <ui-text-input v-model.trim="password" type="password" :disabled="processing" class="w-full mb-3" />
<div class="w-full flex justify-end py-3"> <div class="w-full flex justify-end py-3">
<ui-btn type="submit" :disabled="processing" color="primary" class="leading-none">{{ processing ? 'Checking...' : $strings.ButtonSubmit }}</ui-btn> <ui-btn type="submit" :disabled="processing" color="primary" class="leading-none">{{ processing ? 'Checking...' : $strings.ButtonSubmit }}</ui-btn>
</div> </div>

View File

@ -10,13 +10,16 @@ const languageCodeMap = {
'de': { label: 'Deutsch', dateFnsLocale: 'de' }, 'de': { label: 'Deutsch', dateFnsLocale: 'de' },
'en-us': { label: 'English', dateFnsLocale: 'enUS' }, 'en-us': { label: 'English', dateFnsLocale: 'enUS' },
'es': { label: 'Español', dateFnsLocale: 'es' }, 'es': { label: 'Español', dateFnsLocale: 'es' },
'et': { label: 'Eesti', dateFnsLocale: 'et' },
'fr': { label: 'Français', dateFnsLocale: 'fr' }, 'fr': { label: 'Français', dateFnsLocale: 'fr' },
'hr': { label: 'Hrvatski', dateFnsLocale: 'hr' }, 'hr': { label: 'Hrvatski', dateFnsLocale: 'hr' },
'it': { label: 'Italiano', dateFnsLocale: 'it' }, 'it': { label: 'Italiano', dateFnsLocale: 'it' },
'lt': { label: 'Lietuvių', dateFnsLocale: 'lt' }, 'lt': { label: 'Lietuvių', dateFnsLocale: 'lt' },
'hu': { label: 'Magyar', dateFnsLocale: 'hu' },
'nl': { label: 'Nederlands', dateFnsLocale: 'nl' }, 'nl': { label: 'Nederlands', dateFnsLocale: 'nl' },
'no': { label: 'Norsk', dateFnsLocale: 'no' }, 'no': { label: 'Norsk', dateFnsLocale: 'no' },
'pl': { label: 'Polski', dateFnsLocale: 'pl' }, 'pl': { label: 'Polski', dateFnsLocale: 'pl' },
'pt-br': { label: 'Português (Brasil)', dateFnsLocale: 'ptBR' },
'ru': { label: 'Русский', dateFnsLocale: 'ru' }, 'ru': { label: 'Русский', dateFnsLocale: 'ru' },
'sv': { label: 'Svenska', dateFnsLocale: 'sv' }, 'sv': { label: 'Svenska', dateFnsLocale: 'sv' },
'zh-cn': { label: '简体中文 (Simplified Chinese)', dateFnsLocale: 'zhCN' }, 'zh-cn': { label: '简体中文 (Simplified Chinese)', dateFnsLocale: 'zhCN' },
@ -28,6 +31,18 @@ Vue.prototype.$languageCodeOptions = Object.keys(languageCodeMap).map(code => {
} }
}) })
// iTunes search API uses ISO 3166 country codes: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
const podcastSearchRegionMap = {
'us': { label: 'United States' },
'cn': { label: '中国' }
}
Vue.prototype.$podcastSearchRegionOptions = Object.keys(podcastSearchRegionMap).map(code => {
return {
text: podcastSearchRegionMap[code].label,
value: code
}
})
Vue.prototype.$languageCodes = { Vue.prototype.$languageCodes = {
default: defaultCode, default: defaultCode,
current: defaultCode, current: defaultCode,
@ -83,7 +98,7 @@ async function loadi18n(code) {
Vue.prototype.$setDateFnsLocale(languageCodeMap[code].dateFnsLocale) Vue.prototype.$setDateFnsLocale(languageCodeMap[code].dateFnsLocale)
this.$eventBus.$emit('change-lang', code) this?.$eventBus?.$emit('change-lang', code)
return true return true
} }

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -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
}) })

View File

@ -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
}
}
}

View File

@ -32,6 +32,8 @@
"ButtonHide": "Skrýt", "ButtonHide": "Skrýt",
"ButtonHome": "Domů", "ButtonHome": "Domů",
"ButtonIssues": "Problémy", "ButtonIssues": "Problémy",
"ButtonJumpBackward": "Jump Backward",
"ButtonJumpForward": "Jump Forward",
"ButtonLatest": "Nejnovější", "ButtonLatest": "Nejnovější",
"ButtonLibrary": "Knihovna", "ButtonLibrary": "Knihovna",
"ButtonLogout": "Odhlásit", "ButtonLogout": "Odhlásit",
@ -41,12 +43,17 @@
"ButtonMatchAllAuthors": "Spárovat všechny autory", "ButtonMatchAllAuthors": "Spárovat všechny autory",
"ButtonMatchBooks": "Spárovat Knihy", "ButtonMatchBooks": "Spárovat Knihy",
"ButtonNevermind": "Nevadí", "ButtonNevermind": "Nevadí",
"ButtonNext": "Next",
"ButtonNextChapter": "Next Chapter",
"ButtonOk": "Ok", "ButtonOk": "Ok",
"ButtonOpenFeed": "Otevřít kanál", "ButtonOpenFeed": "Otevřít kanál",
"ButtonOpenManager": "Otevřít správce", "ButtonOpenManager": "Otevřít správce",
"ButtonPause": "Pause",
"ButtonPlay": "Přehrát", "ButtonPlay": "Přehrát",
"ButtonPlaying": "Hraje", "ButtonPlaying": "Hraje",
"ButtonPlaylists": "Seznamy skladeb", "ButtonPlaylists": "Seznamy skladeb",
"ButtonPrevious": "Previous",
"ButtonPreviousChapter": "Previous Chapter",
"ButtonPurgeAllCache": "Vyčistit veškerou mezipaměť", "ButtonPurgeAllCache": "Vyčistit veškerou mezipaměť",
"ButtonPurgeItemsCache": "Vyčistit mezipaměť položek", "ButtonPurgeItemsCache": "Vyčistit mezipaměť položek",
"ButtonPurgeMediaProgress": "Vyčistit průběh médií", "ButtonPurgeMediaProgress": "Vyčistit průběh médií",
@ -54,6 +61,7 @@
"ButtonQueueRemoveItem": "Odstranit z fronty", "ButtonQueueRemoveItem": "Odstranit z fronty",
"ButtonQuickMatch": "Rychlé přiřazení", "ButtonQuickMatch": "Rychlé přiřazení",
"ButtonRead": "Číst", "ButtonRead": "Číst",
"ButtonRefresh": "Refresh",
"ButtonRemove": "Odstranit", "ButtonRemove": "Odstranit",
"ButtonRemoveAll": "Odstranit vše", "ButtonRemoveAll": "Odstranit vše",
"ButtonRemoveAllLibraryItems": "Odstranit všechny položky knihovny", "ButtonRemoveAllLibraryItems": "Odstranit všechny položky knihovny",
@ -73,6 +81,7 @@
"ButtonSelectFolderPath": "Vybrat cestu ke složce", "ButtonSelectFolderPath": "Vybrat cestu ke složce",
"ButtonSeries": "Série", "ButtonSeries": "Série",
"ButtonSetChaptersFromTracks": "Nastavit kapitoly ze stop", "ButtonSetChaptersFromTracks": "Nastavit kapitoly ze stop",
"ButtonShare": "Share",
"ButtonShiftTimes": "Časy posunu", "ButtonShiftTimes": "Časy posunu",
"ButtonShow": "Zobrazit", "ButtonShow": "Zobrazit",
"ButtonStartM4BEncode": "Spustit kódování M4B", "ButtonStartM4BEncode": "Spustit kódování M4B",
@ -104,6 +113,7 @@
"HeaderCollectionItems": "Položky kolekce", "HeaderCollectionItems": "Položky kolekce",
"HeaderCover": "Obálka", "HeaderCover": "Obálka",
"HeaderCurrentDownloads": "Aktuální stahování", "HeaderCurrentDownloads": "Aktuální stahování",
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
"HeaderDetails": "Podrobnosti", "HeaderDetails": "Podrobnosti",
"HeaderDownloadQueue": "Fronta stahování", "HeaderDownloadQueue": "Fronta stahování",
"HeaderEbookFiles": "Soubory elektronických knih", "HeaderEbookFiles": "Soubory elektronických knih",
@ -174,6 +184,7 @@
"HeaderUpdateDetails": "Aktualizovat podrobnosti", "HeaderUpdateDetails": "Aktualizovat podrobnosti",
"HeaderUpdateLibrary": "Aktualizovat knihovnu", "HeaderUpdateLibrary": "Aktualizovat knihovnu",
"HeaderUsers": "Uživatelé", "HeaderUsers": "Uživatelé",
"HeaderYearReview": "Year {0} in Review",
"HeaderYourStats": "Vaše statistiky", "HeaderYourStats": "Vaše statistiky",
"LabelAbridged": "Zkráceno", "LabelAbridged": "Zkráceno",
"LabelAccountType": "Typ účtu", "LabelAccountType": "Typ účtu",
@ -345,7 +356,9 @@
"LabelMetaTags": "Metaznačky", "LabelMetaTags": "Metaznačky",
"LabelMinute": "Minuta", "LabelMinute": "Minuta",
"LabelMissing": "Chybějící", "LabelMissing": "Chybějící",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Chybějící díly", "LabelMissingParts": "Chybějící díly",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
"LabelMore": "Více", "LabelMore": "Více",
@ -385,11 +398,13 @@
"LabelPermissionsDownload": "Může stahovat", "LabelPermissionsDownload": "Může stahovat",
"LabelPermissionsUpdate": "Může aktualizovat", "LabelPermissionsUpdate": "Může aktualizovat",
"LabelPermissionsUpload": "Může nahrávat", "LabelPermissionsUpload": "Může nahrávat",
"LabelPersonalYearReview": "Your Year in Review ({0})",
"LabelPhotoPathURL": "Cesta k fotografii/URL", "LabelPhotoPathURL": "Cesta k fotografii/URL",
"LabelPlaylists": "Seznamy skladeb", "LabelPlaylists": "Seznamy skladeb",
"LabelPlayMethod": "Metoda přehrávání", "LabelPlayMethod": "Metoda přehrávání",
"LabelPodcast": "Podcast", "LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasty", "LabelPodcasts": "Podcasty",
"LabelPodcastSearchRegion": "Oblast vyhledávání podcastu",
"LabelPodcastType": "Typ podcastu", "LabelPodcastType": "Typ podcastu",
"LabelPort": "Port", "LabelPort": "Port",
"LabelPrefixesToIgnore": "Předpony, které se mají ignorovat (nerozlišují se malá a velká písmena)", "LabelPrefixesToIgnore": "Předpony, které se mají ignorovat (nerozlišují se malá a velká písmena)",
@ -429,6 +444,7 @@
"LabelSeries": "Série", "LabelSeries": "Série",
"LabelSeriesName": "Název série", "LabelSeriesName": "Název série",
"LabelSeriesProgress": "Průběh série", "LabelSeriesProgress": "Průběh série",
"LabelServerYearReview": "Server Year in Review ({0})",
"LabelSetEbookAsPrimary": "Nastavit jako primární", "LabelSetEbookAsPrimary": "Nastavit jako primární",
"LabelSetEbookAsSupplementary": "Nastavit jako doplňkové", "LabelSetEbookAsSupplementary": "Nastavit jako doplňkové",
"LabelSettingsAudiobooksOnly": "Pouze audioknihy", "LabelSettingsAudiobooksOnly": "Pouze audioknihy",
@ -545,6 +561,8 @@
"LabelViewQueue": "Zobrazit frontu přehrávače", "LabelViewQueue": "Zobrazit frontu přehrávače",
"LabelVolume": "Hlasitost", "LabelVolume": "Hlasitost",
"LabelWeekdaysToRun": "Dny v týdnu ke spuštění", "LabelWeekdaysToRun": "Dny v týdnu ke spuštění",
"LabelYearReviewHide": "Hide Year in Review",
"LabelYearReviewShow": "See Year in Review",
"LabelYourAudiobookDuration": "Doba trvání vaší audioknihy", "LabelYourAudiobookDuration": "Doba trvání vaší audioknihy",
"LabelYourBookmarks": "Vaše záložky", "LabelYourBookmarks": "Vaše záložky",
"LabelYourPlaylists": "Vaše seznamy přehrávání", "LabelYourPlaylists": "Vaše seznamy přehrávání",

View File

@ -32,6 +32,8 @@
"ButtonHide": "Skjul", "ButtonHide": "Skjul",
"ButtonHome": "Hjem", "ButtonHome": "Hjem",
"ButtonIssues": "Problemer", "ButtonIssues": "Problemer",
"ButtonJumpBackward": "Jump Backward",
"ButtonJumpForward": "Jump Forward",
"ButtonLatest": "Seneste", "ButtonLatest": "Seneste",
"ButtonLibrary": "Bibliotek", "ButtonLibrary": "Bibliotek",
"ButtonLogout": "Log ud", "ButtonLogout": "Log ud",
@ -41,12 +43,17 @@
"ButtonMatchAllAuthors": "Match alle forfattere", "ButtonMatchAllAuthors": "Match alle forfattere",
"ButtonMatchBooks": "Match bøger", "ButtonMatchBooks": "Match bøger",
"ButtonNevermind": "Glem det", "ButtonNevermind": "Glem det",
"ButtonNext": "Next",
"ButtonNextChapter": "Next Chapter",
"ButtonOk": "OK", "ButtonOk": "OK",
"ButtonOpenFeed": "Åbn feed", "ButtonOpenFeed": "Åbn feed",
"ButtonOpenManager": "Åbn manager", "ButtonOpenManager": "Åbn manager",
"ButtonPause": "Pause",
"ButtonPlay": "Afspil", "ButtonPlay": "Afspil",
"ButtonPlaying": "Afspiller", "ButtonPlaying": "Afspiller",
"ButtonPlaylists": "Afspilningslister", "ButtonPlaylists": "Afspilningslister",
"ButtonPrevious": "Previous",
"ButtonPreviousChapter": "Previous Chapter",
"ButtonPurgeAllCache": "Ryd al cache", "ButtonPurgeAllCache": "Ryd al cache",
"ButtonPurgeItemsCache": "Ryd elementcache", "ButtonPurgeItemsCache": "Ryd elementcache",
"ButtonPurgeMediaProgress": "Ryd Medieforløb", "ButtonPurgeMediaProgress": "Ryd Medieforløb",
@ -54,6 +61,7 @@
"ButtonQueueRemoveItem": "Fjern fra kø", "ButtonQueueRemoveItem": "Fjern fra kø",
"ButtonQuickMatch": "Hurtig Match", "ButtonQuickMatch": "Hurtig Match",
"ButtonRead": "Læs", "ButtonRead": "Læs",
"ButtonRefresh": "Refresh",
"ButtonRemove": "Fjern", "ButtonRemove": "Fjern",
"ButtonRemoveAll": "Fjern Alle", "ButtonRemoveAll": "Fjern Alle",
"ButtonRemoveAllLibraryItems": "Fjern Alle Bibliotekselementer", "ButtonRemoveAllLibraryItems": "Fjern Alle Bibliotekselementer",
@ -73,6 +81,7 @@
"ButtonSelectFolderPath": "Vælg Mappen Sti", "ButtonSelectFolderPath": "Vælg Mappen Sti",
"ButtonSeries": "Serie", "ButtonSeries": "Serie",
"ButtonSetChaptersFromTracks": "Sæt kapitler fra spor", "ButtonSetChaptersFromTracks": "Sæt kapitler fra spor",
"ButtonShare": "Share",
"ButtonShiftTimes": "Skift Tider", "ButtonShiftTimes": "Skift Tider",
"ButtonShow": "Vis", "ButtonShow": "Vis",
"ButtonStartM4BEncode": "Start M4B Kode", "ButtonStartM4BEncode": "Start M4B Kode",
@ -104,6 +113,7 @@
"HeaderCollectionItems": "Samlingselementer", "HeaderCollectionItems": "Samlingselementer",
"HeaderCover": "Omslag", "HeaderCover": "Omslag",
"HeaderCurrentDownloads": "Nuværende Downloads", "HeaderCurrentDownloads": "Nuværende Downloads",
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
"HeaderDetails": "Detaljer", "HeaderDetails": "Detaljer",
"HeaderDownloadQueue": "Download Kø", "HeaderDownloadQueue": "Download Kø",
"HeaderEbookFiles": "E-bogsfiler", "HeaderEbookFiles": "E-bogsfiler",
@ -174,6 +184,7 @@
"HeaderUpdateDetails": "Opdater Detaljer", "HeaderUpdateDetails": "Opdater Detaljer",
"HeaderUpdateLibrary": "Opdater Bibliotek", "HeaderUpdateLibrary": "Opdater Bibliotek",
"HeaderUsers": "Brugere", "HeaderUsers": "Brugere",
"HeaderYearReview": "Year {0} in Review",
"HeaderYourStats": "Dine Statistikker", "HeaderYourStats": "Dine Statistikker",
"LabelAbridged": "Abridged", "LabelAbridged": "Abridged",
"LabelAccountType": "Kontotype", "LabelAccountType": "Kontotype",
@ -345,7 +356,9 @@
"LabelMetaTags": "Meta-tags", "LabelMetaTags": "Meta-tags",
"LabelMinute": "Minut", "LabelMinute": "Minut",
"LabelMissing": "Mangler", "LabelMissing": "Mangler",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Manglende dele", "LabelMissingParts": "Manglende dele",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
"LabelMore": "Mere", "LabelMore": "Mere",
@ -385,11 +398,13 @@
"LabelPermissionsDownload": "Kan downloade", "LabelPermissionsDownload": "Kan downloade",
"LabelPermissionsUpdate": "Kan opdatere", "LabelPermissionsUpdate": "Kan opdatere",
"LabelPermissionsUpload": "Kan uploade", "LabelPermissionsUpload": "Kan uploade",
"LabelPersonalYearReview": "Your Year in Review ({0})",
"LabelPhotoPathURL": "Foto sti/URL", "LabelPhotoPathURL": "Foto sti/URL",
"LabelPlaylists": "Afspilningslister", "LabelPlaylists": "Afspilningslister",
"LabelPlayMethod": "Afspilningsmetode", "LabelPlayMethod": "Afspilningsmetode",
"LabelPodcast": "Podcast", "LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts", "LabelPodcasts": "Podcasts",
"LabelPodcastSearchRegion": "Podcast søgeområde",
"LabelPodcastType": "Podcast type", "LabelPodcastType": "Podcast type",
"LabelPort": "Port", "LabelPort": "Port",
"LabelPrefixesToIgnore": "Præfikser der skal ignoreres (skal ikke skelne mellem store og små bogstaver)", "LabelPrefixesToIgnore": "Præfikser der skal ignoreres (skal ikke skelne mellem store og små bogstaver)",
@ -429,6 +444,7 @@
"LabelSeries": "Serie", "LabelSeries": "Serie",
"LabelSeriesName": "Serienavn", "LabelSeriesName": "Serienavn",
"LabelSeriesProgress": "Seriefremskridt", "LabelSeriesProgress": "Seriefremskridt",
"LabelServerYearReview": "Server Year in Review ({0})",
"LabelSetEbookAsPrimary": "Indstil som primær", "LabelSetEbookAsPrimary": "Indstil som primær",
"LabelSetEbookAsSupplementary": "Indstil som supplerende", "LabelSetEbookAsSupplementary": "Indstil som supplerende",
"LabelSettingsAudiobooksOnly": "Kun lydbøger", "LabelSettingsAudiobooksOnly": "Kun lydbøger",
@ -545,6 +561,8 @@
"LabelViewQueue": "Se afspilningskø", "LabelViewQueue": "Se afspilningskø",
"LabelVolume": "Volumen", "LabelVolume": "Volumen",
"LabelWeekdaysToRun": "Ugedage til kørsel", "LabelWeekdaysToRun": "Ugedage til kørsel",
"LabelYearReviewHide": "Hide Year in Review",
"LabelYearReviewShow": "See Year in Review",
"LabelYourAudiobookDuration": "Din lydbogsvarighed", "LabelYourAudiobookDuration": "Din lydbogsvarighed",
"LabelYourBookmarks": "Dine bogmærker", "LabelYourBookmarks": "Dine bogmærker",
"LabelYourPlaylists": "Dine spillelister", "LabelYourPlaylists": "Dine spillelister",

View File

@ -32,6 +32,8 @@
"ButtonHide": "Ausblenden", "ButtonHide": "Ausblenden",
"ButtonHome": "Startseite", "ButtonHome": "Startseite",
"ButtonIssues": "Probleme", "ButtonIssues": "Probleme",
"ButtonJumpBackward": "Zurück springen",
"ButtonJumpForward": "Vorwärts springen",
"ButtonLatest": "Neuste", "ButtonLatest": "Neuste",
"ButtonLibrary": "Bibliothek", "ButtonLibrary": "Bibliothek",
"ButtonLogout": "Abmelden", "ButtonLogout": "Abmelden",
@ -41,12 +43,17 @@
"ButtonMatchAllAuthors": "Online Metadaten-Abgleich (alle Autoren)", "ButtonMatchAllAuthors": "Online Metadaten-Abgleich (alle Autoren)",
"ButtonMatchBooks": "Online Metadaten-Abgleich (alle Medien)", "ButtonMatchBooks": "Online Metadaten-Abgleich (alle Medien)",
"ButtonNevermind": "Abbrechen", "ButtonNevermind": "Abbrechen",
"ButtonNext": "Vor",
"ButtonNextChapter": "Nächstes Kapitel",
"ButtonOk": "Ok", "ButtonOk": "Ok",
"ButtonOpenFeed": "Feed öffnen", "ButtonOpenFeed": "Feed öffnen",
"ButtonOpenManager": "Manager öffnen", "ButtonOpenManager": "Manager öffnen",
"ButtonPause": "Pause",
"ButtonPlay": "Abspielen", "ButtonPlay": "Abspielen",
"ButtonPlaying": "Spielt", "ButtonPlaying": "Spielt",
"ButtonPlaylists": "Wiedergabelisten", "ButtonPlaylists": "Wiedergabelisten",
"ButtonPrevious": "Zurück",
"ButtonPreviousChapter": "Vorheriges Kapitel",
"ButtonPurgeAllCache": "Cache leeren", "ButtonPurgeAllCache": "Cache leeren",
"ButtonPurgeItemsCache": "Lösche Medien-Cache", "ButtonPurgeItemsCache": "Lösche Medien-Cache",
"ButtonPurgeMediaProgress": "Lösche Hörfortschritte", "ButtonPurgeMediaProgress": "Lösche Hörfortschritte",
@ -54,6 +61,7 @@
"ButtonQueueRemoveItem": "Aus der Warteschlange entfernen", "ButtonQueueRemoveItem": "Aus der Warteschlange entfernen",
"ButtonQuickMatch": "Schnellabgleich", "ButtonQuickMatch": "Schnellabgleich",
"ButtonRead": "Lesen", "ButtonRead": "Lesen",
"ButtonRefresh": "Neu Laden",
"ButtonRemove": "Löschen", "ButtonRemove": "Löschen",
"ButtonRemoveAll": "Alles löschen", "ButtonRemoveAll": "Alles löschen",
"ButtonRemoveAllLibraryItems": "Lösche alle Bibliothekseinträge", "ButtonRemoveAllLibraryItems": "Lösche alle Bibliothekseinträge",
@ -73,6 +81,7 @@
"ButtonSelectFolderPath": "Ordnerpfad auswählen", "ButtonSelectFolderPath": "Ordnerpfad auswählen",
"ButtonSeries": "Serien", "ButtonSeries": "Serien",
"ButtonSetChaptersFromTracks": "Kapitelerstellung aus Audiodateien", "ButtonSetChaptersFromTracks": "Kapitelerstellung aus Audiodateien",
"ButtonShare": "Teilen",
"ButtonShiftTimes": "Zeitverschiebung", "ButtonShiftTimes": "Zeitverschiebung",
"ButtonShow": "Anzeigen", "ButtonShow": "Anzeigen",
"ButtonStartM4BEncode": "M4B-Kodierung starten", "ButtonStartM4BEncode": "M4B-Kodierung starten",
@ -104,6 +113,7 @@
"HeaderCollectionItems": "Sammlungseinträge", "HeaderCollectionItems": "Sammlungseinträge",
"HeaderCover": "Titelbild", "HeaderCover": "Titelbild",
"HeaderCurrentDownloads": "Aktuelle Downloads", "HeaderCurrentDownloads": "Aktuelle Downloads",
"HeaderCustomMetadataProviders": "Benutzerdefinierte Metadata Anbieter",
"HeaderDetails": "Details", "HeaderDetails": "Details",
"HeaderDownloadQueue": "Download Warteschlange", "HeaderDownloadQueue": "Download Warteschlange",
"HeaderEbookFiles": "E-Book Dateien", "HeaderEbookFiles": "E-Book Dateien",
@ -174,6 +184,7 @@
"HeaderUpdateDetails": "Details aktualisieren", "HeaderUpdateDetails": "Details aktualisieren",
"HeaderUpdateLibrary": "Bibliothek aktualisieren", "HeaderUpdateLibrary": "Bibliothek aktualisieren",
"HeaderUsers": "Benutzer", "HeaderUsers": "Benutzer",
"HeaderYearReview": "Jahr {0} in Übersicht",
"HeaderYourStats": "Eigene Statistiken", "HeaderYourStats": "Eigene Statistiken",
"LabelAbridged": "Gekürzt", "LabelAbridged": "Gekürzt",
"LabelAccountType": "Kontoart", "LabelAccountType": "Kontoart",
@ -281,11 +292,11 @@
"LabelFinished": "Beendet", "LabelFinished": "Beendet",
"LabelFolder": "Ordner", "LabelFolder": "Ordner",
"LabelFolders": "Verzeichnisse", "LabelFolders": "Verzeichnisse",
"LabelFontBold": "Bold", "LabelFontBold": "Fett",
"LabelFontFamily": "Schriftfamilie", "LabelFontFamily": "Schriftfamilie",
"LabelFontItalic": "Italic", "LabelFontItalic": "Kursiv",
"LabelFontScale": "Schriftgröße", "LabelFontScale": "Schriftgröße",
"LabelFontStrikethrough": "Strikethrough", "LabelFontStrikethrough": "Durchgestrichen",
"LabelFormat": "Format", "LabelFormat": "Format",
"LabelGenre": "Kategorie", "LabelGenre": "Kategorie",
"LabelGenres": "Kategorien", "LabelGenres": "Kategorien",
@ -345,7 +356,9 @@
"LabelMetaTags": "Meta Tags", "LabelMetaTags": "Meta Tags",
"LabelMinute": "Minute", "LabelMinute": "Minute",
"LabelMissing": "Fehlend", "LabelMissing": "Fehlend",
"LabelMissingEbook": "E-Book fehlt",
"LabelMissingParts": "Fehlende Teile", "LabelMissingParts": "Fehlende Teile",
"LabelMissingSupplementaryEbook": "Ergänzendes E-Book fehlt",
"LabelMobileRedirectURIs": "Erlaubte Weiterleitungs-URIs für die mobile App", "LabelMobileRedirectURIs": "Erlaubte Weiterleitungs-URIs für die mobile App",
"LabelMobileRedirectURIsDescription": "Dies ist eine Whitelist gültiger Umleitungs-URIs für mobile Apps. Der Standardwert ist <code>audiobookshelf://oauth</code>, den du entfernen oder durch zusätzliche URIs für die Integration von Drittanbieter-Apps ergänzen kannst. Die Verwendung eines Sternchens (<code>*</code>) als alleiniger Eintrag erlaubt jede URI.", "LabelMobileRedirectURIsDescription": "Dies ist eine Whitelist gültiger Umleitungs-URIs für mobile Apps. Der Standardwert ist <code>audiobookshelf://oauth</code>, den du entfernen oder durch zusätzliche URIs für die Integration von Drittanbieter-Apps ergänzen kannst. Die Verwendung eines Sternchens (<code>*</code>) als alleiniger Eintrag erlaubt jede URI.",
"LabelMore": "Mehr", "LabelMore": "Mehr",
@ -385,11 +398,13 @@
"LabelPermissionsDownload": "Herunterladen", "LabelPermissionsDownload": "Herunterladen",
"LabelPermissionsUpdate": "Aktualisieren", "LabelPermissionsUpdate": "Aktualisieren",
"LabelPermissionsUpload": "Hochladen", "LabelPermissionsUpload": "Hochladen",
"LabelPersonalYearReview": "Dein Jahr in Übersicht ({0})",
"LabelPhotoPathURL": "Foto Pfad/URL", "LabelPhotoPathURL": "Foto Pfad/URL",
"LabelPlaylists": "Wiedergabelisten", "LabelPlaylists": "Wiedergabelisten",
"LabelPlayMethod": "Abspielmethode", "LabelPlayMethod": "Abspielmethode",
"LabelPodcast": "Podcast", "LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts", "LabelPodcasts": "Podcasts",
"LabelPodcastSearchRegion": "Podcast-Suchregion",
"LabelPodcastType": "Podcast Typ", "LabelPodcastType": "Podcast Typ",
"LabelPort": "Port", "LabelPort": "Port",
"LabelPrefixesToIgnore": "Zu ignorierende(s) Vorwort(e) (Groß- und Kleinschreibung wird nicht berücksichtigt)", "LabelPrefixesToIgnore": "Zu ignorierende(s) Vorwort(e) (Groß- und Kleinschreibung wird nicht berücksichtigt)",
@ -406,7 +421,7 @@
"LabelRecentlyAdded": "Kürzlich hinzugefügt", "LabelRecentlyAdded": "Kürzlich hinzugefügt",
"LabelRecentSeries": "Aktuelle Serien", "LabelRecentSeries": "Aktuelle Serien",
"LabelRecommended": "Empfohlen", "LabelRecommended": "Empfohlen",
"LabelRedo": "Redo", "LabelRedo": "Wiederholen",
"LabelRegion": "Region", "LabelRegion": "Region",
"LabelReleaseDate": "Veröffentlichungsdatum", "LabelReleaseDate": "Veröffentlichungsdatum",
"LabelRemoveCover": "Lösche Titelbild", "LabelRemoveCover": "Lösche Titelbild",
@ -429,6 +444,7 @@
"LabelSeries": "Serien", "LabelSeries": "Serien",
"LabelSeriesName": "Serienname", "LabelSeriesName": "Serienname",
"LabelSeriesProgress": "Serienfortschritt", "LabelSeriesProgress": "Serienfortschritt",
"LabelServerYearReview": "Server Jahr in Übersicht ({0})",
"LabelSetEbookAsPrimary": "Als Hauptbuch setzen", "LabelSetEbookAsPrimary": "Als Hauptbuch setzen",
"LabelSetEbookAsSupplementary": "Als Ergänzung setzen", "LabelSetEbookAsSupplementary": "Als Ergänzung setzen",
"LabelSettingsAudiobooksOnly": "Nur Hörbücher", "LabelSettingsAudiobooksOnly": "Nur Hörbücher",
@ -495,10 +511,10 @@
"LabelTagsAccessibleToUser": "Für Benutzer zugängliche Schlagwörter", "LabelTagsAccessibleToUser": "Für Benutzer zugängliche Schlagwörter",
"LabelTagsNotAccessibleToUser": "Für Benutzer nicht zugängliche Schlagwörter", "LabelTagsNotAccessibleToUser": "Für Benutzer nicht zugängliche Schlagwörter",
"LabelTasks": "Laufende Aufgaben", "LabelTasks": "Laufende Aufgaben",
"LabelTextEditorBulletedList": "Bulleted list", "LabelTextEditorBulletedList": "Aufzählungsliste",
"LabelTextEditorLink": "Link", "LabelTextEditorLink": "Link",
"LabelTextEditorNumberedList": "Numbered list", "LabelTextEditorNumberedList": "nummerierte Liste",
"LabelTextEditorUnlink": "Unlink", "LabelTextEditorUnlink": "entkoppeln",
"LabelTheme": "Theme", "LabelTheme": "Theme",
"LabelThemeDark": "Dunkel", "LabelThemeDark": "Dunkel",
"LabelThemeLight": "Hell", "LabelThemeLight": "Hell",
@ -524,7 +540,7 @@
"LabelTracksSingleTrack": "Einzeldatei", "LabelTracksSingleTrack": "Einzeldatei",
"LabelType": "Typ", "LabelType": "Typ",
"LabelUnabridged": "Ungekürzt", "LabelUnabridged": "Ungekürzt",
"LabelUndo": "Undo", "LabelUndo": "Rückgängig machen",
"LabelUnknown": "Unbekannt", "LabelUnknown": "Unbekannt",
"LabelUpdateCover": "Titelbild aktualisieren", "LabelUpdateCover": "Titelbild aktualisieren",
"LabelUpdateCoverHelp": "Erlaube das Überschreiben bestehender Titelbilder für die ausgewählten Hörbücher, wenn eine Übereinstimmung gefunden wird", "LabelUpdateCoverHelp": "Erlaube das Überschreiben bestehender Titelbilder für die ausgewählten Hörbücher, wenn eine Übereinstimmung gefunden wird",
@ -545,6 +561,8 @@
"LabelViewQueue": "Player-Warteschlange anzeigen", "LabelViewQueue": "Player-Warteschlange anzeigen",
"LabelVolume": "Lautstärke", "LabelVolume": "Lautstärke",
"LabelWeekdaysToRun": "Wochentage für die Ausführung", "LabelWeekdaysToRun": "Wochentage für die Ausführung",
"LabelYearReviewHide": "Verstecke Jahr in Übersicht",
"LabelYearReviewShow": "Zeige Jahr in Übersicht",
"LabelYourAudiobookDuration": "Laufzeit deines Mediums", "LabelYourAudiobookDuration": "Laufzeit deines Mediums",
"LabelYourBookmarks": "Lesezeichen", "LabelYourBookmarks": "Lesezeichen",
"LabelYourPlaylists": "Eigene Wiedergabelisten", "LabelYourPlaylists": "Eigene Wiedergabelisten",

View File

@ -32,6 +32,8 @@
"ButtonHide": "Hide", "ButtonHide": "Hide",
"ButtonHome": "Home", "ButtonHome": "Home",
"ButtonIssues": "Issues", "ButtonIssues": "Issues",
"ButtonJumpBackward": "Jump Backward",
"ButtonJumpForward": "Jump Forward",
"ButtonLatest": "Latest", "ButtonLatest": "Latest",
"ButtonLibrary": "Library", "ButtonLibrary": "Library",
"ButtonLogout": "Logout", "ButtonLogout": "Logout",
@ -41,12 +43,17 @@
"ButtonMatchAllAuthors": "Match All Authors", "ButtonMatchAllAuthors": "Match All Authors",
"ButtonMatchBooks": "Match Books", "ButtonMatchBooks": "Match Books",
"ButtonNevermind": "Nevermind", "ButtonNevermind": "Nevermind",
"ButtonNext": "Next",
"ButtonNextChapter": "Next Chapter",
"ButtonOk": "Ok", "ButtonOk": "Ok",
"ButtonOpenFeed": "Open Feed", "ButtonOpenFeed": "Open Feed",
"ButtonOpenManager": "Open Manager", "ButtonOpenManager": "Open Manager",
"ButtonPause": "Pause",
"ButtonPlay": "Play", "ButtonPlay": "Play",
"ButtonPlaying": "Playing", "ButtonPlaying": "Playing",
"ButtonPlaylists": "Playlists", "ButtonPlaylists": "Playlists",
"ButtonPrevious": "Previous",
"ButtonPreviousChapter": "Previous Chapter",
"ButtonPurgeAllCache": "Purge All Cache", "ButtonPurgeAllCache": "Purge All Cache",
"ButtonPurgeItemsCache": "Purge Items Cache", "ButtonPurgeItemsCache": "Purge Items Cache",
"ButtonPurgeMediaProgress": "Purge Media Progress", "ButtonPurgeMediaProgress": "Purge Media Progress",
@ -54,6 +61,7 @@
"ButtonQueueRemoveItem": "Remove from queue", "ButtonQueueRemoveItem": "Remove from queue",
"ButtonQuickMatch": "Quick Match", "ButtonQuickMatch": "Quick Match",
"ButtonRead": "Read", "ButtonRead": "Read",
"ButtonRefresh": "Refresh",
"ButtonRemove": "Remove", "ButtonRemove": "Remove",
"ButtonRemoveAll": "Remove All", "ButtonRemoveAll": "Remove All",
"ButtonRemoveAllLibraryItems": "Remove All Library Items", "ButtonRemoveAllLibraryItems": "Remove All Library Items",
@ -73,6 +81,7 @@
"ButtonSelectFolderPath": "Select Folder Path", "ButtonSelectFolderPath": "Select Folder Path",
"ButtonSeries": "Series", "ButtonSeries": "Series",
"ButtonSetChaptersFromTracks": "Set chapters from tracks", "ButtonSetChaptersFromTracks": "Set chapters from tracks",
"ButtonShare": "Share",
"ButtonShiftTimes": "Shift Times", "ButtonShiftTimes": "Shift Times",
"ButtonShow": "Show", "ButtonShow": "Show",
"ButtonStartM4BEncode": "Start M4B Encode", "ButtonStartM4BEncode": "Start M4B Encode",
@ -104,6 +113,7 @@
"HeaderCollectionItems": "Collection Items", "HeaderCollectionItems": "Collection Items",
"HeaderCover": "Cover", "HeaderCover": "Cover",
"HeaderCurrentDownloads": "Current Downloads", "HeaderCurrentDownloads": "Current Downloads",
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
"HeaderDetails": "Details", "HeaderDetails": "Details",
"HeaderDownloadQueue": "Download Queue", "HeaderDownloadQueue": "Download Queue",
"HeaderEbookFiles": "Ebook Files", "HeaderEbookFiles": "Ebook Files",
@ -174,6 +184,7 @@
"HeaderUpdateDetails": "Update Details", "HeaderUpdateDetails": "Update Details",
"HeaderUpdateLibrary": "Update Library", "HeaderUpdateLibrary": "Update Library",
"HeaderUsers": "Users", "HeaderUsers": "Users",
"HeaderYearReview": "Year {0} in Review",
"HeaderYourStats": "Your Stats", "HeaderYourStats": "Your Stats",
"LabelAbridged": "Abridged", "LabelAbridged": "Abridged",
"LabelAccountType": "Account Type", "LabelAccountType": "Account Type",
@ -345,7 +356,9 @@
"LabelMetaTags": "Meta Tags", "LabelMetaTags": "Meta Tags",
"LabelMinute": "Minute", "LabelMinute": "Minute",
"LabelMissing": "Missing", "LabelMissing": "Missing",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Missing Parts", "LabelMissingParts": "Missing Parts",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
"LabelMore": "More", "LabelMore": "More",
@ -385,11 +398,13 @@
"LabelPermissionsDownload": "Can Download", "LabelPermissionsDownload": "Can Download",
"LabelPermissionsUpdate": "Can Update", "LabelPermissionsUpdate": "Can Update",
"LabelPermissionsUpload": "Can Upload", "LabelPermissionsUpload": "Can Upload",
"LabelPersonalYearReview": "Your Year in Review ({0})",
"LabelPhotoPathURL": "Photo Path/URL", "LabelPhotoPathURL": "Photo Path/URL",
"LabelPlaylists": "Playlists", "LabelPlaylists": "Playlists",
"LabelPlayMethod": "Play Method", "LabelPlayMethod": "Play Method",
"LabelPodcast": "Podcast", "LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts", "LabelPodcasts": "Podcasts",
"LabelPodcastSearchRegion": "Podcast search region",
"LabelPodcastType": "Podcast Type", "LabelPodcastType": "Podcast Type",
"LabelPort": "Port", "LabelPort": "Port",
"LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)", "LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)",
@ -429,6 +444,7 @@
"LabelSeries": "Series", "LabelSeries": "Series",
"LabelSeriesName": "Series Name", "LabelSeriesName": "Series Name",
"LabelSeriesProgress": "Series Progress", "LabelSeriesProgress": "Series Progress",
"LabelServerYearReview": "Server Year in Review ({0})",
"LabelSetEbookAsPrimary": "Set as primary", "LabelSetEbookAsPrimary": "Set as primary",
"LabelSetEbookAsSupplementary": "Set as supplementary", "LabelSetEbookAsSupplementary": "Set as supplementary",
"LabelSettingsAudiobooksOnly": "Audiobooks only", "LabelSettingsAudiobooksOnly": "Audiobooks only",
@ -545,6 +561,8 @@
"LabelViewQueue": "View player queue", "LabelViewQueue": "View player queue",
"LabelVolume": "Volume", "LabelVolume": "Volume",
"LabelWeekdaysToRun": "Weekdays to run", "LabelWeekdaysToRun": "Weekdays to run",
"LabelYearReviewHide": "Hide Year in Review",
"LabelYearReviewShow": "See Year in Review",
"LabelYourAudiobookDuration": "Your audiobook duration", "LabelYourAudiobookDuration": "Your audiobook duration",
"LabelYourBookmarks": "Your Bookmarks", "LabelYourBookmarks": "Your Bookmarks",
"LabelYourPlaylists": "Your Playlists", "LabelYourPlaylists": "Your Playlists",

View File

@ -1,11 +1,11 @@
{ {
"ButtonAdd": "Agregar", "ButtonAdd": "Agregar",
"ButtonAddChapters": "Agregar Capitulo", "ButtonAddChapters": "Agregar Capitulo",
"ButtonAddDevice": "Add Device", "ButtonAddDevice": "Agregar Dispositivo",
"ButtonAddLibrary": "Add Library", "ButtonAddLibrary": "Crear Biblioteca",
"ButtonAddPodcasts": "Agregar Podcasts", "ButtonAddPodcasts": "Agregar Podcasts",
"ButtonAddUser": "Add User", "ButtonAddUser": "Crear Usuario",
"ButtonAddYourFirstLibrary": "Agrega tu Primera Biblioteca", "ButtonAddYourFirstLibrary": "Crea tu Primera Biblioteca",
"ButtonApply": "Aplicar", "ButtonApply": "Aplicar",
"ButtonApplyChapters": "Aplicar Capítulos", "ButtonApplyChapters": "Aplicar Capítulos",
"ButtonAuthors": "Autores", "ButtonAuthors": "Autores",
@ -32,6 +32,8 @@
"ButtonHide": "Esconder", "ButtonHide": "Esconder",
"ButtonHome": "Inicio", "ButtonHome": "Inicio",
"ButtonIssues": "Problemas", "ButtonIssues": "Problemas",
"ButtonJumpBackward": "Retroceder",
"ButtonJumpForward": "Adelantar",
"ButtonLatest": "Últimos", "ButtonLatest": "Últimos",
"ButtonLibrary": "Biblioteca", "ButtonLibrary": "Biblioteca",
"ButtonLogout": "Cerrar Sesión", "ButtonLogout": "Cerrar Sesión",
@ -41,12 +43,17 @@
"ButtonMatchAllAuthors": "Encontrar Todos los Autores", "ButtonMatchAllAuthors": "Encontrar Todos los Autores",
"ButtonMatchBooks": "Encontrar Libros", "ButtonMatchBooks": "Encontrar Libros",
"ButtonNevermind": "Olvidar", "ButtonNevermind": "Olvidar",
"ButtonNext": "Next",
"ButtonNextChapter": "Siguiente Capítulo",
"ButtonOk": "Ok", "ButtonOk": "Ok",
"ButtonOpenFeed": "Abrir Fuente", "ButtonOpenFeed": "Abrir Fuente",
"ButtonOpenManager": "Abrir Editor", "ButtonOpenManager": "Abrir Editor",
"ButtonPause": "Pausar",
"ButtonPlay": "Reproducir", "ButtonPlay": "Reproducir",
"ButtonPlaying": "Reproduciendo", "ButtonPlaying": "Reproduciendo",
"ButtonPlaylists": "Listas de Reproducción", "ButtonPlaylists": "Listas de Reproducción",
"ButtonPrevious": "Previous",
"ButtonPreviousChapter": "Capítulo Anterior",
"ButtonPurgeAllCache": "Purgar Todo el Cache", "ButtonPurgeAllCache": "Purgar Todo el Cache",
"ButtonPurgeItemsCache": "Purgar Elementos de Cache", "ButtonPurgeItemsCache": "Purgar Elementos de Cache",
"ButtonPurgeMediaProgress": "Purgar Progreso de Multimedia", "ButtonPurgeMediaProgress": "Purgar Progreso de Multimedia",
@ -54,6 +61,7 @@
"ButtonQueueRemoveItem": "Remover de la Fila", "ButtonQueueRemoveItem": "Remover de la Fila",
"ButtonQuickMatch": "Encontrar Rápido", "ButtonQuickMatch": "Encontrar Rápido",
"ButtonRead": "Leer", "ButtonRead": "Leer",
"ButtonRefresh": "Refresh",
"ButtonRemove": "Remover", "ButtonRemove": "Remover",
"ButtonRemoveAll": "Remover Todos", "ButtonRemoveAll": "Remover Todos",
"ButtonRemoveAllLibraryItems": "Remover Todos los Elementos de la Biblioteca", "ButtonRemoveAllLibraryItems": "Remover Todos los Elementos de la Biblioteca",
@ -73,6 +81,7 @@
"ButtonSelectFolderPath": "Seleccionar Ruta de Carpeta", "ButtonSelectFolderPath": "Seleccionar Ruta de Carpeta",
"ButtonSeries": "Series", "ButtonSeries": "Series",
"ButtonSetChaptersFromTracks": "Seleccionar Capítulos Según las Pistas", "ButtonSetChaptersFromTracks": "Seleccionar Capítulos Según las Pistas",
"ButtonShare": "Share",
"ButtonShiftTimes": "Desplazar Tiempos", "ButtonShiftTimes": "Desplazar Tiempos",
"ButtonShow": "Mostrar", "ButtonShow": "Mostrar",
"ButtonStartM4BEncode": "Iniciar Codificación M4B", "ButtonStartM4BEncode": "Iniciar Codificación M4B",
@ -87,15 +96,15 @@
"ButtonUserEdit": "Editar Usuario {0}", "ButtonUserEdit": "Editar Usuario {0}",
"ButtonViewAll": "Ver Todos", "ButtonViewAll": "Ver Todos",
"ButtonYes": "Aceptar", "ButtonYes": "Aceptar",
"ErrorUploadFetchMetadataAPI": "Error fetching metadata", "ErrorUploadFetchMetadataAPI": "Error obteniendo metadatos",
"ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author", "ErrorUploadFetchMetadataNoResults": "No se pudo obtener metadatos - Intenta actualizar el título y/o autor",
"ErrorUploadLacksTitle": "Must have a title", "ErrorUploadLacksTitle": "Se debe tener título",
"HeaderAccount": "Cuenta", "HeaderAccount": "Cuenta",
"HeaderAdvanced": "Avanzado", "HeaderAdvanced": "Avanzado",
"HeaderAppriseNotificationSettings": "Ajustes de Notificaciones de Apprise", "HeaderAppriseNotificationSettings": "Ajustes de Notificaciones de Apprise",
"HeaderAudiobookTools": "Herramientas de Gestión de Archivos de Audiolibro", "HeaderAudiobookTools": "Herramientas de Gestión de Archivos de Audiolibro",
"HeaderAudioTracks": "Pistas de Audio", "HeaderAudioTracks": "Pistas de Audio",
"HeaderAuthentication": "Authentication", "HeaderAuthentication": "Autenticación",
"HeaderBackups": "Respaldos", "HeaderBackups": "Respaldos",
"HeaderChangePassword": "Cambiar Contraseña", "HeaderChangePassword": "Cambiar Contraseña",
"HeaderChapters": "Capítulos", "HeaderChapters": "Capítulos",
@ -104,6 +113,7 @@
"HeaderCollectionItems": "Elementos en la Colección", "HeaderCollectionItems": "Elementos en la Colección",
"HeaderCover": "Portada", "HeaderCover": "Portada",
"HeaderCurrentDownloads": "Descargando Actualmente", "HeaderCurrentDownloads": "Descargando Actualmente",
"HeaderCustomMetadataProviders": "Proveedores de metadatos personalizados",
"HeaderDetails": "Detalles", "HeaderDetails": "Detalles",
"HeaderDownloadQueue": "Lista de Descarga", "HeaderDownloadQueue": "Lista de Descarga",
"HeaderEbookFiles": "Archivos de Ebook", "HeaderEbookFiles": "Archivos de Ebook",
@ -130,15 +140,15 @@
"HeaderManageTags": "Administrar Etiquetas", "HeaderManageTags": "Administrar Etiquetas",
"HeaderMapDetails": "Asignar Detalles", "HeaderMapDetails": "Asignar Detalles",
"HeaderMatch": "Encontrar", "HeaderMatch": "Encontrar",
"HeaderMetadataOrderOfPrecedence": "Metadata order of precedence", "HeaderMetadataOrderOfPrecedence": "Orden de precedencia de metadatos",
"HeaderMetadataToEmbed": "Metadatos para Insertar", "HeaderMetadataToEmbed": "Metadatos para Insertar",
"HeaderNewAccount": "Nueva Cuenta", "HeaderNewAccount": "Nueva Cuenta",
"HeaderNewLibrary": "Nueva Biblioteca", "HeaderNewLibrary": "Nueva Biblioteca",
"HeaderNotifications": "Notificaciones", "HeaderNotifications": "Notificaciones",
"HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", "HeaderOpenIDConnectAuthentication": "Autenticación OpenID Connect",
"HeaderOpenRSSFeed": "Abrir fuente RSS", "HeaderOpenRSSFeed": "Abrir fuente RSS",
"HeaderOtherFiles": "Otros Archivos", "HeaderOtherFiles": "Otros Archivos",
"HeaderPasswordAuthentication": "Password Authentication", "HeaderPasswordAuthentication": "Autenticación por contraseña",
"HeaderPermissions": "Permisos", "HeaderPermissions": "Permisos",
"HeaderPlayerQueue": "Fila del Reproductor", "HeaderPlayerQueue": "Fila del Reproductor",
"HeaderPlaylist": "Lista de Reproducción", "HeaderPlaylist": "Lista de Reproducción",
@ -174,6 +184,7 @@
"HeaderUpdateDetails": "Actualizar Detalles", "HeaderUpdateDetails": "Actualizar Detalles",
"HeaderUpdateLibrary": "Actualizar Biblioteca", "HeaderUpdateLibrary": "Actualizar Biblioteca",
"HeaderUsers": "Usuarios", "HeaderUsers": "Usuarios",
"HeaderYearReview": "Year {0} in Review",
"HeaderYourStats": "Tus Estadísticas", "HeaderYourStats": "Tus Estadísticas",
"LabelAbridged": "Abreviado", "LabelAbridged": "Abreviado",
"LabelAccountType": "Tipo de Cuenta", "LabelAccountType": "Tipo de Cuenta",
@ -187,11 +198,11 @@
"LabelAddToCollectionBatch": "Se Añadieron {0} Libros a la Colección", "LabelAddToCollectionBatch": "Se Añadieron {0} Libros a la Colección",
"LabelAddToPlaylist": "Añadido a la Lista de Reproducción", "LabelAddToPlaylist": "Añadido a la Lista de Reproducción",
"LabelAddToPlaylistBatch": "Se Añadieron {0} Artículos a la Lista de Reproducción", "LabelAddToPlaylistBatch": "Se Añadieron {0} Artículos a la Lista de Reproducción",
"LabelAdminUsersOnly": "Admin users only", "LabelAdminUsersOnly": "Solamente usuarios administradores",
"LabelAll": "Todos", "LabelAll": "Todos",
"LabelAllUsers": "Todos los Usuarios", "LabelAllUsers": "Todos los Usuarios",
"LabelAllUsersExcludingGuests": "All users excluding guests", "LabelAllUsersExcludingGuests": "Todos los usuarios excepto invitados",
"LabelAllUsersIncludingGuests": "All users including guests", "LabelAllUsersIncludingGuests": "Todos los usuarios e invitados",
"LabelAlreadyInYourLibrary": "Ya en la Biblioteca", "LabelAlreadyInYourLibrary": "Ya en la Biblioteca",
"LabelAppend": "Adjuntar", "LabelAppend": "Adjuntar",
"LabelAuthor": "Autor", "LabelAuthor": "Autor",
@ -199,12 +210,12 @@
"LabelAuthorLastFirst": "Autor (Apellido, Nombre)", "LabelAuthorLastFirst": "Autor (Apellido, Nombre)",
"LabelAuthors": "Autores", "LabelAuthors": "Autores",
"LabelAutoDownloadEpisodes": "Descargar Episodios Automáticamente", "LabelAutoDownloadEpisodes": "Descargar Episodios Automáticamente",
"LabelAutoFetchMetadata": "Auto Fetch Metadata", "LabelAutoFetchMetadata": "Actualizar Metadatos Automáticamente",
"LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.", "LabelAutoFetchMetadataHelp": "Obtiene metadatos de título, autor y serie para agilizar la carga. Es posible que haya que cotejar metadatos adicionales después de la carga.",
"LabelAutoLaunch": "Auto Launch", "LabelAutoLaunch": "Lanzamiento automático",
"LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)", "LabelAutoLaunchDescription": "Redirigir al proveedor de autenticación automáticamente al navegar a la página de inicio de sesión (ruta de sobreescritura manual <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Auto Register", "LabelAutoRegister": "Registro automático",
"LabelAutoRegisterDescription": "Automatically create new users after logging in", "LabelAutoRegisterDescription": "Crear usuarios automáticamente tras iniciar sesión",
"LabelBackToUser": "Regresar a Usuario", "LabelBackToUser": "Regresar a Usuario",
"LabelBackupLocation": "Ubicación del Respaldo", "LabelBackupLocation": "Ubicación del Respaldo",
"LabelBackupsEnableAutomaticBackups": "Habilitar Respaldo Automático", "LabelBackupsEnableAutomaticBackups": "Habilitar Respaldo Automático",
@ -215,13 +226,13 @@
"LabelBackupsNumberToKeepHelp": "Solamente 1 respaldo se removerá a la vez. Si tiene mas respaldos guardados, debe removerlos manualmente.", "LabelBackupsNumberToKeepHelp": "Solamente 1 respaldo se removerá a la vez. Si tiene mas respaldos guardados, debe removerlos manualmente.",
"LabelBitrate": "Bitrate", "LabelBitrate": "Bitrate",
"LabelBooks": "Libros", "LabelBooks": "Libros",
"LabelButtonText": "Button Text", "LabelButtonText": "Texto del botón",
"LabelChangePassword": "Cambiar Contraseña", "LabelChangePassword": "Cambiar Contraseña",
"LabelChannels": "Canales", "LabelChannels": "Canales",
"LabelChapters": "Capítulos", "LabelChapters": "Capítulos",
"LabelChaptersFound": "Capítulo Encontrado", "LabelChaptersFound": "Capítulo Encontrado",
"LabelChapterTitle": "Titulo del Capítulo", "LabelChapterTitle": "Titulo del Capítulo",
"LabelClickForMoreInfo": "Click for more info", "LabelClickForMoreInfo": "Click para más información",
"LabelClosePlayer": "Cerrar Reproductor", "LabelClosePlayer": "Cerrar Reproductor",
"LabelCodec": "Codec", "LabelCodec": "Codec",
"LabelCollapseSeries": "Colapsar Serie", "LabelCollapseSeries": "Colapsar Serie",
@ -240,12 +251,12 @@
"LabelCurrently": "En este momento:", "LabelCurrently": "En este momento:",
"LabelCustomCronExpression": "Expresión de Cron Personalizada:", "LabelCustomCronExpression": "Expresión de Cron Personalizada:",
"LabelDatetime": "Hora y Fecha", "LabelDatetime": "Hora y Fecha",
"LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)", "LabelDeleteFromFileSystemCheckbox": "Eliminar archivos del sistema (desmarcar para eliminar sólo de la base de datos)",
"LabelDescription": "Descripción", "LabelDescription": "Descripción",
"LabelDeselectAll": "Deseleccionar Todos", "LabelDeselectAll": "Deseleccionar Todos",
"LabelDevice": "Dispositivo", "LabelDevice": "Dispositivo",
"LabelDeviceInfo": "Información de Dispositivo", "LabelDeviceInfo": "Información de Dispositivo",
"LabelDeviceIsAvailableTo": "Device is available to...", "LabelDeviceIsAvailableTo": "El dispositivo está disponible para...",
"LabelDirectory": "Directorio", "LabelDirectory": "Directorio",
"LabelDiscFromFilename": "Disco a partir del Nombre del Archivo", "LabelDiscFromFilename": "Disco a partir del Nombre del Archivo",
"LabelDiscFromMetadata": "Disco a partir de Metadata", "LabelDiscFromMetadata": "Disco a partir de Metadata",
@ -271,7 +282,7 @@
"LabelExample": "Ejemplo", "LabelExample": "Ejemplo",
"LabelExplicit": "Explicito", "LabelExplicit": "Explicito",
"LabelFeedURL": "Fuente de URL", "LabelFeedURL": "Fuente de URL",
"LabelFetchingMetadata": "Fetching Metadata", "LabelFetchingMetadata": "Obteniendo metadatos",
"LabelFile": "Archivo", "LabelFile": "Archivo",
"LabelFileBirthtime": "Archivo Creado en", "LabelFileBirthtime": "Archivo Creado en",
"LabelFileModified": "Archivo modificado", "LabelFileModified": "Archivo modificado",
@ -281,23 +292,23 @@
"LabelFinished": "Terminado", "LabelFinished": "Terminado",
"LabelFolder": "Carpeta", "LabelFolder": "Carpeta",
"LabelFolders": "Carpetas", "LabelFolders": "Carpetas",
"LabelFontBold": "Bold", "LabelFontBold": "Negrilla",
"LabelFontFamily": "Familia tipográfica", "LabelFontFamily": "Familia tipográfica",
"LabelFontItalic": "Italic", "LabelFontItalic": "Itálica",
"LabelFontScale": "Tamaño de Fuente", "LabelFontScale": "Tamaño de Fuente",
"LabelFontStrikethrough": "Strikethrough", "LabelFontStrikethrough": "Tachado",
"LabelFormat": "Formato", "LabelFormat": "Formato",
"LabelGenre": "Genero", "LabelGenre": "Genero",
"LabelGenres": "Géneros", "LabelGenres": "Géneros",
"LabelHardDeleteFile": "Eliminar Definitivamente", "LabelHardDeleteFile": "Eliminar Definitivamente",
"LabelHasEbook": "Tiene Ebook", "LabelHasEbook": "Tiene Ebook",
"LabelHasSupplementaryEbook": "Tiene Ebook Suplementario", "LabelHasSupplementaryEbook": "Tiene Ebook Suplementario",
"LabelHighestPriority": "Highest priority", "LabelHighestPriority": "Mayor prioridad",
"LabelHost": "Host", "LabelHost": "Host",
"LabelHour": "Hora", "LabelHour": "Hora",
"LabelIcon": "Icono", "LabelIcon": "Icono",
"LabelImageURLFromTheWeb": "Image URL from the web", "LabelImageURLFromTheWeb": "URL de la imagen",
"LabelIncludeInTracklist": "Incluir en Tracklist", "LabelIncludeInTracklist": "Incluir en la Tracklist",
"LabelIncomplete": "Incompleto", "LabelIncomplete": "Incompleto",
"LabelInProgress": "En Proceso", "LabelInProgress": "En Proceso",
"LabelInterval": "Intervalo", "LabelInterval": "Intervalo",
@ -334,20 +345,22 @@
"LabelLogLevelInfo": "Información", "LabelLogLevelInfo": "Información",
"LabelLogLevelWarn": "Advertencia", "LabelLogLevelWarn": "Advertencia",
"LabelLookForNewEpisodesAfterDate": "Buscar Nuevos Episodios a partir de esta Fecha", "LabelLookForNewEpisodesAfterDate": "Buscar Nuevos Episodios a partir de esta Fecha",
"LabelLowestPriority": "Lowest Priority", "LabelLowestPriority": "Menor prioridad",
"LabelMatchExistingUsersBy": "Match existing users by", "LabelMatchExistingUsersBy": "Emparejar a los usuarios existentes por",
"LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMatchExistingUsersByDescription": "Se utiliza para conectar usuarios existentes. Una vez conectados, los usuarios serán emparejados por un identificador único de su proveedor de SSO",
"LabelMediaPlayer": "Reproductor de Medios", "LabelMediaPlayer": "Reproductor de Medios",
"LabelMediaType": "Tipo de Multimedia", "LabelMediaType": "Tipo de Multimedia",
"LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", "LabelMetadataOrderOfPrecedenceDescription": "Las fuentes de metadatos de mayor prioridad prevalecerán sobre las de menor prioridad",
"LabelMetadataProvider": "Proveedor de Metadata", "LabelMetadataProvider": "Proveedor de Metadatos",
"LabelMetaTag": "Meta Tag", "LabelMetaTag": "Metaetiqueta",
"LabelMetaTags": "Meta Tags", "LabelMetaTags": "Metaetiquetas",
"LabelMinute": "Minuto", "LabelMinute": "Minuto",
"LabelMissing": "Ausente", "LabelMissing": "Ausente",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Partes Ausentes", "LabelMissingParts": "Partes Ausentes",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", "LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", "LabelMobileRedirectURIs": "URIs de redirección a móviles permitidos",
"LabelMobileRedirectURIsDescription": "Esta es una lista de URIs válidos para redireccionamiento de apps móviles. La URI por defecto es <code>audiobookshelf://oauth</code>, la cual puedes remover or corroborar con URIs adicionales para la integración con apps de terceros. Utilizando un asterisco (<code>*</code>) como el único punto de entrada permite cualquier URI.",
"LabelMore": "Más", "LabelMore": "Más",
"LabelMoreInfo": "Más Información", "LabelMoreInfo": "Más Información",
"LabelName": "Nombre", "LabelName": "Nombre",
@ -385,11 +398,13 @@
"LabelPermissionsDownload": "Puede Descargar", "LabelPermissionsDownload": "Puede Descargar",
"LabelPermissionsUpdate": "Puede Actualizar", "LabelPermissionsUpdate": "Puede Actualizar",
"LabelPermissionsUpload": "Puede Subir", "LabelPermissionsUpload": "Puede Subir",
"LabelPersonalYearReview": "Your Year in Review ({0})",
"LabelPhotoPathURL": "Ruta de Acceso/URL de Foto", "LabelPhotoPathURL": "Ruta de Acceso/URL de Foto",
"LabelPlaylists": "Lista de Reproducción", "LabelPlaylists": "Lista de Reproducción",
"LabelPlayMethod": "Método de Reproducción", "LabelPlayMethod": "Método de Reproducción",
"LabelPodcast": "Podcast", "LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts", "LabelPodcasts": "Podcasts",
"LabelPodcastSearchRegion": "Región de búsqueda de podcasts",
"LabelPodcastType": "Tipo Podcast", "LabelPodcastType": "Tipo Podcast",
"LabelPort": "Puerto", "LabelPort": "Puerto",
"LabelPrefixesToIgnore": "Prefijos para Ignorar (no distingue entre mayúsculas y minúsculas.)", "LabelPrefixesToIgnore": "Prefijos para Ignorar (no distingue entre mayúsculas y minúsculas.)",
@ -406,11 +421,11 @@
"LabelRecentlyAdded": "Agregado Recientemente", "LabelRecentlyAdded": "Agregado Recientemente",
"LabelRecentSeries": "Series Recientes", "LabelRecentSeries": "Series Recientes",
"LabelRecommended": "Recomendados", "LabelRecommended": "Recomendados",
"LabelRedo": "Redo", "LabelRedo": "Rehacer",
"LabelRegion": "Región", "LabelRegion": "Región",
"LabelReleaseDate": "Fecha de Estreno", "LabelReleaseDate": "Fecha de Estreno",
"LabelRemoveCover": "Remover Portada", "LabelRemoveCover": "Remover Portada",
"LabelRowsPerPage": "Rows per page", "LabelRowsPerPage": "Filas por página",
"LabelRSSFeedCustomOwnerEmail": "Email de dueño personalizado", "LabelRSSFeedCustomOwnerEmail": "Email de dueño personalizado",
"LabelRSSFeedCustomOwnerName": "Nombre de dueño personalizado", "LabelRSSFeedCustomOwnerName": "Nombre de dueño personalizado",
"LabelRSSFeedOpen": "Fuente RSS Abierta", "LabelRSSFeedOpen": "Fuente RSS Abierta",
@ -423,12 +438,13 @@
"LabelSeason": "Temporada", "LabelSeason": "Temporada",
"LabelSelectAllEpisodes": "Seleccionar todos los episodios", "LabelSelectAllEpisodes": "Seleccionar todos los episodios",
"LabelSelectEpisodesShowing": "Seleccionar los {0} episodios visibles", "LabelSelectEpisodesShowing": "Seleccionar los {0} episodios visibles",
"LabelSelectUsers": "Select users", "LabelSelectUsers": "Seleccionar usuarios",
"LabelSendEbookToDevice": "Enviar Ebook a...", "LabelSendEbookToDevice": "Enviar Ebook a...",
"LabelSequence": "Secuencia", "LabelSequence": "Secuencia",
"LabelSeries": "Series", "LabelSeries": "Series",
"LabelSeriesName": "Nombre de la Serie", "LabelSeriesName": "Nombre de la Serie",
"LabelSeriesProgress": "Progreso de la Serie", "LabelSeriesProgress": "Progreso de la Serie",
"LabelServerYearReview": "Server Year in Review ({0})",
"LabelSetEbookAsPrimary": "Establecer como primario", "LabelSetEbookAsPrimary": "Establecer como primario",
"LabelSetEbookAsSupplementary": "Establecer como suplementario", "LabelSetEbookAsSupplementary": "Establecer como suplementario",
"LabelSettingsAudiobooksOnly": "Sólo Audiolibros", "LabelSettingsAudiobooksOnly": "Sólo Audiolibros",
@ -495,14 +511,14 @@
"LabelTagsAccessibleToUser": "Etiquetas Accessibles al Usuario", "LabelTagsAccessibleToUser": "Etiquetas Accessibles al Usuario",
"LabelTagsNotAccessibleToUser": "Etiquetas no Accesibles al Usuario", "LabelTagsNotAccessibleToUser": "Etiquetas no Accesibles al Usuario",
"LabelTasks": "Tareas Corriendo", "LabelTasks": "Tareas Corriendo",
"LabelTextEditorBulletedList": "Bulleted list", "LabelTextEditorBulletedList": "Lista con viñetas",
"LabelTextEditorLink": "Link", "LabelTextEditorLink": "Enlazar",
"LabelTextEditorNumberedList": "Numbered list", "LabelTextEditorNumberedList": "Lista numerada",
"LabelTextEditorUnlink": "Unlink", "LabelTextEditorUnlink": "Desenlazar",
"LabelTheme": "Tema", "LabelTheme": "Tema",
"LabelThemeDark": "Oscuro", "LabelThemeDark": "Oscuro",
"LabelThemeLight": "Claro", "LabelThemeLight": "Claro",
"LabelTimeBase": "Time Base", "LabelTimeBase": "Tiempo Base",
"LabelTimeListened": "Tiempo Escuchando", "LabelTimeListened": "Tiempo Escuchando",
"LabelTimeListenedToday": "Tiempo Escuchando Hoy", "LabelTimeListenedToday": "Tiempo Escuchando Hoy",
"LabelTimeRemaining": "{0} restante", "LabelTimeRemaining": "{0} restante",
@ -524,7 +540,7 @@
"LabelTracksSingleTrack": "Una pista", "LabelTracksSingleTrack": "Una pista",
"LabelType": "Tipo", "LabelType": "Tipo",
"LabelUnabridged": "No Abreviado", "LabelUnabridged": "No Abreviado",
"LabelUndo": "Undo", "LabelUndo": "Deshacer",
"LabelUnknown": "Desconocido", "LabelUnknown": "Desconocido",
"LabelUpdateCover": "Actualizar Portada", "LabelUpdateCover": "Actualizar Portada",
"LabelUpdateCoverHelp": "Permitir sobrescribir las portadas existentes de los libros seleccionados cuando sean encontradas.", "LabelUpdateCoverHelp": "Permitir sobrescribir las portadas existentes de los libros seleccionados cuando sean encontradas.",
@ -533,7 +549,7 @@
"LabelUpdateDetailsHelp": "Permitir sobrescribir detalles existentes de los libros seleccionados cuando sean encontrados", "LabelUpdateDetailsHelp": "Permitir sobrescribir detalles existentes de los libros seleccionados cuando sean encontrados",
"LabelUploaderDragAndDrop": "Arrastre y suelte archivos o carpetas", "LabelUploaderDragAndDrop": "Arrastre y suelte archivos o carpetas",
"LabelUploaderDropFiles": "Suelte los Archivos", "LabelUploaderDropFiles": "Suelte los Archivos",
"LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series", "LabelUploaderItemFetchMetadataHelp": "Buscar título, autor y series automáticamente",
"LabelUseChapterTrack": "Usar pista por capitulo", "LabelUseChapterTrack": "Usar pista por capitulo",
"LabelUseFullTrack": "Usar pista completa", "LabelUseFullTrack": "Usar pista completa",
"LabelUser": "Usuario", "LabelUser": "Usuario",
@ -545,6 +561,8 @@
"LabelViewQueue": "Ver Fila del Reproductor", "LabelViewQueue": "Ver Fila del Reproductor",
"LabelVolume": "Volumen", "LabelVolume": "Volumen",
"LabelWeekdaysToRun": "Correr en Días de la Semana", "LabelWeekdaysToRun": "Correr en Días de la Semana",
"LabelYearReviewHide": "Hide Year in Review",
"LabelYearReviewShow": "See Year in Review",
"LabelYourAudiobookDuration": "Duración de tu Audiolibro", "LabelYourAudiobookDuration": "Duración de tu Audiolibro",
"LabelYourBookmarks": "Tus Marcadores", "LabelYourBookmarks": "Tus Marcadores",
"LabelYourPlaylists": "Tus Listas", "LabelYourPlaylists": "Tus Listas",
@ -567,15 +585,15 @@
"MessageConfirmDeleteBackup": "¿Está seguro de que desea eliminar el respaldo {0}?", "MessageConfirmDeleteBackup": "¿Está seguro de que desea eliminar el respaldo {0}?",
"MessageConfirmDeleteFile": "Esto eliminará el archivo de su sistema de archivos. ¿Está seguro?", "MessageConfirmDeleteFile": "Esto eliminará el archivo de su sistema de archivos. ¿Está seguro?",
"MessageConfirmDeleteLibrary": "¿Está seguro de que desea eliminar permanentemente la biblioteca \"{0}\"?", "MessageConfirmDeleteLibrary": "¿Está seguro de que desea eliminar permanentemente la biblioteca \"{0}\"?",
"MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?", "MessageConfirmDeleteLibraryItem": "Esto removerá la librería de la base de datos y archivos en tu sistema. ¿Estás seguro?",
"MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?", "MessageConfirmDeleteLibraryItems": "Esto removerá {0} elemento(s) de la librería en base de datos y archivos en tu sistema. ¿Estás seguro?",
"MessageConfirmDeleteSession": "¿Está seguro de que desea eliminar esta sesión?", "MessageConfirmDeleteSession": "¿Está seguro de que desea eliminar esta sesión?",
"MessageConfirmForceReScan": "¿Está seguro de que desea forzar un re-escaneo?", "MessageConfirmForceReScan": "¿Está seguro de que desea forzar un re-escaneo?",
"MessageConfirmMarkAllEpisodesFinished": "¿Está seguro de que desea marcar todos los episodios como terminados?", "MessageConfirmMarkAllEpisodesFinished": "¿Está seguro de que desea marcar todos los episodios como terminados?",
"MessageConfirmMarkAllEpisodesNotFinished": "¿Está seguro de que desea marcar todos los episodios como no terminados?", "MessageConfirmMarkAllEpisodesNotFinished": "¿Está seguro de que desea marcar todos los episodios como no terminados?",
"MessageConfirmMarkSeriesFinished": "¿Está seguro de que desea marcar todos los libros en esta serie como terminados?", "MessageConfirmMarkSeriesFinished": "¿Está seguro de que desea marcar todos los libros en esta serie como terminados?",
"MessageConfirmMarkSeriesNotFinished": "¿Está seguro de que desea marcar todos los libros en esta serie como no terminados?", "MessageConfirmMarkSeriesNotFinished": "¿Está seguro de que desea marcar todos los libros en esta serie como no terminados?",
"MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?", "MessageConfirmQuickEmbed": "¡Advertencia! La integración rápida no realiza copias de seguridad a ninguno de tus archivos de audio. Asegúrate de haber realizado una copia de los mismos previamente. <br><br>¿Deseas continuar?",
"MessageConfirmRemoveAllChapters": "¿Está seguro de que desea remover todos los capitulos?", "MessageConfirmRemoveAllChapters": "¿Está seguro de que desea remover todos los capitulos?",
"MessageConfirmRemoveAuthor": "¿Está seguro de que desea remover el autor \"{0}\"?", "MessageConfirmRemoveAuthor": "¿Está seguro de que desea remover el autor \"{0}\"?",
"MessageConfirmRemoveCollection": "¿Está seguro de que desea remover la colección \"{0}\"?", "MessageConfirmRemoveCollection": "¿Está seguro de que desea remover la colección \"{0}\"?",
@ -590,7 +608,7 @@
"MessageConfirmRenameTag": "¿Está seguro de que desea renombrar la etiqueta \"{0}\" a \"{1}\" de todos los elementos?", "MessageConfirmRenameTag": "¿Está seguro de que desea renombrar la etiqueta \"{0}\" a \"{1}\" de todos los elementos?",
"MessageConfirmRenameTagMergeNote": "Nota: Esta etiqueta ya existe, por lo que se fusionarán.", "MessageConfirmRenameTagMergeNote": "Nota: Esta etiqueta ya existe, por lo que se fusionarán.",
"MessageConfirmRenameTagWarning": "Advertencia! Una etiqueta similar ya existe \"{0}\".", "MessageConfirmRenameTagWarning": "Advertencia! Una etiqueta similar ya existe \"{0}\".",
"MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?", "MessageConfirmReScanLibraryItems": "¿Estás seguro de querer re escanear {0} elemento(s)?",
"MessageConfirmSendEbookToDevice": "¿Está seguro de que enviar {0} ebook(s) \"{1}\" al dispositivo \"{2}\"?", "MessageConfirmSendEbookToDevice": "¿Está seguro de que enviar {0} ebook(s) \"{1}\" al dispositivo \"{2}\"?",
"MessageDownloadingEpisode": "Descargando Capitulo", "MessageDownloadingEpisode": "Descargando Capitulo",
"MessageDragFilesIntoTrackOrder": "Arrastra los archivos al orden correcto de las pistas.", "MessageDragFilesIntoTrackOrder": "Arrastra los archivos al orden correcto de las pistas.",
@ -661,7 +679,7 @@
"MessageRestoreBackupConfirm": "¿Está seguro de que desea para restaurar del respaldo creado en", "MessageRestoreBackupConfirm": "¿Está seguro de que desea para restaurar del respaldo creado en",
"MessageRestoreBackupWarning": "Restaurar sobrescribirá toda la base de datos localizada en /config y las imágenes de portadas en /metadata/items y /metadata/authors.<br /><br />El respaldo no modifica ningún archivo en las carpetas de su biblioteca. Si ha habilitado la opción del servidor para almacenar portadas y metadata en las carpetas de su biblioteca, esos archivos no se respaldan o sobrescriben.<br /><br />Todos los clientes que usen su servidor se actualizarán automáticamente.", "MessageRestoreBackupWarning": "Restaurar sobrescribirá toda la base de datos localizada en /config y las imágenes de portadas en /metadata/items y /metadata/authors.<br /><br />El respaldo no modifica ningún archivo en las carpetas de su biblioteca. Si ha habilitado la opción del servidor para almacenar portadas y metadata en las carpetas de su biblioteca, esos archivos no se respaldan o sobrescriben.<br /><br />Todos los clientes que usen su servidor se actualizarán automáticamente.",
"MessageSearchResultsFor": "Resultados de la búsqueda de", "MessageSearchResultsFor": "Resultados de la búsqueda de",
"MessageSelected": "{0} selected", "MessageSelected": "{0} seleccionado(s)",
"MessageServerCouldNotBeReached": "No se pudo establecer la conexión con el servidor", "MessageServerCouldNotBeReached": "No se pudo establecer la conexión con el servidor",
"MessageSetChaptersFromTracksDescription": "Establecer capítulos usando cada archivo de audio como un capítulo y el título del capítulo como el nombre del archivo de audio", "MessageSetChaptersFromTracksDescription": "Establecer capítulos usando cada archivo de audio como un capítulo y el título del capítulo como el nombre del archivo de audio",
"MessageStartPlaybackAtTime": "Iniciar reproducción para \"{0}\" en {1}?", "MessageStartPlaybackAtTime": "Iniciar reproducción para \"{0}\" en {1}?",

779
client/strings/et.json Normal file
View File

@ -0,0 +1,779 @@
{
"ButtonAdd": "Lisa",
"ButtonAddChapters": "Lisa peatükid",
"ButtonAddDevice": "Lisa seade",
"ButtonAddLibrary": "Lisa raamatukogu",
"ButtonAddPodcasts": "Lisa podcastid",
"ButtonAddUser": "Lisa kasutaja",
"ButtonAddYourFirstLibrary": "Lisa oma esimene raamatukogu",
"ButtonApply": "Rakenda",
"ButtonApplyChapters": "Rakenda peatükid",
"ButtonAuthors": "Autorid",
"ButtonBrowseForFolder": "Sirvi kausta",
"ButtonCancel": "Tühista",
"ButtonCancelEncode": "Tühista kodeerimine",
"ButtonChangeRootPassword": "Muuda põhiparooli",
"ButtonCheckAndDownloadNewEpisodes": "Kontrolli ja laadi alla uued episoodid",
"ButtonChooseAFolder": "Vali kaust",
"ButtonChooseFiles": "Vali failid",
"ButtonClearFilter": "Tühista filter",
"ButtonCloseFeed": "Sulge voog",
"ButtonCollections": "Kogud",
"ButtonConfigureScanner": "Konfigureeri skanner",
"ButtonCreate": "Loo",
"ButtonCreateBackup": "Loo varundus",
"ButtonDelete": "Kustuta",
"ButtonDownloadQueue": "Järjekord",
"ButtonEdit": "Muuda",
"ButtonEditChapters": "Muuda peatükke",
"ButtonEditPodcast": "Muuda podcasti",
"ButtonForceReScan": "Sunnitud uuestiskaneerimine",
"ButtonFullPath": "Täielik asukoht",
"ButtonHide": "Peida",
"ButtonHome": "Avaleht",
"ButtonIssues": "Probleemid",
"ButtonJumpBackward": "Hüppa tagasi",
"ButtonJumpForward": "Hüppa edasi",
"ButtonLatest": "Uusim",
"ButtonLibrary": "Raamatukogu",
"ButtonLogout": "Logi välja",
"ButtonLookup": "Otsi",
"ButtonManageTracks": "Halda lugusid",
"ButtonMapChapterTitles": "Kaardista peatükkide pealkirjad",
"ButtonMatchAllAuthors": "Sobita kõik autorid",
"ButtonMatchBooks": "Sobita raamatud",
"ButtonNevermind": "Pole tähtis",
"ButtonNext": "Next",
"ButtonNextChapter": "Järgmine peatükk",
"ButtonOk": "Ok",
"ButtonOpenFeed": "Ava voog",
"ButtonOpenManager": "Ava haldur",
"ButtonPause": "Peata",
"ButtonPlay": "Mängi",
"ButtonPlaying": "Mängib",
"ButtonPlaylists": "Esitusloendid",
"ButtonPrevious": "Previous",
"ButtonPreviousChapter": "Eelmine peatükk",
"ButtonPurgeAllCache": "Tühjenda kogu vahemälu",
"ButtonPurgeItemsCache": "Tühjenda esemete vahemälu",
"ButtonPurgeMediaProgress": "Tühjenda meedia edenemine",
"ButtonQueueAddItem": "Lisa järjekorda",
"ButtonQueueRemoveItem": "Eemalda järjekorrast",
"ButtonQuickMatch": "Kiire sobitamine",
"ButtonRead": "Loe",
"ButtonRefresh": "Refresh",
"ButtonRemove": "Eemalda",
"ButtonRemoveAll": "Eemalda kõik",
"ButtonRemoveAllLibraryItems": "Eemalda kõik raamatukogu esemed",
"ButtonRemoveFromContinueListening": "Eemalda jätkake kuulamisest",
"ButtonRemoveFromContinueReading": "Eemalda jätkake lugemisest",
"ButtonRemoveSeriesFromContinueSeries": "Eemalda seeria jätkamisest",
"ButtonReScan": "Uuestiskaneeri",
"ButtonReset": "Lähtesta",
"ButtonResetToDefault": "Lähtesta vaikeseade",
"ButtonRestore": "Taasta",
"ButtonSave": "Salvesta",
"ButtonSaveAndClose": "Salvesta ja sulge",
"ButtonSaveTracklist": "Salvesta lugude loend",
"ButtonScan": "Skanneeri",
"ButtonScanLibrary": "Skanneeri raamatukogu",
"ButtonSearch": "Otsi",
"ButtonSelectFolderPath": "Vali kaustatee",
"ButtonSeries": "Sarjad",
"ButtonSetChaptersFromTracks": "Määra peatükid lugudest",
"ButtonShare": "Share",
"ButtonShiftTimes": "Nihke ajad",
"ButtonShow": "Näita",
"ButtonStartM4BEncode": "Alusta M4B kodeerimist",
"ButtonStartMetadataEmbed": "Alusta metaandmete lisamist",
"ButtonSubmit": "Esita",
"ButtonTest": "Test",
"ButtonUpload": "Lae üles",
"ButtonUploadBackup": "Lae üles varundus",
"ButtonUploadCover": "Lae üles ümbris",
"ButtonUploadOPMLFile": "Lae üles OPML-fail",
"ButtonUserDelete": "Kustuta kasutaja {0}",
"ButtonUserEdit": "Muuda kasutajat {0}",
"ButtonViewAll": "Vaata kõiki",
"ButtonYes": "Jah",
"ErrorUploadFetchMetadataAPI": "Viga metaandmete hankimisel",
"ErrorUploadFetchMetadataNoResults": "Ei saanud metaandmeid hankida - proovi tiitlit ja/või autorit uuendada",
"ErrorUploadLacksTitle": "Peab olema pealkiri",
"HeaderAccount": "Konto",
"HeaderAdvanced": "Täpsem",
"HeaderAppriseNotificationSettings": "Apprise teavitamise seaded",
"HeaderAudiobookTools": "Heliraamatu failihaldustööriistad",
"HeaderAudioTracks": "Helirajad",
"HeaderAuthentication": "Autentimine",
"HeaderBackups": "Varukoopiad",
"HeaderChangePassword": "Muuda parooli",
"HeaderChapters": "Peatükid",
"HeaderChooseAFolder": "Vali kaust",
"HeaderCollection": "Kogu",
"HeaderCollectionItems": "Kogu esemed",
"HeaderCover": "Ümbris",
"HeaderCurrentDownloads": "Praegused allalaadimised",
"HeaderCustomMetadataProviders": "Kohandatud metaandmete pakkujad",
"HeaderDetails": "Detailid",
"HeaderDownloadQueue": "Allalaadimise järjekord",
"HeaderEbookFiles": "E-raamatute failid",
"HeaderEmail": "E-post",
"HeaderEmailSettings": "E-posti seaded",
"HeaderEpisodes": "Episoodid",
"HeaderEreaderDevices": "E-lugerite seadmed",
"HeaderEreaderSettings": "E-lugerite seadistused",
"HeaderFiles": "Failid",
"HeaderFindChapters": "Leia peatükid",
"HeaderIgnoredFiles": "Ignoreeritud failid",
"HeaderItemFiles": "Esemete failid",
"HeaderItemMetadataUtils": "Eseme metaandmete tööriistad",
"HeaderLastListeningSession": "Viimane kuulamissessioon",
"HeaderLatestEpisodes": "Viimased episoodid",
"HeaderLibraries": "Raamatukogud",
"HeaderLibraryFiles": "Raamatukogu failid",
"HeaderLibraryStats": "Raamatukogu statistika",
"HeaderListeningSessions": "Kuulamissessioonid",
"HeaderListeningStats": "Kuulamise statistika",
"HeaderLogin": "Logi sisse",
"HeaderLogs": "Logid",
"HeaderManageGenres": "Halda žanre",
"HeaderManageTags": "Halda silte",
"HeaderMapDetails": "Kaardi detailid",
"HeaderMatch": "Sobita",
"HeaderMetadataOrderOfPrecedence": "Metaandmete eelnevusjärjestus",
"HeaderMetadataToEmbed": "Manusta metaandmed",
"HeaderNewAccount": "Uus konto",
"HeaderNewLibrary": "Uus raamatukogu",
"HeaderNotifications": "Teatised",
"HeaderOpenIDConnectAuthentication": "OpenID Connect autentimine",
"HeaderOpenRSSFeed": "Ava RSS-voog",
"HeaderOtherFiles": "Muud failid",
"HeaderPasswordAuthentication": "Parooli autentimine",
"HeaderPermissions": "Õigused",
"HeaderPlayerQueue": "Mängija järjekord",
"HeaderPlaylist": "Mänguloend",
"HeaderPlaylistItems": "Mänguloendi esemed",
"HeaderPodcastsToAdd": "Lisatavad podcastid",
"HeaderPreviewCover": "Eelvaate kaas",
"HeaderRemoveEpisode": "Eemalda episood",
"HeaderRemoveEpisodes": "Eemalda {0} episoodi",
"HeaderRSSFeedGeneral": "RSS-i üksikasjad",
"HeaderRSSFeedIsOpen": "RSS-voog on avatud",
"HeaderRSSFeeds": "RSS-vooged",
"HeaderSavedMediaProgress": "Salvestatud meedia edenemine",
"HeaderSchedule": "Ajakava",
"HeaderScheduleLibraryScans": "Ajasta automaatsed raamatukogu skaneerimised",
"HeaderSession": "Sessioon",
"HeaderSetBackupSchedule": "Määra varunduse ajakava",
"HeaderSettings": "Seaded",
"HeaderSettingsDisplay": "Kuva",
"HeaderSettingsExperimental": "Katsetusfunktsioonid",
"HeaderSettingsGeneral": "Üldised",
"HeaderSettingsScanner": "Skanner",
"HeaderSleepTimer": "Uinaku taimer",
"HeaderStatsLargestItems": "Suurimad esemed",
"HeaderStatsLongestItems": "Kõige pikemad esemed (tunnid)",
"HeaderStatsMinutesListeningChart": "Kuulamise minutid (viimased 7 päeva)",
"HeaderStatsRecentSessions": "Hiljutised sessioonid",
"HeaderStatsTop10Authors": "Top 10 autorit",
"HeaderStatsTop5Genres": "Top 5 žanrit",
"HeaderTableOfContents": "Sisukord",
"HeaderTools": "Tööriistad",
"HeaderUpdateAccount": "Uuenda kontot",
"HeaderUpdateAuthor": "Uuenda autorit",
"HeaderUpdateDetails": "Uuenda detaile",
"HeaderUpdateLibrary": "Uuenda raamatukogu",
"HeaderUsers": "Kasutajad",
"HeaderYearReview": "Year {0} in Review",
"HeaderYourStats": "Sinu statistika",
"LabelAbridged": "Kärbitud",
"LabelAccountType": "Konto tüüp",
"LabelAccountTypeAdmin": "Administraator",
"LabelAccountTypeGuest": "Külaline",
"LabelAccountTypeUser": "Kasutaja",
"LabelActivity": "Tegevus",
"LabelAdded": "Lisatud",
"LabelAddedAt": "Lisatud",
"LabelAddToCollection": "Lisa kogusse",
"LabelAddToCollectionBatch": "Lisa {0} raamatut kogusse",
"LabelAddToPlaylist": "Lisa mänguloendisse",
"LabelAddToPlaylistBatch": "Lisa {0} eset mänguloendisse",
"LabelAdminUsersOnly": "Ainult administraatorid",
"LabelAll": "Kõik",
"LabelAllUsers": "Kõik kasutajad",
"LabelAllUsersExcludingGuests": "Kõik kasutajad, välja arvatud külalised",
"LabelAllUsersIncludingGuests": "Kõik kasutajad, kaasa arvatud külalised",
"LabelAlreadyInYourLibrary": "Juba teie raamatukogus",
"LabelAppend": "Lisa",
"LabelAuthor": "Autor",
"LabelAuthorFirstLast": "Autor (Eesnimi Perekonnanimi)",
"LabelAuthorLastFirst": "Autor (Perekonnanimi, Eesnimi)",
"LabelAuthors": "Autorid",
"LabelAutoDownloadEpisodes": "Automaatne episoodide allalaadimine",
"LabelAutoFetchMetadata": "Automaatne metaandmete hankimine",
"LabelAutoFetchMetadataHelp": "Toob tiitli, autori ja seeria metaandmed üleslaadimise hõlbustamiseks. Lisametaandmed võivad pärast üleslaadimist vajada vastavust.",
"LabelAutoLaunch": "Automaatne käivitamine",
"LabelAutoLaunchDescription": "Suunab automaatselt autentimist pakkuvale teenusele, kui navigeeritakse sisselogimislehele (käsitsi ülekirjutuse tee <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Automaatne registreerimine",
"LabelAutoRegisterDescription": "Loo uued kasutajad automaatselt sisselogimisel",
"LabelBackToUser": "Tagasi kasutajale",
"LabelBackupLocation": "Varukoopia asukoht",
"LabelBackupsEnableAutomaticBackups": "Luba automaatsed varukoopiad",
"LabelBackupsEnableAutomaticBackupsHelp": "Varukoopiad salvestatakse /metadata/backups kausta",
"LabelBackupsMaxBackupSize": "Maksimaalne varukoopia suurus (GB-des)",
"LabelBackupsMaxBackupSizeHelp": "Kaitsena valesti seadistamise vastu ebaõnnestuvad varukoopiad, kui need ületavad seadistatud suuruse.",
"LabelBackupsNumberToKeep": "Varukoopiate arv, mida hoida",
"LabelBackupsNumberToKeepHelp": "Ühel ajal eemaldatakse ainult 1 varukoopia, seega kui teil on juba rohkem varukoopiaid kui siin määratud, peaksite need käsitsi eemaldama.",
"LabelBitrate": "Bittkiirus",
"LabelBooks": "Raamatud",
"LabelButtonText": "Nupu tekst",
"LabelChangePassword": "Muuda parooli",
"LabelChannels": "Kanalid",
"LabelChapters": "Peatükid",
"LabelChaptersFound": "peatükid leitud",
"LabelChapterTitle": "Peatüki pealkiri",
"LabelClickForMoreInfo": "Klõpsa lisateabe saamiseks",
"LabelClosePlayer": "Sulge mängija",
"LabelCodec": "Kodek",
"LabelCollapseSeries": "Ahenda seeria",
"LabelCollection": "Kogu",
"LabelCollections": "Kogud",
"LabelComplete": "Valmis",
"LabelConfirmPassword": "Kinnita parool",
"LabelContinueListening": "Jätka kuulamist",
"LabelContinueReading": "Jätka lugemist",
"LabelContinueSeries": "Jätka seeriat",
"LabelCover": "Ümbris",
"LabelCoverImageURL": "Ümbrise pildi URL",
"LabelCreatedAt": "Loodud",
"LabelCronExpression": "Croni valem",
"LabelCurrent": "Praegune",
"LabelCurrently": "Praegu:",
"LabelCustomCronExpression": "Kohandatud Croni valem:",
"LabelDatetime": "Kuupäev ja kellaaeg",
"LabelDeleteFromFileSystemCheckbox": "Kustuta failisüsteemist (ärge märkige seda ära, et eemaldada ainult andmebaasist)",
"LabelDescription": "Kirjeldus",
"LabelDeselectAll": "Tühista kõigi valimine",
"LabelDevice": "Seade",
"LabelDeviceInfo": "Seadme info",
"LabelDeviceIsAvailableTo": "Seade on saadaval kasutajale...",
"LabelDirectory": "Kataloog",
"LabelDiscFromFilename": "Ketas failinimest",
"LabelDiscFromMetadata": "Ketas metaandmetest",
"LabelDiscover": "Avasta",
"LabelDownload": "Lae alla",
"LabelDownloadNEpisodes": "Lae alla {0} episoodi",
"LabelDuration": "Kestus",
"LabelDurationFound": "Leitud kestus:",
"LabelEbook": "E-raamat",
"LabelEbooks": "E-raamatud",
"LabelEdit": "Muuda",
"LabelEmail": "E-post",
"LabelEmailSettingsFromAddress": "Saatja aadress",
"LabelEmailSettingsSecure": "Turvaline",
"LabelEmailSettingsSecureHelp": "Kui see on tõene, kasutab ühendus serveriga ühenduse loomisel TLS-i. Kui see on väär, kasutatakse TLS-i, kui server toetab STARTTLS-i laiendust. Enamikul juhtudest seadke see väärtus tõeks, kui ühendate pordile 465. Pordi 587 või 25 korral hoidke seda väär. (nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsTestAddress": "Testi aadress",
"LabelEmbeddedCover": "Manustatud kaas",
"LabelEnable": "Luba",
"LabelEnd": "Lõpp",
"LabelEpisode": "Episood",
"LabelEpisodeTitle": "Episoodi pealkiri",
"LabelEpisodeType": "Episoodi tüüp",
"LabelExample": "Näide",
"LabelExplicit": "Vulgaarne",
"LabelFeedURL": "Voogu URL",
"LabelFetchingMetadata": "Metaandmete hankimine",
"LabelFile": "Fail",
"LabelFileBirthtime": "Faili sünniaeg",
"LabelFileModified": "Faili muudetud",
"LabelFilename": "Failinimi",
"LabelFilterByUser": "Filtri alusel kasutaja järgi",
"LabelFindEpisodes": "Otsi episoodid",
"LabelFinished": "Lõpetatud",
"LabelFolder": "Kaust",
"LabelFolders": "Kataloogid",
"LabelFontBold": "Paks",
"LabelFontFamily": "Fondi pere",
"LabelFontItalic": "Kaldkiri",
"LabelFontScale": "Fondi suurus",
"LabelFontStrikethrough": "Üle joonitud",
"LabelFormat": "Vorming",
"LabelGenre": "Žanr",
"LabelGenres": "Žanrid",
"LabelHardDeleteFile": "Faili lõplik kustutamine",
"LabelHasEbook": "On e-raamat",
"LabelHasSupplementaryEbook": "On täiendav e-raamat",
"LabelHighestPriority": "Kõrgeim prioriteet",
"LabelHost": "Host",
"LabelHour": "Tund",
"LabelIcon": "Ikoon",
"LabelImageURLFromTheWeb": "Pildi URL veebist",
"LabelIncludeInTracklist": "Kaasa jälgimisloendis",
"LabelIncomplete": "Puudulik",
"LabelInProgress": "Pooleli",
"LabelInterval": "Intervall",
"LabelIntervalCustomDailyWeekly": "Kohandatud päevane/nädalane",
"LabelIntervalEvery12Hours": "Iga 12 tunni tagant",
"LabelIntervalEvery15Minutes": "Iga 15 minuti tagant",
"LabelIntervalEvery2Hours": "Iga 2 tunni tagant",
"LabelIntervalEvery30Minutes": "Iga 30 minuti tagant",
"LabelIntervalEvery6Hours": "Iga 6 tunni tagant",
"LabelIntervalEveryDay": "Iga päev",
"LabelIntervalEveryHour": "Iga tunni tagant",
"LabelInvalidParts": "Vigased osad",
"LabelInvert": "Pööra ümber",
"LabelItem": "Kirje",
"LabelLanguage": "Keel",
"LabelLanguageDefaultServer": "Vaikeserveri keel",
"LabelLastBookAdded": "Viimati lisatud raamat",
"LabelLastBookUpdated": "Viimati uuendatud raamat",
"LabelLastSeen": "Viimati nähtud",
"LabelLastTime": "Viimati aeg",
"LabelLastUpdate": "Viimane uuendus",
"LabelLayout": "Paigutus",
"LabelLayoutSinglePage": "Üks lehekülg",
"LabelLayoutSplitPage": "Jagatud lehekülg",
"LabelLess": "Vähem",
"LabelLibrariesAccessibleToUser": "Kasutajale ligipääsetavad raamatukogud",
"LabelLibrary": "Raamatukogu",
"LabelLibraryItem": "Raamatukogu kirje",
"LabelLibraryName": "Raamatukogu nimi",
"LabelLimit": "Piirang",
"LabelLineSpacing": "Joonevahe",
"LabelListenAgain": "Kuula uuesti",
"LabelLogLevelDebug": "Silumine",
"LabelLogLevelInfo": "Teave",
"LabelLogLevelWarn": "Hoiatus",
"LabelLookForNewEpisodesAfterDate": "Otsi uusi episoodid pärast seda kuupäeva",
"LabelLowestPriority": "Madalaim prioriteet",
"LabelMatchExistingUsersBy": "Sobita olemasolevad kasutajad",
"LabelMatchExistingUsersByDescription": "Kasutatakse olemasolevate kasutajate ühendamiseks. Ühendatud kasutajaid sobitatakse teie SSO pakkuja unikaalse ID järgi.",
"LabelMediaPlayer": "Meediapleier",
"LabelMediaType": "Meedia tüüp",
"LabelMetadataOrderOfPrecedenceDescription": "Kõrgema prioriteediga metaandmete allikad võtavad üle madalama prioriteediga metaandmete allikad",
"LabelMetadataProvider": "Metaandmete pakkuja",
"LabelMetaTag": "Meta märge",
"LabelMetaTags": "Meta märgendid",
"LabelMinute": "Minut",
"LabelMissing": "Puudub",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Puuduvad osad",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "Lubatud mobiilile suunamise URI-d",
"LabelMobileRedirectURIsDescription": "See on mobiilirakenduste jaoks kehtivate suunamise URI-de lubatud nimekiri. Vaikimisi on selleks <code>audiobookshelf://oauth</code>, mida saate eemaldada või täiendada täiendavate URI-dega kolmanda osapoole rakenduste integreerimiseks. Tärni (<code>*</code>) ainukese kirjena kasutamine võimaldab mis tahes URI-d.",
"LabelMore": "Rohkem",
"LabelMoreInfo": "Rohkem infot",
"LabelName": "Nimi",
"LabelNarrator": "Jutustaja",
"LabelNarrators": "Jutustajad",
"LabelNew": "Uus",
"LabelNewestAuthors": "Uusimad autorid",
"LabelNewestEpisodes": "Uusimad episoodid",
"LabelNewPassword": "Uus parool",
"LabelNextBackupDate": "Järgmine varukoopia kuupäev",
"LabelNextScheduledRun": "Järgmine ajakava järgmine",
"LabelNoEpisodesSelected": "Episoodid pole valitud",
"LabelNotes": "Märkused",
"LabelNotFinished": "Ei ole lõpetatud",
"LabelNotificationAppriseURL": "Apprise URL-id",
"LabelNotificationAvailableVariables": "Saadaolevad muutujad",
"LabelNotificationBodyTemplate": "Keha mall",
"LabelNotificationEvent": "Teavituse sündmus",
"LabelNotificationsMaxFailedAttempts": "Maksimaalsed ebaõnnestunud katsed",
"LabelNotificationsMaxFailedAttemptsHelp": "Teatised keelatakse, kui need ebaõnnestuvad nii palju kordi",
"LabelNotificationsMaxQueueSize": "Teavituste sündmuste maksimaalne järjekorra suurus",
"LabelNotificationsMaxQueueSizeHelp": "Sündmused on piiratud 1 sekundiga. Sündmusi ignoreeritakse, kui järjekord on maksimumsuuruses. See takistab teavituste rämpsposti.",
"LabelNotificationTitleTemplate": "Pealkirja mall",
"LabelNotStarted": "Pole alustatud",
"LabelNumberOfBooks": "Raamatute arv",
"LabelNumberOfEpisodes": "Episoodide arv",
"LabelOpenRSSFeed": "Ava RSS voog",
"LabelOverwrite": "Kirjuta üle",
"LabelPassword": "Parool",
"LabelPath": "Asukoht",
"LabelPermissionsAccessAllLibraries": "Saab ligi kõikidele raamatukogudele",
"LabelPermissionsAccessAllTags": "Saab ligi kõikidele siltidele",
"LabelPermissionsAccessExplicitContent": "Saab ligi vulgaarsele sisule",
"LabelPermissionsDelete": "Saab kustutada",
"LabelPermissionsDownload": "Saab alla laadida",
"LabelPermissionsUpdate": "Saab uuendada",
"LabelPermissionsUpload": "Saab üles laadida",
"LabelPersonalYearReview": "Your Year in Review ({0})",
"LabelPhotoPathURL": "Foto tee/URL",
"LabelPlaylists": "Mänguloendid",
"LabelPlayMethod": "Esitusmeetod",
"LabelPodcast": "Podcast",
"LabelPodcasts": "Podcastid",
"LabelPodcastSearchRegion": "Podcasti otsingu piirkond",
"LabelPodcastType": "Podcasti tüüp",
"LabelPort": "Port",
"LabelPrefixesToIgnore": "Eiramiseks eesliited (tõstutundetu)",
"LabelPreventIndexing": "Vältige oma voogu indekseerimist iTunes'i ja Google podcasti kataloogides",
"LabelPrimaryEbook": "Esmane e-raamat",
"LabelProgress": "Edenemine",
"LabelProvider": "Pakkuja",
"LabelPubDate": "Avaldamise kuupäev",
"LabelPublisher": "Kirjastaja",
"LabelPublishYear": "Aasta avaldamine",
"LabelRead": "Lugenud",
"LabelReadAgain": "Loe uuesti",
"LabelReadEbookWithoutProgress": "Lugege e-raamatut ilma edenemist säilitamata",
"LabelRecentlyAdded": "Hiljuti lisatud",
"LabelRecentSeries": "Hiljutised seeriad",
"LabelRecommended": "Soovitatud",
"LabelRedo": "Tee uuesti",
"LabelRegion": "Piirkond",
"LabelReleaseDate": "Väljalaske kuupäev",
"LabelRemoveCover": "Eemalda ümbris",
"LabelRowsPerPage": "Rida lehe kohta",
"LabelRSSFeedCustomOwnerEmail": "Kohandatud omaniku e-post",
"LabelRSSFeedCustomOwnerName": "Kohandatud omaniku nimi",
"LabelRSSFeedOpen": "Ava RSS voog",
"LabelRSSFeedPreventIndexing": "Vältige indekseerimist",
"LabelRSSFeedSlug": "RSS voog Slug",
"LabelRSSFeedURL": "RSS voog URL",
"LabelSearchTerm": "Otsingutermin",
"LabelSearchTitle": "Otsi pealkirja",
"LabelSearchTitleOrASIN": "Otsi pealkirja või ASIN-i",
"LabelSeason": "Hooaeg",
"LabelSelectAllEpisodes": "Vali kõik episoodid",
"LabelSelectEpisodesShowing": "Valige {0} näidatavat episoodi",
"LabelSelectUsers": "Valige kasutajad",
"LabelSendEbookToDevice": "Saada e-raamat seadmele...",
"LabelSequence": "Järjestus",
"LabelSeries": "Seeria",
"LabelSeriesName": "Seeria nimi",
"LabelSeriesProgress": "Seeria edenemine",
"LabelServerYearReview": "Server Year in Review ({0})",
"LabelSetEbookAsPrimary": "Määra peamiseks",
"LabelSetEbookAsSupplementary": "Määra täiendavaks",
"LabelSettingsAudiobooksOnly": "Ainult heliraamatud",
"LabelSettingsAudiobooksOnlyHelp": "Selle seadistuse lubamine eirab e-raamatute faile, välja arvatud juhul, kui need on heliraamatu kaustas, kus need seatakse täiendavate e-raamatutena",
"LabelSettingsBookshelfViewHelp": "Skeumorfne kujundus puidust riiulitega",
"LabelSettingsChromecastSupport": "Chromecasti tugi",
"LabelSettingsDateFormat": "Kuupäeva vorming",
"LabelSettingsDisableWatcher": "Keela vaatamine",
"LabelSettingsDisableWatcherForLibrary": "Keela kaustavaatamine raamatukogu jaoks",
"LabelSettingsDisableWatcherHelp": "Keelab automaatse lisamise/uuendamise, kui failimuudatusi tuvastatakse. *Nõuab serveri taaskäivitamist",
"LabelSettingsEnableWatcher": "Luba vaatamine",
"LabelSettingsEnableWatcherForLibrary": "Luba kaustavaatamine raamatukogu jaoks",
"LabelSettingsEnableWatcherHelp": "Lubab automaatset lisamist/uuendamist, kui tuvastatakse failimuudatused. *Nõuab serveri taaskäivitamist",
"LabelSettingsExperimentalFeatures": "Eksperimentaalsed funktsioonid",
"LabelSettingsExperimentalFeaturesHelp": "Arengus olevad funktsioonid, mis vajavad teie tagasisidet ja abi testimisel. Klõpsake GitHubi arutelu avamiseks.",
"LabelSettingsFindCovers": "Leia ümbrised",
"LabelSettingsFindCoversHelp": "Kui teie heliraamatul pole sisseehitatud ümbrist ega ümbrise pilti kaustas, proovib skanner leida ümbrist.<br>Märkus: see pikendab skaneerimisaega",
"LabelSettingsHideSingleBookSeries": "Peida üksikute raamatute seeriad",
"LabelSettingsHideSingleBookSeriesHelp": "Ühe raamatuga seeriaid peidetakse seeria lehelt ja avalehe riiulitelt.",
"LabelSettingsHomePageBookshelfView": "Avaleht kasutage raamatukoguvaadet",
"LabelSettingsLibraryBookshelfView": "Raamatukogu kasutamiseks kasutage raamatukoguvaadet",
"LabelSettingsParseSubtitles": "Lugege subtiitreid",
"LabelSettingsParseSubtitlesHelp": "Eraldage subtiitrid heliraamatu kaustade nimedest.<br>Subtiitrid peavad olema eraldatud \" - \".<br>Näiteks: \"Raamatu pealkiri - Siin on alapealkiri\" alapealkiri on \"Siin on alapealkiri\"",
"LabelSettingsPreferMatchedMetadata": "Eelista sobitatud metaandmeid",
"LabelSettingsPreferMatchedMetadataHelp": "Sobitatud andmed kirjutavad Kiir Sobitamise kasutamisel üle üksikasjad.",
"LabelSettingsSkipMatchingBooksWithASIN": "Jätke ASIN-iga sobituvad raamatud vahele",
"LabelSettingsSkipMatchingBooksWithISBN": "Jätke ISBN-iga sobituvad raamatud vahele",
"LabelSettingsSortingIgnorePrefixes": "Ignoreeri eesliiteid sortimisel",
"LabelSettingsSortingIgnorePrefixesHelp": "nt. eesliidet \"the\" kasutades raamatu pealkiri \"The Book Title\" sorteeritakse \"Book Title, The\"",
"LabelSettingsSquareBookCovers": "Kasutage ruudukujulisi raamatu kaasi",
"LabelSettingsSquareBookCoversHelp": "Eelistage ruudukujulisi kaasi tavaliste 1.6:1 raamatu ümbrise asemel",
"LabelSettingsStoreCoversWithItem": "Salvesta kaaned üksusega",
"LabelSettingsStoreCoversWithItemHelp": "Vaikimisi salvestatakse kaaned /metadata/items kausta. Selle seadistuse lubamine salvestab kaaned teie raamatukogu üksuse kausta. Hoitakse ainult ühte faili nimega \"kaas\"",
"LabelSettingsStoreMetadataWithItem": "Salvesta metaandmed üksusega",
"LabelSettingsStoreMetadataWithItemHelp": "Vaikimisi salvestatakse metaandmed /metadata/items kausta. Selle seadistuse lubamine salvestab metaandmed teie raamatukogu üksuse kaustadesse",
"LabelSettingsTimeFormat": "Kellaaja vorming",
"LabelShowAll": "Näita kõiki",
"LabelSize": "Suurus",
"LabelSleepTimer": "Uinaku taimer",
"LabelSlug": "Slug",
"LabelStart": "Alusta",
"LabelStarted": "Alustatud",
"LabelStartedAt": "Alustatud",
"LabelStartTime": "Alustamise aeg",
"LabelStatsAudioTracks": "Audiojäljed",
"LabelStatsAuthors": "Autorid",
"LabelStatsBestDay": "Parim päev",
"LabelStatsDailyAverage": "Päevane keskmine",
"LabelStatsDays": "Päevad",
"LabelStatsDaysListened": "Kuulatud päevad",
"LabelStatsHours": "Tunnid",
"LabelStatsInARow": "järjest",
"LabelStatsItemsFinished": "Lõpetatud üksused",
"LabelStatsItemsInLibrary": "Üksused raamatukogus",
"LabelStatsMinutes": "minutit",
"LabelStatsMinutesListening": "Kuulamise minutid",
"LabelStatsOverallDays": "Kokku päevad",
"LabelStatsOverallHours": "Kokku tunnid",
"LabelStatsWeekListening": "Nädala kuulamine",
"LabelSubtitle": "Alapealkiri",
"LabelSupportedFileTypes": "Toetatud failitüübid",
"LabelTag": "Silt",
"LabelTags": "Sildid",
"LabelTagsAccessibleToUser": "Kasutajale kättesaadavad sildid",
"LabelTagsNotAccessibleToUser": "Kasutajale mittekättesaadavad sildid",
"LabelTasks": "Käimasolevad ülesanded",
"LabelTextEditorBulletedList": "Punktloend",
"LabelTextEditorLink": "Link",
"LabelTextEditorNumberedList": "Numberloend",
"LabelTextEditorUnlink": "Eemalda link",
"LabelTheme": "Teema",
"LabelThemeDark": "Tume",
"LabelThemeLight": "Hele",
"LabelTimeBase": "Aja alus",
"LabelTimeListened": "Kuulatud aeg",
"LabelTimeListenedToday": "Täna kuulatud aeg",
"LabelTimeRemaining": "{0} jäänud",
"LabelTimeToShift": "Nihutamiseks sekundites kuluv aeg",
"LabelTitle": "Pealkiri",
"LabelToolsEmbedMetadata": "Manusta metaandmed",
"LabelToolsEmbedMetadataDescription": "Manusta metaandmed helifailidesse, sealhulgas kaanepilt ja peatükid.",
"LabelToolsMakeM4b": "Loo M4B heliraamatu fail",
"LabelToolsMakeM4bDescription": "Loo .M4B heliraamatu fail, kuhu on manustatud metaandmed, kaanepilt ja peatükid.",
"LabelToolsSplitM4b": "Jaga M4B MP3-deks",
"LabelToolsSplitM4bDescription": "Loo MP3-d M4B-st peatükkide kaupa, kus on manustatud metaandmed, kaanepilt ja peatükid.",
"LabelTotalDuration": "Kogukestus",
"LabelTotalTimeListened": "Kogu kuulatud aeg",
"LabelTrackFromFilename": "Jälg nimest",
"LabelTrackFromMetadata": "Jälg metaandmetest",
"LabelTracks": "Jäljed",
"LabelTracksMultiTrack": "Mitmejälg",
"LabelTracksNone": "Ühtegi jälgimist",
"LabelTracksSingleTrack": "Üksikjälg",
"LabelType": "Tüüp",
"LabelUnabridged": "Täismahus",
"LabelUndo": "Võta tagasi",
"LabelUnknown": "Tundmatu",
"LabelUpdateCover": "Uuenda kaant",
"LabelUpdateCoverHelp": "Luba üle kirjutamine olemasolevate kaante jaoks valitud raamatutele, kui leitakse sobivus",
"LabelUpdatedAt": "Uuendatud",
"LabelUpdateDetails": "Uuenda üksikasju",
"LabelUpdateDetailsHelp": "Luba üle kirjutamine olemasolevate üksikasjade jaoks valitud raamatutele, kui leitakse sobivus",
"LabelUploaderDragAndDrop": "Lohista ja aseta faile või kaustu",
"LabelUploaderDropFiles": "Aseta failid",
"LabelUploaderItemFetchMetadataHelp": "Hangi automaatselt pealkiri, autor ja seeria",
"LabelUseChapterTrack": "Kasuta peatüki jälge",
"LabelUseFullTrack": "Kasuta täielikku jälge",
"LabelUser": "Kasutaja",
"LabelUsername": "Kasutajanimi",
"LabelValue": "Väärtus",
"LabelVersion": "Versioon",
"LabelViewBookmarks": "Vaata järjehoidjaid",
"LabelViewChapters": "Vaata peatükke",
"LabelViewQueue": "Vaata esitusjärjekorda",
"LabelVolume": "Heli tugevus",
"LabelWeekdaysToRun": "Päevad nädalas käivitamiseks",
"LabelYearReviewHide": "Hide Year in Review",
"LabelYearReviewShow": "See Year in Review",
"LabelYourAudiobookDuration": "Teie heliraamatu kestus",
"LabelYourBookmarks": "Teie järjehoidjad",
"LabelYourPlaylists": "Teie esitusloendid",
"LabelYourProgress": "Teie edenemine",
"MessageAddToPlayerQueue": "Lisa esitusjärjekorda",
"MessageAppriseDescription": "Selle funktsiooni kasutamiseks peate käivitama <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> eksemplari või API, mis töötleb samu päringuid. <br />Apprise API URL peaks olema täielik URL-rada teatise saatmiseks, näiteks kui teie API eksemplar töötab aadressil <code>http://192.168.1.1:8337</code>, siis peaksite sisestama <code>http://192.168.1.1:8337/notify</code>.",
"MessageBackupsDescription": "Varukoopiad hõlmavad kasutajaid, kasutajate edenemist, raamatukogu üksikasju, serveri seadeid ja kaustades <code>/metadata/items</code> ja <code>/metadata/authors</code> salvestatud pilte. Varukoopiad ei hõlma ühtegi teie raamatukogu kaustades olevat faili.",
"MessageBatchQuickMatchDescription": "Kiire sobitamine üritab lisada valitud üksustele puuduvad kaaned ja metaandmed. Luba allpool olevad valikud, et lubada Kiire sobitamine'il üle kirjutada olemasolevaid kaasi ja/või metaandmeid.",
"MessageBookshelfNoCollections": "Te pole veel ühtegi kogumit teinud",
"MessageBookshelfNoResultsForFilter": "Filtrile \"{0}: {1}\" pole tulemusi",
"MessageBookshelfNoRSSFeeds": "Ühtegi RSS-i voogu pole avatud",
"MessageBookshelfNoSeries": "Teil pole ühtegi seeriat",
"MessageChapterEndIsAfter": "Peatüki lõpp on pärast teie heliraamatu lõppu",
"MessageChapterErrorFirstNotZero": "Esimene peatükk peab algama 0-st",
"MessageChapterErrorStartGteDuration": "Vigane algusaeg peab olema väiksem kui heliraamatu kestus",
"MessageChapterErrorStartLtPrev": "Vigane algusaeg peab olema suurem või võrdne eelneva peatüki algusajaga",
"MessageChapterStartIsAfter": "Peatüki algus on pärast teie heliraamatu lõppu",
"MessageCheckingCron": "Croni kontrollimine...",
"MessageConfirmCloseFeed": "Olete kindel, et soovite selle voo sulgeda?",
"MessageConfirmDeleteBackup": "Olete kindel, et soovite varukoopia kustutada {0} kohta?",
"MessageConfirmDeleteFile": "See kustutab faili teie failisüsteemist. Olete kindel?",
"MessageConfirmDeleteLibrary": "Olete kindel, et soovite raamatukogu \"{0}\" lõplikult kustutada?",
"MessageConfirmDeleteLibraryItem": "See kustutab raamatukogu üksuse andmebaasist ja failisüsteemist. Olete kindel?",
"MessageConfirmDeleteLibraryItems": "See kustutab {0} raamatukogu üksust andmebaasist ja failisüsteemist. Olete kindel?",
"MessageConfirmDeleteSession": "Olete kindel, et soovite selle seansi kustutada?",
"MessageConfirmForceReScan": "Olete kindel, et soovite jõuga uuesti skannida?",
"MessageConfirmMarkAllEpisodesFinished": "Olete kindel, et soovite kõik episoodid lõpetatuks märkida?",
"MessageConfirmMarkAllEpisodesNotFinished": "Olete kindel, et soovite kõik episoodid mitte lõpetatuks märkida?",
"MessageConfirmMarkSeriesFinished": "Olete kindel, et soovite selle seeria kõik raamatud lõpetatuks märkida?",
"MessageConfirmMarkSeriesNotFinished": "Olete kindel, et soovite selle seeria kõik raamatud mitte lõpetatuks märkida?",
"MessageConfirmQuickEmbed": "Hoiatus! Quick Embed ei tee varukoopiaid teie helifailidest. Veenduge, et teil oleks varukoopia oma helifailidest. <br><br>Kas soovite jätkata?",
"MessageConfirmRemoveAllChapters": "Olete kindel, et soovite eemaldada kõik peatükid?",
"MessageConfirmRemoveAuthor": "Olete kindel, et soovite autori \"{0}\" eemaldada?",
"MessageConfirmRemoveCollection": "Olete kindel, et soovite kogumi \"{0}\" eemaldada?",
"MessageConfirmRemoveEpisode": "Olete kindel, et soovite episoodi \"{0}\" eemaldada?",
"MessageConfirmRemoveEpisodes": "Olete kindel, et soovite eemaldada {0} episoodi?",
"MessageConfirmRemoveListeningSessions": "Olete kindel, et soovite eemaldada {0} kuulamise sessiooni?",
"MessageConfirmRemoveNarrator": "Olete kindel, et soovite jutustaja \"{0}\" eemaldada?",
"MessageConfirmRemovePlaylist": "Olete kindel, et soovite eemaldada oma esitusloendi \"{0}\"?",
"MessageConfirmRenameGenre": "Olete kindel, et soovite žanri \"{0}\" ümber nimetada kujule \"{1}\" kõikidele üksustele?",
"MessageConfirmRenameGenreMergeNote": "Märkus: See žanr on juba olemas, nii et need ühendatakse.",
"MessageConfirmRenameGenreWarning": "Hoiatus! Sarnane žanr erineva puhvriga on juba olemas \"{0}\".",
"MessageConfirmRenameTag": "Olete kindel, et soovite silti \"{0}\" ümber nimetada kujule \"{1}\" kõikidele üksustele?",
"MessageConfirmRenameTagMergeNote": "Märkus: See silt on juba olemas, nii et need ühendatakse.",
"MessageConfirmRenameTagWarning": "Hoiatus! Sarnane silt erineva puhvriga on juba olemas \"{0}\".",
"MessageConfirmReScanLibraryItems": "Olete kindel, et soovite uuesti skannida {0} üksust?",
"MessageConfirmSendEbookToDevice": "Olete kindel, et soovite saata {0} e-raamatu \"{1}\" seadmesse \"{2}\"?",
"MessageDownloadingEpisode": "Episoodi allalaadimine",
"MessageDragFilesIntoTrackOrder": "Lohistage failid õigesse järjekorda",
"MessageEmbedFinished": "Manustamine lõpetatud!",
"MessageEpisodesQueuedForDownload": "{0} Episood(i) on allalaadimiseks järjekorras",
"MessageFeedURLWillBe": "Toite URL saab olema {0}",
"MessageFetching": "Hangitakse...",
"MessageForceReScanDescription": "skaneerib kõik failid uuesti nagu värsket skannimist. Heli faili ID3 silte, OPF faile ja tekstifaile skaneeritakse uuesti.",
"MessageImportantNotice": "Oluline märkus!",
"MessageInsertChapterBelow": "Sisesta peatükk allapoole",
"MessageItemsSelected": "{0} Valitud üksust",
"MessageItemsUpdated": "{0} Üksust on uuendatud",
"MessageJoinUsOn": "Liitu meiega",
"MessageListeningSessionsInTheLastYear": "Kuulamissessioone viimase aasta jooksul: {0}",
"MessageLoading": "Laadimine...",
"MessageLoadingFolders": "Kaustade laadimine...",
"MessageM4BFailed": "M4B ebaõnnestus!",
"MessageM4BFinished": "M4B lõpetatud!",
"MessageMapChapterTitles": "Kaarda peatükkide pealkirjad olemasolevatele heliraamatu peatükkidele, ajatempe ei muudeta",
"MessageMarkAllEpisodesFinished": "Märgi kõik episoodid lõpetatuks",
"MessageMarkAllEpisodesNotFinished": "Märgi kõik episoodid mitte lõpetatuks",
"MessageMarkAsFinished": "Märgi lõpetatuks",
"MessageMarkAsNotFinished": "Märgi mitte lõpetatuks",
"MessageMatchBooksDescription": "üritab raamatuid raamatukogus sobitada otsingupakkujast leitud raamatuga ning täita tühjad üksikasjad ja kaas. Ei üle kirjuta üksikasju.",
"MessageNoAudioTracks": "Ühtegi helijälge pole",
"MessageNoAuthors": "Ühtegi autori pole",
"MessageNoBackups": "Ühtegi varukoopia pole",
"MessageNoBookmarks": "Ühtegi järjehoidjat pole",
"MessageNoChapters": "Ühtegi peatükki pole",
"MessageNoCollections": "Ühtegi kogumit pole",
"MessageNoCoversFound": "Ühtegi kaant pole leitud",
"MessageNoDescription": "Kirjeldust pole",
"MessageNoDownloadsInProgress": "Praegu allalaadimisi pole",
"MessageNoDownloadsQueued": "Pole järjekorras allalaadimisi",
"MessageNoEpisodeMatchesFound": "Ühtegi episoodi vastet pole leitud",
"MessageNoEpisodes": "Ühtegi episoodi pole",
"MessageNoFoldersAvailable": "Ühtegi kausta pole saadaval",
"MessageNoGenres": "Ühtegi žanrit pole",
"MessageNoIssues": "Ühtegi probleemi pole",
"MessageNoItems": "Ühtegi üksust pole",
"MessageNoItemsFound": "Ühtegi üksust pole leitud",
"MessageNoListeningSessions": "Ühtegi kuulamissessiooni pole",
"MessageNoLogs": "Ühtegi logi pole",
"MessageNoMediaProgress": "Ühtegi meediaprogressi pole",
"MessageNoNotifications": "Ühtegi teavitust pole",
"MessageNoPodcastsFound": "Ühtegi podcasti pole leitud",
"MessageNoResults": "Ühtegi tulemust pole",
"MessageNoSearchResultsFor": "Otsingutulemusi pole märksõna kohta: \"{0}\"",
"MessageNoSeries": "Ühtegi seeriat pole",
"MessageNoTags": "Ühtegi silti pole",
"MessageNoTasksRunning": "Ühtegi käimasolevat ülesannet pole",
"MessageNotYetImplemented": "Pole veel ellu viidud",
"MessageNoUpdateNecessary": "Ühtegi värskendust pole vaja",
"MessageNoUpdatesWereNecessary": "Ühtegi värskendust polnud vaja",
"MessageNoUserPlaylists": "Teil pole ühtegi esitusloendit",
"MessageOr": "või",
"MessagePauseChapter": "Peata peatüki esitamine",
"MessagePlayChapter": "Kuula peatüki algust",
"MessagePlaylistCreateFromCollection": "Loo esitusloend kogumist",
"MessagePodcastHasNoRSSFeedForMatching": "Podcastil pole sobitamiseks RSS-voogu",
"MessageQuickMatchDescription": "täidab tühjad üksikasjad ja kaaned raamatukogus esimese otsingutulemusega rakendusest '{0}'. Ei üle kirjuta üksikasju, välja arvatud juhul, kui serveri sätetes on lubatud 'Eelista sobitatud metaandmeid'.",
"MessageRemoveChapter": "Eemalda peatükk",
"MessageRemoveEpisodes": "Eemalda {0} episood(i)",
"MessageRemoveFromPlayerQueue": "Eemalda esitusjärjekorrast",
"MessageRemoveUserWarning": "Olete kindel, et soovite kasutaja \"{0}\" lõplikult kustutada?",
"MessageReportBugsAndContribute": "Raporteeri vigu, palu funktsioone ja aita kaasa",
"MessageResetChaptersConfirm": "Olete kindel, et soovite peatükkide lähtestada ja tehtud muudatused tagasi võtta?",
"MessageRestoreBackupConfirm": "Olete kindel, et soovite taastada varukoopia, mis loodi",
"MessageRestoreBackupWarning": "Varukoopia taastamine kirjutab üle kogu /config ja /metadata/items & /metadata/authors kaustas oleva andmebaasi. <br /><br />Varukoopiad ei muuda teie raamatukogukaustades olevaid faile. Kui olete lubanud serveri sätetel salvestada kaane kunsti ja metaandmed teie raamatukogu kaustadesse, siis neid ei varundata ega kirjutata üle.<br /><br />Kõik teie serveri kasutavad kliendid värskendatakse automaatselt.",
"MessageSearchResultsFor": "Otsingutulemused märksõnale",
"MessageSelected": "{0} valitud",
"MessageServerCouldNotBeReached": "Serveriga ei saanud ühendust luua",
"MessageSetChaptersFromTracksDescription": "Määrake peatükid, kasutades iga helifaili peatükina ja peatüki pealkirjana helifaili nime",
"MessageStartPlaybackAtTime": "Alustage \"{0}\" esitamist kell {1}?",
"MessageThinking": "Mõtlen...",
"MessageUploaderItemFailed": "Üleslaadimine ebaõnnestus",
"MessageUploaderItemSuccess": "Edukalt üles laaditud!",
"MessageUploading": "Üles laadimine...",
"MessageValidCronExpression": "Kehtiv cron-väljend",
"MessageWatcherIsDisabledGlobally": "Vaatleja on ülemaailmselt keelatud serveri sätetes",
"MessageXLibraryIsEmpty": "{0} raamatukogu on tühi!",
"MessageYourAudiobookDurationIsLonger": "Teie heliraamatu kestus on pikem kui leitud kestus",
"MessageYourAudiobookDurationIsShorter": "Teie heliraamatu kestus on lühem kui leitud kestus",
"NoteChangeRootPassword": "Root kasutajal võib olla ainus kasutaja, kellel võib olla tühi parool",
"NoteChapterEditorTimes": "Märkus: Esimese peatüki algusaeg peab jääma 0:00 ja viimase peatüki algusaeg ei tohi ületada selle heliraamatu kestust.",
"NoteFolderPicker": "Märkus: juba kaardistatud kaustu ei kuvata",
"NoteRSSFeedPodcastAppsHttps": "Hoiatus: Enamik podcasti rakendusi nõuab, et RSS-voogu URL kasutaks HTTPS-i",
"NoteRSSFeedPodcastAppsPubDate": "Hoiatus: Üks või mitu teie episoodi ei sisalda publikatsioonikuupäeva. Mõned podcasti rakendused nõuavad seda.",
"NoteUploaderFoldersWithMediaFiles": "Kaustu, kus on meediat, käsitletakse eraldi raamatukogu üksustena.",
"NoteUploaderOnlyAudioFiles": "Kui laadite üles ainult helifaile, käsitletakse iga helifaili eraldi heliraamatuna.",
"NoteUploaderUnsupportedFiles": "Toetamata failid jäetakse tähelepanuta. Kausta valimisel või lohistamisel jäetakse tähelepanuta muud failid, mis pole üksuse kaustas.",
"PlaceholderNewCollection": "Uue kogumi nimi",
"PlaceholderNewFolderPath": "Uus kausta tee",
"PlaceholderNewPlaylist": "Uue esitusloendi nimi",
"PlaceholderSearch": "Otsi...",
"PlaceholderSearchEpisode": "Otsi episoodi...",
"ToastAccountUpdateFailed": "Konto värskendamine ebaõnnestus",
"ToastAccountUpdateSuccess": "Konto on värskendatud",
"ToastAuthorImageRemoveFailed": "Pildi eemaldamine ebaõnnestus",
"ToastAuthorImageRemoveSuccess": "Autori pilt on eemaldatud",
"ToastAuthorUpdateFailed": "Autori värskendamine ebaõnnestus",
"ToastAuthorUpdateMerged": "Autor liidetud",
"ToastAuthorUpdateSuccess": "Autor värskendatud",
"ToastAuthorUpdateSuccessNoImageFound": "Autor värskendatud (pilti ei leitud)",
"ToastBackupCreateFailed": "Varukoopia loomine ebaõnnestus",
"ToastBackupCreateSuccess": "Varukoopia loodud",
"ToastBackupDeleteFailed": "Varukoopia kustutamine ebaõnnestus",
"ToastBackupDeleteSuccess": "Varukoopia kustutatud",
"ToastBackupRestoreFailed": "Varukoopia taastamine ebaõnnestus",
"ToastBackupUploadFailed": "Varukoopia üles laadimine ebaõnnestus",
"ToastBackupUploadSuccess": "Varukoopia üles laaditud",
"ToastBatchUpdateFailed": "Partii värskendamine ebaõnnestus",
"ToastBatchUpdateSuccess": "Partii värskendamine õnnestus",
"ToastBookmarkCreateFailed": "Järjehoidja loomine ebaõnnestus",
"ToastBookmarkCreateSuccess": "Järjehoidja lisatud",
"ToastBookmarkRemoveFailed": "Järjehoidja eemaldamine ebaõnnestus",
"ToastBookmarkRemoveSuccess": "Järjehoidja eemaldatud",
"ToastBookmarkUpdateFailed": "Järjehoidja värskendamine ebaõnnestus",
"ToastBookmarkUpdateSuccess": "Järjehoidja värskendatud",
"ToastChaptersHaveErrors": "Peatükkidel on vigu",
"ToastChaptersMustHaveTitles": "Peatükkidel peab olema pealkiri",
"ToastCollectionItemsRemoveFailed": "Üksuse(te) eemaldamine kogumist ebaõnnestus",
"ToastCollectionItemsRemoveSuccess": "Üksus(ed) eemaldatud kogumist",
"ToastCollectionRemoveFailed": "Kogumi eemaldamine ebaõnnestus",
"ToastCollectionRemoveSuccess": "Kogum eemaldatud",
"ToastCollectionUpdateFailed": "Kogumi värskendamine ebaõnnestus",
"ToastCollectionUpdateSuccess": "Kogum värskendatud",
"ToastItemCoverUpdateFailed": "Üksuse kaane värskendamine ebaõnnestus",
"ToastItemCoverUpdateSuccess": "Üksuse kaas värskendatud",
"ToastItemDetailsUpdateFailed": "Üksuse üksikasjade värskendamine ebaõnnestus",
"ToastItemDetailsUpdateSuccess": "Üksuse üksikasjad värskendatud",
"ToastItemDetailsUpdateUnneeded": "Üksuse üksikasjade värskendamine pole vajalik",
"ToastItemMarkedAsFinishedFailed": "Märgistamine kui lõpetatud ebaõnnestus",
"ToastItemMarkedAsFinishedSuccess": "Üksus märgitud kui lõpetatud",
"ToastItemMarkedAsNotFinishedFailed": "Märgistamine kui mitte lõpetatud ebaõnnestus",
"ToastItemMarkedAsNotFinishedSuccess": "Üksus märgitud kui mitte lõpetatud",
"ToastLibraryCreateFailed": "Raamatukogu loomine ebaõnnestus",
"ToastLibraryCreateSuccess": "Raamatukogu \"{0}\" loodud",
"ToastLibraryDeleteFailed": "Raamatukogu kustutamine ebaõnnestus",
"ToastLibraryDeleteSuccess": "Raamatukogu kustutatud",
"ToastLibraryScanFailedToStart": "Skanneerimine ei käivitunud",
"ToastLibraryScanStarted": "Raamatukogu skaneerimine alustatud",
"ToastLibraryUpdateFailed": "Raamatukogu värskendamine ebaõnnestus",
"ToastLibraryUpdateSuccess": "Raamatukogu \"{0}\" värskendatud",
"ToastPlaylistCreateFailed": "Esitusloendi loomine ebaõnnestus",
"ToastPlaylistCreateSuccess": "Esitusloend loodud",
"ToastPlaylistRemoveFailed": "Esitusloendi eemaldamine ebaõnnestus",
"ToastPlaylistRemoveSuccess": "Esitusloend eemaldatud",
"ToastPlaylistUpdateFailed": "Esitusloendi värskendamine ebaõnnestus",
"ToastPlaylistUpdateSuccess": "Esitusloend värskendatud",
"ToastPodcastCreateFailed": "Podcasti loomine ebaõnnestus",
"ToastPodcastCreateSuccess": "Podcast loodud edukalt",
"ToastRemoveItemFromCollectionFailed": "Üksuse eemaldamine kogumist ebaõnnestus",
"ToastRemoveItemFromCollectionSuccess": "Üksus eemaldatud kogumist",
"ToastRSSFeedCloseFailed": "RSS-voogu sulgemine ebaõnnestus",
"ToastRSSFeedCloseSuccess": "RSS-voog suletud",
"ToastSendEbookToDeviceFailed": "E-raamatu saatmine seadmesse ebaõnnestus",
"ToastSendEbookToDeviceSuccess": "E-raamat saadetud seadmesse \"{0}\"",
"ToastSeriesUpdateFailed": "Sarja värskendamine ebaõnnestus",
"ToastSeriesUpdateSuccess": "Sarja värskendamine õnnestus",
"ToastSessionDeleteFailed": "Seansi kustutamine ebaõnnestus",
"ToastSessionDeleteSuccess": "Sessioon kustutatud",
"ToastSocketConnected": "Pesa ühendatud",
"ToastSocketDisconnected": "Pesa ühendus katkenud",
"ToastSocketFailedToConnect": "Pesa ühendamine ebaõnnestus",
"ToastUserDeleteFailed": "Kasutaja kustutamine ebaõnnestus",
"ToastUserDeleteSuccess": "Kasutaja kustutatud"
}

View File

@ -7,7 +7,7 @@
"ButtonAddUser": "Ajouter un utilisateur", "ButtonAddUser": "Ajouter un utilisateur",
"ButtonAddYourFirstLibrary": "Ajouter votre première bibliothèque", "ButtonAddYourFirstLibrary": "Ajouter votre première bibliothèque",
"ButtonApply": "Appliquer", "ButtonApply": "Appliquer",
"ButtonApplyChapters": "Appliquer les chapitres", "ButtonApplyChapters": "Appliquer aux chapitres",
"ButtonAuthors": "Auteurs", "ButtonAuthors": "Auteurs",
"ButtonBrowseForFolder": "Naviguer vers le répertoire", "ButtonBrowseForFolder": "Naviguer vers le répertoire",
"ButtonCancel": "Annuler", "ButtonCancel": "Annuler",
@ -32,6 +32,8 @@
"ButtonHide": "Cacher", "ButtonHide": "Cacher",
"ButtonHome": "Accueil", "ButtonHome": "Accueil",
"ButtonIssues": "Parutions", "ButtonIssues": "Parutions",
"ButtonJumpBackward": "Retour",
"ButtonJumpForward": "Avancer",
"ButtonLatest": "Dernière version", "ButtonLatest": "Dernière version",
"ButtonLibrary": "Bibliothèque", "ButtonLibrary": "Bibliothèque",
"ButtonLogout": "Me déconnecter", "ButtonLogout": "Me déconnecter",
@ -41,12 +43,17 @@
"ButtonMatchAllAuthors": "Chercher tous les auteurs", "ButtonMatchAllAuthors": "Chercher tous les auteurs",
"ButtonMatchBooks": "Chercher les livres", "ButtonMatchBooks": "Chercher les livres",
"ButtonNevermind": "Non merci", "ButtonNevermind": "Non merci",
"ButtonNext": "Suivant",
"ButtonNextChapter": "Chapitre suivant",
"ButtonOk": "Ok", "ButtonOk": "Ok",
"ButtonOpenFeed": "Ouvrir le flux", "ButtonOpenFeed": "Ouvrir le flux",
"ButtonOpenManager": "Ouvrir le gestionnaire", "ButtonOpenManager": "Ouvrir le gestionnaire",
"ButtonPause": "Pause",
"ButtonPlay": "Écouter", "ButtonPlay": "Écouter",
"ButtonPlaying": "En lecture", "ButtonPlaying": "En lecture",
"ButtonPlaylists": "Listes de lecture", "ButtonPlaylists": "Listes de lecture",
"ButtonPrevious": "Précédent",
"ButtonPreviousChapter": "Chapitre précédent",
"ButtonPurgeAllCache": "Purger le cache", "ButtonPurgeAllCache": "Purger le cache",
"ButtonPurgeItemsCache": "Purger le cache des articles", "ButtonPurgeItemsCache": "Purger le cache des articles",
"ButtonPurgeMediaProgress": "Purger la progression des médias", "ButtonPurgeMediaProgress": "Purger la progression des médias",
@ -54,6 +61,7 @@
"ButtonQueueRemoveItem": "Supprimer de la liste de lecture", "ButtonQueueRemoveItem": "Supprimer de la liste de lecture",
"ButtonQuickMatch": "Recherche rapide", "ButtonQuickMatch": "Recherche rapide",
"ButtonRead": "Lire", "ButtonRead": "Lire",
"ButtonRefresh": "Rafraîchir",
"ButtonRemove": "Supprimer", "ButtonRemove": "Supprimer",
"ButtonRemoveAll": "Supprimer tout", "ButtonRemoveAll": "Supprimer tout",
"ButtonRemoveAllLibraryItems": "Supprimer tous les articles de la bibliothèque", "ButtonRemoveAllLibraryItems": "Supprimer tous les articles de la bibliothèque",
@ -73,6 +81,7 @@
"ButtonSelectFolderPath": "Sélectionner le chemin du dossier", "ButtonSelectFolderPath": "Sélectionner le chemin du dossier",
"ButtonSeries": "Séries", "ButtonSeries": "Séries",
"ButtonSetChaptersFromTracks": "Positionner les chapitres par rapports aux pistes", "ButtonSetChaptersFromTracks": "Positionner les chapitres par rapports aux pistes",
"ButtonShare": "Partager",
"ButtonShiftTimes": "Décaler lhorodatage du livre", "ButtonShiftTimes": "Décaler lhorodatage du livre",
"ButtonShow": "Afficher", "ButtonShow": "Afficher",
"ButtonStartM4BEncode": "Démarrer lencodage M4B", "ButtonStartM4BEncode": "Démarrer lencodage M4B",
@ -83,7 +92,7 @@
"ButtonUploadBackup": "Téléverser une sauvegarde", "ButtonUploadBackup": "Téléverser une sauvegarde",
"ButtonUploadCover": "Téléverser une couverture", "ButtonUploadCover": "Téléverser une couverture",
"ButtonUploadOPMLFile": "Téléverser un fichier OPML", "ButtonUploadOPMLFile": "Téléverser un fichier OPML",
"ButtonUserDelete": "Effacer lutilisateur {0}", "ButtonUserDelete": "Supprimer lutilisateur {0}",
"ButtonUserEdit": "Modifier lutilisateur {0}", "ButtonUserEdit": "Modifier lutilisateur {0}",
"ButtonViewAll": "Afficher tout", "ButtonViewAll": "Afficher tout",
"ButtonYes": "Oui", "ButtonYes": "Oui",
@ -92,8 +101,8 @@
"ErrorUploadLacksTitle": "Doit avoir un titre", "ErrorUploadLacksTitle": "Doit avoir un titre",
"HeaderAccount": "Compte", "HeaderAccount": "Compte",
"HeaderAdvanced": "Avancé", "HeaderAdvanced": "Avancé",
"HeaderAppriseNotificationSettings": "Configuration des Notifications Apprise", "HeaderAppriseNotificationSettings": "Configuration des notifications Apprise",
"HeaderAudiobookTools": "Outils de Gestion de Fichier Audiobook", "HeaderAudiobookTools": "Outils de gestion de fichiers de livres audio",
"HeaderAudioTracks": "Pistes audio", "HeaderAudioTracks": "Pistes audio",
"HeaderAuthentication": "Authentication", "HeaderAuthentication": "Authentication",
"HeaderBackups": "Sauvegardes", "HeaderBackups": "Sauvegardes",
@ -104,6 +113,7 @@
"HeaderCollectionItems": "Entrées de la collection", "HeaderCollectionItems": "Entrées de la collection",
"HeaderCover": "Couverture", "HeaderCover": "Couverture",
"HeaderCurrentDownloads": "Téléchargements en cours", "HeaderCurrentDownloads": "Téléchargements en cours",
"HeaderCustomMetadataProviders": "Fournisseurs de métadonnées personnalisés",
"HeaderDetails": "Détails", "HeaderDetails": "Détails",
"HeaderDownloadQueue": "File dattente de téléchargements", "HeaderDownloadQueue": "File dattente de téléchargements",
"HeaderEbookFiles": "Fichier des livres numériques", "HeaderEbookFiles": "Fichier des livres numériques",
@ -174,6 +184,7 @@
"HeaderUpdateDetails": "Mettre à jour les détails", "HeaderUpdateDetails": "Mettre à jour les détails",
"HeaderUpdateLibrary": "Mettre à jour la bibliothèque", "HeaderUpdateLibrary": "Mettre à jour la bibliothèque",
"HeaderUsers": "Utilisateurs", "HeaderUsers": "Utilisateurs",
"HeaderYearReview": "Year {0} in Review",
"HeaderYourStats": "Vos statistiques", "HeaderYourStats": "Vos statistiques",
"LabelAbridged": "Version courte", "LabelAbridged": "Version courte",
"LabelAccountType": "Type de compte", "LabelAccountType": "Type de compte",
@ -281,11 +292,11 @@
"LabelFinished": "Terminé le", "LabelFinished": "Terminé le",
"LabelFolder": "Dossier", "LabelFolder": "Dossier",
"LabelFolders": "Dossiers", "LabelFolders": "Dossiers",
"LabelFontBold": "Bold", "LabelFontBold": "Gras",
"LabelFontFamily": "Polices de caractères", "LabelFontFamily": "Polices de caractères",
"LabelFontItalic": "Italic", "LabelFontItalic": "Italique",
"LabelFontScale": "Taille de la police de caractère", "LabelFontScale": "Taille de la police de caractère",
"LabelFontStrikethrough": "Strikethrough", "LabelFontStrikethrough": "Barrer",
"LabelFormat": "Format", "LabelFormat": "Format",
"LabelGenre": "Genre", "LabelGenre": "Genre",
"LabelGenres": "Genres", "LabelGenres": "Genres",
@ -345,7 +356,9 @@
"LabelMetaTags": "Balises de métadonnée", "LabelMetaTags": "Balises de métadonnée",
"LabelMinute": "Minute", "LabelMinute": "Minute",
"LabelMissing": "Manquant", "LabelMissing": "Manquant",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Parties manquantes", "LabelMissingParts": "Parties manquantes",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "URI de redirection mobile autorisés", "LabelMobileRedirectURIs": "URI de redirection mobile autorisés",
"LabelMobileRedirectURIsDescription": "Il s'agit d'une liste blanche dURI de redirection valides pour les applications mobiles. Celui par défaut est <code>audiobookshelf://oauth</code>, que vous pouvez supprimer ou compléter avec des URIs supplémentaires pour l'intégration d'applications tierces. Lutilisation dun astérisque (<code>*</code>) comme seule entrée autorise nimporte quel URI.", "LabelMobileRedirectURIsDescription": "Il s'agit d'une liste blanche dURI de redirection valides pour les applications mobiles. Celui par défaut est <code>audiobookshelf://oauth</code>, que vous pouvez supprimer ou compléter avec des URIs supplémentaires pour l'intégration d'applications tierces. Lutilisation dun astérisque (<code>*</code>) comme seule entrée autorise nimporte quel URI.",
"LabelMore": "Plus", "LabelMore": "Plus",
@ -385,11 +398,13 @@
"LabelPermissionsDownload": "Peut télécharger", "LabelPermissionsDownload": "Peut télécharger",
"LabelPermissionsUpdate": "Peut mettre à jour", "LabelPermissionsUpdate": "Peut mettre à jour",
"LabelPermissionsUpload": "Peut téléverser", "LabelPermissionsUpload": "Peut téléverser",
"LabelPersonalYearReview": "Your Year in Review ({0})",
"LabelPhotoPathURL": "Chemin / URL des photos", "LabelPhotoPathURL": "Chemin / URL des photos",
"LabelPlaylists": "Listes de lecture", "LabelPlaylists": "Listes de lecture",
"LabelPlayMethod": "Méthode découte", "LabelPlayMethod": "Méthode découte",
"LabelPodcast": "Podcast", "LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts", "LabelPodcasts": "Podcasts",
"LabelPodcastSearchRegion": "Région de recherche de podcasts",
"LabelPodcastType": "Type de Podcast", "LabelPodcastType": "Type de Podcast",
"LabelPort": "Port", "LabelPort": "Port",
"LabelPrefixesToIgnore": "Préfixes à Ignorer (Insensible à la Casse)", "LabelPrefixesToIgnore": "Préfixes à Ignorer (Insensible à la Casse)",
@ -406,7 +421,7 @@
"LabelRecentlyAdded": "Derniers ajouts", "LabelRecentlyAdded": "Derniers ajouts",
"LabelRecentSeries": "Séries récentes", "LabelRecentSeries": "Séries récentes",
"LabelRecommended": "Recommandé", "LabelRecommended": "Recommandé",
"LabelRedo": "Redo", "LabelRedo": "Refaire",
"LabelRegion": "Région", "LabelRegion": "Région",
"LabelReleaseDate": "Date de parution", "LabelReleaseDate": "Date de parution",
"LabelRemoveCover": "Supprimer la couverture", "LabelRemoveCover": "Supprimer la couverture",
@ -429,6 +444,7 @@
"LabelSeries": "Séries", "LabelSeries": "Séries",
"LabelSeriesName": "Nom de la série", "LabelSeriesName": "Nom de la série",
"LabelSeriesProgress": "Progression de séries", "LabelSeriesProgress": "Progression de séries",
"LabelServerYearReview": "Server Year in Review ({0})",
"LabelSetEbookAsPrimary": "Définir comme principale", "LabelSetEbookAsPrimary": "Définir comme principale",
"LabelSetEbookAsSupplementary": "Définir comme supplémentaire", "LabelSetEbookAsSupplementary": "Définir comme supplémentaire",
"LabelSettingsAudiobooksOnly": "Livres audios seulement", "LabelSettingsAudiobooksOnly": "Livres audios seulement",
@ -495,10 +511,10 @@
"LabelTagsAccessibleToUser": "Étiquettes accessibles à lutilisateur", "LabelTagsAccessibleToUser": "Étiquettes accessibles à lutilisateur",
"LabelTagsNotAccessibleToUser": "Étiquettes non accessibles à lutilisateur", "LabelTagsNotAccessibleToUser": "Étiquettes non accessibles à lutilisateur",
"LabelTasks": "Tâches en cours", "LabelTasks": "Tâches en cours",
"LabelTextEditorBulletedList": "Bulleted list", "LabelTextEditorBulletedList": "Liste à puces",
"LabelTextEditorLink": "Link", "LabelTextEditorLink": "Lien",
"LabelTextEditorNumberedList": "Numbered list", "LabelTextEditorNumberedList": "Liste numérotée",
"LabelTextEditorUnlink": "Unlink", "LabelTextEditorUnlink": "Dissocier",
"LabelTheme": "Thème", "LabelTheme": "Thème",
"LabelThemeDark": "Sombre", "LabelThemeDark": "Sombre",
"LabelThemeLight": "Clair", "LabelThemeLight": "Clair",
@ -524,7 +540,7 @@
"LabelTracksSingleTrack": "Piste simple", "LabelTracksSingleTrack": "Piste simple",
"LabelType": "Type", "LabelType": "Type",
"LabelUnabridged": "Version intégrale", "LabelUnabridged": "Version intégrale",
"LabelUndo": "Undo", "LabelUndo": "Annuler",
"LabelUnknown": "Inconnu", "LabelUnknown": "Inconnu",
"LabelUpdateCover": "Mettre à jour la couverture", "LabelUpdateCover": "Mettre à jour la couverture",
"LabelUpdateCoverHelp": "Autoriser la mise à jour de la couverture existante lorsquune correspondance est trouvée", "LabelUpdateCoverHelp": "Autoriser la mise à jour de la couverture existante lorsquune correspondance est trouvée",
@ -545,6 +561,8 @@
"LabelViewQueue": "Afficher la liste de lecture", "LabelViewQueue": "Afficher la liste de lecture",
"LabelVolume": "Volume", "LabelVolume": "Volume",
"LabelWeekdaysToRun": "Jours de la semaine à exécuter", "LabelWeekdaysToRun": "Jours de la semaine à exécuter",
"LabelYearReviewHide": "Masquer le bilan de lannée",
"LabelYearReviewShow": "Afficher le bilan de lannée",
"LabelYourAudiobookDuration": "Durée de vos livres audios", "LabelYourAudiobookDuration": "Durée de vos livres audios",
"LabelYourBookmarks": "Vos signets", "LabelYourBookmarks": "Vos signets",
"LabelYourPlaylists": "Vos listes de lecture", "LabelYourPlaylists": "Vos listes de lecture",
@ -676,7 +694,7 @@
"MessageYourAudiobookDurationIsShorter": "La durée de votre livre audio est plus courte que la durée trouvée", "MessageYourAudiobookDurationIsShorter": "La durée de votre livre audio est plus courte que la durée trouvée",
"NoteChangeRootPassword": "seul lutilisateur « root » peut utiliser un mot de passe vide", "NoteChangeRootPassword": "seul lutilisateur « root » peut utiliser un mot de passe vide",
"NoteChapterEditorTimes": "Information : lhorodatage du premier chapitre doit être à 0:00 et celui du dernier chapitre ne peut se situer au-delà de la durée du livre audio.", "NoteChapterEditorTimes": "Information : lhorodatage du premier chapitre doit être à 0:00 et celui du dernier chapitre ne peut se situer au-delà de la durée du livre audio.",
"NoteFolderPicker": "Information : Les dossiers déjà surveillés ne sont pas affichés", "NoteFolderPicker": "Information : les dossiers déjà surveillés ne sont pas affichés",
"NoteRSSFeedPodcastAppsHttps": "Attention : la majorité des application de podcast nécessite une adresse de flux en HTTPS.", "NoteRSSFeedPodcastAppsHttps": "Attention : la majorité des application de podcast nécessite une adresse de flux en HTTPS.",
"NoteRSSFeedPodcastAppsPubDate": "Attention : un ou plusieurs de vos épisodes ne possèdent pas de date de publication. Certaines applications de podcast le requièrent.", "NoteRSSFeedPodcastAppsPubDate": "Attention : un ou plusieurs de vos épisodes ne possèdent pas de date de publication. Certaines applications de podcast le requièrent.",
"NoteUploaderFoldersWithMediaFiles": "Les dossiers contenant des fichiers multimédias seront traités comme des éléments distincts de la bibliothèque.", "NoteUploaderFoldersWithMediaFiles": "Les dossiers contenant des fichiers multimédias seront traités comme des éléments distincts de la bibliothèque.",

View File

@ -32,6 +32,8 @@
"ButtonHide": "છુપાવો", "ButtonHide": "છુપાવો",
"ButtonHome": "ઘર", "ButtonHome": "ઘર",
"ButtonIssues": "સમસ્યાઓ", "ButtonIssues": "સમસ્યાઓ",
"ButtonJumpBackward": "Jump Backward",
"ButtonJumpForward": "Jump Forward",
"ButtonLatest": "નવીનતમ", "ButtonLatest": "નવીનતમ",
"ButtonLibrary": "પુસ્તકાલય", "ButtonLibrary": "પુસ્તકાલય",
"ButtonLogout": "લૉગ આઉટ", "ButtonLogout": "લૉગ આઉટ",
@ -41,12 +43,17 @@
"ButtonMatchAllAuthors": "બધા મેળ ખાતા લેખકો શોધો", "ButtonMatchAllAuthors": "બધા મેળ ખાતા લેખકો શોધો",
"ButtonMatchBooks": "મેળ ખાતી પુસ્તકો શોધો", "ButtonMatchBooks": "મેળ ખાતી પુસ્તકો શોધો",
"ButtonNevermind": "કંઈ વાંધો નહીં", "ButtonNevermind": "કંઈ વાંધો નહીં",
"ButtonNext": "Next",
"ButtonNextChapter": "Next Chapter",
"ButtonOk": "ઓકે", "ButtonOk": "ઓકે",
"ButtonOpenFeed": "ફીડ ખોલો", "ButtonOpenFeed": "ફીડ ખોલો",
"ButtonOpenManager": "મેનેજર ખોલો", "ButtonOpenManager": "મેનેજર ખોલો",
"ButtonPause": "Pause",
"ButtonPlay": "ચલાવો", "ButtonPlay": "ચલાવો",
"ButtonPlaying": "ચલાવી રહ્યું છે", "ButtonPlaying": "ચલાવી રહ્યું છે",
"ButtonPlaylists": "પ્લેલિસ્ટ", "ButtonPlaylists": "પ્લેલિસ્ટ",
"ButtonPrevious": "Previous",
"ButtonPreviousChapter": "Previous Chapter",
"ButtonPurgeAllCache": "બધો Cache કાઢી નાખો", "ButtonPurgeAllCache": "બધો Cache કાઢી નાખો",
"ButtonPurgeItemsCache": "વસ્તુઓનો Cache કાઢી નાખો", "ButtonPurgeItemsCache": "વસ્તુઓનો Cache કાઢી નાખો",
"ButtonPurgeMediaProgress": "બધું સાંભળ્યું કાઢી નાખો", "ButtonPurgeMediaProgress": "બધું સાંભળ્યું કાઢી નાખો",
@ -54,6 +61,7 @@
"ButtonQueueRemoveItem": "કતારથી કાઢી નાખો", "ButtonQueueRemoveItem": "કતારથી કાઢી નાખો",
"ButtonQuickMatch": "ઝડપી મેળ ખવડાવો", "ButtonQuickMatch": "ઝડપી મેળ ખવડાવો",
"ButtonRead": "વાંચો", "ButtonRead": "વાંચો",
"ButtonRefresh": "Refresh",
"ButtonRemove": "કાઢી નાખો", "ButtonRemove": "કાઢી નાખો",
"ButtonRemoveAll": "બધું કાઢી નાખો", "ButtonRemoveAll": "બધું કાઢી નાખો",
"ButtonRemoveAllLibraryItems": "બધું પુસ્તકાલય વસ્તુઓ કાઢી નાખો", "ButtonRemoveAllLibraryItems": "બધું પુસ્તકાલય વસ્તુઓ કાઢી નાખો",
@ -73,6 +81,7 @@
"ButtonSelectFolderPath": "ફોલ્ડર પથ પસંદ કરો", "ButtonSelectFolderPath": "ફોલ્ડર પથ પસંદ કરો",
"ButtonSeries": "સિરીઝ", "ButtonSeries": "સિરીઝ",
"ButtonSetChaptersFromTracks": "ટ્રેક્સથી પ્રકરણો સેટ કરો", "ButtonSetChaptersFromTracks": "ટ્રેક્સથી પ્રકરણો સેટ કરો",
"ButtonShare": "Share",
"ButtonShiftTimes": "સમય શિફ્ટ કરો", "ButtonShiftTimes": "સમય શિફ્ટ કરો",
"ButtonShow": "બતાવો", "ButtonShow": "બતાવો",
"ButtonStartM4BEncode": "M4B એન્કોડ શરૂ કરો", "ButtonStartM4BEncode": "M4B એન્કોડ શરૂ કરો",
@ -104,6 +113,7 @@
"HeaderCollectionItems": "સંગ્રહ વસ્તુઓ", "HeaderCollectionItems": "સંગ્રહ વસ્તુઓ",
"HeaderCover": "આવરણ", "HeaderCover": "આવરણ",
"HeaderCurrentDownloads": "વર્તમાન ડાઉનલોડ્સ", "HeaderCurrentDownloads": "વર્તમાન ડાઉનલોડ્સ",
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
"HeaderDetails": "વિગતો", "HeaderDetails": "વિગતો",
"HeaderDownloadQueue": "ડાઉનલોડ કતાર", "HeaderDownloadQueue": "ડાઉનલોડ કતાર",
"HeaderEbookFiles": "ઇબુક ફાઇલો", "HeaderEbookFiles": "ઇબુક ફાઇલો",
@ -174,6 +184,7 @@
"HeaderUpdateDetails": "Update Details", "HeaderUpdateDetails": "Update Details",
"HeaderUpdateLibrary": "Update Library", "HeaderUpdateLibrary": "Update Library",
"HeaderUsers": "Users", "HeaderUsers": "Users",
"HeaderYearReview": "Year {0} in Review",
"HeaderYourStats": "Your Stats", "HeaderYourStats": "Your Stats",
"LabelAbridged": "Abridged", "LabelAbridged": "Abridged",
"LabelAccountType": "Account Type", "LabelAccountType": "Account Type",
@ -345,7 +356,9 @@
"LabelMetaTags": "Meta Tags", "LabelMetaTags": "Meta Tags",
"LabelMinute": "Minute", "LabelMinute": "Minute",
"LabelMissing": "Missing", "LabelMissing": "Missing",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Missing Parts", "LabelMissingParts": "Missing Parts",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
"LabelMore": "More", "LabelMore": "More",
@ -385,11 +398,13 @@
"LabelPermissionsDownload": "Can Download", "LabelPermissionsDownload": "Can Download",
"LabelPermissionsUpdate": "Can Update", "LabelPermissionsUpdate": "Can Update",
"LabelPermissionsUpload": "Can Upload", "LabelPermissionsUpload": "Can Upload",
"LabelPersonalYearReview": "Your Year in Review ({0})",
"LabelPhotoPathURL": "Photo Path/URL", "LabelPhotoPathURL": "Photo Path/URL",
"LabelPlaylists": "Playlists", "LabelPlaylists": "Playlists",
"LabelPlayMethod": "Play Method", "LabelPlayMethod": "Play Method",
"LabelPodcast": "Podcast", "LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts", "LabelPodcasts": "Podcasts",
"LabelPodcastSearchRegion": "પોડકાસ્ટ શોધ પ્રદેશ",
"LabelPodcastType": "Podcast Type", "LabelPodcastType": "Podcast Type",
"LabelPort": "Port", "LabelPort": "Port",
"LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)", "LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)",
@ -429,6 +444,7 @@
"LabelSeries": "Series", "LabelSeries": "Series",
"LabelSeriesName": "Series Name", "LabelSeriesName": "Series Name",
"LabelSeriesProgress": "Series Progress", "LabelSeriesProgress": "Series Progress",
"LabelServerYearReview": "Server Year in Review ({0})",
"LabelSetEbookAsPrimary": "Set as primary", "LabelSetEbookAsPrimary": "Set as primary",
"LabelSetEbookAsSupplementary": "Set as supplementary", "LabelSetEbookAsSupplementary": "Set as supplementary",
"LabelSettingsAudiobooksOnly": "Audiobooks only", "LabelSettingsAudiobooksOnly": "Audiobooks only",
@ -545,6 +561,8 @@
"LabelViewQueue": "View player queue", "LabelViewQueue": "View player queue",
"LabelVolume": "Volume", "LabelVolume": "Volume",
"LabelWeekdaysToRun": "Weekdays to run", "LabelWeekdaysToRun": "Weekdays to run",
"LabelYearReviewHide": "Hide Year in Review",
"LabelYearReviewShow": "See Year in Review",
"LabelYourAudiobookDuration": "Your audiobook duration", "LabelYourAudiobookDuration": "Your audiobook duration",
"LabelYourBookmarks": "Your Bookmarks", "LabelYourBookmarks": "Your Bookmarks",
"LabelYourPlaylists": "Your Playlists", "LabelYourPlaylists": "Your Playlists",

View File

@ -32,6 +32,8 @@
"ButtonHide": "छुपाएं", "ButtonHide": "छुपाएं",
"ButtonHome": "घर", "ButtonHome": "घर",
"ButtonIssues": "समस्याएं", "ButtonIssues": "समस्याएं",
"ButtonJumpBackward": "Jump Backward",
"ButtonJumpForward": "Jump Forward",
"ButtonLatest": "नवीनतम", "ButtonLatest": "नवीनतम",
"ButtonLibrary": "पुस्तकालय", "ButtonLibrary": "पुस्तकालय",
"ButtonLogout": "लॉग आउट", "ButtonLogout": "लॉग आउट",
@ -41,12 +43,17 @@
"ButtonMatchAllAuthors": "सभी लेखकों को तलाश करें", "ButtonMatchAllAuthors": "सभी लेखकों को तलाश करें",
"ButtonMatchBooks": "संबंधित पुस्तकों का मिलान करें", "ButtonMatchBooks": "संबंधित पुस्तकों का मिलान करें",
"ButtonNevermind": "कोई बात नहीं", "ButtonNevermind": "कोई बात नहीं",
"ButtonNext": "Next",
"ButtonNextChapter": "Next Chapter",
"ButtonOk": "ठीक है", "ButtonOk": "ठीक है",
"ButtonOpenFeed": "फ़ीड खोलें", "ButtonOpenFeed": "फ़ीड खोलें",
"ButtonOpenManager": "मैनेजर खोलें", "ButtonOpenManager": "मैनेजर खोलें",
"ButtonPause": "Pause",
"ButtonPlay": "चलाएँ", "ButtonPlay": "चलाएँ",
"ButtonPlaying": "चल रही है", "ButtonPlaying": "चल रही है",
"ButtonPlaylists": "प्लेलिस्ट्स", "ButtonPlaylists": "प्लेलिस्ट्स",
"ButtonPrevious": "Previous",
"ButtonPreviousChapter": "Previous Chapter",
"ButtonPurgeAllCache": "सभी Cache मिटाएं", "ButtonPurgeAllCache": "सभी Cache मिटाएं",
"ButtonPurgeItemsCache": "आइटम Cache मिटाएं", "ButtonPurgeItemsCache": "आइटम Cache मिटाएं",
"ButtonPurgeMediaProgress": "अभी तक सुना हुआ सब हटा दे", "ButtonPurgeMediaProgress": "अभी तक सुना हुआ सब हटा दे",
@ -54,6 +61,7 @@
"ButtonQueueRemoveItem": "कतार से हटाएं", "ButtonQueueRemoveItem": "कतार से हटाएं",
"ButtonQuickMatch": "जल्दी से समानता की तलाश करें", "ButtonQuickMatch": "जल्दी से समानता की तलाश करें",
"ButtonRead": "पढ़ लिया", "ButtonRead": "पढ़ लिया",
"ButtonRefresh": "Refresh",
"ButtonRemove": "हटाएं", "ButtonRemove": "हटाएं",
"ButtonRemoveAll": "सभी हटाएं", "ButtonRemoveAll": "सभी हटाएं",
"ButtonRemoveAllLibraryItems": "पुस्तकालय की सभी आइटम हटाएं", "ButtonRemoveAllLibraryItems": "पुस्तकालय की सभी आइटम हटाएं",
@ -73,6 +81,7 @@
"ButtonSelectFolderPath": "फ़ोल्डर का पथ चुनें", "ButtonSelectFolderPath": "फ़ोल्डर का पथ चुनें",
"ButtonSeries": "सीरीज", "ButtonSeries": "सीरीज",
"ButtonSetChaptersFromTracks": "ट्रैक्स से अध्याय बनाएं", "ButtonSetChaptersFromTracks": "ट्रैक्स से अध्याय बनाएं",
"ButtonShare": "Share",
"ButtonShiftTimes": "समय खिसकाए", "ButtonShiftTimes": "समय खिसकाए",
"ButtonShow": "दिखाएं", "ButtonShow": "दिखाएं",
"ButtonStartM4BEncode": "M4B एन्कोडिंग शुरू करें", "ButtonStartM4BEncode": "M4B एन्कोडिंग शुरू करें",
@ -104,6 +113,7 @@
"HeaderCollectionItems": "Collection Items", "HeaderCollectionItems": "Collection Items",
"HeaderCover": "Cover", "HeaderCover": "Cover",
"HeaderCurrentDownloads": "Current Downloads", "HeaderCurrentDownloads": "Current Downloads",
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
"HeaderDetails": "Details", "HeaderDetails": "Details",
"HeaderDownloadQueue": "Download Queue", "HeaderDownloadQueue": "Download Queue",
"HeaderEbookFiles": "Ebook Files", "HeaderEbookFiles": "Ebook Files",
@ -174,6 +184,7 @@
"HeaderUpdateDetails": "Update Details", "HeaderUpdateDetails": "Update Details",
"HeaderUpdateLibrary": "Update Library", "HeaderUpdateLibrary": "Update Library",
"HeaderUsers": "Users", "HeaderUsers": "Users",
"HeaderYearReview": "Year {0} in Review",
"HeaderYourStats": "Your Stats", "HeaderYourStats": "Your Stats",
"LabelAbridged": "Abridged", "LabelAbridged": "Abridged",
"LabelAccountType": "Account Type", "LabelAccountType": "Account Type",
@ -345,7 +356,9 @@
"LabelMetaTags": "Meta Tags", "LabelMetaTags": "Meta Tags",
"LabelMinute": "Minute", "LabelMinute": "Minute",
"LabelMissing": "Missing", "LabelMissing": "Missing",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Missing Parts", "LabelMissingParts": "Missing Parts",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
"LabelMore": "More", "LabelMore": "More",
@ -385,11 +398,13 @@
"LabelPermissionsDownload": "Can Download", "LabelPermissionsDownload": "Can Download",
"LabelPermissionsUpdate": "Can Update", "LabelPermissionsUpdate": "Can Update",
"LabelPermissionsUpload": "Can Upload", "LabelPermissionsUpload": "Can Upload",
"LabelPersonalYearReview": "Your Year in Review ({0})",
"LabelPhotoPathURL": "Photo Path/URL", "LabelPhotoPathURL": "Photo Path/URL",
"LabelPlaylists": "Playlists", "LabelPlaylists": "Playlists",
"LabelPlayMethod": "Play Method", "LabelPlayMethod": "Play Method",
"LabelPodcast": "Podcast", "LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts", "LabelPodcasts": "Podcasts",
"LabelPodcastSearchRegion": "पॉडकास्ट खोज क्षेत्र",
"LabelPodcastType": "Podcast Type", "LabelPodcastType": "Podcast Type",
"LabelPort": "Port", "LabelPort": "Port",
"LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)", "LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)",
@ -429,6 +444,7 @@
"LabelSeries": "Series", "LabelSeries": "Series",
"LabelSeriesName": "Series Name", "LabelSeriesName": "Series Name",
"LabelSeriesProgress": "Series Progress", "LabelSeriesProgress": "Series Progress",
"LabelServerYearReview": "Server Year in Review ({0})",
"LabelSetEbookAsPrimary": "Set as primary", "LabelSetEbookAsPrimary": "Set as primary",
"LabelSetEbookAsSupplementary": "Set as supplementary", "LabelSetEbookAsSupplementary": "Set as supplementary",
"LabelSettingsAudiobooksOnly": "Audiobooks only", "LabelSettingsAudiobooksOnly": "Audiobooks only",
@ -545,6 +561,8 @@
"LabelViewQueue": "View player queue", "LabelViewQueue": "View player queue",
"LabelVolume": "Volume", "LabelVolume": "Volume",
"LabelWeekdaysToRun": "Weekdays to run", "LabelWeekdaysToRun": "Weekdays to run",
"LabelYearReviewHide": "Hide Year in Review",
"LabelYearReviewShow": "See Year in Review",
"LabelYourAudiobookDuration": "Your audiobook duration", "LabelYourAudiobookDuration": "Your audiobook duration",
"LabelYourBookmarks": "Your Bookmarks", "LabelYourBookmarks": "Your Bookmarks",
"LabelYourPlaylists": "Your Playlists", "LabelYourPlaylists": "Your Playlists",

View File

@ -32,6 +32,8 @@
"ButtonHide": "Sakrij", "ButtonHide": "Sakrij",
"ButtonHome": "Početna stranica", "ButtonHome": "Početna stranica",
"ButtonIssues": "Problemi", "ButtonIssues": "Problemi",
"ButtonJumpBackward": "Jump Backward",
"ButtonJumpForward": "Jump Forward",
"ButtonLatest": "Najnovije", "ButtonLatest": "Najnovije",
"ButtonLibrary": "Biblioteka", "ButtonLibrary": "Biblioteka",
"ButtonLogout": "Odjavi se", "ButtonLogout": "Odjavi se",
@ -41,12 +43,17 @@
"ButtonMatchAllAuthors": "Matchaj sve autore", "ButtonMatchAllAuthors": "Matchaj sve autore",
"ButtonMatchBooks": "Matchaj knjige", "ButtonMatchBooks": "Matchaj knjige",
"ButtonNevermind": "Nije bitno", "ButtonNevermind": "Nije bitno",
"ButtonNext": "Next",
"ButtonNextChapter": "Next Chapter",
"ButtonOk": "Ok", "ButtonOk": "Ok",
"ButtonOpenFeed": "Otvori feed", "ButtonOpenFeed": "Otvori feed",
"ButtonOpenManager": "Otvori menadžera", "ButtonOpenManager": "Otvori menadžera",
"ButtonPause": "Pause",
"ButtonPlay": "Pokreni", "ButtonPlay": "Pokreni",
"ButtonPlaying": "Playing", "ButtonPlaying": "Playing",
"ButtonPlaylists": "Playlists", "ButtonPlaylists": "Playlists",
"ButtonPrevious": "Previous",
"ButtonPreviousChapter": "Previous Chapter",
"ButtonPurgeAllCache": "Isprazni sav cache", "ButtonPurgeAllCache": "Isprazni sav cache",
"ButtonPurgeItemsCache": "Isprazni Items Cache", "ButtonPurgeItemsCache": "Isprazni Items Cache",
"ButtonPurgeMediaProgress": "Purge Media Progress", "ButtonPurgeMediaProgress": "Purge Media Progress",
@ -54,6 +61,7 @@
"ButtonQueueRemoveItem": "Remove from queue", "ButtonQueueRemoveItem": "Remove from queue",
"ButtonQuickMatch": "Brzi match", "ButtonQuickMatch": "Brzi match",
"ButtonRead": "Pročitaj", "ButtonRead": "Pročitaj",
"ButtonRefresh": "Refresh",
"ButtonRemove": "Ukloni", "ButtonRemove": "Ukloni",
"ButtonRemoveAll": "Ukloni sve", "ButtonRemoveAll": "Ukloni sve",
"ButtonRemoveAllLibraryItems": "Ukloni sve stvari iz biblioteke", "ButtonRemoveAllLibraryItems": "Ukloni sve stvari iz biblioteke",
@ -73,6 +81,7 @@
"ButtonSelectFolderPath": "Odaberi putanju do folder", "ButtonSelectFolderPath": "Odaberi putanju do folder",
"ButtonSeries": "Serije", "ButtonSeries": "Serije",
"ButtonSetChaptersFromTracks": "Set chapters from tracks", "ButtonSetChaptersFromTracks": "Set chapters from tracks",
"ButtonShare": "Share",
"ButtonShiftTimes": "Pomakni vremena", "ButtonShiftTimes": "Pomakni vremena",
"ButtonShow": "Prikaži", "ButtonShow": "Prikaži",
"ButtonStartM4BEncode": "Pokreni M4B kodiranje", "ButtonStartM4BEncode": "Pokreni M4B kodiranje",
@ -104,6 +113,7 @@
"HeaderCollectionItems": "Stvari u kolekciji", "HeaderCollectionItems": "Stvari u kolekciji",
"HeaderCover": "Cover", "HeaderCover": "Cover",
"HeaderCurrentDownloads": "Current Downloads", "HeaderCurrentDownloads": "Current Downloads",
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
"HeaderDetails": "Detalji", "HeaderDetails": "Detalji",
"HeaderDownloadQueue": "Download Queue", "HeaderDownloadQueue": "Download Queue",
"HeaderEbookFiles": "Ebook Files", "HeaderEbookFiles": "Ebook Files",
@ -174,6 +184,7 @@
"HeaderUpdateDetails": "Aktualiziraj detalje", "HeaderUpdateDetails": "Aktualiziraj detalje",
"HeaderUpdateLibrary": "Aktualiziraj biblioteku", "HeaderUpdateLibrary": "Aktualiziraj biblioteku",
"HeaderUsers": "Korinici", "HeaderUsers": "Korinici",
"HeaderYearReview": "Year {0} in Review",
"HeaderYourStats": "Tvoja statistika", "HeaderYourStats": "Tvoja statistika",
"LabelAbridged": "Abridged", "LabelAbridged": "Abridged",
"LabelAccountType": "Vrsta korisničkog računa", "LabelAccountType": "Vrsta korisničkog računa",
@ -345,7 +356,9 @@
"LabelMetaTags": "Meta Tags", "LabelMetaTags": "Meta Tags",
"LabelMinute": "Minuta", "LabelMinute": "Minuta",
"LabelMissing": "Nedostaje", "LabelMissing": "Nedostaje",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Nedostajali dijelovi", "LabelMissingParts": "Nedostajali dijelovi",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
"LabelMore": "Više", "LabelMore": "Više",
@ -385,11 +398,13 @@
"LabelPermissionsDownload": "Smije preuzimati", "LabelPermissionsDownload": "Smije preuzimati",
"LabelPermissionsUpdate": "Smije aktualizirati", "LabelPermissionsUpdate": "Smije aktualizirati",
"LabelPermissionsUpload": "Smije uploadati", "LabelPermissionsUpload": "Smije uploadati",
"LabelPersonalYearReview": "Your Year in Review ({0})",
"LabelPhotoPathURL": "Slika putanja/URL", "LabelPhotoPathURL": "Slika putanja/URL",
"LabelPlaylists": "Playlists", "LabelPlaylists": "Playlists",
"LabelPlayMethod": "Vrsta reprodukcije", "LabelPlayMethod": "Vrsta reprodukcije",
"LabelPodcast": "Podcast", "LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts", "LabelPodcasts": "Podcasts",
"LabelPodcastSearchRegion": "Područje pretrage podcasta",
"LabelPodcastType": "Podcast Type", "LabelPodcastType": "Podcast Type",
"LabelPort": "Port", "LabelPort": "Port",
"LabelPrefixesToIgnore": "Prefiksi za ignorirati (mala i velika slova nisu bitna)", "LabelPrefixesToIgnore": "Prefiksi za ignorirati (mala i velika slova nisu bitna)",
@ -429,6 +444,7 @@
"LabelSeries": "Serije", "LabelSeries": "Serije",
"LabelSeriesName": "Ime serije", "LabelSeriesName": "Ime serije",
"LabelSeriesProgress": "Series Progress", "LabelSeriesProgress": "Series Progress",
"LabelServerYearReview": "Server Year in Review ({0})",
"LabelSetEbookAsPrimary": "Set as primary", "LabelSetEbookAsPrimary": "Set as primary",
"LabelSetEbookAsSupplementary": "Set as supplementary", "LabelSetEbookAsSupplementary": "Set as supplementary",
"LabelSettingsAudiobooksOnly": "Audiobooks only", "LabelSettingsAudiobooksOnly": "Audiobooks only",
@ -545,6 +561,8 @@
"LabelViewQueue": "View player queue", "LabelViewQueue": "View player queue",
"LabelVolume": "Volume", "LabelVolume": "Volume",
"LabelWeekdaysToRun": "Radnih dana da radi", "LabelWeekdaysToRun": "Radnih dana da radi",
"LabelYearReviewHide": "Hide Year in Review",
"LabelYearReviewShow": "See Year in Review",
"LabelYourAudiobookDuration": "Tvoje trajanje audiobooka", "LabelYourAudiobookDuration": "Tvoje trajanje audiobooka",
"LabelYourBookmarks": "Tvoje knjižne oznake", "LabelYourBookmarks": "Tvoje knjižne oznake",
"LabelYourPlaylists": "Your Playlists", "LabelYourPlaylists": "Your Playlists",

779
client/strings/hu.json Normal file
View File

@ -0,0 +1,779 @@
{
"ButtonAdd": "Hozzáadás",
"ButtonAddChapters": "Fejezetek hozzáadása",
"ButtonAddDevice": "Eszköz hozzáadása",
"ButtonAddLibrary": "Könyvtár hozzáadása",
"ButtonAddPodcasts": "Podcastok hozzáadása",
"ButtonAddUser": "Felhasználó hozzáadása",
"ButtonAddYourFirstLibrary": "Az első könyvtár hozzáadása",
"ButtonApply": "Alkalmaz",
"ButtonApplyChapters": "Fejezetek alkalmazása",
"ButtonAuthors": "Szerzők",
"ButtonBrowseForFolder": "Mappa keresése",
"ButtonCancel": "Mégse",
"ButtonCancelEncode": "Kódolás megszakítása",
"ButtonChangeRootPassword": "Gyökérjelszó megváltoztatása",
"ButtonCheckAndDownloadNewEpisodes": "Új epizódok ellenőrzése és letöltése",
"ButtonChooseAFolder": "Válassz egy mappát",
"ButtonChooseFiles": "Fájlok kiválasztása",
"ButtonClearFilter": "Szűrő törlése",
"ButtonCloseFeed": "Hírcsatorna bezárása",
"ButtonCollections": "Gyűjtemény",
"ButtonConfigureScanner": "Szkenner konfigurálása",
"ButtonCreate": "Létrehozás",
"ButtonCreateBackup": "Biztonsági másolat készítése",
"ButtonDelete": "Törlés",
"ButtonDownloadQueue": "Sor",
"ButtonEdit": "Szerkesztés",
"ButtonEditChapters": "Fejezetek szerkesztése",
"ButtonEditPodcast": "Podcast szerkesztése",
"ButtonForceReScan": "Újraszkennelés kényszerítése",
"ButtonFullPath": "Teljes útvonal",
"ButtonHide": "Elrejtés",
"ButtonHome": "Kezdőlap",
"ButtonIssues": "Problémák",
"ButtonJumpBackward": "Ugrás vissza",
"ButtonJumpForward": "Ugrás előre",
"ButtonLatest": "Legújabb",
"ButtonLibrary": "Könyvtár",
"ButtonLogout": "Kijelentkezés",
"ButtonLookup": "Keresés",
"ButtonManageTracks": "Sávok kezelése",
"ButtonMapChapterTitles": "Fejezetcímek hozzárendelése",
"ButtonMatchAllAuthors": "Minden szerző egyeztetése",
"ButtonMatchBooks": "Könyvek egyeztetése",
"ButtonNevermind": "Mindegy",
"ButtonNext": "Next",
"ButtonNextChapter": "Következő fejezet",
"ButtonOk": "Oké",
"ButtonOpenFeed": "Hírcsatorna megnyitása",
"ButtonOpenManager": "Kezelő megnyitása",
"ButtonPause": "Szünet",
"ButtonPlay": "Lejátszás",
"ButtonPlaying": "Lejátszás folyamatban",
"ButtonPlaylists": "Lejátszási listák",
"ButtonPrevious": "Previous",
"ButtonPreviousChapter": "Előző fejezet",
"ButtonPurgeAllCache": "Összes gyorsítótár törlése",
"ButtonPurgeItemsCache": "Elemek gyorsítótárának törlése",
"ButtonPurgeMediaProgress": "Médialejátszás állapotának törlése",
"ButtonQueueAddItem": "Hozzáadás a sorhoz",
"ButtonQueueRemoveItem": "Eltávolítás a sorból",
"ButtonQuickMatch": "Gyors egyeztetés",
"ButtonRead": "Olvasás",
"ButtonRefresh": "Refresh",
"ButtonRemove": "Eltávolítás",
"ButtonRemoveAll": "Összes eltávolítása",
"ButtonRemoveAllLibraryItems": "Összes könyvtárelem eltávolítása",
"ButtonRemoveFromContinueListening": "Eltávolítás a Folytatás hallgatásából",
"ButtonRemoveFromContinueReading": "Eltávolítás a Folytatás olvasásából",
"ButtonRemoveSeriesFromContinueSeries": "Sorozat eltávolítása a Folytatás sorozatokból",
"ButtonReScan": "Újraszkennelés",
"ButtonReset": "Visszaállítás",
"ButtonResetToDefault": "Alapértelmezésre állítás",
"ButtonRestore": "Visszaállítás",
"ButtonSave": "Mentés",
"ButtonSaveAndClose": "Mentés és bezárás",
"ButtonSaveTracklist": "Sávlista mentése",
"ButtonScan": "Szkennelés",
"ButtonScanLibrary": "Könyvtár szkennelése",
"ButtonSearch": "Keresés",
"ButtonSelectFolderPath": "Mappa útvonalának kiválasztása",
"ButtonSeries": "Sorozatok",
"ButtonSetChaptersFromTracks": "Fejezetek beállítása sávokból",
"ButtonShare": "Share",
"ButtonShiftTimes": "Idők eltolása",
"ButtonShow": "Megjelenítés",
"ButtonStartM4BEncode": "M4B kódolás indítása",
"ButtonStartMetadataEmbed": "Metaadatok beágyazásának indítása",
"ButtonSubmit": "Beküldés",
"ButtonTest": "Teszt",
"ButtonUpload": "Feltöltés",
"ButtonUploadBackup": "Biztonsági másolat feltöltése",
"ButtonUploadCover": "Borító feltöltése",
"ButtonUploadOPMLFile": "OPML fájl feltöltése",
"ButtonUserDelete": "Felhasználó törlése {0}",
"ButtonUserEdit": "Felhasználó szerkesztése {0}",
"ButtonViewAll": "Összes megtekintése",
"ButtonYes": "Igen",
"ErrorUploadFetchMetadataAPI": "Hiba a metaadatok lekérésekor",
"ErrorUploadFetchMetadataNoResults": "Nem sikerült a metaadatok lekérése - próbálja meg frissíteni a címet és/vagy a szerzőt",
"ErrorUploadLacksTitle": "Cím szükséges",
"HeaderAccount": "Fiók",
"HeaderAdvanced": "Haladó",
"HeaderAppriseNotificationSettings": "Apprise értesítési beállítások",
"HeaderAudiobookTools": "Hangoskönyv fájlkezelő eszközök",
"HeaderAudioTracks": "Audiósávok",
"HeaderAuthentication": "Hitelesítés",
"HeaderBackups": "Biztonsági másolatok",
"HeaderChangePassword": "Jelszó megváltoztatása",
"HeaderChapters": "Fejezetek",
"HeaderChooseAFolder": "Válasszon egy mappát",
"HeaderCollection": "Gyűjtemény",
"HeaderCollectionItems": "Gyűjtemény elemek",
"HeaderCover": "Borító",
"HeaderCurrentDownloads": "Jelenlegi letöltések",
"HeaderCustomMetadataProviders": "Egyéni metaadat-szolgáltatók",
"HeaderDetails": "Részletek",
"HeaderDownloadQueue": "Letöltési sor",
"HeaderEbookFiles": "E-könyv fájlok",
"HeaderEmail": "E-mail",
"HeaderEmailSettings": "E-mail beállítások",
"HeaderEpisodes": "Epizódok",
"HeaderEreaderDevices": "E-olvasó eszközök",
"HeaderEreaderSettings": "E-olvasó beállítások",
"HeaderFiles": "Fájlok",
"HeaderFindChapters": "Fejezetek keresése",
"HeaderIgnoredFiles": "Figyelmen kívül hagyott fájlok",
"HeaderItemFiles": "Elemfájlok",
"HeaderItemMetadataUtils": "Elem metaadat eszközök",
"HeaderLastListeningSession": "Utolsó hallgatási munkamenet",
"HeaderLatestEpisodes": "Legújabb epizódok",
"HeaderLibraries": "Könyvtárak",
"HeaderLibraryFiles": "Könyvtárfájlok",
"HeaderLibraryStats": "Könyvtár statisztikák",
"HeaderListeningSessions": "Hallgatási munkamenetek",
"HeaderListeningStats": "Hallgatási statisztikák",
"HeaderLogin": "Bejelentkezés",
"HeaderLogs": "Naplók",
"HeaderManageGenres": "Műfajok kezelése",
"HeaderManageTags": "Címkék kezelése",
"HeaderMapDetails": "Részletek hozzárendelése",
"HeaderMatch": "Egyeztetés",
"HeaderMetadataOrderOfPrecedence": "Metaadatok előnyben részesítési sorrendje",
"HeaderMetadataToEmbed": "Beágyazandó metaadatok",
"HeaderNewAccount": "Új fiók",
"HeaderNewLibrary": "Új könyvtár",
"HeaderNotifications": "Értesítések",
"HeaderOpenIDConnectAuthentication": "OpenID Connect hitelesítés",
"HeaderOpenRSSFeed": "RSS hírcsatorna megnyitása",
"HeaderOtherFiles": "Egyéb fájlok",
"HeaderPasswordAuthentication": "Jelszó hitelesítés",
"HeaderPermissions": "Engedélyek",
"HeaderPlayerQueue": "Lejátszó sor",
"HeaderPlaylist": "Lejátszási lista",
"HeaderPlaylistItems": "Lejátszási lista elemek",
"HeaderPodcastsToAdd": "Hozzáadandó podcastok",
"HeaderPreviewCover": "Borító előnézete",
"HeaderRemoveEpisode": "Epizód eltávolítása",
"HeaderRemoveEpisodes": "{0} epizód eltávolítása",
"HeaderRSSFeedGeneral": "RSS részletek",
"HeaderRSSFeedIsOpen": "RSS hírcsatorna nyitva",
"HeaderRSSFeeds": "RSS hírcsatornák",
"HeaderSavedMediaProgress": "Mentett médialejátszási állapot",
"HeaderSchedule": "Ütemezés",
"HeaderScheduleLibraryScans": "Könyvtárak automatikus szkennelésének ütemezése",
"HeaderSession": "Munkamenet",
"HeaderSetBackupSchedule": "Biztonsági másolatok ütemezésének beállítása",
"HeaderSettings": "Beállítások",
"HeaderSettingsDisplay": "Kijelző",
"HeaderSettingsExperimental": "Kísérleti funkciók",
"HeaderSettingsGeneral": "Általános",
"HeaderSettingsScanner": "Szkenner",
"HeaderSleepTimer": "Alvásidőzítő",
"HeaderStatsLargestItems": "Legnagyobb elemek",
"HeaderStatsLongestItems": "Leghosszabb elemek (órákban)",
"HeaderStatsMinutesListeningChart": "Hallgatási percek (az utolsó 7 napban)",
"HeaderStatsRecentSessions": "Legutóbbi munkamenetek",
"HeaderStatsTop10Authors": "Top 10 szerzők",
"HeaderStatsTop5Genres": "Top 5 műfajok",
"HeaderTableOfContents": "Tartalomjegyzék",
"HeaderTools": "Eszközök",
"HeaderUpdateAccount": "Fiók frissítése",
"HeaderUpdateAuthor": "Szerző frissítése",
"HeaderUpdateDetails": "Részletek frissítése",
"HeaderUpdateLibrary": "Könyvtár frissítése",
"HeaderUsers": "Felhasználók",
"HeaderYearReview": "Year {0} in Review",
"HeaderYourStats": "Saját statisztikák",
"LabelAbridged": "Tömörített",
"LabelAccountType": "Fióktípus",
"LabelAccountTypeAdmin": "Admin",
"LabelAccountTypeGuest": "Vendég",
"LabelAccountTypeUser": "Felhasználó",
"LabelActivity": "Tevékenység",
"LabelAdded": "Hozzáadva",
"LabelAddedAt": "Hozzáadás ideje",
"LabelAddToCollection": "Hozzáadás a gyűjteményhez",
"LabelAddToCollectionBatch": "{0} könyv hozzáadása a gyűjteményhez",
"LabelAddToPlaylist": "Hozzáadás a lejátszási listához",
"LabelAddToPlaylistBatch": "{0} elem hozzáadása a lejátszási listához",
"LabelAdminUsersOnly": "Csak admin felhasználók",
"LabelAll": "Minden",
"LabelAllUsers": "Minden felhasználó",
"LabelAllUsersExcludingGuests": "Minden felhasználó, vendégek kivételével",
"LabelAllUsersIncludingGuests": "Minden felhasználó, beleértve a vendégeket is",
"LabelAlreadyInYourLibrary": "Már a könyvtárában van",
"LabelAppend": "Hozzáfűzés",
"LabelAuthor": "Szerző",
"LabelAuthorFirstLast": "Szerző (Keresztnév Vezetéknév)",
"LabelAuthorLastFirst": "Szerző (Vezetéknév, Keresztnév)",
"LabelAuthors": "Szerzők",
"LabelAutoDownloadEpisodes": "Epizódok automatikus letöltése",
"LabelAutoFetchMetadata": "Metaadatok automatikus lekérése",
"LabelAutoFetchMetadataHelp": "Cím, szerző és sorozat metaadatok automatikus lekérése a feltöltés megkönnyítése érdekében. További metaadatok egyeztetése szükséges lehet a feltöltés után.",
"LabelAutoLaunch": "Automatikus indítás",
"LabelAutoLaunchDescription": "Automatikus átirányítás az hitelesítő szolgáltatóhoz a bejelentkezési oldalra navigáláskor (kézi felülbírálás útvonala <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Automatikus regisztráció",
"LabelAutoRegisterDescription": "Új felhasználók automatikus létrehozása bejelentkezés után",
"LabelBackToUser": "Vissza a felhasználóhoz",
"LabelBackupLocation": "Biztonsági másolat helye",
"LabelBackupsEnableAutomaticBackups": "Automatikus biztonsági másolatok engedélyezése",
"LabelBackupsEnableAutomaticBackupsHelp": "Biztonsági másolatok mentése a /metadata/backups mappába",
"LabelBackupsMaxBackupSize": "Maximális biztonsági másolat méret (GB-ban)",
"LabelBackupsMaxBackupSizeHelp": "A rossz konfiguráció elleni védelem érdekében a biztonsági másolatok meghiúsulnak, ha meghaladják a beállított méretet.",
"LabelBackupsNumberToKeep": "Megtartandó biztonsági másolatok száma",
"LabelBackupsNumberToKeepHelp": "Egyszerre csak 1 biztonsági másolat kerül eltávolításra, tehát ha már több biztonsági másolat van, mint ez a szám, akkor manuálisan kell eltávolítani őket.",
"LabelBitrate": "Bitráta",
"LabelBooks": "Könyvek",
"LabelButtonText": "Gomb szövege",
"LabelChangePassword": "Jelszó megváltoztatása",
"LabelChannels": "Csatornák",
"LabelChapters": "Fejezetek",
"LabelChaptersFound": "fejezet található",
"LabelChapterTitle": "Fejezet címe",
"LabelClickForMoreInfo": "További információkért kattintson",
"LabelClosePlayer": "Lejátszó bezárása",
"LabelCodec": "Kodek",
"LabelCollapseSeries": "Sorozat összecsukása",
"LabelCollection": "Gyűjtemény",
"LabelCollections": "Gyűjtemények",
"LabelComplete": "Teljes",
"LabelConfirmPassword": "Jelszó megerősítése",
"LabelContinueListening": "Hallgatás folytatása",
"LabelContinueReading": "Olvasás folytatása",
"LabelContinueSeries": "Sorozat folytatása",
"LabelCover": "Borító",
"LabelCoverImageURL": "Borítókép URL",
"LabelCreatedAt": "Létrehozás ideje",
"LabelCronExpression": "Cron kifejezés",
"LabelCurrent": "Jelenlegi",
"LabelCurrently": "Jelenleg:",
"LabelCustomCronExpression": "Egyéni Cron kifejezés:",
"LabelDatetime": "Dátumidő",
"LabelDeleteFromFileSystemCheckbox": "Törlés a fájlrendszerről (ne jelölje be, ha csak az adatbázisból szeretné eltávolítani)",
"LabelDescription": "Leírás",
"LabelDeselectAll": "Minden kijelölés megszüntetése",
"LabelDevice": "Eszköz",
"LabelDeviceInfo": "Eszköz információ",
"LabelDeviceIsAvailableTo": "Eszköz elérhető a következő számára...",
"LabelDirectory": "Könyvtár",
"LabelDiscFromFilename": "Lemez a fájlnévből",
"LabelDiscFromMetadata": "Lemez a metaadatokból",
"LabelDiscover": "Felfedezés",
"LabelDownload": "Letöltés",
"LabelDownloadNEpisodes": "{0} epizód letöltése",
"LabelDuration": "Időtartam",
"LabelDurationFound": "Megtalált időtartam:",
"LabelEbook": "E-könyv",
"LabelEbooks": "E-könyvek",
"LabelEdit": "Szerkesztés",
"LabelEmail": "E-mail",
"LabelEmailSettingsFromAddress": "Feladó címe",
"LabelEmailSettingsSecure": "Biztonságos",
"LabelEmailSettingsSecureHelp": "Ha igaz, a kapcsolat TLS-t használ a szerverhez való csatlakozáskor. Ha hamis, akkor TLS-t használ, ha a szerver támogatja a STARTTLS kiterjesztést. A legtöbb esetben állítsa ezt az értéket igazra, ha a 465-ös portra csatlakozik. A 587-es vagy 25-ös port esetében tartsa hamis értéken. (a nodemailer.com/smtp/#authentication oldalról)",
"LabelEmailSettingsTestAddress": "Teszt cím",
"LabelEmbeddedCover": "Beágyazott borító",
"LabelEnable": "Engedélyezés",
"LabelEnd": "Vége",
"LabelEpisode": "Epizód",
"LabelEpisodeTitle": "Epizód címe",
"LabelEpisodeType": "Epizód típusa",
"LabelExample": "Példa",
"LabelExplicit": "Explicit",
"LabelFeedURL": "Hírcsatorna URL",
"LabelFetchingMetadata": "Metaadatok lekérése",
"LabelFile": "Fájl",
"LabelFileBirthtime": "Fájl létrehozásának ideje",
"LabelFileModified": "Fájl módosításának ideje",
"LabelFilename": "Fájlnév",
"LabelFilterByUser": "Szűrés felhasználó szerint",
"LabelFindEpisodes": "Epizódok keresése",
"LabelFinished": "Befejezett",
"LabelFolder": "Mappa",
"LabelFolders": "Mappák",
"LabelFontBold": "Félkövér",
"LabelFontFamily": "Betűtípus család",
"LabelFontItalic": "Dőlt",
"LabelFontScale": "Betűméret skála",
"LabelFontStrikethrough": "Áthúzott",
"LabelFormat": "Formátum",
"LabelGenre": "Műfaj",
"LabelGenres": "Műfajok",
"LabelHardDeleteFile": "Fájl végleges törlése",
"LabelHasEbook": "Van e-könyve",
"LabelHasSupplementaryEbook": "Van kiegészítő e-könyve",
"LabelHighestPriority": "Legmagasabb prioritás",
"LabelHost": "Hoszt",
"LabelHour": "Óra",
"LabelIcon": "Ikon",
"LabelImageURLFromTheWeb": "Kép URL a weben",
"LabelIncludeInTracklist": "Beleértve a sávlistába",
"LabelIncomplete": "Befejezetlen",
"LabelInProgress": "Folyamatban",
"LabelInterval": "Intervallum",
"LabelIntervalCustomDailyWeekly": "Egyéni napi/heti",
"LabelIntervalEvery12Hours": "Minden 12 órában",
"LabelIntervalEvery15Minutes": "Minden 15 percben",
"LabelIntervalEvery2Hours": "Minden 2 órában",
"LabelIntervalEvery30Minutes": "Minden 30 percben",
"LabelIntervalEvery6Hours": "Minden 6 órában",
"LabelIntervalEveryDay": "Minden nap",
"LabelIntervalEveryHour": "Minden órában",
"LabelInvalidParts": "Érvénytelen részek",
"LabelInvert": "Megfordítás",
"LabelItem": "Elem",
"LabelLanguage": "Nyelv",
"LabelLanguageDefaultServer": "Szerver alapértelmezett nyelve",
"LabelLastBookAdded": "Utolsó hozzáadott könyv",
"LabelLastBookUpdated": "Utolsó frissített könyv",
"LabelLastSeen": "Utolsó látogatás",
"LabelLastTime": "Utolsó alkalom",
"LabelLastUpdate": "Utolsó frissítés",
"LabelLayout": "Elrendezés",
"LabelLayoutSinglePage": "Egyoldalas",
"LabelLayoutSplitPage": "Kétoldalas",
"LabelLess": "Kevesebb",
"LabelLibrariesAccessibleToUser": "A felhasználó számára elérhető könyvtárak",
"LabelLibrary": "Könyvtár",
"LabelLibraryItem": "Könyvtári elem",
"LabelLibraryName": "Könyvtár neve",
"LabelLimit": "Korlát",
"LabelLineSpacing": "Sorköz",
"LabelListenAgain": "Újrahallgatás",
"LabelLogLevelDebug": "Debug",
"LabelLogLevelInfo": "Információ",
"LabelLogLevelWarn": "Figyelmeztetés",
"LabelLookForNewEpisodesAfterDate": "Új epizódok keresése ezen a dátum után",
"LabelLowestPriority": "Legalacsonyabb prioritás",
"LabelMatchExistingUsersBy": "Meglévő felhasználók egyeztetése",
"LabelMatchExistingUsersByDescription": "Meglévő felhasználók összekapcsolására használt. Egyszer összekapcsolva, a felhasználók egyedülálló azonosítóval lesznek egyeztetve az Ön SSO szolgáltatójától",
"LabelMediaPlayer": "Médialejátszó",
"LabelMediaType": "Média típus",
"LabelMetadataOrderOfPrecedenceDescription": "A magasabb prioritású metaadat-források felülírják az alacsonyabb prioritásúakat",
"LabelMetadataProvider": "Metaadat-szolgáltató",
"LabelMetaTag": "Meta címke",
"LabelMetaTags": "Meta címkék",
"LabelMinute": "Perc",
"LabelMissing": "Hiányzó",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Hiányzó részek",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "Engedélyezett mobil átirányítási URI-k",
"LabelMobileRedirectURIsDescription": "Ez egy fehérlista az érvényes mobilalkalmazás-átirányítási URI-k számára. Az alapértelmezett <code>audiobookshelf://oauth</code>, amely eltávolítható vagy kiegészíthető további URI-kkal harmadik féltől származó alkalmazásintegráció érdekében. Ha az egyetlen bejegyzés egy csillag (<code>*</code>), akkor bármely URI engedélyezett.",
"LabelMore": "Több",
"LabelMoreInfo": "További információ",
"LabelName": "Név",
"LabelNarrator": "Előadó",
"LabelNarrators": "Előadók",
"LabelNew": "Új",
"LabelNewestAuthors": "Legújabb szerzők",
"LabelNewestEpisodes": "Legújabb epizódok",
"LabelNewPassword": "Új jelszó",
"LabelNextBackupDate": "Következő biztonsági másolat dátuma",
"LabelNextScheduledRun": "Következő ütemezett futtatás",
"LabelNoEpisodesSelected": "Nincsenek kiválasztott epizódok",
"LabelNotes": "Megjegyzések",
"LabelNotFinished": "Nem befejezett",
"LabelNotificationAppriseURL": "Apprise URL(ek)",
"LabelNotificationAvailableVariables": "Elérhető változók",
"LabelNotificationBodyTemplate": "Törzs sablon",
"LabelNotificationEvent": "Értesítési esemény",
"LabelNotificationsMaxFailedAttempts": "Maximális sikertelen próbálkozások",
"LabelNotificationsMaxFailedAttemptsHelp": "Az értesítések akkor kerülnek letiltásra, ha ennyiszer nem sikerül elküldeni őket",
"LabelNotificationsMaxQueueSize": "Maximális értesítési események sorának mérete",
"LabelNotificationsMaxQueueSizeHelp": "Az események korlátozva vannak, hogy másodpercenként 1-szer történjenek. Ha a sor maximális méretű, akkor az események figyelmen kívül lesznek hagyva. Ez megakadályozza az értesítések spamelését.",
"LabelNotificationTitleTemplate": "Cím sablon",
"LabelNotStarted": "Nem indult el",
"LabelNumberOfBooks": "Könyvek száma",
"LabelNumberOfEpisodes": "Epizódok száma",
"LabelOpenRSSFeed": "RSS hírcsatorna megnyitása",
"LabelOverwrite": "Felülírás",
"LabelPassword": "Jelszó",
"LabelPath": "Útvonal",
"LabelPermissionsAccessAllLibraries": "Hozzáférhet az összes könyvtárhoz",
"LabelPermissionsAccessAllTags": "Hozzáférhet az összes címkéhez",
"LabelPermissionsAccessExplicitContent": "Hozzáférhet explicit tartalomhoz",
"LabelPermissionsDelete": "Törölhet",
"LabelPermissionsDownload": "Letölthet",
"LabelPermissionsUpdate": "Frissíthet",
"LabelPermissionsUpload": "Feltölthet",
"LabelPersonalYearReview": "Your Year in Review ({0})",
"LabelPhotoPathURL": "Fénykép útvonal/URL",
"LabelPlaylists": "Lejátszási listák",
"LabelPlayMethod": "Lejátszási módszer",
"LabelPodcast": "Podcast",
"LabelPodcasts": "Podcastok",
"LabelPodcastSearchRegion": "Podcast keresési régió",
"LabelPodcastType": "Podcast típus",
"LabelPort": "Port",
"LabelPrefixesToIgnore": "Figyelmen kívül hagyandó előtagok (nem érzékeny a kis- és nagybetűkre)",
"LabelPreventIndexing": "A hírcsatorna indexelésének megakadályozása az iTunes és a Google podcast könyvtáraiban",
"LabelPrimaryEbook": "Elsődleges e-könyv",
"LabelProgress": "Haladás",
"LabelProvider": "Szolgáltató",
"LabelPubDate": "Kiadás dátuma",
"LabelPublisher": "Kiadó",
"LabelPublishYear": "Kiadás éve",
"LabelRead": "Olvasás",
"LabelReadAgain": "Újraolvasás",
"LabelReadEbookWithoutProgress": "E-könyv olvasása haladás nélkül",
"LabelRecentlyAdded": "Nemrég hozzáadva",
"LabelRecentSeries": "Legutóbbi sorozatok",
"LabelRecommended": "Ajánlott",
"LabelRedo": "Újra",
"LabelRegion": "Régió",
"LabelReleaseDate": "Megjelenés dátuma",
"LabelRemoveCover": "Borító eltávolítása",
"LabelRowsPerPage": "Sorok száma oldalanként",
"LabelRSSFeedCustomOwnerEmail": "Egyéni tulajdonos e-mail",
"LabelRSSFeedCustomOwnerName": "Egyéni tulajdonos neve",
"LabelRSSFeedOpen": "RSS hírcsatorna nyitva",
"LabelRSSFeedPreventIndexing": "Indexelés megakadályozása",
"LabelRSSFeedSlug": "RSS hírcsatorna slug",
"LabelRSSFeedURL": "RSS hírcsatorna URL",
"LabelSearchTerm": "Keresési kifejezés",
"LabelSearchTitle": "Cím keresése",
"LabelSearchTitleOrASIN": "Cím vagy ASIN keresése",
"LabelSeason": "Évad",
"LabelSelectAllEpisodes": "Összes epizód kiválasztása",
"LabelSelectEpisodesShowing": "Kiválasztás {0} megjelenített epizód",
"LabelSelectUsers": "Felhasználók kiválasztása",
"LabelSendEbookToDevice": "E-könyv küldése...",
"LabelSequence": "Sorozat",
"LabelSeries": "Sorozat",
"LabelSeriesName": "Sorozat neve",
"LabelSeriesProgress": "Sorozat haladása",
"LabelServerYearReview": "Server Year in Review ({0})",
"LabelSetEbookAsPrimary": "Beállítás elsődlegesként",
"LabelSetEbookAsSupplementary": "Beállítás kiegészítőként",
"LabelSettingsAudiobooksOnly": "Csak hangoskönyvek",
"LabelSettingsAudiobooksOnlyHelp": "Ennek a beállításnak az engedélyezése figyelmen kívül hagyja az e-könyv fájlokat, kivéve, ha azok egy hangoskönyv mappában vannak, ebben az esetben kiegészítő e-könyvként lesznek beállítva",
"LabelSettingsBookshelfViewHelp": "Skeuomorfikus dizájn fa polcokkal",
"LabelSettingsChromecastSupport": "Chromecast támogatás",
"LabelSettingsDateFormat": "Dátumformátum",
"LabelSettingsDisableWatcher": "Figyelő letiltása",
"LabelSettingsDisableWatcherForLibrary": "Mappafigyelő letiltása a könyvtárban",
"LabelSettingsDisableWatcherHelp": "Letiltja az automatikus elem hozzáadás/frissítés funkciót, amikor fájlváltozásokat észlel. *Szerver újraindítása szükséges",
"LabelSettingsEnableWatcher": "Figyelő engedélyezése",
"LabelSettingsEnableWatcherForLibrary": "Mappafigyelő engedélyezése a könyvtárban",
"LabelSettingsEnableWatcherHelp": "Engedélyezi az automatikus elem hozzáadás/frissítés funkciót, amikor fájlváltozásokat észlel. *Szerver újraindítása szükséges",
"LabelSettingsExperimentalFeatures": "Kísérleti funkciók",
"LabelSettingsExperimentalFeaturesHelp": "Fejlesztés alatt álló funkciók, amelyek visszajelzésre és tesztelésre szorulnak. Kattintson a github megbeszélés megnyitásához.",
"LabelSettingsFindCovers": "Borítók keresése",
"LabelSettingsFindCoversHelp": "Ha a hangoskönyvnek nincs beágyazott borítója vagy borítóképe a mappában, a szkenner megpróbálja megtalálni a borítót.<br>Megjegyzés: Ez meghosszabbítja a szkennelési időt",
"LabelSettingsHideSingleBookSeries": "Egykönyves sorozatok elrejtése",
"LabelSettingsHideSingleBookSeriesHelp": "A csak egy könyvet tartalmazó sorozatok el lesznek rejtve a sorozatok oldalról és a kezdőlap polcairól.",
"LabelSettingsHomePageBookshelfView": "Kezdőlap használja a könyvespolc nézetet",
"LabelSettingsLibraryBookshelfView": "Könyvtár használja a könyvespolc nézetet",
"LabelSettingsParseSubtitles": "Feliratok elemzése",
"LabelSettingsParseSubtitlesHelp": "Feliratok kinyerése a hangoskönyv mappaneveiből.<br>A feliratnak el kell különülnie egy \" - \" jellel<br>például: \"Könyv címe - Egy felirat itt\" esetén a felirat \"Egy felirat itt\"",
"LabelSettingsPreferMatchedMetadata": "Preferált egyeztetett metaadatok",
"LabelSettingsPreferMatchedMetadataHelp": "Az egyeztetett adatok felülírják az elem részleteit a Gyors egyeztetés használatakor. Alapértelmezés szerint a Gyors egyeztetés csak a hiányzó részleteket tölti ki.",
"LabelSettingsSkipMatchingBooksWithASIN": "Már ASIN-nel rendelkező könyvek egyeztetésének kihagyása",
"LabelSettingsSkipMatchingBooksWithISBN": "Már ISBN-nel rendelkező könyvek egyeztetésének kihagyása",
"LabelSettingsSortingIgnorePrefixes": "Előtagok figyelmen kívül hagyása rendezéskor",
"LabelSettingsSortingIgnorePrefixesHelp": "például az \"a\" előtag esetén a \"A könyv címe\" könyv címe \"Könyv címe, A\" szerint rendeződik",
"LabelSettingsSquareBookCovers": "Négyzet alakú könyvborítók használata",
"LabelSettingsSquareBookCoversHelp": "Négyzet alakú borítók használata az 1,6:1 arányú standard könyvborítók helyett",
"LabelSettingsStoreCoversWithItem": "Borítók tárolása az elemmel",
"LabelSettingsStoreCoversWithItemHelp": "Alapértelmezés szerint a borítók a /metadata/items mappában vannak tárolva, ennek a beállításnak az engedélyezése a borítókat a könyvtári elem mappájában tárolja. Csak egy \"cover\" nevű fájl lesz megtartva",
"LabelSettingsStoreMetadataWithItem": "Metaadatok tárolása az elemmel",
"LabelSettingsStoreMetadataWithItemHelp": "Alapértelmezés szerint a metaadatfájlok a /metadata/items mappában vannak tárolva, ennek a beállításnak az engedélyezése a metaadatfájlokat a könyvtári elem mappáiban tárolja",
"LabelSettingsTimeFormat": "Időformátum",
"LabelShowAll": "Mindent mutat",
"LabelSize": "Méret",
"LabelSleepTimer": "Alvásidőzítő",
"LabelSlug": "Rövid cím",
"LabelStart": "Kezdés",
"LabelStarted": "Elkezdődött",
"LabelStartedAt": "Kezdés ideje",
"LabelStartTime": "Kezdési idő",
"LabelStatsAudioTracks": "Audiósávok",
"LabelStatsAuthors": "Szerzők",
"LabelStatsBestDay": "Legjobb nap",
"LabelStatsDailyAverage": "Napi átlag",
"LabelStatsDays": "Napok",
"LabelStatsDaysListened": "Hallgatott napok",
"LabelStatsHours": "Órák",
"LabelStatsInARow": "egymás után",
"LabelStatsItemsFinished": "Befejezett elemek",
"LabelStatsItemsInLibrary": "Elemek a könyvtárban",
"LabelStatsMinutes": "percek",
"LabelStatsMinutesListening": "Hallgatási percek",
"LabelStatsOverallDays": "Összes nap",
"LabelStatsOverallHours": "Összes óra",
"LabelStatsWeekListening": "Heti hallgatás",
"LabelSubtitle": "Felirat",
"LabelSupportedFileTypes": "Támogatott fájltípusok",
"LabelTag": "Címke",
"LabelTags": "Címkék",
"LabelTagsAccessibleToUser": "A felhasználó számára elérhető címkék",
"LabelTagsNotAccessibleToUser": "A felhasználó számára nem elérhető címkék",
"LabelTasks": "Futó feladatok",
"LabelTextEditorBulletedList": "Pontozott lista",
"LabelTextEditorLink": "Link",
"LabelTextEditorNumberedList": "Számozott lista",
"LabelTextEditorUnlink": "Link eltávolítása",
"LabelTheme": "Téma",
"LabelThemeDark": "Sötét",
"LabelThemeLight": "Világos",
"LabelTimeBase": "Időalap",
"LabelTimeListened": "Hallgatott idő",
"LabelTimeListenedToday": "Ma hallgatott idő",
"LabelTimeRemaining": "{0} maradt",
"LabelTimeToShift": "Eltolás ideje másodpercben",
"LabelTitle": "Cím",
"LabelToolsEmbedMetadata": "Metaadatok beágyazása",
"LabelToolsEmbedMetadataDescription": "Metaadatok beágyazása az audiofájlokba, beleértve a borítóképet és a fejezeteket.",
"LabelToolsMakeM4b": "M4B Hangoskönyv fájl készítése",
"LabelToolsMakeM4bDescription": ".M4B hangoskönyv fájl generálása beágyazott metaadatokkal, borítóképpel és fejezetekkel.",
"LabelToolsSplitM4b": "M4B felosztása MP3-ra",
"LabelToolsSplitM4bDescription": "MP3 fájlok létrehozása egy M4B-ből, fejezetenként felosztva, beágyazott metaadatokkal, borítóképpel és fejezetekkel.",
"LabelTotalDuration": "Teljes időtartam",
"LabelTotalTimeListened": "Teljes hallgatási idő",
"LabelTrackFromFilename": "Sáv a fájlnévből",
"LabelTrackFromMetadata": "Sáv a metaadatokból",
"LabelTracks": "Sávok",
"LabelTracksMultiTrack": "Többsávos",
"LabelTracksNone": "Nincsenek sávok",
"LabelTracksSingleTrack": "Egysávos",
"LabelType": "Típus",
"LabelUnabridged": "Nem tömörített",
"LabelUndo": "Visszavonás",
"LabelUnknown": "Ismeretlen",
"LabelUpdateCover": "Borító frissítése",
"LabelUpdateCoverHelp": "Lehetővé teszi a meglévő borítók felülírását a kiválasztott könyveknél, amikor találatot talál",
"LabelUpdatedAt": "Frissítve",
"LabelUpdateDetails": "Részletek frissítése",
"LabelUpdateDetailsHelp": "Lehetővé teszi a meglévő részletek felülírását a kiválasztott könyveknél, amikor találatot talál",
"LabelUploaderDragAndDrop": "Fájlok vagy mappák húzása és elengedése",
"LabelUploaderDropFiles": "Fájlok elengedése",
"LabelUploaderItemFetchMetadataHelp": "Cím, szerző és sorozat automatikus lekérése",
"LabelUseChapterTrack": "Fejezetsáv használata",
"LabelUseFullTrack": "Teljes sáv használata",
"LabelUser": "Felhasználó",
"LabelUsername": "Felhasználónév",
"LabelValue": "Érték",
"LabelVersion": "Verzió",
"LabelViewBookmarks": "Könyvjelzők megtekintése",
"LabelViewChapters": "Fejezetek megtekintése",
"LabelViewQueue": "Lejátszó sor megtekintése",
"LabelVolume": "Hangerő",
"LabelWeekdaysToRun": "Futás napjai",
"LabelYearReviewHide": "Hide Year in Review",
"LabelYearReviewShow": "See Year in Review",
"LabelYourAudiobookDuration": "Hangoskönyv időtartama",
"LabelYourBookmarks": "Könyvjelzőid",
"LabelYourPlaylists": "Lejátszási listáid",
"LabelYourProgress": "Haladásod",
"MessageAddToPlayerQueue": "Hozzáadás a lejátszó sorhoz",
"MessageAppriseDescription": "Ennek a funkció használatához futtatnia kell egy <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> példányt vagy egy olyan API-t, amely kezeli ezeket a kéréseket. <br />Az Apprise API URL-nek a teljes URL útvonalat kell tartalmaznia az értesítés elküldéséhez, például, ha az API példánya a <code>http://192.168.1.1:8337</code> címen szolgáltatva, akkor <code>http://192.168.1.1:8337/notify</code> értéket kell megadnia.",
"MessageBackupsDescription": "A biztonsági másolatok tartalmazzák a felhasználókat, a felhasználói haladást, a könyvtári elem részleteit, a szerver beállításait és a képeket, amelyek a <code>/metadata/items</code> és <code>/metadata/authors</code> mappákban vannak tárolva. A biztonsági másolatok <strong>nem</strong> tartalmazzák a könyvtári mappákban tárolt fájlokat.",
"MessageBatchQuickMatchDescription": "A Gyors egyeztetés megpróbálja hozzáadni a hiányzó borítókat és metaadatokat a kiválasztott elemekhez. Engedélyezze az alábbi opciókat, hogy a Gyors egyeztetés felülírhassa a meglévő borítókat és/vagy metaadatokat.",
"MessageBookshelfNoCollections": "Még nem készített gyűjteményeket",
"MessageBookshelfNoResultsForFilter": "Nincs eredmény a \"{0}: {1}\" szűrőre",
"MessageBookshelfNoRSSFeeds": "Nincsenek nyitott RSS hírcsatornák",
"MessageBookshelfNoSeries": "Nincsenek sorozatai",
"MessageChapterEndIsAfter": "A fejezet vége a hangoskönyv végét követi",
"MessageChapterErrorFirstNotZero": "Az első fejezetnek 0:00-kor kell kezdődnie",
"MessageChapterErrorStartGteDuration": "Érvénytelen kezdési idő, kevesebbnek kell lennie, mint a hangoskönyv időtartama",
"MessageChapterErrorStartLtPrev": "Érvénytelen kezdési idő, nagyobbnak kell lennie, mint az előző fejezet kezdési ideje",
"MessageChapterStartIsAfter": "A fejezet kezdete a hangoskönyv végét követi",
"MessageCheckingCron": "Cron ellenőrzése...",
"MessageConfirmCloseFeed": "Biztosan be szeretné zárni ezt a hírcsatornát?",
"MessageConfirmDeleteBackup": "Biztosan törölni szeretné a(z) {0} biztonsági másolatot?",
"MessageConfirmDeleteFile": "Ez törölni fogja a fájlt a fájlrendszerből. Biztos benne?",
"MessageConfirmDeleteLibrary": "Biztosan véglegesen törölni szeretné a(z) \"{0}\" könyvtárat?",
"MessageConfirmDeleteLibraryItem": "Ez eltávolítja a könyvtári elemet az adatbázisból és a fájlrendszerből. Biztos benne?",
"MessageConfirmDeleteLibraryItems": "Ez eltávolítja a(z) {0} könyvtári elemet az adatbázisból és a fájlrendszerből. Biztos benne?",
"MessageConfirmDeleteSession": "Biztosan törölni szeretné ezt a munkamenetet?",
"MessageConfirmForceReScan": "Biztosan kényszeríteni szeretné az újraszkennelést?",
"MessageConfirmMarkAllEpisodesFinished": "Biztosan meg szeretné jelölni az összes epizódot befejezettnek?",
"MessageConfirmMarkAllEpisodesNotFinished": "Biztosan meg szeretné jelölni az összes epizódot nem befejezettnek?",
"MessageConfirmMarkSeriesFinished": "Biztosan meg szeretné jelölni a sorozat összes könyvét befejezettnek?",
"MessageConfirmMarkSeriesNotFinished": "Biztosan meg szeretné jelölni a sorozat összes könyvét nem befejezettnek?",
"MessageConfirmQuickEmbed": "Figyelem! A Gyors beágyazás nem készít biztonsági másolatot az audiofájlokról. Győződjön meg arról, hogy van biztonsági másolata az audiofájlokról. <br><br>Szeretné folytatni?",
"MessageConfirmRemoveAllChapters": "Biztosan eltávolítja az összes fejezetet?",
"MessageConfirmRemoveAuthor": "Biztosan eltávolítja a(z) \"{0}\" szerzőt?",
"MessageConfirmRemoveCollection": "Biztosan eltávolítja a(z) \"{0}\" gyűjteményt?",
"MessageConfirmRemoveEpisode": "Biztosan eltávolítja a(z) \"{0}\" epizódot?",
"MessageConfirmRemoveEpisodes": "Biztosan eltávolítja a(z) {0} epizódot?",
"MessageConfirmRemoveListeningSessions": "Biztosan eltávolítja a(z) {0} hallgatási munkamenetet?",
"MessageConfirmRemoveNarrator": "Biztosan eltávolítja a(z) \"{0}\" előadót?",
"MessageConfirmRemovePlaylist": "Biztosan eltávolítja a(z) \"{0}\" lejátszási listáját?",
"MessageConfirmRenameGenre": "Biztosan át szeretné nevezni a(z) \"{0}\" műfajt \"{1}\"-re az összes elemnél?",
"MessageConfirmRenameGenreMergeNote": "Megjegyzés: Ez a műfaj már létezik, így össze lesznek vonva.",
"MessageConfirmRenameGenreWarning": "Figyelem! Egy hasonló, de eltérő nagybetűkkel rendelkező műfaj már létezik \"{0}\".",
"MessageConfirmRenameTag": "Biztosan át szeretné nevezni a(z) \"{0}\" címkét \"{1}\"-re az összes elemnél?",
"MessageConfirmRenameTagMergeNote": "Megjegyzés: Ez a címke már létezik, így össze lesznek vonva.",
"MessageConfirmRenameTagWarning": "Figyelem! Egy hasonló, de eltérő nagybetűkkel rendelkező címke már létezik \"{0}\".",
"MessageConfirmReScanLibraryItems": "Biztosan újra szeretné szkennelni a(z) {0} elemet?",
"MessageConfirmSendEbookToDevice": "Biztosan el szeretné küldeni a(z) {0} e-könyvet a(z) \"{1}\" eszközre?",
"MessageDownloadingEpisode": "Epizód letöltése",
"MessageDragFilesIntoTrackOrder": "Húzza a fájlokat a helyes sávrendbe",
"MessageEmbedFinished": "Beágyazás befejeződött!",
"MessageEpisodesQueuedForDownload": "{0} Epizód letöltésre várakozik",
"MessageFeedURLWillBe": "A hírcsatorna URL-je {0} lesz",
"MessageFetching": "Lekérés...",
"MessageForceReScanDescription": "minden fájlt újra szkennel, mint egy friss szkennelés. Az audiofájlok ID3 címkéi, OPF fájlok és szövegfájlok újként lesznek szkennelve.",
"MessageImportantNotice": "Fontos közlemény!",
"MessageInsertChapterBelow": "Fejezet beszúrása alulra",
"MessageItemsSelected": "{0} kiválasztott elem",
"MessageItemsUpdated": "{0} frissített elem",
"MessageJoinUsOn": "Csatlakozzon hozzánk: ",
"MessageListeningSessionsInTheLastYear": "{0} hallgatási munkamenet az elmúlt évben",
"MessageLoading": "Betöltés...",
"MessageLoadingFolders": "Mappák betöltése...",
"MessageM4BFailed": "M4B sikertelen!",
"MessageM4BFinished": "M4B befejeződött!",
"MessageMapChapterTitles": "Fejezetcímek hozzárendelése a meglévő hangoskönyv fejezeteihez anélkül, hogy az időbélyegeket módosítaná",
"MessageMarkAllEpisodesFinished": "Az összes epizód megjelölése befejezettnek",
"MessageMarkAllEpisodesNotFinished": "Az összes epizód megjelölése nem befejezettnek",
"MessageMarkAsFinished": "Megjelölés befejezettnek",
"MessageMarkAsNotFinished": "Megjelölés nem befejezettnek",
"MessageMatchBooksDescription": "megpróbálja egyeztetni a könyvtár könyveit egy kiválasztott keresési szolgáltató könyvével, és kitölti az üres részleteket és a borítót. Nem írja felül a részleteket.",
"MessageNoAudioTracks": "Nincsenek audiósávok",
"MessageNoAuthors": "Nincsenek szerzők",
"MessageNoBackups": "Nincsenek biztonsági másolatok",
"MessageNoBookmarks": "Nincsenek könyvjelzők",
"MessageNoChapters": "Nincsenek fejezetek",
"MessageNoCollections": "Nincsenek gyűjtemények",
"MessageNoCoversFound": "Nem találhatóak borítók",
"MessageNoDescription": "Nincs leírás",
"MessageNoDownloadsInProgress": "Jelenleg nincsenek folyamatban lévő letöltések",
"MessageNoDownloadsQueued": "Nincsenek várakozó letöltések",
"MessageNoEpisodeMatchesFound": "Nincs találat az epizódokra",
"MessageNoEpisodes": "Nincsenek epizódok",
"MessageNoFoldersAvailable": "Nincsenek elérhető mappák",
"MessageNoGenres": "Nincsenek műfajok",
"MessageNoIssues": "Nincsenek problémák",
"MessageNoItems": "Nincsenek elemek",
"MessageNoItemsFound": "Nem találhatóak elemek",
"MessageNoListeningSessions": "Nincsenek hallgatási munkamenetek",
"MessageNoLogs": "Nincsenek naplók",
"MessageNoMediaProgress": "Nincs előrehaladás a médialejátszásban",
"MessageNoNotifications": "Nincsenek értesítések",
"MessageNoPodcastsFound": "Nem találhatóak podcastok",
"MessageNoResults": "Nincsenek eredmények",
"MessageNoSearchResultsFor": "Nincs keresési eredmény erre: \"{0}\"",
"MessageNoSeries": "Nincsenek sorozatok",
"MessageNoTags": "Nincsenek címkék",
"MessageNoTasksRunning": "Nincsenek futó feladatok",
"MessageNotYetImplemented": "Még nem implementált",
"MessageNoUpdateNecessary": "Nincs szükség frissítésre",
"MessageNoUpdatesWereNecessary": "Nem volt szükség frissítésekre",
"MessageNoUserPlaylists": "Nincsenek felhasználói lejátszási listák",
"MessageOr": "vagy",
"MessagePauseChapter": "Fejezet lejátszásának szüneteltetése",
"MessagePlayChapter": "Fejezet elejének meghallgatása",
"MessagePlaylistCreateFromCollection": "Lejátszási lista létrehozása gyűjteményből",
"MessagePodcastHasNoRSSFeedForMatching": "A podcastnak nincs RSS hírcsatorna URL-je az egyeztetéshez",
"MessageQuickMatchDescription": "Üres elem részletek és borító feltöltése az első találati eredménnyel a(z) '{0}'-ból. Nem írja felül a részleteket, kivéve, ha a 'Preferált egyeztetett metaadatok' szerverbeállítás engedélyezve van.",
"MessageRemoveChapter": "Fejezet eltávolítása",
"MessageRemoveEpisodes": "Epizód(ok) eltávolítása: {0}",
"MessageRemoveFromPlayerQueue": "Eltávolítás a lejátszási sorból",
"MessageRemoveUserWarning": "Biztosan véglegesen törölni szeretné a(z) \"{0}\" felhasználót?",
"MessageReportBugsAndContribute": "Hibák jelentése, funkciók kérése és hozzájárulás itt:",
"MessageResetChaptersConfirm": "Biztosan alaphelyzetbe szeretné állítani a fejezeteket és visszavonni a módosításokat?",
"MessageRestoreBackupConfirm": "Biztosan vissza szeretné állítani a biztonsági másolatot, amely ekkor készült:",
"MessageRestoreBackupWarning": "A biztonsági mentés visszaállítása felülírja az egész adatbázist, amely a /config mappában található, valamint a borítóképeket a /metadata/items és /metadata/authors mappákban.<br /><br />A biztonsági mentések nem módosítják a könyvtár mappáiban található fájlokat. Ha engedélyezte a szerverbeállításokat a borítóképek és a metaadatok könyvtármappákban való tárolására, akkor ezek nem kerülnek biztonsági mentésre vagy felülírásra.<br /><br />A szerver használó összes kliens automatikusan frissül.",
"MessageSearchResultsFor": "Keresési eredmények",
"MessageSelected": "{0} kiválasztva",
"MessageServerCouldNotBeReached": "A szervert nem lehet elérni",
"MessageSetChaptersFromTracksDescription": "Fejezetek beállítása minden egyes hangfájlt egy fejezetként használva, és a fejezet címét a hangfájl neveként",
"MessageStartPlaybackAtTime": "\"{0}\" lejátszásának kezdése {1} -tól?",
"MessageThinking": "Gondolkodás...",
"MessageUploaderItemFailed": "A feltöltés sikertelen",
"MessageUploaderItemSuccess": "Sikeresen feltöltve!",
"MessageUploading": "Feltöltés...",
"MessageValidCronExpression": "Érvényes cron kifejezés",
"MessageWatcherIsDisabledGlobally": "A megfigyelő globálisan le van tiltva a szerver beállításokban",
"MessageXLibraryIsEmpty": "{0} könyvtár üres!",
"MessageYourAudiobookDurationIsLonger": "Az Ön hangoskönyvének hossza hosszabb, mint a talált időtartam",
"MessageYourAudiobookDurationIsShorter": "Az Ön hangoskönyvének hossza rövidebb, mint a talált időtartam",
"NoteChangeRootPassword": "A Root felhasználó az egyetlen felhasználó, akinek lehet üres jelszava",
"NoteChapterEditorTimes": "Megjegyzés: Az első fejezet kezdőidejének 0:00 kell lennie, és az utolsó fejezet kezdőideje nem haladhatja meg a hangoskönyv időtartamát.",
"NoteFolderPicker": "Megjegyzés: azok a mappák, amelyek már hozzá vannak rendelve, nem jelennek meg",
"NoteRSSFeedPodcastAppsHttps": "Figyelem: A legtöbb podcast alkalmazás megköveteli, hogy az RSS feed URL HTTPS-t használjon",
"NoteRSSFeedPodcastAppsPubDate": "Figyelem: Az egy vagy több epizódnak nincs Közzétételi dátuma. Néhány podcast alkalmazás ezt megköveteli.",
"NoteUploaderFoldersWithMediaFiles": "A médiafájlokat tartalmazó mappák külön könyvtári tételekként lesznek kezelve.",
"NoteUploaderOnlyAudioFiles": "Ha csak hangfájlokat tölt fel, akkor minden egyes hangfájl külön hangoskönyvként lesz kezelve.",
"NoteUploaderUnsupportedFiles": "A nem támogatott fájlok figyelmen kívül hagyásra kerülnek. Mappa kiválasztása vagy elengedésekor az elem mappáján kívüli egyéb fájlok figyelmen kívül lesznek hagyva.",
"PlaceholderNewCollection": "Új gyűjtemény neve",
"PlaceholderNewFolderPath": "Új mappa útvonala",
"PlaceholderNewPlaylist": "Új lejátszási lista neve",
"PlaceholderSearch": "Keresés..",
"PlaceholderSearchEpisode": "Epizód keresése..",
"ToastAccountUpdateFailed": "A fiók frissítése sikertelen",
"ToastAccountUpdateSuccess": "Fiók frissítve",
"ToastAuthorImageRemoveFailed": "A kép eltávolítása sikertelen",
"ToastAuthorImageRemoveSuccess": "Szerző képe eltávolítva",
"ToastAuthorUpdateFailed": "A szerző frissítése sikertelen",
"ToastAuthorUpdateMerged": "Szerző összevonva",
"ToastAuthorUpdateSuccess": "Szerző frissítve",
"ToastAuthorUpdateSuccessNoImageFound": "Szerző frissítve (nem található kép)",
"ToastBackupCreateFailed": "A biztonsági mentés létrehozása sikertelen",
"ToastBackupCreateSuccess": "Biztonsági mentés létrehozva",
"ToastBackupDeleteFailed": "A biztonsági mentés törlése sikertelen",
"ToastBackupDeleteSuccess": "Biztonsági mentés törölve",
"ToastBackupRestoreFailed": "A biztonsági mentés visszaállítása sikertelen",
"ToastBackupUploadFailed": "A biztonsági mentés feltöltése sikertelen",
"ToastBackupUploadSuccess": "Biztonsági mentés feltöltve",
"ToastBatchUpdateFailed": "Kötegelt frissítés sikertelen",
"ToastBatchUpdateSuccess": "Kötegelt frissítés sikeres",
"ToastBookmarkCreateFailed": "Könyvjelző létrehozása sikertelen",
"ToastBookmarkCreateSuccess": "Könyvjelző hozzáadva",
"ToastBookmarkRemoveFailed": "Könyvjelző eltávolítása sikertelen",
"ToastBookmarkRemoveSuccess": "Könyvjelző eltávolítva",
"ToastBookmarkUpdateFailed": "Könyvjelző frissítése sikertelen",
"ToastBookmarkUpdateSuccess": "Könyvjelző frissítve",
"ToastChaptersHaveErrors": "A fejezetek hibákat tartalmaznak",
"ToastChaptersMustHaveTitles": "A fejezeteknek címekkel kell rendelkezniük",
"ToastCollectionItemsRemoveFailed": "Elem(ek) eltávolítása a gyűjteményből sikertelen",
"ToastCollectionItemsRemoveSuccess": "Elem(ek) eltávolítva a gyűjteményből",
"ToastCollectionRemoveFailed": "Gyűjtemény eltávolítása sikertelen",
"ToastCollectionRemoveSuccess": "Gyűjtemény eltávolítva",
"ToastCollectionUpdateFailed": "Gyűjtemény frissítése sikertelen",
"ToastCollectionUpdateSuccess": "Gyűjtemény frissítve",
"ToastItemCoverUpdateFailed": "Elem borítójának frissítése sikertelen",
"ToastItemCoverUpdateSuccess": "Elem borítója frissítve",
"ToastItemDetailsUpdateFailed": "Elem részleteinek frissítése sikertelen",
"ToastItemDetailsUpdateSuccess": "Elem részletei frissítve",
"ToastItemDetailsUpdateUnneeded": "Nincsenek szükséges frissítések a tétel részletein",
"ToastItemMarkedAsFinishedFailed": "Megjelölés Befejezettként sikertelen",
"ToastItemMarkedAsFinishedSuccess": "Elem megjelölve Befejezettként",
"ToastItemMarkedAsNotFinishedFailed": "Nem sikerült Nem Befejezettként megjelölni az elemet",
"ToastItemMarkedAsNotFinishedSuccess": "Elem megjelölve Nem Befejezettként",
"ToastLibraryCreateFailed": "Könyvtár létrehozása sikertelen",
"ToastLibraryCreateSuccess": "\"{0}\" könyvtár létrehozva",
"ToastLibraryDeleteFailed": "Könyvtár törlése sikertelen",
"ToastLibraryDeleteSuccess": "Könyvtár törölve",
"ToastLibraryScanFailedToStart": "A beolvasás elindítása sikertelen",
"ToastLibraryScanStarted": "Könyvtár beolvasása elindítva",
"ToastLibraryUpdateFailed": "Könyvtár frissítése sikertelen",
"ToastLibraryUpdateSuccess": "\"{0}\" könyvtár frissítve",
"ToastPlaylistCreateFailed": "Lejátszási lista létrehozása sikertelen",
"ToastPlaylistCreateSuccess": "Lejátszási lista létrehozva",
"ToastPlaylistRemoveFailed": "Lejátszási lista eltávolítása sikertelen",
"ToastPlaylistRemoveSuccess": "Lejátszási lista eltávolítva",
"ToastPlaylistUpdateFailed": "Lejátszási lista frissítése sikertelen",
"ToastPlaylistUpdateSuccess": "Lejátszási lista frissítve",
"ToastPodcastCreateFailed": "Podcast létrehozása sikertelen",
"ToastPodcastCreateSuccess": "Podcast sikeresen létrehozva",
"ToastRemoveItemFromCollectionFailed": "Tétel eltávolítása a gyűjteményből sikertelen",
"ToastRemoveItemFromCollectionSuccess": "Tétel eltávolítva a gyűjteményből",
"ToastRSSFeedCloseFailed": "RSS feed bezárása sikertelen",
"ToastRSSFeedCloseSuccess": "RSS feed bezárva",
"ToastSendEbookToDeviceFailed": "E-könyv küldése az eszközre sikertelen",
"ToastSendEbookToDeviceSuccess": "E-könyv elküldve az eszközre \"{0}\"",
"ToastSeriesUpdateFailed": "Sorozat frissítése sikertelen",
"ToastSeriesUpdateSuccess": "Sorozat frissítése sikeres",
"ToastSessionDeleteFailed": "Munkamenet törlése sikertelen",
"ToastSessionDeleteSuccess": "Munkamenet törölve",
"ToastSocketConnected": "Socket csatlakoztatva",
"ToastSocketDisconnected": "Socket lecsatlakoztatva",
"ToastSocketFailedToConnect": "A Socket csatlakoztatása sikertelen",
"ToastUserDeleteFailed": "Felhasználó törlése sikertelen",
"ToastUserDeleteSuccess": "Felhasználó törölve"
}

View File

@ -32,6 +32,8 @@
"ButtonHide": "Nascondi", "ButtonHide": "Nascondi",
"ButtonHome": "Home", "ButtonHome": "Home",
"ButtonIssues": "Errori", "ButtonIssues": "Errori",
"ButtonJumpBackward": "Salta indietro",
"ButtonJumpForward": "Salta Avanti",
"ButtonLatest": "Ultimi", "ButtonLatest": "Ultimi",
"ButtonLibrary": "Libreria", "ButtonLibrary": "Libreria",
"ButtonLogout": "Disconnetti", "ButtonLogout": "Disconnetti",
@ -41,12 +43,17 @@
"ButtonMatchAllAuthors": "Aggiungi metadata agli Autori", "ButtonMatchAllAuthors": "Aggiungi metadata agli Autori",
"ButtonMatchBooks": "Aggiungi metadata della Libreria", "ButtonMatchBooks": "Aggiungi metadata della Libreria",
"ButtonNevermind": "Nevermind", "ButtonNevermind": "Nevermind",
"ButtonNext": "Next",
"ButtonNextChapter": "Prossimo Capitolo",
"ButtonOk": "Ok", "ButtonOk": "Ok",
"ButtonOpenFeed": "Apri Feed", "ButtonOpenFeed": "Apri Feed",
"ButtonOpenManager": "Apri Manager", "ButtonOpenManager": "Apri Manager",
"ButtonPause": "Pausa",
"ButtonPlay": "Play", "ButtonPlay": "Play",
"ButtonPlaying": "In Riproduzione", "ButtonPlaying": "In Riproduzione",
"ButtonPlaylists": "Playlists", "ButtonPlaylists": "Playlists",
"ButtonPrevious": "Previous",
"ButtonPreviousChapter": "Capitolo Precendente",
"ButtonPurgeAllCache": "Elimina tutta la Cache", "ButtonPurgeAllCache": "Elimina tutta la Cache",
"ButtonPurgeItemsCache": "Elimina la Cache selezionata", "ButtonPurgeItemsCache": "Elimina la Cache selezionata",
"ButtonPurgeMediaProgress": "Elimina info dei media ascoltati", "ButtonPurgeMediaProgress": "Elimina info dei media ascoltati",
@ -54,6 +61,7 @@
"ButtonQueueRemoveItem": "Rimuovi dalla Coda", "ButtonQueueRemoveItem": "Rimuovi dalla Coda",
"ButtonQuickMatch": "Controlla Metadata Auto", "ButtonQuickMatch": "Controlla Metadata Auto",
"ButtonRead": "Leggi", "ButtonRead": "Leggi",
"ButtonRefresh": "Refresh",
"ButtonRemove": "Rimuovi", "ButtonRemove": "Rimuovi",
"ButtonRemoveAll": "Rimuovi Tutto", "ButtonRemoveAll": "Rimuovi Tutto",
"ButtonRemoveAllLibraryItems": "Rimuovi tutto il contenuto della libreria", "ButtonRemoveAllLibraryItems": "Rimuovi tutto il contenuto della libreria",
@ -73,6 +81,7 @@
"ButtonSelectFolderPath": "Seleziona percorso cartella", "ButtonSelectFolderPath": "Seleziona percorso cartella",
"ButtonSeries": "Serie", "ButtonSeries": "Serie",
"ButtonSetChaptersFromTracks": "Impostare i capitoli dalle tracce", "ButtonSetChaptersFromTracks": "Impostare i capitoli dalle tracce",
"ButtonShare": "Share",
"ButtonShiftTimes": "Ricerca veloce", "ButtonShiftTimes": "Ricerca veloce",
"ButtonShow": "Mostra", "ButtonShow": "Mostra",
"ButtonStartM4BEncode": "Inizia L'Encode del M4B", "ButtonStartM4BEncode": "Inizia L'Encode del M4B",
@ -87,15 +96,15 @@
"ButtonUserEdit": "Modifica Utente {0}", "ButtonUserEdit": "Modifica Utente {0}",
"ButtonViewAll": "Mostra Tutto", "ButtonViewAll": "Mostra Tutto",
"ButtonYes": "Si", "ButtonYes": "Si",
"ErrorUploadFetchMetadataAPI": "Error fetching metadata", "ErrorUploadFetchMetadataAPI": "Errore Recupero metadati",
"ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author", "ErrorUploadFetchMetadataNoResults": "Impossibile recuperare i metadati: prova a modificate il titolo e/o l'autore",
"ErrorUploadLacksTitle": "Must have a title", "ErrorUploadLacksTitle": "Deve avere un titolo",
"HeaderAccount": "Account", "HeaderAccount": "Account",
"HeaderAdvanced": "Avanzate", "HeaderAdvanced": "Avanzate",
"HeaderAppriseNotificationSettings": "Apprendi le impostazioni di Notifica", "HeaderAppriseNotificationSettings": "Apprendi le impostazioni di Notifica",
"HeaderAudiobookTools": "Utilità Audiobook File Management", "HeaderAudiobookTools": "Utilità Audiobook File Management",
"HeaderAudioTracks": "Tracce Audio", "HeaderAudioTracks": "Tracce Audio",
"HeaderAuthentication": "Authentication", "HeaderAuthentication": "Authenticazione",
"HeaderBackups": "Backup", "HeaderBackups": "Backup",
"HeaderChangePassword": "Cambia Password", "HeaderChangePassword": "Cambia Password",
"HeaderChapters": "Capitoli", "HeaderChapters": "Capitoli",
@ -104,8 +113,9 @@
"HeaderCollectionItems": "Elementi della Raccolta", "HeaderCollectionItems": "Elementi della Raccolta",
"HeaderCover": "Cover", "HeaderCover": "Cover",
"HeaderCurrentDownloads": "Download Correnti", "HeaderCurrentDownloads": "Download Correnti",
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
"HeaderDetails": "Dettagli", "HeaderDetails": "Dettagli",
"HeaderDownloadQueue": "Download Queue", "HeaderDownloadQueue": "Download coda",
"HeaderEbookFiles": "Ebook File", "HeaderEbookFiles": "Ebook File",
"HeaderEmail": "Email", "HeaderEmail": "Email",
"HeaderEmailSettings": "Email Settings", "HeaderEmailSettings": "Email Settings",
@ -130,7 +140,7 @@
"HeaderManageTags": "Gestisci Tags", "HeaderManageTags": "Gestisci Tags",
"HeaderMapDetails": "Mappa Dettagli", "HeaderMapDetails": "Mappa Dettagli",
"HeaderMatch": "Trova Corrispondenza", "HeaderMatch": "Trova Corrispondenza",
"HeaderMetadataOrderOfPrecedence": "Metadata order of precedence", "HeaderMetadataOrderOfPrecedence": "Priorità ordine Metadata",
"HeaderMetadataToEmbed": "Metadata da incorporare", "HeaderMetadataToEmbed": "Metadata da incorporare",
"HeaderNewAccount": "Nuovo Account", "HeaderNewAccount": "Nuovo Account",
"HeaderNewLibrary": "Nuova Libreria", "HeaderNewLibrary": "Nuova Libreria",
@ -174,6 +184,7 @@
"HeaderUpdateDetails": "Aggiorna Dettagli", "HeaderUpdateDetails": "Aggiorna Dettagli",
"HeaderUpdateLibrary": "Aggiorna Libreria", "HeaderUpdateLibrary": "Aggiorna Libreria",
"HeaderUsers": "Utenti", "HeaderUsers": "Utenti",
"HeaderYearReview": "Year {0} in Review",
"HeaderYourStats": "Statistiche Personali", "HeaderYourStats": "Statistiche Personali",
"LabelAbridged": "Abbreviato", "LabelAbridged": "Abbreviato",
"LabelAccountType": "Tipo di Account", "LabelAccountType": "Tipo di Account",
@ -199,12 +210,12 @@
"LabelAuthorLastFirst": "Autori (Per Cognome)", "LabelAuthorLastFirst": "Autori (Per Cognome)",
"LabelAuthors": "Autori", "LabelAuthors": "Autori",
"LabelAutoDownloadEpisodes": "Auto Download Episodi", "LabelAutoDownloadEpisodes": "Auto Download Episodi",
"LabelAutoFetchMetadata": "Auto Fetch Metadata", "LabelAutoFetchMetadata": "Auto controllo Metadata",
"LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.", "LabelAutoFetchMetadataHelp": "Recupera i metadati per titolo, autore e serie per semplificare il caricamento. Potrebbe essere necessario abbinare metadati aggiuntivi dopo il caricamento.",
"LabelAutoLaunch": "Auto Launch", "LabelAutoLaunch": "Auto Launch",
"LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)", "LabelAutoLaunchDescription": "Reindirizzamento automatico al provider di autenticazione quando si accede alla pagina di accesso (percorso di sostituzione manuale <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Auto Register", "LabelAutoRegister": "Auto Registrazione",
"LabelAutoRegisterDescription": "Automatically create new users after logging in", "LabelAutoRegisterDescription": "Crea automaticamente nuovi utenti dopo aver effettuato l'accesso",
"LabelBackToUser": "Torna a Utenti", "LabelBackToUser": "Torna a Utenti",
"LabelBackupLocation": "Percorso del Backup", "LabelBackupLocation": "Percorso del Backup",
"LabelBackupsEnableAutomaticBackups": "Abilita backup Automatico", "LabelBackupsEnableAutomaticBackups": "Abilita backup Automatico",
@ -215,7 +226,7 @@
"LabelBackupsNumberToKeepHelp": "Verrà rimosso solo 1 backup alla volta, quindi se hai più backup, dovrai rimuoverli manualmente.", "LabelBackupsNumberToKeepHelp": "Verrà rimosso solo 1 backup alla volta, quindi se hai più backup, dovrai rimuoverli manualmente.",
"LabelBitrate": "Bitrate", "LabelBitrate": "Bitrate",
"LabelBooks": "Libri", "LabelBooks": "Libri",
"LabelButtonText": "Button Text", "LabelButtonText": "Buttone Testo",
"LabelChangePassword": "Cambia Password", "LabelChangePassword": "Cambia Password",
"LabelChannels": "Canali", "LabelChannels": "Canali",
"LabelChapters": "Capitoli", "LabelChapters": "Capitoli",
@ -271,7 +282,7 @@
"LabelExample": "Esempio", "LabelExample": "Esempio",
"LabelExplicit": "Esplicito", "LabelExplicit": "Esplicito",
"LabelFeedURL": "Feed URL", "LabelFeedURL": "Feed URL",
"LabelFetchingMetadata": "Fetching Metadata", "LabelFetchingMetadata": "Recupero dei metadati",
"LabelFile": "File", "LabelFile": "File",
"LabelFileBirthtime": "Data Creazione", "LabelFileBirthtime": "Data Creazione",
"LabelFileModified": "Ultima modifica", "LabelFileModified": "Ultima modifica",
@ -292,7 +303,7 @@
"LabelHardDeleteFile": "Elimina Definitivamente", "LabelHardDeleteFile": "Elimina Definitivamente",
"LabelHasEbook": "Un ebook", "LabelHasEbook": "Un ebook",
"LabelHasSupplementaryEbook": "Un ebook Supplementare", "LabelHasSupplementaryEbook": "Un ebook Supplementare",
"LabelHighestPriority": "Highest priority", "LabelHighestPriority": "Priorità Massima",
"LabelHost": "Host", "LabelHost": "Host",
"LabelHour": "Ora", "LabelHour": "Ora",
"LabelIcon": "Icona", "LabelIcon": "Icona",
@ -334,20 +345,22 @@
"LabelLogLevelInfo": "Info", "LabelLogLevelInfo": "Info",
"LabelLogLevelWarn": "Allarme", "LabelLogLevelWarn": "Allarme",
"LabelLookForNewEpisodesAfterDate": "Cerca nuovi episodi dopo questa data", "LabelLookForNewEpisodesAfterDate": "Cerca nuovi episodi dopo questa data",
"LabelLowestPriority": "Lowest Priority", "LabelLowestPriority": "Priorità Minima",
"LabelMatchExistingUsersBy": "Match existing users by", "LabelMatchExistingUsersBy": "Abbina gli utenti esistenti per",
"LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMatchExistingUsersByDescription": "Utilizzato per connettere gli utenti esistenti. Una volta connessi, gli utenti verranno abbinati a un ID univoco dal tuo provider SSO",
"LabelMediaPlayer": "Media Player", "LabelMediaPlayer": "Media Player",
"LabelMediaType": "Tipo Media", "LabelMediaType": "Tipo Media",
"LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", "LabelMetadataOrderOfPrecedenceDescription": "Le origini di metadati con priorità più alta sovrascriveranno le origini di metadati con priorità inferiore",
"LabelMetadataProvider": "Metadata Provider", "LabelMetadataProvider": "Metadata Provider",
"LabelMetaTag": "Meta Tag", "LabelMetaTag": "Meta Tag",
"LabelMetaTags": "Meta Tags", "LabelMetaTags": "Meta Tags",
"LabelMinute": "Minuto", "LabelMinute": "Minuto",
"LabelMissing": "Altro", "LabelMissing": "Altro",
"LabelMissingParts": "Parti rimantenti", "LabelMissingEbook": "Has no ebook",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", "LabelMissingParts": "Parti rimanenti",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", "LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "URI di reindirizzamento mobile consentiti",
"LabelMobileRedirectURIsDescription": "Questa è una lista bianca di URI di reindirizzamento validi per le app mobili. Quello predefinito è <code>audiobookshelf://oauth</code>, che puoi rimuovere o integrare con URI aggiuntivi per l'integrazione di app di terze parti. Utilizzando un asterisco (<code>*</code>) poiché l'unica voce consente qualsiasi URI.",
"LabelMore": "Molto", "LabelMore": "Molto",
"LabelMoreInfo": "Più Info", "LabelMoreInfo": "Più Info",
"LabelName": "Nome", "LabelName": "Nome",
@ -385,11 +398,13 @@
"LabelPermissionsDownload": "Può Scaricare", "LabelPermissionsDownload": "Può Scaricare",
"LabelPermissionsUpdate": "Può Aggiornare", "LabelPermissionsUpdate": "Può Aggiornare",
"LabelPermissionsUpload": "Può caricare", "LabelPermissionsUpload": "Può caricare",
"LabelPersonalYearReview": "Your Year in Review ({0})",
"LabelPhotoPathURL": "foto Path/URL", "LabelPhotoPathURL": "foto Path/URL",
"LabelPlaylists": "Playlists", "LabelPlaylists": "Playlists",
"LabelPlayMethod": "Metodo di riproduzione", "LabelPlayMethod": "Metodo di riproduzione",
"LabelPodcast": "Podcast", "LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts", "LabelPodcasts": "Podcasts",
"LabelPodcastSearchRegion": "Area di ricerca podcast",
"LabelPodcastType": "Tipo di Podcast", "LabelPodcastType": "Tipo di Podcast",
"LabelPort": "Port", "LabelPort": "Port",
"LabelPrefixesToIgnore": "Suffissi da ignorare (specificando maiuscole e minuscole)", "LabelPrefixesToIgnore": "Suffissi da ignorare (specificando maiuscole e minuscole)",
@ -406,7 +421,7 @@
"LabelRecentlyAdded": "Aggiunti Recentemente", "LabelRecentlyAdded": "Aggiunti Recentemente",
"LabelRecentSeries": "Serie Recenti", "LabelRecentSeries": "Serie Recenti",
"LabelRecommended": "Raccomandati", "LabelRecommended": "Raccomandati",
"LabelRedo": "Redo", "LabelRedo": "Rifai",
"LabelRegion": "Regione", "LabelRegion": "Regione",
"LabelReleaseDate": "Data Release", "LabelReleaseDate": "Data Release",
"LabelRemoveCover": "Rimuovi cover", "LabelRemoveCover": "Rimuovi cover",
@ -429,6 +444,7 @@
"LabelSeries": "Serie", "LabelSeries": "Serie",
"LabelSeriesName": "Nome Serie", "LabelSeriesName": "Nome Serie",
"LabelSeriesProgress": "Cominciato", "LabelSeriesProgress": "Cominciato",
"LabelServerYearReview": "Server Year in Review ({0})",
"LabelSetEbookAsPrimary": "Immposta come Primario", "LabelSetEbookAsPrimary": "Immposta come Primario",
"LabelSetEbookAsSupplementary": "Imposta come Suplementare", "LabelSetEbookAsSupplementary": "Imposta come Suplementare",
"LabelSettingsAudiobooksOnly": "Solo Audiolibri", "LabelSettingsAudiobooksOnly": "Solo Audiolibri",
@ -468,7 +484,7 @@
"LabelShowAll": "Mostra Tutto", "LabelShowAll": "Mostra Tutto",
"LabelSize": "Dimensione", "LabelSize": "Dimensione",
"LabelSleepTimer": "Sleep timer", "LabelSleepTimer": "Sleep timer",
"LabelSlug": "Slug", "LabelSlug": "Lento",
"LabelStart": "Inizo", "LabelStart": "Inizo",
"LabelStarted": "Iniziato", "LabelStarted": "Iniziato",
"LabelStartedAt": "Iniziato al", "LabelStartedAt": "Iniziato al",
@ -495,9 +511,9 @@
"LabelTagsAccessibleToUser": "Tags permessi agli Utenti", "LabelTagsAccessibleToUser": "Tags permessi agli Utenti",
"LabelTagsNotAccessibleToUser": "Tags non accessibile agli Utenti", "LabelTagsNotAccessibleToUser": "Tags non accessibile agli Utenti",
"LabelTasks": "Processi in esecuzione", "LabelTasks": "Processi in esecuzione",
"LabelTextEditorBulletedList": "Bulleted list", "LabelTextEditorBulletedList": "Elenco puntato",
"LabelTextEditorLink": "Link", "LabelTextEditorLink": "Link",
"LabelTextEditorNumberedList": "Numbered list", "LabelTextEditorNumberedList": "Elenco Numerato",
"LabelTextEditorUnlink": "Unlink", "LabelTextEditorUnlink": "Unlink",
"LabelTheme": "Tema", "LabelTheme": "Tema",
"LabelThemeDark": "Scuro", "LabelThemeDark": "Scuro",
@ -520,7 +536,7 @@
"LabelTrackFromMetadata": "Traccia da Metadata", "LabelTrackFromMetadata": "Traccia da Metadata",
"LabelTracks": "Traccia", "LabelTracks": "Traccia",
"LabelTracksMultiTrack": "Multi-traccia", "LabelTracksMultiTrack": "Multi-traccia",
"LabelTracksNone": "No tracks", "LabelTracksNone": "Nessuna traccia",
"LabelTracksSingleTrack": "Traccia-singola", "LabelTracksSingleTrack": "Traccia-singola",
"LabelType": "Tipo", "LabelType": "Tipo",
"LabelUnabridged": "Integrale", "LabelUnabridged": "Integrale",
@ -533,7 +549,7 @@
"LabelUpdateDetailsHelp": "Consenti la sovrascrittura dei dettagli esistenti per i libri selezionati quando viene individuata una corrispondenza", "LabelUpdateDetailsHelp": "Consenti la sovrascrittura dei dettagli esistenti per i libri selezionati quando viene individuata una corrispondenza",
"LabelUploaderDragAndDrop": "Drag & drop file o Cartelle", "LabelUploaderDragAndDrop": "Drag & drop file o Cartelle",
"LabelUploaderDropFiles": "Elimina file", "LabelUploaderDropFiles": "Elimina file",
"LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series", "LabelUploaderItemFetchMetadataHelp": "Recupera automaticamente titolo, autore e serie",
"LabelUseChapterTrack": "Usa il Capitolo della Traccia", "LabelUseChapterTrack": "Usa il Capitolo della Traccia",
"LabelUseFullTrack": "Usa la traccia totale", "LabelUseFullTrack": "Usa la traccia totale",
"LabelUser": "Utente", "LabelUser": "Utente",
@ -545,6 +561,8 @@
"LabelViewQueue": "Visualizza coda", "LabelViewQueue": "Visualizza coda",
"LabelVolume": "Volume", "LabelVolume": "Volume",
"LabelWeekdaysToRun": "Giorni feriali da eseguire", "LabelWeekdaysToRun": "Giorni feriali da eseguire",
"LabelYearReviewHide": "Hide Year in Review",
"LabelYearReviewShow": "See Year in Review",
"LabelYourAudiobookDuration": "La durata dell'audiolibro", "LabelYourAudiobookDuration": "La durata dell'audiolibro",
"LabelYourBookmarks": "I tuoi Preferiti", "LabelYourBookmarks": "I tuoi Preferiti",
"LabelYourPlaylists": "le tue Playlist", "LabelYourPlaylists": "le tue Playlist",
@ -581,7 +599,7 @@
"MessageConfirmRemoveCollection": "Sei sicuro di voler rimuovere la Raccolta \"{0}\"?", "MessageConfirmRemoveCollection": "Sei sicuro di voler rimuovere la Raccolta \"{0}\"?",
"MessageConfirmRemoveEpisode": "Sei sicuro di voler rimuovere l'episodio \"{0}\"?", "MessageConfirmRemoveEpisode": "Sei sicuro di voler rimuovere l'episodio \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Sei sicuro di voler rimuovere {0} episodi?", "MessageConfirmRemoveEpisodes": "Sei sicuro di voler rimuovere {0} episodi?",
"MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?", "MessageConfirmRemoveListeningSessions": "Sei sicuro di voler rimuovere {0} sessioni di Ascolto?",
"MessageConfirmRemoveNarrator": "Sei sicuro di voler rimuovere il narratore \"{0}\"?", "MessageConfirmRemoveNarrator": "Sei sicuro di voler rimuovere il narratore \"{0}\"?",
"MessageConfirmRemovePlaylist": "Sei sicuro di voler rimuovere la tua playlist \"{0}\"?", "MessageConfirmRemovePlaylist": "Sei sicuro di voler rimuovere la tua playlist \"{0}\"?",
"MessageConfirmRenameGenre": "Sei sicuro di voler rinominare il genere \"{0}\" in \"{1}\" per tutti gli oggetti?", "MessageConfirmRenameGenre": "Sei sicuro di voler rinominare il genere \"{0}\" in \"{1}\" per tutti gli oggetti?",
@ -661,7 +679,7 @@
"MessageRestoreBackupConfirm": "Sei sicuro di voler ripristinare il backup creato su", "MessageRestoreBackupConfirm": "Sei sicuro di voler ripristinare il backup creato su",
"MessageRestoreBackupWarning": "Il ripristino di un backup sovrascriverà l'intero database situato in /config e sovrascrive le immagini in /metadata/items & /metadata/authors.<br /><br />I backup non modificano alcun file nelle cartelle della libreria. Se hai abilitato le impostazioni del server per archiviare copertine e metadati nelle cartelle della libreria, questi non vengono sottoposti a backup o sovrascritti.<br /><br />Tutti i client che utilizzano il tuo server verranno aggiornati automaticamente.", "MessageRestoreBackupWarning": "Il ripristino di un backup sovrascriverà l'intero database situato in /config e sovrascrive le immagini in /metadata/items & /metadata/authors.<br /><br />I backup non modificano alcun file nelle cartelle della libreria. Se hai abilitato le impostazioni del server per archiviare copertine e metadati nelle cartelle della libreria, questi non vengono sottoposti a backup o sovrascritti.<br /><br />Tutti i client che utilizzano il tuo server verranno aggiornati automaticamente.",
"MessageSearchResultsFor": "cerca risultati per", "MessageSearchResultsFor": "cerca risultati per",
"MessageSelected": "{0} selected", "MessageSelected": "{0} selezionati",
"MessageServerCouldNotBeReached": "Impossibile raggiungere il server", "MessageServerCouldNotBeReached": "Impossibile raggiungere il server",
"MessageSetChaptersFromTracksDescription": "Impostare i capitoli utilizzando ciascun file audio come capitolo e il titolo del capitolo come nome del file audio", "MessageSetChaptersFromTracksDescription": "Impostare i capitoli utilizzando ciascun file audio come capitolo e il titolo del capitolo come nome del file audio",
"MessageStartPlaybackAtTime": "Avvia la riproduzione per \"{0}\" a {1}?", "MessageStartPlaybackAtTime": "Avvia la riproduzione per \"{0}\" a {1}?",
@ -750,7 +768,7 @@
"ToastSendEbookToDeviceFailed": "Impossibile inviare l'ebook al dispositivo", "ToastSendEbookToDeviceFailed": "Impossibile inviare l'ebook al dispositivo",
"ToastSendEbookToDeviceSuccess": "Ebook inviato al dispositivo \"{0}\"", "ToastSendEbookToDeviceSuccess": "Ebook inviato al dispositivo \"{0}\"",
"ToastSeriesUpdateFailed": "Aggiornamento Serie Fallito", "ToastSeriesUpdateFailed": "Aggiornamento Serie Fallito",
"ToastSeriesUpdateSuccess": "Serie Aggornate", "ToastSeriesUpdateSuccess": "Serie Aggiornate",
"ToastSessionDeleteFailed": "Errore eliminazione sessione", "ToastSessionDeleteFailed": "Errore eliminazione sessione",
"ToastSessionDeleteSuccess": "Sessione cancellata", "ToastSessionDeleteSuccess": "Sessione cancellata",
"ToastSocketConnected": "Socket connesso", "ToastSocketConnected": "Socket connesso",

View File

@ -32,6 +32,8 @@
"ButtonHide": "Slėpti", "ButtonHide": "Slėpti",
"ButtonHome": "Pradžia", "ButtonHome": "Pradžia",
"ButtonIssues": "Problemos", "ButtonIssues": "Problemos",
"ButtonJumpBackward": "Jump Backward",
"ButtonJumpForward": "Jump Forward",
"ButtonLatest": "Naujausias", "ButtonLatest": "Naujausias",
"ButtonLibrary": "Biblioteka", "ButtonLibrary": "Biblioteka",
"ButtonLogout": "Atsijungti", "ButtonLogout": "Atsijungti",
@ -41,12 +43,17 @@
"ButtonMatchAllAuthors": "Pritaikyti visus autorius", "ButtonMatchAllAuthors": "Pritaikyti visus autorius",
"ButtonMatchBooks": "Pritaikyti knygas", "ButtonMatchBooks": "Pritaikyti knygas",
"ButtonNevermind": "Nesvarbu", "ButtonNevermind": "Nesvarbu",
"ButtonNext": "Next",
"ButtonNextChapter": "Next Chapter",
"ButtonOk": "Ok", "ButtonOk": "Ok",
"ButtonOpenFeed": "Atidaryti srautą", "ButtonOpenFeed": "Atidaryti srautą",
"ButtonOpenManager": "Atidaryti tvarkyklę", "ButtonOpenManager": "Atidaryti tvarkyklę",
"ButtonPause": "Pause",
"ButtonPlay": "Groti", "ButtonPlay": "Groti",
"ButtonPlaying": "Grojama", "ButtonPlaying": "Grojama",
"ButtonPlaylists": "Grojaraščiai", "ButtonPlaylists": "Grojaraščiai",
"ButtonPrevious": "Previous",
"ButtonPreviousChapter": "Previous Chapter",
"ButtonPurgeAllCache": "Valyti visą saugyklą", "ButtonPurgeAllCache": "Valyti visą saugyklą",
"ButtonPurgeItemsCache": "Valyti elementų saugyklą", "ButtonPurgeItemsCache": "Valyti elementų saugyklą",
"ButtonPurgeMediaProgress": "Valyti medijos progresą", "ButtonPurgeMediaProgress": "Valyti medijos progresą",
@ -54,6 +61,7 @@
"ButtonQueueRemoveItem": "Pašalinti iš eilės", "ButtonQueueRemoveItem": "Pašalinti iš eilės",
"ButtonQuickMatch": "Greitas pritaikymas", "ButtonQuickMatch": "Greitas pritaikymas",
"ButtonRead": "Skaityti", "ButtonRead": "Skaityti",
"ButtonRefresh": "Refresh",
"ButtonRemove": "Pašalinti", "ButtonRemove": "Pašalinti",
"ButtonRemoveAll": "Pašalinti viską", "ButtonRemoveAll": "Pašalinti viską",
"ButtonRemoveAllLibraryItems": "Pašalinti visus bibliotekos elementus", "ButtonRemoveAllLibraryItems": "Pašalinti visus bibliotekos elementus",
@ -73,6 +81,7 @@
"ButtonSelectFolderPath": "Pasirinkti aplanko kelią", "ButtonSelectFolderPath": "Pasirinkti aplanko kelią",
"ButtonSeries": "Serijos", "ButtonSeries": "Serijos",
"ButtonSetChaptersFromTracks": "Nustatyti skyrius iš takelių", "ButtonSetChaptersFromTracks": "Nustatyti skyrius iš takelių",
"ButtonShare": "Share",
"ButtonShiftTimes": "Perstumti laikus", "ButtonShiftTimes": "Perstumti laikus",
"ButtonShow": "Rodyti", "ButtonShow": "Rodyti",
"ButtonStartM4BEncode": "Pradėti M4B kodavimą", "ButtonStartM4BEncode": "Pradėti M4B kodavimą",
@ -104,6 +113,7 @@
"HeaderCollectionItems": "Kolekcijos elementai", "HeaderCollectionItems": "Kolekcijos elementai",
"HeaderCover": "Viršelis", "HeaderCover": "Viršelis",
"HeaderCurrentDownloads": "Dabartiniai parsisiuntimai", "HeaderCurrentDownloads": "Dabartiniai parsisiuntimai",
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
"HeaderDetails": "Detalės", "HeaderDetails": "Detalės",
"HeaderDownloadQueue": "Parsisiuntimo eilė", "HeaderDownloadQueue": "Parsisiuntimo eilė",
"HeaderEbookFiles": "Eknygos failai", "HeaderEbookFiles": "Eknygos failai",
@ -174,6 +184,7 @@
"HeaderUpdateDetails": "Atnaujinti informaciją", "HeaderUpdateDetails": "Atnaujinti informaciją",
"HeaderUpdateLibrary": "Atnaujinti biblioteką", "HeaderUpdateLibrary": "Atnaujinti biblioteką",
"HeaderUsers": "Naudotojai", "HeaderUsers": "Naudotojai",
"HeaderYearReview": "Year {0} in Review",
"HeaderYourStats": "Jūsų statistika", "HeaderYourStats": "Jūsų statistika",
"LabelAbridged": "Santrauka", "LabelAbridged": "Santrauka",
"LabelAccountType": "Paskyros tipas", "LabelAccountType": "Paskyros tipas",
@ -345,7 +356,9 @@
"LabelMetaTags": "Meta žymos", "LabelMetaTags": "Meta žymos",
"LabelMinute": "Minutė", "LabelMinute": "Minutė",
"LabelMissing": "Trūksta", "LabelMissing": "Trūksta",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Trūkstamos dalys", "LabelMissingParts": "Trūkstamos dalys",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
"LabelMore": "Daugiau", "LabelMore": "Daugiau",
@ -385,11 +398,13 @@
"LabelPermissionsDownload": "Gali atsisiųsti", "LabelPermissionsDownload": "Gali atsisiųsti",
"LabelPermissionsUpdate": "Gali atnaujinti", "LabelPermissionsUpdate": "Gali atnaujinti",
"LabelPermissionsUpload": "Gali įkelti", "LabelPermissionsUpload": "Gali įkelti",
"LabelPersonalYearReview": "Your Year in Review ({0})",
"LabelPhotoPathURL": "Nuotraukos kelias/URL", "LabelPhotoPathURL": "Nuotraukos kelias/URL",
"LabelPlaylists": "Grojaraščiai", "LabelPlaylists": "Grojaraščiai",
"LabelPlayMethod": "Grojimo metodas", "LabelPlayMethod": "Grojimo metodas",
"LabelPodcast": "Tinklalaidė", "LabelPodcast": "Tinklalaidė",
"LabelPodcasts": "Tinklalaidės", "LabelPodcasts": "Tinklalaidės",
"LabelPodcastSearchRegion": "Podcast paieškos regionas",
"LabelPodcastType": "Tinklalaidės tipas", "LabelPodcastType": "Tinklalaidės tipas",
"LabelPort": "Prievadas", "LabelPort": "Prievadas",
"LabelPrefixesToIgnore": "Ignoruojami priešdėliai (didžiosios/mažosios nesvarbu)", "LabelPrefixesToIgnore": "Ignoruojami priešdėliai (didžiosios/mažosios nesvarbu)",
@ -429,6 +444,7 @@
"LabelSeries": "Serija", "LabelSeries": "Serija",
"LabelSeriesName": "Serijos pavadinimas", "LabelSeriesName": "Serijos pavadinimas",
"LabelSeriesProgress": "Serijos progresas", "LabelSeriesProgress": "Serijos progresas",
"LabelServerYearReview": "Server Year in Review ({0})",
"LabelSetEbookAsPrimary": "Nustatyti kaip pagrindinę", "LabelSetEbookAsPrimary": "Nustatyti kaip pagrindinę",
"LabelSetEbookAsSupplementary": "Nustatyti kaip papildomą", "LabelSetEbookAsSupplementary": "Nustatyti kaip papildomą",
"LabelSettingsAudiobooksOnly": "Tik garso knygos", "LabelSettingsAudiobooksOnly": "Tik garso knygos",
@ -545,6 +561,8 @@
"LabelViewQueue": "Peržiūrėti grotuvo eilę", "LabelViewQueue": "Peržiūrėti grotuvo eilę",
"LabelVolume": "Garsumas", "LabelVolume": "Garsumas",
"LabelWeekdaysToRun": "Dienos, kuriomis vykdyti", "LabelWeekdaysToRun": "Dienos, kuriomis vykdyti",
"LabelYearReviewHide": "Hide Year in Review",
"LabelYearReviewShow": "See Year in Review",
"LabelYourAudiobookDuration": "Jūsų garso knygos trukmė", "LabelYourAudiobookDuration": "Jūsų garso knygos trukmė",
"LabelYourBookmarks": "Jūsų skirtukai", "LabelYourBookmarks": "Jūsų skirtukai",
"LabelYourPlaylists": "Jūsų grojaraščiai", "LabelYourPlaylists": "Jūsų grojaraščiai",

View File

@ -32,6 +32,8 @@
"ButtonHide": "Verberg", "ButtonHide": "Verberg",
"ButtonHome": "Home", "ButtonHome": "Home",
"ButtonIssues": "Issues", "ButtonIssues": "Issues",
"ButtonJumpBackward": "Jump Backward",
"ButtonJumpForward": "Jump Forward",
"ButtonLatest": "Meest recent", "ButtonLatest": "Meest recent",
"ButtonLibrary": "Bibliotheek", "ButtonLibrary": "Bibliotheek",
"ButtonLogout": "Log uit", "ButtonLogout": "Log uit",
@ -41,12 +43,17 @@
"ButtonMatchAllAuthors": "Alle auteurs matchen", "ButtonMatchAllAuthors": "Alle auteurs matchen",
"ButtonMatchBooks": "Alle boeken matchen", "ButtonMatchBooks": "Alle boeken matchen",
"ButtonNevermind": "Laat maar", "ButtonNevermind": "Laat maar",
"ButtonNext": "Next",
"ButtonNextChapter": "Next Chapter",
"ButtonOk": "Ok", "ButtonOk": "Ok",
"ButtonOpenFeed": "Feed openen", "ButtonOpenFeed": "Feed openen",
"ButtonOpenManager": "Manager openen", "ButtonOpenManager": "Manager openen",
"ButtonPause": "Pause",
"ButtonPlay": "Afspelen", "ButtonPlay": "Afspelen",
"ButtonPlaying": "Speelt", "ButtonPlaying": "Speelt",
"ButtonPlaylists": "Afspeellijsten", "ButtonPlaylists": "Afspeellijsten",
"ButtonPrevious": "Previous",
"ButtonPreviousChapter": "Previous Chapter",
"ButtonPurgeAllCache": "Volledige cache legen", "ButtonPurgeAllCache": "Volledige cache legen",
"ButtonPurgeItemsCache": "Onderdelen-cache legen", "ButtonPurgeItemsCache": "Onderdelen-cache legen",
"ButtonPurgeMediaProgress": "Mediavoortgang legen", "ButtonPurgeMediaProgress": "Mediavoortgang legen",
@ -54,6 +61,7 @@
"ButtonQueueRemoveItem": "Uit wachtrij verwijderen", "ButtonQueueRemoveItem": "Uit wachtrij verwijderen",
"ButtonQuickMatch": "Snelle match", "ButtonQuickMatch": "Snelle match",
"ButtonRead": "Lees", "ButtonRead": "Lees",
"ButtonRefresh": "Refresh",
"ButtonRemove": "Verwijder", "ButtonRemove": "Verwijder",
"ButtonRemoveAll": "Alles verwijderen", "ButtonRemoveAll": "Alles verwijderen",
"ButtonRemoveAllLibraryItems": "Verwijder volledige bibliotheekinhoud", "ButtonRemoveAllLibraryItems": "Verwijder volledige bibliotheekinhoud",
@ -73,6 +81,7 @@
"ButtonSelectFolderPath": "Maplocatie selecteren", "ButtonSelectFolderPath": "Maplocatie selecteren",
"ButtonSeries": "Series", "ButtonSeries": "Series",
"ButtonSetChaptersFromTracks": "Maak hoofdstukken op basis van tracks", "ButtonSetChaptersFromTracks": "Maak hoofdstukken op basis van tracks",
"ButtonShare": "Share",
"ButtonShiftTimes": "Tijden verschuiven", "ButtonShiftTimes": "Tijden verschuiven",
"ButtonShow": "Toon", "ButtonShow": "Toon",
"ButtonStartM4BEncode": "Start M4B-encoding", "ButtonStartM4BEncode": "Start M4B-encoding",
@ -104,6 +113,7 @@
"HeaderCollectionItems": "Collectie-objecten", "HeaderCollectionItems": "Collectie-objecten",
"HeaderCover": "Cover", "HeaderCover": "Cover",
"HeaderCurrentDownloads": "Huidige downloads", "HeaderCurrentDownloads": "Huidige downloads",
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
"HeaderDetails": "Details", "HeaderDetails": "Details",
"HeaderDownloadQueue": "Download-wachtrij", "HeaderDownloadQueue": "Download-wachtrij",
"HeaderEbookFiles": "Ebook Files", "HeaderEbookFiles": "Ebook Files",
@ -174,6 +184,7 @@
"HeaderUpdateDetails": "Details bijwerken", "HeaderUpdateDetails": "Details bijwerken",
"HeaderUpdateLibrary": "Bibliotheek bijwerken", "HeaderUpdateLibrary": "Bibliotheek bijwerken",
"HeaderUsers": "Gebruikers", "HeaderUsers": "Gebruikers",
"HeaderYearReview": "Year {0} in Review",
"HeaderYourStats": "Je statistieken", "HeaderYourStats": "Je statistieken",
"LabelAbridged": "Verkort", "LabelAbridged": "Verkort",
"LabelAccountType": "Accounttype", "LabelAccountType": "Accounttype",
@ -345,7 +356,9 @@
"LabelMetaTags": "Meta-tags", "LabelMetaTags": "Meta-tags",
"LabelMinute": "Minuut", "LabelMinute": "Minuut",
"LabelMissing": "Ontbrekend", "LabelMissing": "Ontbrekend",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Ontbrekende delen", "LabelMissingParts": "Ontbrekende delen",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
"LabelMore": "Meer", "LabelMore": "Meer",
@ -385,11 +398,13 @@
"LabelPermissionsDownload": "Kan downloaden", "LabelPermissionsDownload": "Kan downloaden",
"LabelPermissionsUpdate": "Kan bijwerken", "LabelPermissionsUpdate": "Kan bijwerken",
"LabelPermissionsUpload": "Kan uploaden", "LabelPermissionsUpload": "Kan uploaden",
"LabelPersonalYearReview": "Your Year in Review ({0})",
"LabelPhotoPathURL": "Foto pad/URL", "LabelPhotoPathURL": "Foto pad/URL",
"LabelPlaylists": "Afspeellijsten", "LabelPlaylists": "Afspeellijsten",
"LabelPlayMethod": "Afspeelwijze", "LabelPlayMethod": "Afspeelwijze",
"LabelPodcast": "Podcast", "LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts", "LabelPodcasts": "Podcasts",
"LabelPodcastSearchRegion": "Podcast zoekregio",
"LabelPodcastType": "Podcasttype", "LabelPodcastType": "Podcasttype",
"LabelPort": "Poort", "LabelPort": "Poort",
"LabelPrefixesToIgnore": "Te negeren voorzetsels (ongeacht hoofdlettergebruik)", "LabelPrefixesToIgnore": "Te negeren voorzetsels (ongeacht hoofdlettergebruik)",
@ -429,6 +444,7 @@
"LabelSeries": "Serie", "LabelSeries": "Serie",
"LabelSeriesName": "Naam serie", "LabelSeriesName": "Naam serie",
"LabelSeriesProgress": "Voortgang serie", "LabelSeriesProgress": "Voortgang serie",
"LabelServerYearReview": "Server Year in Review ({0})",
"LabelSetEbookAsPrimary": "Stel in als primair", "LabelSetEbookAsPrimary": "Stel in als primair",
"LabelSetEbookAsSupplementary": "Stel in als supplementair", "LabelSetEbookAsSupplementary": "Stel in als supplementair",
"LabelSettingsAudiobooksOnly": "Alleen audiobooks", "LabelSettingsAudiobooksOnly": "Alleen audiobooks",
@ -545,6 +561,8 @@
"LabelViewQueue": "Bekijk afspeelwachtrij", "LabelViewQueue": "Bekijk afspeelwachtrij",
"LabelVolume": "Volume", "LabelVolume": "Volume",
"LabelWeekdaysToRun": "Weekdagen om te draaien", "LabelWeekdaysToRun": "Weekdagen om te draaien",
"LabelYearReviewHide": "Hide Year in Review",
"LabelYearReviewShow": "See Year in Review",
"LabelYourAudiobookDuration": "Je audioboekduur", "LabelYourAudiobookDuration": "Je audioboekduur",
"LabelYourBookmarks": "Je boekwijzers", "LabelYourBookmarks": "Je boekwijzers",
"LabelYourPlaylists": "Je afspeellijsten", "LabelYourPlaylists": "Je afspeellijsten",

View File

@ -32,6 +32,8 @@
"ButtonHide": "Gjøm", "ButtonHide": "Gjøm",
"ButtonHome": "Hjem", "ButtonHome": "Hjem",
"ButtonIssues": "Problemer", "ButtonIssues": "Problemer",
"ButtonJumpBackward": "Jump Backward",
"ButtonJumpForward": "Jump Forward",
"ButtonLatest": "Siste", "ButtonLatest": "Siste",
"ButtonLibrary": "Bibliotek", "ButtonLibrary": "Bibliotek",
"ButtonLogout": "Logg ut", "ButtonLogout": "Logg ut",
@ -41,12 +43,17 @@
"ButtonMatchAllAuthors": "Søk opp alle forfattere", "ButtonMatchAllAuthors": "Søk opp alle forfattere",
"ButtonMatchBooks": "Søk opp bøker", "ButtonMatchBooks": "Søk opp bøker",
"ButtonNevermind": "Avbryt", "ButtonNevermind": "Avbryt",
"ButtonNext": "Next",
"ButtonNextChapter": "Next Chapter",
"ButtonOk": "Ok", "ButtonOk": "Ok",
"ButtonOpenFeed": "Åpne Feed", "ButtonOpenFeed": "Åpne Feed",
"ButtonOpenManager": "Åpne behandler", "ButtonOpenManager": "Åpne behandler",
"ButtonPause": "Pause",
"ButtonPlay": "Spill av", "ButtonPlay": "Spill av",
"ButtonPlaying": "Spiller av", "ButtonPlaying": "Spiller av",
"ButtonPlaylists": "Spilleliste", "ButtonPlaylists": "Spilleliste",
"ButtonPrevious": "Previous",
"ButtonPreviousChapter": "Previous Chapter",
"ButtonPurgeAllCache": "Tøm alle mellomlager", "ButtonPurgeAllCache": "Tøm alle mellomlager",
"ButtonPurgeItemsCache": "Tøm mellomlager", "ButtonPurgeItemsCache": "Tøm mellomlager",
"ButtonPurgeMediaProgress": "Slett medie fremgang", "ButtonPurgeMediaProgress": "Slett medie fremgang",
@ -54,6 +61,7 @@
"ButtonQueueRemoveItem": "Fjern fra kø", "ButtonQueueRemoveItem": "Fjern fra kø",
"ButtonQuickMatch": "Kjapt søk", "ButtonQuickMatch": "Kjapt søk",
"ButtonRead": "Les", "ButtonRead": "Les",
"ButtonRefresh": "Refresh",
"ButtonRemove": "Fjern", "ButtonRemove": "Fjern",
"ButtonRemoveAll": "Fjern alle", "ButtonRemoveAll": "Fjern alle",
"ButtonRemoveAllLibraryItems": "Fjern alle bibliotekobjekter", "ButtonRemoveAllLibraryItems": "Fjern alle bibliotekobjekter",
@ -73,6 +81,7 @@
"ButtonSelectFolderPath": "Velg mappe", "ButtonSelectFolderPath": "Velg mappe",
"ButtonSeries": "Serier", "ButtonSeries": "Serier",
"ButtonSetChaptersFromTracks": "Sett kapittel fra spor", "ButtonSetChaptersFromTracks": "Sett kapittel fra spor",
"ButtonShare": "Share",
"ButtonShiftTimes": "Forskyv tider", "ButtonShiftTimes": "Forskyv tider",
"ButtonShow": "Vis", "ButtonShow": "Vis",
"ButtonStartM4BEncode": "Start M4B Koding", "ButtonStartM4BEncode": "Start M4B Koding",
@ -104,6 +113,7 @@
"HeaderCollectionItems": "Samlingsgjenstander", "HeaderCollectionItems": "Samlingsgjenstander",
"HeaderCover": "Omslag", "HeaderCover": "Omslag",
"HeaderCurrentDownloads": "Aktive nedlastinger", "HeaderCurrentDownloads": "Aktive nedlastinger",
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
"HeaderDetails": "Detaljer", "HeaderDetails": "Detaljer",
"HeaderDownloadQueue": "Last ned kø", "HeaderDownloadQueue": "Last ned kø",
"HeaderEbookFiles": "Ebook filer", "HeaderEbookFiles": "Ebook filer",
@ -174,6 +184,7 @@
"HeaderUpdateDetails": "Oppdater detaljer", "HeaderUpdateDetails": "Oppdater detaljer",
"HeaderUpdateLibrary": "Oppdater bibliotek", "HeaderUpdateLibrary": "Oppdater bibliotek",
"HeaderUsers": "Brukere", "HeaderUsers": "Brukere",
"HeaderYearReview": "Year {0} in Review",
"HeaderYourStats": "Din statistikk", "HeaderYourStats": "Din statistikk",
"LabelAbridged": "Forkortet", "LabelAbridged": "Forkortet",
"LabelAccountType": "Kontotype", "LabelAccountType": "Kontotype",
@ -345,7 +356,9 @@
"LabelMetaTags": "Meta Tags", "LabelMetaTags": "Meta Tags",
"LabelMinute": "Minutt", "LabelMinute": "Minutt",
"LabelMissing": "Mangler", "LabelMissing": "Mangler",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Manglende deler", "LabelMissingParts": "Manglende deler",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
"LabelMore": "Mer", "LabelMore": "Mer",
@ -385,11 +398,13 @@
"LabelPermissionsDownload": "Kan laste ned", "LabelPermissionsDownload": "Kan laste ned",
"LabelPermissionsUpdate": "Kan oppdatere", "LabelPermissionsUpdate": "Kan oppdatere",
"LabelPermissionsUpload": "Kan laste opp", "LabelPermissionsUpload": "Kan laste opp",
"LabelPersonalYearReview": "Your Year in Review ({0})",
"LabelPhotoPathURL": "Bilde sti/URL", "LabelPhotoPathURL": "Bilde sti/URL",
"LabelPlaylists": "Spilleliste", "LabelPlaylists": "Spilleliste",
"LabelPlayMethod": "Avspillingsmetode", "LabelPlayMethod": "Avspillingsmetode",
"LabelPodcast": "Podcast", "LabelPodcast": "Podcast",
"LabelPodcasts": "Podcaster", "LabelPodcasts": "Podcaster",
"LabelPodcastSearchRegion": "Podcast-søkeområde",
"LabelPodcastType": "Podcast type", "LabelPodcastType": "Podcast type",
"LabelPort": "Port", "LabelPort": "Port",
"LabelPrefixesToIgnore": "Prefiks som skal ignoreres (skiller ikke mellom store og små bokstaver)", "LabelPrefixesToIgnore": "Prefiks som skal ignoreres (skiller ikke mellom store og små bokstaver)",
@ -429,6 +444,7 @@
"LabelSeries": "Serier", "LabelSeries": "Serier",
"LabelSeriesName": "Serier Navn", "LabelSeriesName": "Serier Navn",
"LabelSeriesProgress": "Serier fremgang", "LabelSeriesProgress": "Serier fremgang",
"LabelServerYearReview": "Server Year in Review ({0})",
"LabelSetEbookAsPrimary": "Sett som primær", "LabelSetEbookAsPrimary": "Sett som primær",
"LabelSetEbookAsSupplementary": "Sett som supplerende", "LabelSetEbookAsSupplementary": "Sett som supplerende",
"LabelSettingsAudiobooksOnly": "Kun lydbøker", "LabelSettingsAudiobooksOnly": "Kun lydbøker",
@ -545,6 +561,8 @@
"LabelViewQueue": "Vis spillerkø", "LabelViewQueue": "Vis spillerkø",
"LabelVolume": "Volum", "LabelVolume": "Volum",
"LabelWeekdaysToRun": "Ukedager å kjøre", "LabelWeekdaysToRun": "Ukedager å kjøre",
"LabelYearReviewHide": "Hide Year in Review",
"LabelYearReviewShow": "See Year in Review",
"LabelYourAudiobookDuration": "Din lydbok lengde", "LabelYourAudiobookDuration": "Din lydbok lengde",
"LabelYourBookmarks": "Dine bokmerker", "LabelYourBookmarks": "Dine bokmerker",
"LabelYourPlaylists": "Dine spillelister", "LabelYourPlaylists": "Dine spillelister",

View File

@ -32,6 +32,8 @@
"ButtonHide": "Ukryj", "ButtonHide": "Ukryj",
"ButtonHome": "Strona główna", "ButtonHome": "Strona główna",
"ButtonIssues": "Błędy", "ButtonIssues": "Błędy",
"ButtonJumpBackward": "Jump Backward",
"ButtonJumpForward": "Jump Forward",
"ButtonLatest": "Aktualna wersja:", "ButtonLatest": "Aktualna wersja:",
"ButtonLibrary": "Biblioteka", "ButtonLibrary": "Biblioteka",
"ButtonLogout": "Wyloguj", "ButtonLogout": "Wyloguj",
@ -41,12 +43,17 @@
"ButtonMatchAllAuthors": "Dopasuj wszystkich autorów", "ButtonMatchAllAuthors": "Dopasuj wszystkich autorów",
"ButtonMatchBooks": "Dopasuj książki", "ButtonMatchBooks": "Dopasuj książki",
"ButtonNevermind": "Anuluj", "ButtonNevermind": "Anuluj",
"ButtonNext": "Next",
"ButtonNextChapter": "Next Chapter",
"ButtonOk": "Ok", "ButtonOk": "Ok",
"ButtonOpenFeed": "Otwórz feed", "ButtonOpenFeed": "Otwórz feed",
"ButtonOpenManager": "Otwórz menadżera", "ButtonOpenManager": "Otwórz menadżera",
"ButtonPause": "Pause",
"ButtonPlay": "Odtwarzaj", "ButtonPlay": "Odtwarzaj",
"ButtonPlaying": "Odtwarzane", "ButtonPlaying": "Odtwarzane",
"ButtonPlaylists": "Playlists", "ButtonPlaylists": "Playlists",
"ButtonPrevious": "Previous",
"ButtonPreviousChapter": "Previous Chapter",
"ButtonPurgeAllCache": "Wyczyść dane tymczasowe", "ButtonPurgeAllCache": "Wyczyść dane tymczasowe",
"ButtonPurgeItemsCache": "Wyczyść dane tymczasowe pozycji", "ButtonPurgeItemsCache": "Wyczyść dane tymczasowe pozycji",
"ButtonPurgeMediaProgress": "Wyczyść postęp", "ButtonPurgeMediaProgress": "Wyczyść postęp",
@ -54,6 +61,7 @@
"ButtonQueueRemoveItem": "Usuń z kolejki", "ButtonQueueRemoveItem": "Usuń z kolejki",
"ButtonQuickMatch": "Szybkie dopasowanie", "ButtonQuickMatch": "Szybkie dopasowanie",
"ButtonRead": "Czytaj", "ButtonRead": "Czytaj",
"ButtonRefresh": "Refresh",
"ButtonRemove": "Usuń", "ButtonRemove": "Usuń",
"ButtonRemoveAll": "Usuń wszystko", "ButtonRemoveAll": "Usuń wszystko",
"ButtonRemoveAllLibraryItems": "Usuń wszystkie elementy z biblioteki", "ButtonRemoveAllLibraryItems": "Usuń wszystkie elementy z biblioteki",
@ -73,6 +81,7 @@
"ButtonSelectFolderPath": "Wybierz ścieżkę folderu", "ButtonSelectFolderPath": "Wybierz ścieżkę folderu",
"ButtonSeries": "Seria", "ButtonSeries": "Seria",
"ButtonSetChaptersFromTracks": "Set chapters from tracks", "ButtonSetChaptersFromTracks": "Set chapters from tracks",
"ButtonShare": "Share",
"ButtonShiftTimes": "Przesunięcie czasowe", "ButtonShiftTimes": "Przesunięcie czasowe",
"ButtonShow": "Pokaż", "ButtonShow": "Pokaż",
"ButtonStartM4BEncode": "Eksportuj jako plik M4B", "ButtonStartM4BEncode": "Eksportuj jako plik M4B",
@ -104,6 +113,7 @@
"HeaderCollectionItems": "Elementy kolekcji", "HeaderCollectionItems": "Elementy kolekcji",
"HeaderCover": "Okładka", "HeaderCover": "Okładka",
"HeaderCurrentDownloads": "Current Downloads", "HeaderCurrentDownloads": "Current Downloads",
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
"HeaderDetails": "Szczegóły", "HeaderDetails": "Szczegóły",
"HeaderDownloadQueue": "Download Queue", "HeaderDownloadQueue": "Download Queue",
"HeaderEbookFiles": "Ebook Files", "HeaderEbookFiles": "Ebook Files",
@ -174,6 +184,7 @@
"HeaderUpdateDetails": "Zaktualizuj szczegóły", "HeaderUpdateDetails": "Zaktualizuj szczegóły",
"HeaderUpdateLibrary": "Zaktualizuj bibliotekę", "HeaderUpdateLibrary": "Zaktualizuj bibliotekę",
"HeaderUsers": "Użytkownicy", "HeaderUsers": "Użytkownicy",
"HeaderYearReview": "Year {0} in Review",
"HeaderYourStats": "Twoje statystyki", "HeaderYourStats": "Twoje statystyki",
"LabelAbridged": "Abridged", "LabelAbridged": "Abridged",
"LabelAccountType": "Typ konta", "LabelAccountType": "Typ konta",
@ -345,7 +356,9 @@
"LabelMetaTags": "Meta Tags", "LabelMetaTags": "Meta Tags",
"LabelMinute": "Minuta", "LabelMinute": "Minuta",
"LabelMissing": "Brakujący", "LabelMissing": "Brakujący",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Brakujące cześci", "LabelMissingParts": "Brakujące cześci",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
"LabelMore": "Więcej", "LabelMore": "Więcej",
@ -385,11 +398,13 @@
"LabelPermissionsDownload": "Ma możliwość pobierania", "LabelPermissionsDownload": "Ma możliwość pobierania",
"LabelPermissionsUpdate": "Ma możliwość aktualizowania", "LabelPermissionsUpdate": "Ma możliwość aktualizowania",
"LabelPermissionsUpload": "Ma możliwość dodawania", "LabelPermissionsUpload": "Ma możliwość dodawania",
"LabelPersonalYearReview": "Your Year in Review ({0})",
"LabelPhotoPathURL": "Scieżka/URL do zdjęcia", "LabelPhotoPathURL": "Scieżka/URL do zdjęcia",
"LabelPlaylists": "Playlists", "LabelPlaylists": "Playlists",
"LabelPlayMethod": "Metoda odtwarzania", "LabelPlayMethod": "Metoda odtwarzania",
"LabelPodcast": "Podcast", "LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasty", "LabelPodcasts": "Podcasty",
"LabelPodcastSearchRegion": "Obszar wyszukiwania podcastów",
"LabelPodcastType": "Podcast Type", "LabelPodcastType": "Podcast Type",
"LabelPort": "Port", "LabelPort": "Port",
"LabelPrefixesToIgnore": "Ignorowane prefiksy (wielkość liter nie ma znaczenia)", "LabelPrefixesToIgnore": "Ignorowane prefiksy (wielkość liter nie ma znaczenia)",
@ -429,6 +444,7 @@
"LabelSeries": "Serie", "LabelSeries": "Serie",
"LabelSeriesName": "Nazwy serii", "LabelSeriesName": "Nazwy serii",
"LabelSeriesProgress": "Postęp w serii", "LabelSeriesProgress": "Postęp w serii",
"LabelServerYearReview": "Server Year in Review ({0})",
"LabelSetEbookAsPrimary": "Set as primary", "LabelSetEbookAsPrimary": "Set as primary",
"LabelSetEbookAsSupplementary": "Set as supplementary", "LabelSetEbookAsSupplementary": "Set as supplementary",
"LabelSettingsAudiobooksOnly": "Audiobooks only", "LabelSettingsAudiobooksOnly": "Audiobooks only",
@ -545,6 +561,8 @@
"LabelViewQueue": "Wyświetlaj kolejkę odtwarzania", "LabelViewQueue": "Wyświetlaj kolejkę odtwarzania",
"LabelVolume": "Głośność", "LabelVolume": "Głośność",
"LabelWeekdaysToRun": "Dni tygodnia", "LabelWeekdaysToRun": "Dni tygodnia",
"LabelYearReviewHide": "Hide Year in Review",
"LabelYearReviewShow": "See Year in Review",
"LabelYourAudiobookDuration": "Czas trwania audiobooka", "LabelYourAudiobookDuration": "Czas trwania audiobooka",
"LabelYourBookmarks": "Twoje zakładki", "LabelYourBookmarks": "Twoje zakładki",
"LabelYourPlaylists": "Your Playlists", "LabelYourPlaylists": "Your Playlists",

779
client/strings/pt-br.json Normal file
View File

@ -0,0 +1,779 @@
{
"ButtonAdd": "Adicionar",
"ButtonAddChapters": "Adicionar Capítulos",
"ButtonAddDevice": "Adicionar Dispositivo",
"ButtonAddLibrary": "Adicionar Biblioteca",
"ButtonAddPodcasts": "Adicionar Podcasts",
"ButtonAddUser": "Adicionar Usuário",
"ButtonAddYourFirstLibrary": "Adicionar sua primeira biblioteca",
"ButtonApply": "Aplicar",
"ButtonApplyChapters": "Aplicar Capítulos",
"ButtonAuthors": "Autores",
"ButtonBrowseForFolder": "Procurar por Pasta",
"ButtonCancel": "Cancelar",
"ButtonCancelEncode": "Cancelar Codificação",
"ButtonChangeRootPassword": "Alterar senha do administrador",
"ButtonCheckAndDownloadNewEpisodes": "Verificar & Baixar Novos Episódios",
"ButtonChooseAFolder": "Escolha uma pasta",
"ButtonChooseFiles": "Escolha arquivos",
"ButtonClearFilter": "Limpar Filtro",
"ButtonCloseFeed": "Fechar Feed",
"ButtonCollections": "Coleções",
"ButtonConfigureScanner": "Configurar Verificador",
"ButtonCreate": "Criar",
"ButtonCreateBackup": "Criar Backup",
"ButtonDelete": "Apagar",
"ButtonDownloadQueue": "Fila de download",
"ButtonEdit": "Editar",
"ButtonEditChapters": "Editar Capítulos",
"ButtonEditPodcast": "Editar Podcast",
"ButtonForceReScan": "Forcar Nova Verificação",
"ButtonFullPath": "Caminho Completo",
"ButtonHide": "Ocultar",
"ButtonHome": "Principal",
"ButtonIssues": "Problemas",
"ButtonJumpBackward": "Retroceder",
"ButtonJumpForward": "Adiantar",
"ButtonLatest": "Mais Recentes",
"ButtonLibrary": "Biblioteca",
"ButtonLogout": "Logout",
"ButtonLookup": "Procurar",
"ButtonManageTracks": "Gerenciar Faixas",
"ButtonMapChapterTitles": "Designar Títulos de Capítulos",
"ButtonMatchAllAuthors": "Consultar Todos os Autores",
"ButtonMatchBooks": "Consultar Livros",
"ButtonNevermind": "Cancelar",
"ButtonNext": "Próximo",
"ButtonNextChapter": "Próximo Capítulo",
"ButtonOk": "Ok",
"ButtonOpenFeed": "Abrir Feed",
"ButtonOpenManager": "Abrir Gerenciador",
"ButtonPause": "Pausar",
"ButtonPlay": "Reproduzir",
"ButtonPlaying": "Reproduzindo",
"ButtonPlaylists": "Lista de Reprodução",
"ButtonPrevious": "Anterior",
"ButtonPreviousChapter": "Capítulo Anterior",
"ButtonPurgeAllCache": "Apagar Todo o Cache",
"ButtonPurgeItemsCache": "Apagar o Cache de Itens",
"ButtonPurgeMediaProgress": "Apagar o Progresso nas Mídias",
"ButtonQueueAddItem": "Adicionar à Lista",
"ButtonQueueRemoveItem": "Remover da Lista",
"ButtonQuickMatch": "Consulta rápida",
"ButtonRead": "Ler",
"ButtonRefresh": "Atualizar",
"ButtonRemove": "Remover",
"ButtonRemoveAll": "Remover Todos",
"ButtonRemoveAllLibraryItems": "Remover Todos os Itens da Biblioteca",
"ButtonRemoveFromContinueListening": "Remover de Continuar Escutando",
"ButtonRemoveFromContinueReading": "Remover de Continuar Lendo",
"ButtonRemoveSeriesFromContinueSeries": "Remover Série de Continuar Série",
"ButtonReScan": "Nova Verificação",
"ButtonReset": "Resetar",
"ButtonResetToDefault": "Resetar para valores padrão",
"ButtonRestore": "Restaurar",
"ButtonSave": "Salvar",
"ButtonSaveAndClose": "Salvar & Fechar",
"ButtonSaveTracklist": "Salvar Lista de Faixas",
"ButtonScan": "Verificar",
"ButtonScanLibrary": "Verificar Biblioteca",
"ButtonSearch": "Pesquisar",
"ButtonSelectFolderPath": "Selecionar Caminho da Pasta",
"ButtonSeries": "Séries",
"ButtonSetChaptersFromTracks": "Definir Capítulos Segundo Faixas",
"ButtonShare": "Compartilhar",
"ButtonShiftTimes": "Deslocar tempos",
"ButtonShow": "Exibir",
"ButtonStartM4BEncode": "Iniciar Codificação M4B",
"ButtonStartMetadataEmbed": "Iniciar Inclusão de Metadados",
"ButtonSubmit": "Enviar",
"ButtonTest": "Testar",
"ButtonUpload": "Upload",
"ButtonUploadBackup": "Upload de Backup",
"ButtonUploadCover": "Upload de Capa",
"ButtonUploadOPMLFile": "Upload Arquivo OPML",
"ButtonUserDelete": "Apagar usuário {0}",
"ButtonUserEdit": "Editar usuário {0}",
"ButtonViewAll": "Ver tudo",
"ButtonYes": "Sim",
"ErrorUploadFetchMetadataAPI": "Erro buscando metadados",
"ErrorUploadFetchMetadataNoResults": "Não foi possível buscar metadados - tente atualizar o título e/ou autor",
"ErrorUploadLacksTitle": "É preciso ter um título",
"HeaderAccount": "Conta",
"HeaderAdvanced": "Avançado",
"HeaderAppriseNotificationSettings": "Configuração de notificações Apprise",
"HeaderAudiobookTools": "Ferramentas de Gerenciamento de Arquivos de Audiobooks",
"HeaderAudioTracks": "Trilhas de áudio",
"HeaderAuthentication": "Autenticação",
"HeaderBackups": "Backups",
"HeaderChangePassword": "Trocar Senha",
"HeaderChapters": "Capítulos",
"HeaderChooseAFolder": "Escolha uma Pasta",
"HeaderCollection": "Coleção",
"HeaderCollectionItems": "Itens da Coleção",
"HeaderCover": "Capas",
"HeaderCurrentDownloads": "Downloads em andamento",
"HeaderCustomMetadataProviders": "Fontes de Metadados Customizados",
"HeaderDetails": "Detalhes",
"HeaderDownloadQueue": "Fila de Download",
"HeaderEbookFiles": "Arquivos Ebook",
"HeaderEmail": "Email",
"HeaderEmailSettings": "Configurações de Email",
"HeaderEpisodes": "Episódios",
"HeaderEreaderDevices": "Dispositivos Ereader",
"HeaderEreaderSettings": "Configurações Ereader",
"HeaderFiles": "Arquivos",
"HeaderFindChapters": "Localizar Capítulos",
"HeaderIgnoredFiles": "Arquivos Ignorados",
"HeaderItemFiles": "Arquivos de Itens",
"HeaderItemMetadataUtils": "Utilidades para Metadados dos Itens",
"HeaderLastListeningSession": "Última sessão",
"HeaderLatestEpisodes": "Últimos episódios",
"HeaderLibraries": "Bibliotecas",
"HeaderLibraryFiles": "Arquivos da Biblioteca",
"HeaderLibraryStats": "Estatísticas da Biblioteca",
"HeaderListeningSessions": "Sessões",
"HeaderListeningStats": "Estatísticas",
"HeaderLogin": "Login",
"HeaderLogs": "Logs",
"HeaderManageGenres": "Gerenciar Gêneros",
"HeaderManageTags": "Gerenciar Etiquetas",
"HeaderMapDetails": "Designar Detalhes",
"HeaderMatch": "Consultar",
"HeaderMetadataOrderOfPrecedence": "Ordem de Prioridade dos Metadados",
"HeaderMetadataToEmbed": "Metadados a Serem Incluídos",
"HeaderNewAccount": "Nova Conta",
"HeaderNewLibrary": "Nova Biblioteca",
"HeaderNotifications": "Notificações",
"HeaderOpenIDConnectAuthentication": "Autenticação via OpenID Connect",
"HeaderOpenRSSFeed": "Abrir Feed RSS",
"HeaderOtherFiles": "Outros Arquivos",
"HeaderPasswordAuthentication": "Autenticação por Senha",
"HeaderPermissions": "Permissões",
"HeaderPlayerQueue": "Fila do reprodutor",
"HeaderPlaylist": "Lista de Reprodução",
"HeaderPlaylistItems": "Itens da lista de reprodução",
"HeaderPodcastsToAdd": "Podcasts para Adicionar",
"HeaderPreviewCover": "Visualização da Capa",
"HeaderRemoveEpisode": "Remover Episódio",
"HeaderRemoveEpisodes": "Remover {0} Episódios",
"HeaderRSSFeedGeneral": "Detalhes RSS",
"HeaderRSSFeedIsOpen": "Feed RSS está aberto",
"HeaderRSSFeeds": "Feeds RSS",
"HeaderSavedMediaProgress": "Progresso da gravação das mídias",
"HeaderSchedule": "Programação",
"HeaderScheduleLibraryScans": "Programar Verificação Automática da Biblioteca",
"HeaderSession": "Sessão",
"HeaderSetBackupSchedule": "Definir Programação de Backup",
"HeaderSettings": "Configurações",
"HeaderSettingsDisplay": "Exibição",
"HeaderSettingsExperimental": "Funcionalidades experimentais",
"HeaderSettingsGeneral": "Geral",
"HeaderSettingsScanner": "Verificador",
"HeaderSleepTimer": "Timer",
"HeaderStatsLargestItems": "Maiores Itens",
"HeaderStatsLongestItems": "Itens mais longos (hrs)",
"HeaderStatsMinutesListeningChart": "Minutos Escutados (últimos 7 dias)",
"HeaderStatsRecentSessions": "Sessões Recentes",
"HeaderStatsTop10Authors": "Top 10 Autores",
"HeaderStatsTop5Genres": "Top 5 Gêneros",
"HeaderTableOfContents": "Sumário",
"HeaderTools": "Ferramentas",
"HeaderUpdateAccount": "Atualizar Conta",
"HeaderUpdateAuthor": "Atualizar Autor",
"HeaderUpdateDetails": "Atualizar Detalhes",
"HeaderUpdateLibrary": "Atualizar Biblioteca",
"HeaderUsers": "Usuários",
"HeaderYearReview": "Retrospectiva de {0} ",
"HeaderYourStats": "Suas Estatísticas",
"LabelAbridged": "Versão Abreviada",
"LabelAccountType": "Tipo de Conta",
"LabelAccountTypeAdmin": "Administrador",
"LabelAccountTypeGuest": "Convidado",
"LabelAccountTypeUser": "Usuário",
"LabelActivity": "Atividade",
"LabelAdded": "Acrescentado",
"LabelAddedAt": "Acrescentado em",
"LabelAddToCollection": "Adicionar à Coleção",
"LabelAddToCollectionBatch": "Adicionar {0} Livros à Coleção",
"LabelAddToPlaylist": "Adicionar à Lista de Reprodução",
"LabelAddToPlaylistBatch": "Adicionar {0} itens à Lista de Reprodução",
"LabelAdminUsersOnly": "Apenas usuários administradores",
"LabelAll": "Todos",
"LabelAllUsers": "Todos Usuários",
"LabelAllUsersExcludingGuests": "Todos usuários exceto convidados",
"LabelAllUsersIncludingGuests": "Todos usuários incluindo convidados",
"LabelAlreadyInYourLibrary": "Já na sua biblioteca",
"LabelAppend": "Acrescentar",
"LabelAuthor": "Autor",
"LabelAuthorFirstLast": "Autor (Nome Sobrenome)",
"LabelAuthorLastFirst": "Autor (Sobrenome, Nome)",
"LabelAuthors": "Autores",
"LabelAutoDownloadEpisodes": "Download Automático de Episódios",
"LabelAutoFetchMetadata": "Buscar Metadados Automaticamente",
"LabelAutoFetchMetadataHelp": "Busca metadados de título, autor e série para otimizar o upload. Pode ser necessário buscas metadados adicionais após o upload.",
"LabelAutoLaunch": "Iniciar Automaticamente",
"LabelAutoLaunchDescription": "Redireciona para o fornecedor de autenticação automaticamente ao navegar para a tela de login (caminho para substituição manual <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Registrar Automaticamente",
"LabelAutoRegisterDescription": "Registra automaticamente novos usuários após login",
"LabelBackToUser": "Voltar para Usuário",
"LabelBackupLocation": "Localização do Backup",
"LabelBackupsEnableAutomaticBackups": "Ativar backups automáticos",
"LabelBackupsEnableAutomaticBackupsHelp": "Backups salvos em /metadata/backups",
"LabelBackupsMaxBackupSize": "Tamanho máximo do backup (em GB)",
"LabelBackupsMaxBackupSizeHelp": "Como proteção contra uma configuração incorreta, backups darão erro se excederem o tamanho configurado.",
"LabelBackupsNumberToKeep": "Número de backups para guardar",
"LabelBackupsNumberToKeepHelp": "Apenas 1 backup será removido por vez, então, se já existem mais backups, você deve apagá-los manualmente.",
"LabelBitrate": "Bitrate",
"LabelBooks": "Livros",
"LabelButtonText": "Texto do botão",
"LabelChangePassword": "Trocar Senha",
"LabelChannels": "Canais",
"LabelChapters": "Capítulos",
"LabelChaptersFound": "capítulos encontrados",
"LabelChapterTitle": "Título do Capítulo",
"LabelClickForMoreInfo": "Clique para mais informações",
"LabelClosePlayer": "Fechar Reprodutor",
"LabelCodec": "Codec",
"LabelCollapseSeries": "Fechar Série",
"LabelCollection": "Coleção",
"LabelCollections": "Coleções",
"LabelComplete": "Concluído",
"LabelConfirmPassword": "Confirmar Senha",
"LabelContinueListening": "Continuar Escutando",
"LabelContinueReading": "Continuar Lendo",
"LabelContinueSeries": "Continuar Série",
"LabelCover": "Capa",
"LabelCoverImageURL": "URL da Imagem da Capa",
"LabelCreatedAt": "Criado em",
"LabelCronExpression": "Expressão para o Cron",
"LabelCurrent": "Atual",
"LabelCurrently": "Atualmente:",
"LabelCustomCronExpression": "Expressão personalizada para o Cron:",
"LabelDatetime": "Data e Hora",
"LabelDeleteFromFileSystemCheckbox": "Apagar do sistema de arquivos (desmarcar para remover apenas da base de dados)",
"LabelDescription": "Descrição",
"LabelDeselectAll": "Desmarcar tudo",
"LabelDevice": "Dispositivo",
"LabelDeviceInfo": "Informação do Dispositivo",
"LabelDeviceIsAvailableTo": "Dispositivo está disponível para...",
"LabelDirectory": "Diretório",
"LabelDiscFromFilename": "Disco a partir do nome do arquivo",
"LabelDiscFromMetadata": "Disco a partir dos metadados",
"LabelDiscover": "Descobrir",
"LabelDownload": "Download",
"LabelDownloadNEpisodes": "Download de {0} Episódios",
"LabelDuration": "Duração",
"LabelDurationFound": "Duração comprovada:",
"LabelEbook": "Ebook",
"LabelEbooks": "Ebooks",
"LabelEdit": "Editar",
"LabelEmail": "Email",
"LabelEmailSettingsFromAddress": "Remetente",
"LabelEmailSettingsSecure": "Seguro",
"LabelEmailSettingsSecureHelp": "Se ativado, a conexão utilizará TLS para a conexão ao servidor. Se desativado TLS será usado se o servidor suportar a extensão STARTTLS. Na maioria dos casos ative esse valor se estiver conectando pela porta 465. Para portas 587 ou 25, mantenha inativo. (de nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsTestAddress": "Endereço de teste",
"LabelEmbeddedCover": "Capa Integrada",
"LabelEnable": "Habilitar",
"LabelEnd": "Fim",
"LabelEpisode": "Episódio",
"LabelEpisodeTitle": "Título do Episódio",
"LabelEpisodeType": "Tipo do Episódio",
"LabelExample": "Exemplo",
"LabelExplicit": "Explícito",
"LabelFeedURL": "URL do Feed",
"LabelFetchingMetadata": "Buscando Metadados",
"LabelFile": "Arquivo",
"LabelFileBirthtime": "Criação do Arquivo",
"LabelFileModified": "Modificação do Arquivo",
"LabelFilename": "Nome do Arquivo",
"LabelFilterByUser": "Filtrar por Usuário",
"LabelFindEpisodes": "Localizar Episódios",
"LabelFinished": "Concluído",
"LabelFolder": "Pasta",
"LabelFolders": "Pastas",
"LabelFontBold": "Negrito",
"LabelFontFamily": "Família de fonte",
"LabelFontItalic": "Itálico",
"LabelFontScale": "Escala de fonte",
"LabelFontStrikethrough": "Tachado",
"LabelFormat": "Formato",
"LabelGenre": "Gênero",
"LabelGenres": "Gêneros",
"LabelHardDeleteFile": "Apagar definitivamente",
"LabelHasEbook": "Tem ebook",
"LabelHasSupplementaryEbook": "Tem ebook complementar",
"LabelHighestPriority": "Prioridade mais alta",
"LabelHost": "Host",
"LabelHour": "Hora",
"LabelIcon": "Ícone",
"LabelImageURLFromTheWeb": "URL da imagem na internet",
"LabelIncludeInTracklist": "Incluir na Lista de Faixas",
"LabelIncomplete": "Incompleto",
"LabelInProgress": "Em Andamento",
"LabelInterval": "Intervalo",
"LabelIntervalCustomDailyWeekly": "Personalizar diário/semanal",
"LabelIntervalEvery12Hours": "A cada 12 horas",
"LabelIntervalEvery15Minutes": "A cada 15 minutos",
"LabelIntervalEvery2Hours": "A cada 2 horas",
"LabelIntervalEvery30Minutes": "A cada 30 minutos",
"LabelIntervalEvery6Hours": "A cada 6 horas",
"LabelIntervalEveryDay": "Todo dia",
"LabelIntervalEveryHour": "Toda hora",
"LabelInvalidParts": "Partes Inválidas",
"LabelInvert": "Inverter",
"LabelItem": "Item",
"LabelLanguage": "Idioma",
"LabelLanguageDefaultServer": "Idioma Padrão do Servidor",
"LabelLastBookAdded": "Último Livro Acrescentado",
"LabelLastBookUpdated": "Último Livro Atualizado",
"LabelLastSeen": "Visto pela Última Vez",
"LabelLastTime": "Progresso",
"LabelLastUpdate": "Última Atualização",
"LabelLayout": "Layout",
"LabelLayoutSinglePage": "Uma página",
"LabelLayoutSplitPage": "Página dividida",
"LabelLess": "Menos",
"LabelLibrariesAccessibleToUser": "Bibliotecas Acessíveis ao Usuário",
"LabelLibrary": "Biblioteca",
"LabelLibraryItem": "Item da Biblioteca",
"LabelLibraryName": "Nome da Biblioteca",
"LabelLimit": "Limite",
"LabelLineSpacing": "Espaçamento entre linhas",
"LabelListenAgain": "Escutar novamente",
"LabelLogLevelDebug": "Debug",
"LabelLogLevelInfo": "Info",
"LabelLogLevelWarn": "Atenção",
"LabelLookForNewEpisodesAfterDate": "Procurar por novos Episódios após essa data",
"LabelLowestPriority": "Prioridade mais baixa",
"LabelMatchExistingUsersBy": "Consultar usuários existentes usando",
"LabelMatchExistingUsersByDescription": "Utilizado para conectar usuários já existentes. Uma vez conectados, usuários serão consultados utilizando uma identificação única do seu provedor de SSO",
"LabelMediaPlayer": "Reprodutor de mídia",
"LabelMediaType": "Tipo de Mídia",
"LabelMetadataOrderOfPrecedenceDescription": "Fontes de metadados de alta prioridade terão preferência sobre as fontes de metadados de prioridade baixa",
"LabelMetadataProvider": "Fonte de Metadados",
"LabelMetaTag": "Etiqueta Meta",
"LabelMetaTags": "Etiquetas Meta",
"LabelMinute": "Minuto",
"LabelMissing": "Ausente",
"LabelMissingEbook": "Ebook não existe",
"LabelMissingParts": "Partes Ausentes",
"LabelMissingSupplementaryEbook": "Ebook complementar não existe",
"LabelMobileRedirectURIs": "URIs de redirecionamento móveis permitidas",
"LabelMobileRedirectURIsDescription": "Essa é uma lista de permissionamento para URIs válidas para o redirecionamento de aplicativos móveis. A padrão é <code>audiobookshelf://oauth</code>, que pode ser removida ou acrescentada com novas URIs para integração com apps de terceiros. Usando um asterisco (<code>*</code>) como um item único dará permissão para qualquer URI.",
"LabelMore": "Mais",
"LabelMoreInfo": "Mais Informações",
"LabelName": "Nome",
"LabelNarrator": "Narrador",
"LabelNarrators": "Narradores",
"LabelNew": "Novo",
"LabelNewestAuthors": "Novos Autores",
"LabelNewestEpisodes": "Episódios mais recentes",
"LabelNewPassword": "Nova Senha",
"LabelNextBackupDate": "Data do próximo backup",
"LabelNextScheduledRun": "Próxima execução programada",
"LabelNoEpisodesSelected": "Nenhum episódio selecionado",
"LabelNotes": "Notas",
"LabelNotFinished": "Não concluído",
"LabelNotificationAppriseURL": "URL(s) Apprise",
"LabelNotificationAvailableVariables": "Variáveis disponíveis",
"LabelNotificationBodyTemplate": "Modelo de Corpo",
"LabelNotificationEvent": "Evento de Notificação",
"LabelNotificationsMaxFailedAttempts": "Máximo de tentativas com falhas",
"LabelNotificationsMaxFailedAttemptsHelp": "Notificações serão desabilitadas após falharem este número de vezes",
"LabelNotificationsMaxQueueSize": "Tamanho máximo da fila de eventos de notificação",
"LabelNotificationsMaxQueueSizeHelp": "Eventos estão limitados a um disparo por segundo. Eventos serão ignorados se a fila estiver no tamanho máximo. Isso evita o excesso de notificações.",
"LabelNotificationTitleTemplate": "Modelo de Título",
"LabelNotStarted": "Não iniciado",
"LabelNumberOfBooks": "Número de Livros",
"LabelNumberOfEpisodes": "# de Episódios",
"LabelOpenRSSFeed": "Abrir Feed RSS",
"LabelOverwrite": "Sobrescrever",
"LabelPassword": "Senha",
"LabelPath": "Caminho",
"LabelPermissionsAccessAllLibraries": "Pode Acessar Todas Bibliotecas",
"LabelPermissionsAccessAllTags": "Pode Acessar Todas as Etiquetas",
"LabelPermissionsAccessExplicitContent": "Pode Acessar Conteúdos Explícitos",
"LabelPermissionsDelete": "Pode Apagar",
"LabelPermissionsDownload": "Pode Fazer Download",
"LabelPermissionsUpdate": "Pode Atualizar",
"LabelPermissionsUpload": "Pode Fazer Upload",
"LabelPersonalYearReview": "Sua Retrospectiva Anual ({0})",
"LabelPhotoPathURL": "Caminho/URL para Foto",
"LabelPlaylists": "Listas de Reprodução",
"LabelPlayMethod": "Método de Reprodução",
"LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts",
"LabelPodcastSearchRegion": "Região de busca do podcast",
"LabelPodcastType": "Tipo de Podcast",
"LabelPort": "Porta",
"LabelPrefixesToIgnore": "Prefixos para Ignorar (sem distinção entre maiúsculas e minúsculas)",
"LabelPreventIndexing": "Evitar que o seu feed seja indexado pelos diretórios de podcast do iTunes e Google",
"LabelPrimaryEbook": "Ebook principal",
"LabelProgress": "Progresso",
"LabelProvider": "Fonte",
"LabelPubDate": "Data de Publicação",
"LabelPublisher": "Editora",
"LabelPublishYear": "Ano de Publicação",
"LabelRead": "Lido",
"LabelReadAgain": "Ler novamente",
"LabelReadEbookWithoutProgress": "Ler ebook sem armazenar progresso",
"LabelRecentlyAdded": "Novidades",
"LabelRecentSeries": "Séries Recentes",
"LabelRecommended": "Recomendado",
"LabelRedo": "Refazer",
"LabelRegion": "Região",
"LabelReleaseDate": "Data de Lançamento",
"LabelRemoveCover": "Remover capa",
"LabelRowsPerPage": "Linhas por Página",
"LabelRSSFeedCustomOwnerEmail": "Email do dono personalizado",
"LabelRSSFeedCustomOwnerName": "Nome do dono personalizado",
"LabelRSSFeedOpen": "Feed RSS Aberto",
"LabelRSSFeedPreventIndexing": "Impedir Indexação",
"LabelRSSFeedSlug": "Slug do Feed RSS",
"LabelRSSFeedURL": "URL do Feed RSS",
"LabelSearchTerm": "Busca por Termo",
"LabelSearchTitle": "Busca por Título",
"LabelSearchTitleOrASIN": "Busca por Título ou ASIN",
"LabelSeason": "Temporada",
"LabelSelectAllEpisodes": "Selecionar todos os Episódios",
"LabelSelectEpisodesShowing": "Selecionar os {0} Episódios Visíveis",
"LabelSelectUsers": "Selecionar usuários",
"LabelSendEbookToDevice": "Enviar Ebook para...",
"LabelSequence": "Sequência",
"LabelSeries": "Série",
"LabelSeriesName": "Nome da Série",
"LabelSeriesProgress": "Progresso da Série",
"LabelServerYearReview": "Retrospectiva Anual do Servidor ({0})",
"LabelSetEbookAsPrimary": "Definir como principal",
"LabelSetEbookAsSupplementary": "Definir como complementar",
"LabelSettingsAudiobooksOnly": "Apenas Audiobooks",
"LabelSettingsAudiobooksOnlyHelp": "Ao ativar essa configuração os arquivos de ebooks serão ignorados a não ser que estejam dentro de uma pasta com um audiobook. Nesse caso eles serão definidos como ebooks complementares",
"LabelSettingsBookshelfViewHelp": "Aparência esqueomorfa com prateleiras de madeira",
"LabelSettingsChromecastSupport": "Suporte ao Chromecast",
"LabelSettingsDateFormat": "Formato de data",
"LabelSettingsDisableWatcher": "Desativar Monitoramento",
"LabelSettingsDisableWatcherForLibrary": "Desativa o monitoramento de pastas para a biblioteca",
"LabelSettingsDisableWatcherHelp": "Desativa o acréscimo/atualização de itens quando forem detectadas mudanças no arquivo. *Requer reiniciar o servidor",
"LabelSettingsEnableWatcher": "Ativar Monitoramento",
"LabelSettingsEnableWatcherForLibrary": "Ativa o monitoramento de pastas para a biblioteca",
"LabelSettingsEnableWatcherHelp": "Ativa o acréscimo/atualização de itens quando forem detectadas mudanças no arquivo. *Requer reiniciar o servidor",
"LabelSettingsExperimentalFeatures": "Funcionalidade experimentais",
"LabelSettingsExperimentalFeaturesHelp": "Funcionalidade em desenvolvimento que se beneficiairam dos seus comentários e da sua ajuda para testar. Clique para abrir a discussão no github.",
"LabelSettingsFindCovers": "Localizar capas",
"LabelSettingsFindCoversHelp": "Se o seu audiobook não tiver uma capa incluída ou uma imagem de capa na pasta, o verificador tentará localizar uma capa.<br>Atenção: Isso irá estender o tempo de análise",
"LabelSettingsHideSingleBookSeries": "Ocultar séries com um só livro",
"LabelSettingsHideSingleBookSeriesHelp": "Séries com um só livro serão ocultadas na página de séries e na prateleira de séries na página principal.",
"LabelSettingsHomePageBookshelfView": "Usar visão estante na página principal",
"LabelSettingsLibraryBookshelfView": "Usar visão estante na página da biblioteca",
"LabelSettingsParseSubtitles": "Analisar subtítulos",
"LabelSettingsParseSubtitlesHelp": "Extrair subtítulos do nome da pasta do audiobook.<br>Subtítulo deve estar separado por \" - \"<br>ex: \"Título do Livro - Um Subtítulo Aqui\" tem o subtítulo \"Um Subtítulo Aqui\"",
"LabelSettingsPreferMatchedMetadata": "Preferir metadados consultados",
"LabelSettingsPreferMatchedMetadataHelp": "Dados consultados serão priorizados sobre os detalhes do item quando usada a Consulta Rápida. Por padrão, Consulta Rápida só preencherá os detalhes ausentes.",
"LabelSettingsSkipMatchingBooksWithASIN": "Pular consulta de livros que já têm um ASIN",
"LabelSettingsSkipMatchingBooksWithISBN": "Pular consulta de livros que já têm um ISBN",
"LabelSettingsSortingIgnorePrefixes": "Ignorar prefixos ao ordenar",
"LabelSettingsSortingIgnorePrefixesHelp": "ex: o prefixo \"o\" do título \"O Título do Livro\" seria ordenado como \"Título do Livro, O\"",
"LabelSettingsSquareBookCovers": "Usar capas de livro quadradas",
"LabelSettingsSquareBookCoversHelp": "Preferir capas quadradas ao invés das capas 1.6:1 padrão",
"LabelSettingsStoreCoversWithItem": "Armazenar capas com o item",
"LabelSettingsStoreCoversWithItemHelp": "Por padrão as capas são armazenadas em /metadata/items. Ao ativar essa configuração as capas serão armazenadas na pasta do item na sua biblioteca. Apenas um arquivo chamado \"cover\" será mantido",
"LabelSettingsStoreMetadataWithItem": "Armazenar metadados com o item",
"LabelSettingsStoreMetadataWithItemHelp": "Por padrão os arquivos de metadados são armazenados em /metadata/items. Ao ativar essa configuração os arquivos de metadados serão armazenadas nas pastas dos itens na sua biblioteca",
"LabelSettingsTimeFormat": "Formato da Tempo",
"LabelShowAll": "Exibir Todos",
"LabelSize": "Tamanho",
"LabelSleepTimer": "Timer",
"LabelSlug": "Slug",
"LabelStart": "Iniciar",
"LabelStarted": "Iniciado",
"LabelStartedAt": "Iniciado Em",
"LabelStartTime": "Horário do Início",
"LabelStatsAudioTracks": "Trilhas de Áudio",
"LabelStatsAuthors": "Autores",
"LabelStatsBestDay": "Melhor Dia",
"LabelStatsDailyAverage": "Média Diária",
"LabelStatsDays": "Dias",
"LabelStatsDaysListened": "Dias Escutando",
"LabelStatsHours": "Horas",
"LabelStatsInARow": "seguidos",
"LabelStatsItemsFinished": "itens Concluídos",
"LabelStatsItemsInLibrary": "itens na biblioteca",
"LabelStatsMinutes": "minutos",
"LabelStatsMinutesListening": "Minutos Escutando",
"LabelStatsOverallDays": "Total de Dias",
"LabelStatsOverallHours": "Total de Horas",
"LabelStatsWeekListening": "Tempo escutando na semana",
"LabelSubtitle": "Subtítulo",
"LabelSupportedFileTypes": "Tipos de arquivos suportados",
"LabelTag": "Etiqueta",
"LabelTags": "Etiquetas",
"LabelTagsAccessibleToUser": "Etiquetas Acessíveis ao Usuário",
"LabelTagsNotAccessibleToUser": "Etiquetas não Acessíveis Usuário",
"LabelTasks": "Tarefas em Execuçào",
"LabelTextEditorBulletedList": "Lista com marcadores",
"LabelTextEditorLink": "Link",
"LabelTextEditorNumberedList": "Lista numerada",
"LabelTextEditorUnlink": "Remover link",
"LabelTheme": "Tema",
"LabelThemeDark": "Escuro",
"LabelThemeLight": "Claro",
"LabelTimeBase": "Base de tempo",
"LabelTimeListened": "Tempo de escuta",
"LabelTimeListenedToday": "Tempo de escuta hoje",
"LabelTimeRemaining": "{0} restantes",
"LabelTimeToShift": "Deslocamento de tempo em segundos",
"LabelTitle": "Título",
"LabelToolsEmbedMetadata": "Incluir Metadados",
"LabelToolsEmbedMetadataDescription": "Incluir metadados no arquivo de áudio, com imagem da capa e capítulos.",
"LabelToolsMakeM4b": "Gerar audiobook no formato M4B",
"LabelToolsMakeM4bDescription": "Gerar um arquivo de audiobook no formato .M4B com metadados, imagem da capa e capítulos.",
"LabelToolsSplitM4b": "Dividir um M4B em MP3s",
"LabelToolsSplitM4bDescription": "Criar arquivos MP3s a partir da divisão de um M4B em capítulos, com metadados e imagem de capa.",
"LabelTotalDuration": "Duração Total",
"LabelTotalTimeListened": "Tempo Total Escutado",
"LabelTrackFromFilename": "Trilha a partir do nome do arquivo",
"LabelTrackFromMetadata": "Trilha a partir dos Metadados",
"LabelTracks": "Trilhas",
"LabelTracksMultiTrack": "Várias trilhas",
"LabelTracksNone": "Sem trilha",
"LabelTracksSingleTrack": "Trilha única",
"LabelType": "Tipo",
"LabelUnabridged": "Não Abreviada",
"LabelUndo": "Undo",
"LabelUnknown": "Desconhecido",
"LabelUpdateCover": "Atualizar Capa",
"LabelUpdateCoverHelp": "Permite sobrescrever capas existentes para os livros selecionados quando uma consulta for localizada",
"LabelUpdatedAt": "Atualizado em",
"LabelUpdateDetails": "Atualizar Detalhes",
"LabelUpdateDetailsHelp": "Permite sobrescrever detalhes existentes para os livros selecionados quando uma consulta for localizada",
"LabelUploaderDragAndDrop": "Arraste e solte arquivos ou pastas",
"LabelUploaderDropFiles": "Solte os arquivos",
"LabelUploaderItemFetchMetadataHelp": "Busca título, autor e série automaticamente",
"LabelUseChapterTrack": "Usar a trilha do capítulo",
"LabelUseFullTrack": "Usar a trilha toda",
"LabelUser": "Usuário",
"LabelUsername": "Nome do usuário",
"LabelValue": "Valor",
"LabelVersion": "Versão",
"LabelViewBookmarks": "Ver marcadores",
"LabelViewChapters": "Ver capítulos",
"LabelViewQueue": "Ver fila do reprodutor",
"LabelVolume": "Volume",
"LabelWeekdaysToRun": "Dias da semana para executar",
"LabelYearReviewHide": "Ocultar Retrospectiva Anual",
"LabelYearReviewShow": "Exibir Retrospectiva Anual",
"LabelYourAudiobookDuration": "Duração do seu audiobook",
"LabelYourBookmarks": "Seus Marcadores",
"LabelYourPlaylists": "Suas Listas de Reprodução",
"LabelYourProgress": "Seu Progresso",
"MessageAddToPlayerQueue": "Adicionar à lista do reprodutor",
"MessageAppriseDescription": "Para utilizar essa funcionalidade é preciso ter uma instância da <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">API do Apprise</a> em execução ou uma api que possa tratar esses mesmos chamados. <br />A URL da API do Apprise deve conter o caminho completo da URL para enviar as notificações. Ex: se a sua instância da API estiver em <code>http://192.168.1.1:8337</code> você deve utilizar <code>http://192.168.1.1:8337/notify</code>.",
"MessageBackupsDescription": "Backups incluem usuários, progresso dos usuários, detalhes dos itens da biblioteca, configurações do servidor e imagens armazenadas em <code>/metadata/items</code> & <code>/metadata/authors</code>. Backups <strong>não</strong> incluem quaisquer arquivos armazenados nas pastas da sua biblioteca.",
"MessageBatchQuickMatchDescription": "Consulta Rápida tentará adicionar capas e metadados ausentes para os itens selecionados. Ative as opções abaixo para permitir que a Consulta Rápida sobrescreva capas e/ou metadados existentes.",
"MessageBookshelfNoCollections": "Você ainda não criou coleções",
"MessageBookshelfNoResultsForFilter": "Sem Resultados para o filtro \"{0}: {1}\"",
"MessageBookshelfNoRSSFeeds": "Não existem feeds RSS abertos",
"MessageBookshelfNoSeries": "Você não tem séries",
"MessageChapterEndIsAfter": "O final do capítulo está além do final do seu audiobook",
"MessageChapterErrorFirstNotZero": "O primeiro capítulo precisa começar no 0",
"MessageChapterErrorStartGteDuration": "Tempo de início não é válido pois precisa ser menor do que a duração do audioboook",
"MessageChapterErrorStartLtPrev": "Tempo de início não é válido pois precisa ser igual ou maior que o tempo de início do capítulo anterior",
"MessageChapterStartIsAfter": "Início do capítulo está além do final do seu audiobook",
"MessageCheckingCron": "Verificando o cron...",
"MessageConfirmCloseFeed": "Tem certeza de que deseja fechar esse feed?",
"MessageConfirmDeleteBackup": "Tem certeza de que deseja apagar o backup {0}?",
"MessageConfirmDeleteFile": "Essa ação apagará o arquivo do seu sistema de arquivos. Tem certeza?",
"MessageConfirmDeleteLibrary": "Tem certeza de que deseja apagar a biblioteca \"{0}\" definitivamente?",
"MessageConfirmDeleteLibraryItem": "Essa ação apagará o item da biblioteca do banco de dados e do seu sistema de arquivos. Tem certeza?",
"MessageConfirmDeleteLibraryItems": "Essa ação apagará {0} itens da biblioteca do banco de dados e do seu sistema de arquivos. Tem certeza?",
"MessageConfirmDeleteSession": "Tem certeza de que deseja apagar essa sessão?",
"MessageConfirmForceReScan": "Tem certeza de que deseja forçar a nova verificação?",
"MessageConfirmMarkAllEpisodesFinished": "Tem certeza de que deseja marcar todos os episódios como concluídos?",
"MessageConfirmMarkAllEpisodesNotFinished": "Tem certeza de que deseja marcar todos os episódios como não concluídos?",
"MessageConfirmMarkSeriesFinished": "Tem certeza de que deseja marcar todos os livros nesta série como concluídos?",
"MessageConfirmMarkSeriesNotFinished": "Tem certeza de que deseja marcar todos os livros nesta série como não concluídos?",
"MessageConfirmQuickEmbed": "Aviso! Inclusão rápida não fará backup dos seus arquivos de áudio. Verifique se tem um backup dos seus arquivos de áudio. <br><br>Quer continuar?",
"MessageConfirmRemoveAllChapters": "Tem certeza de que deseja remover todos os capítulos?",
"MessageConfirmRemoveAuthor": "Tem certeza de que deseja remover o autor \"{0}\"?",
"MessageConfirmRemoveCollection": "Tem certeza de que deseja remover a coleção \"{0}\"?",
"MessageConfirmRemoveEpisode": "Tem certeza de que deseja remover o episódio \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Tem certeza de que deseja remover os {0} episódios?",
"MessageConfirmRemoveListeningSessions": "Tem certeza de que deseja remover as {0} sessões de escuta?",
"MessageConfirmRemoveNarrator": "Tem certeza de que deseja remover o narrador \"{0}\"?",
"MessageConfirmRemovePlaylist": "Tem certeza de que deseja remover a sua lista de reprodução \"{0}\"?",
"MessageConfirmRenameGenre": "Tem certeza de que deseja renomear o gênero \"{0}\" para \"{1}\" em todos os itens?",
"MessageConfirmRenameGenreMergeNote": "Aviso: Este gênero já existe então eles serão combinados.",
"MessageConfirmRenameGenreWarning": "Atenção! Um gênero com um nome semelhante já existe \"{0}\".",
"MessageConfirmRenameTag": "Tem certeza de que deseja renomear a etiqueta \"{0}\" para \"{1}\" em todos os itens?",
"MessageConfirmRenameTagMergeNote": "Aviso: Esta etiqueta já existe então elas serão combinadas.",
"MessageConfirmRenameTagWarning": "Atenção! Uma etiqueta com um nome semelhante já existe \"{0}\".",
"MessageConfirmReScanLibraryItems": "Tem certeza de que deseja uma nova verificação de {0} itens?",
"MessageConfirmSendEbookToDevice": "Tem certeza de que deseja enviar {0} ebook(s) \"{1}\" para o dispositivo \"{2}\"?",
"MessageDownloadingEpisode": "Realizando o download do episódio",
"MessageDragFilesIntoTrackOrder": "Arraste os arquivos para ordenar as trilhas corretamente",
"MessageEmbedFinished": "Inclusão Concluída!",
"MessageEpisodesQueuedForDownload": "{0} Episódio(s) na fila de download",
"MessageFeedURLWillBe": "URL do Feed será {0}",
"MessageFetching": "Buscando...",
"MessageForceReScanDescription": "verificará todos os arquivos, como uma verificação nova. Etiquetas ID3 de arquivos de áudio, arquivos OPF e arquivos de texto serão tratados como novos.",
"MessageImportantNotice": "Aviso Importante!",
"MessageInsertChapterBelow": "Inserir capítulo abaixo",
"MessageItemsSelected": "{0} Itens Selecionados",
"MessageItemsUpdated": "{0} Itens Atualizados",
"MessageJoinUsOn": "Junte-se a nós",
"MessageListeningSessionsInTheLastYear": "{0} sessões de escuta no ano anterior",
"MessageLoading": "Carregando...",
"MessageLoadingFolders": "Carregando pastas...",
"MessageM4BFailed": "Falha no M4B!",
"MessageM4BFinished": "M4B Concluído!",
"MessageMapChapterTitles": "Designar títulos de capítulos a partir dos capítulos existentes no audiobook sem ajustar seus tempos",
"MessageMarkAllEpisodesFinished": "Marcar todos os episódios como concluídos",
"MessageMarkAllEpisodesNotFinished": "Marcar todos os episódios como não concluídos",
"MessageMarkAsFinished": "Marcar como Concluído",
"MessageMarkAsNotFinished": "Marcar como Não Concluído",
"MessageMatchBooksDescription": "tentará consultar os livros da biblioteca no fornecedor de busca selecionado e preencher os detalhes ausentes e a capa. Não sobrescreve os detalhes.",
"MessageNoAudioTracks": "Sem trilhas de áudio",
"MessageNoAuthors": "Sem Autores",
"MessageNoBackups": "Sem Backups",
"MessageNoBookmarks": "Sem Marcadores",
"MessageNoChapters": "Sem Capítulos",
"MessageNoCollections": "Sem Coleções",
"MessageNoCoversFound": "Nenhuma Capa Encontrada",
"MessageNoDescription": "Sem Descrições",
"MessageNoDownloadsInProgress": "Não existem downloads em andamento",
"MessageNoDownloadsQueued": "Não existem itens na fila de download",
"MessageNoEpisodeMatchesFound": "Não existem episódios correspondentes",
"MessageNoEpisodes": "Sem Episódios",
"MessageNoFoldersAvailable": "Nenhuma Pasta Disponível",
"MessageNoGenres": "Sem Gêneros",
"MessageNoIssues": "Sem Problemas",
"MessageNoItems": "Sem Itens",
"MessageNoItemsFound": "Nenhum item encontrado",
"MessageNoListeningSessions": "Sem Sessões de Escuta",
"MessageNoLogs": "Sem Logs",
"MessageNoMediaProgress": "Sem Progresso de Mídia",
"MessageNoNotifications": "Sem Notificações",
"MessageNoPodcastsFound": "Nenhum podcast encontrado",
"MessageNoResults": "Sem resultados",
"MessageNoSearchResultsFor": "Sem resultados para \"{0}\"",
"MessageNoSeries": "Sem Séries",
"MessageNoTags": "Sem etiquetas",
"MessageNoTasksRunning": "Sem Tarefas em Execução",
"MessageNotYetImplemented": "Ainda não implementado",
"MessageNoUpdateNecessary": "Não é necessária a atualização",
"MessageNoUpdatesWereNecessary": "Nenhuma atualização é necessária",
"MessageNoUserPlaylists": "Você não tem listas de reprodução",
"MessageOr": "ou",
"MessagePauseChapter": "Pausar reprodução do capítulo",
"MessagePlayChapter": "Escutar o início do capítulo",
"MessagePlaylistCreateFromCollection": "Criar uma lista de reprodução a partir da coleção",
"MessagePodcastHasNoRSSFeedForMatching": "Podcast não tem uma URL do feed RSS para ser usada na consulta",
"MessageQuickMatchDescription": "Preenche detalhes vazios do item & capa com o primeiro resultado de '{0}'. Não sobrescreve detalhes a não ser que a configuração 'Preferir metadados consultados' do servidor esteja ativa.",
"MessageRemoveChapter": "Remover capítulo",
"MessageRemoveEpisodes": "Remover {0} episódio(s)",
"MessageRemoveFromPlayerQueue": "Remover da lista do reprodutor",
"MessageRemoveUserWarning": "Tem certeza de que deseja apagar definitivamente o usuário \"{0}\"?",
"MessageReportBugsAndContribute": "Reporte bugs, peça funcionalidades e contribua em",
"MessageResetChaptersConfirm": "Tem certeza de que deseja resetar os capítulos e desfazer as alterações realizadas?",
"MessageRestoreBackupConfirm": "Tem certeza de que deseja restaurar o backup criado em",
"MessageRestoreBackupWarning": "Restaurar um backup sobrescreverá totalmente o banco de dados localizado em /config e as imagens de capa em /metadata/items & /metadata/authors.<br /><br />Backups não alteram quaisquer arquivos nas pastas da sua biblioteca. Se a configuração do servidor de armazenar a arte da capa e os metadados nas pastas da sua biblioteca estiver ativa, esses itens não estão no backup e não serão sobrescritos.<br /><br />Todos os clientes usando o seu servidor serão atualizados automaticamente.",
"MessageSearchResultsFor": "Resultado da busca por",
"MessageSelected": "{0} selecionado(s)",
"MessageServerCouldNotBeReached": "Não foi possível estabelecer conexão com o servidor",
"MessageSetChaptersFromTracksDescription": "Definir os capítulos usando cada arquivo de áudio como um capítulo e o nome do arquivo como o título do capítulo",
"MessageStartPlaybackAtTime": "Iniciar a reprodução de \"{0}\" em {1}?",
"MessageThinking": "Pensando...",
"MessageUploaderItemFailed": "Falha no upload",
"MessageUploaderItemSuccess": "Upload realizado!",
"MessageUploading": "Realizando o upload...",
"MessageValidCronExpression": "Expressão do cron válida",
"MessageWatcherIsDisabledGlobally": "Monitoramento está desativado nas configurações do servidor",
"MessageXLibraryIsEmpty": "Biblioteca {0} está vazia!",
"MessageYourAudiobookDurationIsLonger": "A duração do seu audiobook é maior do que a duração encontrada",
"MessageYourAudiobookDurationIsShorter": "A duração do seu audiobook é menor do que a duração encontrada",
"NoteChangeRootPassword": "O usuário Admiistrador é o único usuário que pode não ter uma senha",
"NoteChapterEditorTimes": "Aviso: O tempo de início do primeiro capítulo precisa ficar em 0:00 e o tempo de início do último capítulo não pode exceder a duração deste audiobook.",
"NoteFolderPicker": "Aviso: pastas já designadas não serão exibidas",
"NoteRSSFeedPodcastAppsHttps": "Atenção: A maioria dos aplicativos de podcasts requer que a URL do feed RSS use HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Atenção: Um ou mais dos seus episódios não tem uma data de publicação. Alguns aplicativos de podcasts requerem isto.",
"NoteUploaderFoldersWithMediaFiles": "Pastas com arquivos de mídia serão tratadas como itens de biblioteca distintos.",
"NoteUploaderOnlyAudioFiles": "Ao subir apenas arquivos de áudio, cada arquivo será tratado como um audiobook distinto.",
"NoteUploaderUnsupportedFiles": "Arquivos não suportados serão ignorados. Ao escolher ou arrastar uma pasta, outros arquivos que não estão em uma pasta dentro do item serão ignorados.",
"PlaceholderNewCollection": "Novo nome da coleção",
"PlaceholderNewFolderPath": "Novo caminho para a pasta",
"PlaceholderNewPlaylist": "Novo nome da lista de reprodução",
"PlaceholderSearch": "Buscar..",
"PlaceholderSearchEpisode": "Buscar Episódio..",
"ToastAccountUpdateFailed": "Falha ao atualizar a conta",
"ToastAccountUpdateSuccess": "Conta atualizada",
"ToastAuthorImageRemoveFailed": "Falha ao remover imagem",
"ToastAuthorImageRemoveSuccess": "Imagem do autor removida",
"ToastAuthorUpdateFailed": "Falha ao atualizar o autor",
"ToastAuthorUpdateMerged": "Autor combinado",
"ToastAuthorUpdateSuccess": "Autor atualizado",
"ToastAuthorUpdateSuccessNoImageFound": "Autor atualizado (nenhuma imagem encontrada)",
"ToastBackupCreateFailed": "Falha ao criar backup",
"ToastBackupCreateSuccess": "Backup criado",
"ToastBackupDeleteFailed": "Falha ao apagar backup",
"ToastBackupDeleteSuccess": "Backup apagado",
"ToastBackupRestoreFailed": "Falha ao restaurar backup",
"ToastBackupUploadFailed": "Falha no upload do backup",
"ToastBackupUploadSuccess": "Upload do backup realizado",
"ToastBatchUpdateFailed": "Falha na atualização em lote",
"ToastBatchUpdateSuccess": "Atualização em lote realizada",
"ToastBookmarkCreateFailed": "Falha ao criar marcador",
"ToastBookmarkCreateSuccess": "Marcador adicionado",
"ToastBookmarkRemoveFailed": "Falha ao remover marcador",
"ToastBookmarkRemoveSuccess": "Marcador removido",
"ToastBookmarkUpdateFailed": "Falha ao atualizar o marcador",
"ToastBookmarkUpdateSuccess": "Marcador atualizado",
"ToastChaptersHaveErrors": "Capítulos com erro",
"ToastChaptersMustHaveTitles": "Capítulos precisam ter títulos",
"ToastCollectionItemsRemoveFailed": "Falha ao remover item(ns) da coleção",
"ToastCollectionItemsRemoveSuccess": "Item(ns) removidos da coleção",
"ToastCollectionRemoveFailed": "Falha ao remover coleção",
"ToastCollectionRemoveSuccess": "Coleção removida",
"ToastCollectionUpdateFailed": "Falha ao atualizar coleção",
"ToastCollectionUpdateSuccess": "Coleção atualizada",
"ToastItemCoverUpdateFailed": "Falha ao atualizar capa do item",
"ToastItemCoverUpdateSuccess": "Capa do item atualizada",
"ToastItemDetailsUpdateFailed": "Falha ao atualizar detalhes do item",
"ToastItemDetailsUpdateSuccess": "Detalhes do item atualizados",
"ToastItemDetailsUpdateUnneeded": "Nenhuma atualização necessária para os detalhes do item",
"ToastItemMarkedAsFinishedFailed": "Falha ao marcar como Concluído",
"ToastItemMarkedAsFinishedSuccess": "Item marcado como Concluído",
"ToastItemMarkedAsNotFinishedFailed": "Falha ao marcar como Não Concluído",
"ToastItemMarkedAsNotFinishedSuccess": "Item marcado como Não Concluído",
"ToastLibraryCreateFailed": "Falha ao criar biblioteca",
"ToastLibraryCreateSuccess": "Biblioteca \"{0}\" criada",
"ToastLibraryDeleteFailed": "Falha ao apagar biblioteca",
"ToastLibraryDeleteSuccess": "Biblioteca apagada",
"ToastLibraryScanFailedToStart": "Falha ao iniciar verificação",
"ToastLibraryScanStarted": "Verificação da biblioteca iniciada",
"ToastLibraryUpdateFailed": "Falha ao atualizar a biblioteca",
"ToastLibraryUpdateSuccess": "Biblioteca \"{0}\" atualizada",
"ToastPlaylistCreateFailed": "Falha ao criar lista de reprodução",
"ToastPlaylistCreateSuccess": "Lista de reprodução criada",
"ToastPlaylistRemoveFailed": "Falha ao remover lista de reprodução",
"ToastPlaylistRemoveSuccess": "Lista de reprodução removida",
"ToastPlaylistUpdateFailed": "Falha ao atualizar lista de reprodução",
"ToastPlaylistUpdateSuccess": "Lista de reprodução atualizada",
"ToastPodcastCreateFailed": "Falha ao criar podcast",
"ToastPodcastCreateSuccess": "Podcast criado",
"ToastRemoveItemFromCollectionFailed": "Falha ao remover item da coleção",
"ToastRemoveItemFromCollectionSuccess": "Item removido da coleção",
"ToastRSSFeedCloseFailed": "Falha ao fechar feed RSS",
"ToastRSSFeedCloseSuccess": "Feed RSS fechado",
"ToastSendEbookToDeviceFailed": "Falha ao enviar ebook para dispositivo",
"ToastSendEbookToDeviceSuccess": "Ebook enviado para o dispositivo \"{0}\"",
"ToastSeriesUpdateFailed": "Falha ao atualizar série",
"ToastSeriesUpdateSuccess": "Série atualizada",
"ToastSessionDeleteFailed": "Falha ao apagar sessão",
"ToastSessionDeleteSuccess": "Sessão apagada",
"ToastSocketConnected": "Socket conectado",
"ToastSocketDisconnected": "Socket desconectado",
"ToastSocketFailedToConnect": "Falha na conexão do socket",
"ToastUserDeleteFailed": "Falha ao apagar usuário",
"ToastUserDeleteSuccess": "Usuário apagado"
}

View File

@ -32,6 +32,8 @@
"ButtonHide": "Скрыть", "ButtonHide": "Скрыть",
"ButtonHome": "Домой", "ButtonHome": "Домой",
"ButtonIssues": "Проблемы", "ButtonIssues": "Проблемы",
"ButtonJumpBackward": "Jump Backward",
"ButtonJumpForward": "Jump Forward",
"ButtonLatest": "Последнее", "ButtonLatest": "Последнее",
"ButtonLibrary": "Библиотека", "ButtonLibrary": "Библиотека",
"ButtonLogout": "Выход", "ButtonLogout": "Выход",
@ -41,12 +43,17 @@
"ButtonMatchAllAuthors": "Найти всех авторов", "ButtonMatchAllAuthors": "Найти всех авторов",
"ButtonMatchBooks": "Найти книги", "ButtonMatchBooks": "Найти книги",
"ButtonNevermind": "Не важно", "ButtonNevermind": "Не важно",
"ButtonNext": "Next",
"ButtonNextChapter": "Next Chapter",
"ButtonOk": "Ok", "ButtonOk": "Ok",
"ButtonOpenFeed": "Открыть канал", "ButtonOpenFeed": "Открыть канал",
"ButtonOpenManager": "Открыть менеджер", "ButtonOpenManager": "Открыть менеджер",
"ButtonPause": "Pause",
"ButtonPlay": "Слушать", "ButtonPlay": "Слушать",
"ButtonPlaying": "Проигрывается", "ButtonPlaying": "Проигрывается",
"ButtonPlaylists": "Плейлисты", "ButtonPlaylists": "Плейлисты",
"ButtonPrevious": "Previous",
"ButtonPreviousChapter": "Previous Chapter",
"ButtonPurgeAllCache": "Очистить весь кэш", "ButtonPurgeAllCache": "Очистить весь кэш",
"ButtonPurgeItemsCache": "Очистить кэш элементов", "ButtonPurgeItemsCache": "Очистить кэш элементов",
"ButtonPurgeMediaProgress": "Очистить прогресс медиа", "ButtonPurgeMediaProgress": "Очистить прогресс медиа",
@ -54,6 +61,7 @@
"ButtonQueueRemoveItem": "Удалить из очереди", "ButtonQueueRemoveItem": "Удалить из очереди",
"ButtonQuickMatch": "Быстрый поиск", "ButtonQuickMatch": "Быстрый поиск",
"ButtonRead": "Читать", "ButtonRead": "Читать",
"ButtonRefresh": "Refresh",
"ButtonRemove": "Удалить", "ButtonRemove": "Удалить",
"ButtonRemoveAll": "Удалить всё", "ButtonRemoveAll": "Удалить всё",
"ButtonRemoveAllLibraryItems": "Удалить все элементы библиотеки", "ButtonRemoveAllLibraryItems": "Удалить все элементы библиотеки",
@ -73,6 +81,7 @@
"ButtonSelectFolderPath": "Выберите путь папки", "ButtonSelectFolderPath": "Выберите путь папки",
"ButtonSeries": "Серии", "ButtonSeries": "Серии",
"ButtonSetChaptersFromTracks": "Установить главы из треков", "ButtonSetChaptersFromTracks": "Установить главы из треков",
"ButtonShare": "Share",
"ButtonShiftTimes": "Смещение", "ButtonShiftTimes": "Смещение",
"ButtonShow": "Показать", "ButtonShow": "Показать",
"ButtonStartM4BEncode": "Начать кодирование M4B", "ButtonStartM4BEncode": "Начать кодирование M4B",
@ -104,6 +113,7 @@
"HeaderCollectionItems": "Элементы коллекции", "HeaderCollectionItems": "Элементы коллекции",
"HeaderCover": "Обложка", "HeaderCover": "Обложка",
"HeaderCurrentDownloads": "Текущие закачки", "HeaderCurrentDownloads": "Текущие закачки",
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
"HeaderDetails": "Подробности", "HeaderDetails": "Подробности",
"HeaderDownloadQueue": "Очередь скачивания", "HeaderDownloadQueue": "Очередь скачивания",
"HeaderEbookFiles": "Файлы e-книг", "HeaderEbookFiles": "Файлы e-книг",
@ -174,6 +184,7 @@
"HeaderUpdateDetails": "Обновить детали", "HeaderUpdateDetails": "Обновить детали",
"HeaderUpdateLibrary": "Обновить библиотеку", "HeaderUpdateLibrary": "Обновить библиотеку",
"HeaderUsers": "Пользователи", "HeaderUsers": "Пользователи",
"HeaderYearReview": "Year {0} in Review",
"HeaderYourStats": "Ваша статистика", "HeaderYourStats": "Ваша статистика",
"LabelAbridged": "Сокращенное издание", "LabelAbridged": "Сокращенное издание",
"LabelAccountType": "Тип учетной записи", "LabelAccountType": "Тип учетной записи",
@ -345,7 +356,9 @@
"LabelMetaTags": "Мета теги", "LabelMetaTags": "Мета теги",
"LabelMinute": "Минуты", "LabelMinute": "Минуты",
"LabelMissing": "Потеряно", "LabelMissing": "Потеряно",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Потерянные части", "LabelMissingParts": "Потерянные части",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "Разрешенные URI перенаправления с мобильных устройств", "LabelMobileRedirectURIs": "Разрешенные URI перенаправления с мобильных устройств",
"LabelMobileRedirectURIsDescription": "Это белый список допустимых URI перенаправления для мобильных приложений. По умолчанию используется <code>audiobookshelf://oauth</code>, который можно удалить или дополнить дополнительными URI для интеграции со сторонними приложениями. Использование звездочки (<code>*</code>) в качестве единственной записи разрешает любой URI.", "LabelMobileRedirectURIsDescription": "Это белый список допустимых URI перенаправления для мобильных приложений. По умолчанию используется <code>audiobookshelf://oauth</code>, который можно удалить или дополнить дополнительными URI для интеграции со сторонними приложениями. Использование звездочки (<code>*</code>) в качестве единственной записи разрешает любой URI.",
"LabelMore": "Еще", "LabelMore": "Еще",
@ -385,11 +398,13 @@
"LabelPermissionsDownload": "Может скачивать", "LabelPermissionsDownload": "Может скачивать",
"LabelPermissionsUpdate": "Может обновлять", "LabelPermissionsUpdate": "Может обновлять",
"LabelPermissionsUpload": "Может закачивать", "LabelPermissionsUpload": "Может закачивать",
"LabelPersonalYearReview": "Your Year in Review ({0})",
"LabelPhotoPathURL": "Путь к фото/URL", "LabelPhotoPathURL": "Путь к фото/URL",
"LabelPlaylists": "Плейлисты", "LabelPlaylists": "Плейлисты",
"LabelPlayMethod": "Метод воспроизведения", "LabelPlayMethod": "Метод воспроизведения",
"LabelPodcast": "Подкаст", "LabelPodcast": "Подкаст",
"LabelPodcasts": "Подкасты", "LabelPodcasts": "Подкасты",
"LabelPodcastSearchRegion": "Регион поиска подкастов",
"LabelPodcastType": "Тип подкаста", "LabelPodcastType": "Тип подкаста",
"LabelPort": "Порт", "LabelPort": "Порт",
"LabelPrefixesToIgnore": "Игнорируемые префиксы (без учета регистра)", "LabelPrefixesToIgnore": "Игнорируемые префиксы (без учета регистра)",
@ -429,6 +444,7 @@
"LabelSeries": "Серия", "LabelSeries": "Серия",
"LabelSeriesName": "Имя серии", "LabelSeriesName": "Имя серии",
"LabelSeriesProgress": "Прогресс серии", "LabelSeriesProgress": "Прогресс серии",
"LabelServerYearReview": "Server Year in Review ({0})",
"LabelSetEbookAsPrimary": "Установить как основную", "LabelSetEbookAsPrimary": "Установить как основную",
"LabelSetEbookAsSupplementary": "Установить как дополнительную", "LabelSetEbookAsSupplementary": "Установить как дополнительную",
"LabelSettingsAudiobooksOnly": "Только аудиокниги", "LabelSettingsAudiobooksOnly": "Только аудиокниги",
@ -545,6 +561,8 @@
"LabelViewQueue": "Очередь воспроизведения", "LabelViewQueue": "Очередь воспроизведения",
"LabelVolume": "Громкость", "LabelVolume": "Громкость",
"LabelWeekdaysToRun": "Дни недели для запуска", "LabelWeekdaysToRun": "Дни недели для запуска",
"LabelYearReviewHide": "Hide Year in Review",
"LabelYearReviewShow": "See Year in Review",
"LabelYourAudiobookDuration": "Продолжительность Вашей книги", "LabelYourAudiobookDuration": "Продолжительность Вашей книги",
"LabelYourBookmarks": "Ваши закладки", "LabelYourBookmarks": "Ваши закладки",
"LabelYourPlaylists": "Ваши плейлисты", "LabelYourPlaylists": "Ваши плейлисты",

View File

@ -32,6 +32,8 @@
"ButtonHide": "Dölj", "ButtonHide": "Dölj",
"ButtonHome": "Hem", "ButtonHome": "Hem",
"ButtonIssues": "Problem", "ButtonIssues": "Problem",
"ButtonJumpBackward": "Jump Backward",
"ButtonJumpForward": "Jump Forward",
"ButtonLatest": "Senaste", "ButtonLatest": "Senaste",
"ButtonLibrary": "Bibliotek", "ButtonLibrary": "Bibliotek",
"ButtonLogout": "Logga ut", "ButtonLogout": "Logga ut",
@ -41,12 +43,17 @@
"ButtonMatchAllAuthors": "Matcha alla författare", "ButtonMatchAllAuthors": "Matcha alla författare",
"ButtonMatchBooks": "Matcha böcker", "ButtonMatchBooks": "Matcha böcker",
"ButtonNevermind": "Glöm det", "ButtonNevermind": "Glöm det",
"ButtonNext": "Next",
"ButtonNextChapter": "Next Chapter",
"ButtonOk": "Okej", "ButtonOk": "Okej",
"ButtonOpenFeed": "Öppna flöde", "ButtonOpenFeed": "Öppna flöde",
"ButtonOpenManager": "Öppna Manager", "ButtonOpenManager": "Öppna Manager",
"ButtonPause": "Pause",
"ButtonPlay": "Spela", "ButtonPlay": "Spela",
"ButtonPlaying": "Spelar", "ButtonPlaying": "Spelar",
"ButtonPlaylists": "Spellistor", "ButtonPlaylists": "Spellistor",
"ButtonPrevious": "Previous",
"ButtonPreviousChapter": "Previous Chapter",
"ButtonPurgeAllCache": "Rensa all cache", "ButtonPurgeAllCache": "Rensa all cache",
"ButtonPurgeItemsCache": "Rensa föremåls-cache", "ButtonPurgeItemsCache": "Rensa föremåls-cache",
"ButtonPurgeMediaProgress": "Rensa medieförlopp", "ButtonPurgeMediaProgress": "Rensa medieförlopp",
@ -54,6 +61,7 @@
"ButtonQueueRemoveItem": "Ta bort från kön", "ButtonQueueRemoveItem": "Ta bort från kön",
"ButtonQuickMatch": "Snabb matchning", "ButtonQuickMatch": "Snabb matchning",
"ButtonRead": "Läs", "ButtonRead": "Läs",
"ButtonRefresh": "Refresh",
"ButtonRemove": "Ta bort", "ButtonRemove": "Ta bort",
"ButtonRemoveAll": "Ta bort alla", "ButtonRemoveAll": "Ta bort alla",
"ButtonRemoveAllLibraryItems": "Ta bort alla biblioteksobjekt", "ButtonRemoveAllLibraryItems": "Ta bort alla biblioteksobjekt",
@ -73,6 +81,7 @@
"ButtonSelectFolderPath": "Välj mappens sökväg", "ButtonSelectFolderPath": "Välj mappens sökväg",
"ButtonSeries": "Serie", "ButtonSeries": "Serie",
"ButtonSetChaptersFromTracks": "Ställ in kapitel från spår", "ButtonSetChaptersFromTracks": "Ställ in kapitel från spår",
"ButtonShare": "Share",
"ButtonShiftTimes": "Förskjut tider", "ButtonShiftTimes": "Förskjut tider",
"ButtonShow": "Visa", "ButtonShow": "Visa",
"ButtonStartM4BEncode": "Starta M4B-kodning", "ButtonStartM4BEncode": "Starta M4B-kodning",
@ -104,6 +113,7 @@
"HeaderCollectionItems": "Samlingselement", "HeaderCollectionItems": "Samlingselement",
"HeaderCover": "Omslag", "HeaderCover": "Omslag",
"HeaderCurrentDownloads": "Aktuella nedladdningar", "HeaderCurrentDownloads": "Aktuella nedladdningar",
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
"HeaderDetails": "Detaljer", "HeaderDetails": "Detaljer",
"HeaderDownloadQueue": "Nedladdningskö", "HeaderDownloadQueue": "Nedladdningskö",
"HeaderEbookFiles": "E-boksfiler", "HeaderEbookFiles": "E-boksfiler",
@ -174,6 +184,7 @@
"HeaderUpdateDetails": "Uppdatera detaljer", "HeaderUpdateDetails": "Uppdatera detaljer",
"HeaderUpdateLibrary": "Uppdatera bibliotek", "HeaderUpdateLibrary": "Uppdatera bibliotek",
"HeaderUsers": "Användare", "HeaderUsers": "Användare",
"HeaderYearReview": "Year {0} in Review",
"HeaderYourStats": "Dina statistik", "HeaderYourStats": "Dina statistik",
"LabelAbridged": "Förkortad", "LabelAbridged": "Förkortad",
"LabelAccountType": "Kontotyp", "LabelAccountType": "Kontotyp",
@ -345,7 +356,9 @@
"LabelMetaTags": "Metamärken", "LabelMetaTags": "Metamärken",
"LabelMinute": "Minut", "LabelMinute": "Minut",
"LabelMissing": "Saknad", "LabelMissing": "Saknad",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Saknade delar", "LabelMissingParts": "Saknade delar",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", "LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", "LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
"LabelMore": "Mer", "LabelMore": "Mer",
@ -385,11 +398,13 @@
"LabelPermissionsDownload": "Kan ladda ner", "LabelPermissionsDownload": "Kan ladda ner",
"LabelPermissionsUpdate": "Kan uppdatera", "LabelPermissionsUpdate": "Kan uppdatera",
"LabelPermissionsUpload": "Kan ladda upp", "LabelPermissionsUpload": "Kan ladda upp",
"LabelPersonalYearReview": "Your Year in Review ({0})",
"LabelPhotoPathURL": "Bildsökväg/URL", "LabelPhotoPathURL": "Bildsökväg/URL",
"LabelPlaylists": "Spellistor", "LabelPlaylists": "Spellistor",
"LabelPlayMethod": "Spelläge", "LabelPlayMethod": "Spelläge",
"LabelPodcast": "Podcast", "LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts", "LabelPodcasts": "Podcasts",
"LabelPodcastSearchRegion": "Podcast-sökområde",
"LabelPodcastType": "Podcasttyp", "LabelPodcastType": "Podcasttyp",
"LabelPort": "Port", "LabelPort": "Port",
"LabelPrefixesToIgnore": "Prefix att ignorera (skiftlägesokänsligt)", "LabelPrefixesToIgnore": "Prefix att ignorera (skiftlägesokänsligt)",
@ -429,6 +444,7 @@
"LabelSeries": "Serie", "LabelSeries": "Serie",
"LabelSeriesName": "Serienamn", "LabelSeriesName": "Serienamn",
"LabelSeriesProgress": "Serieframsteg", "LabelSeriesProgress": "Serieframsteg",
"LabelServerYearReview": "Server Year in Review ({0})",
"LabelSetEbookAsPrimary": "Ange som primär", "LabelSetEbookAsPrimary": "Ange som primär",
"LabelSetEbookAsSupplementary": "Ange som kompletterande", "LabelSetEbookAsSupplementary": "Ange som kompletterande",
"LabelSettingsAudiobooksOnly": "Endast ljudböcker", "LabelSettingsAudiobooksOnly": "Endast ljudböcker",
@ -545,6 +561,8 @@
"LabelViewQueue": "Visa spellista", "LabelViewQueue": "Visa spellista",
"LabelVolume": "Volym", "LabelVolume": "Volym",
"LabelWeekdaysToRun": "Vardagar att köra", "LabelWeekdaysToRun": "Vardagar att köra",
"LabelYearReviewHide": "Hide Year in Review",
"LabelYearReviewShow": "See Year in Review",
"LabelYourAudiobookDuration": "Din ljudboks varaktighet", "LabelYourAudiobookDuration": "Din ljudboks varaktighet",
"LabelYourBookmarks": "Dina bokmärken", "LabelYourBookmarks": "Dina bokmärken",
"LabelYourPlaylists": "Dina spellistor", "LabelYourPlaylists": "Dina spellistor",

View File

@ -32,6 +32,8 @@
"ButtonHide": "隐藏", "ButtonHide": "隐藏",
"ButtonHome": "首页", "ButtonHome": "首页",
"ButtonIssues": "问题", "ButtonIssues": "问题",
"ButtonJumpBackward": "Jump Backward",
"ButtonJumpForward": "Jump Forward",
"ButtonLatest": "最新", "ButtonLatest": "最新",
"ButtonLibrary": "媒体库", "ButtonLibrary": "媒体库",
"ButtonLogout": "注销", "ButtonLogout": "注销",
@ -41,12 +43,17 @@
"ButtonMatchAllAuthors": "匹配所有作者", "ButtonMatchAllAuthors": "匹配所有作者",
"ButtonMatchBooks": "匹配图书", "ButtonMatchBooks": "匹配图书",
"ButtonNevermind": "没有关系", "ButtonNevermind": "没有关系",
"ButtonNext": "Next",
"ButtonNextChapter": "Next Chapter",
"ButtonOk": "确定", "ButtonOk": "确定",
"ButtonOpenFeed": "打开源", "ButtonOpenFeed": "打开源",
"ButtonOpenManager": "打开管理器", "ButtonOpenManager": "打开管理器",
"ButtonPause": "Pause",
"ButtonPlay": "播放", "ButtonPlay": "播放",
"ButtonPlaying": "正在播放", "ButtonPlaying": "正在播放",
"ButtonPlaylists": "播放列表", "ButtonPlaylists": "播放列表",
"ButtonPrevious": "Previous",
"ButtonPreviousChapter": "Previous Chapter",
"ButtonPurgeAllCache": "清理所有缓存", "ButtonPurgeAllCache": "清理所有缓存",
"ButtonPurgeItemsCache": "清理项目缓存", "ButtonPurgeItemsCache": "清理项目缓存",
"ButtonPurgeMediaProgress": "清理媒体进度", "ButtonPurgeMediaProgress": "清理媒体进度",
@ -54,6 +61,7 @@
"ButtonQueueRemoveItem": "从队列中移除", "ButtonQueueRemoveItem": "从队列中移除",
"ButtonQuickMatch": "快速匹配", "ButtonQuickMatch": "快速匹配",
"ButtonRead": "读取", "ButtonRead": "读取",
"ButtonRefresh": "Refresh",
"ButtonRemove": "移除", "ButtonRemove": "移除",
"ButtonRemoveAll": "移除所有", "ButtonRemoveAll": "移除所有",
"ButtonRemoveAllLibraryItems": "移除所有媒体库项目", "ButtonRemoveAllLibraryItems": "移除所有媒体库项目",
@ -73,6 +81,7 @@
"ButtonSelectFolderPath": "选择文件夹路径", "ButtonSelectFolderPath": "选择文件夹路径",
"ButtonSeries": "系列", "ButtonSeries": "系列",
"ButtonSetChaptersFromTracks": "将音轨设置为章节", "ButtonSetChaptersFromTracks": "将音轨设置为章节",
"ButtonShare": "Share",
"ButtonShiftTimes": "快速调整时间", "ButtonShiftTimes": "快速调整时间",
"ButtonShow": "显示", "ButtonShow": "显示",
"ButtonStartM4BEncode": "开始 M4B 编码", "ButtonStartM4BEncode": "开始 M4B 编码",
@ -104,6 +113,7 @@
"HeaderCollectionItems": "收藏项目", "HeaderCollectionItems": "收藏项目",
"HeaderCover": "封面", "HeaderCover": "封面",
"HeaderCurrentDownloads": "当前下载", "HeaderCurrentDownloads": "当前下载",
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
"HeaderDetails": "详情", "HeaderDetails": "详情",
"HeaderDownloadQueue": "下载队列", "HeaderDownloadQueue": "下载队列",
"HeaderEbookFiles": "电子书文件", "HeaderEbookFiles": "电子书文件",
@ -174,6 +184,7 @@
"HeaderUpdateDetails": "更新详情", "HeaderUpdateDetails": "更新详情",
"HeaderUpdateLibrary": "更新媒体库", "HeaderUpdateLibrary": "更新媒体库",
"HeaderUsers": "用户", "HeaderUsers": "用户",
"HeaderYearReview": "Year {0} in Review",
"HeaderYourStats": "你的统计数据", "HeaderYourStats": "你的统计数据",
"LabelAbridged": "概要", "LabelAbridged": "概要",
"LabelAccountType": "帐户类型", "LabelAccountType": "帐户类型",
@ -345,7 +356,9 @@
"LabelMetaTags": "元标签", "LabelMetaTags": "元标签",
"LabelMinute": "分钟", "LabelMinute": "分钟",
"LabelMissing": "丢失", "LabelMissing": "丢失",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "丢失的部分", "LabelMissingParts": "丢失的部分",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "允许移动应用重定向 URI", "LabelMobileRedirectURIs": "允许移动应用重定向 URI",
"LabelMobileRedirectURIsDescription": "这是移动应用程序的有效重定向 URI 白名单. 默认值为 <code>audiobookshelf://oauth</code>,您可以删除它或添加其他 URI 以进行第三方应用集成. 使用星号 (<code>*</code>) 作为唯一条目允许任何 URI.", "LabelMobileRedirectURIsDescription": "这是移动应用程序的有效重定向 URI 白名单. 默认值为 <code>audiobookshelf://oauth</code>,您可以删除它或添加其他 URI 以进行第三方应用集成. 使用星号 (<code>*</code>) 作为唯一条目允许任何 URI.",
"LabelMore": "更多", "LabelMore": "更多",
@ -385,11 +398,13 @@
"LabelPermissionsDownload": "可以下载", "LabelPermissionsDownload": "可以下载",
"LabelPermissionsUpdate": "可以更新", "LabelPermissionsUpdate": "可以更新",
"LabelPermissionsUpload": "可以上传", "LabelPermissionsUpload": "可以上传",
"LabelPersonalYearReview": "Your Year in Review ({0})",
"LabelPhotoPathURL": "图片路径或 URL", "LabelPhotoPathURL": "图片路径或 URL",
"LabelPlaylists": "播放列表", "LabelPlaylists": "播放列表",
"LabelPlayMethod": "播放方法", "LabelPlayMethod": "播放方法",
"LabelPodcast": "播客", "LabelPodcast": "播客",
"LabelPodcasts": "播客", "LabelPodcasts": "播客",
"LabelPodcastSearchRegion": "播客搜索地区",
"LabelPodcastType": "播客类型", "LabelPodcastType": "播客类型",
"LabelPort": "端口", "LabelPort": "端口",
"LabelPrefixesToIgnore": "忽略的前缀 (不区分大小写)", "LabelPrefixesToIgnore": "忽略的前缀 (不区分大小写)",
@ -429,6 +444,7 @@
"LabelSeries": "系列", "LabelSeries": "系列",
"LabelSeriesName": "系列名称", "LabelSeriesName": "系列名称",
"LabelSeriesProgress": "系列进度", "LabelSeriesProgress": "系列进度",
"LabelServerYearReview": "Server Year in Review ({0})",
"LabelSetEbookAsPrimary": "设置为主", "LabelSetEbookAsPrimary": "设置为主",
"LabelSetEbookAsSupplementary": "设置为补充", "LabelSetEbookAsSupplementary": "设置为补充",
"LabelSettingsAudiobooksOnly": "只有有声读物", "LabelSettingsAudiobooksOnly": "只有有声读物",
@ -545,6 +561,8 @@
"LabelViewQueue": "查看播放列表", "LabelViewQueue": "查看播放列表",
"LabelVolume": "音量", "LabelVolume": "音量",
"LabelWeekdaysToRun": "工作日运行", "LabelWeekdaysToRun": "工作日运行",
"LabelYearReviewHide": "Hide Year in Review",
"LabelYearReviewShow": "See Year in Review",
"LabelYourAudiobookDuration": "你的有声读物持续时间", "LabelYourAudiobookDuration": "你的有声读物持续时间",
"LabelYourBookmarks": "你的书签", "LabelYourBookmarks": "你的书签",
"LabelYourPlaylists": "你的播放列表", "LabelYourPlaylists": "你的播放列表",

View 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
View File

@ -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",

View File

@ -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",

View File

@ -76,12 +76,16 @@ class Auth {
return return
} }
// Custom req timeout see: https://github.com/panva/node-openid-client/blob/main/docs/README.md#customizing
OpenIDClient.custom.setHttpOptionsDefaults({ timeout: 10000 })
const openIdIssuerClient = new OpenIDClient.Issuer({ const openIdIssuerClient = new OpenIDClient.Issuer({
issuer: global.ServerSettings.authOpenIDIssuerURL, issuer: global.ServerSettings.authOpenIDIssuerURL,
authorization_endpoint: global.ServerSettings.authOpenIDAuthorizationURL, authorization_endpoint: global.ServerSettings.authOpenIDAuthorizationURL,
token_endpoint: global.ServerSettings.authOpenIDTokenURL, token_endpoint: global.ServerSettings.authOpenIDTokenURL,
userinfo_endpoint: global.ServerSettings.authOpenIDUserInfoURL, userinfo_endpoint: global.ServerSettings.authOpenIDUserInfoURL,
jwks_uri: global.ServerSettings.authOpenIDJwksURL jwks_uri: global.ServerSettings.authOpenIDJwksURL,
end_session_endpoint: global.ServerSettings.authOpenIDLogoutURL
}).Client }).Client
const openIdClient = new openIdIssuerClient({ const openIdClient = new openIdIssuerClient({
client_id: global.ServerSettings.authOpenIDClientID, client_id: global.ServerSettings.authOpenIDClientID,
@ -153,6 +157,9 @@ class Auth {
return return
} }
// We also have to save the id_token for later (used for logout) because we cannot set cookies here
user.openid_id_token = tokenset.id_token
// permit login // permit login
return done(null, user) return done(null, user)
})) }))
@ -183,49 +190,48 @@ class Auth {
} }
/** /**
* Stores the client's choice how the login callback should happen in temp cookies * Returns if the given auth method is API based.
*
* @param {string} authMethod
* @returns {boolean}
*/
isAuthMethodAPIBased(authMethod) {
return ['api', 'openid-mobile'].includes(authMethod)
}
/**
* Stores the client's choice of login callback method in temporary cookies.
*
* The `authMethod` parameter specifies the authentication strategy and can have the following values:
* - 'local': Standard authentication,
* - 'api': Authentication for API use
* - 'openid': OpenID authentication directly over web
* - 'openid-mobile': OpenID authentication, but done via an mobile device
* *
* @param {import('express').Request} req * @param {import('express').Request} req
* @param {import('express').Response} res * @param {import('express').Response} res
* @param {string} authMethod - The authentication method, default is 'local'.
*/ */
paramsToCookies(req, res) { paramsToCookies(req, res, authMethod = 'local') {
// Set if isRest flag is set or if mobile oauth flow is used const TWO_MINUTES = 120000 // 2 minutes in milliseconds
if (req.query.isRest?.toLowerCase() == 'true' || req.query.redirect_uri) {
// store the isRest flag to the is_rest cookie
res.cookie('is_rest', 'true', {
maxAge: 120000, // 2 min
httpOnly: true
})
} else {
// no isRest-flag set -> set is_rest cookie to false
res.cookie('is_rest', 'false', {
maxAge: 120000, // 2 min
httpOnly: true
})
// persist state if passed in
if (req.query.state) {
res.cookie('auth_state', req.query.state, {
maxAge: 120000, // 2 min
httpOnly: true
})
}
const callback = req.query.redirect_uri || req.query.callback const callback = req.query.redirect_uri || req.query.callback
// check if we are missing a callback parameter - we need one if isRest=false // Additional handling for non-API based authMethod
if (!this.isAuthMethodAPIBased(authMethod)) {
// Store 'auth_state' if present in the request
if (req.query.state) {
res.cookie('auth_state', req.query.state, { maxAge: TWO_MINUTES, httpOnly: true })
}
// Validate and store the callback URL
if (!callback) { if (!callback) {
res.status(400).send({ return res.status(400).send({ message: 'No callback parameter' })
message: 'No callback parameter'
})
return
} }
// store the callback url to the auth_cb cookie res.cookie('auth_cb', callback, { maxAge: TWO_MINUTES, httpOnly: true })
res.cookie('auth_cb', callback, {
maxAge: 120000, // 2 min
httpOnly: true
})
} }
// Store the authentication method for long
res.cookie('auth_method', authMethod, { maxAge: 1000 * 60 * 60 * 24 * 365 * 10, httpOnly: true })
} }
/** /**
@ -239,7 +245,7 @@ class Auth {
// get userLogin json (information about the user, server and the session) // get userLogin json (information about the user, server and the session)
const data_json = await this.getUserLoginResponsePayload(req.user) const data_json = await this.getUserLoginResponsePayload(req.user)
if (req.cookies.is_rest === 'true') { if (this.isAuthMethodAPIBased(req.cookies.auth_method)) {
// REST request - send data // REST request - send data
res.json(data_json) res.json(data_json)
} else { } else {
@ -269,109 +275,105 @@ class Auth {
// openid strategy login route (this redirects to the configured openid login provider) // openid strategy login route (this redirects to the configured openid login provider)
router.get('/auth/openid', (req, res, next) => { router.get('/auth/openid', (req, res, next) => {
try {
// helper function from openid-client
function pick(object, ...paths) {
const obj = {}
for (const path of paths) {
if (object[path] !== undefined) {
obj[path] = object[path]
}
}
return obj
}
// Get the OIDC client from the strategy // Get the OIDC client from the strategy
// We need to call the client manually, because the strategy does not support forwarding the code challenge // We need to call the client manually, because the strategy does not support forwarding the code challenge
// for API or mobile clients // for API or mobile clients
const oidcStrategy = passport._strategy('openid-client') const oidcStrategy = passport._strategy('openid-client')
const protocol = (req.secure || req.get('x-forwarded-proto') === 'https') ? 'https' : 'http'
let mobile_redirect_uri = null
// The client wishes a different redirect_uri
// We will allow if it is in the whitelist, by saving it into this.openIdAuthSession and setting the redirect uri to /auth/openid/mobile-redirect
// where we will handle the redirect to it
if (req.query.redirect_uri) {
// Check if the redirect_uri is in the whitelist
if (Database.serverSettings.authOpenIDMobileRedirectURIs.includes(req.query.redirect_uri) ||
(Database.serverSettings.authOpenIDMobileRedirectURIs.length === 1 && Database.serverSettings.authOpenIDMobileRedirectURIs[0] === '*')) {
oidcStrategy._params.redirect_uri = new URL(`${protocol}://${req.get('host')}/auth/openid/mobile-redirect`).toString()
mobile_redirect_uri = req.query.redirect_uri
} else {
Logger.debug(`[Auth] Invalid redirect_uri=${req.query.redirect_uri} - not in whitelist`)
return res.status(400).send('Invalid redirect_uri')
}
} else {
oidcStrategy._params.redirect_uri = new URL(`${protocol}://${req.get('host')}/auth/openid/callback`).toString()
}
Logger.debug(`[Auth] Oidc redirect_uri=${oidcStrategy._params.redirect_uri}`)
const client = oidcStrategy._client const client = oidcStrategy._client
const sessionKey = oidcStrategy._key const sessionKey = oidcStrategy._key
let code_challenge try {
let code_challenge_method const protocol = req.secure || req.get('x-forwarded-proto') === 'https' ? 'https' : 'http'
const hostUrl = new URL(`${protocol}://${req.get('host')}`)
const isMobileFlow = req.query.response_type === 'code' || req.query.redirect_uri || req.query.code_challenge
// If code_challenge is provided, expect that code_verifier will be handled by the client (mobile app) // Only allow code flow (for mobile clients)
// The web frontend of ABS does not need to do a PKCE itself, because it never handles the "code" of the oauth flow if (req.query.response_type && req.query.response_type !== 'code') {
// and as such will not send a code challenge, we will generate then one Logger.debug(`[Auth] OIDC Invalid response_type=${req.query.response_type}`)
if (req.query.code_challenge) { return res.status(400).send('Invalid response_type, only code supported')
code_challenge = req.query.code_challenge
code_challenge_method = req.query.code_challenge_method || 'S256'
if (!['S256', 'plain'].includes(code_challenge_method)) {
return res.status(400).send('Invalid code_challenge_method')
} }
// Generate a state on web flow or if no state supplied
const state = (!isMobileFlow || !req.query.state) ? OpenIDClient.generators.random() : req.query.state
// Redirect URL for the SSO provider
let redirectUri
if (isMobileFlow) {
// Mobile required redirect uri
// If it is in the whitelist, we will save into this.openIdAuthSession and set the redirect uri to /auth/openid/mobile-redirect
// where we will handle the redirect to it
if (!req.query.redirect_uri || !isValidRedirectUri(req.query.redirect_uri)) {
Logger.debug(`[Auth] Invalid redirect_uri=${req.query.redirect_uri}`)
return res.status(400).send('Invalid redirect_uri')
}
// We cannot save the supplied redirect_uri in the session, because it the mobile client uses browser instead of the API
// for the request to mobile-redirect and as such the session is not shared
this.openIdAuthSession.set(state, { mobile_redirect_uri: req.query.redirect_uri })
redirectUri = new URL('/auth/openid/mobile-redirect', hostUrl).toString()
} else { } else {
// If no code_challenge is provided, assume a web application flow and generate one redirectUri = new URL('/auth/openid/callback', hostUrl).toString()
const code_verifier = OpenIDClient.generators.codeVerifier()
code_challenge = OpenIDClient.generators.codeChallenge(code_verifier)
code_challenge_method = 'S256'
// Store the code_verifier in the session for later use in the token exchange if (req.query.state) {
req.session[sessionKey] = { ...req.session[sessionKey], code_verifier } Logger.debug(`[Auth] Invalid state - not allowed on web openid flow`)
return res.status(400).send('Invalid state, not allowed on web flow')
} }
}
oidcStrategy._params.redirect_uri = redirectUri
Logger.debug(`[Auth] OIDC redirect_uri=${redirectUri}`)
const params = { let { code_challenge, code_challenge_method, code_verifier } = generatePkce(req, isMobileFlow)
state: OpenIDClient.generators.random(),
// Other params by the passport strategy
...oidcStrategy._params
}
if (!params.nonce && params.response_type.includes('id_token')) {
params.nonce = OpenIDClient.generators.random()
}
req.session[sessionKey] = { req.session[sessionKey] = {
...req.session[sessionKey], ...req.session[sessionKey],
...pick(params, 'nonce', 'state', 'max_age', 'response_type'), state: state,
max_age: oidcStrategy._params.max_age,
response_type: 'code',
code_verifier: code_verifier, // not null if web flow
mobile: req.query.redirect_uri, // Used in the abs callback later, set mobile if redirect_uri is filled out mobile: req.query.redirect_uri, // Used in the abs callback later, set mobile if redirect_uri is filled out
sso_redirect_uri: oidcStrategy._params.redirect_uri // Save the redirect_uri (for the SSO Provider) for the callback sso_redirect_uri: oidcStrategy._params.redirect_uri // Save the redirect_uri (for the SSO Provider) for the callback
} }
// We cannot save redirect_uri in the session, because it the mobile client uses browser instead of the API
// for the request to mobile-redirect and as such the session is not shared
this.openIdAuthSession.set(params.state, { mobile_redirect_uri: mobile_redirect_uri })
// Now get the URL to direct to
const authorizationUrl = client.authorizationUrl({ const authorizationUrl = client.authorizationUrl({
...params, ...oidcStrategy._params,
scope: 'openid profile email', state: state,
response_type: 'code', response_type: 'code',
code_challenge, code_challenge,
code_challenge_method code_challenge_method
}) })
// params (isRest, callback) to a cookie that will be send to the client this.paramsToCookies(req, res, isMobileFlow ? 'openid-mobile' : 'openid')
this.paramsToCookies(req, res)
// Redirect the user agent (browser) to the authorization URL
res.redirect(authorizationUrl) res.redirect(authorizationUrl)
} catch (error) { } catch (error) {
Logger.error(`[Auth] Error in /auth/openid route: ${error}`) Logger.error(`[Auth] Error in /auth/openid route: ${error}`)
res.status(500).send('Internal Server Error') res.status(500).send('Internal Server Error')
} }
function generatePkce(req, isMobileFlow) {
if (isMobileFlow) {
if (!req.query.code_challenge) {
throw new Error('code_challenge required for mobile flow (PKCE)')
}
if (req.query.code_challenge_method && req.query.code_challenge_method !== 'S256') {
throw new Error('Only S256 code_challenge_method method supported')
}
return {
code_challenge: req.query.code_challenge,
code_challenge_method: req.query.code_challenge_method || 'S256'
}
} else {
const code_verifier = OpenIDClient.generators.codeVerifier()
const code_challenge = OpenIDClient.generators.codeChallenge(code_verifier)
return { code_challenge, code_challenge_method: 'S256', code_verifier }
}
}
function isValidRedirectUri(uri) {
// Check if the redirect_uri is in the whitelist
return Database.serverSettings.authOpenIDMobileRedirectURIs.includes(uri) ||
(Database.serverSettings.authOpenIDMobileRedirectURIs.length === 1 && Database.serverSettings.authOpenIDMobileRedirectURIs[0] === '*')
}
}) })
// This will be the oauth2 callback route for mobile clients // This will be the oauth2 callback route for mobile clients
@ -453,6 +455,12 @@ class Auth {
if (loginError) { if (loginError) {
return handleAuthError(isMobile, 500, 'Error during login', `[Auth] Error in openid callback: ${loginError}`) return handleAuthError(isMobile, 500, 'Error during login', `[Auth] Error in openid callback: ${loginError}`)
} }
// The id_token does not provide access to the user, but is used to identify the user to the SSO provider
// instead it containts a JWT with userinfo like user email, username, etc.
// the client will get to know it anyway in the logout url according to the oauth2 spec
// so it is safe to send it to the client, but we use strict settings
res.cookie('openid_id_token', user.openid_id_token, { maxAge: 1000 * 60 * 60 * 24 * 365 * 10, httpOnly: true, secure: true, sameSite: 'Strict' })
next() next()
}) })
} }
@ -520,9 +528,48 @@ class Auth {
req.logout((err) => { req.logout((err) => {
if (err) { if (err) {
res.sendStatus(500) res.sendStatus(500)
} else {
const authMethod = req.cookies.auth_method
res.clearCookie('auth_method')
if (authMethod === 'openid' || authMethod === 'openid-mobile') {
// If we are using openid, we need to redirect to the logout endpoint
// node-openid-client does not support doing it over passport
const oidcStrategy = passport._strategy('openid-client')
const client = oidcStrategy._client
let postLogoutRedirectUri = null
if (authMethod === 'openid') {
const protocol = (req.secure || req.get('x-forwarded-proto') === 'https') ? 'https' : 'http'
const host = req.get('host')
// TODO: ABS does currently not support subfolders for installation
// If we want to support it we need to include a config for the serverurl
postLogoutRedirectUri = `${protocol}://${host}/login`
}
// else for openid-mobile we keep postLogoutRedirectUri on null
// nice would be to redirect to the app here, but for example Authentik does not implement
// the post_logout_redirect_uri parameter at all and for other providers
// we would also need again to implement (and even before get to know somehow for 3rd party apps)
// the correct app link like audiobookshelf://login (and maybe also provide a redirect like mobile-redirect).
// Instead because its null (and this way the parameter will be omitted completly), the client/app can simply append something like
// &post_logout_redirect_uri=audiobookshelf://login to the received logout url by itself which is the simplest solution
// (The URL needs to be whitelisted in the config of the SSO/ID provider)
const logoutUrl = client.endSessionUrl({
id_token_hint: req.cookies.openid_id_token,
post_logout_redirect_uri: postLogoutRedirectUri
})
res.clearCookie('openid_id_token')
// Tell the user agent (browser) to redirect to the authentification provider's logout URL
res.send({ redirect_url: logoutUrl })
} else { } else {
res.sendStatus(200) res.sendStatus(200)
} }
}
}) })
}) })
} }
@ -612,7 +659,7 @@ class Auth {
* Checks if a username and password tuple is valid and the user active. * Checks if a username and password tuple is valid and the user active.
* @param {string} username * @param {string} username
* @param {string} password * @param {string} password
* @param {function} done * @param {Promise<function>} done
*/ */
async localAuthCheckUserPw(username, password, done) { async localAuthCheckUserPw(username, password, done) {
// Load the user given it's username // Load the user given it's username
@ -654,7 +701,7 @@ class Auth {
/** /**
* Hashes a password with bcrypt. * Hashes a password with bcrypt.
* @param {string} password * @param {string} password
* @returns {string} hash * @returns {Promise<string>} hash
*/ */
hashPass(password) { hashPass(password) {
return new Promise((resolve) => { return new Promise((resolve) => {
@ -688,8 +735,8 @@ class Auth {
/** /**
* *
* @param {string} password * @param {string} password
* @param {*} user * @param {import('./models/User')} user
* @returns {boolean} * @returns {Promise<boolean>}
*/ */
comparePassword(password, user) { comparePassword(password, user) {
if (user.type === 'root' && !password && !user.pash) return true if (user.type === 'root' && !password && !user.pash) return true

View File

@ -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) {

View File

@ -3,13 +3,17 @@ const { LogLevel } = require('./utils/constants')
class Logger { class Logger {
constructor() { constructor() {
/** @type {import('./managers/LogManager')} */
this.logManager = null
this.isDev = process.env.NODE_ENV !== 'production' this.isDev = process.env.NODE_ENV !== 'production'
this.logLevel = !this.isDev ? LogLevel.INFO : LogLevel.TRACE this.logLevel = !this.isDev ? LogLevel.INFO : LogLevel.TRACE
this.socketListeners = [] this.socketListeners = []
this.logManager = null
} }
/**
* @returns {string}
*/
get timestamp() { get timestamp() {
return date.format(new Date(), 'YYYY-MM-DD HH:mm:ss.SSS') return date.format(new Date(), 'YYYY-MM-DD HH:mm:ss.SSS')
} }
@ -23,6 +27,9 @@ class Logger {
return 'UNKNOWN' return 'UNKNOWN'
} }
/**
* @returns {string}
*/
get source() { get source() {
try { try {
throw new Error() throw new Error()
@ -62,24 +69,32 @@ class Logger {
this.socketListeners = this.socketListeners.filter(s => s.id !== socketId) this.socketListeners = this.socketListeners.filter(s => s.id !== socketId)
} }
handleLog(level, args) { /**
*
* @param {number} level
* @param {string[]} args
* @param {string} src
*/
async handleLog(level, args, src) {
const logObj = { const logObj = {
timestamp: this.timestamp, timestamp: this.timestamp,
source: this.source, source: src,
message: args.join(' '), message: args.join(' '),
levelName: this.getLogLevelString(level), levelName: this.getLogLevelString(level),
level level
} }
if (level >= this.logLevel && this.logManager) { // Emit log to sockets that are listening to log events
this.logManager.logToFile(logObj)
}
this.socketListeners.forEach((socketListener) => { this.socketListeners.forEach((socketListener) => {
if (socketListener.level <= level) { if (socketListener.level <= level) {
socketListener.socket.emit('log', logObj) socketListener.socket.emit('log', logObj)
} }
}) })
// Save log to file
if (level >= this.logLevel) {
await this.logManager.logToFile(logObj)
}
} }
setLogLevel(level) { setLogLevel(level) {
@ -90,41 +105,47 @@ class Logger {
trace(...args) { trace(...args) {
if (this.logLevel > LogLevel.TRACE) return if (this.logLevel > LogLevel.TRACE) return
console.trace(`[${this.timestamp}] TRACE:`, ...args) console.trace(`[${this.timestamp}] TRACE:`, ...args)
this.handleLog(LogLevel.TRACE, args) this.handleLog(LogLevel.TRACE, args, this.source)
} }
debug(...args) { debug(...args) {
if (this.logLevel > LogLevel.DEBUG) return if (this.logLevel > LogLevel.DEBUG) return
console.debug(`[${this.timestamp}] DEBUG:`, ...args, `(${this.source})`) console.debug(`[${this.timestamp}] DEBUG:`, ...args, `(${this.source})`)
this.handleLog(LogLevel.DEBUG, args) this.handleLog(LogLevel.DEBUG, args, this.source)
} }
info(...args) { info(...args) {
if (this.logLevel > LogLevel.INFO) return if (this.logLevel > LogLevel.INFO) return
console.info(`[${this.timestamp}] INFO:`, ...args) console.info(`[${this.timestamp}] INFO:`, ...args)
this.handleLog(LogLevel.INFO, args) this.handleLog(LogLevel.INFO, args, this.source)
} }
warn(...args) { warn(...args) {
if (this.logLevel > LogLevel.WARN) return if (this.logLevel > LogLevel.WARN) return
console.warn(`[${this.timestamp}] WARN:`, ...args, `(${this.source})`) console.warn(`[${this.timestamp}] WARN:`, ...args, `(${this.source})`)
this.handleLog(LogLevel.WARN, args) this.handleLog(LogLevel.WARN, args, this.source)
} }
error(...args) { error(...args) {
if (this.logLevel > LogLevel.ERROR) return if (this.logLevel > LogLevel.ERROR) return
console.error(`[${this.timestamp}] ERROR:`, ...args, `(${this.source})`) console.error(`[${this.timestamp}] ERROR:`, ...args, `(${this.source})`)
this.handleLog(LogLevel.ERROR, args) this.handleLog(LogLevel.ERROR, args, this.source)
} }
/**
* Fatal errors are ones that exit the process
* Fatal logs are saved to crash_logs.txt
*
* @param {...any} args
*/
fatal(...args) { fatal(...args) {
console.error(`[${this.timestamp}] FATAL:`, ...args, `(${this.source})`) console.error(`[${this.timestamp}] FATAL:`, ...args, `(${this.source})`)
this.handleLog(LogLevel.FATAL, args) return this.handleLog(LogLevel.FATAL, args, this.source)
} }
note(...args) { note(...args) {
console.log(`[${this.timestamp}] NOTE:`, ...args) console.log(`[${this.timestamp}] NOTE:`, ...args)
this.handleLog(LogLevel.NOTE, args) this.handleLog(LogLevel.NOTE, args, this.source)
} }
} }
module.exports = new Logger() module.exports = new Logger()

View File

@ -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

View File

@ -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)

View 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()

View File

@ -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) {

View File

@ -124,6 +124,11 @@ class LibraryItemController {
const libraryItem = req.libraryItem const libraryItem = req.libraryItem
const mediaPayload = req.body const mediaPayload = req.body
if (mediaPayload.url) {
await LibraryItemController.prototype.uploadCover.bind(this)(req, res, false)
if (res.writableEnded) return
}
// Book specific // Book specific
if (libraryItem.isBook) { if (libraryItem.isBook) {
await this.createAuthorsAndSeriesForItemUpdate(mediaPayload, libraryItem.libraryId) await this.createAuthorsAndSeriesForItemUpdate(mediaPayload, libraryItem.libraryId)
@ -146,7 +151,7 @@ class LibraryItemController {
seriesRemoved = libraryItem.media.metadata.series.filter(se => !seriesIdsInUpdate.includes(se.id)) seriesRemoved = libraryItem.media.metadata.series.filter(se => !seriesIdsInUpdate.includes(se.id))
} }
const hasUpdates = libraryItem.media.update(mediaPayload) const hasUpdates = libraryItem.media.update(mediaPayload) || mediaPayload.url
if (hasUpdates) { if (hasUpdates) {
libraryItem.updatedAt = Date.now() libraryItem.updatedAt = Date.now()
@ -171,7 +176,7 @@ class LibraryItemController {
} }
// POST: api/items/:id/cover // POST: api/items/:id/cover
async uploadCover(req, res) { async uploadCover(req, res, updateAndReturnJson = true) {
if (!req.user.canUpload) { if (!req.user.canUpload) {
Logger.warn('User attempted to upload a cover without permission', req.user) Logger.warn('User attempted to upload a cover without permission', req.user)
return res.sendStatus(403) return res.sendStatus(403)
@ -196,6 +201,7 @@ class LibraryItemController {
return res.status(500).send('Unknown error occurred') return res.status(500).send('Unknown error occurred')
} }
if (updateAndReturnJson) {
await Database.updateLibraryItem(libraryItem) await Database.updateLibraryItem(libraryItem)
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
res.json({ res.json({
@ -203,6 +209,7 @@ class LibraryItemController {
cover: result.cover cover: result.cover
}) })
} }
}
// PATCH: api/items/:id/cover // PATCH: api/items/:id/cover
async updateCover(req, res) { async updateCover(req, res) {
@ -276,6 +283,9 @@ class LibraryItemController {
return res.sendStatus(404) return res.sendStatus(404)
} }
if (req.query.ts)
res.set('Cache-Control', 'private, max-age=86400')
if (raw) { // any value if (raw) { // any value
if (global.XAccel) { if (global.XAccel) {
const encodedURI = encodeUriPath(global.XAccel + libraryItem.media.coverPath) const encodedURI = encodeUriPath(global.XAccel + libraryItem.media.coverPath)

View File

@ -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

View File

@ -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()

View File

@ -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)
} }

View File

@ -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) {

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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
} }

View File

@ -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.

View File

@ -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;

View File

@ -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;

View File

@ -1,3 +1,6 @@
const child_process = require('child_process')
const { promisify } = require('util')
const exec = promisify(child_process.exec)
const path = require('path') const path = require('path')
const which = require('../libs/which') const which = require('../libs/which')
const fs = require('../libs/fsExtra') const fs = require('../libs/fsExtra')
@ -8,67 +11,143 @@ const fileUtils = require('../utils/fileUtils')
class BinaryManager { class BinaryManager {
defaultRequiredBinaries = [ defaultRequiredBinaries = [
{ name: 'ffmpeg', envVariable: 'FFMPEG_PATH' }, { name: 'ffmpeg', envVariable: 'FFMPEG_PATH', validVersions: ['5.1'] },
{ name: 'ffprobe', envVariable: 'FFPROBE_PATH' } { name: 'ffprobe', envVariable: 'FFPROBE_PATH', validVersions: ['5.1'] }
] ]
constructor(requiredBinaries = this.defaultRequiredBinaries) { constructor(requiredBinaries = this.defaultRequiredBinaries) {
this.requiredBinaries = requiredBinaries this.requiredBinaries = requiredBinaries
this.mainInstallPath = process.pkg ? path.dirname(process.execPath) : global.appRoot this.mainInstallPath = process.pkg ? path.dirname(process.execPath) : global.appRoot
this.altInstallPath = global.ConfigPath this.altInstallPath = global.ConfigPath
this.initialized = false
this.exec = exec
} }
async init() { async init() {
if (this.initialized) return if (this.initialized) return
const missingBinaries = await this.findRequiredBinaries() const missingBinaries = await this.findRequiredBinaries()
if (missingBinaries.length == 0) return if (missingBinaries.length == 0) return
await this.removeOldBinaries(missingBinaries)
await this.install(missingBinaries) await this.install(missingBinaries)
const missingBinariesAfterInstall = await this.findRequiredBinaries() const missingBinariesAfterInstall = await this.findRequiredBinaries()
if (missingBinariesAfterInstall.length != 0) { if (missingBinariesAfterInstall.length) {
Logger.error(`[BinaryManager] Failed to find or install required binaries: ${missingBinariesAfterInstall.join(', ')}`) Logger.error(`[BinaryManager] Failed to find or install required binaries: ${missingBinariesAfterInstall.join(', ')}`)
process.exit(1) process.exit(1)
} }
this.initialized = true this.initialized = true
} }
/**
* Remove old/invalid binaries in main or alt install path
*
* @param {string[]} binaryNames
*/
async removeOldBinaries(binaryNames) {
for (const binaryName of binaryNames) {
const executable = this.getExecutableFileName(binaryName)
const mainInstallPath = path.join(this.mainInstallPath, executable)
if (await fs.pathExists(mainInstallPath)) {
Logger.debug(`[BinaryManager] Removing old binary: ${mainInstallPath}`)
await fs.remove(mainInstallPath)
}
const altInstallPath = path.join(this.altInstallPath, executable)
if (await fs.pathExists(altInstallPath)) {
Logger.debug(`[BinaryManager] Removing old binary: ${altInstallPath}`)
await fs.remove(altInstallPath)
}
}
}
/**
* Find required binaries and return array of binary names that are missing
*
* @returns {Promise<string[]>}
*/
async findRequiredBinaries() { async findRequiredBinaries() {
const missingBinaries = [] const missingBinaries = []
for (const binary of this.requiredBinaries) { for (const binary of this.requiredBinaries) {
const binaryPath = await this.findBinary(binary.name, binary.envVariable) const binaryPath = await this.findBinary(binary.name, binary.envVariable, binary.validVersions)
if (binaryPath) { if (binaryPath) {
Logger.info(`[BinaryManager] Found ${binary.name} at ${binaryPath}`) Logger.info(`[BinaryManager] Found valid binary ${binary.name} at ${binaryPath}`)
if (process.env[binary.envVariable] !== binaryPath) { if (process.env[binary.envVariable] !== binaryPath) {
Logger.info(`[BinaryManager] Updating process.env.${binary.envVariable}`) Logger.info(`[BinaryManager] Updating process.env.${binary.envVariable}`)
process.env[binary.envVariable] = binaryPath process.env[binary.envVariable] = binaryPath
} }
} else { } else {
Logger.info(`[BinaryManager] ${binary.name} not found`) Logger.info(`[BinaryManager] ${binary.name} not found or version too old`)
missingBinaries.push(binary.name) missingBinaries.push(binary.name)
} }
} }
return missingBinaries return missingBinaries
} }
async findBinary(name, envVariable) { /**
const executable = name + (process.platform == 'win32' ? '.exe' : '') * Find absolute path for binary
*
* @param {string} name
* @param {string} envVariable
* @param {string[]} [validVersions]
* @returns {Promise<string>} Path to binary
*/
async findBinary(name, envVariable, validVersions = []) {
const executable = this.getExecutableFileName(name)
// 1. check path specified in environment variable
const defaultPath = process.env[envVariable] const defaultPath = process.env[envVariable]
if (defaultPath && await fs.pathExists(defaultPath)) return defaultPath if (await this.isBinaryGood(defaultPath, validVersions)) return defaultPath
// 2. find the first instance of the binary in the PATH environment variable
const whichPath = which.sync(executable, { nothrow: true }) const whichPath = which.sync(executable, { nothrow: true })
if (whichPath) return whichPath if (await this.isBinaryGood(whichPath, validVersions)) return whichPath
// 3. check main install path (binary root dir)
const mainInstallPath = path.join(this.mainInstallPath, executable) const mainInstallPath = path.join(this.mainInstallPath, executable)
if (await fs.pathExists(mainInstallPath)) return mainInstallPath if (await this.isBinaryGood(mainInstallPath, validVersions)) return mainInstallPath
// 4. check alt install path (/config)
const altInstallPath = path.join(this.altInstallPath, executable) const altInstallPath = path.join(this.altInstallPath, executable)
if (await fs.pathExists(altInstallPath)) return altInstallPath if (await this.isBinaryGood(altInstallPath, validVersions)) return altInstallPath
return null return null
} }
/**
* Check binary path exists and optionally check version is valid
*
* @param {string} binaryPath
* @param {string[]} [validVersions]
* @returns {Promise<boolean>}
*/
async isBinaryGood(binaryPath, validVersions = []) {
if (!binaryPath || !await fs.pathExists(binaryPath)) return false
if (!validVersions.length) return true
try {
const { stdout } = await this.exec('"' + binaryPath + '"' + ' -version')
const version = stdout.match(/version\s([\d\.]+)/)?.[1]
if (!version) return false
return validVersions.some(validVersion => version.startsWith(validVersion))
} catch (err) {
Logger.error(`[BinaryManager] Failed to check version of ${binaryPath}`)
return false
}
}
/**
*
* @param {string[]} binaries
*/
async install(binaries) { async install(binaries) {
if (binaries.length == 0) return if (!binaries.length) return
Logger.info(`[BinaryManager] Installing binaries: ${binaries.join(', ')}`) Logger.info(`[BinaryManager] Installing binaries: ${binaries.join(', ')}`)
let destination = await fileUtils.isWritable(this.mainInstallPath) ? this.mainInstallPath : this.altInstallPath let destination = await fileUtils.isWritable(this.mainInstallPath) ? this.mainInstallPath : this.altInstallPath
await ffbinaries.downloadBinaries(binaries, { destination }) await ffbinaries.downloadBinaries(binaries, { destination, version: '5.1', force: true })
Logger.info(`[BinaryManager] Binaries installed to ${destination}`) Logger.info(`[BinaryManager] Binaries installed to ${destination}`)
} }
/**
* Append .exe to binary name for Windows
*
* @param {string} name
* @returns {string}
*/
getExecutableFileName(name) {
return name + (process.platform == 'win32' ? '.exe' : '')
}
} }
module.exports = BinaryManager module.exports = BinaryManager

View File

@ -1,19 +1,34 @@
const Path = require('path') const Path = require('path')
const fs = require('../libs/fsExtra') const fs = require('../libs/fsExtra')
const Logger = require('../Logger')
const DailyLog = require('../objects/DailyLog') const DailyLog = require('../objects/DailyLog')
const Logger = require('../Logger') const { LogLevel } = require('../utils/constants')
const TAG = '[LogManager]' const TAG = '[LogManager]'
/**
* @typedef LogObject
* @property {string} timestamp
* @property {string} source
* @property {string} message
* @property {string} levelName
* @property {number} level
*/
class LogManager { class LogManager {
constructor() { constructor() {
this.DailyLogPath = Path.posix.join(global.MetadataPath, 'logs', 'daily') this.DailyLogPath = Path.posix.join(global.MetadataPath, 'logs', 'daily')
this.ScanLogPath = Path.posix.join(global.MetadataPath, 'logs', 'scans') this.ScanLogPath = Path.posix.join(global.MetadataPath, 'logs', 'scans')
/** @type {DailyLog} */
this.currentDailyLog = null this.currentDailyLog = null
/** @type {LogObject[]} */
this.dailyLogBuffer = [] this.dailyLogBuffer = []
/** @type {string[]} */
this.dailyLogFiles = [] this.dailyLogFiles = []
} }
@ -26,12 +41,12 @@ class LogManager {
await fs.ensureDir(this.ScanLogPath) await fs.ensureDir(this.ScanLogPath)
} }
async ensureScanLogDir() { /**
if (!(await fs.pathExists(this.ScanLogPath))) { * 1. Ensure log directories exist
await fs.mkdir(this.ScanLogPath) * 2. Load daily log files
} * 3. Remove old daily log files
} * 4. Create/set current daily log file
*/
async init() { async init() {
await this.ensureLogDirs() await this.ensureLogDirs()
@ -46,11 +61,11 @@ class LogManager {
} }
} }
// set current daily log file or create if does not exist
const currentDailyLogFilename = DailyLog.getCurrentDailyLogFilename() const currentDailyLogFilename = DailyLog.getCurrentDailyLogFilename()
Logger.info(TAG, `Init current daily log filename: ${currentDailyLogFilename}`) Logger.info(TAG, `Init current daily log filename: ${currentDailyLogFilename}`)
this.currentDailyLog = new DailyLog() this.currentDailyLog = new DailyLog(this.DailyLogPath)
this.currentDailyLog.setData({ dailyLogDirPath: this.DailyLogPath })
if (this.dailyLogFiles.includes(currentDailyLogFilename)) { if (this.dailyLogFiles.includes(currentDailyLogFilename)) {
Logger.debug(TAG, `Daily log file already exists - set in Logger`) Logger.debug(TAG, `Daily log file already exists - set in Logger`)
@ -59,7 +74,7 @@ class LogManager {
this.dailyLogFiles.push(this.currentDailyLog.filename) this.dailyLogFiles.push(this.currentDailyLog.filename)
} }
// Log buffered Logs // Log buffered daily logs
if (this.dailyLogBuffer.length) { if (this.dailyLogBuffer.length) {
this.dailyLogBuffer.forEach((logObj) => { this.dailyLogBuffer.forEach((logObj) => {
this.currentDailyLog.appendLog(logObj) this.currentDailyLog.appendLog(logObj)
@ -68,9 +83,12 @@ class LogManager {
} }
} }
/**
* Load all daily log filenames in /metadata/logs/daily
*/
async scanLogFiles() { async scanLogFiles() {
const dailyFiles = await fs.readdir(this.DailyLogPath) const dailyFiles = await fs.readdir(this.DailyLogPath)
if (dailyFiles && dailyFiles.length) { if (dailyFiles?.length) {
dailyFiles.forEach((logFile) => { dailyFiles.forEach((logFile) => {
if (Path.extname(logFile) === '.txt') { if (Path.extname(logFile) === '.txt') {
Logger.debug('Daily Log file found', logFile) Logger.debug('Daily Log file found', logFile)
@ -83,30 +101,38 @@ class LogManager {
this.dailyLogFiles.sort() this.dailyLogFiles.sort()
} }
async removeOldestLog() { /**
if (!this.dailyLogFiles.length) return *
const oldestLog = this.dailyLogFiles[0] * @param {string} filename
return this.removeLogFile(oldestLog) */
}
async removeLogFile(filename) { async removeLogFile(filename) {
const fullPath = Path.join(this.DailyLogPath, filename) const fullPath = Path.join(this.DailyLogPath, filename)
const exists = await fs.pathExists(fullPath) const exists = await fs.pathExists(fullPath)
if (!exists) { if (!exists) {
Logger.error(TAG, 'Invalid log dne ' + fullPath) Logger.error(TAG, 'Invalid log dne ' + fullPath)
this.dailyLogFiles = this.dailyLogFiles.filter(dlf => dlf.filename !== filename) this.dailyLogFiles = this.dailyLogFiles.filter(dlf => dlf !== filename)
} else { } else {
try { try {
await fs.unlink(fullPath) await fs.unlink(fullPath)
Logger.info(TAG, 'Removed daily log: ' + filename) Logger.info(TAG, 'Removed daily log: ' + filename)
this.dailyLogFiles = this.dailyLogFiles.filter(dlf => dlf.filename !== filename) this.dailyLogFiles = this.dailyLogFiles.filter(dlf => dlf !== filename)
} catch (error) { } catch (error) {
Logger.error(TAG, 'Failed to unlink log file ' + fullPath) Logger.error(TAG, 'Failed to unlink log file ' + fullPath)
} }
} }
} }
logToFile(logObj) { /**
*
* @param {LogObject} logObj
*/
async logToFile(logObj) {
// Fatal crashes get logged to a separate file
if (logObj.level === LogLevel.FATAL) {
await this.logCrashToFile(logObj)
}
// Buffer when logging before daily logs have been initialized
if (!this.currentDailyLog) { if (!this.currentDailyLog) {
this.dailyLogBuffer.push(logObj) this.dailyLogBuffer.push(logObj)
return return
@ -114,25 +140,39 @@ class LogManager {
// Check log rolls to next day // Check log rolls to next day
if (this.currentDailyLog.id !== DailyLog.getCurrentDateString()) { if (this.currentDailyLog.id !== DailyLog.getCurrentDateString()) {
const newDailyLog = new DailyLog() this.currentDailyLog = new DailyLog(this.DailyLogPath)
newDailyLog.setData({ dailyLogDirPath: this.DailyLogPath })
this.currentDailyLog = newDailyLog
if (this.dailyLogFiles.length > this.loggerDailyLogsToKeep) { if (this.dailyLogFiles.length > this.loggerDailyLogsToKeep) {
this.removeOldestLog() // Remove oldest log
this.removeLogFile(this.dailyLogFiles[0])
} }
} }
// Append log line to log file // Append log line to log file
this.currentDailyLog.appendLog(logObj) return this.currentDailyLog.appendLog(logObj)
} }
socketRequestDailyLogs(socket) { /**
if (!this.currentDailyLog) { *
return * @param {LogObject} logObj
*/
async logCrashToFile(logObj) {
const line = JSON.stringify(logObj) + '\n'
const logsDir = Path.join(global.MetadataPath, 'logs')
await fs.ensureDir(logsDir)
const crashLogPath = Path.join(logsDir, 'crash_logs.txt')
return fs.writeFile(crashLogPath, line, { flag: "a+" }).catch((error) => {
console.log('[LogManager] Appended crash log', error)
})
} }
const lastLogs = this.currentDailyLog.logs.slice(-5000) /**
socket.emit('daily_logs', lastLogs) * Most recent 5000 daily logs
*
* @returns {string}
*/
getMostRecentCurrentDailyLogs() {
return this.currentDailyLog?.logs.slice(-5000) || ''
} }
} }
module.exports = LogManager module.exports = LogManager

View 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

View File

@ -225,6 +225,12 @@ class LibraryItem extends Model {
return newLibraryItem return newLibraryItem
} }
/**
* Updates libraryItem, book, authors and series from old library item
*
* @param {oldLibraryItem} oldLibraryItem
* @returns {Promise<boolean>} true if updates were made
*/
static async fullUpdateFromOld(oldLibraryItem) { static async fullUpdateFromOld(oldLibraryItem) {
const libraryItemExpanded = await this.findByPk(oldLibraryItem.id, { const libraryItemExpanded = await this.findByPk(oldLibraryItem.id, {
include: [ include: [
@ -306,17 +312,18 @@ class LibraryItem extends Model {
const existingAuthors = libraryItemExpanded.media.authors || [] const existingAuthors = libraryItemExpanded.media.authors || []
const existingSeriesAll = libraryItemExpanded.media.series || [] const existingSeriesAll = libraryItemExpanded.media.series || []
const updatedAuthors = oldLibraryItem.media.metadata.authors || [] const updatedAuthors = oldLibraryItem.media.metadata.authors || []
const uniqueUpdatedAuthors = updatedAuthors.filter((au, idx) => updatedAuthors.findIndex(a => a.id === au.id) === idx)
const updatedSeriesAll = oldLibraryItem.media.metadata.series || [] const updatedSeriesAll = oldLibraryItem.media.metadata.series || []
for (const existingAuthor of existingAuthors) { for (const existingAuthor of existingAuthors) {
// Author was removed from Book // Author was removed from Book
if (!updatedAuthors.some(au => au.id === existingAuthor.id)) { if (!uniqueUpdatedAuthors.some(au => au.id === existingAuthor.id)) {
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" author "${existingAuthor.name}" was removed`) Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" author "${existingAuthor.name}" was removed`)
await this.sequelize.models.bookAuthor.removeByIds(existingAuthor.id, libraryItemExpanded.media.id) await this.sequelize.models.bookAuthor.removeByIds(existingAuthor.id, libraryItemExpanded.media.id)
hasUpdates = true hasUpdates = true
} }
} }
for (const updatedAuthor of updatedAuthors) { for (const updatedAuthor of uniqueUpdatedAuthors) {
// Author was added // Author was added
if (!existingAuthors.some(au => au.id === updatedAuthor.id)) { if (!existingAuthors.some(au => au.id === updatedAuthor.id)) {
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" author "${updatedAuthor.name}" was added`) Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" author "${updatedAuthor.name}" was added`)
@ -372,6 +379,9 @@ class LibraryItem extends Model {
if (!areEquivalent(updatedLibraryItem[key], existingValue, true)) { if (!areEquivalent(updatedLibraryItem[key], existingValue, true)) {
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" ${key} updated from ${existingValue} to ${updatedLibraryItem[key]}`) Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" ${key} updated from ${existingValue} to ${updatedLibraryItem[key]}`)
hasLibraryItemUpdates = true hasLibraryItemUpdates = true
if (key === 'updatedAt') {
libraryItemExpanded.changed('updatedAt', true)
}
} }
} }
if (hasLibraryItemUpdates) { if (hasLibraryItemUpdates) {
@ -399,6 +409,7 @@ class LibraryItem extends Model {
isInvalid: !!oldLibraryItem.isInvalid, isInvalid: !!oldLibraryItem.isInvalid,
mtime: oldLibraryItem.mtimeMs, mtime: oldLibraryItem.mtimeMs,
ctime: oldLibraryItem.ctimeMs, ctime: oldLibraryItem.ctimeMs,
updatedAt: oldLibraryItem.updatedAt,
birthtime: oldLibraryItem.birthtimeMs, birthtime: oldLibraryItem.birthtimeMs,
size: oldLibraryItem.size, size: oldLibraryItem.size,
lastScan: oldLibraryItem.lastScan, lastScan: oldLibraryItem.lastScan,

View File

@ -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
}) })
} }

View File

@ -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')
} }

View File

@ -42,7 +42,13 @@ class Podcast {
this.autoDownloadSchedule = podcast.autoDownloadSchedule || '0 * * * *' // Added in 2.1.3 so default to hourly this.autoDownloadSchedule = podcast.autoDownloadSchedule || '0 * * * *' // Added in 2.1.3 so default to hourly
this.lastEpisodeCheck = podcast.lastEpisodeCheck || 0 this.lastEpisodeCheck = podcast.lastEpisodeCheck || 0
this.maxEpisodesToKeep = podcast.maxEpisodesToKeep || 0 this.maxEpisodesToKeep = podcast.maxEpisodesToKeep || 0
this.maxNewEpisodesToDownload = podcast.maxNewEpisodesToDownload || 3
// Default is 3 but 0 is allowed
if (typeof podcast.maxNewEpisodesToDownload !== 'number') {
this.maxNewEpisodesToDownload = 3
} else {
this.maxNewEpisodesToDownload = podcast.maxNewEpisodesToDownload
}
} }
toJSON() { toJSON() {

View File

@ -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
} }
} }

View File

@ -113,7 +113,7 @@ class ServerSettings {
this.version = settings.version || null this.version = settings.version || null
this.buildNumber = settings.buildNumber || 0 // Added v2.4.5 this.buildNumber = settings.buildNumber || 0 // Added v2.4.5
this.authLoginCustomMessage = settings.authLoginCustomMessage || null // Added v2.7.3 this.authLoginCustomMessage = settings.authLoginCustomMessage || null // Added v2.8.0
this.authActiveAuthMethods = settings.authActiveAuthMethods || ['local'] this.authActiveAuthMethods = settings.authActiveAuthMethods || ['local']
this.authOpenIDIssuerURL = settings.authOpenIDIssuerURL || null this.authOpenIDIssuerURL = settings.authOpenIDIssuerURL || null

View File

@ -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

View File

@ -2,15 +2,30 @@ const axios = require('axios')
const { levenshteinDistance } = require('../utils/index') const { levenshteinDistance } = require('../utils/index')
const Logger = require('../Logger') const Logger = require('../Logger')
/**
* @typedef AuthorSearchObj
* @property {string} asin
* @property {string} description
* @property {string} image
* @property {string} name
*/
class Audnexus { class Audnexus {
constructor() { constructor() {
this.baseUrl = 'https://api.audnex.us' this.baseUrl = 'https://api.audnex.us'
} }
/**
*
* @param {string} name
* @param {string} region
* @returns {Promise<{asin:string, name:string}[]>}
*/
authorASINsRequest(name, region) { authorASINsRequest(name, region) {
name = encodeURIComponent(name) const searchParams = new URLSearchParams()
const regionQuery = region ? `&region=${region}` : '' searchParams.set('name', name)
const authorRequestUrl = `${this.baseUrl}/authors?name=${name}${regionQuery}` if (region) searchParams.set('region', region)
const authorRequestUrl = `${this.baseUrl}/authors?${searchParams.toString()}`
Logger.info(`[Audnexus] Searching for author "${authorRequestUrl}"`) Logger.info(`[Audnexus] Searching for author "${authorRequestUrl}"`)
return axios.get(authorRequestUrl).then((res) => { return axios.get(authorRequestUrl).then((res) => {
return res.data || [] return res.data || []
@ -20,6 +35,12 @@ class Audnexus {
}) })
} }
/**
*
* @param {string} asin
* @param {string} region
* @returns {Promise<AuthorSearchObj>}
*/
authorRequest(asin, region) { authorRequest(asin, region) {
asin = encodeURIComponent(asin) asin = encodeURIComponent(asin)
const regionQuery = region ? `?region=${region}` : '' const regionQuery = region ? `?region=${region}` : ''
@ -33,6 +54,12 @@ class Audnexus {
}) })
} }
/**
*
* @param {string} asin
* @param {string} region
* @returns {Promise<AuthorSearchObj>}
*/
async findAuthorByASIN(asin, region) { async findAuthorByASIN(asin, region) {
const author = await this.authorRequest(asin, region) const author = await this.authorRequest(asin, region)
if (!author) { if (!author) {
@ -46,14 +73,28 @@ class Audnexus {
} }
} }
/**
*
* @param {string} name
* @param {string} region
* @param {number} maxLevenshtein
* @returns {Promise<AuthorSearchObj>}
*/
async findAuthorByName(name, region, maxLevenshtein = 3) { async findAuthorByName(name, region, maxLevenshtein = 3) {
Logger.debug(`[Audnexus] Looking up author by name ${name}`) Logger.debug(`[Audnexus] Looking up author by name ${name}`)
const asins = await this.authorASINsRequest(name, region) const authorAsinObjs = await this.authorASINsRequest(name, region)
const matchingAsin = asins.find(obj => levenshteinDistance(obj.name, name) <= maxLevenshtein)
if (!matchingAsin) { let closestMatch = null
authorAsinObjs.forEach((authorAsinObj) => {
authorAsinObj.levenshteinDistance = levenshteinDistance(authorAsinObj.name, name)
if (!closestMatch || closestMatch.levenshteinDistance > authorAsinObj.levenshteinDistance) {
closestMatch = authorAsinObj
}
})
if (!closestMatch || closestMatch.levenshteinDistance > maxLevenshtein) {
return null return null
} }
const author = await this.authorRequest(matchingAsin.asin) const author = await this.authorRequest(closestMatch.asin)
if (!author) { if (!author) {
return null return null
} }

View 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

View File

@ -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))

View File

@ -28,6 +28,7 @@ const SearchController = require('../controllers/SearchController')
const CacheController = require('../controllers/CacheController') const CacheController = require('../controllers/CacheController')
const ToolsController = require('../controllers/ToolsController') const ToolsController = require('../controllers/ToolsController')
const RSSFeedController = require('../controllers/RSSFeedController') const RSSFeedController = require('../controllers/RSSFeedController')
const CustomMetadataProviderController = require('../controllers/CustomMetadataProviderController')
const MiscController = require('../controllers/MiscController') const MiscController = require('../controllers/MiscController')
const Author = require('../objects/entities/Author') const Author = require('../objects/entities/Author')
@ -129,7 +130,7 @@ class ApiRouter {
this.router.get('/users/:id', UserController.middleware.bind(this), UserController.findOne.bind(this)) this.router.get('/users/:id', UserController.middleware.bind(this), UserController.findOne.bind(this))
this.router.patch('/users/:id', UserController.middleware.bind(this), UserController.update.bind(this)) this.router.patch('/users/:id', UserController.middleware.bind(this), UserController.update.bind(this))
this.router.delete('/users/:id', UserController.middleware.bind(this), UserController.delete.bind(this)) this.router.delete('/users/:id', UserController.middleware.bind(this), UserController.delete.bind(this))
this.router.patch('/users/:id/openid-unlink', UserController.middleware.bind(this), UserController.unlinkFromOpenID.bind(this))
this.router.get('/users/:id/listening-sessions', UserController.middleware.bind(this), UserController.getListeningSessions.bind(this)) this.router.get('/users/:id/listening-sessions', UserController.middleware.bind(this), UserController.getListeningSessions.bind(this))
this.router.get('/users/:id/listening-stats', UserController.middleware.bind(this), UserController.getListeningStats.bind(this)) this.router.get('/users/:id/listening-stats', UserController.middleware.bind(this), UserController.getListeningStats.bind(this))
@ -299,6 +300,14 @@ class ApiRouter {
this.router.post('/feeds/series/:seriesId/open', RSSFeedController.middleware.bind(this), RSSFeedController.openRSSFeedForSeries.bind(this)) this.router.post('/feeds/series/:seriesId/open', RSSFeedController.middleware.bind(this), RSSFeedController.openRSSFeedForSeries.bind(this))
this.router.post('/feeds/:id/close', RSSFeedController.middleware.bind(this), RSSFeedController.closeRSSFeed.bind(this)) this.router.post('/feeds/:id/close', RSSFeedController.middleware.bind(this), RSSFeedController.closeRSSFeed.bind(this))
//
// Custom Metadata Provider routes
//
this.router.get('/custom-metadata-providers', CustomMetadataProviderController.middleware.bind(this), CustomMetadataProviderController.getAll.bind(this))
this.router.post('/custom-metadata-providers', CustomMetadataProviderController.middleware.bind(this), CustomMetadataProviderController.create.bind(this))
this.router.delete('/custom-metadata-providers/:id', CustomMetadataProviderController.middleware.bind(this), CustomMetadataProviderController.delete.bind(this))
// //
// Misc Routes // Misc Routes
// //
@ -318,6 +327,7 @@ class ApiRouter {
this.router.patch('/auth-settings', MiscController.updateAuthSettings.bind(this)) this.router.patch('/auth-settings', MiscController.updateAuthSettings.bind(this))
this.router.post('/watcher/update', MiscController.updateWatchedPath.bind(this)) this.router.post('/watcher/update', MiscController.updateWatchedPath.bind(this))
this.router.get('/stats/year/:year', MiscController.getAdminStatsForYear.bind(this)) this.router.get('/stats/year/:year', MiscController.getAdminStatsForYear.bind(this))
this.router.get('/logger-data', MiscController.getLoggerData.bind(this))
} }
// //

Some files were not shown because too many files have changed in this diff Show More