chore: merge master

This commit is contained in:
jfrazx 2024-01-13 11:46:38 -08:00
commit 79c34d0638
No known key found for this signature in database
GPG Key ID: 7E72C3BCC0F85A7B
217 changed files with 15818 additions and 7758 deletions

View File

@ -11,7 +11,7 @@ body:
value: "### Mobile app issues report [here](https://github.com/advplyr/audiobookshelf-app/issues/new/choose)." value: "### Mobile app issues report [here](https://github.com/advplyr/audiobookshelf-app/issues/new/choose)."
- type: markdown - type: markdown
attributes: attributes:
value: "### Join the [discord server](https://discord.gg/pJsjuNCKRq) for questions or if you are not sure about a bug." value: "### Join the [discord server](https://discord.gg/HQgCbd6E75) for questions or if you are not sure about a bug."
- type: markdown - type: markdown
attributes: attributes:
value: "## Be as descriptive as you can. Include screenshots, error logs, browser, file types, everything you can think of that might be relevant." value: "## Be as descriptive as you can. Include screenshots, error logs, browser, file types, everything you can think of that might be relevant."

View File

@ -1,7 +1,7 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: Discord - name: Discord
url: https://discord.gg/pJsjuNCKRq url: https://discord.gg/HQgCbd6E75
about: Ask questions, get help troubleshooting, and join the Abs community here. about: Ask questions, get help troubleshooting, and join the Abs community here.
- name: Matrix - name: Matrix
url: https://matrix.to/#/#audiobookshelf:matrix.org url: https://matrix.to/#/#audiobookshelf:matrix.org

5
.gitignore vendored
View File

@ -7,11 +7,14 @@
/podcasts/ /podcasts/
/media/ /media/
/metadata/ /metadata/
test/
/client/.nuxt/ /client/.nuxt/
/client/dist/ /client/dist/
/dist/ /dist/
/deploy/ /deploy/
/coverage/
/.nyc_output/
/ffmpeg*
/ffprobe*
sw.* sw.*
.DS_STORE .DS_STORE

View File

@ -16,5 +16,6 @@
}, },
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.detectIndentation": true, "editor.detectIndentation": true,
"editor.tabSize": 2 "editor.tabSize": 2,
"javascript.format.semicolons": "remove"
} }

View File

@ -18,7 +18,8 @@ RUN apk update && \
ffmpeg \ ffmpeg \
make \ make \
python3 \ python3 \
g++ g++ \
tini
COPY --from=tone /usr/local/bin/tone /usr/local/bin/ COPY --from=tone /usr/local/bin/tone /usr/local/bin/
COPY --from=build /client/dist /client/dist COPY --from=build /client/dist /client/dist
@ -31,4 +32,5 @@ RUN apk del make python3 g++
EXPOSE 80 EXPOSE 80
ENTRYPOINT ["tini", "--"]
CMD ["node", "index.js"] CMD ["node", "index.js"]

View File

@ -259,3 +259,23 @@ Bookshelf Label
.no-bars .Vue-Toastification__container.top-right { .no-bars .Vue-Toastification__container.top-right {
padding-top: 8px; padding-top: 8px;
} }
.abs-btn::before {
content: '';
position: absolute;
border-radius: 6px;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0);
transition: all 0.1s ease-in-out;
}
.abs-btn:hover:not(:disabled)::before {
background-color: rgba(255, 255, 255, 0.1);
}
.abs-btn:disabled::before {
background-color: rgba(0, 0, 0, 0.2);
}

View File

@ -186,7 +186,7 @@ export default {
methods: { methods: {
requestBatchQuickEmbed() { requestBatchQuickEmbed() {
const payload = { const payload = {
message: '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?', message: this.$strings.MessageConfirmQuickEmbed,
callback: (confirmed) => { callback: (confirmed) => {
if (confirmed) { if (confirmed) {
this.$axios this.$axios
@ -219,7 +219,7 @@ export default {
}, },
async batchRescan() { async batchRescan() {
const payload = { const payload = {
message: `Are you sure you want to re-scan ${this.selectedMediaItems.length} items?`, message: this.$getString('MessageConfirmReScanLibraryItems', [this.selectedMediaItems.length]),
callback: (confirmed) => { callback: (confirmed) => {
if (confirmed) { if (confirmed) {
this.$axios this.$axios
@ -316,13 +316,15 @@ export default {
}, },
batchDeleteClick() { batchDeleteClick() {
const payload = { const payload = {
message: `This will delete ${this.numMediaItemsSelected} library items from the database and your file system. Are you sure?`, message: this.$getString('MessageConfirmDeleteLibraryItems', [this.numMediaItemsSelected]),
checkboxLabel: 'Delete from file system. Uncheck to only remove from database.', checkboxLabel: this.$strings.LabelDeleteFromFileSystemCheckbox,
yesButtonText: this.$strings.ButtonDelete, yesButtonText: this.$strings.ButtonDelete,
yesButtonColor: 'error', yesButtonColor: 'error',
checkboxDefaultValue: true, checkboxDefaultValue: !Number(localStorage.getItem('softDeleteDefault') || 0),
callback: (confirmed, hardDelete) => { callback: (confirmed, hardDelete) => {
if (confirmed) { if (confirmed) {
localStorage.setItem('softDeleteDefault', hardDelete ? 0 : 1)
this.$store.commit('setProcessingBatch', true) this.$store.commit('setProcessingBatch', true)
this.$axios this.$axios

View File

@ -1,7 +1,7 @@
<template> <template>
<div id="bookshelf" ref="wrapper" class="w-full max-w-full h-full overflow-y-scroll relative"> <div id="bookshelf" ref="wrapper" class="w-full max-w-full h-full overflow-y-scroll relative">
<!-- Cover size widget --> <!-- Cover size widget -->
<widgets-cover-size-widget class="fixed bottom-4 right-4 z-50" /> <widgets-cover-size-widget class="fixed right-4 z-50" :style="{ bottom: streamLibraryItem ? '181px' : '16px' }" />
<div v-if="loaded && !shelves.length && !search" class="w-full flex flex-col items-center justify-center py-12"> <div v-if="loaded && !shelves.length && !search" class="w-full flex flex-col items-center justify-center py-12">
<p class="text-center text-2xl mb-4 py-4">{{ libraryName }} Library is empty!</p> <p class="text-center text-2xl mb-4 py-4">{{ libraryName }} Library is empty!</p>
@ -94,6 +94,9 @@ export default {
}, },
selectedMediaItems() { selectedMediaItems() {
return this.$store.state.globals.selectedMediaItems || [] return this.$store.state.globals.selectedMediaItems || []
},
streamLibraryItem() {
return this.$store.state.streamLibraryItem
} }
}, },
methods: { methods: {
@ -338,9 +341,15 @@ export default {
libraryItemsAdded(libraryItems) { libraryItemsAdded(libraryItems) {
console.log('libraryItems added', libraryItems) console.log('libraryItems added', libraryItems)
const isThisLibrary = !libraryItems.some((li) => li.libraryId !== this.currentLibraryId) const recentlyAddedShelf = this.shelves.find((shelf) => shelf.id === 'recently-added')
if (!this.search && isThisLibrary) { if (!recentlyAddedShelf) return
this.fetchCategories()
// Add new library item to the recently added shelf
for (const libraryItem of libraryItems) {
if (libraryItem.libraryId === this.currentLibraryId && !recentlyAddedShelf.entities.some((ent) => ent.id === libraryItem.id)) {
// Add to front of array
recentlyAddedShelf.entities.unshift(libraryItem)
}
} }
}, },
libraryItemsUpdated(items) { libraryItemsUpdated(items) {

View File

@ -22,6 +22,10 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
</svg> </svg>
</nuxt-link> </nuxt-link>
<nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="flex-grow h-full flex justify-center items-center" :class="isPlaylistsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
<p v-if="isPlaylistsPage || isPodcastLibrary" class="text-sm">{{ $strings.ButtonPlaylists }}</p>
<span v-else class="material-icons-outlined text-lg">queue_music</span>
</nuxt-link>
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="flex-grow h-full flex justify-center items-center" :class="isCollectionsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'"> <nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="flex-grow h-full flex justify-center items-center" :class="isCollectionsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
<p v-if="isCollectionsPage" class="text-sm">{{ $strings.ButtonCollections }}</p> <p v-if="isCollectionsPage" class="text-sm">{{ $strings.ButtonCollections }}</p>
<span v-else class="material-icons-outlined text-lg">collections_bookmark</span> <span v-else class="material-icons-outlined text-lg">collections_bookmark</span>
@ -36,7 +40,7 @@
</svg> </svg>
</nuxt-link> </nuxt-link>
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="flex-grow h-full flex justify-center items-center" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'"> <nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="flex-grow h-full flex justify-center items-center" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
<p class="text-sm">{{ $strings.ButtonSearch }}</p> <p class="text-sm">{{ $strings.ButtonAdd }}</p>
</nuxt-link> </nuxt-link>
</div> </div>
<div id="toolbar" class="absolute top-10 md:top-0 left-0 w-full h-10 md:h-full z-40 flex items-center justify-end md:justify-start px-2 md:px-8"> <div id="toolbar" class="absolute top-10 md:top-0 left-0 w-full h-10 md:h-full z-40 flex items-center justify-end md:justify-start px-2 md:px-8">
@ -293,6 +297,9 @@ export default {
} }
return items return items
},
showPlaylists() {
return this.$store.state.libraries.numUserPlaylists > 0
} }
}, },
methods: { methods: {

View File

@ -14,10 +14,10 @@
</div> </div>
<div class="w-44 h-12 px-4 border-t bg-bg border-black border-opacity-20 fixed left-0 flex flex-col justify-center" :class="wrapperClass" :style="{ bottom: streamLibraryItem ? '160px' : '0px' }"> <div class="w-44 h-12 px-4 border-t bg-bg border-black border-opacity-20 fixed left-0 flex flex-col justify-center" :class="wrapperClass" :style="{ bottom: streamLibraryItem ? '160px' : '0px' }">
<div class="flex justify-between"> <div class="flex items-center justify-between">
<p class="underline font-mono text-sm" @click="clickChangelog">v{{ $config.version }}</p> <button type="button" class="underline font-mono text-sm" @click="clickChangelog">v{{ $config.version }}</button>
<p class="font-mono text-xs text-gray-300 italic">{{ Source }}</p> <p class="text-xs text-gray-300 italic">{{ Source }}</p>
</div> </div>
<a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xs">Latest: {{ latestVersion }}</a> <a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xs">Latest: {{ latestVersion }}</a>
</div> </div>
@ -104,6 +104,11 @@ export default {
id: 'config-rss-feeds', id: 'config-rss-feeds',
title: this.$strings.HeaderRSSFeeds, title: this.$strings.HeaderRSSFeeds,
path: '/config/rss-feeds' path: '/config/rss-feeds'
},
{
id: 'config-authentication',
title: this.$strings.HeaderAuthentication,
path: '/config/authentication'
} }
] ]

View File

@ -21,7 +21,7 @@
</div> </div>
</div> </div>
<widgets-cover-size-widget class="fixed bottom-4 right-4 z-50" /> <widgets-cover-size-widget class="fixed right-4 z-50" :style="{ bottom: streamLibraryItem ? '181px' : '16px' }" />
</div> </div>
</template> </template>
@ -205,6 +205,9 @@ export default {
sizeMultiplier() { sizeMultiplier() {
const baseSize = this.isCoverSquareAspectRatio ? 192 : 120 const baseSize = this.isCoverSquareAspectRatio ? 192 : 120
return this.entityWidth / baseSize return this.entityWidth / baseSize
},
streamLibraryItem() {
return this.$store.state.streamLibraryItem
} }
}, },
methods: { methods: {

View File

@ -3,9 +3,7 @@
<div class="flex items-center mb-2"> <div class="flex items-center mb-2">
<h1 class="text-xl">{{ headerText }}</h1> <h1 class="text-xl">{{ headerText }}</h1>
<div v-if="showAddButton" class="mx-2 w-7 h-7 flex items-center justify-center rounded-full cursor-pointer hover:bg-white hover:bg-opacity-10 text-center" @click="clicked"> <slot name="header-items"></slot>
<button type="button" class="material-icons" :aria-label="$strings.ButtonAdd + ': ' + headerText" style="font-size: 1.4rem">add</button>
</div>
</div> </div>
<p v-if="description" id="settings-description" class="mb-6 text-gray-200" v-html="description" /> <p v-if="description" id="settings-description" class="mb-6 text-gray-200" v-html="description" />
@ -19,14 +17,9 @@ export default {
props: { props: {
headerText: String, headerText: String,
description: String, description: String,
note: String, note: String
showAddButton: Boolean
}, },
methods: { methods: {}
clicked() {
this.$emit('clicked')
}
}
} }
</script> </script>

View File

@ -3,6 +3,7 @@
<!-- ugly little workaround to cover up the shadow overlapping the bookshelf toolbar --> <!-- ugly little workaround to cover up the shadow overlapping the bookshelf toolbar -->
<div v-if="isShowingBookshelfToolbar" class="absolute top-0 -right-4 w-4 bg-bg h-10 pointer-events-none" /> <div v-if="isShowingBookshelfToolbar" class="absolute top-0 -right-4 w-4 bg-bg h-10 pointer-events-none" />
<div id="siderail-buttons-container" :class="{ 'player-open': streamLibraryItem }" class="w-full overflow-y-auto overflow-x-hidden">
<nuxt-link :to="`/library/${currentLibraryId}`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="homePage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> <nuxt-link :to="`/library/${currentLibraryId}`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="homePage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
@ -81,7 +82,7 @@
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> <nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<span class="abs-icons icon-podcast text-xl"></span> <span class="abs-icons icon-podcast text-xl"></span>
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonSearch }}</p> <p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonAdd }}</p>
<div v-show="isPodcastSearchPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> <div v-show="isPodcastSearchPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link> </nuxt-link>
@ -112,8 +113,9 @@
<p class="text-xs font-mono pb-0.5">{{ numIssues }}</p> <p class="text-xs font-mono pb-0.5">{{ numIssues }}</p>
</div> </div>
</nuxt-link> </nuxt-link>
</div>
<div class="w-full h-12 px-1 py-2 border-t border-black border-opacity-20 absolute left-0" :style="{ bottom: streamLibraryItem ? '240px' : '65px' }"> <div class="w-full h-12 px-1 py-2 border-t border-black/20 bg-bg absolute left-0" :style="{ bottom: streamLibraryItem ? '224px' : '65px' }">
<p class="underline font-mono text-xs text-center text-gray-300 leading-3 mb-1" @click="clickChangelog">v{{ $config.version }}</p> <p class="underline font-mono text-xs text-center text-gray-300 leading-3 mb-1" @click="clickChangelog">v{{ $config.version }}</p>
<a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xxs text-center block leading-3">Update</a> <a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xxs text-center block leading-3">Update</a>
<p v-else class="text-xxs text-gray-400 leading-3 text-center italic">{{ Source }}</p> <p v-else class="text-xxs text-gray-400 leading-3 text-center italic">{{ Source }}</p>
@ -235,3 +237,12 @@ export default {
mounted() {} mounted() {}
} }
</script> </script>
<style>
#siderail-buttons-container {
max-height: calc(100vh - 64px - 48px);
}
#siderail-buttons-container.player-open {
max-height: calc(100vh - 64px - 48px - 160px);
}
</style>

View File

@ -1,9 +1,9 @@
<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="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 id="videoDock" /> <div id="videoDock" />
<nuxt-link v-if="!playerHandler.isVideo" :to="`/item/${streamLibraryItem.id}`" 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 :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" />
</nuxt-link> </div>
<div class="flex items-start mb-6 md:mb-0" :class="playerHandler.isVideo ? 'ml-4 pl-96' : isSquareCover ? 'pl-18 sm:pl-24' : 'pl-12 sm:pl-16'"> <div class="flex items-start mb-6 md:mb-0" :class="playerHandler.isVideo ? 'ml-4 pl-96' : isSquareCover ? 'pl-18 sm:pl-24' : 'pl-12 sm:pl-16'">
<div class="min-w-0"> <div class="min-w-0">
<nuxt-link :to="`/item/${streamLibraryItem.id}`" class="hover:underline cursor-pointer text-sm sm:text-lg block truncate"> <nuxt-link :to="`/item/${streamLibraryItem.id}`" class="hover:underline cursor-pointer text-sm sm:text-lg block truncate">
@ -15,7 +15,7 @@
<div v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ podcastAuthor }}</div> <div v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ podcastAuthor }}</div>
<div v-else-if="musicArtists" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ musicArtists }}</div> <div v-else-if="musicArtists" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ musicArtists }}</div>
<div v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base"> <div v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base">
<nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}?library=${libraryId}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">,&nbsp;</span></nuxt-link> <nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">,&nbsp;</span></nuxt-link>
</div> </div>
<div v-else class="text-xs sm:text-base cursor-pointer pl-1 sm:pl-1.5">{{ $strings.LabelUnknown }}</div> <div v-else class="text-xs sm:text-base cursor-pointer pl-1 sm:pl-1.5">{{ $strings.LabelUnknown }}</div>
<widgets-explicit-indicator :explicit="isExplicit"></widgets-explicit-indicator> <widgets-explicit-indicator :explicit="isExplicit"></widgets-explicit-indicator>

View File

@ -1,5 +1,5 @@
<template> <template>
<nuxt-link :to="`/author/${author.id}?library=${currentLibraryId}`"> <nuxt-link :to="`/author/${author.id}`">
<div @mouseover="mouseover" @mouseleave="mouseleave"> <div @mouseover="mouseover" @mouseleave="mouseleave">
<div :style="{ width: width + 'px', height: height + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden"> <div :style="{ width: width + 'px', height: height + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
<!-- Image or placeholder --> <!-- Image or placeholder -->
@ -8,7 +8,7 @@
<!-- Author name & num books overlay --> <!-- Author name & num books overlay -->
<div v-show="!searching && !nameBelow" class="absolute bottom-0 left-0 w-full py-1 bg-black bg-opacity-60 px-2"> <div v-show="!searching && !nameBelow" class="absolute bottom-0 left-0 w-full py-1 bg-black bg-opacity-60 px-2">
<p class="text-center font-semibold truncate" :style="{ fontSize: sizeMultiplier * 0.75 + 'rem' }">{{ name }}</p> <p class="text-center font-semibold truncate" :style="{ fontSize: sizeMultiplier * 0.75 + 'rem' }">{{ name }}</p>
<p class="text-center text-gray-200" :style="{ fontSize: sizeMultiplier * 0.65 + 'rem' }">{{ numBooks }} Book{{ numBooks === 1 ? '' : 's' }}</p> <p class="text-center text-gray-200" :style="{ fontSize: sizeMultiplier * 0.65 + 'rem' }">{{ numBooks }} {{ $strings.LabelBooks }}</p>
</div> </div>
<!-- Search icon btn --> <!-- Search icon btn -->

View File

@ -70,14 +70,14 @@ export default {
return (this.book.duration || 0) * 60 return (this.book.duration || 0) * 60
}, },
bookDurationComparison() { bookDurationComparison() {
if (!this.bookDuration || !this.currentBookDuration) return '' if (!this.book.duration || !this.currentBookDuration) return ''
let differenceInSeconds = this.currentBookDuration - this.bookDuration const currentBookDurationMinutes = Math.floor(this.currentBookDuration / 60)
// Only show seconds on difference if difference is less than an hour let differenceInMinutes = currentBookDurationMinutes - this.book.duration
if (differenceInSeconds < 0) { if (differenceInMinutes < 0) {
differenceInSeconds = Math.abs(differenceInSeconds) differenceInMinutes = Math.abs(differenceInMinutes)
return `(${this.$elapsedPrettyExtended(differenceInSeconds, false, differenceInSeconds < 3600)} shorter)` return `(${this.$elapsedPrettyExtended(differenceInMinutes * 60, false, false)} shorter)`
} else if (differenceInSeconds > 0) { } else if (differenceInMinutes > 0) {
return `(${this.$elapsedPrettyExtended(differenceInSeconds, false, differenceInSeconds < 3600)} longer)` return `(${this.$elapsedPrettyExtended(differenceInMinutes * 60, false, false)} longer)`
} }
return '(exact match)' return '(exact match)'
} }

View File

@ -1,10 +1,8 @@
<template> <template>
<div class="flex items-center px-1 overflow-hidden"> <div class="flex items-center px-1 overflow-hidden">
<div class="w-8 flex items-center justify-center"> <div class="w-8 flex items-center justify-center">
<!-- <div class="text-lg"> -->
<span v-if="isFinished" :class="taskIconStatus" class="material-icons text-base">{{ actionIcon }}</span> <span v-if="isFinished" :class="taskIconStatus" class="material-icons text-base">{{ actionIcon }}</span>
<widgets-loading-spinner v-else /> <widgets-loading-spinner v-else />
<!-- </div> -->
</div> </div>
<div class="flex-grow px-2 taskRunningCardContent"> <div class="flex-grow px-2 taskRunningCardContent">
<p class="truncate text-sm">{{ title }}</p> <p class="truncate text-sm">{{ title }}</p>
@ -12,7 +10,9 @@
<p class="truncate text-xs text-gray-300">{{ description }}</p> <p class="truncate text-xs text-gray-300">{{ description }}</p>
<p v-if="isFailed && failedMessage" class="text-xs truncate text-red-500">{{ failedMessage }}</p> <p v-if="isFailed && failedMessage" class="text-xs truncate text-red-500">{{ failedMessage }}</p>
<p v-else-if="!isFinished && cancelingScan" class="text-xs truncate">Canceling...</p>
</div> </div>
<ui-btn v-if="userIsAdminOrUp && !isFinished && isLibraryScan && !cancelingScan" color="primary" :padding-y="1" :padding-x="1" class="text-xs w-16 max-w-16 truncate mr-1" @click.stop="cancelScan">{{ this.$strings.ButtonCancel }}</ui-btn>
</div> </div>
</template> </template>
@ -25,9 +25,14 @@ export default {
} }
}, },
data() { data() {
return {} return {
cancelingScan: false
}
}, },
computed: { computed: {
userIsAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp']
},
title() { title() {
return this.task.title || 'No Title' return this.task.title || 'No Title'
}, },
@ -76,9 +81,22 @@ export default {
} }
return '' return ''
},
isLibraryScan() {
return this.action === 'library-scan' || this.action === 'library-match-all'
}
},
methods: {
cancelScan() {
const libraryId = this.task?.data?.libraryId
if (!libraryId) {
console.error('No library id in library-scan task', this.task)
return
}
this.cancelingScan = true
this.$root.socket.emit('cancel_scan', libraryId)
} }
}, },
methods: {},
mounted() {} mounted() {}
} }
</script> </script>

View File

@ -15,24 +15,33 @@
<div class="flex my-2 -mx-2"> <div class="flex my-2 -mx-2">
<div class="w-1/2 px-2"> <div class="w-1/2 px-2">
<ui-text-input-with-label v-model="itemData.title" :disabled="processing" :label="$strings.LabelTitle" @input="titleUpdated" /> <ui-text-input-with-label v-model.trim="itemData.title" :disabled="processing" :label="$strings.LabelTitle" @input="titleUpdated" />
</div> </div>
<div class="w-1/2 px-2"> <div class="w-1/2 px-2">
<ui-text-input-with-label v-if="!isPodcast" v-model="itemData.author" :disabled="processing" :label="$strings.LabelAuthor" /> <div v-if="!isPodcast" class="flex items-end">
<ui-text-input-with-label v-model.trim="itemData.author" :disabled="processing" :label="$strings.LabelAuthor" />
<ui-tooltip :text="$strings.LabelUploaderItemFetchMetadataHelp">
<div
class="ml-2 mb-1 w-8 h-8 bg-bg border border-white border-opacity-10 flex items-center justify-center rounded-full hover:bg-primary cursor-pointer"
@click="fetchMetadata">
<span class="text-base text-white text-opacity-80 font-mono material-icons">sync</span>
</div>
</ui-tooltip>
</div>
<div v-else class="w-full"> <div v-else class="w-full">
<p class="px-1 text-sm font-semibold">{{ $strings.LabelDirectory }} <em class="font-normal text-xs pl-2">(auto)</em></p> <p class="px-1 text-sm font-semibold">{{ $strings.LabelDirectory }} <em class="font-normal text-xs pl-2">(auto)</em></p>
<ui-text-input :value="directory" disabled class="w-full font-mono text-xs" style="height: 38px" /> <ui-text-input :value="directory" disabled class="w-full font-mono text-xs" />
</div> </div>
</div> </div>
</div> </div>
<div v-if="!isPodcast" class="flex my-2 -mx-2"> <div v-if="!isPodcast" class="flex my-2 -mx-2">
<div class="w-1/2 px-2"> <div class="w-1/2 px-2">
<ui-text-input-with-label v-model="itemData.series" :disabled="processing" :label="$strings.LabelSeries" note="(optional)" /> <ui-text-input-with-label v-model.trim="itemData.series" :disabled="processing" :label="$strings.LabelSeries" note="(optional)" inputClass="h-10" />
</div> </div>
<div class="w-1/2 px-2"> <div class="w-1/2 px-2">
<div class="w-full"> <div class="w-full">
<p class="px-1 text-sm font-semibold">{{ $strings.LabelDirectory }} <em class="font-normal text-xs pl-2">(auto)</em></p> <label class="px-1 text-sm font-semibold">{{ $strings.LabelDirectory }} <em class="font-normal text-xs pl-2">(auto)</em></label>
<ui-text-input :value="directory" disabled class="w-full font-mono text-xs" style="height: 38px" /> <ui-text-input :value="directory" disabled class="w-full font-mono text-xs h-10" />
</div> </div>
</div> </div>
</div> </div>
@ -48,8 +57,8 @@
<p class="text-base">{{ $strings.MessageUploaderItemFailed }}</p> <p class="text-base">{{ $strings.MessageUploaderItemFailed }}</p>
</widgets-alert> </widgets-alert>
<div v-if="isUploading" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 flex items-center justify-center z-20"> <div v-if="isNonInteractable" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 flex items-center justify-center z-20">
<ui-loading-indicator :text="$strings.MessageUploading" /> <ui-loading-indicator :text="nonInteractionLabel" />
</div> </div>
</div> </div>
</template> </template>
@ -64,7 +73,8 @@ export default {
default: () => { } default: () => { }
}, },
mediaType: String, mediaType: String,
processing: Boolean processing: Boolean,
provider: String
}, },
data() { data() {
return { return {
@ -76,7 +86,8 @@ export default {
error: '', error: '',
isUploading: false, isUploading: false,
uploadFailed: false, uploadFailed: false,
uploadSuccess: false uploadSuccess: false,
isFetchingMetadata: false
} }
}, },
computed: { computed: {
@ -87,12 +98,19 @@ export default {
if (!this.itemData.title) return '' if (!this.itemData.title) return ''
if (this.isPodcast) return this.itemData.title if (this.isPodcast) return this.itemData.title
if (this.itemData.series && this.itemData.author) { const outputPathParts = [this.itemData.author, this.itemData.series, this.itemData.title]
return Path.join(this.itemData.author, this.itemData.series, this.itemData.title) const cleanedOutputPathParts = outputPathParts.filter(Boolean).map(part => this.$sanitizeFilename(part))
} else if (this.itemData.author) {
return Path.join(this.itemData.author, this.itemData.title) return Path.join(...cleanedOutputPathParts)
} else { },
return this.itemData.title isNonInteractable() {
return this.isUploading || this.isFetchingMetadata
},
nonInteractionLabel() {
if (this.isUploading) {
return this.$strings.MessageUploading
} else if (this.isFetchingMetadata) {
return this.$strings.LabelFetchingMetadata
} }
} }
}, },
@ -105,9 +123,42 @@ export default {
titleUpdated() { titleUpdated() {
this.error = '' this.error = ''
}, },
async fetchMetadata() {
if (!this.itemData.title.trim().length) {
return
}
this.isFetchingMetadata = true
this.error = ''
try {
const searchQueryString = new URLSearchParams({
title: this.itemData.title,
author: this.itemData.author,
provider: this.provider
})
const [bestCandidate, ..._rest] = await this.$axios.$get(`/api/search/books?${searchQueryString}`)
if (bestCandidate) {
this.itemData = {
...this.itemData,
title: bestCandidate.title,
author: bestCandidate.author,
series: (bestCandidate.series || [])[0]?.series
}
} else {
this.error = this.$strings.ErrorUploadFetchMetadataNoResults
}
} catch (e) {
console.error('Failed', e)
this.error = this.$strings.ErrorUploadFetchMetadataAPI
} finally {
this.isFetchingMetadata = false
}
},
getData() { getData() {
if (!this.itemData.title) { if (!this.itemData.title) {
this.error = 'Must have a title' this.error = this.$strings.ErrorUploadLacksTitle
return null return null
} }
this.error = '' this.error = ''

View File

@ -8,10 +8,10 @@
<!-- Alternative bookshelf title/author/sort --> <!-- Alternative bookshelf title/author/sort -->
<div v-if="isAlternativeBookshelfView || isAuthorBookshelfView" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }"> <div v-if="isAlternativeBookshelfView || isAuthorBookshelfView" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }">
<div :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }"> <div :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }">
<div class="flex items-center"> <ui-tooltip :text="displayTitle" :disabled="!displayTitleTruncated" direction="bottom" :delayOnShow="500" class="flex items-center">
<span class="truncate">{{ displayTitle }}</span> <p ref="displayTitle" class="truncate">{{ displayTitle }}</p>
<widgets-explicit-indicator :explicit="isExplicit" /> <widgets-explicit-indicator :explicit="isExplicit" />
</div> </ui-tooltip>
</div> </div>
<p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayLineTwo || '&nbsp;' }}</p> <p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayLineTwo || '&nbsp;' }}</p>
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p> <p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
@ -68,7 +68,8 @@
<span class="material-icons" :style="{ fontSize: sizeMultiplier + 'rem' }">edit</span> <span class="material-icons" :style="{ fontSize: sizeMultiplier + 'rem' }">edit</span>
</div> </div>
<div class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-100" :style="{ top: 0.375 * sizeMultiplier + 'rem', left: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="selectBtnClick"> <!-- Radio button -->
<div v-if="!isAuthorBookshelfView" class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-100" :style="{ top: 0.375 * sizeMultiplier + 'rem', left: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="selectBtnClick">
<span class="material-icons" :class="selected ? 'text-yellow-400' : ''" :style="{ fontSize: 1.25 * sizeMultiplier + 'rem' }">{{ selected ? 'radio_button_checked' : 'radio_button_unchecked' }}</span> <span class="material-icons" :class="selected ? 'text-yellow-400' : ''" :style="{ fontSize: 1.25 * sizeMultiplier + 'rem' }">{{ selected ? 'radio_button_checked' : 'radio_button_unchecked' }}</span>
</div> </div>
@ -163,6 +164,7 @@ export default {
imageReady: false, imageReady: false,
selected: false, selected: false,
isSelectionMode: false, isSelectionMode: false,
displayTitleTruncated: false,
showCoverBg: false showCoverBg: false
} }
}, },
@ -641,6 +643,12 @@ export default {
} }
this.libraryItem = libraryItem this.libraryItem = libraryItem
this.$nextTick(() => {
if (this.$refs.displayTitle) {
this.displayTitleTruncated = this.$refs.displayTitle.scrollWidth > this.$refs.displayTitle.clientWidth
}
})
}, },
clickCard(e) { clickCard(e) {
if (this.processing) return if (this.processing) return
@ -843,13 +851,15 @@ export default {
}, },
deleteLibraryItem() { deleteLibraryItem() {
const payload = { const payload = {
message: 'This will delete the library item from the database and your file system. Are you sure?', message: this.$strings.MessageConfirmDeleteLibraryItem,
checkboxLabel: 'Delete from file system. Uncheck to only remove from database.', checkboxLabel: this.$strings.LabelDeleteFromFileSystemCheckbox,
yesButtonText: this.$strings.ButtonDelete, yesButtonText: this.$strings.ButtonDelete,
yesButtonColor: 'error', yesButtonColor: 'error',
checkboxDefaultValue: true, checkboxDefaultValue: !Number(localStorage.getItem('softDeleteDefault') || 0),
callback: (confirmed, hardDelete) => { callback: (confirmed, hardDelete) => {
if (confirmed) { if (confirmed) {
localStorage.setItem('softDeleteDefault', hardDelete ? 0 : 1)
this.processing = true this.processing = true
const axios = this.$axios || this.$nuxt.$axios const axios = this.$axios || this.$nuxt.$axios
axios axios

View File

@ -5,7 +5,8 @@
<div class="absolute cover-bg" ref="coverBg" /> <div class="absolute cover-bg" ref="coverBg" />
</div> </div>
<img v-if="libraryItem" ref="cover" :src="fullCoverUrl" loading="lazy" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0 z-10 duration-300 transition-opacity" :style="{ opacity: imageReady ? '1' : '0' }" :class="showCoverBg ? 'object-contain' : 'object-fill'" /> <img v-if="libraryItem" ref="cover" :src="fullCoverUrl" loading="lazy" draggable="false" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0 z-10 duration-300 transition-opacity" :style="{ opacity: imageReady ? '1' : '0' }" :class="showCoverBg ? 'object-contain' : 'object-fill'" @click="clickCover" />
<div v-show="loading && libraryItem" class="absolute top-0 left-0 h-full w-full flex items-center justify-center"> <div v-show="loading && libraryItem" class="absolute top-0 left-0 h-full w-full flex items-center justify-center">
<p class="text-center" :style="{ fontSize: 0.75 * sizeMultiplier + 'rem' }">{{ title }}</p> <p class="text-center" :style="{ fontSize: 0.75 * sizeMultiplier + 'rem' }">{{ title }}</p>
<div class="absolute top-2 right-2"> <div class="absolute top-2 right-2">
@ -43,6 +44,7 @@ export default {
type: Number, type: Number,
default: 120 default: 120
}, },
expandOnClick: Boolean,
bookCoverAspectRatio: Number bookCoverAspectRatio: Number
}, },
data() { data() {
@ -132,6 +134,11 @@ export default {
} }
}, },
methods: { methods: {
clickCover() {
if (this.expandOnClick && this.libraryItem) {
this.$store.commit('globals/setRawCoverPreviewModal', this.libraryItem.id)
}
},
setCoverBg() { setCoverBg() {
if (this.$refs.coverBg) { if (this.$refs.coverBg) {
this.$refs.coverBg.style.backgroundImage = `url("${this.fullCoverUrl}")` this.$refs.coverBg.style.backgroundImage = `url("${this.fullCoverUrl}")`

View File

@ -14,13 +14,17 @@
</div> </div>
<div class="w-1/2 px-2"> <div class="w-1/2 px-2">
<ui-text-input-with-label v-if="!isEditingRoot" v-model="newUser.password" :label="isNew ? $strings.LabelPassword : $strings.LabelChangePassword" type="password" /> <ui-text-input-with-label v-if="!isEditingRoot" v-model="newUser.password" :label="isNew ? $strings.LabelPassword : $strings.LabelChangePassword" type="password" />
<ui-text-input-with-label v-else v-model="newUser.email" :label="$strings.LabelEmail" />
</div> </div>
</div> </div>
<div v-show="!isEditingRoot" class="flex py-2"> <div v-show="!isEditingRoot" class="flex py-2">
<div class="px-2 w-52"> <div class="w-1/2 px-2">
<ui-dropdown v-model="newUser.type" :label="$strings.LabelAccountType" :disabled="isEditingRoot" :items="accountTypes" @input="userTypeUpdated" /> <ui-text-input-with-label v-model="newUser.email" :label="$strings.LabelEmail" />
</div> </div>
<div class="flex-grow" /> <div class="px-2 w-52">
<ui-dropdown v-model="newUser.type" :label="$strings.LabelAccountType" :disabled="isEditingRoot" :items="accountTypes" small @input="userTypeUpdated" />
</div>
<!-- <div class="flex-grow" /> -->
<div class="flex items-center pt-4 px-2"> <div class="flex items-center pt-4 px-2">
<p class="px-3 font-semibold" id="user-enabled-toggle" :class="isEditingRoot ? 'text-gray-300' : ''">{{ $strings.LabelEnable }}</p> <p class="px-3 font-semibold" id="user-enabled-toggle" :class="isEditingRoot ? 'text-gray-300' : ''">{{ $strings.LabelEnable }}</p>
<ui-toggle-switch labeledBy="user-enabled-toggle" v-model="newUser.isActive" :disabled="isEditingRoot" /> <ui-toggle-switch labeledBy="user-enabled-toggle" v-model="newUser.isActive" :disabled="isEditingRoot" />
@ -257,7 +261,6 @@ export default {
if (account.type === 'root' && !account.isActive) return if (account.type === 'root' && !account.isActive) return
this.processing = true this.processing = true
console.log('Calling update', account)
this.$axios this.$axios
.$patch(`/api/users/${this.account.id}`, account) .$patch(`/api/users/${this.account.id}`, account)
.then((data) => { .then((data) => {
@ -326,9 +329,11 @@ export default {
init() { init() {
this.fetchAllTags() this.fetchAllTags()
this.isNew = !this.account this.isNew = !this.account
if (this.account) { if (this.account) {
this.newUser = { this.newUser = {
username: this.account.username, username: this.account.username,
email: this.account.email,
password: this.account.password, password: this.account.password,
type: this.account.type, type: this.account.type,
isActive: this.account.isActive, isActive: this.account.isActive,
@ -337,9 +342,9 @@ export default {
itemTagsSelected: [...(this.account.itemTagsSelected || [])] itemTagsSelected: [...(this.account.itemTagsSelected || [])]
} }
} else { } else {
this.fetchAllTags()
this.newUser = { this.newUser = {
username: null, username: null,
email: null,
password: null, password: null,
type: 'user', type: 'user',
isActive: true, isActive: true,

View File

@ -0,0 +1,33 @@
<template>
<modals-modal v-model="show" name="cover" :width="'90%'" :height="'90%'" :contentMarginTop="0">
<div class="w-full h-full" @click="show = false">
<img loading="lazy" :src="rawCoverUrl" class="w-full h-full z-10 object-scale-down" />
</div>
</modals-modal>
</template>
<script>
export default {
data() {
return {}
},
computed: {
show: {
get() {
return this.$store.state.globals.showRawCoverPreviewModal
},
set(val) {
this.$store.commit('globals/setShowRawCoverPreviewModal', val)
}
},
selectedLibraryItemId() {
return this.$store.state.globals.selectedLibraryItemId
},
rawCoverUrl() {
return this.$store.getters['globals/getLibraryItemCoverSrcById'](this.selectedLibraryItemId, null, true)
}
},
methods: {},
mounted() {}
}
</script>

View File

@ -5,18 +5,23 @@
<p class="text-3xl text-white truncate">{{ title }}</p> <p class="text-3xl text-white truncate">{{ title }}</p>
</div> </div>
</template> </template>
<div class="p-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh"> <div v-if="author" class="p-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
<form v-if="author" @submit.prevent="submitForm">
<div class="flex"> <div class="flex">
<div class="w-40 p-2"> <div class="w-40 p-2">
<div class="w-full h-45 relative"> <div class="w-full h-45 relative">
<covers-author-image :author="author" /> <covers-author-image :author="author" />
<div v-show="!processing && author.imagePath" class="absolute top-0 left-0 w-full h-full opacity-0 hover:opacity-100"> <div v-if="userCanDelete && !processing && author.imagePath" class="absolute top-0 left-0 w-full h-full opacity-0 hover:opacity-100">
<span class="absolute top-2 right-2 material-icons text-error transform hover:scale-125 transition-transform cursor-pointer text-lg" @click="removeCover">delete</span> <span class="absolute top-2 right-2 material-icons text-error transform hover:scale-125 transition-transform cursor-pointer text-lg" @click="removeCover">delete</span>
</div> </div>
</div> </div>
</div> </div>
<div class="flex-grow"> <div class="flex-grow">
<form @submit.prevent="submitUploadCover" class="flex flex-grow mb-2 p-2">
<ui-text-input v-model="imageUrl" :placeholder="$strings.LabelImageURLFromTheWeb" class="h-9 w-full" />
<ui-btn color="success" type="submit" :padding-x="4" :disabled="!imageUrl" class="ml-2 sm:ml-3 w-24 h-9">{{ $strings.ButtonSubmit }}</ui-btn>
</form>
<form v-if="author" @submit.prevent="submitForm">
<div class="flex"> <div class="flex">
<div class="w-3/4 p-2"> <div class="w-3/4 p-2">
<ui-text-input-with-label v-model="authorCopy.name" :disabled="processing" :label="$strings.LabelName" /> <ui-text-input-with-label v-model="authorCopy.name" :disabled="processing" :label="$strings.LabelName" />
@ -25,9 +30,9 @@
<ui-text-input-with-label v-model="authorCopy.asin" :disabled="processing" label="ASIN" /> <ui-text-input-with-label v-model="authorCopy.asin" :disabled="processing" label="ASIN" />
</div> </div>
</div> </div>
<div class="p-2"> <!-- <div class="p-2">
<ui-text-input-with-label v-model="authorCopy.imagePath" :disabled="processing" :label="$strings.LabelPhotoPathURL" /> <ui-text-input-with-label v-model="authorCopy.imagePath" :disabled="processing" :label="$strings.LabelPhotoPathURL" />
</div> </div> -->
<div class="p-2"> <div class="p-2">
<ui-textarea-with-label v-model="authorCopy.description" :disabled="processing" :label="$strings.LabelDescription" :rows="8" /> <ui-textarea-with-label v-model="authorCopy.description" :disabled="processing" :label="$strings.LabelDescription" :rows="8" />
</div> </div>
@ -39,10 +44,10 @@
<ui-btn type="submit">{{ $strings.ButtonSave }}</ui-btn> <ui-btn type="submit">{{ $strings.ButtonSave }}</ui-btn>
</div> </div>
</div>
</div>
</form> </form>
</div> </div>
</div>
</div>
</modals-modal> </modals-modal>
</template> </template>
@ -53,9 +58,9 @@ export default {
authorCopy: { authorCopy: {
name: '', name: '',
asin: '', asin: '',
description: '', description: ''
imagePath: ''
}, },
imageUrl: '',
processing: false processing: false
} }
}, },
@ -100,10 +105,10 @@ export default {
}, },
methods: { methods: {
init() { init() {
this.imageUrl = ''
this.authorCopy.name = this.author.name this.authorCopy.name = this.author.name
this.authorCopy.asin = this.author.asin this.authorCopy.asin = this.author.asin
this.authorCopy.description = this.author.description this.authorCopy.description = this.author.description
this.authorCopy.imagePath = this.author.imagePath
}, },
removeClick() { removeClick() {
const payload = { const payload = {
@ -131,7 +136,7 @@ export default {
this.$store.commit('globals/setConfirmPrompt', payload) this.$store.commit('globals/setConfirmPrompt', payload)
}, },
async submitForm() { async submitForm() {
var keysToCheck = ['name', 'asin', 'description', 'imagePath'] var keysToCheck = ['name', 'asin', 'description']
var updatePayload = {} var updatePayload = {}
keysToCheck.forEach((key) => { keysToCheck.forEach((key) => {
if (this.authorCopy[key] !== this.author[key]) { if (this.authorCopy[key] !== this.author[key]) {
@ -160,21 +165,46 @@ export default {
} }
this.processing = false this.processing = false
}, },
async removeCover() { removeCover() {
var updatePayload = {
imagePath: null
}
this.processing = true this.processing = true
var result = await this.$axios.$patch(`/api/authors/${this.authorId}`, updatePayload).catch((error) => { this.$axios
.$delete(`/api/authors/${this.authorId}/image`)
.then((data) => {
this.$toast.success(this.$strings.ToastAuthorImageRemoveSuccess)
this.$store.commit('globals/showEditAuthorModal', data.author)
})
.catch((error) => {
console.error('Failed', error) console.error('Failed', error)
this.$toast.error(this.$strings.ToastAuthorImageRemoveFailed) this.$toast.error(this.$strings.ToastAuthorImageRemoveFailed)
return null
}) })
if (result && result.updated) { .finally(() => {
this.$toast.success(this.$strings.ToastAuthorImageRemoveSuccess)
this.$store.commit('globals/showEditAuthorModal', result.author)
}
this.processing = false this.processing = false
})
},
submitUploadCover() {
if (!this.imageUrl?.startsWith('http:') && !this.imageUrl?.startsWith('https:')) {
this.$toast.error('Invalid image url')
return
}
this.processing = true
const updatePayload = {
url: this.imageUrl
}
this.$axios
.$post(`/api/authors/${this.authorId}/image`, updatePayload)
.then((data) => {
this.imageUrl = ''
this.$toast.success('Author image updated')
this.$store.commit('globals/showEditAuthorModal', data.author)
})
.catch((error) => {
console.error('Failed', error)
this.$toast.error(error.response.data || 'Failed to remove author image')
})
.finally(() => {
this.processing = false
})
}, },
async searchAuthor() { async searchAuthor() {
if (!this.authorCopy.name && !this.authorCopy.asin) { if (!this.authorCopy.name && !this.authorCopy.asin) {

View File

@ -8,7 +8,7 @@
<form @submit.prevent="submitForm"> <form @submit.prevent="submitForm">
<div class="w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300"> <div class="w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300">
<div class="w-full px-3 py-5 md:p-12"> <div class="w-full px-3 py-5 md:p-12">
<div class="flex items-center -mx-1 mb-2"> <div class="flex items-center -mx-1 mb-4">
<div class="w-full md:w-1/2 px-1"> <div class="w-full md:w-1/2 px-1">
<ui-text-input-with-label ref="ereaderNameInput" v-model="newDevice.name" :disabled="processing" :label="$strings.LabelName" /> <ui-text-input-with-label ref="ereaderNameInput" v-model="newDevice.name" :disabled="processing" :label="$strings.LabelName" />
</div> </div>
@ -16,6 +16,14 @@
<ui-text-input-with-label ref="ereaderEmailInput" v-model="newDevice.email" :disabled="processing" :label="$strings.LabelEmail" /> <ui-text-input-with-label ref="ereaderEmailInput" v-model="newDevice.email" :disabled="processing" :label="$strings.LabelEmail" />
</div> </div>
</div> </div>
<div class="flex items-center -mx-1 mb-4">
<div class="w-full md:w-1/2 px-1">
<ui-dropdown v-model="newDevice.availabilityOption" :label="$strings.LabelDeviceIsAvailableTo" :items="userAvailabilityOptions" @input="availabilityOptionChanged" />
</div>
<div class="w-full md:w-1/2 px-1">
<ui-multi-select-dropdown v-if="newDevice.availabilityOption === 'specificUsers'" v-model="newDevice.users" :label="$strings.HeaderUsers" :items="userOptions" />
</div>
</div>
<div class="flex items-center pt-4"> <div class="flex items-center pt-4">
<div class="flex-grow" /> <div class="flex-grow" />
@ -45,8 +53,11 @@ export default {
processing: false, processing: false,
newDevice: { newDevice: {
name: '', name: '',
email: '' email: '',
} availabilityOption: 'adminAndUp',
users: []
},
users: []
} }
}, },
watch: { watch: {
@ -68,10 +79,55 @@ export default {
} }
}, },
title() { title() {
return this.ereaderDevice ? 'Create Device' : 'Update Device' return !this.ereaderDevice ? 'Create Device' : 'Update Device'
},
userAvailabilityOptions() {
return [
{
text: this.$strings.LabelAdminUsersOnly,
value: 'adminOrUp'
},
{
text: this.$strings.LabelAllUsersExcludingGuests,
value: 'userOrUp'
},
{
text: this.$strings.LabelAllUsersIncludingGuests,
value: 'guestOrUp'
},
{
text: this.$strings.LabelSelectUsers,
value: 'specificUsers'
}
]
},
userOptions() {
return this.users.map((u) => ({ text: u.username, value: u.id }))
} }
}, },
methods: { methods: {
availabilityOptionChanged(option) {
if (option === 'specificUsers' && !this.users.length) {
this.loadUsers()
}
},
async loadUsers() {
this.processing = true
this.users = await this.$axios
.$get('/api/users')
.then((res) => {
return res.users.sort((a, b) => {
return a.createdAt - b.createdAt
})
})
.catch((error) => {
console.error('Failed', error)
return []
})
.finally(() => {
this.processing = false
})
},
submitForm() { submitForm() {
this.$refs.ereaderNameInput.blur() this.$refs.ereaderNameInput.blur()
this.$refs.ereaderEmailInput.blur() this.$refs.ereaderEmailInput.blur()
@ -81,19 +137,27 @@ export default {
return return
} }
if (this.newDevice.availabilityOption === 'specificUsers' && !this.newDevice.users.length) {
this.$toast.error('Must select at least one user')
return
}
if (this.newDevice.availabilityOption !== 'specificUsers') {
this.newDevice.users = []
}
this.newDevice.name = this.newDevice.name.trim() this.newDevice.name = this.newDevice.name.trim()
this.newDevice.email = this.newDevice.email.trim() this.newDevice.email = this.newDevice.email.trim()
if (!this.ereaderDevice) { if (!this.ereaderDevice) {
if (this.existingDevices.some((d) => d.name === this.newDevice.name)) { if (this.existingDevices.some((d) => d.name === this.newDevice.name)) {
this.$toast.error('EReader device with that name already exists') this.$toast.error('Ereader device with that name already exists')
return return
} }
this.submitCreate() this.submitCreate()
} else { } else {
if (this.ereaderDevice.name !== this.newDevice.name && this.existingDevices.some((d) => d.name === this.newDevice.name)) { if (this.ereaderDevice.name !== this.newDevice.name && this.existingDevices.some((d) => d.name === this.newDevice.name)) {
this.$toast.error('EReader device with that name already exists') this.$toast.error('Ereader device with that name already exists')
return return
} }
@ -160,9 +224,17 @@ export default {
if (this.ereaderDevice) { if (this.ereaderDevice) {
this.newDevice.name = this.ereaderDevice.name this.newDevice.name = this.ereaderDevice.name
this.newDevice.email = this.ereaderDevice.email this.newDevice.email = this.ereaderDevice.email
this.newDevice.availabilityOption = this.ereaderDevice.availabilityOption || 'adminOrUp'
this.newDevice.users = this.ereaderDevice.users || []
if (this.newDevice.availabilityOption === 'specificUsers' && !this.users.length) {
this.loadUsers()
}
} else { } else {
this.newDevice.name = '' this.newDevice.name = ''
this.newDevice.email = '' this.newDevice.email = ''
this.newDevice.availabilityOption = 'adminOrUp'
this.newDevice.users = []
} }
} }
}, },

View File

@ -7,7 +7,7 @@
<!-- book cover overlay --> <!-- book cover overlay -->
<div v-if="media.coverPath" class="absolute top-0 left-0 w-full h-full z-10 opacity-0 hover:opacity-100 transition-opacity duration-100"> <div v-if="media.coverPath" class="absolute top-0 left-0 w-full h-full z-10 opacity-0 hover:opacity-100 transition-opacity duration-100">
<div class="absolute top-0 left-0 w-full h-16 bg-gradient-to-b from-black-600 to-transparent" /> <div class="absolute top-0 left-0 w-full h-16 bg-gradient-to-b from-black-600 to-transparent" />
<div class="p-1 absolute top-1 right-1 text-red-500 rounded-full w-8 h-8 cursor-pointer hover:text-red-400 shadow-sm" @click="removeCover"> <div v-if="userCanDelete" class="p-1 absolute top-1 right-1 text-red-500 rounded-full w-8 h-8 cursor-pointer hover:text-red-400 shadow-sm" @click="removeCover">
<ui-tooltip direction="top" :text="$strings.LabelRemoveCover"> <ui-tooltip direction="top" :text="$strings.LabelRemoveCover">
<span class="material-icons text-2xl">delete</span> <span class="material-icons text-2xl">delete</span>
</ui-tooltip> </ui-tooltip>
@ -16,15 +16,16 @@
</div> </div>
<div class="flex-grow sm:pl-2 md:pl-6 sm:pr-2 mt-2 md:mt-0"> <div class="flex-grow sm:pl-2 md:pl-6 sm:pr-2 mt-2 md:mt-0">
<div class="flex items-center"> <div class="flex items-center">
<div v-if="userCanUpload" class="w-10 md:w-40 pr-2 pt-4 md:min-w-32"> <div v-if="userCanUpload" class="w-10 md:w-40 pr-2 md:min-w-32">
<ui-file-input ref="fileInput" @change="fileUploadSelected"> <ui-file-input ref="fileInput" @change="fileUploadSelected">
<span class="hidden md:inline-block">{{ $strings.ButtonUploadCover }}</span> <span class="hidden md:inline-block">{{ $strings.ButtonUploadCover }}</span>
<span class="material-icons text-2xl inline-block md:!hidden">upload</span> <span class="material-icons text-2xl inline-block md:!hidden">upload</span>
</ui-file-input> </ui-file-input>
</div> </div>
<form @submit.prevent="submitForm" class="flex flex-grow"> <form @submit.prevent="submitForm" class="flex flex-grow">
<ui-text-input-with-label v-model="imageUrl" :label="$strings.LabelCoverImageURL" /> <ui-text-input v-model="imageUrl" :placeholder="$strings.LabelImageURLFromTheWeb" class="h-9 w-full" />
<ui-btn color="success" type="submit" :padding-x="4" class="mt-5 ml-2 sm:ml-3 w-24">{{ $strings.ButtonSave }}</ui-btn> <ui-btn color="success" type="submit" :padding-x="4" :disabled="!imageUrl" class="ml-2 sm:ml-3 w-24 h-9">{{ $strings.ButtonSubmit }}</ui-btn>
</form> </form>
</div> </div>
@ -64,7 +65,7 @@
<div v-if="hasSearched" class="flex items-center flex-wrap justify-center max-h-80 overflow-y-scroll mt-2 max-w-full"> <div v-if="hasSearched" class="flex items-center flex-wrap justify-center max-h-80 overflow-y-scroll mt-2 max-w-full">
<p v-if="!coversFound.length">{{ $strings.MessageNoCoversFound }}</p> <p v-if="!coversFound.length">{{ $strings.MessageNoCoversFound }}</p>
<template v-for="cover in coversFound"> <template v-for="cover in coversFound">
<div :key="cover" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover === imageUrl ? 'border-yellow-300' : ''" @click="updateCover(cover)"> <div :key="cover" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover === coverPath ? 'border-yellow-300' : ''" @click="updateCover(cover)">
<covers-preview-cover :src="cover" :width="80" show-open-new-tab :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-preview-cover :src="cover" :width="80" show-open-new-tab :book-cover-aspect-ratio="bookCoverAspectRatio" />
</div> </div>
</template> </template>
@ -165,6 +166,9 @@ export default {
userCanUpload() { userCanUpload() {
return this.$store.getters['user/getUserCanUpload'] return this.$store.getters['user/getUserCanUpload']
}, },
userCanDelete() {
return this.$store.getters['user/getUserCanDelete']
},
userToken() { userToken() {
return this.$store.getters['user/getToken'] return this.$store.getters['user/getToken']
}, },
@ -222,71 +226,53 @@ export default {
this.coversFound = [] this.coversFound = []
this.hasSearched = false this.hasSearched = false
} }
this.imageUrl = this.media.coverPath || '' this.imageUrl = ''
this.searchTitle = this.mediaMetadata.title || '' this.searchTitle = this.mediaMetadata.title || ''
this.searchAuthor = this.mediaMetadata.authorName || '' this.searchAuthor = this.mediaMetadata.authorName || ''
if (this.isPodcast) this.provider = 'itunes' if (this.isPodcast) this.provider = 'itunes'
else this.provider = localStorage.getItem('book-cover-provider') || localStorage.getItem('book-provider') || 'google' else this.provider = localStorage.getItem('book-cover-provider') || localStorage.getItem('book-provider') || 'google'
}, },
removeCover() { removeCover() {
if (!this.media.coverPath) { if (!this.coverPath) {
this.imageUrl = ''
return return
} }
this.updateCover('') this.isProcessing = true
this.$axios
.$delete(`/api/items/${this.libraryItemId}/cover`)
.then(() => {})
.catch((error) => {
console.error('Failed to remove cover', error)
if (error.response?.data) {
this.$toast.error(error.response.data)
}
})
.finally(() => {
this.isProcessing = false
})
}, },
submitForm() { submitForm() {
this.updateCover(this.imageUrl) this.updateCover(this.imageUrl)
}, },
async updateCover(cover) { async updateCover(cover) {
if (cover === this.coverPath) { if (!cover.startsWith('http:') && !cover.startsWith('https:')) {
console.warn('Cover has not changed..', cover) this.$toast.error('Invalid URL')
return return
} }
this.isProcessing = true this.isProcessing = true
var success = false this.$axios
.$post(`/api/items/${this.libraryItemId}/cover`, { url: cover })
if (!cover) { .then(() => {
// Remove cover this.imageUrl = ''
success = await this.$axios
.$delete(`/api/items/${this.libraryItemId}/cover`)
.then(() => true)
.catch((error) => {
console.error('Failed to remove cover', error)
if (error.response && error.response.data) {
this.$toast.error(error.response.data)
}
return false
})
} else if (cover.startsWith('http:') || cover.startsWith('https:')) {
// Download cover from url and use
success = await this.$axios.$post(`/api/items/${this.libraryItemId}/cover`, { url: cover }).catch((error) => {
console.error('Failed to download cover from url', error)
if (error.response && error.response.data) {
this.$toast.error(error.response.data)
}
return false
})
} else {
// Update local cover url
const updatePayload = {
cover
}
success = await this.$axios.$patch(`/api/items/${this.libraryItemId}/cover`, updatePayload).catch((error) => {
console.error('Failed to update', error)
if (error.response && error.response.data) {
this.$toast.error(error.response.data)
}
return false
})
}
if (success) {
this.$toast.success('Update Successful') this.$toast.success('Update Successful')
} else if (this.media.coverPath) { })
this.imageUrl = this.media.coverPath .catch((error) => {
} console.error('Failed to update cover', error)
this.$toast.error(error.response?.data || 'Failed to update cover')
})
.finally(() => {
this.isProcessing = false this.isProcessing = false
})
}, },
getSearchQuery() { getSearchQuery() {
var searchQuery = `provider=${this.provider}&title=${this.searchTitle}` var searchQuery = `provider=${this.provider}&title=${this.searchTitle}`
@ -319,7 +305,19 @@ export default {
this.hasSearched = true this.hasSearched = true
}, },
setCover(coverFile) { setCover(coverFile) {
this.updateCover(coverFile.metadata.path) this.isProcessing = true
this.$axios
.$patch(`/api/items/${this.libraryItemId}/cover`, { cover: coverFile.metadata.path })
.then(() => {
this.$toast.success('Update Successful')
})
.catch((error) => {
console.error('Failed to set local cover', error)
this.$toast.error(error.response?.data || 'Failed to set cover')
})
.finally(() => {
this.isProcessing = false
})
} }
} }
} }

View File

@ -11,8 +11,8 @@
<ui-btn v-if="userIsAdminOrUp" :loading="quickMatching" color="bg" type="button" class="h-full" small @click.stop.prevent="quickMatch">{{ $strings.ButtonQuickMatch }}</ui-btn> <ui-btn v-if="userIsAdminOrUp" :loading="quickMatching" color="bg" type="button" class="h-full" small @click.stop.prevent="quickMatch">{{ $strings.ButtonQuickMatch }}</ui-btn>
</ui-tooltip> </ui-tooltip>
<ui-tooltip :disabled="!!libraryScan" text="Rescan library item including metadata" direction="bottom" class="mr-2 md:mr-4"> <ui-tooltip :disabled="isLibraryScanning" text="Rescan library item including metadata" direction="bottom" class="mr-2 md:mr-4">
<ui-btn v-if="userIsAdminOrUp && !isFile" :loading="rescanning" :disabled="!!libraryScan" color="bg" type="button" class="h-full" small @click.stop.prevent="rescan">{{ $strings.ButtonReScan }}</ui-btn> <ui-btn v-if="userIsAdminOrUp && !isFile" :loading="rescanning" :disabled="isLibraryScanning" color="bg" type="button" class="h-full" small @click.stop.prevent="rescan">{{ $strings.ButtonReScan }}</ui-btn>
</ui-tooltip> </ui-tooltip>
<div class="flex-grow" /> <div class="flex-grow" />
@ -80,9 +80,9 @@ export default {
libraryProvider() { libraryProvider() {
return this.$store.getters['libraries/getLibraryProvider'](this.libraryId) || 'google' return this.$store.getters['libraries/getLibraryProvider'](this.libraryId) || 'google'
}, },
libraryScan() { isLibraryScanning() {
if (!this.libraryId) return null if (!this.libraryId) return null
return this.$store.getters['scanners/getLibraryScan'](this.libraryId) return !!this.$store.getters['tasks/getRunningLibraryScanTask'](this.libraryId)
} }
}, },
methods: { methods: {

View File

@ -14,8 +14,7 @@ export default {
}, },
data() { data() {
return { return {
tracks: [], tracks: []
showFullPath: false
} }
}, },
watch: { watch: {

View File

@ -332,6 +332,7 @@ export default {
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)}`
if (this.searchAuthor) searchQuery += `&author=${encodeURIComponent(this.searchAuthor)}` if (this.searchAuthor) searchQuery += `&author=${encodeURIComponent(this.searchAuthor)}`
if (this.libraryItemId) searchQuery += `&id=${this.libraryItemId}`
return searchQuery return searchQuery
}, },
submitSearch() { submitSearch() {

View File

@ -31,7 +31,7 @@
<ui-btn class="w-full mt-2" color="primary" @click="browseForFolder">{{ $strings.ButtonBrowseForFolder }}</ui-btn> <ui-btn class="w-full mt-2" color="primary" @click="browseForFolder">{{ $strings.ButtonBrowseForFolder }}</ui-btn>
</div> </div>
</div> </div>
<modals-libraries-folder-chooser v-else :paths="folderPaths" @back="showDirectoryPicker = false" @select="selectFolder" /> <modals-libraries-lazy-folder-chooser v-else :paths="folderPaths" @back="showDirectoryPicker = false" @select="selectFolder" />
</div> </div>
</template> </template>

View File

@ -1,5 +1,5 @@
<template> <template>
<modals-modal v-model="show" name="edit-library" :width="700" :height="'unset'" :processing="processing"> <modals-modal v-model="show" name="edit-library" :width="800" :height="'unset'" :processing="processing">
<template #outer> <template #outer>
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden"> <div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
<p class="text-xl md:text-3xl text-white truncate">{{ title }}</p> <p class="text-xl md:text-3xl text-white truncate">{{ title }}</p>
@ -12,9 +12,9 @@
</div> </div>
<div class="px-2 md:px-4 w-full text-sm pt-2 md:pt-6 pb-20 rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh"> <div class="px-2 md:px-4 w-full text-sm pt-2 md:pt-6 pb-20 rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
<component v-if="libraryCopy && show" ref="tabComponent" :is="tabName" :is-new="!library" :library="libraryCopy" :processing.sync="processing" @update="updateLibrary" @close="show = false" /> <component v-if="libraryCopy && show" ref="tabComponent" :is="tabName" :is-new="!library" :library="libraryCopy" :library-id="libraryId" :processing.sync="processing" @update="updateLibrary" @close="show = false" />
<div class="absolute bottom-0 left-0 w-full px-4 py-4 border-t border-white border-opacity-10"> <div v-show="selectedTab !== 'tools'" class="absolute bottom-0 left-0 w-full px-4 py-4 border-t border-white border-opacity-10">
<div class="flex justify-end"> <div class="flex justify-end">
<ui-btn @click="submit">{{ buttonText }}</ui-btn> <ui-btn @click="submit">{{ buttonText }}</ui-btn>
</div> </div>
@ -54,6 +54,12 @@ export default {
buttonText() { buttonText() {
return this.library ? this.$strings.ButtonSave : this.$strings.ButtonCreate return this.library ? this.$strings.ButtonSave : this.$strings.ButtonCreate
}, },
mediaType() {
return this.libraryCopy?.mediaType
},
libraryId() {
return this.library?.id
},
tabs() { tabs() {
return [ return [
{ {
@ -66,12 +72,26 @@ export default {
title: this.$strings.HeaderSettings, title: this.$strings.HeaderSettings,
component: 'modals-libraries-library-settings' component: 'modals-libraries-library-settings'
}, },
{
id: 'scanner',
title: this.$strings.HeaderSettingsScanner,
component: 'modals-libraries-library-scanner-settings'
},
{ {
id: 'schedule', id: 'schedule',
title: this.$strings.HeaderSchedule, title: this.$strings.HeaderSchedule,
component: 'modals-libraries-schedule-scan' component: 'modals-libraries-schedule-scan'
},
{
id: 'tools',
title: this.$strings.HeaderTools,
component: 'modals-libraries-library-tools'
} }
] ].filter((tab) => {
// Do not show tools tab for new libraries
if (tab.id === 'tools' && !this.library) return false
return tab.id !== 'scanner' || this.mediaType === 'book'
})
}, },
tabName() { tabName() {
var _tab = this.tabs.find((t) => t.id === this.selectedTab) var _tab = this.tabs.find((t) => t.id === this.selectedTab)
@ -105,7 +125,9 @@ export default {
disableWatcher: false, disableWatcher: false,
skipMatchingMediaWithAsin: false, skipMatchingMediaWithAsin: false,
skipMatchingMediaWithIsbn: false, skipMatchingMediaWithIsbn: false,
autoScanCronExpression: null autoScanCronExpression: null,
hideSingleBookSeries: false,
metadataPrecedence: ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata']
} }
} }
}, },

View File

@ -4,29 +4,32 @@
<span class="material-icons text-3xl cursor-pointer hover:text-gray-300" @click="$emit('back')">arrow_back</span> <span class="material-icons text-3xl cursor-pointer hover:text-gray-300" @click="$emit('back')">arrow_back</span>
<p class="px-4 text-xl">{{ $strings.HeaderChooseAFolder }}</p> <p class="px-4 text-xl">{{ $strings.HeaderChooseAFolder }}</p>
</div> </div>
<div v-if="allFolders.length" class="w-full bg-primary bg-opacity-70 py-1 px-4 mb-2"> <div v-if="rootDirs.length" class="w-full bg-primary bg-opacity-70 py-1 px-4 mb-2">
<p class="font-mono truncate">{{ selectedPath || '\\' }}</p> <p class="font-mono truncate">{{ selectedPath || '/' }}</p>
</div> </div>
<div v-if="allFolders.length" class="flex bg-primary bg-opacity-50 p-4 folder-container"> <div v-if="rootDirs.length" class="relative flex bg-primary bg-opacity-50 p-4 folder-container">
<div class="w-1/2 border-r border-bg h-full overflow-y-auto"> <div class="w-1/2 border-r border-bg h-full overflow-y-auto">
<div v-if="level > 0" class="w-full p-1 cursor-pointer flex items-center" @click="goBack"> <div v-if="level > 0" class="w-full p-1 cursor-pointer flex items-center hover:bg-white/10" @click="goBack">
<span class="material-icons bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span> <span class="material-icons bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span>
<p class="text-base font-mono px-2">..</p> <p class="text-base font-mono px-2">..</p>
</div> </div>
<div v-for="dir in _directories" :key="dir.path" class="dir-item w-full p-1 cursor-pointer flex items-center hover:text-white text-gray-200" :class="dir.className" @click="selectDir(dir)"> <div v-for="dir in _directories" :key="dir.path" class="dir-item w-full p-1 cursor-pointer flex items-center hover:text-white text-gray-200 hover:bg-white/10" :class="dir.className" @click="selectDir(dir)">
<span class="material-icons bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span> <span class="material-icons bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span>
<p class="text-base font-mono px-2 truncate">{{ dir.dirname }}</p> <p class="text-base font-mono px-2 truncate">{{ dir.dirname }}</p>
<span v-if="dir.dirs && dir.dirs.length && dir.path === selectedPath" class="material-icons" style="font-size: 1.1rem">arrow_right</span> <span v-if="dir.path === selectedPath" class="material-icons" style="font-size: 1.1rem">arrow_right</span>
</div> </div>
</div> </div>
<div class="w-1/2 h-full overflow-y-auto"> <div class="w-1/2 h-full overflow-y-auto">
<div v-for="dir in _subdirs" :key="dir.path" :class="dir.className" class="dir-item w-full p-1 cursor-pointer flex items-center hover:text-white text-gray-200" @click="selectSubDir(dir)"> <div v-for="dir in _subdirs" :key="dir.path" :class="dir.className" class="dir-item w-full p-1 cursor-pointer flex items-center hover:text-white text-gray-200 hover:bg-white/10" @click="selectSubDir(dir)">
<span class="material-icons bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span> <span class="material-icons bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span>
<p class="text-base font-mono px-2 truncate">{{ dir.dirname }}</p> <p class="text-base font-mono px-2 truncate">{{ dir.dirname }}</p>
</div> </div>
</div> </div>
<div v-if="loadingDirs" class="absolute inset-0 w-full h-full flex items-center justify-center bg-black/10">
<ui-loading-indicator />
</div> </div>
<div v-else-if="loadingFolders" class="py-12 text-center"> </div>
<div v-else-if="initialLoad" class="py-12 text-center">
<p>{{ $strings.MessageLoadingFolders }}</p> <p>{{ $strings.MessageLoadingFolders }}</p>
</div> </div>
<div v-else class="py-12 text-center max-w-sm mx-auto"> <div v-else class="py-12 text-center max-w-sm mx-auto">
@ -51,11 +54,12 @@ export default {
}, },
data() { data() {
return { return {
loadingFolders: false, initialLoad: false,
allFolders: [], loadingDirs: false,
isPosix: true,
rootDirs: [],
directories: [], directories: [],
selectedPath: '', selectedPath: '',
selectedFullPath: '',
subdirs: [], subdirs: [],
level: 0, level: 0,
currentDir: null, currentDir: null,
@ -98,59 +102,88 @@ export default {
} }
}, },
methods: { methods: {
goBack() { async goBack() {
var splitPaths = this.selectedPath.split('\\').slice(1) let selPath = this.selectedPath.replace(/^\//, '')
var prev = splitPaths.slice(0, -1).join('\\') var splitPaths = selPath.split('/')
var currDirs = this.allFolders let previousPath = ''
for (let i = 0; i < splitPaths.length; i++) { let lookupPath = ''
var _dir = currDirs.find((dir) => dir.dirname === splitPaths[i])
if (_dir && _dir.path.slice(1) === prev) { if (splitPaths.length > 2) {
this.directories = currDirs lookupPath = splitPaths.slice(0, -2).join('/')
this.selectDir(_dir)
return
} else if (_dir) {
currDirs = _dir.dirs
} }
previousPath = splitPaths.slice(0, -1).join('/')
if (!this.isPosix) {
// For windows drives add a trailing slash. e.g. C:/
if (!this.isPosix && lookupPath.endsWith(':')) {
lookupPath += '/'
} }
if (!this.isPosix && previousPath.endsWith(':')) {
previousPath += '/'
}
} else {
// Add leading slash
if (previousPath) previousPath = '/' + previousPath
if (lookupPath) lookupPath = '/' + lookupPath
}
this.level--
this.subdirs = this.directories
this.selectedPath = previousPath
this.directories = await this.fetchDirs(lookupPath, this.level)
}, },
selectDir(dir) { async selectDir(dir) {
if (dir.isUsed) return if (dir.isUsed) return
this.selectedPath = dir.path this.selectedPath = dir.path
this.selectedFullPath = dir.fullPath
this.level = dir.level this.level = dir.level
this.subdirs = dir.dirs this.subdirs = await this.fetchDirs(dir.path, dir.level + 1)
}, },
selectSubDir(dir) { async selectSubDir(dir) {
if (dir.isUsed) return if (dir.isUsed) return
this.selectedPath = dir.path this.selectedPath = dir.path
this.selectedFullPath = dir.fullPath
this.level = dir.level this.level = dir.level
this.directories = this.subdirs this.directories = this.subdirs
this.subdirs = dir.dirs this.subdirs = await this.fetchDirs(dir.path, dir.level + 1)
}, },
selectFolder() { selectFolder() {
if (!this.selectedPath) { if (!this.selectedPath) {
console.error('No Selected path') console.error('No Selected path')
return return
} }
if (this.paths.find((p) => p.startsWith(this.selectedFullPath))) { if (this.paths.find((p) => p.startsWith(this.selectedPath))) {
this.$toast.error(`Oops, you cannot add a parent directory of a folder already added`) this.$toast.error(`Oops, you cannot add a parent directory of a folder already added`)
return return
} }
this.$emit('select', this.selectedFullPath) this.$emit('select', this.selectedPath)
this.selectedPath = '' this.selectedPath = ''
this.selectedFullPath = '' },
fetchDirs(path, level) {
this.loadingDirs = true
return this.$axios
.$get(`/api/filesystem?path=${path}&level=${level}`)
.then((data) => {
console.log('Fetched directories', data.directories)
this.isPosix = !!data.posix
return data.directories
})
.catch((error) => {
console.error('Failed to get filesystem paths', error)
this.$toast.error('Failed to get filesystem paths')
return []
})
.finally(() => {
this.loadingDirs = false
})
}, },
async init() { async init() {
this.loadingFolders = true this.initialLoad = true
this.allFolders = await this.$store.dispatch('libraries/loadFolders') this.rootDirs = await this.fetchDirs('', 0)
this.loadingFolders = false this.initialLoad = false
this.directories = this.allFolders this.directories = this.rootDirs
this.subdirs = [] this.subdirs = []
this.selectedPath = '' this.selectedPath = ''
this.selectedFullPath = ''
} }
}, },
mounted() { mounted() {

View File

@ -0,0 +1,160 @@
<template>
<div class="w-full h-full px-1 md:px-4 py-1 mb-4">
<div class="flex items-center justify-between mb-2">
<h2 class="text-base md:text-lg text-gray-200">{{ $strings.HeaderMetadataOrderOfPrecedence }}</h2>
<ui-btn small @click="resetToDefault">{{ $strings.ButtonResetToDefault }}</ui-btn>
</div>
<div class="flex items-center justify-between md:justify-start mb-4">
<p class="text-sm text-gray-300 pr-2">{{ $strings.LabelMetadataOrderOfPrecedenceDescription }}</p>
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex">
<a href="https://www.audiobookshelf.org/guides/book-scanner" target="_blank" class="inline-flex">
<span class="material-icons text-xl w-5">help_outline</span>
</a>
</ui-tooltip>
</div>
<draggable v-model="metadataSourceMapped" v-bind="dragOptions" class="list-group" draggable=".item" handle=".drag-handle" tag="ul" @start="drag = true" @end="drag = false" @update="draggableUpdate">
<transition-group type="transition" :name="!drag ? 'flip-list' : null">
<li v-for="(source, index) in metadataSourceMapped" :key="source.id" :class="source.include ? 'item' : 'opacity-50'" class="w-full px-2 flex items-center relative border border-white/10">
<span class="material-icons drag-handle text-xl text-gray-400 hover:text-gray-50 mr-2 md:mr-4">reorder</span>
<div class="text-center py-1 w-8 min-w-8">
{{ source.include ? getSourceIndex(source.id) : '' }}
</div>
<div class="flex-grow inline-flex justify-between px-4 py-3">
{{ source.name }} <span v-if="source.include && (index === firstActiveSourceIndex || index === lastActiveSourceIndex)" class="px-2 italic font-semibold text-xs text-gray-400">{{ index === firstActiveSourceIndex ? $strings.LabelHighestPriority : $strings.LabelLowestPriority }}</span>
</div>
<div class="px-2 opacity-100">
<ui-toggle-switch v-model="source.include" :off-color="'error'" @input="includeToggled(source)" />
</div>
</li>
</transition-group>
</draggable>
</div>
</template>
<script>
import draggable from 'vuedraggable'
export default {
components: {
draggable
},
props: {
library: {
type: Object,
default: () => null
},
processing: Boolean
},
data() {
return {
drag: false,
dragOptions: {
animation: 200,
group: 'description',
ghostClass: 'ghost'
},
metadataSourceData: {
folderStructure: {
id: 'folderStructure',
name: 'Folder structure',
include: true
},
audioMetatags: {
id: 'audioMetatags',
name: 'Audio file meta tags OR ebook metadata',
include: true
},
nfoFile: {
id: 'nfoFile',
name: 'NFO file',
include: true
},
txtFiles: {
id: 'txtFiles',
name: 'desc.txt & reader.txt files',
include: true
},
opfFile: {
id: 'opfFile',
name: 'OPF file',
include: true
},
absMetadata: {
id: 'absMetadata',
name: 'Audiobookshelf metadata file',
include: true
}
},
metadataSourceMapped: []
}
},
computed: {
librarySettings() {
return this.library.settings || {}
},
mediaType() {
return this.library.mediaType
},
isBookLibrary() {
return this.mediaType === 'book'
},
firstActiveSourceIndex() {
return this.metadataSourceMapped.findIndex((source) => source.include)
},
lastActiveSourceIndex() {
return this.metadataSourceMapped.findLastIndex((source) => source.include)
}
},
methods: {
getSourceIndex(source) {
const activeSources = (this.librarySettings.metadataPrecedence || []).map((s) => s).reverse()
return activeSources.findIndex((s) => s === source) + 1
},
resetToDefault() {
this.metadataSourceMapped = []
for (const key in this.metadataSourceData) {
this.metadataSourceMapped.push({ ...this.metadataSourceData[key] })
}
this.metadataSourceMapped.reverse()
this.$emit('update', this.getLibraryData())
},
getLibraryData() {
const metadataSourceIds = this.metadataSourceMapped.map((source) => (source.include ? source.id : null)).filter((s) => s)
metadataSourceIds.reverse()
return {
settings: {
metadataPrecedence: metadataSourceIds
}
}
},
includeToggled(source) {
this.updated()
},
draggableUpdate() {
this.updated()
},
updated() {
this.$emit('update', this.getLibraryData())
},
init() {
const metadataPrecedence = this.librarySettings.metadataPrecedence || []
this.metadataSourceMapped = metadataPrecedence.map((source) => this.metadataSourceData[source]).filter((s) => s)
for (const sourceKey in this.metadataSourceData) {
if (!metadataPrecedence.includes(sourceKey)) {
const unusedSourceData = { ...this.metadataSourceData[sourceKey], include: false }
this.metadataSourceMapped.unshift(unusedSourceData)
}
}
this.metadataSourceMapped.reverse()
}
},
mounted() {
this.init()
}
}
</script>

View File

@ -0,0 +1,81 @@
<template>
<div class="w-full h-full px-1 md:px-2 py-1 mb-4">
<div class="w-full border border-black-200 p-4 my-8">
<div class="flex flex-wrap items-center">
<div>
<p class="text-lg">Remove metadata files in library item folders</p>
<p class="max-w-sm text-sm pt-2 text-gray-300">Remove all metadata.json or metadata.abs files in your {{ mediaType }} folders</p>
</div>
<div class="flex-grow" />
<div>
<ui-btn class="mb-4 block" @click.stop="removeAllMetadataClick('json')">Remove all metadata.json</ui-btn>
<ui-btn @click.stop="removeAllMetadataClick('abs')">Remove all metadata.abs</ui-btn>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
library: {
type: Object,
default: () => null
},
libraryId: String,
processing: Boolean
},
data() {
return {}
},
computed: {
librarySettings() {
return this.library.settings || {}
},
mediaType() {
return this.library.mediaType
},
isBookLibrary() {
return this.mediaType === 'book'
}
},
methods: {
removeAllMetadataClick(ext) {
const payload = {
message: `Are you sure you want to remove all metadata.${ext} files in your library item folders?`,
persistent: true,
callback: (confirmed) => {
if (confirmed) {
this.removeAllMetadataInLibrary(ext)
}
},
type: 'yesNo'
}
this.$store.commit('globals/setConfirmPrompt', payload)
},
removeAllMetadataInLibrary(ext) {
this.$emit('update:processing', true)
this.$axios
.$post(`/api/libraries/${this.libraryId}/remove-metadata?ext=${ext}`)
.then((data) => {
if (!data.found) {
this.$toast.info(`No metadata.${ext} files were found in library`)
} else if (!data.removed) {
this.$toast.success(`No metadata.${ext} files removed`)
} else {
this.$toast.success(`Successfully removed ${data.removed} metadata.${ext} files`)
}
})
.catch((error) => {
console.error('Failed to remove metadata files', error)
this.$toast.error('Failed to remove metadata files')
})
.finally(() => {
this.$emit('update:processing', false)
})
}
},
mounted() {}
}
</script>

View File

@ -16,11 +16,11 @@
v-for="(episode, index) in episodesList" v-for="(episode, index) in episodesList"
:key="index" :key="index"
class="relative" class="relative"
:class="itemEpisodeMap[episode.cleanUrl] ? 'bg-primary bg-opacity-40' : selectedEpisodes[episode.cleanUrl] ? 'cursor-pointer bg-success bg-opacity-10' : index % 2 == 0 ? 'cursor-pointer bg-primary bg-opacity-25 hover:bg-opacity-40' : 'cursor-pointer bg-primary bg-opacity-5 hover:bg-opacity-25'" :class="getIsEpisodeDownloaded(episode) ? 'bg-primary bg-opacity-40' : selectedEpisodes[episode.cleanUrl] ? 'cursor-pointer bg-success bg-opacity-10' : index % 2 == 0 ? 'cursor-pointer bg-primary bg-opacity-25 hover:bg-opacity-40' : 'cursor-pointer bg-primary bg-opacity-5 hover:bg-opacity-25'"
@click="toggleSelectEpisode(episode)" @click="toggleSelectEpisode(episode)"
> >
<div class="absolute top-0 left-0 h-full flex items-center p-2"> <div class="absolute top-0 left-0 h-full flex items-center p-2">
<span v-if="itemEpisodeMap[episode.cleanUrl]" class="material-icons text-success text-xl">download_done</span> <span v-if="getIsEpisodeDownloaded(episode)" class="material-icons text-success text-xl">download_done</span>
<ui-checkbox v-else v-model="selectedEpisodes[episode.cleanUrl]" small checkbox-bg="primary" border-color="gray-600" /> <ui-checkbox v-else v-model="selectedEpisodes[episode.cleanUrl]" small checkbox-bg="primary" border-color="gray-600" />
</div> </div>
<div class="px-8 py-2"> <div class="px-8 py-2">
@ -68,7 +68,9 @@ export default {
selectAll: false, selectAll: false,
search: null, search: null,
searchTimeout: null, searchTimeout: null,
searchText: null searchText: null,
downloadedEpisodeGuidMap: {},
downloadedEpisodeUrlMap: {}
} }
}, },
watch: { watch: {
@ -93,7 +95,7 @@ export default {
return this.libraryItem.media.metadata.title || 'Unknown' return this.libraryItem.media.metadata.title || 'Unknown'
}, },
allDownloaded() { allDownloaded() {
return !this.episodesCleaned.some((episode) => !this.itemEpisodeMap[episode.cleanUrl]) return !this.episodesCleaned.some((episode) => !this.getIsEpisodeDownloaded(episode))
}, },
episodesSelected() { episodesSelected() {
return Object.keys(this.selectedEpisodes).filter((key) => !!this.selectedEpisodes[key]) return Object.keys(this.selectedEpisodes).filter((key) => !!this.selectedEpisodes[key])
@ -104,18 +106,7 @@ export default {
return this.$getString('LabelDownloadNEpisodes', [this.episodesSelected.length]) return this.$getString('LabelDownloadNEpisodes', [this.episodesSelected.length])
}, },
itemEpisodes() { itemEpisodes() {
if (!this.libraryItem) return [] return this.libraryItem?.media.episodes || []
return this.libraryItem.media.episodes || []
},
itemEpisodeMap() {
const map = {}
this.itemEpisodes.forEach((item) => {
if (item.enclosure) {
const cleanUrl = this.getCleanEpisodeUrl(item.enclosure.url)
map[cleanUrl] = true
}
})
return map
}, },
episodesList() { episodesList() {
return this.episodesCleaned.filter((episode) => { return this.episodesCleaned.filter((episode) => {
@ -127,12 +118,25 @@ export default {
if (this.episodesList.length === this.episodesCleaned.length) { if (this.episodesList.length === this.episodesCleaned.length) {
return this.$strings.LabelSelectAllEpisodes return this.$strings.LabelSelectAllEpisodes
} }
const episodesNotDownloaded = this.episodesList.filter((ep) => !this.itemEpisodeMap[ep.cleanUrl]).length const episodesNotDownloaded = this.episodesList.filter((ep) => !this.getIsEpisodeDownloaded(ep)).length
return this.$getString('LabelSelectEpisodesShowing', [episodesNotDownloaded]) return this.$getString('LabelSelectEpisodesShowing', [episodesNotDownloaded])
} }
}, },
methods: { methods: {
getIsEpisodeDownloaded(episode) {
if (episode.guid && !!this.downloadedEpisodeGuidMap[episode.guid]) {
return true
}
if (this.downloadedEpisodeUrlMap[episode.cleanUrl]) {
return true
}
return false
},
/** /**
* UPDATE: As of v2.4.5 guid is used for matching existing downloaded episodes if it is found on the RSS feed.
* Fallback to checking the clean url
* @see https://github.com/advplyr/audiobookshelf/issues/2207
*
* RSS feed episode url is used for matching with existing downloaded episodes. * RSS feed episode url is used for matching with existing downloaded episodes.
* Some RSS feeds include timestamps in the episode url (e.g. patreon) that can change on requests. * Some RSS feeds include timestamps in the episode url (e.g. patreon) that can change on requests.
* These need to be removed in order to detect the same episode each time the feed is pulled. * These need to be removed in order to detect the same episode each time the feed is pulled.
@ -169,13 +173,13 @@ export default {
}, },
toggleSelectAll(val) { toggleSelectAll(val) {
for (const episode of this.episodesList) { for (const episode of this.episodesList) {
if (this.itemEpisodeMap[episode.cleanUrl]) this.selectedEpisodes[episode.cleanUrl] = false if (this.getIsEpisodeDownloaded(episode)) this.selectedEpisodes[episode.cleanUrl] = false
else this.$set(this.selectedEpisodes, episode.cleanUrl, val) else this.$set(this.selectedEpisodes, episode.cleanUrl, val)
} }
}, },
checkSetIsSelectedAll() { checkSetIsSelectedAll() {
for (const episode of this.episodesList) { for (const episode of this.episodesList) {
if (!this.itemEpisodeMap[episode.cleanUrl] && !this.selectedEpisodes[episode.cleanUrl]) { if (!this.getIsEpisodeDownloaded(episode) && !this.selectedEpisodes[episode.cleanUrl]) {
this.selectAll = false this.selectAll = false
return return
} }
@ -183,7 +187,7 @@ export default {
this.selectAll = true this.selectAll = true
}, },
toggleSelectEpisode(episode) { toggleSelectEpisode(episode) {
if (this.itemEpisodeMap[episode.cleanUrl]) return if (this.getIsEpisodeDownloaded(episode)) return
this.$set(this.selectedEpisodes, episode.cleanUrl, !this.selectedEpisodes[episode.cleanUrl]) this.$set(this.selectedEpisodes, episode.cleanUrl, !this.selectedEpisodes[episode.cleanUrl])
this.checkSetIsSelectedAll() this.checkSetIsSelectedAll()
}, },
@ -219,6 +223,14 @@ export default {
}) })
}, },
init() { init() {
this.downloadedEpisodeGuidMap = {}
this.downloadedEpisodeUrlMap = {}
this.itemEpisodes.forEach((episode) => {
if (episode.guid) this.downloadedEpisodeGuidMap[episode.guid] = episode.id
if (episode.enclosure?.url) this.downloadedEpisodeUrlMap[this.getCleanEpisodeUrl(episode.enclosure.url)] = episode.id
})
this.episodesCleaned = this.episodes this.episodesCleaned = this.episodes
.filter((ep) => ep.enclosure?.url) .filter((ep) => ep.enclosure?.url)
.map((_ep) => { .map((_ep) => {

View File

@ -14,34 +14,40 @@
</div> </div>
</div> </div>
<a v-if="pages && numPages" :href="mainImg" :download="pages[page - 1]" class="absolute top-0 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" :class="comicMetadata ? 'left-32' : 'left-20'"> <div v-if="numPages" class="absolute top-0 left-4 sm:left-8 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @click.stop.prevent="clickShowPageMenu">
<span class="material-icons text-xl">download</span>
</a>
<div v-if="comicMetadata" class="absolute top-0 left-20 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @click.stop.prevent="clickShowInfoMenu">
<span class="material-icons text-xl">more</span>
</div>
<div v-if="numPages" class="absolute top-0 left-8 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @click.stop.prevent="clickShowPageMenu">
<span class="material-icons text-xl">menu</span> <span class="material-icons text-xl">menu</span>
</div> </div>
<div v-if="numPages" class="absolute top-0 right-16 bg-bg text-gray-100 border-b border-l border-r border-gray-400 rounded-b-md px-2 h-9 flex items-center text-center z-20"> <div v-if="comicMetadata" class="absolute top-0 left-16 sm:left-20 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @click.stop.prevent="clickShowInfoMenu">
<span class="material-icons text-xl">more</span>
</div>
<a v-if="pages && numPages" :href="mainImg" :download="pages[page - 1]" class="absolute top-0 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" :class="comicMetadata ? 'left-28 sm:left-32' : 'left-16 sm:left-20'">
<span class="material-icons text-xl">download</span>
</a>
<div v-if="numPages" class="absolute top-0 right-14 sm:right-16 bg-bg text-gray-100 border-b border-l border-r border-gray-400 rounded-b-md px-2 h-9 flex items-center text-center z-20">
<p class="font-mono">{{ page }} / {{ numPages }}</p> <p class="font-mono">{{ page }} / {{ numPages }}</p>
</div> </div>
<div v-if="mainImg" class="absolute top-0 right-36 sm:right-40 bg-bg text-gray-100 border-b border-l border-r border-gray-400 rounded-b-md px-2 h-9 flex items-center text-center z-20">
<ui-icon-btn icon="zoom_out" :size="8" :disabled="!canScaleDown" borderless class="mr-px" @click="zoomOut" />
<ui-icon-btn icon="zoom_in" :size="8" :disabled="!canScaleUp" borderless class="ml-px" @click="zoomIn" />
</div>
<div class="overflow-hidden w-full h-full relative"> <div class="w-full h-full relative">
<div v-show="canGoPrev" class="absolute top-0 left-0 h-full w-1/2 lg:w-1/3 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="prev" @mousedown.prevent> <div v-show="canGoPrev" ref="prevButton" class="absolute top-0 left-0 h-full w-1/2 lg:w-1/3 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="prev" @mousedown.prevent>
<div class="flex items-center justify-center h-full w-1/2"> <div class="flex items-center justify-center h-full w-1/2">
<span v-show="loadedFirstPage" class="material-icons text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_back_ios</span> <span v-show="loadedFirstPage" class="material-icons text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_back_ios</span>
</div> </div>
</div> </div>
<div v-show="canGoNext" class="absolute top-0 right-0 h-full w-1/2 lg:w-1/3 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="next" @mousedown.prevent> <div v-show="canGoNext" ref="nextButton" class="absolute top-0 right-0 h-full w-1/2 lg:w-1/3 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="next" @mousedown.prevent>
<div class="flex items-center justify-center h-full w-1/2 ml-auto"> <div class="flex items-center justify-center h-full w-1/2 ml-auto">
<span v-show="loadedFirstPage" class="material-icons text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_forward_ios</span> <span v-show="loadedFirstPage" class="material-icons text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_forward_ios</span>
</div> </div>
</div> </div>
<div class="h-full flex justify-center"> <div ref="imageContainer" class="w-full h-full relative overflow-auto">
<img v-if="mainImg" :src="mainImg" class="object-contain h-full m-auto" /> <div class="h-full flex" :class="scale > 100 ? '' : 'justify-center'">
<img v-if="mainImg" :style="{ minWidth: scale + '%', width: scale + '%' }" :src="mainImg" class="object-contain m-auto" />
</div>
</div> </div>
<div v-show="loading" class="w-full h-full absolute top-0 left-0 flex items-center justify-center z-10"> <div v-show="loading" class="w-full h-full absolute top-0 left-0 flex items-center justify-center z-10">
<ui-loading-indicator /> <ui-loading-indicator />
</div> </div>
@ -54,6 +60,10 @@ import Path from 'path'
import { Archive } from 'libarchive.js/main.js' import { Archive } from 'libarchive.js/main.js'
import { CompressedFile } from 'libarchive.js/src/compressed-file' import { CompressedFile } from 'libarchive.js/src/compressed-file'
// This is % with respect to the screen width
const MAX_SCALE = 400
const MIN_SCALE = 10
Archive.init({ Archive.init({
workerUrl: '/libarchive/worker-bundle.js' workerUrl: '/libarchive/worker-bundle.js'
}) })
@ -81,7 +91,8 @@ export default {
showInfoMenu: false, showInfoMenu: false,
loadTimeout: null, loadTimeout: null,
loadedFirstPage: false, loadedFirstPage: false,
comicMetadata: null comicMetadata: null,
scale: 80
} }
}, },
watch: { watch: {
@ -136,6 +147,12 @@ export default {
return p return p
}) || [] }) || []
) )
},
canScaleUp() {
return this.scale < MAX_SCALE
},
canScaleDown() {
return this.scale > MIN_SCALE
} }
}, },
methods: { methods: {
@ -331,10 +348,37 @@ export default {
orderedImages = orderedImages.concat(noNumImages.map((i) => i.filename)) orderedImages = orderedImages.concat(noNumImages.map((i) => i.filename))
this.pages = orderedImages this.pages = orderedImages
},
zoomIn() {
this.scale += 10
},
zoomOut() {
this.scale -= 10
},
scroll(event) {
const imageContainer = this.$refs.imageContainer
imageContainer.scrollBy({
top: event.deltaY,
left: event.deltaX,
behavior: 'auto'
})
} }
}, },
mounted() {}, mounted() {
beforeDestroy() {} const prevButton = this.$refs.prevButton
const nextButton = this.$refs.nextButton
prevButton.addEventListener('wheel', this.scroll, { passive: false })
nextButton.addEventListener('wheel', this.scroll, { passive: false })
},
beforeDestroy() {
const prevButton = this.$refs.prevButton
const nextButton = this.$refs.nextButton
prevButton.removeEventListener('wheel', this.scroll, { passive: false })
nextButton.removeEventListener('wheel', this.scroll, { passive: false })
}
} }
</script> </script>

View File

@ -40,8 +40,10 @@ export default {
book: null, book: null,
/** @type {ePub.Rendition} */ /** @type {ePub.Rendition} */
rendition: null, rendition: null,
chapters: [],
ereaderSettings: { ereaderSettings: {
theme: 'dark', theme: 'dark',
font: 'serif',
fontScale: 100, fontScale: 100,
lineSpacing: 115, lineSpacing: 115,
spread: 'auto' spread: 'auto'
@ -67,10 +69,6 @@ export default {
hasNext() { hasNext() {
return !this.rendition?.location?.atEnd return !this.rendition?.location?.atEnd
}, },
/** @returns {Array<ePub.NavItem>} */
chapters() {
return this.book?.navigation?.toc || []
},
userMediaProgress() { userMediaProgress() {
if (!this.libraryItemId) return if (!this.libraryItemId) return
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId) return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId)
@ -130,6 +128,7 @@ export default {
const fontScale = settings.fontScale || 100 const fontScale = settings.fontScale || 100
this.rendition.themes.fontSize(`${fontScale}%`) this.rendition.themes.fontSize(`${fontScale}%`)
this.rendition.themes.font(settings.font)
this.rendition.spread(settings.spread || 'auto') this.rendition.spread(settings.spread || 'auto')
}, },
prev() { prev() {
@ -144,6 +143,40 @@ export default {
if (!this.rendition?.manager) return if (!this.rendition?.manager) return
return this.rendition?.display(href) return this.rendition?.display(href)
}, },
/** @returns {object} Returns the chapter that the `position` in the book is in */
findChapterFromPosition(chapters, position) {
let foundChapter
for (let i = 0; i < chapters.length; i++) {
if (position >= chapters[i].start && (!chapters[i + 1] || position < chapters[i + 1].start)) {
foundChapter = chapters[i]
if (chapters[i].subitems && chapters[i].subitems.length > 0) {
return this.findChapterFromPosition(chapters[i].subitems, position, foundChapter)
}
break
}
}
return foundChapter
},
/** @returns {Array} Returns an array of chapters that only includes chapters with query results */
async searchBook(query) {
const chapters = structuredClone(await this.chapters)
const searchResults = await Promise.all(this.book.spine.spineItems.map((item) => item.load(this.book.load.bind(this.book)).then(item.find.bind(item, query)).finally(item.unload.bind(item))))
const mergedResults = [].concat(...searchResults)
mergedResults.forEach((chapter) => {
chapter.start = this.book.locations.percentageFromCfi(chapter.cfi)
const foundChapter = this.findChapterFromPosition(chapters, chapter.start)
if (foundChapter) foundChapter.searchResults.push(chapter)
})
let filteredResults = chapters.filter(function f(o) {
if (o.searchResults.length) return true
if (o.subitems.length) {
return (o.subitems = o.subitems.filter(f)).length
}
})
return filteredResults
},
keyUp(e) { keyUp(e) {
const rtl = this.book.package.metadata.direction === 'rtl' const rtl = this.book.package.metadata.direction === 'rtl'
if ((e.keyCode || e.which) == 37) { if ((e.keyCode || e.which) == 37) {
@ -317,8 +350,77 @@ export default {
this.checkSaveLocations(reader.book.locations.save()) this.checkSaveLocations(reader.book.locations.save())
}) })
} }
this.getChapters()
}) })
}, },
getChapters() {
// Load the list of chapters in the book. See https://github.com/futurepress/epub.js/issues/759
const toc = this.book?.navigation?.toc || []
const tocTree = []
const resolveURL = (url, relativeTo) => {
// see https://github.com/futurepress/epub.js/issues/1084
// HACK-ish: abuse the URL API a little to resolve the path
// the base needs to be a valid URL, or it will throw a TypeError,
// so we just set a random base URI and remove it later
const base = 'https://example.invalid/'
return new URL(url, base + relativeTo).href.replace(base, '')
}
const basePath = this.book.packaging.navPath || this.book.packaging.ncxPath
const createTree = async (toc, parent) => {
const promises = toc.map(async (tocItem, i) => {
const href = resolveURL(tocItem.href, basePath)
const id = href.split('#')[1]
const item = this.book.spine.get(href)
await item.load(this.book.load.bind(this.book))
const el = id ? item.document.getElementById(id) : item.document.body
const cfi = item.cfiFromElement(el)
parent[i] = {
title: tocItem.label.trim(),
subitems: [],
href,
cfi,
start: this.book.locations.percentageFromCfi(cfi),
end: null, // set by flattenChapters()
id: null, // set by flattenChapters()
searchResults: []
}
if (tocItem.subitems) {
await createTree(tocItem.subitems, parent[i].subitems)
}
})
await Promise.all(promises)
}
return createTree(toc, tocTree).then(() => {
this.chapters = tocTree
})
},
flattenChapters(chapters) {
// Convert the nested epub chapters into something that looks like audiobook chapters for player-ui
const unwrap = (chapters) => {
return chapters.reduce((acc, chapter) => {
return chapter.subitems ? [...acc, chapter, ...unwrap(chapter.subitems)] : [...acc, chapter]
}, [])
}
let flattenedChapters = unwrap(chapters)
flattenedChapters = flattenedChapters.sort((a, b) => a.start - b.start)
for (let i = 0; i < flattenedChapters.length; i++) {
flattenedChapters[i].id = i
if (i < flattenedChapters.length - 1) {
flattenedChapters[i].end = flattenedChapters[i + 1].start
} else {
flattenedChapters[i].end = 1
}
}
return flattenedChapters
},
resize() { resize() {
this.windowWidth = window.innerWidth this.windowWidth = window.innerWidth
this.windowHeight = window.innerHeight this.windowHeight = window.innerHeight

View File

@ -26,9 +26,9 @@
<component v-if="componentName" ref="readerComponent" :is="componentName" :library-item="selectedLibraryItem" :player-open="!!streamLibraryItem" :keep-progress="keepProgress" :file-id="ebookFileId" @touchstart="touchstart" @touchend="touchend" @hook:mounted="readerMounted" /> <component v-if="componentName" ref="readerComponent" :is="componentName" :library-item="selectedLibraryItem" :player-open="!!streamLibraryItem" :keep-progress="keepProgress" :file-id="ebookFileId" @touchstart="touchstart" @touchend="touchend" @hook:mounted="readerMounted" />
<!-- TOC side nav --> <!-- TOC side nav -->
<div v-if="tocOpen" class="w-full h-full fixed inset-0 bg-black/20 z-20" @click.stop.prevent="toggleToC"></div> <div v-if="tocOpen" class="w-full h-full overflow-y-scroll absolute inset-0 bg-black/20 z-20" @click.stop.prevent="toggleToC"></div>
<div v-if="isEpub" class="w-96 h-full max-h-full absolute top-0 left-0 shadow-xl transition-transform z-30 group-data-[theme=dark]:bg-primary group-data-[theme=dark]:text-white group-data-[theme=light]:bg-white group-data-[theme=light]:text-black" :class="tocOpen ? 'translate-x-0' : '-translate-x-96'" @click.stop.prevent="toggleToC"> <div v-if="isEpub" class="w-96 h-full max-h-full absolute top-0 left-0 shadow-xl transition-transform z-30 group-data-[theme=dark]:bg-primary group-data-[theme=dark]:text-white group-data-[theme=light]:bg-white group-data-[theme=light]:text-black" :class="tocOpen ? 'translate-x-0' : '-translate-x-96'" @click.stop.prevent>
<div class="p-4 h-full"> <div class="flex flex-col p-4 h-full">
<div class="flex items-center mb-2"> <div class="flex items-center mb-2">
<button @click.stop.prevent="toggleToC" type="button" aria-label="Close table of contents" class="inline-flex opacity-80 hover:opacity-100"> <button @click.stop.prevent="toggleToC" type="button" aria-label="Close table of contents" class="inline-flex opacity-80 hover:opacity-100">
<span class="material-icons text-2xl">arrow_back</span> <span class="material-icons text-2xl">arrow_back</span>
@ -36,13 +36,28 @@
<p class="text-lg font-semibold ml-2">{{ $strings.HeaderTableOfContents }}</p> <p class="text-lg font-semibold ml-2">{{ $strings.HeaderTableOfContents }}</p>
</div> </div>
<div class="tocContent"> <form @submit.prevent="searchBook" @click.stop.prevent>
<ui-text-input clearable ref="input" @clear="searchBook" v-model="searchQuery" :placeholder="$strings.PlaceholderSearch" class="h-8 w-full text-sm flex mb-2" />
</form>
<div class="overflow-y-auto">
<div v-if="isSearching && !this.searchResults.length" class="w-full h-40 justify-center">
<p class="text-center text-xl py-4">{{ $strings.MessageNoResults }}</p>
</div>
<ul> <ul>
<li v-for="chapter in chapters" :key="chapter.id" class="py-1"> <li v-for="chapter in isSearching ? this.searchResults : chapters" :key="chapter.id" class="py-1">
<a :href="chapter.href" class="opacity-80 hover:opacity-100" @click.prevent="$refs.readerComponent.goToChapter(chapter.href)">{{ chapter.label }}</a> <a :href="chapter.href" class="opacity-80 hover:opacity-100" @click.prevent="goToChapter(chapter.href)">{{ chapter.title }}</a>
<div v-for="searchResults in chapter.searchResults" :key="searchResults.cfi" class="text-sm py-1 pl-4">
<a :href="searchResults.cfi" class="opacity-50 hover:opacity-100" @click.prevent="goToChapter(searchResults.cfi)">{{ searchResults.excerpt }}</a>
</div>
<ul v-if="chapter.subitems.length"> <ul v-if="chapter.subitems.length">
<li v-for="subchapter in chapter.subitems" :key="subchapter.id" class="py-1 pl-4"> <li v-for="subchapter in chapter.subitems" :key="subchapter.id" class="py-1 pl-4">
<a :href="subchapter.href" class="opacity-80 hover:opacity-100" @click.prevent="$refs.readerComponent.goToChapter(subchapter.href)">{{ subchapter.label }}</a> <a :href="subchapter.href" class="opacity-80 hover:opacity-100" @click.prevent="goToChapter(subchapter.href)">{{ subchapter.title }}</a>
<div v-for="subChapterSearchResults in subchapter.searchResults" :key="subChapterSearchResults.cfi" class="text-sm py-1 pl-4">
<a :href="subChapterSearchResults.cfi" class="opacity-50 hover:opacity-100" @click.prevent="goToChapter(subChapterSearchResults.cfi)">{{ subChapterSearchResults.excerpt }}</a>
</div>
</li> </li>
</ul> </ul>
</li> </li>
@ -63,7 +78,13 @@
<div class="w-40"> <div class="w-40">
<p class="text-lg">{{ $strings.LabelTheme }}:</p> <p class="text-lg">{{ $strings.LabelTheme }}:</p>
</div> </div>
<ui-toggle-btns v-model="ereaderSettings.theme" :items="themeItems" @input="settingsUpdated" /> <ui-toggle-btns v-model="ereaderSettings.theme" :items="themeItems.theme" @input="settingsUpdated" />
</div>
<div class="flex items-center mb-4">
<div class="w-40">
<p class="text-lg">{{ $strings.LabelFontFamily }}:</p>
</div>
<ui-toggle-btns v-model="ereaderSettings.font" :items="themeItems.font" @input="settingsUpdated" />
</div> </div>
<div class="flex items-center mb-4"> <div class="flex items-center mb-4">
<div class="w-40"> <div class="w-40">
@ -99,10 +120,14 @@ export default {
touchstartTime: 0, touchstartTime: 0,
touchIdentifier: null, touchIdentifier: null,
chapters: [], chapters: [],
isSearching: false,
searchResults: [],
searchQuery: '',
tocOpen: false, tocOpen: false,
showSettings: false, showSettings: false,
ereaderSettings: { ereaderSettings: {
theme: 'dark', theme: 'dark',
font: 'serif',
fontScale: 100, fontScale: 100,
lineSpacing: 115, lineSpacing: 115,
spread: 'auto' spread: 'auto'
@ -142,7 +167,8 @@ export default {
] ]
}, },
themeItems() { themeItems() {
return [ return {
theme: [
{ {
text: this.$strings.LabelThemeDark, text: this.$strings.LabelThemeDark,
value: 'dark' value: 'dark'
@ -151,7 +177,18 @@ export default {
text: this.$strings.LabelThemeLight, text: this.$strings.LabelThemeLight,
value: 'light' value: 'light'
} }
],
font: [
{
text: 'Sans',
value: 'sans-serif'
},
{
text: 'Serif',
value: 'serif'
}
] ]
}
}, },
componentName() { componentName() {
if (this.ebookType === 'epub') return 'readers-epub-reader' if (this.ebookType === 'epub') return 'readers-epub-reader'
@ -235,6 +272,10 @@ export default {
} }
}, },
methods: { methods: {
goToChapter(uri) {
this.toggleToC()
this.$refs.readerComponent.goToChapter(uri)
},
readerMounted() { readerMounted() {
if (this.isEpub) { if (this.isEpub) {
this.loadEreaderSettings() this.loadEreaderSettings()
@ -262,6 +303,15 @@ export default {
this.close() this.close()
} }
}, },
async searchBook() {
if (this.searchQuery.length > 1) {
this.searchResults = await this.$refs.readerComponent.searchBook(this.searchQuery)
this.isSearching = true
} else {
this.isSearching = false
this.searchResults = []
}
},
next() { next() {
if (this.$refs.readerComponent?.next) this.$refs.readerComponent.next() if (this.$refs.readerComponent?.next) this.$refs.readerComponent.next()
}, },
@ -340,6 +390,8 @@ export default {
}, },
close() { close() {
this.unregisterListeners() this.unregisterListeners()
this.isSearching = false
this.searchQuery = ''
this.show = false this.show = false
} }
}, },
@ -353,10 +405,6 @@ export default {
</script> </script>
<style> <style>
.tocContent {
height: calc(100% - 36px);
overflow-y: auto;
}
#reader { #reader {
height: 100%; height: 100%;
} }

View File

@ -0,0 +1,285 @@
<template>
<div>
<div v-if="processing" class="max-w-[800px] h-80 md:h-[800px] mx-auto flex items-center justify-center">
<widgets-loading-spinner />
</div>
<img v-else-if="dataUrl" :src="dataUrl" class="mx-auto" />
</div>
</template>
<script>
export default {
props: {
variant: {
type: Number,
default: 0
},
year: Number,
processing: Boolean
},
data() {
return {
canvas: null,
dataUrl: null,
yearStats: null
}
},
watch: {
variant() {
this.init()
}
},
methods: {
async initCanvas() {
if (!this.yearStats) return
const canvas = document.createElement('canvas')
canvas.width = 800
canvas.height = 800
const ctx = canvas.getContext('2d')
const createRoundedRect = (x, y, w, h) => {
const grd1 = ctx.createLinearGradient(x, y, x + w, y + h)
grd1.addColorStop(0, '#44444455')
grd1.addColorStop(1, '#ffffff11')
ctx.fillStyle = grd1
ctx.strokeStyle = '#C0C0C088'
ctx.beginPath()
ctx.roundRect(x, y, w, h, [20])
ctx.fill()
ctx.stroke()
}
const addText = (text, fontSize, fontWeight, color, letterSpacing, x, y, maxWidth = 0) => {
ctx.fillStyle = color
ctx.font = `${fontWeight} ${fontSize} Source Sans Pro`
ctx.letterSpacing = letterSpacing
// If maxWidth is specified then continue to remove chars until under maxWidth and add ellipsis
if (maxWidth) {
let txtWidth = ctx.measureText(text).width
while (txtWidth > maxWidth) {
console.warn(`Text "${text}" is greater than max width ${maxWidth} (width:${txtWidth})`)
if (text.endsWith('...')) text = text.slice(0, -4) // Repeated checks remove 1 char at a time
else text = text.slice(0, -3) // First check remove last 3 chars
text += '...'
txtWidth = ctx.measureText(text).width
console.log(`Checking text "${text}" (width:${txtWidth})`)
}
}
ctx.fillText(text, x, y)
}
const addIcon = (icon, color, fontSize, x, y) => {
ctx.fillStyle = color
ctx.font = `${fontSize} Material Icons Outlined`
ctx.fillText(icon, x, y)
}
// Bg color
ctx.fillStyle = '#232323'
ctx.fillRect(0, 0, canvas.width, canvas.height)
// Cover image tiles
const bookCovers = this.yearStats.finishedBooksWithCovers
bookCovers.push(...this.yearStats.booksWithCovers)
let finishedBookCoverImgs = {}
if (bookCovers.length) {
let index = 0
ctx.globalAlpha = 0.25
ctx.save()
ctx.translate(canvas.width / 2, canvas.height / 2)
ctx.rotate((-Math.PI / 180) * 25)
ctx.translate(-canvas.width / 2, -canvas.height / 2)
ctx.translate(-130, -120)
for (let x = 0; x < 5; x++) {
for (let y = 0; y < 5; y++) {
const coverIndex = index % bookCovers.length
let libraryItemId = bookCovers[coverIndex]
index++
await new Promise((resolve) => {
const img = new Image()
img.crossOrigin = 'anonymous'
img.addEventListener('load', () => {
let sw = img.width
if (img.width > img.height) {
sw = img.height
}
let sx = -(sw - img.width) / 2
let sy = -(sw - img.height) / 2
ctx.drawImage(img, sx, sy, sw, sw, 215 * x, 215 * y, 215, 215)
resolve()
if (this.yearStats.finishedBooksWithCovers.includes(libraryItemId) && !finishedBookCoverImgs[libraryItemId]) {
finishedBookCoverImgs[libraryItemId] = {
img,
sx,
sy,
sw
}
}
})
img.addEventListener('error', () => {
resolve()
})
img.src = this.$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId)
})
}
}
ctx.restore()
}
ctx.globalAlpha = 1
ctx.textBaseline = 'middle'
// Create gradient
const grd1 = ctx.createLinearGradient(0, 0, canvas.width, canvas.height)
grd1.addColorStop(0, '#000000aa')
grd1.addColorStop(1, '#cd9d49aa')
ctx.fillStyle = grd1
ctx.fillRect(0, 0, canvas.width, canvas.height)
// Top Abs icon
let tanColor = '#ffdb70'
ctx.fillStyle = tanColor
ctx.font = '42px absicons'
ctx.fillText('\ue900', 15, 36)
// Top text
addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28)
addText(`${this.year} YEAR IN REVIEW`, '18px', 'bold', 'white', '1px', 65, 51)
// Top left box
createRoundedRect(50, 100, 340, 160)
addText(this.yearStats.numBooksFinished, '64px', 'bold', 'white', '0px', 160, 165)
addText('books finished', '28px', 'normal', tanColor, '0px', 160, 210)
const readIconPath = new Path2D()
readIconPath.addPath(new Path2D('M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z'), { a: 2, d: 2, e: 100, f: 160 })
ctx.fillStyle = '#ffffff'
ctx.fill(readIconPath)
// Box top right
createRoundedRect(410, 100, 340, 160)
addText(this.$elapsedPrettyExtended(this.yearStats.totalListeningTime, true, false), '40px', 'bold', 'white', '0px', 500, 165)
addText('spent listening', '28px', 'normal', tanColor, '0px', 500, 205)
addIcon('watch_later', 'white', '52px', 440, 180)
// Box bottom left
createRoundedRect(50, 280, 340, 160)
addText(this.yearStats.totalListeningSessions, '64px', 'bold', 'white', '0px', 160, 345)
addText('sessions', '28px', 'normal', tanColor, '1px', 160, 390)
addIcon('headphones', 'white', '52px', 95, 360)
// Box bottom right
createRoundedRect(410, 280, 340, 160)
addText(this.yearStats.numBooksListened, '64px', 'bold', 'white', '0px', 500, 345)
addText('books listened to', '28px', 'normal', tanColor, '0px', 500, 390)
addIcon('local_library', 'white', '52px', 440, 360)
if (!this.variant) {
// Text stats
const topNarrator = this.yearStats.mostListenedNarrator
if (topNarrator) {
addText('TOP NARRATOR', '24px', 'normal', tanColor, '1px', 70, 520)
addText(topNarrator.name, '36px', 'bolder', 'white', '0px', 70, 564, 330)
addText(this.$elapsedPrettyExtended(topNarrator.time, true, false), '24px', 'lighter', 'white', '1px', 70, 599)
}
const topGenre = this.yearStats.topGenres[0]
if (topGenre) {
addText('TOP GENRE', '24px', 'normal', tanColor, '1px', 430, 520)
addText(topGenre.genre, '36px', 'bolder', 'white', '0px', 430, 564, 330)
addText(this.$elapsedPrettyExtended(topGenre.time, true, false), '24px', 'lighter', 'white', '1px', 430, 599)
}
const topAuthor = this.yearStats.topAuthors[0]
if (topAuthor) {
addText('TOP AUTHOR', '24px', 'normal', tanColor, '1px', 70, 670)
addText(topAuthor.name, '36px', 'bolder', 'white', '0px', 70, 714, 330)
addText(this.$elapsedPrettyExtended(topAuthor.time, true, false), '24px', 'lighter', 'white', '1px', 70, 749)
}
if (this.yearStats.mostListenedMonth?.time) {
const jsdate = new Date(this.year, this.yearStats.mostListenedMonth.month, 1)
const monthName = this.$formatJsDate(jsdate, 'LLLL')
addText('TOP MONTH', '24px', 'normal', tanColor, '1px', 430, 670)
addText(monthName, '36px', 'bolder', 'white', '0px', 430, 714, 330)
addText(this.$elapsedPrettyExtended(this.yearStats.mostListenedMonth.time, true, false), '24px', 'lighter', 'white', '1px', 430, 749)
}
} else if (this.variant === 1) {
// Bottom images
finishedBookCoverImgs = Object.values(finishedBookCoverImgs)
if (finishedBookCoverImgs.length > 0) {
ctx.textAlign = 'center'
addText('Some books finished this year...', '28px', 'normal', tanColor, '0px', canvas.width / 2, 530)
for (let i = 0; i < Math.min(5, finishedBookCoverImgs.length); i++) {
let imgToAdd = finishedBookCoverImgs[i]
ctx.drawImage(imgToAdd.img, imgToAdd.sx, imgToAdd.sy, imgToAdd.sw, imgToAdd.sw, 40 + 145 * i, 570, 140, 140)
}
}
} else if (this.variant === 2) {
// Text stats
if (this.yearStats.topAuthors.length) {
addText('TOP AUTHORS', '24px', 'normal', tanColor, '1px', 70, 524)
for (let i = 0; i < this.yearStats.topAuthors.length; i++) {
addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 584 + i * 60, 330)
}
}
if (this.yearStats.topGenres.length) {
addText('TOP GENRES', '24px', 'normal', tanColor, '1px', 430, 524)
for (let i = 0; i < this.yearStats.topGenres.length; i++) {
addText(this.yearStats.topGenres[i].genre, '36px', 'bolder', 'white', '0px', 430, 584 + i * 60, 330)
}
}
}
this.canvas = canvas
this.dataUrl = canvas.toDataURL('png')
},
refresh() {
this.init()
},
share() {
this.canvas.toBlob((blob) => {
const file = new File([blob], 'yearinreview.png', { type: blob.type })
const shareData = {
files: [file]
}
if (navigator.canShare(shareData)) {
navigator
.share(shareData)
.then(() => {
console.log('Share success')
})
.catch((error) => {
console.error('Failed to share', error)
if (error.name !== 'AbortError') {
this.$toast.error('Failed to share: ' + error.message)
}
})
} else {
this.$toast.error('Cannot share natively on this device')
}
})
},
async init() {
this.$emit('update:processing', true)
this.yearStats = await this.$axios.$get(`/api/me/stats/year/${this.year}`).catch((err) => {
console.error('Failed to load stats for year', err)
this.$toast.error('Failed to load year stats')
return null
})
await this.initCanvas()
this.$emit('update:processing', false)
}
},
mounted() {
this.init()
}
}
</script>

View File

@ -0,0 +1,141 @@
<template>
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-1 sm:p-4 mb-4">
<!-- hack to get icon fonts loaded on init -->
<div class="h-0 w-0 overflow-hidden opacity-0">
<span class="material-icons-outlined">close</span>
<span class="abs-icons icon-audiobookshelf" />
</div>
<div class="flex items-center">
<p class="hidden md:block text-xl font-semibold">{{ yearInReviewYear }} Year in Review</p>
<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>
</div>
<!-- your year in review -->
<div v-if="showYearInReview">
<div class="w-full h-px bg-slate-200/10 my-4" />
<div class="flex items-center justify-center mb-2 max-w-[800px] mx-auto">
<!-- previous button -->
<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="hidden sm:inline-block pr-2">Previous</span>
</ui-btn>
<!-- 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>
<div class="flex-grow" />
<p class="hidden sm:block text-lg font-semibold">Your Year in Review ({{ yearInReviewVariant + 1 }})</p>
<p class="block sm:hidden text-lg font-semibold">{{ yearInReviewVariant + 1 }}</p>
<div class="flex-grow" />
<!-- refresh button -->
<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="material-icons sm:!hidden text-lg py-px">refresh</span>
</ui-btn>
<!-- next button -->
<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="material-icons-outlined text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
</ui-btn>
</div>
<stats-year-in-review ref="yearInReview" :variant="yearInReviewVariant" :year="yearInReviewYear" :processing.sync="processingYearInReview" />
<!-- your year in review short -->
<div class="w-full max-w-[800px] mx-auto my-4">
<!-- 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>
<stats-year-in-review-short ref="yearInReviewShort" :year="yearInReviewYear" :processing.sync="processingYearInReviewShort" />
</div>
<!-- your server in review -->
<div v-if="isAdminOrUp" class="w-full max-w-[800px] mx-auto mb-2 mt-4 border-t pt-4 border-white/10">
<div class="flex items-center justify-center mb-2">
<!-- previous button -->
<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="hidden sm:inline-block pr-2">Previous</span>
</ui-btn>
<!-- 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>
<div class="flex-grow" />
<p class="hidden sm:block text-lg font-semibold">Server Year in Review ({{ yearInReviewServerVariant + 1 }})</p>
<p class="block sm:hidden text-lg font-semibold">{{ yearInReviewServerVariant + 1 }}</p>
<div class="flex-grow" />
<!-- refresh button -->
<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="material-icons sm:!hidden text-lg py-px">refresh</span>
</ui-btn>
<!-- next button -->
<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="material-icons-outlined text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
</ui-btn>
</div>
</div>
<stats-year-in-review-server v-if="isAdminOrUp" ref="yearInReviewServer" :year="yearInReviewYear" :variant="yearInReviewServerVariant" :processing.sync="processingYearInReviewServer" />
</div>
</div>
</template>
<script>
export default {
data() {
return {
showYearInReview: false,
yearInReviewYear: 0,
yearInReviewVariant: 0,
yearInReviewServerVariant: 0,
processingYearInReview: false,
processingYearInReviewShort: false,
processingYearInReviewServer: false,
showShareButton: false
}
},
computed: {
isAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp']
}
},
methods: {
shareYearInReviewServer() {
this.$refs.yearInReviewServer.share()
},
shareYearInReview() {
this.$refs.yearInReview.share()
},
shareYearInReviewShort() {
this.$refs.yearInReviewShort.share()
},
refreshYearInReviewServer() {
this.$refs.yearInReviewServer.refresh()
},
refreshYearInReview() {
this.$refs.yearInReview.refresh()
this.$refs.yearInReviewShort.refresh()
},
clickShowYearInReview() {
this.showYearInReview = !this.showYearInReview
}
},
beforeMount() {
this.yearInReviewYear = new Date().getFullYear()
// When not December show previous year
if (new Date().getMonth() < 11) {
this.yearInReviewYear--
}
},
mounted() {
if (typeof navigator.share !== 'undefined' && navigator.share) {
this.showShareButton = true
} else {
console.warn('Navigator.share not supported')
}
}
}
</script>

View File

@ -0,0 +1,264 @@
<template>
<div>
<div v-if="processing" class="max-w-[800px] h-80 md:h-[800px] mx-auto flex items-center justify-center">
<widgets-loading-spinner />
</div>
<img v-else-if="dataUrl" :src="dataUrl" class="mx-auto" />
</div>
</template>
<script>
export default {
props: {
variant: {
type: Number,
default: 0
},
processing: Boolean,
year: Number
},
data() {
return {
canvas: null,
dataUrl: null,
yearStats: null
}
},
watch: {
variant() {
this.init()
}
},
methods: {
async initCanvas() {
if (!this.yearStats) return
const canvas = document.createElement('canvas')
canvas.width = 800
canvas.height = 800
const ctx = canvas.getContext('2d')
const createRoundedRect = (x, y, w, h) => {
const grd1 = ctx.createLinearGradient(x, y, x + w, y + h)
grd1.addColorStop(0, '#44444455')
grd1.addColorStop(1, '#ffffff11')
ctx.fillStyle = grd1
ctx.strokeStyle = '#C0C0C088'
ctx.beginPath()
ctx.roundRect(x, y, w, h, [20])
ctx.fill()
ctx.stroke()
}
const addText = (text, fontSize, fontWeight, color, letterSpacing, x, y, maxWidth = 0) => {
ctx.fillStyle = color
ctx.font = `${fontWeight} ${fontSize} Source Sans Pro`
ctx.letterSpacing = letterSpacing
// If maxWidth is specified then continue to remove chars until under maxWidth and add ellipsis
if (maxWidth) {
let txtWidth = ctx.measureText(text).width
while (txtWidth > maxWidth) {
console.warn(`Text "${text}" is greater than max width ${maxWidth} (width:${txtWidth})`)
if (text.endsWith('...')) text = text.slice(0, -4) // Repeated checks remove 1 char at a time
else text = text.slice(0, -3) // First check remove last 3 chars
text += '...'
txtWidth = ctx.measureText(text).width
console.log(`Checking text "${text}" (width:${txtWidth})`)
}
}
ctx.fillText(text, x, y)
}
// Bg color
ctx.fillStyle = '#232323'
ctx.fillRect(0, 0, canvas.width, canvas.height)
// Cover image tiles
let imgsToAdd = {}
if (this.yearStats.booksAddedWithCovers.length) {
let index = 0
ctx.globalAlpha = 0.25
ctx.save()
ctx.translate(canvas.width / 2, canvas.height / 2)
ctx.rotate((-Math.PI / 180) * 25)
ctx.translate(-canvas.width / 2, -canvas.height / 2)
ctx.translate(-130, -120)
for (let x = 0; x < 5; x++) {
for (let y = 0; y < 5; y++) {
const coverIndex = index % this.yearStats.booksAddedWithCovers.length
let libraryItemId = this.yearStats.booksAddedWithCovers[coverIndex]
index++
await new Promise((resolve) => {
const img = new Image()
img.crossOrigin = 'anonymous'
img.addEventListener('load', () => {
let sw = img.width
if (img.width > img.height) {
sw = img.height
}
let sx = -(sw - img.width) / 2
let sy = -(sw - img.height) / 2
ctx.drawImage(img, sx, sy, sw, sw, 215 * x, 215 * y, 215, 215)
if (!imgsToAdd[libraryItemId]) {
imgsToAdd[libraryItemId] = {
img,
sx,
sy,
sw
}
}
resolve()
})
img.addEventListener('error', () => {
resolve()
})
img.src = this.$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId)
})
}
}
ctx.restore()
}
ctx.globalAlpha = 1
ctx.textBaseline = 'middle'
// Create gradient
const grd1 = ctx.createLinearGradient(0, 0, canvas.width, canvas.height)
grd1.addColorStop(0, '#000000aa')
grd1.addColorStop(1, '#cd9d49aa')
ctx.fillStyle = grd1
ctx.fillRect(0, 0, canvas.width, canvas.height)
// Top Abs icon
let tanColor = '#ffdb70'
ctx.fillStyle = tanColor
ctx.font = '42px absicons'
ctx.fillText('\ue900', 15, 36)
// Top text
addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28)
addText(`${this.year} YEAR IN REVIEW`, '18px', 'bold', 'white', '1px', 65, 51)
// Top left box
createRoundedRect(40, 100, 230, 100)
ctx.textAlign = 'center'
addText(this.yearStats.numBooksAdded, '48px', 'bold', 'white', '0px', 155, 140)
addText('books added', '18px', 'normal', tanColor, '0px', 155, 170)
// Box top right
createRoundedRect(285, 100, 230, 100)
addText(this.yearStats.numAuthorsAdded, '48px', 'bold', 'white', '0px', 400, 140)
addText('authors added', '18px', 'normal', tanColor, '0px', 400, 170)
// Box bottom left
createRoundedRect(530, 100, 230, 100)
addText(this.yearStats.numListeningSessions, '48px', 'bold', 'white', '0px', 645, 140)
addText('sessions', '18px', 'normal', tanColor, '1px', 645, 170)
// Text stats
if (this.yearStats.totalBooksAddedSize) {
addText('Your book collection grew to...', '24px', 'normal', tanColor, '0px', canvas.width / 2, 260)
addText(this.$bytesPretty(this.yearStats.totalBooksSize), '36px', 'bolder', 'white', '0px', canvas.width / 2, 300)
addText('+' + this.$bytesPretty(this.yearStats.totalBooksAddedSize), '20px', 'lighter', 'white', '0px', canvas.width / 2, 330)
}
if (this.yearStats.totalBooksAddedDuration) {
addText('With a total duration of...', '24px', 'normal', tanColor, '0px', canvas.width / 2, 400)
addText(this.$elapsedPrettyExtended(this.yearStats.totalBooksDuration, true, false), '36px', 'bolder', 'white', '0px', canvas.width / 2, 440)
addText('+' + this.$elapsedPrettyExtended(this.yearStats.totalBooksAddedDuration, true, false), '20px', 'lighter', 'white', '0px', canvas.width / 2, 470)
}
if (!this.variant) {
// Bottom images
imgsToAdd = Object.values(imgsToAdd)
if (imgsToAdd.length > 0) {
addText('Some additions include...', '24px', 'normal', tanColor, '0px', canvas.width / 2, 540)
for (let i = 0; i < Math.min(5, imgsToAdd.length); i++) {
let imgToAdd = imgsToAdd[i]
ctx.drawImage(imgToAdd.img, imgToAdd.sx, imgToAdd.sy, imgToAdd.sw, imgToAdd.sw, 40 + 145 * i, 580, 140, 140)
}
}
} else if (this.variant === 1) {
// Text stats
ctx.textAlign = 'left'
if (this.yearStats.topAuthors.length) {
addText('TOP AUTHORS', '24px', 'normal', tanColor, '1px', 70, 549)
for (let i = 0; i < this.yearStats.topAuthors.length; i++) {
addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 609 + i * 60, 330)
}
}
if (this.yearStats.topNarrators.length) {
addText('TOP NARRATORS', '24px', 'normal', tanColor, '1px', 430, 549)
for (let i = 0; i < this.yearStats.topNarrators.length; i++) {
addText(this.yearStats.topNarrators[i].name, '36px', 'bolder', 'white', '0px', 430, 609 + i * 60, 330)
}
}
} else if (this.variant === 2) {
// Text stats
ctx.textAlign = 'left'
if (this.yearStats.topAuthors.length) {
addText('TOP AUTHORS', '24px', 'normal', tanColor, '1px', 70, 549)
for (let i = 0; i < this.yearStats.topAuthors.length; i++) {
addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 609 + i * 60, 330)
}
}
if (this.yearStats.topGenres.length) {
addText('TOP GENRES', '24px', 'normal', tanColor, '1px', 430, 549)
for (let i = 0; i < this.yearStats.topGenres.length; i++) {
addText(this.yearStats.topGenres[i].genre, '36px', 'bolder', 'white', '0px', 430, 609 + i * 60, 330)
}
}
}
this.canvas = canvas
this.dataUrl = canvas.toDataURL('png')
},
share() {
this.canvas.toBlob((blob) => {
const file = new File([blob], 'yearinreviewserver.png', { type: blob.type })
const shareData = {
files: [file]
}
if (navigator.canShare(shareData)) {
navigator
.share(shareData)
.then(() => {
console.log('Share success')
})
.catch((error) => {
console.error('Failed to share', error)
if (error.name !== 'AbortError') {
this.$toast.error('Failed to share: ' + error.message)
}
})
} else {
this.$toast.error('Cannot share natively on this device')
}
})
},
refresh() {
this.init()
},
async init() {
this.$emit('update:processing', true)
this.yearStats = await this.$axios.$get(`/api/stats/year/${this.year}`).catch((err) => {
console.error('Failed to load stats for year', err)
this.$toast.error('Failed to load year stats')
return null
})
await this.initCanvas()
this.$emit('update:processing', false)
}
},
mounted() {
this.init()
}
}
</script>

View File

@ -0,0 +1,194 @@
<template>
<div>
<div v-if="processing" class="max-w-[600px] h-32 sm:h-[200px] flex items-center justify-center">
<widgets-loading-spinner />
</div>
<img v-else-if="dataUrl" :src="dataUrl" />
</div>
</template>
<script>
export default {
props: {
processing: Boolean,
year: Number
},
data() {
return {
canvas: null,
dataUrl: null,
yearStats: null
}
},
methods: {
async initCanvas() {
if (!this.yearStats) return
const canvas = document.createElement('canvas')
canvas.width = 600
canvas.height = 200
const ctx = canvas.getContext('2d')
const createRoundedRect = (x, y, w, h) => {
const grd1 = ctx.createLinearGradient(x, y, x + w, y + h)
grd1.addColorStop(0, '#44444455')
grd1.addColorStop(1, '#ffffff11')
ctx.fillStyle = grd1
ctx.strokeStyle = '#C0C0C088'
ctx.beginPath()
ctx.roundRect(x, y, w, h, [20])
ctx.fill()
ctx.stroke()
}
const addText = (text, fontSize, fontWeight, color, letterSpacing, x, y, maxWidth = 0) => {
ctx.fillStyle = color
ctx.font = `${fontWeight} ${fontSize} Source Sans Pro`
ctx.letterSpacing = letterSpacing
// If maxWidth is specified then continue to remove chars until under maxWidth and add ellipsis
if (maxWidth) {
let txtWidth = ctx.measureText(text).width
while (txtWidth > maxWidth) {
console.warn(`Text "${text}" is greater than max width ${maxWidth} (width:${txtWidth})`)
if (text.endsWith('...')) text = text.slice(0, -4) // Repeated checks remove 1 char at a time
else text = text.slice(0, -3) // First check remove last 3 chars
text += '...'
txtWidth = ctx.measureText(text).width
console.log(`Checking text "${text}" (width:${txtWidth})`)
}
}
ctx.fillText(text, x, y)
}
const addIcon = (icon, color, fontSize, x, y) => {
ctx.fillStyle = color
ctx.font = `${fontSize} Material Icons Outlined`
ctx.fillText(icon, x, y)
}
// Bg color
ctx.fillStyle = '#232323'
ctx.fillRect(0, 0, canvas.width, canvas.height)
// Cover image tiles
const bookCovers = this.yearStats.finishedBooksWithCovers
bookCovers.push(...this.yearStats.booksWithCovers)
if (bookCovers.length) {
let index = 0
ctx.globalAlpha = 0.25
ctx.save()
ctx.translate(canvas.width / 2, canvas.height / 2)
ctx.rotate((-Math.PI / 180) * 25)
ctx.translate(-canvas.width / 2, -canvas.height / 2)
ctx.translate(-10, -90)
for (let x = 0; x < 4; x++) {
for (let y = 0; y < 3; y++) {
const coverIndex = index % bookCovers.length
let libraryItemId = bookCovers[coverIndex]
index++
await new Promise((resolve) => {
const img = new Image()
img.crossOrigin = 'anonymous'
img.addEventListener('load', () => {
let sw = img.width
if (img.width > img.height) {
sw = img.height
}
let sx = -(sw - img.width) / 2
let sy = -(sw - img.height) / 2
ctx.drawImage(img, sx, sy, sw, sw, 155 * x, 155 * y, 155, 155)
resolve()
})
img.addEventListener('error', () => {
resolve()
})
img.src = this.$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId)
})
}
}
ctx.restore()
}
ctx.globalAlpha = 1
ctx.textBaseline = 'middle'
// Create gradient
const grd1 = ctx.createLinearGradient(0, 0, canvas.width, canvas.height)
grd1.addColorStop(0, '#000000aa')
grd1.addColorStop(1, '#cd9d49aa')
ctx.fillStyle = grd1
ctx.fillRect(0, 0, canvas.width, canvas.height)
// Top Abs icon
let tanColor = '#ffdb70'
ctx.fillStyle = tanColor
ctx.font = '42px absicons'
ctx.fillText('\ue900', 15, 36)
// Top text
addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28)
addText(`${this.year} YEAR IN REVIEW`, '18px', 'bold', 'white', '1px', 65, 51)
// Top left box
createRoundedRect(15, 75, 280, 110)
addText(this.yearStats.numBooksFinished, '48px', 'bold', 'white', '0px', 105, 120)
addText('books finished', '20px', 'normal', tanColor, '0px', 105, 155)
const readIconPath = new Path2D()
readIconPath.addPath(new Path2D('M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z'), { a: 1.5, d: 1.5, e: 55, f: 115 })
ctx.fillStyle = '#ffffff'
ctx.fill(readIconPath)
createRoundedRect(305, 75, 280, 110)
addText(this.yearStats.numBooksListened, '48px', 'bold', 'white', '0px', 400, 120)
addText('books listened to', '20px', 'normal', tanColor, '0px', 400, 155)
addIcon('local_library', 'white', '42px', 345, 130)
this.canvas = canvas
this.dataUrl = canvas.toDataURL('png')
},
share() {
this.canvas.toBlob((blob) => {
const file = new File([blob], 'yearinreviewshort.png', { type: blob.type })
const shareData = {
files: [file]
}
if (navigator.canShare(shareData)) {
navigator
.share(shareData)
.then(() => {
console.log('Share success')
})
.catch((error) => {
console.error('Failed to share', error)
if (error.name !== 'AbortError') {
this.$toast.error('Failed to share: ' + error.message)
}
})
} else {
this.$toast.error('Cannot share natively on this device')
}
})
},
refresh() {
this.init()
},
async init() {
this.$emit('update:processing', true)
this.yearStats = await this.$axios.$get(`/api/me/stats/year/${this.year}`).catch((err) => {
console.error('Failed to load stats for year', err)
this.$toast.error('Failed to load year stats')
return null
})
await this.initCanvas()
this.$emit('update:processing', false)
}
},
mounted() {
this.init()
}
}
</script>

View File

@ -6,7 +6,7 @@
<span class="text-sm font-mono">{{ ebookFiles.length }}</span> <span class="text-sm font-mono">{{ ebookFiles.length }}</span>
</div> </div>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn v-if="userIsAdmin" small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="showFullPath = !showFullPath">{{ $strings.ButtonFullPath }}</ui-btn> <ui-btn v-if="userIsAdmin" small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="toggleFullPath">{{ $strings.ButtonFullPath }}</ui-btn>
<div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showFiles ? 'transform rotate-180' : ''"> <div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showFiles ? 'transform rotate-180' : ''">
<span class="material-icons text-4xl">expand_more</span> <span class="material-icons text-4xl">expand_more</span>
</div> </div>
@ -75,6 +75,10 @@ export default {
} }
}, },
methods: { methods: {
toggleFullPath() {
this.showFullPath = !this.showFullPath
localStorage.setItem('showFullPath', this.showFullPath ? 1 : 0)
},
readEbook(fileIno) { readEbook(fileIno) {
this.$store.commit('showEReader', { libraryItem: this.libraryItem, keepProgress: false, fileId: fileIno }) this.$store.commit('showEReader', { libraryItem: this.libraryItem, keepProgress: false, fileId: fileIno })
}, },
@ -82,6 +86,10 @@ export default {
this.showFiles = !this.showFiles this.showFiles = !this.showFiles
} }
}, },
mounted() {} mounted() {
if (this.userIsAdmin) {
this.showFullPath = !!Number(localStorage.getItem('showFullPath') || 0)
}
}
} }
</script> </script>

View File

@ -6,13 +6,13 @@
<span class="text-sm font-mono">{{ files.length }}</span> <span class="text-sm font-mono">{{ files.length }}</span>
</div> </div>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn v-if="userIsAdmin" small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="showFullPath = !showFullPath">{{ $strings.ButtonFullPath }}</ui-btn> <ui-btn v-if="userIsAdmin" small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="toggleFullPath">{{ $strings.ButtonFullPath }}</ui-btn>
<div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showFiles ? 'transform rotate-180' : ''"> <div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showFiles ? 'transform rotate-180' : ''">
<span class="material-icons text-4xl">expand_more</span> <span class="material-icons text-4xl">expand_more</span>
</div> </div>
</div> </div>
<transition name="slide"> <transition name="slide">
<div class="w-full" v-show="showFiles"> <div class="w-full" v-if="showFiles">
<table class="text-sm tracksTable"> <table class="text-sm tracksTable">
<tr> <tr>
<th class="text-left px-4">{{ $strings.LabelPath }}</th> <th class="text-left px-4">{{ $strings.LabelPath }}</th>
@ -70,7 +70,7 @@ export default {
}, },
audioFiles() { audioFiles() {
if (this.libraryItem.mediaType === 'podcast') { if (this.libraryItem.mediaType === 'podcast') {
return this.libraryItem.media?.episodes.map((ep) => ep.audioFile) || [] return this.libraryItem.media?.episodes.map((ep) => ep.audioFile).filter((af) => af) || []
} }
return this.libraryItem.media?.audioFiles || [] return this.libraryItem.media?.audioFiles || []
}, },
@ -84,6 +84,10 @@ export default {
} }
}, },
methods: { methods: {
toggleFullPath() {
this.showFullPath = !this.showFullPath
localStorage.setItem('showFullPath', this.showFullPath ? 1 : 0)
},
clickBar() { clickBar() {
this.showFiles = !this.showFiles this.showFiles = !this.showFiles
}, },
@ -93,6 +97,9 @@ export default {
} }
}, },
mounted() { mounted() {
if (this.userIsAdmin) {
this.showFullPath = !!Number(localStorage.getItem('showFullPath') || 0)
}
this.showFiles = this.expanded this.showFiles = this.expanded
} }
} }

View File

@ -6,7 +6,7 @@
<span class="text-sm font-mono">{{ tracks.length }}</span> <span class="text-sm font-mono">{{ tracks.length }}</span>
</div> </div>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn v-if="userIsAdmin" small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="showFullPath = !showFullPath">{{ $strings.ButtonFullPath }}</ui-btn> <ui-btn v-if="userIsAdmin" small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="toggleFullPath">{{ $strings.ButtonFullPath }}</ui-btn>
<nuxt-link v-if="userCanUpdate && !isFile" :to="`/audiobook/${libraryItemId}/edit`" class="mr-2 md:mr-4" @mousedown.prevent> <nuxt-link v-if="userCanUpdate && !isFile" :to="`/audiobook/${libraryItemId}/edit`" class="mr-2 md:mr-4" @mousedown.prevent>
<ui-btn small color="primary">{{ $strings.ButtonManageTracks }}</ui-btn> <ui-btn small color="primary">{{ $strings.ButtonManageTracks }}</ui-btn>
</nuxt-link> </nuxt-link>
@ -74,6 +74,10 @@ export default {
} }
}, },
methods: { methods: {
toggleFullPath() {
this.showFullPath = !this.showFullPath
localStorage.setItem('showFullPath', this.showFullPath ? 1 : 0)
},
clickBar() { clickBar() {
this.showTracks = !this.showTracks this.showTracks = !this.showTracks
}, },
@ -82,6 +86,10 @@ export default {
this.showAudioFileDataModal = true this.showAudioFileDataModal = true
} }
}, },
mounted() {} mounted() {
if (this.userIsAdmin) {
this.showFullPath = !!Number(localStorage.getItem('showFullPath') || 0)
}
}
} }
</script> </script>

View File

@ -52,8 +52,6 @@
</tr> </tr>
</table> </table>
</div> </div>
<modals-account-modal ref="accountModal" v-model="showAccountModal" :account="selectedAccount" />
</div> </div>
</template> </template>
@ -62,8 +60,6 @@ export default {
data() { data() {
return { return {
users: [], users: [],
selectedAccount: null,
showAccountModal: false,
isDeletingUser: false isDeletingUser: false
} }
}, },
@ -114,13 +110,8 @@ export default {
}) })
} }
}, },
clickAddUser() {
this.selectedAccount = null
this.showAccountModal = true
},
editUser(user) { editUser(user) {
this.selectedAccount = user this.$emit('edit', user)
this.showAccountModal = true
}, },
loadUsers() { loadUsers() {
this.$axios this.$axios
@ -129,7 +120,6 @@ export default {
this.users = res.users.sort((a, b) => { this.users = res.users.sort((a, b) => {
return a.createdAt - b.createdAt return a.createdAt - b.createdAt
}) })
console.log('Loaded users', this.users)
}) })
.catch((error) => { .catch((error) => {
console.error('Failed', error) console.error('Failed', error)

View File

@ -26,11 +26,11 @@
</div> </div>
<div class="truncate max-w-48 md:max-w-md text-xs md:text-sm text-gray-300"> <div class="truncate max-w-48 md:max-w-md text-xs md:text-sm text-gray-300">
<template v-for="(author, index) in bookAuthors"> <template v-for="(author, index) in bookAuthors">
<nuxt-link :key="author.id" :to="`/author/${author.id}?library=${book.libraryId}`" class="truncate hover:underline">{{ author.name }}</nuxt-link <nuxt-link :key="author.id" :to="`/author/${author.id}`" class="truncate hover:underline">{{ author.name }}</nuxt-link
><span :key="author.id + '-comma'" v-if="index < bookAuthors.length - 1">,&nbsp;</span> ><span :key="author.id + '-comma'" v-if="index < bookAuthors.length - 1">,&nbsp;</span>
</template> </template>
</div> </div>
<p class="text-xs md:text-sm text-gray-400">{{ bookDuration }}</p> <p v-if="media.duration" class="text-xs md:text-sm text-gray-400">{{ bookDuration }}</p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -42,13 +42,10 @@ export default {
return this.$store.getters['libraries/getCurrentLibrary'] return this.$store.getters['libraries/getCurrentLibrary']
}, },
currentLibraryId() { currentLibraryId() {
return this.currentLibrary ? this.currentLibrary.id : null return this.currentLibrary?.id || null
}, },
libraries() { libraries() {
return this.$store.getters['libraries/getSortedLibraries']() return this.$store.getters['libraries/getSortedLibraries']()
},
libraryScans() {
return this.$store.state.scanners.libraryScans
} }
}, },
methods: { methods: {

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="w-full pl-2 pr-4 md:px-4 h-12 border border-white border-opacity-10 flex items-center relative -mt-px" :class="selected ? 'bg-primary bg-opacity-50' : 'hover:bg-primary hover:bg-opacity-25'" @mouseover="mouseover = true" @mouseleave="mouseover = false"> <div class="w-full pl-2 pr-4 md:px-4 h-12 border border-white border-opacity-10 flex items-center relative -mt-px" :class="selected ? 'bg-primary bg-opacity-50' : 'hover:bg-primary hover:bg-opacity-25'" @mouseover="mouseover = true" @mouseleave="mouseover = false">
<div v-show="selected" class="absolute top-0 left-0 h-full w-0.5 bg-warning z-10" /> <div v-show="selected" class="absolute top-0 left-0 h-full w-0.5 bg-warning z-10" />
<ui-library-icon v-if="!libraryScan" :icon="library.icon" :size="6" font-size="lg md:text-xl" class="text-white" :class="isHovering ? 'text-opacity-90' : 'text-opacity-50'" /> <ui-library-icon v-if="!isScanning" :icon="library.icon" :size="6" font-size="lg md:text-xl" class="text-white" :class="isHovering ? 'text-opacity-90' : 'text-opacity-50'" />
<svg v-else viewBox="0 0 24 24" class="h-6 w-6 text-white text-opacity-50 animate-spin"> <svg v-else viewBox="0 0 24 24" class="h-6 w-6 text-white text-opacity-50 animate-spin">
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" /> <path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
</svg> </svg>
@ -9,11 +9,14 @@
<div class="flex-grow" /> <div class="flex-grow" />
<!-- Scan button only shown on desktop -->
<ui-btn v-if="!isScanning && !isDeleting" color="bg" class="hidden md:block mx-2 text-xs" :padding-y="1" :padding-x="3" @click.stop="scanBtnClick">{{ this.$strings.ButtonScan }}</ui-btn>
<!-- Desktop context menu icon --> <!-- Desktop context menu icon -->
<ui-context-menu-dropdown v-if="!libraryScan && !isDeleting" :items="contextMenuItems" :icon-class="`text-1.5xl text-gray-${isHovering ? 50 : 400}`" class="!hidden md:!block" @action="contextMenuAction" /> <ui-context-menu-dropdown v-if="!isScanning && !isDeleting" :items="contextMenuItems" :icon-class="`text-1.5xl text-gray-${isHovering ? 50 : 400}`" class="!hidden md:!block" @action="contextMenuAction" />
<!-- Mobile context menu icon --> <!-- Mobile context menu icon -->
<span v-if="!libraryScan && !isDeleting" class="!block md:!hidden material-icons text-xl text-gray-300 ml-3 cursor-pointer" @click.stop="showMenu">more_vert</span> <span v-if="!isScanning && !isDeleting" class="!block md:!hidden material-icons text-xl text-gray-300 ml-3 cursor-pointer" @click.stop="showMenu">more_vert</span>
<div v-show="isDeleting" class="text-xl text-gray-300 ml-3 animate-spin"> <div v-show="isDeleting" class="text-xl text-gray-300 ml-3 animate-spin">
<svg viewBox="0 0 24 24" class="w-6 h-6"> <svg viewBox="0 0 24 24" class="w-6 h-6">
@ -48,8 +51,8 @@ export default {
isHovering() { isHovering() {
return this.mouseover && !this.dragging return this.mouseover && !this.dragging
}, },
libraryScan() { isScanning() {
return this.$store.getters['scanners/getLibraryScan'](this.library.id) return !!this.$store.getters['tasks/getRunningLibraryScanTask'](this.library.id)
}, },
mediaType() { mediaType() {
return this.library.mediaType return this.library.mediaType
@ -89,14 +92,17 @@ export default {
} }
}, },
methods: { methods: {
scanBtnClick() {
this.scan()
},
contextMenuAction({ action }) { contextMenuAction({ action }) {
this.showMobileMenu = false this.showMobileMenu = false
if (action === 'edit') { if (action === 'edit') {
this.editClick() this.editClick()
} else if (action === 'scan') { } else if (action === 'scan') {
this.scan() this.scan()
} else if (action === 'force-scan') { } else if (action === 'force-rescan') {
this.forceScan() this.scan(true)
} else if (action === 'match-books') { } else if (action === 'match-books') {
this.matchAll() this.matchAll()
} else if (action === 'delete') { } else if (action === 'delete') {
@ -121,11 +127,11 @@ export default {
editClick() { editClick() {
this.$emit('edit', this.library) this.$emit('edit', this.library)
}, },
scan() { scan(force = false) {
this.$store this.$store
.dispatch('libraries/requestLibraryScan', { libraryId: this.library.id }) .dispatch('libraries/requestLibraryScan', { libraryId: this.library.id, force })
.then(() => { .then(() => {
this.$toast.success(this.$strings.ToastLibraryScanStarted) // this.$toast.success(this.$strings.ToastLibraryScanStarted)
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to start scan', error) console.error('Failed to start scan', error)

View File

@ -21,7 +21,7 @@
</div> </div>
<div class="truncate max-w-48 md:max-w-md text-xs md:text-sm text-gray-300"> <div class="truncate max-w-48 md:max-w-md text-xs md:text-sm text-gray-300">
<template v-for="(author, index) in bookAuthors"> <template v-for="(author, index) in bookAuthors">
<nuxt-link :key="author.id" :to="`/author/${author.id}?library=${libraryItem.libraryId}`" class="truncate hover:underline">{{ author.name }}</nuxt-link <nuxt-link :key="author.id" :to="`/author/${author.id}`" class="truncate hover:underline">{{ author.name }}</nuxt-link
><span :key="author.id + '-comma'" v-if="index < bookAuthors.length - 1">,&nbsp;</span> ><span :key="author.id + '-comma'" v-if="index < bookAuthors.length - 1">,&nbsp;</span>
</template> </template>
<nuxt-link v-if="episode" :to="`/item/${libraryItem.id}`" class="truncate hover:underline">{{ mediaMetadata.title }}</nuxt-link> <nuxt-link v-if="episode" :to="`/item/${libraryItem.id}`" class="truncate hover:underline">{{ mediaMetadata.title }}</nuxt-link>

View File

@ -1,19 +1,23 @@
<template> <template>
<div class="w-full px-2 py-3 overflow-hidden relative border-b border-white border-opacity-10" @mouseover="mouseover" @mouseleave="mouseleave"> <div :id="`lazy-episode-${index}`" class="w-full h-full cursor-pointer" @mouseover="mouseover" @mouseleave="mouseleave">
<div v-if="episode" class="flex items-center cursor-pointer" :class="{ 'opacity-70': isSelected || selectionMode }" @click="clickedEpisode"> <div class="flex" @click="clickedEpisode">
<div class="flex-grow px-2"> <div class="flex-grow">
<div class="flex items-center"> <div class="flex items-center">
<span class="text-sm font-semibold">{{ title }}</span> <span class="text-sm font-semibold">{{ episodeTitle }}</span>
<widgets-podcast-type-indicator :type="episode.episodeType" /> <widgets-podcast-type-indicator :type="episodeType" />
</div> </div>
<p class="text-sm text-gray-200 episode-subtitle mt-1.5 mb-0.5" v-html="subtitle"></p> <div class="h-10 flex items-center mt-1.5 mb-0.5">
<div class="flex justify-between pt-2 max-w-xl"> <p class="text-sm text-gray-200 episode-subtitle" v-html="episodeSubtitle"></p>
<p v-if="episode.season" class="text-sm text-gray-300">Season #{{ episode.season }}</p> </div>
<p v-if="episode.episode" class="text-sm text-gray-300">Episode #{{ episode.episode }}</p> <div class="h-8 flex items-center">
<p v-if="episode.chapters?.length" class="text-sm text-gray-300">{{ episode.chapters.length }} Chapters</p> <div class="w-full inline-flex justify-between max-w-xl">
<p v-if="episode?.season" class="text-sm text-gray-300">Season #{{ episode.season }}</p>
<p v-if="episode?.episode" class="text-sm text-gray-300">Episode #{{ episode.episode }}</p>
<p v-if="episode?.chapters?.length" class="text-sm text-gray-300">{{ episode.chapters.length }} Chapters</p>
<p v-if="publishedAt" class="text-sm text-gray-300">Published {{ $formatDate(publishedAt, dateFormat) }}</p> <p v-if="publishedAt" class="text-sm text-gray-300">Published {{ $formatDate(publishedAt, dateFormat) }}</p>
</div> </div>
</div>
<div class="flex items-center pt-2"> <div class="flex items-center pt-2">
<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="userIsFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick"> <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="userIsFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick">
@ -37,10 +41,11 @@
<ui-icon-btn v-if="userCanDelete" icon="close" borderless @click="removeClick" /> <ui-icon-btn v-if="userCanDelete" icon="close" borderless @click="removeClick" />
</div> </div>
</div> </div>
<div v-if="isHovering || isSelected || selectionMode" class="hidden md:block w-12 min-w-12" /> <div v-if="isHovering || isSelected || isSelectionMode" class="hidden md:block w-12 min-w-12" />
</div> </div>
<div v-if="isSelected || selectionMode" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-10 z-10 cursor-pointer" @click.stop="clickedSelectionBg" />
<div class="hidden md:block md:w-12 md:min-w-12 md:-right-0 md:absolute md:top-0 h-full transform transition-transform z-20" :class="!isHovering && !isSelected && !selectionMode ? 'translate-x-24' : 'translate-x-0'"> <div v-if="isSelected || isSelectionMode" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-10 z-10 cursor-pointer" @click.stop="clickedSelectionBg" />
<div class="hidden md:block md:w-12 md:min-w-12 md:-right-0 md:absolute md:top-0 h-full transform transition-transform z-20" :class="!isHovering && !isSelected && !isSelectionMode ? 'translate-x-24' : 'translate-x-0'">
<div class="flex h-full items-center"> <div class="flex h-full items-center">
<div class="mx-1"> <div class="mx-1">
<ui-checkbox v-model="isSelected" @input="selectedUpdated" checkbox-bg="bg" /> <ui-checkbox v-model="isSelected" @input="selectedUpdated" checkbox-bg="bg" />
@ -55,84 +60,91 @@
<script> <script>
export default { export default {
props: { props: {
index: Number,
libraryItemId: String, libraryItemId: String,
episode: { episode: {
type: Object, type: Object,
default: () => {} default: () => null
}, }
selectionMode: Boolean
}, },
data() { data() {
return { return {
isProcessingReadUpdate: false, isProcessingReadUpdate: false,
processingRemove: false, processingRemove: false,
isHovering: false, isHovering: false,
isSelected: false isSelected: false,
isSelectionMode: false
} }
}, },
computed: { computed: {
store() {
return this.$store || this.$nuxt.$store
},
axios() {
return this.$axios || this.$nuxt.$axios
},
userCanUpdate() { userCanUpdate() {
return this.$store.getters['user/getUserCanUpdate'] return this.store.getters['user/getUserCanUpdate']
}, },
userCanDelete() { userCanDelete() {
return this.$store.getters['user/getUserCanDelete'] return this.store.getters['user/getUserCanDelete']
}, },
audioFile() { episodeId() {
return this.episode.audioFile return this.episode?.id || ''
}, },
title() { episodeTitle() {
return this.episode.title || '' return this.episode?.title || ''
}, },
subtitle() { episodeSubtitle() {
return this.episode.subtitle || this.description return this.episode?.subtitle || ''
}, },
description() { episodeType() {
return this.episode.description || '' return this.episode?.episodeType || ''
}, },
duration() { publishedAt() {
return this.$secondsToTimestamp(this.episode.duration) return this.episode?.publishedAt
}, },
libraryItemIdStreaming() { dateFormat() {
return this.$store.getters['getLibraryItemIdStreaming'] return this.store.state.serverSettings.dateFormat
},
isStreamingFromDifferentLibrary() {
return this.$store.getters['getIsStreamingFromDifferentLibrary']
},
isStreaming() {
return this.$store.getters['getIsMediaStreaming'](this.libraryItemId, this.episode.id)
},
isQueued() {
return this.$store.getters['getIsMediaQueued'](this.libraryItemId, this.episode.id)
},
streamIsPlaying() {
return this.$store.state.streamIsPlaying && this.isStreaming
}, },
itemProgress() { itemProgress() {
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId, this.episode.id) return this.store.getters['user/getUserMediaProgress'](this.libraryItemId, this.episodeId)
}, },
itemProgressPercent() { itemProgressPercent() {
return this.itemProgress ? this.itemProgress.progress : 0 return this.itemProgress?.progress || 0
}, },
userIsFinished() { userIsFinished() {
return this.itemProgress ? !!this.itemProgress.isFinished : false return !!this.itemProgress?.isFinished
},
libraryItemIdStreaming() {
return this.store.getters['getLibraryItemIdStreaming']
},
isStreamingFromDifferentLibrary() {
return this.store.getters['getIsStreamingFromDifferentLibrary']
},
isStreaming() {
return this.store.getters['getIsMediaStreaming'](this.libraryItemId, this.episodeId)
},
isQueued() {
return this.store.getters['getIsMediaQueued'](this.libraryItemId, this.episodeId)
},
streamIsPlaying() {
return this.store.state.streamIsPlaying && this.isStreaming
}, },
timeRemaining() { timeRemaining() {
if (this.streamIsPlaying) return 'Playing' if (this.streamIsPlaying) return 'Playing'
if (!this.itemProgress) return this.$elapsedPretty(this.episode.duration) if (!this.itemProgress) return this.$elapsedPretty(this.episode?.duration || 0)
if (this.userIsFinished) return 'Finished' if (this.userIsFinished) return 'Finished'
var remaining = Math.floor(this.itemProgress.duration - this.itemProgress.currentTime)
const duration = this.itemProgress.duration || this.episode?.duration || 0
const remaining = Math.floor(duration - this.itemProgress.currentTime)
return `${this.$elapsedPretty(remaining)} left` return `${this.$elapsedPretty(remaining)} left`
},
publishedAt() {
return this.episode.publishedAt
},
dateFormat() {
return this.$store.state.serverSettings.dateFormat
} }
}, },
methods: { methods: {
clickAddToPlaylist() { setSelectionMode(isSelectionMode) {
this.$emit('addToPlaylist', this.episode) this.isSelectionMode = isSelectionMode
if (!this.isSelectionMode) this.isSelected = false
}, },
clickedEpisode() { clickedEpisode() {
this.$emit('view', this.episode) this.$emit('view', this.episode)
@ -150,16 +162,23 @@ export default {
mouseleave() { mouseleave() {
this.isHovering = false this.isHovering = false
}, },
clickEdit() {
this.$emit('edit', this.episode)
},
playClick() { playClick() {
if (this.streamIsPlaying) { if (this.streamIsPlaying) {
this.$eventBus.$emit('pause-item') const eventBus = this.$eventBus || this.$nuxt.$eventBus
eventBus.$emit('pause-item')
} else { } else {
this.$emit('play', this.episode) this.$emit('play', this.episode)
} }
}, },
queueBtnClick() {
if (this.isQueued) {
// Remove from queue
this.store.commit('removeItemFromQueue', { libraryItemId: this.libraryItemId, episodeId: this.episodeId })
} else {
// Add to queue
this.$emit('addToQueue', this.episode)
}
},
toggleFinished(confirmed = false) { toggleFinished(confirmed = false) {
if (!this.userIsFinished && this.itemProgressPercent > 0 && !confirmed) { if (!this.userIsFinished && this.itemProgressPercent > 0 && !confirmed) {
const payload = { const payload = {
@ -171,37 +190,47 @@ export default {
}, },
type: 'yesNo' type: 'yesNo'
} }
this.$store.commit('globals/setConfirmPrompt', payload) this.store.commit('globals/setConfirmPrompt', payload)
return return
} }
var updatePayload = { const updatePayload = {
isFinished: !this.userIsFinished isFinished: !this.userIsFinished
} }
this.isProcessingReadUpdate = true this.isProcessingReadUpdate = true
this.$axios this.axios
.$patch(`/api/me/progress/${this.libraryItemId}/${this.episode.id}`, updatePayload) .$patch(`/api/me/progress/${this.libraryItemId}/${this.episodeId}`, updatePayload)
.then(() => { .then(() => {
this.isProcessingReadUpdate = false this.isProcessingReadUpdate = false
}) })
.catch((error) => { .catch((error) => {
console.error('Failed', error) console.error('Failed', error)
this.isProcessingReadUpdate = false this.isProcessingReadUpdate = false
this.$toast.error(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedFailed : this.$strings.ToastItemMarkedAsNotFinishedFailed) const toast = this.$toast || this.$nuxt.$toast
toast.error(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedFailed : this.$strings.ToastItemMarkedAsNotFinishedFailed)
}) })
}, },
clickAddToPlaylist() {
this.$emit('addToPlaylist', this.episode)
},
clickEdit() {
this.$emit('edit', this.episode)
},
removeClick() { removeClick() {
this.$emit('remove', this.episode) this.$emit('remove', this.episode)
}, },
queueBtnClick() { destroy() {
if (this.isQueued) { // destroy the vue listeners, etc
// Remove from queue this.$destroy()
this.$store.commit('removeItemFromQueue', { libraryItemId: this.libraryItemId, episodeId: this.episode.id })
} else { // remove the element from the DOM
// Add to queue if (this.$el && this.$el.parentNode) {
this.$emit('addToQueue', this.episode) this.$el.parentNode.removeChild(this.$el)
} } else if (this.$el && this.$el.remove) {
this.$el.remove()
} }
} }
},
mounted() {}
} }
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="w-full py-6"> <div id="lazy-episodes-table" class="w-full py-6">
<div class="flex flex-wrap flex-col md:flex-row md:items-center mb-4"> <div class="flex flex-wrap flex-col md:flex-row md:items-center mb-4">
<div class="flex items-center flex-nowrap whitespace-nowrap mb-2 md:mb-0"> <div class="flex items-center flex-nowrap whitespace-nowrap mb-2 md:mb-0">
<p class="text-lg mb-0 font-semibold">{{ $strings.HeaderEpisodes }}</p> <p class="text-lg mb-0 font-semibold">{{ $strings.HeaderEpisodes }}</p>
@ -18,28 +18,41 @@
<ui-btn :disabled="processing" small class="ml-2 h-9" @click="clearSelected">{{ $strings.ButtonCancel }}</ui-btn> <ui-btn :disabled="processing" small class="ml-2 h-9" @click="clearSelected">{{ $strings.ButtonCancel }}</ui-btn>
</template> </template>
<template v-else> <template v-else>
<controls-filter-select v-model="filterKey" :items="filterItems" class="w-36 h-9 md:ml-4" /> <controls-filter-select v-model="filterKey" :items="filterItems" class="w-36 h-9 md:ml-4" @change="filterSortChanged" />
<controls-sort-select v-model="sortKey" :descending.sync="sortDesc" :items="sortItems" class="w-44 md:w-48 h-9 ml-1 sm:ml-4" /> <controls-sort-select v-model="sortKey" :descending.sync="sortDesc" :items="sortItems" class="w-44 md:w-48 h-9 ml-1 sm:ml-4" @change="filterSortChanged" />
<div class="flex-grow md:hidden" /> <div class="flex-grow md:hidden" />
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" class="ml-1" @action="contextMenuAction" /> <ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" class="ml-1" @action="contextMenuAction" />
</template> </template>
</div> </div>
</div> </div>
<p v-if="!episodes.length" class="py-4 text-center text-lg">{{ $strings.MessageNoEpisodes }}</p> <!-- <p v-if="!episodes.length" class="py-4 text-center text-lg">{{ $strings.MessageNoEpisodes }}</p> -->
<div v-if="episodes.length" class="w-full py-3 mx-auto flex"> <div v-if="episodes.length" class="w-full py-3 mx-auto flex">
<form @submit.prevent="submit" class="flex flex-grow"> <form @submit.prevent="submit" class="flex flex-grow">
<ui-text-input v-model="search" @input="inputUpdate" type="search" :placeholder="$strings.PlaceholderSearchEpisode" class="flex-grow mr-2 text-sm md:text-base" /> <ui-text-input v-model="search" @input="inputUpdate" type="search" :placeholder="$strings.PlaceholderSearchEpisode" class="flex-grow mr-2 text-sm md:text-base" />
</form> </form>
</div> </div>
<template v-for="episode in episodesList"> <div class="relative min-h-[176px]">
<tables-podcast-episode-table-row ref="episodeRow" :key="episode.id" :episode="episode" :library-item-id="libraryItem.id" :selection-mode="isSelectionMode" class="item" @play="playEpisode" @remove="removeEpisode" @edit="editEpisode" @view="viewEpisode" @selected="episodeSelected" @addToQueue="addEpisodeToQueue" @addToPlaylist="addToPlaylist" /> <template v-for="episode in totalEpisodes">
<div :key="episode" :id="`episode-${episode - 1}`" class="w-full h-44 px-2 py-3 overflow-hidden relative border-b border-white/10">
<!-- episode is mounted here -->
</div>
</template> </template>
<div v-if="isSearching" class="w-full h-full absolute inset-0 flex justify-center py-12" :class="{ 'bg-black/50': totalEpisodes }">
<ui-loading-indicator />
</div>
<div v-else-if="!totalEpisodes" class="h-44 flex items-center justify-center">
<p class="text-lg">{{ $strings.MessageNoEpisodes }}</p>
</div>
</div>
<modals-podcast-remove-episode v-model="showPodcastRemoveModal" @input="removeEpisodeModalToggled" :library-item="libraryItem" :episodes="episodesToRemove" @clearSelected="clearSelected" /> <modals-podcast-remove-episode v-model="showPodcastRemoveModal" @input="removeEpisodeModalToggled" :library-item="libraryItem" :episodes="episodesToRemove" @clearSelected="clearSelected" />
</div> </div>
</template> </template>
<script> <script>
import Vue from 'vue'
import LazyEpisodeRow from './LazyEpisodeRow.vue'
export default { export default {
props: { props: {
libraryItem: { libraryItem: {
@ -60,13 +73,21 @@ export default {
processing: false, processing: false,
search: null, search: null,
searchTimeout: null, searchTimeout: null,
searchText: null searchText: null,
isSearching: false,
totalEpisodes: 0,
episodesPerPage: null,
episodeIndexesMounted: [],
episodeComponentRefs: {},
windowHeight: 0,
episodesTableOffsetTop: 0,
episodeRowHeight: 176
} }
}, },
watch: { watch: {
libraryItem: { libraryItem: {
handler() { handler() {
this.init() this.refresh()
} }
} }
}, },
@ -194,13 +215,19 @@ export default {
submit() {}, submit() {},
inputUpdate() { inputUpdate() {
clearTimeout(this.searchTimeout) clearTimeout(this.searchTimeout)
this.isSearching = true
let searchStart = this.searchText
this.searchTimeout = setTimeout(() => { this.searchTimeout = setTimeout(() => {
if (!this.search || !this.search.trim()) { this.isSearching = false
if (!this.search?.trim()) {
this.searchText = '' this.searchText = ''
return } else {
}
this.searchText = this.search.toLowerCase().trim() this.searchText = this.search.toLowerCase().trim()
}, 500) }
if (searchStart !== this.searchText) {
this.init()
}
}, 750)
}, },
contextMenuAction({ action }) { contextMenuAction({ action }) {
if (action === 'quick-match-episodes') { if (action === 'quick-match-episodes') {
@ -304,24 +331,30 @@ export default {
if (!val) this.episodesToRemove = [] if (!val) this.episodesToRemove = []
}, },
clearSelected() { clearSelected() {
const episodeRows = this.$refs.episodeRow
if (episodeRows && episodeRows.length) {
for (const epRow of episodeRows) {
if (epRow) epRow.isSelected = false
}
}
this.selectedEpisodes = [] this.selectedEpisodes = []
this.setSelectionModeForEpisodes()
}, },
removeSelectedEpisodes() { removeSelectedEpisodes() {
this.episodesToRemove = this.selectedEpisodes this.episodesToRemove = this.selectedEpisodes
this.showPodcastRemoveModal = true this.showPodcastRemoveModal = true
}, },
episodeSelected({ isSelected, episode }) { episodeSelected({ isSelected, episode }) {
let isSelectionModeBefore = this.isSelectionMode
if (isSelected) { if (isSelected) {
this.selectedEpisodes.push(episode) this.selectedEpisodes.push(episode)
} else { } else {
this.selectedEpisodes = this.selectedEpisodes.filter((ep) => ep.id !== episode.id) this.selectedEpisodes = this.selectedEpisodes.filter((ep) => ep.id !== episode.id)
} }
if (this.isSelectionMode !== isSelectionModeBefore) {
this.setSelectionModeForEpisodes()
}
},
setSelectionModeForEpisodes() {
for (const key in this.episodeComponentRefs) {
if (this.episodeComponentRefs[key]?.setSelectionMode) {
this.episodeComponentRefs[key].setSelectionMode(this.isSelectionMode)
}
}
}, },
playEpisode(episode) { playEpisode(episode) {
const queueItems = [] const queueItems = []
@ -367,12 +400,147 @@ export default {
this.$store.commit('globals/setSelectedEpisode', episode) this.$store.commit('globals/setSelectedEpisode', episode)
this.$store.commit('globals/setShowViewPodcastEpisodeModal', true) this.$store.commit('globals/setShowViewPodcastEpisodeModal', true)
}, },
init() { destroyEpisodeComponents() {
for (const key in this.episodeComponentRefs) {
if (this.episodeComponentRefs[key]?.destroy) {
this.episodeComponentRefs[key].destroy()
}
}
this.episodeComponentRefs = {}
this.episodeIndexesMounted = []
},
mountEpisode(index) {
const episodeEl = document.getElementById(`episode-${index}`)
if (!episodeEl) {
console.warn('Episode row el not found at ' + index)
return
}
this.episodeIndexesMounted.push(index)
if (this.episodeComponentRefs[index]) {
const episodeComponent = this.episodeComponentRefs[index]
episodeEl.appendChild(episodeComponent.$el)
if (this.isSelectionMode) {
episodeComponent.setSelectionMode(true)
if (this.selectedEpisodes.some((i) => i.id === episodeComponent.episodeId)) {
episodeComponent.isSelected = true
} else {
episodeComponent.isSelected = false
}
} else {
episodeComponent.setSelectionMode(false)
}
} else {
const _this = this
const ComponentClass = Vue.extend(LazyEpisodeRow)
const instance = new ComponentClass({
propsData: {
index,
libraryItemId: this.libraryItem.id,
episode: this.episodesList[index]
},
created() {
this.$on('selected', (payload) => {
_this.episodeSelected(payload)
})
this.$on('view', (payload) => {
_this.viewEpisode(payload)
})
this.$on('play', (payload) => {
_this.playEpisode(payload)
})
this.$on('addToQueue', (payload) => {
_this.addEpisodeToQueue(payload)
})
this.$on('remove', (payload) => {
_this.removeEpisode(payload)
})
this.$on('edit', (payload) => {
_this.editEpisode(payload)
})
this.$on('addToPlaylist', (payload) => {
_this.addToPlaylist(payload)
})
}
})
this.episodeComponentRefs[index] = instance
instance.$mount()
episodeEl.appendChild(instance.$el)
if (this.isSelectionMode) {
instance.setSelectionMode(true)
if (this.selectedEpisodes.some((i) => i.id === this.episodesList[index].id)) {
instance.isSelected = true
}
}
}
},
mountEpisodes(startIndex, endIndex) {
for (let i = startIndex; i < endIndex; i++) {
if (!this.episodeIndexesMounted.includes(i)) {
this.mountEpisode(i)
}
}
},
scroll(evt) {
if (!evt?.target?.scrollTop) return
const scrollTop = Math.max(evt.target.scrollTop - this.episodesTableOffsetTop, 0)
let firstEpisodeIndex = Math.floor(scrollTop / this.episodeRowHeight)
let lastEpisodeIndex = Math.ceil((scrollTop + this.windowHeight) / this.episodeRowHeight)
lastEpisodeIndex = Math.min(this.totalEpisodes - 1, lastEpisodeIndex)
this.episodeIndexesMounted = this.episodeIndexesMounted.filter((_index) => {
if (_index < firstEpisodeIndex || _index >= lastEpisodeIndex) {
const el = document.getElementById(`lazy-episode-${_index}`)
if (el) el.remove()
return false
}
return true
})
this.mountEpisodes(firstEpisodeIndex, lastEpisodeIndex + 1)
},
initListeners() {
const itemPageWrapper = document.getElementById('item-page-wrapper')
if (itemPageWrapper) {
itemPageWrapper.addEventListener('scroll', this.scroll)
}
},
removeListeners() {
const itemPageWrapper = document.getElementById('item-page-wrapper')
if (itemPageWrapper) {
itemPageWrapper.removeEventListener('scroll', this.scroll)
}
},
filterSortChanged() {
this.init()
},
refresh() {
this.episodesCopy = this.episodes.map((ep) => ({ ...ep })) this.episodesCopy = this.episodes.map((ep) => ({ ...ep }))
this.init()
},
init() {
this.destroyEpisodeComponents()
this.totalEpisodes = this.episodesList.length
const lazyEpisodesTableEl = document.getElementById('lazy-episodes-table')
this.episodesTableOffsetTop = (lazyEpisodesTableEl?.offsetTop || 0) + 64
this.windowHeight = window.innerHeight
this.episodesPerPage = Math.ceil(this.windowHeight / this.episodeRowHeight)
this.$nextTick(() => {
this.mountEpisodes(0, Math.min(this.episodesPerPage, this.totalEpisodes))
})
} }
}, },
mounted() { mounted() {
this.episodesCopy = this.episodes.map((ep) => ({ ...ep }))
this.initListeners()
this.init() this.init()
},
beforeDestroy() {
this.removeListeners()
} }
} }
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<nuxt-link v-if="to" :to="to" class="btn outline-none rounded-md shadow-md relative border border-gray-600 text-center" :disabled="disabled || loading" :class="classList"> <nuxt-link v-if="to" :to="to" class="abs-btn outline-none rounded-md shadow-md relative border border-gray-600 text-center" :disabled="disabled || loading" :class="classList">
<slot /> <slot />
<div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100"> <div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
<svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24"> <svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">
@ -7,7 +7,7 @@
</svg> </svg>
</div> </div>
</nuxt-link> </nuxt-link>
<button v-else class="btn outline-none rounded-md shadow-md relative border border-gray-600" :disabled="disabled || loading" :type="type" :class="classList" @mousedown.prevent @click="click"> <button v-else class="abs-btn outline-none rounded-md shadow-md relative border border-gray-600" :disabled="disabled || loading" :type="type" :class="classList" @mousedown.prevent @click="click">
<slot /> <slot />
<div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100"> <div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
<svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24"> <svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">
@ -72,23 +72,3 @@ export default {
mounted() {} mounted() {}
} }
</script> </script>
<style scoped>
.btn::before {
content: '';
position: absolute;
border-radius: 6px;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0);
transition: all 0.1s ease-in-out;
}
.btn:hover:not(:disabled)::before {
background-color: rgba(255, 255, 255, 0.1);
}
button:disabled::before {
background-color: rgba(0, 0, 0, 0.2);
}
</style>

View File

@ -2,7 +2,8 @@
<label class="flex justify-start items-center" :class="!disabled ? 'cursor-pointer' : ''"> <label class="flex justify-start items-center" :class="!disabled ? 'cursor-pointer' : ''">
<div class="border-2 rounded flex flex-shrink-0 justify-center items-center" :class="wrapperClass"> <div class="border-2 rounded flex flex-shrink-0 justify-center items-center" :class="wrapperClass">
<input v-model="selected" :disabled="disabled" type="checkbox" class="opacity-0 absolute" :class="!disabled ? 'cursor-pointer' : ''" /> <input v-model="selected" :disabled="disabled" type="checkbox" class="opacity-0 absolute" :class="!disabled ? 'cursor-pointer' : ''" />
<svg v-if="selected" class="fill-current pointer-events-none" :class="svgClass" viewBox="0 0 20 20"><path d="M0 11l2-2 5 5L18 3l2 2L7 18z" /></svg> <span v-if="partial" class="material-icons text-base leading-none text-gray-400">remove</span>
<svg v-else-if="selected" class="fill-current pointer-events-none" :class="svgClass" viewBox="0 0 20 20"><path d="M0 11l2-2 5 5L18 3l2 2L7 18z" /></svg>
</div> </div>
<div v-if="label" class="select-none" :class="[labelClassname, disabled ? 'text-gray-400' : 'text-gray-100']">{{ label }}</div> <div v-if="label" class="select-none" :class="[labelClassname, disabled ? 'text-gray-400' : 'text-gray-100']">{{ label }}</div>
</label> </label>
@ -31,7 +32,8 @@ export default {
type: String, type: String,
default: '' default: ''
}, },
disabled: Boolean disabled: Boolean,
partial: Boolean
}, },
data() { data() {
return {} return {}

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="relative w-full" v-click-outside="clickOutsideObj"> <div class="relative w-full" v-click-outside="clickOutsideObj">
<p class="text-sm font-semibold px-1" :class="disabled ? 'text-gray-300' : ''">{{ label }}</p> <p v-if="label" class="text-sm font-semibold px-1" :class="disabled ? 'text-gray-300' : ''">{{ label }}</p>
<button type="button" :aria-label="longLabel" :disabled="disabled" class="relative w-full border rounded shadow-sm pl-3 pr-8 py-2 text-left sm:text-sm" :class="buttonClass" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu"> <button type="button" :aria-label="longLabel" :disabled="disabled" class="relative w-full border rounded shadow-sm pl-3 pr-8 py-2 text-left sm:text-sm" :class="buttonClass" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
<span class="flex items-center"> <span class="flex items-center">
<span class="block truncate font-sans" :class="{ 'font-semibold': selectedSubtext, 'text-sm': small }">{{ selectedText }}</span> <span class="block truncate font-sans" :class="{ 'font-semibold': selectedSubtext, 'text-sm': small }">{{ selectedText }}</span>
@ -13,7 +13,7 @@
</button> </button>
<transition name="menu"> <transition name="menu">
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-black-200 shadow-lg max-h-56 rounded-b-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto sm:text-sm" tabindex="-1" role="listbox"> <ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-black-200 shadow-lg max-h-56 rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto sm:text-sm" tabindex="-1" role="listbox">
<template v-for="item in itemsToShow"> <template v-for="item in itemsToShow">
<li :key="item.value" class="text-gray-100 relative py-2 cursor-pointer hover:bg-black-400" :id="'listbox-option-' + item.value" role="option" tabindex="0" @keyup.enter="clickedOption(item.value)" @click="clickedOption(item.value)"> <li :key="item.value" class="text-gray-100 relative py-2 cursor-pointer hover:bg-black-400" :id="'listbox-option-' + item.value" role="option" tabindex="0" @keyup.enter="clickedOption(item.value)" @click="clickedOption(item.value)">
<div class="flex items-center"> <div class="flex items-center">
@ -64,7 +64,7 @@ export default {
}, },
itemsToShow() { itemsToShow() {
return this.items.map((i) => { return this.items.map((i) => {
if (typeof i === 'string') { if (typeof i === 'string' || typeof i === 'number') {
return { return {
text: i, text: i,
value: i value: i

View File

@ -1,10 +1,10 @@
<template> <template>
<div class="w-full"> <div class="w-full">
<label class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</label> <label v-if="label" class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</label>
<div ref="wrapper" class="relative"> <div ref="wrapper" class="relative">
<form @submit.prevent="submitForm"> <form @submit.prevent="submitForm">
<div ref="inputWrapper" class="input-wrapper flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-2" :class="disabled ? 'pointer-events-none bg-black-300 text-gray-400' : 'bg-primary'"> <div ref="inputWrapper" class="input-wrapper flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-2" :class="disabled ? 'pointer-events-none bg-black-300 text-gray-400' : 'bg-primary'">
<input ref="input" v-model="textInput" :disabled="disabled" :readonly="!editable" class="h-full w-full bg-transparent focus:outline-none px-1" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" /> <input ref="input" v-model="textInput" :disabled="disabled" :readonly="!editable" class="h-full w-full bg-transparent focus:outline-none px-1" @focus="inputFocus" @blur="inputBlur" />
</div> </div>
</form> </form>
@ -48,8 +48,6 @@ export default {
data() { data() {
return { return {
isFocused: false, isFocused: false,
// currentSearch: null,
typingTimeout: null,
textInput: null textInput: null
} }
}, },
@ -83,12 +81,6 @@ export default {
} }
}, },
methods: { methods: {
keydownInput() {
clearTimeout(this.typingTimeout)
this.typingTimeout = setTimeout(() => {
// this.currentSearch = this.textInput
}, 100)
},
setFocus() { setFocus() {
if (this.$refs.input && this.editable) this.$refs.input.focus() if (this.$refs.input && this.editable) this.$refs.input.focus()
}, },
@ -133,11 +125,9 @@ export default {
if (val && !this.items.includes(val)) { if (val && !this.items.includes(val)) {
this.$emit('newItem', val) this.$emit('newItem', val)
} }
// this.currentSearch = null
}, },
clickedOption(e, item) { clickedOption(e, item) {
this.textInput = null this.textInput = null
// this.currentSearch = null
this.input = item this.input = item
if (this.$refs.input) this.$refs.input.blur() if (this.$refs.input) this.$refs.input.blur()
} }

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" /> <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" />
</div> </div>
</form> </form>
@ -50,7 +50,11 @@ export default {
label: String, label: String,
disabled: Boolean, disabled: Boolean,
readonly: Boolean, readonly: Boolean,
showEdit: Boolean showEdit: Boolean,
menuDisabled: {
type: Boolean,
default: false
},
}, },
data() { data() {
return { return {
@ -77,7 +81,7 @@ export default {
} }
}, },
showMenu() { showMenu() {
return this.isFocused return this.isFocused && !this.menuDisabled
}, },
wrapperClass() { wrapperClass() {
var classes = [] var classes = []
@ -145,6 +149,31 @@ export default {
this.menu.style.left = boundingBox.x + 'px' this.menu.style.left = boundingBox.x + 'px'
this.menu.style.width = boundingBox.width + 'px' this.menu.style.width = boundingBox.width + 'px'
}, },
inputPaste(evt) {
setTimeout(() => {
const pastedText = evt.target?.value || ''
console.log('Pasted text=', pastedText)
const pastedItems = [
...new Set(
pastedText
.split(';')
.map((i) => i.trim())
.filter((i) => i)
)
]
// Filter out items already selected
const itemsToAdd = pastedItems.filter((i) => !this.selected.some((_i) => _i.toLowerCase() === i.toLowerCase()))
if (pastedItems.length && !itemsToAdd.length) {
this.textInput = null
this.currentSearch = null
} else {
for (const itemToAdd of itemsToAdd) {
this.insertNewItem(itemToAdd)
}
}
}, 10)
},
inputFocus() { inputFocus() {
if (!this.menu) { if (!this.menu) {
this.unmountMountMenu() this.unmountMountMenu()

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="w-full" v-click-outside="closeMenu"> <div class="w-full" v-click-outside="clickOutsideObj">
<p class="px-1 text-sm font-semibold">{{ label }}</p> <p class="px-1 text-sm font-semibold">{{ label }}</p>
<div ref="wrapper" class="relative"> <div ref="wrapper" class="relative">
<div ref="inputWrapper" style="min-height: 40px" class="flex-wrap relative w-full shadow-sm flex items-center bg-primary border border-gray-600 rounded-md px-2 py-1 cursor-pointer" @click.stop.prevent="clickWrapper" @mouseup.stop.prevent @mousedown.prevent> <div ref="inputWrapper" style="min-height: 40px" class="flex-wrap relative w-full shadow-sm flex items-center bg-primary border border-gray-600 rounded-md px-2 py-1 cursor-pointer" @click.stop.prevent="clickWrapper" @mouseup.stop.prevent @mousedown.prevent>
@ -11,15 +11,15 @@
</div> </div>
</div> </div>
<ul ref="menu" v-show="showMenu" class="absolute z-60 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label"> <transition name="menu">
<ul ref="menu" v-show="showMenu" class="absolute z-60 -mt-px w-full bg-primary border border-black-200 shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label">
<template v-for="item in items"> <template v-for="item in items">
<li :key="item.value" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent> <li :key="item.value" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent>
<div class="flex items-center"> <p class="font-normal ml-3 block truncate">{{ item.text }}</p>
<span class="font-normal ml-3 block truncate">{{ item.text }}</span>
</div> <div v-if="selected.includes(item.value)" class="text-yellow-400 absolute inset-y-0 right-0 my-auto w-5 h-5 mr-3 overflow-hidden">
<span v-if="selected.includes(item.value)" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4">
<span class="material-icons text-xl">checkmark</span> <span class="material-icons text-xl">checkmark</span>
</span> </div>
</li> </li>
</template> </template>
<li v-if="!items.length" class="text-gray-50 select-none relative py-2 pr-9" role="option"> <li v-if="!items.length" class="text-gray-50 select-none relative py-2 pr-9" role="option">
@ -28,6 +28,7 @@
</div> </div>
</li> </li>
</ul> </ul>
</transition>
</div> </div>
</div> </div>
</template> </template>
@ -48,7 +49,12 @@ export default {
data() { data() {
return { return {
showMenu: false, showMenu: false,
menu: null menu: null,
clickOutsideObj: {
handler: this.closeMenu,
events: ['mousedown'],
isActive: true
}
} }
}, },
computed: { computed: {

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" /> <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" />
</div> </div>
</form> </form>
@ -112,6 +112,7 @@ export default {
return !!this.selected.find((i) => i.id === itemValue) return !!this.selected.find((i) => i.id === itemValue)
}, },
search() { search() {
if (!this.textInput) return
this.currentSearch = this.textInput this.currentSearch = this.textInput
const dataToSearch = this.filterData[this.filterKey] || [] const dataToSearch = this.filterData[this.filterKey] || []
@ -165,6 +166,34 @@ export default {
this.menu.style.left = boundingBox.x + 'px' this.menu.style.left = boundingBox.x + 'px'
this.menu.style.width = boundingBox.width + 'px' this.menu.style.width = boundingBox.width + 'px'
}, },
inputPaste(evt) {
setTimeout(() => {
const pastedText = evt.target?.value || ''
console.log('Pasted text=', pastedText)
const pastedItems = [
...new Set(
pastedText
.split(';')
.map((i) => i.trim())
.filter((i) => i)
)
]
// Filter out items already selected
const itemsToAdd = pastedItems.filter((i) => !this.selected.some((_i) => _i[this.textKey].toLowerCase() === i.toLowerCase()))
if (pastedItems.length && !itemsToAdd.length) {
this.textInput = null
this.currentSearch = null
} else {
for (const [index, itemToAdd] of itemsToAdd.entries()) {
this.insertNewItem({
id: `new-${Date.now()}-${index}`,
name: itemToAdd
})
}
}
}, 10)
},
inputFocus() { inputFocus() {
if (!this.menu) { if (!this.menu) {
this.unmountMountMenu() this.unmountMountMenu()

View File

@ -68,6 +68,7 @@ export default {
methods: { methods: {
clear() { clear() {
this.inputValue = '' this.inputValue = ''
this.$emit('clear')
}, },
focused() { focused() {
this.isFocused = true this.isFocused = true

View File

@ -15,6 +15,13 @@ export default {
type: String, type: String,
default: 'right' default: 'right'
}, },
/**
* Delay showing the tooltip after X milliseconds of hovering
*/
delayOnShow: {
type: Number,
default: 0
},
disabled: Boolean disabled: Boolean
}, },
data() { data() {
@ -22,7 +29,8 @@ export default {
tooltip: null, tooltip: null,
tooltipId: null, tooltipId: null,
isShowing: false, isShowing: false,
hideTimeout: null hideTimeout: null,
delayOnShowTimeout: null
} }
}, },
watch: { watch: {
@ -59,29 +67,44 @@ export default {
this.tooltip = tooltip this.tooltip = tooltip
}, },
setTooltipPosition(tooltip) { setTooltipPosition(tooltip) {
var boxChow = this.$refs.box.getBoundingClientRect() const boxRect = this.$refs.box.getBoundingClientRect()
const shouldMount = !tooltip.isConnected
var shouldMount = !tooltip.isConnected
// Calculate size of tooltip // Calculate size of tooltip
if (shouldMount) document.body.appendChild(tooltip) if (shouldMount) document.body.appendChild(tooltip)
var { width, height } = tooltip.getBoundingClientRect() const tooltipRect = tooltip.getBoundingClientRect()
if (shouldMount) tooltip.remove() if (shouldMount) tooltip.remove()
var top = 0 // Subtracting scrollbar size
var left = 0 const windowHeight = window.innerHeight - 8
const windowWidth = window.innerWidth - 8
let top = 0
let left = 0
if (this.direction === 'right') { if (this.direction === 'right') {
top = boxChow.top - height / 2 + boxChow.height / 2 top = Math.max(0, boxRect.top - tooltipRect.height / 2 + boxRect.height / 2)
left = boxChow.left + boxChow.width + 4 left = Math.max(0, boxRect.left + boxRect.width + 4)
} else if (this.direction === 'bottom') { } else if (this.direction === 'bottom') {
top = boxChow.top + boxChow.height + 4 top = Math.max(0, boxRect.top + boxRect.height + 4)
left = boxChow.left - width / 2 + boxChow.width / 2 left = Math.max(0, boxRect.left - tooltipRect.width / 2 + boxRect.width / 2)
} else if (this.direction === 'top') { } else if (this.direction === 'top') {
top = boxChow.top - height - 4 top = Math.max(0, boxRect.top - tooltipRect.height - 4)
left = boxChow.left - width / 2 + boxChow.width / 2 left = Math.max(0, boxRect.left - tooltipRect.width / 2 + boxRect.width / 2)
} else if (this.direction === 'left') { } else if (this.direction === 'left') {
top = boxChow.top - height / 2 + boxChow.height / 2 top = Math.max(0, boxRect.top - tooltipRect.height / 2 + boxRect.height / 2)
left = boxChow.left - width - 4 left = Math.max(0, boxRect.left - tooltipRect.width - 4)
} }
// Shift left if tooltip would overflow the window on the right
if (left + tooltipRect.width > windowWidth) {
left -= left + tooltipRect.width - windowWidth
}
// Shift up if tooltip would overflow the window on the bottom
if (top + tooltipRect.height > windowHeight) {
top -= top + tooltipRect.height - windowHeight
}
tooltip.style.top = top + 'px' tooltip.style.top = top + 'px'
tooltip.style.left = left + 'px' tooltip.style.left = left + 'px'
}, },
@ -107,15 +130,33 @@ export default {
this.isShowing = false this.isShowing = false
}, },
cancelHide() { cancelHide() {
if (this.hideTimeout) clearTimeout(this.hideTimeout) clearTimeout(this.hideTimeout)
}, },
mouseover() { mouseover() {
if (!this.isShowing) this.showTooltip() if (this.isShowing || this.disabled) return
if (this.delayOnShow) {
if (this.delayOnShowTimeout) {
// Delay already running
return
}
this.delayOnShowTimeout = setTimeout(() => {
this.showTooltip()
this.delayOnShowTimeout = null
}, this.delayOnShow)
} else {
this.showTooltip()
}
}, },
mouseleave() { mouseleave() {
if (this.isShowing) { if (!this.isShowing) {
this.hideTimeout = setTimeout(this.hideTooltip, 100) clearTimeout(this.delayOnShowTimeout)
this.delayOnShowTimeout = null
return
} }
this.hideTimeout = setTimeout(this.hideTooltip, 100)
} }
}, },
beforeDestroy() { beforeDestroy() {

View File

@ -1,33 +0,0 @@
<template>
<button class="bg-error text-white px-2 py-1 shadow-md" @click="$emit('click', $event)">Cancel</button>
</template>
<script>
export default {
data() {
return {}
},
computed: {},
methods: {},
mounted() {}
}
</script>
<style>
.Vue-Toastification__close-button.cancel-scan-btn {
background-color: rgb(255, 82, 82);
color: white;
font-size: 0.9rem;
opacity: 1;
padding: 0px 10px;
border-radius: 6px;
font-weight: normal;
font-family: 'Open Sans';
margin-left: 10px;
opacity: 0.3;
}
.Vue-Toastification__close-button.cancel-scan-btn:hover {
background-color: rgb(235, 65, 65);
opacity: 1;
}
</style>

View File

@ -9,6 +9,8 @@
<span class="material-icons text-1.5xl" aria-label="Activities" role="button">notifications</span> <span class="material-icons text-1.5xl" aria-label="Activities" role="button">notifications</span>
</ui-tooltip> </ui-tooltip>
</div> </div>
<div v-if="showUnseenSuccessIndicator" class="w-2 h-2 rounded-full bg-success pointer-events-none absolute -top-1 -right-0.5" />
<div v-if="showUnseenSuccessIndicator" class="w-2 h-2 rounded-full bg-success/50 pointer-events-none absolute animate-ping -top-1 -right-0.5" />
</button> </button>
<transition name="menu"> <transition name="menu">
<div class="sm:w-80 w-full relative"> <div class="sm:w-80 w-full relative">
@ -46,7 +48,8 @@ export default {
isActive: true isActive: true
}, },
showMenu: false, showMenu: false,
disabled: false disabled: false,
tasksSeen: []
} }
}, },
computed: { computed: {
@ -60,12 +63,20 @@ export default {
// return just the tasks that are running or failed (or show success) in the last 1 minute // return just the tasks that are running or failed (or show success) in the last 1 minute
const tasks = this.tasks.filter((t) => !t.isFinished || ((t.isFailed || t.showSuccess) && t.finishedAt > new Date().getTime() - 1000 * 60)) || [] const tasks = this.tasks.filter((t) => !t.isFinished || ((t.isFailed || t.showSuccess) && t.finishedAt > new Date().getTime() - 1000 * 60)) || []
return tasks.sort((a, b) => b.startedAt - a.startedAt) return tasks.sort((a, b) => b.startedAt - a.startedAt)
},
showUnseenSuccessIndicator() {
return this.tasksToShow.some((t) => t.isFinished && !t.isFailed && !this.tasksSeen.includes(t.id))
} }
}, },
methods: { methods: {
clickShowMenu() { clickShowMenu() {
if (this.disabled) return if (this.disabled) return
this.showMenu = !this.showMenu this.showMenu = !this.showMenu
if (this.showMenu) {
this.tasksToShow.forEach((t) => {
if (!this.tasksSeen.includes(t.id)) this.tasksSeen.push(t.id)
})
}
}, },
clickedOutside() { clickedOutside() {
this.showMenu = false this.showMenu = false
@ -83,9 +94,20 @@ export default {
default: default:
return '' return ''
} }
},
taskFinished(task) {
// add task as seen if menu is open when it finished
if (this.showMenu && !this.tasksSeen.includes(task.id)) {
this.tasksSeen.push(task.id)
}
} }
}, },
mounted() {} mounted() {
this.$root.socket?.on('task_finished', this.taskFinished)
},
beforeDestroy() {
this.$root.socket?.off('task_finished', this.taskFinished)
}
} }
</script> </script>

View File

@ -19,14 +19,13 @@
<modals-authors-edit-modal /> <modals-authors-edit-modal />
<modals-batch-quick-match-model /> <modals-batch-quick-match-model />
<modals-rssfeed-open-close-modal /> <modals-rssfeed-open-close-modal />
<modals-raw-cover-preview-modal />
<prompt-confirm /> <prompt-confirm />
<readers-reader /> <readers-reader />
</div> </div>
</template> </template>
<script> <script>
import CloseButton from '@/components/widgets/CloseButton'
export default { export default {
middleware: 'authenticated', middleware: 'authenticated',
data() { data() {
@ -123,22 +122,6 @@ export default {
init(payload) { init(payload) {
console.log('Init Payload', payload) console.log('Init Payload', payload)
// Start scans currently running
if (payload.librariesScanning) {
payload.librariesScanning.forEach((libraryScan) => {
this.scanStart(libraryScan)
})
}
// Remove any current scans that are no longer running
var currentScans = [...this.$store.state.scanners.libraryScans]
currentScans.forEach((ls) => {
if (!payload.librariesScanning || !payload.librariesScanning.find((_ls) => _ls.id === ls.id)) {
this.$toast.dismiss(ls.toastId)
this.$store.commit('scanners/remove', ls)
}
})
if (payload.usersOnline) { if (payload.usersOnline) {
this.$store.commit('users/setUsersOnline', payload.usersOnline) this.$store.commit('users/setUsersOnline', payload.usersOnline)
} }
@ -228,54 +211,6 @@ export default {
this.libraryItemAdded(ab) this.libraryItemAdded(ab)
}) })
}, },
scanComplete(data) {
console.log('Scan complete received', data)
let message = `${data.type === 'match' ? 'Match' : 'Scan'} "${data.name}" complete!`
let toastType = 'success'
if (data.error) {
message = `${data.type === 'match' ? 'Match' : 'Scan'} "${data.name}" finished with error:\n${data.error}`
toastType = 'error'
} else if (data.results) {
var scanResultMsgs = []
var results = data.results
if (results.added) scanResultMsgs.push(`${results.added} added`)
if (results.updated) scanResultMsgs.push(`${results.updated} updated`)
if (results.removed) scanResultMsgs.push(`${results.removed} removed`)
if (results.missing) scanResultMsgs.push(`${results.missing} missing`)
if (!scanResultMsgs.length) message += '\nEverything was up to date'
else message += '\n' + scanResultMsgs.join('\n')
} else {
message = `${data.type === 'match' ? 'Match' : 'Scan'} "${data.name}" was canceled`
}
var existingScan = this.$store.getters['scanners/getLibraryScan'](data.id)
if (existingScan && !isNaN(existingScan.toastId)) {
this.$toast.update(existingScan.toastId, { content: message, options: { timeout: 5000, type: toastType, closeButton: false, onClose: () => null } }, true)
} else {
this.$toast[toastType](message, { timeout: 5000 })
}
this.$store.commit('scanners/remove', data)
},
onScanToastCancel(id) {
this.$root.socket.emit('cancel_scan', id)
},
scanStart(data) {
data.toastId = this.$toast(`${data.type === 'match' ? 'Matching' : 'Scanning'} "${data.name}"...`, { timeout: false, type: 'info', draggable: false, closeOnClick: false, closeButton: CloseButton, closeButtonClassName: 'cancel-scan-btn', showCloseButtonOnHover: false, onClose: () => this.onScanToastCancel(data.id) })
this.$store.commit('scanners/addUpdate', data)
},
scanProgress(data) {
var existingScan = this.$store.getters['scanners/getLibraryScan'](data.id)
if (existingScan && !isNaN(existingScan.toastId)) {
data.toastId = existingScan.toastId
this.$toast.update(existingScan.toastId, { content: `Scanning "${existingScan.name}"... ${data.progress.progress || 0}%`, options: { timeout: false } }, true)
} else {
data.toastId = this.$toast(`Scanning "${data.name}"...`, { timeout: false, type: 'info', draggable: false, closeOnClick: false, closeButton: CloseButton, closeButtonClassName: 'cancel-scan-btn', showCloseButtonOnHover: false, onClose: () => this.onScanToastCancel(data.id) })
}
this.$store.commit('scanners/addUpdate', data)
},
taskStarted(task) { taskStarted(task) {
console.log('Task started', task) console.log('Task started', task)
this.$store.commit('tasks/addUpdateTask', task) this.$store.commit('tasks/addUpdateTask', task)
@ -458,11 +393,6 @@ export default {
this.socket.on('playlist_updated', this.playlistUpdated) this.socket.on('playlist_updated', this.playlistUpdated)
this.socket.on('playlist_removed', this.playlistRemoved) this.socket.on('playlist_removed', this.playlistRemoved)
// Scan Listeners
this.socket.on('scan_start', this.scanStart)
this.socket.on('scan_complete', this.scanComplete)
this.socket.on('scan_progress', this.scanProgress)
// Task Listeners // Task Listeners
this.socket.on('task_started', this.taskStarted) this.socket.on('task_started', this.taskStarted)
this.socket.on('task_finished', this.taskFinished) this.socket.on('task_finished', this.taskFinished)

View File

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

View File

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

View File

@ -19,8 +19,8 @@
<div class="w-full h-px bg-white/10 my-4" /> <div class="w-full h-px bg-white/10 my-4" />
<p v-if="!isGuest" class="mb-4 text-lg">{{ $strings.HeaderChangePassword }}</p> <p v-if="showChangePasswordForm" class="mb-4 text-lg">{{ $strings.HeaderChangePassword }}</p>
<form v-if="!isGuest" @submit.prevent="submitChangePassword"> <form v-if="showChangePasswordForm" @submit.prevent="submitChangePassword">
<ui-text-input-with-label v-model="password" :disabled="changingPassword" type="password" :label="$strings.LabelPassword" class="my-2" /> <ui-text-input-with-label v-model="password" :disabled="changingPassword" type="password" :label="$strings.LabelPassword" class="my-2" />
<ui-text-input-with-label v-model="newPassword" :disabled="changingPassword" type="password" :label="$strings.LabelNewPassword" class="my-2" /> <ui-text-input-with-label v-model="newPassword" :disabled="changingPassword" type="password" :label="$strings.LabelNewPassword" class="my-2" />
<ui-text-input-with-label v-model="confirmPassword" :disabled="changingPassword" type="password" :label="$strings.LabelConfirmPassword" class="my-2" /> <ui-text-input-with-label v-model="confirmPassword" :disabled="changingPassword" type="password" :label="$strings.LabelConfirmPassword" class="my-2" />
@ -68,6 +68,13 @@ export default {
}, },
isGuest() { isGuest() {
return this.usertype === 'guest' return this.usertype === 'guest'
},
isPasswordAuthEnabled() {
const activeAuthMethods = this.$store.getters['getServerSetting']('authActiveAuthMethods') || []
return activeAuthMethods.includes('local')
},
showChangePasswordForm() {
return !this.isGuest && this.isPasswordAuthEnabled
} }
}, },
methods: { methods: {

View File

@ -3,7 +3,7 @@
<div class="max-w-6xl mx-auto"> <div class="max-w-6xl mx-auto">
<div class="flex flex-wrap sm:flex-nowrap justify-center mb-6"> <div class="flex flex-wrap sm:flex-nowrap justify-center mb-6">
<div class="w-48 min-w-48"> <div class="w-48 min-w-48">
<div class="w-full h-52"> <div class="w-full h-60">
<covers-author-image :author="author" rounded="0" /> <covers-author-image :author="author" rounded="0" />
</div> </div>
</div> </div>
@ -17,7 +17,7 @@
</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">{{ author.description }}</p> <p class="text-white max-w-3xl text-sm leading-5 whitespace-pre-wrap">{{ author.description }}</p>
</div> </div>
</div> </div>
@ -44,7 +44,7 @@
<script> <script>
export default { export default {
async asyncData({ store, app, params, redirect, query }) { async asyncData({ store, app, params, redirect, query }) {
const author = await app.$axios.$get(`/api/authors/${params.id}?library=${query.library || store.state.libraries.currentLibraryId}&include=items,series`).catch((error) => { const author = await app.$axios.$get(`/api/authors/${params.id}?include=items,series`).catch((error) => {
console.error('Failed to get author', error) console.error('Failed to get author', error)
return null return null
}) })

View File

@ -57,6 +57,7 @@ export default {
else if (pageName === 'item-metadata-utils') return this.$strings.HeaderItemMetadataUtils else if (pageName === 'item-metadata-utils') return this.$strings.HeaderItemMetadataUtils
else if (pageName === 'rss-feeds') return this.$strings.HeaderRSSFeeds else if (pageName === 'rss-feeds') return this.$strings.HeaderRSSFeeds
else if (pageName === 'email') return this.$strings.HeaderEmail else if (pageName === 'email') return this.$strings.HeaderEmail
else if (pageName === 'authentication') return this.$strings.HeaderAuthentication
} }
return this.$strings.HeaderSettings return this.$strings.HeaderSettings
} }

View File

@ -0,0 +1,270 @@
<template>
<div id="authentication-settings">
<app-settings-content :header-text="$strings.HeaderAuthentication">
<div class="w-full border border-white/10 rounded-xl p-4 my-4 bg-primary/25">
<div class="flex items-center">
<ui-checkbox v-model="enableLocalAuth" checkbox-bg="bg" />
<p class="text-lg pl-4">{{ $strings.HeaderPasswordAuthentication }}</p>
</div>
</div>
<div class="w-full border border-white/10 rounded-xl p-4 my-4 bg-primary/25">
<div class="flex items-center">
<ui-checkbox v-model="enableOpenIDAuth" checkbox-bg="bg" />
<p class="text-lg pl-4">{{ $strings.HeaderOpenIDConnectAuthentication }}</p>
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
<a href="https://www.audiobookshelf.org/guides/oidc_authentication" target="_blank" class="inline-flex">
<span class="material-icons text-xl w-5 text-gray-200">help_outline</span>
</a>
</ui-tooltip>
</div>
<transition name="slide">
<div v-if="enableOpenIDAuth" class="flex flex-wrap pt-4">
<div class="w-full flex items-center mb-2">
<div class="flex-grow">
<ui-text-input-with-label ref="issuerUrl" v-model="newAuthSettings.authOpenIDIssuerURL" :disabled="savingSettings" :label="'Issuer URL'" />
</div>
<div class="w-36 mx-1 mt-[1.375rem]">
<ui-btn class="h-[2.375rem] text-sm inline-flex items-center justify-center w-full" type="button" :padding-y="0" :padding-x="4" @click.stop="autoPopulateOIDCClick">
<span class="material-icons text-base">auto_fix_high</span>
<span class="whitespace-nowrap break-keep pl-1">Auto-populate</span></ui-btn
>
</div>
</div>
<ui-text-input-with-label ref="authorizationUrl" v-model="newAuthSettings.authOpenIDAuthorizationURL" :disabled="savingSettings" :label="'Authorize URL'" class="mb-2" />
<ui-text-input-with-label ref="tokenUrl" v-model="newAuthSettings.authOpenIDTokenURL" :disabled="savingSettings" :label="'Token URL'" class="mb-2" />
<ui-text-input-with-label ref="userInfoUrl" v-model="newAuthSettings.authOpenIDUserInfoURL" :disabled="savingSettings" :label="'Userinfo URL'" class="mb-2" />
<ui-text-input-with-label ref="jwksUrl" v-model="newAuthSettings.authOpenIDJwksURL" :disabled="savingSettings" :label="'JWKS URL'" class="mb-2" />
<ui-text-input-with-label ref="logoutUrl" v-model="newAuthSettings.authOpenIDLogoutURL" :disabled="savingSettings" :label="'Logout URL'" class="mb-2" />
<ui-text-input-with-label ref="openidClientId" v-model="newAuthSettings.authOpenIDClientID" :disabled="savingSettings" :label="'Client ID'" class="mb-2" />
<ui-text-input-with-label ref="openidClientSecret" v-model="newAuthSettings.authOpenIDClientSecret" :disabled="savingSettings" :label="'Client Secret'" class="mb-2" />
<ui-multi-select ref="redirectUris" v-model="newAuthSettings.authOpenIDMobileRedirectURIs" :items="newAuthSettings.authOpenIDMobileRedirectURIs" :label="$strings.LabelMobileRedirectURIs" class="mb-2" :menuDisabled="true" :disabled="savingSettings" />
<p class="pl-4 text-sm text-gray-300 mb-2" v-html="$strings.LabelMobileRedirectURIsDescription" />
<ui-text-input-with-label ref="buttonTextInput" v-model="newAuthSettings.authOpenIDButtonText" :disabled="savingSettings" :label="$strings.LabelButtonText" class="mb-2" />
<div class="flex items-center pt-1 mb-2">
<div class="w-44">
<ui-dropdown v-model="newAuthSettings.authOpenIDMatchExistingBy" small :items="matchingExistingOptions" :label="$strings.LabelMatchExistingUsersBy" :disabled="savingSettings" />
</div>
<p class="pl-4 text-sm text-gray-300 mt-5">{{ $strings.LabelMatchExistingUsersByDescription }}</p>
</div>
<div class="flex items-center py-4 px-1">
<ui-toggle-switch labeledBy="auto-redirect-toggle" v-model="newAuthSettings.authOpenIDAutoLaunch" :disabled="savingSettings" />
<p id="auto-redirect-toggle" class="pl-4 whitespace-nowrap">{{ $strings.LabelAutoLaunch }}</p>
<p class="pl-4 text-sm text-gray-300" v-html="$strings.LabelAutoLaunchDescription" />
</div>
<div class="flex items-center py-4 px-1">
<ui-toggle-switch labeledBy="auto-register-toggle" v-model="newAuthSettings.authOpenIDAutoRegister" :disabled="savingSettings" />
<p id="auto-register-toggle" class="pl-4 whitespace-nowrap">{{ $strings.LabelAutoRegister }}</p>
<p class="pl-4 text-sm text-gray-300">{{ $strings.LabelAutoRegisterDescription }}</p>
</div>
</div>
</transition>
</div>
<div class="w-full flex items-center justify-end p-4">
<ui-btn color="success" :padding-x="8" small class="text-base" :loading="savingSettings" @click="saveSettings">{{ $strings.ButtonSave }}</ui-btn>
</div>
</app-settings-content>
</div>
</template>
<script>
export default {
async asyncData({ store, redirect, app }) {
if (!store.getters['user/getIsAdminOrUp']) {
redirect('/')
return
}
const authSettings = await app.$axios.$get('/api/auth-settings').catch((error) => {
console.error('Failed', error)
return null
})
if (!authSettings) {
redirect('/config')
return
}
return {
authSettings
}
},
data() {
return {
enableLocalAuth: false,
enableOpenIDAuth: false,
savingSettings: false,
newAuthSettings: {}
}
},
computed: {
authMethods() {
return this.authSettings.authActiveAuthMethods || []
},
matchingExistingOptions() {
return [
{
text: 'Do not match',
value: null
},
{
text: 'Match by email',
value: 'email'
},
{
text: 'Match by username',
value: 'username'
}
]
}
},
methods: {
autoPopulateOIDCClick() {
if (!this.newAuthSettings.authOpenIDIssuerURL) {
this.$toast.error('Issuer URL required')
return
}
// Remove trailing slash
let issuerUrl = this.newAuthSettings.authOpenIDIssuerURL
if (issuerUrl.endsWith('/')) issuerUrl = issuerUrl.slice(0, -1)
// If the full config path is on the issuer url then remove it
if (issuerUrl.endsWith('/.well-known/openid-configuration')) {
issuerUrl = issuerUrl.replace('/.well-known/openid-configuration', '')
this.newAuthSettings.authOpenIDIssuerURL = this.newAuthSettings.authOpenIDIssuerURL.replace('/.well-known/openid-configuration', '')
}
this.$axios
.$get(`/auth/openid/config?issuer=${issuerUrl}`)
.then((data) => {
if (data.issuer) this.newAuthSettings.authOpenIDIssuerURL = data.issuer
if (data.authorization_endpoint) this.newAuthSettings.authOpenIDAuthorizationURL = data.authorization_endpoint
if (data.token_endpoint) this.newAuthSettings.authOpenIDTokenURL = data.token_endpoint
if (data.userinfo_endpoint) this.newAuthSettings.authOpenIDUserInfoURL = data.userinfo_endpoint
if (data.end_session_endpoint) this.newAuthSettings.authOpenIDLogoutURL = data.end_session_endpoint
if (data.jwks_uri) this.newAuthSettings.authOpenIDJwksURL = data.jwks_uri
})
.catch((error) => {
console.error('Failed to receive data', error)
const errorMsg = error.response?.data || 'Unknown error'
this.$toast.error(errorMsg)
})
},
validateOpenID() {
let isValid = true
if (!this.newAuthSettings.authOpenIDIssuerURL) {
this.$toast.error('Issuer URL required')
isValid = false
}
if (!this.newAuthSettings.authOpenIDAuthorizationURL) {
this.$toast.error('Authorize URL required')
isValid = false
}
if (!this.newAuthSettings.authOpenIDTokenURL) {
this.$toast.error('Token URL required')
isValid = false
}
if (!this.newAuthSettings.authOpenIDUserInfoURL) {
this.$toast.error('Userinfo URL required')
isValid = false
}
if (!this.newAuthSettings.authOpenIDJwksURL) {
this.$toast.error('JWKS URL required')
isValid = false
}
if (!this.newAuthSettings.authOpenIDClientID) {
this.$toast.error('Client ID required')
isValid = false
}
if (!this.newAuthSettings.authOpenIDClientSecret) {
this.$toast.error('Client Secret required')
isValid = false
}
function isValidRedirectURI(uri) {
// Check for somestring://someother/string
const pattern = new RegExp('^\\w+://[\\w\\.-]+$', 'i')
return pattern.test(uri)
}
const uris = this.newAuthSettings.authOpenIDMobileRedirectURIs
if (uris.includes('*') && uris.length > 1) {
this.$toast.error('Mobile Redirect URIs: Asterisk (*) must be the only entry if used')
isValid = false
} else {
uris.forEach((uri) => {
if (uri !== '*' && !isValidRedirectURI(uri)) {
this.$toast.error(`Mobile Redirect URIs: Invalid URI ${uri}`)
isValid = false
}
})
}
return isValid
},
async saveSettings() {
if (!this.enableLocalAuth && !this.enableOpenIDAuth) {
this.$toast.error('Must have at least one authentication method enabled')
return
}
if (this.enableOpenIDAuth && !this.validateOpenID()) {
return
}
this.newAuthSettings.authActiveAuthMethods = []
if (this.enableLocalAuth) this.newAuthSettings.authActiveAuthMethods.push('local')
if (this.enableOpenIDAuth) this.newAuthSettings.authActiveAuthMethods.push('openid')
this.savingSettings = true
this.$axios
.$patch('/api/auth-settings', this.newAuthSettings)
.then((data) => {
this.$store.commit('setServerSettings', data.serverSettings)
if (data.updated) {
this.$toast.success('Server settings updated')
} else {
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
}
})
.catch((error) => {
console.error('Failed to update server settings', error)
this.$toast.error('Failed to update server settings')
})
.finally(() => {
this.savingSettings = false
})
},
init() {
this.newAuthSettings = {
...this.authSettings
}
this.enableLocalAuth = this.authMethods.includes('local')
this.enableOpenIDAuth = this.authMethods.includes('openid')
}
},
mounted() {
this.init()
}
}
</script>
<style>
#authentication-settings code {
font-size: 0.8rem;
border-radius: 6px;
background-color: rgb(82, 82, 82);
color: white;
padding: 2px 4px;
white-space: nowrap;
}
</style>

View File

@ -51,8 +51,14 @@
</div> </div>
</app-settings-content> </app-settings-content>
<app-settings-content :header-text="$strings.HeaderEreaderDevices" showAddButton :description="''" @clicked="addNewDeviceClick"> <app-settings-content :header-text="$strings.HeaderEreaderDevices" :description="''">
<table v-if="existingEReaderDevices.length" class="tracksTable my-4"> <template #header-items>
<div class="flex-grow" />
<ui-btn color="primary" small @click="addNewDeviceClick">{{ $strings.ButtonAddDevice }}</ui-btn>
</template>
<table v-if="existingEReaderDevices.length" class="tracksTable mt-4">
<tr> <tr>
<th class="text-left">{{ $strings.LabelName }}</th> <th class="text-left">{{ $strings.LabelName }}</th>
<th class="text-left">{{ $strings.LabelEmail }}</th> <th class="text-left">{{ $strings.LabelEmail }}</th>

View File

@ -47,10 +47,56 @@
<p class="pl-4" id="settings-chromecast-support">{{ $strings.LabelSettingsChromecastSupport }}</p> <p class="pl-4" id="settings-chromecast-support">{{ $strings.LabelSettingsChromecastSupport }}</p>
</div> </div>
<div class="w-44 mb-2"> <div class="pt-4">
<ui-dropdown v-model="newServerSettings.metadataFileFormat" small :items="metadataFileFormats" label="Metadata File Format" @input="updateMetadataFileFormat" :disabled="updatingServerSettings" /> <h2 class="font-semibold">{{ $strings.HeaderSettingsScanner }}</h2>
</div> </div>
<div class="flex items-center py-2">
<ui-toggle-switch labeledBy="settings-parse-subtitles" v-model="newServerSettings.scannerParseSubtitle" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerParseSubtitle', val)" />
<ui-tooltip :text="$strings.LabelSettingsParseSubtitlesHelp">
<p class="pl-4">
<span id="settings-parse-subtitles">{{ $strings.LabelSettingsParseSubtitles }}</span>
<span class="material-icons icon-text">info_outlined</span>
</p>
</ui-tooltip>
</div>
<div class="flex items-center py-2">
<ui-toggle-switch labeledBy="settings-find-covers" v-model="newServerSettings.scannerFindCovers" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerFindCovers', val)" />
<ui-tooltip :text="$strings.LabelSettingsFindCoversHelp">
<p class="pl-4">
<span id="settings-find-covers">{{ $strings.LabelSettingsFindCovers }}</span>
<span class="material-icons icon-text">info_outlined</span>
</p>
</ui-tooltip>
<div class="flex-grow" />
</div>
<div v-if="newServerSettings.scannerFindCovers" class="w-44 ml-14 mb-2">
<ui-dropdown v-model="newServerSettings.scannerCoverProvider" small :items="providers" label="Cover Provider" @input="updateScannerCoverProvider" :disabled="updatingServerSettings" />
</div>
<div class="flex items-center py-2">
<ui-toggle-switch labeledBy="settings-prefer-matched-metadata" v-model="newServerSettings.scannerPreferMatchedMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferMatchedMetadata', val)" />
<ui-tooltip :text="$strings.LabelSettingsPreferMatchedMetadataHelp">
<p class="pl-4">
<span id="settings-prefer-matched-metadata">{{ $strings.LabelSettingsPreferMatchedMetadata }}</span>
<span class="material-icons icon-text">info_outlined</span>
</p>
</ui-tooltip>
</div>
<div class="flex items-center py-2">
<ui-toggle-switch labeledBy="settings-disable-watcher" v-model="scannerEnableWatcher" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerDisableWatcher', !val)" />
<ui-tooltip :text="$strings.LabelSettingsEnableWatcherHelp">
<p class="pl-4">
<span id="settings-disable-watcher">{{ $strings.LabelSettingsEnableWatcher }}</span>
<span class="material-icons icon-text">info_outlined</span>
</p>
</ui-tooltip>
</div>
</div>
<div class="flex-1">
<div class="pt-4"> <div class="pt-4">
<h2 class="font-semibold">{{ $strings.HeaderSettingsDisplay }}</h2> <h2 class="font-semibold">{{ $strings.HeaderSettingsDisplay }}</h2>
</div> </div>
@ -88,86 +134,6 @@
<div class="py-2"> <div class="py-2">
<ui-dropdown :label="$strings.LabelLanguageDefaultServer" ref="langDropdown" v-model="newServerSettings.language" :items="$languageCodeOptions" small class="max-w-52" @input="updateServerLanguage" /> <ui-dropdown :label="$strings.LabelLanguageDefaultServer" ref="langDropdown" v-model="newServerSettings.language" :items="$languageCodeOptions" small class="max-w-52" @input="updateServerLanguage" />
</div> </div>
</div>
<div class="flex-1">
<div class="pt-4">
<h2 class="font-semibold">{{ $strings.HeaderSettingsScanner }}</h2>
</div>
<div class="flex items-center py-2">
<ui-toggle-switch labeledBy="settings-parse-subtitles" v-model="newServerSettings.scannerParseSubtitle" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerParseSubtitle', val)" />
<ui-tooltip :text="$strings.LabelSettingsParseSubtitlesHelp">
<p class="pl-4">
<span id="settings-parse-subtitles">{{ $strings.LabelSettingsParseSubtitles }}</span>
<span class="material-icons icon-text">info_outlined</span>
</p>
</ui-tooltip>
</div>
<div class="flex items-center py-2">
<ui-toggle-switch labeledBy="settings-find-covers" v-model="newServerSettings.scannerFindCovers" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerFindCovers', val)" />
<ui-tooltip :text="$strings.LabelSettingsFindCoversHelp">
<p class="pl-4">
<span id="settings-find-covers">{{ $strings.LabelSettingsFindCovers }}</span>
<span class="material-icons icon-text">info_outlined</span>
</p>
</ui-tooltip>
<div class="flex-grow" />
</div>
<div v-if="newServerSettings.scannerFindCovers" class="w-44 ml-14 mb-2">
<ui-dropdown v-model="newServerSettings.scannerCoverProvider" small :items="providers" label="Cover Provider" @input="updateScannerCoverProvider" :disabled="updatingServerSettings" />
</div>
<div class="flex items-center py-2">
<ui-toggle-switch labeledBy="settings-overdrive-media-markers" v-model="newServerSettings.scannerPreferOverdriveMediaMarker" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferOverdriveMediaMarker', val)" />
<ui-tooltip :text="$strings.LabelSettingsOverdriveMediaMarkersHelp">
<p class="pl-4">
<span id="settings-overdrive-media-markers">{{ $strings.LabelSettingsOverdriveMediaMarkers }}</span>
<span class="material-icons icon-text">info_outlined</span>
</p>
</ui-tooltip>
</div>
<div class="flex items-center py-2">
<ui-toggle-switch labeledBy="settings-prefer-audio-metadata" v-model="newServerSettings.scannerPreferAudioMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferAudioMetadata', val)" />
<ui-tooltip :text="$strings.LabelSettingsPreferAudioMetadataHelp">
<p class="pl-4">
<span id="settings-prefer-audio-metadata">{{ $strings.LabelSettingsPreferAudioMetadata }}</span>
<span class="material-icons icon-text">info_outlined</span>
</p>
</ui-tooltip>
</div>
<div class="flex items-center py-2">
<ui-toggle-switch labeledBy="settings-prefer-opf-metadata" v-model="newServerSettings.scannerPreferOpfMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferOpfMetadata', val)" />
<ui-tooltip :text="$strings.LabelSettingsPreferOPFMetadataHelp">
<p class="pl-4">
<span id="settings-prefer-opf-metadata">{{ $strings.LabelSettingsPreferOPFMetadata }}</span>
<span class="material-icons icon-text">info_outlined</span>
</p>
</ui-tooltip>
</div>
<div class="flex items-center py-2">
<ui-toggle-switch labeledBy="settings-prefer-matched-metadata" v-model="newServerSettings.scannerPreferMatchedMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferMatchedMetadata', val)" />
<ui-tooltip :text="$strings.LabelSettingsPreferMatchedMetadataHelp">
<p class="pl-4">
<span id="settings-prefer-matched-metadata">{{ $strings.LabelSettingsPreferMatchedMetadata }}</span>
<span class="material-icons icon-text">info_outlined</span>
</p>
</ui-tooltip>
</div>
<div class="flex items-center py-2">
<ui-toggle-switch labeledBy="settings-disable-watcher" v-model="scannerEnableWatcher" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerDisableWatcher', !val)" />
<ui-tooltip :text="$strings.LabelSettingsEnableWatcherHelp">
<p class="pl-4">
<span id="settings-disable-watcher">{{ $strings.LabelSettingsEnableWatcher }}</span>
<span class="material-icons icon-text">info_outlined</span>
</p>
</ui-tooltip>
</div>
<!-- old experimental features --> <!-- old experimental features -->
<!-- <div class="pt-4"> <!-- <div class="pt-4">
@ -212,9 +178,9 @@
</a> </a>
<p class="pl-4 pr-2 text-sm text-yellow-400"> <p class="pl-4 pr-2 text-sm text-yellow-400">
{{ $strings.MessageJoinUsOn }} {{ $strings.MessageJoinUsOn }}
<a class="underline" href="https://discord.gg/pJsjuNCKRq" target="_blank">discord</a> <a class="underline" href="https://discord.gg/HQgCbd6E75" target="_blank">discord</a>
</p> </p>
<a href="https://discord.gg/pJsjuNCKRq" target="_blank" class="text-white hover:text-gray-200 hover:scale-150 hover:rotate-6 transform duration-500"> <a href="https://discord.gg/HQgCbd6E75" target="_blank" class="text-white hover:text-gray-200 hover:scale-150 hover:rotate-6 transform duration-500">
<svg width="31" height="24" viewBox="0 0 71 55" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="31" height="24" viewBox="0 0 71 55" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)"> <g clip-path="url(#clip0)">
<path <path
@ -267,17 +233,7 @@ export default {
hasPrefixesChanged: false, hasPrefixesChanged: false,
newServerSettings: {}, newServerSettings: {},
showConfirmPurgeCache: false, showConfirmPurgeCache: false,
savingPrefixes: false, savingPrefixes: false
metadataFileFormats: [
{
text: '.json',
value: 'json'
},
{
text: '.abs',
value: 'abs'
}
]
} }
}, },
watch: { watch: {
@ -359,10 +315,6 @@ export default {
updateServerLanguage(val) { updateServerLanguage(val) {
this.updateSettingsKey('language', val) this.updateSettingsKey('language', val)
}, },
updateMetadataFileFormat(val) {
if (this.serverSettings.metadataFileFormat === val) return
this.updateSettingsKey('metadataFileFormat', val)
},
updateSettingsKey(key, val) { updateSettingsKey(key, val) {
if (key === 'scannerDisableWatcher') { if (key === 'scannerDisableWatcher') {
this.newServerSettings.scannerDisableWatcher = val this.newServerSettings.scannerDisableWatcher = val

View File

@ -1,7 +1,18 @@
<template> <template>
<div> <div>
<app-settings-content :header-text="$strings.HeaderLibraries" show-add-button @clicked="setShowLibraryModal"> <app-settings-content :header-text="$strings.HeaderLibraries">
<tables-library-libraries-table @showLibraryModal="setShowLibraryModal" /> <template #header-items>
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
<a href="https://www.audiobookshelf.org/guides/library_creation" 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="setShowLibraryModal()">{{ $strings.ButtonAddLibrary }}</ui-btn>
</template>
<tables-library-libraries-table @showLibraryModal="setShowLibraryModal" class="pt-2" />
</app-settings-content> </app-settings-content>
<modals-libraries-edit-modal v-model="showLibraryModal" :library="selectedLibrary" /> <modals-libraries-edit-modal v-model="showLibraryModal" :library="selectedLibrary" />
</div> </div>

View File

@ -1,7 +1,15 @@
<template> <template>
<div> <div>
<app-settings-content :header-text="$strings.HeaderRSSFeeds"> <app-settings-content :header-text="$strings.HeaderRSSFeeds">
<div v-if="feeds.length" class="block max-w-full"> <template #header-items>
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
<a href="https://www.audiobookshelf.org/guides/rss_feeds" target="_blank" class="inline-flex">
<span class="material-icons text-xl w-5 text-gray-200">help_outline</span>
</a>
</ui-tooltip>
</template>
<div v-if="feeds.length" class="block max-w-full pt-2">
<table class="rssFeedsTable text-xs"> <table class="rssFeedsTable text-xs">
<tr class="bg-primary bg-opacity-40 h-12"> <tr class="bg-primary bg-opacity-40 h-12">
<th class="w-16 min-w-16"></th> <th class="w-16 min-w-16"></th>

View File

@ -5,37 +5,72 @@
<ui-dropdown v-model="selectedUser" :items="userItems" :label="$strings.LabelFilterByUser" small class="max-w-48" @input="updateUserFilter" /> <ui-dropdown v-model="selectedUser" :items="userItems" :label="$strings.LabelFilterByUser" small class="max-w-48" @input="updateUserFilter" />
</div> </div>
<div v-if="listeningSessions.length" class="block max-w-full"> <div v-if="listeningSessions.length" class="block max-w-full relative">
<table class="userSessionsTable"> <table class="userSessionsTable">
<tr class="bg-primary bg-opacity-40"> <tr class="bg-primary bg-opacity-40">
<th class="w-48 min-w-48 text-left">{{ $strings.LabelItem }}</th> <th class="w-6 min-w-6 text-left hidden md:table-cell h-11">
<th class="w-20 min-w-20 text-left hidden md:table-cell">{{ $strings.LabelUser }}</th> <ui-checkbox v-model="isAllSelected" :partial="numSelected > 0 && !isAllSelected" small checkbox-bg="bg" />
<th class="w-32 min-w-32 text-left hidden md:table-cell">{{ $strings.LabelPlayMethod }}</th> </th>
<th class="w-32 min-w-32 text-left hidden sm:table-cell">{{ $strings.LabelDeviceInfo }}</th> <th v-if="numSelected" class="flex-grow text-left" :colspan="7">
<th class="w-32 min-w-32">{{ $strings.LabelTimeListened }}</th> <div class="flex items-center">
<th class="w-16 min-w-16">{{ $strings.LabelLastTime }}</th> <p>{{ $getString('MessageSelected', [numSelected]) }}</p>
<th class="flex-grow hidden sm:table-cell">{{ $strings.LabelLastUpdate }}</th> <div class="flex-grow" />
<ui-btn small color="error" :loading="deletingSessions" @click.stop="removeSessionsClick">{{ $strings.ButtonRemove }}</ui-btn>
</div>
</th>
<th v-if="!numSelected" class="flex-grow sm:flex-grow-0 sm:w-48 sm:max-w-48 text-left group cursor-pointer" @click.stop="sortColumn('displayTitle')">
<div class="inline-flex items-center">
{{ $strings.LabelItem }} <span :class="{ 'opacity-0 group-hover:opacity-30': !isSortSelected('displayTitle') }" class="material-icons text-base pl-px">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span>
</div>
</th>
<th v-if="!numSelected" class="w-20 min-w-20 text-left hidden md:table-cell">{{ $strings.LabelUser }}</th>
<th v-if="!numSelected" class="w-26 min-w-26 text-left hidden md:table-cell group cursor-pointer" @click.stop="sortColumn('playMethod')">
<div class="inline-flex items-center">
{{ $strings.LabelPlayMethod }} <span :class="{ 'opacity-0 group-hover:opacity-30': !isSortSelected('playMethod') }" class="material-icons text-base pl-px">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span>
</div>
</th>
<th v-if="!numSelected" class="w-32 min-w-32 text-left hidden sm:table-cell">{{ $strings.LabelDeviceInfo }}</th>
<th v-if="!numSelected" class="w-24 min-w-24 sm:w-32 sm:min-w-32 group cursor-pointer" @click.stop="sortColumn('timeListening')">
<div class="inline-flex items-center">
{{ $strings.LabelTimeListened }} <span :class="{ 'opacity-0 group-hover:opacity-30': !isSortSelected('timeListening') }" class="material-icons text-base pl-px hidden sm:inline-block">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span>
</div>
</th>
<th v-if="!numSelected" class="w-24 min-w-24 group cursor-pointer" @click.stop="sortColumn('currentTime')">
<div class="inline-flex items-center">
{{ $strings.LabelLastTime }} <span :class="{ 'opacity-0 group-hover:opacity-30': !isSortSelected('currentTime') }" class="material-icons text-base pl-px hidden sm:inline-block">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span>
</div>
</th>
<th v-if="!numSelected" class="flex-grow hidden sm:table-cell cursor-pointer group" @click.stop="sortColumn('updatedAt')">
<div class="inline-flex items-center">
{{ $strings.LabelLastUpdate }} <span :class="{ 'opacity-0 group-hover:opacity-30': !isSortSelected('updatedAt') }" class="material-icons text-base pl-px">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span>
</div>
</th>
</tr> </tr>
<tr v-for="session in listeningSessions" :key="session.id" class="cursor-pointer" @click="showSession(session)"> <tr v-for="session in listeningSessions" :key="session.id" :class="{ selected: session.selected }" class="cursor-pointer" @click="clickSessionRow(session)">
<td class="py-1 max-w-48"> <td class="hidden md:table-cell py-1 max-w-6 relative">
<ui-checkbox v-model="session.selected" small checkbox-bg="bg" />
<!-- overlay of the checkbox so that the entire box is clickable -->
<div class="absolute inset-0 w-full h-full" @click.stop="session.selected = !session.selected" />
</td>
<td class="py-1 flex-grow sm:flex-grow-0 sm:w-48 sm:max-w-48">
<p class="text-xs text-gray-200 truncate">{{ session.displayTitle }}</p> <p class="text-xs text-gray-200 truncate">{{ session.displayTitle }}</p>
<p class="text-xs text-gray-400 truncate">{{ session.displayAuthor }}</p> <p class="text-xs text-gray-400 truncate">{{ session.displayAuthor }}</p>
</td> </td>
<td class="hidden md:table-cell"> <td class="hidden md:table-cell w-20 min-w-20">
<p v-if="filteredUserUsername" class="text-xs">{{ filteredUserUsername }}</p> <p v-if="filteredUserUsername" class="text-xs">{{ filteredUserUsername }}</p>
<p v-else class="text-xs">{{ session.user ? session.user.username : 'N/A' }}</p> <p v-else class="text-xs">{{ session.user ? session.user.username : 'N/A' }}</p>
</td> </td>
<td class="hidden md:table-cell"> <td class="hidden md:table-cell w-26 min-w-26">
<p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p> <p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p>
</td> </td>
<td class="hidden sm:table-cell"> <td class="hidden sm:table-cell w-32 min-w-32">
<p class="text-xs" v-html="getDeviceInfoString(session.deviceInfo)" /> <p class="text-xs" v-html="getDeviceInfoString(session.deviceInfo)" />
</td> </td>
<td class="text-center"> <td class="text-center w-24 min-w-24 sm:w-32 sm:min-w-32">
<p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p> <p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p>
</td> </td>
<td class="text-center hover:underline" @click.stop="clickCurrentTime(session)"> <td class="text-center hover:underline w-24 min-w-24" @click.stop="clickCurrentTime(session)">
<p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p> <p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p>
</td> </td>
<td class="text-center hidden sm:table-cell"> <td class="text-center hidden sm:table-cell">
@ -45,10 +80,22 @@
</td> </td>
</tr> </tr>
</table> </table>
<div class="flex items-center justify-end my-2"> <!-- table bottom options -->
<ui-icon-btn icon="arrow_back_ios_new" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage === 0" @click="prevPage" /> <div class="flex items-center my-2">
<p class="text-sm mx-1">Page {{ currentPage + 1 }} of {{ numPages }}</p> <div class="flex-grow" />
<ui-icon-btn icon="arrow_forward_ios" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage >= numPages - 1" @click="nextPage" /> <div class="hidden sm:inline-flex items-center">
<p class="text-sm">{{ $strings.LabelRowsPerPage }}</p>
<ui-dropdown v-model="itemsPerPage" :items="itemsPerPageOptions" small class="w-24 mx-2" @input="updatedItemsPerPage" />
</div>
<div class="inline-flex items-center">
<p class="text-sm mx-2">Page {{ currentPage + 1 }} of {{ numPages }}</p>
<ui-icon-btn icon="arrow_back_ios_new" :size="9" icon-font-size="1rem" class="mx-1" :disabled="currentPage === 0" @click="prevPage" />
<ui-icon-btn icon="arrow_forward_ios" :size="9" icon-font-size="1rem" class="mx-1" :disabled="currentPage >= numPages - 1" @click="nextPage" />
</div>
</div>
<div v-if="deletingSessions || loading" class="absolute inset-0 w-full h-full flex items-center justify-center">
<ui-loading-indicator />
</div> </div>
</div> </div>
<p v-else class="text-white text-opacity-50">{{ $strings.MessageNoListeningSessions }}</p> <p v-else class="text-white text-opacity-50">{{ $strings.MessageNoListeningSessions }}</p>
@ -128,6 +175,7 @@ export default {
}, },
data() { data() {
return { return {
loading: false,
showSessionModal: false, showSessionModal: false,
selectedSession: null, selectedSession: null,
listeningSessions: [], listeningSessions: [],
@ -138,7 +186,11 @@ export default {
itemsPerPage: 10, itemsPerPage: 10,
userFilter: null, userFilter: null,
selectedUser: '', selectedUser: '',
processingGoToTimestamp: false sortBy: 'updatedAt',
sortDesc: true,
processingGoToTimestamp: false,
deletingSessions: false,
itemsPerPageOptions: [10, 25, 50, 100]
} }
}, },
computed: { computed: {
@ -162,9 +214,85 @@ export default {
}, },
timeFormat() { timeFormat() {
return this.$store.state.serverSettings.timeFormat return this.$store.state.serverSettings.timeFormat
},
numSelected() {
return this.listeningSessions.filter((s) => s.selected).length
},
isAllSelected: {
get() {
return this.numSelected === this.listeningSessions.length
},
set(val) {
this.setSelectionForAll(val)
}
} }
}, },
methods: { methods: {
isSortSelected(column) {
return this.sortBy === column
},
sortColumn(column) {
if (this.sortBy === column) {
this.sortDesc = !this.sortDesc
} else {
this.sortBy = column
}
this.loadSessions(this.currentPage)
},
removeSelectedSessions() {
if (!this.numSelected) return
this.deletingSessions = true
let isAllSessions = this.isAllSelected
const payload = {
sessions: this.listeningSessions.filter((s) => s.selected).map((s) => s.id)
}
this.$axios
.$post(`/api/sessions/batch/delete`, payload)
.then(() => {
this.$toast.success('Sessions removed')
if (isAllSessions) {
// If all sessions were removed from the current page then go to the previous page
if (this.currentPage > 0) {
this.currentPage--
}
this.loadSessions(this.currentPage)
} else {
// Filter out the deleted sessions
this.listeningSessions = this.listeningSessions.filter((ls) => !payload.sessions.includes(ls.id))
}
})
.catch((error) => {
const errorMsg = error.response?.data || 'Failed to remove sessions'
this.$toast.error(errorMsg)
})
.finally(() => {
this.deletingSessions = false
})
},
removeSessionsClick() {
if (!this.numSelected) return
const payload = {
message: this.$getString('MessageConfirmRemoveListeningSessions', [this.numSelected]),
callback: (confirmed) => {
if (confirmed) {
this.removeSelectedSessions()
}
},
type: 'yesNo'
}
this.$store.commit('globals/setConfirmPrompt', payload)
},
setSelectionForAll(val) {
this.listeningSessions = this.listeningSessions.map((s) => {
s.selected = val
return s
})
},
updatedItemsPerPage() {
this.currentPage = 0
this.loadSessions(this.currentPage)
},
closedSession() { closedSession() {
this.loadOpenSessions() this.loadOpenSessions()
}, },
@ -252,6 +380,13 @@ export default {
nextPage() { nextPage() {
this.loadSessions(this.currentPage + 1) this.loadSessions(this.currentPage + 1)
}, },
clickSessionRow(session) {
if (this.numSelected > 0) {
session.selected = !session.selected
} else {
this.showSession(session)
}
},
showSession(session) { showSession(session) {
this.selectedSession = session this.selectedSession = session
this.showSessionModal = true this.showSessionModal = true
@ -274,11 +409,21 @@ export default {
return 'Unknown' return 'Unknown'
}, },
async loadSessions(page) { async loadSessions(page) {
const userFilterQuery = this.selectedUser ? `&user=${this.selectedUser}` : '' this.loading = true
const data = await this.$axios.$get(`/api/sessions?page=${page}&itemsPerPage=${this.itemsPerPage}${userFilterQuery}`).catch((err) => { const urlSearchParams = new URLSearchParams()
urlSearchParams.set('page', page)
urlSearchParams.set('itemsPerPage', this.itemsPerPage)
urlSearchParams.set('sort', this.sortBy)
urlSearchParams.set('desc', this.sortDesc ? '1' : '0')
if (this.selectedUser) {
urlSearchParams.set('user', this.selectedUser)
}
const data = await this.$axios.$get(`/api/sessions?${urlSearchParams.toString()}`).catch((err) => {
console.error('Failed to load listening sessions', err) console.error('Failed to load listening sessions', err)
return null return null
}) })
this.loading = false
if (!data) { if (!data) {
this.$toast.error('Failed to load listening sessions') this.$toast.error('Failed to load listening sessions')
return return
@ -287,8 +432,13 @@ export default {
this.numPages = data.numPages this.numPages = data.numPages
this.total = data.total this.total = data.total
this.currentPage = data.page this.currentPage = data.page
this.listeningSessions = data.sessions this.listeningSessions = data.sessions.map((ls) => {
this.userFilter = data.userFilter return {
...ls,
selected: false
}
})
this.userFilter = data.userId
}, },
async loadOpenSessions() { async loadOpenSessions() {
const data = await this.$axios.$get('/api/sessions/open').catch((err) => { const data = await this.$axios.$get('/api/sessions/open').catch((err) => {
@ -326,15 +476,18 @@ export default {
.userSessionsTable tr:first-child { .userSessionsTable tr:first-child {
background-color: #272727; background-color: #272727;
} }
.userSessionsTable tr:not(:first-child) { .userSessionsTable tr:not(:first-child):not(.selected) {
background-color: #373838; background-color: #373838;
} }
.userSessionsTable tr:not(:first-child):nth-child(odd) { .userSessionsTable tr:not(:first-child):nth-child(odd):not(.selected):not(:hover) {
background-color: #2f2f2f; background-color: #2f2f2f;
} }
.userSessionsTable tr:hover:not(:first-child) { .userSessionsTable tr:hover:not(:first-child) {
background-color: #474747; background-color: #474747;
} }
.userSessionsTable tr.selected {
background-color: #474747;
}
.userSessionsTable td { .userSessionsTable td {
padding: 4px 8px; padding: 4px 8px;
} }

View File

@ -1,6 +1,9 @@
<template> <template>
<div> <div>
<app-settings-content :header-text="$strings.HeaderYourStats"> <!-- Year in review banner shown at the top in December and January -->
<stats-year-in-review-banner v-if="showYearInReviewBanner" />
<app-settings-content :header-text="$strings.HeaderYourStats" class="!mb-4">
<div class="flex justify-center"> <div class="flex justify-center">
<div class="flex p-2"> <div class="flex p-2">
<svg class="hidden sm:block h-14 w-14 lg:h-18 lg:w-18" viewBox="0 0 24 24"> <svg class="hidden sm:block h-14 w-14 lg:h-18 lg:w-18" viewBox="0 0 24 24">
@ -63,6 +66,9 @@
</div> </div>
<stats-heatmap v-if="listeningStats" :days-listening="listeningStats.days" class="my-2" /> <stats-heatmap v-if="listeningStats" :days-listening="listeningStats.days" class="my-2" />
</app-settings-content> </app-settings-content>
<!-- Year in review banner shown at the bottom Feb - Nov -->
<stats-year-in-review-banner v-if="!showYearInReviewBanner" />
</div> </div>
</template> </template>
@ -71,7 +77,8 @@ export default {
data() { data() {
return { return {
listeningStats: null, listeningStats: null,
windowWidth: 0 windowWidth: 0,
showYearInReviewBanner: false
} }
}, },
watch: { watch: {
@ -119,6 +126,12 @@ export default {
console.error('Failed to load listening sesions', err) console.error('Failed to load listening sesions', err)
return [] return []
}) })
let month = new Date().getMonth()
// January and December show year in review banner
if (month === 11 || month === 0) {
this.showYearInReviewBanner = true
}
} }
}, },
mounted() { mounted() {

View File

@ -1,7 +1,19 @@
<template> <template>
<div> <div>
<app-settings-content :header-text="$strings.HeaderUsers" show-add-button @clicked="setShowUserModal"> <app-settings-content :header-text="$strings.HeaderUsers">
<tables-users-table /> <template #header-items>
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
<a href="https://www.audiobookshelf.org/guides/users" 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="setShowUserModal()">{{ $strings.ButtonAddUser }}</ui-btn>
</template>
<tables-users-table class="pt-2" @edit="setShowUserModal" />
</app-settings-content> </app-settings-content>
<modals-account-modal ref="accountModal" v-model="showAccountModal" :account="selectedAccount" /> <modals-account-modal ref="accountModal" v-model="showAccountModal" :account="selectedAccount" />
</div> </div>

View File

@ -1,23 +1,23 @@
<template> <template>
<div id="page-wrapper" class="bg-bg page overflow-hidden" :class="streamLibraryItem ? 'streaming' : ''"> <div id="page-wrapper" class="bg-bg page overflow-hidden" :class="streamLibraryItem ? 'streaming' : ''">
<div class="w-full h-full overflow-y-auto px-2 py-6 lg:p-8"> <div id="item-page-wrapper" class="w-full h-full overflow-y-auto px-2 py-6 lg:p-8">
<div class="flex flex-col lg:flex-row max-w-6xl mx-auto"> <div class="flex flex-col lg:flex-row max-w-6xl mx-auto">
<div class="w-full flex justify-center lg:block lg:w-52" style="min-width: 208px"> <div class="w-full flex justify-center lg:block lg:w-52" style="min-width: 208px">
<div class="relative" style="height: fit-content"> <div class="relative group" style="height: fit-content">
<covers-book-cover :library-item="libraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-book-cover class="relative group-hover:brightness-75 transition cursor-pointer" expand-on-click :library-item="libraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<!-- Item Progress Bar --> <!-- Item Progress Bar -->
<div v-if="!isPodcast" class="absolute bottom-0 left-0 h-1.5 shadow-sm z-10" :class="userIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: 208 * progressPercent + 'px' }"></div> <div v-if="!isPodcast" class="absolute bottom-0 left-0 h-1.5 shadow-sm z-10" :class="userIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: 208 * progressPercent + 'px' }"></div>
<!-- Item Cover Overlay --> <!-- Item Cover Overlay -->
<div class="absolute top-0 left-0 w-full h-full z-10 bg-black bg-opacity-30 opacity-0 hover:opacity-100 transition-opacity" @mousedown.prevent @mouseup.prevent> <div class="absolute top-0 left-0 w-full h-full z-10 opacity-0 group-hover:opacity-100 pointer-events-none">
<div v-show="showPlayButton && !isStreaming" class="h-full flex items-center justify-center pointer-events-none"> <div v-show="showPlayButton && !isStreaming" class="h-full flex items-center justify-center pointer-events-none">
<div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto cursor-pointer" @click.stop.prevent="playItem"> <div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto cursor-pointer" @click.stop.prevent="playItem">
<span class="material-icons text-4xl">play_circle_filled</span> <span class="material-icons text-4xl">play_circle_filled</span>
</div> </div>
</div> </div>
<span class="absolute bottom-2.5 right-2.5 z-10 material-icons text-lg cursor-pointer text-white text-opacity-75 hover:text-opacity-100 hover:scale-110 transform duration-200" @click="showEditCover">edit</span> <span class="absolute bottom-2.5 right-2.5 z-10 material-icons text-lg cursor-pointer text-white text-opacity-75 hover:text-opacity-100 hover:scale-110 transform duration-200 pointer-events-auto" @click="showEditCover">edit</span>
</div> </div>
</div> </div>
</div> </div>
@ -42,7 +42,7 @@
<nuxt-link v-for="(artist, index) in musicArtists" :key="index" :to="`/artist/${$encode(artist)}`" class="hover:underline">{{ artist }}<span v-if="index < musicArtists.length - 1">,&nbsp;</span></nuxt-link> <nuxt-link v-for="(artist, index) in musicArtists" :key="index" :to="`/artist/${$encode(artist)}`" class="hover:underline">{{ artist }}<span v-if="index < musicArtists.length - 1">,&nbsp;</span></nuxt-link>
</p> </p>
<p v-else-if="authors.length" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl max-w-[calc(100vw-2rem)] overflow-hidden overflow-ellipsis"> <p v-else-if="authors.length" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl max-w-[calc(100vw-2rem)] overflow-hidden overflow-ellipsis">
by <nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}?library=${libraryItem.libraryId}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">,&nbsp;</span></nuxt-link> by <nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">,&nbsp;</span></nuxt-link>
</p> </p>
<p v-else class="mb-2 mt-0.5 text-gray-200 text-xl">by Unknown</p> <p v-else class="mb-2 mt-0.5 text-gray-200 text-xl">by Unknown</p>
</template> </template>
@ -124,7 +124,7 @@
</ui-context-menu-dropdown> </ui-context-menu-dropdown>
</div> </div>
<div class="my-4 max-w-2xl"> <div class="my-4 w-full">
<p class="text-base text-gray-100 whitespace-pre-line">{{ description }}</p> <p class="text-base text-gray-100 whitespace-pre-line">{{ description }}</p>
</div> </div>
@ -136,7 +136,7 @@
<widgets-audiobook-data v-if="tracks.length" :library-item-id="libraryItemId" :is-file="isFile" :media="media" /> <widgets-audiobook-data v-if="tracks.length" :library-item-id="libraryItemId" :is-file="isFile" :media="media" />
<tables-podcast-episodes-table v-if="isPodcast" :library-item="libraryItem" /> <tables-podcast-lazy-episodes-table v-if="isPodcast" :library-item="libraryItem" />
<tables-chapters-table v-if="chapters.length" :library-item="libraryItem" class="mt-6" /> <tables-chapters-table v-if="chapters.length" :library-item="libraryItem" class="mt-6" />
@ -682,13 +682,15 @@ export default {
}, },
deleteLibraryItem() { deleteLibraryItem() {
const payload = { const payload = {
message: 'This will delete the library item from the database and your file system. Are you sure?', message: this.$strings.MessageConfirmDeleteLibraryItem,
checkboxLabel: 'Delete from file system. Uncheck to only remove from database.', checkboxLabel: this.$strings.LabelDeleteFromFileSystemCheckbox,
yesButtonText: this.$strings.ButtonDelete, yesButtonText: this.$strings.ButtonDelete,
yesButtonColor: 'error', yesButtonColor: 'error',
checkboxDefaultValue: true, checkboxDefaultValue: !Number(localStorage.getItem('softDeleteDefault') || 0),
callback: (confirmed, hardDelete) => { callback: (confirmed, hardDelete) => {
if (confirmed) { if (confirmed) {
localStorage.setItem('softDeleteDefault', hardDelete ? 0 : 1)
this.$axios this.$axios
.$delete(`/api/items/${this.libraryItemId}?hard=${hardDelete ? 1 : 0}`) .$delete(`/api/items/${this.libraryItemId}?hard=${hardDelete ? 1 : 0}`)
.then(() => { .then(() => {

View File

@ -54,9 +54,16 @@
<p class="pl-2 pr-1 text-sm font-semibold">{{ getButtonText(episode) }}</p> <p class="pl-2 pr-1 text-sm font-semibold">{{ getButtonText(episode) }}</p>
</button> </button>
<button v-if="libraryItemIdStreaming && !isStreamingFromDifferentLibrary" class="h-8 w-8 flex justify-center items-center mx-2" :class="playerQueueEpisodeIdMap[episode.id] ? 'text-success' : ''" @click.stop="queueBtnClick(episode)"> <ui-tooltip v-if="libraryItemIdStreaming && !isStreamingFromDifferentLibrary" :text="playerQueueEpisodeIdMap[episode.id] ? $strings.MessageRemoveFromPlayerQueue : $strings.MessageAddToPlayerQueue" :class="playerQueueEpisodeIdMap[episode.id] ? 'text-success' : ''" direction="top">
<ui-icon-btn :icon="playerQueueEpisodeIdMap[episode.id] ? 'playlist_add_check' : 'playlist_play'" borderless @click="queueBtnClick(episode)" />
<!-- <button class="h-8 w-8 flex justify-center items-center mx-2" :class="playerQueueEpisodeIdMap[episode.id] ? 'text-success' : ''" @click.stop="queueBtnClick(episode)">
<span class="material-icons-outlined text-2xl">{{ playerQueueEpisodeIdMap[episode.id] ? 'playlist_add_check' : 'playlist_add' }}</span> <span class="material-icons-outlined text-2xl">{{ playerQueueEpisodeIdMap[episode.id] ? 'playlist_add_check' : 'playlist_add' }}</span>
</button> </button> -->
</ui-tooltip>
<ui-tooltip :text="$strings.LabelYourPlaylists" direction="top">
<ui-icon-btn icon="playlist_add" borderless @click="clickAddToPlaylist(episode)" />
</ui-tooltip>
</div> </div>
</div> </div>
@ -136,6 +143,15 @@ export default {
} }
}, },
methods: { methods: {
clickAddToPlaylist(episode) {
// Makeshift libraryItem
const libraryItem = {
id: episode.libraryItemId,
media: episode.podcast
}
this.$store.commit('globals/setSelectedPlaylistItems', [{ libraryItem: libraryItem, episode }])
this.$store.commit('globals/setShowPlaylistsModal', true)
},
async clickEpisode(episode) { async clickEpisode(episode) {
if (this.openingItem) return if (this.openingItem) return
this.openingItem = true this.openingItem = true
@ -155,7 +171,9 @@ export default {
if (this.episodeIdStreaming === episode.id) return this.streamIsPlaying ? 'Streaming' : 'Play' if (this.episodeIdStreaming === episode.id) return this.streamIsPlaying ? 'Streaming' : 'Play'
if (!episode.progress) return this.$elapsedPretty(episode.duration) if (!episode.progress) return this.$elapsedPretty(episode.duration)
if (episode.progress.isFinished) return 'Finished' if (episode.progress.isFinished) return 'Finished'
var remaining = Math.floor(episode.progress.duration - episode.progress.currentTime)
const duration = episode.progress.duration || episode.duration
const remaining = Math.floor(duration - episode.progress.currentTime)
return `${this.$elapsedPretty(remaining)} left` return `${this.$elapsedPretty(remaining)} left`
}, },
playClick(episodeToPlay) { playClick(episodeToPlay) {

View File

@ -45,6 +45,11 @@
<script> <script>
export default { export default {
async asyncData({ params, query, store, app, redirect }) { async asyncData({ params, query, store, app, redirect }) {
// Podcast search/add page is restricted to admins
if (!store.getters['user/getIsAdminOrUp']) {
return redirect(`/library/${params.library}`)
}
var libraryId = params.library var libraryId = params.library
var libraryData = await store.dispatch('libraries/fetch', libraryId) var libraryData = await store.dispatch('libraries/fetch', libraryId)
if (!libraryData) { if (!libraryData) {

View File

@ -25,9 +25,12 @@
</div> </div>
<div v-else-if="isInit" class="w-full max-w-md px-8 pb-8 pt-4 -mt-40"> <div v-else-if="isInit" class="w-full max-w-md px-8 pb-8 pt-4 -mt-40">
<p class="text-3xl text-white text-center mb-4">{{ $strings.HeaderLogin }}</p> <p class="text-3xl text-white text-center mb-4">{{ $strings.HeaderLogin }}</p>
<div class="w-full h-px bg-white bg-opacity-10 my-4" /> <div class="w-full h-px bg-white bg-opacity-10 my-4" />
<p v-if="error" class="text-error text-center py-2">{{ error }}</p> <p v-if="error" class="text-error text-center py-2">{{ error }}</p>
<form @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="username" :disabled="processing" class="mb-3 w-full" />
@ -37,6 +40,14 @@
<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>
</form> </form>
<div v-if="login_local && login_openid" class="w-full h-px bg-white bg-opacity-10 my-4" />
<div class="w-full flex py-3">
<a v-if="login_openid" :href="openidAuthUri" class="w-full abs-btn outline-none rounded-md shadow-md relative border border-gray-600 text-center bg-primary text-white px-8 py-2 leading-none">
{{ openIDButtonText }}
</a>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -60,7 +71,10 @@ export default {
}, },
confirmPassword: '', confirmPassword: '',
ConfigPath: '', ConfigPath: '',
MetadataPath: '' MetadataPath: '',
login_local: true,
login_openid: false,
authFormData: null
} }
}, },
watch: { watch: {
@ -93,6 +107,12 @@ export default {
computed: { computed: {
user() { user() {
return this.$store.state.user.user return this.$store.state.user.user
},
openidAuthUri() {
return `${process.env.serverUrl}/auth/openid?callback=${location.href.split('?').shift()}`
},
openIDButtonText() {
return this.authFormData?.authOpenIDButtonText || 'Login with OpenId'
} }
}, },
methods: { methods: {
@ -162,6 +182,7 @@ export default {
else this.error = 'Unknown Error' else this.error = 'Unknown Error'
return false return false
}) })
if (authRes?.error) { if (authRes?.error) {
this.error = authRes.error this.error = authRes.error
} else if (authRes) { } else if (authRes) {
@ -196,28 +217,62 @@ export default {
this.processing = true this.processing = true
this.$axios this.$axios
.$get('/status') .$get('/status')
.then((res) => { .then((data) => {
this.processing = false this.isInit = data.isInit
this.isInit = res.isInit this.showInitScreen = !data.isInit
this.showInitScreen = !res.isInit this.$setServerLanguageCode(data.language)
this.$setServerLanguageCode(res.language)
if (this.showInitScreen) { if (this.showInitScreen) {
this.ConfigPath = res.ConfigPath || '' this.ConfigPath = data.ConfigPath || ''
this.MetadataPath = res.MetadataPath || '' this.MetadataPath = data.MetadataPath || ''
} else {
this.authFormData = data.authFormData
this.updateLoginVisibility(data.authMethods || [])
} }
}) })
.catch((error) => { .catch((error) => {
console.error('Status check failed', error) console.error('Status check failed', error)
this.processing = false
this.criticalError = 'Status check failed' this.criticalError = 'Status check failed'
}) })
.finally(() => {
this.processing = false
})
},
updateLoginVisibility(authMethods) {
if (this.$route.query?.error) {
this.error = this.$route.query.error
// Remove error query string
const newurl = new URL(location.href)
newurl.searchParams.delete('error')
window.history.replaceState({ path: newurl.href }, '', newurl.href)
}
if (authMethods.includes('local') || !authMethods.length) {
this.login_local = true
} else {
this.login_local = false
}
if (authMethods.includes('openid')) {
// Auto redirect unless query string ?autoLaunch=0
if (this.authFormData?.authOpenIDAutoLaunch && this.$route.query?.autoLaunch !== '0') {
window.location.href = this.openidAuthUri
}
this.login_openid = true
} else {
this.login_openid = false
}
} }
}, },
async mounted() { async mounted() {
if (localStorage.getItem('token')) { if (this.$route.query?.setToken) {
var userfound = await this.checkAuth() localStorage.setItem('token', this.$route.query.setToken)
if (userfound) return // if valid user no need to check status
} }
if (localStorage.getItem('token')) {
if (await this.checkAuth()) return // if valid user no need to check status
}
this.checkStatus() this.checkStatus()
} }
} }

View File

@ -14,6 +14,20 @@
</div> </div>
</div> </div>
<div v-if="!selectedLibraryIsPodcast" class="flex items-center mb-6">
<label class="flex cursor-pointer pt-4">
<ui-toggle-switch v-model="fetchMetadata.enabled" class="inline-flex" />
<span class="pl-2 text-base">{{ $strings.LabelAutoFetchMetadata }}</span>
</label>
<ui-tooltip :text="$strings.LabelAutoFetchMetadataHelp" class="inline-flex pt-4">
<span class="pl-1 material-icons icon-text text-sm cursor-pointer">info_outlined</span>
</ui-tooltip>
<div class="flex-grow ml-4">
<ui-dropdown v-model="fetchMetadata.provider" :items="providers" :label="$strings.LabelProvider" />
</div>
</div>
<widgets-alert v-if="error" type="error"> <widgets-alert v-if="error" type="error">
<p class="text-lg">{{ error }}</p> <p class="text-lg">{{ error }}</p>
</widgets-alert> </widgets-alert>
@ -61,9 +75,7 @@
</widgets-alert> </widgets-alert>
<!-- Item Upload cards --> <!-- Item Upload cards -->
<template v-for="item in items"> <cards-item-upload-card v-for="item in items" :key="item.index" :ref="`itemCard-${item.index}`" :media-type="selectedLibraryMediaType" :item="item" :provider="fetchMetadata.provider" :processing="processing" @remove="removeItem(item)" />
<cards-item-upload-card :ref="`itemCard-${item.index}`" :key="item.index" :media-type="selectedLibraryMediaType" :item="item" :processing="processing" @remove="removeItem(item)" />
</template>
<!-- Upload/Reset btns --> <!-- Upload/Reset btns -->
<div v-show="items.length" class="flex justify-end pb-8 pt-4"> <div v-show="items.length" class="flex justify-end pb-8 pt-4">
@ -92,13 +104,18 @@ export default {
selectedLibraryId: null, selectedLibraryId: null,
selectedFolderId: null, selectedFolderId: null,
processing: false, processing: false,
uploadFinished: false uploadFinished: false,
fetchMetadata: {
enabled: false,
provider: null
}
} }
}, },
watch: { watch: {
selectedLibrary(newVal) { selectedLibrary(newVal) {
if (newVal && !this.selectedFolderId) { if (newVal && !this.selectedFolderId) {
this.setDefaultFolder() this.setDefaultFolder()
this.setMetadataProvider()
} }
} }
}, },
@ -133,6 +150,13 @@ export default {
selectedLibraryIsPodcast() { selectedLibraryIsPodcast() {
return this.selectedLibraryMediaType === 'podcast' return this.selectedLibraryMediaType === 'podcast'
}, },
providers() {
if (this.selectedLibraryIsPodcast) return this.$store.state.scanners.podcastProviders
return this.$store.state.scanners.providers
},
canFetchMetadata() {
return !this.selectedLibraryIsPodcast && this.fetchMetadata.enabled
},
selectedFolder() { selectedFolder() {
if (!this.selectedLibrary) return null if (!this.selectedLibrary) return null
return this.selectedLibrary.folders.find((fold) => fold.id === this.selectedFolderId) return this.selectedLibrary.folders.find((fold) => fold.id === this.selectedFolderId)
@ -160,12 +184,16 @@ export default {
} }
} }
this.setDefaultFolder() this.setDefaultFolder()
this.setMetadataProvider()
}, },
setDefaultFolder() { setDefaultFolder() {
if (!this.selectedFolderId && this.selectedLibrary && this.selectedLibrary.folders.length) { if (!this.selectedFolderId && this.selectedLibrary && this.selectedLibrary.folders.length) {
this.selectedFolderId = this.selectedLibrary.folders[0].id this.selectedFolderId = this.selectedLibrary.folders[0].id
} }
}, },
setMetadataProvider() {
this.fetchMetadata.provider ||= this.$store.getters['libraries/getLibraryProvider'](this.selectedLibraryId)
},
removeItem(item) { removeItem(item) {
this.items = this.items.filter((b) => b.index !== item.index) this.items = this.items.filter((b) => b.index !== item.index)
if (!this.items.length) { if (!this.items.length) {
@ -213,27 +241,49 @@ export default {
var items = e.dataTransfer.items || [] var items = e.dataTransfer.items || []
var itemResults = await this.uploadHelpers.getItemsFromDrop(items, this.selectedLibraryMediaType) var itemResults = await this.uploadHelpers.getItemsFromDrop(items, this.selectedLibraryMediaType)
this.setResults(itemResults) this.onItemsSelected(itemResults)
}, },
inputChanged(e) { inputChanged(e) {
if (!e.target || !e.target.files) return if (!e.target || !e.target.files) return
var _files = Array.from(e.target.files) var _files = Array.from(e.target.files)
if (_files && _files.length) { if (_files && _files.length) {
var itemResults = this.uploadHelpers.getItemsFromPicker(_files, this.selectedLibraryMediaType) var itemResults = this.uploadHelpers.getItemsFromPicker(_files, this.selectedLibraryMediaType)
this.setResults(itemResults) this.onItemsSelected(itemResults)
} }
}, },
setResults(itemResults) { onItemsSelected(itemResults) {
if (this.itemSelectionSuccessful(itemResults)) {
// setTimeout ensures the new item ref is attached before this method is called
setTimeout(this.attemptMetadataFetch, 0)
}
},
itemSelectionSuccessful(itemResults) {
console.log('Upload results', itemResults)
if (itemResults.error) { if (itemResults.error) {
this.error = itemResults.error this.error = itemResults.error
this.items = [] this.items = []
this.ignoredFiles = [] this.ignoredFiles = []
} else { return false
}
this.error = '' this.error = ''
this.items = itemResults.items this.items = itemResults.items
this.ignoredFiles = itemResults.ignoredFiles this.ignoredFiles = itemResults.ignoredFiles
return true
},
attemptMetadataFetch() {
if (!this.canFetchMetadata) {
return false
} }
console.log('Upload results', itemResults)
this.items.forEach((item) => {
let itemRef = this.$refs[`itemCard-${item.index}`]
if (itemRef?.length) {
itemRef[0].fetchMetadata(this.fetchMetadata.provider)
}
})
}, },
updateItemCardStatus(index, status) { updateItemCardStatus(index, status) {
var ref = this.$refs[`itemCard-${index}`] var ref = this.$refs[`itemCard-${index}`]
@ -248,8 +298,8 @@ export default {
var form = new FormData() var form = new FormData()
form.set('title', item.title) form.set('title', item.title)
if (!this.selectedLibraryIsPodcast) { if (!this.selectedLibraryIsPodcast) {
form.set('author', item.author) form.set('author', item.author || '')
form.set('series', item.series) form.set('series', item.series || '')
} }
form.set('library', this.selectedLibraryId) form.set('library', this.selectedLibraryId)
form.set('folder', this.selectedFolderId) form.set('folder', this.selectedFolderId)
@ -346,6 +396,8 @@ export default {
}, },
mounted() { mounted() {
this.selectedLibraryId = this.$store.state.libraries.currentLibraryId this.selectedLibraryId = this.$store.state.libraries.currentLibraryId
this.setMetadataProvider()
this.setDefaultFolder() this.setDefaultFolder()
window.addEventListener('dragenter', this.dragenter) window.addEventListener('dragenter', this.dragenter)
window.addEventListener('dragleave', this.dragleave) window.addEventListener('dragleave', this.dragleave)

View File

@ -5,6 +5,8 @@ import { supplant } from './utils'
const defaultCode = 'en-us' const defaultCode = 'en-us'
const languageCodeMap = { const languageCodeMap = {
'cs': { label: 'Čeština', dateFnsLocale: 'cs' },
'da': { label: 'Dansk', dateFnsLocale: 'da' },
'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' },
@ -16,6 +18,7 @@ const languageCodeMap = {
'no': { label: 'Norsk', dateFnsLocale: 'no' }, 'no': { label: 'Norsk', dateFnsLocale: 'no' },
'pl': { label: 'Polski', dateFnsLocale: 'pl' }, 'pl': { label: 'Polski', dateFnsLocale: 'pl' },
'ru': { label: 'Русский', dateFnsLocale: 'ru' }, 'ru': { label: 'Русский', dateFnsLocale: 'ru' },
'sv': { label: 'Svenska', dateFnsLocale: 'sv' },
'zh-cn': { label: '简体中文 (Simplified Chinese)', dateFnsLocale: 'zhCN' }, 'zh-cn': { label: '简体中文 (Simplified Chinese)', dateFnsLocale: 'zhCN' },
} }
Vue.prototype.$languageCodeOptions = Object.keys(languageCodeMap).map(code => { Vue.prototype.$languageCodeOptions = Object.keys(languageCodeMap).map(code => {

View File

@ -77,6 +77,7 @@ Vue.prototype.$sanitizeFilename = (filename, colonReplacement = ' - ') => {
.replace(lineBreaks, replacement) .replace(lineBreaks, replacement)
.replace(windowsReservedRe, replacement) .replace(windowsReservedRe, replacement)
.replace(windowsTrailingRe, replacement) .replace(windowsTrailingRe, replacement)
.replace(/\s+/g, ' ') // Replace consecutive spaces with a single space
// Check if basename is too many bytes // Check if basename is too many bytes
const ext = Path.extname(sanitized) // separate out file extension const ext = Path.extname(sanitized) // separate out file extension

View File

@ -11,6 +11,7 @@ export const state = () => ({
showViewPodcastEpisodeModal: false, showViewPodcastEpisodeModal: false,
showRSSFeedOpenCloseModal: false, showRSSFeedOpenCloseModal: false,
showConfirmPrompt: false, showConfirmPrompt: false,
showRawCoverPreviewModal: false,
confirmPromptOptions: null, confirmPromptOptions: null,
showEditAuthorModal: false, showEditAuthorModal: false,
rssFeedEntity: null, rssFeedEntity: null,
@ -20,6 +21,7 @@ export const state = () => ({
selectedCollection: null, selectedCollection: null,
selectedAuthor: null, selectedAuthor: null,
selectedMediaItems: [], selectedMediaItems: [],
selectedLibraryItemId: null,
isCasting: false, // Actively casting isCasting: false, // Actively casting
isChromecastInitialized: false, // Script loadeds isChromecastInitialized: false, // Script loadeds
showBatchQuickMatchModal: false, showBatchQuickMatchModal: false,
@ -80,7 +82,7 @@ export const state = () => ({
}) })
export const getters = { export const getters = {
getLibraryItemCoverSrc: (state, getters, rootState, rootGetters) => (libraryItem, placeholder = null) => { getLibraryItemCoverSrc: (state, getters, rootState, rootGetters) => (libraryItem, placeholder = null, raw = false) => {
if (!placeholder) placeholder = `${rootState.routerBasePath}/book_placeholder.jpg` if (!placeholder) placeholder = `${rootState.routerBasePath}/book_placeholder.jpg`
if (!libraryItem) return placeholder if (!libraryItem) return placeholder
const media = libraryItem.media const media = libraryItem.media
@ -94,7 +96,7 @@ export const getters = {
const libraryItemId = libraryItem.libraryItemId || libraryItem.id // Workaround for /users/:id page showing media progress covers const libraryItemId = libraryItem.libraryItemId || libraryItem.id // Workaround for /users/:id page showing media progress covers
if (process.env.NODE_ENV !== 'production') { // Testing if (process.env.NODE_ENV !== 'production') { // Testing
return `http://localhost:3333${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}&ts=${lastUpdate}` return `http://localhost:3333${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}&ts=${lastUpdate}${raw ? '&raw=1' : ''}`
} }
return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}&ts=${lastUpdate}` return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}&ts=${lastUpdate}`
@ -156,6 +158,13 @@ export const mutations = {
state.confirmPromptOptions = options state.confirmPromptOptions = options
state.showConfirmPrompt = true state.showConfirmPrompt = true
}, },
setShowRawCoverPreviewModal(state, val) {
state.showRawCoverPreviewModal = val
},
setRawCoverPreviewModal(state, libraryItemId) {
state.selectedLibraryItemId = libraryItemId
state.showRawCoverPreviewModal = true
},
setEditCollection(state, collection) { setEditCollection(state, collection) {
state.selectedCollection = collection state.selectedCollection = collection
state.showEditCollectionModal = true state.showEditCollectionModal = true

View File

@ -66,7 +66,7 @@ export const getters = {
export const actions = { export const actions = {
updateServerSettings({ commit }, payload) { updateServerSettings({ commit }, payload) {
var updatePayload = { const updatePayload = {
...payload ...payload
} }
return this.$axios.$patch('/api/settings', updatePayload).then((result) => { return this.$axios.$patch('/api/settings', updatePayload).then((result) => {

View File

@ -80,13 +80,11 @@ export const actions = {
return state.folders return state.folders
} }
} }
console.log('Loading folders')
commit('setFoldersLastUpdate') commit('setFoldersLastUpdate')
return this.$axios return this.$axios
.$get('/api/filesystem') .$get('/api/filesystem')
.then((res) => { .then((res) => {
console.log('Settings folders', res)
commit('setFolders', res.directories) commit('setFolders', res.directories)
return res.directories return res.directories
}) })
@ -119,15 +117,16 @@ export const actions = {
dispatch('user/checkUpdateLibrarySortFilter', library.mediaType, { root: true }) dispatch('user/checkUpdateLibrarySortFilter', library.mediaType, { root: true })
if (libraryChanging) {
commit('setCollections', [])
commit('setUserPlaylists', [])
}
commit('addUpdate', library) commit('addUpdate', library)
commit('setLibraryIssues', issues) commit('setLibraryIssues', issues)
commit('setLibraryFilterData', filterData) commit('setLibraryFilterData', filterData)
commit('setNumUserPlaylists', numUserPlaylists) commit('setNumUserPlaylists', numUserPlaylists)
commit('setCurrentLibrary', libraryId) commit('setCurrentLibrary', libraryId)
if (libraryChanging) {
commit('setCollections', [])
commit('setUserPlaylists', [])
}
return data return data
}) })
.catch((error) => { .catch((error) => {

View File

@ -1,5 +1,4 @@
export const state = () => ({ export const state = () => ({
libraryScans: [],
providers: [ providers: [
{ {
text: 'Google Books', text: 'Google Books',
@ -72,26 +71,8 @@ export const state = () => ({
] ]
}) })
export const getters = { export const getters = {}
getLibraryScan: state => id => {
return state.libraryScans.find(ls => ls.id === id)
}
}
export const actions = { export const actions = {}
} export const mutations = {}
export const mutations = {
addUpdate(state, data) {
var index = state.libraryScans.findIndex(lib => lib.id === data.id)
if (index >= 0) {
state.libraryScans.splice(index, 1, data)
} else {
state.libraryScans.push(data)
}
},
remove(state, data) {
state.libraryScans = state.libraryScans.filter(scan => scan.id !== data.id)
}
}

View File

@ -6,7 +6,11 @@ export const state = () => ({
export const getters = { export const getters = {
getTasksByLibraryItemId: (state) => (libraryItemId) => { getTasksByLibraryItemId: (state) => (libraryItemId) => {
return state.tasks.filter(t => t.data && t.data.libraryItemId === libraryItemId) return state.tasks.filter(t => t.data?.libraryItemId === libraryItemId)
},
getRunningLibraryScanTask: (state) => (libraryId) => {
const libraryScanActions = ['library-scan', 'library-match-all']
return state.tasks.find(t => libraryScanActions.includes(t.action) && t.data?.libraryId === libraryId && !t.isFinished)
} }
} }

753
client/strings/cs.json Normal file
View File

@ -0,0 +1,753 @@
{
"ButtonAdd": "Přidat",
"ButtonAddChapters": "Přidat kapitoly",
"ButtonAddDevice": "Přidat zařízení",
"ButtonAddLibrary": "Přidat knihovnu",
"ButtonAddPodcasts": "Přidat podcasty",
"ButtonAddUser": "Přidat uživatele",
"ButtonAddYourFirstLibrary": "Vytvořte svou první knihovnu",
"ButtonApply": "Aplikovat",
"ButtonApplyChapters": "Aplikovat kapitoly",
"ButtonAuthors": "Autoři",
"ButtonBrowseForFolder": "Vyhledat složku",
"ButtonCancel": "Zrušit",
"ButtonCancelEncode": "Zrušit kódování",
"ButtonChangeRootPassword": "Změnit 'Root' heslo",
"ButtonCheckAndDownloadNewEpisodes": "Zkontrolovat & stáhnout nové epizody",
"ButtonChooseAFolder": "Vybrat složku",
"ButtonChooseFiles": "Vybrat soubory",
"ButtonClearFilter": "Vymazat filtr",
"ButtonCloseFeed": "Zavřít kanál",
"ButtonCollections": "Kolekce",
"ButtonConfigureScanner": "Konfigurovat Prohledávání",
"ButtonCreate": "Vytvořit",
"ButtonCreateBackup": "Vytvořit zálohu",
"ButtonDelete": "Smazat",
"ButtonDownloadQueue": "Fronta",
"ButtonEdit": "Upravit",
"ButtonEditChapters": "Upravit kapitoly",
"ButtonEditPodcast": "Upravit podcast",
"ButtonForceReScan": "Vynutit opětovné prohledání",
"ButtonFullPath": "Úplná cesta",
"ButtonHide": "Skrýt",
"ButtonHome": "Domů",
"ButtonIssues": "Problémy",
"ButtonLatest": "Nejnovější",
"ButtonLibrary": "Knihovna",
"ButtonLogout": "Odhlásit",
"ButtonLookup": "Vyhledat",
"ButtonManageTracks": "Správa stop",
"ButtonMapChapterTitles": "Mapovat názvy kapitol",
"ButtonMatchAllAuthors": "Spárovat všechny autory",
"ButtonMatchBooks": "Spárovat Knihy",
"ButtonNevermind": "Nevadí",
"ButtonOk": "Ok",
"ButtonOpenFeed": "Otevřít kanál",
"ButtonOpenManager": "Otevřít správce",
"ButtonPlay": "Přehrát",
"ButtonPlaying": "Hraje",
"ButtonPlaylists": "Seznamy skladeb",
"ButtonPurgeAllCache": "Vyčistit veškerou mezipaměť",
"ButtonPurgeItemsCache": "Vyčistit mezipaměť položek",
"ButtonPurgeMediaProgress": "Vyčistit průběh médií",
"ButtonQueueAddItem": "Přidat do fronty",
"ButtonQueueRemoveItem": "Odstranit z fronty",
"ButtonQuickMatch": "Rychlé přiřazení",
"ButtonRead": "Číst",
"ButtonRemove": "Odstranit",
"ButtonRemoveAll": "Odstranit vše",
"ButtonRemoveAllLibraryItems": "Odstranit všechny položky knihovny",
"ButtonRemoveFromContinueListening": "Odstranit z Pokračovat v poslechu",
"ButtonRemoveFromContinueReading": "Odstranit z Pokračovat ve čtení",
"ButtonRemoveSeriesFromContinueSeries": "Odstranit sérii z Pokračovat v sérii",
"ButtonReScan": "Znovu prohledat",
"ButtonReset": "Resetovat",
"ButtonResetToDefault": "Obnovit výchozí",
"ButtonRestore": "Obnovit",
"ButtonSave": "Uložit",
"ButtonSaveAndClose": "Uložit a zavřít",
"ButtonSaveTracklist": "Uložit seznam skladeb",
"ButtonScan": "Prohledat",
"ButtonScanLibrary": "Prohledat Knihovnu",
"ButtonSearch": "Hledat",
"ButtonSelectFolderPath": "Vybrat cestu ke složce",
"ButtonSeries": "Série",
"ButtonSetChaptersFromTracks": "Nastavit kapitoly ze stop",
"ButtonShiftTimes": "Časy posunu",
"ButtonShow": "Zobrazit",
"ButtonStartM4BEncode": "Spustit kódování M4B",
"ButtonStartMetadataEmbed": "Spustit vkládání metadat",
"ButtonSubmit": "Odeslat",
"ButtonTest": "Test",
"ButtonUpload": "Nahrát",
"ButtonUploadBackup": "Nahrát zálohu",
"ButtonUploadCover": "Nahrát obálku",
"ButtonUploadOPMLFile": "Nahrát soubor OPML",
"ButtonUserDelete": "Smazat uživatelský {0}",
"ButtonUserEdit": "Upravit uživatelské {0}",
"ButtonViewAll": "Zobrazit vše",
"ButtonYes": "Ano",
"ErrorUploadFetchMetadataAPI": "Error fetching metadata",
"ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
"ErrorUploadLacksTitle": "Must have a title",
"HeaderAccount": "Účet",
"HeaderAdvanced": "Pokročilé",
"HeaderAppriseNotificationSettings": "Nastavení oznámení Apprise",
"HeaderAudiobookTools": "Nástroje pro správu souborů audioknih",
"HeaderAudioTracks": "Zvukové stopy",
"HeaderAuthentication": "Authentication",
"HeaderBackups": "Zálohy",
"HeaderChangePassword": "Změnit heslo",
"HeaderChapters": "Kapitoly",
"HeaderChooseAFolder": "Zvolte složku",
"HeaderCollection": "Kolekce",
"HeaderCollectionItems": "Položky kolekce",
"HeaderCover": "Obálka",
"HeaderCurrentDownloads": "Aktuální stahování",
"HeaderDetails": "Podrobnosti",
"HeaderDownloadQueue": "Fronta stahování",
"HeaderEbookFiles": "Soubory elektronických knih",
"HeaderEmail": "E-mail",
"HeaderEmailSettings": "Nastavení e-mailu",
"HeaderEpisodes": "Epizody",
"HeaderEreaderDevices": "Čtečky elektronických knih",
"HeaderEreaderSettings": "Nastavení čtečky elektronických knih",
"HeaderFiles": "Soubory",
"HeaderFindChapters": "Najít kapitoly",
"HeaderIgnoredFiles": "Ignorované soubory",
"HeaderItemFiles": "Soubory položek",
"HeaderItemMetadataUtils": "Nástroje metadat položek",
"HeaderLastListeningSession": "Poslední poslechová relace",
"HeaderLatestEpisodes": "Poslední epizody",
"HeaderLibraries": "Knihovny",
"HeaderLibraryFiles": "Soubory knihovny",
"HeaderLibraryStats": "Statistiky knihovny",
"HeaderListeningSessions": "Poslechové relace",
"HeaderListeningStats": "Statistiky poslechu",
"HeaderLogin": "Přihlásit",
"HeaderLogs": "Záznamy",
"HeaderManageGenres": "Spravovat žánry",
"HeaderManageTags": "Spravovat štítky",
"HeaderMapDetails": "Podrobnosti mapování",
"HeaderMatch": "Shoda",
"HeaderMetadataOrderOfPrecedence": "Pořadí priorit metadat",
"HeaderMetadataToEmbed": "Metadata k vložení",
"HeaderNewAccount": "Nový účet",
"HeaderNewLibrary": "Nová knihovna",
"HeaderNotifications": "Oznámení",
"HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication",
"HeaderOpenRSSFeed": "Otevřít RSS kanál",
"HeaderOtherFiles": "Ostatní soubory",
"HeaderPasswordAuthentication": "Password Authentication",
"HeaderPermissions": "Oprávnění",
"HeaderPlayerQueue": "Fronta přehrávače",
"HeaderPlaylist": "Seznam skladeb",
"HeaderPlaylistItems": "Položky seznamu přehrávání",
"HeaderPodcastsToAdd": "Podcasty k přidání",
"HeaderPreviewCover": "Náhled obálky",
"HeaderRemoveEpisode": "Odstranit epizodu",
"HeaderRemoveEpisodes": "Odstranit {0} epizody",
"HeaderRSSFeedGeneral": "Podrobnosti o RSS",
"HeaderRSSFeedIsOpen": "Informační kanál RSS je otevřený",
"HeaderRSSFeeds": "RSS kanály",
"HeaderSavedMediaProgress": "Průběh uložených médií",
"HeaderSchedule": "Plán",
"HeaderScheduleLibraryScans": "Naplánovat automatické prohledávání knihoven",
"HeaderSession": "Relace",
"HeaderSetBackupSchedule": "Nastavit plán zálohování",
"HeaderSettings": "Nastavení",
"HeaderSettingsDisplay": "Zobrazit",
"HeaderSettingsExperimental": "Experimentální funkce",
"HeaderSettingsGeneral": "Obecné",
"HeaderSettingsScanner": "Skener",
"HeaderSleepTimer": "Časovač vypnutí",
"HeaderStatsLargestItems": "Největší položky",
"HeaderStatsLongestItems": "Nejdelší položky (hod.)",
"HeaderStatsMinutesListeningChart": "Počet minut poslechu (posledních 7 dní)",
"HeaderStatsRecentSessions": "Poslední relace",
"HeaderStatsTop10Authors": "Top 10 autorů",
"HeaderStatsTop5Genres": "Top 5 žánrů",
"HeaderTableOfContents": "Obsah",
"HeaderTools": "Nástroje",
"HeaderUpdateAccount": "Aktualizovat účet",
"HeaderUpdateAuthor": "Aktualizovat autora",
"HeaderUpdateDetails": "Aktualizovat podrobnosti",
"HeaderUpdateLibrary": "Aktualizovat knihovnu",
"HeaderUsers": "Uživatelé",
"HeaderYourStats": "Vaše statistiky",
"LabelAbridged": "Zkráceno",
"LabelAccountType": "Typ účtu",
"LabelAccountTypeAdmin": "Správce",
"LabelAccountTypeGuest": "Host",
"LabelAccountTypeUser": "Uživatel",
"LabelActivity": "Aktivita",
"LabelAdded": "Přidáno",
"LabelAddedAt": "Přidáno v",
"LabelAddToCollection": "Přidat do kolekce",
"LabelAddToCollectionBatch": "Přidat {0} knihy do kolekce",
"LabelAddToPlaylist": "Přidat do seznamu přehrávání",
"LabelAddToPlaylistBatch": "Přidat {0} položky do seznamu přehrávání",
"LabelAdminUsersOnly": "Pouze administrátoři",
"LabelAll": "Vše",
"LabelAllUsers": "Všichni uživatelé",
"LabelAllUsersExcludingGuests": "Všichni uživatelé kromě hostů",
"LabelAllUsersIncludingGuests": "Všichni uživatelé včetně hostů",
"LabelAlreadyInYourLibrary": "Již ve vaší knihovně",
"LabelAppend": "Připojit",
"LabelAuthor": "Autor",
"LabelAuthorFirstLast": "Autor (jméno a příjmení)",
"LabelAuthorLastFirst": "Autor (příjmení a jméno)",
"LabelAuthors": "Autoři",
"LabelAutoDownloadEpisodes": "Automaticky stahovat epizody",
"LabelAutoFetchMetadata": "Auto Fetch Metadata",
"LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
"LabelAutoLaunch": "Auto Launch",
"LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Auto Register",
"LabelAutoRegisterDescription": "Automatically create new users after logging in",
"LabelBackToUser": "Zpět k uživateli",
"LabelBackupLocation": "Umístění zálohy",
"LabelBackupsEnableAutomaticBackups": "Povolit automatické zálohování",
"LabelBackupsEnableAutomaticBackupsHelp": "Zálohy uložené v /metadata/backups",
"LabelBackupsMaxBackupSize": "Maximální velikost zálohy (v GB)",
"LabelBackupsMaxBackupSizeHelp": "Ochrana proti chybné konfiguraci: Zálohování se nezdaří, pokud překročí nastavenou velikost.",
"LabelBackupsNumberToKeep": "Počet záloh, které se mají uchovat",
"LabelBackupsNumberToKeepHelp": "Najednou bude odstraněna pouze 1 záloha, takže pokud již máte více záloh, měli byste je odstranit ručně.",
"LabelBitrate": "Datový tok",
"LabelBooks": "Knihy",
"LabelButtonText": "Button Text",
"LabelChangePassword": "Změnit heslo",
"LabelChannels": "Kanály",
"LabelChapters": "Kapitoly",
"LabelChaptersFound": "Kapitoly nalezeny",
"LabelChapterTitle": "Název kapitoly",
"LabelClickForMoreInfo": "Klikněte pro více informací",
"LabelClosePlayer": "Zavřít přehrávač",
"LabelCodec": "Kodek",
"LabelCollapseSeries": "Sbalit sérii",
"LabelCollection": "Kolekce",
"LabelCollections": "Kolekce",
"LabelComplete": "Dokončeno",
"LabelConfirmPassword": "Potvrdit heslo",
"LabelContinueListening": "Pokračovat v poslechu",
"LabelContinueReading": "Pokračovat ve čtení",
"LabelContinueSeries": "Pokračovat v sérii",
"LabelCover": "Obálka",
"LabelCoverImageURL": "URL obrázku obálky",
"LabelCreatedAt": "Vytvořeno v",
"LabelCronExpression": "Výraz Cronu",
"LabelCurrent": "Aktuální",
"LabelCurrently": "Aktuálně:",
"LabelCustomCronExpression": "Vlastní výraz cronu:",
"LabelDatetime": "Datum a čas",
"LabelDeleteFromFileSystemCheckbox": "Smazat ze souborového systému (zrušte zaškrtnutí pro odstranění pouze z databáze)",
"LabelDescription": "Popis",
"LabelDeselectAll": "Odznačit vše",
"LabelDevice": "Zařízení",
"LabelDeviceInfo": "Informace o zařízení",
"LabelDeviceIsAvailableTo": "Zařízení je dostupné pro...",
"LabelDirectory": "Adresář",
"LabelDiscFromFilename": "Disk z názvu souboru",
"LabelDiscFromMetadata": "Disk z metadat",
"LabelDiscover": "Objevit",
"LabelDownload": "Stáhnout",
"LabelDownloadNEpisodes": "Stáhnout {0} epizody",
"LabelDuration": "Doba trvání",
"LabelDurationFound": "Doba trvání nalezena:",
"LabelEbook": "Elektronická kniha",
"LabelEbooks": "Elektronické knihy",
"LabelEdit": "Upravit",
"LabelEmail": "E-mail",
"LabelEmailSettingsFromAddress": "Z adresy",
"LabelEmailSettingsSecure": "Zabezpečené",
"LabelEmailSettingsSecureHelp": "Pokud je true, připojení bude při připojování k serveru používat TLS. Pokud je false, použije se protokol TLS, pokud server podporuje rozšíření STARTTLS. Ve většině případů nastavte tuto hodnotu na true, pokud se připojujete k portu 465. Pro port 587 nebo 25 ponechte hodnotu false. (z nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsTestAddress": "Testovací adresa",
"LabelEmbeddedCover": "Vložená obálka",
"LabelEnable": "Povolit",
"LabelEnd": "Konec",
"LabelEpisode": "Epizoda",
"LabelEpisodeTitle": "Název epizody",
"LabelEpisodeType": "Typ epizody",
"LabelExample": "Příklad",
"LabelExplicit": "Explicitní",
"LabelFeedURL": "URL zdroje",
"LabelFetchingMetadata": "Fetching Metadata",
"LabelFile": "Soubor",
"LabelFileBirthtime": "Čas vzniku souboru",
"LabelFileModified": "Soubor změněn",
"LabelFilename": "Název souboru",
"LabelFilterByUser": "Filtrovat podle uživatele",
"LabelFindEpisodes": "Najít epizody",
"LabelFinished": "Dokončeno",
"LabelFolder": "Složka",
"LabelFolders": "Složky",
"LabelFontFamily": "Rodina písem",
"LabelFontScale": "Měřítko písma",
"LabelFormat": "Formát",
"LabelGenre": "Žánr",
"LabelGenres": "Žánry",
"LabelHardDeleteFile": "Trvale smazat soubor",
"LabelHasEbook": "Obsahuje elektronickou knihu",
"LabelHasSupplementaryEbook": "Obsahuje doplňkovou elektronickou knihu",
"LabelHighestPriority": "Highest priority",
"LabelHost": "Hostitel",
"LabelHour": "Hodina",
"LabelIcon": "Ikona",
"LabelImageURLFromTheWeb": "URL obrázku z webu",
"LabelIncludeInTracklist": "Zahrnout do seznamu stop",
"LabelIncomplete": "Neúplné",
"LabelInProgress": "Probíhá",
"LabelInterval": "Interval",
"LabelIntervalCustomDailyWeekly": "Vlastní denně/týdně",
"LabelIntervalEvery12Hours": "Každých 12 hodin",
"LabelIntervalEvery15Minutes": "Každých 15 minut",
"LabelIntervalEvery2Hours": "Každé 2 hodiny",
"LabelIntervalEvery30Minutes": "Každých 30 minut",
"LabelIntervalEvery6Hours": "Každých 6 hodin",
"LabelIntervalEveryDay": "Každý den",
"LabelIntervalEveryHour": "Každou hodinu",
"LabelInvalidParts": "Neplatné části",
"LabelInvert": "Invertovat",
"LabelItem": "Položka",
"LabelLanguage": "Jazyk",
"LabelLanguageDefaultServer": "Výchozí jazyk serveru",
"LabelLastBookAdded": "Poslední kniha přidána",
"LabelLastBookUpdated": "Poslední kniha aktualizována",
"LabelLastSeen": "Naposledy viděno",
"LabelLastTime": "Naposledy",
"LabelLastUpdate": "Poslední aktualizace",
"LabelLayout": "Rozvržení",
"LabelLayoutSinglePage": "Jedna stránka",
"LabelLayoutSplitPage": "Rozdělit stránku",
"LabelLess": "Méně",
"LabelLibrariesAccessibleToUser": "Knihovny přístupné uživateli",
"LabelLibrary": "Knihovna",
"LabelLibraryItem": "Položka knihovny",
"LabelLibraryName": "Název knihovny",
"LabelLimit": "Omezit",
"LabelLineSpacing": "Řádkování",
"LabelListenAgain": "Poslouchat znovu",
"LabelLogLevelDebug": "Ladit",
"LabelLogLevelInfo": "Informace",
"LabelLogLevelWarn": "Varovat",
"LabelLookForNewEpisodesAfterDate": "Hledat nové epizody po tomto datu",
"LabelLowestPriority": "Lowest Priority",
"LabelMatchExistingUsersBy": "Match existing users by",
"LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider",
"LabelMediaPlayer": "Přehrávač médií",
"LabelMediaType": "Typ média",
"LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources",
"LabelMetadataProvider": "Poskytovatel metadat",
"LabelMetaTag": "Metaznačka",
"LabelMetaTags": "Metaznačky",
"LabelMinute": "Minuta",
"LabelMissing": "Chybějící",
"LabelMissingParts": "Chybějící díly",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
"LabelMore": "Více",
"LabelMoreInfo": "Více informací",
"LabelName": "Jméno",
"LabelNarrator": "Interpret",
"LabelNarrators": "Interpreti",
"LabelNew": "Nový",
"LabelNewestAuthors": "Nejnovější autoři",
"LabelNewestEpisodes": "Nejnovější epizody",
"LabelNewPassword": "Nové heslo",
"LabelNextBackupDate": "Datum příští zálohy",
"LabelNextScheduledRun": "Další naplánované spuštění",
"LabelNoEpisodesSelected": "Nebyly vybrány žádné epizody",
"LabelNotes": "Poznámky",
"LabelNotFinished": "Nedokončeno",
"LabelNotificationAppriseURL": "URL adresy Apprise",
"LabelNotificationAvailableVariables": "Dostupné proměnné",
"LabelNotificationBodyTemplate": "Šablona těla",
"LabelNotificationEvent": "Událost oznámení",
"LabelNotificationsMaxFailedAttempts": "Maximální počet neúspěšných pokusů",
"LabelNotificationsMaxFailedAttemptsHelp": "Oznámení jsou vypnuta, pokud se jim to nepodaří odeslat",
"LabelNotificationsMaxQueueSize": "Maximální velikost fronty pro oznamovací události",
"LabelNotificationsMaxQueueSizeHelp": "Události jsou omezeny na 1 za sekundu. Události budou ignorovány, pokud je fronta v maximální velikosti. Tím se zabrání spamování oznámení.",
"LabelNotificationTitleTemplate": "Šablona názvu",
"LabelNotStarted": "Nezahájeno",
"LabelNumberOfBooks": "Počet knih",
"LabelNumberOfEpisodes": "Počet epizod",
"LabelOpenRSSFeed": "Otevřít RSS kanál",
"LabelOverwrite": "Přepsat",
"LabelPassword": "Heslo",
"LabelPath": "Cesta",
"LabelPermissionsAccessAllLibraries": "Má přístup ke všem knihovnám",
"LabelPermissionsAccessAllTags": "Má přístup ke všem značkám",
"LabelPermissionsAccessExplicitContent": "Má přístup k explicitnímu obsahu",
"LabelPermissionsDelete": "Může mazat",
"LabelPermissionsDownload": "Může stahovat",
"LabelPermissionsUpdate": "Může aktualizovat",
"LabelPermissionsUpload": "Může nahrávat",
"LabelPhotoPathURL": "Cesta k fotografii/URL",
"LabelPlaylists": "Seznamy skladeb",
"LabelPlayMethod": "Metoda přehrávání",
"LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasty",
"LabelPodcastType": "Typ podcastu",
"LabelPort": "Port",
"LabelPrefixesToIgnore": "Předpony, které se mají ignorovat (nerozlišují se malá a velká písmena)",
"LabelPreventIndexing": "Zabránit indexování vašeho kanálu v adresářích podcastů iTunes a Google",
"LabelPrimaryEbook": "Hlavní e-kniha",
"LabelProgress": "Průběh",
"LabelProvider": "Poskytovatel",
"LabelPubDate": "Datum vydání",
"LabelPublisher": "Vydavatel",
"LabelPublishYear": "Rok vydání",
"LabelRead": "Číst",
"LabelReadAgain": "Číst znovu",
"LabelReadEbookWithoutProgress": "Číst e-knihu bez zachování průběhu",
"LabelRecentlyAdded": "Nedávno přidané",
"LabelRecentSeries": "Nedávné série",
"LabelRecommended": "Doporučeno",
"LabelRegion": "Region",
"LabelReleaseDate": "Datum vydání",
"LabelRemoveCover": "Odstranit obálku",
"LabelRowsPerPage": "Rows per page",
"LabelRSSFeedCustomOwnerEmail": "Vlastní e-mail vlastníka",
"LabelRSSFeedCustomOwnerName": "Vlastní jméno vlastníka",
"LabelRSSFeedOpen": "Otevření RSS kanálu",
"LabelRSSFeedPreventIndexing": "Zabránit indexování",
"LabelRSSFeedSlug": "RSS kanál Slug",
"LabelRSSFeedURL": "URL RSS kanálu",
"LabelSearchTerm": "Vyhledat termín",
"LabelSearchTitle": "Vyhledat název",
"LabelSearchTitleOrASIN": "Vyhledat název nebo ASIN",
"LabelSeason": "Sezóna",
"LabelSelectAllEpisodes": "Vybrat všechny epizody",
"LabelSelectEpisodesShowing": "Vyberte {0} epizody, které se zobrazují",
"LabelSelectUsers": "Vybrat uživatele",
"LabelSendEbookToDevice": "Odeslat e-knihu do...",
"LabelSequence": "Sekvence",
"LabelSeries": "Série",
"LabelSeriesName": "Název série",
"LabelSeriesProgress": "Průběh série",
"LabelSetEbookAsPrimary": "Nastavit jako primární",
"LabelSetEbookAsSupplementary": "Nastavit jako doplňkové",
"LabelSettingsAudiobooksOnly": "Pouze audioknihy",
"LabelSettingsAudiobooksOnlyHelp": "Povolením tohoto nastavení budou soubory e-knih ignorovány, pokud nejsou ve složce audioknih, v takovém případě budou nastaveny jako doplňkové e-knihy",
"LabelSettingsBookshelfViewHelp": "Skeumorfní design s dřevěnými policemi",
"LabelSettingsChromecastSupport": "Podpora Chromecastu",
"LabelSettingsDateFormat": "Formát data",
"LabelSettingsDisableWatcher": "Zakázat sledování",
"LabelSettingsDisableWatcherForLibrary": "Zakázat sledování složky pro knihovnu",
"LabelSettingsDisableWatcherHelp": "Zakáže automatické přidávání/aktualizaci položek při zjištění změn v souboru. *Vyžaduje restart serveru",
"LabelSettingsEnableWatcher": "Povolit sledování",
"LabelSettingsEnableWatcherForLibrary": "Povolit sledování složky pro knihovnu",
"LabelSettingsEnableWatcherHelp": "Povoluje automatické přidávání/aktualizaci položek, když jsou zjištěny změny souborů. *Vyžaduje restart serveru",
"LabelSettingsExperimentalFeatures": "Experimentální funkce",
"LabelSettingsExperimentalFeaturesHelp": "Funkce ve vývoji, které by mohly využít vaši zpětnou vazbu a pomoc s testováním. Kliknutím otevřete diskuzi na githubu.",
"LabelSettingsFindCovers": "Najít obálky",
"LabelSettingsFindCoversHelp": "Pokud vaše audiokniha nemá vloženou obálku nebo obrázek obálky uvnitř složky, skener se pokusí obálku najít.<br>Poznámka: Tím se prodlouží doba prohledávání",
"LabelSettingsHideSingleBookSeries": "Skrýt sérii s jedinou knihou",
"LabelSettingsHideSingleBookSeriesHelp": "Série, které mají jedinou knihu, budou skryty na stránce série a na domovské stránce.",
"LabelSettingsHomePageBookshelfView": "Domovská stránka používá zobrazení police s knihami",
"LabelSettingsLibraryBookshelfView": "Knihovna používá zobrazení police s knihami",
"LabelSettingsParseSubtitles": "Analzyovat podtitul",
"LabelSettingsParseSubtitlesHelp": "Rozparsovat podtitul z názvů složek audioknih.<br>Podtiul musí být oddělen znakem \" - \"<br>tj. \"Název knihy - Zde Podtitul\" má podtitul \"Zde podtitul\"",
"LabelSettingsPreferMatchedMetadata": "Preferovat spárovaná metadata",
"LabelSettingsPreferMatchedMetadataHelp": "Spárovaná data budou mít při použití funkce Rychlé párování přednost před údaji o položce. Ve výchozím nastavení funkce Rychlé párování pouze doplní chybějící údaje.",
"LabelSettingsSkipMatchingBooksWithASIN": "Přeskočit párování knih, které již mají ASIN",
"LabelSettingsSkipMatchingBooksWithISBN": "Přeskočit párování knih, které již mají ISBN",
"LabelSettingsSortingIgnorePrefixes": "Ignorovat předpony při třídění",
"LabelSettingsSortingIgnorePrefixesHelp": "tj. pro předponu \"the\" název knihy \"Název knihy\" by se třídil jako \"Název knihy, The\"",
"LabelSettingsSquareBookCovers": "Použít čtvercové obálky knih",
"LabelSettingsSquareBookCoversHelp": "Preferovat použití čtvercových obálek před standardními obálkami 1.6:1",
"LabelSettingsStoreCoversWithItem": "Uložit obálky s položkou",
"LabelSettingsStoreCoversWithItemHelp": "Ve výchozím nastavení jsou obálky uloženy v adresáři /metadata/items, povolením tohoto nastavení se obálky uloží do složky položek knihovny. Zůstane zachován pouze jeden soubor s názvem \"cover\"",
"LabelSettingsStoreMetadataWithItem": "Uložit metadata s položkou",
"LabelSettingsStoreMetadataWithItemHelp": "Ve výchozím nastavení jsou soubory metadat uloženy v adresáři /metadata/items, povolením tohoto nastavení budou soubory metadat uloženy ve složkách položek knihovny",
"LabelSettingsTimeFormat": "Formát času",
"LabelShowAll": "Zobrazit vše",
"LabelSize": "Velikost",
"LabelSleepTimer": "Časovač vypnutí",
"LabelSlug": "Slug",
"LabelStart": "Spustit",
"LabelStarted": "Spuštěno",
"LabelStartedAt": "Spuštěno v",
"LabelStartTime": "Čas Spuštění",
"LabelStatsAudioTracks": "Zvukové stopy",
"LabelStatsAuthors": "Autoři",
"LabelStatsBestDay": "Nejlepší den",
"LabelStatsDailyAverage": "Denní průměr",
"LabelStatsDays": "Dny",
"LabelStatsDaysListened": "Dny poslechu",
"LabelStatsHours": "Hodiny",
"LabelStatsInARow": "v řadě",
"LabelStatsItemsFinished": "Dokončené Položky",
"LabelStatsItemsInLibrary": "Položky v knihovně",
"LabelStatsMinutes": "minut",
"LabelStatsMinutesListening": "Minuty poslechu",
"LabelStatsOverallDays": "Celkový počet dní",
"LabelStatsOverallHours": "Celkový počet hodin",
"LabelStatsWeekListening": "Týdenní poslech",
"LabelSubtitle": "Podtitul",
"LabelSupportedFileTypes": "Podporované typy souborů",
"LabelTag": "Značka",
"LabelTags": "Značky",
"LabelTagsAccessibleToUser": "Značky přístupné uživateli",
"LabelTagsNotAccessibleToUser": "Značky nepřístupné uživateli",
"LabelTasks": "Spuštěné Úlohy",
"LabelTheme": "Téma",
"LabelThemeDark": "Tmavé",
"LabelThemeLight": "Světlé",
"LabelTimeBase": "Časová základna",
"LabelTimeListened": "Čas poslechu",
"LabelTimeListenedToday": "Čas poslechu dnes",
"LabelTimeRemaining": "{0} zbývá",
"LabelTimeToShift": "Čas posunu v sekundách",
"LabelTitle": "Název",
"LabelToolsEmbedMetadata": "Vložit metadata",
"LabelToolsEmbedMetadataDescription": "Vložit metadata do zvukových souborů včetně obálky a kapitol.",
"LabelToolsMakeM4b": "Vytvořit soubor audioknihy M4B",
"LabelToolsMakeM4bDescription": "Vygenerovat soubor audioknihy M4B s vloženými metadaty, obálkou a kapitolami.",
"LabelToolsSplitM4b": "Rozdělit M4B na MP3",
"LabelToolsSplitM4bDescription": "Vytvořit soubory MP3 z M4B rozděleného podle kapitol s vloženými metadaty, obrázku obálky a kapitol.",
"LabelTotalDuration": "Celková doba trvání",
"LabelTotalTimeListened": "Celkový čas poslechu",
"LabelTrackFromFilename": "Stopa z názvu souboru",
"LabelTrackFromMetadata": "Stopa z metadat",
"LabelTracks": "Stopy",
"LabelTracksMultiTrack": "Více stop",
"LabelTracksNone": "Žádné stopy",
"LabelTracksSingleTrack": "Jedna stopa",
"LabelType": "Typ",
"LabelUnabridged": "Nezkráceno",
"LabelUnknown": "Neznámý",
"LabelUpdateCover": "Aktualizovat obálku",
"LabelUpdateCoverHelp": "Povolit přepsání existujících obálek pro vybrané knihy, pokud je nalezena shoda",
"LabelUpdatedAt": "Aktualizováno v",
"LabelUpdateDetails": "Aktualizovat podrobnosti",
"LabelUpdateDetailsHelp": "Povolit přepsání existujících údajů o vybraných knihách, když je nalezena shoda",
"LabelUploaderDragAndDrop": "Přetáhnout soubory nebo složky",
"LabelUploaderDropFiles": "Odstranit soubory",
"LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
"LabelUseChapterTrack": "Použít stopu kapitoly",
"LabelUseFullTrack": "Použít celou stopu",
"LabelUser": "Uživatel",
"LabelUsername": "Uživatelské jméno",
"LabelValue": "Hodnota",
"LabelVersion": "Verze",
"LabelViewBookmarks": "Zobrazit záložky",
"LabelViewChapters": "Zobrazit kapitoly",
"LabelViewQueue": "Zobrazit frontu přehrávače",
"LabelVolume": "Hlasitost",
"LabelWeekdaysToRun": "Dny v týdnu ke spuštění",
"LabelYourAudiobookDuration": "Doba trvání vaší audioknihy",
"LabelYourBookmarks": "Vaše záložky",
"LabelYourPlaylists": "Vaše seznamy přehrávání",
"LabelYourProgress": "Váš pokrok",
"MessageAddToPlayerQueue": "Přidat do fronty přehrávače",
"MessageAppriseDescription": "Abyste mohli používat tuto funkci, musíte mít spuštěnou instanci <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> nebo API, které bude zpracovávat stejné požadavky. <br />Adresa URL API Apprise by měla být úplná URL cesta pro odeslání oznámení, např. pokud je vaše instance API obsluhována na adrese <code>http://192.168.1.1:8337</code> pak byste měli zadat <code>http://192.168.1.1:8337/notify</code>.",
"MessageBackupsDescription": "Zálohy zahrnují uživatele, průběh uživatele, podrobnosti o položkách knihovny, nastavení serveru a obrázky uložené v <code>/metadata/items</code> a <code>/metadata/authors</code>. Zálohy <strong>ne</strong> zahrnují všechny soubory uložené ve složkách knihovny.",
"MessageBatchQuickMatchDescription": "Rychlá párování se pokusí přidat chybějící obálky a metadata pro vybrané položky. Povolením níže uvedených možností umožníte funkci Rychlé párování přepsat stávající obálky a/nebo metadata.",
"MessageBookshelfNoCollections": "Ještě jste nevytvořili žádnou sbírku",
"MessageBookshelfNoResultsForFilter": "Filtr \"{0}: {1}\"",
"MessageBookshelfNoRSSFeeds": "Nejsou otevřeny žádné RSS kanály",
"MessageBookshelfNoSeries": "Nemáte žádnou sérii",
"MessageChapterEndIsAfter": "Konec kapitoly přesahuje konec audioknihy",
"MessageChapterErrorFirstNotZero": "První kapitola musí začínat na 0",
"MessageChapterErrorStartGteDuration": "Neplatný čas začátku, musí být kratší než doba trvání audioknihy",
"MessageChapterErrorStartLtPrev": "Neplatný čas začátku, musí být větší nebo roven času začátku předchozí kapitoly",
"MessageChapterStartIsAfter": "Začátek kapitoly přesahuje konec audioknihy",
"MessageCheckingCron": "Kontrola cronu...",
"MessageConfirmCloseFeed": "Opravdu chcete zavřít tento kanál?",
"MessageConfirmDeleteBackup": "Opravdu chcete smazat zálohu pro {0}?",
"MessageConfirmDeleteFile": "Tento krok smaže soubor ze souborového systému. Jsi si jisti?",
"MessageConfirmDeleteLibrary": "Opravdu chcete trvale smazat knihovnu \"{0}\"?",
"MessageConfirmDeleteLibraryItem": "Tento krok odstraní položku knihovny z databáze a vašeho souborového systému. Jste si jisti?",
"MessageConfirmDeleteLibraryItems": "Tímto smažete {0} položkek knihovny z databáze a vašeho souborového systému. Jsi si jisti?",
"MessageConfirmDeleteSession": "Opravdu chcete smazat tuto relaci?",
"MessageConfirmForceReScan": "Opravdu chcete vynutit opětovné prohledání?",
"MessageConfirmMarkAllEpisodesFinished": "Opravdu chcete označit všechny epizody jako dokončené?",
"MessageConfirmMarkAllEpisodesNotFinished": "Opravdu chcete označit všechny epizody jako nedokončené?",
"MessageConfirmMarkSeriesFinished": "Opravdu chcete označit všechny knihy z této série jako dokončené?",
"MessageConfirmMarkSeriesNotFinished": "Opravdu chcete označit všechny knihy z této série jako nedokončené?",
"MessageConfirmQuickEmbed": "Varování! Rychlé vložení nezálohuje vaše zvukové soubory. Ujistěte se, že máte zálohu zvukových souborů. <br><br>Chcete pokračovat?",
"MessageConfirmRemoveAllChapters": "Opravdu chcete odstranit všechny kapitoly?",
"MessageConfirmRemoveAuthor": "Opravdu chcete odstranit autora \"{0}\"?",
"MessageConfirmRemoveCollection": "Opravdu chcete odstranit kolekci \"{0}\"?",
"MessageConfirmRemoveEpisode": "Opravdu chcete odstranit epizodu \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Opravdu chcete odstranit {0} epizody?",
"MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "Opravdu chcete odebrat předčítání \"{0}\"?",
"MessageConfirmRemovePlaylist": "Opravdu chcete odstranit svůj playlist \"{0}\"?",
"MessageConfirmRenameGenre": "Opravdu chcete přejmenovat žánr \"{0}\" na \"{1}\" pro všechny položky?",
"MessageConfirmRenameGenreMergeNote": "Poznámka: Tento žánr již existuje, takže budou sloučeny.",
"MessageConfirmRenameGenreWarning": "Varování! Podobný žánr s jiným obalem již existuje \"{0}\".",
"MessageConfirmRenameTag": "Opravdu chcete přejmenovat tag \"{0}\" na \"{1}\" pro všechny položky?",
"MessageConfirmRenameTagMergeNote": "Poznámka: Tato značka již existuje, takže budou sloučeny.",
"MessageConfirmRenameTagWarning": "Varování! Podobná značka s jinými velkými a malými písmeny již existuje \"{0}\".",
"MessageConfirmReScanLibraryItems": "Opravdu chcete znovu prohledat {0} položky?",
"MessageConfirmSendEbookToDevice": "Opravdu chcete odeslat e-knihu {0} {1}\" do zařízení \"{2}\"?",
"MessageDownloadingEpisode": "Stahuji epizodu",
"MessageDragFilesIntoTrackOrder": "Přetáhněte soubory do správného pořadí stop",
"MessageEmbedFinished": "Vložení dokončeno!",
"MessageEpisodesQueuedForDownload": "{0} epizody zařazené do fronty ke stažení",
"MessageFeedURLWillBe": "URL zdroje bude {0}",
"MessageFetching": "Stahování...",
"MessageForceReScanDescription": "znovu prohledá všechny soubory jako při novém skenování. ID3 tagy zvukových souborů OPF soubory a textové soubory budou skenovány jako nové.",
"MessageImportantNotice": "Důležité upozornění!",
"MessageInsertChapterBelow": "Vložit kapitolu níže",
"MessageItemsSelected": "{0} vybraných položek",
"MessageItemsUpdated": "{0} položky byly aktualizovány",
"MessageJoinUsOn": "Přidejte se k nám",
"MessageListeningSessionsInTheLastYear": "{0} poslechových relací za poslední rok",
"MessageLoading": "Načítá se...",
"MessageLoadingFolders": "Načítám složky...",
"MessageM4BFailed": "M4B se nezdařil!",
"MessageM4BFinished": "M4B dokončen!",
"MessageMapChapterTitles": "Mapování názvů kapitol ke stávajícím kapitolám audioknihy bez úpravy časových razítek",
"MessageMarkAllEpisodesFinished": "Označit všechny epizody za dokončené",
"MessageMarkAllEpisodesNotFinished": "Označit všechny epizody jako nedokončené",
"MessageMarkAsFinished": "Označit jako dokončené",
"MessageMarkAsNotFinished": "Označit jako nedokončené",
"MessageMatchBooksDescription": "pokusí se spárovat knihy v knihovně s knihou od vybraného vyhledávače a vyplnit prázdné údaje a obálku. Nepřepisuje detaily.",
"MessageNoAudioTracks": "Žádné zvukové stopy",
"MessageNoAuthors": "Žádní autoři",
"MessageNoBackups": "Žádné zálohy",
"MessageNoBookmarks": "Žádné záložky",
"MessageNoChapters": "Žádné kapitoly",
"MessageNoCollections": "Žádné kolekce",
"MessageNoCoversFound": "Nebyly nalezeny žádné obálky",
"MessageNoDescription": "Bez popisu",
"MessageNoDownloadsInProgress": "Momentálně neprobíhá žádné stahování",
"MessageNoDownloadsQueued": "Žádné stahování ve frontě",
"MessageNoEpisodeMatchesFound": "Nebyly nalezeny žádné odpovídající epizody",
"MessageNoEpisodes": "Žádné epizody",
"MessageNoFoldersAvailable": "Nejsou k dispozici žádné složky",
"MessageNoGenres": "Žádné žánry",
"MessageNoIssues": "Žádné výtisk",
"MessageNoItems": "Žádné položky",
"MessageNoItemsFound": "Nebyly nalezeny žádné položky",
"MessageNoListeningSessions": "Žádné poslechové relace",
"MessageNoLogs": "Žádné protokoly",
"MessageNoMediaProgress": "Žádný průběh médií",
"MessageNoNotifications": "Žádná oznámení",
"MessageNoPodcastsFound": "Nebyly nalezeny žádné podcasty",
"MessageNoResults": "Žádné výsledky",
"MessageNoSearchResultsFor": "Nebyly nalezeny žádné výsledky hledání pro \"{0}\"",
"MessageNoSeries": "Žádné série",
"MessageNoTags": "Žádné značky",
"MessageNoTasksRunning": "Nejsou spuštěny žádné úlohy",
"MessageNotYetImplemented": "Ještě není implementováno",
"MessageNoUpdateNecessary": "Není nutná žádná aktualizace",
"MessageNoUpdatesWereNecessary": "Nebyly nutné žádné aktualizace",
"MessageNoUserPlaylists": "Nemáte žádné seznamy skladeb",
"MessageOr": "nebo",
"MessagePauseChapter": "Pozastavit přehrávání kapitoly",
"MessagePlayChapter": "Poslechnout si začátek kapitoly",
"MessagePlaylistCreateFromCollection": "Vytvořit seznam skladeb z kolekce",
"MessagePodcastHasNoRSSFeedForMatching": "Podcast nemá žádnou adresu URL kanálu RSS, kterou by mohl použít pro porovnávání",
"MessageQuickMatchDescription": "Vyplňte prázdné detaily položky a obálku prvním výsledkem shody z '{0}'. Nepřepisuje podrobnosti, pokud není povoleno nastavení serveru \"Preferovat párování metadata\".",
"MessageRemoveChapter": "Odstranit kapitolu",
"MessageRemoveEpisodes": "Odstranit {0} epizodu",
"MessageRemoveFromPlayerQueue": "Odstranit z fronty přehrávače",
"MessageRemoveUserWarning": "Opravdu chcete trvale smazat uživatele \"{0}\"?",
"MessageReportBugsAndContribute": "Hlásit chyby, žádat o funkce a přispívat",
"MessageResetChaptersConfirm": "Opravdu chcete resetovat kapitoly a vrátit zpět provedené změny?",
"MessageRestoreBackupConfirm": "Opravdu chcete obnovit zálohu vytvořenou dne?",
"MessageRestoreBackupWarning": "Obnovení zálohy přepíše celou databázi umístěnou v /config a obálku obrázků v /metadata/items & /metadata/authors.<br /><br />Backups nezmění žádné soubory ve složkách knihovny. Pokud jste povolili nastavení serveru pro ukládání obrázků obalu a metadat do složek knihovny, nebudou zálohovány ani přepsány.<br /><br />Všichni klienti používající váš server budou automaticky obnoveni.",
"MessageSearchResultsFor": "Výsledky hledání pro",
"MessageSelected": "{0} selected",
"MessageServerCouldNotBeReached": "Server je nedostupný",
"MessageSetChaptersFromTracksDescription": "Nastavit kapitoly jako kapitolu a název kapitoly jako název zvukového souboru",
"MessageStartPlaybackAtTime": "Spustit přehrávání pro \"{0}\" v {1}?",
"MessageThinking": "Přemýšlení...",
"MessageUploaderItemFailed": "Nahrávání se nezdařilo",
"MessageUploaderItemSuccess": "Nahráno bylo úspěšně!",
"MessageUploading": "Odesílám...",
"MessageValidCronExpression": "Platný výraz cronu",
"MessageWatcherIsDisabledGlobally": "Hlídač je globálně zakázán v nastavení serveru",
"MessageXLibraryIsEmpty": "{0} knihovna je prázdná!",
"MessageYourAudiobookDurationIsLonger": "Doba trvání audioknihy je delší než nalezená délka",
"MessageYourAudiobookDurationIsShorter": "Délka audioknihy je kratší, než byla nalezena.",
"NoteChangeRootPassword": "Uživatel root je jediný uživatel, který může mít prázdné heslo",
"NoteChapterEditorTimes": "Poznámka: Čas začátku první kapitoly musí zůstat v 0:00 a čas začátku poslední kapitoly nesmí překročit tuto dobu trvání audioknihy.",
"NoteFolderPicker": "Poznámka: složky, které jsou již namapovány, nebudou zobrazeny",
"NoteFolderPickerDebian": "Poznámka: Výběr složek pro instalaci debianu není plně implementován. Cestu ke své knihovně byste měli zadat přímo.",
"NoteRSSFeedPodcastAppsHttps": "Upozornění: Většina aplikací pro podcasty bude vyžadovat, aby adresa URL kanálu RSS používala protokol HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Upozornění: 1 nebo více epizod nemá datum vydání. Některé podcastové aplikace to vyžadují.",
"NoteUploaderFoldersWithMediaFiles": "Se složkami s multimediálními soubory bude zacházeno jako se samostatnými položkami knihovny.",
"NoteUploaderOnlyAudioFiles": "Pokud nahráváte pouze zvukové soubory, bude s každým zvukovým souborem zacházeno jako se samostatnou audioknihou.",
"NoteUploaderUnsupportedFiles": "Nepodporované soubory jsou ignorovány. Při výběru nebo přetažení složky jsou ostatní soubory, které nejsou ve složce položek, ignorovány.",
"PlaceholderNewCollection": "Nový název kolekce",
"PlaceholderNewFolderPath": "Nová cesta ke složce",
"PlaceholderNewPlaylist": "Nový název seznamu přehrávání",
"PlaceholderSearch": "Hledat..",
"PlaceholderSearchEpisode": "Hledat epizodu..",
"ToastAccountUpdateFailed": "Aktualizace účtu se nezdařila",
"ToastAccountUpdateSuccess": "Účet aktualizován",
"ToastAuthorImageRemoveFailed": "Nepodařilo se odstranit obrázek",
"ToastAuthorImageRemoveSuccess": "Obrázek autora odstraněn",
"ToastAuthorUpdateFailed": "Aktualizace autora se nezdařila",
"ToastAuthorUpdateMerged": "Autor sloučen",
"ToastAuthorUpdateSuccess": "Autor aktualizován",
"ToastAuthorUpdateSuccessNoImageFound": "Autor aktualizován (nebyl nalezen žádný obrázek)",
"ToastBackupCreateFailed": "Vytvoření zálohy se nezdařilo",
"ToastBackupCreateSuccess": "Záloha vytvořena",
"ToastBackupDeleteFailed": "Nepodařilo se smazat zálohu",
"ToastBackupDeleteSuccess": "Záloha smazána",
"ToastBackupRestoreFailed": "Nepodařilo se obnovit zálohu",
"ToastBackupUploadFailed": "Nepodařilo se nahrát zálohu",
"ToastBackupUploadSuccess": "Záloha nahrána",
"ToastBatchUpdateFailed": "Dávková aktualizace se nezdařila",
"ToastBatchUpdateSuccess": "Dávková aktualizace proběhla úspěšně",
"ToastBookmarkCreateFailed": "Vytvoření záložky se nezdařilo",
"ToastBookmarkCreateSuccess": "Přidána záložka",
"ToastBookmarkRemoveFailed": "Nepodařilo se odstranit záložku",
"ToastBookmarkRemoveSuccess": "Záložka odstraněna",
"ToastBookmarkUpdateFailed": "Aktualizace záložky se nezdařila",
"ToastBookmarkUpdateSuccess": "Záložka aktualizována",
"ToastChaptersHaveErrors": "Kapitoly obsahují chyby",
"ToastChaptersMustHaveTitles": "Kapitoly musí mít názvy",
"ToastCollectionItemsRemoveFailed": "Nepodařilo se odstranit položky z kolekce",
"ToastCollectionItemsRemoveSuccess": "Položky odstraněny z kolekce",
"ToastCollectionRemoveFailed": "Nepodařilo se odstranit kolekci",
"ToastCollectionRemoveSuccess": "Kolekce odstraněna",
"ToastCollectionUpdateFailed": "Aktualizace kolekce se nezdařila",
"ToastCollectionUpdateSuccess": "Kolekce aktualizována",
"ToastItemCoverUpdateFailed": "Aktualizace obálky se nezdařila",
"ToastItemCoverUpdateSuccess": "Obálka předmětu byl aktualizována",
"ToastItemDetailsUpdateFailed": "Nepodařilo se aktualizovat podrobnosti o položce",
"ToastItemDetailsUpdateSuccess": "Podrobnosti o položce byly aktualizovány",
"ToastItemDetailsUpdateUnneeded": "Podrobnosti o položce nejsou potřeba aktualizovat",
"ToastItemMarkedAsFinishedFailed": "Nepodařilo se označit jako dokončené",
"ToastItemMarkedAsFinishedSuccess": "Položka označena jako dokončená",
"ToastItemMarkedAsNotFinishedFailed": "Nepodařilo se označit jako nedokončené",
"ToastItemMarkedAsNotFinishedSuccess": "Položka označena jako nedokončená",
"ToastLibraryCreateFailed": "Vytvoření knihovny se nezdařilo",
"ToastLibraryCreateSuccess": "Knihovna \"{0}\" vytvořena",
"ToastLibraryDeleteFailed": "Nepodařilo se smazat knihovnu",
"ToastLibraryDeleteSuccess": "Knihovna smazána",
"ToastLibraryScanFailedToStart": "Nepodařilo se spustit kontrolu",
"ToastLibraryScanStarted": "Kontrola knihovny spuštěna",
"ToastLibraryUpdateFailed": "Aktualizace knihovny se nezdařila",
"ToastLibraryUpdateSuccess": "Knihovna \"{0}\" aktualizována",
"ToastPlaylistCreateFailed": "Vytvoření seznamu přehrávání se nezdařilo",
"ToastPlaylistCreateSuccess": "Seznam přehrávání vytvořen",
"ToastPlaylistRemoveFailed": "Nepodařilo se odstranit seznamu přehrávání",
"ToastPlaylistRemoveSuccess": "Seznam přehrávání odstraněn",
"ToastPlaylistUpdateFailed": "Aktualizace seznamu přehrávání se nezdařila",
"ToastPlaylistUpdateSuccess": "Seznam přehrávání aktualizován",
"ToastPodcastCreateFailed": "Vytvoření podcastu se nezdařilo",
"ToastPodcastCreateSuccess": "Podcast byl úspěšně vytvořen",
"ToastRemoveItemFromCollectionFailed": "Nepodařilo se odebrat položku z kolekce",
"ToastRemoveItemFromCollectionSuccess": "Položka odstraněna z kolekce",
"ToastRSSFeedCloseFailed": "Nepodařilo se zavřít RSS kanál",
"ToastRSSFeedCloseSuccess": "RSS kanál uzavřen",
"ToastSendEbookToDeviceFailed": "Odeslání e-knihy do zařízení se nezdařilo",
"ToastSendEbookToDeviceSuccess": "E-kniha odeslána do zařízení \"{0}\"",
"ToastSeriesUpdateFailed": "Aktualizace série se nezdařila",
"ToastSeriesUpdateSuccess": "Aktualizace série byla úspěšná",
"ToastSessionDeleteFailed": "Nepodařilo se smazat relaci",
"ToastSessionDeleteSuccess": "Relace smazána",
"ToastSocketConnected": "Socket připojen",
"ToastSocketDisconnected": "Socket odpojen",
"ToastSocketFailedToConnect": "Socket se nepodařilo připojit",
"ToastUserDeleteFailed": "Nepodařilo se smazat uživatele",
"ToastUserDeleteSuccess": "Uživatel smazán"
}

753
client/strings/da.json Normal file
View File

@ -0,0 +1,753 @@
{
"ButtonAdd": "Tilføj",
"ButtonAddChapters": "Tilføj kapitler",
"ButtonAddDevice": "Add Device",
"ButtonAddLibrary": "Add Library",
"ButtonAddPodcasts": "Tilføj podcasts",
"ButtonAddUser": "Add User",
"ButtonAddYourFirstLibrary": "Tilføj din første bibliotek",
"ButtonApply": "Anvend",
"ButtonApplyChapters": "Anvend kapitler",
"ButtonAuthors": "Forfattere",
"ButtonBrowseForFolder": "Gennemse mappe",
"ButtonCancel": "Annuller",
"ButtonCancelEncode": "Annuller kodning",
"ButtonChangeRootPassword": "Ændr rodadgangskode",
"ButtonCheckAndDownloadNewEpisodes": "Tjek og download nye episoder",
"ButtonChooseAFolder": "Vælg en mappe",
"ButtonChooseFiles": "Vælg filer",
"ButtonClearFilter": "Ryd filter",
"ButtonCloseFeed": "Luk feed",
"ButtonCollections": "Samlinger",
"ButtonConfigureScanner": "Konfigurer scanner",
"ButtonCreate": "Opret",
"ButtonCreateBackup": "Opret sikkerhedskopi",
"ButtonDelete": "Slet",
"ButtonDownloadQueue": "Kø",
"ButtonEdit": "Rediger",
"ButtonEditChapters": "Rediger kapitler",
"ButtonEditPodcast": "Rediger podcast",
"ButtonForceReScan": "Tvungen genindlæsning",
"ButtonFullPath": "Fuld sti",
"ButtonHide": "Skjul",
"ButtonHome": "Hjem",
"ButtonIssues": "Problemer",
"ButtonLatest": "Seneste",
"ButtonLibrary": "Bibliotek",
"ButtonLogout": "Log ud",
"ButtonLookup": "Slå op",
"ButtonManageTracks": "Administrer spor",
"ButtonMapChapterTitles": "Kortlæg kapiteloverskrifter",
"ButtonMatchAllAuthors": "Match alle forfattere",
"ButtonMatchBooks": "Match bøger",
"ButtonNevermind": "Glem det",
"ButtonOk": "OK",
"ButtonOpenFeed": "Åbn feed",
"ButtonOpenManager": "Åbn manager",
"ButtonPlay": "Afspil",
"ButtonPlaying": "Afspiller",
"ButtonPlaylists": "Afspilningslister",
"ButtonPurgeAllCache": "Ryd al cache",
"ButtonPurgeItemsCache": "Ryd elementcache",
"ButtonPurgeMediaProgress": "Ryd Medieforløb",
"ButtonQueueAddItem": "Tilføj til kø",
"ButtonQueueRemoveItem": "Fjern fra kø",
"ButtonQuickMatch": "Hurtig Match",
"ButtonRead": "Læs",
"ButtonRemove": "Fjern",
"ButtonRemoveAll": "Fjern Alle",
"ButtonRemoveAllLibraryItems": "Fjern Alle Bibliotekselementer",
"ButtonRemoveFromContinueListening": "Fjern fra Fortsæt Lytning",
"ButtonRemoveFromContinueReading": "Fjern fra Fortsæt Læsning",
"ButtonRemoveSeriesFromContinueSeries": "Fjern Serie fra Fortsæt Serie",
"ButtonReScan": "Gen-scan",
"ButtonReset": "Nulstil",
"ButtonResetToDefault": "Reset to default",
"ButtonRestore": "Gendan",
"ButtonSave": "Gem",
"ButtonSaveAndClose": "Gem & Luk",
"ButtonSaveTracklist": "Gem Sporliste",
"ButtonScan": "Scan",
"ButtonScanLibrary": "Scan Bibliotek",
"ButtonSearch": "Søg",
"ButtonSelectFolderPath": "Vælg Mappen Sti",
"ButtonSeries": "Serie",
"ButtonSetChaptersFromTracks": "Sæt kapitler fra spor",
"ButtonShiftTimes": "Skift Tider",
"ButtonShow": "Vis",
"ButtonStartM4BEncode": "Start M4B Kode",
"ButtonStartMetadataEmbed": "Start Metadata Indlejring",
"ButtonSubmit": "Send",
"ButtonTest": "Test",
"ButtonUpload": "Upload",
"ButtonUploadBackup": "Upload Backup",
"ButtonUploadCover": "Upload Omslag",
"ButtonUploadOPMLFile": "Upload OPML Fil",
"ButtonUserDelete": "Slet bruger {0}",
"ButtonUserEdit": "Rediger bruger {0}",
"ButtonViewAll": "Vis Alle",
"ButtonYes": "Ja",
"ErrorUploadFetchMetadataAPI": "Error fetching metadata",
"ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
"ErrorUploadLacksTitle": "Must have a title",
"HeaderAccount": "Konto",
"HeaderAdvanced": "Avanceret",
"HeaderAppriseNotificationSettings": "Apprise Notifikationsindstillinger",
"HeaderAudiobookTools": "Audiobog Filhåndteringsværktøjer",
"HeaderAudioTracks": "Lydspor",
"HeaderAuthentication": "Authentication",
"HeaderBackups": "Sikkerhedskopier",
"HeaderChangePassword": "Skift Adgangskode",
"HeaderChapters": "Kapitler",
"HeaderChooseAFolder": "Vælg en Mappe",
"HeaderCollection": "Samling",
"HeaderCollectionItems": "Samlingselementer",
"HeaderCover": "Omslag",
"HeaderCurrentDownloads": "Nuværende Downloads",
"HeaderDetails": "Detaljer",
"HeaderDownloadQueue": "Download Kø",
"HeaderEbookFiles": "E-bogsfiler",
"HeaderEmail": "Email",
"HeaderEmailSettings": "Email Indstillinger",
"HeaderEpisodes": "Episoder",
"HeaderEreaderDevices": "E-læser Enheder",
"HeaderEreaderSettings": "E-læser Indstillinger",
"HeaderFiles": "Filer",
"HeaderFindChapters": "Find Kapitler",
"HeaderIgnoredFiles": "Ignorerede Filer",
"HeaderItemFiles": "Emnefiler",
"HeaderItemMetadataUtils": "Emne Metadata Værktøjer",
"HeaderLastListeningSession": "Seneste Lyttesession",
"HeaderLatestEpisodes": "Seneste episoder",
"HeaderLibraries": "Biblioteker",
"HeaderLibraryFiles": "Biblioteksfiler",
"HeaderLibraryStats": "Biblioteksstatistik",
"HeaderListeningSessions": "Lyttesessioner",
"HeaderListeningStats": "Lyttestatistik",
"HeaderLogin": "Log ind",
"HeaderLogs": "Logs",
"HeaderManageGenres": "Administrer Genrer",
"HeaderManageTags": "Administrer Tags",
"HeaderMapDetails": "Kort Detaljer",
"HeaderMatch": "Match",
"HeaderMetadataOrderOfPrecedence": "Metadata order of precedence",
"HeaderMetadataToEmbed": "Metadata til indlejring",
"HeaderNewAccount": "Ny Konto",
"HeaderNewLibrary": "Nyt Bibliotek",
"HeaderNotifications": "Meddelelser",
"HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication",
"HeaderOpenRSSFeed": "Åbn RSS Feed",
"HeaderOtherFiles": "Andre Filer",
"HeaderPasswordAuthentication": "Password Authentication",
"HeaderPermissions": "Tilladelser",
"HeaderPlayerQueue": "Afspilningskø",
"HeaderPlaylist": "Afspilningsliste",
"HeaderPlaylistItems": "Afspilningsliste Elementer",
"HeaderPodcastsToAdd": "Podcasts til Tilføjelse",
"HeaderPreviewCover": "Forhåndsvis Omslag",
"HeaderRemoveEpisode": "Fjern Episode",
"HeaderRemoveEpisodes": "Fjern {0} Episoder",
"HeaderRSSFeedGeneral": "RSS Detaljer",
"HeaderRSSFeedIsOpen": "RSS Feed er Åben",
"HeaderRSSFeeds": "RSS Feeds",
"HeaderSavedMediaProgress": "Gemt Medieforløb",
"HeaderSchedule": "Planlæg",
"HeaderScheduleLibraryScans": "Planlæg Automatiske Biblioteksscanninger",
"HeaderSession": "Session",
"HeaderSetBackupSchedule": "Indstil Sikkerhedskopieringsplan",
"HeaderSettings": "Indstillinger",
"HeaderSettingsDisplay": "Skærm",
"HeaderSettingsExperimental": "Eksperimentelle Funktioner",
"HeaderSettingsGeneral": "Generelt",
"HeaderSettingsScanner": "Scanner",
"HeaderSleepTimer": "Søvntimer",
"HeaderStatsLargestItems": "Største Elementer",
"HeaderStatsLongestItems": "Længste Elementer (timer)",
"HeaderStatsMinutesListeningChart": "Minutter Lyttet (sidste 7 dage)",
"HeaderStatsRecentSessions": "Seneste Sessions",
"HeaderStatsTop10Authors": "Top 10 Forfattere",
"HeaderStatsTop5Genres": "Top 5 Genrer",
"HeaderTableOfContents": "Indholdsfortegnelse",
"HeaderTools": "Værktøjer",
"HeaderUpdateAccount": "Opdater Konto",
"HeaderUpdateAuthor": "Opdater Forfatter",
"HeaderUpdateDetails": "Opdater Detaljer",
"HeaderUpdateLibrary": "Opdater Bibliotek",
"HeaderUsers": "Brugere",
"HeaderYourStats": "Dine Statistikker",
"LabelAbridged": "Abridged",
"LabelAccountType": "Kontotype",
"LabelAccountTypeAdmin": "Administrator",
"LabelAccountTypeGuest": "Gæst",
"LabelAccountTypeUser": "Bruger",
"LabelActivity": "Aktivitet",
"LabelAdded": "Tilføjet",
"LabelAddedAt": "Tilføjet Kl.",
"LabelAddToCollection": "Tilføj til Samling",
"LabelAddToCollectionBatch": "Tilføj {0} Bøger til Samling",
"LabelAddToPlaylist": "Tilføj til Afspilningsliste",
"LabelAddToPlaylistBatch": "Tilføj {0} Elementer til Afspilningsliste",
"LabelAdminUsersOnly": "Admin users only",
"LabelAll": "Alle",
"LabelAllUsers": "Alle Brugere",
"LabelAllUsersExcludingGuests": "All users excluding guests",
"LabelAllUsersIncludingGuests": "All users including guests",
"LabelAlreadyInYourLibrary": "Allerede i dit bibliotek",
"LabelAppend": "Tilføj",
"LabelAuthor": "Forfatter",
"LabelAuthorFirstLast": "Forfatter (Fornavn Efternavn)",
"LabelAuthorLastFirst": "Forfatter (Efternavn, Fornavn)",
"LabelAuthors": "Forfattere",
"LabelAutoDownloadEpisodes": "Auto Download Episoder",
"LabelAutoFetchMetadata": "Auto Fetch Metadata",
"LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
"LabelAutoLaunch": "Auto Launch",
"LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Auto Register",
"LabelAutoRegisterDescription": "Automatically create new users after logging in",
"LabelBackToUser": "Tilbage til Bruger",
"LabelBackupLocation": "Backup Placering",
"LabelBackupsEnableAutomaticBackups": "Aktivér automatisk sikkerhedskopiering",
"LabelBackupsEnableAutomaticBackupsHelp": "Sikkerhedskopier gemt i /metadata/backups",
"LabelBackupsMaxBackupSize": "Maksimal sikkerhedskopistørrelse (i GB)",
"LabelBackupsMaxBackupSizeHelp": "Som en beskyttelse mod fejlkonfiguration fejler sikkerhedskopier, hvis de overstiger den konfigurerede størrelse.",
"LabelBackupsNumberToKeep": "Antal sikkerhedskopier at beholde",
"LabelBackupsNumberToKeepHelp": "Kun 1 sikkerhedskopi fjernes ad gangen, så hvis du allerede har flere sikkerhedskopier end dette, skal du fjerne dem manuelt.",
"LabelBitrate": "Bitrate",
"LabelBooks": "Bøger",
"LabelButtonText": "Button Text",
"LabelChangePassword": "Ændre Adgangskode",
"LabelChannels": "Kanaler",
"LabelChapters": "Kapitler",
"LabelChaptersFound": "fundne kapitler",
"LabelChapterTitle": "Kapitel Titel",
"LabelClickForMoreInfo": "Click for more info",
"LabelClosePlayer": "Luk afspiller",
"LabelCodec": "Codec",
"LabelCollapseSeries": "Fold Serie Sammen",
"LabelCollection": "Samling",
"LabelCollections": "Samlinger",
"LabelComplete": "Fuldfør",
"LabelConfirmPassword": "Bekræft Adgangskode",
"LabelContinueListening": "Fortsæt Lytning",
"LabelContinueReading": "Fortsæt Læsning",
"LabelContinueSeries": "Fortsæt Serie",
"LabelCover": "Omslag",
"LabelCoverImageURL": "Omslagsbillede URL",
"LabelCreatedAt": "Oprettet Kl.",
"LabelCronExpression": "Cron Udtryk",
"LabelCurrent": "Aktuel",
"LabelCurrently": "Aktuelt:",
"LabelCustomCronExpression": "Brugerdefineret Cron Udtryk:",
"LabelDatetime": "Dato og Tid",
"LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)",
"LabelDescription": "Beskrivelse",
"LabelDeselectAll": "Fravælg Alle",
"LabelDevice": "Enheds",
"LabelDeviceInfo": "Enhedsinformation",
"LabelDeviceIsAvailableTo": "Device is available to...",
"LabelDirectory": "Mappe",
"LabelDiscFromFilename": "Disk fra Filnavn",
"LabelDiscFromMetadata": "Disk fra Metadata",
"LabelDiscover": "Opdag",
"LabelDownload": "Download",
"LabelDownloadNEpisodes": "Download {0} episoder",
"LabelDuration": "Varighed",
"LabelDurationFound": "Fundet varighed:",
"LabelEbook": "E-bog",
"LabelEbooks": "E-bøger",
"LabelEdit": "Rediger",
"LabelEmail": "Email",
"LabelEmailSettingsFromAddress": "Fra Adresse",
"LabelEmailSettingsSecure": "Sikker",
"LabelEmailSettingsSecureHelp": "Hvis sandt, vil forbindelsen bruge TLS ved tilslutning til serveren. Hvis falsk, bruges TLS, hvis serveren understøtter STARTTLS-udvidelsen. I de fleste tilfælde skal denne værdi sættes til sandt, hvis du tilslutter til port 465. Til port 587 eller 25 skal du holde det falsk. (fra nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsTestAddress": "Test Adresse",
"LabelEmbeddedCover": "Indlejret Omslag",
"LabelEnable": "Aktivér",
"LabelEnd": "Slut",
"LabelEpisode": "Episode",
"LabelEpisodeTitle": "Episodetitel",
"LabelEpisodeType": "Episodetype",
"LabelExample": "Eksempel",
"LabelExplicit": "Eksplisit",
"LabelFeedURL": "Feed URL",
"LabelFetchingMetadata": "Fetching Metadata",
"LabelFile": "Fil",
"LabelFileBirthtime": "Fødselstidspunkt for fil",
"LabelFileModified": "Fil ændret",
"LabelFilename": "Filnavn",
"LabelFilterByUser": "Filtrér efter bruger",
"LabelFindEpisodes": "Find episoder",
"LabelFinished": "Færdig",
"LabelFolder": "Mappe",
"LabelFolders": "Mapper",
"LabelFontFamily": "Fontfamilie",
"LabelFontScale": "Skriftstørrelse",
"LabelFormat": "Format",
"LabelGenre": "Genre",
"LabelGenres": "Genrer",
"LabelHardDeleteFile": "Permanent slet fil",
"LabelHasEbook": "Har e-bog",
"LabelHasSupplementaryEbook": "Har supplerende e-bog",
"LabelHighestPriority": "Highest priority",
"LabelHost": "Vært",
"LabelHour": "Time",
"LabelIcon": "Ikon",
"LabelImageURLFromTheWeb": "Image URL from the web",
"LabelIncludeInTracklist": "Inkluder i afspilningsliste",
"LabelIncomplete": "Ufuldstændig",
"LabelInProgress": "I gang",
"LabelInterval": "Interval",
"LabelIntervalCustomDailyWeekly": "Tilpasset dagligt/ugentligt",
"LabelIntervalEvery12Hours": "Hver 12. time",
"LabelIntervalEvery15Minutes": "Hver 15. minut",
"LabelIntervalEvery2Hours": "Hver 2. time",
"LabelIntervalEvery30Minutes": "Hver 30. minut",
"LabelIntervalEvery6Hours": "Hver 6. time",
"LabelIntervalEveryDay": "Hver dag",
"LabelIntervalEveryHour": "Hver time",
"LabelInvalidParts": "Ugyldige dele",
"LabelInvert": "Inverter",
"LabelItem": "Element",
"LabelLanguage": "Sprog",
"LabelLanguageDefaultServer": "Standard server sprog",
"LabelLastBookAdded": "Senest tilføjede bog",
"LabelLastBookUpdated": "Senest opdaterede bog",
"LabelLastSeen": "Sidst set",
"LabelLastTime": "Sidste gang",
"LabelLastUpdate": "Seneste opdatering",
"LabelLayout": "Layout",
"LabelLayoutSinglePage": "Enkeltside",
"LabelLayoutSplitPage": "Opdelt side",
"LabelLess": "Mindre",
"LabelLibrariesAccessibleToUser": "Biblioteker tilgængelige for bruger",
"LabelLibrary": "Bibliotek",
"LabelLibraryItem": "Bibliotekselement",
"LabelLibraryName": "Biblioteksnavn",
"LabelLimit": "Grænse",
"LabelLineSpacing": "Linjeafstand",
"LabelListenAgain": "Lyt igen",
"LabelLogLevelDebug": "Fejlsøgning",
"LabelLogLevelInfo": "Information",
"LabelLogLevelWarn": "Advarsel",
"LabelLookForNewEpisodesAfterDate": "Søg efter nye episoder efter denne dato",
"LabelLowestPriority": "Lowest Priority",
"LabelMatchExistingUsersBy": "Match existing users by",
"LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider",
"LabelMediaPlayer": "Medieafspiller",
"LabelMediaType": "Medietype",
"LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources",
"LabelMetadataProvider": "Metadataudbyder",
"LabelMetaTag": "Meta-tag",
"LabelMetaTags": "Meta-tags",
"LabelMinute": "Minut",
"LabelMissing": "Mangler",
"LabelMissingParts": "Manglende dele",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
"LabelMore": "Mere",
"LabelMoreInfo": "Mere info",
"LabelName": "Navn",
"LabelNarrator": "Fortæller",
"LabelNarrators": "Fortællere",
"LabelNew": "Ny",
"LabelNewestAuthors": "Nyeste forfattere",
"LabelNewestEpisodes": "Nyeste episoder",
"LabelNewPassword": "Nyt kodeord",
"LabelNextBackupDate": "Næste sikkerhedskopi dato",
"LabelNextScheduledRun": "Næste planlagte kørsel",
"LabelNoEpisodesSelected": "Ingen episoder valgt",
"LabelNotes": "Noter",
"LabelNotFinished": "Ikke færdig",
"LabelNotificationAppriseURL": "Apprise URL'er",
"LabelNotificationAvailableVariables": "Tilgængelige variabler",
"LabelNotificationBodyTemplate": "Kropsskabelon",
"LabelNotificationEvent": "Meddelelseshændelse",
"LabelNotificationsMaxFailedAttempts": "Maksimalt antal mislykkede forsøg",
"LabelNotificationsMaxFailedAttemptsHelp": "Meddelelser deaktiveres, når de mislykkes med at sende så mange gange",
"LabelNotificationsMaxQueueSize": "Maksimal køstørrelse for meddelelseshændelser",
"LabelNotificationsMaxQueueSizeHelp": "Hændelser begrænses til at udløse en gang pr. sekund. Hændelser ignoreres, hvis køen er fyldt. Dette forhindrer meddelelsesspam.",
"LabelNotificationTitleTemplate": "Titelskabelon",
"LabelNotStarted": "Ikke påbegyndt",
"LabelNumberOfBooks": "Antal bøger",
"LabelNumberOfEpisodes": "Antal episoder",
"LabelOpenRSSFeed": "Åbn RSS-feed",
"LabelOverwrite": "Overskriv",
"LabelPassword": "Kodeord",
"LabelPath": "Sti",
"LabelPermissionsAccessAllLibraries": "Kan få adgang til alle biblioteker",
"LabelPermissionsAccessAllTags": "Kan få adgang til alle tags",
"LabelPermissionsAccessExplicitContent": "Kan få adgang til eksplicit indhold",
"LabelPermissionsDelete": "Kan slette",
"LabelPermissionsDownload": "Kan downloade",
"LabelPermissionsUpdate": "Kan opdatere",
"LabelPermissionsUpload": "Kan uploade",
"LabelPhotoPathURL": "Foto sti/URL",
"LabelPlaylists": "Afspilningslister",
"LabelPlayMethod": "Afspilningsmetode",
"LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts",
"LabelPodcastType": "Podcast type",
"LabelPort": "Port",
"LabelPrefixesToIgnore": "Præfikser der skal ignoreres (skal ikke skelne mellem store og små bogstaver)",
"LabelPreventIndexing": "Forhindrer, at dit feed bliver indekseret af iTunes og Google podcastkataloger",
"LabelPrimaryEbook": "Primær e-bog",
"LabelProgress": "Fremskridt",
"LabelProvider": "Udbyder",
"LabelPubDate": "Udgivelsesdato",
"LabelPublisher": "Forlag",
"LabelPublishYear": "Udgivelsesår",
"LabelRead": "Læst",
"LabelReadAgain": "Læs igen",
"LabelReadEbookWithoutProgress": "Læs e-bog uden at følge fremskridt",
"LabelRecentlyAdded": "Senest tilføjet",
"LabelRecentSeries": "Seneste serie",
"LabelRecommended": "Anbefalet",
"LabelRegion": "Region",
"LabelReleaseDate": "Udgivelsesdato",
"LabelRemoveCover": "Fjern omslag",
"LabelRowsPerPage": "Rows per page",
"LabelRSSFeedCustomOwnerEmail": "Brugerdefineret ejerens e-mail",
"LabelRSSFeedCustomOwnerName": "Brugerdefineret ejerens navn",
"LabelRSSFeedOpen": "Åben RSS-feed",
"LabelRSSFeedPreventIndexing": "Forhindrer indeksering",
"LabelRSSFeedSlug": "RSS-feed-slug",
"LabelRSSFeedURL": "RSS-feed-URL",
"LabelSearchTerm": "Søgeterm",
"LabelSearchTitle": "Søg efter titel",
"LabelSearchTitleOrASIN": "Søg efter titel eller ASIN",
"LabelSeason": "Sæson",
"LabelSelectAllEpisodes": "Vælg alle episoder",
"LabelSelectEpisodesShowing": "Vælg {0} episoder vist",
"LabelSelectUsers": "Select users",
"LabelSendEbookToDevice": "Send e-bog til...",
"LabelSequence": "Sekvens",
"LabelSeries": "Serie",
"LabelSeriesName": "Serienavn",
"LabelSeriesProgress": "Seriefremskridt",
"LabelSetEbookAsPrimary": "Indstil som primær",
"LabelSetEbookAsSupplementary": "Indstil som supplerende",
"LabelSettingsAudiobooksOnly": "Kun lydbøger",
"LabelSettingsAudiobooksOnlyHelp": "Aktivering af denne indstilling vil ignorere e-bogsfiler, medmindre de er inde i en lydbogmappe, hvor de vil blive indstillet som supplerende e-bøger",
"LabelSettingsBookshelfViewHelp": "Skeumorfisk design med træhylder",
"LabelSettingsChromecastSupport": "Chromecast-understøttelse",
"LabelSettingsDateFormat": "Datoformat",
"LabelSettingsDisableWatcher": "Deaktiver overvågning",
"LabelSettingsDisableWatcherForLibrary": "Deaktiver mappeovervågning for bibliotek",
"LabelSettingsDisableWatcherHelp": "Deaktiverer automatisk tilføjelse/opdatering af elementer, når der registreres filændringer. *Kræver servergenstart",
"LabelSettingsEnableWatcher": "Aktiver overvågning",
"LabelSettingsEnableWatcherForLibrary": "Aktiver mappeovervågning for bibliotek",
"LabelSettingsEnableWatcherHelp": "Aktiverer automatisk tilføjelse/opdatering af elementer, når filændringer registreres. *Kræver servergenstart",
"LabelSettingsExperimentalFeatures": "Eksperimentelle funktioner",
"LabelSettingsExperimentalFeaturesHelp": "Funktioner under udvikling, der kunne bruge din feedback og hjælp til test. Klik for at åbne Github-diskussionen.",
"LabelSettingsFindCovers": "Find omslag",
"LabelSettingsFindCoversHelp": "Hvis din lydbog ikke har et indlejret omslag eller et omslagsbillede i mappen, vil skanneren forsøge at finde et omslag.<br>Bemærk: Dette vil forlænge scanntiden",
"LabelSettingsHideSingleBookSeries": "Skjul enkeltbogsserier",
"LabelSettingsHideSingleBookSeriesHelp": "Serier med en enkelt bog vil blive skjult fra serie-siden og hjemmesidehylder.",
"LabelSettingsHomePageBookshelfView": "Brug bogreolvisning på startside",
"LabelSettingsLibraryBookshelfView": "Brug bogreolvisning i biblioteket",
"LabelSettingsParseSubtitles": "Fortolk undertekster",
"LabelSettingsParseSubtitlesHelp": "Udtræk undertekster fra lydbogsmappenavne.<br>Undertitler skal adskilles af \" - \"<br>f.eks. \"Bogtitel - En undertitel her\" har undertitlen \"En undertitel her\"",
"LabelSettingsPreferMatchedMetadata": "Foretræk matchede metadata",
"LabelSettingsPreferMatchedMetadataHelp": "Matchede data vil tilsidesætte elementdetaljer ved brug af Hurtig Match. Som standard udfylder Hurtig Match kun manglende detaljer.",
"LabelSettingsSkipMatchingBooksWithASIN": "Spring over matchende bøger, der allerede har en ASIN",
"LabelSettingsSkipMatchingBooksWithISBN": "Spring over matchende bøger, der allerede har en ISBN",
"LabelSettingsSortingIgnorePrefixes": "Ignorer præfikser ved sortering",
"LabelSettingsSortingIgnorePrefixesHelp": "f.eks. for præfikset \"the\" vil bogtitlen \"The Book Title\" blive sorteret som \"Book Title, The\"",
"LabelSettingsSquareBookCovers": "Brug kvadratiske bogomslag",
"LabelSettingsSquareBookCoversHelp": "Foretræk at bruge kvadratiske omslag frem for standard 1,6:1 bogomslag",
"LabelSettingsStoreCoversWithItem": "Gem omslag med element",
"LabelSettingsStoreCoversWithItemHelp": "Som standard gemmes omslag i /metadata/items, aktivering af denne indstilling vil gemme omslag i mappen for dit bibliotekselement. Kun én fil med navnet \"cover\" vil blive bevaret",
"LabelSettingsStoreMetadataWithItem": "Gem metadata med element",
"LabelSettingsStoreMetadataWithItemHelp": "Som standard gemmes metadatafiler i /metadata/items, aktivering af denne indstilling vil gemme metadatafiler i dine bibliotekselementmapper",
"LabelSettingsTimeFormat": "Tidsformat",
"LabelShowAll": "Vis alle",
"LabelSize": "Størrelse",
"LabelSleepTimer": "Søvntimer",
"LabelSlug": "Slug",
"LabelStart": "Start",
"LabelStarted": "Startet",
"LabelStartedAt": "Startet klokken",
"LabelStartTime": "Starttid",
"LabelStatsAudioTracks": "Lydspor",
"LabelStatsAuthors": "Forfattere",
"LabelStatsBestDay": "Bedste dag",
"LabelStatsDailyAverage": "Daglig gennemsnit",
"LabelStatsDays": "Dage",
"LabelStatsDaysListened": "Dage hørt",
"LabelStatsHours": "Timer",
"LabelStatsInARow": "i træk",
"LabelStatsItemsFinished": "Elementer færdige",
"LabelStatsItemsInLibrary": "Elementer i biblioteket",
"LabelStatsMinutes": "minutter",
"LabelStatsMinutesListening": "Minutter hørt",
"LabelStatsOverallDays": "Samlede dage",
"LabelStatsOverallHours": "Samlede timer",
"LabelStatsWeekListening": "Ugens lytning",
"LabelSubtitle": "Undertekst",
"LabelSupportedFileTypes": "Understøttede filtyper",
"LabelTag": "Mærke",
"LabelTags": "Mærker",
"LabelTagsAccessibleToUser": "Mærker tilgængelige for bruger",
"LabelTagsNotAccessibleToUser": "Mærker ikke tilgængelige for bruger",
"LabelTasks": "Kører opgaver",
"LabelTheme": "Tema",
"LabelThemeDark": "Mørk",
"LabelThemeLight": "Lys",
"LabelTimeBase": "Tidsbase",
"LabelTimeListened": "Tid hørt",
"LabelTimeListenedToday": "Tid hørt i dag",
"LabelTimeRemaining": "{0} tilbage",
"LabelTimeToShift": "Tid til skift i sekunder",
"LabelTitle": "Titel",
"LabelToolsEmbedMetadata": "Indlejre metadata",
"LabelToolsEmbedMetadataDescription": "Indlejr metadata i lydfiler, inklusive omslag og kapitler.",
"LabelToolsMakeM4b": "Lav M4B lydbogsfil",
"LabelToolsMakeM4bDescription": "Generer en .M4B lydbogsfil med indlejret metadata, omslag og kapitler.",
"LabelToolsSplitM4b": "Opdel M4B til MP3'er",
"LabelToolsSplitM4bDescription": "Opret MP3'er fra en M4B opdelt efter kapitler med indlejret metadata, omslag og kapitler.",
"LabelTotalDuration": "Samlet varighed",
"LabelTotalTimeListened": "Samlet lyttetid",
"LabelTrackFromFilename": "Spor fra filnavn",
"LabelTrackFromMetadata": "Spor fra metadata",
"LabelTracks": "Spor",
"LabelTracksMultiTrack": "Flerspors",
"LabelTracksNone": "Ingen spor",
"LabelTracksSingleTrack": "Enkeltspors",
"LabelType": "Type",
"LabelUnabridged": "Uforkortet",
"LabelUnknown": "Ukendt",
"LabelUpdateCover": "Opdater omslag",
"LabelUpdateCoverHelp": "Tillad overskrivning af eksisterende omslag for de valgte bøger, når der findes en match",
"LabelUpdatedAt": "Opdateret ved",
"LabelUpdateDetails": "Opdater detaljer",
"LabelUpdateDetailsHelp": "Tillad overskrivning af eksisterende detaljer for de valgte bøger, når der findes en match",
"LabelUploaderDragAndDrop": "Træk og slip filer eller mapper",
"LabelUploaderDropFiles": "Smid filer",
"LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
"LabelUseChapterTrack": "Brug kapitel-spor",
"LabelUseFullTrack": "Brug fuldt spor",
"LabelUser": "Bruger",
"LabelUsername": "Brugernavn",
"LabelValue": "Værdi",
"LabelVersion": "Version",
"LabelViewBookmarks": "Se bogmærker",
"LabelViewChapters": "Se kapitler",
"LabelViewQueue": "Se afspilningskø",
"LabelVolume": "Volumen",
"LabelWeekdaysToRun": "Ugedage til kørsel",
"LabelYourAudiobookDuration": "Din lydbogsvarighed",
"LabelYourBookmarks": "Dine bogmærker",
"LabelYourPlaylists": "Dine spillelister",
"LabelYourProgress": "Din fremgang",
"MessageAddToPlayerQueue": "Tilføj til afspilningskø",
"MessageAppriseDescription": "For at bruge denne funktion skal du have en instans af <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> kørende eller en API, der håndterer de samme anmodninger. <br /> Apprise API-webadressen skal være den fulde URL-sti for at sende underretningen, f.eks. hvis din API-instans er tilgængelig på <code>http://192.168.1.1:8337</code>, så skal du bruge <code>http://192.168.1.1:8337/notify</code>.",
"MessageBackupsDescription": "Backups inkluderer brugere, brugerfremskridt, biblioteksvareoplysninger, serverindstillinger og billeder gemt i <code>/metadata/items</code> og <code>/metadata/authors</code>. Backups inkluderer <strong>ikke</strong> nogen filer gemt i dine biblioteksmapper.",
"MessageBatchQuickMatchDescription": "Quick Match vil forsøge at tilføje manglende omslag og metadata til de valgte elementer. Aktivér indstillingerne nedenfor for at tillade Quick Match at overskrive eksisterende omslag og/eller metadata.",
"MessageBookshelfNoCollections": "Du har ikke oprettet nogen samlinger endnu",
"MessageBookshelfNoResultsForFilter": "Ingen resultater for filter \"{0}: {1}\"",
"MessageBookshelfNoRSSFeeds": "Ingen RSS-feeds er åbne",
"MessageBookshelfNoSeries": "Du har ingen serier",
"MessageChapterEndIsAfter": "Kapitelslutningen er efter slutningen af din lydbog",
"MessageChapterErrorFirstNotZero": "Første kapitel skal starte ved 0",
"MessageChapterErrorStartGteDuration": "Ugyldig starttid skal være mindre end lydbogens varighed",
"MessageChapterErrorStartLtPrev": "Ugyldig starttid skal være større end eller lig med den foregående kapitels starttid",
"MessageChapterStartIsAfter": "Kapitelstarten er efter slutningen af din lydbog",
"MessageCheckingCron": "Tjekker cron...",
"MessageConfirmCloseFeed": "Er du sikker på, at du vil lukke dette feed?",
"MessageConfirmDeleteBackup": "Er du sikker på, at du vil slette backup for {0}?",
"MessageConfirmDeleteFile": "Dette vil slette filen fra dit filsystem. Er du sikker?",
"MessageConfirmDeleteLibrary": "Er du sikker på, at du vil slette biblioteket permanent \"{0}\"?",
"MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?",
"MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?",
"MessageConfirmDeleteSession": "Er du sikker på, at du vil slette denne session?",
"MessageConfirmForceReScan": "Er du sikker på, at du vil tvinge en genindlæsning?",
"MessageConfirmMarkAllEpisodesFinished": "Er du sikker på, at du vil markere alle episoder som afsluttet?",
"MessageConfirmMarkAllEpisodesNotFinished": "Er du sikker på, at du vil markere alle episoder som ikke afsluttet?",
"MessageConfirmMarkSeriesFinished": "Er du sikker på, at du vil markere alle bøger i denne serie som afsluttet?",
"MessageConfirmMarkSeriesNotFinished": "Er du sikker på, at du vil markere alle bøger i denne serie som ikke afsluttet?",
"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?",
"MessageConfirmRemoveAllChapters": "Er du sikker på, at du vil fjerne alle kapitler?",
"MessageConfirmRemoveAuthor": "Er du sikker på, at du vil fjerne forfatteren \"{0}\"?",
"MessageConfirmRemoveCollection": "Er du sikker på, at du vil fjerne samlingen \"{0}\"?",
"MessageConfirmRemoveEpisode": "Er du sikker på, at du vil fjerne episoden \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Er du sikker på, at du vil fjerne {0} episoder?",
"MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "Er du sikker på, at du vil fjerne fortælleren \"{0}\"?",
"MessageConfirmRemovePlaylist": "Er du sikker på, at du vil fjerne din spilleliste \"{0}\"?",
"MessageConfirmRenameGenre": "Er du sikker på, at du vil omdøbe genre \"{0}\" til \"{1}\" for alle elementer?",
"MessageConfirmRenameGenreMergeNote": "Bemærk: Denne genre findes allerede, så de vil blive fusioneret.",
"MessageConfirmRenameGenreWarning": "Advarsel! En lignende genre med en anden skrivemåde eksisterer allerede \"{0}\".",
"MessageConfirmRenameTag": "Er du sikker på, at du vil omdøbe tag \"{0}\" til \"{1}\" for alle elementer?",
"MessageConfirmRenameTagMergeNote": "Bemærk: Dette tag findes allerede, så de vil blive fusioneret.",
"MessageConfirmRenameTagWarning": "Advarsel! Et lignende tag med en anden skrivemåde eksisterer allerede \"{0}\".",
"MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?",
"MessageConfirmSendEbookToDevice": "Er du sikker på, at du vil sende {0} e-bog \"{1}\" til enhed \"{2}\"?",
"MessageDownloadingEpisode": "Downloader episode",
"MessageDragFilesIntoTrackOrder": "Træk filer ind i korrekt spororden",
"MessageEmbedFinished": "Indlejring færdig!",
"MessageEpisodesQueuedForDownload": "{0} episoder er sat i kø til download",
"MessageFeedURLWillBe": "Feed-URL vil være {0}",
"MessageFetching": "Henter...",
"MessageForceReScanDescription": "vil scanne alle filer igen som en frisk scanning. Lydfilens ID3-tags, OPF-filer og tekstfiler scannes som nye.",
"MessageImportantNotice": "Vigtig besked!",
"MessageInsertChapterBelow": "Indsæt kapitel nedenfor",
"MessageItemsSelected": "{0} elementer valgt",
"MessageItemsUpdated": "{0} elementer opdateret",
"MessageJoinUsOn": "Deltag i os på",
"MessageListeningSessionsInTheLastYear": "{0} lyttesessioner i det sidste år",
"MessageLoading": "Indlæser...",
"MessageLoadingFolders": "Indlæser mapper...",
"MessageM4BFailed": "M4B mislykkedes!",
"MessageM4BFinished": "M4B afsluttet!",
"MessageMapChapterTitles": "Tilknyt kapiteloverskrifter til dine eksisterende lydbogskapitler uden at justere tidsstempler",
"MessageMarkAllEpisodesFinished": "Markér alle episoder som afsluttet",
"MessageMarkAllEpisodesNotFinished": "Markér alle episoder som ikke afsluttet",
"MessageMarkAsFinished": "Markér som afsluttet",
"MessageMarkAsNotFinished": "Markér som ikke afsluttet",
"MessageMatchBooksDescription": "vil forsøge at matche bøger i biblioteket med en bog fra den valgte søgeudbyder og udfylde tomme detaljer og omslag. Overskriver ikke detaljer.",
"MessageNoAudioTracks": "Ingen lydspor",
"MessageNoAuthors": "Ingen forfattere",
"MessageNoBackups": "Ingen sikkerhedskopier",
"MessageNoBookmarks": "Ingen bogmærker",
"MessageNoChapters": "Ingen kapitler",
"MessageNoCollections": "Ingen samlinger",
"MessageNoCoversFound": "Ingen omslag fundet",
"MessageNoDescription": "Ingen beskrivelse",
"MessageNoDownloadsInProgress": "Ingen downloads i gang lige nu",
"MessageNoDownloadsQueued": "Ingen downloads i kø",
"MessageNoEpisodeMatchesFound": "Ingen episode-matcher fundet",
"MessageNoEpisodes": "Ingen episoder",
"MessageNoFoldersAvailable": "Ingen mapper tilgængelige",
"MessageNoGenres": "Ingen genrer",
"MessageNoIssues": "Ingen problemer",
"MessageNoItems": "Ingen elementer",
"MessageNoItemsFound": "Ingen elementer fundet",
"MessageNoListeningSessions": "Ingen lyttesessioner",
"MessageNoLogs": "Ingen logfiler",
"MessageNoMediaProgress": "Ingen medieforløb",
"MessageNoNotifications": "Ingen meddelelser",
"MessageNoPodcastsFound": "Ingen podcasts fundet",
"MessageNoResults": "Ingen resultater",
"MessageNoSearchResultsFor": "Ingen søgeresultater for \"{0}\"",
"MessageNoSeries": "Ingen serier",
"MessageNoTags": "Ingen tags",
"MessageNoTasksRunning": "Ingen opgaver kører",
"MessageNotYetImplemented": "Endnu ikke implementeret",
"MessageNoUpdateNecessary": "Ingen opdatering nødvendig",
"MessageNoUpdatesWereNecessary": "Ingen opdateringer var nødvendige",
"MessageNoUserPlaylists": "Du har ingen afspilningslister",
"MessageOr": "eller",
"MessagePauseChapter": "Pause kapitelafspilning",
"MessagePlayChapter": "Lyt til begyndelsen af kapitlet",
"MessagePlaylistCreateFromCollection": "Opret afspilningsliste fra samling",
"MessagePodcastHasNoRSSFeedForMatching": "Podcast har ingen RSS-feed-URL at bruge til matchning",
"MessageQuickMatchDescription": "Udfyld tomme elementoplysninger og omslag med første matchresultat fra '{0}'. Overskriver ikke oplysninger, medmindre serverindstillingen 'Foretræk matchet metadata' er aktiveret.",
"MessageRemoveChapter": "Fjern kapitel",
"MessageRemoveEpisodes": "Fjern {0} episode(r)",
"MessageRemoveFromPlayerQueue": "Fjern fra afspillingskøen",
"MessageRemoveUserWarning": "Er du sikker på, at du vil slette brugeren permanent \"{0}\"?",
"MessageReportBugsAndContribute": "Rapporter fejl, anmod om funktioner og bidrag på",
"MessageResetChaptersConfirm": "Er du sikker på, at du vil nulstille kapitler og annullere ændringerne, du har foretaget?",
"MessageRestoreBackupConfirm": "Er du sikker på, at du vil gendanne sikkerhedskopien oprettet den",
"MessageRestoreBackupWarning": "Gendannelse af en sikkerhedskopi vil overskrive hele databasen, som er placeret på /config, og omslagsbilleder i /metadata/items & /metadata/authors.<br /><br />Sikkerhedskopier ændrer ikke nogen filer i dine biblioteksmapper. Hvis du har aktiveret serverindstillinger for at gemme omslagskunst og metadata i dine biblioteksmapper, sikkerhedskopieres eller overskrives disse ikke.<br /><br />Alle klienter, der bruger din server, opdateres automatisk.",
"MessageSearchResultsFor": "Søgeresultater for",
"MessageSelected": "{0} selected",
"MessageServerCouldNotBeReached": "Serveren kunne ikke nås",
"MessageSetChaptersFromTracksDescription": "Indstil kapitler ved at bruge hver lydfil som et kapitel og kapiteloverskrift som lydfilnavn",
"MessageStartPlaybackAtTime": "Start afspilning for \"{0}\" kl. {1}?",
"MessageThinking": "Tænker...",
"MessageUploaderItemFailed": "Fejl ved upload",
"MessageUploaderItemSuccess": "Uploadet med succes!",
"MessageUploading": "Uploader...",
"MessageValidCronExpression": "Gyldigt cron-udtryk",
"MessageWatcherIsDisabledGlobally": "Watcher er deaktiveret globalt i serverindstillinger",
"MessageXLibraryIsEmpty": "{0} bibliotek er tomt!",
"MessageYourAudiobookDurationIsLonger": "Din lydbogsvarighed er længere end den fundne varighed",
"MessageYourAudiobookDurationIsShorter": "Din lydbogsvarighed er kortere end den fundne varighed",
"NoteChangeRootPassword": "Root-brugeren er den eneste bruger, der kan have en tom adgangskode",
"NoteChapterEditorTimes": "Bemærk: Første kapitel starttidspunkt skal forblive kl. 0:00, og det sidste kapitel starttidspunkt må ikke overstige denne lydbogs varighed.",
"NoteFolderPicker": "Bemærk: Mapper, der allerede er mappet, vises ikke",
"NoteFolderPickerDebian": "Bemærk: Mappicker for Debian-installationen er ikke fuldt implementeret. Du bør indtaste stien til dit bibliotek direkte.",
"NoteRSSFeedPodcastAppsHttps": "Advarsel: De fleste podcast-apps kræver, at RSS-feedets URL bruger HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Advarsel: En eller flere af dine episoder har ikke en Pub Date. Nogle podcast-apps kræver dette.",
"NoteUploaderFoldersWithMediaFiles": "Mapper med mediefiler håndteres som separate bibliotekselementer.",
"NoteUploaderOnlyAudioFiles": "Hvis du kun uploader lydfiler, håndteres hver lydfil som en separat lydbog.",
"NoteUploaderUnsupportedFiles": "Ikke-understøttede filer ignoreres. Når du vælger eller slipper en mappe, ignoreres andre filer, der ikke er i en emnemappe.",
"PlaceholderNewCollection": "Nyt samlingnavn",
"PlaceholderNewFolderPath": "Ny mappes sti",
"PlaceholderNewPlaylist": "Nyt afspilningslistnavn",
"PlaceholderSearch": "Søg..",
"PlaceholderSearchEpisode": "Søg efter episode..",
"ToastAccountUpdateFailed": "Mislykkedes opdatering af konto",
"ToastAccountUpdateSuccess": "Konto opdateret",
"ToastAuthorImageRemoveFailed": "Mislykkedes fjernelse af forfatterbillede",
"ToastAuthorImageRemoveSuccess": "Forfatterbillede fjernet",
"ToastAuthorUpdateFailed": "Mislykkedes opdatering af forfatter",
"ToastAuthorUpdateMerged": "Forfatter fusioneret",
"ToastAuthorUpdateSuccess": "Forfatter opdateret",
"ToastAuthorUpdateSuccessNoImageFound": "Forfatter opdateret (ingen billede fundet)",
"ToastBackupCreateFailed": "Mislykkedes oprettelse af sikkerhedskopi",
"ToastBackupCreateSuccess": "Sikkerhedskopi oprettet",
"ToastBackupDeleteFailed": "Mislykkedes sletning af sikkerhedskopi",
"ToastBackupDeleteSuccess": "Sikkerhedskopi slettet",
"ToastBackupRestoreFailed": "Mislykkedes gendannelse af sikkerhedskopi",
"ToastBackupUploadFailed": "Mislykkedes upload af sikkerhedskopi",
"ToastBackupUploadSuccess": "Sikkerhedskopi uploadet",
"ToastBatchUpdateFailed": "Mislykkedes batchopdatering",
"ToastBatchUpdateSuccess": "Batchopdatering lykkedes",
"ToastBookmarkCreateFailed": "Mislykkedes oprettelse af bogmærke",
"ToastBookmarkCreateSuccess": "Bogmærke tilføjet",
"ToastBookmarkRemoveFailed": "Mislykkedes fjernelse af bogmærke",
"ToastBookmarkRemoveSuccess": "Bogmærke fjernet",
"ToastBookmarkUpdateFailed": "Mislykkedes opdatering af bogmærke",
"ToastBookmarkUpdateSuccess": "Bogmærke opdateret",
"ToastChaptersHaveErrors": "Kapitler har fejl",
"ToastChaptersMustHaveTitles": "Kapitler skal have titler",
"ToastCollectionItemsRemoveFailed": "Mislykkedes fjernelse af element(er) fra samlingen",
"ToastCollectionItemsRemoveSuccess": "Element(er) fjernet fra samlingen",
"ToastCollectionRemoveFailed": "Mislykkedes fjernelse af samling",
"ToastCollectionRemoveSuccess": "Samling fjernet",
"ToastCollectionUpdateFailed": "Mislykkedes opdatering af samling",
"ToastCollectionUpdateSuccess": "Samling opdateret",
"ToastItemCoverUpdateFailed": "Mislykkedes opdatering af varens omslag",
"ToastItemCoverUpdateSuccess": "Varens omslag opdateret",
"ToastItemDetailsUpdateFailed": "Mislykkedes opdatering af varedetaljer",
"ToastItemDetailsUpdateSuccess": "Varedetaljer opdateret",
"ToastItemDetailsUpdateUnneeded": "Ingen opdateringer er nødvendige for varedetaljer",
"ToastItemMarkedAsFinishedFailed": "Mislykkedes markering som afsluttet",
"ToastItemMarkedAsFinishedSuccess": "Vare markeret som afsluttet",
"ToastItemMarkedAsNotFinishedFailed": "Mislykkedes markering som ikke afsluttet",
"ToastItemMarkedAsNotFinishedSuccess": "Vare markeret som ikke afsluttet",
"ToastLibraryCreateFailed": "Mislykkedes oprettelse af bibliotek",
"ToastLibraryCreateSuccess": "Bibliotek \"{0}\" oprettet",
"ToastLibraryDeleteFailed": "Mislykkedes sletning af bibliotek",
"ToastLibraryDeleteSuccess": "Bibliotek slettet",
"ToastLibraryScanFailedToStart": "Mislykkedes start af skanning",
"ToastLibraryScanStarted": "Biblioteksskanning startet",
"ToastLibraryUpdateFailed": "Mislykkedes opdatering af bibliotek",
"ToastLibraryUpdateSuccess": "Bibliotek \"{0}\" opdateret",
"ToastPlaylistCreateFailed": "Mislykkedes oprettelse af afspilningsliste",
"ToastPlaylistCreateSuccess": "Afspilningsliste oprettet",
"ToastPlaylistRemoveFailed": "Mislykkedes fjernelse af afspilningsliste",
"ToastPlaylistRemoveSuccess": "Afspilningsliste fjernet",
"ToastPlaylistUpdateFailed": "Mislykkedes opdatering af afspilningsliste",
"ToastPlaylistUpdateSuccess": "Afspilningsliste opdateret",
"ToastPodcastCreateFailed": "Mislykkedes oprettelse af podcast",
"ToastPodcastCreateSuccess": "Podcast oprettet med succes",
"ToastRemoveItemFromCollectionFailed": "Mislykkedes fjernelse af element fra samling",
"ToastRemoveItemFromCollectionSuccess": "Element fjernet fra samling",
"ToastRSSFeedCloseFailed": "Mislykkedes lukning af RSS-feed",
"ToastRSSFeedCloseSuccess": "RSS-feed lukket",
"ToastSendEbookToDeviceFailed": "Mislykkedes afsendelse af e-bog til enhed",
"ToastSendEbookToDeviceSuccess": "E-bog afsendt til enhed \"{0}\"",
"ToastSeriesUpdateFailed": "Mislykkedes opdatering af serie",
"ToastSeriesUpdateSuccess": "Serieopdatering lykkedes",
"ToastSessionDeleteFailed": "Mislykkedes sletning af session",
"ToastSessionDeleteSuccess": "Session slettet",
"ToastSocketConnected": "Socket forbundet",
"ToastSocketDisconnected": "Socket afbrudt",
"ToastSocketFailedToConnect": "Socket kunne ikke oprettes",
"ToastUserDeleteFailed": "Mislykkedes sletning af bruger",
"ToastUserDeleteSuccess": "Bruger slettet"
}

View File

@ -1,7 +1,10 @@
{ {
"ButtonAdd": "Hinzufügen", "ButtonAdd": "Hinzufügen",
"ButtonAddChapters": "Kapitel hinzufügen", "ButtonAddChapters": "Kapitel hinzufügen",
"ButtonAddDevice": "Gerät hinzufügen",
"ButtonAddLibrary": "Bibliothek hinzufügen",
"ButtonAddPodcasts": "Podcasts hinzufügen", "ButtonAddPodcasts": "Podcasts hinzufügen",
"ButtonAddUser": "Benutzer hinzufügen",
"ButtonAddYourFirstLibrary": "Erstelle deine erste Bibliothek", "ButtonAddYourFirstLibrary": "Erstelle deine erste Bibliothek",
"ButtonApply": "Übernehmen", "ButtonApply": "Übernehmen",
"ButtonApplyChapters": "Kapitel anwenden", "ButtonApplyChapters": "Kapitel anwenden",
@ -55,10 +58,11 @@
"ButtonRemoveAll": "Alles löschen", "ButtonRemoveAll": "Alles löschen",
"ButtonRemoveAllLibraryItems": "Lösche alle Bibliothekseinträge", "ButtonRemoveAllLibraryItems": "Lösche alle Bibliothekseinträge",
"ButtonRemoveFromContinueListening": "Lösche den Eintrag aus der Fortsetzungsliste", "ButtonRemoveFromContinueListening": "Lösche den Eintrag aus der Fortsetzungsliste",
"ButtonRemoveFromContinueReading": "Remove from Continue Reading", "ButtonRemoveFromContinueReading": "Lösche die Serie aus der Lesefortsetzungsliste",
"ButtonRemoveSeriesFromContinueSeries": "Lösche die Serie aus der Serienfortsetzungsliste", "ButtonRemoveSeriesFromContinueSeries": "Lösche die Serie aus der Serienfortsetzungsliste",
"ButtonReScan": "Neu scannen", "ButtonReScan": "Neu scannen",
"ButtonReset": "Zurücksetzen", "ButtonReset": "Zurücksetzen",
"ButtonResetToDefault": "Zurücksetzen auf Standard",
"ButtonRestore": "Wiederherstellen", "ButtonRestore": "Wiederherstellen",
"ButtonSave": "Speichern", "ButtonSave": "Speichern",
"ButtonSaveAndClose": "Speichern & Schließen", "ButtonSaveAndClose": "Speichern & Schließen",
@ -83,11 +87,15 @@
"ButtonUserEdit": "Benutzer {0} bearbeiten", "ButtonUserEdit": "Benutzer {0} bearbeiten",
"ButtonViewAll": "Alles anzeigen", "ButtonViewAll": "Alles anzeigen",
"ButtonYes": "Ja", "ButtonYes": "Ja",
"ErrorUploadFetchMetadataAPI": "Fehler beim Abrufen der Metadaten",
"ErrorUploadFetchMetadataNoResults": "Metadaten konnten nicht abgerufen werden. Versuchen Sie den Titel und oder den Autor zu updaten",
"ErrorUploadLacksTitle": "Es muss ein Titel eingegeben werden",
"HeaderAccount": "Konto", "HeaderAccount": "Konto",
"HeaderAdvanced": "Erweitert", "HeaderAdvanced": "Erweitert",
"HeaderAppriseNotificationSettings": "Apprise Benachrichtigungseinstellungen", "HeaderAppriseNotificationSettings": "Apprise Benachrichtigungseinstellungen",
"HeaderAudiobookTools": "Hörbuch-Dateiverwaltungstools", "HeaderAudiobookTools": "Hörbuch-Dateiverwaltungstools",
"HeaderAudioTracks": "Audiodateien", "HeaderAudioTracks": "Audiodateien",
"HeaderAuthentication": "Authentifizierung",
"HeaderBackups": "Sicherungen", "HeaderBackups": "Sicherungen",
"HeaderChangePassword": "Passwort ändern", "HeaderChangePassword": "Passwort ändern",
"HeaderChapters": "Kapitel", "HeaderChapters": "Kapitel",
@ -122,12 +130,15 @@
"HeaderManageTags": "Tags verwalten", "HeaderManageTags": "Tags verwalten",
"HeaderMapDetails": "Stapelverarbeitung", "HeaderMapDetails": "Stapelverarbeitung",
"HeaderMatch": "Metadaten", "HeaderMatch": "Metadaten",
"HeaderMetadataOrderOfPrecedence": "Metadata order of precedence",
"HeaderMetadataToEmbed": "Einzubettende Metadaten", "HeaderMetadataToEmbed": "Einzubettende Metadaten",
"HeaderNewAccount": "Neues Konto", "HeaderNewAccount": "Neues Konto",
"HeaderNewLibrary": "Neue Bibliothek", "HeaderNewLibrary": "Neue Bibliothek",
"HeaderNotifications": "Benachrichtigungen", "HeaderNotifications": "Benachrichtigungen",
"HeaderOpenIDConnectAuthentication": "OpenID Connect Authentifizierung",
"HeaderOpenRSSFeed": "RSS-Feed öffnen", "HeaderOpenRSSFeed": "RSS-Feed öffnen",
"HeaderOtherFiles": "Sonstige Dateien", "HeaderOtherFiles": "Sonstige Dateien",
"HeaderPasswordAuthentication": "Password Authentifizierung",
"HeaderPermissions": "Berechtigungen", "HeaderPermissions": "Berechtigungen",
"HeaderPlayerQueue": "Spieler Warteschlange", "HeaderPlayerQueue": "Spieler Warteschlange",
"HeaderPlaylist": "Wiedergabeliste", "HeaderPlaylist": "Wiedergabeliste",
@ -176,8 +187,11 @@
"LabelAddToCollectionBatch": "Füge {0} Hörbüch(er)/Podcast(s) der Sammlung hinzu", "LabelAddToCollectionBatch": "Füge {0} Hörbüch(er)/Podcast(s) der Sammlung hinzu",
"LabelAddToPlaylist": "Zur Wiedergabeliste hinzufügen", "LabelAddToPlaylist": "Zur Wiedergabeliste hinzufügen",
"LabelAddToPlaylistBatch": "Füge {0} Hörbüch(er)/Podcast(s) der Wiedergabeliste hinzu", "LabelAddToPlaylistBatch": "Füge {0} Hörbüch(er)/Podcast(s) der Wiedergabeliste hinzu",
"LabelAdminUsersOnly": "Nur Admin Benutzer",
"LabelAll": "Alle", "LabelAll": "Alle",
"LabelAllUsers": "Alle Benutzer", "LabelAllUsers": "Alle Benutzer",
"LabelAllUsersExcludingGuests": "Alle Benutzer außer Gästen",
"LabelAllUsersIncludingGuests": "All Benutzer und Gäste",
"LabelAlreadyInYourLibrary": "In der Bibliothek vorhanden", "LabelAlreadyInYourLibrary": "In der Bibliothek vorhanden",
"LabelAppend": "Anhängen", "LabelAppend": "Anhängen",
"LabelAuthor": "Autor", "LabelAuthor": "Autor",
@ -185,6 +199,12 @@
"LabelAuthorLastFirst": "Autor (Nachname, Vorname)", "LabelAuthorLastFirst": "Autor (Nachname, Vorname)",
"LabelAuthors": "Autoren", "LabelAuthors": "Autoren",
"LabelAutoDownloadEpisodes": "Episoden automatisch herunterladen", "LabelAutoDownloadEpisodes": "Episoden automatisch herunterladen",
"LabelAutoFetchMetadata": "Automatisches Abholen der Metadaten",
"LabelAutoFetchMetadataHelp": "Abholen der Metadaten von Titel, Autor und Serien, um das Hochladen zu optimieren. Möglicherweise müssen zusätzliche Metadaten nach dem Hochladen abgeglichen werden.",
"LabelAutoLaunch": "Automatischer Start",
"LabelAutoLaunchDescription": "Automatische Weiterleitung zum Authentifizierungsanbieter beim Navigieren zur Anmeldeseite (manueller Überschreibungspfad <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Automatische Registrierung",
"LabelAutoRegisterDescription": "Automatische neue Neutzer anlegen nach dem Einloggen",
"LabelBackToUser": "Zurück zum Benutzer", "LabelBackToUser": "Zurück zum Benutzer",
"LabelBackupLocation": "Backup-Ort", "LabelBackupLocation": "Backup-Ort",
"LabelBackupsEnableAutomaticBackups": "Automatische Sicherung aktivieren", "LabelBackupsEnableAutomaticBackups": "Automatische Sicherung aktivieren",
@ -195,11 +215,13 @@
"LabelBackupsNumberToKeepHelp": "Es wird immer nur 1 Sicherung auf einmal entfernt. Wenn Sie bereits mehrere Sicherungen als die definierte max. Anzahl haben, sollten Sie diese manuell entfernen.", "LabelBackupsNumberToKeepHelp": "Es wird immer nur 1 Sicherung auf einmal entfernt. Wenn Sie bereits mehrere Sicherungen als die definierte max. Anzahl haben, sollten Sie diese manuell entfernen.",
"LabelBitrate": "Bitrate", "LabelBitrate": "Bitrate",
"LabelBooks": "Bücher", "LabelBooks": "Bücher",
"LabelButtonText": "Button Text",
"LabelChangePassword": "Passwort ändern", "LabelChangePassword": "Passwort ändern",
"LabelChannels": "Kanäle", "LabelChannels": "Kanäle",
"LabelChapters": "Kapitel", "LabelChapters": "Kapitel",
"LabelChaptersFound": "gefundene Kapitel", "LabelChaptersFound": "gefundene Kapitel",
"LabelChapterTitle": "Kapitelüberschrift", "LabelChapterTitle": "Kapitelüberschrift",
"LabelClickForMoreInfo": "Klicken für mehr Informationen",
"LabelClosePlayer": "Player schließen", "LabelClosePlayer": "Player schließen",
"LabelCodec": "Codec", "LabelCodec": "Codec",
"LabelCollapseSeries": "Serien zusammenfassen", "LabelCollapseSeries": "Serien zusammenfassen",
@ -218,16 +240,18 @@
"LabelCurrently": "Aktuell:", "LabelCurrently": "Aktuell:",
"LabelCustomCronExpression": "Benutzerdefinierter Cron-Ausdruck", "LabelCustomCronExpression": "Benutzerdefinierter Cron-Ausdruck",
"LabelDatetime": "Datum & Uhrzeit", "LabelDatetime": "Datum & Uhrzeit",
"LabelDeleteFromFileSystemCheckbox": "Löschen von der Festplatte + Datenbank (deaktivieren um nur aus der Datenbank zu löschen)",
"LabelDescription": "Beschreibung", "LabelDescription": "Beschreibung",
"LabelDeselectAll": "Alles abwählen", "LabelDeselectAll": "Alles abwählen",
"LabelDevice": "Gerät", "LabelDevice": "Gerät",
"LabelDeviceInfo": "Geräteinformationen", "LabelDeviceInfo": "Geräteinformationen",
"LabelDeviceIsAvailableTo": "Dem Geärt ist es möglich zu ...",
"LabelDirectory": "Verzeichnis", "LabelDirectory": "Verzeichnis",
"LabelDiscFromFilename": "CD aus dem Dateinamen", "LabelDiscFromFilename": "CD aus dem Dateinamen",
"LabelDiscFromMetadata": "CD aus den Metadaten", "LabelDiscFromMetadata": "CD aus den Metadaten",
"LabelDiscover": "Entdecken", "LabelDiscover": "Entdecken",
"LabelDownload": "Herunterladen", "LabelDownload": "Herunterladen",
"LabelDownloadNEpisodes": "Download {0} episodes", "LabelDownloadNEpisodes": "Download {0} Episoden",
"LabelDuration": "Laufzeit", "LabelDuration": "Laufzeit",
"LabelDurationFound": "Gefundene Laufzeit:", "LabelDurationFound": "Gefundene Laufzeit:",
"LabelEbook": "E-Book", "LabelEbook": "E-Book",
@ -247,6 +271,7 @@
"LabelExample": "Beispiel", "LabelExample": "Beispiel",
"LabelExplicit": "Explizit (Altersbeschränkung)", "LabelExplicit": "Explizit (Altersbeschränkung)",
"LabelFeedURL": "Feed URL", "LabelFeedURL": "Feed URL",
"LabelFetchingMetadata": "Abholen der Metadaten",
"LabelFile": "Datei", "LabelFile": "Datei",
"LabelFileBirthtime": "Datei erstellt", "LabelFileBirthtime": "Datei erstellt",
"LabelFileModified": "Datei geändert", "LabelFileModified": "Datei geändert",
@ -256,6 +281,7 @@
"LabelFinished": "beendet", "LabelFinished": "beendet",
"LabelFolder": "Ordner", "LabelFolder": "Ordner",
"LabelFolders": "Verzeichnisse", "LabelFolders": "Verzeichnisse",
"LabelFontFamily": "Schriftfamilie",
"LabelFontScale": "Schriftgröße", "LabelFontScale": "Schriftgröße",
"LabelFormat": "Format", "LabelFormat": "Format",
"LabelGenre": "Kategorie", "LabelGenre": "Kategorie",
@ -263,9 +289,11 @@
"LabelHardDeleteFile": "Datei dauerhaft löschen", "LabelHardDeleteFile": "Datei dauerhaft löschen",
"LabelHasEbook": "mit E-Book", "LabelHasEbook": "mit E-Book",
"LabelHasSupplementaryEbook": "mit zusätlichem E-Book", "LabelHasSupplementaryEbook": "mit zusätlichem E-Book",
"LabelHighestPriority": "Höchste Priorität",
"LabelHost": "Host", "LabelHost": "Host",
"LabelHour": "Stunde", "LabelHour": "Stunde",
"LabelIcon": "Symbol", "LabelIcon": "Symbol",
"LabelImageURLFromTheWeb": "Bild-URL vom Internet",
"LabelIncludeInTracklist": "In die Titelliste aufnehmen", "LabelIncludeInTracklist": "In die Titelliste aufnehmen",
"LabelIncomplete": "Unvollständig", "LabelIncomplete": "Unvollständig",
"LabelInProgress": "In Bearbeitung", "LabelInProgress": "In Bearbeitung",
@ -303,14 +331,20 @@
"LabelLogLevelInfo": "Informationen", "LabelLogLevelInfo": "Informationen",
"LabelLogLevelWarn": "Warnungen", "LabelLogLevelWarn": "Warnungen",
"LabelLookForNewEpisodesAfterDate": "Suchen nach neuen Episoden nach diesem Datum", "LabelLookForNewEpisodesAfterDate": "Suchen nach neuen Episoden nach diesem Datum",
"LabelLowestPriority": "Niedrigste Priorität",
"LabelMatchExistingUsersBy": "Zuordnen existierender Benutzer mit",
"LabelMatchExistingUsersByDescription": "Wird zum Verbinden vorhandener Benutzer verwendet. Sobald die Verbindung hergestellt ist, wird den Benutzern eine eindeutige ID von Ihrem SSO-Anbieter zugeordnet",
"LabelMediaPlayer": "Mediaplayer", "LabelMediaPlayer": "Mediaplayer",
"LabelMediaType": "Medientyp", "LabelMediaType": "Medientyp",
"LabelMetadataOrderOfPrecedenceDescription": "Eine Höhere Priorität Quelle für Metadaten wird die Metadaten aus eine Quelle mit niedrigerer Priorität überschreiben.",
"LabelMetadataProvider": "Metadatenanbieter", "LabelMetadataProvider": "Metadatenanbieter",
"LabelMetaTag": "Meta Schlagwort", "LabelMetaTag": "Meta Schlagwort",
"LabelMetaTags": "Meta Tags", "LabelMetaTags": "Meta Tags",
"LabelMinute": "Minute", "LabelMinute": "Minute",
"LabelMissing": "Fehlend", "LabelMissing": "Fehlend",
"LabelMissingParts": "Fehlende Teile", "LabelMissingParts": "Fehlende Teile",
"LabelMobileRedirectURIs": "Erlaubte Weiterleitungs-URIs für die mobile App",
"LabelMobileRedirectURIsDescription": "Dies ist eine Whitelist gültiger Umleitungs-URIs für mobile Apps. Der Standardwert ist <code>audiobookshelf://oauth</code>, den Sie entfernen oder durch zusätzliche URIs für die Integration von Drittanbieter-Apps ergänzen können. Die Verwendung eines Sternchens (<code>*</code>) als alleiniger Eintrag erlaubt jede URI.",
"LabelMore": "Mehr", "LabelMore": "Mehr",
"LabelMoreInfo": "Mehr Info", "LabelMoreInfo": "Mehr Info",
"LabelName": "Name", "LabelName": "Name",
@ -372,6 +406,7 @@
"LabelRegion": "Region", "LabelRegion": "Region",
"LabelReleaseDate": "Veröffentlichungsdatum", "LabelReleaseDate": "Veröffentlichungsdatum",
"LabelRemoveCover": "Lösche Titelbild", "LabelRemoveCover": "Lösche Titelbild",
"LabelRowsPerPage": "Zeilen pro Seite",
"LabelRSSFeedCustomOwnerEmail": "Benutzerdefinierte Eigentümer-E-Mail", "LabelRSSFeedCustomOwnerEmail": "Benutzerdefinierte Eigentümer-E-Mail",
"LabelRSSFeedCustomOwnerName": "Benutzerdefinierter Name des Eigentümers", "LabelRSSFeedCustomOwnerName": "Benutzerdefinierter Name des Eigentümers",
"LabelRSSFeedOpen": "RSS Feed Offen", "LabelRSSFeedOpen": "RSS Feed Offen",
@ -384,6 +419,7 @@
"LabelSeason": "Staffel", "LabelSeason": "Staffel",
"LabelSelectAllEpisodes": "Alle Episoden auswählen", "LabelSelectAllEpisodes": "Alle Episoden auswählen",
"LabelSelectEpisodesShowing": "{0} ausgewählte Episoden werden angezeigt", "LabelSelectEpisodesShowing": "{0} ausgewählte Episoden werden angezeigt",
"LabelSelectUsers": "Benutzer auswählen",
"LabelSendEbookToDevice": "E-Book senden an...", "LabelSendEbookToDevice": "E-Book senden an...",
"LabelSequence": "Reihenfolge", "LabelSequence": "Reihenfolge",
"LabelSeries": "Serien", "LabelSeries": "Serien",
@ -410,16 +446,10 @@
"LabelSettingsHideSingleBookSeriesHelp": "Serien, die ein einzelnes Buch enthalten, werden in den Regalen der Serienseite und der Startseite ausgeblendet.", "LabelSettingsHideSingleBookSeriesHelp": "Serien, die ein einzelnes Buch enthalten, werden in den Regalen der Serienseite und der Startseite ausgeblendet.",
"LabelSettingsHomePageBookshelfView": "Startseite verwendet die Bücherregalansicht", "LabelSettingsHomePageBookshelfView": "Startseite verwendet die Bücherregalansicht",
"LabelSettingsLibraryBookshelfView": "Bibliothek verwendet die Bücherregalansicht", "LabelSettingsLibraryBookshelfView": "Bibliothek verwendet die Bücherregalansicht",
"LabelSettingsOverdriveMediaMarkers": "Verwende Overdrive Media Marker für Kapitel",
"LabelSettingsOverdriveMediaMarkersHelp": "MP3-Dateien von Overdrive werden mit eingebetteten Kapitel-Timings als benutzerdefinierte Metadaten geliefert. Wenn Sie dies aktivieren, werden diese Markierungen automatisch für die Kapiteltaktung verwendet",
"LabelSettingsParseSubtitles": "Analysiere Untertitel", "LabelSettingsParseSubtitles": "Analysiere Untertitel",
"LabelSettingsParseSubtitlesHelp": "Extrahiere den Untertitel von Medium-Ordnernamen.<br>Untertitel müssen vom eigentlichem Titel durch ein \" - \" getrennt sein. <br>Beispiel: \"Titel - Untertitel\"", "LabelSettingsParseSubtitlesHelp": "Extrahiere den Untertitel von Medium-Ordnernamen.<br>Untertitel müssen vom eigentlichem Titel durch ein \" - \" getrennt sein. <br>Beispiel: \"Titel - Untertitel\"",
"LabelSettingsPreferAudioMetadata": "Bevorzuge lokale ID3-Audiometadaten",
"LabelSettingsPreferAudioMetadataHelp": "In den Audiodateien eingebettete ID3 Tags werden anstelle der Ordnernamen für die Bereitstellung der Metadaten verwendet. Wenn keine ID3 Tags zur Verfügung stehen, werden die Ordnernamen verwendet.",
"LabelSettingsPreferMatchedMetadata": "Bevorzuge online abgestimmte Metadaten", "LabelSettingsPreferMatchedMetadata": "Bevorzuge online abgestimmte Metadaten",
"LabelSettingsPreferMatchedMetadataHelp": "Bei einem Schnellabgleich überschreiben online neu abgestimmte Metadaten alle schon vorhandenen Metadaten eines Mediums. Standardmäßig werden bei einem Schnellabgleich nur fehlende Metadaten ersetzt.", "LabelSettingsPreferMatchedMetadataHelp": "Bei einem Schnellabgleich überschreiben online neu abgestimmte Metadaten alle schon vorhandenen Metadaten eines Mediums. Standardmäßig werden bei einem Schnellabgleich nur fehlende Metadaten ersetzt.",
"LabelSettingsPreferOPFMetadata": "Bevorzuge OPF-Metadaten",
"LabelSettingsPreferOPFMetadataHelp": "In OPF-Dateien gespeicherte Metadaten werden anstelle der Ordnernamen für die Bereitstellung der Metadaten verwendet. OPF-Datein sind seperate \"Textdateien\" mit der Endung \".abs\" welche in dem gleichen Ordner liegen wie das Medium selber. In dieser sind verschiedene Metadaten (z.B. Titel, Autor, Jahr, Erzähler, Handlung, ISBN, ...) gespeichert. Wenn keine OPF Datei zur Verfügung steht, wird der Ordnername verwendet.",
"LabelSettingsSkipMatchingBooksWithASIN": "Überspringe beim Online-Abgleich alle Bücher die bereits eine ASIN haben", "LabelSettingsSkipMatchingBooksWithASIN": "Überspringe beim Online-Abgleich alle Bücher die bereits eine ASIN haben",
"LabelSettingsSkipMatchingBooksWithISBN": "Überspringe beim Online-Abgleich alle Bücher die bereits eine ISBN haben", "LabelSettingsSkipMatchingBooksWithISBN": "Überspringe beim Online-Abgleich alle Bücher die bereits eine ISBN haben",
"LabelSettingsSortingIgnorePrefixes": "Vorwort/Artikel beim Sortieren ignorieren", "LabelSettingsSortingIgnorePrefixes": "Vorwort/Artikel beim Sortieren ignorieren",
@ -429,7 +459,7 @@
"LabelSettingsStoreCoversWithItem": "Titelbilder im Medienordner speichern", "LabelSettingsStoreCoversWithItem": "Titelbilder im Medienordner speichern",
"LabelSettingsStoreCoversWithItemHelp": "Standardmäßig werden die Titelbilder in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Titelbilder als jpg Datei in dem gleichen Ordner gespeichert in welchem sich auch das Medium befindet. Es wird immer nur eine Datei mit dem Namen \"cover.jpg\" gespeichert.", "LabelSettingsStoreCoversWithItemHelp": "Standardmäßig werden die Titelbilder in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Titelbilder als jpg Datei in dem gleichen Ordner gespeichert in welchem sich auch das Medium befindet. Es wird immer nur eine Datei mit dem Namen \"cover.jpg\" gespeichert.",
"LabelSettingsStoreMetadataWithItem": "Metadaten als OPF-Datei im Medienordner speichern", "LabelSettingsStoreMetadataWithItem": "Metadaten als OPF-Datei im Medienordner speichern",
"LabelSettingsStoreMetadataWithItemHelp": "Standardmäßig werden die Metadaten in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Metadaten als OPF-Datei (Textdatei) in dem gleichen Ordner gespeichert in welchem sich auch das Medium befindet. Es wird immer nur eine Datei mit dem Namen \"matadata.abs\" gespeichert.", "LabelSettingsStoreMetadataWithItemHelp": "Standardmäßig werden die Metadaten in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Metadaten als OPF-Datei (Textdatei) in dem gleichen Ordner gespeichert in welchem sich auch das Medium befindet",
"LabelSettingsTimeFormat": "Zeitformat", "LabelSettingsTimeFormat": "Zeitformat",
"LabelShowAll": "Alles anzeigen", "LabelShowAll": "Alles anzeigen",
"LabelSize": "Größe", "LabelSize": "Größe",
@ -494,6 +524,7 @@
"LabelUpdateDetailsHelp": "Erlaube das Überschreiben bestehender Details für die ausgewählten Hörbücher wenn eine Übereinstimmung gefunden wird", "LabelUpdateDetailsHelp": "Erlaube das Überschreiben bestehender Details für die ausgewählten Hörbücher wenn eine Übereinstimmung gefunden wird",
"LabelUploaderDragAndDrop": "Ziehen und Ablegen von Dateien oder Ordnern", "LabelUploaderDragAndDrop": "Ziehen und Ablegen von Dateien oder Ordnern",
"LabelUploaderDropFiles": "Dateien löschen", "LabelUploaderDropFiles": "Dateien löschen",
"LabelUploaderItemFetchMetadataHelp": "Automatisches Abholden von Titel, Author und Serien",
"LabelUseChapterTrack": "Kapiteldatei verwenden", "LabelUseChapterTrack": "Kapiteldatei verwenden",
"LabelUseFullTrack": "Gesamte Datei verwenden", "LabelUseFullTrack": "Gesamte Datei verwenden",
"LabelUser": "Benutzer", "LabelUser": "Benutzer",
@ -523,30 +554,35 @@
"MessageChapterErrorStartLtPrev": "Ungültige Kapitelstartzeit: Kapitelanfang < Kapitelanfang vorheriges Kapitel (Kapitelanfang liegt zeitlich vor dem Beginn des vorherigen Kapitels -> Lösung: Kapitelanfang >= Startzeit des vorherigen Kapitels)", "MessageChapterErrorStartLtPrev": "Ungültige Kapitelstartzeit: Kapitelanfang < Kapitelanfang vorheriges Kapitel (Kapitelanfang liegt zeitlich vor dem Beginn des vorherigen Kapitels -> Lösung: Kapitelanfang >= Startzeit des vorherigen Kapitels)",
"MessageChapterStartIsAfter": "Ungültige Kapitelstartzeit: Kapitelanfang > Mediumende (Kapitelanfang liegt nach dem Ende des Mediums)", "MessageChapterStartIsAfter": "Ungültige Kapitelstartzeit: Kapitelanfang > Mediumende (Kapitelanfang liegt nach dem Ende des Mediums)",
"MessageCheckingCron": "Überprüfe Cron...", "MessageCheckingCron": "Überprüfe Cron...",
"MessageConfirmCloseFeed": "Sind Sie sicher, dass Sie diesen Feed schließen wollen?", "MessageConfirmCloseFeed": "Feed wird geschlossen! Sind Sie sicher?",
"MessageConfirmDeleteBackup": "Sind Sie sicher, dass Sie die Sicherung für {0} löschen wollen?", "MessageConfirmDeleteBackup": "Sicherung für {0} wird gelöscht! Sind Sie sicher?",
"MessageConfirmDeleteFile": "Es wird die Datei vom System löschen. Sind Sie sicher?", "MessageConfirmDeleteFile": "Datei wird vom System gelöscht! Sind Sie sicher?",
"MessageConfirmDeleteLibrary": "Sind Sie sicher, dass Sie die Bibliothek \"{0}\" dauerhaft löschen wollen?", "MessageConfirmDeleteLibrary": "Bibliothek \"{0}\" wird dauerhaft gelöscht! Sind Sie sicher?",
"MessageConfirmDeleteSession": "Sind Sie sicher, dass Sie diese Sitzung löschen möchten?", "MessageConfirmDeleteLibraryItem": "Bibliothekselement wird aus der Datenbank + Festplatte gelöscht? Sind Sie sicher?",
"MessageConfirmForceReScan": "Sind Sie sicher, dass Sie einen erneuten Scanvorgang erzwingen wollen?", "MessageConfirmDeleteLibraryItems": "{0} Bibliothekselemente werden aus der Datenbank + Festplatte gelöscht? Sind Sie sicher?",
"MessageConfirmMarkAllEpisodesFinished": "Sind Sie sicher, dass Sie alle Episoden als abgeschlossen markieren möchten?", "MessageConfirmDeleteSession": "Sitzung wird gelöscht! Sind Sie sicher?",
"MessageConfirmMarkAllEpisodesNotFinished": "Sind Sie sicher, dass Sie alle Episoden als nicht abgeschlossen markieren möchten?", "MessageConfirmForceReScan": "Scanvorgang erzwingen! Sind Sie sicher?",
"MessageConfirmMarkSeriesFinished": "Sind Sie sicher, dass Sie alle Medien dieser Reihe als abgeschlossen markieren wollen?", "MessageConfirmMarkAllEpisodesFinished": "Alle Episoden werden als abgeschlossen markiert! Sind Sie sicher?",
"MessageConfirmMarkSeriesNotFinished": "Sind Sie sicher, dass Sie alle Medien dieser Reihe als nicht abgeschlossen markieren wollen?", "MessageConfirmMarkAllEpisodesNotFinished": "Alle Episoden werden als nicht abgeschlossen markiert! Sind Sie sicher?",
"MessageConfirmRemoveAllChapters": "Sind Sie sicher, dass Sie alle Kapitel entfernen möchten?", "MessageConfirmMarkSeriesFinished": "Alle Medien dieser Reihe werden als abgeschlossen markiert! Sind Sie sicher?",
"MessageConfirmRemoveAuthor": "Sind Sie sicher, dass Sie den Autor \"{0}\" enfernen möchten?", "MessageConfirmMarkSeriesNotFinished": "Alle Medien dieser Reihe werden als nicht abgeschlossen markiert! Sind Sie sicher?",
"MessageConfirmRemoveCollection": "Sind Sie sicher, dass Sie die Sammlung \"{0}\" löschen wollen?", "MessageConfirmQuickEmbed": "Warnung! Audiodateien werden bei der Schnelleinbettung nicht gesichert! Achten Sie darauf, dass Sie eine Sicherungskopie der Audiodateien besitzen. <br><br>Möchten Sie fortfahren?",
"MessageConfirmRemoveEpisode": "Sind Sie sicher, dass Sie die Episode \"{0}\" löschen möchten?", "MessageConfirmRemoveAllChapters": "Alle Kapitel werden entfernt! Sind Sie sicher?",
"MessageConfirmRemoveEpisodes": "Sind Sie sicher, dass Sie {0} Episoden löschen wollen?", "MessageConfirmRemoveAuthor": "Autor \"{0}\" wird enfernt! Sind Sie sicher?",
"MessageConfirmRemoveNarrator": "Sind Sie sicher, dass Sie den Erzähler \"{0}\" löschen möchten?", "MessageConfirmRemoveCollection": "Sammlung \"{0}\" wird gelöscht! Sind Sie sicher?",
"MessageConfirmRemovePlaylist": "Sind Sie sicher, dass Sie die Wiedergabeliste \"{0}\" entfernen möchten?", "MessageConfirmRemoveEpisode": "Episode \"{0}\" wird geloscht! Sind Sie sicher?",
"MessageConfirmRenameGenre": "Sind Sie sicher, dass Sie die Kategorie \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts umbenennen wollen?", "MessageConfirmRemoveEpisodes": "{0} Episoden werden gelöscht! Sind Sie sicher?",
"MessageConfirmRemoveListeningSessions": "Sind Sie sicher, dass sie {0} Hörsitzungen enfernen möchten?",
"MessageConfirmRemoveNarrator": "Erzähler \"{0}\" wird gelöscht! Sind Sie sicher?",
"MessageConfirmRemovePlaylist": "Wiedergabeliste \"{0}\" wird entfernt! Sind Sie sicher?",
"MessageConfirmRenameGenre": "Kategorie \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts werden umbenannt! Sind Sie sicher?",
"MessageConfirmRenameGenreMergeNote": "Hinweis: Kategorie existiert bereits -> Kategorien werden zusammengelegt.", "MessageConfirmRenameGenreMergeNote": "Hinweis: Kategorie existiert bereits -> Kategorien werden zusammengelegt.",
"MessageConfirmRenameGenreWarning": "Warnung! Ein ähnliche Kategorie mit einem anderen Wortlaut existiert bereits: \"{0}\".", "MessageConfirmRenameGenreWarning": "Warnung! Ein ähnliche Kategorie mit einem anderen Wortlaut existiert bereits: \"{0}\".",
"MessageConfirmRenameTag": "Sind Sie sicher, dass Sie den Tag \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts umbenennen wollen?", "MessageConfirmRenameTag": "Tag \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts werden umbenannt! Sind Sie sicher?",
"MessageConfirmRenameTagMergeNote": "Hinweis: Tag existiert bereits -> Tags werden zusammengelegt.", "MessageConfirmRenameTagMergeNote": "Hinweis: Tag existiert bereits -> Tags werden zusammengelegt.",
"MessageConfirmRenameTagWarning": "Warnung! Ein ähnlicher Tag mit einem anderen Wortlaut existiert bereits: \"{0}\".", "MessageConfirmRenameTagWarning": "Warnung! Ein ähnlicher Tag mit einem anderen Wortlaut existiert bereits: \"{0}\".",
"MessageConfirmSendEbookToDevice": "Sind Sie sicher, dass sie {0} ebook \"{1}\" auf das Gerät \"{2}\" senden wollen?", "MessageConfirmReScanLibraryItems": "{0} Elemente werden erneut gescannt! Sind Sie sicher?",
"MessageConfirmSendEbookToDevice": "{0} E-Book \"{1}\" werden auf das Gerät \"{2}\" gesendet! Sind Sie sicher?",
"MessageDownloadingEpisode": "Episode herunterladen", "MessageDownloadingEpisode": "Episode herunterladen",
"MessageDragFilesIntoTrackOrder": "Verschieben Sie die Dateien in die richtige Reihenfolge", "MessageDragFilesIntoTrackOrder": "Verschieben Sie die Dateien in die richtige Reihenfolge",
"MessageEmbedFinished": "Einbettung abgeschlossen!", "MessageEmbedFinished": "Einbettung abgeschlossen!",
@ -610,12 +646,13 @@
"MessageRemoveChapter": "Kapitel löschen", "MessageRemoveChapter": "Kapitel löschen",
"MessageRemoveEpisodes": "Entferne {0} Episode(n)", "MessageRemoveEpisodes": "Entferne {0} Episode(n)",
"MessageRemoveFromPlayerQueue": "Aus der Abspielwarteliste löschen", "MessageRemoveFromPlayerQueue": "Aus der Abspielwarteliste löschen",
"MessageRemoveUserWarning": "Sind Sie sicher, dass Sie den Benutzer \"{0}\" dauerhaft löschen wollen?", "MessageRemoveUserWarning": "Benutzer \"{0}\" wird dauerhaft gelöscht! Sind Sie sicher?",
"MessageReportBugsAndContribute": "Fehler melden, Funktionen anfordern und Beiträge leisten auf", "MessageReportBugsAndContribute": "Fehler melden, Funktionen anfordern und Beiträge leisten auf",
"MessageResetChaptersConfirm": "Sind Sie sicher, dass Sie die Kapitel zurücksetzen und die vorgenommenen Änderungen rückgängig machen wollen?", "MessageResetChaptersConfirm": "Kapitel und vorgenommenen Änderungen werden zurückgesetzt und rückgängig gemacht! Sind Sie sicher?",
"MessageRestoreBackupConfirm": "Sind Sie sicher, dass Sie die Sicherung wiederherstellen wollen, welche am", "MessageRestoreBackupConfirm": "Sind Sie sicher, dass Sie die Sicherung wiederherstellen wollen, welche am",
"MessageRestoreBackupWarning": "Bei der Wiederherstellung einer Sicherung wird die gesamte Datenbank unter /config und die Titelbilder in /metadata/items und /metadata/authors überschrieben.<br /><br />Bei der Sicherung werden keine Dateien in Ihren Bibliotheksordnern verändert. Wenn Sie die Servereinstellungen aktiviert haben, um Cover und Metadaten in Ihren Bibliotheksordnern zu speichern, werden diese nicht gesichert oder überschrieben.<br /><br />Alle Clients, die Ihren Server nutzen, werden automatisch aktualisiert.", "MessageRestoreBackupWarning": "Bei der Wiederherstellung einer Sicherung wird die gesamte Datenbank unter /config und die Titelbilder in /metadata/items und /metadata/authors überschrieben.<br /><br />Bei der Sicherung werden keine Dateien in Ihren Bibliotheksordnern verändert. Wenn Sie die Servereinstellungen aktiviert haben, um Cover und Metadaten in Ihren Bibliotheksordnern zu speichern, werden diese nicht gesichert oder überschrieben.<br /><br />Alle Clients, die Ihren Server nutzen, werden automatisch aktualisiert.",
"MessageSearchResultsFor": "Suchergebnisse für", "MessageSearchResultsFor": "Suchergebnisse für",
"MessageSelected": "{0} ausgewählt",
"MessageServerCouldNotBeReached": "Server kann nicht erreicht werden", "MessageServerCouldNotBeReached": "Server kann nicht erreicht werden",
"MessageSetChaptersFromTracksDescription": "Kaitelerstellung basiert auf den existierenden einzelnen Audiodateien. Pro existierende Audiodatei wird 1 Kapitel erstellt, wobei deren Kapitelname aus dem Audiodateinamen extrahiert wird", "MessageSetChaptersFromTracksDescription": "Kaitelerstellung basiert auf den existierenden einzelnen Audiodateien. Pro existierende Audiodatei wird 1 Kapitel erstellt, wobei deren Kapitelname aus dem Audiodateinamen extrahiert wird",
"MessageStartPlaybackAtTime": "Start der Wiedergabe für \"{0}\" bei {1}?", "MessageStartPlaybackAtTime": "Start der Wiedergabe für \"{0}\" bei {1}?",

View File

@ -1,7 +1,10 @@
{ {
"ButtonAdd": "Add", "ButtonAdd": "Add",
"ButtonAddChapters": "Add Chapters", "ButtonAddChapters": "Add Chapters",
"ButtonAddDevice": "Add Device",
"ButtonAddLibrary": "Add Library",
"ButtonAddPodcasts": "Add Podcasts", "ButtonAddPodcasts": "Add Podcasts",
"ButtonAddUser": "Add User",
"ButtonAddYourFirstLibrary": "Add your first library", "ButtonAddYourFirstLibrary": "Add your first library",
"ButtonApply": "Apply", "ButtonApply": "Apply",
"ButtonApplyChapters": "Apply Chapters", "ButtonApplyChapters": "Apply Chapters",
@ -59,6 +62,7 @@
"ButtonRemoveSeriesFromContinueSeries": "Remove Series from Continue Series", "ButtonRemoveSeriesFromContinueSeries": "Remove Series from Continue Series",
"ButtonReScan": "Re-Scan", "ButtonReScan": "Re-Scan",
"ButtonReset": "Reset", "ButtonReset": "Reset",
"ButtonResetToDefault": "Reset to default",
"ButtonRestore": "Restore", "ButtonRestore": "Restore",
"ButtonSave": "Save", "ButtonSave": "Save",
"ButtonSaveAndClose": "Save & Close", "ButtonSaveAndClose": "Save & Close",
@ -83,11 +87,15 @@
"ButtonUserEdit": "Edit user {0}", "ButtonUserEdit": "Edit user {0}",
"ButtonViewAll": "View All", "ButtonViewAll": "View All",
"ButtonYes": "Yes", "ButtonYes": "Yes",
"ErrorUploadFetchMetadataAPI": "Error fetching metadata",
"ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
"ErrorUploadLacksTitle": "Must have a title",
"HeaderAccount": "Account", "HeaderAccount": "Account",
"HeaderAdvanced": "Advanced", "HeaderAdvanced": "Advanced",
"HeaderAppriseNotificationSettings": "Apprise Notification Settings", "HeaderAppriseNotificationSettings": "Apprise Notification Settings",
"HeaderAudiobookTools": "Audiobook File Management Tools", "HeaderAudiobookTools": "Audiobook File Management Tools",
"HeaderAudioTracks": "Audio Tracks", "HeaderAudioTracks": "Audio Tracks",
"HeaderAuthentication": "Authentication",
"HeaderBackups": "Backups", "HeaderBackups": "Backups",
"HeaderChangePassword": "Change Password", "HeaderChangePassword": "Change Password",
"HeaderChapters": "Chapters", "HeaderChapters": "Chapters",
@ -122,12 +130,15 @@
"HeaderManageTags": "Manage Tags", "HeaderManageTags": "Manage Tags",
"HeaderMapDetails": "Map details", "HeaderMapDetails": "Map details",
"HeaderMatch": "Match", "HeaderMatch": "Match",
"HeaderMetadataOrderOfPrecedence": "Metadata order of precedence",
"HeaderMetadataToEmbed": "Metadata to embed", "HeaderMetadataToEmbed": "Metadata to embed",
"HeaderNewAccount": "New Account", "HeaderNewAccount": "New Account",
"HeaderNewLibrary": "New Library", "HeaderNewLibrary": "New Library",
"HeaderNotifications": "Notifications", "HeaderNotifications": "Notifications",
"HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication",
"HeaderOpenRSSFeed": "Open RSS Feed", "HeaderOpenRSSFeed": "Open RSS Feed",
"HeaderOtherFiles": "Other Files", "HeaderOtherFiles": "Other Files",
"HeaderPasswordAuthentication": "Password Authentication",
"HeaderPermissions": "Permissions", "HeaderPermissions": "Permissions",
"HeaderPlayerQueue": "Player Queue", "HeaderPlayerQueue": "Player Queue",
"HeaderPlaylist": "Playlist", "HeaderPlaylist": "Playlist",
@ -176,8 +187,11 @@
"LabelAddToCollectionBatch": "Add {0} Books to Collection", "LabelAddToCollectionBatch": "Add {0} Books to Collection",
"LabelAddToPlaylist": "Add to Playlist", "LabelAddToPlaylist": "Add to Playlist",
"LabelAddToPlaylistBatch": "Add {0} Items to Playlist", "LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
"LabelAdminUsersOnly": "Admin users only",
"LabelAll": "All", "LabelAll": "All",
"LabelAllUsers": "All Users", "LabelAllUsers": "All Users",
"LabelAllUsersExcludingGuests": "All users excluding guests",
"LabelAllUsersIncludingGuests": "All users including guests",
"LabelAlreadyInYourLibrary": "Already in your library", "LabelAlreadyInYourLibrary": "Already in your library",
"LabelAppend": "Append", "LabelAppend": "Append",
"LabelAuthor": "Author", "LabelAuthor": "Author",
@ -185,6 +199,12 @@
"LabelAuthorLastFirst": "Author (Last, First)", "LabelAuthorLastFirst": "Author (Last, First)",
"LabelAuthors": "Authors", "LabelAuthors": "Authors",
"LabelAutoDownloadEpisodes": "Auto Download Episodes", "LabelAutoDownloadEpisodes": "Auto Download Episodes",
"LabelAutoFetchMetadata": "Auto Fetch Metadata",
"LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
"LabelAutoLaunch": "Auto Launch",
"LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Auto Register",
"LabelAutoRegisterDescription": "Automatically create new users after logging in",
"LabelBackToUser": "Back to User", "LabelBackToUser": "Back to User",
"LabelBackupLocation": "Backup Location", "LabelBackupLocation": "Backup Location",
"LabelBackupsEnableAutomaticBackups": "Enable automatic backups", "LabelBackupsEnableAutomaticBackups": "Enable automatic backups",
@ -195,11 +215,13 @@
"LabelBackupsNumberToKeepHelp": "Only 1 backup will be removed at a time so if you already have more backups than this you should manually remove them.", "LabelBackupsNumberToKeepHelp": "Only 1 backup will be removed at a time so if you already have more backups than this you should manually remove them.",
"LabelBitrate": "Bitrate", "LabelBitrate": "Bitrate",
"LabelBooks": "Books", "LabelBooks": "Books",
"LabelButtonText": "Button Text",
"LabelChangePassword": "Change Password", "LabelChangePassword": "Change Password",
"LabelChannels": "Channels", "LabelChannels": "Channels",
"LabelChapters": "Chapters", "LabelChapters": "Chapters",
"LabelChaptersFound": "chapters found", "LabelChaptersFound": "chapters found",
"LabelChapterTitle": "Chapter Title", "LabelChapterTitle": "Chapter Title",
"LabelClickForMoreInfo": "Click for more info",
"LabelClosePlayer": "Close player", "LabelClosePlayer": "Close player",
"LabelCodec": "Codec", "LabelCodec": "Codec",
"LabelCollapseSeries": "Collapse Series", "LabelCollapseSeries": "Collapse Series",
@ -218,10 +240,12 @@
"LabelCurrently": "Currently:", "LabelCurrently": "Currently:",
"LabelCustomCronExpression": "Custom Cron Expression:", "LabelCustomCronExpression": "Custom Cron Expression:",
"LabelDatetime": "Datetime", "LabelDatetime": "Datetime",
"LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)",
"LabelDescription": "Description", "LabelDescription": "Description",
"LabelDeselectAll": "Deselect All", "LabelDeselectAll": "Deselect All",
"LabelDevice": "Device", "LabelDevice": "Device",
"LabelDeviceInfo": "Device Info", "LabelDeviceInfo": "Device Info",
"LabelDeviceIsAvailableTo": "Device is available to...",
"LabelDirectory": "Directory", "LabelDirectory": "Directory",
"LabelDiscFromFilename": "Disc from Filename", "LabelDiscFromFilename": "Disc from Filename",
"LabelDiscFromMetadata": "Disc from Metadata", "LabelDiscFromMetadata": "Disc from Metadata",
@ -247,6 +271,7 @@
"LabelExample": "Example", "LabelExample": "Example",
"LabelExplicit": "Explicit", "LabelExplicit": "Explicit",
"LabelFeedURL": "Feed URL", "LabelFeedURL": "Feed URL",
"LabelFetchingMetadata": "Fetching Metadata",
"LabelFile": "File", "LabelFile": "File",
"LabelFileBirthtime": "File Birthtime", "LabelFileBirthtime": "File Birthtime",
"LabelFileModified": "File Modified", "LabelFileModified": "File Modified",
@ -256,6 +281,7 @@
"LabelFinished": "Finished", "LabelFinished": "Finished",
"LabelFolder": "Folder", "LabelFolder": "Folder",
"LabelFolders": "Folders", "LabelFolders": "Folders",
"LabelFontFamily": "Font family",
"LabelFontScale": "Font scale", "LabelFontScale": "Font scale",
"LabelFormat": "Format", "LabelFormat": "Format",
"LabelGenre": "Genre", "LabelGenre": "Genre",
@ -263,9 +289,11 @@
"LabelHardDeleteFile": "Hard delete file", "LabelHardDeleteFile": "Hard delete file",
"LabelHasEbook": "Has ebook", "LabelHasEbook": "Has ebook",
"LabelHasSupplementaryEbook": "Has supplementary ebook", "LabelHasSupplementaryEbook": "Has supplementary ebook",
"LabelHighestPriority": "Highest priority",
"LabelHost": "Host", "LabelHost": "Host",
"LabelHour": "Hour", "LabelHour": "Hour",
"LabelIcon": "Icon", "LabelIcon": "Icon",
"LabelImageURLFromTheWeb": "Image URL from the web",
"LabelIncludeInTracklist": "Include in Tracklist", "LabelIncludeInTracklist": "Include in Tracklist",
"LabelIncomplete": "Incomplete", "LabelIncomplete": "Incomplete",
"LabelInProgress": "In Progress", "LabelInProgress": "In Progress",
@ -303,14 +331,20 @@
"LabelLogLevelInfo": "Info", "LabelLogLevelInfo": "Info",
"LabelLogLevelWarn": "Warn", "LabelLogLevelWarn": "Warn",
"LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date", "LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date",
"LabelLowestPriority": "Lowest Priority",
"LabelMatchExistingUsersBy": "Match existing users by",
"LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider",
"LabelMediaPlayer": "Media Player", "LabelMediaPlayer": "Media Player",
"LabelMediaType": "Media Type", "LabelMediaType": "Media Type",
"LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources",
"LabelMetadataProvider": "Metadata Provider", "LabelMetadataProvider": "Metadata Provider",
"LabelMetaTag": "Meta Tag", "LabelMetaTag": "Meta Tag",
"LabelMetaTags": "Meta Tags", "LabelMetaTags": "Meta Tags",
"LabelMinute": "Minute", "LabelMinute": "Minute",
"LabelMissing": "Missing", "LabelMissing": "Missing",
"LabelMissingParts": "Missing Parts", "LabelMissingParts": "Missing Parts",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
"LabelMore": "More", "LabelMore": "More",
"LabelMoreInfo": "More Info", "LabelMoreInfo": "More Info",
"LabelName": "Name", "LabelName": "Name",
@ -372,6 +406,7 @@
"LabelRegion": "Region", "LabelRegion": "Region",
"LabelReleaseDate": "Release Date", "LabelReleaseDate": "Release Date",
"LabelRemoveCover": "Remove cover", "LabelRemoveCover": "Remove cover",
"LabelRowsPerPage": "Rows per page",
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email", "LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
"LabelRSSFeedCustomOwnerName": "Custom owner Name", "LabelRSSFeedCustomOwnerName": "Custom owner Name",
"LabelRSSFeedOpen": "RSS Feed Open", "LabelRSSFeedOpen": "RSS Feed Open",
@ -384,6 +419,7 @@
"LabelSeason": "Season", "LabelSeason": "Season",
"LabelSelectAllEpisodes": "Select all episodes", "LabelSelectAllEpisodes": "Select all episodes",
"LabelSelectEpisodesShowing": "Select {0} episodes showing", "LabelSelectEpisodesShowing": "Select {0} episodes showing",
"LabelSelectUsers": "Select users",
"LabelSendEbookToDevice": "Send Ebook to...", "LabelSendEbookToDevice": "Send Ebook to...",
"LabelSequence": "Sequence", "LabelSequence": "Sequence",
"LabelSeries": "Series", "LabelSeries": "Series",
@ -410,16 +446,10 @@
"LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.", "LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.",
"LabelSettingsHomePageBookshelfView": "Home page use bookshelf view", "LabelSettingsHomePageBookshelfView": "Home page use bookshelf view",
"LabelSettingsLibraryBookshelfView": "Library use bookshelf view", "LabelSettingsLibraryBookshelfView": "Library use bookshelf view",
"LabelSettingsOverdriveMediaMarkers": "Use Overdrive Media Markers for chapters",
"LabelSettingsOverdriveMediaMarkersHelp": "MP3 files from Overdrive come with chapter timings embedded as custom metadata. Enabling this will use these tags for chapter timings automatically",
"LabelSettingsParseSubtitles": "Parse subtitles", "LabelSettingsParseSubtitles": "Parse subtitles",
"LabelSettingsParseSubtitlesHelp": "Extract subtitles from audiobook folder names.<br>Subtitle must be seperated by \" - \"<br>i.e. \"Book Title - A Subtitle Here\" has the subtitle \"A Subtitle Here\"", "LabelSettingsParseSubtitlesHelp": "Extract subtitles from audiobook folder names.<br>Subtitle must be seperated by \" - \"<br>i.e. \"Book Title - A Subtitle Here\" has the subtitle \"A Subtitle Here\"",
"LabelSettingsPreferAudioMetadata": "Prefer audio metadata",
"LabelSettingsPreferAudioMetadataHelp": "Audio file ID3 meta tags will be used for book details over folder names",
"LabelSettingsPreferMatchedMetadata": "Prefer matched metadata", "LabelSettingsPreferMatchedMetadata": "Prefer matched metadata",
"LabelSettingsPreferMatchedMetadataHelp": "Matched data will override item details when using Quick Match. By default Quick Match will only fill in missing details.", "LabelSettingsPreferMatchedMetadataHelp": "Matched data will override item details when using Quick Match. By default Quick Match will only fill in missing details.",
"LabelSettingsPreferOPFMetadata": "Prefer OPF metadata",
"LabelSettingsPreferOPFMetadataHelp": "OPF file metadata will be used for book details over folder names",
"LabelSettingsSkipMatchingBooksWithASIN": "Skip matching books that already have an ASIN", "LabelSettingsSkipMatchingBooksWithASIN": "Skip matching books that already have an ASIN",
"LabelSettingsSkipMatchingBooksWithISBN": "Skip matching books that already have an ISBN", "LabelSettingsSkipMatchingBooksWithISBN": "Skip matching books that already have an ISBN",
"LabelSettingsSortingIgnorePrefixes": "Ignore prefixes when sorting", "LabelSettingsSortingIgnorePrefixes": "Ignore prefixes when sorting",
@ -429,7 +459,7 @@
"LabelSettingsStoreCoversWithItem": "Store covers with item", "LabelSettingsStoreCoversWithItem": "Store covers with item",
"LabelSettingsStoreCoversWithItemHelp": "By default covers are stored in /metadata/items, enabling this setting will store covers in your library item folder. Only one file named \"cover\" will be kept", "LabelSettingsStoreCoversWithItemHelp": "By default covers are stored in /metadata/items, enabling this setting will store covers in your library item folder. Only one file named \"cover\" will be kept",
"LabelSettingsStoreMetadataWithItem": "Store metadata with item", "LabelSettingsStoreMetadataWithItem": "Store metadata with item",
"LabelSettingsStoreMetadataWithItemHelp": "By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders. Uses .abs file extension", "LabelSettingsStoreMetadataWithItemHelp": "By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders",
"LabelSettingsTimeFormat": "Time Format", "LabelSettingsTimeFormat": "Time Format",
"LabelShowAll": "Show All", "LabelShowAll": "Show All",
"LabelSize": "Size", "LabelSize": "Size",
@ -494,6 +524,7 @@
"LabelUpdateDetailsHelp": "Allow overwriting of existing details for the selected books when a match is located", "LabelUpdateDetailsHelp": "Allow overwriting of existing details for the selected books when a match is located",
"LabelUploaderDragAndDrop": "Drag & drop files or folders", "LabelUploaderDragAndDrop": "Drag & drop files or folders",
"LabelUploaderDropFiles": "Drop files", "LabelUploaderDropFiles": "Drop files",
"LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
"LabelUseChapterTrack": "Use chapter track", "LabelUseChapterTrack": "Use chapter track",
"LabelUseFullTrack": "Use full track", "LabelUseFullTrack": "Use full track",
"LabelUser": "User", "LabelUser": "User",
@ -527,17 +558,21 @@
"MessageConfirmDeleteBackup": "Are you sure you want to delete backup for {0}?", "MessageConfirmDeleteBackup": "Are you sure you want to delete backup for {0}?",
"MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?", "MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?",
"MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?", "MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?",
"MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?",
"MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?",
"MessageConfirmDeleteSession": "Are you sure you want to delete this session?", "MessageConfirmDeleteSession": "Are you sure you want to delete this session?",
"MessageConfirmForceReScan": "Are you sure you want to force re-scan?", "MessageConfirmForceReScan": "Are you sure you want to force re-scan?",
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?", "MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?", "MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
"MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?", "MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
"MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?", "MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
"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?",
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?", "MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
"MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?", "MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?",
"MessageConfirmRemoveCollection": "Are you sure you want to remove collection \"{0}\"?", "MessageConfirmRemoveCollection": "Are you sure you want to remove collection \"{0}\"?",
"MessageConfirmRemoveEpisode": "Are you sure you want to remove episode \"{0}\"?", "MessageConfirmRemoveEpisode": "Are you sure you want to remove episode \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} episodes?", "MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} episodes?",
"MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?", "MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?", "MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
"MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?", "MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?",
@ -546,6 +581,7 @@
"MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?", "MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
"MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.", "MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
"MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".", "MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
"MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?",
"MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?", "MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
"MessageDownloadingEpisode": "Downloading episode", "MessageDownloadingEpisode": "Downloading episode",
"MessageDragFilesIntoTrackOrder": "Drag files into correct track order", "MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
@ -616,6 +652,7 @@
"MessageRestoreBackupConfirm": "Are you sure you want to restore the backup created on", "MessageRestoreBackupConfirm": "Are you sure you want to restore the backup created on",
"MessageRestoreBackupWarning": "Restoring a backup will overwrite the entire database located at /config and cover images in /metadata/items & /metadata/authors.<br /><br />Backups do not modify any files in your library folders. If you have enabled server settings to store cover art and metadata in your library folders then those are not backed up or overwritten.<br /><br />All clients using your server will be automatically refreshed.", "MessageRestoreBackupWarning": "Restoring a backup will overwrite the entire database located at /config and cover images in /metadata/items & /metadata/authors.<br /><br />Backups do not modify any files in your library folders. If you have enabled server settings to store cover art and metadata in your library folders then those are not backed up or overwritten.<br /><br />All clients using your server will be automatically refreshed.",
"MessageSearchResultsFor": "Search results for", "MessageSearchResultsFor": "Search results for",
"MessageSelected": "{0} selected",
"MessageServerCouldNotBeReached": "Server could not be reached", "MessageServerCouldNotBeReached": "Server could not be reached",
"MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name", "MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name",
"MessageStartPlaybackAtTime": "Start playback for \"{0}\" at {1}?", "MessageStartPlaybackAtTime": "Start playback for \"{0}\" at {1}?",

View File

@ -1,7 +1,10 @@
{ {
"ButtonAdd": "Agregar", "ButtonAdd": "Agregar",
"ButtonAddChapters": "Agregar Capitulo", "ButtonAddChapters": "Agregar Capitulo",
"ButtonAddDevice": "Add Device",
"ButtonAddLibrary": "Add Library",
"ButtonAddPodcasts": "Agregar Podcasts", "ButtonAddPodcasts": "Agregar Podcasts",
"ButtonAddUser": "Add User",
"ButtonAddYourFirstLibrary": "Agrega tu Primera Biblioteca", "ButtonAddYourFirstLibrary": "Agrega tu Primera Biblioteca",
"ButtonApply": "Aplicar", "ButtonApply": "Aplicar",
"ButtonApplyChapters": "Aplicar Capítulos", "ButtonApplyChapters": "Aplicar Capítulos",
@ -59,6 +62,7 @@
"ButtonRemoveSeriesFromContinueSeries": "Remover Serie de Continuar Series", "ButtonRemoveSeriesFromContinueSeries": "Remover Serie de Continuar Series",
"ButtonReScan": "Re-Escanear", "ButtonReScan": "Re-Escanear",
"ButtonReset": "Reiniciar", "ButtonReset": "Reiniciar",
"ButtonResetToDefault": "Reset to default",
"ButtonRestore": "Restaurar", "ButtonRestore": "Restaurar",
"ButtonSave": "Guardar", "ButtonSave": "Guardar",
"ButtonSaveAndClose": "Guardar y Cerrar", "ButtonSaveAndClose": "Guardar y Cerrar",
@ -83,11 +87,15 @@
"ButtonUserEdit": "Editar Usuario {0}", "ButtonUserEdit": "Editar Usuario {0}",
"ButtonViewAll": "Ver Todos", "ButtonViewAll": "Ver Todos",
"ButtonYes": "Aceptar", "ButtonYes": "Aceptar",
"ErrorUploadFetchMetadataAPI": "Error fetching metadata",
"ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
"ErrorUploadLacksTitle": "Must have a title",
"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",
"HeaderBackups": "Respaldos", "HeaderBackups": "Respaldos",
"HeaderChangePassword": "Cambiar Contraseña", "HeaderChangePassword": "Cambiar Contraseña",
"HeaderChapters": "Capítulos", "HeaderChapters": "Capítulos",
@ -122,12 +130,15 @@
"HeaderManageTags": "Administrar Etiquetas", "HeaderManageTags": "Administrar Etiquetas",
"HeaderMapDetails": "Asignar Detalles", "HeaderMapDetails": "Asignar Detalles",
"HeaderMatch": "Encontrar", "HeaderMatch": "Encontrar",
"HeaderMetadataOrderOfPrecedence": "Metadata order of precedence",
"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",
"HeaderOpenRSSFeed": "Abrir fuente RSS", "HeaderOpenRSSFeed": "Abrir fuente RSS",
"HeaderOtherFiles": "Otros Archivos", "HeaderOtherFiles": "Otros Archivos",
"HeaderPasswordAuthentication": "Password Authentication",
"HeaderPermissions": "Permisos", "HeaderPermissions": "Permisos",
"HeaderPlayerQueue": "Fila del Reproductor", "HeaderPlayerQueue": "Fila del Reproductor",
"HeaderPlaylist": "Lista de Reproducción", "HeaderPlaylist": "Lista de Reproducción",
@ -176,8 +187,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",
"LabelAll": "Todos", "LabelAll": "Todos",
"LabelAllUsers": "Todos los Usuarios", "LabelAllUsers": "Todos los Usuarios",
"LabelAllUsersExcludingGuests": "All users excluding guests",
"LabelAllUsersIncludingGuests": "All users including guests",
"LabelAlreadyInYourLibrary": "Ya en la Biblioteca", "LabelAlreadyInYourLibrary": "Ya en la Biblioteca",
"LabelAppend": "Adjuntar", "LabelAppend": "Adjuntar",
"LabelAuthor": "Autor", "LabelAuthor": "Autor",
@ -185,6 +199,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",
"LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
"LabelAutoLaunch": "Auto Launch",
"LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Auto Register",
"LabelAutoRegisterDescription": "Automatically create new users after logging in",
"LabelBackToUser": "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",
@ -195,11 +215,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",
"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",
"LabelClosePlayer": "Cerrar Reproductor", "LabelClosePlayer": "Cerrar Reproductor",
"LabelCodec": "Codec", "LabelCodec": "Codec",
"LabelCollapseSeries": "Colapsar Serie", "LabelCollapseSeries": "Colapsar Serie",
@ -218,10 +240,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)",
"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...",
"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",
@ -247,6 +271,7 @@
"LabelExample": "Ejemplo", "LabelExample": "Ejemplo",
"LabelExplicit": "Explicito", "LabelExplicit": "Explicito",
"LabelFeedURL": "Fuente de URL", "LabelFeedURL": "Fuente de URL",
"LabelFetchingMetadata": "Fetching Metadata",
"LabelFile": "Archivo", "LabelFile": "Archivo",
"LabelFileBirthtime": "Archivo Creado en", "LabelFileBirthtime": "Archivo Creado en",
"LabelFileModified": "Archivo modificado", "LabelFileModified": "Archivo modificado",
@ -256,6 +281,7 @@
"LabelFinished": "Terminado", "LabelFinished": "Terminado",
"LabelFolder": "Carpeta", "LabelFolder": "Carpeta",
"LabelFolders": "Carpetas", "LabelFolders": "Carpetas",
"LabelFontFamily": "Familia tipográfica",
"LabelFontScale": "Tamaño de Fuente", "LabelFontScale": "Tamaño de Fuente",
"LabelFormat": "Formato", "LabelFormat": "Formato",
"LabelGenre": "Genero", "LabelGenre": "Genero",
@ -263,9 +289,11 @@
"LabelHardDeleteFile": "Eliminar Definitivamente", "LabelHardDeleteFile": "Eliminar Definitivamente",
"LabelHasEbook": "Tiene Ebook", "LabelHasEbook": "Tiene Ebook",
"LabelHasSupplementaryEbook": "Tiene Ebook Suplementario", "LabelHasSupplementaryEbook": "Tiene Ebook Suplementario",
"LabelHighestPriority": "Highest priority",
"LabelHost": "Host", "LabelHost": "Host",
"LabelHour": "Hora", "LabelHour": "Hora",
"LabelIcon": "Icono", "LabelIcon": "Icono",
"LabelImageURLFromTheWeb": "Image URL from the web",
"LabelIncludeInTracklist": "Incluir en Tracklist", "LabelIncludeInTracklist": "Incluir en Tracklist",
"LabelIncomplete": "Incompleto", "LabelIncomplete": "Incompleto",
"LabelInProgress": "En Proceso", "LabelInProgress": "En Proceso",
@ -303,14 +331,20 @@
"LabelLogLevelInfo": "Información", "LabelLogLevelInfo": "Información",
"LabelLogLevelWarn": "Advertencia", "LabelLogLevelWarn": "Advertencia",
"LabelLookForNewEpisodesAfterDate": "Buscar Nuevos Episodios a partir de esta Fecha", "LabelLookForNewEpisodesAfterDate": "Buscar Nuevos Episodios a partir de esta Fecha",
"LabelLowestPriority": "Lowest Priority",
"LabelMatchExistingUsersBy": "Match existing users by",
"LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider",
"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",
"LabelMetadataProvider": "Proveedor de Metadata", "LabelMetadataProvider": "Proveedor de Metadata",
"LabelMetaTag": "Meta Tag", "LabelMetaTag": "Meta Tag",
"LabelMetaTags": "Meta Tags", "LabelMetaTags": "Meta Tags",
"LabelMinute": "Minuto", "LabelMinute": "Minuto",
"LabelMissing": "Ausente", "LabelMissing": "Ausente",
"LabelMissingParts": "Partes Ausentes", "LabelMissingParts": "Partes Ausentes",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
"LabelMore": "Más", "LabelMore": "Más",
"LabelMoreInfo": "Más Información", "LabelMoreInfo": "Más Información",
"LabelName": "Nombre", "LabelName": "Nombre",
@ -372,6 +406,7 @@
"LabelRegion": "Región", "LabelRegion": "Región",
"LabelReleaseDate": "Fecha de Estreno", "LabelReleaseDate": "Fecha de Estreno",
"LabelRemoveCover": "Remover Portada", "LabelRemoveCover": "Remover Portada",
"LabelRowsPerPage": "Rows per page",
"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",
@ -384,6 +419,7 @@
"LabelSeason": "Temporada", "LabelSeason": "Temporada",
"LabelSelectAllEpisodes": "Seleccionar todos los episodios", "LabelSelectAllEpisodes": "Seleccionar todos los episodios",
"LabelSelectEpisodesShowing": "Seleccionar los {0} episodios visibles", "LabelSelectEpisodesShowing": "Seleccionar los {0} episodios visibles",
"LabelSelectUsers": "Select users",
"LabelSendEbookToDevice": "Enviar Ebook a...", "LabelSendEbookToDevice": "Enviar Ebook a...",
"LabelSequence": "Secuencia", "LabelSequence": "Secuencia",
"LabelSeries": "Series", "LabelSeries": "Series",
@ -410,16 +446,10 @@
"LabelSettingsHideSingleBookSeriesHelp": "Las series con un solo libro no aparecerán en la página de series ni la repisa para series de la página principal.", "LabelSettingsHideSingleBookSeriesHelp": "Las series con un solo libro no aparecerán en la página de series ni la repisa para series de la página principal.",
"LabelSettingsHomePageBookshelfView": "Usar la vista de librero en la página principal", "LabelSettingsHomePageBookshelfView": "Usar la vista de librero en la página principal",
"LabelSettingsLibraryBookshelfView": "Usar la vista de librero en la biblioteca", "LabelSettingsLibraryBookshelfView": "Usar la vista de librero en la biblioteca",
"LabelSettingsOverdriveMediaMarkers": "Usar Overdrive Media Markers para estos capítulos",
"LabelSettingsOverdriveMediaMarkersHelp": "Los archivos MP3 de Overdrive vienen con capítulos con tiempos incrustados como metadatos personalizados. Habilitar esta opción utilizará automáticamente estas etiquetas para los tiempos de los capítulos.",
"LabelSettingsParseSubtitles": "Extraer Subtítulos", "LabelSettingsParseSubtitles": "Extraer Subtítulos",
"LabelSettingsParseSubtitlesHelp": "Extraer subtítulos de los nombres de las carpetas de los audiolibros.<br>Los subtítulos deben estar separados por \" - \"<br>Por ejemplo: \"Ejemplo de Título - Subtítulo Aquí\" tiene el subtítulo \"Subtítulo Aquí\"", "LabelSettingsParseSubtitlesHelp": "Extraer subtítulos de los nombres de las carpetas de los audiolibros.<br>Los subtítulos deben estar separados por \" - \"<br>Por ejemplo: \"Ejemplo de Título - Subtítulo Aquí\" tiene el subtítulo \"Subtítulo Aquí\"",
"LabelSettingsPreferAudioMetadata": "Preferir metadatos del archivo de audio",
"LabelSettingsPreferAudioMetadataHelp": "Preferir los metadatos ID3 del archivo de audio en vez de los nombres de carpetas para los detalles de libros",
"LabelSettingsPreferMatchedMetadata": "Preferir metadatos encontrados", "LabelSettingsPreferMatchedMetadata": "Preferir metadatos encontrados",
"LabelSettingsPreferMatchedMetadataHelp": "Los datos encontrados sobreescribirán los detalles del elemento cuando se use \"Encontrar Rápido\". Por defecto, \"Encontrar Rápido\" sólo completará los detalles faltantes.", "LabelSettingsPreferMatchedMetadataHelp": "Los datos encontrados sobreescribirán los detalles del elemento cuando se use \"Encontrar Rápido\". Por defecto, \"Encontrar Rápido\" sólo completará los detalles faltantes.",
"LabelSettingsPreferOPFMetadata": "Preferir Metadatos OPF",
"LabelSettingsPreferOPFMetadataHelp": "Preferir los archivos de metadatos OPF en vez de los nombres de carpetas para los detalles de los libros.",
"LabelSettingsSkipMatchingBooksWithASIN": "Omitir libros coincidentes que ya tengan un ASIN", "LabelSettingsSkipMatchingBooksWithASIN": "Omitir libros coincidentes que ya tengan un ASIN",
"LabelSettingsSkipMatchingBooksWithISBN": "Omitir libros coincidentes que ya tengan un ISBN", "LabelSettingsSkipMatchingBooksWithISBN": "Omitir libros coincidentes que ya tengan un ISBN",
"LabelSettingsSortingIgnorePrefixes": "Ignorar prefijos al ordenar", "LabelSettingsSortingIgnorePrefixes": "Ignorar prefijos al ordenar",
@ -429,7 +459,7 @@
"LabelSettingsStoreCoversWithItem": "Guardar portadas con elementos", "LabelSettingsStoreCoversWithItem": "Guardar portadas con elementos",
"LabelSettingsStoreCoversWithItemHelp": "Por defecto, las portadas se almacenan en /metadata/items. Si habilita esta opción, las portadas se almacenarán en la carpeta de elementos de su biblioteca. Se guardará un solo archivo llamado \"cover\".", "LabelSettingsStoreCoversWithItemHelp": "Por defecto, las portadas se almacenan en /metadata/items. Si habilita esta opción, las portadas se almacenarán en la carpeta de elementos de su biblioteca. Se guardará un solo archivo llamado \"cover\".",
"LabelSettingsStoreMetadataWithItem": "Guardar metadatos con elementos", "LabelSettingsStoreMetadataWithItem": "Guardar metadatos con elementos",
"LabelSettingsStoreMetadataWithItemHelp": "Por defecto, los archivos de metadatos se almacenan en /metadata/items. Si habilita esta opción, los archivos de metadatos se guardarán en la carpeta de elementos de su biblioteca. Usa la extensión .abs", "LabelSettingsStoreMetadataWithItemHelp": "Por defecto, los archivos de metadatos se almacenan en /metadata/items. Si habilita esta opción, los archivos de metadatos se guardarán en la carpeta de elementos de su biblioteca",
"LabelSettingsTimeFormat": "Formato de Tiempo", "LabelSettingsTimeFormat": "Formato de Tiempo",
"LabelShowAll": "Mostrar Todos", "LabelShowAll": "Mostrar Todos",
"LabelSize": "Tamaño", "LabelSize": "Tamaño",
@ -494,6 +524,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",
"LabelUseChapterTrack": "Usar pista por capitulo", "LabelUseChapterTrack": "Usar pista por capitulo",
"LabelUseFullTrack": "Usar pista completa", "LabelUseFullTrack": "Usar pista completa",
"LabelUser": "Usuario", "LabelUser": "Usuario",
@ -527,17 +558,21 @@
"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?",
"MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?",
"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?",
"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}\"?",
"MessageConfirmRemoveEpisode": "¿Está seguro de que desea remover el episodio \"{0}\"?", "MessageConfirmRemoveEpisode": "¿Está seguro de que desea remover el episodio \"{0}\"?",
"MessageConfirmRemoveEpisodes": "¿Está seguro de que desea remover {0} episodios?", "MessageConfirmRemoveEpisodes": "¿Está seguro de que desea remover {0} episodios?",
"MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "¿Está seguro de que desea remover el narrador \"{0}\"?", "MessageConfirmRemoveNarrator": "¿Está seguro de que desea remover el narrador \"{0}\"?",
"MessageConfirmRemovePlaylist": "¿Está seguro de que desea remover la lista de reproducción \"{0}\"?", "MessageConfirmRemovePlaylist": "¿Está seguro de que desea remover la lista de reproducción \"{0}\"?",
"MessageConfirmRenameGenre": "¿Está seguro de que desea renombrar el genero \"{0}\" a \"{1}\" de todos los elementos?", "MessageConfirmRenameGenre": "¿Está seguro de que desea renombrar el genero \"{0}\" a \"{1}\" de todos los elementos?",
@ -546,6 +581,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?",
"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.",
@ -616,6 +652,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",
"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}?",

View File

@ -1,7 +1,10 @@
{ {
"ButtonAdd": "Ajouter", "ButtonAdd": "Ajouter",
"ButtonAddChapters": "Ajouter le chapitre", "ButtonAddChapters": "Ajouter le chapitre",
"ButtonAddDevice": "Ajouter un appareil",
"ButtonAddLibrary": "Ajouter une bibliothèque",
"ButtonAddPodcasts": "Ajouter des podcasts", "ButtonAddPodcasts": "Ajouter des podcasts",
"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 les chapitres",
@ -59,6 +62,7 @@
"ButtonRemoveSeriesFromContinueSeries": "Ne plus continuer à écouter la série", "ButtonRemoveSeriesFromContinueSeries": "Ne plus continuer à écouter la série",
"ButtonReScan": "Nouvelle analyse", "ButtonReScan": "Nouvelle analyse",
"ButtonReset": "Réinitialiser", "ButtonReset": "Réinitialiser",
"ButtonResetToDefault": "Réinitialiser aux valeurs par défaut",
"ButtonRestore": "Rétablir", "ButtonRestore": "Rétablir",
"ButtonSave": "Sauvegarder", "ButtonSave": "Sauvegarder",
"ButtonSaveAndClose": "Sauvegarder et Fermer", "ButtonSaveAndClose": "Sauvegarder et Fermer",
@ -83,17 +87,21 @@
"ButtonUserEdit": "Modifier lutilisateur {0}", "ButtonUserEdit": "Modifier lutilisateur {0}",
"ButtonViewAll": "Afficher tout", "ButtonViewAll": "Afficher tout",
"ButtonYes": "Oui", "ButtonYes": "Oui",
"ErrorUploadFetchMetadataAPI": "Erreur lors de la récupération des métadonnées",
"ErrorUploadFetchMetadataNoResults": "Impossible de récupérer les métadonnées - essayez de mettre à jour le titre et/ou lauteur.",
"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 Fichier Audiobook",
"HeaderAudioTracks": "Pistes audio", "HeaderAudioTracks": "Pistes audio",
"HeaderAuthentication": "Authentication",
"HeaderBackups": "Sauvegardes", "HeaderBackups": "Sauvegardes",
"HeaderChangePassword": "Modifier le mot de passe", "HeaderChangePassword": "Modifier le mot de passe",
"HeaderChapters": "Chapitres", "HeaderChapters": "Chapitres",
"HeaderChooseAFolder": "Choisir un dossier", "HeaderChooseAFolder": "Choisir un dossier",
"HeaderCollection": "Collection", "HeaderCollection": "Collection",
"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",
"HeaderDetails": "Détails", "HeaderDetails": "Détails",
@ -106,10 +114,10 @@
"HeaderEreaderSettings": "Options Ereader", "HeaderEreaderSettings": "Options Ereader",
"HeaderFiles": "Fichiers", "HeaderFiles": "Fichiers",
"HeaderFindChapters": "Trouver les chapitres", "HeaderFindChapters": "Trouver les chapitres",
"HeaderIgnoredFiles": "Fichiers Ignorés", "HeaderIgnoredFiles": "Fichiers ignorés",
"HeaderItemFiles": "Fichiers des Articles", "HeaderItemFiles": "Fichiers des articles",
"HeaderItemMetadataUtils": "Outils de gestion des métadonnées", "HeaderItemMetadataUtils": "Outils de gestion des métadonnées",
"HeaderLastListeningSession": "Dernière Session découte", "HeaderLastListeningSession": "Dernière session découte",
"HeaderLatestEpisodes": "Dernier épisodes", "HeaderLatestEpisodes": "Dernier épisodes",
"HeaderLibraries": "Bibliothèque", "HeaderLibraries": "Bibliothèque",
"HeaderLibraryFiles": "Fichier de bibliothèque", "HeaderLibraryFiles": "Fichier de bibliothèque",
@ -122,12 +130,15 @@
"HeaderManageTags": "Gérer les étiquettes", "HeaderManageTags": "Gérer les étiquettes",
"HeaderMapDetails": "Édition en masse", "HeaderMapDetails": "Édition en masse",
"HeaderMatch": "Chercher", "HeaderMatch": "Chercher",
"HeaderMetadataToEmbed": "Métadonnée à intégrer", "HeaderMetadataOrderOfPrecedence": "Ordre de priorité des métadonnées",
"HeaderMetadataToEmbed": "Métadonnées à intégrer",
"HeaderNewAccount": "Nouveau compte", "HeaderNewAccount": "Nouveau compte",
"HeaderNewLibrary": "Nouvelle bibliothèque", "HeaderNewLibrary": "Nouvelle bibliothèque",
"HeaderNotifications": "Notifications", "HeaderNotifications": "Notifications",
"HeaderOpenRSSFeed": "Ouvrir Flux RSS", "HeaderOpenIDConnectAuthentication": "Authentification via OpenID Connect",
"HeaderOpenRSSFeed": "Ouvrir un flux RSS",
"HeaderOtherFiles": "Autres fichiers", "HeaderOtherFiles": "Autres fichiers",
"HeaderPasswordAuthentication": "Authentification par mot de passe",
"HeaderPermissions": "Permissions", "HeaderPermissions": "Permissions",
"HeaderPlayerQueue": "Liste découte", "HeaderPlayerQueue": "Liste découte",
"HeaderPlaylist": "Liste de lecture", "HeaderPlaylist": "Liste de lecture",
@ -143,7 +154,7 @@
"HeaderSchedule": "Programmation", "HeaderSchedule": "Programmation",
"HeaderScheduleLibraryScans": "Analyse automatique de la bibliothèque", "HeaderScheduleLibraryScans": "Analyse automatique de la bibliothèque",
"HeaderSession": "Session", "HeaderSession": "Session",
"HeaderSetBackupSchedule": "Activer la Sauvegarde Automatique", "HeaderSetBackupSchedule": "Activer la sauvegarde automatique",
"HeaderSettings": "Paramètres", "HeaderSettings": "Paramètres",
"HeaderSettingsDisplay": "Affichage", "HeaderSettingsDisplay": "Affichage",
"HeaderSettingsExperimental": "Fonctionnalités expérimentales", "HeaderSettingsExperimental": "Fonctionnalités expérimentales",
@ -176,8 +187,11 @@
"LabelAddToCollectionBatch": "Ajout de {0} livres à la lollection", "LabelAddToCollectionBatch": "Ajout de {0} livres à la lollection",
"LabelAddToPlaylist": "Ajouter à la liste de lecture", "LabelAddToPlaylist": "Ajouter à la liste de lecture",
"LabelAddToPlaylistBatch": "{0} éléments ajoutés à la liste de lecture", "LabelAddToPlaylistBatch": "{0} éléments ajoutés à la liste de lecture",
"LabelAdminUsersOnly": "Administrateurs uniquement",
"LabelAll": "Tout", "LabelAll": "Tout",
"LabelAllUsers": "Tous les utilisateurs", "LabelAllUsers": "Tous les utilisateurs",
"LabelAllUsersExcludingGuests": "Tous les utilisateurs à lexception des invités",
"LabelAllUsersIncludingGuests": "Tous les utilisateurs, y compris les invités",
"LabelAlreadyInYourLibrary": "Déjà dans la bibliothèque", "LabelAlreadyInYourLibrary": "Déjà dans la bibliothèque",
"LabelAppend": "Ajouter", "LabelAppend": "Ajouter",
"LabelAuthor": "Auteur", "LabelAuthor": "Auteur",
@ -185,21 +199,29 @@
"LabelAuthorLastFirst": "Auteur (Nom, Prénom)", "LabelAuthorLastFirst": "Auteur (Nom, Prénom)",
"LabelAuthors": "Auteurs", "LabelAuthors": "Auteurs",
"LabelAutoDownloadEpisodes": "Téléchargement automatique dépisode", "LabelAutoDownloadEpisodes": "Téléchargement automatique dépisode",
"LabelBackToUser": "Revenir à lUtilisateur", "LabelAutoFetchMetadata": "Recherche automatique de métadonnées",
"LabelBackupLocation": "Backup Location", "LabelAutoFetchMetadataHelp": "Récupère les métadonnées du titre, de lauteur et de la série pour simplifier le téléchargement. Il se peut que des métadonnées supplémentaires doivent être ajoutées après le téléchargement.",
"LabelAutoLaunch": "Lancement automatique",
"LabelAutoLaunchDescription": "Redirection automatique vers le fournisseur d'authentification lors de la navigation vers la page de connexion (chemin de remplacement manuel <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Enregistrement automatique",
"LabelAutoRegisterDescription": "Créer automatiquement de nouveaux utilisateurs après la connexion",
"LabelBackToUser": "Retour à lutilisateur",
"LabelBackupLocation": "Emplacement de la sauvegarde",
"LabelBackupsEnableAutomaticBackups": "Activer les sauvegardes automatiques", "LabelBackupsEnableAutomaticBackups": "Activer les sauvegardes automatiques",
"LabelBackupsEnableAutomaticBackupsHelp": "Sauvegardes Enregistrées dans /metadata/backups", "LabelBackupsEnableAutomaticBackupsHelp": "Sauvegardes enregistrées dans /metadata/backups",
"LabelBackupsMaxBackupSize": "Taille maximale de la sauvegarde (en Go)", "LabelBackupsMaxBackupSize": "Taille maximale de la sauvegarde (en Go)",
"LabelBackupsMaxBackupSizeHelp": "Afin de prévenir les mauvaises configuration, la sauvegarde échouera si elle excède la taille limite.", "LabelBackupsMaxBackupSizeHelp": "Afin de prévenir les mauvaises configuration, la sauvegarde échouera si elle excède la taille limite.",
"LabelBackupsNumberToKeep": "Nombre de sauvegardes à maintenir", "LabelBackupsNumberToKeep": "Nombre de sauvegardes à conserver",
"LabelBackupsNumberToKeepHelp": "Une seule sauvegarde sera effacée à la fois. Si vous avez plus de sauvegardes à effacer, vous devrez le faire manuellement.", "LabelBackupsNumberToKeepHelp": "Seule une sauvegarde sera supprimée à la fois. Si vous avez déjà plus de sauvegardes à effacer, vous devez les supprimer manuellement.",
"LabelBitrate": "Bitrate", "LabelBitrate": "Bitrate",
"LabelBooks": "Livres", "LabelBooks": "Livres",
"LabelButtonText": "Texte du bouton",
"LabelChangePassword": "Modifier le mot de passe", "LabelChangePassword": "Modifier le mot de passe",
"LabelChannels": "Canaux", "LabelChannels": "Canaux",
"LabelChapters": "Chapitres", "LabelChapters": "Chapitres",
"LabelChaptersFound": "Chapitres trouvés", "LabelChaptersFound": "chapitres trouvés",
"LabelChapterTitle": "Titres du chapitre", "LabelChapterTitle": "Titre du chapitre",
"LabelClickForMoreInfo": "Cliquez ici pour plus dinformations",
"LabelClosePlayer": "Fermer le lecteur", "LabelClosePlayer": "Fermer le lecteur",
"LabelCodec": "Codec", "LabelCodec": "Codec",
"LabelCollapseSeries": "Réduire les séries", "LabelCollapseSeries": "Réduire les séries",
@ -213,18 +235,20 @@
"LabelCover": "Couverture", "LabelCover": "Couverture",
"LabelCoverImageURL": "URL vers limage de couverture", "LabelCoverImageURL": "URL vers limage de couverture",
"LabelCreatedAt": "Créé le", "LabelCreatedAt": "Créé le",
"LabelCronExpression": "Expression Cron", "LabelCronExpression": "Expression cron",
"LabelCurrent": "Courrant", "LabelCurrent": "Actuel",
"LabelCurrently": "En ce moment :", "LabelCurrently": "Actuellement :",
"LabelCustomCronExpression": "Expression cron personnalisée :", "LabelCustomCronExpression": "Expression cron personnalisée :",
"LabelDatetime": "Datetime", "LabelDatetime": "Date",
"LabelDeleteFromFileSystemCheckbox": "Supprimer du système de fichiers (décocher pour ne supprimer que de la base de données)",
"LabelDescription": "Description", "LabelDescription": "Description",
"LabelDeselectAll": "Tout déselectionner", "LabelDeselectAll": "Tout déselectionner",
"LabelDevice": "Appareil", "LabelDevice": "Appareil",
"LabelDeviceInfo": "Détail de lappareil", "LabelDeviceInfo": "Détail de lappareil",
"LabelDeviceIsAvailableTo": "Lappareil est disponible pour…",
"LabelDirectory": "Répertoire", "LabelDirectory": "Répertoire",
"LabelDiscFromFilename": "Disque depuis le fichier", "LabelDiscFromFilename": "Depuis le fichier",
"LabelDiscFromMetadata": "Disque depuis les métadonnées", "LabelDiscFromMetadata": "Depuis les métadonnées",
"LabelDiscover": "Découvrir", "LabelDiscover": "Découvrir",
"LabelDownload": "Téléchargement", "LabelDownload": "Téléchargement",
"LabelDownloadNEpisodes": "Télécharger {0} épisode(s)", "LabelDownloadNEpisodes": "Télécharger {0} épisode(s)",
@ -247,15 +271,17 @@
"LabelExample": "Exemple", "LabelExample": "Exemple",
"LabelExplicit": "Restriction", "LabelExplicit": "Restriction",
"LabelFeedURL": "URL du flux", "LabelFeedURL": "URL du flux",
"LabelFetchingMetadata": "Récupération des métadonnées",
"LabelFile": "Fichier", "LabelFile": "Fichier",
"LabelFileBirthtime": "Création du fichier", "LabelFileBirthtime": "Création du fichier",
"LabelFileModified": "Modification du fichier", "LabelFileModified": "Modification du fichier",
"LabelFilename": "Nom de fichier", "LabelFilename": "Nom de fichier",
"LabelFilterByUser": "Filtrer par lutilisateur", "LabelFilterByUser": "Filtrer par utilisateur",
"LabelFindEpisodes": "Trouver des épisodes", "LabelFindEpisodes": "Trouver des épisodes",
"LabelFinished": "Fini(e)", "LabelFinished": "Terminé le",
"LabelFolder": "Dossier", "LabelFolder": "Dossier",
"LabelFolders": "Dossiers", "LabelFolders": "Dossiers",
"LabelFontFamily": "Polices de caractères",
"LabelFontScale": "Taille de la police de caractère", "LabelFontScale": "Taille de la police de caractère",
"LabelFormat": "Format", "LabelFormat": "Format",
"LabelGenre": "Genre", "LabelGenre": "Genre",
@ -263,14 +289,16 @@
"LabelHardDeleteFile": "Suppression du fichier", "LabelHardDeleteFile": "Suppression du fichier",
"LabelHasEbook": "Dispose dun livre numérique", "LabelHasEbook": "Dispose dun livre numérique",
"LabelHasSupplementaryEbook": "Dispose dun livre numérique supplémentaire", "LabelHasSupplementaryEbook": "Dispose dun livre numérique supplémentaire",
"LabelHighestPriority": "Priorité la plus élevée",
"LabelHost": "Hôte", "LabelHost": "Hôte",
"LabelHour": "Heure", "LabelHour": "Heure",
"LabelIcon": "Icone", "LabelIcon": "Icône",
"LabelIncludeInTracklist": "Inclure dans la liste des pistes", "LabelImageURLFromTheWeb": "URL de limage à partir du web",
"LabelIncludeInTracklist": "Inclure dans la liste de lecture",
"LabelIncomplete": "Incomplet", "LabelIncomplete": "Incomplet",
"LabelInProgress": "En cours", "LabelInProgress": "En cours",
"LabelInterval": "Intervalle", "LabelInterval": "Intervalle",
"LabelIntervalCustomDailyWeekly": "Journalier / Hebdomadaire personnalisé", "LabelIntervalCustomDailyWeekly": "Personnaliser quotidiennement / hebdomadairement",
"LabelIntervalEvery12Hours": "Toutes les 12 heures", "LabelIntervalEvery12Hours": "Toutes les 12 heures",
"LabelIntervalEvery15Minutes": "Toutes les 15 minutes", "LabelIntervalEvery15Minutes": "Toutes les 15 minutes",
"LabelIntervalEvery2Hours": "Toutes les 2 heures", "LabelIntervalEvery2Hours": "Toutes les 2 heures",
@ -303,16 +331,22 @@
"LabelLogLevelInfo": "Info", "LabelLogLevelInfo": "Info",
"LabelLogLevelWarn": "Warn", "LabelLogLevelWarn": "Warn",
"LabelLookForNewEpisodesAfterDate": "Chercher de nouveaux épisode après cette date", "LabelLookForNewEpisodesAfterDate": "Chercher de nouveaux épisode après cette date",
"LabelLowestPriority": "Priorité la plus basse",
"LabelMatchExistingUsersBy": "Faire correspondre les utilisateurs existants par",
"LabelMatchExistingUsersByDescription": "Utilisé pour connecter les utilisateurs existants. Une fois connectés, les utilisateurs seront associés à un identifiant unique provenant de votre fournisseur SSO.",
"LabelMediaPlayer": "Lecteur multimédia", "LabelMediaPlayer": "Lecteur multimédia",
"LabelMediaType": "Type de média", "LabelMediaType": "Type de média",
"LabelMetadataOrderOfPrecedenceDescription": "Les sources de métadonnées ayant une priorité plus élevée auront la priorité sur celles ayant une priorité moins élevée.",
"LabelMetadataProvider": "Fournisseur de métadonnées", "LabelMetadataProvider": "Fournisseur de métadonnées",
"LabelMetaTag": "Etiquette de métadonnée", "LabelMetaTag": "Balise de métadonnée",
"LabelMetaTags": "Etiquettes de métadonnée", "LabelMetaTags": "Balises de métadonnée",
"LabelMinute": "Minute", "LabelMinute": "Minute",
"LabelMissing": "Manquant", "LabelMissing": "Manquant",
"LabelMissingParts": "Parties manquantes", "LabelMissingParts": "Parties manquantes",
"LabelMobileRedirectURIs": "URI de redirection mobile autorisés",
"LabelMobileRedirectURIsDescription": "Il s'agit d'une liste blanche dURI de redirection valides pour les applications mobiles. Celui par défaut est <code>audiobookshelf://oauth</code>, que vous pouvez supprimer ou compléter avec des URIs supplémentaires pour l'intégration d'applications tierces. Lutilisation dun astérisque (<code>*</code>) comme seule entrée autorise nimporte quel URI.",
"LabelMore": "Plus", "LabelMore": "Plus",
"LabelMoreInfo": "Plus dinfo", "LabelMoreInfo": "Plus dinformations",
"LabelName": "Nom", "LabelName": "Nom",
"LabelNarrator": "Narrateur", "LabelNarrator": "Narrateur",
"LabelNarrators": "Narrateurs", "LabelNarrators": "Narrateurs",
@ -324,7 +358,7 @@
"LabelNextScheduledRun": "Prochain lancement prévu", "LabelNextScheduledRun": "Prochain lancement prévu",
"LabelNoEpisodesSelected": "Aucun épisode sélectionné", "LabelNoEpisodesSelected": "Aucun épisode sélectionné",
"LabelNotes": "Notes", "LabelNotes": "Notes",
"LabelNotFinished": "Non terminé(e)", "LabelNotFinished": "Non terminé",
"LabelNotificationAppriseURL": "URL(s) dApprise", "LabelNotificationAppriseURL": "URL(s) dApprise",
"LabelNotificationAvailableVariables": "Variables disponibles", "LabelNotificationAvailableVariables": "Variables disponibles",
"LabelNotificationBodyTemplate": "Modèle de Message", "LabelNotificationBodyTemplate": "Modèle de Message",
@ -333,10 +367,10 @@
"LabelNotificationsMaxFailedAttemptsHelp": "La notification est abandonnée une fois ce seuil atteint", "LabelNotificationsMaxFailedAttemptsHelp": "La notification est abandonnée une fois ce seuil atteint",
"LabelNotificationsMaxQueueSize": "Nombres de notifications maximum à mettre en attente", "LabelNotificationsMaxQueueSize": "Nombres de notifications maximum à mettre en attente",
"LabelNotificationsMaxQueueSizeHelp": "La limite de notification est de un évènement par seconde. Les notifications seront ignorées si la file dattente est à son maximum. Cela empêche un flot trop important.", "LabelNotificationsMaxQueueSizeHelp": "La limite de notification est de un évènement par seconde. Les notifications seront ignorées si la file dattente est à son maximum. Cela empêche un flot trop important.",
"LabelNotificationTitleTemplate": "Modèle de Titre", "LabelNotificationTitleTemplate": "Modèle de titre",
"LabelNotStarted": "Non Démarré(e)", "LabelNotStarted": "Pas commencé",
"LabelNumberOfBooks": "Nombre de Livres", "LabelNumberOfBooks": "Nombre de livres",
"LabelNumberOfEpisodes": "Nombre dEpisodes", "LabelNumberOfEpisodes": "Nombre dépisodes",
"LabelOpenRSSFeed": "Ouvrir le flux RSS", "LabelOpenRSSFeed": "Ouvrir le flux RSS",
"LabelOverwrite": "Écraser", "LabelOverwrite": "Écraser",
"LabelPassword": "Mot de passe", "LabelPassword": "Mot de passe",
@ -372,11 +406,12 @@
"LabelRegion": "Région", "LabelRegion": "Région",
"LabelReleaseDate": "Date de parution", "LabelReleaseDate": "Date de parution",
"LabelRemoveCover": "Supprimer la couverture", "LabelRemoveCover": "Supprimer la couverture",
"LabelRowsPerPage": "Lignes par page",
"LabelRSSFeedCustomOwnerEmail": "Courriel du propriétaire personnalisé", "LabelRSSFeedCustomOwnerEmail": "Courriel du propriétaire personnalisé",
"LabelRSSFeedCustomOwnerName": "Nom propriétaire personnalisé", "LabelRSSFeedCustomOwnerName": "Nom propriétaire personnalisé",
"LabelRSSFeedOpen": "Flux RSS ouvert", "LabelRSSFeedOpen": "Flux RSS ouvert",
"LabelRSSFeedPreventIndexing": "Empêcher lindexation", "LabelRSSFeedPreventIndexing": "Empêcher lindexation",
"LabelRSSFeedSlug": "Identificateur dadresse du Flux RSS ", "LabelRSSFeedSlug": "Balise URL du flux RSS",
"LabelRSSFeedURL": "Adresse du flux RSS", "LabelRSSFeedURL": "Adresse du flux RSS",
"LabelSearchTerm": "Terme de recherche", "LabelSearchTerm": "Terme de recherche",
"LabelSearchTitle": "Titre de recherche", "LabelSearchTitle": "Titre de recherche",
@ -384,7 +419,8 @@
"LabelSeason": "Saison", "LabelSeason": "Saison",
"LabelSelectAllEpisodes": "Sélectionner tous les épisodes", "LabelSelectAllEpisodes": "Sélectionner tous les épisodes",
"LabelSelectEpisodesShowing": "Sélectionner {0} episode(s) en cours", "LabelSelectEpisodesShowing": "Sélectionner {0} episode(s) en cours",
"LabelSendEbookToDevice": "Envoyer le livre numérique à...", "LabelSelectUsers": "Sélectionner les utilisateurs",
"LabelSendEbookToDevice": "Envoyer le livre numérique à…",
"LabelSequence": "Séquence", "LabelSequence": "Séquence",
"LabelSeries": "Séries", "LabelSeries": "Séries",
"LabelSeriesName": "Nom de la série", "LabelSeriesName": "Nom de la série",
@ -392,69 +428,63 @@
"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",
"LabelSettingsAudiobooksOnlyHelp": "Lactivation de ce paramètre ignorera les fichiers “ ebook ”, à moins quils ne se trouvent dans un dossier de livres audio, auquel cas ils seront définis comme des livres numériques supplémentaires.", "LabelSettingsAudiobooksOnlyHelp": "L'activation de ce paramètre ignorera les fichiers de type « livre numériques », sauf s'ils se trouvent dans un dossier spécifique , auquel cas ils seront définis comme des livres numériques supplémentaires.",
"LabelSettingsBookshelfViewHelp": "Interface skeuomorphique avec une étagère en bois", "LabelSettingsBookshelfViewHelp": "Interface skeuomorphique avec une étagère en bois",
"LabelSettingsChromecastSupport": "Support du Chromecast", "LabelSettingsChromecastSupport": "Support du Chromecast",
"LabelSettingsDateFormat": "Format de date", "LabelSettingsDateFormat": "Format de date",
"LabelSettingsDisableWatcher": "Désactiver la surveillance", "LabelSettingsDisableWatcher": "Désactiver la surveillance",
"LabelSettingsDisableWatcherForLibrary": "Désactiver la surveillance des dossiers pour la bibliothèque", "LabelSettingsDisableWatcherForLibrary": "Désactiver la surveillance des dossiers pour la bibliothèque",
"LabelSettingsDisableWatcherHelp": "Désactive la mise à jour automatique lorsque des modifications de fichiers sont détectées. *Nécessite le redémarrage du serveur", "LabelSettingsDisableWatcherHelp": "Désactive la mise à jour automatique lorsque des modifications de fichiers sont détectées. * nécessite le redémarrage du serveur",
"LabelSettingsEnableWatcher": "Activer la veille", "LabelSettingsEnableWatcher": "Activer la veille",
"LabelSettingsEnableWatcherForLibrary": "Activer la surveillance des dossiers pour la bibliothèque", "LabelSettingsEnableWatcherForLibrary": "Activer la surveillance des dossiers pour la bibliothèque",
"LabelSettingsEnableWatcherHelp": "Active la mise à jour automatique automatique lorsque des modifications de fichiers sont détectées. *Nécessite le redémarrage du serveur", "LabelSettingsEnableWatcherHelp": "Active la mise à jour automatique automatique lorsque des modifications de fichiers sont détectées. * nécessite le redémarrage du serveur",
"LabelSettingsExperimentalFeatures": "Fonctionnalités expérimentales", "LabelSettingsExperimentalFeatures": "Fonctionnalités expérimentales",
"LabelSettingsExperimentalFeaturesHelp": "Fonctionnalités en cours de développement sur lesquelles nous attendons votre retour et expérience. Cliquez pour ouvrir la discussion Github.", "LabelSettingsExperimentalFeaturesHelp": "Fonctionnalités en cours de développement sur lesquelles nous attendons votre retour et expérience. Cliquez pour ouvrir la discussion GitHub.",
"LabelSettingsFindCovers": "Chercher des couvertures de livre", "LabelSettingsFindCovers": "Chercher des couvertures de livre",
"LabelSettingsFindCoversHelp": "Si votre livre audio ne possède pas de couverture intégrée ou une image de couverture dans le dossier, lanalyseur tentera de récupérer une couverture.<br>Attention, cela peut augmenter le temps danalyse.", "LabelSettingsFindCoversHelp": "Si votre livre audio ne possède pas de couverture intégrée ou une image de couverture dans le dossier, lanalyseur tentera de récupérer une couverture.<br>Attention, cela peut augmenter le temps danalyse.",
"LabelSettingsHideSingleBookSeries": "Masquer les séries de livres uniques", "LabelSettingsHideSingleBookSeries": "Masquer les séries de livres uniques",
"LabelSettingsHideSingleBookSeriesHelp": "Les séries qui ne comportent quun seul livre seront masquées sur la page de la série et sur les étagères de la page daccueil.", "LabelSettingsHideSingleBookSeriesHelp": "Les séries qui ne comportent quun seul livre seront masquées sur la page de la série et sur les étagères de la page daccueil.",
"LabelSettingsHomePageBookshelfView": "La page daccueil utilise la vue étagère", "LabelSettingsHomePageBookshelfView": "La page daccueil utilise la vue étagère",
"LabelSettingsLibraryBookshelfView": "La bibliothèque utilise la vue étagère", "LabelSettingsLibraryBookshelfView": "La bibliothèque utilise la vue étagère",
"LabelSettingsOverdriveMediaMarkers": "Utiliser Overdrive Media Marker pour les chapitres",
"LabelSettingsOverdriveMediaMarkersHelp": "Les fichiers MP3 dOverdrive viennent avec les minutages des chapitres intégrés en métadonnées. Activer ce paramètre utilisera ces minutages pour les chapitres automatiquement.",
"LabelSettingsParseSubtitles": "Analyser les sous-titres", "LabelSettingsParseSubtitles": "Analyser les sous-titres",
"LabelSettingsParseSubtitlesHelp": "Extrait les sous-titres depuis le dossier du Livre Audio.<br>Les sous-titres doivent être séparés par « - »<br>i.e. « Titre du Livre - Ceci est un sous-titre » aura le sous-titre « Ceci est un sous-titre »", "LabelSettingsParseSubtitlesHelp": "Extrait les sous-titres depuis le dossier du livre audio.<br>Les sous-titres doivent être séparés par « - »<br>cest-à-dire : « Titre du livre - Ceci est un sous-titre » aura le sous-titre « Ceci est un sous-titre »",
"LabelSettingsPreferAudioMetadata": "Préférer les métadonnées audio",
"LabelSettingsPreferAudioMetadataHelp": "Les méta étiquettes ID3 des fichiers audios seront utilisés à la place des noms de dossier pour les détails du livre audio",
"LabelSettingsPreferMatchedMetadata": "Préférer les métadonnées par correspondance", "LabelSettingsPreferMatchedMetadata": "Préférer les métadonnées par correspondance",
"LabelSettingsPreferMatchedMetadataHelp": "Les métadonnées par correspondance écrase les détails de larticle lors dune recherche par correspondance rapide. Par défaut, la recherche par correspondance rapide ne comblera que les éléments manquant.", "LabelSettingsPreferMatchedMetadataHelp": "Les métadonnées par correspondance écrase les détails de larticle lors dune recherche par correspondance rapide. Par défaut, la recherche par correspondance rapide ne comblera que les éléments manquant.",
"LabelSettingsPreferOPFMetadata": "Préférer les métadonnées OPF",
"LabelSettingsPreferOPFMetadataHelp": "Les fichiers de métadonnées OPF seront utilisés à la place des noms de dossier pour les détails du Livre Audio",
"LabelSettingsSkipMatchingBooksWithASIN": "Ignorer la recherche par correspondance sur les livres ayant déjà un ASIN", "LabelSettingsSkipMatchingBooksWithASIN": "Ignorer la recherche par correspondance sur les livres ayant déjà un ASIN",
"LabelSettingsSkipMatchingBooksWithISBN": "Ignorer la recherche par correspondance sur les livres ayant déjà un ISBN", "LabelSettingsSkipMatchingBooksWithISBN": "Ignorer la recherche par correspondance sur les livres ayant déjà un ISBN",
"LabelSettingsSortingIgnorePrefixes": "Ignorer les préfixes lors du tri", "LabelSettingsSortingIgnorePrefixes": "Ignorer les préfixes lors du tri",
"LabelSettingsSortingIgnorePrefixesHelp": "i.e. pour le préfixe « le », le livre avec pour titre « Le Titre du Livre » sera trié en tant que « Titre du Livre, Le »", "LabelSettingsSortingIgnorePrefixesHelp": "cest-à-dire : pour le préfixe « le », le livre avec pour titre « Le Titre du Livre » sera trié en tant que « Titre du Livre, Le »",
"LabelSettingsSquareBookCovers": "Utiliser des couvertures carrées", "LabelSettingsSquareBookCovers": "Utiliser des couvertures carrées",
"LabelSettingsSquareBookCoversHelp": "Préférer les couvertures carrées par rapport aux couvertures standards de ratio 1.6:1.", "LabelSettingsSquareBookCoversHelp": "Préférer les couvertures carrées par rapport aux couvertures standards de ratio 1.6:1.",
"LabelSettingsStoreCoversWithItem": "Enregistrer la couverture avec les articles", "LabelSettingsStoreCoversWithItem": "Enregistrer la couverture avec les articles",
"LabelSettingsStoreCoversWithItemHelp": "Par défaut, les couvertures sont enregistrées dans /metadata/items. Activer ce paramètre enregistrera les couvertures dans le dossier avec les fichiers de larticle. Seul un fichier nommé « cover » sera conservé.", "LabelSettingsStoreCoversWithItemHelp": "Par défaut, les couvertures sont enregistrées dans /metadata/items. Activer ce paramètre enregistrera les couvertures dans le dossier avec les fichiers de larticle. Seul un fichier nommé « cover » sera conservé.",
"LabelSettingsStoreMetadataWithItem": "Enregistrer les Métadonnées avec les articles", "LabelSettingsStoreMetadataWithItem": "Enregistrer les Métadonnées avec les articles",
"LabelSettingsStoreMetadataWithItemHelp": "Par défaut, les métadonnées sont enregistrées dans /metadata/items. Activer ce paramètre enregistrera les métadonnées dans le dossier de larticle avec une extension « .abs ».", "LabelSettingsStoreMetadataWithItemHelp": "Par défaut, les métadonnées sont enregistrées dans /metadata/items",
"LabelSettingsTimeFormat": "Format dheure", "LabelSettingsTimeFormat": "Format dheure",
"LabelShowAll": "Afficher Tout", "LabelShowAll": "Tout afficher",
"LabelSize": "Taille", "LabelSize": "Taille",
"LabelSleepTimer": "Minuterie", "LabelSleepTimer": "Minuterie",
"LabelSlug": "Slug", "LabelSlug": "Balise",
"LabelStart": "Démarrer", "LabelStart": "Démarrer",
"LabelStarted": "Démarré", "LabelStarted": "Démarré",
"LabelStartedAt": "Démarré à", "LabelStartedAt": "Démarré à",
"LabelStartTime": "Heure de Démarrage", "LabelStartTime": "Heure de démarrage",
"LabelStatsAudioTracks": "Pistes Audios", "LabelStatsAudioTracks": "Pistes Audios",
"LabelStatsAuthors": "Auteurs", "LabelStatsAuthors": "Auteurs",
"LabelStatsBestDay": "Meilleur Jour", "LabelStatsBestDay": "Meilleur jour",
"LabelStatsDailyAverage": "Moyenne Journalière", "LabelStatsDailyAverage": "Moyenne journalière",
"LabelStatsDays": "Jours", "LabelStatsDays": "Jours",
"LabelStatsDaysListened": "Jours découte", "LabelStatsDaysListened": "Jours découte",
"LabelStatsHours": "Heures", "LabelStatsHours": "Heures",
"LabelStatsInARow": "daffilé(s)", "LabelStatsInARow": "daffilée(s)",
"LabelStatsItemsFinished": "Articles terminés", "LabelStatsItemsFinished": "Articles terminés",
"LabelStatsItemsInLibrary": "Articles dans la Bibliothèque", "LabelStatsItemsInLibrary": "Articles dans la bibliothèque",
"LabelStatsMinutes": "minutes", "LabelStatsMinutes": "minutes",
"LabelStatsMinutesListening": "Minutes découte", "LabelStatsMinutesListening": "Minutes découte",
"LabelStatsOverallDays": "Jours au total", "LabelStatsOverallDays": "Nombre total de jours",
"LabelStatsOverallHours": "Heures au total", "LabelStatsOverallHours": "Nombre total d'heures",
"LabelStatsWeekListening": "Écoute de la semaine", "LabelStatsWeekListening": "Écoute de la semaine",
"LabelSubtitle": "Sous-Titre", "LabelSubtitle": "Sous-titre",
"LabelSupportedFileTypes": "Types de fichiers supportés", "LabelSupportedFileTypes": "Types de fichiers supportés",
"LabelTag": "Étiquette", "LabelTag": "Étiquette",
"LabelTags": "Étiquettes", "LabelTags": "Étiquettes",
@ -466,23 +496,23 @@
"LabelThemeLight": "Clair", "LabelThemeLight": "Clair",
"LabelTimeBase": "Base de temps", "LabelTimeBase": "Base de temps",
"LabelTimeListened": "Temps découte", "LabelTimeListened": "Temps découte",
"LabelTimeListenedToday": "Nombres découtes Aujourdhui", "LabelTimeListenedToday": "Nombres découtes aujourdhui",
"LabelTimeRemaining": "{0} restantes", "LabelTimeRemaining": "{0} restantes",
"LabelTimeToShift": "Temps de décalage en secondes", "LabelTimeToShift": "Temps de décalage en secondes",
"LabelTitle": "Titre", "LabelTitle": "Titre",
"LabelToolsEmbedMetadata": "Métadonnées Intégrées", "LabelToolsEmbedMetadata": "Métadonnées intégrées",
"LabelToolsEmbedMetadataDescription": "Intègre les métadonnées au fichier audio avec la couverture et les chapitres.", "LabelToolsEmbedMetadataDescription": "Intègre les métadonnées au fichier audio avec la couverture et les chapitres.",
"LabelToolsMakeM4b": "Créer un fichier Livre Audio M4B", "LabelToolsMakeM4b": "Créer un fichier livre audio M4B",
"LabelToolsMakeM4bDescription": "Génère un fichier Livre Audio .M4B avec intégration des métadonnées, image de couverture et les chapitres.", "LabelToolsMakeM4bDescription": "Générer un fichier de livre audio .M4B avec des métadonnées intégrées, une image de couverture et des chapitres.",
"LabelToolsSplitM4b": "Scinde le fichier M4B en fichiers MP3", "LabelToolsSplitM4b": "Scinde le fichier M4B en fichiers MP3",
"LabelToolsSplitM4bDescription": "Créer plusieurs fichier MP3 à partir du découpage par chapitre, en incluant les métadonnées, limage de couverture et les chapitres.", "LabelToolsSplitM4bDescription": "Créer plusieurs fichier MP3 à partir du découpage par chapitre, en incluant les métadonnées, limage de couverture et les chapitres.",
"LabelTotalDuration": "Durée Totale", "LabelTotalDuration": "Durée totale",
"LabelTotalTimeListened": "Temps découte total", "LabelTotalTimeListened": "Temps découte total",
"LabelTrackFromFilename": "Piste depuis le fichier", "LabelTrackFromFilename": "Piste depuis le fichier",
"LabelTrackFromMetadata": "Piste depuis les métadonnées", "LabelTrackFromMetadata": "Piste depuis les métadonnées",
"LabelTracks": "Pistes", "LabelTracks": "Pistes",
"LabelTracksMultiTrack": "Piste multiple", "LabelTracksMultiTrack": "Piste multiple",
"LabelTracksNone": "No tracks", "LabelTracksNone": "Aucune piste",
"LabelTracksSingleTrack": "Piste simple", "LabelTracksSingleTrack": "Piste simple",
"LabelType": "Type", "LabelType": "Type",
"LabelUnabridged": "Version intégrale", "LabelUnabridged": "Version intégrale",
@ -494,8 +524,9 @@
"LabelUpdateDetailsHelp": "Autoriser la mise à jour des détails existants lorsquune correspondance est trouvée", "LabelUpdateDetailsHelp": "Autoriser la mise à jour des détails existants lorsquune correspondance est trouvée",
"LabelUploaderDragAndDrop": "Glisser et déposer des fichiers ou dossiers", "LabelUploaderDragAndDrop": "Glisser et déposer des fichiers ou dossiers",
"LabelUploaderDropFiles": "Déposer des fichiers", "LabelUploaderDropFiles": "Déposer des fichiers",
"LabelUploaderItemFetchMetadataHelp": "Récupérer automatiquement le titre, lauteur et la série",
"LabelUseChapterTrack": "Utiliser la piste du chapitre", "LabelUseChapterTrack": "Utiliser la piste du chapitre",
"LabelUseFullTrack": "Utiliser la piste Complète", "LabelUseFullTrack": "Utiliser la piste complète",
"LabelUser": "Utilisateur", "LabelUser": "Utilisateur",
"LabelUsername": "Nom dutilisateur", "LabelUsername": "Nom dutilisateur",
"LabelValue": "Valeur", "LabelValue": "Valeur",
@ -510,14 +541,14 @@
"LabelYourPlaylists": "Vos listes de lecture", "LabelYourPlaylists": "Vos listes de lecture",
"LabelYourProgress": "Votre progression", "LabelYourProgress": "Votre progression",
"MessageAddToPlayerQueue": "Ajouter en file dattente", "MessageAddToPlayerQueue": "Ajouter en file dattente",
"MessageAppriseDescription": "Nécessite une instance d<a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">API Apprise</a> pour utiliser cette fonctionnalité ou une api qui prend en charge les mêmes requêtes. <br />lURL de lAPI Apprise doit comprendre le chemin complet pour envoyer la notification. Par exemple, si votre instance écoute sur <code>http://192.168.1.1:8337</code> alors vous devez mettre <code>http://192.168.1.1:8337/notify</code>.", "MessageAppriseDescription": "Nécessite une instance d<a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">API Apprise</a> pour utiliser cette fonctionnalité ou une api qui prend en charge les mêmes requêtes.<br>LURL de lAPI Apprise doit comprendre le chemin complet pour envoyer la notification. Par exemple, si votre instance écoute sur <code>http://192.168.1.1:8337</code> alors vous devez mettre <code>http://192.168.1.1:8337/notify</code>.",
"MessageBackupsDescription": "Les sauvegardes incluent les utilisateurs, la progression de lecture par utilisateur, les détails des articles des bibliothèques, les paramètres du serveur et les images sauvegardées. Les sauvegardes nincluent pas les fichiers de votre bibliothèque.", "MessageBackupsDescription": "Les sauvegardes incluent les utilisateurs, la progression de lecture par utilisateur, les détails des articles des bibliothèques, les paramètres du serveur et les images sauvegardées. Les sauvegardes nincluent pas les fichiers de votre bibliothèque.",
"MessageBatchQuickMatchDescription": "La recherche par correspondance rapide tentera dajouter les couvertures et les métadonnées manquantes pour les articles sélectionnés. Activer loption suivante pour autoriser la recherche par correspondance à écraser les données existantes.", "MessageBatchQuickMatchDescription": "La recherche par correspondance rapide tentera dajouter les couvertures et les métadonnées manquantes pour les articles sélectionnés. Activer loption suivante pour autoriser la recherche par correspondance à écraser les données existantes.",
"MessageBookshelfNoCollections": "Vous navez pas encore de collections", "MessageBookshelfNoCollections": "Vous navez pas encore de collections",
"MessageBookshelfNoResultsForFilter": "Aucun résultat pour le filtre « {0} : {1} »", "MessageBookshelfNoResultsForFilter": "Aucun résultat pour le filtre « {0} : {1} »",
"MessageBookshelfNoRSSFeeds": "Aucun flux RSS nest ouvert", "MessageBookshelfNoRSSFeeds": "Aucun flux RSS nest ouvert",
"MessageBookshelfNoSeries": "Vous navez aucune série", "MessageBookshelfNoSeries": "Vous navez aucune série",
"MessageChapterEndIsAfter": "Le Chapitre Fin est situé à la fin de votre Livre Audio", "MessageChapterEndIsAfter": "La fin du chapitre se situe après la fin de votre livre audio.",
"MessageChapterErrorFirstNotZero": "Le premier capitre doit débuter à 0", "MessageChapterErrorFirstNotZero": "Le premier capitre doit débuter à 0",
"MessageChapterErrorStartGteDuration": "Horodatage invalide car il doit débuter avant la fin du livre", "MessageChapterErrorStartGteDuration": "Horodatage invalide car il doit débuter avant la fin du livre",
"MessageChapterErrorStartLtPrev": "Horodatage invalide car il doit débuter au moins après le précédent chapitre", "MessageChapterErrorStartLtPrev": "Horodatage invalide car il doit débuter au moins après le précédent chapitre",
@ -527,17 +558,21 @@
"MessageConfirmDeleteBackup": "Êtes-vous sûr de vouloir supprimer la sauvegarde de « {0} » ?", "MessageConfirmDeleteBackup": "Êtes-vous sûr de vouloir supprimer la sauvegarde de « {0} » ?",
"MessageConfirmDeleteFile": "Cela supprimera le fichier de votre système de fichiers. Êtes-vous sûr ?", "MessageConfirmDeleteFile": "Cela supprimera le fichier de votre système de fichiers. Êtes-vous sûr ?",
"MessageConfirmDeleteLibrary": "Êtes-vous sûr de vouloir supprimer définitivement la bibliothèque « {0} » ?", "MessageConfirmDeleteLibrary": "Êtes-vous sûr de vouloir supprimer définitivement la bibliothèque « {0} » ?",
"MessageConfirmDeleteLibraryItem": "Cette opération supprimera lélément de la base de données et de votre système de fichiers. Êtes-vous sûr ?",
"MessageConfirmDeleteLibraryItems": "Cette opération supprimera {0} éléments de la base de données et de votre système de fichiers. Êtes-vous sûr ?",
"MessageConfirmDeleteSession": "Êtes-vous sûr de vouloir supprimer cette session ?", "MessageConfirmDeleteSession": "Êtes-vous sûr de vouloir supprimer cette session ?",
"MessageConfirmForceReScan": "Êtes-vous sûr de vouloir lancer une analyse forcée ?", "MessageConfirmForceReScan": "Êtes-vous sûr de vouloir lancer une analyse forcée ?",
"MessageConfirmMarkAllEpisodesFinished": "Êtes-vous sûr de marquer tous les épisodes comme terminés ?", "MessageConfirmMarkAllEpisodesFinished": "Êtes-vous sûr de marquer tous les épisodes comme terminés ?",
"MessageConfirmMarkAllEpisodesNotFinished": "Êtes-vous sûr de vouloir marquer tous les épisodes comme non terminés ?", "MessageConfirmMarkAllEpisodesNotFinished": "Êtes-vous sûr de vouloir marquer tous les épisodes comme non terminés ?",
"MessageConfirmMarkSeriesFinished": "Êtes-vous sûr de vouloir marquer tous les livres de cette série comme terminées ?", "MessageConfirmMarkSeriesFinished": "Êtes-vous sûr de vouloir marquer tous les livres de cette série comme terminées ?",
"MessageConfirmMarkSeriesNotFinished": "Êtes-vous sûr de vouloir marquer tous les livres de cette série comme comme non terminés ?", "MessageConfirmMarkSeriesNotFinished": "Êtes-vous sûr de vouloir marquer tous les livres de cette série comme comme non terminés ?",
"MessageConfirmQuickEmbed": "Attention ! Lintégration rapide ne sauvegardera pas vos fichiers audio. Assurez-vous davoir effectuer une sauvegarde de vos fichiers audio.<br><br>Souhaitez-vous continuer ?",
"MessageConfirmRemoveAllChapters": "Êtes-vous sûr de vouloir supprimer tous les chapitres ?", "MessageConfirmRemoveAllChapters": "Êtes-vous sûr de vouloir supprimer tous les chapitres ?",
"MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?", "MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?",
"MessageConfirmRemoveCollection": "Êtes-vous sûr de vouloir supprimer la collection « {0} » ?", "MessageConfirmRemoveCollection": "Êtes-vous sûr de vouloir supprimer la collection « {0} » ?",
"MessageConfirmRemoveEpisode": "Êtes-vous sûr de vouloir supprimer lépisode « {0} » ?", "MessageConfirmRemoveEpisode": "Êtes-vous sûr de vouloir supprimer lépisode « {0} » ?",
"MessageConfirmRemoveEpisodes": "Êtes-vous sûr de vouloir supprimer {0} épisodes ?", "MessageConfirmRemoveEpisodes": "Êtes-vous sûr de vouloir supprimer {0} épisodes ?",
"MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "Êtes-vous sûr de vouloir supprimer le narrateur « {0} » ?", "MessageConfirmRemoveNarrator": "Êtes-vous sûr de vouloir supprimer le narrateur « {0} » ?",
"MessageConfirmRemovePlaylist": "Êtes-vous sûr de vouloir supprimer la liste de lecture « {0} » ?", "MessageConfirmRemovePlaylist": "Êtes-vous sûr de vouloir supprimer la liste de lecture « {0} » ?",
"MessageConfirmRenameGenre": "Êtes-vous sûr de vouloir renommer le genre « {0} » en « {1} » pour tous les articles ?", "MessageConfirmRenameGenre": "Êtes-vous sûr de vouloir renommer le genre « {0} » en « {1} » pour tous les articles ?",
@ -546,15 +581,16 @@
"MessageConfirmRenameTag": "Êtes-vous sûr de vouloir renommer létiquette « {0} » en « {1} » pour tous les articles ?", "MessageConfirmRenameTag": "Êtes-vous sûr de vouloir renommer létiquette « {0} » en « {1} » pour tous les articles ?",
"MessageConfirmRenameTagMergeNote": "Information: Cette étiquette existe déjà et sera fusionnée.", "MessageConfirmRenameTagMergeNote": "Information: Cette étiquette existe déjà et sera fusionnée.",
"MessageConfirmRenameTagWarning": "Attention ! Une étiquette similaire avec une casse différente existe déjà « {0} ».", "MessageConfirmRenameTagWarning": "Attention ! Une étiquette similaire avec une casse différente existe déjà « {0} ».",
"MessageConfirmReScanLibraryItems": "Êtes-vous sûr de vouloir re-analyser {0} éléments ?",
"MessageConfirmSendEbookToDevice": "Êtes-vous sûr de vouloir envoyer le livre numérique {0} « {1} » à lappareil « {2} »?", "MessageConfirmSendEbookToDevice": "Êtes-vous sûr de vouloir envoyer le livre numérique {0} « {1} » à lappareil « {2} »?",
"MessageDownloadingEpisode": "Téléchargement de lépisode", "MessageDownloadingEpisode": "Téléchargement de lépisode",
"MessageDragFilesIntoTrackOrder": "Faire glisser les fichiers dans lordre correct", "MessageDragFilesIntoTrackOrder": "Faites glisser les fichiers dans lordre correct des pistes",
"MessageEmbedFinished": "Intégration terminée !", "MessageEmbedFinished": "Intégration terminée !",
"MessageEpisodesQueuedForDownload": "{0} épisode(s) mis en file pour téléchargement", "MessageEpisodesQueuedForDownload": "{0} épisode(s) mis en file pour téléchargement",
"MessageFeedURLWillBe": "lURL du flux sera {0}", "MessageFeedURLWillBe": "LURL du flux sera {0}",
"MessageFetching": "Récupération…", "MessageFetching": "Récupération…",
"MessageForceReScanDescription": "Analysera tous les fichiers de nouveau. Les étiquettes ID3 des fichiers audios, fichiers OPF, et les fichiers textes seront analysés comme sils étaient nouveaux.", "MessageForceReScanDescription": "analysera de nouveau tous les fichiers. Les étiquettes ID3 des fichiers audio, les fichiers OPF et les fichiers texte seront analysés comme sils étaient nouveaux.",
"MessageImportantNotice": "Information Importante !", "MessageImportantNotice": "Information importante !",
"MessageInsertChapterBelow": "Insérer le chapitre ci-dessous", "MessageInsertChapterBelow": "Insérer le chapitre ci-dessous",
"MessageItemsSelected": "{0} articles sélectionnés", "MessageItemsSelected": "{0} articles sélectionnés",
"MessageItemsUpdated": "{0} articles mis à jour", "MessageItemsUpdated": "{0} articles mis à jour",
@ -610,12 +646,13 @@
"MessageRemoveChapter": "Supprimer le chapitre", "MessageRemoveChapter": "Supprimer le chapitre",
"MessageRemoveEpisodes": "Suppression de {0} épisode(s)", "MessageRemoveEpisodes": "Suppression de {0} épisode(s)",
"MessageRemoveFromPlayerQueue": "Supprimer de la liste découte", "MessageRemoveFromPlayerQueue": "Supprimer de la liste découte",
"MessageRemoveUserWarning": "Êtes-vous certain de vouloir supprimer définitivement lutilisateur « {0} » ?", "MessageRemoveUserWarning": "Êtes-vous sûr de vouloir supprimer définitivement lutilisateur « {0} » ?",
"MessageReportBugsAndContribute": "Remonter des anomalies, demander des fonctionnalités et contribuer sur", "MessageReportBugsAndContribute": "Remonter des anomalies, demander des fonctionnalités et contribuer sur",
"MessageResetChaptersConfirm": "Êtes-vous certain de vouloir réinitialiser les chapitres et annuler les changements effectués ?", "MessageResetChaptersConfirm": "Êtes-vous sûr de vouloir réinitialiser les chapitres et annuler les changements effectués ?",
"MessageRestoreBackupConfirm": "Êtes-vous certain de vouloir restaurer la sauvegarde créée le", "MessageRestoreBackupConfirm": "Êtes-vous sûr de vouloir restaurer la sauvegarde créée le",
"MessageRestoreBackupWarning": "Restaurer la sauvegarde écrasera la base de donnée située dans le dossier /config ainsi que les images sur /metadata/items et /metadata/authors.<br /><br />Les sauvegardes ne touchent pas aux fichiers de la bibliothèque. Si vous avez activé le paramètre pour sauvegarder les métadonnées et les images de couverture dans le même dossier que les fichiers, ceux-ci ne ni sauvegardés, ni écrasés lors de la restauration.<br /><br />Tous les clients utilisant votre serveur seront automatiquement mis à jour.", "MessageRestoreBackupWarning": "Restaurer la sauvegarde écrasera la base de donnée située dans le dossier /config ainsi que les images sur /metadata/items et /metadata/authors.<br><br>Les sauvegardes ne touchent pas aux fichiers de la bibliothèque. Si vous avez activé le paramètre pour sauvegarder les métadonnées et les images de couverture dans le même dossier que les fichiers, ceux-ci ne ni sauvegardés, ni écrasés lors de la restauration.<br><br>Tous les clients utilisant votre serveur seront automatiquement mis à jour.",
"MessageSearchResultsFor": "Résultats de recherche pour", "MessageSearchResultsFor": "Résultats de recherche pour",
"MessageSelected": "{0} sélectionnés",
"MessageServerCouldNotBeReached": "Serveur inaccessible", "MessageServerCouldNotBeReached": "Serveur inaccessible",
"MessageSetChaptersFromTracksDescription": "Positionne un chapitre par fichier audio, avec le titre du fichier comme titre de chapitre", "MessageSetChaptersFromTracksDescription": "Positionne un chapitre par fichier audio, avec le titre du fichier comme titre de chapitre",
"MessageStartPlaybackAtTime": "Démarrer la lecture pour « {0} » à {1} ?", "MessageStartPlaybackAtTime": "Démarrer la lecture pour « {0} » à {1} ?",
@ -626,10 +663,10 @@
"MessageValidCronExpression": "Expression cron valide", "MessageValidCronExpression": "Expression cron valide",
"MessageWatcherIsDisabledGlobally": "La surveillance est désactivée par un paramètre global du serveur", "MessageWatcherIsDisabledGlobally": "La surveillance est désactivée par un paramètre global du serveur",
"MessageXLibraryIsEmpty": "La bibliothèque {0} est vide !", "MessageXLibraryIsEmpty": "La bibliothèque {0} est vide !",
"MessageYourAudiobookDurationIsLonger": "La durée de votre Livre Audio est plus longue que la durée trouvée", "MessageYourAudiobookDurationIsLonger": "La durée de votre livre audio est plus longue que la durée trouvée",
"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",
"NoteFolderPickerDebian": "Information : La sélection de dossier sur une installation debian nest pas finalisée. Merci de renseigner le chemin complet vers votre bibliothèque manuellement.", "NoteFolderPickerDebian": "Information : La sélection de dossier sur une installation debian nest pas finalisée. Merci de renseigner le chemin complet vers votre bibliothèque manuellement.",
"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.",
@ -640,8 +677,8 @@
"PlaceholderNewCollection": "Nom de la nouvelle collection", "PlaceholderNewCollection": "Nom de la nouvelle collection",
"PlaceholderNewFolderPath": "Nouveau chemin de dossier", "PlaceholderNewFolderPath": "Nouveau chemin de dossier",
"PlaceholderNewPlaylist": "Nouveau nom de liste de lecture", "PlaceholderNewPlaylist": "Nouveau nom de liste de lecture",
"PlaceholderSearch": "Recherche...", "PlaceholderSearch": "Recherche",
"PlaceholderSearchEpisode": "Recherche dépisode...", "PlaceholderSearchEpisode": "Recherche dépisode",
"ToastAccountUpdateFailed": "Échec de la mise à jour du compte", "ToastAccountUpdateFailed": "Échec de la mise à jour du compte",
"ToastAccountUpdateSuccess": "Compte mis à jour", "ToastAccountUpdateSuccess": "Compte mis à jour",
"ToastAuthorImageRemoveFailed": "Échec de la suppression de limage", "ToastAuthorImageRemoveFailed": "Échec de la suppression de limage",

View File

@ -1,7 +1,10 @@
{ {
"ButtonAdd": "ઉમેરો", "ButtonAdd": "ઉમેરો",
"ButtonAddChapters": "પ્રકરણો ઉમેરો", "ButtonAddChapters": "પ્રકરણો ઉમેરો",
"ButtonAddDevice": "ઉપકરણ ઉમેરો",
"ButtonAddLibrary": "પુસ્તકાલય ઉમેરો",
"ButtonAddPodcasts": "પોડકાસ્ટ ઉમેરો", "ButtonAddPodcasts": "પોડકાસ્ટ ઉમેરો",
"ButtonAddUser": "વપરાશકર્તા ઉમેરો",
"ButtonAddYourFirstLibrary": "તમારી પ્રથમ પુસ્તકાલય ઉમેરો", "ButtonAddYourFirstLibrary": "તમારી પ્રથમ પુસ્તકાલય ઉમેરો",
"ButtonApply": "લાગુ કરો", "ButtonApply": "લાગુ કરો",
"ButtonApplyChapters": "પ્રકરણો લાગુ કરો", "ButtonApplyChapters": "પ્રકરણો લાગુ કરો",
@ -55,10 +58,11 @@
"ButtonRemoveAll": "બધું કાઢી નાખો", "ButtonRemoveAll": "બધું કાઢી નાખો",
"ButtonRemoveAllLibraryItems": "બધું પુસ્તકાલય વસ્તુઓ કાઢી નાખો", "ButtonRemoveAllLibraryItems": "બધું પુસ્તકાલય વસ્તુઓ કાઢી નાખો",
"ButtonRemoveFromContinueListening": "સાંભળતી પુસ્તકો માંથી કાઢી નાખો", "ButtonRemoveFromContinueListening": "સાંભળતી પુસ્તકો માંથી કાઢી નાખો",
"ButtonRemoveFromContinueReading": "Remove from Continue Reading", "ButtonRemoveFromContinueReading": "સાંભળતી પુસ્તકો માંથી કાઢી નાખો",
"ButtonRemoveSeriesFromContinueSeries": "સાંભળતી સિરીઝ માંથી કાઢી નાખો", "ButtonRemoveSeriesFromContinueSeries": "સાંભળતી સિરીઝ માંથી કાઢી નાખો",
"ButtonReScan": "ફરીથી સ્કેન કરો", "ButtonReScan": "ફરીથી સ્કેન કરો",
"ButtonReset": "રીસેટ કરો", "ButtonReset": "રીસેટ કરો",
"ButtonResetToDefault": "ડિફોલ્ટ પર રીસેટ કરો",
"ButtonRestore": "પુનઃસ્થાપિત કરો", "ButtonRestore": "પુનઃસ્થાપિત કરો",
"ButtonSave": "સાચવો", "ButtonSave": "સાચવો",
"ButtonSaveAndClose": "સાચવો અને બંધ કરો", "ButtonSaveAndClose": "સાચવો અને બંધ કરો",
@ -74,7 +78,7 @@
"ButtonStartM4BEncode": "M4B એન્કોડ શરૂ કરો", "ButtonStartM4BEncode": "M4B એન્કોડ શરૂ કરો",
"ButtonStartMetadataEmbed": "મેટાડેટા એમ્બેડ શરૂ કરો", "ButtonStartMetadataEmbed": "મેટાડેટા એમ્બેડ શરૂ કરો",
"ButtonSubmit": "સબમિટ કરો", "ButtonSubmit": "સબમિટ કરો",
"ButtonTest": "Test", "ButtonTest": "પરખ કરો",
"ButtonUpload": "અપલોડ કરો", "ButtonUpload": "અપલોડ કરો",
"ButtonUploadBackup": "બેકઅપ અપલોડ કરો", "ButtonUploadBackup": "બેકઅપ અપલોડ કરો",
"ButtonUploadCover": "કવર અપલોડ કરો", "ButtonUploadCover": "કવર અપલોડ કરો",
@ -83,77 +87,84 @@
"ButtonUserEdit": "વપરાશકર્તા {0} સંપાદિત કરો", "ButtonUserEdit": "વપરાશકર્તા {0} સંપાદિત કરો",
"ButtonViewAll": "બધું જુઓ", "ButtonViewAll": "બધું જુઓ",
"ButtonYes": "હા", "ButtonYes": "હા",
"ErrorUploadFetchMetadataAPI": "Error fetching metadata",
"ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
"ErrorUploadLacksTitle": "Must have a title",
"HeaderAccount": "એકાઉન્ટ", "HeaderAccount": "એકાઉન્ટ",
"HeaderAdvanced": "અડ્વાન્સડ", "HeaderAdvanced": "અડ્વાન્સડ",
"HeaderAppriseNotificationSettings": "Apprise સૂચના સેટિંગ્સ", "HeaderAppriseNotificationSettings": "Apprise સૂચના સેટિંગ્સ",
"HeaderAudiobookTools": "Audiobook File Management Tools", "HeaderAudiobookTools": "ઓડિયોબુક ફાઇલ વ્યવસ્થાપન ટૂલ્સ",
"HeaderAudioTracks": "Audio Tracks", "HeaderAudioTracks": "ઓડિયો ટ્રેક્સ",
"HeaderBackups": "Backups", "HeaderAuthentication": "Authentication",
"HeaderChangePassword": "Change Password", "HeaderBackups": "બેકઅપ્સ",
"HeaderChapters": "Chapters", "HeaderChangePassword": "પાસવર્ડ બદલો",
"HeaderChooseAFolder": "Choose a Folder", "HeaderChapters": "પ્રકરણો",
"HeaderCollection": "Collection", "HeaderChooseAFolder": "ફોલ્ડર પસંદ કરો",
"HeaderCollectionItems": "Collection Items", "HeaderCollection": "સંગ્રહ",
"HeaderCover": "Cover", "HeaderCollectionItems": "સંગ્રહ વસ્તુઓ",
"HeaderCurrentDownloads": "Current Downloads", "HeaderCover": "આવરણ",
"HeaderDetails": "Details", "HeaderCurrentDownloads": "વર્તમાન ડાઉનલોડ્સ",
"HeaderDownloadQueue": "Download Queue", "HeaderDetails": "વિગતો",
"HeaderEbookFiles": "Ebook Files", "HeaderDownloadQueue": "ડાઉનલોડ કતાર",
"HeaderEmail": "Email", "HeaderEbookFiles": "ઇબુક ફાઇલો",
"HeaderEmailSettings": "Email Settings", "HeaderEmail": "ઈમેલ",
"HeaderEpisodes": "Episodes", "HeaderEmailSettings": "ઈમેલ સેટિંગ્સ",
"HeaderEreaderDevices": "Ereader Devices", "HeaderEpisodes": "એપિસોડ્સ",
"HeaderEreaderSettings": "Ereader Settings", "HeaderEreaderDevices": "ઇરીડર ઉપકરણો",
"HeaderFiles": "Files", "HeaderEreaderSettings": "ઇરીડર સેટિંગ્સ",
"HeaderFindChapters": "Find Chapters", "HeaderFiles": "ફાઇલો",
"HeaderIgnoredFiles": "Ignored Files", "HeaderFindChapters": "પ્રકરણો શોધો",
"HeaderItemFiles": "Item Files", "HeaderIgnoredFiles": "અવગણેલી ફાઇલો",
"HeaderItemMetadataUtils": "Item Metadata Utils", "HeaderItemFiles": "વાસ્તુ ની ફાઈલો",
"HeaderLastListeningSession": "Last Listening Session", "HeaderItemMetadataUtils": "વસ્તુ મેટાડેટા સાધનો",
"HeaderLatestEpisodes": "Latest episodes", "HeaderLastListeningSession": "છેલ્લી સાંભળતી સેશન",
"HeaderLibraries": "Libraries", "HeaderLatestEpisodes": "નવીનતમ એપિસોડ્સ",
"HeaderLibraryFiles": "Library Files", "HeaderLibraries": "પુસ્તકાલયો",
"HeaderLibraryStats": "Library Stats", "HeaderLibraryFiles": "પુસ્તકાલય ફાઇલો",
"HeaderListeningSessions": "Listening Sessions", "HeaderLibraryStats": "પુસ્તકાલય આંકડા",
"HeaderListeningStats": "Listening Stats", "HeaderListeningSessions": "સાંભળતી સેશન્સ",
"HeaderLogin": "Login", "HeaderListeningStats": "સાંભળતી આંકડા",
"HeaderLogs": "Logs", "HeaderLogin": "લોગિન",
"HeaderManageGenres": "Manage Genres", "HeaderLogs": "લોગ્સ",
"HeaderManageTags": "Manage Tags", "HeaderManageGenres": "જાતિઓ મેનેજ કરો",
"HeaderMapDetails": "Map details", "HeaderManageTags": "ટેગ્સ મેનેજ કરો",
"HeaderMatch": "Match", "HeaderMapDetails": "વિગતો મેપ કરો",
"HeaderMetadataToEmbed": "Metadata to embed", "HeaderMatch": "મેળ ખાતી શોધો",
"HeaderNewAccount": "New Account", "HeaderMetadataOrderOfPrecedence": "મેટાડેટા પ્રાધાન્યતાનો ક્રમ",
"HeaderNewLibrary": "New Library", "HeaderMetadataToEmbed": "એમ્બેડ કરવા માટે મેટાડેટા",
"HeaderNotifications": "Notifications", "HeaderNewAccount": "નવું એકાઉન્ટ",
"HeaderOpenRSSFeed": "Open RSS Feed", "HeaderNewLibrary": "નવી પુસ્તકાલય",
"HeaderOtherFiles": "Other Files", "HeaderNotifications": "સૂચનાઓ",
"HeaderPermissions": "Permissions", "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication",
"HeaderPlayerQueue": "Player Queue", "HeaderOpenRSSFeed": "RSS ફીડ ખોલો",
"HeaderPlaylist": "Playlist", "HeaderOtherFiles": "અન્ય ફાઇલો",
"HeaderPlaylistItems": "Playlist Items", "HeaderPasswordAuthentication": "Password Authentication",
"HeaderPodcastsToAdd": "Podcasts to Add", "HeaderPermissions": "પરવાનગીઓ",
"HeaderPreviewCover": "Preview Cover", "HeaderPlayerQueue": "પ્લેયર કતાર",
"HeaderRemoveEpisode": "Remove Episode", "HeaderPlaylist": "પ્લેલિસ્ટ",
"HeaderRemoveEpisodes": "Remove {0} Episodes", "HeaderPlaylistItems": "પ્લેલિસ્ટ ની વસ્તુઓ",
"HeaderRSSFeedGeneral": "RSS Details", "HeaderPodcastsToAdd": "ઉમેરવા માટે પોડકાસ્ટ્સ",
"HeaderRSSFeedIsOpen": "RSS Feed is Open", "HeaderPreviewCover": "પૂર્વાવલોકન કવર",
"HeaderRSSFeeds": "RSS Feeds", "HeaderRemoveEpisode": "એપિસોડ કાઢી નાખો",
"HeaderSavedMediaProgress": "Saved Media Progress", "HeaderRemoveEpisodes": "{0} એપિસોડ્સ કાઢી નાખો",
"HeaderSchedule": "Schedule", "HeaderRSSFeedGeneral": "સામાન્ય RSS ફીડ",
"HeaderScheduleLibraryScans": "Schedule Automatic Library Scans", "HeaderRSSFeedIsOpen": "RSS ફીડ ખોલેલી છે",
"HeaderSession": "Session", "HeaderRSSFeeds": "RSS ફીડ્સ",
"HeaderSetBackupSchedule": "Set Backup Schedule", "HeaderSavedMediaProgress": "સાચવેલ મીડિયા પ્રગતિ",
"HeaderSettings": "Settings", "HeaderSchedule": "સમયપત્રક",
"HeaderSettingsDisplay": "Display", "HeaderScheduleLibraryScans": "પુસ્તકાલય સ્કેન સમયપત્રક",
"HeaderSettingsExperimental": "Experimental Features", "HeaderSession": "સેશન",
"HeaderSettingsGeneral": "General", "HeaderSetBackupSchedule": "બેકઅપ સમયપત્રક સેટ કરો",
"HeaderSettingsScanner": "Scanner", "HeaderSettings": "સેટિંગ્સ",
"HeaderSleepTimer": "Sleep Timer", "HeaderSettingsDisplay": "ડિસ્પ્લે સેટિંગ્સ",
"HeaderStatsLargestItems": "Largest Items", "HeaderSettingsExperimental": "પ્રયોગશીલ સેટિંગ્સ",
"HeaderStatsLongestItems": "Longest Items (hrs)", "HeaderSettingsGeneral": "સામાન્ય સેટિંગ્સ",
"HeaderStatsMinutesListeningChart": "Minutes Listening (last 7 days)", "HeaderSettingsScanner": "સ્કેનર સેટિંગ્સ",
"HeaderStatsRecentSessions": "Recent Sessions", "HeaderSleepTimer": "સ્લીપ ટાઈમર",
"HeaderStatsLargestItems": "સૌથી મોટી વસ્તુઓ",
"HeaderStatsLongestItems": "સૌથી લાંબી વસ્તુઓ (કલાક)",
"HeaderStatsMinutesListeningChart": "સાંભળવાની મિનિટ (છેલ્લા ૭ દિવસ)",
"HeaderStatsRecentSessions": "છેલ્લી સાંભળતી સેશન્સ",
"HeaderStatsTop10Authors": "Top 10 Authors", "HeaderStatsTop10Authors": "Top 10 Authors",
"HeaderStatsTop5Genres": "Top 5 Genres", "HeaderStatsTop5Genres": "Top 5 Genres",
"HeaderTableOfContents": "Table of Contents", "HeaderTableOfContents": "Table of Contents",
@ -176,8 +187,11 @@
"LabelAddToCollectionBatch": "Add {0} Books to Collection", "LabelAddToCollectionBatch": "Add {0} Books to Collection",
"LabelAddToPlaylist": "Add to Playlist", "LabelAddToPlaylist": "Add to Playlist",
"LabelAddToPlaylistBatch": "Add {0} Items to Playlist", "LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
"LabelAdminUsersOnly": "Admin users only",
"LabelAll": "All", "LabelAll": "All",
"LabelAllUsers": "All Users", "LabelAllUsers": "All Users",
"LabelAllUsersExcludingGuests": "All users excluding guests",
"LabelAllUsersIncludingGuests": "All users including guests",
"LabelAlreadyInYourLibrary": "Already in your library", "LabelAlreadyInYourLibrary": "Already in your library",
"LabelAppend": "Append", "LabelAppend": "Append",
"LabelAuthor": "Author", "LabelAuthor": "Author",
@ -185,6 +199,12 @@
"LabelAuthorLastFirst": "Author (Last, First)", "LabelAuthorLastFirst": "Author (Last, First)",
"LabelAuthors": "Authors", "LabelAuthors": "Authors",
"LabelAutoDownloadEpisodes": "Auto Download Episodes", "LabelAutoDownloadEpisodes": "Auto Download Episodes",
"LabelAutoFetchMetadata": "Auto Fetch Metadata",
"LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
"LabelAutoLaunch": "Auto Launch",
"LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Auto Register",
"LabelAutoRegisterDescription": "Automatically create new users after logging in",
"LabelBackToUser": "Back to User", "LabelBackToUser": "Back to User",
"LabelBackupLocation": "Backup Location", "LabelBackupLocation": "Backup Location",
"LabelBackupsEnableAutomaticBackups": "Enable automatic backups", "LabelBackupsEnableAutomaticBackups": "Enable automatic backups",
@ -195,11 +215,13 @@
"LabelBackupsNumberToKeepHelp": "Only 1 backup will be removed at a time so if you already have more backups than this you should manually remove them.", "LabelBackupsNumberToKeepHelp": "Only 1 backup will be removed at a time so if you already have more backups than this you should manually remove them.",
"LabelBitrate": "Bitrate", "LabelBitrate": "Bitrate",
"LabelBooks": "Books", "LabelBooks": "Books",
"LabelButtonText": "Button Text",
"LabelChangePassword": "Change Password", "LabelChangePassword": "Change Password",
"LabelChannels": "Channels", "LabelChannels": "Channels",
"LabelChapters": "Chapters", "LabelChapters": "Chapters",
"LabelChaptersFound": "chapters found", "LabelChaptersFound": "chapters found",
"LabelChapterTitle": "Chapter Title", "LabelChapterTitle": "Chapter Title",
"LabelClickForMoreInfo": "Click for more info",
"LabelClosePlayer": "Close player", "LabelClosePlayer": "Close player",
"LabelCodec": "Codec", "LabelCodec": "Codec",
"LabelCollapseSeries": "Collapse Series", "LabelCollapseSeries": "Collapse Series",
@ -218,10 +240,12 @@
"LabelCurrently": "Currently:", "LabelCurrently": "Currently:",
"LabelCustomCronExpression": "Custom Cron Expression:", "LabelCustomCronExpression": "Custom Cron Expression:",
"LabelDatetime": "Datetime", "LabelDatetime": "Datetime",
"LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)",
"LabelDescription": "Description", "LabelDescription": "Description",
"LabelDeselectAll": "Deselect All", "LabelDeselectAll": "Deselect All",
"LabelDevice": "Device", "LabelDevice": "Device",
"LabelDeviceInfo": "Device Info", "LabelDeviceInfo": "Device Info",
"LabelDeviceIsAvailableTo": "Device is available to...",
"LabelDirectory": "Directory", "LabelDirectory": "Directory",
"LabelDiscFromFilename": "Disc from Filename", "LabelDiscFromFilename": "Disc from Filename",
"LabelDiscFromMetadata": "Disc from Metadata", "LabelDiscFromMetadata": "Disc from Metadata",
@ -247,6 +271,7 @@
"LabelExample": "Example", "LabelExample": "Example",
"LabelExplicit": "Explicit", "LabelExplicit": "Explicit",
"LabelFeedURL": "Feed URL", "LabelFeedURL": "Feed URL",
"LabelFetchingMetadata": "Fetching Metadata",
"LabelFile": "File", "LabelFile": "File",
"LabelFileBirthtime": "File Birthtime", "LabelFileBirthtime": "File Birthtime",
"LabelFileModified": "File Modified", "LabelFileModified": "File Modified",
@ -256,6 +281,7 @@
"LabelFinished": "Finished", "LabelFinished": "Finished",
"LabelFolder": "Folder", "LabelFolder": "Folder",
"LabelFolders": "Folders", "LabelFolders": "Folders",
"LabelFontFamily": "ફોન્ટ કુટુંબ",
"LabelFontScale": "Font scale", "LabelFontScale": "Font scale",
"LabelFormat": "Format", "LabelFormat": "Format",
"LabelGenre": "Genre", "LabelGenre": "Genre",
@ -263,9 +289,11 @@
"LabelHardDeleteFile": "Hard delete file", "LabelHardDeleteFile": "Hard delete file",
"LabelHasEbook": "Has ebook", "LabelHasEbook": "Has ebook",
"LabelHasSupplementaryEbook": "Has supplementary ebook", "LabelHasSupplementaryEbook": "Has supplementary ebook",
"LabelHighestPriority": "Highest priority",
"LabelHost": "Host", "LabelHost": "Host",
"LabelHour": "Hour", "LabelHour": "Hour",
"LabelIcon": "Icon", "LabelIcon": "Icon",
"LabelImageURLFromTheWeb": "Image URL from the web",
"LabelIncludeInTracklist": "Include in Tracklist", "LabelIncludeInTracklist": "Include in Tracklist",
"LabelIncomplete": "Incomplete", "LabelIncomplete": "Incomplete",
"LabelInProgress": "In Progress", "LabelInProgress": "In Progress",
@ -303,14 +331,20 @@
"LabelLogLevelInfo": "Info", "LabelLogLevelInfo": "Info",
"LabelLogLevelWarn": "Warn", "LabelLogLevelWarn": "Warn",
"LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date", "LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date",
"LabelLowestPriority": "Lowest Priority",
"LabelMatchExistingUsersBy": "Match existing users by",
"LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider",
"LabelMediaPlayer": "Media Player", "LabelMediaPlayer": "Media Player",
"LabelMediaType": "Media Type", "LabelMediaType": "Media Type",
"LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources",
"LabelMetadataProvider": "Metadata Provider", "LabelMetadataProvider": "Metadata Provider",
"LabelMetaTag": "Meta Tag", "LabelMetaTag": "Meta Tag",
"LabelMetaTags": "Meta Tags", "LabelMetaTags": "Meta Tags",
"LabelMinute": "Minute", "LabelMinute": "Minute",
"LabelMissing": "Missing", "LabelMissing": "Missing",
"LabelMissingParts": "Missing Parts", "LabelMissingParts": "Missing Parts",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
"LabelMore": "More", "LabelMore": "More",
"LabelMoreInfo": "More Info", "LabelMoreInfo": "More Info",
"LabelName": "Name", "LabelName": "Name",
@ -372,6 +406,7 @@
"LabelRegion": "Region", "LabelRegion": "Region",
"LabelReleaseDate": "Release Date", "LabelReleaseDate": "Release Date",
"LabelRemoveCover": "Remove cover", "LabelRemoveCover": "Remove cover",
"LabelRowsPerPage": "Rows per page",
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email", "LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
"LabelRSSFeedCustomOwnerName": "Custom owner Name", "LabelRSSFeedCustomOwnerName": "Custom owner Name",
"LabelRSSFeedOpen": "RSS Feed Open", "LabelRSSFeedOpen": "RSS Feed Open",
@ -384,6 +419,7 @@
"LabelSeason": "Season", "LabelSeason": "Season",
"LabelSelectAllEpisodes": "Select all episodes", "LabelSelectAllEpisodes": "Select all episodes",
"LabelSelectEpisodesShowing": "Select {0} episodes showing", "LabelSelectEpisodesShowing": "Select {0} episodes showing",
"LabelSelectUsers": "Select users",
"LabelSendEbookToDevice": "Send Ebook to...", "LabelSendEbookToDevice": "Send Ebook to...",
"LabelSequence": "Sequence", "LabelSequence": "Sequence",
"LabelSeries": "Series", "LabelSeries": "Series",
@ -410,16 +446,10 @@
"LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.", "LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.",
"LabelSettingsHomePageBookshelfView": "Home page use bookshelf view", "LabelSettingsHomePageBookshelfView": "Home page use bookshelf view",
"LabelSettingsLibraryBookshelfView": "Library use bookshelf view", "LabelSettingsLibraryBookshelfView": "Library use bookshelf view",
"LabelSettingsOverdriveMediaMarkers": "Use Overdrive Media Markers for chapters",
"LabelSettingsOverdriveMediaMarkersHelp": "MP3 files from Overdrive come with chapter timings embedded as custom metadata. Enabling this will use these tags for chapter timings automatically",
"LabelSettingsParseSubtitles": "Parse subtitles", "LabelSettingsParseSubtitles": "Parse subtitles",
"LabelSettingsParseSubtitlesHelp": "Extract subtitles from audiobook folder names.<br>Subtitle must be seperated by \" - \"<br>i.e. \"Book Title - A Subtitle Here\" has the subtitle \"A Subtitle Here\"", "LabelSettingsParseSubtitlesHelp": "Extract subtitles from audiobook folder names.<br>Subtitle must be seperated by \" - \"<br>i.e. \"Book Title - A Subtitle Here\" has the subtitle \"A Subtitle Here\"",
"LabelSettingsPreferAudioMetadata": "Prefer audio metadata",
"LabelSettingsPreferAudioMetadataHelp": "Audio file ID3 meta tags will be used for book details over folder names",
"LabelSettingsPreferMatchedMetadata": "Prefer matched metadata", "LabelSettingsPreferMatchedMetadata": "Prefer matched metadata",
"LabelSettingsPreferMatchedMetadataHelp": "Matched data will overide item details when using Quick Match. By default Quick Match will only fill in missing details.", "LabelSettingsPreferMatchedMetadataHelp": "Matched data will overide item details when using Quick Match. By default Quick Match will only fill in missing details.",
"LabelSettingsPreferOPFMetadata": "Prefer OPF metadata",
"LabelSettingsPreferOPFMetadataHelp": "OPF file metadata will be used for book details over folder names",
"LabelSettingsSkipMatchingBooksWithASIN": "Skip matching books that already have an ASIN", "LabelSettingsSkipMatchingBooksWithASIN": "Skip matching books that already have an ASIN",
"LabelSettingsSkipMatchingBooksWithISBN": "Skip matching books that already have an ISBN", "LabelSettingsSkipMatchingBooksWithISBN": "Skip matching books that already have an ISBN",
"LabelSettingsSortingIgnorePrefixes": "Ignore prefixes when sorting", "LabelSettingsSortingIgnorePrefixes": "Ignore prefixes when sorting",
@ -429,7 +459,7 @@
"LabelSettingsStoreCoversWithItem": "Store covers with item", "LabelSettingsStoreCoversWithItem": "Store covers with item",
"LabelSettingsStoreCoversWithItemHelp": "By default covers are stored in /metadata/items, enabling this setting will store covers in your library item folder. Only one file named \"cover\" will be kept", "LabelSettingsStoreCoversWithItemHelp": "By default covers are stored in /metadata/items, enabling this setting will store covers in your library item folder. Only one file named \"cover\" will be kept",
"LabelSettingsStoreMetadataWithItem": "Store metadata with item", "LabelSettingsStoreMetadataWithItem": "Store metadata with item",
"LabelSettingsStoreMetadataWithItemHelp": "By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders. Uses .abs file extension", "LabelSettingsStoreMetadataWithItemHelp": "By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders",
"LabelSettingsTimeFormat": "Time Format", "LabelSettingsTimeFormat": "Time Format",
"LabelShowAll": "Show All", "LabelShowAll": "Show All",
"LabelSize": "Size", "LabelSize": "Size",
@ -494,6 +524,7 @@
"LabelUpdateDetailsHelp": "Allow overwriting of existing details for the selected books when a match is located", "LabelUpdateDetailsHelp": "Allow overwriting of existing details for the selected books when a match is located",
"LabelUploaderDragAndDrop": "Drag & drop files or folders", "LabelUploaderDragAndDrop": "Drag & drop files or folders",
"LabelUploaderDropFiles": "Drop files", "LabelUploaderDropFiles": "Drop files",
"LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
"LabelUseChapterTrack": "Use chapter track", "LabelUseChapterTrack": "Use chapter track",
"LabelUseFullTrack": "Use full track", "LabelUseFullTrack": "Use full track",
"LabelUser": "User", "LabelUser": "User",
@ -527,17 +558,21 @@
"MessageConfirmDeleteBackup": "Are you sure you want to delete backup for {0}?", "MessageConfirmDeleteBackup": "Are you sure you want to delete backup for {0}?",
"MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?", "MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?",
"MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?", "MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?",
"MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?",
"MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?",
"MessageConfirmDeleteSession": "Are you sure you want to delete this session?", "MessageConfirmDeleteSession": "Are you sure you want to delete this session?",
"MessageConfirmForceReScan": "Are you sure you want to force re-scan?", "MessageConfirmForceReScan": "Are you sure you want to force re-scan?",
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?", "MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?", "MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
"MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?", "MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
"MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?", "MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
"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?",
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?", "MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
"MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?", "MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?",
"MessageConfirmRemoveCollection": "Are you sure you want to remove collection \"{0}\"?", "MessageConfirmRemoveCollection": "Are you sure you want to remove collection \"{0}\"?",
"MessageConfirmRemoveEpisode": "Are you sure you want to remove episode \"{0}\"?", "MessageConfirmRemoveEpisode": "Are you sure you want to remove episode \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} episodes?", "MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} episodes?",
"MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?", "MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?", "MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
"MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?", "MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?",
@ -546,6 +581,7 @@
"MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?", "MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
"MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.", "MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
"MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".", "MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
"MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?",
"MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?", "MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
"MessageDownloadingEpisode": "Downloading episode", "MessageDownloadingEpisode": "Downloading episode",
"MessageDragFilesIntoTrackOrder": "Drag files into correct track order", "MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
@ -616,6 +652,7 @@
"MessageRestoreBackupConfirm": "Are you sure you want to restore the backup created on", "MessageRestoreBackupConfirm": "Are you sure you want to restore the backup created on",
"MessageRestoreBackupWarning": "Restoring a backup will overwrite the entire database located at /config and cover images in /metadata/items & /metadata/authors.<br /><br />Backups do not modify any files in your library folders. If you have enabled server settings to store cover art and metadata in your library folders then those are not backed up or overwritten.<br /><br />All clients using your server will be automatically refreshed.", "MessageRestoreBackupWarning": "Restoring a backup will overwrite the entire database located at /config and cover images in /metadata/items & /metadata/authors.<br /><br />Backups do not modify any files in your library folders. If you have enabled server settings to store cover art and metadata in your library folders then those are not backed up or overwritten.<br /><br />All clients using your server will be automatically refreshed.",
"MessageSearchResultsFor": "Search results for", "MessageSearchResultsFor": "Search results for",
"MessageSelected": "{0} selected",
"MessageServerCouldNotBeReached": "Server could not be reached", "MessageServerCouldNotBeReached": "Server could not be reached",
"MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name", "MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name",
"MessageStartPlaybackAtTime": "Start playback for \"{0}\" at {1}?", "MessageStartPlaybackAtTime": "Start playback for \"{0}\" at {1}?",

View File

@ -1,7 +1,10 @@
{ {
"ButtonAdd": "जोड़ें", "ButtonAdd": "जोड़ें",
"ButtonAddChapters": "अध्याय जोड़ें", "ButtonAddChapters": "अध्याय जोड़ें",
"ButtonAddDevice": "Add Device",
"ButtonAddLibrary": "Add Library",
"ButtonAddPodcasts": "पॉडकास्ट जोड़ें", "ButtonAddPodcasts": "पॉडकास्ट जोड़ें",
"ButtonAddUser": "Add User",
"ButtonAddYourFirstLibrary": "अपनी पहली पुस्तकालय जोड़ें", "ButtonAddYourFirstLibrary": "अपनी पहली पुस्तकालय जोड़ें",
"ButtonApply": "लागू करें", "ButtonApply": "लागू करें",
"ButtonApplyChapters": "अध्यायों में परिवर्तन लागू करें", "ButtonApplyChapters": "अध्यायों में परिवर्तन लागू करें",
@ -59,6 +62,7 @@
"ButtonRemoveSeriesFromContinueSeries": "इस सीरीज को कंटिन्यू सीरीज से हटा दें", "ButtonRemoveSeriesFromContinueSeries": "इस सीरीज को कंटिन्यू सीरीज से हटा दें",
"ButtonReScan": "पुन: स्कैन करें", "ButtonReScan": "पुन: स्कैन करें",
"ButtonReset": "रीसेट करें", "ButtonReset": "रीसेट करें",
"ButtonResetToDefault": "Reset to default",
"ButtonRestore": "पुनर्स्थापित करें", "ButtonRestore": "पुनर्स्थापित करें",
"ButtonSave": "सहेजें", "ButtonSave": "सहेजें",
"ButtonSaveAndClose": "सहेजें और बंद करें", "ButtonSaveAndClose": "सहेजें और बंद करें",
@ -83,11 +87,15 @@
"ButtonUserEdit": "उपयोगकर्ता {0} को संपादित करें", "ButtonUserEdit": "उपयोगकर्ता {0} को संपादित करें",
"ButtonViewAll": "सभी को देखें", "ButtonViewAll": "सभी को देखें",
"ButtonYes": "हाँ", "ButtonYes": "हाँ",
"ErrorUploadFetchMetadataAPI": "Error fetching metadata",
"ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
"ErrorUploadLacksTitle": "Must have a title",
"HeaderAccount": "खाता", "HeaderAccount": "खाता",
"HeaderAdvanced": "विकसित", "HeaderAdvanced": "विकसित",
"HeaderAppriseNotificationSettings": "Apprise अधिसूचना सेटिंग्स", "HeaderAppriseNotificationSettings": "Apprise अधिसूचना सेटिंग्स",
"HeaderAudiobookTools": "Audiobook File Management Tools", "HeaderAudiobookTools": "Audiobook File Management Tools",
"HeaderAudioTracks": "Audio Tracks", "HeaderAudioTracks": "Audio Tracks",
"HeaderAuthentication": "Authentication",
"HeaderBackups": "Backups", "HeaderBackups": "Backups",
"HeaderChangePassword": "Change Password", "HeaderChangePassword": "Change Password",
"HeaderChapters": "Chapters", "HeaderChapters": "Chapters",
@ -122,12 +130,15 @@
"HeaderManageTags": "Manage Tags", "HeaderManageTags": "Manage Tags",
"HeaderMapDetails": "Map details", "HeaderMapDetails": "Map details",
"HeaderMatch": "Match", "HeaderMatch": "Match",
"HeaderMetadataOrderOfPrecedence": "Metadata order of precedence",
"HeaderMetadataToEmbed": "Metadata to embed", "HeaderMetadataToEmbed": "Metadata to embed",
"HeaderNewAccount": "New Account", "HeaderNewAccount": "New Account",
"HeaderNewLibrary": "New Library", "HeaderNewLibrary": "New Library",
"HeaderNotifications": "Notifications", "HeaderNotifications": "Notifications",
"HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication",
"HeaderOpenRSSFeed": "Open RSS Feed", "HeaderOpenRSSFeed": "Open RSS Feed",
"HeaderOtherFiles": "Other Files", "HeaderOtherFiles": "Other Files",
"HeaderPasswordAuthentication": "Password Authentication",
"HeaderPermissions": "Permissions", "HeaderPermissions": "Permissions",
"HeaderPlayerQueue": "Player Queue", "HeaderPlayerQueue": "Player Queue",
"HeaderPlaylist": "Playlist", "HeaderPlaylist": "Playlist",
@ -176,8 +187,11 @@
"LabelAddToCollectionBatch": "Add {0} Books to Collection", "LabelAddToCollectionBatch": "Add {0} Books to Collection",
"LabelAddToPlaylist": "Add to Playlist", "LabelAddToPlaylist": "Add to Playlist",
"LabelAddToPlaylistBatch": "Add {0} Items to Playlist", "LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
"LabelAdminUsersOnly": "Admin users only",
"LabelAll": "All", "LabelAll": "All",
"LabelAllUsers": "All Users", "LabelAllUsers": "All Users",
"LabelAllUsersExcludingGuests": "All users excluding guests",
"LabelAllUsersIncludingGuests": "All users including guests",
"LabelAlreadyInYourLibrary": "Already in your library", "LabelAlreadyInYourLibrary": "Already in your library",
"LabelAppend": "Append", "LabelAppend": "Append",
"LabelAuthor": "Author", "LabelAuthor": "Author",
@ -185,6 +199,12 @@
"LabelAuthorLastFirst": "Author (Last, First)", "LabelAuthorLastFirst": "Author (Last, First)",
"LabelAuthors": "Authors", "LabelAuthors": "Authors",
"LabelAutoDownloadEpisodes": "Auto Download Episodes", "LabelAutoDownloadEpisodes": "Auto Download Episodes",
"LabelAutoFetchMetadata": "Auto Fetch Metadata",
"LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
"LabelAutoLaunch": "Auto Launch",
"LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Auto Register",
"LabelAutoRegisterDescription": "Automatically create new users after logging in",
"LabelBackToUser": "Back to User", "LabelBackToUser": "Back to User",
"LabelBackupLocation": "Backup Location", "LabelBackupLocation": "Backup Location",
"LabelBackupsEnableAutomaticBackups": "Enable automatic backups", "LabelBackupsEnableAutomaticBackups": "Enable automatic backups",
@ -195,11 +215,13 @@
"LabelBackupsNumberToKeepHelp": "Only 1 backup will be removed at a time so if you already have more backups than this you should manually remove them.", "LabelBackupsNumberToKeepHelp": "Only 1 backup will be removed at a time so if you already have more backups than this you should manually remove them.",
"LabelBitrate": "Bitrate", "LabelBitrate": "Bitrate",
"LabelBooks": "Books", "LabelBooks": "Books",
"LabelButtonText": "Button Text",
"LabelChangePassword": "Change Password", "LabelChangePassword": "Change Password",
"LabelChannels": "Channels", "LabelChannels": "Channels",
"LabelChapters": "Chapters", "LabelChapters": "Chapters",
"LabelChaptersFound": "chapters found", "LabelChaptersFound": "chapters found",
"LabelChapterTitle": "Chapter Title", "LabelChapterTitle": "Chapter Title",
"LabelClickForMoreInfo": "Click for more info",
"LabelClosePlayer": "Close player", "LabelClosePlayer": "Close player",
"LabelCodec": "Codec", "LabelCodec": "Codec",
"LabelCollapseSeries": "Collapse Series", "LabelCollapseSeries": "Collapse Series",
@ -218,10 +240,12 @@
"LabelCurrently": "Currently:", "LabelCurrently": "Currently:",
"LabelCustomCronExpression": "Custom Cron Expression:", "LabelCustomCronExpression": "Custom Cron Expression:",
"LabelDatetime": "Datetime", "LabelDatetime": "Datetime",
"LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)",
"LabelDescription": "Description", "LabelDescription": "Description",
"LabelDeselectAll": "Deselect All", "LabelDeselectAll": "Deselect All",
"LabelDevice": "Device", "LabelDevice": "Device",
"LabelDeviceInfo": "Device Info", "LabelDeviceInfo": "Device Info",
"LabelDeviceIsAvailableTo": "Device is available to...",
"LabelDirectory": "Directory", "LabelDirectory": "Directory",
"LabelDiscFromFilename": "Disc from Filename", "LabelDiscFromFilename": "Disc from Filename",
"LabelDiscFromMetadata": "Disc from Metadata", "LabelDiscFromMetadata": "Disc from Metadata",
@ -247,6 +271,7 @@
"LabelExample": "Example", "LabelExample": "Example",
"LabelExplicit": "Explicit", "LabelExplicit": "Explicit",
"LabelFeedURL": "Feed URL", "LabelFeedURL": "Feed URL",
"LabelFetchingMetadata": "Fetching Metadata",
"LabelFile": "File", "LabelFile": "File",
"LabelFileBirthtime": "File Birthtime", "LabelFileBirthtime": "File Birthtime",
"LabelFileModified": "File Modified", "LabelFileModified": "File Modified",
@ -256,6 +281,7 @@
"LabelFinished": "Finished", "LabelFinished": "Finished",
"LabelFolder": "Folder", "LabelFolder": "Folder",
"LabelFolders": "Folders", "LabelFolders": "Folders",
"LabelFontFamily": "फुहारा परिवार",
"LabelFontScale": "Font scale", "LabelFontScale": "Font scale",
"LabelFormat": "Format", "LabelFormat": "Format",
"LabelGenre": "Genre", "LabelGenre": "Genre",
@ -263,9 +289,11 @@
"LabelHardDeleteFile": "Hard delete file", "LabelHardDeleteFile": "Hard delete file",
"LabelHasEbook": "Has ebook", "LabelHasEbook": "Has ebook",
"LabelHasSupplementaryEbook": "Has supplementary ebook", "LabelHasSupplementaryEbook": "Has supplementary ebook",
"LabelHighestPriority": "Highest priority",
"LabelHost": "Host", "LabelHost": "Host",
"LabelHour": "Hour", "LabelHour": "Hour",
"LabelIcon": "Icon", "LabelIcon": "Icon",
"LabelImageURLFromTheWeb": "Image URL from the web",
"LabelIncludeInTracklist": "Include in Tracklist", "LabelIncludeInTracklist": "Include in Tracklist",
"LabelIncomplete": "Incomplete", "LabelIncomplete": "Incomplete",
"LabelInProgress": "In Progress", "LabelInProgress": "In Progress",
@ -303,14 +331,20 @@
"LabelLogLevelInfo": "Info", "LabelLogLevelInfo": "Info",
"LabelLogLevelWarn": "Warn", "LabelLogLevelWarn": "Warn",
"LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date", "LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date",
"LabelLowestPriority": "Lowest Priority",
"LabelMatchExistingUsersBy": "Match existing users by",
"LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider",
"LabelMediaPlayer": "Media Player", "LabelMediaPlayer": "Media Player",
"LabelMediaType": "Media Type", "LabelMediaType": "Media Type",
"LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources",
"LabelMetadataProvider": "Metadata Provider", "LabelMetadataProvider": "Metadata Provider",
"LabelMetaTag": "Meta Tag", "LabelMetaTag": "Meta Tag",
"LabelMetaTags": "Meta Tags", "LabelMetaTags": "Meta Tags",
"LabelMinute": "Minute", "LabelMinute": "Minute",
"LabelMissing": "Missing", "LabelMissing": "Missing",
"LabelMissingParts": "Missing Parts", "LabelMissingParts": "Missing Parts",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
"LabelMore": "More", "LabelMore": "More",
"LabelMoreInfo": "More Info", "LabelMoreInfo": "More Info",
"LabelName": "Name", "LabelName": "Name",
@ -372,6 +406,7 @@
"LabelRegion": "Region", "LabelRegion": "Region",
"LabelReleaseDate": "Release Date", "LabelReleaseDate": "Release Date",
"LabelRemoveCover": "Remove cover", "LabelRemoveCover": "Remove cover",
"LabelRowsPerPage": "Rows per page",
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email", "LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
"LabelRSSFeedCustomOwnerName": "Custom owner Name", "LabelRSSFeedCustomOwnerName": "Custom owner Name",
"LabelRSSFeedOpen": "RSS Feed Open", "LabelRSSFeedOpen": "RSS Feed Open",
@ -384,6 +419,7 @@
"LabelSeason": "Season", "LabelSeason": "Season",
"LabelSelectAllEpisodes": "Select all episodes", "LabelSelectAllEpisodes": "Select all episodes",
"LabelSelectEpisodesShowing": "Select {0} episodes showing", "LabelSelectEpisodesShowing": "Select {0} episodes showing",
"LabelSelectUsers": "Select users",
"LabelSendEbookToDevice": "Send Ebook to...", "LabelSendEbookToDevice": "Send Ebook to...",
"LabelSequence": "Sequence", "LabelSequence": "Sequence",
"LabelSeries": "Series", "LabelSeries": "Series",
@ -410,16 +446,10 @@
"LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.", "LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.",
"LabelSettingsHomePageBookshelfView": "Home page use bookshelf view", "LabelSettingsHomePageBookshelfView": "Home page use bookshelf view",
"LabelSettingsLibraryBookshelfView": "Library use bookshelf view", "LabelSettingsLibraryBookshelfView": "Library use bookshelf view",
"LabelSettingsOverdriveMediaMarkers": "Use Overdrive Media Markers for chapters",
"LabelSettingsOverdriveMediaMarkersHelp": "MP3 files from Overdrive come with chapter timings embedded as custom metadata. Enabling this will use these tags for chapter timings automatically",
"LabelSettingsParseSubtitles": "Parse subtitles", "LabelSettingsParseSubtitles": "Parse subtitles",
"LabelSettingsParseSubtitlesHelp": "Extract subtitles from audiobook folder names.<br>Subtitle must be seperated by \" - \"<br>i.e. \"Book Title - A Subtitle Here\" has the subtitle \"A Subtitle Here\"", "LabelSettingsParseSubtitlesHelp": "Extract subtitles from audiobook folder names.<br>Subtitle must be seperated by \" - \"<br>i.e. \"Book Title - A Subtitle Here\" has the subtitle \"A Subtitle Here\"",
"LabelSettingsPreferAudioMetadata": "Prefer audio metadata",
"LabelSettingsPreferAudioMetadataHelp": "Audio file ID3 meta tags will be used for book details over folder names",
"LabelSettingsPreferMatchedMetadata": "Prefer matched metadata", "LabelSettingsPreferMatchedMetadata": "Prefer matched metadata",
"LabelSettingsPreferMatchedMetadataHelp": "Matched data will overide item details when using Quick Match. By default Quick Match will only fill in missing details.", "LabelSettingsPreferMatchedMetadataHelp": "Matched data will overide item details when using Quick Match. By default Quick Match will only fill in missing details.",
"LabelSettingsPreferOPFMetadata": "Prefer OPF metadata",
"LabelSettingsPreferOPFMetadataHelp": "OPF file metadata will be used for book details over folder names",
"LabelSettingsSkipMatchingBooksWithASIN": "Skip matching books that already have an ASIN", "LabelSettingsSkipMatchingBooksWithASIN": "Skip matching books that already have an ASIN",
"LabelSettingsSkipMatchingBooksWithISBN": "Skip matching books that already have an ISBN", "LabelSettingsSkipMatchingBooksWithISBN": "Skip matching books that already have an ISBN",
"LabelSettingsSortingIgnorePrefixes": "Ignore prefixes when sorting", "LabelSettingsSortingIgnorePrefixes": "Ignore prefixes when sorting",
@ -429,7 +459,7 @@
"LabelSettingsStoreCoversWithItem": "Store covers with item", "LabelSettingsStoreCoversWithItem": "Store covers with item",
"LabelSettingsStoreCoversWithItemHelp": "By default covers are stored in /metadata/items, enabling this setting will store covers in your library item folder. Only one file named \"cover\" will be kept", "LabelSettingsStoreCoversWithItemHelp": "By default covers are stored in /metadata/items, enabling this setting will store covers in your library item folder. Only one file named \"cover\" will be kept",
"LabelSettingsStoreMetadataWithItem": "Store metadata with item", "LabelSettingsStoreMetadataWithItem": "Store metadata with item",
"LabelSettingsStoreMetadataWithItemHelp": "By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders. Uses .abs file extension", "LabelSettingsStoreMetadataWithItemHelp": "By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders",
"LabelSettingsTimeFormat": "Time Format", "LabelSettingsTimeFormat": "Time Format",
"LabelShowAll": "Show All", "LabelShowAll": "Show All",
"LabelSize": "Size", "LabelSize": "Size",
@ -494,6 +524,7 @@
"LabelUpdateDetailsHelp": "Allow overwriting of existing details for the selected books when a match is located", "LabelUpdateDetailsHelp": "Allow overwriting of existing details for the selected books when a match is located",
"LabelUploaderDragAndDrop": "Drag & drop files or folders", "LabelUploaderDragAndDrop": "Drag & drop files or folders",
"LabelUploaderDropFiles": "Drop files", "LabelUploaderDropFiles": "Drop files",
"LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
"LabelUseChapterTrack": "Use chapter track", "LabelUseChapterTrack": "Use chapter track",
"LabelUseFullTrack": "Use full track", "LabelUseFullTrack": "Use full track",
"LabelUser": "User", "LabelUser": "User",
@ -527,17 +558,21 @@
"MessageConfirmDeleteBackup": "Are you sure you want to delete backup for {0}?", "MessageConfirmDeleteBackup": "Are you sure you want to delete backup for {0}?",
"MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?", "MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?",
"MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?", "MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?",
"MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?",
"MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?",
"MessageConfirmDeleteSession": "Are you sure you want to delete this session?", "MessageConfirmDeleteSession": "Are you sure you want to delete this session?",
"MessageConfirmForceReScan": "Are you sure you want to force re-scan?", "MessageConfirmForceReScan": "Are you sure you want to force re-scan?",
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?", "MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?", "MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
"MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?", "MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
"MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?", "MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
"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?",
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?", "MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
"MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?", "MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?",
"MessageConfirmRemoveCollection": "Are you sure you want to remove collection \"{0}\"?", "MessageConfirmRemoveCollection": "Are you sure you want to remove collection \"{0}\"?",
"MessageConfirmRemoveEpisode": "Are you sure you want to remove episode \"{0}\"?", "MessageConfirmRemoveEpisode": "Are you sure you want to remove episode \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} episodes?", "MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} episodes?",
"MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?", "MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?", "MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
"MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?", "MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?",
@ -546,6 +581,7 @@
"MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?", "MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
"MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.", "MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
"MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".", "MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
"MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?",
"MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?", "MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
"MessageDownloadingEpisode": "Downloading episode", "MessageDownloadingEpisode": "Downloading episode",
"MessageDragFilesIntoTrackOrder": "Drag files into correct track order", "MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
@ -616,6 +652,7 @@
"MessageRestoreBackupConfirm": "Are you sure you want to restore the backup created on", "MessageRestoreBackupConfirm": "Are you sure you want to restore the backup created on",
"MessageRestoreBackupWarning": "Restoring a backup will overwrite the entire database located at /config and cover images in /metadata/items & /metadata/authors.<br /><br />Backups do not modify any files in your library folders. If you have enabled server settings to store cover art and metadata in your library folders then those are not backed up or overwritten.<br /><br />All clients using your server will be automatically refreshed.", "MessageRestoreBackupWarning": "Restoring a backup will overwrite the entire database located at /config and cover images in /metadata/items & /metadata/authors.<br /><br />Backups do not modify any files in your library folders. If you have enabled server settings to store cover art and metadata in your library folders then those are not backed up or overwritten.<br /><br />All clients using your server will be automatically refreshed.",
"MessageSearchResultsFor": "Search results for", "MessageSearchResultsFor": "Search results for",
"MessageSelected": "{0} selected",
"MessageServerCouldNotBeReached": "Server could not be reached", "MessageServerCouldNotBeReached": "Server could not be reached",
"MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name", "MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name",
"MessageStartPlaybackAtTime": "Start playback for \"{0}\" at {1}?", "MessageStartPlaybackAtTime": "Start playback for \"{0}\" at {1}?",

View File

@ -1,7 +1,10 @@
{ {
"ButtonAdd": "Dodaj", "ButtonAdd": "Dodaj",
"ButtonAddChapters": "Dodaj poglavlja", "ButtonAddChapters": "Dodaj poglavlja",
"ButtonAddDevice": "Add Device",
"ButtonAddLibrary": "Add Library",
"ButtonAddPodcasts": "Dodaj podcaste", "ButtonAddPodcasts": "Dodaj podcaste",
"ButtonAddUser": "Add User",
"ButtonAddYourFirstLibrary": "Dodaj svoju prvu biblioteku", "ButtonAddYourFirstLibrary": "Dodaj svoju prvu biblioteku",
"ButtonApply": "Primijeni", "ButtonApply": "Primijeni",
"ButtonApplyChapters": "Primijeni poglavlja", "ButtonApplyChapters": "Primijeni poglavlja",
@ -59,6 +62,7 @@
"ButtonRemoveSeriesFromContinueSeries": "Ukloni seriju iz Nastavi seriju", "ButtonRemoveSeriesFromContinueSeries": "Ukloni seriju iz Nastavi seriju",
"ButtonReScan": "Skeniraj ponovno", "ButtonReScan": "Skeniraj ponovno",
"ButtonReset": "Poništi", "ButtonReset": "Poništi",
"ButtonResetToDefault": "Reset to default",
"ButtonRestore": "Povrati", "ButtonRestore": "Povrati",
"ButtonSave": "Spremi", "ButtonSave": "Spremi",
"ButtonSaveAndClose": "Spremi i zatvori", "ButtonSaveAndClose": "Spremi i zatvori",
@ -83,11 +87,15 @@
"ButtonUserEdit": "Edit user {0}", "ButtonUserEdit": "Edit user {0}",
"ButtonViewAll": "Prikaži sve", "ButtonViewAll": "Prikaži sve",
"ButtonYes": "Da", "ButtonYes": "Da",
"ErrorUploadFetchMetadataAPI": "Error fetching metadata",
"ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
"ErrorUploadLacksTitle": "Must have a title",
"HeaderAccount": "Korisnički račun", "HeaderAccount": "Korisnički račun",
"HeaderAdvanced": "Napredno", "HeaderAdvanced": "Napredno",
"HeaderAppriseNotificationSettings": "Apprise Notification Settings", "HeaderAppriseNotificationSettings": "Apprise Notification Settings",
"HeaderAudiobookTools": "Audiobook File Management alati", "HeaderAudiobookTools": "Audiobook File Management alati",
"HeaderAudioTracks": "Audio Tracks", "HeaderAudioTracks": "Audio Tracks",
"HeaderAuthentication": "Authentication",
"HeaderBackups": "Backups", "HeaderBackups": "Backups",
"HeaderChangePassword": "Promijeni lozinku", "HeaderChangePassword": "Promijeni lozinku",
"HeaderChapters": "Poglavlja", "HeaderChapters": "Poglavlja",
@ -122,12 +130,15 @@
"HeaderManageTags": "Manage Tags", "HeaderManageTags": "Manage Tags",
"HeaderMapDetails": "Map details", "HeaderMapDetails": "Map details",
"HeaderMatch": "Match", "HeaderMatch": "Match",
"HeaderMetadataOrderOfPrecedence": "Metadata order of precedence",
"HeaderMetadataToEmbed": "Metapodatci za ugradnju", "HeaderMetadataToEmbed": "Metapodatci za ugradnju",
"HeaderNewAccount": "Novi korisnički račun", "HeaderNewAccount": "Novi korisnički račun",
"HeaderNewLibrary": "Nova biblioteka", "HeaderNewLibrary": "Nova biblioteka",
"HeaderNotifications": "Obavijesti", "HeaderNotifications": "Obavijesti",
"HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication",
"HeaderOpenRSSFeed": "Otvori RSS Feed", "HeaderOpenRSSFeed": "Otvori RSS Feed",
"HeaderOtherFiles": "Druge datoteke", "HeaderOtherFiles": "Druge datoteke",
"HeaderPasswordAuthentication": "Password Authentication",
"HeaderPermissions": "Dozvole", "HeaderPermissions": "Dozvole",
"HeaderPlayerQueue": "Player Queue", "HeaderPlayerQueue": "Player Queue",
"HeaderPlaylist": "Playlist", "HeaderPlaylist": "Playlist",
@ -176,8 +187,11 @@
"LabelAddToCollectionBatch": "Add {0} Books to Collection", "LabelAddToCollectionBatch": "Add {0} Books to Collection",
"LabelAddToPlaylist": "Add to Playlist", "LabelAddToPlaylist": "Add to Playlist",
"LabelAddToPlaylistBatch": "Add {0} Items to Playlist", "LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
"LabelAdminUsersOnly": "Admin users only",
"LabelAll": "All", "LabelAll": "All",
"LabelAllUsers": "Svi korisnici", "LabelAllUsers": "Svi korisnici",
"LabelAllUsersExcludingGuests": "All users excluding guests",
"LabelAllUsersIncludingGuests": "All users including guests",
"LabelAlreadyInYourLibrary": "Already in your library", "LabelAlreadyInYourLibrary": "Already in your library",
"LabelAppend": "Append", "LabelAppend": "Append",
"LabelAuthor": "Autor", "LabelAuthor": "Autor",
@ -185,6 +199,12 @@
"LabelAuthorLastFirst": "Author (Last, First)", "LabelAuthorLastFirst": "Author (Last, First)",
"LabelAuthors": "Autori", "LabelAuthors": "Autori",
"LabelAutoDownloadEpisodes": "Automatski preuzmi epizode", "LabelAutoDownloadEpisodes": "Automatski preuzmi epizode",
"LabelAutoFetchMetadata": "Auto Fetch Metadata",
"LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
"LabelAutoLaunch": "Auto Launch",
"LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Auto Register",
"LabelAutoRegisterDescription": "Automatically create new users after logging in",
"LabelBackToUser": "Nazad k korisniku", "LabelBackToUser": "Nazad k korisniku",
"LabelBackupLocation": "Backup Location", "LabelBackupLocation": "Backup Location",
"LabelBackupsEnableAutomaticBackups": "Uključi automatski backup", "LabelBackupsEnableAutomaticBackups": "Uključi automatski backup",
@ -195,11 +215,13 @@
"LabelBackupsNumberToKeepHelp": "Samo 1 backup će biti odjednom obrisan. Ako koristite više njih, morati ćete ih ručno ukloniti.", "LabelBackupsNumberToKeepHelp": "Samo 1 backup će biti odjednom obrisan. Ako koristite više njih, morati ćete ih ručno ukloniti.",
"LabelBitrate": "Bitrate", "LabelBitrate": "Bitrate",
"LabelBooks": "Knjige", "LabelBooks": "Knjige",
"LabelButtonText": "Button Text",
"LabelChangePassword": "Promijeni lozinku", "LabelChangePassword": "Promijeni lozinku",
"LabelChannels": "Channels", "LabelChannels": "Channels",
"LabelChapters": "Chapters", "LabelChapters": "Chapters",
"LabelChaptersFound": "poglavlja pronađena", "LabelChaptersFound": "poglavlja pronađena",
"LabelChapterTitle": "Ime poglavlja", "LabelChapterTitle": "Ime poglavlja",
"LabelClickForMoreInfo": "Click for more info",
"LabelClosePlayer": "Close player", "LabelClosePlayer": "Close player",
"LabelCodec": "Codec", "LabelCodec": "Codec",
"LabelCollapseSeries": "Collapse Series", "LabelCollapseSeries": "Collapse Series",
@ -218,10 +240,12 @@
"LabelCurrently": "Trenutno:", "LabelCurrently": "Trenutno:",
"LabelCustomCronExpression": "Custom Cron Expression:", "LabelCustomCronExpression": "Custom Cron Expression:",
"LabelDatetime": "Datetime", "LabelDatetime": "Datetime",
"LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)",
"LabelDescription": "Opis", "LabelDescription": "Opis",
"LabelDeselectAll": "Odznači sve", "LabelDeselectAll": "Odznači sve",
"LabelDevice": "Uređaj", "LabelDevice": "Uređaj",
"LabelDeviceInfo": "O uređaju", "LabelDeviceInfo": "O uređaju",
"LabelDeviceIsAvailableTo": "Device is available to...",
"LabelDirectory": "Direktorij", "LabelDirectory": "Direktorij",
"LabelDiscFromFilename": "CD iz imena datoteke", "LabelDiscFromFilename": "CD iz imena datoteke",
"LabelDiscFromMetadata": "CD iz metapodataka", "LabelDiscFromMetadata": "CD iz metapodataka",
@ -247,6 +271,7 @@
"LabelExample": "Example", "LabelExample": "Example",
"LabelExplicit": "Explicit", "LabelExplicit": "Explicit",
"LabelFeedURL": "Feed URL", "LabelFeedURL": "Feed URL",
"LabelFetchingMetadata": "Fetching Metadata",
"LabelFile": "Datoteka", "LabelFile": "Datoteka",
"LabelFileBirthtime": "File Birthtime", "LabelFileBirthtime": "File Birthtime",
"LabelFileModified": "File Modified", "LabelFileModified": "File Modified",
@ -256,6 +281,7 @@
"LabelFinished": "Finished", "LabelFinished": "Finished",
"LabelFolder": "Folder", "LabelFolder": "Folder",
"LabelFolders": "Folderi", "LabelFolders": "Folderi",
"LabelFontFamily": "Font family",
"LabelFontScale": "Font scale", "LabelFontScale": "Font scale",
"LabelFormat": "Format", "LabelFormat": "Format",
"LabelGenre": "Genre", "LabelGenre": "Genre",
@ -263,9 +289,11 @@
"LabelHardDeleteFile": "Obriši datoteku zauvijek", "LabelHardDeleteFile": "Obriši datoteku zauvijek",
"LabelHasEbook": "Has ebook", "LabelHasEbook": "Has ebook",
"LabelHasSupplementaryEbook": "Has supplementary ebook", "LabelHasSupplementaryEbook": "Has supplementary ebook",
"LabelHighestPriority": "Highest priority",
"LabelHost": "Host", "LabelHost": "Host",
"LabelHour": "Sat", "LabelHour": "Sat",
"LabelIcon": "Ikona", "LabelIcon": "Ikona",
"LabelImageURLFromTheWeb": "Image URL from the web",
"LabelIncludeInTracklist": "Dodaj u Tracklist", "LabelIncludeInTracklist": "Dodaj u Tracklist",
"LabelIncomplete": "Nepotpuno", "LabelIncomplete": "Nepotpuno",
"LabelInProgress": "U tijeku", "LabelInProgress": "U tijeku",
@ -303,14 +331,20 @@
"LabelLogLevelInfo": "Info", "LabelLogLevelInfo": "Info",
"LabelLogLevelWarn": "Warn", "LabelLogLevelWarn": "Warn",
"LabelLookForNewEpisodesAfterDate": "Traži nove epizode nakon ovog datuma", "LabelLookForNewEpisodesAfterDate": "Traži nove epizode nakon ovog datuma",
"LabelLowestPriority": "Lowest Priority",
"LabelMatchExistingUsersBy": "Match existing users by",
"LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider",
"LabelMediaPlayer": "Media Player", "LabelMediaPlayer": "Media Player",
"LabelMediaType": "Media Type", "LabelMediaType": "Media Type",
"LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources",
"LabelMetadataProvider": "Poslužitelj metapodataka ", "LabelMetadataProvider": "Poslužitelj metapodataka ",
"LabelMetaTag": "Meta Tag", "LabelMetaTag": "Meta Tag",
"LabelMetaTags": "Meta Tags", "LabelMetaTags": "Meta Tags",
"LabelMinute": "Minuta", "LabelMinute": "Minuta",
"LabelMissing": "Nedostaje", "LabelMissing": "Nedostaje",
"LabelMissingParts": "Nedostajali dijelovi", "LabelMissingParts": "Nedostajali dijelovi",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
"LabelMore": "Više", "LabelMore": "Više",
"LabelMoreInfo": "More Info", "LabelMoreInfo": "More Info",
"LabelName": "Ime", "LabelName": "Ime",
@ -372,6 +406,7 @@
"LabelRegion": "Regija", "LabelRegion": "Regija",
"LabelReleaseDate": "Datum izlaska", "LabelReleaseDate": "Datum izlaska",
"LabelRemoveCover": "Remove cover", "LabelRemoveCover": "Remove cover",
"LabelRowsPerPage": "Rows per page",
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email", "LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
"LabelRSSFeedCustomOwnerName": "Custom owner Name", "LabelRSSFeedCustomOwnerName": "Custom owner Name",
"LabelRSSFeedOpen": "RSS Feed Open", "LabelRSSFeedOpen": "RSS Feed Open",
@ -384,6 +419,7 @@
"LabelSeason": "Sezona", "LabelSeason": "Sezona",
"LabelSelectAllEpisodes": "Select all episodes", "LabelSelectAllEpisodes": "Select all episodes",
"LabelSelectEpisodesShowing": "Select {0} episodes showing", "LabelSelectEpisodesShowing": "Select {0} episodes showing",
"LabelSelectUsers": "Select users",
"LabelSendEbookToDevice": "Send Ebook to...", "LabelSendEbookToDevice": "Send Ebook to...",
"LabelSequence": "Sekvenca", "LabelSequence": "Sekvenca",
"LabelSeries": "Serije", "LabelSeries": "Serije",
@ -410,16 +446,10 @@
"LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.", "LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.",
"LabelSettingsHomePageBookshelfView": "Koristi bookshelf pogled za početnu stranicu", "LabelSettingsHomePageBookshelfView": "Koristi bookshelf pogled za početnu stranicu",
"LabelSettingsLibraryBookshelfView": "Koristi bookshelf pogled za biblioteku", "LabelSettingsLibraryBookshelfView": "Koristi bookshelf pogled za biblioteku",
"LabelSettingsOverdriveMediaMarkers": "Koristi Overdrive Media Markers za poglavlja",
"LabelSettingsOverdriveMediaMarkersHelp": "MP3 datoteke iz Overdriva dolaze sa vremenima od poglavlja embedani kao posebni metapodatci. Ova postavka će koristiti tagove za vremena od poglavlja automatski.",
"LabelSettingsParseSubtitles": "Parsaj podnapise", "LabelSettingsParseSubtitles": "Parsaj podnapise",
"LabelSettingsParseSubtitlesHelp": "Izvadi podnapise iz imena od audiobook foldera.<br>Podnapis mora biti odvojen sa \" - \"<br>npr. \"Ime knjige - Podnapis ovdje\" ima podnapis \"Podnapis ovdje\"", "LabelSettingsParseSubtitlesHelp": "Izvadi podnapise iz imena od audiobook foldera.<br>Podnapis mora biti odvojen sa \" - \"<br>npr. \"Ime knjige - Podnapis ovdje\" ima podnapis \"Podnapis ovdje\"",
"LabelSettingsPreferAudioMetadata": "Preferiraj audio metapodatke",
"LabelSettingsPreferAudioMetadataHelp": "Audio file ID3 meta tagovi u audio datoteci će biti korišteni za detalje knjige.",
"LabelSettingsPreferMatchedMetadata": "Preferiraj matchane metapodatke", "LabelSettingsPreferMatchedMetadata": "Preferiraj matchane metapodatke",
"LabelSettingsPreferMatchedMetadataHelp": "Matchani podatci će biti korišteni kada se koristi Quick Match. Po defaultu Quick Match će ispuniti samo prazne detalje.", "LabelSettingsPreferMatchedMetadataHelp": "Matchani podatci će biti korišteni kada se koristi Quick Match. Po defaultu Quick Match će ispuniti samo prazne detalje.",
"LabelSettingsPreferOPFMetadata": "Preferiraj OPF metapodatke",
"LabelSettingsPreferOPFMetadataHelp": "OPF metapodatci datoteke će biti korišteni za detalje knjige.",
"LabelSettingsSkipMatchingBooksWithASIN": "Preskoči matchanje knjiga koje već imaju ASIN", "LabelSettingsSkipMatchingBooksWithASIN": "Preskoči matchanje knjiga koje već imaju ASIN",
"LabelSettingsSkipMatchingBooksWithISBN": "SPreskoči matchanje knjiga koje već imaju ISBN", "LabelSettingsSkipMatchingBooksWithISBN": "SPreskoči matchanje knjiga koje već imaju ISBN",
"LabelSettingsSortingIgnorePrefixes": "Zanemari prefikse tokom sortiranja", "LabelSettingsSortingIgnorePrefixes": "Zanemari prefikse tokom sortiranja",
@ -429,7 +459,7 @@
"LabelSettingsStoreCoversWithItem": "Spremi cover uz stakvu", "LabelSettingsStoreCoversWithItem": "Spremi cover uz stakvu",
"LabelSettingsStoreCoversWithItemHelp": "By default covers are stored in /metadata/items, enabling this setting will store covers in your library item folder. Only one file named \"cover\" will be kept", "LabelSettingsStoreCoversWithItemHelp": "By default covers are stored in /metadata/items, enabling this setting will store covers in your library item folder. Only one file named \"cover\" will be kept",
"LabelSettingsStoreMetadataWithItem": "Spremi metapodatke uz stavku", "LabelSettingsStoreMetadataWithItem": "Spremi metapodatke uz stavku",
"LabelSettingsStoreMetadataWithItemHelp": "Po defaultu metapodatci su spremljeni u /metadata/items, uključujućite li ovu postavku, metapodatci će biti spremljeni u folderima od biblioteke. Koristi .abs ekstenziju.", "LabelSettingsStoreMetadataWithItemHelp": "Po defaultu metapodatci su spremljeni u /metadata/items, uključujućite li ovu postavku, metapodatci će biti spremljeni u folderima od biblioteke",
"LabelSettingsTimeFormat": "Time Format", "LabelSettingsTimeFormat": "Time Format",
"LabelShowAll": "Prikaži sve", "LabelShowAll": "Prikaži sve",
"LabelSize": "Veličina", "LabelSize": "Veličina",
@ -494,6 +524,7 @@
"LabelUpdateDetailsHelp": "Dozvoli postavljanje novih detalja za odabrane knjige nakon što je match pronađen", "LabelUpdateDetailsHelp": "Dozvoli postavljanje novih detalja za odabrane knjige nakon što je match pronađen",
"LabelUploaderDragAndDrop": "Drag & Drop datoteke ili foldere", "LabelUploaderDragAndDrop": "Drag & Drop datoteke ili foldere",
"LabelUploaderDropFiles": "Ubaci datoteke", "LabelUploaderDropFiles": "Ubaci datoteke",
"LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
"LabelUseChapterTrack": "Koristi poglavlja track", "LabelUseChapterTrack": "Koristi poglavlja track",
"LabelUseFullTrack": "Koristi cijeli track", "LabelUseFullTrack": "Koristi cijeli track",
"LabelUser": "Korisnik", "LabelUser": "Korisnik",
@ -527,17 +558,21 @@
"MessageConfirmDeleteBackup": "Jeste li sigurni da želite obrisati backup za {0}?", "MessageConfirmDeleteBackup": "Jeste li sigurni da želite obrisati backup za {0}?",
"MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?", "MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?",
"MessageConfirmDeleteLibrary": "Jeste li sigurni da želite trajno obrisati biblioteku \"{0}\"?", "MessageConfirmDeleteLibrary": "Jeste li sigurni da želite trajno obrisati biblioteku \"{0}\"?",
"MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?",
"MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?",
"MessageConfirmDeleteSession": "Jeste li sigurni da želite obrisati ovu sesiju?", "MessageConfirmDeleteSession": "Jeste li sigurni da želite obrisati ovu sesiju?",
"MessageConfirmForceReScan": "Jeste li sigurni da želite ponovno skenirati?", "MessageConfirmForceReScan": "Jeste li sigurni da želite ponovno skenirati?",
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?", "MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?", "MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
"MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?", "MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
"MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?", "MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
"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?",
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?", "MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
"MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?", "MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?",
"MessageConfirmRemoveCollection": "AJeste li sigurni da želite obrisati kolekciju \"{0}\"?", "MessageConfirmRemoveCollection": "AJeste li sigurni da želite obrisati kolekciju \"{0}\"?",
"MessageConfirmRemoveEpisode": "Jeste li sigurni da želite obrisati epizodu \"{0}\"?", "MessageConfirmRemoveEpisode": "Jeste li sigurni da želite obrisati epizodu \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Jeste li sigurni da želite obrisati {0} epizoda/-u?", "MessageConfirmRemoveEpisodes": "Jeste li sigurni da želite obrisati {0} epizoda/-u?",
"MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?", "MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?", "MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
"MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?", "MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?",
@ -546,6 +581,7 @@
"MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?", "MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
"MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.", "MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
"MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".", "MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
"MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?",
"MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?", "MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
"MessageDownloadingEpisode": "Preuzimam epizodu", "MessageDownloadingEpisode": "Preuzimam epizodu",
"MessageDragFilesIntoTrackOrder": "Povuci datoteke u pravilan redoslijed tracka.", "MessageDragFilesIntoTrackOrder": "Povuci datoteke u pravilan redoslijed tracka.",
@ -616,6 +652,7 @@
"MessageRestoreBackupConfirm": "Jeste li sigurni da želite povratiti backup kreiran", "MessageRestoreBackupConfirm": "Jeste li sigurni da želite povratiti backup kreiran",
"MessageRestoreBackupWarning": "Povračanje backupa će zamijeniti postoječu bazu podataka u /config i slike covera u /metadata/items i /metadata/authors.<br /><br />Backups ne modificiraju nikakve datoteke u folderu od biblioteke. Ako imate uključene server postavke da spremate cover i metapodtake u folderu od biblioteke, onda oni neće biti backupani ili overwritten.<br /><br />Svi klijenti koji koriste tvoj server će biti automatski osvježeni.", "MessageRestoreBackupWarning": "Povračanje backupa će zamijeniti postoječu bazu podataka u /config i slike covera u /metadata/items i /metadata/authors.<br /><br />Backups ne modificiraju nikakve datoteke u folderu od biblioteke. Ako imate uključene server postavke da spremate cover i metapodtake u folderu od biblioteke, onda oni neće biti backupani ili overwritten.<br /><br />Svi klijenti koji koriste tvoj server će biti automatski osvježeni.",
"MessageSearchResultsFor": "Traži rezultate za", "MessageSearchResultsFor": "Traži rezultate za",
"MessageSelected": "{0} selected",
"MessageServerCouldNotBeReached": "Server ne može biti kontaktiran", "MessageServerCouldNotBeReached": "Server ne može biti kontaktiran",
"MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name", "MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name",
"MessageStartPlaybackAtTime": "Pokreni reprodukciju za \"{0}\" na {1}?", "MessageStartPlaybackAtTime": "Pokreni reprodukciju za \"{0}\" na {1}?",

View File

@ -1,7 +1,10 @@
{ {
"ButtonAdd": "Aggiungi", "ButtonAdd": "Aggiungi",
"ButtonAddChapters": "Aggiungi Capitoli", "ButtonAddChapters": "Aggiungi Capitoli",
"ButtonAddDevice": "Aggiungi Dispositivo",
"ButtonAddLibrary": "Aggiungi Libreria",
"ButtonAddPodcasts": "Aggiungi Podcast", "ButtonAddPodcasts": "Aggiungi Podcast",
"ButtonAddUser": "Aggiungi User",
"ButtonAddYourFirstLibrary": "Aggiungi la tua prima libreria", "ButtonAddYourFirstLibrary": "Aggiungi la tua prima libreria",
"ButtonApply": "Applica", "ButtonApply": "Applica",
"ButtonApplyChapters": "Applica", "ButtonApplyChapters": "Applica",
@ -59,6 +62,7 @@
"ButtonRemoveSeriesFromContinueSeries": "Rimuovi la Serie per Continuarla", "ButtonRemoveSeriesFromContinueSeries": "Rimuovi la Serie per Continuarla",
"ButtonReScan": "Ri-scansiona", "ButtonReScan": "Ri-scansiona",
"ButtonReset": "Reset", "ButtonReset": "Reset",
"ButtonResetToDefault": "Ripristino di default",
"ButtonRestore": "Ripristina", "ButtonRestore": "Ripristina",
"ButtonSave": "Salva", "ButtonSave": "Salva",
"ButtonSaveAndClose": "Salva & Chiudi", "ButtonSaveAndClose": "Salva & Chiudi",
@ -71,7 +75,7 @@
"ButtonSetChaptersFromTracks": "Impostare i capitoli dalle tracce", "ButtonSetChaptersFromTracks": "Impostare i capitoli dalle tracce",
"ButtonShiftTimes": "Ricerca veloce", "ButtonShiftTimes": "Ricerca veloce",
"ButtonShow": "Mostra", "ButtonShow": "Mostra",
"ButtonStartM4BEncode": "Inizia L'Encoda del M4B", "ButtonStartM4BEncode": "Inizia L'Encode del M4B",
"ButtonStartMetadataEmbed": "Inizia Incorporo Metadata", "ButtonStartMetadataEmbed": "Inizia Incorporo Metadata",
"ButtonSubmit": "Invia", "ButtonSubmit": "Invia",
"ButtonTest": "Test", "ButtonTest": "Test",
@ -83,11 +87,15 @@
"ButtonUserEdit": "Modifica Utente {0}", "ButtonUserEdit": "Modifica Utente {0}",
"ButtonViewAll": "Mostra Tutto", "ButtonViewAll": "Mostra Tutto",
"ButtonYes": "Si", "ButtonYes": "Si",
"ErrorUploadFetchMetadataAPI": "Error fetching metadata",
"ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
"ErrorUploadLacksTitle": "Must have a title",
"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",
"HeaderBackups": "Backup", "HeaderBackups": "Backup",
"HeaderChangePassword": "Cambia Password", "HeaderChangePassword": "Cambia Password",
"HeaderChapters": "Capitoli", "HeaderChapters": "Capitoli",
@ -98,7 +106,7 @@
"HeaderCurrentDownloads": "Download Correnti", "HeaderCurrentDownloads": "Download Correnti",
"HeaderDetails": "Dettagli", "HeaderDetails": "Dettagli",
"HeaderDownloadQueue": "Download Queue", "HeaderDownloadQueue": "Download Queue",
"HeaderEbookFiles": "Ebook Files", "HeaderEbookFiles": "Ebook File",
"HeaderEmail": "Email", "HeaderEmail": "Email",
"HeaderEmailSettings": "Email Settings", "HeaderEmailSettings": "Email Settings",
"HeaderEpisodes": "Episodi", "HeaderEpisodes": "Episodi",
@ -122,12 +130,15 @@
"HeaderManageTags": "Gestisci Tags", "HeaderManageTags": "Gestisci Tags",
"HeaderMapDetails": "Mappa Dettagli", "HeaderMapDetails": "Mappa Dettagli",
"HeaderMatch": "Trova Corrispondenza", "HeaderMatch": "Trova Corrispondenza",
"HeaderMetadataOrderOfPrecedence": "Metadata order of precedence",
"HeaderMetadataToEmbed": "Metadata da incorporare", "HeaderMetadataToEmbed": "Metadata da incorporare",
"HeaderNewAccount": "Nuovo Account", "HeaderNewAccount": "Nuovo Account",
"HeaderNewLibrary": "Nuova Libreria", "HeaderNewLibrary": "Nuova Libreria",
"HeaderNotifications": "Notifiche", "HeaderNotifications": "Notifiche",
"HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication",
"HeaderOpenRSSFeed": "Apri RSS Feed", "HeaderOpenRSSFeed": "Apri RSS Feed",
"HeaderOtherFiles": "Altri File", "HeaderOtherFiles": "Altri File",
"HeaderPasswordAuthentication": "Password Authentication",
"HeaderPermissions": "Permessi", "HeaderPermissions": "Permessi",
"HeaderPlayerQueue": "Coda Riproduzione", "HeaderPlayerQueue": "Coda Riproduzione",
"HeaderPlaylist": "Playlist", "HeaderPlaylist": "Playlist",
@ -156,7 +167,7 @@
"HeaderStatsRecentSessions": "Sessioni Recenti", "HeaderStatsRecentSessions": "Sessioni Recenti",
"HeaderStatsTop10Authors": "Top 10 Autori", "HeaderStatsTop10Authors": "Top 10 Autori",
"HeaderStatsTop5Genres": "Top 5 Generi", "HeaderStatsTop5Genres": "Top 5 Generi",
"HeaderTableOfContents": "Tabellla dei Contenuti", "HeaderTableOfContents": "Tabella dei Contenuti",
"HeaderTools": "Strumenti", "HeaderTools": "Strumenti",
"HeaderUpdateAccount": "Aggiorna Account", "HeaderUpdateAccount": "Aggiorna Account",
"HeaderUpdateAuthor": "Aggiorna Autore", "HeaderUpdateAuthor": "Aggiorna Autore",
@ -176,8 +187,11 @@
"LabelAddToCollectionBatch": "Aggiungi {0} Libri alla Raccolta", "LabelAddToCollectionBatch": "Aggiungi {0} Libri alla Raccolta",
"LabelAddToPlaylist": "aggiungi alla Playlist", "LabelAddToPlaylist": "aggiungi alla Playlist",
"LabelAddToPlaylistBatch": "Aggiungi {0} file alla Playlist", "LabelAddToPlaylistBatch": "Aggiungi {0} file alla Playlist",
"LabelAdminUsersOnly": "Solo utenti Amministratori",
"LabelAll": "Tutti", "LabelAll": "Tutti",
"LabelAllUsers": "Tutti gli Utenti", "LabelAllUsers": "Tutti gli Utenti",
"LabelAllUsersExcludingGuests": "Tutti gli Utenti Esclusi gli ospiti",
"LabelAllUsersIncludingGuests": "Tutti gli Utenti Inclusi gli ospiti",
"LabelAlreadyInYourLibrary": "Già esistente nella libreria", "LabelAlreadyInYourLibrary": "Già esistente nella libreria",
"LabelAppend": "Appese", "LabelAppend": "Appese",
"LabelAuthor": "Autore", "LabelAuthor": "Autore",
@ -185,8 +199,14 @@
"LabelAuthorLastFirst": "Autori (Per Cognome)", "LabelAuthorLastFirst": "Autori (Per Cognome)",
"LabelAuthors": "Autori", "LabelAuthors": "Autori",
"LabelAutoDownloadEpisodes": "Auto Download Episodi", "LabelAutoDownloadEpisodes": "Auto Download Episodi",
"LabelAutoFetchMetadata": "Auto Fetch Metadata",
"LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
"LabelAutoLaunch": "Auto Launch",
"LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Auto Register",
"LabelAutoRegisterDescription": "Automatically create new users after logging in",
"LabelBackToUser": "Torna a Utenti", "LabelBackToUser": "Torna a Utenti",
"LabelBackupLocation": "Backup Location", "LabelBackupLocation": "Percorso del Backup",
"LabelBackupsEnableAutomaticBackups": "Abilita backup Automatico", "LabelBackupsEnableAutomaticBackups": "Abilita backup Automatico",
"LabelBackupsEnableAutomaticBackupsHelp": "I Backup saranno salvati in /metadata/backups", "LabelBackupsEnableAutomaticBackupsHelp": "I Backup saranno salvati in /metadata/backups",
"LabelBackupsMaxBackupSize": "Dimensione massima backup (in GB)", "LabelBackupsMaxBackupSize": "Dimensione massima backup (in GB)",
@ -195,15 +215,17 @@
"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",
"LabelChangePassword": "Cambia Password", "LabelChangePassword": "Cambia Password",
"LabelChannels": "Canali", "LabelChannels": "Canali",
"LabelChapters": "Capitoli", "LabelChapters": "Capitoli",
"LabelChaptersFound": "Capitoli Trovati", "LabelChaptersFound": "Capitoli Trovati",
"LabelChapterTitle": "Titoli dei Capitoli", "LabelChapterTitle": "Titoli dei Capitoli",
"LabelClickForMoreInfo": "Click per altre Info",
"LabelClosePlayer": "Chiudi player", "LabelClosePlayer": "Chiudi player",
"LabelCodec": "Codec", "LabelCodec": "Codec",
"LabelCollapseSeries": "Comprimi Serie", "LabelCollapseSeries": "Comprimi Serie",
"LabelCollection": "Collection", "LabelCollection": "Raccolta",
"LabelCollections": "Raccolte", "LabelCollections": "Raccolte",
"LabelComplete": "Completo", "LabelComplete": "Completo",
"LabelConfirmPassword": "Conferma Password", "LabelConfirmPassword": "Conferma Password",
@ -211,21 +233,23 @@
"LabelContinueReading": "Continua la Lettura", "LabelContinueReading": "Continua la Lettura",
"LabelContinueSeries": "Continua Serie", "LabelContinueSeries": "Continua Serie",
"LabelCover": "Cover", "LabelCover": "Cover",
"LabelCoverImageURL": "Cover Image URL", "LabelCoverImageURL": "Indirizzo della cover URL",
"LabelCreatedAt": "Creato A", "LabelCreatedAt": "Creato A",
"LabelCronExpression": "Espressione Cron", "LabelCronExpression": "Espressione Cron",
"LabelCurrent": "Attuale", "LabelCurrent": "Attuale",
"LabelCurrently": "Attualmente:", "LabelCurrently": "Attualmente:",
"LabelCustomCronExpression": "Custom Cron Expression:", "LabelCustomCronExpression": "Espressione Cron personalizzata:",
"LabelDatetime": "Data & Ora", "LabelDatetime": "Data & Ora",
"LabelDeleteFromFileSystemCheckbox": "Elimina dal file system (togli la spunta per eliminarla solo dal DB)",
"LabelDescription": "Descrizione", "LabelDescription": "Descrizione",
"LabelDeselectAll": "Deseleziona Tutto", "LabelDeselectAll": "Deseleziona Tutto",
"LabelDevice": "Dispositivo", "LabelDevice": "Dispositivo",
"LabelDeviceInfo": "Info Dispositivo", "LabelDeviceInfo": "Info Dispositivo",
"LabelDeviceIsAvailableTo": "Il dispositivo e disponibile su...",
"LabelDirectory": "Elenco", "LabelDirectory": "Elenco",
"LabelDiscFromFilename": "Disco dal nome file", "LabelDiscFromFilename": "Disco dal nome file",
"LabelDiscFromMetadata": "Disco dal Metadata", "LabelDiscFromMetadata": "Disco dal Metadata",
"LabelDiscover": "Discover", "LabelDiscover": "Scopri",
"LabelDownload": "Download", "LabelDownload": "Download",
"LabelDownloadNEpisodes": "Download {0} episodes", "LabelDownloadNEpisodes": "Download {0} episodes",
"LabelDuration": "Durata", "LabelDuration": "Durata",
@ -247,6 +271,7 @@
"LabelExample": "Esempio", "LabelExample": "Esempio",
"LabelExplicit": "Esplicito", "LabelExplicit": "Esplicito",
"LabelFeedURL": "Feed URL", "LabelFeedURL": "Feed URL",
"LabelFetchingMetadata": "Fetching Metadata",
"LabelFile": "File", "LabelFile": "File",
"LabelFileBirthtime": "Data Creazione", "LabelFileBirthtime": "Data Creazione",
"LabelFileModified": "Ultima modifica", "LabelFileModified": "Ultima modifica",
@ -256,6 +281,7 @@
"LabelFinished": "Finita", "LabelFinished": "Finita",
"LabelFolder": "Cartella", "LabelFolder": "Cartella",
"LabelFolders": "Cartelle", "LabelFolders": "Cartelle",
"LabelFontFamily": "Font family",
"LabelFontScale": "Dimensione Font", "LabelFontScale": "Dimensione Font",
"LabelFormat": "Formato", "LabelFormat": "Formato",
"LabelGenre": "Genere", "LabelGenre": "Genere",
@ -263,9 +289,11 @@
"LabelHardDeleteFile": "Elimina Definitivamente", "LabelHardDeleteFile": "Elimina Definitivamente",
"LabelHasEbook": "Un ebook", "LabelHasEbook": "Un ebook",
"LabelHasSupplementaryEbook": "Un ebook Supplementare", "LabelHasSupplementaryEbook": "Un ebook Supplementare",
"LabelHighestPriority": "Highest priority",
"LabelHost": "Host", "LabelHost": "Host",
"LabelHour": "Ora", "LabelHour": "Ora",
"LabelIcon": "Icona", "LabelIcon": "Icona",
"LabelImageURLFromTheWeb": "Immagine URL da internet",
"LabelIncludeInTracklist": "Includi nella Tracklist", "LabelIncludeInTracklist": "Includi nella Tracklist",
"LabelIncomplete": "Incompleta", "LabelIncomplete": "Incompleta",
"LabelInProgress": "In Corso", "LabelInProgress": "In Corso",
@ -290,27 +318,33 @@
"LabelLastUpdate": "Ultimo Aggiornamento", "LabelLastUpdate": "Ultimo Aggiornamento",
"LabelLayout": "Layout", "LabelLayout": "Layout",
"LabelLayoutSinglePage": "Pagina Singola", "LabelLayoutSinglePage": "Pagina Singola",
"LabelLayoutSplitPage": "DIvidi Pagina", "LabelLayoutSplitPage": "Dividi Pagina",
"LabelLess": "Poco", "LabelLess": "Poco",
"LabelLibrariesAccessibleToUser": "Librerie Accessibili agli Utenti", "LabelLibrariesAccessibleToUser": "Librerie Accessibili agli Utenti",
"LabelLibrary": "Libreria", "LabelLibrary": "Libreria",
"LabelLibraryItem": "Elementi della Library", "LabelLibraryItem": "Elementi della Library",
"LabelLibraryName": "Nome Libreria", "LabelLibraryName": "Nome Libreria",
"LabelLimit": "Limiti", "LabelLimit": "Limiti",
"LabelLineSpacing": "Line spacing", "LabelLineSpacing": "Interlinea",
"LabelListenAgain": "Ri-ascolta", "LabelListenAgain": "Ri-ascolta",
"LabelLogLevelDebug": "Debug", "LabelLogLevelDebug": "Debug",
"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",
"LabelMatchExistingUsersBy": "Match existing users by",
"LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider",
"LabelMediaPlayer": "Media Player", "LabelMediaPlayer": "Media Player",
"LabelMediaType": "Tipo Media", "LabelMediaType": "Tipo Media",
"LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources",
"LabelMetadataProvider": "Metadata Provider", "LabelMetadataProvider": "Metadata Provider",
"LabelMetaTag": "Meta Tag", "LabelMetaTag": "Meta Tag",
"LabelMetaTags": "Meta Tags", "LabelMetaTags": "Meta Tags",
"LabelMinute": "Minuto", "LabelMinute": "Minuto",
"LabelMissing": "Altro", "LabelMissing": "Altro",
"LabelMissingParts": "Parti rimantenti", "LabelMissingParts": "Parti rimantenti",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
"LabelMore": "Molto", "LabelMore": "Molto",
"LabelMoreInfo": "Più Info", "LabelMoreInfo": "Più Info",
"LabelName": "Nome", "LabelName": "Nome",
@ -372,6 +406,7 @@
"LabelRegion": "Regione", "LabelRegion": "Regione",
"LabelReleaseDate": "Data Release", "LabelReleaseDate": "Data Release",
"LabelRemoveCover": "Rimuovi cover", "LabelRemoveCover": "Rimuovi cover",
"LabelRowsPerPage": "Rows per page",
"LabelRSSFeedCustomOwnerEmail": "Email del proprietario personalizzato", "LabelRSSFeedCustomOwnerEmail": "Email del proprietario personalizzato",
"LabelRSSFeedCustomOwnerName": "Nome del proprietario personalizzato", "LabelRSSFeedCustomOwnerName": "Nome del proprietario personalizzato",
"LabelRSSFeedOpen": "RSS Feed Aperto", "LabelRSSFeedOpen": "RSS Feed Aperto",
@ -384,6 +419,7 @@
"LabelSeason": "Stagione", "LabelSeason": "Stagione",
"LabelSelectAllEpisodes": "Seleziona tutti gli Episodi", "LabelSelectAllEpisodes": "Seleziona tutti gli Episodi",
"LabelSelectEpisodesShowing": "Episodi {0} selezionati ", "LabelSelectEpisodesShowing": "Episodi {0} selezionati ",
"LabelSelectUsers": "Selezione Utenti",
"LabelSendEbookToDevice": "Invia ebook a...", "LabelSendEbookToDevice": "Invia ebook a...",
"LabelSequence": "Sequenza", "LabelSequence": "Sequenza",
"LabelSeries": "Serie", "LabelSeries": "Serie",
@ -399,9 +435,9 @@
"LabelSettingsDisableWatcher": "Disattiva Watcher", "LabelSettingsDisableWatcher": "Disattiva Watcher",
"LabelSettingsDisableWatcherForLibrary": "Disattiva Watcher per le librerie", "LabelSettingsDisableWatcherForLibrary": "Disattiva Watcher per le librerie",
"LabelSettingsDisableWatcherHelp": "Disattiva il controllo automatico libri nelle cartelle delle librerie. *Richiede il Riavvio del Server", "LabelSettingsDisableWatcherHelp": "Disattiva il controllo automatico libri nelle cartelle delle librerie. *Richiede il Riavvio del Server",
"LabelSettingsEnableWatcher": "Enable Watcher", "LabelSettingsEnableWatcher": "Abilita Watcher",
"LabelSettingsEnableWatcherForLibrary": "Enable folder watcher for library", "LabelSettingsEnableWatcherForLibrary": "Abilita il controllo cartelle per la libreria",
"LabelSettingsEnableWatcherHelp": "Enables the automatic adding/updating of items when file changes are detected. *Requires server restart", "LabelSettingsEnableWatcherHelp": "Abilita l'aggiunta/aggiornamento automatico degli elementi quando vengono rilevate modifiche ai file. *Richiede il riavvio del Server",
"LabelSettingsExperimentalFeatures": "Opzioni Sperimentali", "LabelSettingsExperimentalFeatures": "Opzioni Sperimentali",
"LabelSettingsExperimentalFeaturesHelp": "Funzionalità in fase di sviluppo che potrebbero utilizzare i tuoi feedback e aiutare i test. Fare clic per aprire la discussione github.", "LabelSettingsExperimentalFeaturesHelp": "Funzionalità in fase di sviluppo che potrebbero utilizzare i tuoi feedback e aiutare i test. Fare clic per aprire la discussione github.",
"LabelSettingsFindCovers": "Trova covers", "LabelSettingsFindCovers": "Trova covers",
@ -410,16 +446,10 @@
"LabelSettingsHideSingleBookSeriesHelp": "Le serie che hanno un solo libro saranno nascoste dalla pagina della serie e dagli scaffali della home page.", "LabelSettingsHideSingleBookSeriesHelp": "Le serie che hanno un solo libro saranno nascoste dalla pagina della serie e dagli scaffali della home page.",
"LabelSettingsHomePageBookshelfView": "Home page con sfondo legno", "LabelSettingsHomePageBookshelfView": "Home page con sfondo legno",
"LabelSettingsLibraryBookshelfView": "Libreria con sfondo legno", "LabelSettingsLibraryBookshelfView": "Libreria con sfondo legno",
"LabelSettingsOverdriveMediaMarkers": "Usa Overdrive Media Markers per i capitoli",
"LabelSettingsOverdriveMediaMarkersHelp": "I file MP3 di Overdrive vengono forniti con i tempi dei capitoli incorporati come metadati personalizzati. Abilitando questa funzione verranno utilizzati automaticamente questi tag per i tempi dei capitoli",
"LabelSettingsParseSubtitles": "Analizza sottotitoli", "LabelSettingsParseSubtitles": "Analizza sottotitoli",
"LabelSettingsParseSubtitlesHelp": "Estrai i sottotitoli dai nomi delle cartelle degli audiolibri. <br> I sottotitoli devono essere separati da \" - \"<br> Per esempio \"Il signore degli anelli - Le due Torri \" avrà il sottotitolo \"Le due Torri\"", "LabelSettingsParseSubtitlesHelp": "Estrai i sottotitoli dai nomi delle cartelle degli audiolibri. <br> I sottotitoli devono essere separati da \" - \"<br> Per esempio \"Il signore degli anelli - Le due Torri \" avrà il sottotitolo \"Le due Torri\"",
"LabelSettingsPreferAudioMetadata": "Preferisci i metadati audio",
"LabelSettingsPreferAudioMetadataHelp": "I meta tag ID3 del file audio verrano preferiti rispetto al nome della cartella",
"LabelSettingsPreferMatchedMetadata": "Preferisci i metadata trovati", "LabelSettingsPreferMatchedMetadata": "Preferisci i metadata trovati",
"LabelSettingsPreferMatchedMetadataHelp": "I dati trovati in internet sovrascriveranno i dettagli del libro quando si utilizza quick Match. Per impostazione predefinita, Quick Match riempirà solo i dettagli mancanti.", "LabelSettingsPreferMatchedMetadataHelp": "I dati trovati in internet sovrascriveranno i dettagli del libro quando si utilizza quick Match. Per impostazione predefinita, Quick Match riempirà solo i dettagli mancanti.",
"LabelSettingsPreferOPFMetadata": "Preferisci OPF metadata",
"LabelSettingsPreferOPFMetadataHelp": "I metadati del file OPF verranno utilizzati per i dettagli del libro e non il nome della cartella",
"LabelSettingsSkipMatchingBooksWithASIN": "Salta la ricerca dati in internet se è già presente un codice ASIN", "LabelSettingsSkipMatchingBooksWithASIN": "Salta la ricerca dati in internet se è già presente un codice ASIN",
"LabelSettingsSkipMatchingBooksWithISBN": "Salta la ricerca dati in internet se è già presente un codice ISBN", "LabelSettingsSkipMatchingBooksWithISBN": "Salta la ricerca dati in internet se è già presente un codice ISBN",
"LabelSettingsSortingIgnorePrefixes": "Ignora i prefissi nei titoli durante l'aggiunta", "LabelSettingsSortingIgnorePrefixes": "Ignora i prefissi nei titoli durante l'aggiunta",
@ -429,7 +459,7 @@
"LabelSettingsStoreCoversWithItem": "Archivia le copertine con il file", "LabelSettingsStoreCoversWithItem": "Archivia le copertine con il file",
"LabelSettingsStoreCoversWithItemHelp": "Di default, le immagini di copertina sono salvate dentro /metadata/items, abilitando questa opzione le copertine saranno archiviate nella cartella della libreria corrispondente. Verrà conservato solo un file denominato \"cover\"", "LabelSettingsStoreCoversWithItemHelp": "Di default, le immagini di copertina sono salvate dentro /metadata/items, abilitando questa opzione le copertine saranno archiviate nella cartella della libreria corrispondente. Verrà conservato solo un file denominato \"cover\"",
"LabelSettingsStoreMetadataWithItem": "Archivia i metadata con il file", "LabelSettingsStoreMetadataWithItem": "Archivia i metadata con il file",
"LabelSettingsStoreMetadataWithItemHelp": "Di default, i metadati sono salvati dentro /metadata/items, abilitando questa opzione si memorizzeranno i metadata nella cartella della libreria. I file avranno estensione .abs", "LabelSettingsStoreMetadataWithItemHelp": "Di default, i metadati sono salvati dentro /metadata/items, abilitando questa opzione si memorizzeranno i metadata nella cartella della libreria",
"LabelSettingsTimeFormat": "Formato Ora", "LabelSettingsTimeFormat": "Formato Ora",
"LabelShowAll": "Mostra Tutto", "LabelShowAll": "Mostra Tutto",
"LabelSize": "Dimensione", "LabelSize": "Dimensione",
@ -462,8 +492,8 @@
"LabelTagsNotAccessibleToUser": "Tags non accessibile agli Utenti", "LabelTagsNotAccessibleToUser": "Tags non accessibile agli Utenti",
"LabelTasks": "Processi in esecuzione", "LabelTasks": "Processi in esecuzione",
"LabelTheme": "Tema", "LabelTheme": "Tema",
"LabelThemeDark": "Dark", "LabelThemeDark": "Scuro",
"LabelThemeLight": "Light", "LabelThemeLight": "Chiaro",
"LabelTimeBase": "Time Base", "LabelTimeBase": "Time Base",
"LabelTimeListened": "Tempo di Ascolto", "LabelTimeListened": "Tempo di Ascolto",
"LabelTimeListenedToday": "Tempo di Ascolto Oggi", "LabelTimeListenedToday": "Tempo di Ascolto Oggi",
@ -494,6 +524,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",
"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",
@ -523,21 +554,25 @@
"MessageChapterErrorStartLtPrev": "L'ora di inizio non valida deve essere maggiore o uguale all'ora di inizio del capitolo precedente", "MessageChapterErrorStartLtPrev": "L'ora di inizio non valida deve essere maggiore o uguale all'ora di inizio del capitolo precedente",
"MessageChapterStartIsAfter": "L'inizio del capitolo è dopo la fine del tuo audiolibro", "MessageChapterStartIsAfter": "L'inizio del capitolo è dopo la fine del tuo audiolibro",
"MessageCheckingCron": "Controllo cron...", "MessageCheckingCron": "Controllo cron...",
"MessageConfirmCloseFeed": "Are you sure you want to close this feed?", "MessageConfirmCloseFeed": "Sei sicuro di voler chiudere questo feed?",
"MessageConfirmDeleteBackup": "Sei sicuro di voler eliminare il backup {0}?", "MessageConfirmDeleteBackup": "Sei sicuro di voler eliminare il backup {0}?",
"MessageConfirmDeleteFile": "Questo eliminerà il file dal tuo file system. Sei sicuro?", "MessageConfirmDeleteFile": "Questo eliminerà il file dal tuo file system. Sei sicuro?",
"MessageConfirmDeleteLibrary": "Sei sicuro di voler eliminare definitivamente la libreria \"{0}\"?", "MessageConfirmDeleteLibrary": "Sei sicuro di voler eliminare definitivamente la libreria \"{0}\"?",
"MessageConfirmDeleteLibraryItem": " l'elemento della libreria dal database e dal file system. Sei sicuro?",
"MessageConfirmDeleteLibraryItems": "Ciò eliminerà {0} elementi della libreria dal database e dal file system. Sei sicuro?",
"MessageConfirmDeleteSession": "Sei sicuro di voler eliminare questa sessione?", "MessageConfirmDeleteSession": "Sei sicuro di voler eliminare questa sessione?",
"MessageConfirmForceReScan": "Sei sicuro di voler forzare una nuova scansione?", "MessageConfirmForceReScan": "Sei sicuro di voler forzare una nuova scansione?",
"MessageConfirmMarkAllEpisodesFinished": "Sei sicuro di voler contrassegnare tutti gli episodi come finiti?", "MessageConfirmMarkAllEpisodesFinished": "Sei sicuro di voler contrassegnare tutti gli episodi come finiti?",
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?", "MessageConfirmMarkAllEpisodesNotFinished": "Sei sicuro di voler contrassegnare tutti gli episodi come non completati?",
"MessageConfirmMarkSeriesFinished": "Sei sicuro di voler contrassegnare tutti i libri di questa serie come completati?", "MessageConfirmMarkSeriesFinished": "Sei sicuro di voler contrassegnare tutti i libri di questa serie come completati?",
"MessageConfirmMarkSeriesNotFinished": "Sei sicuro di voler contrassegnare tutti i libri di questa serie come non completati?", "MessageConfirmMarkSeriesNotFinished": "Sei sicuro di voler contrassegnare tutti i libri di questa serie come non completati?",
"MessageConfirmQuickEmbed": "Attenzione! L'incorporamento rapido non eseguirà il backup dei file audio. Assicurati di avere un backup dei tuoi file audio. <br><br>Vuoi Continuare?",
"MessageConfirmRemoveAllChapters": "Sei sicuro di voler rimuovere tutti i capitoli?", "MessageConfirmRemoveAllChapters": "Sei sicuro di voler rimuovere tutti i capitoli?",
"MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?", "MessageConfirmRemoveAuthor": "Sei sicuro di voler rimuovere l'autore? \"{0}\"?",
"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?",
"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?",
@ -546,6 +581,7 @@
"MessageConfirmRenameTag": "Sei sicuro di voler rinominare il tag \"{0}\" in \"{1}\" per tutti gli oggetti?", "MessageConfirmRenameTag": "Sei sicuro di voler rinominare il tag \"{0}\" in \"{1}\" per tutti gli oggetti?",
"MessageConfirmRenameTagMergeNote": "Nota: Questo tag esiste già e verrà unito nel vecchio.", "MessageConfirmRenameTagMergeNote": "Nota: Questo tag esiste già e verrà unito nel vecchio.",
"MessageConfirmRenameTagWarning": "Avvertimento! Esiste già un tag simile con un nome simile \"{0}\".", "MessageConfirmRenameTagWarning": "Avvertimento! Esiste già un tag simile con un nome simile \"{0}\".",
"MessageConfirmReScanLibraryItems": "Sei sicuro di voler ripetere la scansione? {0} oggetti?",
"MessageConfirmSendEbookToDevice": "Sei sicuro di voler inviare {0} ebook \"{1}\" al Device \"{2}\"?", "MessageConfirmSendEbookToDevice": "Sei sicuro di voler inviare {0} ebook \"{1}\" al Device \"{2}\"?",
"MessageDownloadingEpisode": "Download episodio in corso", "MessageDownloadingEpisode": "Download episodio in corso",
"MessageDragFilesIntoTrackOrder": "Trascina i file nell'ordine di traccia corretto", "MessageDragFilesIntoTrackOrder": "Trascina i file nell'ordine di traccia corretto",
@ -595,7 +631,7 @@
"MessageNoResults": "Nessun Risultato", "MessageNoResults": "Nessun Risultato",
"MessageNoSearchResultsFor": "Nessun risultato per \"{0}\"", "MessageNoSearchResultsFor": "Nessun risultato per \"{0}\"",
"MessageNoSeries": "Nessuna Serie", "MessageNoSeries": "Nessuna Serie",
"MessageNoTags": "No Tags", "MessageNoTags": "Nessun Tags",
"MessageNoTasksRunning": "Nessun processo in esecuzione", "MessageNoTasksRunning": "Nessun processo in esecuzione",
"MessageNotYetImplemented": "Non Ancora Implementato", "MessageNotYetImplemented": "Non Ancora Implementato",
"MessageNoUpdateNecessary": "Nessun aggiornamento necessario", "MessageNoUpdateNecessary": "Nessun aggiornamento necessario",
@ -616,6 +652,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",
"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}?",
@ -624,7 +661,7 @@
"MessageUploaderItemSuccess": "Caricato con successo!", "MessageUploaderItemSuccess": "Caricato con successo!",
"MessageUploading": "Caricamento...", "MessageUploading": "Caricamento...",
"MessageValidCronExpression": "Espressione Cron Valida", "MessageValidCronExpression": "Espressione Cron Valida",
"MessageWatcherIsDisabledGlobally": "Watcher è disabilitato a livello globale nelle impostazioni del server", "MessageWatcherIsDisabledGlobally": "Controllo file automatico è disabilitato a livello globale nelle impostazioni del server",
"MessageXLibraryIsEmpty": "{0} libreria vuota!", "MessageXLibraryIsEmpty": "{0} libreria vuota!",
"MessageYourAudiobookDurationIsLonger": "La durata dell'audiolibro è più lunga della durata trovata", "MessageYourAudiobookDurationIsLonger": "La durata dell'audiolibro è più lunga della durata trovata",
"MessageYourAudiobookDurationIsShorter": "La durata dell'audiolibro è inferiore alla durata trovata", "MessageYourAudiobookDurationIsShorter": "La durata dell'audiolibro è inferiore alla durata trovata",
@ -638,7 +675,7 @@
"NoteUploaderOnlyAudioFiles": "Se carichi solo file audio, ogni file audio verrà gestito come un audiolibro separato.", "NoteUploaderOnlyAudioFiles": "Se carichi solo file audio, ogni file audio verrà gestito come un audiolibro separato.",
"NoteUploaderUnsupportedFiles": "I file non supportati vengono ignorati. Quando si sceglie o si elimina una cartella, gli altri file che non si trovano in una cartella di elementi vengono ignorati.", "NoteUploaderUnsupportedFiles": "I file non supportati vengono ignorati. Quando si sceglie o si elimina una cartella, gli altri file che non si trovano in una cartella di elementi vengono ignorati.",
"PlaceholderNewCollection": "Nome Nuova Raccolta", "PlaceholderNewCollection": "Nome Nuova Raccolta",
"PlaceholderNewFolderPath": "Nuovo percorso Cartella", "PlaceholderNewFolderPath": "Nuovo Percorso Cartella",
"PlaceholderNewPlaylist": "Nome nuova playlist", "PlaceholderNewPlaylist": "Nome nuova playlist",
"PlaceholderSearch": "Cerca..", "PlaceholderSearch": "Cerca..",
"PlaceholderSearchEpisode": "Cerca Episodio..", "PlaceholderSearchEpisode": "Cerca Episodio..",
@ -704,7 +741,7 @@
"ToastRSSFeedCloseSuccess": "RSS feed chiuso", "ToastRSSFeedCloseSuccess": "RSS feed chiuso",
"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": "Aggiornaento Serie Fallito", "ToastSeriesUpdateFailed": "Aggiornamento Serie Fallito",
"ToastSeriesUpdateSuccess": "Serie Aggornate", "ToastSeriesUpdateSuccess": "Serie Aggornate",
"ToastSessionDeleteFailed": "Errore eliminazione sessione", "ToastSessionDeleteFailed": "Errore eliminazione sessione",
"ToastSessionDeleteSuccess": "Sessione cancellata", "ToastSessionDeleteSuccess": "Sessione cancellata",

View File

@ -1,7 +1,10 @@
{ {
"ButtonAdd": "Pridėti", "ButtonAdd": "Pridėti",
"ButtonAddChapters": "Pridėti skyrius", "ButtonAddChapters": "Pridėti skyrius",
"ButtonAddDevice": "Add Device",
"ButtonAddLibrary": "Add Library",
"ButtonAddPodcasts": "Pridėti tinklalaides", "ButtonAddPodcasts": "Pridėti tinklalaides",
"ButtonAddUser": "Add User",
"ButtonAddYourFirstLibrary": "Pridėkite savo pirmąją biblioteką", "ButtonAddYourFirstLibrary": "Pridėkite savo pirmąją biblioteką",
"ButtonApply": "Taikyti", "ButtonApply": "Taikyti",
"ButtonApplyChapters": "Taikyti skyrius", "ButtonApplyChapters": "Taikyti skyrius",
@ -59,6 +62,7 @@
"ButtonRemoveSeriesFromContinueSeries": "Pašalinti seriją iš Tęsti Seriją", "ButtonRemoveSeriesFromContinueSeries": "Pašalinti seriją iš Tęsti Seriją",
"ButtonReScan": "Iš naujo nuskaityti", "ButtonReScan": "Iš naujo nuskaityti",
"ButtonReset": "Atstatyti", "ButtonReset": "Atstatyti",
"ButtonResetToDefault": "Reset to default",
"ButtonRestore": "Atkurti", "ButtonRestore": "Atkurti",
"ButtonSave": "Išsaugoti", "ButtonSave": "Išsaugoti",
"ButtonSaveAndClose": "Išsaugoti ir uždaryti", "ButtonSaveAndClose": "Išsaugoti ir uždaryti",
@ -83,11 +87,15 @@
"ButtonUserEdit": "Redaguoti naudotoją {0}", "ButtonUserEdit": "Redaguoti naudotoją {0}",
"ButtonViewAll": "Peržiūrėti visus", "ButtonViewAll": "Peržiūrėti visus",
"ButtonYes": "Taip", "ButtonYes": "Taip",
"ErrorUploadFetchMetadataAPI": "Error fetching metadata",
"ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
"ErrorUploadLacksTitle": "Must have a title",
"HeaderAccount": "Paskyra", "HeaderAccount": "Paskyra",
"HeaderAdvanced": "Papildomi", "HeaderAdvanced": "Papildomi",
"HeaderAppriseNotificationSettings": "Apprise pranešimo nustatymai", "HeaderAppriseNotificationSettings": "Apprise pranešimo nustatymai",
"HeaderAudiobookTools": "Audioknygų failų valdymo įrankiai", "HeaderAudiobookTools": "Audioknygų failų valdymo įrankiai",
"HeaderAudioTracks": "Garso takeliai", "HeaderAudioTracks": "Garso takeliai",
"HeaderAuthentication": "Authentication",
"HeaderBackups": "Atsarginės kopijos", "HeaderBackups": "Atsarginės kopijos",
"HeaderChangePassword": "Pakeisti slaptažodį", "HeaderChangePassword": "Pakeisti slaptažodį",
"HeaderChapters": "Skyriai", "HeaderChapters": "Skyriai",
@ -122,12 +130,15 @@
"HeaderManageTags": "Tvarkyti žymas", "HeaderManageTags": "Tvarkyti žymas",
"HeaderMapDetails": "Susieti detales", "HeaderMapDetails": "Susieti detales",
"HeaderMatch": "Atitaikyti", "HeaderMatch": "Atitaikyti",
"HeaderMetadataOrderOfPrecedence": "Metadata order of precedence",
"HeaderMetadataToEmbed": "Metaduomenys įterpimui", "HeaderMetadataToEmbed": "Metaduomenys įterpimui",
"HeaderNewAccount": "Nauja paskyra", "HeaderNewAccount": "Nauja paskyra",
"HeaderNewLibrary": "Nauja biblioteka", "HeaderNewLibrary": "Nauja biblioteka",
"HeaderNotifications": "Pranešimai", "HeaderNotifications": "Pranešimai",
"HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication",
"HeaderOpenRSSFeed": "Atidaryti RSS srautą", "HeaderOpenRSSFeed": "Atidaryti RSS srautą",
"HeaderOtherFiles": "Kiti failai", "HeaderOtherFiles": "Kiti failai",
"HeaderPasswordAuthentication": "Password Authentication",
"HeaderPermissions": "Leidimai", "HeaderPermissions": "Leidimai",
"HeaderPlayerQueue": "Grotuvo eilė", "HeaderPlayerQueue": "Grotuvo eilė",
"HeaderPlaylist": "Grojaraštis", "HeaderPlaylist": "Grojaraštis",
@ -176,8 +187,11 @@
"LabelAddToCollectionBatch": "Pridėti {0} knygas į kolekciją", "LabelAddToCollectionBatch": "Pridėti {0} knygas į kolekciją",
"LabelAddToPlaylist": "Pridėti į grojaraštį", "LabelAddToPlaylist": "Pridėti į grojaraštį",
"LabelAddToPlaylistBatch": "Pridėti {0} elementus į grojaraštį", "LabelAddToPlaylistBatch": "Pridėti {0} elementus į grojaraštį",
"LabelAdminUsersOnly": "Admin users only",
"LabelAll": "Visi", "LabelAll": "Visi",
"LabelAllUsers": "Visi naudotojai", "LabelAllUsers": "Visi naudotojai",
"LabelAllUsersExcludingGuests": "All users excluding guests",
"LabelAllUsersIncludingGuests": "All users including guests",
"LabelAlreadyInYourLibrary": "Jau yra jūsų bibliotekoje", "LabelAlreadyInYourLibrary": "Jau yra jūsų bibliotekoje",
"LabelAppend": "Pridėti", "LabelAppend": "Pridėti",
"LabelAuthor": "Autorius", "LabelAuthor": "Autorius",
@ -185,6 +199,12 @@
"LabelAuthorLastFirst": "Autorius (Pavardė, Vardas)", "LabelAuthorLastFirst": "Autorius (Pavardė, Vardas)",
"LabelAuthors": "Autoriai", "LabelAuthors": "Autoriai",
"LabelAutoDownloadEpisodes": "Automatiškai atsisiųsti epizodus", "LabelAutoDownloadEpisodes": "Automatiškai atsisiųsti epizodus",
"LabelAutoFetchMetadata": "Auto Fetch Metadata",
"LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
"LabelAutoLaunch": "Auto Launch",
"LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Auto Register",
"LabelAutoRegisterDescription": "Automatically create new users after logging in",
"LabelBackToUser": "Grįžti į naudotoją", "LabelBackToUser": "Grįžti į naudotoją",
"LabelBackupLocation": "Backup Location", "LabelBackupLocation": "Backup Location",
"LabelBackupsEnableAutomaticBackups": "Įjungti automatinį atsarginių kopijų kūrimą", "LabelBackupsEnableAutomaticBackups": "Įjungti automatinį atsarginių kopijų kūrimą",
@ -195,11 +215,13 @@
"LabelBackupsNumberToKeepHelp": "Tik viena atsarginė kopija bus pašalinta vienu metu, todėl jei jau turite daugiau atsarginių kopijų nei nurodyta, turite jas pašalinti rankiniu būdu.", "LabelBackupsNumberToKeepHelp": "Tik viena atsarginė kopija bus pašalinta vienu metu, todėl jei jau turite daugiau atsarginių kopijų nei nurodyta, turite jas pašalinti rankiniu būdu.",
"LabelBitrate": "Bitų sparta", "LabelBitrate": "Bitų sparta",
"LabelBooks": "Knygos", "LabelBooks": "Knygos",
"LabelButtonText": "Button Text",
"LabelChangePassword": "Pakeisti slaptažodį", "LabelChangePassword": "Pakeisti slaptažodį",
"LabelChannels": "Kanalai", "LabelChannels": "Kanalai",
"LabelChapters": "Skyriai", "LabelChapters": "Skyriai",
"LabelChaptersFound": "rasti skyriai", "LabelChaptersFound": "rasti skyriai",
"LabelChapterTitle": "Skyriaus pavadinimas", "LabelChapterTitle": "Skyriaus pavadinimas",
"LabelClickForMoreInfo": "Click for more info",
"LabelClosePlayer": "Uždaryti grotuvą", "LabelClosePlayer": "Uždaryti grotuvą",
"LabelCodec": "Kodekas", "LabelCodec": "Kodekas",
"LabelCollapseSeries": "Suskleisti seriją", "LabelCollapseSeries": "Suskleisti seriją",
@ -218,10 +240,12 @@
"LabelCurrently": "Šiuo metu:", "LabelCurrently": "Šiuo metu:",
"LabelCustomCronExpression": "Nestandartinė Cron išraiška:", "LabelCustomCronExpression": "Nestandartinė Cron išraiška:",
"LabelDatetime": "Data ir laikas", "LabelDatetime": "Data ir laikas",
"LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)",
"LabelDescription": "Aprašymas", "LabelDescription": "Aprašymas",
"LabelDeselectAll": "Išvalyti pasirinktus", "LabelDeselectAll": "Išvalyti pasirinktus",
"LabelDevice": "Įrenginys", "LabelDevice": "Įrenginys",
"LabelDeviceInfo": "Įrenginio informacija", "LabelDeviceInfo": "Įrenginio informacija",
"LabelDeviceIsAvailableTo": "Device is available to...",
"LabelDirectory": "Katalogas", "LabelDirectory": "Katalogas",
"LabelDiscFromFilename": "Diskas pagal failo pavadinimą", "LabelDiscFromFilename": "Diskas pagal failo pavadinimą",
"LabelDiscFromMetadata": "Diskas pagal metaduomenis", "LabelDiscFromMetadata": "Diskas pagal metaduomenis",
@ -247,6 +271,7 @@
"LabelExample": "Pavyzdys", "LabelExample": "Pavyzdys",
"LabelExplicit": "Suaugusiems", "LabelExplicit": "Suaugusiems",
"LabelFeedURL": "Srauto URL", "LabelFeedURL": "Srauto URL",
"LabelFetchingMetadata": "Fetching Metadata",
"LabelFile": "Failas", "LabelFile": "Failas",
"LabelFileBirthtime": "Failo kūrimo laikas", "LabelFileBirthtime": "Failo kūrimo laikas",
"LabelFileModified": "Failo keitimo laikas", "LabelFileModified": "Failo keitimo laikas",
@ -256,6 +281,7 @@
"LabelFinished": "Baigta", "LabelFinished": "Baigta",
"LabelFolder": "Aplankas", "LabelFolder": "Aplankas",
"LabelFolders": "Aplankai", "LabelFolders": "Aplankai",
"LabelFontFamily": "Famiglia di font",
"LabelFontScale": "Šrifto mastelis", "LabelFontScale": "Šrifto mastelis",
"LabelFormat": "Formatas", "LabelFormat": "Formatas",
"LabelGenre": "Žanras", "LabelGenre": "Žanras",
@ -263,9 +289,11 @@
"LabelHardDeleteFile": "Galutinai ištrinti failą", "LabelHardDeleteFile": "Galutinai ištrinti failą",
"LabelHasEbook": "Turi e-knygą", "LabelHasEbook": "Turi e-knygą",
"LabelHasSupplementaryEbook": "Turi papildomą e-knygą", "LabelHasSupplementaryEbook": "Turi papildomą e-knygą",
"LabelHighestPriority": "Highest priority",
"LabelHost": "Serveris", "LabelHost": "Serveris",
"LabelHour": "Valanda", "LabelHour": "Valanda",
"LabelIcon": "Piktograma", "LabelIcon": "Piktograma",
"LabelImageURLFromTheWeb": "Image URL from the web",
"LabelIncludeInTracklist": "Įtraukti į takelių sąrašą", "LabelIncludeInTracklist": "Įtraukti į takelių sąrašą",
"LabelIncomplete": "Nebaigta", "LabelIncomplete": "Nebaigta",
"LabelInProgress": "Vyksta", "LabelInProgress": "Vyksta",
@ -303,14 +331,20 @@
"LabelLogLevelInfo": "Info", "LabelLogLevelInfo": "Info",
"LabelLogLevelWarn": "Warn", "LabelLogLevelWarn": "Warn",
"LabelLookForNewEpisodesAfterDate": "Ieškoti naujų epizodų po šios datos", "LabelLookForNewEpisodesAfterDate": "Ieškoti naujų epizodų po šios datos",
"LabelLowestPriority": "Lowest Priority",
"LabelMatchExistingUsersBy": "Match existing users by",
"LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider",
"LabelMediaPlayer": "Grotuvas", "LabelMediaPlayer": "Grotuvas",
"LabelMediaType": "Medijos tipas", "LabelMediaType": "Medijos tipas",
"LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources",
"LabelMetadataProvider": "Metaduomenų tiekėjas", "LabelMetadataProvider": "Metaduomenų tiekėjas",
"LabelMetaTag": "Meta žymė", "LabelMetaTag": "Meta žymė",
"LabelMetaTags": "Meta žymos", "LabelMetaTags": "Meta žymos",
"LabelMinute": "Minutė", "LabelMinute": "Minutė",
"LabelMissing": "Trūksta", "LabelMissing": "Trūksta",
"LabelMissingParts": "Trūkstamos dalys", "LabelMissingParts": "Trūkstamos dalys",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
"LabelMore": "Daugiau", "LabelMore": "Daugiau",
"LabelMoreInfo": "Daugiau informacijos", "LabelMoreInfo": "Daugiau informacijos",
"LabelName": "Pavadinimas", "LabelName": "Pavadinimas",
@ -372,6 +406,7 @@
"LabelRegion": "Regionas", "LabelRegion": "Regionas",
"LabelReleaseDate": "Išleidimo data", "LabelReleaseDate": "Išleidimo data",
"LabelRemoveCover": "Pašalinti viršelį", "LabelRemoveCover": "Pašalinti viršelį",
"LabelRowsPerPage": "Rows per page",
"LabelRSSFeedCustomOwnerEmail": "Pasirinktinis savininko el. paštas", "LabelRSSFeedCustomOwnerEmail": "Pasirinktinis savininko el. paštas",
"LabelRSSFeedCustomOwnerName": "Pasirinktinis savininko vardas", "LabelRSSFeedCustomOwnerName": "Pasirinktinis savininko vardas",
"LabelRSSFeedOpen": "Atidarytas RSS srautas", "LabelRSSFeedOpen": "Atidarytas RSS srautas",
@ -384,6 +419,7 @@
"LabelSeason": "Sezonas", "LabelSeason": "Sezonas",
"LabelSelectAllEpisodes": "Pažymėti visus epizodus", "LabelSelectAllEpisodes": "Pažymėti visus epizodus",
"LabelSelectEpisodesShowing": "Pažymėti {0} rodomus epizodus", "LabelSelectEpisodesShowing": "Pažymėti {0} rodomus epizodus",
"LabelSelectUsers": "Select users",
"LabelSendEbookToDevice": "Siųsti e-knygą į...", "LabelSendEbookToDevice": "Siųsti e-knygą į...",
"LabelSequence": "Seka", "LabelSequence": "Seka",
"LabelSeries": "Serija", "LabelSeries": "Serija",
@ -410,16 +446,10 @@
"LabelSettingsHideSingleBookSeriesHelp": "Serijos, turinčios tik vieną knygą, bus paslėptos nuo serijų puslapio ir pagrindinio puslapio lentynų.", "LabelSettingsHideSingleBookSeriesHelp": "Serijos, turinčios tik vieną knygą, bus paslėptos nuo serijų puslapio ir pagrindinio puslapio lentynų.",
"LabelSettingsHomePageBookshelfView": "Naudoti pagrindinio puslapio knygų lentynų vaizdą", "LabelSettingsHomePageBookshelfView": "Naudoti pagrindinio puslapio knygų lentynų vaizdą",
"LabelSettingsLibraryBookshelfView": "Naudoti bibliotekos knygų lentynų vaizdą", "LabelSettingsLibraryBookshelfView": "Naudoti bibliotekos knygų lentynų vaizdą",
"LabelSettingsOverdriveMediaMarkers": "Naudoti Overdrive žymeklius skyriams",
"LabelSettingsOverdriveMediaMarkersHelp": "MP3 failai iš Overdrive turi įterptus skyrių laikus kaip papildomą metaduomenį. Įjungus šią funkciją, skyrių laikai bus automatiškai naudojami.",
"LabelSettingsParseSubtitles": "Analizuoti subtitrus", "LabelSettingsParseSubtitles": "Analizuoti subtitrus",
"LabelSettingsParseSubtitlesHelp": "Išskleisti subtitrus iš audioknygos aplanko pavadinimų.<br>Subtitrai turi būti atskirti brūkšniu \"-\"<br>pavyzdžiui, \"Knygos pavadinimas - Čia yra subtitrai\" turi subtitrą \"Čia yra subtitrai\"", "LabelSettingsParseSubtitlesHelp": "Išskleisti subtitrus iš audioknygos aplanko pavadinimų.<br>Subtitrai turi būti atskirti brūkšniu \"-\"<br>pavyzdžiui, \"Knygos pavadinimas - Čia yra subtitrai\" turi subtitrą \"Čia yra subtitrai\"",
"LabelSettingsPreferAudioMetadata": "Pirmenybė failo metaduomenis",
"LabelSettingsPreferAudioMetadataHelp": "Garso failo ID3 metaduomenys bus naudojami knygos informacijai (vietoj aplankų pavadinimų)",
"LabelSettingsPreferMatchedMetadata": "Pirmenybė atitaikytiems metaduomenis", "LabelSettingsPreferMatchedMetadata": "Pirmenybė atitaikytiems metaduomenis",
"LabelSettingsPreferMatchedMetadataHelp": "Atitaikyti duomenys pakeis elementų informaciją naudojant Greitą atitikimą. Pagal nutylėjimą Greitas atitaikymas užpildys tik trūkstamas detales.", "LabelSettingsPreferMatchedMetadataHelp": "Atitaikyti duomenys pakeis elementų informaciją naudojant Greitą atitikimą. Pagal nutylėjimą Greitas atitaikymas užpildys tik trūkstamas detales.",
"LabelSettingsPreferOPFMetadata": "Pirmenybė OPF metaduomenis",
"LabelSettingsPreferOPFMetadataHelp": "OPF failo metaduomenys bus naudojami knygos informacijai (vietoj aplankų pavadinimų)",
"LabelSettingsSkipMatchingBooksWithASIN": "Praleisti knygas, kurios jau turi ASIN", "LabelSettingsSkipMatchingBooksWithASIN": "Praleisti knygas, kurios jau turi ASIN",
"LabelSettingsSkipMatchingBooksWithISBN": "Praleisti knygas, kurios jau turi ISBN", "LabelSettingsSkipMatchingBooksWithISBN": "Praleisti knygas, kurios jau turi ISBN",
"LabelSettingsSortingIgnorePrefixes": "Ignoruoti priešdėlius rūšiuojant", "LabelSettingsSortingIgnorePrefixes": "Ignoruoti priešdėlius rūšiuojant",
@ -429,7 +459,7 @@
"LabelSettingsStoreCoversWithItem": "Saugoti viršelius su elementu", "LabelSettingsStoreCoversWithItem": "Saugoti viršelius su elementu",
"LabelSettingsStoreCoversWithItemHelp": "Pagal nutylėjimą viršeliai saugomi /metadata/items aplanke, įjungus šią parinktį viršeliai bus saugomi jūsų bibliotekos elemento aplanke. Bus išsaugotas tik vienas „cover“ pavadinimo failas.", "LabelSettingsStoreCoversWithItemHelp": "Pagal nutylėjimą viršeliai saugomi /metadata/items aplanke, įjungus šią parinktį viršeliai bus saugomi jūsų bibliotekos elemento aplanke. Bus išsaugotas tik vienas „cover“ pavadinimo failas.",
"LabelSettingsStoreMetadataWithItem": "Saugoti metaduomenis su elementu", "LabelSettingsStoreMetadataWithItem": "Saugoti metaduomenis su elementu",
"LabelSettingsStoreMetadataWithItemHelp": "Pagal nutylėjimą metaduomenų failai saugomi /metadata/items aplanke, įjungus šią parinktį metaduomenų failai bus saugomi jūsų bibliotekos elemento aplanke. Naudojamas .abs plėtinys.", "LabelSettingsStoreMetadataWithItemHelp": "Pagal nutylėjimą metaduomenų failai saugomi /metadata/items aplanke, įjungus šią parinktį metaduomenų failai bus saugomi jūsų bibliotekos elemento aplanke",
"LabelSettingsTimeFormat": "Laiko formatas", "LabelSettingsTimeFormat": "Laiko formatas",
"LabelShowAll": "Rodyti viską", "LabelShowAll": "Rodyti viską",
"LabelSize": "Dydis", "LabelSize": "Dydis",
@ -494,6 +524,7 @@
"LabelUpdateDetailsHelp": "Leisti perrašyti esamus duomenis pasirinktoms knygoms, kai yra rasta atitikmenų", "LabelUpdateDetailsHelp": "Leisti perrašyti esamus duomenis pasirinktoms knygoms, kai yra rasta atitikmenų",
"LabelUploaderDragAndDrop": "Tempkite ir paleiskite failus ar aplankus", "LabelUploaderDragAndDrop": "Tempkite ir paleiskite failus ar aplankus",
"LabelUploaderDropFiles": "Nutempti failus", "LabelUploaderDropFiles": "Nutempti failus",
"LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
"LabelUseChapterTrack": "Naudoti skyrių takelį", "LabelUseChapterTrack": "Naudoti skyrių takelį",
"LabelUseFullTrack": "Naudoti visą takelį", "LabelUseFullTrack": "Naudoti visą takelį",
"LabelUser": "Vartotojas", "LabelUser": "Vartotojas",
@ -527,17 +558,21 @@
"MessageConfirmDeleteBackup": "Ar tikrai norite ištrinti atsarginę kopiją, skirtą {0}?", "MessageConfirmDeleteBackup": "Ar tikrai norite ištrinti atsarginę kopiją, skirtą {0}?",
"MessageConfirmDeleteFile": "Tai ištrins failą iš jūsų failų sistemos. Ar tikrai?", "MessageConfirmDeleteFile": "Tai ištrins failą iš jūsų failų sistemos. Ar tikrai?",
"MessageConfirmDeleteLibrary": "Ar tikrai norite visam laikui ištrinti biblioteką \"{0}\"?", "MessageConfirmDeleteLibrary": "Ar tikrai norite visam laikui ištrinti biblioteką \"{0}\"?",
"MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?",
"MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?",
"MessageConfirmDeleteSession": "Ar tikrai norite ištrinti šią sesiją?", "MessageConfirmDeleteSession": "Ar tikrai norite ištrinti šią sesiją?",
"MessageConfirmForceReScan": "Ar tikrai norite priversti perskenavimą?", "MessageConfirmForceReScan": "Ar tikrai norite priversti perskenavimą?",
"MessageConfirmMarkAllEpisodesFinished": "Ar tikrai norite pažymėti visus epizodus kaip užbaigtus?", "MessageConfirmMarkAllEpisodesFinished": "Ar tikrai norite pažymėti visus epizodus kaip užbaigtus?",
"MessageConfirmMarkAllEpisodesNotFinished": "Ar tikrai norite pažymėti visus epizodus kaip nebaigtus?", "MessageConfirmMarkAllEpisodesNotFinished": "Ar tikrai norite pažymėti visus epizodus kaip nebaigtus?",
"MessageConfirmMarkSeriesFinished": "Ar tikrai norite pažymėti visas knygas šioje serijoje kaip užbaigtas?", "MessageConfirmMarkSeriesFinished": "Ar tikrai norite pažymėti visas knygas šioje serijoje kaip užbaigtas?",
"MessageConfirmMarkSeriesNotFinished": "Ar tikrai norite pažymėti visas knygas šioje serijoje kaip nebaigtas?", "MessageConfirmMarkSeriesNotFinished": "Ar tikrai norite pažymėti visas knygas šioje serijoje kaip nebaigtas?",
"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?",
"MessageConfirmRemoveAllChapters": "Ar tikrai norite pašalinti visus skyrius?", "MessageConfirmRemoveAllChapters": "Ar tikrai norite pašalinti visus skyrius?",
"MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?", "MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?",
"MessageConfirmRemoveCollection": "Ar tikrai norite pašalinti kolekciją \"{0}\"?", "MessageConfirmRemoveCollection": "Ar tikrai norite pašalinti kolekciją \"{0}\"?",
"MessageConfirmRemoveEpisode": "Ar tikrai norite pašalinti epizodą \"{0}\"?", "MessageConfirmRemoveEpisode": "Ar tikrai norite pašalinti epizodą \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Ar tikrai norite pašalinti {0} epizodus?", "MessageConfirmRemoveEpisodes": "Ar tikrai norite pašalinti {0} epizodus?",
"MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "Ar tikrai norite pašalinti skaitytoją \"{0}\"?", "MessageConfirmRemoveNarrator": "Ar tikrai norite pašalinti skaitytoją \"{0}\"?",
"MessageConfirmRemovePlaylist": "Ar tikrai norite pašalinti savo grojaraštį \"{0}\"?", "MessageConfirmRemovePlaylist": "Ar tikrai norite pašalinti savo grojaraštį \"{0}\"?",
"MessageConfirmRenameGenre": "Ar tikrai norite pervadinti žanrą \"{0}\" į \"{1}\" visiems elementams?", "MessageConfirmRenameGenre": "Ar tikrai norite pervadinti žanrą \"{0}\" į \"{1}\" visiems elementams?",
@ -546,6 +581,7 @@
"MessageConfirmRenameTag": "Ar tikrai norite pervadinti žymą \"{0}\" į \"{1}\" visiems elementams?", "MessageConfirmRenameTag": "Ar tikrai norite pervadinti žymą \"{0}\" į \"{1}\" visiems elementams?",
"MessageConfirmRenameTagMergeNote": "Pastaba: ši žyma jau egzistuoja, todėl jos bus sujungtos.", "MessageConfirmRenameTagMergeNote": "Pastaba: ši žyma jau egzistuoja, todėl jos bus sujungtos.",
"MessageConfirmRenameTagWarning": "Įspėjimas! Panaši žyma jau egzistuoja \"{0}\".", "MessageConfirmRenameTagWarning": "Įspėjimas! Panaši žyma jau egzistuoja \"{0}\".",
"MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?",
"MessageConfirmSendEbookToDevice": "Ar tikrai norite nusiųsti {0} el. knygą \"{1}\" į įrenginį \"{2}\"?", "MessageConfirmSendEbookToDevice": "Ar tikrai norite nusiųsti {0} el. knygą \"{1}\" į įrenginį \"{2}\"?",
"MessageDownloadingEpisode": "Epizodas atsisiunčiamas", "MessageDownloadingEpisode": "Epizodas atsisiunčiamas",
"MessageDragFilesIntoTrackOrder": "Surikiuokite takelius vilkdami failus", "MessageDragFilesIntoTrackOrder": "Surikiuokite takelius vilkdami failus",
@ -616,6 +652,7 @@
"MessageRestoreBackupConfirm": "Ar tikrai norite atkurti atsarginę kopiją, sukurtą", "MessageRestoreBackupConfirm": "Ar tikrai norite atkurti atsarginę kopiją, sukurtą",
"MessageRestoreBackupWarning": "Atkurdami atsarginę kopiją perrašysite visą duomenų bazę, esančią /config ir viršelių vaizdus /metadata/items ir /metadata/authors.<br /><br />Atsarginės kopijos nekeičia jokių failų jūsų bibliotekos aplankuose. Jei esate įgalinę serverio nustatymus, kad viršelio meną ir metaduomenis saugotumėte savo bibliotekos aplankuose, šie neperrašomi ar atkuriami.<br /><br />Visi klientai, naudojantys jūsų serverį, bus automatiškai atnaujinti.", "MessageRestoreBackupWarning": "Atkurdami atsarginę kopiją perrašysite visą duomenų bazę, esančią /config ir viršelių vaizdus /metadata/items ir /metadata/authors.<br /><br />Atsarginės kopijos nekeičia jokių failų jūsų bibliotekos aplankuose. Jei esate įgalinę serverio nustatymus, kad viršelio meną ir metaduomenis saugotumėte savo bibliotekos aplankuose, šie neperrašomi ar atkuriami.<br /><br />Visi klientai, naudojantys jūsų serverį, bus automatiškai atnaujinti.",
"MessageSearchResultsFor": "Paieškos rezultatai „{0}“", "MessageSearchResultsFor": "Paieškos rezultatai „{0}“",
"MessageSelected": "{0} selected",
"MessageServerCouldNotBeReached": "Nepavyko pasiekti serverio", "MessageServerCouldNotBeReached": "Nepavyko pasiekti serverio",
"MessageSetChaptersFromTracksDescription": "Nustatyti skyrius, naudojant kiekvieną garso failą kaip skyrių ir skyriaus pavadinimą kaip garso failo pavadinimą", "MessageSetChaptersFromTracksDescription": "Nustatyti skyrius, naudojant kiekvieną garso failą kaip skyrių ir skyriaus pavadinimą kaip garso failo pavadinimą",
"MessageStartPlaybackAtTime": "Paleisti klausymą „{0}“ nuo {1}?", "MessageStartPlaybackAtTime": "Paleisti klausymą „{0}“ nuo {1}?",

View File

@ -1,7 +1,10 @@
{ {
"ButtonAdd": "Toevoegen", "ButtonAdd": "Toevoegen",
"ButtonAddChapters": "Hoofdstukken toevoegen", "ButtonAddChapters": "Hoofdstukken toevoegen",
"ButtonAddDevice": "Add Device",
"ButtonAddLibrary": "Add Library",
"ButtonAddPodcasts": "Podcasts toevoegen", "ButtonAddPodcasts": "Podcasts toevoegen",
"ButtonAddUser": "Add User",
"ButtonAddYourFirstLibrary": "Voeg je eerste bibliotheek toe", "ButtonAddYourFirstLibrary": "Voeg je eerste bibliotheek toe",
"ButtonApply": "Pas toe", "ButtonApply": "Pas toe",
"ButtonApplyChapters": "Hoofdstukken toepassen", "ButtonApplyChapters": "Hoofdstukken toepassen",
@ -59,6 +62,7 @@
"ButtonRemoveSeriesFromContinueSeries": "Verwijder serie uit Serie vervolgen", "ButtonRemoveSeriesFromContinueSeries": "Verwijder serie uit Serie vervolgen",
"ButtonReScan": "Nieuwe scan", "ButtonReScan": "Nieuwe scan",
"ButtonReset": "Reset", "ButtonReset": "Reset",
"ButtonResetToDefault": "Reset to default",
"ButtonRestore": "Herstel", "ButtonRestore": "Herstel",
"ButtonSave": "Opslaan", "ButtonSave": "Opslaan",
"ButtonSaveAndClose": "Opslaan & sluiten", "ButtonSaveAndClose": "Opslaan & sluiten",
@ -83,11 +87,15 @@
"ButtonUserEdit": "Wijzig gebruiker {0}", "ButtonUserEdit": "Wijzig gebruiker {0}",
"ButtonViewAll": "Toon alle", "ButtonViewAll": "Toon alle",
"ButtonYes": "Ja", "ButtonYes": "Ja",
"ErrorUploadFetchMetadataAPI": "Error fetching metadata",
"ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
"ErrorUploadLacksTitle": "Must have a title",
"HeaderAccount": "Account", "HeaderAccount": "Account",
"HeaderAdvanced": "Geavanceerd", "HeaderAdvanced": "Geavanceerd",
"HeaderAppriseNotificationSettings": "Apprise-notificatie instellingen", "HeaderAppriseNotificationSettings": "Apprise-notificatie instellingen",
"HeaderAudiobookTools": "Audioboekbestandbeheer tools", "HeaderAudiobookTools": "Audioboekbestandbeheer tools",
"HeaderAudioTracks": "Audiotracks", "HeaderAudioTracks": "Audiotracks",
"HeaderAuthentication": "Authentication",
"HeaderBackups": "Back-ups", "HeaderBackups": "Back-ups",
"HeaderChangePassword": "Wachtwoord wijzigen", "HeaderChangePassword": "Wachtwoord wijzigen",
"HeaderChapters": "Hoofdstukken", "HeaderChapters": "Hoofdstukken",
@ -122,12 +130,15 @@
"HeaderManageTags": "Tags beheren", "HeaderManageTags": "Tags beheren",
"HeaderMapDetails": "Map details", "HeaderMapDetails": "Map details",
"HeaderMatch": "Match", "HeaderMatch": "Match",
"HeaderMetadataOrderOfPrecedence": "Metadata order of precedence",
"HeaderMetadataToEmbed": "In te sluiten metadata", "HeaderMetadataToEmbed": "In te sluiten metadata",
"HeaderNewAccount": "Nieuwe account", "HeaderNewAccount": "Nieuwe account",
"HeaderNewLibrary": "Nieuwe bibliotheek", "HeaderNewLibrary": "Nieuwe bibliotheek",
"HeaderNotifications": "Notificaties", "HeaderNotifications": "Notificaties",
"HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication",
"HeaderOpenRSSFeed": "Open RSS-feed", "HeaderOpenRSSFeed": "Open RSS-feed",
"HeaderOtherFiles": "Andere bestanden", "HeaderOtherFiles": "Andere bestanden",
"HeaderPasswordAuthentication": "Password Authentication",
"HeaderPermissions": "Toestemmingen", "HeaderPermissions": "Toestemmingen",
"HeaderPlayerQueue": "Afspeelwachtrij", "HeaderPlayerQueue": "Afspeelwachtrij",
"HeaderPlaylist": "Afspeellijst", "HeaderPlaylist": "Afspeellijst",
@ -176,8 +187,11 @@
"LabelAddToCollectionBatch": "{0} boeken toevoegen aan collectie", "LabelAddToCollectionBatch": "{0} boeken toevoegen aan collectie",
"LabelAddToPlaylist": "Toevoegen aan afspeellijst", "LabelAddToPlaylist": "Toevoegen aan afspeellijst",
"LabelAddToPlaylistBatch": "{0} onderdelen toevoegen aan afspeellijst", "LabelAddToPlaylistBatch": "{0} onderdelen toevoegen aan afspeellijst",
"LabelAdminUsersOnly": "Admin users only",
"LabelAll": "Alle", "LabelAll": "Alle",
"LabelAllUsers": "Alle gebruikers", "LabelAllUsers": "Alle gebruikers",
"LabelAllUsersExcludingGuests": "All users excluding guests",
"LabelAllUsersIncludingGuests": "All users including guests",
"LabelAlreadyInYourLibrary": "Reeds in je bibliotheek", "LabelAlreadyInYourLibrary": "Reeds in je bibliotheek",
"LabelAppend": "Achteraan toevoegen", "LabelAppend": "Achteraan toevoegen",
"LabelAuthor": "Auteur", "LabelAuthor": "Auteur",
@ -185,6 +199,12 @@
"LabelAuthorLastFirst": "Auteur (Achternaam, Voornaam)", "LabelAuthorLastFirst": "Auteur (Achternaam, Voornaam)",
"LabelAuthors": "Auteurs", "LabelAuthors": "Auteurs",
"LabelAutoDownloadEpisodes": "Afleveringen automatisch downloaden", "LabelAutoDownloadEpisodes": "Afleveringen automatisch downloaden",
"LabelAutoFetchMetadata": "Auto Fetch Metadata",
"LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
"LabelAutoLaunch": "Auto Launch",
"LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Auto Register",
"LabelAutoRegisterDescription": "Automatically create new users after logging in",
"LabelBackToUser": "Terug naar gebruiker", "LabelBackToUser": "Terug naar gebruiker",
"LabelBackupLocation": "Back-up locatie", "LabelBackupLocation": "Back-up locatie",
"LabelBackupsEnableAutomaticBackups": "Automatische back-ups inschakelen", "LabelBackupsEnableAutomaticBackups": "Automatische back-ups inschakelen",
@ -195,11 +215,13 @@
"LabelBackupsNumberToKeepHelp": "Er wordt slechts 1 back-up per keer verwijderd, dus als je reeds meer back-ups dan dit hebt moet je ze handmatig verwijderen.", "LabelBackupsNumberToKeepHelp": "Er wordt slechts 1 back-up per keer verwijderd, dus als je reeds meer back-ups dan dit hebt moet je ze handmatig verwijderen.",
"LabelBitrate": "Bitrate", "LabelBitrate": "Bitrate",
"LabelBooks": "Boeken", "LabelBooks": "Boeken",
"LabelButtonText": "Button Text",
"LabelChangePassword": "Wachtwoord wijzigen", "LabelChangePassword": "Wachtwoord wijzigen",
"LabelChannels": "Kanalen", "LabelChannels": "Kanalen",
"LabelChapters": "Hoofdstukken", "LabelChapters": "Hoofdstukken",
"LabelChaptersFound": "Hoofdstukken gevonden", "LabelChaptersFound": "Hoofdstukken gevonden",
"LabelChapterTitle": "Hoofdstuktitel", "LabelChapterTitle": "Hoofdstuktitel",
"LabelClickForMoreInfo": "Click for more info",
"LabelClosePlayer": "Sluit speler", "LabelClosePlayer": "Sluit speler",
"LabelCodec": "Codec", "LabelCodec": "Codec",
"LabelCollapseSeries": "Series inklappen", "LabelCollapseSeries": "Series inklappen",
@ -218,10 +240,12 @@
"LabelCurrently": "Op dit moment:", "LabelCurrently": "Op dit moment:",
"LabelCustomCronExpression": "Aangepaste Cron-uitdrukking:", "LabelCustomCronExpression": "Aangepaste Cron-uitdrukking:",
"LabelDatetime": "Datum-tijd", "LabelDatetime": "Datum-tijd",
"LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)",
"LabelDescription": "Beschrijving", "LabelDescription": "Beschrijving",
"LabelDeselectAll": "Deselecteer alle", "LabelDeselectAll": "Deselecteer alle",
"LabelDevice": "Apparaat", "LabelDevice": "Apparaat",
"LabelDeviceInfo": "Apparaat info", "LabelDeviceInfo": "Apparaat info",
"LabelDeviceIsAvailableTo": "Device is available to...",
"LabelDirectory": "Map", "LabelDirectory": "Map",
"LabelDiscFromFilename": "Schijf uit bestandsnaam", "LabelDiscFromFilename": "Schijf uit bestandsnaam",
"LabelDiscFromMetadata": "Schijf uit metadata", "LabelDiscFromMetadata": "Schijf uit metadata",
@ -247,6 +271,7 @@
"LabelExample": "Voorbeeld", "LabelExample": "Voorbeeld",
"LabelExplicit": "Expliciet", "LabelExplicit": "Expliciet",
"LabelFeedURL": "Feed URL", "LabelFeedURL": "Feed URL",
"LabelFetchingMetadata": "Fetching Metadata",
"LabelFile": "Bestand", "LabelFile": "Bestand",
"LabelFileBirthtime": "Aanmaaktijd bestand", "LabelFileBirthtime": "Aanmaaktijd bestand",
"LabelFileModified": "Bestand gewijzigd", "LabelFileModified": "Bestand gewijzigd",
@ -256,6 +281,7 @@
"LabelFinished": "Voltooid", "LabelFinished": "Voltooid",
"LabelFolder": "Map", "LabelFolder": "Map",
"LabelFolders": "Mappen", "LabelFolders": "Mappen",
"LabelFontFamily": "Lettertypefamilie",
"LabelFontScale": "Lettertype schaal", "LabelFontScale": "Lettertype schaal",
"LabelFormat": "Formaat", "LabelFormat": "Formaat",
"LabelGenre": "Genre", "LabelGenre": "Genre",
@ -263,9 +289,11 @@
"LabelHardDeleteFile": "Hard-delete bestand", "LabelHardDeleteFile": "Hard-delete bestand",
"LabelHasEbook": "Heeft ebook", "LabelHasEbook": "Heeft ebook",
"LabelHasSupplementaryEbook": "Heeft supplementair ebook", "LabelHasSupplementaryEbook": "Heeft supplementair ebook",
"LabelHighestPriority": "Highest priority",
"LabelHost": "Host", "LabelHost": "Host",
"LabelHour": "Uur", "LabelHour": "Uur",
"LabelIcon": "Icoon", "LabelIcon": "Icoon",
"LabelImageURLFromTheWeb": "Image URL from the web",
"LabelIncludeInTracklist": "Includeer in tracklijst", "LabelIncludeInTracklist": "Includeer in tracklijst",
"LabelIncomplete": "Incompleet", "LabelIncomplete": "Incompleet",
"LabelInProgress": "Bezig", "LabelInProgress": "Bezig",
@ -303,14 +331,20 @@
"LabelLogLevelInfo": "Info", "LabelLogLevelInfo": "Info",
"LabelLogLevelWarn": "Waarschuwing", "LabelLogLevelWarn": "Waarschuwing",
"LabelLookForNewEpisodesAfterDate": "Zoek naar nieuwe afleveringen na deze datum", "LabelLookForNewEpisodesAfterDate": "Zoek naar nieuwe afleveringen na deze datum",
"LabelLowestPriority": "Lowest Priority",
"LabelMatchExistingUsersBy": "Match existing users by",
"LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider",
"LabelMediaPlayer": "Mediaspeler", "LabelMediaPlayer": "Mediaspeler",
"LabelMediaType": "Mediatype", "LabelMediaType": "Mediatype",
"LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources",
"LabelMetadataProvider": "Metadatabron", "LabelMetadataProvider": "Metadatabron",
"LabelMetaTag": "Meta-tag", "LabelMetaTag": "Meta-tag",
"LabelMetaTags": "Meta-tags", "LabelMetaTags": "Meta-tags",
"LabelMinute": "Minuut", "LabelMinute": "Minuut",
"LabelMissing": "Ontbrekend", "LabelMissing": "Ontbrekend",
"LabelMissingParts": "Ontbrekende delen", "LabelMissingParts": "Ontbrekende delen",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
"LabelMore": "Meer", "LabelMore": "Meer",
"LabelMoreInfo": "Meer info", "LabelMoreInfo": "Meer info",
"LabelName": "Naam", "LabelName": "Naam",
@ -372,6 +406,7 @@
"LabelRegion": "Regio", "LabelRegion": "Regio",
"LabelReleaseDate": "Verschijningsdatum", "LabelReleaseDate": "Verschijningsdatum",
"LabelRemoveCover": "Verwijder cover", "LabelRemoveCover": "Verwijder cover",
"LabelRowsPerPage": "Rows per page",
"LabelRSSFeedCustomOwnerEmail": "Aangepast e-mailadres eigenaar", "LabelRSSFeedCustomOwnerEmail": "Aangepast e-mailadres eigenaar",
"LabelRSSFeedCustomOwnerName": "Aangepaste naam eigenaar", "LabelRSSFeedCustomOwnerName": "Aangepaste naam eigenaar",
"LabelRSSFeedOpen": "RSS-feed open", "LabelRSSFeedOpen": "RSS-feed open",
@ -384,6 +419,7 @@
"LabelSeason": "Seizoen", "LabelSeason": "Seizoen",
"LabelSelectAllEpisodes": "Selecteer alle afleveringen", "LabelSelectAllEpisodes": "Selecteer alle afleveringen",
"LabelSelectEpisodesShowing": "Selecteer {0} afleveringen laten zien", "LabelSelectEpisodesShowing": "Selecteer {0} afleveringen laten zien",
"LabelSelectUsers": "Select users",
"LabelSendEbookToDevice": "Stuur ebook naar...", "LabelSendEbookToDevice": "Stuur ebook naar...",
"LabelSequence": "Sequentie", "LabelSequence": "Sequentie",
"LabelSeries": "Serie", "LabelSeries": "Serie",
@ -410,16 +446,10 @@
"LabelSettingsHideSingleBookSeriesHelp": "Series die slechts een enkel boek bevatten worden verborgen op de seriespagina en de homepagina-planken.", "LabelSettingsHideSingleBookSeriesHelp": "Series die slechts een enkel boek bevatten worden verborgen op de seriespagina en de homepagina-planken.",
"LabelSettingsHomePageBookshelfView": "Boekenplank-view voor homepagina", "LabelSettingsHomePageBookshelfView": "Boekenplank-view voor homepagina",
"LabelSettingsLibraryBookshelfView": "Boekenplank-view voor bibliotheek", "LabelSettingsLibraryBookshelfView": "Boekenplank-view voor bibliotheek",
"LabelSettingsOverdriveMediaMarkers": "Gebruik Overdrive media markers voor hoofdstukken",
"LabelSettingsOverdriveMediaMarkersHelp": "MP3-bestanden van Overdrive hebben hoofdstuktiming ingesloten als custom ingesloten metadata. Door dit in te schakelen worden deze tags voor hoofdstuktiming automatisch gebruikt.",
"LabelSettingsParseSubtitles": "Parseer subtitel", "LabelSettingsParseSubtitles": "Parseer subtitel",
"LabelSettingsParseSubtitlesHelp": "Haal subtitels uit mapnaam van audioboek.<br>Subtitel moet gescheiden zijn met \" - \"<br>b.v. \"Boektitel - Een Subtitel Hier\" heeft als subtitel \"Een Subtitel Hier\"", "LabelSettingsParseSubtitlesHelp": "Haal subtitels uit mapnaam van audioboek.<br>Subtitel moet gescheiden zijn met \" - \"<br>b.v. \"Boektitel - Een Subtitel Hier\" heeft als subtitel \"Een Subtitel Hier\"",
"LabelSettingsPreferAudioMetadata": "Prefereer audio-metadata",
"LabelSettingsPreferAudioMetadataHelp": "Audiobestand ID3 metatags zullen worden gebruikt voor boekdetails in plaats van mapnamen",
"LabelSettingsPreferMatchedMetadata": "Prefereer gematchte metadata", "LabelSettingsPreferMatchedMetadata": "Prefereer gematchte metadata",
"LabelSettingsPreferMatchedMetadataHelp": "Gematchte data zal onderdeeldetails overschrijven bij gebruik van Quick Match. Standaard vult Quick Match uitsluitend ontbrekende details aan.", "LabelSettingsPreferMatchedMetadataHelp": "Gematchte data zal onderdeeldetails overschrijven bij gebruik van Quick Match. Standaard vult Quick Match uitsluitend ontbrekende details aan.",
"LabelSettingsPreferOPFMetadata": "Prefereer OPF-metadata",
"LabelSettingsPreferOPFMetadataHelp": "OPF-bestand metadata zal worden gebruik in plaats van mapnamen",
"LabelSettingsSkipMatchingBooksWithASIN": "Sla matchen van boeken over die al over een ASIN beschikken", "LabelSettingsSkipMatchingBooksWithASIN": "Sla matchen van boeken over die al over een ASIN beschikken",
"LabelSettingsSkipMatchingBooksWithISBN": "Sla matchen van boeken over die al over een ISBN beschikken", "LabelSettingsSkipMatchingBooksWithISBN": "Sla matchen van boeken over die al over een ISBN beschikken",
"LabelSettingsSortingIgnorePrefixes": "Negeer voorvoegsels bij sorteren", "LabelSettingsSortingIgnorePrefixes": "Negeer voorvoegsels bij sorteren",
@ -429,7 +459,7 @@
"LabelSettingsStoreCoversWithItem": "Bewaar covers bij onderdeel", "LabelSettingsStoreCoversWithItem": "Bewaar covers bij onderdeel",
"LabelSettingsStoreCoversWithItemHelp": "Standaard worden covers bewaard in /metadata/items, door deze instelling in te schakelen zullen covers in de map van je bibliotheekonderdeel bewaard worden. Slechts een bestand genaamd \"cover\" zal worden bewaard", "LabelSettingsStoreCoversWithItemHelp": "Standaard worden covers bewaard in /metadata/items, door deze instelling in te schakelen zullen covers in de map van je bibliotheekonderdeel bewaard worden. Slechts een bestand genaamd \"cover\" zal worden bewaard",
"LabelSettingsStoreMetadataWithItem": "Bewaar metadata bij onderdeel", "LabelSettingsStoreMetadataWithItem": "Bewaar metadata bij onderdeel",
"LabelSettingsStoreMetadataWithItemHelp": "Standaard worden metadata-bestanden bewaard in /metadata/items, door deze instelling in te schakelen zullen metadata bestanden in de map van je bibliotheekonderdeel bewaard worden. Gebruikt .abs-extensie", "LabelSettingsStoreMetadataWithItemHelp": "Standaard worden metadata-bestanden bewaard in /metadata/items, door deze instelling in te schakelen zullen metadata bestanden in de map van je bibliotheekonderdeel bewaard worden",
"LabelSettingsTimeFormat": "Tijdformat", "LabelSettingsTimeFormat": "Tijdformat",
"LabelShowAll": "Toon alle", "LabelShowAll": "Toon alle",
"LabelSize": "Grootte", "LabelSize": "Grootte",
@ -494,6 +524,7 @@
"LabelUpdateDetailsHelp": "Sta overschrijven van bestaande details toe voor de geselecteerde boeken wanneer een match is gevonden", "LabelUpdateDetailsHelp": "Sta overschrijven van bestaande details toe voor de geselecteerde boeken wanneer een match is gevonden",
"LabelUploaderDragAndDrop": "Slepen & neerzeten van bestanden of mappen", "LabelUploaderDragAndDrop": "Slepen & neerzeten van bestanden of mappen",
"LabelUploaderDropFiles": "Bestanden neerzetten", "LabelUploaderDropFiles": "Bestanden neerzetten",
"LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
"LabelUseChapterTrack": "Gebruik hoofdstuktrack", "LabelUseChapterTrack": "Gebruik hoofdstuktrack",
"LabelUseFullTrack": "Gebruik volledige track", "LabelUseFullTrack": "Gebruik volledige track",
"LabelUser": "Gebruiker", "LabelUser": "Gebruiker",
@ -527,17 +558,21 @@
"MessageConfirmDeleteBackup": "Weet je zeker dat je de backup voor {0} wil verwijderen?", "MessageConfirmDeleteBackup": "Weet je zeker dat je de backup voor {0} wil verwijderen?",
"MessageConfirmDeleteFile": "Dit verwijdert het bestand uit het bestandssysteem. Weet je het zeker?", "MessageConfirmDeleteFile": "Dit verwijdert het bestand uit het bestandssysteem. Weet je het zeker?",
"MessageConfirmDeleteLibrary": "Weet je zeker dat je de bibliotheek \"{0}\" permanent wil verwijderen?", "MessageConfirmDeleteLibrary": "Weet je zeker dat je de bibliotheek \"{0}\" permanent wil verwijderen?",
"MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?",
"MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?",
"MessageConfirmDeleteSession": "Weet je zeker dat je deze sessie wil verwijderen?", "MessageConfirmDeleteSession": "Weet je zeker dat je deze sessie wil verwijderen?",
"MessageConfirmForceReScan": "Weet je zeker dat je geforceerd opnieuw wil scannen?", "MessageConfirmForceReScan": "Weet je zeker dat je geforceerd opnieuw wil scannen?",
"MessageConfirmMarkAllEpisodesFinished": "Weet je zeker dat je alle afleveringen als voltooid wil markeren?", "MessageConfirmMarkAllEpisodesFinished": "Weet je zeker dat je alle afleveringen als voltooid wil markeren?",
"MessageConfirmMarkAllEpisodesNotFinished": "Weet je zeker dat je alle afleveringen als niet-voltooid wil markeren?", "MessageConfirmMarkAllEpisodesNotFinished": "Weet je zeker dat je alle afleveringen als niet-voltooid wil markeren?",
"MessageConfirmMarkSeriesFinished": "Weet je zeker dat je alle boeken in deze serie wil markeren als voltooid?", "MessageConfirmMarkSeriesFinished": "Weet je zeker dat je alle boeken in deze serie wil markeren als voltooid?",
"MessageConfirmMarkSeriesNotFinished": "Weet je zeker dat je alle boeken in deze serie wil markeren als niet voltooid?", "MessageConfirmMarkSeriesNotFinished": "Weet je zeker dat je alle boeken in deze serie wil markeren als niet voltooid?",
"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?",
"MessageConfirmRemoveAllChapters": "Weet je zeker dat je alle hoofdstukken wil verwijderen?", "MessageConfirmRemoveAllChapters": "Weet je zeker dat je alle hoofdstukken wil verwijderen?",
"MessageConfirmRemoveAuthor": "Weet je zeker dat je auteur \"{0}\" wil verwijderen?", "MessageConfirmRemoveAuthor": "Weet je zeker dat je auteur \"{0}\" wil verwijderen?",
"MessageConfirmRemoveCollection": "Weet je zeker dat je de collectie \"{0}\" wil verwijderen?", "MessageConfirmRemoveCollection": "Weet je zeker dat je de collectie \"{0}\" wil verwijderen?",
"MessageConfirmRemoveEpisode": "Weet je zeker dat je de aflevering \"{0}\" wil verwijderen?", "MessageConfirmRemoveEpisode": "Weet je zeker dat je de aflevering \"{0}\" wil verwijderen?",
"MessageConfirmRemoveEpisodes": "Weet je zeker dat je {0} afleveringen wil verwijderen?", "MessageConfirmRemoveEpisodes": "Weet je zeker dat je {0} afleveringen wil verwijderen?",
"MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "Weet je zeker dat je verteller \"{0}\" wil verwijderen?", "MessageConfirmRemoveNarrator": "Weet je zeker dat je verteller \"{0}\" wil verwijderen?",
"MessageConfirmRemovePlaylist": "Weet je zeker dat je je afspeellijst \"{0}\" wil verwijderen?", "MessageConfirmRemovePlaylist": "Weet je zeker dat je je afspeellijst \"{0}\" wil verwijderen?",
"MessageConfirmRenameGenre": "Weet je zeker dat je genre \"{0}\" wil hernoemen naar \"{1}\" voor alle onderdelen?", "MessageConfirmRenameGenre": "Weet je zeker dat je genre \"{0}\" wil hernoemen naar \"{1}\" voor alle onderdelen?",
@ -546,6 +581,7 @@
"MessageConfirmRenameTag": "Weet je zeker dat je tag \"{0}\" wil hernoemen naar\"{1}\" voor alle onderdelen?", "MessageConfirmRenameTag": "Weet je zeker dat je tag \"{0}\" wil hernoemen naar\"{1}\" voor alle onderdelen?",
"MessageConfirmRenameTagMergeNote": "Opmerking: Deze tag bestaat al, dus zullen ze worden samengevoegd.", "MessageConfirmRenameTagMergeNote": "Opmerking: Deze tag bestaat al, dus zullen ze worden samengevoegd.",
"MessageConfirmRenameTagWarning": "Waarschuwing! Een gelijknamige tag met ander hoofdlettergebruik bestaat al: \"{0}\".", "MessageConfirmRenameTagWarning": "Waarschuwing! Een gelijknamige tag met ander hoofdlettergebruik bestaat al: \"{0}\".",
"MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?",
"MessageConfirmSendEbookToDevice": "Weet je zeker dat je {0} ebook \"{1}\" naar apparaat \"{2}\" wil sturen?", "MessageConfirmSendEbookToDevice": "Weet je zeker dat je {0} ebook \"{1}\" naar apparaat \"{2}\" wil sturen?",
"MessageDownloadingEpisode": "Aflevering aan het dowloaden", "MessageDownloadingEpisode": "Aflevering aan het dowloaden",
"MessageDragFilesIntoTrackOrder": "Sleep bestanden in de juiste trackvolgorde", "MessageDragFilesIntoTrackOrder": "Sleep bestanden in de juiste trackvolgorde",
@ -616,6 +652,7 @@
"MessageRestoreBackupConfirm": "Weet je zeker dat je wil herstellen met behulp van de back-up gemaakt op", "MessageRestoreBackupConfirm": "Weet je zeker dat je wil herstellen met behulp van de back-up gemaakt op",
"MessageRestoreBackupWarning": "Herstellen met een back-up zal de volledige database in /config en de covers in /metadata/items & /metadata/authors overschrijven.<br /><br />Back-ups wijzigen geen bestanden in je bibliotheekmappen. Als je de serverinstelling gebruikt om covers en metadata in je bibliotheekmappen te bewaren dan worden deze niet geback-upt of overschreven.<br /><br />Alle clients die van je server gebruik maken zullen automatisch worden ververst.", "MessageRestoreBackupWarning": "Herstellen met een back-up zal de volledige database in /config en de covers in /metadata/items & /metadata/authors overschrijven.<br /><br />Back-ups wijzigen geen bestanden in je bibliotheekmappen. Als je de serverinstelling gebruikt om covers en metadata in je bibliotheekmappen te bewaren dan worden deze niet geback-upt of overschreven.<br /><br />Alle clients die van je server gebruik maken zullen automatisch worden ververst.",
"MessageSearchResultsFor": "Zoekresultaten voor", "MessageSearchResultsFor": "Zoekresultaten voor",
"MessageSelected": "{0} selected",
"MessageServerCouldNotBeReached": "Server niet bereikbaar", "MessageServerCouldNotBeReached": "Server niet bereikbaar",
"MessageSetChaptersFromTracksDescription": "Stel hoofdstukken in met ieder audiobestand als een hoofdstuk en de audiobestandsnaam als hoofdstuktitel", "MessageSetChaptersFromTracksDescription": "Stel hoofdstukken in met ieder audiobestand als een hoofdstuk en de audiobestandsnaam als hoofdstuktitel",
"MessageStartPlaybackAtTime": "Afspelen van \"{0}\" beginnen op {1}?", "MessageStartPlaybackAtTime": "Afspelen van \"{0}\" beginnen op {1}?",

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