mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-06-02 13:14:34 -04:00
Merge master
This commit is contained in:
commit
ab14b561f5
@ -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,8 +316,8 @@ 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: true,
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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" />
|
||||||
@ -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>
|
@ -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">, </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">, </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>
|
||||||
|
@ -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 -->
|
||||||
|
@ -15,8 +15,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<p v-if="book.author" class="text-gray-300 text-xs md:text-sm">by {{ book.author }}</p>
|
<p v-if="book.author" class="text-gray-300 text-xs md:text-sm">by {{ book.author }}</p>
|
||||||
<p v-if="book.narrator" class="text-gray-400 text-xs">Narrated by {{ book.narrator }}</p>
|
<p v-if="book.narrator" class="text-gray-400 text-xs">Narrated by {{ book.narrator }}</p>
|
||||||
<p v-if="book.duration" class="text-gray-400 text-xs">Runtime: {{ $elapsedPrettyExtended(book.duration * 60) }}</p>
|
<p v-if="book.duration" class="text-gray-400 text-xs">Runtime: {{ $elapsedPrettyExtended(bookDuration, false) }} {{ bookDurationComparison }}</p>
|
||||||
<div v-if="book.series && book.series.length" class="flex py-1 -mx-1">
|
<div v-if="book.series?.length" class="flex py-1 -mx-1">
|
||||||
<div v-for="(series, index) in book.series" :key="index" class="bg-white bg-opacity-10 rounded-full px-1 py-0.5 mx-1">
|
<div v-for="(series, index) in book.series" :key="index" class="bg-white bg-opacity-10 rounded-full px-1 py-0.5 mx-1">
|
||||||
<p class="leading-3 text-xs text-gray-400">
|
<p class="leading-3 text-xs text-gray-400">
|
||||||
{{ series.series }}<span v-if="series.sequence"> #{{ series.sequence }}</span>
|
{{ series.series }}<span v-if="series.sequence"> #{{ series.sequence }}</span>
|
||||||
@ -29,9 +29,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-else class="px-4 flex-grow">
|
<div v-else class="px-4 flex-grow">
|
||||||
<h1>
|
<h1>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">{{ book.title }}<widgets-explicit-indicator :explicit="book.explicit" /></div>
|
||||||
{{ book.title }}<widgets-explicit-indicator :explicit="book.explicit" />
|
|
||||||
</div>
|
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-base text-gray-300 whitespace-nowrap truncate">by {{ book.author }}</p>
|
<p class="text-base text-gray-300 whitespace-nowrap truncate">by {{ book.author }}</p>
|
||||||
<p v-if="book.genres" class="text-xs text-gray-400 leading-5">{{ book.genres.join(', ') }}</p>
|
<p v-if="book.genres" class="text-xs text-gray-400 leading-5">{{ book.genres.join(', ') }}</p>
|
||||||
@ -56,7 +54,8 @@ export default {
|
|||||||
default: () => {}
|
default: () => {}
|
||||||
},
|
},
|
||||||
isPodcast: Boolean,
|
isPodcast: Boolean,
|
||||||
bookCoverAspectRatio: Number
|
bookCoverAspectRatio: Number,
|
||||||
|
currentBookDuration: Number
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -65,12 +64,27 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
bookCovers() {
|
bookCovers() {
|
||||||
return this.book.covers ? this.book.covers || [] : []
|
return this.book.covers || []
|
||||||
|
},
|
||||||
|
bookDuration() {
|
||||||
|
return (this.book.duration || 0) * 60
|
||||||
|
},
|
||||||
|
bookDurationComparison() {
|
||||||
|
if (!this.book.duration || !this.currentBookDuration) return ''
|
||||||
|
const currentBookDurationMinutes = Math.floor(this.currentBookDuration / 60)
|
||||||
|
let differenceInMinutes = currentBookDurationMinutes - this.book.duration
|
||||||
|
if (differenceInMinutes < 0) {
|
||||||
|
differenceInMinutes = Math.abs(differenceInMinutes)
|
||||||
|
return `(${this.$elapsedPrettyExtended(differenceInMinutes * 60, false, false)} shorter)`
|
||||||
|
} else if (differenceInMinutes > 0) {
|
||||||
|
return `(${this.$elapsedPrettyExtended(differenceInMinutes * 60, false, false)} longer)`
|
||||||
|
}
|
||||||
|
return '(exact match)'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
selectMatch() {
|
selectMatch() {
|
||||||
var book = { ...this.book }
|
const book = { ...this.book }
|
||||||
book.cover = this.selectedCover
|
book.cover = this.selectedCover
|
||||||
this.$emit('select', book)
|
this.$emit('select', book)
|
||||||
},
|
},
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
|
||||||
@ -843,8 +844,8 @@ 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: true,
|
||||||
|
@ -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}")`
|
||||||
|
@ -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,
|
||||||
|
33
client/components/modals/RawCoverPreviewModal.vue
Normal file
33
client/components/modals/RawCoverPreviewModal.vue
Normal 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>
|
@ -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) {
|
||||||
|
@ -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 = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -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
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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: {
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-show="!processing" class="w-full max-h-full overflow-y-auto overflow-x-hidden matchListWrapper mt-4">
|
<div v-show="!processing" class="w-full max-h-full overflow-y-auto overflow-x-hidden matchListWrapper mt-4">
|
||||||
<template v-for="(res, index) in searchResults">
|
<template v-for="(res, index) in searchResults">
|
||||||
<cards-book-match-card :key="index" :book="res" :is-podcast="isPodcast" :book-cover-aspect-ratio="bookCoverAspectRatio" @select="selectMatch" />
|
<cards-book-match-card :key="index" :book="res" :current-book-duration="currentBookDuration" :is-podcast="isPodcast" :book-cover-aspect-ratio="bookCoverAspectRatio" @select="selectMatch" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedMatchOrig" class="absolute top-0 left-0 w-full bg-bg h-full px-2 py-6 md:p-8 max-h-full overflow-y-auto overflow-x-hidden">
|
<div v-if="selectedMatchOrig" class="absolute top-0 left-0 w-full bg-bg h-full px-2 py-6 md:p-8 max-h-full overflow-y-auto overflow-x-hidden">
|
||||||
@ -205,7 +205,7 @@ export default {
|
|||||||
processing: Boolean,
|
processing: Boolean,
|
||||||
libraryItem: {
|
libraryItem: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => { }
|
default: () => {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@ -290,13 +290,17 @@ export default {
|
|||||||
return this.$strings.LabelSearchTitle
|
return this.$strings.LabelSearchTitle
|
||||||
},
|
},
|
||||||
media() {
|
media() {
|
||||||
return this.libraryItem ? this.libraryItem.media || {} : {}
|
return this.libraryItem?.media || {}
|
||||||
},
|
},
|
||||||
mediaMetadata() {
|
mediaMetadata() {
|
||||||
return this.media.metadata || {}
|
return this.media.metadata || {}
|
||||||
},
|
},
|
||||||
|
currentBookDuration() {
|
||||||
|
if (this.isPodcast) return 0
|
||||||
|
return this.media.duration || 0
|
||||||
|
},
|
||||||
mediaType() {
|
mediaType() {
|
||||||
return this.libraryItem ? this.libraryItem.mediaType : null
|
return this.libraryItem?.mediaType || null
|
||||||
},
|
},
|
||||||
isPodcast() {
|
isPodcast() {
|
||||||
return this.mediaType == 'podcast'
|
return this.mediaType == 'podcast'
|
||||||
|
@ -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', 'txtFiles', 'opfFile', 'absMetadata']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
138
client/components/modals/libraries/LibraryScannerSettings.vue
Normal file
138
client/components/modals/libraries/LibraryScannerSettings.vue
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
<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 ? index + 1 : '' }}
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow px-4 py-3">{{ source.name }}</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',
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resetToDefault() {
|
||||||
|
this.metadataSourceMapped = []
|
||||||
|
for (const key in this.metadataSourceData) {
|
||||||
|
this.metadataSourceMapped.push({ ...this.metadataSourceData[key] })
|
||||||
|
}
|
||||||
|
this.$emit('update', this.getLibraryData())
|
||||||
|
},
|
||||||
|
getLibraryData() {
|
||||||
|
return {
|
||||||
|
settings: {
|
||||||
|
metadataPrecedence: this.metadataSourceMapped.map((source) => (source.include ? source.id : null)).filter((s) => s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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.push(unusedSourceData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
81
client/components/modals/libraries/LibraryTools.vue
Normal file
81
client/components/modals/libraries/LibraryTools.vue
Normal 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>
|
@ -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">
|
||||||
@ -93,7 +93,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 +104,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 +116,23 @@ 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) {
|
||||||
|
return this.itemEpisodes.some((downloadedEpisode) => {
|
||||||
|
if (episode.guid && downloadedEpisode.guid === episode.guid) return true
|
||||||
|
if (!downloadedEpisode.enclosure?.url) return false
|
||||||
|
return this.getCleanEpisodeUrl(downloadedEpisode.enclosure.url) === episode.cleanUrl
|
||||||
|
})
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
|
* 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 +169,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 +183,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()
|
||||||
},
|
},
|
||||||
|
@ -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
|
||||||
|
@ -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%;
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -26,7 +26,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=${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">, </span>
|
><span :key="author.id + '-comma'" v-if="index < bookAuthors.length - 1">, </span>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
@ -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: {
|
||||||
|
@ -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)
|
||||||
|
@ -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">, </span>
|
><span :key="author.id + '-comma'" v-if="index < bookAuthors.length - 1">, </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>
|
||||||
|
@ -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">
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<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()
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
|
||||||
@ -145,6 +145,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()
|
||||||
|
@ -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: {
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
|
@ -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>
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
4
client/package-lock.json
generated
4
client/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.4.4",
|
"version": "2.5.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.4.4",
|
"version": "2.5.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/axios": "^5.13.6",
|
"@nuxtjs/axios": "^5.13.6",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.4.4",
|
"version": "2.5.0",
|
||||||
|
"buildNumber": 1,
|
||||||
"description": "Self-hosted audiobook and podcast client",
|
"description": "Self-hosted audiobook and podcast client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -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
|
||||||
})
|
})
|
||||||
|
@ -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>
|
||||||
|
@ -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">
|
||||||
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -3,21 +3,21 @@
|
|||||||
<div class="w-full h-full overflow-y-auto px-2 py-6 lg:p-8">
|
<div 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">, </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">, </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">, </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">, </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>
|
||||||
|
|
||||||
@ -682,8 +682,8 @@ 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: true,
|
||||||
|
@ -5,6 +5,7 @@ import { supplant } from './utils'
|
|||||||
const defaultCode = 'en-us'
|
const defaultCode = 'en-us'
|
||||||
|
|
||||||
const languageCodeMap = {
|
const languageCodeMap = {
|
||||||
|
'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' },
|
||||||
|
@ -54,7 +54,7 @@ Vue.prototype.$secondsToTimestamp = (seconds, includeMs = false, alwaysIncludeHo
|
|||||||
return `${_hours}:${_minutes.toString().padStart(2, '0')}:${_seconds.toString().padStart(2, '0')}${msString}`
|
return `${_hours}:${_minutes.toString().padStart(2, '0')}:${_seconds.toString().padStart(2, '0')}${msString}`
|
||||||
}
|
}
|
||||||
|
|
||||||
Vue.prototype.$elapsedPrettyExtended = (seconds, useDays = true) => {
|
Vue.prototype.$elapsedPrettyExtended = (seconds, useDays = true, showSeconds = true) => {
|
||||||
if (isNaN(seconds) || seconds === null) return ''
|
if (isNaN(seconds) || seconds === null) return ''
|
||||||
seconds = Math.round(seconds)
|
seconds = Math.round(seconds)
|
||||||
|
|
||||||
@ -69,11 +69,16 @@ Vue.prototype.$elapsedPrettyExtended = (seconds, useDays = true) => {
|
|||||||
hours -= days * 24
|
hours -= days * 24
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If not showing seconds then round minutes up
|
||||||
|
if (minutes && seconds && !showSeconds) {
|
||||||
|
if (seconds >= 30) minutes++
|
||||||
|
}
|
||||||
|
|
||||||
const strs = []
|
const strs = []
|
||||||
if (days) strs.push(`${days}d`)
|
if (days) strs.push(`${days}d`)
|
||||||
if (hours) strs.push(`${hours}h`)
|
if (hours) strs.push(`${hours}h`)
|
||||||
if (minutes) strs.push(`${minutes}m`)
|
if (minutes) strs.push(`${minutes}m`)
|
||||||
if (seconds) strs.push(`${seconds}s`)
|
if (seconds && showSeconds) strs.push(`${seconds}s`)
|
||||||
return strs.join(' ')
|
return strs.join(' ')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
729
client/strings/da.json
Normal file
729
client/strings/da.json
Normal file
@ -0,0 +1,729 @@
|
|||||||
|
{
|
||||||
|
"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",
|
||||||
|
"HeaderAccount": "Konto",
|
||||||
|
"HeaderAdvanced": "Avanceret",
|
||||||
|
"HeaderAppriseNotificationSettings": "Apprise Notifikationsindstillinger",
|
||||||
|
"HeaderAudiobookTools": "Audiobog Filhåndteringsværktøjer",
|
||||||
|
"HeaderAudioTracks": "Lydspor",
|
||||||
|
"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",
|
||||||
|
"HeaderOpenRSSFeed": "Åbn RSS Feed",
|
||||||
|
"HeaderOtherFiles": "Andre Filer",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"LabelMediaPlayer": "Medieafspiller",
|
||||||
|
"LabelMediaType": "Medietype",
|
||||||
|
"LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority",
|
||||||
|
"LabelMetadataProvider": "Metadataudbyder",
|
||||||
|
"LabelMetaTag": "Meta-tag",
|
||||||
|
"LabelMetaTags": "Meta-tags",
|
||||||
|
"LabelMinute": "Minut",
|
||||||
|
"LabelMissing": "Mangler",
|
||||||
|
"LabelMissingParts": "Manglende dele",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"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?",
|
||||||
|
"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",
|
||||||
|
"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"
|
||||||
|
}
|
@ -1,7 +1,10 @@
|
|||||||
{
|
{
|
||||||
"ButtonAdd": "Hinzufügen",
|
"ButtonAdd": "Hinzufügen",
|
||||||
"ButtonAddChapters": "Kapitel hinzufügen",
|
"ButtonAddChapters": "Kapitel hinzufügen",
|
||||||
|
"ButtonAddDevice": "Add Device",
|
||||||
|
"ButtonAddLibrary": "Add Library",
|
||||||
"ButtonAddPodcasts": "Podcasts hinzufügen",
|
"ButtonAddPodcasts": "Podcasts hinzufügen",
|
||||||
|
"ButtonAddUser": "Add User",
|
||||||
"ButtonAddYourFirstLibrary": "Erstelle deine erste Bibliothek",
|
"ButtonAddYourFirstLibrary": "Erstelle deine erste Bibliothek",
|
||||||
"ButtonApply": "Übernehmen",
|
"ButtonApply": "Übernehmen",
|
||||||
"ButtonApplyChapters": "Kapitel anwenden",
|
"ButtonApplyChapters": "Kapitel anwenden",
|
||||||
@ -59,6 +62,7 @@
|
|||||||
"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": "Reset to default",
|
||||||
"ButtonRestore": "Wiederherstellen",
|
"ButtonRestore": "Wiederherstellen",
|
||||||
"ButtonSave": "Speichern",
|
"ButtonSave": "Speichern",
|
||||||
"ButtonSaveAndClose": "Speichern & Schließen",
|
"ButtonSaveAndClose": "Speichern & Schließen",
|
||||||
@ -122,6 +126,7 @@
|
|||||||
"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",
|
||||||
@ -176,8 +181,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": "Admin users only",
|
||||||
"LabelAll": "Alle",
|
"LabelAll": "Alle",
|
||||||
"LabelAllUsers": "Alle Benutzer",
|
"LabelAllUsers": "Alle Benutzer",
|
||||||
|
"LabelAllUsersExcludingGuests": "All users excluding guests",
|
||||||
|
"LabelAllUsersIncludingGuests": "All users including guests",
|
||||||
"LabelAlreadyInYourLibrary": "In der Bibliothek vorhanden",
|
"LabelAlreadyInYourLibrary": "In der Bibliothek vorhanden",
|
||||||
"LabelAppend": "Anhängen",
|
"LabelAppend": "Anhängen",
|
||||||
"LabelAuthor": "Autor",
|
"LabelAuthor": "Autor",
|
||||||
@ -200,6 +208,7 @@
|
|||||||
"LabelChapters": "Kapitel",
|
"LabelChapters": "Kapitel",
|
||||||
"LabelChaptersFound": "gefundene Kapitel",
|
"LabelChaptersFound": "gefundene Kapitel",
|
||||||
"LabelChapterTitle": "Kapitelüberschrift",
|
"LabelChapterTitle": "Kapitelüberschrift",
|
||||||
|
"LabelClickForMoreInfo": "Click for more info",
|
||||||
"LabelClosePlayer": "Player schließen",
|
"LabelClosePlayer": "Player schließen",
|
||||||
"LabelCodec": "Codec",
|
"LabelCodec": "Codec",
|
||||||
"LabelCollapseSeries": "Serien zusammenfassen",
|
"LabelCollapseSeries": "Serien zusammenfassen",
|
||||||
@ -218,10 +227,12 @@
|
|||||||
"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": "Device is available to...",
|
||||||
"LabelDirectory": "Verzeichnis",
|
"LabelDirectory": "Verzeichnis",
|
||||||
"LabelDiscFromFilename": "CD aus dem Dateinamen",
|
"LabelDiscFromFilename": "CD aus dem Dateinamen",
|
||||||
"LabelDiscFromMetadata": "CD aus den Metadaten",
|
"LabelDiscFromMetadata": "CD aus den Metadaten",
|
||||||
@ -256,6 +267,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",
|
||||||
@ -266,6 +278,7 @@
|
|||||||
"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",
|
||||||
@ -305,6 +318,7 @@
|
|||||||
"LabelLookForNewEpisodesAfterDate": "Suchen nach neuen Episoden nach diesem Datum",
|
"LabelLookForNewEpisodesAfterDate": "Suchen nach neuen Episoden nach diesem Datum",
|
||||||
"LabelMediaPlayer": "Mediaplayer",
|
"LabelMediaPlayer": "Mediaplayer",
|
||||||
"LabelMediaType": "Medientyp",
|
"LabelMediaType": "Medientyp",
|
||||||
|
"LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority",
|
||||||
"LabelMetadataProvider": "Metadatenanbieter",
|
"LabelMetadataProvider": "Metadatenanbieter",
|
||||||
"LabelMetaTag": "Meta Schlagwort",
|
"LabelMetaTag": "Meta Schlagwort",
|
||||||
"LabelMetaTags": "Meta Tags",
|
"LabelMetaTags": "Meta Tags",
|
||||||
@ -384,6 +398,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": "Select users",
|
||||||
"LabelSendEbookToDevice": "E-Book senden an...",
|
"LabelSendEbookToDevice": "E-Book senden an...",
|
||||||
"LabelSequence": "Reihenfolge",
|
"LabelSequence": "Reihenfolge",
|
||||||
"LabelSeries": "Serien",
|
"LabelSeries": "Serien",
|
||||||
@ -410,16 +425,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 +438,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",
|
||||||
@ -523,30 +532,34 @@
|
|||||||
"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?",
|
||||||
|
"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,9 +623,9 @@
|
|||||||
"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",
|
||||||
|
@ -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",
|
||||||
@ -123,6 +127,7 @@
|
|||||||
"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",
|
||||||
@ -177,8 +182,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",
|
||||||
@ -201,6 +209,7 @@
|
|||||||
"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",
|
||||||
@ -219,10 +228,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",
|
||||||
@ -257,6 +268,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",
|
||||||
@ -267,6 +279,7 @@
|
|||||||
"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",
|
||||||
@ -306,6 +319,7 @@
|
|||||||
"LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date",
|
"LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date",
|
||||||
"LabelMediaPlayer": "Media Player",
|
"LabelMediaPlayer": "Media Player",
|
||||||
"LabelMediaType": "Media Type",
|
"LabelMediaType": "Media Type",
|
||||||
|
"LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority",
|
||||||
"LabelMetadataProvider": "Metadata Provider",
|
"LabelMetadataProvider": "Metadata Provider",
|
||||||
"LabelMetaTag": "Meta Tag",
|
"LabelMetaTag": "Meta Tag",
|
||||||
"LabelMetaTags": "Meta Tags",
|
"LabelMetaTags": "Meta Tags",
|
||||||
@ -385,6 +399,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",
|
||||||
@ -411,16 +426,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 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",
|
||||||
@ -430,7 +439,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",
|
||||||
@ -528,12 +537,15 @@
|
|||||||
"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}\"?",
|
||||||
@ -547,6 +559,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",
|
||||||
|
@ -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",
|
||||||
@ -20,11 +23,11 @@
|
|||||||
"ButtonCreate": "Crear",
|
"ButtonCreate": "Crear",
|
||||||
"ButtonCreateBackup": "Crear Respaldo",
|
"ButtonCreateBackup": "Crear Respaldo",
|
||||||
"ButtonDelete": "Eliminar",
|
"ButtonDelete": "Eliminar",
|
||||||
"ButtonDownloadQueue": "Queue",
|
"ButtonDownloadQueue": "Fila",
|
||||||
"ButtonEdit": "Editar",
|
"ButtonEdit": "Editar",
|
||||||
"ButtonEditChapters": "Editar Capitulo",
|
"ButtonEditChapters": "Editar Capítulo",
|
||||||
"ButtonEditPodcast": "Editar Podcast",
|
"ButtonEditPodcast": "Editar Podcast",
|
||||||
"ButtonForceReScan": "Forzar Re-Escanear",
|
"ButtonForceReScan": "Forzar Re-Escaneo",
|
||||||
"ButtonFullPath": "Ruta de Acceso Completa",
|
"ButtonFullPath": "Ruta de Acceso Completa",
|
||||||
"ButtonHide": "Esconder",
|
"ButtonHide": "Esconder",
|
||||||
"ButtonHome": "Inicio",
|
"ButtonHome": "Inicio",
|
||||||
@ -34,13 +37,13 @@
|
|||||||
"ButtonLogout": "Cerrar Sesión",
|
"ButtonLogout": "Cerrar Sesión",
|
||||||
"ButtonLookup": "Buscar",
|
"ButtonLookup": "Buscar",
|
||||||
"ButtonManageTracks": "Administrar Pistas de Audio",
|
"ButtonManageTracks": "Administrar Pistas de Audio",
|
||||||
"ButtonMapChapterTitles": "Map Chapter Titles",
|
"ButtonMapChapterTitles": "Asignar Títulos a Capítulos",
|
||||||
"ButtonMatchAllAuthors": "Encontrar Todos los Autores",
|
"ButtonMatchAllAuthors": "Encontrar Todos los Autores",
|
||||||
"ButtonMatchBooks": "Encontrar Libros",
|
"ButtonMatchBooks": "Encontrar Libros",
|
||||||
"ButtonNevermind": "Olvidar",
|
"ButtonNevermind": "Olvidar",
|
||||||
"ButtonOk": "Ok",
|
"ButtonOk": "Ok",
|
||||||
"ButtonOpenFeed": "Abrir Fuente",
|
"ButtonOpenFeed": "Abrir Fuente",
|
||||||
"ButtonOpenManager": "Open Manager",
|
"ButtonOpenManager": "Abrir Editor",
|
||||||
"ButtonPlay": "Reproducir",
|
"ButtonPlay": "Reproducir",
|
||||||
"ButtonPlaying": "Reproduciendo",
|
"ButtonPlaying": "Reproduciendo",
|
||||||
"ButtonPlaylists": "Listas de Reproducción",
|
"ButtonPlaylists": "Listas de Reproducción",
|
||||||
@ -55,10 +58,11 @@
|
|||||||
"ButtonRemoveAll": "Remover Todos",
|
"ButtonRemoveAll": "Remover Todos",
|
||||||
"ButtonRemoveAllLibraryItems": "Remover Todos los Elementos de la Biblioteca",
|
"ButtonRemoveAllLibraryItems": "Remover Todos los Elementos de la Biblioteca",
|
||||||
"ButtonRemoveFromContinueListening": "Remover de Continuar Escuchando",
|
"ButtonRemoveFromContinueListening": "Remover de Continuar Escuchando",
|
||||||
"ButtonRemoveFromContinueReading": "Remove from Continue Reading",
|
"ButtonRemoveFromContinueReading": "Remover de Continuar Leyendo",
|
||||||
"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",
|
||||||
@ -74,7 +78,7 @@
|
|||||||
"ButtonStartM4BEncode": "Iniciar Codificación M4B",
|
"ButtonStartM4BEncode": "Iniciar Codificación M4B",
|
||||||
"ButtonStartMetadataEmbed": "Iniciar la Inserción de Metadata",
|
"ButtonStartMetadataEmbed": "Iniciar la Inserción de Metadata",
|
||||||
"ButtonSubmit": "Enviar",
|
"ButtonSubmit": "Enviar",
|
||||||
"ButtonTest": "Test",
|
"ButtonTest": "Prueba",
|
||||||
"ButtonUpload": "Subir",
|
"ButtonUpload": "Subir",
|
||||||
"ButtonUploadBackup": "Subir Respaldo",
|
"ButtonUploadBackup": "Subir Respaldo",
|
||||||
"ButtonUploadCover": "Subir Portada",
|
"ButtonUploadCover": "Subir Portada",
|
||||||
@ -98,12 +102,12 @@
|
|||||||
"HeaderCurrentDownloads": "Descargando Actualmente",
|
"HeaderCurrentDownloads": "Descargando Actualmente",
|
||||||
"HeaderDetails": "Detalles",
|
"HeaderDetails": "Detalles",
|
||||||
"HeaderDownloadQueue": "Lista de Descarga",
|
"HeaderDownloadQueue": "Lista de Descarga",
|
||||||
"HeaderEbookFiles": "Ebook Files",
|
"HeaderEbookFiles": "Archivos de Ebook",
|
||||||
"HeaderEmail": "Email",
|
"HeaderEmail": "Email",
|
||||||
"HeaderEmailSettings": "Email Settings",
|
"HeaderEmailSettings": "Opciones de Email",
|
||||||
"HeaderEpisodes": "Episodios",
|
"HeaderEpisodes": "Episodios",
|
||||||
"HeaderEreaderDevices": "Ereader Devices",
|
"HeaderEreaderDevices": "Dispositivos Ereader",
|
||||||
"HeaderEreaderSettings": "Ereader Settings",
|
"HeaderEreaderSettings": "Opciones de Ereader",
|
||||||
"HeaderFiles": "Elemento",
|
"HeaderFiles": "Elemento",
|
||||||
"HeaderFindChapters": "Buscar Capitulo",
|
"HeaderFindChapters": "Buscar Capitulo",
|
||||||
"HeaderIgnoredFiles": "Ignorar Elemento",
|
"HeaderIgnoredFiles": "Ignorar Elemento",
|
||||||
@ -120,8 +124,9 @@
|
|||||||
"HeaderLogs": "Logs",
|
"HeaderLogs": "Logs",
|
||||||
"HeaderManageGenres": "Administrar Géneros",
|
"HeaderManageGenres": "Administrar Géneros",
|
||||||
"HeaderManageTags": "Administrar Etiquetas",
|
"HeaderManageTags": "Administrar Etiquetas",
|
||||||
"HeaderMapDetails": "Map details",
|
"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",
|
||||||
@ -129,7 +134,7 @@
|
|||||||
"HeaderOpenRSSFeed": "Abrir fuente RSS",
|
"HeaderOpenRSSFeed": "Abrir fuente RSS",
|
||||||
"HeaderOtherFiles": "Otros Archivos",
|
"HeaderOtherFiles": "Otros Archivos",
|
||||||
"HeaderPermissions": "Permisos",
|
"HeaderPermissions": "Permisos",
|
||||||
"HeaderPlayerQueue": "Player Queue",
|
"HeaderPlayerQueue": "Fila del Reproductor",
|
||||||
"HeaderPlaylist": "Lista de Reproducción",
|
"HeaderPlaylist": "Lista de Reproducción",
|
||||||
"HeaderPlaylistItems": "Elementos de Lista de Reproducción",
|
"HeaderPlaylistItems": "Elementos de Lista de Reproducción",
|
||||||
"HeaderPodcastsToAdd": "Podcasts para agregar",
|
"HeaderPodcastsToAdd": "Podcasts para agregar",
|
||||||
@ -139,13 +144,13 @@
|
|||||||
"HeaderRSSFeedGeneral": "Detalles RSS",
|
"HeaderRSSFeedGeneral": "Detalles RSS",
|
||||||
"HeaderRSSFeedIsOpen": "Fuente RSS esta abierta",
|
"HeaderRSSFeedIsOpen": "Fuente RSS esta abierta",
|
||||||
"HeaderRSSFeeds": "RSS Feeds",
|
"HeaderRSSFeeds": "RSS Feeds",
|
||||||
"HeaderSavedMediaProgress": "Guardar Progreso de multimedia",
|
"HeaderSavedMediaProgress": "Guardar Progreso de Multimedia",
|
||||||
"HeaderSchedule": "Horario",
|
"HeaderSchedule": "Horario",
|
||||||
"HeaderScheduleLibraryScans": "Programar Escaneo Automático de Biblioteca",
|
"HeaderScheduleLibraryScans": "Programar Escaneo Automático de Biblioteca",
|
||||||
"HeaderSession": "Session",
|
"HeaderSession": "Session",
|
||||||
"HeaderSetBackupSchedule": "Programar Respaldo",
|
"HeaderSetBackupSchedule": "Programar Respaldo",
|
||||||
"HeaderSettings": "Configuraciones",
|
"HeaderSettings": "Configuraciones",
|
||||||
"HeaderSettingsDisplay": "Display",
|
"HeaderSettingsDisplay": "Interfaz",
|
||||||
"HeaderSettingsExperimental": "Funciones Experimentales",
|
"HeaderSettingsExperimental": "Funciones Experimentales",
|
||||||
"HeaderSettingsGeneral": "General",
|
"HeaderSettingsGeneral": "General",
|
||||||
"HeaderSettingsScanner": "Escáner",
|
"HeaderSettingsScanner": "Escáner",
|
||||||
@ -156,28 +161,31 @@
|
|||||||
"HeaderStatsRecentSessions": "Sesiones Recientes",
|
"HeaderStatsRecentSessions": "Sesiones Recientes",
|
||||||
"HeaderStatsTop10Authors": "Top 10 Autores",
|
"HeaderStatsTop10Authors": "Top 10 Autores",
|
||||||
"HeaderStatsTop5Genres": "Top 5 Géneros",
|
"HeaderStatsTop5Genres": "Top 5 Géneros",
|
||||||
"HeaderTableOfContents": "Table of Contents",
|
"HeaderTableOfContents": "Tabla de Contenidos",
|
||||||
"HeaderTools": "Herramientas",
|
"HeaderTools": "Herramientas",
|
||||||
"HeaderUpdateAccount": "Actualizar Cuenta",
|
"HeaderUpdateAccount": "Actualizar Cuenta",
|
||||||
"HeaderUpdateAuthor": "Actualizar Autor",
|
"HeaderUpdateAuthor": "Actualizar Autor",
|
||||||
"HeaderUpdateDetails": "Actualizar Detalles",
|
"HeaderUpdateDetails": "Actualizar Detalles",
|
||||||
"HeaderUpdateLibrary": "Actualizar Biblioteca",
|
"HeaderUpdateLibrary": "Actualizar Biblioteca",
|
||||||
"HeaderUsers": "Usuarios",
|
"HeaderUsers": "Usuarios",
|
||||||
"HeaderYourStats": "Tus Estáticas",
|
"HeaderYourStats": "Tus Estadísticas",
|
||||||
"LabelAbridged": "Abridged",
|
"LabelAbridged": "Abreviado",
|
||||||
"LabelAccountType": "Tipo de Cuenta",
|
"LabelAccountType": "Tipo de Cuenta",
|
||||||
"LabelAccountTypeAdmin": "Administrador",
|
"LabelAccountTypeAdmin": "Administrador",
|
||||||
"LabelAccountTypeGuest": "Invitado",
|
"LabelAccountTypeGuest": "Invitado",
|
||||||
"LabelAccountTypeUser": "Usuario",
|
"LabelAccountTypeUser": "Usuario",
|
||||||
"LabelActivity": "Actividad",
|
"LabelActivity": "Actividad",
|
||||||
"LabelAdded": "Added",
|
"LabelAdded": "Añadido",
|
||||||
"LabelAddedAt": "Añadido",
|
"LabelAddedAt": "Añadido",
|
||||||
"LabelAddToCollection": "Añadido a la Colección",
|
"LabelAddToCollection": "Añadido a la Colección",
|
||||||
"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",
|
||||||
@ -186,58 +194,61 @@
|
|||||||
"LabelAuthors": "Autores",
|
"LabelAuthors": "Autores",
|
||||||
"LabelAutoDownloadEpisodes": "Descargar Episodios Automáticamente",
|
"LabelAutoDownloadEpisodes": "Descargar Episodios Automáticamente",
|
||||||
"LabelBackToUser": "Regresar a Usuario",
|
"LabelBackToUser": "Regresar a Usuario",
|
||||||
"LabelBackupLocation": "Backup Location",
|
"LabelBackupLocation": "Ubicación del Respaldo",
|
||||||
"LabelBackupsEnableAutomaticBackups": "Habilitar Respaldo Automático",
|
"LabelBackupsEnableAutomaticBackups": "Habilitar Respaldo Automático",
|
||||||
"LabelBackupsEnableAutomaticBackupsHelp": "Respaldo Guardado en /metadata/backups",
|
"LabelBackupsEnableAutomaticBackupsHelp": "Respaldo Guardado en /metadata/backups",
|
||||||
"LabelBackupsMaxBackupSize": "Tamaño Máximo de Respaldos (en GB)",
|
"LabelBackupsMaxBackupSize": "Tamaño Máximo de Respaldos (en GB)",
|
||||||
"LabelBackupsMaxBackupSizeHelp": "Como protección contra una configuración errónea, los respaldos fallaran si se excede el tamaño configurado.",
|
"LabelBackupsMaxBackupSizeHelp": "Como protección contra una configuración errónea, los respaldos fallarán si se excede el tamaño configurado.",
|
||||||
"LabelBackupsNumberToKeep": "Numero de respaldos para conservar",
|
"LabelBackupsNumberToKeep": "Numero de respaldos para conservar",
|
||||||
"LabelBackupsNumberToKeepHelp": "Solamente 1 respaldo se removerá a la vez. Si tiene mas respaldos guardados, necesita 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",
|
||||||
"LabelChangePassword": "Cambiar Contraseña",
|
"LabelChangePassword": "Cambiar Contraseña",
|
||||||
"LabelChannels": "Canales",
|
"LabelChannels": "Canales",
|
||||||
"LabelChapters": "Capitulos",
|
"LabelChapters": "Capítulos",
|
||||||
"LabelChaptersFound": "Capitulo Encontrado",
|
"LabelChaptersFound": "Capítulo Encontrado",
|
||||||
"LabelChapterTitle": "Titulo del Capitulo",
|
"LabelChapterTitle": "Titulo del Capítulo",
|
||||||
"LabelClosePlayer": "Close player",
|
"LabelClickForMoreInfo": "Click for more info",
|
||||||
|
"LabelClosePlayer": "Cerrar Reproductor",
|
||||||
"LabelCodec": "Codec",
|
"LabelCodec": "Codec",
|
||||||
"LabelCollapseSeries": "Colapsar Series",
|
"LabelCollapseSeries": "Colapsar Serie",
|
||||||
"LabelCollection": "Collection",
|
"LabelCollection": "Colección",
|
||||||
"LabelCollections": "Colecciones",
|
"LabelCollections": "Colecciones",
|
||||||
"LabelComplete": "Completo",
|
"LabelComplete": "Completo",
|
||||||
"LabelConfirmPassword": "Confirmar Contraseña",
|
"LabelConfirmPassword": "Confirmar Contraseña",
|
||||||
"LabelContinueListening": "Continuar Escuchando",
|
"LabelContinueListening": "Continuar Escuchando",
|
||||||
"LabelContinueReading": "Continue Reading",
|
"LabelContinueReading": "Continuar Leyendo",
|
||||||
"LabelContinueSeries": "Continuar Series",
|
"LabelContinueSeries": "Continuar Serie",
|
||||||
"LabelCover": "Portada",
|
"LabelCover": "Portada",
|
||||||
"LabelCoverImageURL": "URL de Imagen de Portada",
|
"LabelCoverImageURL": "URL de Imagen de Portada",
|
||||||
"LabelCreatedAt": "Creado",
|
"LabelCreatedAt": "Creado",
|
||||||
"LabelCronExpression": "Cron Expression",
|
"LabelCronExpression": "Expresión de Cron",
|
||||||
"LabelCurrent": "Actual",
|
"LabelCurrent": "Actual",
|
||||||
"LabelCurrently": "En este momento:",
|
"LabelCurrently": "En este momento:",
|
||||||
"LabelCustomCronExpression": "Custom Cron Expression:",
|
"LabelCustomCronExpression": "Expresión de Cron Personalizada:",
|
||||||
"LabelDatetime": "Datetime",
|
"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",
|
||||||
"LabelDiscover": "Discover",
|
"LabelDiscover": "Descubrir",
|
||||||
"LabelDownload": "Descargar",
|
"LabelDownload": "Descargar",
|
||||||
"LabelDownloadNEpisodes": "Download {0} episodes",
|
"LabelDownloadNEpisodes": "Descargar {0} episodios",
|
||||||
"LabelDuration": "Duración",
|
"LabelDuration": "Duración",
|
||||||
"LabelDurationFound": "Duración Comprobada:",
|
"LabelDurationFound": "Duración Comprobada:",
|
||||||
"LabelEbook": "Ebook",
|
"LabelEbook": "Ebook",
|
||||||
"LabelEbooks": "Ebooks",
|
"LabelEbooks": "Ebooks",
|
||||||
"LabelEdit": "Editar",
|
"LabelEdit": "Editar",
|
||||||
"LabelEmail": "Email",
|
"LabelEmail": "Email",
|
||||||
"LabelEmailSettingsFromAddress": "From Address",
|
"LabelEmailSettingsFromAddress": "Remitente",
|
||||||
"LabelEmailSettingsSecure": "Secure",
|
"LabelEmailSettingsSecure": "Seguridad",
|
||||||
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
|
"LabelEmailSettingsSecureHelp": "Si está activado, se usará TLS para conectarse al servidor. Si está apagado, se usará TLS si su servidor tiene soporte para la extensión STARTTLS. En la mayoría de los casos, puede dejar esta opción activada si se está conectando al puerto 465. Apáguela en el caso de usar los puertos 587 o 25. (de nodemailer.com/smtp/#authentication)",
|
||||||
"LabelEmailSettingsTestAddress": "Test Address",
|
"LabelEmailSettingsTestAddress": "Probar Dirección",
|
||||||
"LabelEmbeddedCover": "Portada Integrada",
|
"LabelEmbeddedCover": "Portada Integrada",
|
||||||
"LabelEnable": "Habilitar",
|
"LabelEnable": "Habilitar",
|
||||||
"LabelEnd": "Fin",
|
"LabelEnd": "Fin",
|
||||||
@ -256,16 +267,18 @@
|
|||||||
"LabelFinished": "Terminado",
|
"LabelFinished": "Terminado",
|
||||||
"LabelFolder": "Carpeta",
|
"LabelFolder": "Carpeta",
|
||||||
"LabelFolders": "Carpetas",
|
"LabelFolders": "Carpetas",
|
||||||
"LabelFontScale": "Font scale",
|
"LabelFontFamily": "Familia tipográfica",
|
||||||
|
"LabelFontScale": "Tamaño de Fuente",
|
||||||
"LabelFormat": "Formato",
|
"LabelFormat": "Formato",
|
||||||
"LabelGenre": "Genero",
|
"LabelGenre": "Genero",
|
||||||
"LabelGenres": "Géneros",
|
"LabelGenres": "Géneros",
|
||||||
"LabelHardDeleteFile": "Eliminar Definitivamente",
|
"LabelHardDeleteFile": "Eliminar Definitivamente",
|
||||||
"LabelHasEbook": "Has ebook",
|
"LabelHasEbook": "Tiene Ebook",
|
||||||
"LabelHasSupplementaryEbook": "Has supplementary ebook",
|
"LabelHasSupplementaryEbook": "Tiene Ebook Suplementario",
|
||||||
"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",
|
||||||
@ -276,43 +289,44 @@
|
|||||||
"LabelIntervalEvery2Hours": "Cada 2 Horas",
|
"LabelIntervalEvery2Hours": "Cada 2 Horas",
|
||||||
"LabelIntervalEvery30Minutes": "Cada 30 minutos",
|
"LabelIntervalEvery30Minutes": "Cada 30 minutos",
|
||||||
"LabelIntervalEvery6Hours": "Cada 6 Horas",
|
"LabelIntervalEvery6Hours": "Cada 6 Horas",
|
||||||
"LabelIntervalEveryDay": "Cada Dia",
|
"LabelIntervalEveryDay": "Cada Día",
|
||||||
"LabelIntervalEveryHour": "Cada Hora",
|
"LabelIntervalEveryHour": "Cada Hora",
|
||||||
"LabelInvalidParts": "Partes Invalidas",
|
"LabelInvalidParts": "Partes Inválidas",
|
||||||
"LabelInvert": "Invert",
|
"LabelInvert": "Invertir",
|
||||||
"LabelItem": "Elemento",
|
"LabelItem": "Elemento",
|
||||||
"LabelLanguage": "Lenguaje",
|
"LabelLanguage": "Lenguaje",
|
||||||
"LabelLanguageDefaultServer": "Lenguaje Predeterminado del Servidor",
|
"LabelLanguageDefaultServer": "Lenguaje Predeterminado del Servidor",
|
||||||
"LabelLastBookAdded": "Last Book Added",
|
"LabelLastBookAdded": "Último Libro Agregado",
|
||||||
"LabelLastBookUpdated": "Last Book Updated",
|
"LabelLastBookUpdated": "Último Libro Actualizado",
|
||||||
"LabelLastSeen": "Ultima Vez Visto",
|
"LabelLastSeen": "Última Vez Visto",
|
||||||
"LabelLastTime": "Ultima Vez",
|
"LabelLastTime": "Última Vez",
|
||||||
"LabelLastUpdate": "Ultima Actualización",
|
"LabelLastUpdate": "Última Actualización",
|
||||||
"LabelLayout": "Layout",
|
"LabelLayout": "Distribución",
|
||||||
"LabelLayoutSinglePage": "Single page",
|
"LabelLayoutSinglePage": "Una Página",
|
||||||
"LabelLayoutSplitPage": "Split page",
|
"LabelLayoutSplitPage": "Dos Páginas",
|
||||||
"LabelLess": "Menos",
|
"LabelLess": "Menos",
|
||||||
"LabelLibrariesAccessibleToUser": "Bibliotecas Disponibles para el Usuario",
|
"LabelLibrariesAccessibleToUser": "Bibliotecas Disponibles para el Usuario",
|
||||||
"LabelLibrary": "Biblioteca",
|
"LabelLibrary": "Biblioteca",
|
||||||
"LabelLibraryItem": "Elemento de Biblioteca",
|
"LabelLibraryItem": "Elemento de Biblioteca",
|
||||||
"LabelLibraryName": "Nombre de Biblioteca",
|
"LabelLibraryName": "Nombre de Biblioteca",
|
||||||
"LabelLimit": "Limites",
|
"LabelLimit": "Limites",
|
||||||
"LabelLineSpacing": "Line spacing",
|
"LabelLineSpacing": "Interlineado",
|
||||||
"LabelListenAgain": "Escuchar Otra Vez",
|
"LabelListenAgain": "Escuchar Otra Vez",
|
||||||
"LabelLogLevelDebug": "Debug",
|
"LabelLogLevelDebug": "Debug",
|
||||||
"LabelLogLevelInfo": "Info",
|
"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",
|
||||||
"LabelMediaPlayer": "Media Player",
|
"LabelMediaPlayer": "Reproductor de Medios",
|
||||||
"LabelMediaType": "Tipo de Multimedia",
|
"LabelMediaType": "Tipo de Multimedia",
|
||||||
|
"LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority",
|
||||||
"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",
|
||||||
"LabelMore": "Mas",
|
"LabelMore": "Más",
|
||||||
"LabelMoreInfo": "Mas Información",
|
"LabelMoreInfo": "Más Información",
|
||||||
"LabelName": "Nombre",
|
"LabelName": "Nombre",
|
||||||
"LabelNarrator": "Narrador",
|
"LabelNarrator": "Narrador",
|
||||||
"LabelNarrators": "Narradores",
|
"LabelNarrators": "Narradores",
|
||||||
@ -322,17 +336,17 @@
|
|||||||
"LabelNewPassword": "Nueva Contraseña",
|
"LabelNewPassword": "Nueva Contraseña",
|
||||||
"LabelNextBackupDate": "Fecha del Siguiente Respaldo",
|
"LabelNextBackupDate": "Fecha del Siguiente Respaldo",
|
||||||
"LabelNextScheduledRun": "Próxima Ejecución Programada",
|
"LabelNextScheduledRun": "Próxima Ejecución Programada",
|
||||||
"LabelNoEpisodesSelected": "No episodes selected",
|
"LabelNoEpisodesSelected": "Ningún Episodio Seleccionado",
|
||||||
"LabelNotes": "Notas",
|
"LabelNotes": "Notas",
|
||||||
"LabelNotFinished": "No Terminado",
|
"LabelNotFinished": "No Terminado",
|
||||||
"LabelNotificationAppriseURL": "Apprise URL(s)",
|
"LabelNotificationAppriseURL": "URL(s) de Apprise",
|
||||||
"LabelNotificationAvailableVariables": "Variables Disponibles",
|
"LabelNotificationAvailableVariables": "Variables Disponibles",
|
||||||
"LabelNotificationBodyTemplate": "Plantilla de Cuerpo",
|
"LabelNotificationBodyTemplate": "Plantilla de Cuerpo",
|
||||||
"LabelNotificationEvent": "Evento de Notificación",
|
"LabelNotificationEvent": "Evento de Notificación",
|
||||||
"LabelNotificationsMaxFailedAttempts": "Máximo de Intentos Fallidos",
|
"LabelNotificationsMaxFailedAttempts": "Máximo de Intentos Fallidos",
|
||||||
"LabelNotificationsMaxFailedAttemptsHelp": "Las notificaciones se desactivan después de fallar este numero de veces",
|
"LabelNotificationsMaxFailedAttemptsHelp": "Las notificaciones se desactivan después de fallar este número de veces",
|
||||||
"LabelNotificationsMaxQueueSize": "Tamaño máximo de la cola de notificación",
|
"LabelNotificationsMaxQueueSize": "Tamaño máximo de la cola de notificaciones",
|
||||||
"LabelNotificationsMaxQueueSizeHelp": "Las notificaciones están limitadas a 1 por segundo. Las notificaciones serán ignorados si llegan al numero máximo de cola para prevenir spam de eventos.",
|
"LabelNotificationsMaxQueueSizeHelp": "Las notificaciones están limitadas a 1 por segundo. Las notificaciones serán ignoradas si llegan al numero máximo de cola para prevenir spam de eventos.",
|
||||||
"LabelNotificationTitleTemplate": "Plantilla de Titulo",
|
"LabelNotificationTitleTemplate": "Plantilla de Titulo",
|
||||||
"LabelNotStarted": "Sin Iniciar",
|
"LabelNotStarted": "Sin Iniciar",
|
||||||
"LabelNumberOfBooks": "Numero de Libros",
|
"LabelNumberOfBooks": "Numero de Libros",
|
||||||
@ -354,97 +368,92 @@
|
|||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcasts",
|
"LabelPodcasts": "Podcasts",
|
||||||
"LabelPodcastType": "Tipo Podcast",
|
"LabelPodcastType": "Tipo Podcast",
|
||||||
"LabelPort": "Port",
|
"LabelPort": "Puerto",
|
||||||
"LabelPrefixesToIgnore": "Prefijos para Ignorar (no distingue entre mayúsculas y minúsculas.)",
|
"LabelPrefixesToIgnore": "Prefijos para Ignorar (no distingue entre mayúsculas y minúsculas.)",
|
||||||
"LabelPreventIndexing": "Evite que su fuente sea indexado por iTunes y Google podcast directories",
|
"LabelPreventIndexing": "Evite que su fuente sea indexada por los directorios de podcasts de iTunes y Google",
|
||||||
"LabelPrimaryEbook": "Primary ebook",
|
"LabelPrimaryEbook": "Ebook principal",
|
||||||
"LabelProgress": "Progreso",
|
"LabelProgress": "Progreso",
|
||||||
"LabelProvider": "Proveedor",
|
"LabelProvider": "Proveedor",
|
||||||
"LabelPubDate": "Fecha de Publicación",
|
"LabelPubDate": "Fecha de Publicación",
|
||||||
"LabelPublisher": "Editor",
|
"LabelPublisher": "Editor",
|
||||||
"LabelPublishYear": "Año de Publicación",
|
"LabelPublishYear": "Año de Publicación",
|
||||||
"LabelRead": "Read",
|
"LabelRead": "Leído",
|
||||||
"LabelReadAgain": "Read Again",
|
"LabelReadAgain": "Volver a leer",
|
||||||
"LabelReadEbookWithoutProgress": "Read ebook without keeping progress",
|
"LabelReadEbookWithoutProgress": "Leer Ebook sin guardar progreso",
|
||||||
"LabelRecentlyAdded": "Agregado Reciente",
|
"LabelRecentlyAdded": "Agregado Recientemente",
|
||||||
"LabelRecentSeries": "Series Recientes",
|
"LabelRecentSeries": "Series Recientes",
|
||||||
"LabelRecommended": "Recomendados",
|
"LabelRecommended": "Recomendados",
|
||||||
"LabelRegion": "Region",
|
"LabelRegion": "Región",
|
||||||
"LabelReleaseDate": "Fecha de Estreno",
|
"LabelReleaseDate": "Fecha de Estreno",
|
||||||
"LabelRemoveCover": "Remover Portada",
|
"LabelRemoveCover": "Remover Portada",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
|
"LabelRSSFeedCustomOwnerEmail": "Email de dueño personalizado",
|
||||||
"LabelRSSFeedCustomOwnerName": "Custom owner Name",
|
"LabelRSSFeedCustomOwnerName": "Nombre de dueño personalizado",
|
||||||
"LabelRSSFeedOpen": "Fuente RSS Abierta",
|
"LabelRSSFeedOpen": "Fuente RSS Abierta",
|
||||||
"LabelRSSFeedPreventIndexing": "Prevenir Indaxación",
|
"LabelRSSFeedPreventIndexing": "Prevenir Indexado",
|
||||||
"LabelRSSFeedSlug": "Fuente RSS Slug",
|
"LabelRSSFeedSlug": "Fuente RSS Slug",
|
||||||
"LabelRSSFeedURL": "URL de Fuente RSS",
|
"LabelRSSFeedURL": "URL de Fuente RSS",
|
||||||
"LabelSearchTerm": "Buscar Termino",
|
"LabelSearchTerm": "Buscar Termino",
|
||||||
"LabelSearchTitle": "Buscar Titulo",
|
"LabelSearchTitle": "Buscar Titulo",
|
||||||
"LabelSearchTitleOrASIN": "Buscar Titulo o ASIN",
|
"LabelSearchTitleOrASIN": "Buscar Título o ASIN",
|
||||||
"LabelSeason": "Temporada",
|
"LabelSeason": "Temporada",
|
||||||
"LabelSelectAllEpisodes": "Select all episodes",
|
"LabelSelectAllEpisodes": "Seleccionar todos los episodios",
|
||||||
"LabelSelectEpisodesShowing": "Select {0} episodes showing",
|
"LabelSelectEpisodesShowing": "Seleccionar los {0} episodios visibles",
|
||||||
"LabelSendEbookToDevice": "Send Ebook to...",
|
"LabelSelectUsers": "Select users",
|
||||||
|
"LabelSendEbookToDevice": "Enviar Ebook a...",
|
||||||
"LabelSequence": "Secuencia",
|
"LabelSequence": "Secuencia",
|
||||||
"LabelSeries": "Series",
|
"LabelSeries": "Series",
|
||||||
"LabelSeriesName": "Nombre de la Serie",
|
"LabelSeriesName": "Nombre de la Serie",
|
||||||
"LabelSeriesProgress": "Progreso de la Serie",
|
"LabelSeriesProgress": "Progreso de la Serie",
|
||||||
"LabelSetEbookAsPrimary": "Set as primary",
|
"LabelSetEbookAsPrimary": "Establecer como primario",
|
||||||
"LabelSetEbookAsSupplementary": "Set as supplementary",
|
"LabelSetEbookAsSupplementary": "Establecer como suplementario",
|
||||||
"LabelSettingsAudiobooksOnly": "Audiobooks only",
|
"LabelSettingsAudiobooksOnly": "Sólo Audiolibros",
|
||||||
"LabelSettingsAudiobooksOnlyHelp": "Enabling this setting will ignore ebook files unless they are inside an audiobook folder in which case they will be set as supplementary ebooks",
|
"LabelSettingsAudiobooksOnlyHelp": "Al activar esta opción se ignorarán los archivos de ebook a menos de que estén dentro de la carpeta de un audiolibro, en cuyo caso se marcarán como ebooks suplementarios",
|
||||||
"LabelSettingsBookshelfViewHelp": "Diseño Skeumorphic con Estantes de Madera",
|
"LabelSettingsBookshelfViewHelp": "Diseño Esqueuomorfo con Estantes de Madera",
|
||||||
"LabelSettingsChromecastSupport": "Soporte para Chromecast",
|
"LabelSettingsChromecastSupport": "Soporte para Chromecast",
|
||||||
"LabelSettingsDateFormat": "Formato de Fecha",
|
"LabelSettingsDateFormat": "Formato de Fecha",
|
||||||
"LabelSettingsDisableWatcher": "Deshabilitar Watcher",
|
"LabelSettingsDisableWatcher": "Deshabilitar Watcher",
|
||||||
"LabelSettingsDisableWatcherForLibrary": "Deshabilitar Watcher de Carpetas para esta biblioteca",
|
"LabelSettingsDisableWatcherForLibrary": "Deshabilitar Watcher de Carpetas para esta biblioteca",
|
||||||
"LabelSettingsDisableWatcherHelp": "Deshabilitar la función automática de agregar/actualizar los elementos, cuando se detecta cambio en los archivos. *Require Reiniciar el Servidor",
|
"LabelSettingsDisableWatcherHelp": "Deshabilitar la función de agregar/actualizar elementos automáticamente cuando se detectan cambios en los archivos. *Require Reiniciar el Servidor",
|
||||||
"LabelSettingsEnableWatcher": "Enable Watcher",
|
"LabelSettingsEnableWatcher": "Habilitar Watcher",
|
||||||
"LabelSettingsEnableWatcherForLibrary": "Enable folder watcher for library",
|
"LabelSettingsEnableWatcherForLibrary": "Habilitar Watcher para la carpeta de esta biblioteca",
|
||||||
"LabelSettingsEnableWatcherHelp": "Enables the automatic adding/updating of items when file changes are detected. *Requires server restart",
|
"LabelSettingsEnableWatcherHelp": "Permite agregar/actualizar elementos automáticamente cuando se detectan cambios en los archivos. *Requires server restart",
|
||||||
"LabelSettingsExperimentalFeatures": "Funciones Experimentales",
|
"LabelSettingsExperimentalFeatures": "Funciones Experimentales",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "Funciones en desarrollo sobre las que esperamos sus comentarios y experiencia. Haga click aquí para abrir una conversación en Github.",
|
"LabelSettingsExperimentalFeaturesHelp": "Funciones en desarrollo que se beneficiarían de sus comentarios y experiencias de prueba. Haga click aquí para abrir una conversación en Github.",
|
||||||
"LabelSettingsFindCovers": "Buscar Portadas",
|
"LabelSettingsFindCovers": "Buscar Portadas",
|
||||||
"LabelSettingsFindCoversHelp": "Si tu audiolibro no tiene una portada incluida o la portada no esta dentro de la carpeta, el escaneador tratara de encontrar una portada.<br>Nota: Esto extenderá el tiempo de escaneo",
|
"LabelSettingsFindCoversHelp": "Si tu audiolibro no tiene una portada incluída, o la portada no esta dentro de la carpeta, el escaneador tratará de encontrar una portada.<br>Nota: Esto extenderá el tiempo de escaneo",
|
||||||
"LabelSettingsHideSingleBookSeries": "Hide single book series",
|
"LabelSettingsHideSingleBookSeries": "Esconder series con un solo libro",
|
||||||
"LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.",
|
"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": "La pagina de inicio usa la vista de librero",
|
"LabelSettingsHomePageBookshelfView": "Usar la vista de librero en la página principal",
|
||||||
"LabelSettingsLibraryBookshelfView": "La biblioteca usa la vista de librero",
|
"LabelSettingsLibraryBookshelfView": "Usar la vista de librero en la biblioteca",
|
||||||
"LabelSettingsOverdriveMediaMarkers": "Usar Markers de multimedia en Overdrive para estos capítulos",
|
"LabelSettingsParseSubtitles": "Extraer Subtítulos",
|
||||||
"LabelSettingsOverdriveMediaMarkersHelp": "Archivos MP3 de Overdrive vienen con capítulos con tiempos incrustados como metadata personalizada. Habilitar esto utilizará estas etiquetas para los tiempos de los capítulos automáticamente.",
|
"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í\"",
|
||||||
"LabelSettingsParseSubtitles": "Parse subtitles",
|
"LabelSettingsPreferMatchedMetadata": "Preferir metadatos encontrados",
|
||||||
"LabelSettingsParseSubtitlesHelp": "Extraiga subtítulos de los nombres de las carpetas de los audiolibros.<br>Los subtítulos deben estar separados por \" - \"<br>ejemplo. \"Titulo Libro - Este Subtitulo\" tiene el subtitulo \"Este Subtitulo\"",
|
"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.",
|
||||||
"LabelSettingsPreferAudioMetadata": "Preferir metadata del audio",
|
|
||||||
"LabelSettingsPreferAudioMetadataHelp": "Archivos de Audio ID3 meta tags se utilizarán para detalles de libros en vez de los nombres de carpetas",
|
|
||||||
"LabelSettingsPreferMatchedMetadata": "Prefer matched metadata",
|
|
||||||
"LabelSettingsPreferMatchedMetadataHelp": "Los datos coincidentes anularán los detalles del elemento cuando se utilice Quick Match. Por defecto, Quick Match solo completará los detalles faltantes.",
|
|
||||||
"LabelSettingsPreferOPFMetadata": "Preferir OPF metadata",
|
|
||||||
"LabelSettingsPreferOPFMetadataHelp": "Los archivos de metadata OPF serán usados para los detalles del libro en vez de el nombre de las carpetas",
|
|
||||||
"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 ordenando",
|
"LabelSettingsSortingIgnorePrefixes": "Ignorar prefijos al ordenar",
|
||||||
"LabelSettingsSortingIgnorePrefixesHelp": "ejemplo. el prefijo \"el\" del titulo \"El titulo del libro\" sera ordenado como \"Titulo del Libro, el\"",
|
"LabelSettingsSortingIgnorePrefixesHelp": "Por ejemplo: El prefijo \"el\" del titulo \"El titulo del libro\" se ordenará como \"Titulo del Libro, el\".",
|
||||||
"LabelSettingsSquareBookCovers": "Usar portadas cuadradas",
|
"LabelSettingsSquareBookCovers": "Usar portadas cuadradas",
|
||||||
"LabelSettingsSquareBookCoversHelp": "Prefiere usar portadas cuadradas sobre las portadas estándar 1.6:1",
|
"LabelSettingsSquareBookCoversHelp": "Prefierir usar portadas cuadradas sobre las portadas estándar 1.6:1",
|
||||||
"LabelSettingsStoreCoversWithItem": "Guardar portada con elemento",
|
"LabelSettingsStoreCoversWithItem": "Guardar portadas con elementos",
|
||||||
"LabelSettingsStoreCoversWithItemHelp": "Por defecto, las portadas se almacenan en /metadata/items, si habilita esta configuración, las portadas se almacenará en la carpeta de elementos de su biblioteca. Solamente un archivo llamado \"cover\" sera guardado.",
|
"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 metadata con elemento",
|
"LabelSettingsStoreMetadataWithItem": "Guardar metadatos con elementos",
|
||||||
"LabelSettingsStoreMetadataWithItemHelp": "Por defecto, los archivos de metadatos se almacenan en /metadata/items, si habilita esta configuración, los archivos de metadata se guardaran en la carpeta de elementos de tu biblioteca. Usa la extension .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": "Format de Tiempo",
|
"LabelSettingsTimeFormat": "Formato de Tiempo",
|
||||||
"LabelShowAll": "Mostrar Todos",
|
"LabelShowAll": "Mostrar Todos",
|
||||||
"LabelSize": "Tamaño",
|
"LabelSize": "Tamaño",
|
||||||
"LabelSleepTimer": "Temporizador para Dormir",
|
"LabelSleepTimer": "Temporizador para Dormir",
|
||||||
"LabelSlug": "Slug",
|
"LabelSlug": "Slug",
|
||||||
"LabelStart": "Iniciar",
|
"LabelStart": "Iniciar",
|
||||||
"LabelStarted": "Indiciado",
|
"LabelStarted": "Iniciado",
|
||||||
"LabelStartedAt": "Iniciado En",
|
"LabelStartedAt": "Iniciado En",
|
||||||
"LabelStartTime": "Tiempo de Inicio",
|
"LabelStartTime": "Tiempo de Inicio",
|
||||||
"LabelStatsAudioTracks": "Pistas de Audio",
|
"LabelStatsAudioTracks": "Pistas de Audio",
|
||||||
"LabelStatsAuthors": "Autores",
|
"LabelStatsAuthors": "Autores",
|
||||||
"LabelStatsBestDay": "Mejor Dia",
|
"LabelStatsBestDay": "Mejor Día",
|
||||||
"LabelStatsDailyAverage": "Promedio Diario",
|
"LabelStatsDailyAverage": "Promedio Diario",
|
||||||
"LabelStatsDays": "Dias",
|
"LabelStatsDays": "Días",
|
||||||
"LabelStatsDaysListened": "Dias Escuchando",
|
"LabelStatsDaysListened": "Días Escuchando",
|
||||||
"LabelStatsHours": "Horas",
|
"LabelStatsHours": "Horas",
|
||||||
"LabelStatsInARow": "seguidos",
|
"LabelStatsInARow": "seguidos",
|
||||||
"LabelStatsItemsFinished": "Elementos Terminados",
|
"LabelStatsItemsFinished": "Elementos Terminados",
|
||||||
@ -453,42 +462,42 @@
|
|||||||
"LabelStatsMinutesListening": "Minutos Escuchando",
|
"LabelStatsMinutesListening": "Minutos Escuchando",
|
||||||
"LabelStatsOverallDays": "Total de Dias",
|
"LabelStatsOverallDays": "Total de Dias",
|
||||||
"LabelStatsOverallHours": "Total de Horas",
|
"LabelStatsOverallHours": "Total de Horas",
|
||||||
"LabelStatsWeekListening": "Escuchando en la Semana",
|
"LabelStatsWeekListening": "Tiempo escuchando en la Semana",
|
||||||
"LabelSubtitle": "Subtitulo",
|
"LabelSubtitle": "Subtítulo",
|
||||||
"LabelSupportedFileTypes": "Tipo de Archivos Soportados",
|
"LabelSupportedFileTypes": "Tipos de Archivos Soportados",
|
||||||
"LabelTag": "Etiqueta",
|
"LabelTag": "Etiqueta",
|
||||||
"LabelTags": "Etiquetas",
|
"LabelTags": "Etiquetas",
|
||||||
"LabelTagsAccessibleToUser": "Etiquetas Accessible para el Usuario",
|
"LabelTagsAccessibleToUser": "Etiquetas Accessibles al Usuario",
|
||||||
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
"LabelTagsNotAccessibleToUser": "Etiquetas no Accesibles al Usuario",
|
||||||
"LabelTasks": "Tareas Corriendo",
|
"LabelTasks": "Tareas Corriendo",
|
||||||
"LabelTheme": "Theme",
|
"LabelTheme": "Tema",
|
||||||
"LabelThemeDark": "Dark",
|
"LabelThemeDark": "Oscuro",
|
||||||
"LabelThemeLight": "Light",
|
"LabelThemeLight": "Claro",
|
||||||
"LabelTimeBase": "Time Base",
|
"LabelTimeBase": "Time Base",
|
||||||
"LabelTimeListened": "Tiempo Escuchando",
|
"LabelTimeListened": "Tiempo Escuchando",
|
||||||
"LabelTimeListenedToday": "Tiempo Escuchando Hoy",
|
"LabelTimeListenedToday": "Tiempo Escuchando Hoy",
|
||||||
"LabelTimeRemaining": "{0} restante",
|
"LabelTimeRemaining": "{0} restante",
|
||||||
"LabelTimeToShift": "Tiempo para Cambiar en Segundos",
|
"LabelTimeToShift": "Tiempo para Cambiar en Segundos",
|
||||||
"LabelTitle": "Titulo",
|
"LabelTitle": "Título",
|
||||||
"LabelToolsEmbedMetadata": "Incorporar Metadata",
|
"LabelToolsEmbedMetadata": "Incrustar Metadatos",
|
||||||
"LabelToolsEmbedMetadataDescription": "Incorpora metadata en archivos de audio incluyendo la portada y capítulos.",
|
"LabelToolsEmbedMetadataDescription": "Incrusta metadatos en los archivos de audio, incluyendo la portada y capítulos.",
|
||||||
"LabelToolsMakeM4b": "Hacer Archivo M4B de Audiolibro",
|
"LabelToolsMakeM4b": "Hacer Archivo de Audiolibro M4B",
|
||||||
"LabelToolsMakeM4bDescription": "Generar archivo .M4B de audiolibro con metadata, imágenes de portada y capítulos incorporados.",
|
"LabelToolsMakeM4bDescription": "Generar archivo de audiolibro .M4B con metadatos, imágenes de portada y capítulos incorporados.",
|
||||||
"LabelToolsSplitM4b": "Dividir M4B en Archivos MP3",
|
"LabelToolsSplitM4b": "Dividir M4B en Archivos MP3",
|
||||||
"LabelToolsSplitM4bDescription": "Dividir M4B en Archivos MP3 y incorporar metadata, images de portada y capítulos.",
|
"LabelToolsSplitM4bDescription": "Dividir M4B en Archivos MP3 e incorporar metadatos, imágenes de portada y capítulos.",
|
||||||
"LabelTotalDuration": "Duración Total",
|
"LabelTotalDuration": "Duración Total",
|
||||||
"LabelTotalTimeListened": "Tiempo Total Escuchado",
|
"LabelTotalTimeListened": "Tiempo Total Escuchado",
|
||||||
"LabelTrackFromFilename": "Pista desde el Nombre del Archivo",
|
"LabelTrackFromFilename": "Pista desde el Nombre del Archivo",
|
||||||
"LabelTrackFromMetadata": "Pista desde Metadata",
|
"LabelTrackFromMetadata": "Pista desde Metadatos",
|
||||||
"LabelTracks": "Pistas",
|
"LabelTracks": "Pistas",
|
||||||
"LabelTracksMultiTrack": "Multi-track",
|
"LabelTracksMultiTrack": "Varias pistas",
|
||||||
"LabelTracksNone": "No tracks",
|
"LabelTracksNone": "Ninguna pista",
|
||||||
"LabelTracksSingleTrack": "Single-track",
|
"LabelTracksSingleTrack": "Una pista",
|
||||||
"LabelType": "Tipo",
|
"LabelType": "Tipo",
|
||||||
"LabelUnabridged": "Unabridged",
|
"LabelUnabridged": "No Abreviado",
|
||||||
"LabelUnknown": "Desconocido",
|
"LabelUnknown": "Desconocido",
|
||||||
"LabelUpdateCover": "Actualizar Portada",
|
"LabelUpdateCover": "Actualizar Portada",
|
||||||
"LabelUpdateCoverHelp": "Permitir sobrescribir portadas existentes de los libros seleccionados cuando sean encontrados.",
|
"LabelUpdateCoverHelp": "Permitir sobrescribir las portadas existentes de los libros seleccionados cuando sean encontradas.",
|
||||||
"LabelUpdatedAt": "Actualizado En",
|
"LabelUpdatedAt": "Actualizado En",
|
||||||
"LabelUpdateDetails": "Actualizar Detalles",
|
"LabelUpdateDetails": "Actualizar Detalles",
|
||||||
"LabelUpdateDetailsHelp": "Permitir sobrescribir detalles existentes de los libros seleccionados cuando sean encontrados",
|
"LabelUpdateDetailsHelp": "Permitir sobrescribir detalles existentes de los libros seleccionados cuando sean encontrados",
|
||||||
@ -497,79 +506,83 @@
|
|||||||
"LabelUseChapterTrack": "Usar pista por capitulo",
|
"LabelUseChapterTrack": "Usar pista por capitulo",
|
||||||
"LabelUseFullTrack": "Usar pista completa",
|
"LabelUseFullTrack": "Usar pista completa",
|
||||||
"LabelUser": "Usuario",
|
"LabelUser": "Usuario",
|
||||||
"LabelUsername": "Nombré de Usuario",
|
"LabelUsername": "Nombre de Usuario",
|
||||||
"LabelValue": "Valor",
|
"LabelValue": "Valor",
|
||||||
"LabelVersion": "Versión",
|
"LabelVersion": "Versión",
|
||||||
"LabelViewBookmarks": "Ver Marcadores",
|
"LabelViewBookmarks": "Ver Marcadores",
|
||||||
"LabelViewChapters": "Ver Capítulos",
|
"LabelViewChapters": "Ver Capítulos",
|
||||||
"LabelViewQueue": "Ver player queue",
|
"LabelViewQueue": "Ver Fila del Reproductor",
|
||||||
"LabelVolume": "Volumen",
|
"LabelVolume": "Volumen",
|
||||||
"LabelWeekdaysToRun": "Correr en Dias de la Semana",
|
"LabelWeekdaysToRun": "Correr en Días de la Semana",
|
||||||
"LabelYourAudiobookDuration": "Duración de tu Audiolibro",
|
"LabelYourAudiobookDuration": "Duración de tu Audiolibro",
|
||||||
"LabelYourBookmarks": "Tus Marcadores Bookmarks",
|
"LabelYourBookmarks": "Tus Marcadores",
|
||||||
"LabelYourPlaylists": "Tus Listas",
|
"LabelYourPlaylists": "Tus Listas",
|
||||||
"LabelYourProgress": "Tu Progreso",
|
"LabelYourProgress": "Tu Progreso",
|
||||||
"MessageAddToPlayerQueue": "Agregar a player queue",
|
"MessageAddToPlayerQueue": "Agregar a fila del Reproductor",
|
||||||
"MessageAppriseDescription": "Para usar esta función deberás tener <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> corriendo o un API que maneje los mismos resultados. <br />El Apprise API URL debe tener la misma ruta de archivos que donde se envina las notificaciones, ejemplo, si su API esta en <code>http://192.168.1.1:8337</code> entonces pondría <code>http://192.168.1.1:8337/notify</code>.",
|
"MessageAppriseDescription": "Para usar esta función deberás tener <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">la API de Apprise</a> corriendo o una API que maneje los mismos resultados. <br/>La URL de la API de Apprise debe tener la misma ruta de archivos que donde se envían las notificaciones. Por ejemplo: si su API esta en <code>http://192.168.1.1:8337</code> entonces pondría <code>http://192.168.1.1:8337/notify</code>.",
|
||||||
"MessageBackupsDescription": "Los respaldos incluyen, usuarios, el progreso del los usuarios, detalles de los elementos de la biblioteca, configuración del servidor y las imágenes en <code>/metadata/items</code> & <code>/metadata/authors</code>. Los Respaldo <strong>NO</strong> incluyen ningún archivo guardado en la carpeta de tu biblioteca.",
|
"MessageBackupsDescription": "Los respaldos incluyen: usuarios, el progreso del los usuarios, los detalles de los elementos de la biblioteca, la configuración del servidor y las imágenes en <code>/metadata/items</code> y <code>/metadata/authors</code>. Los Respaldos <strong>NO</strong> incluyen ningún archivo guardado en la carpeta de tu biblioteca.",
|
||||||
"MessageBatchQuickMatchDescription": "Quick Match tratara de agregar porta y metadata faltantes de los elementos seleccionados. Habilite la opción de abajo para que Quick Match pueda sobrescribir portadas y/o metadata existentes.",
|
"MessageBatchQuickMatchDescription": "\"Encontrar Rápido\" tratará de agregar portadas y metadatos faltantes de los elementos seleccionados. Habilite la opción de abajo para que \"Encontrar Rápido\" pueda sobrescribir portadas y/o metadatos existentes.",
|
||||||
"MessageBookshelfNoCollections": "No tienes ninguna colección.",
|
"MessageBookshelfNoCollections": "No tienes ninguna colección.",
|
||||||
"MessageBookshelfNoResultsForFilter": "Ningún Resultado para el filtro \"{0}: {1}\"",
|
"MessageBookshelfNoResultsForFilter": "Ningún Resultado para el filtro \"{0}: {1}\"",
|
||||||
"MessageBookshelfNoRSSFeeds": "Ninguna Fuente RSS esta abierta",
|
"MessageBookshelfNoRSSFeeds": "Ninguna Fuente RSS esta abierta",
|
||||||
"MessageBookshelfNoSeries": "No tienes ninguna series",
|
"MessageBookshelfNoSeries": "No tienes ninguna serie",
|
||||||
"MessageChapterEndIsAfter": "El final del capítulo es después del final de su audiolibro.",
|
"MessageChapterEndIsAfter": "El final del capítulo es después del final de su audiolibro.",
|
||||||
"MessageChapterErrorFirstNotZero": "El primer capitulo debe iniciar en 0",
|
"MessageChapterErrorFirstNotZero": "El primer capitulo debe iniciar en 0",
|
||||||
"MessageChapterErrorStartGteDuration": "El tiempo de inicio no es válida debe ser inferior a la duración del audiolibro.",
|
"MessageChapterErrorStartGteDuration": "El tiempo de inicio no es válido: debe ser inferior a la duración del audiolibro.",
|
||||||
"MessageChapterErrorStartLtPrev": "El tiempo de inicio no es válida debe ser mayor o igual que la hora de inicio del capítulo anterior",
|
"MessageChapterErrorStartLtPrev": "El tiempo de inicio no es válido: debe ser mayor o igual que el tiempo de inicio del capítulo anterior",
|
||||||
"MessageChapterStartIsAfter": "El comienzo del capítulo es después del final de su audiolibro",
|
"MessageChapterStartIsAfter": "El comienzo del capítulo es después del final de su audiolibro",
|
||||||
"MessageCheckingCron": "Checking cron...",
|
"MessageCheckingCron": "Revisando cron...",
|
||||||
"MessageConfirmCloseFeed": "Are you sure you want to close this feed?",
|
"MessageConfirmCloseFeed": "Está seguro de que desea cerrar esta fuente?",
|
||||||
"MessageConfirmDeleteBackup": "Esta seguro que desea eliminar el respaldo {0}?",
|
"MessageConfirmDeleteBackup": "¿Está seguro de que desea eliminar el respaldo {0}?",
|
||||||
"MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?",
|
"MessageConfirmDeleteFile": "Esto eliminará el archivo de su sistema de archivos. ¿Está seguro?",
|
||||||
"MessageConfirmDeleteLibrary": "Esta seguro que desea eliminar permanentemente la biblioteca \"{0}\"?",
|
"MessageConfirmDeleteLibrary": "¿Está seguro de que desea eliminar permanentemente la biblioteca \"{0}\"?",
|
||||||
"MessageConfirmDeleteSession": "Esta seguro que desea eliminar esta session?",
|
"MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?",
|
||||||
"MessageConfirmForceReScan": "Esta seguro que desea forzar re-escanear?",
|
"MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?",
|
||||||
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
|
"MessageConfirmDeleteSession": "¿Está seguro de que desea eliminar esta sesión?",
|
||||||
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
|
"MessageConfirmForceReScan": "¿Está seguro de que desea forzar un re-escaneo?",
|
||||||
"MessageConfirmMarkSeriesFinished": "Esta seguro que desea marcar todos los libros en esta serie como terminados?",
|
"MessageConfirmMarkAllEpisodesFinished": "¿Está seguro de que desea marcar todos los episodios como terminados?",
|
||||||
"MessageConfirmMarkSeriesNotFinished": "Esta seguro que desea marcar todos los libros en esta serie como no terminados?",
|
"MessageConfirmMarkAllEpisodesNotFinished": "¿Está seguro de que desea marcar todos los episodios como no terminados?",
|
||||||
"MessageConfirmRemoveAllChapters": "Esta seguro que desea remover todos los capitulos?",
|
"MessageConfirmMarkSeriesFinished": "¿Está seguro de que desea marcar todos los libros en esta serie como terminados?",
|
||||||
"MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?",
|
"MessageConfirmMarkSeriesNotFinished": "¿Está seguro de que desea marcar todos los libros en esta serie como no terminados?",
|
||||||
"MessageConfirmRemoveCollection": "Esta seguro que desea remover la colección \"{0}\"?",
|
"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?",
|
||||||
"MessageConfirmRemoveEpisode": "Esta seguro que desea remover el episodio \"{0}\"?",
|
"MessageConfirmRemoveAllChapters": "¿Está seguro de que desea remover todos los capitulos?",
|
||||||
"MessageConfirmRemoveEpisodes": "Esta seguro que desea remover {0} episodios?",
|
"MessageConfirmRemoveAuthor": "¿Está seguro de que desea remover el autor \"{0}\"?",
|
||||||
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
|
"MessageConfirmRemoveCollection": "¿Está seguro de que desea remover la colección \"{0}\"?",
|
||||||
"MessageConfirmRemovePlaylist": "Esta seguro que desea remover su lista de reproducción \"{0}\"?",
|
"MessageConfirmRemoveEpisode": "¿Está seguro de que desea remover el episodio \"{0}\"?",
|
||||||
"MessageConfirmRenameGenre": "Esta seguro que desea renombrar el genero \"{0}\" a \"{1}\" de todos los elementos?",
|
"MessageConfirmRemoveEpisodes": "¿Está seguro de que desea remover {0} episodios?",
|
||||||
"MessageConfirmRenameGenreMergeNote": "Nota: Este genero ya existe por lo que se fusionarán.",
|
"MessageConfirmRemoveNarrator": "¿Está seguro de que desea remover el narrador \"{0}\"?",
|
||||||
"MessageConfirmRenameGenreWarning": "Advertencia! un genero similar ya existe \"{0}\".",
|
"MessageConfirmRemovePlaylist": "¿Está seguro de que desea remover la lista de reproducción \"{0}\"?",
|
||||||
"MessageConfirmRenameTag": "Esta seguro que desea renombrar la etiqueta \"{0}\" a \"{1}\" de todos los elementos?",
|
"MessageConfirmRenameGenre": "¿Está seguro de que desea renombrar el genero \"{0}\" a \"{1}\" de todos los elementos?",
|
||||||
"MessageConfirmRenameTagMergeNote": "Nota: Esta etiqueta ya existe por lo que se fusionarán.",
|
"MessageConfirmRenameGenreMergeNote": "Nota: Este género ya existe, por lo que se fusionarán.",
|
||||||
|
"MessageConfirmRenameGenreWarning": "Advertencia! Un genero similar ya existe \"{0}\".",
|
||||||
|
"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.",
|
||||||
"MessageConfirmRenameTagWarning": "Advertencia! Una etiqueta similar ya existe \"{0}\".",
|
"MessageConfirmRenameTagWarning": "Advertencia! Una etiqueta similar ya existe \"{0}\".",
|
||||||
"MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
|
"MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?",
|
||||||
|
"MessageConfirmSendEbookToDevice": "¿Está seguro de que enviar {0} ebook(s) \"{1}\" al dispositivo \"{2}\"?",
|
||||||
"MessageDownloadingEpisode": "Descargando Capitulo",
|
"MessageDownloadingEpisode": "Descargando Capitulo",
|
||||||
"MessageDragFilesIntoTrackOrder": "Arrastras los archivos en el orden correcto de la pista.",
|
"MessageDragFilesIntoTrackOrder": "Arrastra los archivos al orden correcto de las pistas.",
|
||||||
"MessageEmbedFinished": "Incorporación Terminada!",
|
"MessageEmbedFinished": "Incrustación Terminada!",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} Episodio(s) en cola para descargar",
|
"MessageEpisodesQueuedForDownload": "{0} Episodio(s) en cola para descargar",
|
||||||
"MessageFeedURLWillBe": "Fuente URL sera {0}",
|
"MessageFeedURLWillBe": "URL de la fuente será {0}",
|
||||||
"MessageFetching": "Buscando...",
|
"MessageFetching": "Buscando...",
|
||||||
"MessageForceReScanDescription": "Escaneara todos los archivos como un nuevo escaneo. Archivos de audio con etiqueta ID3, archivos OPF y archivos de texto serán escaneados como nuevos.",
|
"MessageForceReScanDescription": "Escaneará todos los archivos como un nuevo escaneo. Archivos de audio con etiquetas ID3, archivos OPF y archivos de texto serán escaneados como nuevos.",
|
||||||
"MessageImportantNotice": "Noticia importante!",
|
"MessageImportantNotice": "¡Notificación importante!",
|
||||||
"MessageInsertChapterBelow": "Insertar Capítulo Abajo",
|
"MessageInsertChapterBelow": "Insertar Capítulo Abajo",
|
||||||
"MessageItemsSelected": "{0} Elementos Seleccionados",
|
"MessageItemsSelected": "{0} Elementos Seleccionados",
|
||||||
"MessageItemsUpdated": "{0} Elementos Actualizados",
|
"MessageItemsUpdated": "{0} Elementos Actualizados",
|
||||||
"MessageJoinUsOn": "Únete en",
|
"MessageJoinUsOn": "Únetenos en",
|
||||||
"MessageListeningSessionsInTheLastYear": "{0} sesiones de escuchadas en el último año",
|
"MessageListeningSessionsInTheLastYear": "{0} sesiones de escucha en el último año",
|
||||||
"MessageLoading": "Cargando...",
|
"MessageLoading": "Cargando...",
|
||||||
"MessageLoadingFolders": "Cargando archivos...",
|
"MessageLoadingFolders": "Cargando archivos...",
|
||||||
"MessageM4BFailed": "M4B Fallo!",
|
"MessageM4BFailed": "¡Fallo de M4B!",
|
||||||
"MessageM4BFinished": "M4B Terminado!",
|
"MessageM4BFinished": "¡M4B Terminado!",
|
||||||
"MessageMapChapterTitles": "Map chapter titles to your existing audiobook chapters without adjusting timestamps",
|
"MessageMapChapterTitles": "Asignar los nombres de capítulos a los capítulos existentes en tu audiolibro sin ajustar sus tiempos",
|
||||||
"MessageMarkAllEpisodesFinished": "Mark all episodes finished",
|
"MessageMarkAllEpisodesFinished": "Marcar todos los episodios como terminados",
|
||||||
"MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
|
"MessageMarkAllEpisodesNotFinished": "Marcar todos los episodios como no terminados",
|
||||||
"MessageMarkAsFinished": "Marcar como Terminado",
|
"MessageMarkAsFinished": "Marcar como Terminado",
|
||||||
"MessageMarkAsNotFinished": "Marcar como No Terminado",
|
"MessageMarkAsNotFinished": "Marcar como No Terminado",
|
||||||
"MessageMatchBooksDescription": "intentará hacer coincidir los libros de la biblioteca con un libro del proveedor de búsqueda seleccionado y rellenará los detalles vacíos y la portada. No sobrescribe los detalles.",
|
"MessageMatchBooksDescription": "Se intentará hacer coincidir los libros de la biblioteca con un libro del proveedor de búsqueda seleccionado, y se rellenarán los detalles vacíos y la portada. No sobrescribe los detalles.",
|
||||||
"MessageNoAudioTracks": "Sin Pista de Audio",
|
"MessageNoAudioTracks": "Sin Pista de Audio",
|
||||||
"MessageNoAuthors": "Sin Autores",
|
"MessageNoAuthors": "Sin Autores",
|
||||||
"MessageNoBackups": "Sin Respaldos",
|
"MessageNoBackups": "Sin Respaldos",
|
||||||
@ -582,18 +595,18 @@
|
|||||||
"MessageNoDownloadsQueued": "Sin Lista de Descarga",
|
"MessageNoDownloadsQueued": "Sin Lista de Descarga",
|
||||||
"MessageNoEpisodeMatchesFound": "No se encontraron episodios que coinciden",
|
"MessageNoEpisodeMatchesFound": "No se encontraron episodios que coinciden",
|
||||||
"MessageNoEpisodes": "Sin Episodios",
|
"MessageNoEpisodes": "Sin Episodios",
|
||||||
"MessageNoFoldersAvailable": "No Carpetas Disponibles",
|
"MessageNoFoldersAvailable": "No Hay Carpetas Disponibles",
|
||||||
"MessageNoGenres": "Sin Géneros",
|
"MessageNoGenres": "Sin Géneros",
|
||||||
"MessageNoIssues": "Sin Problemas",
|
"MessageNoIssues": "Sin Problemas",
|
||||||
"MessageNoItems": "Sin Elementos",
|
"MessageNoItems": "Sin Elementos",
|
||||||
"MessageNoItemsFound": "Ningún Elemento Encontrado",
|
"MessageNoItemsFound": "Ningún Elemento Encontrado",
|
||||||
"MessageNoListeningSessions": "Ninguna Session Escuchada",
|
"MessageNoListeningSessions": "Ninguna Session Escuchada",
|
||||||
"MessageNoLogs": "No Logs",
|
"MessageNoLogs": "No hay logs",
|
||||||
"MessageNoMediaProgress": "Multimedia sin Progreso ",
|
"MessageNoMediaProgress": "Multimedia sin Progreso",
|
||||||
"MessageNoNotifications": "Ninguna Notificación",
|
"MessageNoNotifications": "Ninguna Notificación",
|
||||||
"MessageNoPodcastsFound": "Ningún podcasts encontrado",
|
"MessageNoPodcastsFound": "Ningún podcast encontrado",
|
||||||
"MessageNoResults": "Sin Resultados",
|
"MessageNoResults": "Sin Resultados",
|
||||||
"MessageNoSearchResultsFor": "No hay resultados de la búsqueda para \"{0}\"",
|
"MessageNoSearchResultsFor": "No hay resultados para la búsqueda \"{0}\"",
|
||||||
"MessageNoSeries": "Sin Series",
|
"MessageNoSeries": "Sin Series",
|
||||||
"MessageNoTags": "Sin Etiquetas",
|
"MessageNoTags": "Sin Etiquetas",
|
||||||
"MessageNoTasksRunning": "Ninguna Tarea Corriendo",
|
"MessageNoTasksRunning": "Ninguna Tarea Corriendo",
|
||||||
@ -603,45 +616,45 @@
|
|||||||
"MessageNoUserPlaylists": "No tienes lista de reproducciones",
|
"MessageNoUserPlaylists": "No tienes lista de reproducciones",
|
||||||
"MessageOr": "o",
|
"MessageOr": "o",
|
||||||
"MessagePauseChapter": "Pausar la reproducción del capítulo",
|
"MessagePauseChapter": "Pausar la reproducción del capítulo",
|
||||||
"MessagePlayChapter": "Escuche para comenzar el capítulo",
|
"MessagePlayChapter": "Escuchar el comienzo del capítulo",
|
||||||
"MessagePlaylistCreateFromCollection": "Crear lista de reproducción a partir de colección",
|
"MessagePlaylistCreateFromCollection": "Crear una lista de reproducción a partir de una colección",
|
||||||
"MessagePodcastHasNoRSSFeedForMatching": "El podcast no tiene una URL de fuente RSS que pueda usar que coincida",
|
"MessagePodcastHasNoRSSFeedForMatching": "El podcast no tiene una URL de fuente RSS que pueda usar",
|
||||||
"MessageQuickMatchDescription": "Rellenar detalles de elementos vacíos y portada con los primeros resultados de '{0}'. No sobrescribe los detalles a menos que la configuración 'Prefer matched metadata' del servidor este habilita.",
|
"MessageQuickMatchDescription": "Rellenar detalles de elementos vacíos y portada con los primeros resultados de '{0}'. No sobrescribe los detalles a menos que la opción \"Preferir Metadatos Encontrados\" del servidor esté habilitada.",
|
||||||
"MessageRemoveChapter": "Remover capítulos",
|
"MessageRemoveChapter": "Remover capítulos",
|
||||||
"MessageRemoveEpisodes": "Remover {0} episodio(s)",
|
"MessageRemoveEpisodes": "Remover {0} episodio(s)",
|
||||||
"MessageRemoveFromPlayerQueue": "Romover la cola de reporduccion",
|
"MessageRemoveFromPlayerQueue": "Romover la cola de reproducción",
|
||||||
"MessageRemoveUserWarning": "Esta seguro que desea eliminar el usuario \"{0}\"?",
|
"MessageRemoveUserWarning": "¿Está seguro de que desea eliminar el usuario \"{0}\"?",
|
||||||
"MessageReportBugsAndContribute": "Reporte erres, solicite funciones y contribuye en",
|
"MessageReportBugsAndContribute": "Reporte erres, solicite funciones y contribuya en",
|
||||||
"MessageResetChaptersConfirm": "Esta seguro que desea reiniciar el capitulo y deshacer los cambios que hiciste?",
|
"MessageResetChaptersConfirm": "¿Está seguro de que desea deshacer los cambios y revertir los capítulos a su estado original?",
|
||||||
"MessageRestoreBackupConfirm": "Esta seguro 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 & /metadata/authors.<br /><br />El respaldo no modifica ningún archivo en las carpetas de tu biblioteca. Si ha habilitado la configuración del servidor para almacenar portadas y metadata en las carpetas de su biblioteca, entonces esos no se respaldan o sobrescribe.<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",
|
||||||
"MessageServerCouldNotBeReached": "No se pude 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}?",
|
||||||
"MessageThinking": "Pensando...",
|
"MessageThinking": "Pensando...",
|
||||||
"MessageUploaderItemFailed": "Error al Subir",
|
"MessageUploaderItemFailed": "Error al Subir",
|
||||||
"MessageUploaderItemSuccess": "Éxito al Subir!",
|
"MessageUploaderItemSuccess": "¡Éxito al Subir!",
|
||||||
"MessageUploading": "Subiendo...",
|
"MessageUploading": "Subiendo...",
|
||||||
"MessageValidCronExpression": "Valid cron expression",
|
"MessageValidCronExpression": "Expresión de Cron bálida",
|
||||||
"MessageWatcherIsDisabledGlobally": "Watcher es desactivado globalmente en la configuración del servidor",
|
"MessageWatcherIsDisabledGlobally": "El watcher está desactivado globalmente en la configuración del servidor",
|
||||||
"MessageXLibraryIsEmpty": "{0} La biblioteca esta vacía!",
|
"MessageXLibraryIsEmpty": "La biblioteca {0} está vacía!",
|
||||||
"MessageYourAudiobookDurationIsLonger": "La duración de tu audiolibro es más larga que la duración encontrada",
|
"MessageYourAudiobookDurationIsLonger": "La duración de su audiolibro es más larga que la duración encontrada",
|
||||||
"MessageYourAudiobookDurationIsShorter": "La duración de su audiolibro es más corta que la duración encontrada",
|
"MessageYourAudiobookDurationIsShorter": "La duración de su audiolibro es más corta que la duración encontrada",
|
||||||
"NoteChangeRootPassword": "El usuario Root es el único usuario que puede no tener una contraseña",
|
"NoteChangeRootPassword": "El usuario Root es el único usuario que puede no tener una contraseña",
|
||||||
"NoteChapterEditorTimes": "Nota: La hora de inicio del primer capítulo debe permanecer en 0:00 y la hora de inicio del último capítulo no puede exceder la duración de este audiolibro.",
|
"NoteChapterEditorTimes": "Nota: El tiempo de inicio del primer capítulo debe permanecer en 0:00, y el tiempo de inicio del último capítulo no puede exceder la duración del audiolibro.",
|
||||||
"NoteFolderPicker": "Nota: las carpetas ya asignadas no se mostrarán",
|
"NoteFolderPicker": "Nota: Las carpetas ya asignadas no se mostrarán",
|
||||||
"NoteFolderPickerDebian": "Nota: Folder picker for the debian install is not fully implemented. You should enter the path to your library directly.",
|
"NoteFolderPickerDebian": "Nota: El selector de archivos no está completamente implementado para instalaciones en Debian. Deberá ingresar la ruta de la carpeta de su biblioteca directamente.",
|
||||||
"NoteRSSFeedPodcastAppsHttps": "Advertencia: La mayoría de las aplicaciones de podcast requieren que URL de la fuente RSS use HTTPS",
|
"NoteRSSFeedPodcastAppsHttps": "Advertencia: La mayoría de las aplicaciones de podcast requieren que la URL de la fuente RSS use HTTPS",
|
||||||
"NoteRSSFeedPodcastAppsPubDate": "Advertencia: 1 o más de tus episodios no tienen fecha de publicación. Algunas aplicaciones de podcast lo requieren.",
|
"NoteRSSFeedPodcastAppsPubDate": "Advertencia: 1 o más de sus episodios no tienen fecha de publicación. Algunas aplicaciones de podcast lo requieren.",
|
||||||
"NoteUploaderFoldersWithMediaFiles": "Las carpetas con archivos multimedia se manejarán como elementos separados en la biblioteca.",
|
"NoteUploaderFoldersWithMediaFiles": "Las carpetas con archivos multimedia se manejarán como elementos separados en la biblioteca.",
|
||||||
"NoteUploaderOnlyAudioFiles": "Si subes solamente un archivos de audio, cada archivo se manejara como un audiolibro.",
|
"NoteUploaderOnlyAudioFiles": "Si sube solamente archivos de audio, cada archivo se manejará como un audiolibro por separado.",
|
||||||
"NoteUploaderUnsupportedFiles": "Los archivos no soportados se ignoran. Al elegir o soltar una carpeta, los archivos que no estén en una carpeta serán ignorados.",
|
"NoteUploaderUnsupportedFiles": "Se ignorarán los archivos no soportados. Al elegir o arrastrar una carpeta, los archivos que no estén dentro de una subcarpeta serán ignorados.",
|
||||||
"PlaceholderNewCollection": "Nuevo nombre de la colección",
|
"PlaceholderNewCollection": "Nuevo nombre de la colección",
|
||||||
"PlaceholderNewFolderPath": "Nueva ruta de carpeta",
|
"PlaceholderNewFolderPath": "Nueva ruta de carpeta",
|
||||||
"PlaceholderNewPlaylist": "Nuevo nombre de la lista de reproducción",
|
"PlaceholderNewPlaylist": "Nuevo nombre de la lista de reproducción",
|
||||||
"PlaceholderSearch": "Buscando..",
|
"PlaceholderSearch": "Buscar..",
|
||||||
"PlaceholderSearchEpisode": "Search episode..",
|
"PlaceholderSearchEpisode": "Buscar Episodio..",
|
||||||
"ToastAccountUpdateFailed": "Error al actualizar cuenta",
|
"ToastAccountUpdateFailed": "Error al actualizar cuenta",
|
||||||
"ToastAccountUpdateSuccess": "Cuenta actualizada",
|
"ToastAccountUpdateSuccess": "Cuenta actualizada",
|
||||||
"ToastAuthorImageRemoveFailed": "Error al eliminar la imagen",
|
"ToastAuthorImageRemoveFailed": "Error al eliminar la imagen",
|
||||||
@ -657,16 +670,16 @@
|
|||||||
"ToastBackupRestoreFailed": "Error al restaurar el respaldo",
|
"ToastBackupRestoreFailed": "Error al restaurar el respaldo",
|
||||||
"ToastBackupUploadFailed": "Error al subir el respaldo",
|
"ToastBackupUploadFailed": "Error al subir el respaldo",
|
||||||
"ToastBackupUploadSuccess": "Respaldo cargado",
|
"ToastBackupUploadSuccess": "Respaldo cargado",
|
||||||
"ToastBatchUpdateFailed": "Batch update failed",
|
"ToastBatchUpdateFailed": "Subida masiva fallida",
|
||||||
"ToastBatchUpdateSuccess": "Batch update success",
|
"ToastBatchUpdateSuccess": "Subida masiva exitosa",
|
||||||
"ToastBookmarkCreateFailed": "Error al crear marcador",
|
"ToastBookmarkCreateFailed": "Error al crear marcador",
|
||||||
"ToastBookmarkCreateSuccess": "Marca Agregado",
|
"ToastBookmarkCreateSuccess": "Marcador Agregado",
|
||||||
"ToastBookmarkRemoveFailed": "Error al eliminar marcador",
|
"ToastBookmarkRemoveFailed": "Error al eliminar marcador",
|
||||||
"ToastBookmarkRemoveSuccess": "Marcador eliminado",
|
"ToastBookmarkRemoveSuccess": "Marcador eliminado",
|
||||||
"ToastBookmarkUpdateFailed": "Error al eliminar el marcador",
|
"ToastBookmarkUpdateFailed": "Error al actualizar el marcador",
|
||||||
"ToastBookmarkUpdateSuccess": "Marcador actualizado",
|
"ToastBookmarkUpdateSuccess": "Marcador actualizado",
|
||||||
"ToastChaptersHaveErrors": "Los capítulos tienen errores",
|
"ToastChaptersHaveErrors": "Los capítulos tienen errores",
|
||||||
"ToastChaptersMustHaveTitles": "Los capítulos tienen que tener titulo",
|
"ToastChaptersMustHaveTitles": "Los capítulos tienen que tener un título",
|
||||||
"ToastCollectionItemsRemoveFailed": "Error al remover elemento(s) de la colección",
|
"ToastCollectionItemsRemoveFailed": "Error al remover elemento(s) de la colección",
|
||||||
"ToastCollectionItemsRemoveSuccess": "Elementos(s) removidos de la colección",
|
"ToastCollectionItemsRemoveSuccess": "Elementos(s) removidos de la colección",
|
||||||
"ToastCollectionRemoveFailed": "Error al remover la colección",
|
"ToastCollectionRemoveFailed": "Error al remover la colección",
|
||||||
@ -676,7 +689,7 @@
|
|||||||
"ToastItemCoverUpdateFailed": "Error al actualizar la portada del elemento",
|
"ToastItemCoverUpdateFailed": "Error al actualizar la portada del elemento",
|
||||||
"ToastItemCoverUpdateSuccess": "Portada del elemento actualizada",
|
"ToastItemCoverUpdateSuccess": "Portada del elemento actualizada",
|
||||||
"ToastItemDetailsUpdateFailed": "Error al actualizar los detalles del elemento",
|
"ToastItemDetailsUpdateFailed": "Error al actualizar los detalles del elemento",
|
||||||
"ToastItemDetailsUpdateSuccess": "Detalles de Elemento Actualizados",
|
"ToastItemDetailsUpdateSuccess": "Detalles del Elemento Actualizados",
|
||||||
"ToastItemDetailsUpdateUnneeded": "No se necesitan actualizaciones para los detalles del Elemento",
|
"ToastItemDetailsUpdateUnneeded": "No se necesitan actualizaciones para los detalles del Elemento",
|
||||||
"ToastItemMarkedAsFinishedFailed": "Error al marcar como Terminado",
|
"ToastItemMarkedAsFinishedFailed": "Error al marcar como Terminado",
|
||||||
"ToastItemMarkedAsFinishedSuccess": "Elemento marcado como terminado",
|
"ToastItemMarkedAsFinishedSuccess": "Elemento marcado como terminado",
|
||||||
@ -684,11 +697,11 @@
|
|||||||
"ToastItemMarkedAsNotFinishedSuccess": "Elemento marcado como No Terminado",
|
"ToastItemMarkedAsNotFinishedSuccess": "Elemento marcado como No Terminado",
|
||||||
"ToastLibraryCreateFailed": "Error al crear biblioteca",
|
"ToastLibraryCreateFailed": "Error al crear biblioteca",
|
||||||
"ToastLibraryCreateSuccess": "Biblioteca \"{0}\" creada",
|
"ToastLibraryCreateSuccess": "Biblioteca \"{0}\" creada",
|
||||||
"ToastLibraryDeleteFailed": "Error al eliminar la biblioteca",
|
"ToastLibraryDeleteFailed": "Error al eliminar biblioteca",
|
||||||
"ToastLibraryDeleteSuccess": "Biblioteca eliminada",
|
"ToastLibraryDeleteSuccess": "Biblioteca eliminada",
|
||||||
"ToastLibraryScanFailedToStart": "Error al iniciar la exploración",
|
"ToastLibraryScanFailedToStart": "Error al iniciar el escaneo",
|
||||||
"ToastLibraryScanStarted": "Se inició el escaneo de la biblioteca",
|
"ToastLibraryScanStarted": "Se inició el escaneo de la biblioteca",
|
||||||
"ToastLibraryUpdateFailed": "Error al actualizar biblioteca",
|
"ToastLibraryUpdateFailed": "Error al actualizar la biblioteca",
|
||||||
"ToastLibraryUpdateSuccess": "Biblioteca \"{0}\" actualizada",
|
"ToastLibraryUpdateSuccess": "Biblioteca \"{0}\" actualizada",
|
||||||
"ToastPlaylistCreateFailed": "Error al crear la lista de reproducción.",
|
"ToastPlaylistCreateFailed": "Error al crear la lista de reproducción.",
|
||||||
"ToastPlaylistCreateSuccess": "Lista de reproducción creada",
|
"ToastPlaylistCreateSuccess": "Lista de reproducción creada",
|
||||||
@ -697,15 +710,15 @@
|
|||||||
"ToastPlaylistUpdateFailed": "Error al actualizar la lista de reproducción.",
|
"ToastPlaylistUpdateFailed": "Error al actualizar la lista de reproducción.",
|
||||||
"ToastPlaylistUpdateSuccess": "Lista de reproducción actualizada",
|
"ToastPlaylistUpdateSuccess": "Lista de reproducción actualizada",
|
||||||
"ToastPodcastCreateFailed": "Error al crear podcast",
|
"ToastPodcastCreateFailed": "Error al crear podcast",
|
||||||
"ToastPodcastCreateSuccess": "Podcast creada",
|
"ToastPodcastCreateSuccess": "Podcast creado",
|
||||||
"ToastRemoveItemFromCollectionFailed": "Error al eliminar el elemento de la colección",
|
"ToastRemoveItemFromCollectionFailed": "Error al eliminar el elemento de la colección",
|
||||||
"ToastRemoveItemFromCollectionSuccess": "Elemento eliminado de la colección.",
|
"ToastRemoveItemFromCollectionSuccess": "Elemento eliminado de la colección.",
|
||||||
"ToastRSSFeedCloseFailed": "Error al cerrar fuente RSS",
|
"ToastRSSFeedCloseFailed": "Error al cerrar fuente RSS",
|
||||||
"ToastRSSFeedCloseSuccess": "Fuente RSS cerrada",
|
"ToastRSSFeedCloseSuccess": "Fuente RSS cerrada",
|
||||||
"ToastSendEbookToDeviceFailed": "Failed to Send Ebook to device",
|
"ToastSendEbookToDeviceFailed": "Error al enviar el ebook al dispositivo",
|
||||||
"ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"",
|
"ToastSendEbookToDeviceSuccess": "Ebook enviado al dispositivo \"{0}\"",
|
||||||
"ToastSeriesUpdateFailed": "Error al actualizar la serie",
|
"ToastSeriesUpdateFailed": "Error al actualizar la serie",
|
||||||
"ToastSeriesUpdateSuccess": "Series actualizada",
|
"ToastSeriesUpdateSuccess": "Serie actualizada",
|
||||||
"ToastSessionDeleteFailed": "Error al eliminar sesión",
|
"ToastSessionDeleteFailed": "Error al eliminar sesión",
|
||||||
"ToastSessionDeleteSuccess": "Sesión eliminada",
|
"ToastSessionDeleteSuccess": "Sesión eliminada",
|
||||||
"ToastSocketConnected": "Socket conectado",
|
"ToastSocketConnected": "Socket conectado",
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
{
|
{
|
||||||
"ButtonAdd": "Ajouter",
|
"ButtonAdd": "Ajouter",
|
||||||
"ButtonAddChapters": "Ajouter le chapitre",
|
"ButtonAddChapters": "Ajouter le chapitre",
|
||||||
|
"ButtonAddDevice": "Add Device",
|
||||||
|
"ButtonAddLibrary": "Add Library",
|
||||||
"ButtonAddPodcasts": "Ajouter des podcasts",
|
"ButtonAddPodcasts": "Ajouter des podcasts",
|
||||||
|
"ButtonAddUser": "Add User",
|
||||||
"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": "Reset to default",
|
||||||
"ButtonRestore": "Rétablir",
|
"ButtonRestore": "Rétablir",
|
||||||
"ButtonSave": "Sauvegarder",
|
"ButtonSave": "Sauvegarder",
|
||||||
"ButtonSaveAndClose": "Sauvegarder et Fermer",
|
"ButtonSaveAndClose": "Sauvegarder et Fermer",
|
||||||
@ -122,6 +126,7 @@
|
|||||||
"HeaderManageTags": "Gérer les étiquettes",
|
"HeaderManageTags": "Gérer les étiquettes",
|
||||||
"HeaderMapDetails": "Édition en masse",
|
"HeaderMapDetails": "Édition en masse",
|
||||||
"HeaderMatch": "Chercher",
|
"HeaderMatch": "Chercher",
|
||||||
|
"HeaderMetadataOrderOfPrecedence": "Metadata order of precedence",
|
||||||
"HeaderMetadataToEmbed": "Métadonnée à intégrer",
|
"HeaderMetadataToEmbed": "Métadonnée à intégrer",
|
||||||
"HeaderNewAccount": "Nouveau compte",
|
"HeaderNewAccount": "Nouveau compte",
|
||||||
"HeaderNewLibrary": "Nouvelle bibliothèque",
|
"HeaderNewLibrary": "Nouvelle bibliothèque",
|
||||||
@ -176,8 +181,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": "Admin users only",
|
||||||
"LabelAll": "Tout",
|
"LabelAll": "Tout",
|
||||||
"LabelAllUsers": "Tous les utilisateurs",
|
"LabelAllUsers": "Tous les utilisateurs",
|
||||||
|
"LabelAllUsersExcludingGuests": "All users excluding guests",
|
||||||
|
"LabelAllUsersIncludingGuests": "All users including guests",
|
||||||
"LabelAlreadyInYourLibrary": "Déjà dans la bibliothèque",
|
"LabelAlreadyInYourLibrary": "Déjà dans la bibliothèque",
|
||||||
"LabelAppend": "Ajouter",
|
"LabelAppend": "Ajouter",
|
||||||
"LabelAuthor": "Auteur",
|
"LabelAuthor": "Auteur",
|
||||||
@ -200,6 +208,7 @@
|
|||||||
"LabelChapters": "Chapitres",
|
"LabelChapters": "Chapitres",
|
||||||
"LabelChaptersFound": "Chapitres trouvés",
|
"LabelChaptersFound": "Chapitres trouvés",
|
||||||
"LabelChapterTitle": "Titres du chapitre",
|
"LabelChapterTitle": "Titres du chapitre",
|
||||||
|
"LabelClickForMoreInfo": "Click for more info",
|
||||||
"LabelClosePlayer": "Fermer le lecteur",
|
"LabelClosePlayer": "Fermer le lecteur",
|
||||||
"LabelCodec": "Codec",
|
"LabelCodec": "Codec",
|
||||||
"LabelCollapseSeries": "Réduire les séries",
|
"LabelCollapseSeries": "Réduire les séries",
|
||||||
@ -218,10 +227,12 @@
|
|||||||
"LabelCurrently": "En ce moment :",
|
"LabelCurrently": "En ce moment :",
|
||||||
"LabelCustomCronExpression": "Expression cron personnalisée:",
|
"LabelCustomCronExpression": "Expression cron personnalisée:",
|
||||||
"LabelDatetime": "Datetime",
|
"LabelDatetime": "Datetime",
|
||||||
|
"LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)",
|
||||||
"LabelDescription": "Description",
|
"LabelDescription": "Description",
|
||||||
"LabelDeselectAll": "Tout déselectionner",
|
"LabelDeselectAll": "Tout déselectionner",
|
||||||
"LabelDevice": "Appareil",
|
"LabelDevice": "Appareil",
|
||||||
"LabelDeviceInfo": "Détail de l’appareil",
|
"LabelDeviceInfo": "Détail de l’appareil",
|
||||||
|
"LabelDeviceIsAvailableTo": "Device is available to...",
|
||||||
"LabelDirectory": "Répertoire",
|
"LabelDirectory": "Répertoire",
|
||||||
"LabelDiscFromFilename": "Disque depuis le fichier",
|
"LabelDiscFromFilename": "Disque depuis le fichier",
|
||||||
"LabelDiscFromMetadata": "Disque depuis les métadonnées",
|
"LabelDiscFromMetadata": "Disque depuis les métadonnées",
|
||||||
@ -256,6 +267,7 @@
|
|||||||
"LabelFinished": "Fini(e)",
|
"LabelFinished": "Fini(e)",
|
||||||
"LabelFolder": "Dossier",
|
"LabelFolder": "Dossier",
|
||||||
"LabelFolders": "Dossiers",
|
"LabelFolders": "Dossiers",
|
||||||
|
"LabelFontFamily": "Famille de polices",
|
||||||
"LabelFontScale": "Taille de la police de caractère",
|
"LabelFontScale": "Taille de la police de caractère",
|
||||||
"LabelFormat": "Format",
|
"LabelFormat": "Format",
|
||||||
"LabelGenre": "Genre",
|
"LabelGenre": "Genre",
|
||||||
@ -266,6 +278,7 @@
|
|||||||
"LabelHost": "Hôte",
|
"LabelHost": "Hôte",
|
||||||
"LabelHour": "Heure",
|
"LabelHour": "Heure",
|
||||||
"LabelIcon": "Icone",
|
"LabelIcon": "Icone",
|
||||||
|
"LabelImageURLFromTheWeb": "Image URL from the web",
|
||||||
"LabelIncludeInTracklist": "Inclure dans la liste des pistes",
|
"LabelIncludeInTracklist": "Inclure dans la liste des pistes",
|
||||||
"LabelIncomplete": "Incomplet",
|
"LabelIncomplete": "Incomplet",
|
||||||
"LabelInProgress": "En cours",
|
"LabelInProgress": "En cours",
|
||||||
@ -305,6 +318,7 @@
|
|||||||
"LabelLookForNewEpisodesAfterDate": "Chercher de nouveaux épisode après cette date",
|
"LabelLookForNewEpisodesAfterDate": "Chercher de nouveaux épisode après cette date",
|
||||||
"LabelMediaPlayer": "Lecteur multimédia",
|
"LabelMediaPlayer": "Lecteur multimédia",
|
||||||
"LabelMediaType": "Type de média",
|
"LabelMediaType": "Type de média",
|
||||||
|
"LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority",
|
||||||
"LabelMetadataProvider": "Fournisseur de métadonnées",
|
"LabelMetadataProvider": "Fournisseur de métadonnées",
|
||||||
"LabelMetaTag": "Etiquette de métadonnée",
|
"LabelMetaTag": "Etiquette de métadonnée",
|
||||||
"LabelMetaTags": "Etiquettes de métadonnée",
|
"LabelMetaTags": "Etiquettes de métadonnée",
|
||||||
@ -384,6 +398,7 @@
|
|||||||
"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",
|
||||||
|
"LabelSelectUsers": "Select users",
|
||||||
"LabelSendEbookToDevice": "Envoyer le livre numérique à...",
|
"LabelSendEbookToDevice": "Envoyer le livre numérique à...",
|
||||||
"LabelSequence": "Séquence",
|
"LabelSequence": "Séquence",
|
||||||
"LabelSeries": "Séries",
|
"LabelSeries": "Séries",
|
||||||
@ -410,16 +425,10 @@
|
|||||||
"LabelSettingsHideSingleBookSeriesHelp": "Les séries qui ne comportent qu’un seul livre seront masquées sur la page de la série et sur les étagères de la page d’accueil.",
|
"LabelSettingsHideSingleBookSeriesHelp": "Les séries qui ne comportent qu’un seul livre seront masquées sur la page de la série et sur les étagères de la page d’accueil.",
|
||||||
"LabelSettingsHomePageBookshelfView": "La page d’accueil utilise la vue étagère",
|
"LabelSettingsHomePageBookshelfView": "La page d’accueil 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 d’Overdrive 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>i.e. « 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 l’article lors d’une 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 l’article lors d’une 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",
|
||||||
@ -429,7 +438,7 @@
|
|||||||
"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 l’article. 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 l’article. 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 l’article avec une extension « .abs ».",
|
"LabelSettingsStoreMetadataWithItemHelp": "Par défaut, les métadonnées sont enregistrées dans /metadata/items",
|
||||||
"LabelSettingsTimeFormat": "Format d’heure",
|
"LabelSettingsTimeFormat": "Format d’heure",
|
||||||
"LabelShowAll": "Afficher Tout",
|
"LabelShowAll": "Afficher Tout",
|
||||||
"LabelSize": "Taille",
|
"LabelSize": "Taille",
|
||||||
@ -527,12 +536,15 @@
|
|||||||
"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": "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": "Ê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": "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": "Ê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} » ?",
|
||||||
@ -546,6 +558,7 @@
|
|||||||
"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": "Are you sure you want to re-scan {0} items?",
|
||||||
"MessageConfirmSendEbookToDevice": "Êtes-vous sûr de vouloir envoyer le livre numérique {0} « {1} » à l’appareil « {2} »?",
|
"MessageConfirmSendEbookToDevice": "Êtes-vous sûr de vouloir envoyer le livre numérique {0} « {1} » à l’appareil « {2} »?",
|
||||||
"MessageDownloadingEpisode": "Téléchargement de l’épisode",
|
"MessageDownloadingEpisode": "Téléchargement de l’épisode",
|
||||||
"MessageDragFilesIntoTrackOrder": "Faire glisser les fichiers dans l’ordre correct",
|
"MessageDragFilesIntoTrackOrder": "Faire glisser les fichiers dans l’ordre correct",
|
||||||
|
@ -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": "સાચવો અને બંધ કરો",
|
||||||
@ -122,6 +126,7 @@
|
|||||||
"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",
|
||||||
@ -176,8 +181,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",
|
||||||
@ -200,6 +208,7 @@
|
|||||||
"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 +227,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",
|
||||||
@ -256,6 +267,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",
|
||||||
@ -266,6 +278,7 @@
|
|||||||
"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",
|
||||||
@ -305,6 +318,7 @@
|
|||||||
"LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date",
|
"LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date",
|
||||||
"LabelMediaPlayer": "Media Player",
|
"LabelMediaPlayer": "Media Player",
|
||||||
"LabelMediaType": "Media Type",
|
"LabelMediaType": "Media Type",
|
||||||
|
"LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority",
|
||||||
"LabelMetadataProvider": "Metadata Provider",
|
"LabelMetadataProvider": "Metadata Provider",
|
||||||
"LabelMetaTag": "Meta Tag",
|
"LabelMetaTag": "Meta Tag",
|
||||||
"LabelMetaTags": "Meta Tags",
|
"LabelMetaTags": "Meta Tags",
|
||||||
@ -384,6 +398,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 +425,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 +438,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",
|
||||||
@ -527,12 +536,15 @@
|
|||||||
"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}\"?",
|
||||||
@ -546,6 +558,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",
|
||||||
|
@ -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": "सहेजें और बंद करें",
|
||||||
@ -122,6 +126,7 @@
|
|||||||
"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",
|
||||||
@ -176,8 +181,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",
|
||||||
@ -200,6 +208,7 @@
|
|||||||
"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 +227,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",
|
||||||
@ -256,6 +267,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",
|
||||||
@ -266,6 +278,7 @@
|
|||||||
"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",
|
||||||
@ -305,6 +318,7 @@
|
|||||||
"LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date",
|
"LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date",
|
||||||
"LabelMediaPlayer": "Media Player",
|
"LabelMediaPlayer": "Media Player",
|
||||||
"LabelMediaType": "Media Type",
|
"LabelMediaType": "Media Type",
|
||||||
|
"LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority",
|
||||||
"LabelMetadataProvider": "Metadata Provider",
|
"LabelMetadataProvider": "Metadata Provider",
|
||||||
"LabelMetaTag": "Meta Tag",
|
"LabelMetaTag": "Meta Tag",
|
||||||
"LabelMetaTags": "Meta Tags",
|
"LabelMetaTags": "Meta Tags",
|
||||||
@ -384,6 +398,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 +425,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 +438,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",
|
||||||
@ -527,12 +536,15 @@
|
|||||||
"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}\"?",
|
||||||
@ -546,6 +558,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",
|
||||||
|
@ -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",
|
||||||
@ -122,6 +126,7 @@
|
|||||||
"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",
|
||||||
@ -176,8 +181,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",
|
||||||
@ -200,6 +208,7 @@
|
|||||||
"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 +227,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",
|
||||||
@ -256,6 +267,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",
|
||||||
@ -266,6 +278,7 @@
|
|||||||
"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",
|
||||||
@ -305,6 +318,7 @@
|
|||||||
"LabelLookForNewEpisodesAfterDate": "Traži nove epizode nakon ovog datuma",
|
"LabelLookForNewEpisodesAfterDate": "Traži nove epizode nakon ovog datuma",
|
||||||
"LabelMediaPlayer": "Media Player",
|
"LabelMediaPlayer": "Media Player",
|
||||||
"LabelMediaType": "Media Type",
|
"LabelMediaType": "Media Type",
|
||||||
|
"LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority",
|
||||||
"LabelMetadataProvider": "Poslužitelj metapodataka ",
|
"LabelMetadataProvider": "Poslužitelj metapodataka ",
|
||||||
"LabelMetaTag": "Meta Tag",
|
"LabelMetaTag": "Meta Tag",
|
||||||
"LabelMetaTags": "Meta Tags",
|
"LabelMetaTags": "Meta Tags",
|
||||||
@ -384,6 +398,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 +425,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 +438,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",
|
||||||
@ -527,12 +536,15 @@
|
|||||||
"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}\"?",
|
||||||
@ -546,6 +558,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.",
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
{
|
{
|
||||||
"ButtonAdd": "Aggiungi",
|
"ButtonAdd": "Aggiungi",
|
||||||
"ButtonAddChapters": "Aggiungi Capitoli",
|
"ButtonAddChapters": "Aggiungi Capitoli",
|
||||||
|
"ButtonAddDevice": "Add Device",
|
||||||
|
"ButtonAddLibrary": "Add Library",
|
||||||
"ButtonAddPodcasts": "Aggiungi Podcast",
|
"ButtonAddPodcasts": "Aggiungi Podcast",
|
||||||
|
"ButtonAddUser": "Add 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": "Reset to default",
|
||||||
"ButtonRestore": "Ripristina",
|
"ButtonRestore": "Ripristina",
|
||||||
"ButtonSave": "Salva",
|
"ButtonSave": "Salva",
|
||||||
"ButtonSaveAndClose": "Salva & Chiudi",
|
"ButtonSaveAndClose": "Salva & Chiudi",
|
||||||
@ -122,6 +126,7 @@
|
|||||||
"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",
|
||||||
@ -176,8 +181,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": "Admin users only",
|
||||||
"LabelAll": "Tutti",
|
"LabelAll": "Tutti",
|
||||||
"LabelAllUsers": "Tutti gli Utenti",
|
"LabelAllUsers": "Tutti gli Utenti",
|
||||||
|
"LabelAllUsersExcludingGuests": "All users excluding guests",
|
||||||
|
"LabelAllUsersIncludingGuests": "All users including guests",
|
||||||
"LabelAlreadyInYourLibrary": "Già esistente nella libreria",
|
"LabelAlreadyInYourLibrary": "Già esistente nella libreria",
|
||||||
"LabelAppend": "Appese",
|
"LabelAppend": "Appese",
|
||||||
"LabelAuthor": "Autore",
|
"LabelAuthor": "Autore",
|
||||||
@ -200,6 +208,7 @@
|
|||||||
"LabelChapters": "Capitoli",
|
"LabelChapters": "Capitoli",
|
||||||
"LabelChaptersFound": "Capitoli Trovati",
|
"LabelChaptersFound": "Capitoli Trovati",
|
||||||
"LabelChapterTitle": "Titoli dei Capitoli",
|
"LabelChapterTitle": "Titoli dei Capitoli",
|
||||||
|
"LabelClickForMoreInfo": "Click for more info",
|
||||||
"LabelClosePlayer": "Chiudi player",
|
"LabelClosePlayer": "Chiudi player",
|
||||||
"LabelCodec": "Codec",
|
"LabelCodec": "Codec",
|
||||||
"LabelCollapseSeries": "Comprimi Serie",
|
"LabelCollapseSeries": "Comprimi Serie",
|
||||||
@ -218,10 +227,12 @@
|
|||||||
"LabelCurrently": "Attualmente:",
|
"LabelCurrently": "Attualmente:",
|
||||||
"LabelCustomCronExpression": "Custom Cron Expression:",
|
"LabelCustomCronExpression": "Custom Cron Expression:",
|
||||||
"LabelDatetime": "Data & Ora",
|
"LabelDatetime": "Data & Ora",
|
||||||
|
"LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)",
|
||||||
"LabelDescription": "Descrizione",
|
"LabelDescription": "Descrizione",
|
||||||
"LabelDeselectAll": "Deseleziona Tutto",
|
"LabelDeselectAll": "Deseleziona Tutto",
|
||||||
"LabelDevice": "Dispositivo",
|
"LabelDevice": "Dispositivo",
|
||||||
"LabelDeviceInfo": "Info Dispositivo",
|
"LabelDeviceInfo": "Info Dispositivo",
|
||||||
|
"LabelDeviceIsAvailableTo": "Device is available to...",
|
||||||
"LabelDirectory": "Elenco",
|
"LabelDirectory": "Elenco",
|
||||||
"LabelDiscFromFilename": "Disco dal nome file",
|
"LabelDiscFromFilename": "Disco dal nome file",
|
||||||
"LabelDiscFromMetadata": "Disco dal Metadata",
|
"LabelDiscFromMetadata": "Disco dal Metadata",
|
||||||
@ -256,6 +267,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",
|
||||||
@ -266,6 +278,7 @@
|
|||||||
"LabelHost": "Host",
|
"LabelHost": "Host",
|
||||||
"LabelHour": "Ora",
|
"LabelHour": "Ora",
|
||||||
"LabelIcon": "Icona",
|
"LabelIcon": "Icona",
|
||||||
|
"LabelImageURLFromTheWeb": "Image URL from the web",
|
||||||
"LabelIncludeInTracklist": "Includi nella Tracklist",
|
"LabelIncludeInTracklist": "Includi nella Tracklist",
|
||||||
"LabelIncomplete": "Incompleta",
|
"LabelIncomplete": "Incompleta",
|
||||||
"LabelInProgress": "In Corso",
|
"LabelInProgress": "In Corso",
|
||||||
@ -305,6 +318,7 @@
|
|||||||
"LabelLookForNewEpisodesAfterDate": "Cerca nuovi episodi dopo questa data",
|
"LabelLookForNewEpisodesAfterDate": "Cerca nuovi episodi dopo questa data",
|
||||||
"LabelMediaPlayer": "Media Player",
|
"LabelMediaPlayer": "Media Player",
|
||||||
"LabelMediaType": "Tipo Media",
|
"LabelMediaType": "Tipo Media",
|
||||||
|
"LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority",
|
||||||
"LabelMetadataProvider": "Metadata Provider",
|
"LabelMetadataProvider": "Metadata Provider",
|
||||||
"LabelMetaTag": "Meta Tag",
|
"LabelMetaTag": "Meta Tag",
|
||||||
"LabelMetaTags": "Meta Tags",
|
"LabelMetaTags": "Meta Tags",
|
||||||
@ -384,6 +398,7 @@
|
|||||||
"LabelSeason": "Stagione",
|
"LabelSeason": "Stagione",
|
||||||
"LabelSelectAllEpisodes": "Seleziona tutti gli Episodi",
|
"LabelSelectAllEpisodes": "Seleziona tutti gli Episodi",
|
||||||
"LabelSelectEpisodesShowing": "Episodi {0} selezionati ",
|
"LabelSelectEpisodesShowing": "Episodi {0} selezionati ",
|
||||||
|
"LabelSelectUsers": "Select users",
|
||||||
"LabelSendEbookToDevice": "Invia ebook a...",
|
"LabelSendEbookToDevice": "Invia ebook a...",
|
||||||
"LabelSequence": "Sequenza",
|
"LabelSequence": "Sequenza",
|
||||||
"LabelSeries": "Serie",
|
"LabelSeries": "Serie",
|
||||||
@ -410,16 +425,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 +438,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",
|
||||||
@ -527,12 +536,15 @@
|
|||||||
"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": "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": "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": "Are you sure you want to mark all episodes as not finished?",
|
||||||
"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": "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": "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": "Are you sure you want to remove author \"{0}\"?",
|
||||||
"MessageConfirmRemoveCollection": "Sei sicuro di voler rimuovere la Raccolta \"{0}\"?",
|
"MessageConfirmRemoveCollection": "Sei sicuro di voler rimuovere la Raccolta \"{0}\"?",
|
||||||
@ -546,6 +558,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": "Are you sure you want to re-scan {0} items?",
|
||||||
"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",
|
||||||
|
@ -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",
|
||||||
@ -122,6 +126,7 @@
|
|||||||
"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",
|
||||||
@ -176,8 +181,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",
|
||||||
@ -200,6 +208,7 @@
|
|||||||
"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 +227,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",
|
||||||
@ -256,6 +267,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",
|
||||||
@ -266,6 +278,7 @@
|
|||||||
"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",
|
||||||
@ -305,6 +318,7 @@
|
|||||||
"LabelLookForNewEpisodesAfterDate": "Ieškoti naujų epizodų po šios datos",
|
"LabelLookForNewEpisodesAfterDate": "Ieškoti naujų epizodų po šios datos",
|
||||||
"LabelMediaPlayer": "Grotuvas",
|
"LabelMediaPlayer": "Grotuvas",
|
||||||
"LabelMediaType": "Medijos tipas",
|
"LabelMediaType": "Medijos tipas",
|
||||||
|
"LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority",
|
||||||
"LabelMetadataProvider": "Metaduomenų tiekėjas",
|
"LabelMetadataProvider": "Metaduomenų tiekėjas",
|
||||||
"LabelMetaTag": "Meta žymė",
|
"LabelMetaTag": "Meta žymė",
|
||||||
"LabelMetaTags": "Meta žymos",
|
"LabelMetaTags": "Meta žymos",
|
||||||
@ -384,6 +398,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 +425,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 +438,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",
|
||||||
@ -527,12 +536,15 @@
|
|||||||
"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}\"?",
|
||||||
@ -546,6 +558,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",
|
||||||
|
@ -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",
|
||||||
@ -122,6 +126,7 @@
|
|||||||
"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",
|
||||||
@ -176,8 +181,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",
|
||||||
@ -200,6 +208,7 @@
|
|||||||
"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 +227,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",
|
||||||
@ -256,6 +267,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",
|
||||||
@ -266,6 +278,7 @@
|
|||||||
"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",
|
||||||
@ -305,6 +318,7 @@
|
|||||||
"LabelLookForNewEpisodesAfterDate": "Zoek naar nieuwe afleveringen na deze datum",
|
"LabelLookForNewEpisodesAfterDate": "Zoek naar nieuwe afleveringen na deze datum",
|
||||||
"LabelMediaPlayer": "Mediaspeler",
|
"LabelMediaPlayer": "Mediaspeler",
|
||||||
"LabelMediaType": "Mediatype",
|
"LabelMediaType": "Mediatype",
|
||||||
|
"LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority",
|
||||||
"LabelMetadataProvider": "Metadatabron",
|
"LabelMetadataProvider": "Metadatabron",
|
||||||
"LabelMetaTag": "Meta-tag",
|
"LabelMetaTag": "Meta-tag",
|
||||||
"LabelMetaTags": "Meta-tags",
|
"LabelMetaTags": "Meta-tags",
|
||||||
@ -384,6 +398,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 +425,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 +438,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",
|
||||||
@ -527,12 +536,15 @@
|
|||||||
"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?",
|
||||||
@ -546,6 +558,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",
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
{
|
{
|
||||||
"ButtonAdd": "Legg til",
|
"ButtonAdd": "Legg til",
|
||||||
"ButtonAddChapters": "Legg til kapittel",
|
"ButtonAddChapters": "Legg til kapittel",
|
||||||
|
"ButtonAddDevice": "Add Device",
|
||||||
|
"ButtonAddLibrary": "Add Library",
|
||||||
"ButtonAddPodcasts": "Legg til podcast",
|
"ButtonAddPodcasts": "Legg til podcast",
|
||||||
|
"ButtonAddUser": "Add User",
|
||||||
"ButtonAddYourFirstLibrary": "Legg til ditt første bibliotek",
|
"ButtonAddYourFirstLibrary": "Legg til ditt første bibliotek",
|
||||||
"ButtonApply": "Bruk",
|
"ButtonApply": "Bruk",
|
||||||
"ButtonApplyChapters": "Bruk kapittel",
|
"ButtonApplyChapters": "Bruk kapittel",
|
||||||
@ -59,6 +62,7 @@
|
|||||||
"ButtonRemoveSeriesFromContinueSeries": "Fjern serie fra Fortsett serie",
|
"ButtonRemoveSeriesFromContinueSeries": "Fjern serie fra Fortsett serie",
|
||||||
"ButtonReScan": "Skann på nytt",
|
"ButtonReScan": "Skann på nytt",
|
||||||
"ButtonReset": "Nullstill",
|
"ButtonReset": "Nullstill",
|
||||||
|
"ButtonResetToDefault": "Reset to default",
|
||||||
"ButtonRestore": "Gjenopprett",
|
"ButtonRestore": "Gjenopprett",
|
||||||
"ButtonSave": "Lagre",
|
"ButtonSave": "Lagre",
|
||||||
"ButtonSaveAndClose": "Lagre og lukk",
|
"ButtonSaveAndClose": "Lagre og lukk",
|
||||||
@ -122,6 +126,7 @@
|
|||||||
"HeaderManageTags": "Behandle tags",
|
"HeaderManageTags": "Behandle tags",
|
||||||
"HeaderMapDetails": "Kartleggingsdetaljer",
|
"HeaderMapDetails": "Kartleggingsdetaljer",
|
||||||
"HeaderMatch": "Tilpasse",
|
"HeaderMatch": "Tilpasse",
|
||||||
|
"HeaderMetadataOrderOfPrecedence": "Metadata order of precedence",
|
||||||
"HeaderMetadataToEmbed": "Metadata å bake inn",
|
"HeaderMetadataToEmbed": "Metadata å bake inn",
|
||||||
"HeaderNewAccount": "Ny konto",
|
"HeaderNewAccount": "Ny konto",
|
||||||
"HeaderNewLibrary": "Ny bibliotek",
|
"HeaderNewLibrary": "Ny bibliotek",
|
||||||
@ -176,8 +181,11 @@
|
|||||||
"LabelAddToCollectionBatch": "Legg {0} bøker til samling",
|
"LabelAddToCollectionBatch": "Legg {0} bøker til samling",
|
||||||
"LabelAddToPlaylist": "Legg til i spilleliste",
|
"LabelAddToPlaylist": "Legg til i spilleliste",
|
||||||
"LabelAddToPlaylistBatch": "Legg {0} enheter til i spilleliste",
|
"LabelAddToPlaylistBatch": "Legg {0} enheter til i spilleliste",
|
||||||
|
"LabelAdminUsersOnly": "Admin users only",
|
||||||
"LabelAll": "Alle",
|
"LabelAll": "Alle",
|
||||||
"LabelAllUsers": "Alle brukere",
|
"LabelAllUsers": "Alle brukere",
|
||||||
|
"LabelAllUsersExcludingGuests": "All users excluding guests",
|
||||||
|
"LabelAllUsersIncludingGuests": "All users including guests",
|
||||||
"LabelAlreadyInYourLibrary": "Allerede i biblioteket",
|
"LabelAlreadyInYourLibrary": "Allerede i biblioteket",
|
||||||
"LabelAppend": "Legge til",
|
"LabelAppend": "Legge til",
|
||||||
"LabelAuthor": "Forfatter",
|
"LabelAuthor": "Forfatter",
|
||||||
@ -200,6 +208,7 @@
|
|||||||
"LabelChapters": "Kapitler",
|
"LabelChapters": "Kapitler",
|
||||||
"LabelChaptersFound": "kapitler funnet",
|
"LabelChaptersFound": "kapitler funnet",
|
||||||
"LabelChapterTitle": "Kapittel tittel",
|
"LabelChapterTitle": "Kapittel tittel",
|
||||||
|
"LabelClickForMoreInfo": "Click for more info",
|
||||||
"LabelClosePlayer": "Lukk spiller",
|
"LabelClosePlayer": "Lukk spiller",
|
||||||
"LabelCodec": "Kodek",
|
"LabelCodec": "Kodek",
|
||||||
"LabelCollapseSeries": "Minimer serier",
|
"LabelCollapseSeries": "Minimer serier",
|
||||||
@ -218,10 +227,12 @@
|
|||||||
"LabelCurrently": "Nåværende:",
|
"LabelCurrently": "Nåværende:",
|
||||||
"LabelCustomCronExpression": "Tilpasset Cron utrykk:",
|
"LabelCustomCronExpression": "Tilpasset Cron utrykk:",
|
||||||
"LabelDatetime": "Dato tid",
|
"LabelDatetime": "Dato tid",
|
||||||
|
"LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)",
|
||||||
"LabelDescription": "Beskrivelse",
|
"LabelDescription": "Beskrivelse",
|
||||||
"LabelDeselectAll": "Fjern valg",
|
"LabelDeselectAll": "Fjern valg",
|
||||||
"LabelDevice": "Enhet",
|
"LabelDevice": "Enhet",
|
||||||
"LabelDeviceInfo": "Enhetsinformasjon",
|
"LabelDeviceInfo": "Enhetsinformasjon",
|
||||||
|
"LabelDeviceIsAvailableTo": "Device is available to...",
|
||||||
"LabelDirectory": "Mappe",
|
"LabelDirectory": "Mappe",
|
||||||
"LabelDiscFromFilename": "Disk fra filnavn",
|
"LabelDiscFromFilename": "Disk fra filnavn",
|
||||||
"LabelDiscFromMetadata": "Disk fra metadata",
|
"LabelDiscFromMetadata": "Disk fra metadata",
|
||||||
@ -256,6 +267,7 @@
|
|||||||
"LabelFinished": "Fullført",
|
"LabelFinished": "Fullført",
|
||||||
"LabelFolder": "Mappe",
|
"LabelFolder": "Mappe",
|
||||||
"LabelFolders": "Mapper",
|
"LabelFolders": "Mapper",
|
||||||
|
"LabelFontFamily": "Fontfamilie",
|
||||||
"LabelFontScale": "Font størrelse",
|
"LabelFontScale": "Font størrelse",
|
||||||
"LabelFormat": "Format",
|
"LabelFormat": "Format",
|
||||||
"LabelGenre": "Sjanger",
|
"LabelGenre": "Sjanger",
|
||||||
@ -266,6 +278,7 @@
|
|||||||
"LabelHost": "Tjener",
|
"LabelHost": "Tjener",
|
||||||
"LabelHour": "Time",
|
"LabelHour": "Time",
|
||||||
"LabelIcon": "Ikon",
|
"LabelIcon": "Ikon",
|
||||||
|
"LabelImageURLFromTheWeb": "Image URL from the web",
|
||||||
"LabelIncludeInTracklist": "Inkluder i sporliste",
|
"LabelIncludeInTracklist": "Inkluder i sporliste",
|
||||||
"LabelIncomplete": "Ufullstendig",
|
"LabelIncomplete": "Ufullstendig",
|
||||||
"LabelInProgress": "I gang",
|
"LabelInProgress": "I gang",
|
||||||
@ -305,6 +318,7 @@
|
|||||||
"LabelLookForNewEpisodesAfterDate": "Se etter nye episoder etter denne datoen",
|
"LabelLookForNewEpisodesAfterDate": "Se etter nye episoder etter denne datoen",
|
||||||
"LabelMediaPlayer": "Mediespiller",
|
"LabelMediaPlayer": "Mediespiller",
|
||||||
"LabelMediaType": "Medie type",
|
"LabelMediaType": "Medie type",
|
||||||
|
"LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority",
|
||||||
"LabelMetadataProvider": "Metadata Leverandør",
|
"LabelMetadataProvider": "Metadata Leverandør",
|
||||||
"LabelMetaTag": "Meta Tag",
|
"LabelMetaTag": "Meta Tag",
|
||||||
"LabelMetaTags": "Meta Tags",
|
"LabelMetaTags": "Meta Tags",
|
||||||
@ -384,6 +398,7 @@
|
|||||||
"LabelSeason": "Sesong",
|
"LabelSeason": "Sesong",
|
||||||
"LabelSelectAllEpisodes": "Velg alle episoder",
|
"LabelSelectAllEpisodes": "Velg alle episoder",
|
||||||
"LabelSelectEpisodesShowing": "Velg {0} episoder vist",
|
"LabelSelectEpisodesShowing": "Velg {0} episoder vist",
|
||||||
|
"LabelSelectUsers": "Select users",
|
||||||
"LabelSendEbookToDevice": "Send Ebok til...",
|
"LabelSendEbookToDevice": "Send Ebok til...",
|
||||||
"LabelSequence": "Sekvens",
|
"LabelSequence": "Sekvens",
|
||||||
"LabelSeries": "Serier",
|
"LabelSeries": "Serier",
|
||||||
@ -410,16 +425,10 @@
|
|||||||
"LabelSettingsHideSingleBookSeriesHelp": "Serier som har kun en bok vil bli gjemt på serie- og hjemmeside hyllen.",
|
"LabelSettingsHideSingleBookSeriesHelp": "Serier som har kun en bok vil bli gjemt på serie- og hjemmeside hyllen.",
|
||||||
"LabelSettingsHomePageBookshelfView": "Hjemmeside bruk bokhyllevisning",
|
"LabelSettingsHomePageBookshelfView": "Hjemmeside bruk bokhyllevisning",
|
||||||
"LabelSettingsLibraryBookshelfView": "Bibliotek bruk bokhyllevisning",
|
"LabelSettingsLibraryBookshelfView": "Bibliotek bruk bokhyllevisning",
|
||||||
"LabelSettingsOverdriveMediaMarkers": "Bruk Overdrive mediemerker for kapittel",
|
|
||||||
"LabelSettingsOverdriveMediaMarkersHelp": "MP3 filer fra Overdrive kommer med kapittel tider bakt inn so egendefinert metadata. Aktiveres dette vil disse taggene bli brukt som kapittel tider automatisk",
|
|
||||||
"LabelSettingsParseSubtitles": "Analyser undertekster",
|
"LabelSettingsParseSubtitles": "Analyser undertekster",
|
||||||
"LabelSettingsParseSubtitlesHelp": "Trekk ut undertekster fra lydbok mappenavn.<br>undertekster må være separert med \" - \"<br>f.eks. \"Boktittel - Undertekst her\" har Undertekst \"Undertekst her\"",
|
"LabelSettingsParseSubtitlesHelp": "Trekk ut undertekster fra lydbok mappenavn.<br>undertekster må være separert med \" - \"<br>f.eks. \"Boktittel - Undertekst her\" har Undertekst \"Undertekst her\"",
|
||||||
"LabelSettingsPreferAudioMetadata": "Foretrekk lyd metadata",
|
|
||||||
"LabelSettingsPreferAudioMetadataHelp": "Lydfil ID3 meta tagger vil bli brukt som bokdetaljer i stedet fro mappenavn",
|
|
||||||
"LabelSettingsPreferMatchedMetadata": "Foretrekk funnet metadata",
|
"LabelSettingsPreferMatchedMetadata": "Foretrekk funnet metadata",
|
||||||
"LabelSettingsPreferMatchedMetadataHelp": "Funnet data vil overskrive enhetens detaljene når man bruker Kjapt søk. Som standard vil Kjapt søk kun fylle inn manglende detaljer.",
|
"LabelSettingsPreferMatchedMetadataHelp": "Funnet data vil overskrive enhetens detaljene når man bruker Kjapt søk. Som standard vil Kjapt søk kun fylle inn manglende detaljer.",
|
||||||
"LabelSettingsPreferOPFMetadata": "Foretrekk OPF metadata",
|
|
||||||
"LabelSettingsPreferOPFMetadataHelp": "OPF fil metadata vil bli brukt som bokdetaljer i stedet fro mappenavn",
|
|
||||||
"LabelSettingsSkipMatchingBooksWithASIN": "Hopp over bøker som allerede har ASIN",
|
"LabelSettingsSkipMatchingBooksWithASIN": "Hopp over bøker som allerede har ASIN",
|
||||||
"LabelSettingsSkipMatchingBooksWithISBN": "Hopp over bøker som allerede har ISBN",
|
"LabelSettingsSkipMatchingBooksWithISBN": "Hopp over bøker som allerede har ISBN",
|
||||||
"LabelSettingsSortingIgnorePrefixes": "Ignorer prefiks når under sortering",
|
"LabelSettingsSortingIgnorePrefixes": "Ignorer prefiks når under sortering",
|
||||||
@ -429,7 +438,7 @@
|
|||||||
"LabelSettingsStoreCoversWithItem": "Lagre bokomslag med gjenstand",
|
"LabelSettingsStoreCoversWithItem": "Lagre bokomslag med gjenstand",
|
||||||
"LabelSettingsStoreCoversWithItemHelp": "Som standard vil bokomslag bli lagret under /metadata/items, aktiveres dette valget vil bokomslag bli lagret i samme mappe som gjenstanden. Kun en fil med navn \"cover\" vil bli beholdt",
|
"LabelSettingsStoreCoversWithItemHelp": "Som standard vil bokomslag bli lagret under /metadata/items, aktiveres dette valget vil bokomslag bli lagret i samme mappe som gjenstanden. Kun en fil med navn \"cover\" vil bli beholdt",
|
||||||
"LabelSettingsStoreMetadataWithItem": "Lagre metadata med gjenstand",
|
"LabelSettingsStoreMetadataWithItem": "Lagre metadata med gjenstand",
|
||||||
"LabelSettingsStoreMetadataWithItemHelp": "Som standard vil metadata bli lagret under /metadata/items, aktiveres dette valget vil metadata bli lagret i samme mappe som gjenstanden. Bruker .abs filetternavn",
|
"LabelSettingsStoreMetadataWithItemHelp": "Som standard vil metadata bli lagret under /metadata/items, aktiveres dette valget vil metadata bli lagret i samme mappe som gjenstanden",
|
||||||
"LabelSettingsTimeFormat": "Tid format",
|
"LabelSettingsTimeFormat": "Tid format",
|
||||||
"LabelShowAll": "Vis alt",
|
"LabelShowAll": "Vis alt",
|
||||||
"LabelSize": "Størrelse",
|
"LabelSize": "Størrelse",
|
||||||
@ -527,12 +536,15 @@
|
|||||||
"MessageConfirmDeleteBackup": "Er du sikker på at du vil slette sikkerhetskopi for {0}?",
|
"MessageConfirmDeleteBackup": "Er du sikker på at du vil slette sikkerhetskopi for {0}?",
|
||||||
"MessageConfirmDeleteFile": "Dette vil slette filen fra filsystemet. Er du sikker?",
|
"MessageConfirmDeleteFile": "Dette vil slette filen fra filsystemet. Er du sikker?",
|
||||||
"MessageConfirmDeleteLibrary": "Er du sikker på at du vil slette biblioteket \"{0}\" for godt?",
|
"MessageConfirmDeleteLibrary": "Er du sikker på at du vil slette biblioteket \"{0}\" for godt?",
|
||||||
|
"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 sesjonen?",
|
"MessageConfirmDeleteSession": "Er du sikker på at du vil slette denne sesjonen?",
|
||||||
"MessageConfirmForceReScan": "Er du sikker på at du vil tvinge en ny skann?",
|
"MessageConfirmForceReScan": "Er du sikker på at du vil tvinge en ny skann?",
|
||||||
"MessageConfirmMarkAllEpisodesFinished": "Er du sikker på at du vil markere alle episodene som fullført?",
|
"MessageConfirmMarkAllEpisodesFinished": "Er du sikker på at du vil markere alle episodene som fullført?",
|
||||||
"MessageConfirmMarkAllEpisodesNotFinished": "Er du sikker på at du vil markere alle episodene som ikke fullført?",
|
"MessageConfirmMarkAllEpisodesNotFinished": "Er du sikker på at du vil markere alle episodene som ikke fullført?",
|
||||||
"MessageConfirmMarkSeriesFinished": "Er du sikker på at du vil markere alle bøkene i serien som fullført?",
|
"MessageConfirmMarkSeriesFinished": "Er du sikker på at du vil markere alle bøkene i serien som fullført?",
|
||||||
"MessageConfirmMarkSeriesNotFinished": "Er du sikker på at du vil markere alle bøkene i serien som ikke fullført?",
|
"MessageConfirmMarkSeriesNotFinished": "Er du sikker på at du vil markere alle bøkene i serien som ikke fullført?",
|
||||||
|
"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?",
|
"MessageConfirmRemoveAllChapters": "Er du sikker på at du vil fjerne alle kapitler?",
|
||||||
"MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?",
|
"MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?",
|
||||||
"MessageConfirmRemoveCollection": "Er du sikker på at du vil fjerne samling\"{0}\"?",
|
"MessageConfirmRemoveCollection": "Er du sikker på at du vil fjerne samling\"{0}\"?",
|
||||||
@ -546,6 +558,7 @@
|
|||||||
"MessageConfirmRenameTag": "Er du sikker på at du vil endre tag \"{0}\" til \"{1}\" for alle gjenstandene?",
|
"MessageConfirmRenameTag": "Er du sikker på at du vil endre tag \"{0}\" til \"{1}\" for alle gjenstandene?",
|
||||||
"MessageConfirmRenameTagMergeNote": "Notis: Denne taggen finnes allerede så de vil bli slått sammen.",
|
"MessageConfirmRenameTagMergeNote": "Notis: Denne taggen finnes allerede så de vil bli slått sammen.",
|
||||||
"MessageConfirmRenameTagWarning": "Advarsel! En lignende tag eksisterer allerede (med forsjellige store / små bokstaver) \"{0}\".",
|
"MessageConfirmRenameTagWarning": "Advarsel! En lignende tag eksisterer allerede (med forsjellige store / små bokstaver) \"{0}\".",
|
||||||
|
"MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?",
|
||||||
"MessageConfirmSendEbookToDevice": "Er du sikker på at du vil sende {0} ebok \"{1}\" til enhet \"{2}\"?",
|
"MessageConfirmSendEbookToDevice": "Er du sikker på at du vil sende {0} ebok \"{1}\" til enhet \"{2}\"?",
|
||||||
"MessageDownloadingEpisode": "Laster ned episode",
|
"MessageDownloadingEpisode": "Laster ned episode",
|
||||||
"MessageDragFilesIntoTrackOrder": "Dra filene i rett spor rekkefølge",
|
"MessageDragFilesIntoTrackOrder": "Dra filene i rett spor rekkefølge",
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
{
|
{
|
||||||
"ButtonAdd": "Dodaj",
|
"ButtonAdd": "Dodaj",
|
||||||
"ButtonAddChapters": "Dodaj rozdziały",
|
"ButtonAddChapters": "Dodaj rozdziały",
|
||||||
|
"ButtonAddDevice": "Add Device",
|
||||||
|
"ButtonAddLibrary": "Add Library",
|
||||||
"ButtonAddPodcasts": "Dodaj podcasty",
|
"ButtonAddPodcasts": "Dodaj podcasty",
|
||||||
|
"ButtonAddUser": "Add User",
|
||||||
"ButtonAddYourFirstLibrary": "Dodaj swoją pierwszą bibliotekę",
|
"ButtonAddYourFirstLibrary": "Dodaj swoją pierwszą bibliotekę",
|
||||||
"ButtonApply": "Zatwierdź",
|
"ButtonApply": "Zatwierdź",
|
||||||
"ButtonApplyChapters": "Zatwierdź rozdziały",
|
"ButtonApplyChapters": "Zatwierdź rozdziały",
|
||||||
@ -59,6 +62,7 @@
|
|||||||
"ButtonRemoveSeriesFromContinueSeries": "Usuń serię z listy odtwarzania",
|
"ButtonRemoveSeriesFromContinueSeries": "Usuń serię z listy odtwarzania",
|
||||||
"ButtonReScan": "Ponowne skanowanie",
|
"ButtonReScan": "Ponowne skanowanie",
|
||||||
"ButtonReset": "Resetowanie",
|
"ButtonReset": "Resetowanie",
|
||||||
|
"ButtonResetToDefault": "Reset to default",
|
||||||
"ButtonRestore": "Przywróć",
|
"ButtonRestore": "Przywróć",
|
||||||
"ButtonSave": "Zapisz",
|
"ButtonSave": "Zapisz",
|
||||||
"ButtonSaveAndClose": "Zapisz i zamknij",
|
"ButtonSaveAndClose": "Zapisz i zamknij",
|
||||||
@ -122,6 +126,7 @@
|
|||||||
"HeaderManageTags": "Manage Tags",
|
"HeaderManageTags": "Manage Tags",
|
||||||
"HeaderMapDetails": "Map details",
|
"HeaderMapDetails": "Map details",
|
||||||
"HeaderMatch": "Dopasuj",
|
"HeaderMatch": "Dopasuj",
|
||||||
|
"HeaderMetadataOrderOfPrecedence": "Metadata order of precedence",
|
||||||
"HeaderMetadataToEmbed": "Osadź metadane",
|
"HeaderMetadataToEmbed": "Osadź metadane",
|
||||||
"HeaderNewAccount": "Nowe konto",
|
"HeaderNewAccount": "Nowe konto",
|
||||||
"HeaderNewLibrary": "Nowa biblioteka",
|
"HeaderNewLibrary": "Nowa biblioteka",
|
||||||
@ -176,8 +181,11 @@
|
|||||||
"LabelAddToCollectionBatch": "Dodaj {0} książki do kolekcji",
|
"LabelAddToCollectionBatch": "Dodaj {0} książki do kolekcji",
|
||||||
"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": "Wszyscy użytkownicy",
|
"LabelAllUsers": "Wszyscy użytkownicy",
|
||||||
|
"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",
|
||||||
@ -200,6 +208,7 @@
|
|||||||
"LabelChapters": "Chapters",
|
"LabelChapters": "Chapters",
|
||||||
"LabelChaptersFound": "Znalezione rozdziały",
|
"LabelChaptersFound": "Znalezione rozdziały",
|
||||||
"LabelChapterTitle": "Tytuł rozdziału",
|
"LabelChapterTitle": "Tytuł rozdziału",
|
||||||
|
"LabelClickForMoreInfo": "Click for more info",
|
||||||
"LabelClosePlayer": "Zamknij odtwarzacz",
|
"LabelClosePlayer": "Zamknij odtwarzacz",
|
||||||
"LabelCodec": "Codec",
|
"LabelCodec": "Codec",
|
||||||
"LabelCollapseSeries": "Podsumuj serię",
|
"LabelCollapseSeries": "Podsumuj serię",
|
||||||
@ -218,10 +227,12 @@
|
|||||||
"LabelCurrently": "Obecnie:",
|
"LabelCurrently": "Obecnie:",
|
||||||
"LabelCustomCronExpression": "Custom Cron Expression:",
|
"LabelCustomCronExpression": "Custom Cron Expression:",
|
||||||
"LabelDatetime": "Data i godzina",
|
"LabelDatetime": "Data i godzina",
|
||||||
|
"LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)",
|
||||||
"LabelDescription": "Opis",
|
"LabelDescription": "Opis",
|
||||||
"LabelDeselectAll": "Odznacz wszystko",
|
"LabelDeselectAll": "Odznacz wszystko",
|
||||||
"LabelDevice": "Urządzenie",
|
"LabelDevice": "Urządzenie",
|
||||||
"LabelDeviceInfo": "Informacja o urządzeniu",
|
"LabelDeviceInfo": "Informacja o urządzeniu",
|
||||||
|
"LabelDeviceIsAvailableTo": "Device is available to...",
|
||||||
"LabelDirectory": "Katalog",
|
"LabelDirectory": "Katalog",
|
||||||
"LabelDiscFromFilename": "Oznaczenie dysku z nazwy pliku",
|
"LabelDiscFromFilename": "Oznaczenie dysku z nazwy pliku",
|
||||||
"LabelDiscFromMetadata": "Oznaczenie dysku z metadanych",
|
"LabelDiscFromMetadata": "Oznaczenie dysku z metadanych",
|
||||||
@ -256,6 +267,7 @@
|
|||||||
"LabelFinished": "Zakończone",
|
"LabelFinished": "Zakończone",
|
||||||
"LabelFolder": "Folder",
|
"LabelFolder": "Folder",
|
||||||
"LabelFolders": "Foldery",
|
"LabelFolders": "Foldery",
|
||||||
|
"LabelFontFamily": "Rodzina czcionek",
|
||||||
"LabelFontScale": "Font scale",
|
"LabelFontScale": "Font scale",
|
||||||
"LabelFormat": "Format",
|
"LabelFormat": "Format",
|
||||||
"LabelGenre": "Gatunek",
|
"LabelGenre": "Gatunek",
|
||||||
@ -266,6 +278,7 @@
|
|||||||
"LabelHost": "Host",
|
"LabelHost": "Host",
|
||||||
"LabelHour": "Godzina",
|
"LabelHour": "Godzina",
|
||||||
"LabelIcon": "Ikona",
|
"LabelIcon": "Ikona",
|
||||||
|
"LabelImageURLFromTheWeb": "Image URL from the web",
|
||||||
"LabelIncludeInTracklist": "Dołącz do listy odtwarzania",
|
"LabelIncludeInTracklist": "Dołącz do listy odtwarzania",
|
||||||
"LabelIncomplete": "Nieukończone",
|
"LabelIncomplete": "Nieukończone",
|
||||||
"LabelInProgress": "W trakcie",
|
"LabelInProgress": "W trakcie",
|
||||||
@ -305,6 +318,7 @@
|
|||||||
"LabelLookForNewEpisodesAfterDate": "Szukaj nowych odcinków po dacie",
|
"LabelLookForNewEpisodesAfterDate": "Szukaj nowych odcinków po dacie",
|
||||||
"LabelMediaPlayer": "Odtwarzacz",
|
"LabelMediaPlayer": "Odtwarzacz",
|
||||||
"LabelMediaType": "Typ mediów",
|
"LabelMediaType": "Typ mediów",
|
||||||
|
"LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority",
|
||||||
"LabelMetadataProvider": "Dostawca metadanych",
|
"LabelMetadataProvider": "Dostawca metadanych",
|
||||||
"LabelMetaTag": "Tag",
|
"LabelMetaTag": "Tag",
|
||||||
"LabelMetaTags": "Meta Tags",
|
"LabelMetaTags": "Meta Tags",
|
||||||
@ -384,6 +398,7 @@
|
|||||||
"LabelSeason": "Sezon",
|
"LabelSeason": "Sezon",
|
||||||
"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": "Kolejność",
|
"LabelSequence": "Kolejność",
|
||||||
"LabelSeries": "Serie",
|
"LabelSeries": "Serie",
|
||||||
@ -410,16 +425,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": "Widok półki z książkami na stronie głównej",
|
"LabelSettingsHomePageBookshelfView": "Widok półki z książkami na stronie głównej",
|
||||||
"LabelSettingsLibraryBookshelfView": "Widok półki z książkami na stronie biblioteki",
|
"LabelSettingsLibraryBookshelfView": "Widok półki z książkami na stronie biblioteki",
|
||||||
"LabelSettingsOverdriveMediaMarkers": "Użyj markerów Overdrive Media Markers dla rozdziałów",
|
|
||||||
"LabelSettingsOverdriveMediaMarkersHelp": "Pliki MP3 z serwisu Overdrive mają wbudowane znaczniki czasu rozdziałów jako niestandardowe metadane. Włączenie tej funkcji spowoduje automatyczne użycie tych znaczników do oznaczania czasu rozdziałów.",
|
|
||||||
"LabelSettingsParseSubtitles": "Przetwarzaj podtytuły",
|
"LabelSettingsParseSubtitles": "Przetwarzaj podtytuły",
|
||||||
"LabelSettingsParseSubtitlesHelp": "Opcja pozwala na pobranie podtytułu z nazwy folderu z audiobookiem. <br>Podtytuł musi być rozdzielony za pomocą separatora \" - \"<br>Przykład: \"Book Title - A Subtitle Here\" podtytuł \"A Subtitle Here\"",
|
"LabelSettingsParseSubtitlesHelp": "Opcja pozwala na pobranie podtytułu z nazwy folderu z audiobookiem. <br>Podtytuł musi być rozdzielony za pomocą separatora \" - \"<br>Przykład: \"Book Title - A Subtitle Here\" podtytuł \"A Subtitle Here\"",
|
||||||
"LabelSettingsPreferAudioMetadata": "Preferuj metadane audio",
|
|
||||||
"LabelSettingsPreferAudioMetadataHelp": "Znaczniki meta ID3 plików audio będą używane dla szczegółów książki zamiast nazw folderów",
|
|
||||||
"LabelSettingsPreferMatchedMetadata": "Preferowanie dopasowanych metadanych",
|
"LabelSettingsPreferMatchedMetadata": "Preferowanie dopasowanych metadanych",
|
||||||
"LabelSettingsPreferMatchedMetadataHelp": "Dopasowane dane będą miały pierwszeństwo nad szczegółami pozycji podczas używania Szybkiego dopasowania. Domyślnie Szybkie dopasowanie uzupełnia tylko brakujące szczegóły.",
|
"LabelSettingsPreferMatchedMetadataHelp": "Dopasowane dane będą miały pierwszeństwo nad szczegółami pozycji podczas używania Szybkiego dopasowania. Domyślnie Szybkie dopasowanie uzupełnia tylko brakujące szczegóły.",
|
||||||
"LabelSettingsPreferOPFMetadata": "Preferowanie metadanych OPF",
|
|
||||||
"LabelSettingsPreferOPFMetadataHelp": "Metadane pliku OPF będą używane dla szczegółów książki zamiast nazw folderów",
|
|
||||||
"LabelSettingsSkipMatchingBooksWithASIN": "Pomiń dopasowanie książek, które już mają ASIN",
|
"LabelSettingsSkipMatchingBooksWithASIN": "Pomiń dopasowanie książek, które już mają ASIN",
|
||||||
"LabelSettingsSkipMatchingBooksWithISBN": "Pomiń dopasowanie książek, które już mają ISBN",
|
"LabelSettingsSkipMatchingBooksWithISBN": "Pomiń dopasowanie książek, które już mają ISBN",
|
||||||
"LabelSettingsSortingIgnorePrefixes": "Ignoruj prefiksy podczas sortowania",
|
"LabelSettingsSortingIgnorePrefixes": "Ignoruj prefiksy podczas sortowania",
|
||||||
@ -429,7 +438,7 @@
|
|||||||
"LabelSettingsStoreCoversWithItem": "Przechowuj okładkę w folderze książki",
|
"LabelSettingsStoreCoversWithItem": "Przechowuj okładkę w folderze książki",
|
||||||
"LabelSettingsStoreCoversWithItemHelp": "Domyślnie okładki są przechowywane w folderze /metadata/items, włączenie tej opcji spowoduje, że okładka będzie przechowywana w folderze ksiązki. Tylko jedna okładka o nazwie pliku \"cover\" będzie przechowywana.",
|
"LabelSettingsStoreCoversWithItemHelp": "Domyślnie okładki są przechowywane w folderze /metadata/items, włączenie tej opcji spowoduje, że okładka będzie przechowywana w folderze ksiązki. Tylko jedna okładka o nazwie pliku \"cover\" będzie przechowywana.",
|
||||||
"LabelSettingsStoreMetadataWithItem": "Przechowuj metadane w folderze książki",
|
"LabelSettingsStoreMetadataWithItem": "Przechowuj metadane w folderze książki",
|
||||||
"LabelSettingsStoreMetadataWithItemHelp": "Domyślnie metadane są przechowywane w folderze /metadata/items, włączenie tej opcji spowoduje, że okładka będzie przechowywana w folderze ksiązki. Tylko jedna okładka o nazwie pliku \"cover\" będzie przechowywana. Rozszerzenie pliku metadanych: .abs",
|
"LabelSettingsStoreMetadataWithItemHelp": "Domyślnie metadane są przechowywane w folderze /metadata/items, włączenie tej opcji spowoduje, że okładka będzie przechowywana w folderze ksiązki. Tylko jedna okładka o nazwie pliku \"cover\" będzie przechowywana",
|
||||||
"LabelSettingsTimeFormat": "Time Format",
|
"LabelSettingsTimeFormat": "Time Format",
|
||||||
"LabelShowAll": "Pokaż wszystko",
|
"LabelShowAll": "Pokaż wszystko",
|
||||||
"LabelSize": "Rozmiar",
|
"LabelSize": "Rozmiar",
|
||||||
@ -527,12 +536,15 @@
|
|||||||
"MessageConfirmDeleteBackup": "Czy na pewno chcesz usunąć kopię zapasową dla {0}?",
|
"MessageConfirmDeleteBackup": "Czy na pewno chcesz usunąć kopię zapasową dla {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": "Czy na pewno chcesz trwale usunąć bibliotekę \"{0}\"?",
|
"MessageConfirmDeleteLibrary": "Czy na pewno chcesz trwale usunąć 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": "Czy na pewno chcesz usunąć tę sesję?",
|
"MessageConfirmDeleteSession": "Czy na pewno chcesz usunąć tę sesję?",
|
||||||
"MessageConfirmForceReScan": "Czy na pewno chcesz wymusić ponowne skanowanie?",
|
"MessageConfirmForceReScan": "Czy na pewno chcesz wymusić ponowne skanowanie?",
|
||||||
"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": "Czy na pewno chcesz usunąć kolekcję \"{0}\"?",
|
"MessageConfirmRemoveCollection": "Czy na pewno chcesz usunąć kolekcję \"{0}\"?",
|
||||||
@ -546,6 +558,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": "Pobieranie odcinka",
|
"MessageDownloadingEpisode": "Pobieranie odcinka",
|
||||||
"MessageDragFilesIntoTrackOrder": "przeciągnij pliki aby ustawić właściwą kolejność utworów",
|
"MessageDragFilesIntoTrackOrder": "przeciągnij pliki aby ustawić właściwą kolejność utworów",
|
||||||
|
@ -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": "Сохранить и закрыть",
|
||||||
@ -122,6 +126,7 @@
|
|||||||
"HeaderManageTags": "Редактировать теги",
|
"HeaderManageTags": "Редактировать теги",
|
||||||
"HeaderMapDetails": "Найти подробности",
|
"HeaderMapDetails": "Найти подробности",
|
||||||
"HeaderMatch": "Поиск",
|
"HeaderMatch": "Поиск",
|
||||||
|
"HeaderMetadataOrderOfPrecedence": "Metadata order of precedence",
|
||||||
"HeaderMetadataToEmbed": "Метаинформация для встраивания",
|
"HeaderMetadataToEmbed": "Метаинформация для встраивания",
|
||||||
"HeaderNewAccount": "Новая учетная запись",
|
"HeaderNewAccount": "Новая учетная запись",
|
||||||
"HeaderNewLibrary": "Новая библиотека",
|
"HeaderNewLibrary": "Новая библиотека",
|
||||||
@ -176,8 +181,11 @@
|
|||||||
"LabelAddToCollectionBatch": "Добавить {0} книг в коллекцию",
|
"LabelAddToCollectionBatch": "Добавить {0} книг в коллекцию",
|
||||||
"LabelAddToPlaylist": "Добавить в плейлист",
|
"LabelAddToPlaylist": "Добавить в плейлист",
|
||||||
"LabelAddToPlaylistBatch": "Добавить {0} элементов в плейлист",
|
"LabelAddToPlaylistBatch": "Добавить {0} элементов в плейлист",
|
||||||
|
"LabelAdminUsersOnly": "Admin users only",
|
||||||
"LabelAll": "Все",
|
"LabelAll": "Все",
|
||||||
"LabelAllUsers": "Все пользователи",
|
"LabelAllUsers": "Все пользователи",
|
||||||
|
"LabelAllUsersExcludingGuests": "All users excluding guests",
|
||||||
|
"LabelAllUsersIncludingGuests": "All users including guests",
|
||||||
"LabelAlreadyInYourLibrary": "Уже в Вашей библиотеке",
|
"LabelAlreadyInYourLibrary": "Уже в Вашей библиотеке",
|
||||||
"LabelAppend": "Добавить",
|
"LabelAppend": "Добавить",
|
||||||
"LabelAuthor": "Автор",
|
"LabelAuthor": "Автор",
|
||||||
@ -200,6 +208,7 @@
|
|||||||
"LabelChapters": "Главы",
|
"LabelChapters": "Главы",
|
||||||
"LabelChaptersFound": "глав найдено",
|
"LabelChaptersFound": "глав найдено",
|
||||||
"LabelChapterTitle": "Название главы",
|
"LabelChapterTitle": "Название главы",
|
||||||
|
"LabelClickForMoreInfo": "Click for more info",
|
||||||
"LabelClosePlayer": "Закрыть проигрыватель",
|
"LabelClosePlayer": "Закрыть проигрыватель",
|
||||||
"LabelCodec": "Кодек",
|
"LabelCodec": "Кодек",
|
||||||
"LabelCollapseSeries": "Свернуть серии",
|
"LabelCollapseSeries": "Свернуть серии",
|
||||||
@ -218,10 +227,12 @@
|
|||||||
"LabelCurrently": "Текущее:",
|
"LabelCurrently": "Текущее:",
|
||||||
"LabelCustomCronExpression": "Пользовательское выражение Cron:",
|
"LabelCustomCronExpression": "Пользовательское выражение Cron:",
|
||||||
"LabelDatetime": "Дата и время",
|
"LabelDatetime": "Дата и время",
|
||||||
|
"LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)",
|
||||||
"LabelDescription": "Описание",
|
"LabelDescription": "Описание",
|
||||||
"LabelDeselectAll": "Снять выделение",
|
"LabelDeselectAll": "Снять выделение",
|
||||||
"LabelDevice": "Устройство",
|
"LabelDevice": "Устройство",
|
||||||
"LabelDeviceInfo": "Информация об устройстве",
|
"LabelDeviceInfo": "Информация об устройстве",
|
||||||
|
"LabelDeviceIsAvailableTo": "Device is available to...",
|
||||||
"LabelDirectory": "Каталог",
|
"LabelDirectory": "Каталог",
|
||||||
"LabelDiscFromFilename": "Диск из Имени файла",
|
"LabelDiscFromFilename": "Диск из Имени файла",
|
||||||
"LabelDiscFromMetadata": "Диск из Метаданных",
|
"LabelDiscFromMetadata": "Диск из Метаданных",
|
||||||
@ -256,6 +267,7 @@
|
|||||||
"LabelFinished": "Закончен",
|
"LabelFinished": "Закончен",
|
||||||
"LabelFolder": "Папка",
|
"LabelFolder": "Папка",
|
||||||
"LabelFolders": "Папки",
|
"LabelFolders": "Папки",
|
||||||
|
"LabelFontFamily": "Семейство шрифтов",
|
||||||
"LabelFontScale": "Масштаб шрифта",
|
"LabelFontScale": "Масштаб шрифта",
|
||||||
"LabelFormat": "Формат",
|
"LabelFormat": "Формат",
|
||||||
"LabelGenre": "Жанр",
|
"LabelGenre": "Жанр",
|
||||||
@ -266,6 +278,7 @@
|
|||||||
"LabelHost": "Хост",
|
"LabelHost": "Хост",
|
||||||
"LabelHour": "Часы",
|
"LabelHour": "Часы",
|
||||||
"LabelIcon": "Иконка",
|
"LabelIcon": "Иконка",
|
||||||
|
"LabelImageURLFromTheWeb": "Image URL from the web",
|
||||||
"LabelIncludeInTracklist": "Включать в список воспроизведения",
|
"LabelIncludeInTracklist": "Включать в список воспроизведения",
|
||||||
"LabelIncomplete": "Не завершен",
|
"LabelIncomplete": "Не завершен",
|
||||||
"LabelInProgress": "В процессе",
|
"LabelInProgress": "В процессе",
|
||||||
@ -305,6 +318,7 @@
|
|||||||
"LabelLookForNewEpisodesAfterDate": "Искать новые эпизоды после этой даты",
|
"LabelLookForNewEpisodesAfterDate": "Искать новые эпизоды после этой даты",
|
||||||
"LabelMediaPlayer": "Медиа проигрыватель",
|
"LabelMediaPlayer": "Медиа проигрыватель",
|
||||||
"LabelMediaType": "Тип медиа",
|
"LabelMediaType": "Тип медиа",
|
||||||
|
"LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority",
|
||||||
"LabelMetadataProvider": "Провайдер",
|
"LabelMetadataProvider": "Провайдер",
|
||||||
"LabelMetaTag": "Мета тег",
|
"LabelMetaTag": "Мета тег",
|
||||||
"LabelMetaTags": "Мета теги",
|
"LabelMetaTags": "Мета теги",
|
||||||
@ -384,6 +398,7 @@
|
|||||||
"LabelSeason": "Сезон",
|
"LabelSeason": "Сезон",
|
||||||
"LabelSelectAllEpisodes": "Выбрать все эпизоды",
|
"LabelSelectAllEpisodes": "Выбрать все эпизоды",
|
||||||
"LabelSelectEpisodesShowing": "Выберите {0} эпизодов для показа",
|
"LabelSelectEpisodesShowing": "Выберите {0} эпизодов для показа",
|
||||||
|
"LabelSelectUsers": "Select users",
|
||||||
"LabelSendEbookToDevice": "Отправить e-книгу в...",
|
"LabelSendEbookToDevice": "Отправить e-книгу в...",
|
||||||
"LabelSequence": "Последовательность",
|
"LabelSequence": "Последовательность",
|
||||||
"LabelSeries": "Серия",
|
"LabelSeries": "Серия",
|
||||||
@ -410,16 +425,10 @@
|
|||||||
"LabelSettingsHideSingleBookSeriesHelp": "Серии, в которых всего одна книга, будут скрыты со страницы серий и полок домашней страницы.",
|
"LabelSettingsHideSingleBookSeriesHelp": "Серии, в которых всего одна книга, будут скрыты со страницы серий и полок домашней страницы.",
|
||||||
"LabelSettingsHomePageBookshelfView": "Вид книжной полки на Домашней странице",
|
"LabelSettingsHomePageBookshelfView": "Вид книжной полки на Домашней странице",
|
||||||
"LabelSettingsLibraryBookshelfView": "Вид книжной полки в Библиотеке",
|
"LabelSettingsLibraryBookshelfView": "Вид книжной полки в Библиотеке",
|
||||||
"LabelSettingsOverdriveMediaMarkers": "Overdrive Media Markers для глав",
|
|
||||||
"LabelSettingsOverdriveMediaMarkersHelp": "MP3 файлы из Overdrive поставляется с таймингами глав, встроенными в виде пользовательских метаданных. При включении этого параметра эти теги будут автоматически использоваться для таймингов глав",
|
|
||||||
"LabelSettingsParseSubtitles": "Разбор подзаголовков",
|
"LabelSettingsParseSubtitles": "Разбор подзаголовков",
|
||||||
"LabelSettingsParseSubtitlesHelp": "Извлечение подзаголовков из имен папок аудиокниг.<br>Подзаголовок должны быть отделен \" - \"<br>например \"Название Книги - Тут Подзаголовок\" подзаголовок будет \"Тут Подзаголовок\"",
|
"LabelSettingsParseSubtitlesHelp": "Извлечение подзаголовков из имен папок аудиокниг.<br>Подзаголовок должны быть отделен \" - \"<br>например \"Название Книги - Тут Подзаголовок\" подзаголовок будет \"Тут Подзаголовок\"",
|
||||||
"LabelSettingsPreferAudioMetadata": "Предпочитать аудио метаданные",
|
|
||||||
"LabelSettingsPreferAudioMetadataHelp": "ID3 мета теги будут использоваться для данных книг вместо имен папок",
|
|
||||||
"LabelSettingsPreferMatchedMetadata": "Предпочитать метаданные поиска",
|
"LabelSettingsPreferMatchedMetadata": "Предпочитать метаданные поиска",
|
||||||
"LabelSettingsPreferMatchedMetadataHelp": "Данные поиска будут перезаписывать данные книг при использовании Быстрого Поиска. По умолчанию Быстрый Поиск будет использоваться только при отсутствии данных",
|
"LabelSettingsPreferMatchedMetadataHelp": "Данные поиска будут перезаписывать данные книг при использовании Быстрого Поиска. По умолчанию Быстрый Поиск будет использоваться только при отсутствии данных",
|
||||||
"LabelSettingsPreferOPFMetadata": "Предпочитать OPF метаданные",
|
|
||||||
"LabelSettingsPreferOPFMetadataHelp": "Метаданные из файла OPF будут использованы для данных книги вместо имен папок",
|
|
||||||
"LabelSettingsSkipMatchingBooksWithASIN": "Пропускать Поиск книг у которых уже заполнен ASIN",
|
"LabelSettingsSkipMatchingBooksWithASIN": "Пропускать Поиск книг у которых уже заполнен ASIN",
|
||||||
"LabelSettingsSkipMatchingBooksWithISBN": "Пропускать Поиск книг у которых уже заполнен ISBN",
|
"LabelSettingsSkipMatchingBooksWithISBN": "Пропускать Поиск книг у которых уже заполнен ISBN",
|
||||||
"LabelSettingsSortingIgnorePrefixes": "Игнорировать префиксы при сортировке",
|
"LabelSettingsSortingIgnorePrefixes": "Игнорировать префиксы при сортировке",
|
||||||
@ -429,7 +438,7 @@
|
|||||||
"LabelSettingsStoreCoversWithItem": "Хранить обложки с элементом",
|
"LabelSettingsStoreCoversWithItem": "Хранить обложки с элементом",
|
||||||
"LabelSettingsStoreCoversWithItemHelp": "По умолчанию обложки сохраняются в папке /metadata/items, при включении этой настройки обложка будет храниться в папке элемента. Будет сохраняться только один файл с именем \"cover\"",
|
"LabelSettingsStoreCoversWithItemHelp": "По умолчанию обложки сохраняются в папке /metadata/items, при включении этой настройки обложка будет храниться в папке элемента. Будет сохраняться только один файл с именем \"cover\"",
|
||||||
"LabelSettingsStoreMetadataWithItem": "Хранить метаинформацию с элементом",
|
"LabelSettingsStoreMetadataWithItem": "Хранить метаинформацию с элементом",
|
||||||
"LabelSettingsStoreMetadataWithItemHelp": "По умолчанию метаинформация сохраняется в папке /metadata/items, при включении этой настройки метаинформация будет храниться в папке элемента. Используется расширение файла .abs",
|
"LabelSettingsStoreMetadataWithItemHelp": "По умолчанию метаинформация сохраняется в папке /metadata/items, при включении этой настройки метаинформация будет храниться в папке элемента",
|
||||||
"LabelSettingsTimeFormat": "Формат времени",
|
"LabelSettingsTimeFormat": "Формат времени",
|
||||||
"LabelShowAll": "Показать все",
|
"LabelShowAll": "Показать все",
|
||||||
"LabelSize": "Размер",
|
"LabelSize": "Размер",
|
||||||
@ -527,12 +536,15 @@
|
|||||||
"MessageConfirmDeleteBackup": "Вы уверены, что хотите удалить бэкап для {0}?",
|
"MessageConfirmDeleteBackup": "Вы уверены, что хотите удалить бэкап для {0}?",
|
||||||
"MessageConfirmDeleteFile": "Это удалит файл из Вашей файловой системы. Вы уверены?",
|
"MessageConfirmDeleteFile": "Это удалит файл из Вашей файловой системы. Вы уверены?",
|
||||||
"MessageConfirmDeleteLibrary": "Вы уверены, что хотите навсегда удалить библиотеку \"{0}\"?",
|
"MessageConfirmDeleteLibrary": "Вы уверены, что хотите навсегда удалить библиотеку \"{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": "Вы уверены, что хотите удалить этот сеанс?",
|
"MessageConfirmDeleteSession": "Вы уверены, что хотите удалить этот сеанс?",
|
||||||
"MessageConfirmForceReScan": "Вы уверены, что хотите принудительно выполнить повторное сканирование?",
|
"MessageConfirmForceReScan": "Вы уверены, что хотите принудительно выполнить повторное сканирование?",
|
||||||
"MessageConfirmMarkAllEpisodesFinished": "Вы уверены, что хотите отметить все эпизоды как завершенные?",
|
"MessageConfirmMarkAllEpisodesFinished": "Вы уверены, что хотите отметить все эпизоды как завершенные?",
|
||||||
"MessageConfirmMarkAllEpisodesNotFinished": "Вы уверены, что хотите отметить все эпизоды как не завершенные?",
|
"MessageConfirmMarkAllEpisodesNotFinished": "Вы уверены, что хотите отметить все эпизоды как не завершенные?",
|
||||||
"MessageConfirmMarkSeriesFinished": "Вы уверены, что хотите отметить все книги этой серии как завершенные?",
|
"MessageConfirmMarkSeriesFinished": "Вы уверены, что хотите отметить все книги этой серии как завершенные?",
|
||||||
"MessageConfirmMarkSeriesNotFinished": "Вы уверены, что хотите отметить все книги этой серии как не завершенные?",
|
"MessageConfirmMarkSeriesNotFinished": "Вы уверены, что хотите отметить все книги этой серии как не завершенные?",
|
||||||
|
"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": "Вы уверены, что хотите удалить все главы?",
|
"MessageConfirmRemoveAllChapters": "Вы уверены, что хотите удалить все главы?",
|
||||||
"MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?",
|
"MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?",
|
||||||
"MessageConfirmRemoveCollection": "Вы уверены, что хотите удалить коллекцию \"{0}\"?",
|
"MessageConfirmRemoveCollection": "Вы уверены, что хотите удалить коллекцию \"{0}\"?",
|
||||||
@ -546,6 +558,7 @@
|
|||||||
"MessageConfirmRenameTag": "Вы уверены, что хотите переименовать тег \"{0}\" в \"{1}\" для всех элементов?",
|
"MessageConfirmRenameTag": "Вы уверены, что хотите переименовать тег \"{0}\" в \"{1}\" для всех элементов?",
|
||||||
"MessageConfirmRenameTagMergeNote": "Примечание: Этот тег уже существует, поэтому они будут объединены.",
|
"MessageConfirmRenameTagMergeNote": "Примечание: Этот тег уже существует, поэтому они будут объединены.",
|
||||||
"MessageConfirmRenameTagWarning": "Предупреждение! Похожий тег с другими начальными буквами уже существует \"{0}\".",
|
"MessageConfirmRenameTagWarning": "Предупреждение! Похожий тег с другими начальными буквами уже существует \"{0}\".",
|
||||||
|
"MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?",
|
||||||
"MessageConfirmSendEbookToDevice": "Вы уверены, что хотите отправить {0} e-книгу \"{1}\" на устройство \"{2}\"?",
|
"MessageConfirmSendEbookToDevice": "Вы уверены, что хотите отправить {0} e-книгу \"{1}\" на устройство \"{2}\"?",
|
||||||
"MessageDownloadingEpisode": "Эпизод скачивается",
|
"MessageDownloadingEpisode": "Эпизод скачивается",
|
||||||
"MessageDragFilesIntoTrackOrder": "Перетащите файлы для исправления порядка треков",
|
"MessageDragFilesIntoTrackOrder": "Перетащите файлы для исправления порядка треков",
|
||||||
|
@ -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": "保存并关闭",
|
||||||
@ -122,6 +126,7 @@
|
|||||||
"HeaderManageTags": "管理标签",
|
"HeaderManageTags": "管理标签",
|
||||||
"HeaderMapDetails": "编辑详情",
|
"HeaderMapDetails": "编辑详情",
|
||||||
"HeaderMatch": "匹配",
|
"HeaderMatch": "匹配",
|
||||||
|
"HeaderMetadataOrderOfPrecedence": "Metadata order of precedence",
|
||||||
"HeaderMetadataToEmbed": "嵌入元数据",
|
"HeaderMetadataToEmbed": "嵌入元数据",
|
||||||
"HeaderNewAccount": "新建帐户",
|
"HeaderNewAccount": "新建帐户",
|
||||||
"HeaderNewLibrary": "新建媒体库",
|
"HeaderNewLibrary": "新建媒体库",
|
||||||
@ -138,7 +143,7 @@
|
|||||||
"HeaderRemoveEpisodes": "移除 {0} 剧集",
|
"HeaderRemoveEpisodes": "移除 {0} 剧集",
|
||||||
"HeaderRSSFeedGeneral": "RSS 详细信息",
|
"HeaderRSSFeedGeneral": "RSS 详细信息",
|
||||||
"HeaderRSSFeedIsOpen": "RSS 源已打开",
|
"HeaderRSSFeedIsOpen": "RSS 源已打开",
|
||||||
"HeaderRSSFeeds": "RSS Feeds",
|
"HeaderRSSFeeds": "RSS 订阅",
|
||||||
"HeaderSavedMediaProgress": "保存媒体进度",
|
"HeaderSavedMediaProgress": "保存媒体进度",
|
||||||
"HeaderSchedule": "计划任务",
|
"HeaderSchedule": "计划任务",
|
||||||
"HeaderScheduleLibraryScans": "自动扫描媒体库",
|
"HeaderScheduleLibraryScans": "自动扫描媒体库",
|
||||||
@ -176,8 +181,11 @@
|
|||||||
"LabelAddToCollectionBatch": "批量添加 {0} 个媒体到收藏",
|
"LabelAddToCollectionBatch": "批量添加 {0} 个媒体到收藏",
|
||||||
"LabelAddToPlaylist": "添加到播放列表",
|
"LabelAddToPlaylist": "添加到播放列表",
|
||||||
"LabelAddToPlaylistBatch": "添加 {0} 个项目到播放列表",
|
"LabelAddToPlaylistBatch": "添加 {0} 个项目到播放列表",
|
||||||
|
"LabelAdminUsersOnly": "Admin users only",
|
||||||
"LabelAll": "全部",
|
"LabelAll": "全部",
|
||||||
"LabelAllUsers": "所有用户",
|
"LabelAllUsers": "所有用户",
|
||||||
|
"LabelAllUsersExcludingGuests": "All users excluding guests",
|
||||||
|
"LabelAllUsersIncludingGuests": "All users including guests",
|
||||||
"LabelAlreadyInYourLibrary": "已存在你的库中",
|
"LabelAlreadyInYourLibrary": "已存在你的库中",
|
||||||
"LabelAppend": "附加",
|
"LabelAppend": "附加",
|
||||||
"LabelAuthor": "作者",
|
"LabelAuthor": "作者",
|
||||||
@ -186,7 +194,7 @@
|
|||||||
"LabelAuthors": "作者",
|
"LabelAuthors": "作者",
|
||||||
"LabelAutoDownloadEpisodes": "自动下载剧集",
|
"LabelAutoDownloadEpisodes": "自动下载剧集",
|
||||||
"LabelBackToUser": "返回到用户",
|
"LabelBackToUser": "返回到用户",
|
||||||
"LabelBackupLocation": "Backup Location",
|
"LabelBackupLocation": "备份位置",
|
||||||
"LabelBackupsEnableAutomaticBackups": "启用自动备份",
|
"LabelBackupsEnableAutomaticBackups": "启用自动备份",
|
||||||
"LabelBackupsEnableAutomaticBackupsHelp": "备份保存到 /metadata/backups",
|
"LabelBackupsEnableAutomaticBackupsHelp": "备份保存到 /metadata/backups",
|
||||||
"LabelBackupsMaxBackupSize": "最大备份大小 (GB)",
|
"LabelBackupsMaxBackupSize": "最大备份大小 (GB)",
|
||||||
@ -200,10 +208,11 @@
|
|||||||
"LabelChapters": "章节",
|
"LabelChapters": "章节",
|
||||||
"LabelChaptersFound": "找到的章节",
|
"LabelChaptersFound": "找到的章节",
|
||||||
"LabelChapterTitle": "章节标题",
|
"LabelChapterTitle": "章节标题",
|
||||||
|
"LabelClickForMoreInfo": "Click for more info",
|
||||||
"LabelClosePlayer": "关闭播放器",
|
"LabelClosePlayer": "关闭播放器",
|
||||||
"LabelCodec": "编解码",
|
"LabelCodec": "编解码",
|
||||||
"LabelCollapseSeries": "折叠系列",
|
"LabelCollapseSeries": "折叠系列",
|
||||||
"LabelCollection": "Collection",
|
"LabelCollection": "收藏",
|
||||||
"LabelCollections": "收藏",
|
"LabelCollections": "收藏",
|
||||||
"LabelComplete": "已完成",
|
"LabelComplete": "已完成",
|
||||||
"LabelConfirmPassword": "确认密码",
|
"LabelConfirmPassword": "确认密码",
|
||||||
@ -218,16 +227,18 @@
|
|||||||
"LabelCurrently": "当前:",
|
"LabelCurrently": "当前:",
|
||||||
"LabelCustomCronExpression": "自定义计划任务表达式:",
|
"LabelCustomCronExpression": "自定义计划任务表达式:",
|
||||||
"LabelDatetime": "日期时间",
|
"LabelDatetime": "日期时间",
|
||||||
|
"LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)",
|
||||||
"LabelDescription": "描述",
|
"LabelDescription": "描述",
|
||||||
"LabelDeselectAll": "全部取消选择",
|
"LabelDeselectAll": "全部取消选择",
|
||||||
"LabelDevice": "设备",
|
"LabelDevice": "设备",
|
||||||
"LabelDeviceInfo": "设备信息",
|
"LabelDeviceInfo": "设备信息",
|
||||||
|
"LabelDeviceIsAvailableTo": "Device is available to...",
|
||||||
"LabelDirectory": "目录",
|
"LabelDirectory": "目录",
|
||||||
"LabelDiscFromFilename": "从文件名获取光盘",
|
"LabelDiscFromFilename": "从文件名获取光盘",
|
||||||
"LabelDiscFromMetadata": "从元数据获取光盘",
|
"LabelDiscFromMetadata": "从元数据获取光盘",
|
||||||
"LabelDiscover": "Discover",
|
"LabelDiscover": "发现",
|
||||||
"LabelDownload": "下载",
|
"LabelDownload": "下载",
|
||||||
"LabelDownloadNEpisodes": "Download {0} episodes",
|
"LabelDownloadNEpisodes": "下载 {0} 集",
|
||||||
"LabelDuration": "持续时间",
|
"LabelDuration": "持续时间",
|
||||||
"LabelDurationFound": "找到持续时间:",
|
"LabelDurationFound": "找到持续时间:",
|
||||||
"LabelEbook": "电子书",
|
"LabelEbook": "电子书",
|
||||||
@ -256,6 +267,7 @@
|
|||||||
"LabelFinished": "已听完",
|
"LabelFinished": "已听完",
|
||||||
"LabelFolder": "文件夹",
|
"LabelFolder": "文件夹",
|
||||||
"LabelFolders": "文件夹",
|
"LabelFolders": "文件夹",
|
||||||
|
"LabelFontFamily": "字体系列",
|
||||||
"LabelFontScale": "字体比例",
|
"LabelFontScale": "字体比例",
|
||||||
"LabelFormat": "编码格式",
|
"LabelFormat": "编码格式",
|
||||||
"LabelGenre": "流派",
|
"LabelGenre": "流派",
|
||||||
@ -266,6 +278,7 @@
|
|||||||
"LabelHost": "主机",
|
"LabelHost": "主机",
|
||||||
"LabelHour": "小时",
|
"LabelHour": "小时",
|
||||||
"LabelIcon": "图标",
|
"LabelIcon": "图标",
|
||||||
|
"LabelImageURLFromTheWeb": "来自 Web 图像的 URL",
|
||||||
"LabelIncludeInTracklist": "包含在音轨列表中",
|
"LabelIncludeInTracklist": "包含在音轨列表中",
|
||||||
"LabelIncomplete": "未听完",
|
"LabelIncomplete": "未听完",
|
||||||
"LabelInProgress": "正在听",
|
"LabelInProgress": "正在听",
|
||||||
@ -305,6 +318,7 @@
|
|||||||
"LabelLookForNewEpisodesAfterDate": "在此日期后查找新剧集",
|
"LabelLookForNewEpisodesAfterDate": "在此日期后查找新剧集",
|
||||||
"LabelMediaPlayer": "媒体播放器",
|
"LabelMediaPlayer": "媒体播放器",
|
||||||
"LabelMediaType": "媒体类型",
|
"LabelMediaType": "媒体类型",
|
||||||
|
"LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority",
|
||||||
"LabelMetadataProvider": "元数据提供者",
|
"LabelMetadataProvider": "元数据提供者",
|
||||||
"LabelMetaTag": "元数据标签",
|
"LabelMetaTag": "元数据标签",
|
||||||
"LabelMetaTags": "元标签",
|
"LabelMetaTags": "元标签",
|
||||||
@ -322,7 +336,7 @@
|
|||||||
"LabelNewPassword": "新密码",
|
"LabelNewPassword": "新密码",
|
||||||
"LabelNextBackupDate": "下次备份日期",
|
"LabelNextBackupDate": "下次备份日期",
|
||||||
"LabelNextScheduledRun": "下次任务运行",
|
"LabelNextScheduledRun": "下次任务运行",
|
||||||
"LabelNoEpisodesSelected": "No episodes selected",
|
"LabelNoEpisodesSelected": "未选择任何剧集",
|
||||||
"LabelNotes": "注释",
|
"LabelNotes": "注释",
|
||||||
"LabelNotFinished": "未听完",
|
"LabelNotFinished": "未听完",
|
||||||
"LabelNotificationAppriseURL": "通知 URL(s)",
|
"LabelNotificationAppriseURL": "通知 URL(s)",
|
||||||
@ -382,8 +396,9 @@
|
|||||||
"LabelSearchTitle": "搜索标题",
|
"LabelSearchTitle": "搜索标题",
|
||||||
"LabelSearchTitleOrASIN": "搜索标题或 ASIN",
|
"LabelSearchTitleOrASIN": "搜索标题或 ASIN",
|
||||||
"LabelSeason": "季",
|
"LabelSeason": "季",
|
||||||
"LabelSelectAllEpisodes": "Select all episodes",
|
"LabelSelectAllEpisodes": "选择所有剧集",
|
||||||
"LabelSelectEpisodesShowing": "Select {0} episodes showing",
|
"LabelSelectEpisodesShowing": "选择正在播放的 {0} 剧集",
|
||||||
|
"LabelSelectUsers": "Select users",
|
||||||
"LabelSendEbookToDevice": "发送电子书到...",
|
"LabelSendEbookToDevice": "发送电子书到...",
|
||||||
"LabelSequence": "序列",
|
"LabelSequence": "序列",
|
||||||
"LabelSeries": "系列",
|
"LabelSeries": "系列",
|
||||||
@ -399,27 +414,21 @@
|
|||||||
"LabelSettingsDisableWatcher": "禁用监视程序",
|
"LabelSettingsDisableWatcher": "禁用监视程序",
|
||||||
"LabelSettingsDisableWatcherForLibrary": "禁用媒体库的文件夹监视程序",
|
"LabelSettingsDisableWatcherForLibrary": "禁用媒体库的文件夹监视程序",
|
||||||
"LabelSettingsDisableWatcherHelp": "检测到文件更改时禁用自动添加和更新项目. *需要重启服务器",
|
"LabelSettingsDisableWatcherHelp": "检测到文件更改时禁用自动添加和更新项目. *需要重启服务器",
|
||||||
"LabelSettingsEnableWatcher": "Enable Watcher",
|
"LabelSettingsEnableWatcher": "启用监视程序",
|
||||||
"LabelSettingsEnableWatcherForLibrary": "Enable folder watcher for library",
|
"LabelSettingsEnableWatcherForLibrary": "为库启用文件夹监视程序",
|
||||||
"LabelSettingsEnableWatcherHelp": "Enables the automatic adding/updating of items when file changes are detected. *Requires server restart",
|
"LabelSettingsEnableWatcherHelp": "当检测到文件更改时, 启用项目的自动添加/更新. *需要重新启动服务器",
|
||||||
"LabelSettingsExperimentalFeatures": "实验功能",
|
"LabelSettingsExperimentalFeatures": "实验功能",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "开发中的功能需要你的反馈并帮助测试. 点击打开 github 讨论.",
|
"LabelSettingsExperimentalFeaturesHelp": "开发中的功能需要你的反馈并帮助测试. 点击打开 github 讨论.",
|
||||||
"LabelSettingsFindCovers": "查找封面",
|
"LabelSettingsFindCovers": "查找封面",
|
||||||
"LabelSettingsFindCoversHelp": "如果你的有声读物在文件夹中没有嵌入封面或封面图像, 扫描将尝试查找封面.<br>注意: 这将延长扫描时间",
|
"LabelSettingsFindCoversHelp": "如果你的有声读物在文件夹中没有嵌入封面或封面图像, 扫描将尝试查找封面.<br>注意: 这将延长扫描时间",
|
||||||
"LabelSettingsHideSingleBookSeries": "Hide single book series",
|
"LabelSettingsHideSingleBookSeries": "隐藏单书系列",
|
||||||
"LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.",
|
"LabelSettingsHideSingleBookSeriesHelp": "只有一本书的系列将从系列页面和主页书架中隐藏.",
|
||||||
"LabelSettingsHomePageBookshelfView": "首页使用书架视图",
|
"LabelSettingsHomePageBookshelfView": "首页使用书架视图",
|
||||||
"LabelSettingsLibraryBookshelfView": "媒体库使用书架视图",
|
"LabelSettingsLibraryBookshelfView": "媒体库使用书架视图",
|
||||||
"LabelSettingsOverdriveMediaMarkers": "对章节使用 Overdrive 媒体标记",
|
|
||||||
"LabelSettingsOverdriveMediaMarkersHelp": "Overdrive 的 MP3 文件带有作为自定义元数据嵌入的章节时间. 启用此功能将自动将这些标签用于章节计时",
|
|
||||||
"LabelSettingsParseSubtitles": "解析副标题",
|
"LabelSettingsParseSubtitles": "解析副标题",
|
||||||
"LabelSettingsParseSubtitlesHelp": "从有声读物文件夹中提取副标题.<br>副标题必须用 \" - \" 分隔.<br>例: \"书名 - 这里是副标题\" 则显示副标题 \"这里是副标题\"",
|
"LabelSettingsParseSubtitlesHelp": "从有声读物文件夹中提取副标题.<br>副标题必须用 \" - \" 分隔.<br>例: \"书名 - 这里是副标题\" 则显示副标题 \"这里是副标题\"",
|
||||||
"LabelSettingsPreferAudioMetadata": "首选音频元数据",
|
|
||||||
"LabelSettingsPreferAudioMetadataHelp": "音频文件 ID3 元标记将用于文件夹名称上媒体的详细信息",
|
|
||||||
"LabelSettingsPreferMatchedMetadata": "首选匹配的元数据",
|
"LabelSettingsPreferMatchedMetadata": "首选匹配的元数据",
|
||||||
"LabelSettingsPreferMatchedMetadataHelp": "使用快速匹配时, 匹配的数据将覆盖项目详细信息. 默认情况下, 快速匹配将只填充缺少的详细信息.",
|
"LabelSettingsPreferMatchedMetadataHelp": "使用快速匹配时, 匹配的数据将覆盖项目详细信息. 默认情况下, 快速匹配将只填充缺少的详细信息.",
|
||||||
"LabelSettingsPreferOPFMetadata": "首选 OPF 元数据",
|
|
||||||
"LabelSettingsPreferOPFMetadataHelp": "OPF 文件元数据将用于文件夹名称上媒体的详细信息",
|
|
||||||
"LabelSettingsSkipMatchingBooksWithASIN": "跳过匹配已有 ASIN 的图书",
|
"LabelSettingsSkipMatchingBooksWithASIN": "跳过匹配已有 ASIN 的图书",
|
||||||
"LabelSettingsSkipMatchingBooksWithISBN": "跳过匹配已有 ISBN 的图书",
|
"LabelSettingsSkipMatchingBooksWithISBN": "跳过匹配已有 ISBN 的图书",
|
||||||
"LabelSettingsSortingIgnorePrefixes": "排序时忽略前缀",
|
"LabelSettingsSortingIgnorePrefixes": "排序时忽略前缀",
|
||||||
@ -429,7 +438,7 @@
|
|||||||
"LabelSettingsStoreCoversWithItem": "存储项目封面",
|
"LabelSettingsStoreCoversWithItem": "存储项目封面",
|
||||||
"LabelSettingsStoreCoversWithItemHelp": "默认情况下封面存储在/metadata/items文件夹中, 启用此设置将存储封面在你媒体项目文件夹中. 只保留一个名为 \"cover\" 的文件",
|
"LabelSettingsStoreCoversWithItemHelp": "默认情况下封面存储在/metadata/items文件夹中, 启用此设置将存储封面在你媒体项目文件夹中. 只保留一个名为 \"cover\" 的文件",
|
||||||
"LabelSettingsStoreMetadataWithItem": "存储项目元数据",
|
"LabelSettingsStoreMetadataWithItem": "存储项目元数据",
|
||||||
"LabelSettingsStoreMetadataWithItemHelp": "默认情况下元数据文件存储在/metadata/items文件夹中, 启用此设置将存储元数据在你媒体项目文件夹中. 使 .abs 文件护展名",
|
"LabelSettingsStoreMetadataWithItemHelp": "默认情况下元数据文件存储在/metadata/items文件夹中, 启用此设置将存储元数据在你媒体项目文件夹中",
|
||||||
"LabelSettingsTimeFormat": "时间格式",
|
"LabelSettingsTimeFormat": "时间格式",
|
||||||
"LabelShowAll": "全部显示",
|
"LabelShowAll": "全部显示",
|
||||||
"LabelSize": "文件大小",
|
"LabelSize": "文件大小",
|
||||||
@ -482,7 +491,7 @@
|
|||||||
"LabelTrackFromMetadata": "从源数据获取音轨",
|
"LabelTrackFromMetadata": "从源数据获取音轨",
|
||||||
"LabelTracks": "音轨",
|
"LabelTracks": "音轨",
|
||||||
"LabelTracksMultiTrack": "多轨",
|
"LabelTracksMultiTrack": "多轨",
|
||||||
"LabelTracksNone": "No tracks",
|
"LabelTracksNone": "没有音轨",
|
||||||
"LabelTracksSingleTrack": "单轨",
|
"LabelTracksSingleTrack": "单轨",
|
||||||
"LabelType": "类型",
|
"LabelType": "类型",
|
||||||
"LabelUnabridged": "未删节",
|
"LabelUnabridged": "未删节",
|
||||||
@ -523,20 +532,23 @@
|
|||||||
"MessageChapterErrorStartLtPrev": "无效的开始时间, 必须大于或等于上一章节的开始时间",
|
"MessageChapterErrorStartLtPrev": "无效的开始时间, 必须大于或等于上一章节的开始时间",
|
||||||
"MessageChapterStartIsAfter": "章节开始是在有声读物结束之后",
|
"MessageChapterStartIsAfter": "章节开始是在有声读物结束之后",
|
||||||
"MessageCheckingCron": "检查计划任务...",
|
"MessageCheckingCron": "检查计划任务...",
|
||||||
"MessageConfirmCloseFeed": "Are you sure you want to close this feed?",
|
"MessageConfirmCloseFeed": "你确定要关闭此订阅源吗?",
|
||||||
"MessageConfirmDeleteBackup": "你确定要删除备份 {0}?",
|
"MessageConfirmDeleteBackup": "你确定要删除备份 {0}?",
|
||||||
"MessageConfirmDeleteFile": "这将从文件系统中删除该文件. 你确定吗?",
|
"MessageConfirmDeleteFile": "这将从文件系统中删除该文件. 你确定吗?",
|
||||||
"MessageConfirmDeleteLibrary": "你确定要永久删除媒体库 \"{0}\"?",
|
"MessageConfirmDeleteLibrary": "你确定要永久删除媒体库 \"{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": "你确定要删除此会话吗?",
|
"MessageConfirmDeleteSession": "你确定要删除此会话吗?",
|
||||||
"MessageConfirmForceReScan": "你确定要强制重新扫描吗?",
|
"MessageConfirmForceReScan": "你确定要强制重新扫描吗?",
|
||||||
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
|
"MessageConfirmMarkAllEpisodesFinished": "你确定要将所有剧集都标记为已完成吗?",
|
||||||
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
|
"MessageConfirmMarkAllEpisodesNotFinished": "你确定要将所有剧集都标记为未完成吗?",
|
||||||
"MessageConfirmMarkSeriesFinished": "你确定要将此系列中的所有书籍都标记为已听完吗?",
|
"MessageConfirmMarkSeriesFinished": "你确定要将此系列中的所有书籍都标记为已听完吗?",
|
||||||
"MessageConfirmMarkSeriesNotFinished": "你确定要将此系列中的所有书籍都标记为未听完吗?",
|
"MessageConfirmMarkSeriesNotFinished": "你确定要将此系列中的所有书籍都标记为未听完吗?",
|
||||||
|
"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": "你确定要移除所有章节吗?",
|
"MessageConfirmRemoveAllChapters": "你确定要移除所有章节吗?",
|
||||||
"MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?",
|
"MessageConfirmRemoveAuthor": "你确定要删除作者 \"{0}\"?",
|
||||||
"MessageConfirmRemoveCollection": "您确定要移除收藏 \"{0}\"?",
|
"MessageConfirmRemoveCollection": "你确定要移除收藏 \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisode": "您确定要移除剧集 \"{0}\"?",
|
"MessageConfirmRemoveEpisode": "你确定要移除剧集 \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisodes": "你确定要移除 {0} 剧集?",
|
"MessageConfirmRemoveEpisodes": "你确定要移除 {0} 剧集?",
|
||||||
"MessageConfirmRemoveNarrator": "你确定要删除演播者 \"{0}\"?",
|
"MessageConfirmRemoveNarrator": "你确定要删除演播者 \"{0}\"?",
|
||||||
"MessageConfirmRemovePlaylist": "你确定要移除播放列表 \"{0}\"?",
|
"MessageConfirmRemovePlaylist": "你确定要移除播放列表 \"{0}\"?",
|
||||||
@ -546,6 +558,7 @@
|
|||||||
"MessageConfirmRenameTag": "你确定要将所有项目标签 \"{0}\" 重命名到 \"{1}\"?",
|
"MessageConfirmRenameTag": "你确定要将所有项目标签 \"{0}\" 重命名到 \"{1}\"?",
|
||||||
"MessageConfirmRenameTagMergeNote": "注意: 该标签已经存在, 因此它们将被合并.",
|
"MessageConfirmRenameTagMergeNote": "注意: 该标签已经存在, 因此它们将被合并.",
|
||||||
"MessageConfirmRenameTagWarning": "警告! 已经存在有大小写不同的类似标签 \"{0}\".",
|
"MessageConfirmRenameTagWarning": "警告! 已经存在有大小写不同的类似标签 \"{0}\".",
|
||||||
|
"MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?",
|
||||||
"MessageConfirmSendEbookToDevice": "你确定要发送 {0} 电子书 \"{1}\" 到设备 \"{2}\"?",
|
"MessageConfirmSendEbookToDevice": "你确定要发送 {0} 电子书 \"{1}\" 到设备 \"{2}\"?",
|
||||||
"MessageDownloadingEpisode": "正在下载剧集",
|
"MessageDownloadingEpisode": "正在下载剧集",
|
||||||
"MessageDragFilesIntoTrackOrder": "将文件拖动到正确的音轨顺序",
|
"MessageDragFilesIntoTrackOrder": "将文件拖动到正确的音轨顺序",
|
||||||
@ -565,8 +578,8 @@
|
|||||||
"MessageM4BFailed": "M4B 失败!",
|
"MessageM4BFailed": "M4B 失败!",
|
||||||
"MessageM4BFinished": "M4B 完成!",
|
"MessageM4BFinished": "M4B 完成!",
|
||||||
"MessageMapChapterTitles": "将章节标题映射到现有的有声读物章节, 无需调整时间戳",
|
"MessageMapChapterTitles": "将章节标题映射到现有的有声读物章节, 无需调整时间戳",
|
||||||
"MessageMarkAllEpisodesFinished": "Mark all episodes finished",
|
"MessageMarkAllEpisodesFinished": "标记所有剧集为已完成",
|
||||||
"MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
|
"MessageMarkAllEpisodesNotFinished": "标记所有剧集为未完成",
|
||||||
"MessageMarkAsFinished": "标记为已听完",
|
"MessageMarkAsFinished": "标记为已听完",
|
||||||
"MessageMarkAsNotFinished": "标记为未听完",
|
"MessageMarkAsNotFinished": "标记为未听完",
|
||||||
"MessageMatchBooksDescription": "尝试将媒体库中的图书与所选搜索提供商的图书进行匹配, 并填写空白的详细信息和封面. 不覆盖详细信息.",
|
"MessageMatchBooksDescription": "尝试将媒体库中的图书与所选搜索提供商的图书进行匹配, 并填写空白的详细信息和封面. 不覆盖详细信息.",
|
||||||
|
36
package-lock.json
generated
36
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.4.4",
|
"version": "2.5.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.4.4",
|
"version": "2.5.0",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
@ -24,6 +24,7 @@
|
|||||||
"sequelize": "^6.32.1",
|
"sequelize": "^6.32.1",
|
||||||
"socket.io": "^4.5.4",
|
"socket.io": "^4.5.4",
|
||||||
"sqlite3": "^5.1.6",
|
"sqlite3": "^5.1.6",
|
||||||
|
"ssrf-req-filter": "^1.1.0",
|
||||||
"xml2js": "^0.5.0"
|
"xml2js": "^0.5.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@ -2580,6 +2581,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ssrf-req-filter": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ssrf-req-filter/-/ssrf-req-filter-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-YUyTinAEm52NsoDvkTFN9BQIa5nURNr2aN0BwOiJxHK3tlyGUczHa+2LjcibKNugAk/losB6kXOfcRzy0LQ4uA==",
|
||||||
|
"dependencies": {
|
||||||
|
"ipaddr.js": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ssrf-req-filter/node_modules/ipaddr.js": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ssri": {
|
"node_modules/ssri": {
|
||||||
"version": "8.0.1",
|
"version": "8.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz",
|
||||||
@ -4794,6 +4811,21 @@
|
|||||||
"tar": "^6.1.11"
|
"tar": "^6.1.11"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ssrf-req-filter": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ssrf-req-filter/-/ssrf-req-filter-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-YUyTinAEm52NsoDvkTFN9BQIa5nURNr2aN0BwOiJxHK3tlyGUczHa+2LjcibKNugAk/losB6kXOfcRzy0LQ4uA==",
|
||||||
|
"requires": {
|
||||||
|
"ipaddr.js": "^2.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ipaddr.js": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"ssri": {
|
"ssri": {
|
||||||
"version": "8.0.1",
|
"version": "8.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.4.4",
|
"version": "2.5.0",
|
||||||
|
"buildNumber": 1,
|
||||||
"description": "Self-hosted audiobook and podcast server",
|
"description": "Self-hosted audiobook and podcast server",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -45,6 +46,7 @@
|
|||||||
"sequelize": "^6.32.1",
|
"sequelize": "^6.32.1",
|
||||||
"socket.io": "^4.5.4",
|
"socket.io": "^4.5.4",
|
||||||
"sqlite3": "^5.1.6",
|
"sqlite3": "^5.1.6",
|
||||||
|
"ssrf-req-filter": "^1.1.0",
|
||||||
"xml2js": "^0.5.0"
|
"xml2js": "^0.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -276,21 +276,37 @@ class Database {
|
|||||||
global.ServerSettings = this.serverSettings.toJSON()
|
global.ServerSettings = this.serverSettings.toJSON()
|
||||||
|
|
||||||
// Version specific migrations
|
// Version specific migrations
|
||||||
|
if (packageJson.version !== this.serverSettings.version) {
|
||||||
if (this.serverSettings.version === '2.3.0' && this.compareVersions(packageJson.version, '2.3.0') == 1) {
|
if (this.serverSettings.version === '2.3.0' && this.compareVersions(packageJson.version, '2.3.0') == 1) {
|
||||||
await dbMigration.migrationPatch(this)
|
await dbMigration.migrationPatch(this)
|
||||||
}
|
}
|
||||||
if (['2.3.0', '2.3.1', '2.3.2', '2.3.3'].includes(this.serverSettings.version) && this.compareVersions(packageJson.version, '2.3.3') >= 0) {
|
if (['2.3.0', '2.3.1', '2.3.2', '2.3.3'].includes(this.serverSettings.version) && this.compareVersions(packageJson.version, '2.3.3') >= 0) {
|
||||||
await dbMigration.migrationPatch2(this)
|
await dbMigration.migrationPatch2(this)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// Build migrations
|
||||||
|
if (this.serverSettings.buildNumber <= 0) {
|
||||||
|
await require('./utils/migrations/absMetadataMigration').migrate(this)
|
||||||
|
}
|
||||||
|
|
||||||
await this.cleanDatabase()
|
await this.cleanDatabase()
|
||||||
|
|
||||||
// Set if root user has been created
|
// Set if root user has been created
|
||||||
this.hasRootUser = await this.models.user.getHasRootUser()
|
this.hasRootUser = await this.models.user.getHasRootUser()
|
||||||
|
|
||||||
|
// Update server settings with version/build
|
||||||
|
let updateServerSettings = false
|
||||||
if (packageJson.version !== this.serverSettings.version) {
|
if (packageJson.version !== this.serverSettings.version) {
|
||||||
Logger.info(`[Database] Server upgrade detected from ${this.serverSettings.version} to ${packageJson.version}`)
|
Logger.info(`[Database] Server upgrade detected from ${this.serverSettings.version} to ${packageJson.version}`)
|
||||||
this.serverSettings.version = packageJson.version
|
this.serverSettings.version = packageJson.version
|
||||||
|
this.serverSettings.buildNumber = packageJson.buildNumber
|
||||||
|
updateServerSettings = true
|
||||||
|
} else if (packageJson.buildNumber !== this.serverSettings.buildNumber) {
|
||||||
|
Logger.info(`[Database] Server v${packageJson.version} build upgraded from ${this.serverSettings.buildNumber} to ${packageJson.buildNumber}`)
|
||||||
|
this.serverSettings.buildNumber = packageJson.buildNumber
|
||||||
|
updateServerSettings = true
|
||||||
|
}
|
||||||
|
if (updateServerSettings) {
|
||||||
await this.updateServerSettings()
|
await this.updateServerSettings()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,6 @@ const PodcastManager = require('./managers/PodcastManager')
|
|||||||
const AudioMetadataMangaer = require('./managers/AudioMetadataManager')
|
const AudioMetadataMangaer = require('./managers/AudioMetadataManager')
|
||||||
const RssFeedManager = require('./managers/RssFeedManager')
|
const RssFeedManager = require('./managers/RssFeedManager')
|
||||||
const CronManager = require('./managers/CronManager')
|
const CronManager = require('./managers/CronManager')
|
||||||
const TaskManager = require('./managers/TaskManager')
|
|
||||||
const LibraryScanner = require('./scanner/LibraryScanner')
|
const LibraryScanner = require('./scanner/LibraryScanner')
|
||||||
|
|
||||||
//Import the main Passport and Express-Session library
|
//Import the main Passport and Express-Session library
|
||||||
@ -64,15 +63,14 @@ class Server {
|
|||||||
this.auth = new Auth()
|
this.auth = new Auth()
|
||||||
|
|
||||||
// Managers
|
// Managers
|
||||||
this.taskManager = new TaskManager()
|
|
||||||
this.notificationManager = new NotificationManager()
|
this.notificationManager = new NotificationManager()
|
||||||
this.emailManager = new EmailManager()
|
this.emailManager = new EmailManager()
|
||||||
this.backupManager = new BackupManager()
|
this.backupManager = new BackupManager()
|
||||||
this.logManager = new LogManager()
|
this.logManager = new LogManager()
|
||||||
this.abMergeManager = new AbMergeManager(this.taskManager)
|
this.abMergeManager = new AbMergeManager()
|
||||||
this.playbackSessionManager = new PlaybackSessionManager()
|
this.playbackSessionManager = new PlaybackSessionManager()
|
||||||
this.podcastManager = new PodcastManager(this.watcher, this.notificationManager, this.taskManager)
|
this.podcastManager = new PodcastManager(this.watcher, this.notificationManager)
|
||||||
this.audioMetadataManager = new AudioMetadataMangaer(this.taskManager)
|
this.audioMetadataManager = new AudioMetadataMangaer()
|
||||||
this.rssFeedManager = new RssFeedManager()
|
this.rssFeedManager = new RssFeedManager()
|
||||||
this.cronManager = new CronManager(this.podcastManager)
|
this.cronManager = new CronManager(this.podcastManager)
|
||||||
|
|
||||||
@ -95,10 +93,6 @@ class Server {
|
|||||||
LibraryScanner.setCancelLibraryScan(libraryId)
|
LibraryScanner.setCancelLibraryScan(libraryId)
|
||||||
}
|
}
|
||||||
|
|
||||||
getLibrariesScanning() {
|
|
||||||
return LibraryScanner.librariesScanning
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize database, backups, logs, rss feeds, cron jobs & watcher
|
* Initialize database, backups, logs, rss feeds, cron jobs & watcher
|
||||||
* Cleanup stale/invalid data
|
* Cleanup stale/invalid data
|
||||||
@ -179,7 +173,6 @@ class Server {
|
|||||||
// Static folder
|
// Static folder
|
||||||
router.use(express.static(Path.join(global.appRoot, 'static')))
|
router.use(express.static(Path.join(global.appRoot, 'static')))
|
||||||
|
|
||||||
// router.use('/api/v1', routes) // TODO: New routes
|
|
||||||
router.use('/api', Auth.cors, this.authMiddleware.bind(this), this.apiRouter.router)
|
router.use('/api', Auth.cors, this.authMiddleware.bind(this), this.apiRouter.router)
|
||||||
router.use('/hls', this.authMiddleware.bind(this), this.hlsRouter.router)
|
router.use('/hls', this.authMiddleware.bind(this), this.hlsRouter.router)
|
||||||
|
|
||||||
@ -188,7 +181,7 @@ class Server {
|
|||||||
Logger.info(`[Server] Requesting rss feed ${req.params.slug}`)
|
Logger.info(`[Server] Requesting rss feed ${req.params.slug}`)
|
||||||
this.rssFeedManager.getFeed(req, res)
|
this.rssFeedManager.getFeed(req, res)
|
||||||
})
|
})
|
||||||
router.get('/feed/:slug/cover', (req, res) => {
|
router.get('/feed/:slug/cover*', (req, res) => {
|
||||||
this.rssFeedManager.getFeedCover(req, res)
|
this.rssFeedManager.getFeedCover(req, res)
|
||||||
})
|
})
|
||||||
router.get('/feed/:slug/item/:episodeId/*', (req, res) => {
|
router.get('/feed/:slug/item/:episodeId/*', (req, res) => {
|
||||||
|
@ -198,8 +198,7 @@ class SocketAuthority {
|
|||||||
|
|
||||||
const initialPayload = {
|
const initialPayload = {
|
||||||
userId: client.user.id,
|
userId: client.user.id,
|
||||||
username: client.user.username,
|
username: client.user.username
|
||||||
librariesScanning: this.Server.getLibrariesScanning()
|
|
||||||
}
|
}
|
||||||
if (user.isAdminOrUp) {
|
if (user.isAdminOrUp) {
|
||||||
initialPayload.usersOnline = this.getUsersOnline()
|
initialPayload.usersOnline = this.getUsersOnline()
|
||||||
|
@ -3,8 +3,10 @@ const EventEmitter = require('events')
|
|||||||
const Watcher = require('./libs/watcher/watcher')
|
const Watcher = require('./libs/watcher/watcher')
|
||||||
const Logger = require('./Logger')
|
const Logger = require('./Logger')
|
||||||
const LibraryScanner = require('./scanner/LibraryScanner')
|
const LibraryScanner = require('./scanner/LibraryScanner')
|
||||||
|
const Task = require('./objects/Task')
|
||||||
|
const TaskManager = require('./managers/TaskManager')
|
||||||
|
|
||||||
const { filePathToPOSIX } = require('./utils/fileUtils')
|
const { filePathToPOSIX, isSameOrSubPath, getFileMTimeMs } = require('./utils/fileUtils')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef PendingFileUpdate
|
* @typedef PendingFileUpdate
|
||||||
@ -22,7 +24,12 @@ class FolderWatcher extends EventEmitter {
|
|||||||
/** @type {PendingFileUpdate[]} */
|
/** @type {PendingFileUpdate[]} */
|
||||||
this.pendingFileUpdates = []
|
this.pendingFileUpdates = []
|
||||||
this.pendingDelay = 4000
|
this.pendingDelay = 4000
|
||||||
|
/** @type {NodeJS.Timeout} */
|
||||||
this.pendingTimeout = null
|
this.pendingTimeout = null
|
||||||
|
/** @type {Task} */
|
||||||
|
this.pendingTask = null
|
||||||
|
|
||||||
|
this.filesBeingAdded = new Set()
|
||||||
|
|
||||||
/** @type {string[]} */
|
/** @type {string[]} */
|
||||||
this.ignoreDirs = []
|
this.ignoreDirs = []
|
||||||
@ -59,14 +66,13 @@ class FolderWatcher extends EventEmitter {
|
|||||||
})
|
})
|
||||||
watcher
|
watcher
|
||||||
.on('add', (path) => {
|
.on('add', (path) => {
|
||||||
this.onNewFile(library.id, path)
|
this.onFileAdded(library.id, filePathToPOSIX(path))
|
||||||
}).on('change', (path) => {
|
}).on('change', (path) => {
|
||||||
// This is triggered from metadata changes, not what we want
|
// This is triggered from metadata changes, not what we want
|
||||||
// this.onFileUpdated(path)
|
|
||||||
}).on('unlink', path => {
|
}).on('unlink', path => {
|
||||||
this.onFileRemoved(library.id, path)
|
this.onFileRemoved(library.id, filePathToPOSIX(path))
|
||||||
}).on('rename', (path, pathNext) => {
|
}).on('rename', (path, pathNext) => {
|
||||||
this.onRename(library.id, path, pathNext)
|
this.onFileRename(library.id, filePathToPOSIX(path), filePathToPOSIX(pathNext))
|
||||||
}).on('error', (error) => {
|
}).on('error', (error) => {
|
||||||
Logger.error(`[Watcher] ${error}`)
|
Logger.error(`[Watcher] ${error}`)
|
||||||
}).on('ready', () => {
|
}).on('ready', () => {
|
||||||
@ -132,14 +138,31 @@ class FolderWatcher extends EventEmitter {
|
|||||||
return this.libraryWatchers.map(lib => lib.watcher.close())
|
return this.libraryWatchers.map(lib => lib.watcher.close())
|
||||||
}
|
}
|
||||||
|
|
||||||
onNewFile(libraryId, path) {
|
/**
|
||||||
|
* Watcher detected file added
|
||||||
|
*
|
||||||
|
* @param {string} libraryId
|
||||||
|
* @param {string} path
|
||||||
|
*/
|
||||||
|
onFileAdded(libraryId, path) {
|
||||||
if (this.checkShouldIgnorePath(path)) {
|
if (this.checkShouldIgnorePath(path)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Logger.debug('[Watcher] File Added', path)
|
Logger.debug('[Watcher] File Added', path)
|
||||||
this.addFileUpdate(libraryId, path, 'added')
|
this.addFileUpdate(libraryId, path, 'added')
|
||||||
|
|
||||||
|
if (!this.filesBeingAdded.has(path)) {
|
||||||
|
this.filesBeingAdded.add(path)
|
||||||
|
this.waitForFileToAdd(path)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Watcher detected file removed
|
||||||
|
*
|
||||||
|
* @param {string} libraryId
|
||||||
|
* @param {string} path
|
||||||
|
*/
|
||||||
onFileRemoved(libraryId, path) {
|
onFileRemoved(libraryId, path) {
|
||||||
if (this.checkShouldIgnorePath(path)) {
|
if (this.checkShouldIgnorePath(path)) {
|
||||||
return
|
return
|
||||||
@ -148,11 +171,13 @@ class FolderWatcher extends EventEmitter {
|
|||||||
this.addFileUpdate(libraryId, path, 'deleted')
|
this.addFileUpdate(libraryId, path, 'deleted')
|
||||||
}
|
}
|
||||||
|
|
||||||
onFileUpdated(path) {
|
/**
|
||||||
Logger.debug('[Watcher] Updated File', path)
|
* Watcher detected file renamed
|
||||||
}
|
*
|
||||||
|
* @param {string} libraryId
|
||||||
onRename(libraryId, pathFrom, pathTo) {
|
* @param {string} path
|
||||||
|
*/
|
||||||
|
onFileRename(libraryId, pathFrom, pathTo) {
|
||||||
if (this.checkShouldIgnorePath(pathTo)) {
|
if (this.checkShouldIgnorePath(pathTo)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -161,13 +186,41 @@ class FolderWatcher extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* File update detected from watcher
|
* Get mtimeMs from an added file every second until it is no longer changing
|
||||||
|
* Times out after 180s
|
||||||
|
*
|
||||||
|
* @param {string} path
|
||||||
|
* @param {number} [lastMTimeMs=0]
|
||||||
|
* @param {number} [loop=0]
|
||||||
|
*/
|
||||||
|
async waitForFileToAdd(path, lastMTimeMs = 0, loop = 0) {
|
||||||
|
// Safety to catch infinite loop (180s)
|
||||||
|
if (loop >= 180) {
|
||||||
|
Logger.warn(`[Watcher] Waiting to add file at "${path}" timeout (loop ${loop}) - proceeding`)
|
||||||
|
return this.filesBeingAdded.delete(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
const mtimeMs = await getFileMTimeMs(path)
|
||||||
|
if (mtimeMs === lastMTimeMs) {
|
||||||
|
if (lastMTimeMs) Logger.debug(`[Watcher] File finished adding at "${path}"`)
|
||||||
|
return this.filesBeingAdded.delete(path)
|
||||||
|
}
|
||||||
|
if (lastMTimeMs % 5 === 0) {
|
||||||
|
Logger.debug(`[Watcher] Waiting to add file at "${path}". mtimeMs=${mtimeMs} lastMTimeMs=${lastMTimeMs} (loop ${loop})`)
|
||||||
|
}
|
||||||
|
// Wait 1 second
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
|
this.waitForFileToAdd(path, mtimeMs, ++loop)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queue file update
|
||||||
|
*
|
||||||
* @param {string} libraryId
|
* @param {string} libraryId
|
||||||
* @param {string} path
|
* @param {string} path
|
||||||
* @param {string} type
|
* @param {string} type
|
||||||
*/
|
*/
|
||||||
addFileUpdate(libraryId, path, type) {
|
addFileUpdate(libraryId, path, type) {
|
||||||
path = filePathToPOSIX(path)
|
|
||||||
if (this.pendingFilePaths.includes(path)) return
|
if (this.pendingFilePaths.includes(path)) return
|
||||||
|
|
||||||
// Get file library
|
// Get file library
|
||||||
@ -178,7 +231,7 @@ class FolderWatcher extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get file folder
|
// Get file folder
|
||||||
const folder = libwatcher.folders.find(fold => path.startsWith(filePathToPOSIX(fold.fullPath)))
|
const folder = libwatcher.folders.find(fold => isSameOrSubPath(fold.fullPath, path))
|
||||||
if (!folder) {
|
if (!folder) {
|
||||||
Logger.error(`[Watcher] New file folder not found in library "${libwatcher.name}" with path "${path}"`)
|
Logger.error(`[Watcher] New file folder not found in library "${libwatcher.name}" with path "${path}"`)
|
||||||
return
|
return
|
||||||
@ -202,6 +255,13 @@ class FolderWatcher extends EventEmitter {
|
|||||||
|
|
||||||
Logger.debug(`[Watcher] Modified file in library "${libwatcher.name}" and folder "${folder.id}" with relPath "${relPath}"`)
|
Logger.debug(`[Watcher] Modified file in library "${libwatcher.name}" and folder "${folder.id}" with relPath "${relPath}"`)
|
||||||
|
|
||||||
|
if (!this.pendingTask) {
|
||||||
|
const taskData = {
|
||||||
|
libraryId,
|
||||||
|
libraryName: libwatcher.name
|
||||||
|
}
|
||||||
|
this.pendingTask = TaskManager.createAndAddTask('watcher-scan', `Scanning file changes in "${libwatcher.name}"`, null, true, taskData)
|
||||||
|
}
|
||||||
this.pendingFileUpdates.push({
|
this.pendingFileUpdates.push({
|
||||||
path,
|
path,
|
||||||
relPath,
|
relPath,
|
||||||
@ -210,18 +270,32 @@ class FolderWatcher extends EventEmitter {
|
|||||||
type
|
type
|
||||||
})
|
})
|
||||||
|
|
||||||
// Notify server of update after "pendingDelay"
|
this.handlePendingFileUpdatesTimeout()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait X seconds before notifying scanner that files changed
|
||||||
|
* reset timer if files are still copying
|
||||||
|
*/
|
||||||
|
handlePendingFileUpdatesTimeout() {
|
||||||
clearTimeout(this.pendingTimeout)
|
clearTimeout(this.pendingTimeout)
|
||||||
this.pendingTimeout = setTimeout(() => {
|
this.pendingTimeout = setTimeout(() => {
|
||||||
// this.emit('files', this.pendingFileUpdates)
|
// Check that files are not still being added
|
||||||
LibraryScanner.scanFilesChanged(this.pendingFileUpdates)
|
if (this.pendingFileUpdates.some(pfu => this.filesBeingAdded.has(pfu.path))) {
|
||||||
|
Logger.debug(`[Watcher] Still waiting for pending files "${[...this.filesBeingAdded].join(', ')}"`)
|
||||||
|
return this.handlePendingFileUpdatesTimeout()
|
||||||
|
}
|
||||||
|
|
||||||
|
LibraryScanner.scanFilesChanged(this.pendingFileUpdates, this.pendingTask)
|
||||||
|
this.pendingTask = null
|
||||||
this.pendingFileUpdates = []
|
this.pendingFileUpdates = []
|
||||||
|
this.filesBeingAdded.clear()
|
||||||
}, this.pendingDelay)
|
}, this.pendingDelay)
|
||||||
}
|
}
|
||||||
|
|
||||||
checkShouldIgnorePath(path) {
|
checkShouldIgnorePath(path) {
|
||||||
return !!this.ignoreDirs.find(dirpath => {
|
return !!this.ignoreDirs.find(dirpath => {
|
||||||
return filePathToPOSIX(path).startsWith(dirpath)
|
return isSameOrSubPath(dirpath, path)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,30 +67,10 @@ class AuthorController {
|
|||||||
const payload = req.body
|
const payload = req.body
|
||||||
let hasUpdated = false
|
let hasUpdated = false
|
||||||
|
|
||||||
// Updating/removing cover image
|
// author imagePath must be set through other endpoints as of v2.4.5
|
||||||
if (payload.imagePath !== undefined && payload.imagePath !== req.author.imagePath) {
|
if (payload.imagePath !== undefined) {
|
||||||
if (!payload.imagePath && req.author.imagePath) { // If removing image then remove file
|
Logger.warn(`[AuthorController] Updating local author imagePath is not supported`)
|
||||||
await CacheManager.purgeImageCache(req.author.id) // Purge cache
|
delete payload.imagePath
|
||||||
await CoverManager.removeFile(req.author.imagePath)
|
|
||||||
} else if (payload.imagePath.startsWith('http')) { // Check if image path is a url
|
|
||||||
const imageData = await AuthorFinder.saveAuthorImage(req.author.id, payload.imagePath)
|
|
||||||
if (imageData) {
|
|
||||||
if (req.author.imagePath) {
|
|
||||||
await CacheManager.purgeImageCache(req.author.id) // Purge cache
|
|
||||||
}
|
|
||||||
payload.imagePath = imageData.path
|
|
||||||
hasUpdated = true
|
|
||||||
}
|
|
||||||
} else if (payload.imagePath && payload.imagePath !== req.author.imagePath) { // Changing image path locally
|
|
||||||
if (!await fs.pathExists(payload.imagePath)) { // Make sure image path exists
|
|
||||||
Logger.error(`[AuthorController] Image path does not exist: "${payload.imagePath}"`)
|
|
||||||
return res.status(400).send('Author image path does not exist')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.author.imagePath) {
|
|
||||||
await CacheManager.purgeImageCache(req.author.id) // Purge cache
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const authorNameUpdate = payload.name !== undefined && payload.name !== req.author.name
|
const authorNameUpdate = payload.name !== undefined && payload.name !== req.author.name
|
||||||
@ -131,7 +111,7 @@ class AuthorController {
|
|||||||
Database.removeAuthorFromFilterData(req.author.libraryId, req.author.id)
|
Database.removeAuthorFromFilterData(req.author.libraryId, req.author.id)
|
||||||
|
|
||||||
// Send updated num books for merged author
|
// Send updated num books for merged author
|
||||||
const numBooks = await Database.libraryItemModel.getForAuthor(existingAuthor).length
|
const numBooks = (await Database.libraryItemModel.getForAuthor(existingAuthor)).length
|
||||||
SocketAuthority.emitter('author_updated', existingAuthor.toJSONExpanded(numBooks))
|
SocketAuthority.emitter('author_updated', existingAuthor.toJSONExpanded(numBooks))
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@ -191,6 +171,75 @@ class AuthorController {
|
|||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST: /api/authors/:id/image
|
||||||
|
* Upload author image from web URL
|
||||||
|
*
|
||||||
|
* @param {import('express').Request} req
|
||||||
|
* @param {import('express').Response} res
|
||||||
|
*/
|
||||||
|
async uploadImage(req, res) {
|
||||||
|
if (!req.user.canUpload) {
|
||||||
|
Logger.warn('User attempted to upload an image without permission', req.user)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
if (!req.body.url) {
|
||||||
|
Logger.error(`[AuthorController] Invalid request payload. 'url' not in request body`)
|
||||||
|
return res.status(400).send(`Invalid request payload. 'url' not in request body`)
|
||||||
|
}
|
||||||
|
if (!req.body.url.startsWith?.('http:') && !req.body.url.startsWith?.('https:')) {
|
||||||
|
Logger.error(`[AuthorController] Invalid request payload. Invalid url "${req.body.url}"`)
|
||||||
|
return res.status(400).send(`Invalid request payload. Invalid url "${req.body.url}"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.debug(`[AuthorController] Requesting download author image from url "${req.body.url}"`)
|
||||||
|
const result = await AuthorFinder.saveAuthorImage(req.author.id, req.body.url)
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
return res.status(400).send(result.error)
|
||||||
|
} else if (!result?.path) {
|
||||||
|
return res.status(500).send('Unknown error occurred')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.author.imagePath) {
|
||||||
|
await CacheManager.purgeImageCache(req.author.id) // Purge cache
|
||||||
|
}
|
||||||
|
|
||||||
|
req.author.imagePath = result.path
|
||||||
|
await Database.authorModel.updateFromOld(req.author)
|
||||||
|
|
||||||
|
const numBooks = (await Database.libraryItemModel.getForAuthor(req.author)).length
|
||||||
|
SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks))
|
||||||
|
res.json({
|
||||||
|
author: req.author.toJSON()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE: /api/authors/:id/image
|
||||||
|
* Remove author image & delete image file
|
||||||
|
*
|
||||||
|
* @param {import('express').Request} req
|
||||||
|
* @param {import('express').Response} res
|
||||||
|
*/
|
||||||
|
async deleteImage(req, res) {
|
||||||
|
if (!req.author.imagePath) {
|
||||||
|
Logger.error(`[AuthorController] Author "${req.author.imagePath}" has no imagePath set`)
|
||||||
|
return res.status(400).send('Author has no image path set')
|
||||||
|
}
|
||||||
|
Logger.info(`[AuthorController] Removing image for author "${req.author.name}" at "${req.author.imagePath}"`)
|
||||||
|
await CacheManager.purgeImageCache(req.author.id) // Purge cache
|
||||||
|
await CoverManager.removeFile(req.author.imagePath)
|
||||||
|
req.author.imagePath = null
|
||||||
|
await Database.authorModel.updateFromOld(req.author)
|
||||||
|
|
||||||
|
const numBooks = (await Database.libraryItemModel.getForAuthor(req.author)).length
|
||||||
|
SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks))
|
||||||
|
res.json({
|
||||||
|
author: req.author.toJSON()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async match(req, res) {
|
async match(req, res) {
|
||||||
let authorData = null
|
let authorData = null
|
||||||
const region = req.body.region || 'us'
|
const region = req.body.region || 'us'
|
||||||
@ -215,7 +264,7 @@ class AuthorController {
|
|||||||
await CacheManager.purgeImageCache(req.author.id)
|
await CacheManager.purgeImageCache(req.author.id)
|
||||||
|
|
||||||
const imageData = await AuthorFinder.saveAuthorImage(req.author.id, authorData.image)
|
const imageData = await AuthorFinder.saveAuthorImage(req.author.id, authorData.image)
|
||||||
if (imageData) {
|
if (imageData?.path) {
|
||||||
req.author.imagePath = imageData.path
|
req.author.imagePath = imageData.path
|
||||||
hasUpdates = true
|
hasUpdates = true
|
||||||
}
|
}
|
||||||
@ -231,7 +280,7 @@ class AuthorController {
|
|||||||
|
|
||||||
await Database.updateAuthor(req.author)
|
await Database.updateAuthor(req.author)
|
||||||
|
|
||||||
const numBooks = await Database.libraryItemModel.getForAuthor(req.author).length
|
const numBooks = (await Database.libraryItemModel.getForAuthor(req.author)).length
|
||||||
SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks))
|
SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,32 +51,45 @@ class EmailController {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send ebook to device
|
||||||
|
* User must have access to device and library item
|
||||||
|
*
|
||||||
|
* @param {import('express').Request} req
|
||||||
|
* @param {import('express').Response} res
|
||||||
|
*/
|
||||||
async sendEBookToDevice(req, res) {
|
async sendEBookToDevice(req, res) {
|
||||||
Logger.debug(`[EmailController] Send ebook to device request for libraryItemId=${req.body.libraryItemId}, deviceName=${req.body.deviceName}`)
|
Logger.debug(`[EmailController] Send ebook to device requested by user "${req.user.username}" for libraryItemId=${req.body.libraryItemId}, deviceName=${req.body.deviceName}`)
|
||||||
|
|
||||||
|
const device = Database.emailSettings.getEReaderDevice(req.body.deviceName)
|
||||||
|
if (!device) {
|
||||||
|
return res.status(404).send('Ereader device not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check user has access to device
|
||||||
|
if (!Database.emailSettings.checkUserCanAccessDevice(device, req.user)) {
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
const libraryItem = await Database.libraryItemModel.getOldById(req.body.libraryItemId)
|
const libraryItem = await Database.libraryItemModel.getOldById(req.body.libraryItemId)
|
||||||
if (!libraryItem) {
|
if (!libraryItem) {
|
||||||
return res.status(404).send('Library item not found')
|
return res.status(404).send('Library item not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check user has access to library item
|
||||||
if (!req.user.checkCanAccessLibraryItem(libraryItem)) {
|
if (!req.user.checkCanAccessLibraryItem(libraryItem)) {
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ebookFile = libraryItem.media.ebookFile
|
const ebookFile = libraryItem.media.ebookFile
|
||||||
if (!ebookFile) {
|
if (!ebookFile) {
|
||||||
return res.status(404).send('EBook file not found')
|
return res.status(404).send('Ebook file not found')
|
||||||
}
|
|
||||||
|
|
||||||
const device = Database.emailSettings.getEReaderDevice(req.body.deviceName)
|
|
||||||
if (!device) {
|
|
||||||
return res.status(404).send('E-reader device not found')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.emailManager.sendEBookToDevice(ebookFile, device, res)
|
this.emailManager.sendEBookToDevice(ebookFile, device, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
middleware(req, res, next) {
|
adminMiddleware(req, res, next) {
|
||||||
if (!req.user.isAdminOrUp) {
|
if (!req.user.isAdminOrUp) {
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,8 @@ const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilter
|
|||||||
const libraryItemFilters = require('../utils/queries/libraryItemFilters')
|
const libraryItemFilters = require('../utils/queries/libraryItemFilters')
|
||||||
const seriesFilters = require('../utils/queries/seriesFilters')
|
const seriesFilters = require('../utils/queries/seriesFilters')
|
||||||
const fileUtils = require('../utils/fileUtils')
|
const fileUtils = require('../utils/fileUtils')
|
||||||
const { sort, createNewSortInstance } = require('../libs/fastSort')
|
const { asciiOnlyToLowerCase } = require('../utils/index')
|
||||||
|
const { createNewSortInstance } = require('../libs/fastSort')
|
||||||
const naturalSort = createNewSortInstance({
|
const naturalSort = createNewSortInstance({
|
||||||
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
|
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
|
||||||
})
|
})
|
||||||
@ -555,7 +556,7 @@ class LibraryController {
|
|||||||
return res.status(400).send('No query string')
|
return res.status(400).send('No query string')
|
||||||
}
|
}
|
||||||
const limit = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12
|
const limit = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12
|
||||||
const query = req.query.q.trim().toLowerCase()
|
const query = asciiOnlyToLowerCase(req.query.q.trim())
|
||||||
|
|
||||||
const matches = await libraryItemFilters.search(req.user, req.library, query, limit)
|
const matches = await libraryItemFilters.search(req.user, req.library, query, limit)
|
||||||
res.json(matches)
|
res.json(matches)
|
||||||
@ -620,7 +621,7 @@ class LibraryController {
|
|||||||
model: Database.bookModel,
|
model: Database.bookModel,
|
||||||
attributes: ['id', 'tags', 'explicit'],
|
attributes: ['id', 'tags', 'explicit'],
|
||||||
where: bookWhere,
|
where: bookWhere,
|
||||||
required: false,
|
required: !req.user.isAdminOrUp, // Only show authors with 0 books for admin users or up
|
||||||
through: {
|
through: {
|
||||||
attributes: []
|
attributes: []
|
||||||
}
|
}
|
||||||
@ -774,6 +775,13 @@ class LibraryController {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET: /api/libraries/:id/matchall
|
||||||
|
* Quick match all library items. Book libraries only.
|
||||||
|
*
|
||||||
|
* @param {import('express').Request} req
|
||||||
|
* @param {import('express').Response} res
|
||||||
|
*/
|
||||||
async matchAll(req, res) {
|
async matchAll(req, res) {
|
||||||
if (!req.user.isAdminOrUp) {
|
if (!req.user.isAdminOrUp) {
|
||||||
Logger.error(`[LibraryController] Non-root user attempted to match library items`, req.user)
|
Logger.error(`[LibraryController] Non-root user attempted to match library items`, req.user)
|
||||||
@ -783,7 +791,14 @@ class LibraryController {
|
|||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST: api/libraries/:id/scan
|
/**
|
||||||
|
* POST: /api/libraries/:id/scan
|
||||||
|
* Optional query:
|
||||||
|
* ?force=1
|
||||||
|
*
|
||||||
|
* @param {import('express').Request} req
|
||||||
|
* @param {import('express').Response} res
|
||||||
|
*/
|
||||||
async scan(req, res) {
|
async scan(req, res) {
|
||||||
if (!req.user.isAdminOrUp) {
|
if (!req.user.isAdminOrUp) {
|
||||||
Logger.error(`[LibraryController] Non-root user attempted to scan library`, req.user)
|
Logger.error(`[LibraryController] Non-root user attempted to scan library`, req.user)
|
||||||
@ -791,7 +806,8 @@ class LibraryController {
|
|||||||
}
|
}
|
||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
|
|
||||||
await LibraryScanner.scan(req.library)
|
const forceRescan = req.query.force === '1'
|
||||||
|
await LibraryScanner.scan(req.library, forceRescan)
|
||||||
|
|
||||||
await Database.resetLibraryIssuesFilterData(req.library.id)
|
await Database.resetLibraryIssuesFilterData(req.library.id)
|
||||||
Logger.info('[LibraryController] Scan complete')
|
Logger.info('[LibraryController] Scan complete')
|
||||||
@ -845,6 +861,56 @@ class LibraryController {
|
|||||||
res.send(opmlText)
|
res.send(opmlText)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all metadata.json or metadata.abs files in library item folders
|
||||||
|
*
|
||||||
|
* @param {import('express').Request} req
|
||||||
|
* @param {import('express').Response} res
|
||||||
|
*/
|
||||||
|
async removeAllMetadataFiles(req, res) {
|
||||||
|
if (!req.user.isAdminOrUp) {
|
||||||
|
Logger.error(`[LibraryController] Non-admin user attempted to remove all metadata files`, req.user)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileExt = req.query.ext === 'abs' ? 'abs' : 'json'
|
||||||
|
const metadataFilename = `metadata.${fileExt}`
|
||||||
|
const libraryItemsWithMetadata = await Database.libraryItemModel.findAll({
|
||||||
|
attributes: ['id', 'libraryFiles'],
|
||||||
|
where: [
|
||||||
|
{
|
||||||
|
libraryId: req.library.id
|
||||||
|
},
|
||||||
|
Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(libraryFiles) WHERE json_valid(libraryFiles) AND json_extract(json_each.value, "$.metadata.filename") = "${metadataFilename}")`), {
|
||||||
|
[Sequelize.Op.gte]: 1
|
||||||
|
})
|
||||||
|
]
|
||||||
|
})
|
||||||
|
if (!libraryItemsWithMetadata.length) {
|
||||||
|
Logger.info(`[LibraryController] No ${metadataFilename} files found to remove`)
|
||||||
|
return res.json({
|
||||||
|
found: 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.info(`[LibraryController] Found ${libraryItemsWithMetadata.length} ${metadataFilename} files to remove`)
|
||||||
|
|
||||||
|
let numRemoved = 0
|
||||||
|
for (const libraryItem of libraryItemsWithMetadata) {
|
||||||
|
const metadataFilepath = libraryItem.libraryFiles.find(lf => lf.metadata.filename === metadataFilename)?.metadata.path
|
||||||
|
if (!metadataFilepath) continue
|
||||||
|
Logger.debug(`[LibraryController] Removing file "${metadataFilepath}"`)
|
||||||
|
if ((await fileUtils.removeFile(metadataFilepath))) {
|
||||||
|
numRemoved++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
found: libraryItemsWithMetadata.length,
|
||||||
|
removed: numRemoved
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Middleware that is not using libraryItems from memory
|
* Middleware that is not using libraryItems from memory
|
||||||
* @param {import('express').Request} req
|
* @param {import('express').Request} req
|
||||||
|
@ -85,12 +85,31 @@ class LibraryItemController {
|
|||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET: /api/items/:id/download
|
||||||
|
* Download library item. Zip file if multiple files.
|
||||||
|
*
|
||||||
|
* @param {import('express').Request} req
|
||||||
|
* @param {import('express').Response} res
|
||||||
|
*/
|
||||||
download(req, res) {
|
download(req, res) {
|
||||||
if (!req.user.canDownload) {
|
if (!req.user.canDownload) {
|
||||||
Logger.warn('User attempted to download without permission', req.user)
|
Logger.warn('User attempted to download without permission', req.user)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If library item is a single file in root dir then no need to zip
|
||||||
|
if (req.libraryItem.isFile) {
|
||||||
|
// Express does not set the correct mimetype for m4b files so use our defined mimetypes if available
|
||||||
|
const audioMimeType = getAudioMimeTypeFromExtname(Path.extname(req.libraryItem.path))
|
||||||
|
if (audioMimeType) {
|
||||||
|
res.setHeader('Content-Type', audioMimeType)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.download(req.libraryItem.path, req.libraryItem.relPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const libraryItemPath = req.libraryItem.path
|
const libraryItemPath = req.libraryItem.path
|
||||||
const itemTitle = req.libraryItem.media.metadata.title
|
const itemTitle = req.libraryItem.media.metadata.title
|
||||||
Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${itemTitle}" at "${libraryItemPath}"`)
|
Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${itemTitle}" at "${libraryItemPath}"`)
|
||||||
@ -163,22 +182,22 @@ class LibraryItemController {
|
|||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
|
|
||||||
var libraryItem = req.libraryItem
|
let libraryItem = req.libraryItem
|
||||||
|
|
||||||
var result = null
|
let result = null
|
||||||
if (req.body && req.body.url) {
|
if (req.body?.url) {
|
||||||
Logger.debug(`[LibraryItemController] Requesting download cover from url "${req.body.url}"`)
|
Logger.debug(`[LibraryItemController] Requesting download cover from url "${req.body.url}"`)
|
||||||
result = await CoverManager.downloadCoverFromUrl(libraryItem, req.body.url)
|
result = await CoverManager.downloadCoverFromUrl(libraryItem, req.body.url)
|
||||||
} else if (req.files && req.files.cover) {
|
} else if (req.files?.cover) {
|
||||||
Logger.debug(`[LibraryItemController] Handling uploaded cover`)
|
Logger.debug(`[LibraryItemController] Handling uploaded cover`)
|
||||||
result = await CoverManager.uploadCover(libraryItem, req.files.cover)
|
result = await CoverManager.uploadCover(libraryItem, req.files.cover)
|
||||||
} else {
|
} else {
|
||||||
return res.status(400).send('Invalid request no file or url')
|
return res.status(400).send('Invalid request no file or url')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result && result.error) {
|
if (result?.error) {
|
||||||
return res.status(400).send(result.error)
|
return res.status(400).send(result.error)
|
||||||
} else if (!result || !result.cover) {
|
} else if (!result?.cover) {
|
||||||
return res.status(500).send('Unknown error occurred')
|
return res.status(500).send('Unknown error occurred')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -259,7 +278,6 @@ class LibraryItemController {
|
|||||||
|
|
||||||
// Check if library item media has a cover path
|
// Check if library item media has a cover path
|
||||||
if (!libraryItem.media.coverPath || !await fs.pathExists(libraryItem.media.coverPath)) {
|
if (!libraryItem.media.coverPath || !await fs.pathExists(libraryItem.media.coverPath)) {
|
||||||
Logger.debug(`[LibraryItemController] getCover: Library item "${req.params.id}" has no cover path`)
|
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,8 @@ const libraryItemFilters = require('../utils/queries/libraryItemFilters')
|
|||||||
const patternValidation = require('../libs/nodeCron/pattern-validation')
|
const patternValidation = require('../libs/nodeCron/pattern-validation')
|
||||||
const { isObject, getTitleIgnorePrefix } = require('../utils/index')
|
const { isObject, getTitleIgnorePrefix } = require('../utils/index')
|
||||||
|
|
||||||
|
const TaskManager = require('../managers/TaskManager')
|
||||||
|
|
||||||
//
|
//
|
||||||
// This is a controller for routes that don't have a home yet :(
|
// This is a controller for routes that don't have a home yet :(
|
||||||
//
|
//
|
||||||
@ -102,7 +104,7 @@ class MiscController {
|
|||||||
const includeArray = (req.query.include || '').split(',')
|
const includeArray = (req.query.include || '').split(',')
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
tasks: this.taskManager.tasks.map(t => t.toJSON())
|
tasks: TaskManager.tasks.map(t => t.toJSON())
|
||||||
}
|
}
|
||||||
|
|
||||||
if (includeArray.includes('queue')) {
|
if (includeArray.includes('queue')) {
|
||||||
@ -526,6 +528,54 @@ class MiscController {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST: /api/watcher/update
|
||||||
|
* Update a watch path
|
||||||
|
* Req.body { libraryId, path, type, [oldPath] }
|
||||||
|
* type = add, unlink, rename
|
||||||
|
* oldPath = required only for rename
|
||||||
|
* @this import('../routers/ApiRouter')
|
||||||
|
*
|
||||||
|
* @param {import('express').Request} req
|
||||||
|
* @param {import('express').Response} res
|
||||||
|
*/
|
||||||
|
updateWatchedPath(req, res) {
|
||||||
|
if (!req.user.isAdminOrUp) {
|
||||||
|
Logger.error(`[MiscController] Non-admin user attempted to updateWatchedPath`)
|
||||||
|
return res.sendStatus(404)
|
||||||
|
}
|
||||||
|
|
||||||
|
const libraryId = req.body.libraryId
|
||||||
|
const path = req.body.path
|
||||||
|
const type = req.body.type
|
||||||
|
if (!libraryId || !path || !type) {
|
||||||
|
Logger.error(`[MiscController] Invalid request body for updateWatchedPath. libraryId: "${libraryId}", path: "${path}", type: "${type}"`)
|
||||||
|
return res.sendStatus(400)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'add':
|
||||||
|
this.watcher.onFileAdded(libraryId, path)
|
||||||
|
break;
|
||||||
|
case 'unlink':
|
||||||
|
this.watcher.onFileRemoved(libraryId, path)
|
||||||
|
break;
|
||||||
|
case 'rename':
|
||||||
|
const oldPath = req.body.oldPath
|
||||||
|
if (!oldPath) {
|
||||||
|
Logger.error(`[MiscController] Invalid request body for updateWatchedPath. oldPath is required for rename.`)
|
||||||
|
return res.sendStatus(400)
|
||||||
|
}
|
||||||
|
this.watcher.onFileRename(libraryId, oldPath, path)
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Logger.error(`[MiscController] Invalid type for updateWatchedPath. type: "${type}"`)
|
||||||
|
return res.sendStatus(400)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.sendStatus(200)
|
||||||
|
}
|
||||||
|
|
||||||
validateCronExpression(req, res) {
|
validateCronExpression(req, res) {
|
||||||
const expression = req.body.expression
|
const expression = req.body.expression
|
||||||
if (!expression) {
|
if (!expression) {
|
||||||
|
@ -184,10 +184,9 @@ class PodcastController {
|
|||||||
Logger.error(`[PodcastController] Non-admin user attempted to download episodes`, req.user)
|
Logger.error(`[PodcastController] Non-admin user attempted to download episodes`, req.user)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
var libraryItem = req.libraryItem
|
const libraryItem = req.libraryItem
|
||||||
|
const episodes = req.body
|
||||||
var episodes = req.body
|
if (!episodes?.length) {
|
||||||
if (!episodes || !episodes.length) {
|
|
||||||
return res.sendStatus(400)
|
return res.sendStatus(400)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -286,7 +285,7 @@ class PodcastController {
|
|||||||
const numItems = pmi.playlist.playlistMediaItems.length - 1
|
const numItems = pmi.playlist.playlistMediaItems.length - 1
|
||||||
|
|
||||||
if (!numItems) {
|
if (!numItems) {
|
||||||
Logger.info(`[PodcastController] Playlist "${playlist.name}" has no more items - removing it`)
|
Logger.info(`[PodcastController] Playlist "${pmi.playlist.name}" has no more items - removing it`)
|
||||||
const jsonExpanded = await pmi.playlist.getOldJsonExpanded()
|
const jsonExpanded = await pmi.playlist.getOldJsonExpanded()
|
||||||
SocketAuthority.clientEmitter(pmi.playlist.userId, 'playlist_removed', jsonExpanded)
|
SocketAuthority.clientEmitter(pmi.playlist.userId, 'playlist_removed', jsonExpanded)
|
||||||
await pmi.playlist.destroy()
|
await pmi.playlist.destroy()
|
||||||
|
@ -26,7 +26,7 @@ class SearchController {
|
|||||||
|
|
||||||
let results = null
|
let results = null
|
||||||
if (podcast) results = await PodcastFinder.findCovers(query.title)
|
if (podcast) results = await PodcastFinder.findCovers(query.title)
|
||||||
else results = await BookFinder.findCovers(query.provider || 'google', query.title, query.author || null)
|
else results = await BookFinder.findCovers(query.provider || 'google', query.title, query.author || '')
|
||||||
res.json({
|
res.json({
|
||||||
results
|
results
|
||||||
})
|
})
|
||||||
|
@ -115,6 +115,13 @@ class UserController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PATCH: /api/users/:id
|
||||||
|
* Update user
|
||||||
|
*
|
||||||
|
* @param {import('express').Request} req
|
||||||
|
* @param {import('express').Response} res
|
||||||
|
*/
|
||||||
async update(req, res) {
|
async update(req, res) {
|
||||||
const user = req.reqUser
|
const user = req.reqUser
|
||||||
|
|
||||||
@ -126,6 +133,7 @@ class UserController {
|
|||||||
var account = req.body
|
var account = req.body
|
||||||
var shouldUpdateToken = false
|
var shouldUpdateToken = false
|
||||||
|
|
||||||
|
// When changing username create a new API token
|
||||||
if (account.username !== undefined && account.username !== user.username) {
|
if (account.username !== undefined && account.username !== user.username) {
|
||||||
const usernameExists = await Database.userModel.getUserByUsername(account.username)
|
const usernameExists = await Database.userModel.getUserByUsername(account.username)
|
||||||
if (usernameExists) {
|
if (usernameExists) {
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
const itemDb = require('../db/item.db')
|
|
||||||
|
|
||||||
const getLibraryItem = async (req, res) => {
|
|
||||||
let libraryItem = null
|
|
||||||
if (req.query.expanded == 1) {
|
|
||||||
libraryItem = await itemDb.getLibraryItemExpanded(req.params.id)
|
|
||||||
} else {
|
|
||||||
libraryItem = await itemDb.getLibraryItemMinified(req.params.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json(libraryItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
getLibraryItem
|
|
||||||
}
|
|
@ -1,80 +0,0 @@
|
|||||||
/**
|
|
||||||
* TODO: Unused for testing
|
|
||||||
*/
|
|
||||||
const { Sequelize } = require('sequelize')
|
|
||||||
const Database = require('../Database')
|
|
||||||
|
|
||||||
const getLibraryItemMinified = (libraryItemId) => {
|
|
||||||
return Database.libraryItemModel.findByPk(libraryItemId, {
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: Database.bookModel,
|
|
||||||
attributes: [
|
|
||||||
'id', 'title', 'subtitle', 'publishedYear', 'publishedDate', 'publisher', 'description', 'isbn', 'asin', 'language', 'explicit', 'narrators', 'coverPath', 'genres', 'tags'
|
|
||||||
],
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: Database.authorModel,
|
|
||||||
attributes: ['id', 'name'],
|
|
||||||
through: {
|
|
||||||
attributes: []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: Database.seriesModel,
|
|
||||||
attributes: ['id', 'name'],
|
|
||||||
through: {
|
|
||||||
attributes: ['sequence']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: Database.podcastModel,
|
|
||||||
attributes: [
|
|
||||||
'id', 'title', 'author', 'releaseDate', 'feedURL', 'imageURL', 'description', 'itunesPageURL', 'itunesId', 'itunesArtistId', 'language', 'podcastType', 'explicit', 'autoDownloadEpisodes', 'genres', 'tags',
|
|
||||||
[Sequelize.literal('(SELECT COUNT(*) FROM "podcastEpisodes" WHERE "podcastEpisodes"."podcastId" = podcast.id)'), 'numPodcastEpisodes']
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const getLibraryItemExpanded = (libraryItemId) => {
|
|
||||||
return Database.libraryItemModel.findByPk(libraryItemId, {
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: Database.bookModel,
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: Database.authorModel,
|
|
||||||
through: {
|
|
||||||
attributes: []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: Database.seriesModel,
|
|
||||||
through: {
|
|
||||||
attributes: ['sequence']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: Database.podcastModel,
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: Database.podcastEpisodeModel
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
'libraryFolder',
|
|
||||||
'library'
|
|
||||||
]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
getLibraryItemMinified,
|
|
||||||
getLibraryItemExpanded
|
|
||||||
}
|
|
@ -3,20 +3,13 @@ const Logger = require('../Logger')
|
|||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const Audnexus = require('../providers/Audnexus')
|
const Audnexus = require('../providers/Audnexus')
|
||||||
|
|
||||||
const { downloadFile } = require('../utils/fileUtils')
|
const { downloadImageFile } = require('../utils/fileUtils')
|
||||||
|
|
||||||
class AuthorFinder {
|
class AuthorFinder {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.audnexus = new Audnexus()
|
this.audnexus = new Audnexus()
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadImage(url, outputPath) {
|
|
||||||
return downloadFile(url, outputPath).then(() => true).catch((error) => {
|
|
||||||
Logger.error('[AuthorFinder] Failed to download author image', error)
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
findAuthorByASIN(asin, region) {
|
findAuthorByASIN(asin, region) {
|
||||||
if (!asin) return null
|
if (!asin) return null
|
||||||
return this.audnexus.findAuthorByASIN(asin, region)
|
return this.audnexus.findAuthorByASIN(asin, region)
|
||||||
@ -33,28 +26,36 @@ class AuthorFinder {
|
|||||||
return author
|
return author
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download author image from url and save in authors folder
|
||||||
|
*
|
||||||
|
* @param {string} authorId
|
||||||
|
* @param {string} url
|
||||||
|
* @returns {Promise<{path:string, error:string}>}
|
||||||
|
*/
|
||||||
async saveAuthorImage(authorId, url) {
|
async saveAuthorImage(authorId, url) {
|
||||||
var authorDir = Path.join(global.MetadataPath, 'authors')
|
const authorDir = Path.join(global.MetadataPath, 'authors')
|
||||||
var relAuthorDir = Path.posix.join('/metadata', 'authors')
|
|
||||||
|
|
||||||
if (!await fs.pathExists(authorDir)) {
|
if (!await fs.pathExists(authorDir)) {
|
||||||
await fs.ensureDir(authorDir)
|
await fs.ensureDir(authorDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
var imageExtension = url.toLowerCase().split('.').pop()
|
const imageExtension = url.toLowerCase().split('.').pop()
|
||||||
var ext = imageExtension === 'png' ? 'png' : 'jpg'
|
const ext = imageExtension === 'png' ? 'png' : 'jpg'
|
||||||
var filename = authorId + '.' + ext
|
const filename = authorId + '.' + ext
|
||||||
var outputPath = Path.posix.join(authorDir, filename)
|
const outputPath = Path.posix.join(authorDir, filename)
|
||||||
var relPath = Path.posix.join(relAuthorDir, filename)
|
|
||||||
|
|
||||||
var success = await this.downloadImage(url, outputPath)
|
return downloadImageFile(url, outputPath).then(() => {
|
||||||
if (!success) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
path: outputPath,
|
path: outputPath
|
||||||
relPath
|
|
||||||
}
|
}
|
||||||
|
}).catch((err) => {
|
||||||
|
let errorMsg = err.message || 'Unknown error'
|
||||||
|
Logger.error(`[AuthorFinder] Download image file failed for "${url}"`, errorMsg)
|
||||||
|
return {
|
||||||
|
error: errorMsg
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = new AuthorFinder()
|
module.exports = new AuthorFinder()
|
@ -6,7 +6,7 @@ const Audnexus = require('../providers/Audnexus')
|
|||||||
const FantLab = require('../providers/FantLab')
|
const FantLab = require('../providers/FantLab')
|
||||||
const AudiobookCovers = require('../providers/AudiobookCovers')
|
const AudiobookCovers = require('../providers/AudiobookCovers')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const { levenshteinDistance } = require('../utils/index')
|
const { levenshteinDistance, escapeRegExp } = require('../utils/index')
|
||||||
|
|
||||||
class BookFinder {
|
class BookFinder {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -59,12 +59,17 @@ class BookFinder {
|
|||||||
|
|
||||||
// Remove single quotes (i.e. "Ender's Game" becomes "Enders Game")
|
// Remove single quotes (i.e. "Ender's Game" becomes "Enders Game")
|
||||||
cleaned = cleaned.replace(/'/g, '')
|
cleaned = cleaned.replace(/'/g, '')
|
||||||
return this.replaceAccentedChars(cleaned)
|
return this.replaceAccentedChars(cleaned).toLowerCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanAuthorForCompares(author) {
|
cleanAuthorForCompares(author) {
|
||||||
if (!author) return ''
|
if (!author) return ''
|
||||||
return this.replaceAccentedChars(author)
|
let cleanAuthor = this.replaceAccentedChars(author).toLowerCase()
|
||||||
|
// separate initials
|
||||||
|
cleanAuthor = cleanAuthor.replace(/([a-z])\.([a-z])/g, '$1. $2')
|
||||||
|
// remove middle initials
|
||||||
|
cleanAuthor = cleanAuthor.replace(/(?<=\w\w)(\s+[a-z]\.?)+(?=\s+\w\w)/g, '')
|
||||||
|
return cleanAuthor
|
||||||
}
|
}
|
||||||
|
|
||||||
filterSearchResults(books, title, author, maxTitleDistance, maxAuthorDistance) {
|
filterSearchResults(books, title, author, maxTitleDistance, maxAuthorDistance) {
|
||||||
@ -136,6 +141,10 @@ class BookFinder {
|
|||||||
if (!booksFiltered.length && books.length) {
|
if (!booksFiltered.length && books.length) {
|
||||||
if (this.verbose) Logger.debug(`Search has ${books.length} matches, but no close title matches`)
|
if (this.verbose) Logger.debug(`Search has ${books.length} matches, but no close title matches`)
|
||||||
}
|
}
|
||||||
|
booksFiltered.sort((a, b) => {
|
||||||
|
return a.totalDistance - b.totalDistance
|
||||||
|
})
|
||||||
|
|
||||||
return booksFiltered
|
return booksFiltered
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,34 +188,151 @@ class BookFinder {
|
|||||||
return books
|
return books
|
||||||
}
|
}
|
||||||
|
|
||||||
addTitleCandidate(title, candidates) {
|
static TitleCandidates = class {
|
||||||
|
|
||||||
|
constructor(bookFinder, cleanAuthor) {
|
||||||
|
this.bookFinder = bookFinder
|
||||||
|
this.candidates = new Set()
|
||||||
|
this.cleanAuthor = cleanAuthor
|
||||||
|
this.priorities = {}
|
||||||
|
this.positions = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
add(title, position = 0) {
|
||||||
|
// if title contains the author, remove it
|
||||||
|
if (this.cleanAuthor) {
|
||||||
|
const authorRe = new RegExp(`(^| | by |)${escapeRegExp(this.cleanAuthor)}(?= |$)`, "g")
|
||||||
|
title = this.bookFinder.cleanAuthorForCompares(title).replace(authorRe, '').trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
const titleTransformers = [
|
||||||
|
[/([,:;_]| by ).*/g, ''], // Remove subtitle
|
||||||
|
[/(^| )\d+k(bps)?( |$)/, ' '], // Remove bitrate
|
||||||
|
[/ (2nd|3rd|\d+th)\s+ed(\.|ition)?/g, ''], // Remove edition
|
||||||
|
[/(^| |\.)(m4b|m4a|mp3)( |$)/g, ''], // Remove file-type
|
||||||
|
[/ a novel.*$/g, ''], // Remove "a novel"
|
||||||
|
[/^\d+ | \d+$/g, ''], // Remove preceding/trailing numbers
|
||||||
|
]
|
||||||
|
|
||||||
// Main variant
|
// Main variant
|
||||||
const cleanTitle = this.cleanTitleForCompares(title).trim()
|
const cleanTitle = this.bookFinder.cleanTitleForCompares(title).trim()
|
||||||
if (!cleanTitle) return
|
if (!cleanTitle) return
|
||||||
candidates.add(cleanTitle)
|
this.candidates.add(cleanTitle)
|
||||||
|
this.priorities[cleanTitle] = 0
|
||||||
|
this.positions[cleanTitle] = position
|
||||||
|
|
||||||
let candidate = cleanTitle
|
let candidate = cleanTitle
|
||||||
|
|
||||||
// Remove subtitle
|
for (const transformer of titleTransformers)
|
||||||
candidate = candidate.replace(/([,:;_]| by ).*/g, "").trim()
|
candidate = candidate.replace(transformer[0], transformer[1]).trim()
|
||||||
if (candidate)
|
|
||||||
candidates.add(candidate)
|
|
||||||
|
|
||||||
// Remove preceding/trailing numbers
|
if (candidate != cleanTitle) {
|
||||||
candidate = candidate.replace(/^\d+ | \d+$/g, "").trim()
|
if (candidate) {
|
||||||
if (candidate)
|
this.candidates.add(candidate)
|
||||||
candidates.add(candidate)
|
this.priorities[candidate] = 0
|
||||||
|
this.positions[candidate] = position
|
||||||
// Remove bitrate
|
|
||||||
candidate = candidate.replace(/(^| )\d+k(bps)?( |$)/, " ").trim()
|
|
||||||
if (candidate)
|
|
||||||
candidates.add(candidate)
|
|
||||||
|
|
||||||
// Remove edition
|
|
||||||
candidate = candidate.replace(/ (2nd|3rd|\d+th)\s+ed(\.|ition)?/, "").trim()
|
|
||||||
if (candidate)
|
|
||||||
candidates.add(candidate)
|
|
||||||
}
|
}
|
||||||
|
this.priorities[cleanTitle] = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get size() {
|
||||||
|
return this.candidates.size
|
||||||
|
}
|
||||||
|
|
||||||
|
getCandidates() {
|
||||||
|
var candidates = [...this.candidates]
|
||||||
|
candidates.sort((a, b) => {
|
||||||
|
// Candidates that include the author are likely low quality
|
||||||
|
const includesAuthorDiff = !b.includes(this.cleanAuthor) - !a.includes(this.cleanAuthor)
|
||||||
|
if (includesAuthorDiff) return includesAuthorDiff
|
||||||
|
// Candidates that include only digits are also likely low quality
|
||||||
|
const onlyDigits = /^\d+$/
|
||||||
|
const includesOnlyDigitsDiff = !onlyDigits.test(b) - !onlyDigits.test(a)
|
||||||
|
if (includesOnlyDigitsDiff) return includesOnlyDigitsDiff
|
||||||
|
// transformed candidates receive higher priority
|
||||||
|
const priorityDiff = this.priorities[a] - this.priorities[b]
|
||||||
|
if (priorityDiff) return priorityDiff
|
||||||
|
// if same priorirty, prefer candidates that are closer to the beginning (e.g. titles before subtitles)
|
||||||
|
const positionDiff = this.positions[a] - this.positions[b]
|
||||||
|
if (positionDiff) return positionDiff
|
||||||
|
// Start with longer candidaets, as they are likely more specific
|
||||||
|
const lengthDiff = b.length - a.length
|
||||||
|
if (lengthDiff) return lengthDiff
|
||||||
|
return b.localeCompare(a)
|
||||||
|
})
|
||||||
|
Logger.debug(`[${this.constructor.name}] Found ${candidates.length} fuzzy title candidates`)
|
||||||
|
Logger.debug(candidates)
|
||||||
|
return candidates
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(title) {
|
||||||
|
return this.candidates.delete(title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static AuthorCandidates = class {
|
||||||
|
constructor(bookFinder, cleanAuthor) {
|
||||||
|
this.bookFinder = bookFinder
|
||||||
|
this.candidates = new Set()
|
||||||
|
this.cleanAuthor = cleanAuthor
|
||||||
|
if (cleanAuthor) this.candidates.add(cleanAuthor)
|
||||||
|
}
|
||||||
|
|
||||||
|
validateAuthor(name, region = '', maxLevenshtein = 2) {
|
||||||
|
return this.bookFinder.audnexus.authorASINsRequest(name, region).then((asins) => {
|
||||||
|
for (const [i, asin] of asins.entries()) {
|
||||||
|
if (i > 10) break
|
||||||
|
let cleanName = this.bookFinder.cleanAuthorForCompares(asin.name)
|
||||||
|
if (!cleanName) continue
|
||||||
|
if (cleanName.includes(name)) return name
|
||||||
|
if (name.includes(cleanName)) return cleanName
|
||||||
|
if (levenshteinDistance(cleanName, name) <= maxLevenshtein) return cleanName
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
add(author) {
|
||||||
|
const cleanAuthor = this.bookFinder.cleanAuthorForCompares(author).trim()
|
||||||
|
if (!cleanAuthor) return
|
||||||
|
this.candidates.add(cleanAuthor)
|
||||||
|
}
|
||||||
|
|
||||||
|
get size() {
|
||||||
|
return this.candidates.size
|
||||||
|
}
|
||||||
|
|
||||||
|
get agressivelyCleanAuthor() {
|
||||||
|
if (this.cleanAuthor) {
|
||||||
|
const agressivelyCleanAuthor = this.cleanAuthor.replace(/[,/-].*$/, '').trim()
|
||||||
|
return agressivelyCleanAuthor ? agressivelyCleanAuthor : this.cleanAuthor
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCandidates() {
|
||||||
|
var filteredCandidates = []
|
||||||
|
var promises = []
|
||||||
|
for (const candidate of this.candidates) {
|
||||||
|
promises.push(this.validateAuthor(candidate))
|
||||||
|
}
|
||||||
|
const results = [...new Set(await Promise.all(promises))]
|
||||||
|
filteredCandidates = results.filter(author => author)
|
||||||
|
// If no valid candidates were found, add back an aggresively cleaned author version
|
||||||
|
if (!filteredCandidates.length && this.cleanAuthor) filteredCandidates.push(this.agressivelyCleanAuthor)
|
||||||
|
// Always add an empty author candidate
|
||||||
|
filteredCandidates.push('')
|
||||||
|
Logger.debug(`[${this.constructor.name}] Found ${filteredCandidates.length} fuzzy author candidates`)
|
||||||
|
Logger.debug(filteredCandidates)
|
||||||
|
return filteredCandidates
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(author) {
|
||||||
|
return this.candidates.delete(author)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search for books including fuzzy searches
|
* Search for books including fuzzy searches
|
||||||
@ -232,61 +358,35 @@ class BookFinder {
|
|||||||
books = await this.runSearch(title, author, provider, asin, maxTitleDistance, maxAuthorDistance)
|
books = await this.runSearch(title, author, provider, asin, maxTitleDistance, maxAuthorDistance)
|
||||||
|
|
||||||
if (!books.length && maxFuzzySearches > 0) {
|
if (!books.length && maxFuzzySearches > 0) {
|
||||||
// normalize title and author
|
// Normalize title and author
|
||||||
title = title.trim().toLowerCase()
|
title = title.trim().toLowerCase()
|
||||||
author = author.trim().toLowerCase()
|
author = author?.trim().toLowerCase() || ''
|
||||||
|
|
||||||
|
const cleanAuthor = this.cleanAuthorForCompares(author)
|
||||||
|
|
||||||
// Now run up to maxFuzzySearches fuzzy searches
|
// Now run up to maxFuzzySearches fuzzy searches
|
||||||
let candidates = new Set()
|
let authorCandidates = new BookFinder.AuthorCandidates(this, cleanAuthor)
|
||||||
let cleanedAuthor = this.cleanAuthorForCompares(author)
|
|
||||||
this.addTitleCandidate(title, candidates)
|
|
||||||
|
|
||||||
// remove parentheses and their contents, and replace with a separator
|
// Remove underscores and parentheses with their contents, and replace with a separator
|
||||||
const cleanTitle = title.replace(/\[.*?\]|\(.*?\)|{.*?}/g, " - ")
|
const cleanTitle = title.replace(/\[.*?\]|\(.*?\)|{.*?}|_/g, " - ")
|
||||||
// Split title into hypen-separated parts
|
// Split title into hypen-separated parts
|
||||||
const titleParts = cleanTitle.split(/ - | -|- /)
|
const titleParts = cleanTitle.split(/ - | -|- /)
|
||||||
for (const titlePart of titleParts) {
|
for (const titlePart of titleParts)
|
||||||
this.addTitleCandidate(titlePart, candidates)
|
authorCandidates.add(titlePart)
|
||||||
}
|
authorCandidates = await authorCandidates.getCandidates()
|
||||||
// We already searched for original title
|
for (const authorCandidate of authorCandidates) {
|
||||||
if (author == cleanedAuthor) candidates.delete(title)
|
let titleCandidates = new BookFinder.TitleCandidates(this, authorCandidate)
|
||||||
if (candidates.size > 0) {
|
for (const [position, titlePart] of titleParts.entries())
|
||||||
candidates = [...candidates]
|
titleCandidates.add(titlePart, position)
|
||||||
candidates.sort((a, b) => {
|
titleCandidates = titleCandidates.getCandidates()
|
||||||
// Candidates that include the author are likely low quality
|
for (const titleCandidate of titleCandidates) {
|
||||||
const includesAuthorDiff = !b.includes(cleanedAuthor) - !a.includes(cleanedAuthor)
|
if (titleCandidate == title && authorCandidate == author) continue // We already tried this
|
||||||
if (includesAuthorDiff) return includesAuthorDiff
|
|
||||||
// Candidates that include only digits are also likely low quality
|
|
||||||
const onlyDigits = /^\d+$/
|
|
||||||
const includesOnlyDigitsDiff = !onlyDigits.test(b) - !onlyDigits.test(a)
|
|
||||||
if (includesOnlyDigitsDiff) return includesOnlyDigitsDiff
|
|
||||||
// Start with longer candidaets, as they are likely more specific
|
|
||||||
const lengthDiff = b.length - a.length
|
|
||||||
if (lengthDiff) return lengthDiff
|
|
||||||
return b.localeCompare(a)
|
|
||||||
})
|
|
||||||
Logger.debug(`[BookFinder] Found ${candidates.length} fuzzy title candidates`, candidates)
|
|
||||||
for (const candidate of candidates) {
|
|
||||||
if (++numFuzzySearches > maxFuzzySearches) return books
|
if (++numFuzzySearches > maxFuzzySearches) return books
|
||||||
books = await this.runSearch(candidate, cleanedAuthor, provider, asin, maxTitleDistance, maxAuthorDistance)
|
books = await this.runSearch(titleCandidate, authorCandidate, provider, asin, maxTitleDistance, maxAuthorDistance)
|
||||||
if (books.length) break
|
if (books.length) return books
|
||||||
}
|
|
||||||
if (!books.length) {
|
|
||||||
// Now try searching without the author
|
|
||||||
for (const candidate of candidates) {
|
|
||||||
if (++numFuzzySearches > maxFuzzySearches) return books
|
|
||||||
books = await this.runSearch(candidate, '', provider, asin, maxTitleDistance, maxAuthorDistance)
|
|
||||||
if (books.length) break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (provider === 'openlibrary') {
|
|
||||||
books.sort((a, b) => {
|
|
||||||
return a.totalDistance - b.totalDistance
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return books
|
return books
|
||||||
}
|
}
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
(The MIT License)
|
|
||||||
|
|
||||||
Copyright (c) 2012 TJ Holowaychuk <tj@vision-media.ca>
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -1,2 +0,0 @@
|
|||||||
exports.parse = require('./parse');
|
|
||||||
exports.stringify = require('./stringify');
|
|
@ -1,603 +0,0 @@
|
|||||||
// http://www.w3.org/TR/CSS21/grammar.html
|
|
||||||
// https://github.com/visionmedia/css-parse/pull/49#issuecomment-30088027
|
|
||||||
var commentre = /\/\*[^*]*\*+([^/*][^*]*\*+)*\//g
|
|
||||||
|
|
||||||
module.exports = function(css, options){
|
|
||||||
options = options || {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Positional.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var lineno = 1;
|
|
||||||
var column = 1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update lineno and column based on `str`.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function updatePosition(str) {
|
|
||||||
var lines = str.match(/\n/g);
|
|
||||||
if (lines) lineno += lines.length;
|
|
||||||
var i = str.lastIndexOf('\n');
|
|
||||||
column = ~i ? str.length - i : column + str.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark position and patch `node.position`.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function position() {
|
|
||||||
var start = { line: lineno, column: column };
|
|
||||||
return function(node){
|
|
||||||
node.position = new Position(start);
|
|
||||||
whitespace();
|
|
||||||
return node;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Store position information for a node
|
|
||||||
*/
|
|
||||||
|
|
||||||
function Position(start) {
|
|
||||||
this.start = start;
|
|
||||||
this.end = { line: lineno, column: column };
|
|
||||||
this.source = options.source;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Non-enumerable source string
|
|
||||||
*/
|
|
||||||
|
|
||||||
Position.prototype.content = css;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error `msg`.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var errorsList = [];
|
|
||||||
|
|
||||||
function error(msg) {
|
|
||||||
var err = new Error(options.source + ':' + lineno + ':' + column + ': ' + msg);
|
|
||||||
err.reason = msg;
|
|
||||||
err.filename = options.source;
|
|
||||||
err.line = lineno;
|
|
||||||
err.column = column;
|
|
||||||
err.source = css;
|
|
||||||
|
|
||||||
if (options.silent) {
|
|
||||||
errorsList.push(err);
|
|
||||||
} else {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse stylesheet.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function stylesheet() {
|
|
||||||
var rulesList = rules();
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: 'stylesheet',
|
|
||||||
stylesheet: {
|
|
||||||
source: options.source,
|
|
||||||
rules: rulesList,
|
|
||||||
parsingErrors: errorsList
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opening brace.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function open() {
|
|
||||||
return match(/^{\s*/);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Closing brace.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
return match(/^}/);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse ruleset.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function rules() {
|
|
||||||
var node;
|
|
||||||
var rules = [];
|
|
||||||
whitespace();
|
|
||||||
comments(rules);
|
|
||||||
while (css.length && css.charAt(0) != '}' && (node = atrule() || rule())) {
|
|
||||||
if (node !== false) {
|
|
||||||
rules.push(node);
|
|
||||||
comments(rules);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return rules;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Match `re` and return captures.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function match(re) {
|
|
||||||
var m = re.exec(css);
|
|
||||||
if (!m) return;
|
|
||||||
var str = m[0];
|
|
||||||
updatePosition(str);
|
|
||||||
css = css.slice(str.length);
|
|
||||||
return m;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse whitespace.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function whitespace() {
|
|
||||||
match(/^\s*/);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse comments;
|
|
||||||
*/
|
|
||||||
|
|
||||||
function comments(rules) {
|
|
||||||
var c;
|
|
||||||
rules = rules || [];
|
|
||||||
while (c = comment()) {
|
|
||||||
if (c !== false) {
|
|
||||||
rules.push(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return rules;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse comment.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function comment() {
|
|
||||||
var pos = position();
|
|
||||||
if ('/' != css.charAt(0) || '*' != css.charAt(1)) return;
|
|
||||||
|
|
||||||
var i = 2;
|
|
||||||
while ("" != css.charAt(i) && ('*' != css.charAt(i) || '/' != css.charAt(i + 1))) ++i;
|
|
||||||
i += 2;
|
|
||||||
|
|
||||||
if ("" === css.charAt(i-1)) {
|
|
||||||
return error('End of comment missing');
|
|
||||||
}
|
|
||||||
|
|
||||||
var str = css.slice(2, i - 2);
|
|
||||||
column += 2;
|
|
||||||
updatePosition(str);
|
|
||||||
css = css.slice(i);
|
|
||||||
column += 2;
|
|
||||||
|
|
||||||
return pos({
|
|
||||||
type: 'comment',
|
|
||||||
comment: str
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse selector.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function selector() {
|
|
||||||
var m = match(/^([^{]+)/);
|
|
||||||
if (!m) return;
|
|
||||||
/* @fix Remove all comments from selectors
|
|
||||||
* http://ostermiller.org/findcomment.html */
|
|
||||||
return trim(m[0])
|
|
||||||
.replace(/\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*\/+/g, '')
|
|
||||||
.replace(/"(?:\\"|[^"])*"|'(?:\\'|[^'])*'/g, function(m) {
|
|
||||||
return m.replace(/,/g, '\u200C');
|
|
||||||
})
|
|
||||||
.split(/\s*(?![^(]*\)),\s*/)
|
|
||||||
.map(function(s) {
|
|
||||||
return s.replace(/\u200C/g, ',');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse declaration.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function declaration() {
|
|
||||||
var pos = position();
|
|
||||||
|
|
||||||
// prop
|
|
||||||
var prop = match(/^(\*?[-#\/\*\\\w]+(\[[0-9a-z_-]+\])?)\s*/);
|
|
||||||
if (!prop) return;
|
|
||||||
prop = trim(prop[0]);
|
|
||||||
|
|
||||||
// :
|
|
||||||
if (!match(/^:\s*/)) return error("property missing ':'");
|
|
||||||
|
|
||||||
// val
|
|
||||||
var val = match(/^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^\)]*?\)|[^};])+)/);
|
|
||||||
|
|
||||||
var ret = pos({
|
|
||||||
type: 'declaration',
|
|
||||||
property: prop.replace(commentre, ''),
|
|
||||||
value: val ? trim(val[0]).replace(commentre, '') : ''
|
|
||||||
});
|
|
||||||
|
|
||||||
// ;
|
|
||||||
match(/^[;\s]*/);
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse declarations.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function declarations() {
|
|
||||||
var decls = [];
|
|
||||||
|
|
||||||
if (!open()) return error("missing '{'");
|
|
||||||
comments(decls);
|
|
||||||
|
|
||||||
// declarations
|
|
||||||
var decl;
|
|
||||||
while (decl = declaration()) {
|
|
||||||
if (decl !== false) {
|
|
||||||
decls.push(decl);
|
|
||||||
comments(decls);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!close()) return error("missing '}'");
|
|
||||||
return decls;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse keyframe.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function keyframe() {
|
|
||||||
var m;
|
|
||||||
var vals = [];
|
|
||||||
var pos = position();
|
|
||||||
|
|
||||||
while (m = match(/^((\d+\.\d+|\.\d+|\d+)%?|[a-z]+)\s*/)) {
|
|
||||||
vals.push(m[1]);
|
|
||||||
match(/^,\s*/);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!vals.length) return;
|
|
||||||
|
|
||||||
return pos({
|
|
||||||
type: 'keyframe',
|
|
||||||
values: vals,
|
|
||||||
declarations: declarations()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse keyframes.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function atkeyframes() {
|
|
||||||
var pos = position();
|
|
||||||
var m = match(/^@([-\w]+)?keyframes\s*/);
|
|
||||||
|
|
||||||
if (!m) return;
|
|
||||||
var vendor = m[1];
|
|
||||||
|
|
||||||
// identifier
|
|
||||||
var m = match(/^([-\w]+)\s*/);
|
|
||||||
if (!m) return error("@keyframes missing name");
|
|
||||||
var name = m[1];
|
|
||||||
|
|
||||||
if (!open()) return error("@keyframes missing '{'");
|
|
||||||
|
|
||||||
var frame;
|
|
||||||
var frames = comments();
|
|
||||||
while (frame = keyframe()) {
|
|
||||||
frames.push(frame);
|
|
||||||
frames = frames.concat(comments());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!close()) return error("@keyframes missing '}'");
|
|
||||||
|
|
||||||
return pos({
|
|
||||||
type: 'keyframes',
|
|
||||||
name: name,
|
|
||||||
vendor: vendor,
|
|
||||||
keyframes: frames
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse supports.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function atsupports() {
|
|
||||||
var pos = position();
|
|
||||||
var m = match(/^@supports *([^{]+)/);
|
|
||||||
|
|
||||||
if (!m) return;
|
|
||||||
var supports = trim(m[1]);
|
|
||||||
|
|
||||||
if (!open()) return error("@supports missing '{'");
|
|
||||||
|
|
||||||
var style = comments().concat(rules());
|
|
||||||
|
|
||||||
if (!close()) return error("@supports missing '}'");
|
|
||||||
|
|
||||||
return pos({
|
|
||||||
type: 'supports',
|
|
||||||
supports: supports,
|
|
||||||
rules: style
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse host.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function athost() {
|
|
||||||
var pos = position();
|
|
||||||
var m = match(/^@host\s*/);
|
|
||||||
|
|
||||||
if (!m) return;
|
|
||||||
|
|
||||||
if (!open()) return error("@host missing '{'");
|
|
||||||
|
|
||||||
var style = comments().concat(rules());
|
|
||||||
|
|
||||||
if (!close()) return error("@host missing '}'");
|
|
||||||
|
|
||||||
return pos({
|
|
||||||
type: 'host',
|
|
||||||
rules: style
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse media.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function atmedia() {
|
|
||||||
var pos = position();
|
|
||||||
var m = match(/^@media *([^{]+)/);
|
|
||||||
|
|
||||||
if (!m) return;
|
|
||||||
var media = trim(m[1]);
|
|
||||||
|
|
||||||
if (!open()) return error("@media missing '{'");
|
|
||||||
|
|
||||||
var style = comments().concat(rules());
|
|
||||||
|
|
||||||
if (!close()) return error("@media missing '}'");
|
|
||||||
|
|
||||||
return pos({
|
|
||||||
type: 'media',
|
|
||||||
media: media,
|
|
||||||
rules: style
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse custom-media.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function atcustommedia() {
|
|
||||||
var pos = position();
|
|
||||||
var m = match(/^@custom-media\s+(--[^\s]+)\s*([^{;]+);/);
|
|
||||||
if (!m) return;
|
|
||||||
|
|
||||||
return pos({
|
|
||||||
type: 'custom-media',
|
|
||||||
name: trim(m[1]),
|
|
||||||
media: trim(m[2])
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse paged media.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function atpage() {
|
|
||||||
var pos = position();
|
|
||||||
var m = match(/^@page */);
|
|
||||||
if (!m) return;
|
|
||||||
|
|
||||||
var sel = selector() || [];
|
|
||||||
|
|
||||||
if (!open()) return error("@page missing '{'");
|
|
||||||
var decls = comments();
|
|
||||||
|
|
||||||
// declarations
|
|
||||||
var decl;
|
|
||||||
while (decl = declaration()) {
|
|
||||||
decls.push(decl);
|
|
||||||
decls = decls.concat(comments());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!close()) return error("@page missing '}'");
|
|
||||||
|
|
||||||
return pos({
|
|
||||||
type: 'page',
|
|
||||||
selectors: sel,
|
|
||||||
declarations: decls
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse document.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function atdocument() {
|
|
||||||
var pos = position();
|
|
||||||
var m = match(/^@([-\w]+)?document *([^{]+)/);
|
|
||||||
if (!m) return;
|
|
||||||
|
|
||||||
var vendor = trim(m[1]);
|
|
||||||
var doc = trim(m[2]);
|
|
||||||
|
|
||||||
if (!open()) return error("@document missing '{'");
|
|
||||||
|
|
||||||
var style = comments().concat(rules());
|
|
||||||
|
|
||||||
if (!close()) return error("@document missing '}'");
|
|
||||||
|
|
||||||
return pos({
|
|
||||||
type: 'document',
|
|
||||||
document: doc,
|
|
||||||
vendor: vendor,
|
|
||||||
rules: style
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse font-face.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function atfontface() {
|
|
||||||
var pos = position();
|
|
||||||
var m = match(/^@font-face\s*/);
|
|
||||||
if (!m) return;
|
|
||||||
|
|
||||||
if (!open()) return error("@font-face missing '{'");
|
|
||||||
var decls = comments();
|
|
||||||
|
|
||||||
// declarations
|
|
||||||
var decl;
|
|
||||||
while (decl = declaration()) {
|
|
||||||
decls.push(decl);
|
|
||||||
decls = decls.concat(comments());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!close()) return error("@font-face missing '}'");
|
|
||||||
|
|
||||||
return pos({
|
|
||||||
type: 'font-face',
|
|
||||||
declarations: decls
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse import
|
|
||||||
*/
|
|
||||||
|
|
||||||
var atimport = _compileAtrule('import');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse charset
|
|
||||||
*/
|
|
||||||
|
|
||||||
var atcharset = _compileAtrule('charset');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse namespace
|
|
||||||
*/
|
|
||||||
|
|
||||||
var atnamespace = _compileAtrule('namespace');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse non-block at-rules
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
function _compileAtrule(name) {
|
|
||||||
var re = new RegExp('^@' + name + '\\s*([^;]+);');
|
|
||||||
return function() {
|
|
||||||
var pos = position();
|
|
||||||
var m = match(re);
|
|
||||||
if (!m) return;
|
|
||||||
var ret = { type: name };
|
|
||||||
ret[name] = m[1].trim();
|
|
||||||
return pos(ret);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse at rule.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function atrule() {
|
|
||||||
if (css[0] != '@') return;
|
|
||||||
|
|
||||||
return atkeyframes()
|
|
||||||
|| atmedia()
|
|
||||||
|| atcustommedia()
|
|
||||||
|| atsupports()
|
|
||||||
|| atimport()
|
|
||||||
|| atcharset()
|
|
||||||
|| atnamespace()
|
|
||||||
|| atdocument()
|
|
||||||
|| atpage()
|
|
||||||
|| athost()
|
|
||||||
|| atfontface();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse rule.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function rule() {
|
|
||||||
var pos = position();
|
|
||||||
var sel = selector();
|
|
||||||
|
|
||||||
if (!sel) return error('selector missing');
|
|
||||||
comments();
|
|
||||||
|
|
||||||
return pos({
|
|
||||||
type: 'rule',
|
|
||||||
selectors: sel,
|
|
||||||
declarations: declarations()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return addParent(stylesheet());
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Trim `str`.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function trim(str) {
|
|
||||||
return str ? str.replace(/^\s+|\s+$/g, '') : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds non-enumerable parent node reference to each node.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function addParent(obj, parent) {
|
|
||||||
var isNode = obj && typeof obj.type === 'string';
|
|
||||||
var childParent = isNode ? obj : parent;
|
|
||||||
|
|
||||||
for (var k in obj) {
|
|
||||||
var value = obj[k];
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
value.forEach(function(v) { addParent(v, childParent); });
|
|
||||||
} else if (value && typeof value === 'object') {
|
|
||||||
addParent(value, childParent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isNode) {
|
|
||||||
Object.defineProperty(obj, 'parent', {
|
|
||||||
configurable: true,
|
|
||||||
writable: true,
|
|
||||||
enumerable: false,
|
|
||||||
value: parent || null
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return obj;
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
|
|
||||||
/**
|
|
||||||
* Expose `Compiler`.
|
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports = Compiler;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize a compiler.
|
|
||||||
*
|
|
||||||
* @param {Type} name
|
|
||||||
* @return {Type}
|
|
||||||
* @api public
|
|
||||||
*/
|
|
||||||
|
|
||||||
function Compiler(opts) {
|
|
||||||
this.options = opts || {};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emit `str`
|
|
||||||
*/
|
|
||||||
|
|
||||||
Compiler.prototype.emit = function(str) {
|
|
||||||
return str;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visit `node`.
|
|
||||||
*/
|
|
||||||
|
|
||||||
Compiler.prototype.visit = function(node){
|
|
||||||
return this[node.type](node);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map visit over array of `nodes`, optionally using a `delim`
|
|
||||||
*/
|
|
||||||
|
|
||||||
Compiler.prototype.mapVisit = function(nodes, delim){
|
|
||||||
var buf = '';
|
|
||||||
delim = delim || '';
|
|
||||||
|
|
||||||
for (var i = 0, length = nodes.length; i < length; i++) {
|
|
||||||
buf += this.visit(nodes[i]);
|
|
||||||
if (delim && i < length - 1) buf += this.emit(delim);
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf;
|
|
||||||
};
|
|
@ -1,199 +0,0 @@
|
|||||||
|
|
||||||
/**
|
|
||||||
* Module dependencies.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var Base = require('./compiler');
|
|
||||||
var inherits = require('inherits');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Expose compiler.
|
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports = Compiler;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize a new `Compiler`.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function Compiler(options) {
|
|
||||||
Base.call(this, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inherit from `Base.prototype`.
|
|
||||||
*/
|
|
||||||
|
|
||||||
inherits(Compiler, Base);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compile `node`.
|
|
||||||
*/
|
|
||||||
|
|
||||||
Compiler.prototype.compile = function(node){
|
|
||||||
return node.stylesheet
|
|
||||||
.rules.map(this.visit, this)
|
|
||||||
.join('');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visit comment node.
|
|
||||||
*/
|
|
||||||
|
|
||||||
Compiler.prototype.comment = function(node){
|
|
||||||
return this.emit('', node.position);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visit import node.
|
|
||||||
*/
|
|
||||||
|
|
||||||
Compiler.prototype.import = function(node){
|
|
||||||
return this.emit('@import ' + node.import + ';', node.position);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visit media node.
|
|
||||||
*/
|
|
||||||
|
|
||||||
Compiler.prototype.media = function(node){
|
|
||||||
return this.emit('@media ' + node.media, node.position)
|
|
||||||
+ this.emit('{')
|
|
||||||
+ this.mapVisit(node.rules)
|
|
||||||
+ this.emit('}');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visit document node.
|
|
||||||
*/
|
|
||||||
|
|
||||||
Compiler.prototype.document = function(node){
|
|
||||||
var doc = '@' + (node.vendor || '') + 'document ' + node.document;
|
|
||||||
|
|
||||||
return this.emit(doc, node.position)
|
|
||||||
+ this.emit('{')
|
|
||||||
+ this.mapVisit(node.rules)
|
|
||||||
+ this.emit('}');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visit charset node.
|
|
||||||
*/
|
|
||||||
|
|
||||||
Compiler.prototype.charset = function(node){
|
|
||||||
return this.emit('@charset ' + node.charset + ';', node.position);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visit namespace node.
|
|
||||||
*/
|
|
||||||
|
|
||||||
Compiler.prototype.namespace = function(node){
|
|
||||||
return this.emit('@namespace ' + node.namespace + ';', node.position);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visit supports node.
|
|
||||||
*/
|
|
||||||
|
|
||||||
Compiler.prototype.supports = function(node){
|
|
||||||
return this.emit('@supports ' + node.supports, node.position)
|
|
||||||
+ this.emit('{')
|
|
||||||
+ this.mapVisit(node.rules)
|
|
||||||
+ this.emit('}');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visit keyframes node.
|
|
||||||
*/
|
|
||||||
|
|
||||||
Compiler.prototype.keyframes = function(node){
|
|
||||||
return this.emit('@'
|
|
||||||
+ (node.vendor || '')
|
|
||||||
+ 'keyframes '
|
|
||||||
+ node.name, node.position)
|
|
||||||
+ this.emit('{')
|
|
||||||
+ this.mapVisit(node.keyframes)
|
|
||||||
+ this.emit('}');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visit keyframe node.
|
|
||||||
*/
|
|
||||||
|
|
||||||
Compiler.prototype.keyframe = function(node){
|
|
||||||
var decls = node.declarations;
|
|
||||||
|
|
||||||
return this.emit(node.values.join(','), node.position)
|
|
||||||
+ this.emit('{')
|
|
||||||
+ this.mapVisit(decls)
|
|
||||||
+ this.emit('}');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visit page node.
|
|
||||||
*/
|
|
||||||
|
|
||||||
Compiler.prototype.page = function(node){
|
|
||||||
var sel = node.selectors.length
|
|
||||||
? node.selectors.join(', ')
|
|
||||||
: '';
|
|
||||||
|
|
||||||
return this.emit('@page ' + sel, node.position)
|
|
||||||
+ this.emit('{')
|
|
||||||
+ this.mapVisit(node.declarations)
|
|
||||||
+ this.emit('}');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visit font-face node.
|
|
||||||
*/
|
|
||||||
|
|
||||||
Compiler.prototype['font-face'] = function(node){
|
|
||||||
return this.emit('@font-face', node.position)
|
|
||||||
+ this.emit('{')
|
|
||||||
+ this.mapVisit(node.declarations)
|
|
||||||
+ this.emit('}');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visit host node.
|
|
||||||
*/
|
|
||||||
|
|
||||||
Compiler.prototype.host = function(node){
|
|
||||||
return this.emit('@host', node.position)
|
|
||||||
+ this.emit('{')
|
|
||||||
+ this.mapVisit(node.rules)
|
|
||||||
+ this.emit('}');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visit custom-media node.
|
|
||||||
*/
|
|
||||||
|
|
||||||
Compiler.prototype['custom-media'] = function(node){
|
|
||||||
return this.emit('@custom-media ' + node.name + ' ' + node.media + ';', node.position);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visit rule node.
|
|
||||||
*/
|
|
||||||
|
|
||||||
Compiler.prototype.rule = function(node){
|
|
||||||
var decls = node.declarations;
|
|
||||||
if (!decls.length) return '';
|
|
||||||
|
|
||||||
return this.emit(node.selectors.join(','), node.position)
|
|
||||||
+ this.emit('{')
|
|
||||||
+ this.mapVisit(decls)
|
|
||||||
+ this.emit('}');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visit declaration node.
|
|
||||||
*/
|
|
||||||
|
|
||||||
Compiler.prototype.declaration = function(node){
|
|
||||||
return this.emit(node.property + ':' + node.value, node.position) + this.emit(';');
|
|
||||||
};
|
|
||||||
|
|
@ -1,254 +0,0 @@
|
|||||||
|
|
||||||
/**
|
|
||||||
* Module dependencies.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var Base = require('./compiler');
|
|
||||||
var inherits = require('inherits');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Expose compiler.
|
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports = Compiler;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize a new `Compiler`.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function Compiler(options) {
|
|
||||||
options = options || {};
|
|
||||||
Base.call(this, options);
|
|
||||||
this.indentation = typeof options.indent === 'string' ? options.indent : ' ';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inherit from `Base.prototype`.
|
|
||||||
*/
|
|
||||||
|
|
||||||
inherits(Compiler, Base);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compile `node`.
|
|
||||||
*/
|
|
||||||
|
|
||||||
Compiler.prototype.compile = function(node){
|
|
||||||
return this.stylesheet(node);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visit stylesheet node.
|
|
||||||
*/
|
|
||||||
|
|
||||||
Compiler.prototype.stylesheet = function(node){
|
|
||||||
return this.mapVisit(node.stylesheet.rules, '\n\n');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visit comment node.
|
|
||||||
*/
|
|
||||||
|
|
||||||
Compiler.prototype.comment = function(node){
|
|
||||||
return this.emit(this.indent() + '/*' + node.comment + '*/', node.position);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visit import node.
|
|
||||||
*/
|
|
||||||
|
|
||||||
Compiler.prototype.import = function(node){
|
|
||||||
return this.emit('@import ' + node.import + ';', node.position);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visit media node.
|
|
||||||
*/
|
|
||||||
|
|
||||||
Compiler.prototype.media = function(node){
|
|
||||||
return this.emit('@media ' + node.media, node.position)
|
|
||||||
+ this.emit(
|
|
||||||
' {\n'
|
|
||||||
+ this.indent(1))
|
|
||||||
+ this.mapVisit(node.rules, '\n\n')
|
|
||||||
+ this.emit(
|
|
||||||
this.indent(-1)
|
|
||||||
+ '\n}');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visit document node.
|
|
||||||
*/
|
|
||||||
|
|
||||||
Compiler.prototype.document = function(node){
|
|
||||||
var doc = '@' + (node.vendor || '') + 'document ' + node.document;
|
|
||||||
|
|
||||||
return this.emit(doc, node.position)
|
|
||||||
+ this.emit(
|
|
||||||
' '
|
|
||||||
+ ' {\n'
|
|
||||||
+ this.indent(1))
|
|
||||||
+ this.mapVisit(node.rules, '\n\n')
|
|
||||||
+ this.emit(
|
|
||||||
this.indent(-1)
|
|
||||||
+ '\n}');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visit charset node.
|
|
||||||
*/
|
|
||||||
|
|
||||||
Compiler.prototype.charset = function(node){
|
|
||||||
return this.emit('@charset ' + node.charset + ';', node.position);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visit namespace node.
|
|
||||||
*/
|
|
||||||
|
|
||||||
Compiler.prototype.namespace = function(node){
|
|
||||||
return this.emit('@namespace ' + node.namespace + ';', node.position);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visit supports node.
|
|
||||||
*/
|
|
||||||
|
|
||||||
Compiler.prototype.supports = function(node){
|
|
||||||
return this.emit('@supports ' + node.supports, node.position)
|
|
||||||
+ this.emit(
|
|
||||||
' {\n'
|
|
||||||
+ this.indent(1))
|
|
||||||
+ this.mapVisit(node.rules, '\n\n')
|
|
||||||
+ this.emit(
|
|
||||||
this.indent(-1)
|
|
||||||
+ '\n}');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visit keyframes node.
|
|
||||||
*/
|
|
||||||
|
|
||||||
Compiler.prototype.keyframes = function(node){
|
|
||||||
return this.emit('@' + (node.vendor || '') + 'keyframes ' + node.name, node.position)
|
|
||||||
+ this.emit(
|
|
||||||
' {\n'
|
|
||||||
+ this.indent(1))
|
|
||||||
+ this.mapVisit(node.keyframes, '\n')
|
|
||||||
+ this.emit(
|
|
||||||
this.indent(-1)
|
|
||||||
+ '}');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visit keyframe node.
|
|
||||||
*/
|
|
||||||
|
|
||||||
Compiler.prototype.keyframe = function(node){
|
|
||||||
var decls = node.declarations;
|
|
||||||
|
|
||||||
return this.emit(this.indent())
|
|
||||||
+ this.emit(node.values.join(', '), node.position)
|
|
||||||
+ this.emit(
|
|
||||||
' {\n'
|
|
||||||
+ this.indent(1))
|
|
||||||
+ this.mapVisit(decls, '\n')
|
|
||||||
+ this.emit(
|
|
||||||
this.indent(-1)
|
|
||||||
+ '\n'
|
|
||||||
+ this.indent() + '}\n');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visit page node.
|
|
||||||
*/
|
|
||||||
|
|
||||||
Compiler.prototype.page = function(node){
|
|
||||||
var sel = node.selectors.length
|
|
||||||
? node.selectors.join(', ') + ' '
|
|
||||||
: '';
|
|
||||||
|
|
||||||
return this.emit('@page ' + sel, node.position)
|
|
||||||
+ this.emit('{\n')
|
|
||||||
+ this.emit(this.indent(1))
|
|
||||||
+ this.mapVisit(node.declarations, '\n')
|
|
||||||
+ this.emit(this.indent(-1))
|
|
||||||
+ this.emit('\n}');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visit font-face node.
|
|
||||||
*/
|
|
||||||
|
|
||||||
Compiler.prototype['font-face'] = function(node){
|
|
||||||
return this.emit('@font-face ', node.position)
|
|
||||||
+ this.emit('{\n')
|
|
||||||
+ this.emit(this.indent(1))
|
|
||||||
+ this.mapVisit(node.declarations, '\n')
|
|
||||||
+ this.emit(this.indent(-1))
|
|
||||||
+ this.emit('\n}');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visit host node.
|
|
||||||
*/
|
|
||||||
|
|
||||||
Compiler.prototype.host = function(node){
|
|
||||||
return this.emit('@host', node.position)
|
|
||||||
+ this.emit(
|
|
||||||
' {\n'
|
|
||||||
+ this.indent(1))
|
|
||||||
+ this.mapVisit(node.rules, '\n\n')
|
|
||||||
+ this.emit(
|
|
||||||
this.indent(-1)
|
|
||||||
+ '\n}');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visit custom-media node.
|
|
||||||
*/
|
|
||||||
|
|
||||||
Compiler.prototype['custom-media'] = function(node){
|
|
||||||
return this.emit('@custom-media ' + node.name + ' ' + node.media + ';', node.position);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visit rule node.
|
|
||||||
*/
|
|
||||||
|
|
||||||
Compiler.prototype.rule = function(node){
|
|
||||||
var indent = this.indent();
|
|
||||||
var decls = node.declarations;
|
|
||||||
if (!decls.length) return '';
|
|
||||||
|
|
||||||
return this.emit(node.selectors.map(function(s){ return indent + s }).join(',\n'), node.position)
|
|
||||||
+ this.emit(' {\n')
|
|
||||||
+ this.emit(this.indent(1))
|
|
||||||
+ this.mapVisit(decls, '\n')
|
|
||||||
+ this.emit(this.indent(-1))
|
|
||||||
+ this.emit('\n' + this.indent() + '}');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visit declaration node.
|
|
||||||
*/
|
|
||||||
|
|
||||||
Compiler.prototype.declaration = function(node){
|
|
||||||
return this.emit(this.indent())
|
|
||||||
+ this.emit(node.property + ': ' + node.value, node.position)
|
|
||||||
+ this.emit(';');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Increase, decrease or return current indentation.
|
|
||||||
*/
|
|
||||||
|
|
||||||
Compiler.prototype.indent = function(level) {
|
|
||||||
this.level = this.level || 1;
|
|
||||||
|
|
||||||
if (null != level) {
|
|
||||||
this.level += level;
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array(this.level).join(this.indentation);
|
|
||||||
};
|
|
@ -1,47 +0,0 @@
|
|||||||
|
|
||||||
/**
|
|
||||||
* Module dependencies.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var Compressed = require('./compress');
|
|
||||||
var Identity = require('./identity');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stringfy the given AST `node`.
|
|
||||||
*
|
|
||||||
* Options:
|
|
||||||
*
|
|
||||||
* - `compress` space-optimized output
|
|
||||||
* - `sourcemap` return an object with `.code` and `.map`
|
|
||||||
*
|
|
||||||
* @param {Object} node
|
|
||||||
* @param {Object} [options]
|
|
||||||
* @return {String}
|
|
||||||
* @api public
|
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports = function(node, options){
|
|
||||||
options = options || {};
|
|
||||||
|
|
||||||
var compiler = options.compress
|
|
||||||
? new Compressed(options)
|
|
||||||
: new Identity(options);
|
|
||||||
|
|
||||||
// source maps
|
|
||||||
if (options.sourcemap) {
|
|
||||||
var sourcemaps = require('./source-map-support');
|
|
||||||
sourcemaps(compiler);
|
|
||||||
|
|
||||||
var code = compiler.compile(node);
|
|
||||||
compiler.applySourceMaps();
|
|
||||||
|
|
||||||
var map = options.sourcemap === 'generator'
|
|
||||||
? compiler.map
|
|
||||||
: compiler.map.toJSON();
|
|
||||||
|
|
||||||
return { code: code, map: map };
|
|
||||||
}
|
|
||||||
|
|
||||||
var code = compiler.compile(node);
|
|
||||||
return code;
|
|
||||||
};
|
|
@ -1,133 +0,0 @@
|
|||||||
|
|
||||||
/**
|
|
||||||
* Module dependencies.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var SourceMap = require('source-map').SourceMapGenerator;
|
|
||||||
var SourceMapConsumer = require('source-map').SourceMapConsumer;
|
|
||||||
var sourceMapResolve = require('source-map-resolve');
|
|
||||||
var fs = require('fs');
|
|
||||||
var path = require('path');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Expose `mixin()`.
|
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports = mixin;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure Windows-style paths are formatted properly
|
|
||||||
*/
|
|
||||||
|
|
||||||
const makeFriendlyPath = function(aPath) {
|
|
||||||
return path.sep === "\\" ? aPath.replace(/\\/g, "/").replace(/^[a-z]:\/?/i, "/") : aPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mixin source map support into `compiler`.
|
|
||||||
*
|
|
||||||
* @param {Compiler} compiler
|
|
||||||
* @api public
|
|
||||||
*/
|
|
||||||
|
|
||||||
function mixin(compiler) {
|
|
||||||
compiler._comment = compiler.comment;
|
|
||||||
compiler.map = new SourceMap();
|
|
||||||
compiler.position = { line: 1, column: 1 };
|
|
||||||
compiler.files = {};
|
|
||||||
for (var k in exports) compiler[k] = exports[k];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update position.
|
|
||||||
*
|
|
||||||
* @param {String} str
|
|
||||||
* @api private
|
|
||||||
*/
|
|
||||||
|
|
||||||
exports.updatePosition = function(str) {
|
|
||||||
var lines = str.match(/\n/g);
|
|
||||||
if (lines) this.position.line += lines.length;
|
|
||||||
var i = str.lastIndexOf('\n');
|
|
||||||
this.position.column = ~i ? str.length - i : this.position.column + str.length;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emit `str`.
|
|
||||||
*
|
|
||||||
* @param {String} str
|
|
||||||
* @param {Object} [pos]
|
|
||||||
* @return {String}
|
|
||||||
* @api private
|
|
||||||
*/
|
|
||||||
|
|
||||||
exports.emit = function(str, pos) {
|
|
||||||
if (pos) {
|
|
||||||
var sourceFile = makeFriendlyPath(pos.source || 'source.css');
|
|
||||||
|
|
||||||
this.map.addMapping({
|
|
||||||
source: sourceFile,
|
|
||||||
generated: {
|
|
||||||
line: this.position.line,
|
|
||||||
column: Math.max(this.position.column - 1, 0)
|
|
||||||
},
|
|
||||||
original: {
|
|
||||||
line: pos.start.line,
|
|
||||||
column: pos.start.column - 1
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.addFile(sourceFile, pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updatePosition(str);
|
|
||||||
|
|
||||||
return str;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a file to the source map output if it has not already been added
|
|
||||||
* @param {String} file
|
|
||||||
* @param {Object} pos
|
|
||||||
*/
|
|
||||||
|
|
||||||
exports.addFile = function(file, pos) {
|
|
||||||
if (typeof pos.content !== 'string') return;
|
|
||||||
if (Object.prototype.hasOwnProperty.call(this.files, file)) return;
|
|
||||||
|
|
||||||
this.files[file] = pos.content;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies any original source maps to the output and embeds the source file
|
|
||||||
* contents in the source map.
|
|
||||||
*/
|
|
||||||
|
|
||||||
exports.applySourceMaps = function() {
|
|
||||||
Object.keys(this.files).forEach(function(file) {
|
|
||||||
var content = this.files[file];
|
|
||||||
this.map.setSourceContent(file, content);
|
|
||||||
|
|
||||||
if (this.options.inputSourcemaps !== false) {
|
|
||||||
var originalMap = sourceMapResolve.resolveSync(
|
|
||||||
content, file, fs.readFileSync);
|
|
||||||
if (originalMap) {
|
|
||||||
var map = new SourceMapConsumer(originalMap.map);
|
|
||||||
var relativeTo = originalMap.sourcesRelativeTo;
|
|
||||||
this.map.applySourceMap(map, file, makeFriendlyPath(path.dirname(relativeTo)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, this);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process comments, drops sourceMap comments.
|
|
||||||
* @param {Object} node
|
|
||||||
*/
|
|
||||||
|
|
||||||
exports.comment = function(node) {
|
|
||||||
if (/^# sourceMappingURL=/.test(node.comment))
|
|
||||||
return this.emit('', node.position);
|
|
||||||
else
|
|
||||||
return this._comment(node);
|
|
||||||
};
|
|
@ -4,14 +4,13 @@ const fs = require('../libs/fsExtra')
|
|||||||
|
|
||||||
const workerThreads = require('worker_threads')
|
const workerThreads = require('worker_threads')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
const TaskManager = require('./TaskManager')
|
||||||
const Task = require('../objects/Task')
|
const Task = require('../objects/Task')
|
||||||
const { writeConcatFile } = require('../utils/ffmpegHelpers')
|
const { writeConcatFile } = require('../utils/ffmpegHelpers')
|
||||||
const toneHelpers = require('../utils/toneHelpers')
|
const toneHelpers = require('../utils/toneHelpers')
|
||||||
|
|
||||||
class AbMergeManager {
|
class AbMergeManager {
|
||||||
constructor(taskManager) {
|
constructor() {
|
||||||
this.taskManager = taskManager
|
|
||||||
|
|
||||||
this.itemsCacheDir = Path.join(global.MetadataPath, 'cache/items')
|
this.itemsCacheDir = Path.join(global.MetadataPath, 'cache/items')
|
||||||
|
|
||||||
this.pendingTasks = []
|
this.pendingTasks = []
|
||||||
@ -45,7 +44,7 @@ class AbMergeManager {
|
|||||||
}
|
}
|
||||||
const taskDescription = `Encoding audiobook "${libraryItem.media.metadata.title}" into a single m4b file.`
|
const taskDescription = `Encoding audiobook "${libraryItem.media.metadata.title}" into a single m4b file.`
|
||||||
task.setData('encode-m4b', 'Encoding M4b', taskDescription, false, taskData)
|
task.setData('encode-m4b', 'Encoding M4b', taskDescription, false, taskData)
|
||||||
this.taskManager.addTask(task)
|
TaskManager.addTask(task)
|
||||||
Logger.info(`Start m4b encode for ${libraryItem.id} - TaskId: ${task.id}`)
|
Logger.info(`Start m4b encode for ${libraryItem.id} - TaskId: ${task.id}`)
|
||||||
|
|
||||||
if (!await fs.pathExists(taskData.itemCachePath)) {
|
if (!await fs.pathExists(taskData.itemCachePath)) {
|
||||||
@ -234,7 +233,7 @@ class AbMergeManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.taskManager.taskFinished(task)
|
TaskManager.taskFinished(task)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = AbMergeManager
|
module.exports = AbMergeManager
|
||||||
|
@ -7,12 +7,12 @@ const fs = require('../libs/fsExtra')
|
|||||||
|
|
||||||
const toneHelpers = require('../utils/toneHelpers')
|
const toneHelpers = require('../utils/toneHelpers')
|
||||||
|
|
||||||
|
const TaskManager = require('./TaskManager')
|
||||||
|
|
||||||
const Task = require('../objects/Task')
|
const Task = require('../objects/Task')
|
||||||
|
|
||||||
class AudioMetadataMangaer {
|
class AudioMetadataMangaer {
|
||||||
constructor(taskManager) {
|
constructor() {
|
||||||
this.taskManager = taskManager
|
|
||||||
|
|
||||||
this.itemsCacheDir = Path.join(global.MetadataPath, 'cache/items')
|
this.itemsCacheDir = Path.join(global.MetadataPath, 'cache/items')
|
||||||
|
|
||||||
this.MAX_CONCURRENT_TASKS = 1
|
this.MAX_CONCURRENT_TASKS = 1
|
||||||
@ -101,7 +101,7 @@ class AudioMetadataMangaer {
|
|||||||
|
|
||||||
async runMetadataEmbed(task) {
|
async runMetadataEmbed(task) {
|
||||||
this.tasksRunning.push(task)
|
this.tasksRunning.push(task)
|
||||||
this.taskManager.addTask(task)
|
TaskManager.addTask(task)
|
||||||
|
|
||||||
Logger.info(`[AudioMetadataManager] Starting metadata embed task`, task.description)
|
Logger.info(`[AudioMetadataManager] Starting metadata embed task`, task.description)
|
||||||
|
|
||||||
@ -176,7 +176,7 @@ class AudioMetadataMangaer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleTaskFinished(task) {
|
handleTaskFinished(task) {
|
||||||
this.taskManager.taskFinished(task)
|
TaskManager.taskFinished(task)
|
||||||
this.tasksRunning = this.tasksRunning.filter(t => t.id !== task.id)
|
this.tasksRunning = this.tasksRunning.filter(t => t.id !== task.id)
|
||||||
|
|
||||||
if (this.tasksRunning.length < this.MAX_CONCURRENT_TASKS && this.tasksQueued.length) {
|
if (this.tasksRunning.length < this.MAX_CONCURRENT_TASKS && this.tasksQueued.length) {
|
||||||
|
@ -5,7 +5,7 @@ const readChunk = require('../libs/readChunk')
|
|||||||
const imageType = require('../libs/imageType')
|
const imageType = require('../libs/imageType')
|
||||||
|
|
||||||
const globals = require('../utils/globals')
|
const globals = require('../utils/globals')
|
||||||
const { downloadFile, filePathToPOSIX, checkPathIsFile } = require('../utils/fileUtils')
|
const { downloadImageFile, filePathToPOSIX, checkPathIsFile } = require('../utils/fileUtils')
|
||||||
const { extractCoverArt } = require('../utils/ffmpegHelpers')
|
const { extractCoverArt } = require('../utils/ffmpegHelpers')
|
||||||
const CacheManager = require('../managers/CacheManager')
|
const CacheManager = require('../managers/CacheManager')
|
||||||
|
|
||||||
@ -120,13 +120,16 @@ class CoverManager {
|
|||||||
await fs.ensureDir(coverDirPath)
|
await fs.ensureDir(coverDirPath)
|
||||||
|
|
||||||
var temppath = Path.posix.join(coverDirPath, 'cover')
|
var temppath = Path.posix.join(coverDirPath, 'cover')
|
||||||
var success = await downloadFile(url, temppath).then(() => true).catch((err) => {
|
|
||||||
Logger.error(`[CoverManager] Download image file failed for "${url}"`, err)
|
let errorMsg = ''
|
||||||
|
let success = await downloadImageFile(url, temppath).then(() => true).catch((err) => {
|
||||||
|
errorMsg = err.message || 'Unknown error'
|
||||||
|
Logger.error(`[CoverManager] Download image file failed for "${url}"`, errorMsg)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
if (!success) {
|
if (!success) {
|
||||||
return {
|
return {
|
||||||
error: 'Failed to download image from url'
|
error: 'Failed to download image from url: ' + errorMsg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -284,7 +287,7 @@ class CoverManager {
|
|||||||
await fs.ensureDir(coverDirPath)
|
await fs.ensureDir(coverDirPath)
|
||||||
|
|
||||||
const temppath = Path.posix.join(coverDirPath, 'cover')
|
const temppath = Path.posix.join(coverDirPath, 'cover')
|
||||||
const success = await downloadFile(url, temppath).then(() => true).catch((err) => {
|
const success = await downloadImageFile(url, temppath).then(() => true).catch((err) => {
|
||||||
Logger.error(`[CoverManager] Download image file failed for "${url}"`, err)
|
Logger.error(`[CoverManager] Download image file failed for "${url}"`, err)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
@ -330,14 +330,15 @@ class PlaybackSessionManager {
|
|||||||
Logger.debug(`[PlaybackSessionManager] Removed session "${sessionId}"`)
|
Logger.debug(`[PlaybackSessionManager] Removed session "${sessionId}"`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for streams that are not in memory and remove
|
/**
|
||||||
|
* Remove all stream folders in `/metadata/streams`
|
||||||
|
*/
|
||||||
async removeOrphanStreams() {
|
async removeOrphanStreams() {
|
||||||
await fs.ensureDir(this.StreamsPath)
|
await fs.ensureDir(this.StreamsPath)
|
||||||
try {
|
try {
|
||||||
const streamsInPath = await fs.readdir(this.StreamsPath)
|
const streamsInPath = await fs.readdir(this.StreamsPath)
|
||||||
for (let i = 0; i < streamsInPath.length; i++) {
|
for (const streamId of streamsInPath) {
|
||||||
const streamId = streamsInPath[i]
|
if (/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/.test(streamId)) { // Ensure is uuidv4
|
||||||
if (streamId.startsWith('play_')) { // Make sure to only remove folders that are a stream
|
|
||||||
const session = this.sessions.find(se => se.id === streamId)
|
const session = this.sessions.find(se => se.id === streamId)
|
||||||
if (!session) {
|
if (!session) {
|
||||||
const streamPath = Path.join(this.StreamsPath, streamId)
|
const streamPath = Path.join(this.StreamsPath, streamId)
|
||||||
|
@ -12,17 +12,17 @@ const opmlGenerator = require('../utils/generators/opmlGenerator')
|
|||||||
const prober = require('../utils/prober')
|
const prober = require('../utils/prober')
|
||||||
const ffmpegHelpers = require('../utils/ffmpegHelpers')
|
const ffmpegHelpers = require('../utils/ffmpegHelpers')
|
||||||
|
|
||||||
|
const TaskManager = require('./TaskManager')
|
||||||
|
|
||||||
const LibraryFile = require('../objects/files/LibraryFile')
|
const LibraryFile = require('../objects/files/LibraryFile')
|
||||||
const PodcastEpisodeDownload = require('../objects/PodcastEpisodeDownload')
|
const PodcastEpisodeDownload = require('../objects/PodcastEpisodeDownload')
|
||||||
const PodcastEpisode = require('../objects/entities/PodcastEpisode')
|
const PodcastEpisode = require('../objects/entities/PodcastEpisode')
|
||||||
const AudioFile = require('../objects/files/AudioFile')
|
const AudioFile = require('../objects/files/AudioFile')
|
||||||
const Task = require("../objects/Task")
|
|
||||||
|
|
||||||
class PodcastManager {
|
class PodcastManager {
|
||||||
constructor(watcher, notificationManager, taskManager) {
|
constructor(watcher, notificationManager) {
|
||||||
this.watcher = watcher
|
this.watcher = watcher
|
||||||
this.notificationManager = notificationManager
|
this.notificationManager = notificationManager
|
||||||
this.taskManager = taskManager
|
|
||||||
|
|
||||||
this.downloadQueue = []
|
this.downloadQueue = []
|
||||||
this.currentDownload = null
|
this.currentDownload = null
|
||||||
@ -69,14 +69,12 @@ class PodcastManager {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const task = new Task()
|
|
||||||
const taskDescription = `Downloading episode "${podcastEpisodeDownload.podcastEpisode.title}".`
|
const taskDescription = `Downloading episode "${podcastEpisodeDownload.podcastEpisode.title}".`
|
||||||
const taskData = {
|
const taskData = {
|
||||||
libraryId: podcastEpisodeDownload.libraryId,
|
libraryId: podcastEpisodeDownload.libraryId,
|
||||||
libraryItemId: podcastEpisodeDownload.libraryItemId,
|
libraryItemId: podcastEpisodeDownload.libraryItemId,
|
||||||
}
|
}
|
||||||
task.setData('download-podcast-episode', 'Downloading Episode', taskDescription, false, taskData)
|
const task = TaskManager.createAndAddTask('download-podcast-episode', 'Downloading Episode', taskDescription, false, taskData)
|
||||||
this.taskManager.addTask(task)
|
|
||||||
|
|
||||||
SocketAuthority.emitter('episode_download_started', podcastEpisodeDownload.toJSONForClient())
|
SocketAuthority.emitter('episode_download_started', podcastEpisodeDownload.toJSONForClient())
|
||||||
this.currentDownload = podcastEpisodeDownload
|
this.currentDownload = podcastEpisodeDownload
|
||||||
@ -128,7 +126,7 @@ class PodcastManager {
|
|||||||
this.currentDownload.setFinished(false)
|
this.currentDownload.setFinished(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.taskManager.taskFinished(task)
|
TaskManager.taskFinished(task)
|
||||||
|
|
||||||
SocketAuthority.emitter('episode_download_finished', this.currentDownload.toJSONForClient())
|
SocketAuthority.emitter('episode_download_finished', this.currentDownload.toJSONForClient())
|
||||||
SocketAuthority.emitter('episode_download_queue_updated', this.getDownloadQueueDetails())
|
SocketAuthority.emitter('episode_download_queue_updated', this.getDownloadQueueDetails())
|
||||||
@ -201,7 +199,7 @@ class PodcastManager {
|
|||||||
})
|
})
|
||||||
// TODO: Should we check for open playback sessions for this episode?
|
// TODO: Should we check for open playback sessions for this episode?
|
||||||
// TODO: remove all user progress for this episode
|
// TODO: remove all user progress for this episode
|
||||||
if (oldestEpisode && oldestEpisode.audioFile) {
|
if (oldestEpisode?.audioFile) {
|
||||||
Logger.info(`[PodcastManager] Deleting oldest episode "${oldestEpisode.title}"`)
|
Logger.info(`[PodcastManager] Deleting oldest episode "${oldestEpisode.title}"`)
|
||||||
const successfullyDeleted = await removeFile(oldestEpisode.audioFile.metadata.path)
|
const successfullyDeleted = await removeFile(oldestEpisode.audioFile.metadata.path)
|
||||||
if (successfullyDeleted) {
|
if (successfullyDeleted) {
|
||||||
@ -246,7 +244,7 @@ class PodcastManager {
|
|||||||
Logger.debug(`[PodcastManager] runEpisodeCheck: "${libraryItem.media.metadata.title}" checking for episodes after ${new Date(dateToCheckForEpisodesAfter)}`)
|
Logger.debug(`[PodcastManager] runEpisodeCheck: "${libraryItem.media.metadata.title}" checking for episodes after ${new Date(dateToCheckForEpisodesAfter)}`)
|
||||||
|
|
||||||
var newEpisodes = await this.checkPodcastForNewEpisodes(libraryItem, dateToCheckForEpisodesAfter, libraryItem.media.maxNewEpisodesToDownload)
|
var newEpisodes = await this.checkPodcastForNewEpisodes(libraryItem, dateToCheckForEpisodesAfter, libraryItem.media.maxNewEpisodesToDownload)
|
||||||
Logger.debug(`[PodcastManager] runEpisodeCheck: ${newEpisodes ? newEpisodes.length : 'N/A'} episodes found`)
|
Logger.debug(`[PodcastManager] runEpisodeCheck: ${newEpisodes?.length || 'N/A'} episodes found`)
|
||||||
|
|
||||||
if (!newEpisodes) { // Failed
|
if (!newEpisodes) { // Failed
|
||||||
// Allow up to MaxFailedEpisodeChecks failed attempts before disabling auto download
|
// Allow up to MaxFailedEpisodeChecks failed attempts before disabling auto download
|
||||||
@ -280,14 +278,14 @@ class PodcastManager {
|
|||||||
Logger.error(`[PodcastManager] checkPodcastForNewEpisodes no feed url for ${podcastLibraryItem.media.metadata.title} (ID: ${podcastLibraryItem.id})`)
|
Logger.error(`[PodcastManager] checkPodcastForNewEpisodes no feed url for ${podcastLibraryItem.media.metadata.title} (ID: ${podcastLibraryItem.id})`)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
var feed = await getPodcastFeed(podcastLibraryItem.media.metadata.feedUrl)
|
const feed = await getPodcastFeed(podcastLibraryItem.media.metadata.feedUrl)
|
||||||
if (!feed || !feed.episodes) {
|
if (!feed?.episodes) {
|
||||||
Logger.error(`[PodcastManager] checkPodcastForNewEpisodes invalid feed payload for ${podcastLibraryItem.media.metadata.title} (ID: ${podcastLibraryItem.id})`, feed)
|
Logger.error(`[PodcastManager] checkPodcastForNewEpisodes invalid feed payload for ${podcastLibraryItem.media.metadata.title} (ID: ${podcastLibraryItem.id})`, feed)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter new and not already has
|
// Filter new and not already has
|
||||||
var newEpisodes = feed.episodes.filter(ep => ep.publishedAt > dateToCheckForEpisodesAfter && !podcastLibraryItem.media.checkHasEpisodeByFeedUrl(ep.enclosure.url))
|
let newEpisodes = feed.episodes.filter(ep => ep.publishedAt > dateToCheckForEpisodesAfter && !podcastLibraryItem.media.checkHasEpisodeByFeedUrl(ep.enclosure.url))
|
||||||
|
|
||||||
if (maxNewEpisodes > 0) {
|
if (maxNewEpisodes > 0) {
|
||||||
newEpisodes = newEpisodes.slice(0, maxNewEpisodes)
|
newEpisodes = newEpisodes.slice(0, maxNewEpisodes)
|
||||||
|
@ -1,20 +1,48 @@
|
|||||||
const SocketAuthority = require('../SocketAuthority')
|
const SocketAuthority = require('../SocketAuthority')
|
||||||
|
const Task = require('../objects/Task')
|
||||||
|
|
||||||
class TaskManager {
|
class TaskManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
/** @type {Task[]} */
|
||||||
this.tasks = []
|
this.tasks = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add task and emit socket task_started event
|
||||||
|
*
|
||||||
|
* @param {Task} task
|
||||||
|
*/
|
||||||
addTask(task) {
|
addTask(task) {
|
||||||
this.tasks.push(task)
|
this.tasks.push(task)
|
||||||
SocketAuthority.emitter('task_started', task.toJSON())
|
SocketAuthority.emitter('task_started', task.toJSON())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove task and emit task_finished event
|
||||||
|
*
|
||||||
|
* @param {Task} task
|
||||||
|
*/
|
||||||
taskFinished(task) {
|
taskFinished(task) {
|
||||||
if (this.tasks.some(t => t.id === task.id)) {
|
if (this.tasks.some(t => t.id === task.id)) {
|
||||||
this.tasks = this.tasks.filter(t => t.id !== task.id)
|
this.tasks = this.tasks.filter(t => t.id !== task.id)
|
||||||
SocketAuthority.emitter('task_finished', task.toJSON())
|
SocketAuthority.emitter('task_finished', task.toJSON())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new task and add
|
||||||
|
*
|
||||||
|
* @param {string} action
|
||||||
|
* @param {string} title
|
||||||
|
* @param {string} description
|
||||||
|
* @param {boolean} showSuccess
|
||||||
|
* @param {Object} [data]
|
||||||
|
*/
|
||||||
|
createAndAddTask(action, title, description, showSuccess, data = {}) {
|
||||||
|
const task = new Task()
|
||||||
|
task.setData(action, title, description, showSuccess, data)
|
||||||
|
this.addTask(task)
|
||||||
|
return task
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = TaskManager
|
module.exports = new TaskManager()
|
@ -211,6 +211,32 @@ class Book extends Model {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAbsMetadataJson() {
|
||||||
|
return {
|
||||||
|
tags: this.tags || [],
|
||||||
|
chapters: this.chapters?.map(c => ({ ...c })) || [],
|
||||||
|
title: this.title,
|
||||||
|
subtitle: this.subtitle,
|
||||||
|
authors: this.authors.map(a => a.name),
|
||||||
|
narrators: this.narrators,
|
||||||
|
series: this.series.map(se => {
|
||||||
|
const sequence = se.bookSeries?.sequence || ''
|
||||||
|
if (!sequence) return se.name
|
||||||
|
return `${se.name} #${sequence}`
|
||||||
|
}),
|
||||||
|
genres: this.genres || [],
|
||||||
|
publishedYear: this.publishedYear,
|
||||||
|
publishedDate: this.publishedDate,
|
||||||
|
publisher: this.publisher,
|
||||||
|
description: this.description,
|
||||||
|
isbn: this.isbn,
|
||||||
|
asin: this.asin,
|
||||||
|
language: this.language,
|
||||||
|
explicit: !!this.explicit,
|
||||||
|
abridged: !!this.abridged
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize model
|
* Initialize model
|
||||||
* @param {import('../Database').sequelize} sequelize
|
* @param {import('../Database').sequelize} sequelize
|
||||||
|
@ -11,6 +11,7 @@ const oldLibrary = require('../objects/Library')
|
|||||||
* @property {string} autoScanCronExpression
|
* @property {string} autoScanCronExpression
|
||||||
* @property {boolean} audiobooksOnly
|
* @property {boolean} audiobooksOnly
|
||||||
* @property {boolean} hideSingleBookSeries Do not show series that only have 1 book
|
* @property {boolean} hideSingleBookSeries Do not show series that only have 1 book
|
||||||
|
* @property {string[]} metadataPrecedence
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class Library extends Model {
|
class Library extends Model {
|
||||||
@ -79,6 +80,9 @@ class Library extends Model {
|
|||||||
mediaType: libraryExpanded.mediaType,
|
mediaType: libraryExpanded.mediaType,
|
||||||
provider: libraryExpanded.provider,
|
provider: libraryExpanded.provider,
|
||||||
settings: libraryExpanded.settings,
|
settings: libraryExpanded.settings,
|
||||||
|
lastScan: libraryExpanded.lastScan?.valueOf() || null,
|
||||||
|
lastScanVersion: libraryExpanded.lastScanVersion || null,
|
||||||
|
lastScanMetadataPrecedence: libraryExpanded.extraData?.lastScanMetadataPrecedence || null,
|
||||||
createdAt: libraryExpanded.createdAt.valueOf(),
|
createdAt: libraryExpanded.createdAt.valueOf(),
|
||||||
lastUpdate: libraryExpanded.updatedAt.valueOf()
|
lastUpdate: libraryExpanded.updatedAt.valueOf()
|
||||||
})
|
})
|
||||||
@ -151,6 +155,9 @@ class Library extends Model {
|
|||||||
if (oldLibrary.oldLibraryId) {
|
if (oldLibrary.oldLibraryId) {
|
||||||
extraData.oldLibraryId = oldLibrary.oldLibraryId
|
extraData.oldLibraryId = oldLibrary.oldLibraryId
|
||||||
}
|
}
|
||||||
|
if (oldLibrary.lastScanMetadataPrecedence) {
|
||||||
|
extraData.lastScanMetadataPrecedence = oldLibrary.lastScanMetadataPrecedence
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
id: oldLibrary.id,
|
id: oldLibrary.id,
|
||||||
name: oldLibrary.name,
|
name: oldLibrary.name,
|
||||||
@ -159,6 +166,8 @@ class Library extends Model {
|
|||||||
mediaType: oldLibrary.mediaType || null,
|
mediaType: oldLibrary.mediaType || null,
|
||||||
provider: oldLibrary.provider,
|
provider: oldLibrary.provider,
|
||||||
settings: oldLibrary.settings?.toJSON() || {},
|
settings: oldLibrary.settings?.toJSON() || {},
|
||||||
|
lastScan: oldLibrary.lastScan || null,
|
||||||
|
lastScanVersion: oldLibrary.lastScanVersion || null,
|
||||||
createdAt: oldLibrary.createdAt,
|
createdAt: oldLibrary.createdAt,
|
||||||
updatedAt: oldLibrary.lastUpdate,
|
updatedAt: oldLibrary.lastUpdate,
|
||||||
extraData
|
extraData
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user