mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-01-03 00:06:46 +01:00
Merge commit '5286b533347477ae47d4e56f086285019e8095c1' into feature/nuxt-target-server
This commit is contained in:
commit
883cb46481
@ -28,6 +28,9 @@
|
|||||||
<widgets-authors-slider v-else-if="shelf.type === 'authors'" :key="index + '.'" :items="shelf.entities" :height="192 * sizeMultiplier" class="bookshelf-row pl-8 my-6">
|
<widgets-authors-slider v-else-if="shelf.type === 'authors'" :key="index + '.'" :items="shelf.entities" :height="192 * sizeMultiplier" class="bookshelf-row pl-8 my-6">
|
||||||
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ $strings[shelf.labelStringKey] }}</p>
|
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ $strings[shelf.labelStringKey] }}</p>
|
||||||
</widgets-authors-slider>
|
</widgets-authors-slider>
|
||||||
|
<widgets-narrators-slider v-else-if="shelf.type === 'narrators'" :key="index + '.'" :items="shelf.entities" :height="100 * sizeMultiplier" class="bookshelf-row pl-8 my-6">
|
||||||
|
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ $strings[shelf.labelStringKey] }}</p>
|
||||||
|
</widgets-narrators-slider>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<!-- Regular bookshelf view -->
|
<!-- Regular bookshelf view -->
|
||||||
@ -185,8 +188,8 @@ export default {
|
|||||||
this.shelves = categories
|
this.shelves = categories
|
||||||
},
|
},
|
||||||
async setShelvesFromSearch() {
|
async setShelvesFromSearch() {
|
||||||
var shelves = []
|
const shelves = []
|
||||||
if (this.results.books && this.results.books.length) {
|
if (this.results.books?.length) {
|
||||||
shelves.push({
|
shelves.push({
|
||||||
id: 'books',
|
id: 'books',
|
||||||
label: 'Books',
|
label: 'Books',
|
||||||
@ -196,7 +199,7 @@ export default {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.results.podcasts && this.results.podcasts.length) {
|
if (this.results.podcasts?.length) {
|
||||||
shelves.push({
|
shelves.push({
|
||||||
id: 'podcasts',
|
id: 'podcasts',
|
||||||
label: 'Podcasts',
|
label: 'Podcasts',
|
||||||
@ -206,7 +209,7 @@ export default {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.results.series && this.results.series.length) {
|
if (this.results.series?.length) {
|
||||||
shelves.push({
|
shelves.push({
|
||||||
id: 'series',
|
id: 'series',
|
||||||
label: 'Series',
|
label: 'Series',
|
||||||
@ -221,7 +224,7 @@ export default {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (this.results.tags && this.results.tags.length) {
|
if (this.results.tags?.length) {
|
||||||
shelves.push({
|
shelves.push({
|
||||||
id: 'tags',
|
id: 'tags',
|
||||||
label: 'Tags',
|
label: 'Tags',
|
||||||
@ -236,7 +239,7 @@ export default {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (this.results.authors && this.results.authors.length) {
|
if (this.results.authors?.length) {
|
||||||
shelves.push({
|
shelves.push({
|
||||||
id: 'authors',
|
id: 'authors',
|
||||||
label: 'Authors',
|
label: 'Authors',
|
||||||
@ -250,6 +253,20 @@ export default {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if (this.results.narrators?.length) {
|
||||||
|
shelves.push({
|
||||||
|
id: 'narrators',
|
||||||
|
label: 'Narrators',
|
||||||
|
labelStringKey: 'LabelNarrators',
|
||||||
|
type: 'narrators',
|
||||||
|
entities: this.results.narrators.map((n) => {
|
||||||
|
return {
|
||||||
|
...n,
|
||||||
|
type: 'narrator'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
this.shelves = shelves
|
this.shelves = shelves
|
||||||
},
|
},
|
||||||
scan() {
|
scan() {
|
||||||
|
@ -41,6 +41,11 @@
|
|||||||
<cards-author-card :key="entity.id" :width="bookCoverWidth / 1.25" :height="bookCoverWidth" :author="entity" :size-multiplier="sizeMultiplier" @hook:updated="updatedBookCard" class="pb-6 mx-2" @edit="editAuthor" />
|
<cards-author-card :key="entity.id" :width="bookCoverWidth / 1.25" :height="bookCoverWidth" :author="entity" :size-multiplier="sizeMultiplier" @hook:updated="updatedBookCard" class="pb-6 mx-2" @edit="editAuthor" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="shelf.type === 'narrators'" class="flex items-center">
|
||||||
|
<template v-for="entity in shelf.entities">
|
||||||
|
<cards-narrator-card :key="entity.name" :width="150" :height="100" :narrator="entity" :size-multiplier="sizeMultiplier" @hook:updated="updatedBookCard" class="pb-6 mx-2" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -88,6 +93,7 @@ export default {
|
|||||||
return this.bookCoverWidth * this.bookCoverAspectRatio
|
return this.bookCoverWidth * this.bookCoverAspectRatio
|
||||||
},
|
},
|
||||||
shelfHeight() {
|
shelfHeight() {
|
||||||
|
if (this.shelf.type === 'narrators') return 148
|
||||||
return this.bookCoverHeight + 48
|
return this.bookCoverHeight + 48
|
||||||
},
|
},
|
||||||
paddingLeft() {
|
paddingLeft() {
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<p v-if="matchKey !== 'authors'" class="text-xs text-gray-200 truncate">by {{ authorName }}</p>
|
<p v-if="matchKey !== 'authors'" class="text-xs text-gray-200 truncate">by {{ authorName }}</p>
|
||||||
<p v-else class="truncate text-xs text-gray-200" v-html="matchHtml" />
|
<p v-else class="truncate text-xs text-gray-200" v-html="matchHtml" />
|
||||||
|
|
||||||
<div v-if="matchKey === 'series' || matchKey === 'tags' || matchKey === 'isbn' || matchKey === 'asin' || matchKey === 'episode'" class="m-0 p-0 truncate text-xs" v-html="matchHtml" />
|
<div v-if="matchKey === 'series' || matchKey === 'tags' || matchKey === 'isbn' || matchKey === 'asin' || matchKey === 'episode' || matchKey === 'narrators'" class="m-0 p-0 truncate text-xs" v-html="matchHtml" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -67,12 +67,13 @@ export default {
|
|||||||
// but with removing commas periods etc this is no longer plausible
|
// but with removing commas periods etc this is no longer plausible
|
||||||
const html = this.matchText
|
const html = this.matchText
|
||||||
|
|
||||||
if (this.matchKey === 'episode') return `<p class="truncate">Episode: ${html}</p>`
|
if (this.matchKey === 'episode') return `<p class="truncate">${this.$strings.LabelEpisode}: ${html}</p>`
|
||||||
if (this.matchKey === 'tags') return `<p class="truncate">Tags: ${html}</p>`
|
if (this.matchKey === 'tags') return `<p class="truncate">${this.$strings.LabelTags}: ${html}</p>`
|
||||||
if (this.matchKey === 'authors') return `by ${html}`
|
if (this.matchKey === 'authors') return `by ${html}`
|
||||||
if (this.matchKey === 'isbn') return `<p class="truncate">ISBN: ${html}</p>`
|
if (this.matchKey === 'isbn') return `<p class="truncate">ISBN: ${html}</p>`
|
||||||
if (this.matchKey === 'asin') return `<p class="truncate">ASIN: ${html}</p>`
|
if (this.matchKey === 'asin') return `<p class="truncate">ASIN: ${html}</p>`
|
||||||
if (this.matchKey === 'series') return `<p class="truncate">Series: ${html}</p>`
|
if (this.matchKey === 'series') return `<p class="truncate">${this.$strings.LabelSeries}: ${html}</p>`
|
||||||
|
if (this.matchKey === 'narrators') return `<p class="truncate">${this.$strings.LabelNarrator}: ${html}</p>`
|
||||||
return `${html}`
|
return `${html}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
<div class="absolute z-10 top-1.5 right-1.5 rounded-md leading-3 text-sm p-1 font-semibold text-white flex items-center justify-center" style="background-color: #cd9d49dd">{{ books.length }}</div>
|
<div class="absolute z-10 top-1.5 right-1.5 rounded-md leading-3 text-sm p-1 font-semibold text-white flex items-center justify-center" style="background-color: #cd9d49dd">{{ books.length }}</div>
|
||||||
|
|
||||||
<div v-if="isSeriesFinished" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b bg-success w-full" />
|
<div v-if="seriesPercentInProgress > 0" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b w-full" :class="isSeriesFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: seriesPercentInProgress * 100 + '%' }" />
|
||||||
|
|
||||||
<div v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: `${sizeMultiplier}rem` }">
|
<div v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: `${sizeMultiplier}rem` }">
|
||||||
<p :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ displayTitle }}</p>
|
<p :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ displayTitle }}</p>
|
||||||
@ -87,10 +87,10 @@ export default {
|
|||||||
case 'totalDuration':
|
case 'totalDuration':
|
||||||
return `${this.$strings.LabelDuration} ${this.$elapsedPrettyExtended(this.totalDuration, false)}`
|
return `${this.$strings.LabelDuration} ${this.$elapsedPrettyExtended(this.totalDuration, false)}`
|
||||||
case 'lastBookUpdated':
|
case 'lastBookUpdated':
|
||||||
const lastUpdated = Math.max(...(this.books).map(x => x.updatedAt), 0)
|
const lastUpdated = Math.max(...this.books.map((x) => x.updatedAt), 0)
|
||||||
return `${this.$strings.LabelLastBookUpdated} ${this.$formatDate(lastUpdated, this.dateFormat)}`
|
return `${this.$strings.LabelLastBookUpdated} ${this.$formatDate(lastUpdated, this.dateFormat)}`
|
||||||
case 'lastBookAdded':
|
case 'lastBookAdded':
|
||||||
const lastBookAdded = Math.max(...(this.books).map(x => x.addedAt), 0)
|
const lastBookAdded = Math.max(...this.books.map((x) => x.addedAt), 0)
|
||||||
return `${this.$strings.LabelLastBookAdded} ${this.$formatDate(lastBookAdded, this.dateFormat)}`
|
return `${this.$strings.LabelLastBookAdded} ${this.$formatDate(lastBookAdded, this.dateFormat)}`
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
@ -115,6 +115,14 @@ export default {
|
|||||||
seriesBooksFinished() {
|
seriesBooksFinished() {
|
||||||
return this.seriesBookProgress.filter((p) => p.isFinished)
|
return this.seriesBookProgress.filter((p) => p.isFinished)
|
||||||
},
|
},
|
||||||
|
hasSeriesBookInProgress() {
|
||||||
|
return this.seriesBookProgress.some((p) => !p.isFinished && p.progress > 0)
|
||||||
|
},
|
||||||
|
seriesPercentInProgress() {
|
||||||
|
let totalFinishedAndInProgress = this.seriesBooksFinished.length
|
||||||
|
if (this.hasSeriesBookInProgress) totalFinishedAndInProgress += 1
|
||||||
|
return Math.min(1, Math.max(0, totalFinishedAndInProgress / this.books.length))
|
||||||
|
},
|
||||||
isSeriesFinished() {
|
isSeriesFinished() {
|
||||||
return this.books.length === this.seriesBooksFinished.length
|
return this.books.length === this.seriesBooksFinished.length
|
||||||
},
|
},
|
||||||
|
50
client/components/cards/NarratorCard.vue
Normal file
50
client/components/cards/NarratorCard.vue
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=narrators.${$encode(narrator.name)}`">
|
||||||
|
<div :style="{ width: width + 'px', height: height + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
|
||||||
|
<div class="absolute inset-0 w-full h-full flex items-center justify-center pointer-events-none opacity-20">
|
||||||
|
<span class="material-icons-outlined text-8xl">record_voice_over</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Narrator name & num books overlay -->
|
||||||
|
<div class="absolute bottom-0 left-0 w-full py-1 bg-black bg-opacity-60 px-2">
|
||||||
|
<p class="text-center font-semibold truncate" :style="{ fontSize: sizeMultiplier * 0.75 + 'rem' }">{{ name }}</p>
|
||||||
|
<p class="text-center text-gray-200" :style="{ fontSize: sizeMultiplier * 0.65 + 'rem' }">{{ numBooks }} Book{{ numBooks === 1 ? '' : 's' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nuxt-link>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
narrator: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
|
width: Number,
|
||||||
|
height: Number,
|
||||||
|
sizeMultiplier: {
|
||||||
|
type: Number,
|
||||||
|
default: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
name() {
|
||||||
|
return this.narrator?.name || ''
|
||||||
|
},
|
||||||
|
numBooks() {
|
||||||
|
return this.narrator?.books?.length || 0
|
||||||
|
},
|
||||||
|
userCanUpdate() {
|
||||||
|
return this.$store.getters['user/getUserCanUpdate']
|
||||||
|
},
|
||||||
|
currentLibraryId() {
|
||||||
|
return this.$store.state.libraries.currentLibraryId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {}
|
||||||
|
}
|
||||||
|
</script>
|
34
client/components/cards/NarratorSearchCard.vue
Normal file
34
client/components/cards/NarratorSearchCard.vue
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex h-full px-1 overflow-hidden">
|
||||||
|
<div class="w-10 h-10 flex items-center justify-center">
|
||||||
|
<span class="material-icons text-2xl text-gray-200">record_voice_over</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow px-2 narratorSearchCardContent h-full">
|
||||||
|
<p class="truncate text-sm">{{ narrator }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
narrator: String
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
methods: {},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.narratorSearchCardContent {
|
||||||
|
width: calc(100% - 40px);
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
@ -63,6 +63,15 @@
|
|||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<p v-if="narratorResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">{{ $strings.LabelNarrators }}</p>
|
||||||
|
<template v-for="narrator in narratorResults">
|
||||||
|
<li :key="narrator.name" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
||||||
|
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=narrators.${$encode(narrator.name)}`">
|
||||||
|
<cards-narrator-search-card :narrator="narrator.name" />
|
||||||
|
</nuxt-link>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@ -84,6 +93,7 @@ export default {
|
|||||||
authorResults: [],
|
authorResults: [],
|
||||||
seriesResults: [],
|
seriesResults: [],
|
||||||
tagResults: [],
|
tagResults: [],
|
||||||
|
narratorResults: [],
|
||||||
searchTimeout: null,
|
searchTimeout: null,
|
||||||
lastSearch: null
|
lastSearch: null
|
||||||
}
|
}
|
||||||
@ -114,6 +124,7 @@ export default {
|
|||||||
this.authorResults = []
|
this.authorResults = []
|
||||||
this.seriesResults = []
|
this.seriesResults = []
|
||||||
this.tagResults = []
|
this.tagResults = []
|
||||||
|
this.narratorResults = []
|
||||||
this.showMenu = false
|
this.showMenu = false
|
||||||
this.isFetching = false
|
this.isFetching = false
|
||||||
this.isTyping = false
|
this.isTyping = false
|
||||||
@ -142,7 +153,7 @@ export default {
|
|||||||
}
|
}
|
||||||
this.isFetching = true
|
this.isFetching = true
|
||||||
|
|
||||||
var searchResults = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/search?q=${value}&limit=3`).catch((error) => {
|
const searchResults = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/search?q=${value}&limit=3`).catch((error) => {
|
||||||
console.error('Search error', error)
|
console.error('Search error', error)
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
@ -155,6 +166,7 @@ export default {
|
|||||||
this.authorResults = searchResults.authors || []
|
this.authorResults = searchResults.authors || []
|
||||||
this.seriesResults = searchResults.series || []
|
this.seriesResults = searchResults.series || []
|
||||||
this.tagResults = searchResults.tags || []
|
this.tagResults = searchResults.tags || []
|
||||||
|
this.narratorResults = searchResults.narrators || []
|
||||||
|
|
||||||
this.isFetching = false
|
this.isFetching = false
|
||||||
if (!this.showMenu) {
|
if (!this.showMenu) {
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<form @submit.prevent="submitForm">
|
<form @submit.prevent="submitForm">
|
||||||
<div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300">
|
<div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 overflow-y-auto overflow-x-hidden" style="min-height: 400px; max-height: 80vh">
|
||||||
<div class="w-full p-8">
|
<div class="w-full p-8">
|
||||||
<div class="flex py-2">
|
<div class="flex py-2">
|
||||||
<div class="w-1/2 px-2">
|
<div class="w-1/2 px-2">
|
||||||
@ -96,7 +96,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!newUser.permissions.accessAllTags" class="my-4">
|
<div v-if="!newUser.permissions.accessAllTags" class="my-4">
|
||||||
<ui-multi-select-dropdown v-model="newUser.itemTagsAccessible" :items="itemTags" :label="$strings.LabelTagsAccessibleToUser" />
|
<div class="flex items-center">
|
||||||
|
<ui-multi-select-dropdown v-model="newUser.itemTagsSelected" :items="itemTags" :label="tagsSelectionText" />
|
||||||
|
<div class="flex items-center pt-4 px-2">
|
||||||
|
<p class="px-3 font-semibold" id="selected-tags-not-accessible--permissions-toggle">{{ $strings.LabelInvert }}</p>
|
||||||
|
<ui-toggle-switch labeledBy="selected-tags-not-accessible--permissions-toggle" v-model="newUser.permissions.selectedTagsNotAccessible" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -185,6 +192,9 @@ export default {
|
|||||||
value: t
|
value: t
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
tagsSelectionText() {
|
||||||
|
return this.newUser.permissions.selectedTagsNotAccessible ? this.$strings.LabelTagsNotAccessibleToUser : this.$strings.LabelTagsAccessibleToUser
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -193,8 +203,11 @@ export default {
|
|||||||
if (this.$refs.modal) this.$refs.modal.setHide()
|
if (this.$refs.modal) this.$refs.modal.setHide()
|
||||||
},
|
},
|
||||||
accessAllTagsToggled(val) {
|
accessAllTagsToggled(val) {
|
||||||
if (val && this.newUser.itemTagsAccessible.length) {
|
if (val) {
|
||||||
this.newUser.itemTagsAccessible = []
|
if (this.newUser.itemTagsSelected?.length) {
|
||||||
|
this.newUser.itemTagsSelected = []
|
||||||
|
}
|
||||||
|
this.newUser.permissions.selectedTagsNotAccessible = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fetchAllTags() {
|
fetchAllTags() {
|
||||||
@ -226,7 +239,7 @@ export default {
|
|||||||
this.$toast.error('Must select at least one library')
|
this.$toast.error('Must select at least one library')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!this.newUser.permissions.accessAllTags && !this.newUser.itemTagsAccessible.length) {
|
if (!this.newUser.permissions.accessAllTags && !this.newUser.itemTagsSelected.length) {
|
||||||
this.$toast.error('Must select at least one tag')
|
this.$toast.error('Must select at least one tag')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -307,12 +320,12 @@ export default {
|
|||||||
delete: type === 'admin',
|
delete: type === 'admin',
|
||||||
upload: type === 'admin',
|
upload: type === 'admin',
|
||||||
accessAllLibraries: true,
|
accessAllLibraries: true,
|
||||||
accessAllTags: true
|
accessAllTags: true,
|
||||||
|
selectedTagsNotAccessible: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
init() {
|
init() {
|
||||||
this.fetchAllTags()
|
this.fetchAllTags()
|
||||||
|
|
||||||
this.isNew = !this.account
|
this.isNew = !this.account
|
||||||
if (this.account) {
|
if (this.account) {
|
||||||
this.newUser = {
|
this.newUser = {
|
||||||
@ -322,9 +335,10 @@ export default {
|
|||||||
isActive: this.account.isActive,
|
isActive: this.account.isActive,
|
||||||
permissions: { ...this.account.permissions },
|
permissions: { ...this.account.permissions },
|
||||||
librariesAccessible: [...(this.account.librariesAccessible || [])],
|
librariesAccessible: [...(this.account.librariesAccessible || [])],
|
||||||
itemTagsAccessible: [...(this.account.itemTagsAccessible || [])]
|
itemTagsSelected: [...(this.account.itemTagsSelected || [])]
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
this.fetchAllTags()
|
||||||
this.newUser = {
|
this.newUser = {
|
||||||
username: null,
|
username: null,
|
||||||
password: null,
|
password: null,
|
||||||
@ -336,7 +350,8 @@ export default {
|
|||||||
delete: false,
|
delete: false,
|
||||||
upload: false,
|
upload: false,
|
||||||
accessAllLibraries: true,
|
accessAllLibraries: true,
|
||||||
accessAllTags: true
|
accessAllTags: true,
|
||||||
|
selectedTagsNotAccessible: false
|
||||||
},
|
},
|
||||||
librariesAccessible: []
|
librariesAccessible: []
|
||||||
}
|
}
|
||||||
|
100
client/components/widgets/NarratorsSlider.vue
Normal file
100
client/components/widgets/NarratorsSlider.vue
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full">
|
||||||
|
<div class="flex items-center py-3">
|
||||||
|
<slot />
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<button v-if="isScrollable" class="w-8 h-8 mx-1 flex items-center justify-center rounded-full" :class="canScrollLeft ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollLeft">
|
||||||
|
<span class="material-icons text-2xl">chevron_left</span>
|
||||||
|
</button>
|
||||||
|
<button v-if="isScrollable" class="w-8 h-8 mx-1 flex items-center justify-center rounded-full" :class="canScrollRight ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollRight">
|
||||||
|
<span class="material-icons text-2xl">chevron_right</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll -mx-2" style="scroll-behavior: smooth" @scroll="scrolled">
|
||||||
|
<div class="flex" :style="{ height: height + 'px' }">
|
||||||
|
<template v-for="item in items">
|
||||||
|
<cards-narrator-card :key="item.name" :ref="`slider-item-${item.name}`" :narrator="item" :height="cardHeight" :width="cardWidth" class="relative mx-2" @hook:updated="setScrollVars" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: Number,
|
||||||
|
default: 192
|
||||||
|
},
|
||||||
|
bookshelfView: {
|
||||||
|
type: Number,
|
||||||
|
default: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isScrollable: false,
|
||||||
|
canScrollLeft: false,
|
||||||
|
canScrollRight: false,
|
||||||
|
clientWidth: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
cardHeight() {
|
||||||
|
return this.height
|
||||||
|
},
|
||||||
|
cardWidth() {
|
||||||
|
return this.cardHeight * 1.5
|
||||||
|
},
|
||||||
|
booksPerPage() {
|
||||||
|
return Math.floor(this.clientWidth / (this.cardWidth + 16))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
scrolled() {
|
||||||
|
this.setScrollVars()
|
||||||
|
},
|
||||||
|
scrollRight() {
|
||||||
|
if (!this.canScrollRight) return
|
||||||
|
const slider = this.$refs.slider
|
||||||
|
if (!slider) return
|
||||||
|
const scrollAmount = this.booksPerPage * this.cardWidth
|
||||||
|
const maxScrollLeft = slider.scrollWidth - slider.clientWidth
|
||||||
|
|
||||||
|
const newScrollLeft = Math.min(maxScrollLeft, slider.scrollLeft + scrollAmount)
|
||||||
|
slider.scrollLeft = newScrollLeft
|
||||||
|
},
|
||||||
|
scrollLeft() {
|
||||||
|
if (!this.canScrollLeft) return
|
||||||
|
const slider = this.$refs.slider
|
||||||
|
if (!slider) return
|
||||||
|
|
||||||
|
const scrollAmount = this.booksPerPage * this.cardWidth
|
||||||
|
|
||||||
|
const newScrollLeft = Math.max(0, slider.scrollLeft - scrollAmount)
|
||||||
|
slider.scrollLeft = newScrollLeft
|
||||||
|
},
|
||||||
|
setScrollVars() {
|
||||||
|
const slider = this.$refs.slider
|
||||||
|
if (!slider) return
|
||||||
|
const { scrollLeft, scrollWidth, clientWidth } = slider
|
||||||
|
const scrollPercent = (scrollLeft + clientWidth) / scrollWidth
|
||||||
|
|
||||||
|
this.clientWidth = clientWidth
|
||||||
|
this.isScrollable = scrollWidth > clientWidth
|
||||||
|
this.canScrollRight = scrollPercent < 1
|
||||||
|
this.canScrollLeft = scrollLeft > 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updated() {
|
||||||
|
this.setScrollVars()
|
||||||
|
},
|
||||||
|
mounted() {},
|
||||||
|
beforeDestroy() {}
|
||||||
|
}
|
||||||
|
</script>
|
@ -569,6 +569,7 @@ export default {
|
|||||||
changeLanguage(code) {
|
changeLanguage(code) {
|
||||||
console.log('Changed lang', code)
|
console.log('Changed lang', code)
|
||||||
this.currentLang = code
|
this.currentLang = code
|
||||||
|
document.documentElement.lang = code
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeMount() {
|
beforeMount() {
|
||||||
@ -593,6 +594,11 @@ export default {
|
|||||||
this.$toast.error(this.$route.query.error)
|
this.$toast.error(this.$route.query.error)
|
||||||
this.$router.replace(this.$route.path)
|
this.$router.replace(this.$route.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set lang on HTML tag
|
||||||
|
if (this.$languageCodes?.current) {
|
||||||
|
document.documentElement.lang = this.$languageCodes.current
|
||||||
|
}
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.$eventBus.$off('change-lang', this.changeLanguage)
|
this.$eventBus.$off('change-lang', this.changeLanguage)
|
||||||
|
@ -11,27 +11,27 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
async asyncData({ store, params, redirect, query, app }) {
|
async asyncData({ store, params, redirect, query, app }) {
|
||||||
var libraryId = params.library
|
const libraryId = params.library
|
||||||
var library = await store.dispatch('libraries/fetch', libraryId)
|
const library = await store.dispatch('libraries/fetch', libraryId)
|
||||||
if (!library) {
|
if (!library) {
|
||||||
return redirect('/oops?message=Library not found')
|
return redirect('/oops?message=Library not found')
|
||||||
}
|
}
|
||||||
var query = query.q
|
let results = await app.$axios.$get(`/api/libraries/${libraryId}/search?q=${query.q}`).catch((error) => {
|
||||||
var results = await app.$axios.$get(`/api/libraries/${libraryId}/search?q=${query}`).catch((error) => {
|
|
||||||
console.error('Failed to search library', error)
|
console.error('Failed to search library', error)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
results = {
|
results = {
|
||||||
podcasts: results && results.podcast ? results.podcast : null,
|
podcasts: results?.podcast || [],
|
||||||
books: results && results.book ? results.book : null,
|
books: results?.book || [],
|
||||||
authors: results && results.authors.length ? results.authors : null,
|
authors: results?.authors || [],
|
||||||
series: results && results.series.length ? results.series : null,
|
series: results?.series || [],
|
||||||
tags: results && results.tags.length ? results.tags : null
|
tags: results?.tags || [],
|
||||||
|
narrators: results?.narrators || []
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
libraryId,
|
libraryId,
|
||||||
results,
|
results,
|
||||||
query
|
query: query.q
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@ -55,16 +55,17 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async search() {
|
async search() {
|
||||||
var results = await this.$axios.$get(`/api/libraries/${this.libraryId}/search?q=${this.query}`).catch((error) => {
|
const results = await this.$axios.$get(`/api/libraries/${this.libraryId}/search?q=${this.query}`).catch((error) => {
|
||||||
console.error('Failed to search library', error)
|
console.error('Failed to search library', error)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
this.results = {
|
this.results = {
|
||||||
podcasts: results && results.podcast ? results.podcast : null,
|
podcasts: results?.podcast || [],
|
||||||
books: results && results.book ? results.book : null,
|
books: results?.book || [],
|
||||||
authors: results && results.authors.length ? results.authors : null,
|
authors: results?.authors || [],
|
||||||
series: results && results.series.length ? results.series : null,
|
series: results?.series || [],
|
||||||
tags: results && results.tags.length ? results.tags : null
|
tags: results?.tags || [],
|
||||||
|
narrators: results?.narrators || []
|
||||||
}
|
}
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (this.$refs.bookshelf) {
|
if (this.$refs.bookshelf) {
|
||||||
|
@ -142,8 +142,8 @@
|
|||||||
"HeaderSettingsGeneral": "Allgemein",
|
"HeaderSettingsGeneral": "Allgemein",
|
||||||
"HeaderSettingsScanner": "Scanner",
|
"HeaderSettingsScanner": "Scanner",
|
||||||
"HeaderSleepTimer": "Einschlaf-Timer",
|
"HeaderSleepTimer": "Einschlaf-Timer",
|
||||||
"HeaderStatsLargestItems": "Largest Items",
|
"HeaderStatsLargestItems": "Größte Medien",
|
||||||
"HeaderStatsLongestItems": "Längste Einträge (h)",
|
"HeaderStatsLongestItems": "Längste Medien (h)",
|
||||||
"HeaderStatsMinutesListeningChart": "Hörminuten (letzte 7 Tage)",
|
"HeaderStatsMinutesListeningChart": "Hörminuten (letzte 7 Tage)",
|
||||||
"HeaderStatsRecentSessions": "Neueste Ereignisse",
|
"HeaderStatsRecentSessions": "Neueste Ereignisse",
|
||||||
"HeaderStatsTop10Authors": "Top 10 Autoren",
|
"HeaderStatsTop10Authors": "Top 10 Autoren",
|
||||||
@ -161,7 +161,7 @@
|
|||||||
"LabelAccountTypeGuest": "Gast",
|
"LabelAccountTypeGuest": "Gast",
|
||||||
"LabelAccountTypeUser": "Benutzer",
|
"LabelAccountTypeUser": "Benutzer",
|
||||||
"LabelActivity": "Aktivitäten",
|
"LabelActivity": "Aktivitäten",
|
||||||
"LabelAdded": "Added",
|
"LabelAdded": "Hinzugefügt",
|
||||||
"LabelAddedAt": "Hinzugefügt am",
|
"LabelAddedAt": "Hinzugefügt am",
|
||||||
"LabelAddToCollection": "Zur Sammlung hinzufügen",
|
"LabelAddToCollection": "Zur Sammlung hinzufügen",
|
||||||
"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",
|
||||||
@ -169,7 +169,7 @@
|
|||||||
"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",
|
||||||
"LabelAll": "Alle",
|
"LabelAll": "Alle",
|
||||||
"LabelAllUsers": "Alle Benutzer",
|
"LabelAllUsers": "Alle Benutzer",
|
||||||
"LabelAlreadyInYourLibrary": "Already in your library",
|
"LabelAlreadyInYourLibrary": "In der Bibliothek vorhanden",
|
||||||
"LabelAppend": "Anhängen",
|
"LabelAppend": "Anhängen",
|
||||||
"LabelAuthor": "Autor",
|
"LabelAuthor": "Autor",
|
||||||
"LabelAuthorFirstLast": "Autor (Vorname Nachname)",
|
"LabelAuthorFirstLast": "Autor (Vorname Nachname)",
|
||||||
@ -201,7 +201,7 @@
|
|||||||
"LabelCover": "Titelbild",
|
"LabelCover": "Titelbild",
|
||||||
"LabelCoverImageURL": "URL des Titelbildes",
|
"LabelCoverImageURL": "URL des Titelbildes",
|
||||||
"LabelCreatedAt": "Erstellt am",
|
"LabelCreatedAt": "Erstellt am",
|
||||||
"LabelCronExpression": "Cron Ausdruck",
|
"LabelCronExpression": "Cron-Ausdruck",
|
||||||
"LabelCurrent": "Aktuell",
|
"LabelCurrent": "Aktuell",
|
||||||
"LabelCurrently": "Aktuell:",
|
"LabelCurrently": "Aktuell:",
|
||||||
"LabelCustomCronExpression": "Benutzerdefinierter Cron-Ausdruck",
|
"LabelCustomCronExpression": "Benutzerdefinierter Cron-Ausdruck",
|
||||||
@ -223,7 +223,7 @@
|
|||||||
"LabelEpisode": "Episode",
|
"LabelEpisode": "Episode",
|
||||||
"LabelEpisodeTitle": "Episodentitel",
|
"LabelEpisodeTitle": "Episodentitel",
|
||||||
"LabelEpisodeType": "Episodentyp",
|
"LabelEpisodeType": "Episodentyp",
|
||||||
"LabelExample": "Example",
|
"LabelExample": "Beispiel",
|
||||||
"LabelExplicit": "Explizit (Altersbeschränkung)",
|
"LabelExplicit": "Explizit (Altersbeschränkung)",
|
||||||
"LabelFeedURL": "Feed URL",
|
"LabelFeedURL": "Feed URL",
|
||||||
"LabelFile": "Datei",
|
"LabelFile": "Datei",
|
||||||
@ -254,11 +254,12 @@
|
|||||||
"LabelIntervalEveryDay": "Jeden Tag",
|
"LabelIntervalEveryDay": "Jeden Tag",
|
||||||
"LabelIntervalEveryHour": "Jede Stunde",
|
"LabelIntervalEveryHour": "Jede Stunde",
|
||||||
"LabelInvalidParts": "Ungültige Teile",
|
"LabelInvalidParts": "Ungültige Teile",
|
||||||
|
"LabelInvert": "Invert",
|
||||||
"LabelItem": "Medium",
|
"LabelItem": "Medium",
|
||||||
"LabelLanguage": "Sprache",
|
"LabelLanguage": "Sprache",
|
||||||
"LabelLanguageDefaultServer": "Standard-Server-Sprache",
|
"LabelLanguageDefaultServer": "Standard-Server-Sprache",
|
||||||
"LabelLastBookAdded": "Last Book Added",
|
"LabelLastBookAdded": "Zuletzt hinzugefügtes Medium",
|
||||||
"LabelLastBookUpdated": "Last Book Updated",
|
"LabelLastBookUpdated": "Zuletzt aktualisiertes Medium",
|
||||||
"LabelLastSeen": "Zuletzt angesehen",
|
"LabelLastSeen": "Zuletzt angesehen",
|
||||||
"LabelLastTime": "Letztes Mal",
|
"LabelLastTime": "Letztes Mal",
|
||||||
"LabelLastUpdate": "Letzte Aktualisierung",
|
"LabelLastUpdate": "Letzte Aktualisierung",
|
||||||
@ -384,7 +385,7 @@
|
|||||||
"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. Es wird immer nur eine Datei mit dem Namen \"matadata.abs\" gespeichert.",
|
||||||
"LabelSettingsTimeFormat": "Time Format",
|
"LabelSettingsTimeFormat": "Zeitformat",
|
||||||
"LabelShowAll": "Alles anzeigen",
|
"LabelShowAll": "Alles anzeigen",
|
||||||
"LabelSize": "Größe",
|
"LabelSize": "Größe",
|
||||||
"LabelSleepTimer": "Einschlaf-Timer",
|
"LabelSleepTimer": "Einschlaf-Timer",
|
||||||
@ -412,8 +413,9 @@
|
|||||||
"LabelTag": "Schlagwort",
|
"LabelTag": "Schlagwort",
|
||||||
"LabelTags": "Schlagwörter",
|
"LabelTags": "Schlagwörter",
|
||||||
"LabelTagsAccessibleToUser": "Für Benutzer zugängliche Schlagwörter",
|
"LabelTagsAccessibleToUser": "Für Benutzer zugängliche Schlagwörter",
|
||||||
"LabelTasks": "Tasks Running",
|
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
||||||
"LabelTimeBase": "Time Base",
|
"LabelTasks": "Laufende Aufgaben",
|
||||||
|
"LabelTimeBase": "Basiszeit",
|
||||||
"LabelTimeListened": "Gehörte Zeit",
|
"LabelTimeListened": "Gehörte Zeit",
|
||||||
"LabelTimeListenedToday": "Heute gehörte Zeit",
|
"LabelTimeListenedToday": "Heute gehörte Zeit",
|
||||||
"LabelTimeRemaining": "{0} verbleibend",
|
"LabelTimeRemaining": "{0} verbleibend",
|
||||||
@ -470,7 +472,7 @@
|
|||||||
"MessageChapterErrorStartGteDuration": "Ungültige Kapitelstartzeit: Kapitelanfang > Mediumlänge (Kapitelanfang liegt zeitlich nach dem Ende des Mediums -> Lösung: Kapitelanfang < Mediumlänge)",
|
"MessageChapterErrorStartGteDuration": "Ungültige Kapitelstartzeit: Kapitelanfang > Mediumlänge (Kapitelanfang liegt zeitlich nach dem Ende des Mediums -> Lösung: Kapitelanfang < Mediumlänge)",
|
||||||
"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...",
|
||||||
"MessageConfirmDeleteBackup": "Sind Sie sicher, dass Sie die Sicherung für {0} löschen wollen?",
|
"MessageConfirmDeleteBackup": "Sind Sie sicher, dass Sie die Sicherung für {0} löschen wollen?",
|
||||||
"MessageConfirmDeleteLibrary": "Sind Sie sicher, dass Sie die Bibliothek \"{0}\" dauerhaft löschen wollen?",
|
"MessageConfirmDeleteLibrary": "Sind Sie sicher, dass Sie die Bibliothek \"{0}\" dauerhaft löschen wollen?",
|
||||||
"MessageConfirmDeleteSession": "Sind Sie sicher, dass Sie diese Sitzung löschen möchten?",
|
"MessageConfirmDeleteSession": "Sind Sie sicher, dass Sie diese Sitzung löschen möchten?",
|
||||||
@ -563,7 +565,7 @@
|
|||||||
"MessageUploaderItemFailed": "Hochladen fehlgeschlagen",
|
"MessageUploaderItemFailed": "Hochladen fehlgeschlagen",
|
||||||
"MessageUploaderItemSuccess": "Erfolgreich hochgeladen!",
|
"MessageUploaderItemSuccess": "Erfolgreich hochgeladen!",
|
||||||
"MessageUploading": "Hochladen...",
|
"MessageUploading": "Hochladen...",
|
||||||
"MessageValidCronExpression": "Gültiger cron-ausdruck",
|
"MessageValidCronExpression": "Gültiger Cron-Ausdruck",
|
||||||
"MessageWatcherIsDisabledGlobally": "Überwachung ist in den Servereinstellungen global deaktiviert",
|
"MessageWatcherIsDisabledGlobally": "Überwachung ist in den Servereinstellungen global deaktiviert",
|
||||||
"MessageXLibraryIsEmpty": "{0} Bibliothek ist leer!",
|
"MessageXLibraryIsEmpty": "{0} Bibliothek ist leer!",
|
||||||
"MessageYourAudiobookDurationIsLonger": "Die Dauer Ihres Mediums ist länger als die gefundene Dauer",
|
"MessageYourAudiobookDurationIsLonger": "Die Dauer Ihres Mediums ist länger als die gefundene Dauer",
|
||||||
|
@ -254,6 +254,7 @@
|
|||||||
"LabelIntervalEveryDay": "Every day",
|
"LabelIntervalEveryDay": "Every day",
|
||||||
"LabelIntervalEveryHour": "Every hour",
|
"LabelIntervalEveryHour": "Every hour",
|
||||||
"LabelInvalidParts": "Invalid Parts",
|
"LabelInvalidParts": "Invalid Parts",
|
||||||
|
"LabelInvert": "Invert",
|
||||||
"LabelItem": "Item",
|
"LabelItem": "Item",
|
||||||
"LabelLanguage": "Language",
|
"LabelLanguage": "Language",
|
||||||
"LabelLanguageDefaultServer": "Default Server Language",
|
"LabelLanguageDefaultServer": "Default Server Language",
|
||||||
@ -412,6 +413,7 @@
|
|||||||
"LabelTag": "Tag",
|
"LabelTag": "Tag",
|
||||||
"LabelTags": "Tags",
|
"LabelTags": "Tags",
|
||||||
"LabelTagsAccessibleToUser": "Tags Accessible to User",
|
"LabelTagsAccessibleToUser": "Tags Accessible to User",
|
||||||
|
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
||||||
"LabelTasks": "Tasks Running",
|
"LabelTasks": "Tasks Running",
|
||||||
"LabelTimeBase": "Time Base",
|
"LabelTimeBase": "Time Base",
|
||||||
"LabelTimeListened": "Time Listened",
|
"LabelTimeListened": "Time Listened",
|
||||||
|
@ -186,8 +186,8 @@
|
|||||||
"LabelBitrate": "Bitrate",
|
"LabelBitrate": "Bitrate",
|
||||||
"LabelBooks": "Libros",
|
"LabelBooks": "Libros",
|
||||||
"LabelChangePassword": "Cambiar Contraseña",
|
"LabelChangePassword": "Cambiar Contraseña",
|
||||||
"LabelChannels": "Channels",
|
"LabelChannels": "Canales",
|
||||||
"LabelChapters": "Chapters",
|
"LabelChapters": "Capitulos",
|
||||||
"LabelChaptersFound": "Capitulo Encontrado",
|
"LabelChaptersFound": "Capitulo Encontrado",
|
||||||
"LabelChapterTitle": "Titulo del Capitulo",
|
"LabelChapterTitle": "Titulo del Capitulo",
|
||||||
"LabelClosePlayer": "Close player",
|
"LabelClosePlayer": "Close player",
|
||||||
@ -217,7 +217,7 @@
|
|||||||
"LabelDuration": "Duración",
|
"LabelDuration": "Duración",
|
||||||
"LabelDurationFound": "Duración Comprobada:",
|
"LabelDurationFound": "Duración Comprobada:",
|
||||||
"LabelEdit": "Editar",
|
"LabelEdit": "Editar",
|
||||||
"LabelEmbeddedCover": "Embedded Cover",
|
"LabelEmbeddedCover": "Portada Integrada",
|
||||||
"LabelEnable": "Habilitar",
|
"LabelEnable": "Habilitar",
|
||||||
"LabelEnd": "Fin",
|
"LabelEnd": "Fin",
|
||||||
"LabelEpisode": "Episodio",
|
"LabelEpisode": "Episodio",
|
||||||
@ -235,7 +235,7 @@
|
|||||||
"LabelFinished": "Terminado",
|
"LabelFinished": "Terminado",
|
||||||
"LabelFolder": "Carpeta",
|
"LabelFolder": "Carpeta",
|
||||||
"LabelFolders": "Carpetas",
|
"LabelFolders": "Carpetas",
|
||||||
"LabelFormat": "Format",
|
"LabelFormat": "Formato",
|
||||||
"LabelGenre": "Genero",
|
"LabelGenre": "Genero",
|
||||||
"LabelGenres": "Géneros",
|
"LabelGenres": "Géneros",
|
||||||
"LabelHardDeleteFile": "Eliminar Definitivamente",
|
"LabelHardDeleteFile": "Eliminar Definitivamente",
|
||||||
@ -254,6 +254,7 @@
|
|||||||
"LabelIntervalEveryDay": "Cada Dia",
|
"LabelIntervalEveryDay": "Cada Dia",
|
||||||
"LabelIntervalEveryHour": "Cada Hora",
|
"LabelIntervalEveryHour": "Cada Hora",
|
||||||
"LabelInvalidParts": "Partes Invalidas",
|
"LabelInvalidParts": "Partes Invalidas",
|
||||||
|
"LabelInvert": "Invert",
|
||||||
"LabelItem": "Elemento",
|
"LabelItem": "Elemento",
|
||||||
"LabelLanguage": "Lenguaje",
|
"LabelLanguage": "Lenguaje",
|
||||||
"LabelLanguageDefaultServer": "Lenguaje Predeterminado del Servidor",
|
"LabelLanguageDefaultServer": "Lenguaje Predeterminado del Servidor",
|
||||||
@ -282,7 +283,7 @@
|
|||||||
"LabelMissing": "Ausente",
|
"LabelMissing": "Ausente",
|
||||||
"LabelMissingParts": "Partes Ausentes",
|
"LabelMissingParts": "Partes Ausentes",
|
||||||
"LabelMore": "Mas",
|
"LabelMore": "Mas",
|
||||||
"LabelMoreInfo": "More Info",
|
"LabelMoreInfo": "Mas Información",
|
||||||
"LabelName": "Nombre",
|
"LabelName": "Nombre",
|
||||||
"LabelNarrator": "Narrador",
|
"LabelNarrator": "Narrador",
|
||||||
"LabelNarrators": "Narradores",
|
"LabelNarrators": "Narradores",
|
||||||
@ -412,6 +413,7 @@
|
|||||||
"LabelTag": "Etiqueta",
|
"LabelTag": "Etiqueta",
|
||||||
"LabelTags": "Etiquetas",
|
"LabelTags": "Etiquetas",
|
||||||
"LabelTagsAccessibleToUser": "Etiquetas Accessible para el Usuario",
|
"LabelTagsAccessibleToUser": "Etiquetas Accessible para el Usuario",
|
||||||
|
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
||||||
"LabelTasks": "Tareas Corriendo",
|
"LabelTasks": "Tareas Corriendo",
|
||||||
"LabelTimeBase": "Time Base",
|
"LabelTimeBase": "Time Base",
|
||||||
"LabelTimeListened": "Tiempo Escuchando",
|
"LabelTimeListened": "Tiempo Escuchando",
|
||||||
@ -477,7 +479,7 @@
|
|||||||
"MessageConfirmForceReScan": "Esta seguro que desea forzar re-escanear?",
|
"MessageConfirmForceReScan": "Esta seguro que desea forzar re-escanear?",
|
||||||
"MessageConfirmMarkSeriesFinished": "Esta seguro que desea marcar todos los libros en esta serie como terminados?",
|
"MessageConfirmMarkSeriesFinished": "Esta seguro que desea marcar todos los libros en esta serie como terminados?",
|
||||||
"MessageConfirmMarkSeriesNotFinished": "Esta seguro que desea marcar todos los libros en esta serie como no terminados?",
|
"MessageConfirmMarkSeriesNotFinished": "Esta seguro que desea marcar todos los libros en esta serie como no terminados?",
|
||||||
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
|
"MessageConfirmRemoveAllChapters": "Esta seguro que desea remover todos los capitulos?",
|
||||||
"MessageConfirmRemoveCollection": "Esta seguro que desea remover la colección \"{0}\"?",
|
"MessageConfirmRemoveCollection": "Esta seguro que desea remover la colección \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisode": "Esta seguro que desea remover el episodio \"{0}\"?",
|
"MessageConfirmRemoveEpisode": "Esta seguro que desea remover el episodio \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisodes": "Esta seguro que desea remover {0} episodios?",
|
"MessageConfirmRemoveEpisodes": "Esta seguro que desea remover {0} episodios?",
|
||||||
@ -549,7 +551,7 @@
|
|||||||
"MessageRemoveAllItemsWarning": "ADVERTENCIA! Esta acción eliminará todos los elementos de la biblioteca de la base de datos incluyendo cualquier actualización o match. Esto no hace nada a sus archivos reales. Esta seguro que desea continuar?",
|
"MessageRemoveAllItemsWarning": "ADVERTENCIA! Esta acción eliminará todos los elementos de la biblioteca de la base de datos incluyendo cualquier actualización o match. Esto no hace nada a sus archivos reales. Esta seguro que desea continuar?",
|
||||||
"MessageRemoveChapter": "Remover capítulos",
|
"MessageRemoveChapter": "Remover capítulos",
|
||||||
"MessageRemoveEpisodes": "Remover {0} episodio(s)",
|
"MessageRemoveEpisodes": "Remover {0} episodio(s)",
|
||||||
"MessageRemoveFromPlayerQueue": "Remover de player queue",
|
"MessageRemoveFromPlayerQueue": "Romover la cola de reporduccion",
|
||||||
"MessageRemoveUserWarning": "Esta seguro que desea eliminar el usuario \"{0}\"?",
|
"MessageRemoveUserWarning": "Esta seguro que desea eliminar el usuario \"{0}\"?",
|
||||||
"MessageReportBugsAndContribute": "Reporte erres, solicite funciones y contribuye en",
|
"MessageReportBugsAndContribute": "Reporte erres, solicite funciones y contribuye en",
|
||||||
"MessageResetChaptersConfirm": "Esta seguro que desea reiniciar el capitulo y deshacer los cambios que hiciste?",
|
"MessageResetChaptersConfirm": "Esta seguro que desea reiniciar el capitulo y deshacer los cambios que hiciste?",
|
||||||
|
@ -93,7 +93,7 @@
|
|||||||
"HeaderCollection": "Collection",
|
"HeaderCollection": "Collection",
|
||||||
"HeaderCollectionItems": "Entrées de la Collection",
|
"HeaderCollectionItems": "Entrées de la Collection",
|
||||||
"HeaderCover": "Couverture",
|
"HeaderCover": "Couverture",
|
||||||
"HeaderCurrentDownloads": "Current Downloads",
|
"HeaderCurrentDownloads": "File d’attente de téléchargement",
|
||||||
"HeaderDetails": "Détails",
|
"HeaderDetails": "Détails",
|
||||||
"HeaderDownloadQueue": "Queue de téléchargement",
|
"HeaderDownloadQueue": "Queue de téléchargement",
|
||||||
"HeaderEpisodes": "Épisodes",
|
"HeaderEpisodes": "Épisodes",
|
||||||
@ -161,7 +161,7 @@
|
|||||||
"LabelAccountTypeGuest": "Invité",
|
"LabelAccountTypeGuest": "Invité",
|
||||||
"LabelAccountTypeUser": "Utilisateur",
|
"LabelAccountTypeUser": "Utilisateur",
|
||||||
"LabelActivity": "Activité",
|
"LabelActivity": "Activité",
|
||||||
"LabelAdded": "Added",
|
"LabelAdded": "Ajouté",
|
||||||
"LabelAddedAt": "Date d’ajout",
|
"LabelAddedAt": "Date d’ajout",
|
||||||
"LabelAddToCollection": "Ajouter à la collection",
|
"LabelAddToCollection": "Ajouter à la collection",
|
||||||
"LabelAddToCollectionBatch": "Ajout de {0} livres à la lollection",
|
"LabelAddToCollectionBatch": "Ajout de {0} livres à la lollection",
|
||||||
@ -186,8 +186,8 @@
|
|||||||
"LabelBitrate": "Bitrate",
|
"LabelBitrate": "Bitrate",
|
||||||
"LabelBooks": "Livres",
|
"LabelBooks": "Livres",
|
||||||
"LabelChangePassword": "Modifier le mot de passe",
|
"LabelChangePassword": "Modifier le mot de passe",
|
||||||
"LabelChannels": "Channels",
|
"LabelChannels": "Canaux",
|
||||||
"LabelChapters": "Chapters",
|
"LabelChapters": "Chapitres",
|
||||||
"LabelChaptersFound": "Chapitres trouvés",
|
"LabelChaptersFound": "Chapitres trouvés",
|
||||||
"LabelChapterTitle": "Titres du chapitre",
|
"LabelChapterTitle": "Titres du chapitre",
|
||||||
"LabelClosePlayer": "Fermer le lecteur",
|
"LabelClosePlayer": "Fermer le lecteur",
|
||||||
@ -254,11 +254,12 @@
|
|||||||
"LabelIntervalEveryDay": "Tous les jours",
|
"LabelIntervalEveryDay": "Tous les jours",
|
||||||
"LabelIntervalEveryHour": "Toutes les heures",
|
"LabelIntervalEveryHour": "Toutes les heures",
|
||||||
"LabelInvalidParts": "Parties invalides",
|
"LabelInvalidParts": "Parties invalides",
|
||||||
|
"LabelInvert": "Invert",
|
||||||
"LabelItem": "Article",
|
"LabelItem": "Article",
|
||||||
"LabelLanguage": "Langue",
|
"LabelLanguage": "Langue",
|
||||||
"LabelLanguageDefaultServer": "Langue par défaut",
|
"LabelLanguageDefaultServer": "Langue par défaut",
|
||||||
"LabelLastBookAdded": "Last Book Added",
|
"LabelLastBookAdded": "Dernier livre ajouté",
|
||||||
"LabelLastBookUpdated": "Last Book Updated",
|
"LabelLastBookUpdated": "Dernier livre mis à jour",
|
||||||
"LabelLastSeen": "Vu dernièrement",
|
"LabelLastSeen": "Vu dernièrement",
|
||||||
"LabelLastTime": "Progression",
|
"LabelLastTime": "Progression",
|
||||||
"LabelLastUpdate": "Dernière mise à jour",
|
"LabelLastUpdate": "Dernière mise à jour",
|
||||||
@ -277,12 +278,12 @@
|
|||||||
"LabelMediaType": "Type de média",
|
"LabelMediaType": "Type de média",
|
||||||
"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": "Meta Tags",
|
"LabelMetaTags": "Etiquettes de métadonnée",
|
||||||
"LabelMinute": "Minute",
|
"LabelMinute": "Minute",
|
||||||
"LabelMissing": "Manquant",
|
"LabelMissing": "Manquant",
|
||||||
"LabelMissingParts": "Parties manquantes",
|
"LabelMissingParts": "Parties manquantes",
|
||||||
"LabelMore": "Plus",
|
"LabelMore": "Plus",
|
||||||
"LabelMoreInfo": "More Info",
|
"LabelMoreInfo": "Plus d’info",
|
||||||
"LabelName": "Nom",
|
"LabelName": "Nom",
|
||||||
"LabelNarrator": "Narrateur",
|
"LabelNarrator": "Narrateur",
|
||||||
"LabelNarrators": "Narrateurs",
|
"LabelNarrators": "Narrateurs",
|
||||||
@ -324,7 +325,7 @@
|
|||||||
"LabelPodcasts": "Podcasts",
|
"LabelPodcasts": "Podcasts",
|
||||||
"LabelPodcastType": "Type de Podcast",
|
"LabelPodcastType": "Type de Podcast",
|
||||||
"LabelPrefixesToIgnore": "Préfixes à Ignorer (Insensible à la Casse)",
|
"LabelPrefixesToIgnore": "Préfixes à Ignorer (Insensible à la Casse)",
|
||||||
"LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories",
|
"LabelPreventIndexing": "Empêcher l’indexation de votre flux par les bases de donénes iTunes et Google podcast",
|
||||||
"LabelProgress": "Progression",
|
"LabelProgress": "Progression",
|
||||||
"LabelProvider": "Fournisseur",
|
"LabelProvider": "Fournisseur",
|
||||||
"LabelPubDate": "Date de publication",
|
"LabelPubDate": "Date de publication",
|
||||||
@ -384,7 +385,7 @@
|
|||||||
"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. Activer ce paramètre enregistrera les métadonnées dans le dossier de l’article avec une extension « .abs ».",
|
||||||
"LabelSettingsTimeFormat": "Time Format",
|
"LabelSettingsTimeFormat": "Format d’heure",
|
||||||
"LabelShowAll": "Afficher Tout",
|
"LabelShowAll": "Afficher Tout",
|
||||||
"LabelSize": "Taille",
|
"LabelSize": "Taille",
|
||||||
"LabelSleepTimer": "Minuterie",
|
"LabelSleepTimer": "Minuterie",
|
||||||
@ -412,8 +413,9 @@
|
|||||||
"LabelTag": "Étiquette",
|
"LabelTag": "Étiquette",
|
||||||
"LabelTags": "Étiquettes",
|
"LabelTags": "Étiquettes",
|
||||||
"LabelTagsAccessibleToUser": "Étiquettes accessibles à l’utilisateur",
|
"LabelTagsAccessibleToUser": "Étiquettes accessibles à l’utilisateur",
|
||||||
"LabelTasks": "Tasks Running",
|
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
||||||
"LabelTimeBase": "Time Base",
|
"LabelTasks": "Tâches en cours",
|
||||||
|
"LabelTimeBase": "Base de temps",
|
||||||
"LabelTimeListened": "Temps d’écoute",
|
"LabelTimeListened": "Temps d’écoute",
|
||||||
"LabelTimeListenedToday": "Nombres d’écoutes Aujourd’hui",
|
"LabelTimeListenedToday": "Nombres d’écoutes Aujourd’hui",
|
||||||
"LabelTimeRemaining": "{0} restantes",
|
"LabelTimeRemaining": "{0} restantes",
|
||||||
@ -535,7 +537,7 @@
|
|||||||
"MessageNoSearchResultsFor": "Aucun résultat pour la recherche « {0} »",
|
"MessageNoSearchResultsFor": "Aucun résultat pour la recherche « {0} »",
|
||||||
"MessageNoSeries": "Aucune série",
|
"MessageNoSeries": "Aucune série",
|
||||||
"MessageNoTags": "Aucune d’étiquettes",
|
"MessageNoTags": "Aucune d’étiquettes",
|
||||||
"MessageNoTasksRunning": "No Tasks Running",
|
"MessageNoTasksRunning": "Aucune tâche en cours",
|
||||||
"MessageNotYetImplemented": "Non implémenté",
|
"MessageNotYetImplemented": "Non implémenté",
|
||||||
"MessageNoUpdateNecessary": "Aucune mise à jour nécessaire",
|
"MessageNoUpdateNecessary": "Aucune mise à jour nécessaire",
|
||||||
"MessageNoUpdatesWereNecessary": "Aucune mise à jour n’était nécessaire",
|
"MessageNoUpdatesWereNecessary": "Aucune mise à jour n’était nécessaire",
|
||||||
@ -581,7 +583,7 @@
|
|||||||
"PlaceholderNewFolderPath": "Nouveau chemin de dossier",
|
"PlaceholderNewFolderPath": "Nouveau chemin de dossier",
|
||||||
"PlaceholderNewPlaylist": "Nouveau nom de liste de lecture",
|
"PlaceholderNewPlaylist": "Nouveau nom de liste de lecture",
|
||||||
"PlaceholderSearch": "Recherche...",
|
"PlaceholderSearch": "Recherche...",
|
||||||
"PlaceholderSearchEpisode": "Search episode...",
|
"PlaceholderSearchEpisode": "Recherche d’épisode...",
|
||||||
"ToastAccountUpdateFailed": "Échec de la mise à jour du compte",
|
"ToastAccountUpdateFailed": "Échec de la mise à jour du compte",
|
||||||
"ToastAccountUpdateSuccess": "Compte mis à jour",
|
"ToastAccountUpdateSuccess": "Compte mis à jour",
|
||||||
"ToastAuthorImageRemoveFailed": "Échec de la suppression de l’image",
|
"ToastAuthorImageRemoveFailed": "Échec de la suppression de l’image",
|
||||||
|
@ -254,6 +254,7 @@
|
|||||||
"LabelIntervalEveryDay": "Every day",
|
"LabelIntervalEveryDay": "Every day",
|
||||||
"LabelIntervalEveryHour": "Every hour",
|
"LabelIntervalEveryHour": "Every hour",
|
||||||
"LabelInvalidParts": "Invalid Parts",
|
"LabelInvalidParts": "Invalid Parts",
|
||||||
|
"LabelInvert": "Invert",
|
||||||
"LabelItem": "Item",
|
"LabelItem": "Item",
|
||||||
"LabelLanguage": "Language",
|
"LabelLanguage": "Language",
|
||||||
"LabelLanguageDefaultServer": "Default Server Language",
|
"LabelLanguageDefaultServer": "Default Server Language",
|
||||||
@ -412,6 +413,7 @@
|
|||||||
"LabelTag": "Tag",
|
"LabelTag": "Tag",
|
||||||
"LabelTags": "Tags",
|
"LabelTags": "Tags",
|
||||||
"LabelTagsAccessibleToUser": "Tags Accessible to User",
|
"LabelTagsAccessibleToUser": "Tags Accessible to User",
|
||||||
|
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
||||||
"LabelTasks": "Tasks Running",
|
"LabelTasks": "Tasks Running",
|
||||||
"LabelTimeBase": "Time Base",
|
"LabelTimeBase": "Time Base",
|
||||||
"LabelTimeListened": "Time Listened",
|
"LabelTimeListened": "Time Listened",
|
||||||
|
@ -254,6 +254,7 @@
|
|||||||
"LabelIntervalEveryDay": "Every day",
|
"LabelIntervalEveryDay": "Every day",
|
||||||
"LabelIntervalEveryHour": "Every hour",
|
"LabelIntervalEveryHour": "Every hour",
|
||||||
"LabelInvalidParts": "Invalid Parts",
|
"LabelInvalidParts": "Invalid Parts",
|
||||||
|
"LabelInvert": "Invert",
|
||||||
"LabelItem": "Item",
|
"LabelItem": "Item",
|
||||||
"LabelLanguage": "Language",
|
"LabelLanguage": "Language",
|
||||||
"LabelLanguageDefaultServer": "Default Server Language",
|
"LabelLanguageDefaultServer": "Default Server Language",
|
||||||
@ -412,6 +413,7 @@
|
|||||||
"LabelTag": "Tag",
|
"LabelTag": "Tag",
|
||||||
"LabelTags": "Tags",
|
"LabelTags": "Tags",
|
||||||
"LabelTagsAccessibleToUser": "Tags Accessible to User",
|
"LabelTagsAccessibleToUser": "Tags Accessible to User",
|
||||||
|
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
||||||
"LabelTasks": "Tasks Running",
|
"LabelTasks": "Tasks Running",
|
||||||
"LabelTimeBase": "Time Base",
|
"LabelTimeBase": "Time Base",
|
||||||
"LabelTimeListened": "Time Listened",
|
"LabelTimeListened": "Time Listened",
|
||||||
|
@ -254,6 +254,7 @@
|
|||||||
"LabelIntervalEveryDay": "Every day",
|
"LabelIntervalEveryDay": "Every day",
|
||||||
"LabelIntervalEveryHour": "Every hour",
|
"LabelIntervalEveryHour": "Every hour",
|
||||||
"LabelInvalidParts": "Nevaljajuči dijelovi",
|
"LabelInvalidParts": "Nevaljajuči dijelovi",
|
||||||
|
"LabelInvert": "Invert",
|
||||||
"LabelItem": "Stavka",
|
"LabelItem": "Stavka",
|
||||||
"LabelLanguage": "Jezik",
|
"LabelLanguage": "Jezik",
|
||||||
"LabelLanguageDefaultServer": "Default jezik servera",
|
"LabelLanguageDefaultServer": "Default jezik servera",
|
||||||
@ -412,6 +413,7 @@
|
|||||||
"LabelTag": "Tag",
|
"LabelTag": "Tag",
|
||||||
"LabelTags": "Tags",
|
"LabelTags": "Tags",
|
||||||
"LabelTagsAccessibleToUser": "Tags dostupni korisniku",
|
"LabelTagsAccessibleToUser": "Tags dostupni korisniku",
|
||||||
|
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
||||||
"LabelTasks": "Tasks Running",
|
"LabelTasks": "Tasks Running",
|
||||||
"LabelTimeBase": "Time Base",
|
"LabelTimeBase": "Time Base",
|
||||||
"LabelTimeListened": "Vremena odslušano",
|
"LabelTimeListened": "Vremena odslušano",
|
||||||
|
@ -254,6 +254,7 @@
|
|||||||
"LabelIntervalEveryDay": "Ogni Giorno",
|
"LabelIntervalEveryDay": "Ogni Giorno",
|
||||||
"LabelIntervalEveryHour": "Ogni ora",
|
"LabelIntervalEveryHour": "Ogni ora",
|
||||||
"LabelInvalidParts": "Parti Invalide",
|
"LabelInvalidParts": "Parti Invalide",
|
||||||
|
"LabelInvert": "Invert",
|
||||||
"LabelItem": "Oggetti",
|
"LabelItem": "Oggetti",
|
||||||
"LabelLanguage": "Lingua",
|
"LabelLanguage": "Lingua",
|
||||||
"LabelLanguageDefaultServer": "Lingua di Default",
|
"LabelLanguageDefaultServer": "Lingua di Default",
|
||||||
@ -412,6 +413,7 @@
|
|||||||
"LabelTag": "Tag",
|
"LabelTag": "Tag",
|
||||||
"LabelTags": "Tags",
|
"LabelTags": "Tags",
|
||||||
"LabelTagsAccessibleToUser": "Tags permessi agli Utenti",
|
"LabelTagsAccessibleToUser": "Tags permessi agli Utenti",
|
||||||
|
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
||||||
"LabelTasks": "Processi in esecuzione",
|
"LabelTasks": "Processi in esecuzione",
|
||||||
"LabelTimeBase": "Time Base",
|
"LabelTimeBase": "Time Base",
|
||||||
"LabelTimeListened": "Tempo di Ascolto",
|
"LabelTimeListened": "Tempo di Ascolto",
|
||||||
|
@ -254,6 +254,7 @@
|
|||||||
"LabelIntervalEveryDay": "Każdego dnia",
|
"LabelIntervalEveryDay": "Każdego dnia",
|
||||||
"LabelIntervalEveryHour": "Każdej godziny",
|
"LabelIntervalEveryHour": "Każdej godziny",
|
||||||
"LabelInvalidParts": "Nieprawidłowe części",
|
"LabelInvalidParts": "Nieprawidłowe części",
|
||||||
|
"LabelInvert": "Invert",
|
||||||
"LabelItem": "Pozycja",
|
"LabelItem": "Pozycja",
|
||||||
"LabelLanguage": "Język",
|
"LabelLanguage": "Język",
|
||||||
"LabelLanguageDefaultServer": "Domyślny język serwera",
|
"LabelLanguageDefaultServer": "Domyślny język serwera",
|
||||||
@ -412,6 +413,7 @@
|
|||||||
"LabelTag": "Tag",
|
"LabelTag": "Tag",
|
||||||
"LabelTags": "Tagi",
|
"LabelTags": "Tagi",
|
||||||
"LabelTagsAccessibleToUser": "Tagi dostępne dla użytkownika",
|
"LabelTagsAccessibleToUser": "Tagi dostępne dla użytkownika",
|
||||||
|
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
||||||
"LabelTasks": "Tasks Running",
|
"LabelTasks": "Tasks Running",
|
||||||
"LabelTimeBase": "Time Base",
|
"LabelTimeBase": "Time Base",
|
||||||
"LabelTimeListened": "Czas odtwarzania",
|
"LabelTimeListened": "Czas odtwarzania",
|
||||||
|
@ -254,6 +254,7 @@
|
|||||||
"LabelIntervalEveryDay": "Каждый день",
|
"LabelIntervalEveryDay": "Каждый день",
|
||||||
"LabelIntervalEveryHour": "Каждый час",
|
"LabelIntervalEveryHour": "Каждый час",
|
||||||
"LabelInvalidParts": "Неверные части",
|
"LabelInvalidParts": "Неверные части",
|
||||||
|
"LabelInvert": "Invert",
|
||||||
"LabelItem": "Элемент",
|
"LabelItem": "Элемент",
|
||||||
"LabelLanguage": "Язык",
|
"LabelLanguage": "Язык",
|
||||||
"LabelLanguageDefaultServer": "Язык сервера по умолчанию",
|
"LabelLanguageDefaultServer": "Язык сервера по умолчанию",
|
||||||
@ -412,6 +413,7 @@
|
|||||||
"LabelTag": "Тег",
|
"LabelTag": "Тег",
|
||||||
"LabelTags": "Теги",
|
"LabelTags": "Теги",
|
||||||
"LabelTagsAccessibleToUser": "Теги доступные для пользователя",
|
"LabelTagsAccessibleToUser": "Теги доступные для пользователя",
|
||||||
|
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
||||||
"LabelTasks": "Запущенные задачи",
|
"LabelTasks": "Запущенные задачи",
|
||||||
"LabelTimeBase": "Time Base",
|
"LabelTimeBase": "Time Base",
|
||||||
"LabelTimeListened": "Время прослушивания",
|
"LabelTimeListened": "Время прослушивания",
|
||||||
|
@ -254,6 +254,7 @@
|
|||||||
"LabelIntervalEveryDay": "每天",
|
"LabelIntervalEveryDay": "每天",
|
||||||
"LabelIntervalEveryHour": "每小时",
|
"LabelIntervalEveryHour": "每小时",
|
||||||
"LabelInvalidParts": "无效部件",
|
"LabelInvalidParts": "无效部件",
|
||||||
|
"LabelInvert": "Invert",
|
||||||
"LabelItem": "项目",
|
"LabelItem": "项目",
|
||||||
"LabelLanguage": "语言",
|
"LabelLanguage": "语言",
|
||||||
"LabelLanguageDefaultServer": "默认服务器语言",
|
"LabelLanguageDefaultServer": "默认服务器语言",
|
||||||
@ -412,6 +413,7 @@
|
|||||||
"LabelTag": "标签",
|
"LabelTag": "标签",
|
||||||
"LabelTags": "标签",
|
"LabelTags": "标签",
|
||||||
"LabelTagsAccessibleToUser": "用户可访问的标签",
|
"LabelTagsAccessibleToUser": "用户可访问的标签",
|
||||||
|
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
||||||
"LabelTasks": "正在运行的任务",
|
"LabelTasks": "正在运行的任务",
|
||||||
"LabelTimeBase": "Time Base",
|
"LabelTimeBase": "Time Base",
|
||||||
"LabelTimeListened": "收听时间",
|
"LabelTimeListened": "收听时间",
|
||||||
|
@ -48,18 +48,6 @@
|
|||||||
<Mode>rw</Mode>
|
<Mode>rw</Mode>
|
||||||
</Volume>
|
</Volume>
|
||||||
</Data>
|
</Data>
|
||||||
<Environment>
|
|
||||||
<Variable>
|
|
||||||
<Value>99</Value>
|
|
||||||
<Name>AUDIOBOOKSHELF_UID</Name>
|
|
||||||
<Mode/>
|
|
||||||
</Variable>
|
|
||||||
<Variable>
|
|
||||||
<Value>100</Value>
|
|
||||||
<Name>AUDIOBOOKSHELF_GID</Name>
|
|
||||||
<Mode/>
|
|
||||||
</Variable>
|
|
||||||
</Environment>
|
|
||||||
<Labels/>
|
<Labels/>
|
||||||
<Config Name="Audiobooks" Target="/audiobooks" Default="" Mode="rw" Description="Container Path: /audiobooks" Type="Path" Display="always" Required="true" Mask="false" />
|
<Config Name="Audiobooks" Target="/audiobooks" Default="" Mode="rw" Description="Container Path: /audiobooks" Type="Path" Display="always" Required="true" Mask="false" />
|
||||||
<Config Name="Config" Target="/config" Default="/mnt/user/appdata/audiobookshelf/config/" Mode="rw" Description="Container Path: /config" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/audiobookshelf/config/</Config>
|
<Config Name="Config" Target="/config" Default="/mnt/user/appdata/audiobookshelf/config/" Mode="rw" Description="Container Path: /config" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/audiobookshelf/config/</Config>
|
||||||
|
@ -126,12 +126,12 @@ class Auth {
|
|||||||
|
|
||||||
async login(req, res) {
|
async login(req, res) {
|
||||||
const ipAddress = requestIp.getClientIp(req)
|
const ipAddress = requestIp.getClientIp(req)
|
||||||
var username = (req.body.username || '').toLowerCase()
|
const username = (req.body.username || '').toLowerCase()
|
||||||
var password = req.body.password || ''
|
const password = req.body.password || ''
|
||||||
|
|
||||||
var user = this.users.find(u => u.username.toLowerCase() === username)
|
const user = this.users.find(u => u.username.toLowerCase() === username)
|
||||||
|
|
||||||
if (!user || !user.isActive) {
|
if (!user?.isActive) {
|
||||||
Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`)
|
Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`)
|
||||||
if (req.rateLimit.remaining <= 2) {
|
if (req.rateLimit.remaining <= 2) {
|
||||||
Logger.error(`[Auth] Failed login attempt for username ${username} from ip ${ipAddress}. Attempts: ${req.rateLimit.current}`)
|
Logger.error(`[Auth] Failed login attempt for username ${username} from ip ${ipAddress}. Attempts: ${req.rateLimit.current}`)
|
||||||
@ -145,13 +145,15 @@ class Auth {
|
|||||||
if (password) {
|
if (password) {
|
||||||
return res.status(401).send('Invalid root password (hint: there is none)')
|
return res.status(401).send('Invalid root password (hint: there is none)')
|
||||||
} else {
|
} else {
|
||||||
|
Logger.info(`[Auth] ${user.username} logged in from ${ipAddress}`)
|
||||||
return res.json(this.getUserLoginResponsePayload(user))
|
return res.json(this.getUserLoginResponsePayload(user))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check password match
|
// Check password match
|
||||||
var compare = await bcrypt.compare(password, user.pash)
|
const compare = await bcrypt.compare(password, user.pash)
|
||||||
if (compare) {
|
if (compare) {
|
||||||
|
Logger.info(`[Auth] ${user.username} logged in from ${ipAddress}`)
|
||||||
res.json(this.getUserLoginResponsePayload(user))
|
res.json(this.getUserLoginResponsePayload(user))
|
||||||
} else {
|
} else {
|
||||||
Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`)
|
Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`)
|
||||||
|
@ -596,6 +596,7 @@ class LibraryController {
|
|||||||
|
|
||||||
const itemMatches = []
|
const itemMatches = []
|
||||||
const authorMatches = {}
|
const authorMatches = {}
|
||||||
|
const narratorMatches = {}
|
||||||
const seriesMatches = {}
|
const seriesMatches = {}
|
||||||
const tagMatches = {}
|
const tagMatches = {}
|
||||||
|
|
||||||
@ -608,7 +609,7 @@ class LibraryController {
|
|||||||
matchText: queryResult.matchText
|
matchText: queryResult.matchText
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (queryResult.series && queryResult.series.length) {
|
if (queryResult.series?.length) {
|
||||||
queryResult.series.forEach((se) => {
|
queryResult.series.forEach((se) => {
|
||||||
if (!seriesMatches[se.id]) {
|
if (!seriesMatches[se.id]) {
|
||||||
const _series = this.db.series.find(_se => _se.id === se.id)
|
const _series = this.db.series.find(_se => _se.id === se.id)
|
||||||
@ -618,7 +619,7 @@ class LibraryController {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (queryResult.authors && queryResult.authors.length) {
|
if (queryResult.authors?.length) {
|
||||||
queryResult.authors.forEach((au) => {
|
queryResult.authors.forEach((au) => {
|
||||||
if (!authorMatches[au.id]) {
|
if (!authorMatches[au.id]) {
|
||||||
const _author = this.db.authors.find(_au => _au.id === au.id)
|
const _author = this.db.authors.find(_au => _au.id === au.id)
|
||||||
@ -631,7 +632,7 @@ class LibraryController {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (queryResult.tags && queryResult.tags.length) {
|
if (queryResult.tags?.length) {
|
||||||
queryResult.tags.forEach((tag) => {
|
queryResult.tags.forEach((tag) => {
|
||||||
if (!tagMatches[tag]) {
|
if (!tagMatches[tag]) {
|
||||||
tagMatches[tag] = { name: tag, books: [li.toJSON()] }
|
tagMatches[tag] = { name: tag, books: [li.toJSON()] }
|
||||||
@ -640,13 +641,23 @@ class LibraryController {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if (queryResult.narrators?.length) {
|
||||||
|
queryResult.narrators.forEach((narrator) => {
|
||||||
|
if (!narratorMatches[narrator]) {
|
||||||
|
narratorMatches[narrator] = { name: narrator, books: [li.toJSON()] }
|
||||||
|
} else {
|
||||||
|
narratorMatches[narrator].books.push(li.toJSON())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
const itemKey = req.library.mediaType
|
const itemKey = req.library.mediaType
|
||||||
const results = {
|
const results = {
|
||||||
[itemKey]: itemMatches.slice(0, maxResults),
|
[itemKey]: itemMatches.slice(0, maxResults),
|
||||||
tags: Object.values(tagMatches).slice(0, maxResults),
|
tags: Object.values(tagMatches).slice(0, maxResults),
|
||||||
authors: Object.values(authorMatches).slice(0, maxResults),
|
authors: Object.values(authorMatches).slice(0, maxResults),
|
||||||
series: Object.values(seriesMatches).slice(0, maxResults)
|
series: Object.values(seriesMatches).slice(0, maxResults),
|
||||||
|
narrators: Object.values(narratorMatches).slice(0, maxResults)
|
||||||
}
|
}
|
||||||
res.json(results)
|
res.json(results)
|
||||||
}
|
}
|
||||||
|
@ -322,6 +322,7 @@ class Book {
|
|||||||
tags: this.tags.filter(t => cleanStringForSearch(t).includes(query)),
|
tags: this.tags.filter(t => cleanStringForSearch(t).includes(query)),
|
||||||
series: this.metadata.searchSeries(query),
|
series: this.metadata.searchSeries(query),
|
||||||
authors: this.metadata.searchAuthors(query),
|
authors: this.metadata.searchAuthors(query),
|
||||||
|
narrators: this.metadata.searchNarrators(query),
|
||||||
matchKey: null,
|
matchKey: null,
|
||||||
matchText: null
|
matchText: null
|
||||||
}
|
}
|
||||||
@ -336,10 +337,12 @@ class Book {
|
|||||||
} else if (payload.series.length) {
|
} else if (payload.series.length) {
|
||||||
payload.matchKey = 'series'
|
payload.matchKey = 'series'
|
||||||
payload.matchText = this.metadata.seriesName
|
payload.matchText = this.metadata.seriesName
|
||||||
}
|
} else if (payload.tags.length) {
|
||||||
else if (payload.tags.length) {
|
|
||||||
payload.matchKey = 'tags'
|
payload.matchKey = 'tags'
|
||||||
payload.matchText = this.tags.join(', ')
|
payload.matchText = this.tags.join(', ')
|
||||||
|
} else if (payload.narrators.length) {
|
||||||
|
payload.matchKey = 'narrators'
|
||||||
|
payload.matchText = this.metadata.narratorName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return payload
|
return payload
|
||||||
|
@ -381,6 +381,9 @@ class BookMetadata {
|
|||||||
searchAuthors(query) {
|
searchAuthors(query) {
|
||||||
return this.authors.filter(au => cleanStringForSearch(au.name).includes(query))
|
return this.authors.filter(au => cleanStringForSearch(au.name).includes(query))
|
||||||
}
|
}
|
||||||
|
searchNarrators(query) {
|
||||||
|
return this.narrators.filter(n => cleanStringForSearch(n).includes(query))
|
||||||
|
}
|
||||||
searchQuery(query) { // Returns key if match is found
|
searchQuery(query) { // Returns key if match is found
|
||||||
const keysToCheck = ['title', 'asin', 'isbn']
|
const keysToCheck = ['title', 'asin', 'isbn']
|
||||||
for (const key of keysToCheck) {
|
for (const key of keysToCheck) {
|
||||||
|
@ -20,7 +20,7 @@ class User {
|
|||||||
|
|
||||||
this.permissions = {}
|
this.permissions = {}
|
||||||
this.librariesAccessible = [] // Library IDs (Empty if ALL libraries)
|
this.librariesAccessible = [] // Library IDs (Empty if ALL libraries)
|
||||||
this.itemTagsAccessible = [] // Empty if ALL item tags accessible
|
this.itemTagsSelected = [] // Empty if ALL item tags accessible
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
this.construct(user)
|
this.construct(user)
|
||||||
@ -86,7 +86,7 @@ class User {
|
|||||||
createdAt: this.createdAt,
|
createdAt: this.createdAt,
|
||||||
permissions: this.permissions,
|
permissions: this.permissions,
|
||||||
librariesAccessible: [...this.librariesAccessible],
|
librariesAccessible: [...this.librariesAccessible],
|
||||||
itemTagsAccessible: [...this.itemTagsAccessible]
|
itemTagsSelected: [...this.itemTagsSelected]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,7 +105,7 @@ class User {
|
|||||||
createdAt: this.createdAt,
|
createdAt: this.createdAt,
|
||||||
permissions: this.permissions,
|
permissions: this.permissions,
|
||||||
librariesAccessible: [...this.librariesAccessible],
|
librariesAccessible: [...this.librariesAccessible],
|
||||||
itemTagsAccessible: [...this.itemTagsAccessible]
|
itemTagsSelected: [...this.itemTagsSelected]
|
||||||
}
|
}
|
||||||
if (minimal) {
|
if (minimal) {
|
||||||
delete json.mediaProgress
|
delete json.mediaProgress
|
||||||
@ -169,9 +169,14 @@ class User {
|
|||||||
if (this.permissions.accessAllTags === undefined) this.permissions.accessAllTags = true
|
if (this.permissions.accessAllTags === undefined) this.permissions.accessAllTags = true
|
||||||
// Explicit content restriction permission added v2.0.18
|
// Explicit content restriction permission added v2.0.18
|
||||||
if (this.permissions.accessExplicitContent === undefined) this.permissions.accessExplicitContent = true
|
if (this.permissions.accessExplicitContent === undefined) this.permissions.accessExplicitContent = true
|
||||||
|
// itemTagsAccessible was renamed to itemTagsSelected in version v2.2.20
|
||||||
|
if (user.itemTagsAccessible?.length) {
|
||||||
|
this.permissions.selectedTagsNotAccessible = false
|
||||||
|
user.itemTagsSelected = user.itemTagsAccessible
|
||||||
|
}
|
||||||
|
|
||||||
this.librariesAccessible = [...(user.librariesAccessible || [])]
|
this.librariesAccessible = [...(user.librariesAccessible || [])]
|
||||||
this.itemTagsAccessible = [...(user.itemTagsAccessible || [])]
|
this.itemTagsSelected = [...(user.itemTagsSelected || [])]
|
||||||
}
|
}
|
||||||
|
|
||||||
update(payload) {
|
update(payload) {
|
||||||
@ -228,19 +233,21 @@ class User {
|
|||||||
// Update accessible tags
|
// Update accessible tags
|
||||||
if (this.permissions.accessAllTags) {
|
if (this.permissions.accessAllTags) {
|
||||||
// Access all tags
|
// Access all tags
|
||||||
if (this.itemTagsAccessible.length) {
|
if (this.itemTagsSelected.length) {
|
||||||
this.itemTagsAccessible = []
|
this.itemTagsSelected = []
|
||||||
|
this.permissions.selectedTagsNotAccessible = false
|
||||||
hasUpdates = true
|
hasUpdates = true
|
||||||
}
|
}
|
||||||
} else if (payload.itemTagsAccessible !== undefined) {
|
} else if (payload.itemTagsSelected !== undefined) {
|
||||||
if (payload.itemTagsAccessible.length) {
|
if (payload.itemTagsSelected.length) {
|
||||||
if (payload.itemTagsAccessible.join(',') !== this.itemTagsAccessible.join(',')) {
|
if (payload.itemTagsSelected.join(',') !== this.itemTagsSelected.join(',')) {
|
||||||
hasUpdates = true
|
hasUpdates = true
|
||||||
this.itemTagsAccessible = [...payload.itemTagsAccessible]
|
this.itemTagsSelected = [...payload.itemTagsSelected]
|
||||||
}
|
}
|
||||||
} else if (this.itemTagsAccessible.length > 0) {
|
} else if (this.itemTagsSelected.length > 0) {
|
||||||
hasUpdates = true
|
hasUpdates = true
|
||||||
this.itemTagsAccessible = []
|
this.itemTagsSelected = []
|
||||||
|
this.permissions.selectedTagsNotAccessible = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return hasUpdates
|
return hasUpdates
|
||||||
@ -343,8 +350,12 @@ class User {
|
|||||||
|
|
||||||
checkCanAccessLibraryItemWithTags(tags) {
|
checkCanAccessLibraryItemWithTags(tags) {
|
||||||
if (this.permissions.accessAllTags) return true
|
if (this.permissions.accessAllTags) return true
|
||||||
if (!tags || !tags.length) return false
|
if (this.permissions.selectedTagsNotAccessible) {
|
||||||
return this.itemTagsAccessible.some(tag => tags.includes(tag))
|
if (!tags?.length) return true
|
||||||
|
return tags.every(tag => !this.itemTagsSelected.includes(tag))
|
||||||
|
}
|
||||||
|
if (!tags?.length) return false
|
||||||
|
return this.itemTagsSelected.some(tag => tags.includes(tag))
|
||||||
}
|
}
|
||||||
|
|
||||||
checkCanAccessLibraryItem(libraryItem) {
|
checkCanAccessLibraryItem(libraryItem) {
|
||||||
|
@ -92,7 +92,6 @@ module.exports.setDefault = (path, silent = false) => {
|
|||||||
const gid = global.Gid
|
const gid = global.Gid
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (isNaN(uid) || isNaN(gid)) {
|
if (isNaN(uid) || isNaN(gid)) {
|
||||||
if (!silent) Logger.debug('Not modifying permissions since no uid/gid is specified')
|
|
||||||
return resolve()
|
return resolve()
|
||||||
}
|
}
|
||||||
if (!silent) Logger.debug(`Setting permission "${mode}" for uid ${uid} and gid ${gid} | "${path}"`)
|
if (!silent) Logger.debug(`Setting permission "${mode}" for uid ${uid} and gid ${gid} | "${path}"`)
|
||||||
|
@ -174,20 +174,24 @@ async function recurseFiles(path, relPathToReplace = null) {
|
|||||||
}
|
}
|
||||||
module.exports.recurseFiles = recurseFiles
|
module.exports.recurseFiles = recurseFiles
|
||||||
|
|
||||||
module.exports.downloadFile = async (url, filepath) => {
|
module.exports.downloadFile = (url, filepath) => {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
Logger.debug(`[fileUtils] Downloading file to ${filepath}`)
|
Logger.debug(`[fileUtils] Downloading file to ${filepath}`)
|
||||||
|
axios({
|
||||||
const writer = fs.createWriteStream(filepath)
|
|
||||||
const response = await axios({
|
|
||||||
url,
|
url,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
responseType: 'stream',
|
responseType: 'stream',
|
||||||
timeout: 30000
|
timeout: 30000
|
||||||
})
|
}).then((response) => {
|
||||||
|
const writer = fs.createWriteStream(filepath)
|
||||||
response.data.pipe(writer)
|
response.data.pipe(writer)
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
writer.on('finish', resolve)
|
writer.on('finish', resolve)
|
||||||
writer.on('error', reject)
|
writer.on('error', reject)
|
||||||
|
}).catch((err) => {
|
||||||
|
Logger.error(`[fileUtils] Failed to download file "${filepath}"`, err)
|
||||||
|
reject(err)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user