mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-01-06 06:31:19 -05:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24923c0009 | ||
|
|
a9036c9738 | ||
|
|
f9f7fbed33 | ||
|
|
53b5bee736 | ||
|
|
d0b3726905 | ||
|
|
7a6864507e | ||
|
|
e20563f2e1 | ||
|
|
fea5f8f3d4 | ||
|
|
f9bb529b85 | ||
|
|
60e348fcc1 | ||
|
|
f194c5be0e | ||
|
|
47712e63f1 | ||
|
|
790c1fb34a | ||
|
|
9cca731acc | ||
|
|
48f232790a | ||
|
|
3c55aa5f43 | ||
|
|
8c1edb30a6 | ||
|
|
5e64af4448 | ||
|
|
9f60017cfe | ||
|
|
b6a86d11d2 | ||
|
|
db86bfd63d | ||
|
|
7ff72a8920 | ||
|
|
2c4f86d148 |
@@ -108,9 +108,9 @@ export default {
|
||||
if (res.warning) {
|
||||
this.$toast.warning(res.warning)
|
||||
} else if (res.updated) {
|
||||
this.$toast.success(this.$strings.ToastNoUpdatesNecessary)
|
||||
this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
|
||||
} else {
|
||||
this.$toast.info(this.$strings.ToastItemDetailsUpdateUnneeded)
|
||||
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -170,7 +170,7 @@ export default {
|
||||
this.isProcessing = false
|
||||
if (updateResult) {
|
||||
if (updateResult.updated) {
|
||||
this.$toast.success(this.$strings.MessageItemDetailsUpdated)
|
||||
this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
|
||||
return true
|
||||
} else {
|
||||
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
|
||||
|
||||
@@ -3,67 +3,67 @@
|
||||
<form class="w-full h-full px-2 md:px-4 py-6" @submit.prevent="submitForm">
|
||||
<div class="flex flex-wrap -mx-1">
|
||||
<div class="w-full md:w-1/2 px-1">
|
||||
<ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" />
|
||||
<ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="flex-grow px-1 mt-2 md:mt-0">
|
||||
<ui-text-input-with-label ref="subtitleInput" v-model="details.subtitle" :label="$strings.LabelSubtitle" />
|
||||
<ui-text-input-with-label ref="subtitleInput" v-model="details.subtitle" :label="$strings.LabelSubtitle" @input="handleInputChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap mt-2 -mx-1">
|
||||
<div class="w-full md:w-3/4 px-1">
|
||||
<!-- Authors filter only contains authors in this library, uses filter data -->
|
||||
<ui-multi-select-query-input ref="authorsSelect" v-model="details.authors" :label="$strings.LabelAuthors" filter-key="authors" />
|
||||
<ui-multi-select-query-input ref="authorsSelect" v-model="details.authors" :label="$strings.LabelAuthors" filter-key="authors" @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="flex-grow px-1 mt-2 md:mt-0">
|
||||
<ui-text-input-with-label ref="publishYearInput" v-model="details.publishedYear" type="number" :label="$strings.LabelPublishYear" />
|
||||
<ui-text-input-with-label ref="publishYearInput" v-model="details.publishedYear" type="number" :label="$strings.LabelPublishYear" @input="handleInputChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex mt-2 -mx-1">
|
||||
<div class="flex-grow px-1">
|
||||
<widgets-series-input-widget v-model="details.series" />
|
||||
<widgets-series-input-widget v-model="details.series" @input="handleInputChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" :label="$strings.LabelDescription" class="mt-2" />
|
||||
<ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" :label="$strings.LabelDescription" class="mt-2" @input="handleInputChange" />
|
||||
|
||||
<div class="flex flex-wrap mt-2 -mx-1">
|
||||
<div class="w-full md:w-1/2 px-1">
|
||||
<ui-multi-select ref="genresSelect" v-model="details.genres" :label="$strings.LabelGenres" :items="genres" />
|
||||
<ui-multi-select ref="genresSelect" v-model="details.genres" :label="$strings.LabelGenres" :items="genres" @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="flex-grow px-1 mt-2 md:mt-0">
|
||||
<ui-multi-select ref="tagsSelect" v-model="newTags" :label="$strings.LabelTags" :items="tags" />
|
||||
<ui-multi-select ref="tagsSelect" v-model="newTags" :label="$strings.LabelTags" :items="tags" @input="handleInputChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap mt-2 -mx-1">
|
||||
<div class="w-full md:w-1/2 px-1">
|
||||
<ui-multi-select ref="narratorsSelect" v-model="details.narrators" :label="$strings.LabelNarrators" :items="narrators" />
|
||||
<ui-multi-select ref="narratorsSelect" v-model="details.narrators" :label="$strings.LabelNarrators" :items="narrators" @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
|
||||
<ui-text-input-with-label ref="isbnInput" v-model="details.isbn" label="ISBN" />
|
||||
<ui-text-input-with-label ref="isbnInput" v-model="details.isbn" label="ISBN" @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
|
||||
<ui-text-input-with-label ref="asinInput" v-model="details.asin" label="ASIN" />
|
||||
<ui-text-input-with-label ref="asinInput" v-model="details.asin" label="ASIN" @input="handleInputChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap mt-2 -mx-1">
|
||||
<div class="w-full md:w-1/4 px-1">
|
||||
<ui-text-input-with-label ref="publisherInput" v-model="details.publisher" :label="$strings.LabelPublisher" />
|
||||
<ui-text-input-with-label ref="publisherInput" v-model="details.publisher" :label="$strings.LabelPublisher" @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
|
||||
<ui-text-input-with-label ref="languageInput" v-model="details.language" :label="$strings.LabelLanguage" />
|
||||
<ui-text-input-with-label ref="languageInput" v-model="details.language" :label="$strings.LabelLanguage" @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="flex-grow px-1 pt-6 mt-2 md:mt-0">
|
||||
<div class="flex justify-center">
|
||||
<ui-checkbox v-model="details.explicit" :label="$strings.LabelExplicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
|
||||
<ui-checkbox v-model="details.explicit" :label="$strings.LabelExplicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" @input="handleInputChange" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow px-1 pt-6 mt-2 md:mt-0">
|
||||
<div class="flex justify-center">
|
||||
<ui-checkbox v-model="details.abridged" :label="$strings.LabelAbridged" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
|
||||
<ui-checkbox v-model="details.abridged" :label="$strings.LabelAbridged" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" @input="handleInputChange" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -132,6 +132,12 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleInputChange() {
|
||||
this.$emit('change', {
|
||||
libraryItemId: this.libraryItem.id,
|
||||
hasChanges: this.checkForChanges().hasChanges
|
||||
})
|
||||
},
|
||||
getDetails() {
|
||||
this.forceBlur()
|
||||
return this.checkForChanges()
|
||||
@@ -172,6 +178,7 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
this.handleInputChange()
|
||||
},
|
||||
forceBlur() {
|
||||
if (this.$refs.titleInput) this.$refs.titleInput.blur()
|
||||
@@ -286,4 +293,4 @@ export default {
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -3,45 +3,45 @@
|
||||
<form class="w-full h-full px-4 py-6" @submit.prevent="submitForm">
|
||||
<div class="flex -mx-1">
|
||||
<div class="w-1/2 px-1">
|
||||
<ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" />
|
||||
<ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="flex-grow px-1">
|
||||
<ui-text-input-with-label ref="authorInput" v-model="details.author" :label="$strings.LabelAuthor" />
|
||||
<ui-text-input-with-label ref="authorInput" v-model="details.author" :label="$strings.LabelAuthor" @input="handleInputChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ui-text-input-with-label ref="feedUrlInput" v-model="details.feedUrl" :label="$strings.LabelRSSFeedURL" class="mt-2" />
|
||||
<ui-text-input-with-label ref="feedUrlInput" v-model="details.feedUrl" :label="$strings.LabelRSSFeedURL" class="mt-2" @input="handleInputChange" />
|
||||
|
||||
<ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" :label="$strings.LabelDescription" class="mt-2" />
|
||||
<ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" :label="$strings.LabelDescription" class="mt-2" @input="handleInputChange" />
|
||||
|
||||
<div class="flex mt-2 -mx-1">
|
||||
<div class="w-1/2 px-1">
|
||||
<ui-multi-select ref="genresSelect" v-model="details.genres" :label="$strings.LabelGenres" :items="genres" />
|
||||
<ui-multi-select ref="genresSelect" v-model="details.genres" :label="$strings.LabelGenres" :items="genres" @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="flex-grow px-1">
|
||||
<ui-multi-select ref="tagsSelect" v-model="newTags" :label="$strings.LabelTags" :items="tags" />
|
||||
<ui-multi-select ref="tagsSelect" v-model="newTags" :label="$strings.LabelTags" :items="tags" @input="handleInputChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex mt-2 -mx-1">
|
||||
<div class="w-1/4 px-1">
|
||||
<ui-text-input-with-label ref="releaseDateInput" v-model="details.releaseDate" :label="$strings.LabelReleaseDate" />
|
||||
<ui-text-input-with-label ref="releaseDateInput" v-model="details.releaseDate" :label="$strings.LabelReleaseDate" @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="w-1/4 px-1">
|
||||
<ui-text-input-with-label ref="itunesIdInput" v-model="details.itunesId" label="iTunes ID" />
|
||||
<ui-text-input-with-label ref="itunesIdInput" v-model="details.itunesId" label="iTunes ID" @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="w-1/4 px-1">
|
||||
<ui-text-input-with-label ref="languageInput" v-model="details.language" :label="$strings.LabelLanguage" />
|
||||
<ui-text-input-with-label ref="languageInput" v-model="details.language" :label="$strings.LabelLanguage" @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="flex-grow px-1 pt-6">
|
||||
<div class="flex justify-center">
|
||||
<ui-checkbox v-model="details.explicit" :label="$strings.LabelExplicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
|
||||
<ui-checkbox v-model="details.explicit" :label="$strings.LabelExplicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" @input="handleInputChange" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex mt-2 -mx-1">
|
||||
<div class="w-1/4 px-1">
|
||||
<ui-dropdown :label="$strings.LabelPodcastType" v-model="details.type" :items="podcastTypes" small class="max-w-52" />
|
||||
<ui-dropdown :label="$strings.LabelPodcastType" v-model="details.type" :items="podcastTypes" small class="max-w-52" @input="handleInputChange" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -105,6 +105,12 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleInputChange() {
|
||||
this.$emit('change', {
|
||||
libraryItemId: this.libraryItem.id,
|
||||
hasChanges: this.checkForChanges().hasChanges
|
||||
})
|
||||
},
|
||||
getDetails() {
|
||||
this.forceBlur()
|
||||
return this.checkForChanges()
|
||||
@@ -136,6 +142,8 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.handleInputChange()
|
||||
},
|
||||
forceBlur() {
|
||||
if (this.$refs.titleInput) this.$refs.titleInput.blur()
|
||||
|
||||
4
client/package-lock.json
generated
4
client/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.13.1",
|
||||
"version": "2.13.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.13.1",
|
||||
"version": "2.13.3",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@nuxtjs/axios": "^5.13.6",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.13.1",
|
||||
"version": "2.13.3",
|
||||
"buildNumber": 1,
|
||||
"description": "Self-hosted audiobook and podcast client",
|
||||
"main": "index.js",
|
||||
|
||||
@@ -97,8 +97,8 @@
|
||||
<div class="flex justify-center flex-wrap">
|
||||
<template v-for="libraryItem in libraryItemCopies">
|
||||
<div :key="libraryItem.id" class="w-full max-w-3xl border border-black-300 p-6 -ml-px -mt-px">
|
||||
<widgets-book-details-edit v-if="libraryItem.mediaType === 'book'" :ref="`itemForm-${libraryItem.id}`" :library-item="libraryItem" />
|
||||
<widgets-podcast-details-edit v-else :ref="`itemForm-${libraryItem.id}`" :library-item="libraryItem" />
|
||||
<widgets-book-details-edit v-if="libraryItem.mediaType === 'book'" :ref="`itemForm-${libraryItem.id}`" :library-item="libraryItem" @change="handleItemChange" />
|
||||
<widgets-podcast-details-edit v-else :ref="`itemForm-${libraryItem.id}`" :library-item="libraryItem" @change="handleItemChange" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -108,7 +108,7 @@
|
||||
|
||||
<div :class="isScrollable ? 'fixed left-0 box-shadow-lg-up bg-primary' : ''" class="w-full h-20 px-4 flex items-center border-t border-bg z-40" :style="{ bottom: streamLibraryItem ? '165px' : '0px' }">
|
||||
<div class="flex-grow" />
|
||||
<ui-btn color="success" :padding-x="8" class="text-lg" :loading="isProcessing" @click.prevent="saveClick">{{ $strings.ButtonSave }}</ui-btn>
|
||||
<ui-btn color="success" :padding-x="8" class="text-lg" :loading="isProcessing" :disabled="!hasChanges" @click.prevent="saveClick">{{ $strings.ButtonSave }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -170,7 +170,8 @@ export default {
|
||||
abridged: false
|
||||
},
|
||||
appendableKeys: ['authors', 'genres', 'tags', 'narrators', 'series'],
|
||||
openMapOptions: false
|
||||
openMapOptions: false,
|
||||
itemsWithChanges: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -221,9 +222,19 @@ export default {
|
||||
},
|
||||
hasSelectedBatchUsage() {
|
||||
return Object.values(this.selectedBatchUsage).some((b) => !!b)
|
||||
},
|
||||
hasChanges() {
|
||||
return this.itemsWithChanges.length > 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleItemChange(itemChange) {
|
||||
if (!itemChange.hasChanges) {
|
||||
this.itemsWithChanges = this.itemsWithChanges.filter((id) => id !== itemChange.libraryItemId)
|
||||
} else if (!this.itemsWithChanges.includes(itemChange.libraryItemId)) {
|
||||
this.itemsWithChanges.push(itemChange.libraryItemId)
|
||||
}
|
||||
},
|
||||
blurBatchForm() {
|
||||
if (this.$refs.seriesSelect && this.$refs.seriesSelect.isFocused) {
|
||||
this.$refs.seriesSelect.forceBlur()
|
||||
@@ -283,38 +294,10 @@ export default {
|
||||
removedSeriesItem(item) {},
|
||||
newNarratorItem(item) {},
|
||||
removedNarratorItem(item) {},
|
||||
newTagItem(item) {
|
||||
// if (item && !this.newTagItems.includes(item)) {
|
||||
// this.newTagItems.push(item)
|
||||
// }
|
||||
},
|
||||
removedTagItem(item) {
|
||||
// If newly added, remove if not used on any other items
|
||||
// if (item && this.newTagItems.includes(item)) {
|
||||
// var usedByOtherAb = this.libraryItemCopies.find((ab) => {
|
||||
// return ab.tags && ab.tags.includes(item)
|
||||
// })
|
||||
// if (!usedByOtherAb) {
|
||||
// this.newTagItems = this.newTagItems.filter((t) => t !== item)
|
||||
// }
|
||||
// }
|
||||
},
|
||||
newGenreItem(item) {
|
||||
// if (item && !this.newGenreItems.includes(item)) {
|
||||
// this.newGenreItems.push(item)
|
||||
// }
|
||||
},
|
||||
removedGenreItem(item) {
|
||||
// If newly added, remove if not used on any other items
|
||||
// if (item && this.newGenreItems.includes(item)) {
|
||||
// var usedByOtherAb = this.libraryItemCopies.find((ab) => {
|
||||
// return ab.book.genres && ab.book.genres.includes(item)
|
||||
// })
|
||||
// if (!usedByOtherAb) {
|
||||
// this.newGenreItems = this.newGenreItems.filter((t) => t !== item)
|
||||
// }
|
||||
// }
|
||||
},
|
||||
newTagItem(item) {},
|
||||
removedTagItem(item) {},
|
||||
newGenreItem(item) {},
|
||||
removedGenreItem(item) {},
|
||||
init() {
|
||||
// TODO: Better deep cloning of library items
|
||||
this.libraryItemCopies = this.libraryItems.map((li) => {
|
||||
@@ -376,6 +359,7 @@ export default {
|
||||
.then((data) => {
|
||||
this.isProcessing = false
|
||||
if (data.updates) {
|
||||
this.itemsWithChanges = []
|
||||
this.$toast.success(`Successfully updated ${data.updates} items`)
|
||||
this.$router.replace(`/library/${this.currentLibraryId}/bookshelf`)
|
||||
} else {
|
||||
@@ -387,10 +371,28 @@ export default {
|
||||
this.$toast.error('Failed to batch update')
|
||||
this.isProcessing = false
|
||||
})
|
||||
},
|
||||
beforeUnload(e) {
|
||||
if (!e || !this.hasChanges) return
|
||||
e.preventDefault()
|
||||
e.returnValue = ''
|
||||
}
|
||||
},
|
||||
beforeRouteLeave(to, from, next) {
|
||||
if (this.hasChanges) {
|
||||
next(false)
|
||||
window.location = to.path
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.init()
|
||||
|
||||
window.addEventListener('beforeunload', this.beforeUnload)
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('beforeunload', this.beforeUnload)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"ButtonApply": "প্রয়োগ করুন",
|
||||
"ButtonApplyChapters": "অধ্যায় প্রয়োগ করুন",
|
||||
"ButtonAuthors": "লেখক",
|
||||
"ButtonBack": "পেছনে যান",
|
||||
"ButtonBrowseForFolder": "ফোল্ডারের জন্য ব্রাউজ করুন",
|
||||
"ButtonCancel": "বাতিল করুন",
|
||||
"ButtonCancelEncode": "এনকোড বাতিল করুন",
|
||||
@@ -18,6 +19,7 @@
|
||||
"ButtonChooseFiles": "ফাইল চয়ন করুন",
|
||||
"ButtonClearFilter": "ফিল্টার পরিষ্কার করুন",
|
||||
"ButtonCloseFeed": "ফিড বন্ধ করুন",
|
||||
"ButtonCloseSession": "খোলা সেশন বন্ধ করুন",
|
||||
"ButtonCollections": "সংগ্রহ",
|
||||
"ButtonConfigureScanner": "স্ক্যানার কনফিগার করুন",
|
||||
"ButtonCreate": "তৈরি করুন",
|
||||
@@ -27,6 +29,9 @@
|
||||
"ButtonEdit": "সম্পাদনা করুন",
|
||||
"ButtonEditChapters": "অধ্যায় সম্পাদনা করুন",
|
||||
"ButtonEditPodcast": "পডকাস্ট সম্পাদনা করুন",
|
||||
"ButtonEnable": "সক্রিয় করুন",
|
||||
"ButtonFireAndFail": "সক্রিয় এবং ব্যর্থ",
|
||||
"ButtonFireOnTest": "পরীক্ষামূলক ইভেন্টে সক্রিয় করুন",
|
||||
"ButtonForceReScan": "জোরপূর্বক পুনরায় স্ক্যান করুন",
|
||||
"ButtonFullPath": "সম্পূর্ণ পথ",
|
||||
"ButtonHide": "লুকান",
|
||||
@@ -45,6 +50,7 @@
|
||||
"ButtonNevermind": "কিছু মনে করবেন না",
|
||||
"ButtonNext": "পরবর্তী",
|
||||
"ButtonNextChapter": "পরবর্তী অধ্যায়",
|
||||
"ButtonNextItemInQueue": "সারিতে পরের আইটেম",
|
||||
"ButtonOk": "ঠিক আছে",
|
||||
"ButtonOpenFeed": "ফিড খুলুন",
|
||||
"ButtonOpenManager": "ম্যানেজার খুলুন",
|
||||
@@ -54,13 +60,17 @@
|
||||
"ButtonPlaylists": "প্লেলিস্ট",
|
||||
"ButtonPrevious": "পূর্ববর্তী",
|
||||
"ButtonPreviousChapter": "আগের অধ্যায়",
|
||||
"ButtonProbeAudioFile": "প্রোব অডিও ফাইল",
|
||||
"ButtonPurgeAllCache": "সমস্ত ক্যাশে পরিষ্কার করুন",
|
||||
"ButtonPurgeItemsCache": "আইটেম ক্যাশে পরিষ্কার করুন",
|
||||
"ButtonQueueAddItem": "সারিতে যোগ করুন",
|
||||
"ButtonQueueRemoveItem": "সারি থেকে মুছে ফেলুন",
|
||||
"ButtonQuickEmbedMetadata": "মেটাডেটা দ্রুত এম্বেড করুন",
|
||||
"ButtonQuickMatch": "দ্রুত ম্যাচ",
|
||||
"ButtonReScan": "পুনরায় স্ক্যান",
|
||||
"ButtonRead": "পড়ুন",
|
||||
"ButtonReadLess": "সংক্ষিপ্ত",
|
||||
"ButtonReadMore": "বিস্তারিত পড়ুন",
|
||||
"ButtonRefresh": "রিফ্রেশ",
|
||||
"ButtonRemove": "মুছে ফেলুন",
|
||||
"ButtonRemoveAll": "সব মুছে ফেলুন",
|
||||
@@ -85,6 +95,7 @@
|
||||
"ButtonShow": "দেখান",
|
||||
"ButtonStartM4BEncode": "M4B এনকোড শুরু করুন",
|
||||
"ButtonStartMetadataEmbed": "মেটাডেটা এম্বেড শুরু করুন",
|
||||
"ButtonStats": "পরিসংখ্যান",
|
||||
"ButtonSubmit": "জমা দিন",
|
||||
"ButtonTest": "পরীক্ষা",
|
||||
"ButtonUpload": "আপলোড",
|
||||
@@ -99,9 +110,10 @@
|
||||
"ErrorUploadFetchMetadataNoResults": "মেটাডেটা আনা যায়নি - শিরোনাম এবং/অথবা লেখক আপডেট করার চেষ্টা করুন",
|
||||
"ErrorUploadLacksTitle": "একটি শিরোনাম থাকতে হবে",
|
||||
"HeaderAccount": "অ্যাকাউন্ট",
|
||||
"HeaderAddCustomMetadataProvider": "কাস্টম মেটাডেটা সরবরাহকারী যোগ করুন",
|
||||
"HeaderAdvanced": "অ্যাডভান্সড",
|
||||
"HeaderAppriseNotificationSettings": "বিজ্ঞপ্তি সেটিংস অবহিত করুন",
|
||||
"HeaderAudioTracks": "অডিও ট্র্যাকস",
|
||||
"HeaderAudioTracks": "অডিও ট্র্যাকসগুলো",
|
||||
"HeaderAudiobookTools": "অডিওবই ফাইল ম্যানেজমেন্ট টুলস",
|
||||
"HeaderAuthentication": "প্রমাণীকরণ",
|
||||
"HeaderBackups": "ব্যাকআপ",
|
||||
@@ -112,6 +124,7 @@
|
||||
"HeaderCollectionItems": "সংগ্রহ আইটেম",
|
||||
"HeaderCover": "কভার",
|
||||
"HeaderCurrentDownloads": "বর্তমান ডাউনলোডগুলি",
|
||||
"HeaderCustomMessageOnLogin": "লগইন এ কাস্টম বার্তা",
|
||||
"HeaderCustomMetadataProviders": "কাস্টম মেটাডেটা প্রদানকারী",
|
||||
"HeaderDetails": "বিস্তারিত",
|
||||
"HeaderDownloadQueue": "ডাউনলোড সারি",
|
||||
@@ -143,6 +156,8 @@
|
||||
"HeaderMetadataToEmbed": "এম্বেড করার জন্য মেটাডেটা",
|
||||
"HeaderNewAccount": "নতুন অ্যাকাউন্ট",
|
||||
"HeaderNewLibrary": "নতুন লাইব্রেরি",
|
||||
"HeaderNotificationCreate": "বিজ্ঞপ্তি তৈরি করুন",
|
||||
"HeaderNotificationUpdate": "বিজ্ঞপ্তি আপডেট করুন",
|
||||
"HeaderNotifications": "বিজ্ঞপ্তি",
|
||||
"HeaderOpenIDConnectAuthentication": "ওপেনআইডি সংযোগ প্রমাণীকরণ",
|
||||
"HeaderOpenRSSFeed": "আরএসএস ফিড খুলুন",
|
||||
@@ -150,6 +165,7 @@
|
||||
"HeaderPasswordAuthentication": "পাসওয়ার্ড প্রমাণীকরণ",
|
||||
"HeaderPermissions": "অনুমতি",
|
||||
"HeaderPlayerQueue": "প্লেয়ার সারি",
|
||||
"HeaderPlayerSettings": "প্লেয়ার সেটিংস",
|
||||
"HeaderPlaylist": "প্লেলিস্ট",
|
||||
"HeaderPlaylistItems": "প্লেলিস্ট আইটেম",
|
||||
"HeaderPodcastsToAdd": "যোগ করার জন্য পডকাস্ট",
|
||||
@@ -186,6 +202,9 @@
|
||||
"HeaderYearReview": "বাৎসরিক পর্যালোচনা {0}",
|
||||
"HeaderYourStats": "আপনার পরিসংখ্যান",
|
||||
"LabelAbridged": "সংক্ষিপ্ত",
|
||||
"LabelAbridgedChecked": "সংক্ষিপ্ত (চেক)",
|
||||
"LabelAbridgedUnchecked": "অসংক্ষেপিত (চেক করা হয়নি)",
|
||||
"LabelAccessibleBy": "দ্বারা প্রবেশযোগ্য",
|
||||
"LabelAccountType": "অ্যাকাউন্টের প্রকার",
|
||||
"LabelAccountTypeAdmin": "প্রশাসন",
|
||||
"LabelAccountTypeGuest": "অতিথি",
|
||||
@@ -196,6 +215,7 @@
|
||||
"LabelAddToPlaylist": "প্লেলিস্টে যোগ করুন",
|
||||
"LabelAddToPlaylistBatch": "প্লেলিস্টে {0}টি আইটেম যোগ করুন",
|
||||
"LabelAddedAt": "এতে যোগ করা হয়েছে",
|
||||
"LabelAddedDate": "যোগ করা হয়েছে {0}",
|
||||
"LabelAdminUsersOnly": "শুধু অ্যাডমিন ব্যবহারকারী",
|
||||
"LabelAll": "সব",
|
||||
"LabelAllUsers": "সমস্ত ব্যবহারকারী",
|
||||
@@ -225,6 +245,7 @@
|
||||
"LabelBitrate": "বিটরেট",
|
||||
"LabelBooks": "বইগুলো",
|
||||
"LabelButtonText": "ঘর পাঠ্য",
|
||||
"LabelByAuthor": "দ্বারা {0}",
|
||||
"LabelChangePassword": "পাসওয়ার্ড পরিবর্তন করুন",
|
||||
"LabelChannels": "চ্যানেল",
|
||||
"LabelChapterTitle": "অধ্যায়ের শিরোনাম",
|
||||
@@ -234,6 +255,7 @@
|
||||
"LabelClosePlayer": "প্লেয়ার বন্ধ করুন",
|
||||
"LabelCodec": "কোডেক",
|
||||
"LabelCollapseSeries": "সিরিজ সঙ্কুচিত করুন",
|
||||
"LabelCollapseSubSeries": "উপ-সিরিজ সঙ্কুচিত করুন",
|
||||
"LabelCollection": "সংগ্রহ",
|
||||
"LabelCollections": "সংগ্রহ",
|
||||
"LabelComplete": "সম্পূর্ণ",
|
||||
@@ -249,6 +271,7 @@
|
||||
"LabelCurrently": "বর্তমানে:",
|
||||
"LabelCustomCronExpression": "কাস্টম Cron এক্সপ্রেশন:",
|
||||
"LabelDatetime": "তারিখ সময়",
|
||||
"LabelDays": "দিনগুলো",
|
||||
"LabelDeleteFromFileSystemCheckbox": "ফাইল সিস্টেম থেকে মুছে ফেলুন (শুধু ডাটাবেস থেকে সরাতে টিক চিহ্ন মুক্ত করুন)",
|
||||
"LabelDescription": "বিবরণ",
|
||||
"LabelDeselectAll": "সমস্ত অনির্বাচিত করুন",
|
||||
@@ -262,12 +285,16 @@
|
||||
"LabelDownload": "ডাউনলোড করুন",
|
||||
"LabelDownloadNEpisodes": "{0}টি পর্ব ডাউনলোড করুন",
|
||||
"LabelDuration": "সময়কাল",
|
||||
"LabelDurationComparisonExactMatch": "(সঠিক মিল)",
|
||||
"LabelDurationComparisonLonger": "({0} দীর্ঘ)",
|
||||
"LabelDurationComparisonShorter": "({0} ছোট)",
|
||||
"LabelDurationFound": "সময়কাল পাওয়া গেছে:",
|
||||
"LabelEbook": "ই-বই",
|
||||
"LabelEbooks": "ই-বইগুলো",
|
||||
"LabelEdit": "সম্পাদনা করুন",
|
||||
"LabelEmail": "ইমেইল",
|
||||
"LabelEmailSettingsFromAddress": "ঠিকানা থেকে",
|
||||
"LabelEmailSettingsRejectUnauthorized": "অননুমোদিত সার্টিফিকেট প্রত্যাখ্যান করুন",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to।",
|
||||
"LabelEmailSettingsSecure": "নিরাপদ",
|
||||
"LabelEmailSettingsSecureHelp": "যদি সত্য হয় সার্ভারের সাথে সংযোগ করার সময় সংযোগটি TLS ব্যবহার করবে। মিথ্যা হলে TLS ব্যবহার করা হবে যদি সার্ভার STARTTLS এক্সটেনশন সমর্থন করে। বেশিরভাগ ক্ষেত্রে এই মানটিকে সত্য হিসাবে সেট করুন যদি আপনি পোর্ট 465-এর সাথে সংযোগ করছেন। পোর্ট 587 বা পোর্টের জন্য 25 এটি মিথ্যা রাখুন। (nodemailer.com/smtp/#authentication থেকে)",
|
||||
@@ -275,10 +302,13 @@
|
||||
"LabelEmbeddedCover": "এম্বেডেড কভার",
|
||||
"LabelEnable": "সক্ষম করুন",
|
||||
"LabelEnd": "সমাপ্ত",
|
||||
"LabelEndOfChapter": "অধ্যায়ের সমাপ্তি",
|
||||
"LabelEpisode": "পর্ব",
|
||||
"LabelEpisodeTitle": "পর্বের শিরোনাম",
|
||||
"LabelEpisodeType": "পর্বের ধরন",
|
||||
"LabelEpisodes": "পর্বগুলো",
|
||||
"LabelExample": "উদাহরণ",
|
||||
"LabelExpandSeries": "সিরিজ প্রসারিত করুন",
|
||||
"LabelExplicit": "বিশদ",
|
||||
"LabelFeedURL": "ফিড ইউআরএল",
|
||||
"LabelFetchingMetadata": "মেটাডেটা আনা হচ্ছে",
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
"ButtonStats": "Statistiken",
|
||||
"ButtonSubmit": "Ok",
|
||||
"ButtonTest": "Test",
|
||||
"ButtonUnlinkOpedId": "OpenID trennen",
|
||||
"ButtonUpload": "Hochladen",
|
||||
"ButtonUploadBackup": "Sicherung hochladen",
|
||||
"ButtonUploadCover": "Titelbild hochladen",
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
"ButtonStats": "Stats",
|
||||
"ButtonSubmit": "Submit",
|
||||
"ButtonTest": "Test",
|
||||
"ButtonUnlinkOpedId": "Unlink OpenID",
|
||||
"ButtonUnlinkOpenId": "Unlink OpenID",
|
||||
"ButtonUpload": "Upload",
|
||||
"ButtonUploadBackup": "Upload Backup",
|
||||
"ButtonUploadCover": "Upload Cover",
|
||||
|
||||
@@ -97,7 +97,6 @@
|
||||
"ButtonStats": "Estadísticas",
|
||||
"ButtonSubmit": "Enviar",
|
||||
"ButtonTest": "Prueba",
|
||||
"ButtonUnlinkOpedId": "Desvincular OpenID",
|
||||
"ButtonUpload": "Subir",
|
||||
"ButtonUploadBackup": "Subir Respaldo",
|
||||
"ButtonUploadCover": "Subir Portada",
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
"ButtonStats": "Statistiques",
|
||||
"ButtonSubmit": "Soumettre",
|
||||
"ButtonTest": "Test",
|
||||
"ButtonUnlinkOpedId": "Dissocier OpenID",
|
||||
"ButtonUpload": "Téléverser",
|
||||
"ButtonUploadBackup": "Téléverser une sauvegarde",
|
||||
"ButtonUploadCover": "Téléverser une couverture",
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
"ButtonStats": "Statistika",
|
||||
"ButtonSubmit": "Podnesi",
|
||||
"ButtonTest": "Test",
|
||||
"ButtonUnlinkOpedId": "Odspoji OpenID",
|
||||
"ButtonUpload": "Učitaj",
|
||||
"ButtonUploadBackup": "Učitaj sigurnosnu kopiju",
|
||||
"ButtonUploadCover": "Učitaj naslovnicu",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"ButtonChooseFiles": "Wybierz pliki",
|
||||
"ButtonClearFilter": "Wyczyść filtr",
|
||||
"ButtonCloseFeed": "Zamknij kanał",
|
||||
"ButtonCloseSession": "Zamknij otwartą sesję",
|
||||
"ButtonCollections": "Kolekcje",
|
||||
"ButtonConfigureScanner": "Skonfiguruj skaner",
|
||||
"ButtonCreate": "Utwórz",
|
||||
@@ -28,6 +29,7 @@
|
||||
"ButtonEdit": "Edycja",
|
||||
"ButtonEditChapters": "Edytuj rozdziały",
|
||||
"ButtonEditPodcast": "Edytuj podcast",
|
||||
"ButtonEnable": "Włącz",
|
||||
"ButtonForceReScan": "Wymuś ponowne skanowanie",
|
||||
"ButtonFullPath": "Pełna ścieżka",
|
||||
"ButtonHide": "Ukryj",
|
||||
@@ -47,8 +49,10 @@
|
||||
"ButtonNext": "Następny",
|
||||
"ButtonNextChapter": "Następny rozdział",
|
||||
"ButtonNextItemInQueue": "Następny element w kolejce",
|
||||
"ButtonOk": "Ok",
|
||||
"ButtonOpenFeed": "Otwórz feed",
|
||||
"ButtonOpenManager": "Otwórz menadżera",
|
||||
"ButtonPause": "Wstrzymaj",
|
||||
"ButtonPlay": "Odtwarzaj",
|
||||
"ButtonPlaying": "Odtwarzane",
|
||||
"ButtonPlaylists": "Listy odtwarzania",
|
||||
@@ -90,6 +94,7 @@
|
||||
"ButtonStartMetadataEmbed": "Osadź metadane",
|
||||
"ButtonStats": "Statystyki",
|
||||
"ButtonSubmit": "Zaloguj",
|
||||
"ButtonTest": "Test",
|
||||
"ButtonUpload": "Wgraj",
|
||||
"ButtonUploadBackup": "Wgraj kopię zapasową",
|
||||
"ButtonUploadCover": "Wgraj okładkę",
|
||||
@@ -102,6 +107,7 @@
|
||||
"ErrorUploadFetchMetadataNoResults": "Nie można pobrać metadanych — spróbuj zaktualizować tytuł i/lub autora",
|
||||
"ErrorUploadLacksTitle": "Musi mieć tytuł",
|
||||
"HeaderAccount": "Konto",
|
||||
"HeaderAddCustomMetadataProvider": "Dodaj niestandardowego dostawcę metadanych",
|
||||
"HeaderAdvanced": "Zaawansowane",
|
||||
"HeaderAppriseNotificationSettings": "Ustawienia powiadomień Apprise",
|
||||
"HeaderAudioTracks": "Ścieżki audio",
|
||||
@@ -120,6 +126,7 @@
|
||||
"HeaderDetails": "Szczegóły",
|
||||
"HeaderDownloadQueue": "Kolejka do ściągania",
|
||||
"HeaderEbookFiles": "Pliki Ebook",
|
||||
"HeaderEmail": "E-mail",
|
||||
"HeaderEmailSettings": "Ustawienia e-mail",
|
||||
"HeaderEpisodes": "Rozdziały",
|
||||
"HeaderEreaderDevices": "Czytniki",
|
||||
@@ -145,6 +152,8 @@
|
||||
"HeaderMetadataToEmbed": "Osadź metadane",
|
||||
"HeaderNewAccount": "Nowe konto",
|
||||
"HeaderNewLibrary": "Nowa biblioteka",
|
||||
"HeaderNotificationCreate": "Utwórz powiadomienie",
|
||||
"HeaderNotificationUpdate": "Zaktualizuj powiadomienie",
|
||||
"HeaderNotifications": "Powiadomienia",
|
||||
"HeaderOpenIDConnectAuthentication": "Uwierzytelnianie OpenID Connect",
|
||||
"HeaderOpenRSSFeed": "Utwórz kanał RSS",
|
||||
@@ -157,7 +166,9 @@
|
||||
"HeaderPlaylistItems": "Pozycje listy odtwarzania",
|
||||
"HeaderPodcastsToAdd": "Podcasty do dodania",
|
||||
"HeaderPreviewCover": "Podgląd okładki",
|
||||
"HeaderRSSFeedGeneral": "Szczegóły RSS",
|
||||
"HeaderRSSFeedIsOpen": "Kanał RSS jest otwarty",
|
||||
"HeaderRSSFeeds": "Kanały RSS",
|
||||
"HeaderRemoveEpisode": "Usuń odcinek",
|
||||
"HeaderRemoveEpisodes": "Usuń {0} odcinków",
|
||||
"HeaderSavedMediaProgress": "Zapisany postęp",
|
||||
@@ -200,6 +211,7 @@
|
||||
"LabelAddToPlaylist": "Dodaj do playlisty",
|
||||
"LabelAddToPlaylistBatch": "Dodaj {0} pozycji do playlisty",
|
||||
"LabelAddedAt": "Dodano",
|
||||
"LabelAddedDate": "Dodano {0}",
|
||||
"LabelAdminUsersOnly": "Tylko użytkownicy administracyjni",
|
||||
"LabelAll": "Wszystkie",
|
||||
"LabelAllUsers": "Wszyscy użytkownicy",
|
||||
@@ -215,15 +227,19 @@
|
||||
"LabelAutoFetchMetadata": "Automatycznie pobierz metadane",
|
||||
"LabelAutoFetchMetadataHelp": "Pobiera metadane dotyczące tytułu, autora i serii, aby usprawnić przesyłanie. Po przesłaniu może być konieczne dopasowanie dodatkowych metadanych.",
|
||||
"LabelAutoLaunch": "Uruchom automatycznie",
|
||||
"LabelAutoRegister": "Automatyczna rejestracja",
|
||||
"LabelAutoRegisterDescription": "Automatycznie utwórz nowych użytkowników po zalogowaniu",
|
||||
"LabelBackToUser": "Powrót",
|
||||
"LabelBackupLocation": "Lokalizacja kopii zapasowej",
|
||||
"LabelBackupsEnableAutomaticBackups": "Włącz automatyczne kopie zapasowe",
|
||||
"LabelBackupsEnableAutomaticBackupsHelp": "Kopie zapasowe są zapisywane w folderze /metadata/backups",
|
||||
"LabelBackupsMaxBackupSize": "Maksymalny rozmiar kopii zapasowej (w GB)",
|
||||
"LabelBackupsMaxBackupSize": "Maksymalny rozmiar kopii zapasowej (w GB) (0 oznacza nieograniczony)",
|
||||
"LabelBackupsMaxBackupSizeHelp": "Jako zabezpieczenie przed błędną konfiguracją, kopie zapasowe nie będą wykonywane, jeśli przekroczą skonfigurowany rozmiar.",
|
||||
"LabelBackupsNumberToKeep": "Liczba kopii zapasowych do przechowywania",
|
||||
"LabelBackupsNumberToKeepHelp": "Tylko 1 kopia zapasowa zostanie usunięta, więc jeśli masz już więcej kopii zapasowych, powinieneś je ręcznie usunąć.",
|
||||
"LabelBitrate": "Bitrate",
|
||||
"LabelBooks": "Książki",
|
||||
"LabelButtonText": "Tekst przycisku",
|
||||
"LabelByAuthor": "autorstwa {0}",
|
||||
"LabelChangePassword": "Zmień hasło",
|
||||
"LabelChannels": "Kanały",
|
||||
@@ -232,6 +248,7 @@
|
||||
"LabelChaptersFound": "Znalezione rozdziały",
|
||||
"LabelClickForMoreInfo": "Kliknij po więcej szczegółów",
|
||||
"LabelClosePlayer": "Zamknij odtwarzacz",
|
||||
"LabelCodec": "Kodek",
|
||||
"LabelCollapseSeries": "Podsumuj serię",
|
||||
"LabelCollapseSubSeries": "Zwiń podserie",
|
||||
"LabelCollection": "Kolekcja",
|
||||
@@ -247,6 +264,7 @@
|
||||
"LabelCronExpression": "Wyrażenie CRON",
|
||||
"LabelCurrent": "Aktualny",
|
||||
"LabelCurrently": "Obecnie:",
|
||||
"LabelCustomCronExpression": "Niestandardowe wyrażenie Cron:",
|
||||
"LabelDatetime": "Data i godzina",
|
||||
"LabelDays": "Dni",
|
||||
"LabelDeleteFromFileSystemCheckbox": "Usuń z systemu plików (odznacz, aby usunąć tylko z bazy danych)",
|
||||
@@ -254,6 +272,7 @@
|
||||
"LabelDeselectAll": "Odznacz wszystko",
|
||||
"LabelDevice": "Urządzenie",
|
||||
"LabelDeviceInfo": "Informacja o urządzeniu",
|
||||
"LabelDeviceIsAvailableTo": "Urządzenie jest dostępne do...",
|
||||
"LabelDirectory": "Katalog",
|
||||
"LabelDiscFromFilename": "Oznaczenie dysku z nazwy pliku",
|
||||
"LabelDiscFromMetadata": "Oznaczenie dysku z metadanych",
|
||||
@@ -261,16 +280,20 @@
|
||||
"LabelDownload": "Pobierz",
|
||||
"LabelDownloadNEpisodes": "Ściąganie {0} odcinków",
|
||||
"LabelDuration": "Czas trwania",
|
||||
"LabelDurationComparisonExactMatch": "(dokładne dopasowanie)",
|
||||
"LabelDurationComparisonLonger": "({0} dłużej)",
|
||||
"LabelDurationComparisonShorter": "({0} krócej)",
|
||||
"LabelDurationFound": "Znaleziona długość:",
|
||||
"LabelEbook": "Ebook",
|
||||
"LabelEbooks": "Ebooki",
|
||||
"LabelEdit": "Edytuj",
|
||||
"LabelEmail": "E-mail",
|
||||
"LabelEmailSettingsFromAddress": "Z adresu",
|
||||
"LabelEmailSettingsRejectUnauthorized": "Odrzuć nieautoryzowane certyfikaty",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Wyłączenie walidacji certyfikatów SSL może narazić cię na ryzyka bezpieczeństwa, takie jak ataki man-in-the-middle. Wyłącz tą opcję wyłącznie jeśli rozumiesz tego skutki i ufasz serwerowi pocztowemu, do którego się podłączasz.",
|
||||
"LabelEmailSettingsSecure": "Bezpieczeństwo",
|
||||
"LabelEmailSettingsSecureHelp": "Jeśli włączysz, połączenie będzie korzystać z TLS podczas łączenia do serwera. Jeśli wyłączysz, TLS będzie wykorzystane jeśli serwer wspiera rozszerzenie STARTTLS. W większości przypadków włącz to ustawienie jeśli łączysz się do portu 465. Dla portów 587 lub 25 pozostaw to ustawienie wyłączone. (na podstawie nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Adres testowy",
|
||||
"LabelEmbeddedCover": "Wbudowana okładka",
|
||||
"LabelEnable": "Włącz",
|
||||
"LabelEnd": "Zakończ",
|
||||
@@ -278,16 +301,21 @@
|
||||
"LabelEpisode": "Odcinek",
|
||||
"LabelEpisodeTitle": "Tytuł odcinka",
|
||||
"LabelEpisodeType": "Typ odcinka",
|
||||
"LabelEpisodes": "Epizody",
|
||||
"LabelExample": "Przykład",
|
||||
"LabelExpandSeries": "Rozwiń serie",
|
||||
"LabelExpandSubSeries": "Rozwiń podserie",
|
||||
"LabelExplicit": "Nieprzyzwoite",
|
||||
"LabelExplicitChecked": "Nieprzyzwoite (sprawdzone)",
|
||||
"LabelExplicitUnchecked": "Przyzwoite (niesprawdzone)",
|
||||
"LabelExportOPML": "Wyeksportuj OPML",
|
||||
"LabelFeedURL": "URL kanału",
|
||||
"LabelFetchingMetadata": "Pobieranie metadanych",
|
||||
"LabelFile": "Plik",
|
||||
"LabelFileBirthtime": "Data utworzenia pliku",
|
||||
"LabelFileBornDate": "Utworzony {0}",
|
||||
"LabelFileModified": "Data modyfikacji pliku",
|
||||
"LabelFileModifiedDate": "Modyfikowany {0}",
|
||||
"LabelFilename": "Nazwa pliku",
|
||||
"LabelFilterByUser": "Filtruj według danego użytkownika",
|
||||
"LabelFindEpisodes": "Znajdź odcinki",
|
||||
@@ -297,8 +325,10 @@
|
||||
"LabelFontBold": "Pogrubiony",
|
||||
"LabelFontBoldness": "Grubość czcionki",
|
||||
"LabelFontFamily": "Rodzina czcionek",
|
||||
"LabelFontItalic": "Kursywa",
|
||||
"LabelFontScale": "Rozmiar czcionki",
|
||||
"LabelFontStrikethrough": "Przekreślony",
|
||||
"LabelFormat": "Format",
|
||||
"LabelGenre": "Gatunek",
|
||||
"LabelGenres": "Gatunki",
|
||||
"LabelHardDeleteFile": "Usuń trwale plik",
|
||||
@@ -306,6 +336,7 @@
|
||||
"LabelHasSupplementaryEbook": "Posiada dodatkowy ebook",
|
||||
"LabelHideSubtitles": "Ukryj napisy",
|
||||
"LabelHighestPriority": "Najwyższy priorytet",
|
||||
"LabelHost": "Host",
|
||||
"LabelHour": "Godzina",
|
||||
"LabelHours": "Godziny",
|
||||
"LabelIcon": "Ikona",
|
||||
@@ -324,6 +355,7 @@
|
||||
"LabelIntervalEveryHour": "Każdej godziny",
|
||||
"LabelInvert": "Inversja",
|
||||
"LabelItem": "Pozycja",
|
||||
"LabelJumpBackwardAmount": "Rozmiar skoku do przodu",
|
||||
"LabelLanguage": "Język",
|
||||
"LabelLanguageDefaultServer": "Domyślny język serwera",
|
||||
"LabelLanguages": "Języki",
|
||||
@@ -338,10 +370,13 @@
|
||||
"LabelLess": "Mniej",
|
||||
"LabelLibrariesAccessibleToUser": "Biblioteki dostępne dla użytkownika",
|
||||
"LabelLibrary": "Biblioteka",
|
||||
"LabelLibraryFilterSublistEmpty": "Brak {0}",
|
||||
"LabelLibraryItem": "Element biblioteki",
|
||||
"LabelLibraryName": "Nazwa biblioteki",
|
||||
"LabelLimit": "Limit",
|
||||
"LabelLineSpacing": "Odstęp między wierszami",
|
||||
"LabelListenAgain": "Słuchaj ponownie",
|
||||
"LabelLogLevelDebug": "Debugowanie",
|
||||
"LabelLogLevelInfo": "Informacja",
|
||||
"LabelLogLevelWarn": "Ostrzeżenie",
|
||||
"LabelLookForNewEpisodesAfterDate": "Szukaj nowych odcinków po dacie",
|
||||
@@ -351,6 +386,7 @@
|
||||
"LabelMediaPlayer": "Odtwarzacz",
|
||||
"LabelMediaType": "Typ mediów",
|
||||
"LabelMetaTag": "Tag",
|
||||
"LabelMetaTags": "Meta Tagi",
|
||||
"LabelMetadataOrderOfPrecedenceDescription": "Źródła metadanych o wyższym priorytecie będą zastępują źródła o niższym priorytecie",
|
||||
"LabelMetadataProvider": "Dostawca metadanych",
|
||||
"LabelMinute": "Minuta",
|
||||
@@ -358,6 +394,7 @@
|
||||
"LabelMissing": "Brakujący",
|
||||
"LabelMissingEbook": "Nie posiada ebooka",
|
||||
"LabelMissingSupplementaryEbook": "Nie posiada dodatkowego ebooka",
|
||||
"LabelMobileRedirectURIs": "Dozwolone URI przekierowań mobilnych",
|
||||
"LabelMore": "Więcej",
|
||||
"LabelMoreInfo": "Więcej informacji",
|
||||
"LabelName": "Nazwa",
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
"ButtonStats": "Статистика",
|
||||
"ButtonSubmit": "Применить",
|
||||
"ButtonTest": "Тест",
|
||||
"ButtonUnlinkOpedId": "Отвязать OpenID",
|
||||
"ButtonUpload": "Загрузить",
|
||||
"ButtonUploadBackup": "Загрузить бэкап",
|
||||
"ButtonUploadCover": "Загрузить обложку",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"ButtonAddLibrary": "Dodaj knjižnico",
|
||||
"ButtonAddPodcasts": "Dodaj podcast",
|
||||
"ButtonAddUser": "Dodaj uporabnika",
|
||||
"ButtonAddYourFirstLibrary": "Dodaj tvojo prvo knjižnico",
|
||||
"ButtonAddYourFirstLibrary": "Dodajte svojo prvo knjižnico",
|
||||
"ButtonApply": "Uveljavi",
|
||||
"ButtonApplyChapters": "Uveljavi poglavja",
|
||||
"ButtonAuthors": "Avtorji",
|
||||
@@ -15,13 +15,13 @@
|
||||
"ButtonCancelEncode": "Prekliči prekodiranje",
|
||||
"ButtonChangeRootPassword": "Zamenjaj korensko geslo",
|
||||
"ButtonCheckAndDownloadNewEpisodes": "Preveri in prenesi nove epizode",
|
||||
"ButtonChooseAFolder": "Izberi mapo",
|
||||
"ButtonChooseFiles": "Izberi datoteke",
|
||||
"ButtonChooseAFolder": "Izberite mapo",
|
||||
"ButtonChooseFiles": "Izberite datoteke",
|
||||
"ButtonClearFilter": "Počisti filter",
|
||||
"ButtonCloseFeed": "Zapri vir",
|
||||
"ButtonCloseSession": "Zapri odprto sejo",
|
||||
"ButtonCollections": "Zbirke",
|
||||
"ButtonConfigureScanner": "Nastavi skener",
|
||||
"ButtonConfigureScanner": "Nastavi pregledovalnik",
|
||||
"ButtonCreate": "Ustvari",
|
||||
"ButtonCreateBackup": "Ustvari varnostno kopijo",
|
||||
"ButtonDelete": "Izbriši",
|
||||
@@ -30,9 +30,9 @@
|
||||
"ButtonEditChapters": "Uredi poglavja",
|
||||
"ButtonEditPodcast": "Uredi podcast",
|
||||
"ButtonEnable": "Omogoči",
|
||||
"ButtonFireAndFail": "Zaženi in je neuspešno",
|
||||
"ButtonFireAndFail": "Zaženi in je bilo neuspešno",
|
||||
"ButtonFireOnTest": "Zaženi samo na dogodku onTest",
|
||||
"ButtonForceReScan": "Prisilno ponovno skeniranje",
|
||||
"ButtonForceReScan": "Prisilno ponovno pregledovanje",
|
||||
"ButtonFullPath": "Polna pot",
|
||||
"ButtonHide": "Skrij",
|
||||
"ButtonHome": "Domov",
|
||||
@@ -67,7 +67,7 @@
|
||||
"ButtonQueueRemoveItem": "Odstrani iz čakalne vrste",
|
||||
"ButtonQuickEmbedMetadata": "Hitra vdelava metapodatkov",
|
||||
"ButtonQuickMatch": "Hitro ujemanje",
|
||||
"ButtonReScan": "Ponovno iskanje",
|
||||
"ButtonReScan": "Ponovno pregledovanje",
|
||||
"ButtonRead": "Preberi",
|
||||
"ButtonReadLess": "Preberi manj",
|
||||
"ButtonReadMore": "Preberi več",
|
||||
@@ -84,10 +84,10 @@
|
||||
"ButtonSave": "Shrani",
|
||||
"ButtonSaveAndClose": "Shrani iz zapri",
|
||||
"ButtonSaveTracklist": "Shrani seznam skladb",
|
||||
"ButtonScan": "Skeniranje",
|
||||
"ButtonScanLibrary": "Skeniraj knjižnico",
|
||||
"ButtonScan": "Pregledovanje",
|
||||
"ButtonScanLibrary": "Preglej knjižnico",
|
||||
"ButtonSearch": "Poišči",
|
||||
"ButtonSelectFolderPath": "Izberi pot mape",
|
||||
"ButtonSelectFolderPath": "Izberite pot do mape",
|
||||
"ButtonSeries": "Serije",
|
||||
"ButtonSetChaptersFromTracks": "Nastavi poglavja za posnetke",
|
||||
"ButtonShare": "Deli",
|
||||
@@ -98,7 +98,6 @@
|
||||
"ButtonStats": "Statistika",
|
||||
"ButtonSubmit": "Posreduj",
|
||||
"ButtonTest": "Test",
|
||||
"ButtonUnlinkOpedId": "Prekini povezavo OpenID",
|
||||
"ButtonUpload": "Naloži",
|
||||
"ButtonUploadBackup": "Naloži varnostno kopijo",
|
||||
"ButtonUploadCover": "Naloži naslovnico",
|
||||
@@ -120,7 +119,7 @@
|
||||
"HeaderBackups": "Varnostne kopije",
|
||||
"HeaderChangePassword": "Zamenjaj geslo",
|
||||
"HeaderChapters": "Poglavja",
|
||||
"HeaderChooseAFolder": "Izberi mapo",
|
||||
"HeaderChooseAFolder": "Izberite mapo",
|
||||
"HeaderCollection": "Zbirka",
|
||||
"HeaderCollectionItems": "Elementi zbirke",
|
||||
"HeaderCover": "Naslovnica",
|
||||
@@ -178,7 +177,7 @@
|
||||
"HeaderRemoveEpisodes": "Odstrani {0} epizod",
|
||||
"HeaderSavedMediaProgress": "Shranjen napredek predstavnosti",
|
||||
"HeaderSchedule": "Načrtovanje",
|
||||
"HeaderScheduleLibraryScans": "Načrtuj samodejno skeniranje knjižnice",
|
||||
"HeaderScheduleLibraryScans": "Načrtuj samodejno pregledovanje knjižnice",
|
||||
"HeaderSession": "Seja",
|
||||
"HeaderSetBackupSchedule": "Nastavite urnik varnostnega kopiranja",
|
||||
"HeaderSettings": "Nastavitve",
|
||||
@@ -189,7 +188,7 @@
|
||||
"HeaderSleepTimer": "Časovnik za izklop",
|
||||
"HeaderStatsLargestItems": "Največji elementi",
|
||||
"HeaderStatsLongestItems": "Najdaljši elementi (ure)",
|
||||
"HeaderStatsMinutesListeningChart": "Minute poslušanja (zadnjih 7 dni)",
|
||||
"HeaderStatsMinutesListeningChart": "Minut poslušanja (zadnjih 7 dni)",
|
||||
"HeaderStatsRecentSessions": "Nedavne seje",
|
||||
"HeaderStatsTop10Authors": "Najboljših 10 avtorjev",
|
||||
"HeaderStatsTop5Genres": "Najboljših 5 žanrov",
|
||||
@@ -204,5 +203,773 @@
|
||||
"HeaderYourStats": "Tvoja statistika",
|
||||
"LabelAbridged": "Skrajšano",
|
||||
"LabelAbridgedChecked": "Skrajšano (omogočeno)",
|
||||
"LabelAbridgedUnchecked": "Neskrajšano (onemogočeno)"
|
||||
"LabelAbridgedUnchecked": "Neskrajšano (onemogočeno)",
|
||||
"LabelAccessibleBy": "Dostopno iz",
|
||||
"LabelAccountType": "Vrsta računa",
|
||||
"LabelAccountTypeAdmin": "Administrator",
|
||||
"LabelAccountTypeGuest": "Gost",
|
||||
"LabelAccountTypeUser": "Uporabnik",
|
||||
"LabelActivity": "Aktivnost",
|
||||
"LabelAddToCollection": "Dodaj v zbirko",
|
||||
"LabelAddToCollectionBatch": "Dodaj {0} knjig v zbirko",
|
||||
"LabelAddToPlaylist": "Dodaj na seznam predvajanja",
|
||||
"LabelAddToPlaylistBatch": "Dodaj {0} elementov v seznam predvajanja",
|
||||
"LabelAddedAt": "Dodano ob",
|
||||
"LabelAddedDate": "Dodano {0}",
|
||||
"LabelAdminUsersOnly": "Samo administratorji",
|
||||
"LabelAll": "Vsi",
|
||||
"LabelAllUsers": "Vsi uporabniki",
|
||||
"LabelAllUsersExcludingGuests": "Vsi uporabniki razen gosti",
|
||||
"LabelAllUsersIncludingGuests": "Vsi uporabniki vključno z gosti",
|
||||
"LabelAlreadyInYourLibrary": "Že v tvoji knjižnici",
|
||||
"LabelAppend": "Priloži",
|
||||
"LabelAuthor": "Avtor",
|
||||
"LabelAuthorFirstLast": "Avtor (ime priimek)",
|
||||
"LabelAuthorLastFirst": "Avtor (priimek, ime)",
|
||||
"LabelAuthors": "Avtorji",
|
||||
"LabelAutoDownloadEpisodes": "Samodejni prenos epizod",
|
||||
"LabelAutoFetchMetadata": "Samodejno pridobivanje metapodatkov",
|
||||
"LabelAutoFetchMetadataHelp": "Pridobi metapodatke za naslov, avtorja in serijo za poenostavitev nalaganja. Po nalaganju bo morda treba ujemanje dodatnih metapodatkov.",
|
||||
"LabelAutoLaunch": "Samodejni zagon",
|
||||
"LabelAutoLaunchDescription": "Samodejna preusmeritev na ponudnika avtentikacije ob navigaciji na prijavno stran (ročna preglasitev poti <code>/login?autoLaunch=0</code>)",
|
||||
"LabelAutoRegister": "Samodejna registracija",
|
||||
"LabelAutoRegisterDescription": "Po prijavi samodejno ustvari nove uporabnike",
|
||||
"LabelBackToUser": "Nazaj na uporabnika",
|
||||
"LabelBackupLocation": "Lokacija rezervne kopije",
|
||||
"LabelBackupsEnableAutomaticBackups": "Omogoči samodejno varnostno kopiranje",
|
||||
"LabelBackupsEnableAutomaticBackupsHelp": "Varnostne kopije shranjene v /metadata/backups",
|
||||
"LabelBackupsMaxBackupSize": "Največja velikost varnostne kopije (v GB) (0 za neomejeno)",
|
||||
"LabelBackupsMaxBackupSizeHelp": "Kot zaščita pred napačno konfiguracijo, varnostne kopije ne bodo uspele, če presežejo konfigurirano velikost.",
|
||||
"LabelBackupsNumberToKeep": "Število varnostnih kopij, ki jih je treba hraniti",
|
||||
"LabelBackupsNumberToKeepHelp": "Naenkrat bo odstranjena samo ena varnostna kopija, če že imate več varnostnih kopij, jih odstranite ročno.",
|
||||
"LabelBitrate": "Bitna hitrost",
|
||||
"LabelBooks": "Knjige",
|
||||
"LabelButtonText": "Besedilo gumba",
|
||||
"LabelByAuthor": "od {0}",
|
||||
"LabelChangePassword": "Spremeni geslo",
|
||||
"LabelChannels": "Kanali",
|
||||
"LabelChapterTitle": "Naslov poglavja",
|
||||
"LabelChapters": "Poglavja",
|
||||
"LabelChaptersFound": "najdenih poglavij",
|
||||
"LabelClickForMoreInfo": "Klikni za več informacij",
|
||||
"LabelClosePlayer": "Zapri predvajalnik",
|
||||
"LabelCodec": "Kodek",
|
||||
"LabelCollapseSeries": "Strni serije",
|
||||
"LabelCollapseSubSeries": "Strni podserije",
|
||||
"LabelCollection": "Zbirka",
|
||||
"LabelCollections": "Zbirke",
|
||||
"LabelComplete": "Končano",
|
||||
"LabelConfirmPassword": "Potrdi geslo",
|
||||
"LabelContinueListening": "Nadaljuj poslušanje",
|
||||
"LabelContinueReading": "Nadaljuj branje",
|
||||
"LabelContinueSeries": "Nadaljuj s serijo",
|
||||
"LabelCover": "Naslovnica",
|
||||
"LabelCoverImageURL": "URL naslovne slike",
|
||||
"LabelCreatedAt": "Ustvarjeno ob",
|
||||
"LabelCronExpression": "Cron izraz",
|
||||
"LabelCurrent": "Trenutno",
|
||||
"LabelCurrently": "Trenutno:",
|
||||
"LabelCustomCronExpression": "Cron izraz po meri:",
|
||||
"LabelDatetime": "Datum in ura",
|
||||
"LabelDays": "Dnevi",
|
||||
"LabelDeleteFromFileSystemCheckbox": "Izbriši iz datotečnega sistema (počisti polje, če želiš odstraniti samo iz zbirke podatkov)",
|
||||
"LabelDescription": "Opis",
|
||||
"LabelDeselectAll": "Odznači vse",
|
||||
"LabelDevice": "Naprava",
|
||||
"LabelDeviceInfo": "Podatki o napravi",
|
||||
"LabelDeviceIsAvailableTo": "Naprava je na voljo za...",
|
||||
"LabelDirectory": "Imenik",
|
||||
"LabelDiscFromFilename": "Disk iz imena datoteke",
|
||||
"LabelDiscFromMetadata": "Disk iz metapodatkov",
|
||||
"LabelDiscover": "Odkrij",
|
||||
"LabelDownload": "Prenos",
|
||||
"LabelDownloadNEpisodes": "Prenesi {0} epizod",
|
||||
"LabelDuration": "Trajanje",
|
||||
"LabelDurationComparisonExactMatch": "(natančno ujemanje)",
|
||||
"LabelDurationComparisonLonger": "({0} dlje)",
|
||||
"LabelDurationComparisonShorter": "({0} krajše)",
|
||||
"LabelDurationFound": "Najdeno trajanje:",
|
||||
"LabelEbook": "Eknjiga",
|
||||
"LabelEbooks": "Eknjige",
|
||||
"LabelEdit": "Uredi",
|
||||
"LabelEmail": "E-pošta",
|
||||
"LabelEmailSettingsFromAddress": "Iz naslova",
|
||||
"LabelEmailSettingsRejectUnauthorized": "Zavrni nepooblaščena potrdila",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Če onemogočite preverjanje veljavnosti potrdila SSL, lahko izpostavite svojo povezavo varnostnim tveganjem, kot so napadi človek v sredini. To možnost onemogočite le, če razumete posledice in zaupate poštnemu strežniku, s katerim se povezujete.",
|
||||
"LabelEmailSettingsSecure": "Varno",
|
||||
"LabelEmailSettingsSecureHelp": "Če je omogočeno, bo povezava pri povezovanju s strežnikom uporabljala TLS. Če je onemogočeno, se TLS uporablja, če strežnik podpira razširitev STARTTLS. V večini primerov nastavite to vrednost na omogočeno, če se povezujete z vrati 465. Za vrata 587 ali 25 naj ostane onemogočeno. (iz nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Testiraj naslov",
|
||||
"LabelEmbeddedCover": "Vdelana naslovnica",
|
||||
"LabelEnable": "Omogoči",
|
||||
"LabelEnd": "Konec",
|
||||
"LabelEndOfChapter": "Konec poglavja",
|
||||
"LabelEpisode": "Epizoda",
|
||||
"LabelEpisodeTitle": "Naslov epizode",
|
||||
"LabelEpisodeType": "Tip epizode",
|
||||
"LabelEpisodes": "Epizode",
|
||||
"LabelExample": "Primer",
|
||||
"LabelExpandSeries": "Razširi serije",
|
||||
"LabelExpandSubSeries": "Razširi podserije",
|
||||
"LabelExplicit": "Eksplicitno",
|
||||
"LabelExplicitChecked": "Eksplicitno (omogočeno)",
|
||||
"LabelExplicitUnchecked": "Ne eksplicitno (onemogočeno)",
|
||||
"LabelExportOPML": "Izvozi OPML",
|
||||
"LabelFeedURL": "URL vir",
|
||||
"LabelFetchingMetadata": "Pridobivam metapodatke",
|
||||
"LabelFile": "Datoteka",
|
||||
"LabelFileBirthtime": "Čas ustvarjanja datoteke",
|
||||
"LabelFileBornDate": "Ustvarjena {0}",
|
||||
"LabelFileModified": "Datoteke spremenjena",
|
||||
"LabelFileModifiedDate": "Spremenjena {0}",
|
||||
"LabelFilename": "Ime datoteke",
|
||||
"LabelFilterByUser": "Filtriraj po uporabniku",
|
||||
"LabelFindEpisodes": "Poišči epizode",
|
||||
"LabelFinished": "Zaključeno",
|
||||
"LabelFolder": "Mapa",
|
||||
"LabelFolders": "Mape",
|
||||
"LabelFontBold": "Krepko",
|
||||
"LabelFontBoldness": "Krepkost pisave",
|
||||
"LabelFontFamily": "Družina pisave",
|
||||
"LabelFontItalic": "Ležeče",
|
||||
"LabelFontScale": "Merilo pisave",
|
||||
"LabelFontStrikethrough": "Prečrtano",
|
||||
"LabelFormat": "Oblika",
|
||||
"LabelGenre": "Žanr",
|
||||
"LabelGenres": "Žanri",
|
||||
"LabelHardDeleteFile": "Trdo brisanje datoteke",
|
||||
"LabelHasEbook": "Ima eknjigo",
|
||||
"LabelHasSupplementaryEbook": "Ima dodatno eknjigo",
|
||||
"LabelHideSubtitles": "Skrij podnapise",
|
||||
"LabelHighestPriority": "Najvišja prioriteta",
|
||||
"LabelHost": "Gostitelj",
|
||||
"LabelHour": "Ura",
|
||||
"LabelHours": "Ure",
|
||||
"LabelIcon": "Ikona",
|
||||
"LabelImageURLFromTheWeb": "URL slike iz spleta",
|
||||
"LabelInProgress": "V teku",
|
||||
"LabelIncludeInTracklist": "Vključi v seznam skladb",
|
||||
"LabelIncomplete": "Nepopolno",
|
||||
"LabelInterval": "Interval",
|
||||
"LabelIntervalCustomDailyWeekly": "Dnevno/tedensko po meri",
|
||||
"LabelIntervalEvery12Hours": "Vsakih 12 ur",
|
||||
"LabelIntervalEvery15Minutes": "Vsakih 15 minut",
|
||||
"LabelIntervalEvery2Hours": "Vsake 2 uri",
|
||||
"LabelIntervalEvery30Minutes": "Vsakih 30 minut",
|
||||
"LabelIntervalEvery6Hours": "Vsakih 6 ur",
|
||||
"LabelIntervalEveryDay": "Vsak dan",
|
||||
"LabelIntervalEveryHour": "Vsako uro",
|
||||
"LabelInvert": "Obrni izbor",
|
||||
"LabelItem": "Element",
|
||||
"LabelJumpBackwardAmount": "Količina skoka nazaj",
|
||||
"LabelJumpForwardAmount": "Količina skoka naprej",
|
||||
"LabelLanguage": "Jezik",
|
||||
"LabelLanguageDefaultServer": "Privzeti jezik strežnika",
|
||||
"LabelLanguages": "Jeziki",
|
||||
"LabelLastBookAdded": "Zadnja dodana knjiga",
|
||||
"LabelLastBookUpdated": "Zadnja posodobljena knjiga",
|
||||
"LabelLastSeen": "Nazadnje viden",
|
||||
"LabelLastTime": "Zadnji čas",
|
||||
"LabelLastUpdate": "Zadnja posodobitev",
|
||||
"LabelLayout": "Postavitev",
|
||||
"LabelLayoutSinglePage": "Ena stran",
|
||||
"LabelLayoutSplitPage": "Razdeli stran",
|
||||
"LabelLess": "Manj",
|
||||
"LabelLibrariesAccessibleToUser": "Knjižnice, dostopne uporabniku",
|
||||
"LabelLibrary": "Knjižnica",
|
||||
"LabelLibraryFilterSublistEmpty": "Ne {0}",
|
||||
"LabelLibraryItem": "Element knjižnice",
|
||||
"LabelLibraryName": "Ime knjižnice",
|
||||
"LabelLimit": "Omejitev",
|
||||
"LabelLineSpacing": "Razmik med vrsticami",
|
||||
"LabelListenAgain": "Poslušaj znova",
|
||||
"LabelLogLevelDebug": "Odpravljanje napak",
|
||||
"LabelLogLevelInfo": "Info",
|
||||
"LabelLogLevelWarn": "Opozoritve",
|
||||
"LabelLookForNewEpisodesAfterDate": "Poiščite nove epizode po tem datumu",
|
||||
"LabelLowestPriority": "Najnižja prioriteta",
|
||||
"LabelMatchExistingUsersBy": "Poveži obstoječe uporabnike po",
|
||||
"LabelMatchExistingUsersByDescription": "Uporablja se za povezovanje obstoječih uporabnikov. Ko se vzpostavi povezava, se bodo uporabniki ujemali z enoličnim ID-jem vašega ponudnika SSO",
|
||||
"LabelMediaPlayer": "Medijski predvajalnik",
|
||||
"LabelMediaType": "Vrsta medija",
|
||||
"LabelMetaTag": "Meta oznaka",
|
||||
"LabelMetaTags": "Meta oznake",
|
||||
"LabelMetadataOrderOfPrecedenceDescription": "Viri metapodatkov višje prioritete bodo preglasili vire metapodatkov nižje prioritete",
|
||||
"LabelMetadataProvider": "Ponudnik metapodatkov",
|
||||
"LabelMinute": "Minuta",
|
||||
"LabelMinutes": "Minute",
|
||||
"LabelMissing": "Manjkajoče",
|
||||
"LabelMissingEbook": "Nima nobene eknjige",
|
||||
"LabelMissingSupplementaryEbook": "Nima nobene dodatne eknjige",
|
||||
"LabelMobileRedirectURIs": "Dovoljeni mobilni preusmeritveni URI-ji",
|
||||
"LabelMobileRedirectURIsDescription": "To je seznam dovoljenih veljavnih preusmeritvenih URI-jev za mobilne aplikacije. Privzeti je <code>audiobookshelf://oauth</code>, ki ga lahko odstranite ali dopolnite z dodatnimi URI-ji za integracijo aplikacij tretjih oseb. Uporaba zvezdice (<code>*</code>) kot edinega vnosa dovoljuje kateri koli URI.",
|
||||
"LabelMore": "Več",
|
||||
"LabelMoreInfo": "Več informacij",
|
||||
"LabelName": "Naziv",
|
||||
"LabelNarrator": "Bralec",
|
||||
"LabelNarrators": "Bralci",
|
||||
"LabelNew": "Novo",
|
||||
"LabelNewPassword": "Novo geslo",
|
||||
"LabelNewestAuthors": "Najnovejši avtorji",
|
||||
"LabelNewestEpisodes": "Najnovejše epizode",
|
||||
"LabelNextBackupDate": "Naslednji datum varnostnega kopiranja",
|
||||
"LabelNextScheduledRun": "Naslednji načrtovani zagon",
|
||||
"LabelNoCustomMetadataProviders": "Ni ponudnikov metapodatkov po meri",
|
||||
"LabelNoEpisodesSelected": "Izbrana ni nobena epizoda",
|
||||
"LabelNotFinished": "Ni dokončano",
|
||||
"LabelNotStarted": "Ni zagnano",
|
||||
"LabelNotes": "Opombe",
|
||||
"LabelNotificationAppriseURL": "Apprise URL(ji)",
|
||||
"LabelNotificationAvailableVariables": "Razpoložljive spremenljivke",
|
||||
"LabelNotificationBodyTemplate": "Predloga telesa",
|
||||
"LabelNotificationEvent": "Dogodek obvestila",
|
||||
"LabelNotificationTitleTemplate": "Predloga naslova",
|
||||
"LabelNotificationsMaxFailedAttempts": "Najvišje število neuspelih poskusov",
|
||||
"LabelNotificationsMaxFailedAttemptsHelp": "Obvestila so onemogočena, ko se tolikokrat neuspelo pošljejo",
|
||||
"LabelNotificationsMaxQueueSize": "Največja velikost čakalne vrste za dogodke obvestil",
|
||||
"LabelNotificationsMaxQueueSizeHelp": "Dogodki so omejeni na sprožitev 1 na sekundo. Dogodki bodo prezrti, če je čakalna vrsta najvišja. To preprečuje neželeno pošiljanje obvestil.",
|
||||
"LabelNumberOfBooks": "Število knjig",
|
||||
"LabelNumberOfEpisodes": "# od epizod",
|
||||
"LabelOpenIDAdvancedPermsClaimDescription": "Ime zahtevka OpenID, ki vsebuje napredna dovoljenja za uporabniška dejanja v aplikaciji, ki bodo veljala za neskrbniške vloge (<b>če je konfigurirano</b>). Če trditev manjka v odgovoru, bo dostop do ABS zavrnjen. Če ena možnost manjka, bo obravnavana kot <code>false</code>. Zagotovite, da se zahtevek ponudnika identitete ujema s pričakovano strukturo:",
|
||||
"LabelOpenIDClaims": "Pustite naslednje možnosti prazne, da onemogočite napredno dodeljevanje skupin in dovoljenj, nato pa samodejno dodelite skupino 'Uporabnik'.",
|
||||
"LabelOpenIDGroupClaimDescription": "Ime zahtevka OpenID, ki vsebuje seznam uporabnikovih skupin. Običajno imenovane <code>skupine</code>. <b>Če je konfigurirana</b>, bo aplikacija samodejno dodelila vloge na podlagi članstva v skupini uporabnika, pod pogojem, da so te skupine v zahtevku poimenovane 'admin', 'user' ali 'guest' brez razlikovanja med velikimi in malimi črkami. Zahtevek mora vsebovati seznam in če uporabnik pripada več skupinam, mu aplikacija dodeli vlogo, ki ustreza najvišjemu nivoju dostopa. Če se nobena skupina ne ujema, bo dostop zavrnjen.",
|
||||
"LabelOpenRSSFeed": "Odpri vir RSS",
|
||||
"LabelOverwrite": "Prepiši",
|
||||
"LabelPassword": "Geslo",
|
||||
"LabelPath": "Pot",
|
||||
"LabelPermanent": "Trajno",
|
||||
"LabelPermissionsAccessAllLibraries": "Lahko dostopa do vseh knjižnic",
|
||||
"LabelPermissionsAccessAllTags": "Lahko dostopa do vseh oznak",
|
||||
"LabelPermissionsAccessExplicitContent": "Lahko dostopa do eksplicitne vsebine",
|
||||
"LabelPermissionsDelete": "Lahko briše",
|
||||
"LabelPermissionsDownload": "Lahko prenaša",
|
||||
"LabelPermissionsUpdate": "Lahko posodablja",
|
||||
"LabelPermissionsUpload": "Lahko nalaga",
|
||||
"LabelPersonalYearReview": "Pregled tvojega leta ({0})",
|
||||
"LabelPhotoPathURL": "Slika pot/URL",
|
||||
"LabelPlayMethod": "Metoda predvajanja",
|
||||
"LabelPlayerChapterNumberMarker": "{0} od {1}",
|
||||
"LabelPlaylists": "Seznami predvajanja",
|
||||
"LabelPodcast": "Podcast",
|
||||
"LabelPodcastSearchRegion": "Regija iskanja podcastov",
|
||||
"LabelPodcastType": "Vrsta podcasta",
|
||||
"LabelPodcasts": "Podcasti",
|
||||
"LabelPort": "Vrata",
|
||||
"LabelPrefixesToIgnore": "Predpone, ki jih je treba prezreti (neobčutljivo na velike in male črke)",
|
||||
"LabelPreventIndexing": "Preprečite, da bi vaš vir indeksirali imeniki podcastov iTunes in Google",
|
||||
"LabelPrimaryEbook": "Primarna eknjiga",
|
||||
"LabelProgress": "Napredek",
|
||||
"LabelProvider": "Ponudnik",
|
||||
"LabelProviderAuthorizationValue": "Vrednost glave avtorizacije",
|
||||
"LabelPubDate": "Datum objave",
|
||||
"LabelPublishYear": "Leto objave",
|
||||
"LabelPublishedDate": "Objavljeno {0}",
|
||||
"LabelPublisher": "Založnik",
|
||||
"LabelPublishers": "Založniki",
|
||||
"LabelRSSFeedCustomOwnerEmail": "E-pošta lastnika po meri",
|
||||
"LabelRSSFeedCustomOwnerName": "Ime lastnika po meri",
|
||||
"LabelRSSFeedOpen": "Odprt vir RSS",
|
||||
"LabelRSSFeedPreventIndexing": "Prepreči indeksiranje",
|
||||
"LabelRSSFeedSlug": "Slug RSS vira",
|
||||
"LabelRSSFeedURL": "URL vira RSS",
|
||||
"LabelRandomly": "Naključno",
|
||||
"LabelReAddSeriesToContinueListening": "Znova dodaj serijo za nadaljevanje poslušanja",
|
||||
"LabelRead": "Preberi",
|
||||
"LabelReadAgain": "Ponovno preberi",
|
||||
"LabelReadEbookWithoutProgress": "Preberi eknjigo brez ohranjanja napredka",
|
||||
"LabelRecentSeries": "Nedavne serije",
|
||||
"LabelRecentlyAdded": "Nedavno dodano",
|
||||
"LabelRecommended": "Priporočeno",
|
||||
"LabelRedo": "Ponovi",
|
||||
"LabelRegion": "Regija",
|
||||
"LabelReleaseDate": "Datum izdaje",
|
||||
"LabelRemoveCover": "Odstrani naslovnico",
|
||||
"LabelRowsPerPage": "Vrstic na stran",
|
||||
"LabelSearchTerm": "Iskalni pojem",
|
||||
"LabelSearchTitle": "Naslov iskanja",
|
||||
"LabelSearchTitleOrASIN": "Naslov iskanja ali ASIN",
|
||||
"LabelSeason": "Sezona",
|
||||
"LabelSelectAll": "Izberite vse",
|
||||
"LabelSelectAllEpisodes": "Izberite vse epizode",
|
||||
"LabelSelectEpisodesShowing": "Izberi {0} prikazanih epizod",
|
||||
"LabelSelectUsers": "Izberite uporabnike",
|
||||
"LabelSendEbookToDevice": "Pošlji eknjigo k...",
|
||||
"LabelSequence": "Zaporedje",
|
||||
"LabelSeries": "Serije",
|
||||
"LabelSeriesName": "Ime serije",
|
||||
"LabelSeriesProgress": "Napredek serije",
|
||||
"LabelServerYearReview": "Pregled leta strežnika ({0})",
|
||||
"LabelSetEbookAsPrimary": "Nastavi kot primarno",
|
||||
"LabelSetEbookAsSupplementary": "Nastavi kot dodatno",
|
||||
"LabelSettingsAudiobooksOnly": "Samo zvočne knjige",
|
||||
"LabelSettingsAudiobooksOnlyHelp": "Če omogočite to nastavitev, bodo datoteke eknjig prezrte, razen če so znotraj mape zvočnih knjig, v tem primeru bodo nastavljene kot dodatne e-knjige",
|
||||
"LabelSettingsBookshelfViewHelp": "Skeuomorfna oblika z lesenimi policami",
|
||||
"LabelSettingsChromecastSupport": "Podpora za Chromecast",
|
||||
"LabelSettingsDateFormat": "Oblika datuma",
|
||||
"LabelSettingsDisableWatcher": "Onemogoči pregledovalca",
|
||||
"LabelSettingsDisableWatcherForLibrary": "Onemogoči pregledovalca map za knjižnico",
|
||||
"LabelSettingsDisableWatcherHelp": "Onemogoči samodejno dodajanje/posodabljanje elementov, ko so zaznane spremembe datoteke. *Potreben je ponovni zagon strežnika",
|
||||
"LabelSettingsEnableWatcher": "Omogoči pregledovalca",
|
||||
"LabelSettingsEnableWatcherForLibrary": "Omogoči pregledovalca map za knjižnico",
|
||||
"LabelSettingsEnableWatcherHelp": "Omogoča samodejno dodajanje/posodabljanje elementov, ko so zaznane spremembe datoteke. *Potreben je ponovni zagon strežnika",
|
||||
"LabelSettingsEpubsAllowScriptedContent": "Dovoli skriptirano vsebino v epubih",
|
||||
"LabelSettingsEpubsAllowScriptedContentHelp": "Dovoli datotekam epub izvajanje skript. Priporočljivo je, da to nastavitev pustite onemogočeno, razen če zaupate viru datotek epub.",
|
||||
"LabelSettingsExperimentalFeatures": "Eksperimentalne funkcije",
|
||||
"LabelSettingsExperimentalFeaturesHelp": "Funkcije v razvoju, ki bi lahko uporabile vaše povratne informacije in pomoč pri testiranju. Kliknite, da odprete razpravo na githubu.",
|
||||
"LabelSettingsFindCovers": "Poišči naslovnice",
|
||||
"LabelSettingsFindCoversHelp": "Če vaša zvočna knjiga nima vdelane naslovnice ali slike naslovnice v mapi, bo pregledovalnik poskušal najti naslovnico.<br>Opomba: To bo podaljšalo čas pregledovanja",
|
||||
"LabelSettingsHideSingleBookSeries": "Skrij serije s samo eno knjigo",
|
||||
"LabelSettingsHideSingleBookSeriesHelp": "Serije, ki imajo eno knjigo, bodo skrite na strani serije in policah domače strani.",
|
||||
"LabelSettingsHomePageBookshelfView": "Domača stran bo imela pogled knjižne police",
|
||||
"LabelSettingsLibraryBookshelfView": "Knjižnična uporaba pogleda knjižne police",
|
||||
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Preskoči prejšnje knjige v nadaljevanju serije",
|
||||
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Polica z domačo stranjo Nadaljuj serijo prikazuje prvo nezačeto knjigo v seriji, ki ima vsaj eno dokončano knjigo in ni nobene knjige v teku. Če omogočite to nastavitev, se bo serija nadaljevala od najbolj dokončane knjige namesto od prve nezačete knjige.",
|
||||
"LabelSettingsParseSubtitles": "Uporabi podnapise",
|
||||
"LabelSettingsParseSubtitlesHelp": "Izvleci podnapise iz imen map zvočnih knjig.<br>Podnaslov mora biti ločen z \" - \"<br>npr. »Naslov knjige – Tu podnapis« ima podnaslov »Tu podnapis«",
|
||||
"LabelSettingsPreferMatchedMetadata": "Prednost imajo ujemajoči se metapodatki",
|
||||
"LabelSettingsPreferMatchedMetadataHelp": "Pri uporabi hitrega ujemanja bodo ujemajoči se podatki preglasili podrobnosti artikla. Hitro ujemanje bo privzeto izpolnil samo manjkajoče podrobnosti.",
|
||||
"LabelSettingsSkipMatchingBooksWithASIN": "Preskoči ujemajoče se knjige, ki že imajo ASIN",
|
||||
"LabelSettingsSkipMatchingBooksWithISBN": "Preskoči ujemajoče se knjige, ki že imajo oznako ISBN",
|
||||
"LabelSettingsSortingIgnorePrefixes": "Pri razvrščanju ne upoštevajte predpon",
|
||||
"LabelSettingsSortingIgnorePrefixesHelp": "npr. za naslov knjige s predpono \"the\" bi se \"The Book Title\" razvrstil kot \"Book Title, The\"",
|
||||
"LabelSettingsSquareBookCovers": "Uporabi kvadratne platnice knjig",
|
||||
"LabelSettingsSquareBookCoversHelp": "Raje uporabi kvadratne platnice kot standardne knjižne platnice 1.6:1",
|
||||
"LabelSettingsStoreCoversWithItem": "Shrani naslovnice skupaj z elementom",
|
||||
"LabelSettingsStoreCoversWithItemHelp": "Naslovnice so privzeto shranjene v /metadata/items, če omogočite to nastavitev, bodo platnice shranjene v mapi elementov knjižnice. Shranjena bo samo ena datoteka z imenom \"cover\"",
|
||||
"LabelSettingsStoreMetadataWithItem": "Shrani metapodatke skupaj z elementom",
|
||||
"LabelSettingsStoreMetadataWithItemHelp": "Datoteke z metapodatki so privzeto shranjene v /metadata/items, če omogočite to nastavitev, boste datoteke z metapodatki shranili v mape elementov vaše knjižnice",
|
||||
"LabelSettingsTimeFormat": "Oblika časa",
|
||||
"LabelShare": "Deli",
|
||||
"LabelShareOpen": "Deli odprto",
|
||||
"LabelShareURL": "Deli URL",
|
||||
"LabelShowAll": "Prikaži vse",
|
||||
"LabelShowSeconds": "Prikaži sekunde",
|
||||
"LabelShowSubtitles": "Prikaži podnapise",
|
||||
"LabelSize": "Velikost",
|
||||
"LabelSleepTimer": "Časovnik za spanje",
|
||||
"LabelSlug": "Slug",
|
||||
"LabelStart": "Začetek",
|
||||
"LabelStartTime": "Začetni čas",
|
||||
"LabelStarted": "Začeto",
|
||||
"LabelStartedAt": "Začeto ob",
|
||||
"LabelStatsAudioTracks": "Zvočni posnetki",
|
||||
"LabelStatsAuthors": "Avtorji",
|
||||
"LabelStatsBestDay": "Najboljši dan",
|
||||
"LabelStatsDailyAverage": "Dnevno povprečje",
|
||||
"LabelStatsDays": "Dnevi",
|
||||
"LabelStatsDaysListened": "Poslušani dnevi",
|
||||
"LabelStatsHours": "Ure",
|
||||
"LabelStatsInARow": "v vrsti",
|
||||
"LabelStatsItemsFinished": "Končani elementi",
|
||||
"LabelStatsItemsInLibrary": "Elementi v knjižnici",
|
||||
"LabelStatsMinutes": "minute",
|
||||
"LabelStatsMinutesListening": "Poslušane minute",
|
||||
"LabelStatsOverallDays": "Skupaj dnevi",
|
||||
"LabelStatsOverallHours": "Skupaj ure",
|
||||
"LabelStatsWeekListening": "Tednov poslušanja",
|
||||
"LabelSubtitle": "Podnapis",
|
||||
"LabelSupportedFileTypes": "Podprte vrste datotek",
|
||||
"LabelTag": "Oznaka",
|
||||
"LabelTags": "Oznake",
|
||||
"LabelTagsAccessibleToUser": "Oznake, dostopne uporabniku",
|
||||
"LabelTagsNotAccessibleToUser": "Oznake, ki niso dostopne uporabniku",
|
||||
"LabelTasks": "Tekoče naloge",
|
||||
"LabelTextEditorBulletedList": "Seznam z oznakami",
|
||||
"LabelTextEditorLink": "Povezava",
|
||||
"LabelTextEditorNumberedList": "Številčni seznam",
|
||||
"LabelTextEditorUnlink": "Odveži",
|
||||
"LabelTheme": "Tema",
|
||||
"LabelThemeDark": "Temna",
|
||||
"LabelThemeLight": "Svetla",
|
||||
"LabelTimeBase": "Odvisna od časa",
|
||||
"LabelTimeDurationXHours": "{0} ur",
|
||||
"LabelTimeDurationXMinutes": "{0} minut",
|
||||
"LabelTimeDurationXSeconds": "{0} sekund",
|
||||
"LabelTimeInMinutes": "Čas v minutah",
|
||||
"LabelTimeListened": "Čas poslušanja",
|
||||
"LabelTimeListenedToday": "Čas poslušanja danes",
|
||||
"LabelTimeRemaining": "Še {0}",
|
||||
"LabelTimeToShift": "Čas prestavljanja v sekundah",
|
||||
"LabelTitle": "Naslov",
|
||||
"LabelToolsEmbedMetadata": "Vdelaj metapodatke",
|
||||
"LabelToolsEmbedMetadataDescription": "Vdelajte metapodatke v zvočne datoteke, vključno s sliko naslovnice in poglavji.",
|
||||
"LabelToolsMakeM4b": "Ustvari datoteko zvočne knjige M4B",
|
||||
"LabelToolsMakeM4bDescription": "Ustvarite datoteko zvočne knjige .M4B z vdelanimi metapodatki, sliko naslovnice in poglavji.",
|
||||
"LabelToolsSplitM4b": "Razdeli M4B v MP3 datoteke",
|
||||
"LabelToolsSplitM4bDescription": "Ustvarite MP3 datoteke iz datoteke M4B, razdeljene po poglavjih z vdelanimi metapodatki, naslovno sliko in poglavji.",
|
||||
"LabelTotalDuration": "Skupno trajanje",
|
||||
"LabelTotalTimeListened": "Skupni čas poslušanja",
|
||||
"LabelTrackFromFilename": "Posnetek iz datoteke",
|
||||
"LabelTrackFromMetadata": "Posnetek iz metapodatkov",
|
||||
"LabelTracks": "Posnetki",
|
||||
"LabelTracksMultiTrack": "Več posnetkov",
|
||||
"LabelTracksNone": "Brez posnetka",
|
||||
"LabelTracksSingleTrack": "Enojni posnetek",
|
||||
"LabelType": "Vrsta",
|
||||
"LabelUnabridged": "Neskrajšano",
|
||||
"LabelUndo": "Razveljavi",
|
||||
"LabelUnknown": "Neznano",
|
||||
"LabelUnknownPublishDate": "Neznan datum objave",
|
||||
"LabelUpdateCover": "Posodobi naslovnico",
|
||||
"LabelUpdateCoverHelp": "Dovoli prepisovanje obstoječih naslovnic za izbrane knjige, ko se najde ujemanje",
|
||||
"LabelUpdateDetails": "Posodobi podrobnosti",
|
||||
"LabelUpdateDetailsHelp": "Dovoli prepisovanje obstoječih podrobnosti za izbrane knjige, ko se najde ujemanje",
|
||||
"LabelUpdatedAt": "Posodobljeno ob",
|
||||
"LabelUploaderDragAndDrop": "Povleci in spusti datoteke ali mape",
|
||||
"LabelUploaderDropFiles": "Spusti datoteke",
|
||||
"LabelUploaderItemFetchMetadataHelp": "Samodejno pridobi naslov, avtorja in serijo",
|
||||
"LabelUseChapterTrack": "Uporabi posnetek poglavij",
|
||||
"LabelUseFullTrack": "Uporabi celoten posnetek",
|
||||
"LabelUser": "Uporabnik",
|
||||
"LabelUsername": "Uporabniško ime",
|
||||
"LabelValue": "Vrednost",
|
||||
"LabelVersion": "Verzija",
|
||||
"LabelViewBookmarks": "Ogled zaznamkov",
|
||||
"LabelViewChapters": "Ogled poglavij",
|
||||
"LabelViewPlayerSettings": "Ogled nastavitev predvajalnika",
|
||||
"LabelViewQueue": "Ogled čakalno vrsto predvajalnika",
|
||||
"LabelVolume": "Glasnost",
|
||||
"LabelWeekdaysToRun": "Delovni dnevi predvajanja",
|
||||
"LabelXBooks": "{0} knjig",
|
||||
"LabelXItems": "{0} elementov",
|
||||
"LabelYearReviewHide": "Skrij pregled leta",
|
||||
"LabelYearReviewShow": "Poglej pregled leta",
|
||||
"LabelYourAudiobookDuration": "Trajanje tvojih zvočnih knjig",
|
||||
"LabelYourBookmarks": "Tvoji zaznamki",
|
||||
"LabelYourPlaylists": "Tvoje seznami predvajanj",
|
||||
"LabelYourProgress": "Tvoj napredek",
|
||||
"MessageAddToPlayerQueue": "Dodaj v čakalno vrsto predvajalnika",
|
||||
"MessageAppriseDescription": "Če želite uporabljati to funkcijo, morate imeti zagnan primerek <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">API Apprise</a> ali API, ki bo obravnaval te iste zahteve. <br />Url API-ja Apprise mora biti celotna pot URL-ja za pošiljanje obvestila, npr. če je vaš primerek API-ja postrežen na <code>http://192.168.1.1:8337</code>, bi morali vnesti <code >http://192.168.1.1:8337/notify</code>.",
|
||||
"MessageBackupsDescription": "Varnostne kopije vključujejo uporabnike, napredek uporabnikov, podrobnosti elementov knjižnice, nastavitve strežnika in slike, shranjene v <code>/metadata/items</code> & <code>/metadata/authors</code>. Varnostne kopije <strong>ne</strong> vključujejo datotek, shranjenih v mapah vaše knjižnice.",
|
||||
"MessageBackupsLocationEditNote": "Opomba: Posodabljanje lokacije varnostne kopije ne bo premaknilo ali spremenilo obstoječih varnostnih kopij",
|
||||
"MessageBackupsLocationNoEditNote": "Opomba: Lokacija varnostne kopije je nastavljena s spremenljivko okolja in je tu ni mogoče spremeniti.",
|
||||
"MessageBackupsLocationPathEmpty": "Pot do lokacije varnostne kopije ne sme biti prazna",
|
||||
"MessageBatchQuickMatchDescription": "Hitro ujemanje bo poskušal dodati manjkajoče naslovnice in metapodatke za izbrane elemente. Omogočite spodnje možnosti, da omogočite hitremu ujemanju, da prepiše obstoječe naslovnice in/ali metapodatke.",
|
||||
"MessageBookshelfNoCollections": "Ustvaril nisi še nobene zbirke",
|
||||
"MessageBookshelfNoRSSFeeds": "Noben vir RSS ni odprt",
|
||||
"MessageBookshelfNoResultsForFilter": "Ni rezultatov za filter \"{0}: {1}\"",
|
||||
"MessageBookshelfNoResultsForQuery": "Ni rezultatov za poizvedbo",
|
||||
"MessageBookshelfNoSeries": "Nimate serij",
|
||||
"MessageChapterEndIsAfter": "Konec poglavja je za koncem vaše zvočne knjige",
|
||||
"MessageChapterErrorFirstNotZero": "Prvo poglavje se mora začeti pri 0",
|
||||
"MessageChapterErrorStartGteDuration": "Neveljaven začetni čas mora biti krajši od trajanja zvočne knjige",
|
||||
"MessageChapterErrorStartLtPrev": "Neveljaven začetni čas mora biti večji od ali enak začetnemu času prejšnjega poglavja",
|
||||
"MessageChapterStartIsAfter": "Začetek poglavja je po koncu vaše zvočne knjige",
|
||||
"MessageCheckingCron": "Preverjam cron...",
|
||||
"MessageConfirmCloseFeed": "Ali ste prepričani, da želite zapreti ta vir?",
|
||||
"MessageConfirmDeleteBackup": "Ali ste prepričani, da želite izbrisati varnostno kopijo za {0}?",
|
||||
"MessageConfirmDeleteDevice": "Ali ste prepričani, da želite izbrisati e-bralnik \"{0}\"?",
|
||||
"MessageConfirmDeleteFile": "To bo izbrisalo datoteko iz vašega datotečnega sistema. Ali ste prepričani?",
|
||||
"MessageConfirmDeleteLibrary": "Ali ste prepričani, da želite trajno izbrisati knjižnico \"{0}\"?",
|
||||
"MessageConfirmDeleteLibraryItem": "S tem boste element knjižnice izbrisali iz baze podatkov in vašega datotečnega sistema. Ste prepričani?",
|
||||
"MessageConfirmDeleteLibraryItems": "To bo izbrisalo {0} elementov knjižnice iz baze podatkov in vašega datotečnega sistema. Ste prepričani?",
|
||||
"MessageConfirmDeleteMetadataProvider": "Ali ste prepričani, da želite izbrisati ponudnika metapodatkov po meri \"{0}\"?",
|
||||
"MessageConfirmDeleteNotification": "Ali ste prepričani, da želite izbrisati to obvestilo?",
|
||||
"MessageConfirmDeleteSession": "Ali ste prepričani, da želite izbrisati to sejo?",
|
||||
"MessageConfirmForceReScan": "Ali ste prepričani, da želite vsiliti ponovno iskanje?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Ali ste prepričani, da želite označiti vse epizode kot dokončane?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Ali ste prepričani, da želite vse epizode označiti kot nedokončane?",
|
||||
"MessageConfirmMarkItemFinished": "Ali ste prepričani, da želite \"{0}\" označiti kot dokončanega?",
|
||||
"MessageConfirmMarkItemNotFinished": "Ali ste prepričani, da želite \"{0}\" označiti kot nedokončanega?",
|
||||
"MessageConfirmMarkSeriesFinished": "Ali ste prepričani, da želite vse knjige v tej seriji označiti kot dokončane?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Ali ste prepričani, da želite vse knjige v tej seriji označiti kot nedokončane?",
|
||||
"MessageConfirmNotificationTestTrigger": "Želite sprožiti to obvestilo s testnimi podatki?",
|
||||
"MessageConfirmPurgeCache": "Čiščenje predpomnilnika bo izbrisalo celoten imenik v <code>/metadata/cache</code>. <br /><br />Ali ste prepričani, da želite odstraniti imenik predpomnilnika?",
|
||||
"MessageConfirmPurgeItemsCache": "Čiščenje predpomnilnika elementov bo izbrisalo celoten imenik na <code>/metadata/cache/items</code>.<br />Ste prepričani?",
|
||||
"MessageConfirmQuickEmbed": "Opozorilo! Hitra vdelava ne bo varnostno kopirala vaših zvočnih datotek. Prepričajte se, da imate varnostno kopijo zvočnih datotek. <br><br>Ali želite nadaljevati?",
|
||||
"MessageConfirmReScanLibraryItems": "Ali ste prepričani, da želite ponovno poiskati {0} elementov?",
|
||||
"MessageConfirmRemoveAllChapters": "Ali ste prepričani, da želite odstraniti vsa poglavja?",
|
||||
"MessageConfirmRemoveAuthor": "Ali ste prepričani, da želite odstraniti avtorja \"{0}\"?",
|
||||
"MessageConfirmRemoveCollection": "Ali ste prepričani, da želite odstraniti zbirko \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisode": "Ali ste prepričani, da želite odstraniti epizodo \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisodes": "Ali ste prepričani, da želite odstraniti {0} epizod?",
|
||||
"MessageConfirmRemoveListeningSessions": "Ali ste prepričani, da želite odstraniti {0} sej poslušanja?",
|
||||
"MessageConfirmRemoveNarrator": "Ali ste prepričani, da želite odstraniti bralca \"{0}\"?",
|
||||
"MessageConfirmRemovePlaylist": "Ali ste prepričani, da želite odstraniti svoj seznam predvajanja \"{0}\"?",
|
||||
"MessageConfirmRenameGenre": "Ali ste prepričani, da želite preimenovati žanr \"{0}\" v \"{1}\" za vse elemente?",
|
||||
"MessageConfirmRenameGenreMergeNote": "Opomba: Ta žanr že obstaja, zato bosta združeni.",
|
||||
"MessageConfirmRenameGenreWarning": "Opozorilo! Podoben žanr z različnimi velikosti črk že obstaja \"{0}\".",
|
||||
"MessageConfirmRenameTag": "Ali ste prepričani, da želite preimenovati oznako \"{0}\" v \"{1}\" za vse elemente?",
|
||||
"MessageConfirmRenameTagMergeNote": "Opomba: Ta oznaka že obstaja, zato bosta združeni.",
|
||||
"MessageConfirmRenameTagWarning": "Opozorilo! Podobna oznaka z različnimi velikosti črk že obstaja \"{0}\".",
|
||||
"MessageConfirmResetProgress": "Ali ste prepričani, da želite ponastaviti svoj napredek?",
|
||||
"MessageConfirmSendEbookToDevice": "Ali ste prepričani, da želite poslati {0} e-knjigo \"{1}\" v napravo \"{2}\"?",
|
||||
"MessageConfirmUnlinkOpenId": "Ali ste prepričani, da želite prekiniti povezavo tega uporabnika z OpenID?",
|
||||
"MessageDownloadingEpisode": "Prenašam epizodo",
|
||||
"MessageDragFilesIntoTrackOrder": "Povlecite datoteke v pravilen vrstni red posnetkov",
|
||||
"MessageEmbedFailed": "Vdelava ni uspela!",
|
||||
"MessageEmbedFinished": "Vdelava končana!",
|
||||
"MessageEpisodesQueuedForDownload": "{0} epizod v čakalni vrsti za prenos",
|
||||
"MessageEreaderDevices": "Da zagotovite dostavo e-knjig, boste morda morali dodati zgornji e-poštni naslov kot veljavnega pošiljatelja za vsako spodaj navedeno napravo.",
|
||||
"MessageFeedURLWillBe": "URL vira bo {0}",
|
||||
"MessageFetching": "Pridobivam...",
|
||||
"MessageForceReScanDescription": "bo znova pregledal vse datoteke kot nov pregled. Oznake ID3 zvočnih datotek, datoteke OPF in besedilne datoteke bodo pregledane kot nove.",
|
||||
"MessageImportantNotice": "Pomembno obvestilo!",
|
||||
"MessageInsertChapterBelow": "Spodaj vstavite poglavje",
|
||||
"MessageItemsSelected": "{0} izbranih elementov",
|
||||
"MessageItemsUpdated": "Št. posodobljenih elementov: {0}",
|
||||
"MessageJoinUsOn": "Pridružite se nam",
|
||||
"MessageListeningSessionsInTheLastYear": "{0} sej poslušanja v zadnjem letu",
|
||||
"MessageLoading": "Nalagam...",
|
||||
"MessageLoadingFolders": "Nalagam mape...",
|
||||
"MessageLogsDescription": "Dnevniki so shranjeni v <code>/metadata/logs</code> kot datoteke JSON. Dnevniki zrušitev so shranjeni v <code>/metadata/logs/crash_logs.txt</code>.",
|
||||
"MessageM4BFailed": "M4B ni uspel!",
|
||||
"MessageM4BFinished": "M4B končan!",
|
||||
"MessageMapChapterTitles": "Preslikajte naslove poglavij v obstoječa poglavja zvočne knjige brez prilagajanja časovnih žigov",
|
||||
"MessageMarkAllEpisodesFinished": "Označi vse epizode kot končane",
|
||||
"MessageMarkAllEpisodesNotFinished": "Označi vse epizode kot nedokončane",
|
||||
"MessageMarkAsFinished": "Označi kot dokončano",
|
||||
"MessageMarkAsNotFinished": "Označi kot nedokončano",
|
||||
"MessageMatchBooksDescription": "bo poskušal povezati knjige v knjižnici s knjigo izbranega ponudnika iskanja in izpolniti prazne podatke in naslovnico. Ne prepisujejo se pa podrobnosti.",
|
||||
"MessageNoAudioTracks": "Ni zvočnih posnetkov",
|
||||
"MessageNoAuthors": "Brez avtorjev",
|
||||
"MessageNoBackups": "Brez varnostnih kopij",
|
||||
"MessageNoBookmarks": "Brez zaznamkov",
|
||||
"MessageNoChapters": "Brez poglavij",
|
||||
"MessageNoCollections": "Brez zbirk",
|
||||
"MessageNoCoversFound": "Ni naslovnic",
|
||||
"MessageNoDescription": "Ni opisa",
|
||||
"MessageNoDevices": "Ni naprav",
|
||||
"MessageNoDownloadsInProgress": "Trenutno ni prenosov v teku",
|
||||
"MessageNoDownloadsQueued": "Ni prenosov v čakalni vrsti",
|
||||
"MessageNoEpisodeMatchesFound": "Ni zadetkov za epizodo",
|
||||
"MessageNoEpisodes": "Ni epizod",
|
||||
"MessageNoFoldersAvailable": "Ni na voljo nobene mape",
|
||||
"MessageNoGenres": "Ni žanrov",
|
||||
"MessageNoIssues": "Ni težav",
|
||||
"MessageNoItems": "Ni elementov",
|
||||
"MessageNoItemsFound": "Ni najdenih elementov",
|
||||
"MessageNoListeningSessions": "Ni sej poslušanja",
|
||||
"MessageNoLogs": "Ni dnevnikov",
|
||||
"MessageNoMediaProgress": "Ni medijskega napredka",
|
||||
"MessageNoNotifications": "Ni obvestil",
|
||||
"MessageNoPodcastsFound": "Ni podcastov",
|
||||
"MessageNoResults": "Ni rezultatov",
|
||||
"MessageNoSearchResultsFor": "Ni rezultatov iskanja za \"{0}\"",
|
||||
"MessageNoSeries": "Ni serij",
|
||||
"MessageNoTags": "Ni oznak",
|
||||
"MessageNoTasksRunning": "Nobeno opravili ne teče",
|
||||
"MessageNoUpdatesWereNecessary": "Posodobitve niso bile potrebne",
|
||||
"MessageNoUserPlaylists": "Nimate seznamov predvajanja",
|
||||
"MessageNotYetImplemented": "Še ni implementirano",
|
||||
"MessageOpmlPreviewNote": "Opomba: To je predogled razčlenjene datoteke OPML. Dejanski naslov podcasta bo vzet iz vira RSS.",
|
||||
"MessageOr": "ali",
|
||||
"MessagePauseChapter": "Začasno ustavite predvajanje poglavja",
|
||||
"MessagePlayChapter": "Poslušajte začetek poglavja",
|
||||
"MessagePlaylistCreateFromCollection": "Ustvari seznam predvajanja iz zbirke",
|
||||
"MessagePleaseWait": "Prosim počakajte...",
|
||||
"MessagePodcastHasNoRSSFeedForMatching": "Podcast nima URL-ja vira RSS, ki bi ga lahko uporabil za ujemanje",
|
||||
"MessageQuickMatchDescription": "Izpolni prazne podrobnosti elementa in naslovnico s prvim rezultatom ujemanja iz '{0}'. Ne prepiše podrobnosti, razen če je omogočena nastavitev strežnika 'Prednostno ujemajoči se metapodatki'.",
|
||||
"MessageRemoveChapter": "Odstrani poglavje",
|
||||
"MessageRemoveEpisodes": "Odstrani toliko epizod: {0}",
|
||||
"MessageRemoveFromPlayerQueue": "Odstrani iz čakalne vrste predvajalnika",
|
||||
"MessageRemoveUserWarning": "Ali ste prepričani, da želite trajno izbrisati uporabnika \"{0}\"?",
|
||||
"MessageReportBugsAndContribute": "Prijavite hrošče, zahtevajte nove funkcije in prispevajte še naprej",
|
||||
"MessageResetChaptersConfirm": "Ali ste prepričani, da želite ponastaviti poglavja in razveljaviti spremembe, ki ste jih naredili?",
|
||||
"MessageRestoreBackupConfirm": "Ali ste prepričani, da želite obnoviti varnostno kopijo, ustvarjeno ob",
|
||||
"MessageRestoreBackupWarning": "Obnovitev varnostne kopije bo prepisala celotno zbirko podatkov, ki se nahaja v /config, in zajema slike v /metadata/items in /metadata/authors.<br /><br />Varnostne kopije ne spreminjajo nobenih datotek v mapah vaše knjižnice. Če ste omogočili nastavitve strežnika za shranjevanje naslovnic in metapodatkov v mapah vaše knjižnice, potem ti niso varnostno kopirani ali prepisani.<br /><br />Vsi odjemalci, ki uporabljajo vaš strežnik, bodo samodejno osveženi.",
|
||||
"MessageSearchResultsFor": "Rezultati iskanja za",
|
||||
"MessageSelected": "{0} izbrano",
|
||||
"MessageServerCouldNotBeReached": "Strežnika ni bilo mogoče doseči",
|
||||
"MessageSetChaptersFromTracksDescription": "Nastavite poglavja z uporabo vsake zvočne datoteke kot poglavja in naslova poglavja kot imena zvočne datoteke",
|
||||
"MessageShareExpirationWillBe": "Potečeno bo <strong>{0}</strong>",
|
||||
"MessageShareExpiresIn": "Poteče čez {0}",
|
||||
"MessageShareURLWillBe": "URL za skupno rabo bo <strong>{0}</strong>",
|
||||
"MessageStartPlaybackAtTime": "Začni predvajanje za \"{0}\" ob {1}?",
|
||||
"MessageThinking": "Razmišljam...",
|
||||
"MessageUploaderItemFailed": "Nalaganje ni uspelo",
|
||||
"MessageUploaderItemSuccess": "Uspešno naloženo!",
|
||||
"MessageUploading": "Nalaganje...",
|
||||
"MessageValidCronExpression": "Veljaven cron izraz",
|
||||
"MessageWatcherIsDisabledGlobally": "Pregledovalec je globalno onemogočen v nastavitvah strežnika",
|
||||
"MessageXLibraryIsEmpty": "{0} Knjižnica je prazna!",
|
||||
"MessageYourAudiobookDurationIsLonger": "Trajanje vaše zvočne knjige je daljše od ugotovljenega trajanja",
|
||||
"MessageYourAudiobookDurationIsShorter": "Trajanje vaše zvočne knjige je krajše od ugotovljenega trajanja",
|
||||
"NoteChangeRootPassword": "Korenski uporabnik je edini uporabnik, ki ima lahko prazno geslo",
|
||||
"NoteChapterEditorTimes": "Opomba: Začetni čas prvega poglavja mora ostati pri 0:00 in zadnji čas začetka poglavja ne sme preseči tega trajanja zvočne knjige.",
|
||||
"NoteFolderPicker": "Opomba: že preslikane mape ne bodo prikazane",
|
||||
"NoteRSSFeedPodcastAppsHttps": "Opozorilo: večina aplikacij za podcaste bo zahtevala, da URL vira RSS uporablja HTTPS",
|
||||
"NoteRSSFeedPodcastAppsPubDate": "Opozorilo: 1 ali več vaših epizod nima datuma objave. Nekatere aplikacije za podcaste to zahtevajo.",
|
||||
"NoteUploaderFoldersWithMediaFiles": "Mape z predstavnostnimi datotekami bodo obravnavane kot ločene postavke knjižnice.",
|
||||
"NoteUploaderOnlyAudioFiles": "Če nalagate samo zvočne datoteke, bo vsaka zvočna datoteka obravnavana kot ločena zvočna knjiga.",
|
||||
"NoteUploaderUnsupportedFiles": "Nepodprte datoteke so prezrte. Ko izberete ali spustite mapo, se druge datoteke, ki niso v mapi elementov, prezrejo.",
|
||||
"PlaceholderNewCollection": "Novo ime zbirke",
|
||||
"PlaceholderNewFolderPath": "Pot nove mape",
|
||||
"PlaceholderNewPlaylist": "Novo ime seznama predvajanja",
|
||||
"PlaceholderSearch": "Poišči..",
|
||||
"PlaceholderSearchEpisode": "Poišči epizodo...",
|
||||
"StatsAuthorsAdded": "dodanih avtorjev",
|
||||
"StatsBooksAdded": "dodanih knjig",
|
||||
"StatsBooksAdditional": "Nekateri dodatki vključujejo…",
|
||||
"StatsBooksFinished": "končane knjige",
|
||||
"StatsBooksFinishedThisYear": "Nekaj knjig, ki so bile dokončane letos…",
|
||||
"StatsBooksListenedTo": "poslušane knjige",
|
||||
"StatsCollectionGrewTo": "Vaša zbirka knjig se je povečala na …",
|
||||
"StatsSessions": "seje",
|
||||
"StatsSpentListening": "porabil za poslušanje",
|
||||
"StatsTopAuthor": "TOP AVTOR",
|
||||
"StatsTopAuthors": "TOP AVTORJI",
|
||||
"StatsTopGenre": "TOP ŽANR",
|
||||
"StatsTopGenres": "TOP ŽANRI",
|
||||
"StatsTopMonth": "TOP MESEC",
|
||||
"StatsTopNarrator": "TOP BRALEC",
|
||||
"StatsTopNarrators": "TOP BRALCI",
|
||||
"StatsTotalDuration": "S skupnim trajanjem…",
|
||||
"StatsYearInReview": "PREGLED LETA",
|
||||
"ToastAccountUpdateFailed": "Računa ni bilo mogoče posodobiti",
|
||||
"ToastAccountUpdateSuccess": "Račun posodobljen",
|
||||
"ToastAppriseUrlRequired": "Vnesti morate Apprise URL",
|
||||
"ToastAuthorImageRemoveSuccess": "Slika avtorja je odstranjena",
|
||||
"ToastAuthorNotFound": "Avtor \"{0}\" ni bil najden",
|
||||
"ToastAuthorRemoveSuccess": "Avtor odstranjen",
|
||||
"ToastAuthorSearchNotFound": "Ne najdem avtorja",
|
||||
"ToastAuthorUpdateFailed": "Avtorja ni bilo mogoče posodobiti",
|
||||
"ToastAuthorUpdateMerged": "Avtor združen",
|
||||
"ToastAuthorUpdateSuccess": "Avtor posodobljen",
|
||||
"ToastAuthorUpdateSuccessNoImageFound": "Avtor posodobljen (ne najdem slike)",
|
||||
"ToastBackupAppliedSuccess": "Uporabljena varnostna kopija",
|
||||
"ToastBackupCreateFailed": "Varnostne kopije ni bilo mogoče ustvariti",
|
||||
"ToastBackupCreateSuccess": "Varnostna kopija ustvarjena",
|
||||
"ToastBackupDeleteFailed": "Varnostne kopije ni bilo mogoče izbrisati",
|
||||
"ToastBackupDeleteSuccess": "Varnostna kopija izbrisana",
|
||||
"ToastBackupInvalidMaxKeep": "Neveljavno število varnostnih kopij za ohranjanje",
|
||||
"ToastBackupInvalidMaxSize": "Neveljavna največja velikost varnostne kopije",
|
||||
"ToastBackupPathUpdateFailed": "Posodobitev poti varnostnih kopij ni uspela",
|
||||
"ToastBackupRestoreFailed": "Varnostne kopije ni bilo mogoče obnoviti",
|
||||
"ToastBackupUploadFailed": "Nalaganje varnostne kopije ni uspelo",
|
||||
"ToastBackupUploadSuccess": "Varnostna kopija je naložena",
|
||||
"ToastBatchDeleteFailed": "Paketno brisanje ni uspelo",
|
||||
"ToastBatchDeleteSuccess": "Paketno brisanje je bilo uspešno",
|
||||
"ToastBatchUpdateFailed": "Paketna posodobitev ni uspela",
|
||||
"ToastBatchUpdateSuccess": "Paketna posodobitev je uspela",
|
||||
"ToastBookmarkCreateFailed": "Zaznamka ni bilo mogoče ustvariti",
|
||||
"ToastBookmarkCreateSuccess": "Zaznamek dodan",
|
||||
"ToastBookmarkRemoveSuccess": "Zaznamek odstranjen",
|
||||
"ToastBookmarkUpdateFailed": "Zaznamka ni bilo mogoče posodobiti",
|
||||
"ToastBookmarkUpdateSuccess": "Zaznamek posodobljen",
|
||||
"ToastCachePurgeFailed": "Čiščenje predpomnilnika ni uspelo",
|
||||
"ToastCachePurgeSuccess": "Predpomnilnik je bil uspešno očiščen",
|
||||
"ToastChaptersHaveErrors": "Poglavja imajo napake",
|
||||
"ToastChaptersMustHaveTitles": "Poglavja morajo imeti naslove",
|
||||
"ToastChaptersRemoved": "Poglavja so odstranjena",
|
||||
"ToastCollectionItemsAddFailed": "Dodajanje elementov v zbirko ni uspelo",
|
||||
"ToastCollectionItemsAddSuccess": "Dodajanje elementov v zbirko je bilo uspešno",
|
||||
"ToastCollectionItemsRemoveSuccess": "Elementi so bili odstranjeni iz zbirke",
|
||||
"ToastCollectionRemoveSuccess": "Zbirka je bila odstranjena",
|
||||
"ToastCollectionUpdateFailed": "Zbirke ni bilo mogoče posodobiti",
|
||||
"ToastCollectionUpdateSuccess": "Zbirka je bila posodobljena",
|
||||
"ToastCoverUpdateFailed": "Posodobitev naslovnice ni uspela",
|
||||
"ToastDeleteFileFailed": "Brisanje datoteke ni uspelo",
|
||||
"ToastDeleteFileSuccess": "Datoteka je bila izbrisana",
|
||||
"ToastDeviceAddFailed": "Naprave ni bilo mogoče dodati",
|
||||
"ToastDeviceNameAlreadyExists": "Elektronska naprava s tem imenom že obstaja",
|
||||
"ToastDeviceTestEmailFailed": "Pošiljanje testnega e-poštnega sporočila ni uspelo",
|
||||
"ToastDeviceTestEmailSuccess": "Testno e-poštno sporočilo je poslano",
|
||||
"ToastDeviceUpdateFailed": "Naprave ni bilo mogoče posodobiti",
|
||||
"ToastEmailSettingsUpdateFailed": "E-poštnih nastavitev ni bilo mogoče posodobiti",
|
||||
"ToastEmailSettingsUpdateSuccess": "E-poštne nastavitve so bile posodobljene",
|
||||
"ToastEncodeCancelFailed": "Napaka pri preklicu prekodiranja",
|
||||
"ToastEncodeCancelSucces": "Prekodiranje prekinjeno",
|
||||
"ToastEpisodeDownloadQueueClearFailed": "Čiščenje čakalne vrste ni uspelo",
|
||||
"ToastEpisodeDownloadQueueClearSuccess": "Čakalna vrsta za prenos epizod je počiščena",
|
||||
"ToastErrorCannotShare": "V tej napravi ni mogoče dati v skupno rabo",
|
||||
"ToastFailedToLoadData": "Podatkov ni bilo mogoče naložiti",
|
||||
"ToastFailedToShare": "Skupna raba ni uspela",
|
||||
"ToastFailedToUpdateAccount": "Računa ni bilo mogoče posodobiti",
|
||||
"ToastFailedToUpdateUser": "Uporabnika ni bilo mogoče posodobiti",
|
||||
"ToastInvalidImageUrl": "Neveljaven URL slike",
|
||||
"ToastInvalidUrl": "Neveljaven URL",
|
||||
"ToastItemCoverUpdateFailed": "Naslovnice elementa ni bilo mogoče posodobiti",
|
||||
"ToastItemCoverUpdateSuccess": "Naslovnica elementa je bila posodobljena",
|
||||
"ToastItemDeletedFailed": "Elementa ni bilo mogoče izbrisati",
|
||||
"ToastItemDeletedSuccess": "Element je bil izbrisan",
|
||||
"ToastItemDetailsUpdateFailed": "Posodobitev podrobnosti elementa ni uspela",
|
||||
"ToastItemDetailsUpdateSuccess": "Podrobnosti elementa so bile posodobjene",
|
||||
"ToastItemMarkedAsFinishedFailed": "Označevanje kot dokončano ni uspelo",
|
||||
"ToastItemMarkedAsFinishedSuccess": "Element je označen kot dokončan",
|
||||
"ToastItemMarkedAsNotFinishedFailed": "Ni bilo mogoče označiti kot nedokončano",
|
||||
"ToastItemMarkedAsNotFinishedSuccess": "Element označen kot nedokončan",
|
||||
"ToastItemUpdateFailed": "Elementa ni bilo mogoče posodobiti",
|
||||
"ToastItemUpdateSuccess": "Element je bil posodobljen",
|
||||
"ToastLibraryCreateFailed": "Knjižnice ni bilo mogoče ustvariti",
|
||||
"ToastLibraryCreateSuccess": "Knjižnica \"{0}\" je bila ustvarjena",
|
||||
"ToastLibraryDeleteFailed": "Knjižnice ni bilo mogoče izbrisati",
|
||||
"ToastLibraryDeleteSuccess": "Knjižnica je bila izbrisana",
|
||||
"ToastLibraryScanFailedToStart": "Pregleda ni bilo mogoče začeti",
|
||||
"ToastLibraryScanStarted": "Pregled knjižnice se je začel",
|
||||
"ToastLibraryUpdateFailed": "Knjižnice ni bilo mogoče posodobiti",
|
||||
"ToastLibraryUpdateSuccess": "Knjižnica \"{0}\" je bila posodobljena",
|
||||
"ToastNameEmailRequired": "Ime in e-pošta sta obvezna",
|
||||
"ToastNameRequired": "Ime je obvezno",
|
||||
"ToastNewUserCreatedFailed": "Računa ni bilo mogoče ustvariti: \"{0}\"",
|
||||
"ToastNewUserCreatedSuccess": "Nov račun je bil ustvarjen",
|
||||
"ToastNewUserLibraryError": "Izbrati morate vsaj eno knjižnico",
|
||||
"ToastNewUserPasswordError": "Mora imeti geslo, samo korenski uporabnik ima lahko prazno geslo",
|
||||
"ToastNewUserTagError": "Izbrati morate vsaj eno oznako",
|
||||
"ToastNewUserUsernameError": "Vnesite uporabniško ime",
|
||||
"ToastNoUpdatesNecessary": "Posodobitve niso potrebne",
|
||||
"ToastNotificationCreateFailed": "Obvestila ni bilo mogoče ustvariti",
|
||||
"ToastNotificationDeleteFailed": "Brisanje obvestila ni uspelo",
|
||||
"ToastNotificationFailedMaximum": "Največje število neuspelih poskusov mora biti >= 0",
|
||||
"ToastNotificationQueueMaximum": "Največja čakalna vrsta obvestil mora biti >= 0",
|
||||
"ToastNotificationSettingsUpdateFailed": "Nastavitev obvestil ni bilo mogoče posodobiti",
|
||||
"ToastNotificationSettingsUpdateSuccess": "Nastavitve obvestil so bile posodobljene",
|
||||
"ToastNotificationTestTriggerFailed": "Sprožitev testnega obvestila ni uspela",
|
||||
"ToastNotificationTestTriggerSuccess": "Sproženo testno obvestilo",
|
||||
"ToastNotificationUpdateFailed": "Obvestila ni bilo mogoče posodobiti",
|
||||
"ToastNotificationUpdateSuccess": "Obvestilo posodobljeno",
|
||||
"ToastPlaylistCreateFailed": "Seznama predvajanja ni bilo mogoče ustvariti",
|
||||
"ToastPlaylistCreateSuccess": "Seznam predvajanja je bil ustvarjen",
|
||||
"ToastPlaylistRemoveSuccess": "Seznam predvajanja odstranjen",
|
||||
"ToastPlaylistUpdateFailed": "Seznama predvajanja ni bilo mogoče posodobiti",
|
||||
"ToastPlaylistUpdateSuccess": "Seznam predvajanja je bil posodobljen",
|
||||
"ToastPodcastCreateFailed": "Podcasta ni bilo mogoče ustvariti",
|
||||
"ToastPodcastCreateSuccess": "Podcast je bil uspešno ustvarjen",
|
||||
"ToastPodcastGetFeedFailed": "Vira podcasta ni bilo mogoče pridobiti",
|
||||
"ToastPodcastNoEpisodesInFeed": "V viru RSS ni bilo mogoče najti nobene epizode",
|
||||
"ToastPodcastNoRssFeed": "Podcast nima vira RSS",
|
||||
"ToastProviderCreatedFailed": "Ponudnika ni bilo mogoče dodati",
|
||||
"ToastProviderCreatedSuccess": "Dodan je bil nov ponudnik",
|
||||
"ToastProviderNameAndUrlRequired": "Obvezen podatek sta ime in URL",
|
||||
"ToastProviderRemoveSuccess": "Ponudnik je bil odstranjen",
|
||||
"ToastRSSFeedCloseFailed": "Vira RSS ni bilo mogoče zapreti",
|
||||
"ToastRSSFeedCloseSuccess": "Vir RSS je bil zaprt",
|
||||
"ToastRemoveFailed": "Odstranitev ni uspela",
|
||||
"ToastRemoveItemFromCollectionFailed": "Elementa ni bilo mogoče odstraniti iz zbirke",
|
||||
"ToastRemoveItemFromCollectionSuccess": "Element je bil odstranjen iz zbirke",
|
||||
"ToastRemoveItemsWithIssuesFailed": "Elementov knjižnice s težavami ni bilo mogoče odstraniti",
|
||||
"ToastRemoveItemsWithIssuesSuccess": "Odstranjeni so bili elementi knjižnice s težavami",
|
||||
"ToastRenameFailed": "Preimenovanje ni uspelo",
|
||||
"ToastRescanFailed": "Ponovni pregled ni uspel za {0}",
|
||||
"ToastRescanRemoved": "Ponovni pregled celotnega elementa je bil odstranjen",
|
||||
"ToastRescanUpToDate": "Ponovni pregled celotnega elementa je bil ažuren",
|
||||
"ToastRescanUpdated": "Ponovni pregled celotnega elementa je bil posodobljen",
|
||||
"ToastScanFailed": "Pregled elementa knjižnice ni uspel",
|
||||
"ToastSelectAtLeastOneUser": "Izberite vsaj enega uporabnika",
|
||||
"ToastSendEbookToDeviceFailed": "E-knjige ni bilo mogoče poslati v napravo",
|
||||
"ToastSendEbookToDeviceSuccess": "E-knjiga je bila poslana v napravo \"{0}\"",
|
||||
"ToastSeriesUpdateFailed": "Posodobitev serije ni uspela",
|
||||
"ToastSeriesUpdateSuccess": "Uspešna posodobitev serije",
|
||||
"ToastServerSettingsUpdateFailed": "Nastavitev strežnika ni bilo mogoče posodobiti",
|
||||
"ToastServerSettingsUpdateSuccess": "Nastavitve strežnika so bile posodobljene",
|
||||
"ToastSessionCloseFailed": "Seje ni bilo mogoče zapreti",
|
||||
"ToastSessionDeleteFailed": "Brisanje seje ni uspelo",
|
||||
"ToastSessionDeleteSuccess": "Seja je bila izbrisana",
|
||||
"ToastSlugMustChange": "Slug vsebuje neveljavne znake",
|
||||
"ToastSlugRequired": "Slug je obvezen podatek",
|
||||
"ToastSocketConnected": "Omrežna povezava je priklopljena",
|
||||
"ToastSocketDisconnected": "Omrežna povezava je odklopljena",
|
||||
"ToastSocketFailedToConnect": "Omrežna povezava ni uspela vzpostaviti priklopa",
|
||||
"ToastSortingPrefixesEmptyError": "Imeti mora vsaj 1 predpono za razvrščanje",
|
||||
"ToastSortingPrefixesUpdateFailed": "Posodobitev predpon za razvrščanje ni uspela",
|
||||
"ToastSortingPrefixesUpdateSuccess": "Predpone za razvrščanje so bile posodobljene ({0} elementov)",
|
||||
"ToastTitleRequired": "Naslov je obvezen",
|
||||
"ToastUnknownError": "Neznana napaka",
|
||||
"ToastUnlinkOpenIdFailed": "Prekinitev povezave uporabnika z OpenID ni uspela",
|
||||
"ToastUnlinkOpenIdSuccess": "Uporabnik je prekinil povezavo z OpenID",
|
||||
"ToastUserDeleteFailed": "Brisanje uporabnika ni uspelo",
|
||||
"ToastUserDeleteSuccess": "Uporabnik je bil izbrisan",
|
||||
"ToastUserPasswordChangeSuccess": "Geslo je bilo uspešno spremenjeno",
|
||||
"ToastUserPasswordMismatch": "Gesli se ne ujemata",
|
||||
"ToastUserPasswordMustChange": "Novo geslo se ne sme ujemati s starim geslom",
|
||||
"ToastUserRootRequireName": "Vnesti morate korensko uporabniško ime"
|
||||
}
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.13.1",
|
||||
"version": "2.13.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.13.1",
|
||||
"version": "2.13.3",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"axios": "^0.27.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.13.1",
|
||||
"version": "2.13.3",
|
||||
"buildNumber": 1,
|
||||
"description": "Self-hosted audiobook and podcast server",
|
||||
"main": "index.js",
|
||||
|
||||
@@ -442,26 +442,6 @@ class Database {
|
||||
await this.models.feed.removeById(feedId)
|
||||
}
|
||||
|
||||
updateSeries(oldSeries) {
|
||||
if (!this.sequelize) return false
|
||||
return this.models.series.updateFromOld(oldSeries)
|
||||
}
|
||||
|
||||
async createSeries(oldSeries) {
|
||||
if (!this.sequelize) return false
|
||||
await this.models.series.createFromOld(oldSeries)
|
||||
}
|
||||
|
||||
async createBulkSeries(oldSeriesObjs) {
|
||||
if (!this.sequelize) return false
|
||||
await this.models.series.createBulkFromOld(oldSeriesObjs)
|
||||
}
|
||||
|
||||
async removeSeries(seriesId) {
|
||||
if (!this.sequelize) return false
|
||||
await this.models.series.removeById(seriesId)
|
||||
}
|
||||
|
||||
async createBulkBookAuthors(bookAuthors) {
|
||||
if (!this.sequelize) return false
|
||||
await this.models.bookAuthor.bulkCreate(bookAuthors)
|
||||
@@ -678,7 +658,7 @@ class Database {
|
||||
*/
|
||||
async getSeriesIdByName(libraryId, seriesName) {
|
||||
if (!this.libraryFilterData[libraryId]) {
|
||||
return (await this.seriesModel.getOldByNameAndLibrary(seriesName, libraryId))?.id || null
|
||||
return (await this.seriesModel.getByNameAndLibrary(seriesName, libraryId))?.id || null
|
||||
}
|
||||
return this.libraryFilterData[libraryId].series.find((se) => se.name === seriesName)?.id || null
|
||||
}
|
||||
|
||||
@@ -104,6 +104,9 @@ class AuthorController {
|
||||
let hasUpdated = false
|
||||
|
||||
const authorNameUpdate = payload.name !== undefined && payload.name !== req.author.name
|
||||
if (authorNameUpdate) {
|
||||
payload.lastFirst = Database.authorModel.getLastFirst(payload.name)
|
||||
}
|
||||
|
||||
// Check if author name matches another author and merge the authors
|
||||
let existingAuthor = null
|
||||
@@ -169,6 +172,11 @@ class AuthorController {
|
||||
return
|
||||
}
|
||||
|
||||
// If lastFirst is not set, get it from the name
|
||||
if (!authorNameUpdate && !req.author.lastFirst) {
|
||||
payload.lastFirst = Database.authorModel.getLastFirst(req.author.name)
|
||||
}
|
||||
|
||||
// Regular author update
|
||||
req.author.set(payload)
|
||||
if (req.author.changed()) {
|
||||
|
||||
@@ -629,11 +629,10 @@ class LibraryController {
|
||||
|
||||
const series = await Database.seriesModel.findByPk(req.params.seriesId)
|
||||
if (!series) return res.sendStatus(404)
|
||||
const oldSeries = series.getOldSeries()
|
||||
|
||||
const libraryItemsInSeries = await libraryItemsBookFilters.getLibraryItemsForSeries(oldSeries, req.user)
|
||||
const libraryItemsInSeries = await libraryItemsBookFilters.getLibraryItemsForSeries(series, req.user)
|
||||
|
||||
const seriesJson = oldSeries.toJSON()
|
||||
const seriesJson = series.toOldJSON()
|
||||
if (include.includes('progress')) {
|
||||
const libraryItemsFinished = libraryItemsInSeries.filter((li) => !!req.user.getMediaProgress(li.media.id)?.isFinished)
|
||||
seriesJson.progress = {
|
||||
|
||||
@@ -125,7 +125,7 @@ class RSSFeedController {
|
||||
async openRSSFeedForSeries(req, res) {
|
||||
const options = req.body || {}
|
||||
|
||||
const series = await Database.seriesModel.getOldById(req.params.seriesId)
|
||||
const series = await Database.seriesModel.findByPk(req.params.seriesId)
|
||||
if (!series) return res.sendStatus(404)
|
||||
|
||||
// Check request body options exist
|
||||
@@ -140,7 +140,7 @@ class RSSFeedController {
|
||||
return res.status(400).send('Slug already in use')
|
||||
}
|
||||
|
||||
const seriesJson = series.toJSON()
|
||||
const seriesJson = series.toOldJSON()
|
||||
|
||||
// Get books in series that have audio tracks
|
||||
seriesJson.books = (await libraryItemsBookFilters.getLibraryItemsForSeries(series)).filter((li) => li.media.numTracks)
|
||||
|
||||
@@ -9,6 +9,11 @@ const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilter
|
||||
* @property {import('../models/User')} user
|
||||
*
|
||||
* @typedef {Request & RequestUserObject} RequestWithUser
|
||||
*
|
||||
* @typedef RequestEntityObject
|
||||
* @property {import('../models/Series')} series
|
||||
*
|
||||
* @typedef {RequestWithUser & RequestEntityObject} SeriesControllerRequest
|
||||
*/
|
||||
|
||||
class SeriesController {
|
||||
@@ -21,7 +26,7 @@ class SeriesController {
|
||||
* TODO: Update mobile app to use /api/libraries/:id/series/:seriesId API route instead
|
||||
* Series are not library specific so we need to know what the library id is
|
||||
*
|
||||
* @param {RequestWithUser} req
|
||||
* @param {SeriesControllerRequest} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async findOne(req, res) {
|
||||
@@ -30,7 +35,7 @@ class SeriesController {
|
||||
.map((v) => v.trim())
|
||||
.filter((v) => !!v)
|
||||
|
||||
const seriesJson = req.series.toJSON()
|
||||
const seriesJson = req.series.toOldJSON()
|
||||
|
||||
// Add progress map with isFinished flag
|
||||
if (include.includes('progress')) {
|
||||
@@ -54,17 +59,28 @@ class SeriesController {
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Currently unused in the client, should check for duplicate name
|
||||
*
|
||||
* @param {RequestWithUser} req
|
||||
* @param {SeriesControllerRequest} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async update(req, res) {
|
||||
const hasUpdated = req.series.update(req.body)
|
||||
if (hasUpdated) {
|
||||
await Database.updateSeries(req.series)
|
||||
SocketAuthority.emitter('series_updated', req.series.toJSON())
|
||||
const keysToUpdate = ['name', 'description']
|
||||
const payload = {}
|
||||
for (const key of keysToUpdate) {
|
||||
if (req.body[key] !== undefined && typeof req.body[key] === 'string') {
|
||||
payload[key] = req.body[key]
|
||||
}
|
||||
}
|
||||
res.json(req.series.toJSON())
|
||||
if (!Object.keys(payload).length) {
|
||||
return res.status(400).send('No valid fields to update')
|
||||
}
|
||||
req.series.set(payload)
|
||||
if (req.series.changed()) {
|
||||
await req.series.save()
|
||||
SocketAuthority.emitter('series_updated', req.series.toOldJSON())
|
||||
}
|
||||
res.json(req.series.toOldJSON())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,7 +90,7 @@ class SeriesController {
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async middleware(req, res, next) {
|
||||
const series = await Database.seriesModel.getOldById(req.params.id)
|
||||
const series = await Database.seriesModel.findByPk(req.params.id)
|
||||
if (!series) return res.sendStatus(404)
|
||||
|
||||
/**
|
||||
|
||||
@@ -205,9 +205,12 @@ class UserController {
|
||||
async update(req, res) {
|
||||
const user = req.reqUser
|
||||
|
||||
if (user.type === 'root' && !req.user.isRoot) {
|
||||
if (user.isRoot && !req.user.isRoot) {
|
||||
Logger.error(`[UserController] Admin user "${req.user.username}" attempted to update root user`)
|
||||
return res.sendStatus(403)
|
||||
} else if (user.isRoot) {
|
||||
// Root user cannot update type
|
||||
delete req.body.type
|
||||
}
|
||||
|
||||
const updatePayload = req.body
|
||||
@@ -270,8 +273,10 @@ class UserController {
|
||||
const permissions = {
|
||||
...user.permissions
|
||||
}
|
||||
const defaultPermissions = Database.userModel.getDefaultPermissionsForUserType(updatePayload.type || user.type || 'user')
|
||||
for (const key in updatePayload.permissions) {
|
||||
if (permissions[key] !== undefined) {
|
||||
// Check that the key is a valid permission key or is included in the default permissions
|
||||
if (permissions[key] !== undefined || defaultPermissions[key] !== undefined) {
|
||||
if (typeof updatePayload.permissions[key] !== 'boolean') {
|
||||
Logger.warn(`[UserController] update: Invalid permission value for key ${key}. Should be boolean`)
|
||||
} else if (permissions[key] !== updatePayload.permissions[key]) {
|
||||
|
||||
@@ -25,7 +25,7 @@ class RssFeedManager {
|
||||
return false
|
||||
}
|
||||
} else if (feedObj.entityType === 'series') {
|
||||
const series = await Database.seriesModel.getOldById(feedObj.entityId)
|
||||
const series = await Database.seriesModel.findByPk(feedObj.entityId)
|
||||
if (!series) {
|
||||
Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Series "${feedObj.entityId}" not found`)
|
||||
return false
|
||||
@@ -133,9 +133,9 @@ class RssFeedManager {
|
||||
}
|
||||
}
|
||||
} else if (feed.entityType === 'series') {
|
||||
const series = await Database.seriesModel.getOldById(feed.entityId)
|
||||
const series = await Database.seriesModel.findByPk(feed.entityId)
|
||||
if (series) {
|
||||
const seriesJson = series.toJSON()
|
||||
const seriesJson = series.toOldJSON()
|
||||
|
||||
// Get books in series that have audio tracks
|
||||
seriesJson.books = (await libraryItemsBookFilters.getLibraryItemsForSeries(series)).filter((li) => li.media.numTracks)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const { DataTypes, Model, where, fn, col } = require('sequelize')
|
||||
const parseNameString = require('../utils/parsers/parseNameString')
|
||||
|
||||
class Author extends Model {
|
||||
constructor(values, options) {
|
||||
@@ -24,6 +25,16 @@ class Author extends Model {
|
||||
this.createdAt
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} name
|
||||
* @returns {string}
|
||||
*/
|
||||
static getLastFirst(name) {
|
||||
if (!name) return null
|
||||
return parseNameString.nameToLastFirst(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if author exists
|
||||
* @param {string} authorId
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const { DataTypes, Model, where, fn, col } = require('sequelize')
|
||||
|
||||
const oldSeries = require('../objects/entities/Series')
|
||||
const { getTitlePrefixAtEnd } = require('../utils/index')
|
||||
|
||||
class Series extends Model {
|
||||
constructor(values, options) {
|
||||
@@ -22,70 +22,6 @@ class Series extends Model {
|
||||
this.updatedAt
|
||||
}
|
||||
|
||||
static async getAllOldSeries() {
|
||||
const series = await this.findAll()
|
||||
return series.map((se) => se.getOldSeries())
|
||||
}
|
||||
|
||||
getOldSeries() {
|
||||
return new oldSeries({
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
libraryId: this.libraryId,
|
||||
addedAt: this.createdAt.valueOf(),
|
||||
updatedAt: this.updatedAt.valueOf()
|
||||
})
|
||||
}
|
||||
|
||||
static updateFromOld(oldSeries) {
|
||||
const series = this.getFromOld(oldSeries)
|
||||
return this.update(series, {
|
||||
where: {
|
||||
id: series.id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static createFromOld(oldSeries) {
|
||||
const series = this.getFromOld(oldSeries)
|
||||
return this.create(series)
|
||||
}
|
||||
|
||||
static createBulkFromOld(oldSeriesObjs) {
|
||||
const series = oldSeriesObjs.map(this.getFromOld)
|
||||
return this.bulkCreate(series)
|
||||
}
|
||||
|
||||
static getFromOld(oldSeries) {
|
||||
return {
|
||||
id: oldSeries.id,
|
||||
name: oldSeries.name,
|
||||
nameIgnorePrefix: oldSeries.nameIgnorePrefix,
|
||||
description: oldSeries.description,
|
||||
libraryId: oldSeries.libraryId
|
||||
}
|
||||
}
|
||||
|
||||
static removeById(seriesId) {
|
||||
return this.destroy({
|
||||
where: {
|
||||
id: seriesId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get oldSeries by id
|
||||
* @param {string} seriesId
|
||||
* @returns {Promise<oldSeries>}
|
||||
*/
|
||||
static async getOldById(seriesId) {
|
||||
const series = await this.findByPk(seriesId)
|
||||
if (!series) return null
|
||||
return series.getOldSeries()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if series exists
|
||||
* @param {string} seriesId
|
||||
@@ -96,24 +32,21 @@ class Series extends Model {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get old series by name and libraryId. name case insensitive
|
||||
* Get series by name and libraryId. name case insensitive
|
||||
*
|
||||
* @param {string} seriesName
|
||||
* @param {string} libraryId
|
||||
* @returns {Promise<oldSeries>}
|
||||
* @returns {Promise<Series>}
|
||||
*/
|
||||
static async getOldByNameAndLibrary(seriesName, libraryId) {
|
||||
const series = (
|
||||
await this.findOne({
|
||||
where: [
|
||||
where(fn('lower', col('name')), seriesName.toLowerCase()),
|
||||
{
|
||||
libraryId
|
||||
}
|
||||
]
|
||||
})
|
||||
)?.getOldSeries()
|
||||
return series
|
||||
static async getByNameAndLibrary(seriesName, libraryId) {
|
||||
return this.findOne({
|
||||
where: [
|
||||
where(fn('lower', col('name')), seriesName.toLowerCase()),
|
||||
{
|
||||
libraryId
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -163,6 +96,26 @@ class Series extends Model {
|
||||
})
|
||||
Series.belongsTo(library)
|
||||
}
|
||||
|
||||
toOldJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
nameIgnorePrefix: getTitlePrefixAtEnd(this.name),
|
||||
description: this.description,
|
||||
addedAt: this.createdAt.valueOf(),
|
||||
updatedAt: this.updatedAt.valueOf(),
|
||||
libraryId: this.libraryId
|
||||
}
|
||||
}
|
||||
|
||||
toJSONMinimal(sequence) {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
sequence
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Series
|
||||
|
||||
@@ -108,6 +108,7 @@ class User extends Model {
|
||||
accessAllLibraries: true,
|
||||
accessAllTags: true,
|
||||
accessExplicitContent: true,
|
||||
selectedTagsNotAccessible: false,
|
||||
librariesAccessible: [],
|
||||
itemTagsSelected: []
|
||||
}
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
const uuidv4 = require("uuid").v4
|
||||
const { getTitleIgnorePrefix, getTitlePrefixAtEnd } = require('../../utils/index')
|
||||
|
||||
class Series {
|
||||
constructor(series) {
|
||||
this.id = null
|
||||
this.name = null
|
||||
this.description = null
|
||||
this.addedAt = null
|
||||
this.updatedAt = null
|
||||
this.libraryId = null
|
||||
|
||||
if (series) {
|
||||
this.construct(series)
|
||||
}
|
||||
}
|
||||
|
||||
construct(series) {
|
||||
this.id = series.id
|
||||
this.name = series.name
|
||||
this.description = series.description || null
|
||||
this.addedAt = series.addedAt
|
||||
this.updatedAt = series.updatedAt
|
||||
this.libraryId = series.libraryId
|
||||
}
|
||||
|
||||
get nameIgnorePrefix() {
|
||||
if (!this.name) return ''
|
||||
return getTitleIgnorePrefix(this.name)
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
nameIgnorePrefix: getTitlePrefixAtEnd(this.name),
|
||||
description: this.description,
|
||||
addedAt: this.addedAt,
|
||||
updatedAt: this.updatedAt,
|
||||
libraryId: this.libraryId
|
||||
}
|
||||
}
|
||||
|
||||
toJSONMinimal(sequence) {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
sequence
|
||||
}
|
||||
}
|
||||
|
||||
setData(data, libraryId) {
|
||||
this.id = uuidv4()
|
||||
this.name = data.name
|
||||
this.description = data.description || null
|
||||
this.addedAt = Date.now()
|
||||
this.updatedAt = Date.now()
|
||||
this.libraryId = libraryId
|
||||
}
|
||||
|
||||
update(series) {
|
||||
if (!series) return false
|
||||
const keysToUpdate = ['name', 'description']
|
||||
let hasUpdated = false
|
||||
for (const key of keysToUpdate) {
|
||||
if (series[key] !== undefined && series[key] !== this[key]) {
|
||||
this[key] = series[key]
|
||||
hasUpdated = true
|
||||
}
|
||||
}
|
||||
return hasUpdated
|
||||
}
|
||||
|
||||
checkNameEquals(name) {
|
||||
if (!name || !this.name) return false
|
||||
return this.name.toLowerCase() == name.toLowerCase().trim()
|
||||
}
|
||||
}
|
||||
module.exports = Series
|
||||
@@ -33,7 +33,7 @@ const CustomMetadataProviderController = require('../controllers/CustomMetadataP
|
||||
const MiscController = require('../controllers/MiscController')
|
||||
const ShareController = require('../controllers/ShareController')
|
||||
|
||||
const Series = require('../objects/entities/Series')
|
||||
const { getTitleIgnorePrefix } = require('../utils/index')
|
||||
|
||||
class ApiRouter {
|
||||
constructor(Server) {
|
||||
@@ -524,13 +524,15 @@ class ApiRouter {
|
||||
async removeEmptySeries(series) {
|
||||
await this.rssFeedManager.closeFeedForEntityId(series.id)
|
||||
Logger.info(`[ApiRouter] Series "${series.name}" is now empty. Removing series`)
|
||||
await Database.removeSeries(series.id)
|
||||
|
||||
// Remove series from library filter data
|
||||
Database.removeSeriesFromFilterData(series.libraryId, series.id)
|
||||
SocketAuthority.emitter('series_removed', {
|
||||
id: series.id,
|
||||
libraryId: series.libraryId
|
||||
})
|
||||
|
||||
await series.destroy()
|
||||
}
|
||||
|
||||
async getUserListeningSessionsHelper(userId) {
|
||||
@@ -619,6 +621,7 @@ class ApiRouter {
|
||||
if (!author) {
|
||||
author = await Database.authorModel.create({
|
||||
name: authorName,
|
||||
lastFirst: Database.authorModel.getLastFirst(authorName),
|
||||
libraryId
|
||||
})
|
||||
Logger.debug(`[ApiRouter] Creating new author "${author.name}"`)
|
||||
@@ -663,11 +666,14 @@ class ApiRouter {
|
||||
}
|
||||
|
||||
if (!mediaMetadata.series[i].id) {
|
||||
let seriesItem = await Database.seriesModel.getOldByNameAndLibrary(seriesName, libraryId)
|
||||
let seriesItem = await Database.seriesModel.getByNameAndLibrary(seriesName, libraryId)
|
||||
if (!seriesItem) {
|
||||
seriesItem = new Series()
|
||||
seriesItem.setData(mediaMetadata.series[i], libraryId)
|
||||
Logger.debug(`[ApiRouter] Created new series "${seriesItem.name}"`)
|
||||
seriesItem = await Database.seriesModel.create({
|
||||
name: seriesName,
|
||||
nameIgnorePrefix: getTitleIgnorePrefix(seriesName),
|
||||
libraryId
|
||||
})
|
||||
Logger.debug(`[ApiRouter] Creating new series "${seriesItem.name}"`)
|
||||
newSeries.push(seriesItem)
|
||||
// Update filter data
|
||||
Database.addSeriesToFilterData(libraryId, seriesItem.name, seriesItem.id)
|
||||
@@ -680,10 +686,9 @@ class ApiRouter {
|
||||
// Remove series without an id
|
||||
mediaMetadata.series = mediaMetadata.series.filter((se) => se.id)
|
||||
if (newSeries.length) {
|
||||
await Database.createBulkSeries(newSeries)
|
||||
SocketAuthority.emitter(
|
||||
'multiple_series_added',
|
||||
newSeries.map((se) => se.toJSON())
|
||||
newSeries.map((se) => se.toOldJSON())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const uuidv4 = require("uuid").v4
|
||||
const uuidv4 = require('uuid').v4
|
||||
const Path = require('path')
|
||||
const sequelize = require('sequelize')
|
||||
const { LogLevel } = require('../utils/constants')
|
||||
@@ -13,14 +13,14 @@ const AudioFile = require('../objects/files/AudioFile')
|
||||
const CoverManager = require('../managers/CoverManager')
|
||||
const LibraryFile = require('../objects/files/LibraryFile')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const fsExtra = require("../libs/fsExtra")
|
||||
const fsExtra = require('../libs/fsExtra')
|
||||
const BookFinder = require('../finders/BookFinder')
|
||||
|
||||
const LibraryScan = require("./LibraryScan")
|
||||
const LibraryScan = require('./LibraryScan')
|
||||
const OpfFileScanner = require('./OpfFileScanner')
|
||||
const NfoFileScanner = require('./NfoFileScanner')
|
||||
const AbsMetadataFileScanner = require('./AbsMetadataFileScanner')
|
||||
const EBookFile = require("../objects/files/EBookFile")
|
||||
const EBookFile = require('../objects/files/EBookFile')
|
||||
|
||||
/**
|
||||
* Metadata for books pulled from files
|
||||
@@ -46,13 +46,13 @@ const EBookFile = require("../objects/files/EBookFile")
|
||||
*/
|
||||
|
||||
class BookScanner {
|
||||
constructor() { }
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* @param {import('../models/LibraryItem')} existingLibraryItem
|
||||
* @param {import('./LibraryItemScanData')} libraryItemData
|
||||
* @param {import('../models/LibraryItem')} existingLibraryItem
|
||||
* @param {import('./LibraryItemScanData')} libraryItemData
|
||||
* @param {import('../models/Library').LibrarySettingsObject} librarySettings
|
||||
* @param {LibraryScan} libraryScan
|
||||
* @param {LibraryScan} libraryScan
|
||||
* @returns {Promise<{libraryItem:import('../models/LibraryItem'), wasUpdated:boolean}>}
|
||||
*/
|
||||
async rescanExistingBookLibraryItem(existingLibraryItem, libraryItemData, librarySettings, libraryScan) {
|
||||
@@ -81,19 +81,23 @@ class BookScanner {
|
||||
let hasMediaChanges = libraryItemData.hasAudioFileChanges || libraryItemData.audioLibraryFiles.length !== media.audioFiles.length
|
||||
if (hasMediaChanges) {
|
||||
// Filter out audio files that were removed
|
||||
media.audioFiles = media.audioFiles.filter(af => !libraryItemData.checkAudioFileRemoved(af))
|
||||
media.audioFiles = media.audioFiles.filter((af) => !libraryItemData.checkAudioFileRemoved(af))
|
||||
|
||||
// Update audio files that were modified
|
||||
if (libraryItemData.audioLibraryFilesModified.length) {
|
||||
let scannedAudioFiles = await AudioFileScanner.executeMediaFileScans(existingLibraryItem.mediaType, libraryItemData, libraryItemData.audioLibraryFilesModified.map(lf => lf.new))
|
||||
let scannedAudioFiles = await AudioFileScanner.executeMediaFileScans(
|
||||
existingLibraryItem.mediaType,
|
||||
libraryItemData,
|
||||
libraryItemData.audioLibraryFilesModified.map((lf) => lf.new)
|
||||
)
|
||||
media.audioFiles = media.audioFiles.map((audioFileObj) => {
|
||||
let matchedScannedAudioFile = scannedAudioFiles.find(saf => saf.metadata.path === audioFileObj.metadata.path)
|
||||
let matchedScannedAudioFile = scannedAudioFiles.find((saf) => saf.metadata.path === audioFileObj.metadata.path)
|
||||
if (!matchedScannedAudioFile) {
|
||||
matchedScannedAudioFile = scannedAudioFiles.find(saf => saf.ino === audioFileObj.ino)
|
||||
matchedScannedAudioFile = scannedAudioFiles.find((saf) => saf.ino === audioFileObj.ino)
|
||||
}
|
||||
|
||||
if (matchedScannedAudioFile) {
|
||||
scannedAudioFiles = scannedAudioFiles.filter(saf => saf !== matchedScannedAudioFile)
|
||||
scannedAudioFiles = scannedAudioFiles.filter((saf) => saf !== matchedScannedAudioFile)
|
||||
const audioFile = new AudioFile(audioFileObj)
|
||||
audioFile.updateFromScan(matchedScannedAudioFile)
|
||||
return audioFile.toJSON()
|
||||
@@ -115,7 +119,7 @@ class BookScanner {
|
||||
// Add audio library files that are not already set on the book (safety check)
|
||||
let audioLibraryFilesToAdd = []
|
||||
for (const audioLibraryFile of libraryItemData.audioLibraryFiles) {
|
||||
if (!media.audioFiles.some(af => af.ino === audioLibraryFile.ino)) {
|
||||
if (!media.audioFiles.some((af) => af.ino === audioLibraryFile.ino)) {
|
||||
libraryScan.addLog(LogLevel.DEBUG, `Existing audio library file "${audioLibraryFile.metadata.relPath}" was not set on book "${media.title}" so setting it now`)
|
||||
|
||||
audioLibraryFilesToAdd.push(audioLibraryFile)
|
||||
@@ -139,14 +143,14 @@ class BookScanner {
|
||||
}
|
||||
|
||||
// Check if cover was removed
|
||||
if (media.coverPath && libraryItemData.imageLibraryFilesRemoved.some(lf => lf.metadata.path === media.coverPath) && !(await fsExtra.pathExists(media.coverPath))) {
|
||||
if (media.coverPath && libraryItemData.imageLibraryFilesRemoved.some((lf) => lf.metadata.path === media.coverPath) && !(await fsExtra.pathExists(media.coverPath))) {
|
||||
media.coverPath = null
|
||||
hasMediaChanges = true
|
||||
}
|
||||
|
||||
// Update cover if it was modified
|
||||
if (media.coverPath && libraryItemData.imageLibraryFilesModified.length) {
|
||||
let coverMatch = libraryItemData.imageLibraryFilesModified.find(iFile => iFile.old.metadata.path === media.coverPath)
|
||||
let coverMatch = libraryItemData.imageLibraryFilesModified.find((iFile) => iFile.old.metadata.path === media.coverPath)
|
||||
if (coverMatch) {
|
||||
const coverPath = coverMatch.new.metadata.path
|
||||
if (coverPath !== media.coverPath) {
|
||||
@@ -161,7 +165,7 @@ class BookScanner {
|
||||
// Check if cover is not set and image files were found
|
||||
if (!media.coverPath && libraryItemData.imageLibraryFiles.length) {
|
||||
// Prefer using a cover image with the name "cover" otherwise use the first image
|
||||
const coverMatch = libraryItemData.imageLibraryFiles.find(iFile => /\/cover\.[^.\/]*$/.test(iFile.metadata.path))
|
||||
const coverMatch = libraryItemData.imageLibraryFiles.find((iFile) => /\/cover\.[^.\/]*$/.test(iFile.metadata.path))
|
||||
media.coverPath = coverMatch?.metadata.path || libraryItemData.imageLibraryFiles[0].metadata.path
|
||||
hasMediaChanges = true
|
||||
}
|
||||
@@ -174,7 +178,7 @@ class BookScanner {
|
||||
|
||||
// Update ebook if it was modified
|
||||
if (media.ebookFile && libraryItemData.ebookLibraryFilesModified.length) {
|
||||
let ebookMatch = libraryItemData.ebookLibraryFilesModified.find(eFile => eFile.old.metadata.path === media.ebookFile.metadata.path)
|
||||
let ebookMatch = libraryItemData.ebookLibraryFilesModified.find((eFile) => eFile.old.metadata.path === media.ebookFile.metadata.path)
|
||||
if (ebookMatch) {
|
||||
const ebookFile = new EBookFile(ebookMatch.new)
|
||||
ebookFile.ebookFormat = ebookFile.metadata.ext.slice(1).toLowerCase()
|
||||
@@ -188,7 +192,7 @@ class BookScanner {
|
||||
// Check if ebook is not set and ebooks were found
|
||||
if (!media.ebookFile && !librarySettings.audiobooksOnly && libraryItemData.ebookLibraryFiles.length) {
|
||||
// Prefer to use an epub ebook then fallback to the first ebook found
|
||||
let ebookLibraryFile = libraryItemData.ebookLibraryFiles.find(lf => lf.metadata.ext.slice(1).toLowerCase() === 'epub')
|
||||
let ebookLibraryFile = libraryItemData.ebookLibraryFiles.find((lf) => lf.metadata.ext.slice(1).toLowerCase() === 'epub')
|
||||
if (!ebookLibraryFile) ebookLibraryFile = libraryItemData.ebookLibraryFiles[0]
|
||||
ebookLibraryFile = ebookLibraryFile.toJSON()
|
||||
// Ebook file is the same as library file except for additional `ebookFormat`
|
||||
@@ -213,7 +217,7 @@ class BookScanner {
|
||||
if (key === 'authors') {
|
||||
// Check for authors added
|
||||
for (const authorName of bookMetadata.authors) {
|
||||
if (!media.authors.some(au => au.name === authorName)) {
|
||||
if (!media.authors.some((au) => au.name === authorName)) {
|
||||
const existingAuthorId = await Database.getAuthorIdByName(libraryItemData.libraryId, authorName)
|
||||
if (existingAuthorId) {
|
||||
await Database.bookAuthorModel.create({
|
||||
@@ -225,7 +229,7 @@ class BookScanner {
|
||||
} else {
|
||||
const newAuthor = await Database.authorModel.create({
|
||||
name: authorName,
|
||||
lastFirst: parseNameString.nameToLastFirst(authorName),
|
||||
lastFirst: Database.authorModel.getLastFirst(authorName),
|
||||
libraryId: libraryItemData.libraryId
|
||||
})
|
||||
await media.addAuthor(newAuthor)
|
||||
@@ -247,7 +251,7 @@ class BookScanner {
|
||||
} else if (key === 'series') {
|
||||
// Check for series added
|
||||
for (const seriesObj of bookMetadata.series) {
|
||||
const existingBookSeries = media.series.find(se => se.name === seriesObj.name)
|
||||
const existingBookSeries = media.series.find((se) => se.name === seriesObj.name)
|
||||
if (!existingBookSeries) {
|
||||
const existingSeriesId = await Database.getSeriesIdByName(libraryItemData.libraryId, seriesObj.name)
|
||||
if (existingSeriesId) {
|
||||
@@ -278,7 +282,7 @@ class BookScanner {
|
||||
}
|
||||
// Check for series removed
|
||||
for (const series of media.series) {
|
||||
if (!bookMetadata.series.some(se => se.name === series.name)) {
|
||||
if (!bookMetadata.series.some((se) => se.name === series.name)) {
|
||||
await series.bookSeries.destroy()
|
||||
libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" removed series "${series.name}"`)
|
||||
seriesUpdated = true
|
||||
@@ -287,21 +291,21 @@ class BookScanner {
|
||||
}
|
||||
} else if (key === 'genres') {
|
||||
const existingGenres = media.genres || []
|
||||
if (bookMetadata.genres.some(g => !existingGenres.includes(g)) || existingGenres.some(g => !bookMetadata.genres.includes(g))) {
|
||||
if (bookMetadata.genres.some((g) => !existingGenres.includes(g)) || existingGenres.some((g) => !bookMetadata.genres.includes(g))) {
|
||||
libraryScan.addLog(LogLevel.DEBUG, `Updating book genres "${existingGenres.join(',')}" => "${bookMetadata.genres.join(',')}" for book "${bookMetadata.title}"`)
|
||||
media.genres = bookMetadata.genres
|
||||
hasMediaChanges = true
|
||||
}
|
||||
} else if (key === 'tags') {
|
||||
const existingTags = media.tags || []
|
||||
if (bookMetadata.tags.some(t => !existingTags.includes(t)) || existingTags.some(t => !bookMetadata.tags.includes(t))) {
|
||||
if (bookMetadata.tags.some((t) => !existingTags.includes(t)) || existingTags.some((t) => !bookMetadata.tags.includes(t))) {
|
||||
libraryScan.addLog(LogLevel.DEBUG, `Updating book tags "${existingTags.join(',')}" => "${bookMetadata.tags.join(',')}" for book "${bookMetadata.title}"`)
|
||||
media.tags = bookMetadata.tags
|
||||
hasMediaChanges = true
|
||||
}
|
||||
} else if (key === 'narrators') {
|
||||
const existingNarrators = media.narrators || []
|
||||
if (bookMetadata.narrators.some(t => !existingNarrators.includes(t)) || existingNarrators.some(t => !bookMetadata.narrators.includes(t))) {
|
||||
if (bookMetadata.narrators.some((t) => !existingNarrators.includes(t)) || existingNarrators.some((t) => !bookMetadata.narrators.includes(t))) {
|
||||
libraryScan.addLog(LogLevel.DEBUG, `Updating book narrators "${existingNarrators.join(',')}" => "${bookMetadata.narrators.join(',')}" for book "${bookMetadata.title}"`)
|
||||
media.narrators = bookMetadata.narrators
|
||||
hasMediaChanges = true
|
||||
@@ -333,17 +337,13 @@ class BookScanner {
|
||||
if (authorsUpdated) {
|
||||
media.authors = await media.getAuthors({
|
||||
joinTableAttributes: ['createdAt'],
|
||||
order: [
|
||||
sequelize.literal(`bookAuthor.createdAt ASC`)
|
||||
]
|
||||
order: [sequelize.literal(`bookAuthor.createdAt ASC`)]
|
||||
})
|
||||
}
|
||||
if (seriesUpdated) {
|
||||
media.series = await media.getSeries({
|
||||
joinTableAttributes: ['sequence', 'createdAt'],
|
||||
order: [
|
||||
sequelize.literal(`bookSeries.createdAt ASC`)
|
||||
]
|
||||
order: [sequelize.literal(`bookSeries.createdAt ASC`)]
|
||||
})
|
||||
}
|
||||
|
||||
@@ -367,7 +367,10 @@ class BookScanner {
|
||||
|
||||
// If no cover then search for cover if enabled in server settings
|
||||
if (!media.coverPath && Database.serverSettings.scannerFindCovers) {
|
||||
const authorName = media.authors.map(au => au.name).filter(au => au).join(', ')
|
||||
const authorName = media.authors
|
||||
.map((au) => au.name)
|
||||
.filter((au) => au)
|
||||
.join(', ')
|
||||
const coverPath = await this.searchForCover(existingLibraryItem.id, libraryItemDir, media.title, authorName, libraryScan)
|
||||
if (coverPath) {
|
||||
media.coverPath = coverPath
|
||||
@@ -428,10 +431,10 @@ class BookScanner {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('./LibraryItemScanData')} libraryItemData
|
||||
*
|
||||
* @param {import('./LibraryItemScanData')} libraryItemData
|
||||
* @param {import('../models/Library').LibrarySettingsObject} librarySettings
|
||||
* @param {LibraryScan} libraryScan
|
||||
* @param {LibraryScan} libraryScan
|
||||
* @returns {Promise<import('../models/LibraryItem')>}
|
||||
*/
|
||||
async scanNewBookLibraryItem(libraryItemData, librarySettings, libraryScan) {
|
||||
@@ -440,7 +443,7 @@ class BookScanner {
|
||||
scannedAudioFiles = AudioFileScanner.runSmartTrackOrder(libraryItemData.relPath, scannedAudioFiles)
|
||||
|
||||
// Find ebook file (prefer epub)
|
||||
let ebookLibraryFile = librarySettings.audiobooksOnly ? null : libraryItemData.ebookLibraryFiles.find(lf => lf.metadata.ext.slice(1).toLowerCase() === 'epub') || libraryItemData.ebookLibraryFiles[0]
|
||||
let ebookLibraryFile = librarySettings.audiobooksOnly ? null : libraryItemData.ebookLibraryFiles.find((lf) => lf.metadata.ext.slice(1).toLowerCase() === 'epub') || libraryItemData.ebookLibraryFiles[0]
|
||||
|
||||
// Do not add library items that have no valid audio files and no ebook file
|
||||
if (!ebookLibraryFile && !scannedAudioFiles.length) {
|
||||
@@ -460,7 +463,7 @@ class BookScanner {
|
||||
bookMetadata.abridged = !!bookMetadata.abridged // Ensure boolean
|
||||
|
||||
let duration = 0
|
||||
scannedAudioFiles.forEach((af) => duration += (!isNaN(af.duration) ? Number(af.duration) : 0))
|
||||
scannedAudioFiles.forEach((af) => (duration += !isNaN(af.duration) ? Number(af.duration) : 0))
|
||||
const bookObject = {
|
||||
...bookMetadata,
|
||||
audioFiles: scannedAudioFiles,
|
||||
@@ -482,7 +485,7 @@ class BookScanner {
|
||||
author: {
|
||||
libraryId: libraryItemData.libraryId,
|
||||
name: authorName,
|
||||
lastFirst: parseNameString.nameToLastFirst(authorName)
|
||||
lastFirst: Database.authorModel.getLastFirst(authorName)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -619,11 +622,11 @@ class BookScanner {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('../models/Book').AudioFileObject[]} audioFiles
|
||||
*
|
||||
* @param {import('../models/Book').AudioFileObject[]} audioFiles
|
||||
* @param {import('../utils/parsers/parseEbookMetadata').EBookFileScanData} ebookFileScanData
|
||||
* @param {import('./LibraryItemScanData')} libraryItemData
|
||||
* @param {LibraryScan} libraryScan
|
||||
* @param {import('./LibraryItemScanData')} libraryItemData
|
||||
* @param {LibraryScan} libraryScan
|
||||
* @param {import('../models/Library').LibrarySettingsObject} librarySettings
|
||||
* @param {string} [existingLibraryItemId]
|
||||
* @returns {Promise<BookMetadataObject>}
|
||||
@@ -664,7 +667,7 @@ class BookScanner {
|
||||
|
||||
// Set cover from library file if one is found otherwise check audiofile
|
||||
if (libraryItemData.imageLibraryFiles.length) {
|
||||
const coverMatch = libraryItemData.imageLibraryFiles.find(iFile => /\/cover\.[^.\/]*$/.test(iFile.metadata.path))
|
||||
const coverMatch = libraryItemData.imageLibraryFiles.find((iFile) => /\/cover\.[^.\/]*$/.test(iFile.metadata.path))
|
||||
bookMetadata.coverPath = coverMatch?.metadata.path || libraryItemData.imageLibraryFiles[0].metadata.path
|
||||
}
|
||||
|
||||
@@ -673,16 +676,15 @@ class BookScanner {
|
||||
return bookMetadata
|
||||
}
|
||||
|
||||
|
||||
static BookMetadataSourceHandler = class {
|
||||
/**
|
||||
*
|
||||
* @param {Object} bookMetadata
|
||||
* @param {import('../models/Book').AudioFileObject[]} audioFiles
|
||||
*
|
||||
* @param {Object} bookMetadata
|
||||
* @param {import('../models/Book').AudioFileObject[]} audioFiles
|
||||
* @param {import('../utils/parsers/parseEbookMetadata').EBookFileScanData} ebookFileScanData
|
||||
* @param {import('./LibraryItemScanData')} libraryItemData
|
||||
* @param {LibraryScan} libraryScan
|
||||
* @param {string} existingLibraryItemId
|
||||
* @param {import('./LibraryItemScanData')} libraryItemData
|
||||
* @param {LibraryScan} libraryScan
|
||||
* @param {string} existingLibraryItemId
|
||||
*/
|
||||
constructor(bookMetadata, audioFiles, ebookFileScanData, libraryItemData, libraryScan, existingLibraryItemId) {
|
||||
this.bookMetadata = bookMetadata
|
||||
@@ -785,8 +787,8 @@ class BookScanner {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('../models/LibraryItem')} libraryItem
|
||||
*
|
||||
* @param {import('../models/LibraryItem')} libraryItem
|
||||
* @param {LibraryScan} libraryScan
|
||||
* @returns {Promise}
|
||||
*/
|
||||
@@ -805,12 +807,12 @@ class BookScanner {
|
||||
|
||||
const jsonObject = {
|
||||
tags: libraryItem.media.tags || [],
|
||||
chapters: libraryItem.media.chapters?.map(c => ({ ...c })) || [],
|
||||
chapters: libraryItem.media.chapters?.map((c) => ({ ...c })) || [],
|
||||
title: libraryItem.media.title,
|
||||
subtitle: libraryItem.media.subtitle,
|
||||
authors: libraryItem.media.authors.map(a => a.name),
|
||||
authors: libraryItem.media.authors.map((a) => a.name),
|
||||
narrators: libraryItem.media.narrators,
|
||||
series: libraryItem.media.series.map(se => {
|
||||
series: libraryItem.media.series.map((se) => {
|
||||
const sequence = se.bookSeries?.sequence || ''
|
||||
if (!sequence) return se.name
|
||||
return `${se.name} #${sequence}`
|
||||
@@ -826,70 +828,75 @@ class BookScanner {
|
||||
explicit: !!libraryItem.media.explicit,
|
||||
abridged: !!libraryItem.media.abridged
|
||||
}
|
||||
return fsExtra.writeFile(metadataFilePath, JSON.stringify(jsonObject, null, 2)).then(async () => {
|
||||
// Add metadata.json to libraryFiles array if it is new
|
||||
let metadataLibraryFile = libraryItem.libraryFiles.find(lf => lf.metadata.path === filePathToPOSIX(metadataFilePath))
|
||||
if (storeMetadataWithItem) {
|
||||
if (!metadataLibraryFile) {
|
||||
const newLibraryFile = new LibraryFile()
|
||||
await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`)
|
||||
metadataLibraryFile = newLibraryFile.toJSON()
|
||||
libraryItem.libraryFiles.push(metadataLibraryFile)
|
||||
} else {
|
||||
const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath)
|
||||
if (fileTimestamps) {
|
||||
metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs
|
||||
metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs
|
||||
metadataLibraryFile.metadata.size = fileTimestamps.size
|
||||
metadataLibraryFile.ino = fileTimestamps.ino
|
||||
return fsExtra
|
||||
.writeFile(metadataFilePath, JSON.stringify(jsonObject, null, 2))
|
||||
.then(async () => {
|
||||
// Add metadata.json to libraryFiles array if it is new
|
||||
let metadataLibraryFile = libraryItem.libraryFiles.find((lf) => lf.metadata.path === filePathToPOSIX(metadataFilePath))
|
||||
if (storeMetadataWithItem) {
|
||||
if (!metadataLibraryFile) {
|
||||
const newLibraryFile = new LibraryFile()
|
||||
await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`)
|
||||
metadataLibraryFile = newLibraryFile.toJSON()
|
||||
libraryItem.libraryFiles.push(metadataLibraryFile)
|
||||
} else {
|
||||
const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath)
|
||||
if (fileTimestamps) {
|
||||
metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs
|
||||
metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs
|
||||
metadataLibraryFile.metadata.size = fileTimestamps.size
|
||||
metadataLibraryFile.ino = fileTimestamps.ino
|
||||
}
|
||||
}
|
||||
const libraryItemDirTimestamps = await getFileTimestampsWithIno(libraryItem.path)
|
||||
if (libraryItemDirTimestamps) {
|
||||
libraryItem.mtime = libraryItemDirTimestamps.mtimeMs
|
||||
libraryItem.ctime = libraryItemDirTimestamps.ctimeMs
|
||||
let size = 0
|
||||
libraryItem.libraryFiles.forEach((lf) => (size += !isNaN(lf.metadata.size) ? Number(lf.metadata.size) : 0))
|
||||
libraryItem.size = size
|
||||
}
|
||||
}
|
||||
const libraryItemDirTimestamps = await getFileTimestampsWithIno(libraryItem.path)
|
||||
if (libraryItemDirTimestamps) {
|
||||
libraryItem.mtime = libraryItemDirTimestamps.mtimeMs
|
||||
libraryItem.ctime = libraryItemDirTimestamps.ctimeMs
|
||||
let size = 0
|
||||
libraryItem.libraryFiles.forEach((lf) => size += (!isNaN(lf.metadata.size) ? Number(lf.metadata.size) : 0))
|
||||
libraryItem.size = size
|
||||
}
|
||||
}
|
||||
|
||||
libraryScan.addLog(LogLevel.DEBUG, `Success saving abmetadata to "${metadataFilePath}"`)
|
||||
libraryScan.addLog(LogLevel.DEBUG, `Success saving abmetadata to "${metadataFilePath}"`)
|
||||
|
||||
return metadataLibraryFile
|
||||
}).catch((error) => {
|
||||
libraryScan.addLog(LogLevel.ERROR, `Failed to save json file at "${metadataFilePath}"`, error)
|
||||
return null
|
||||
})
|
||||
return metadataLibraryFile
|
||||
})
|
||||
.catch((error) => {
|
||||
libraryScan.addLog(LogLevel.ERROR, `Failed to save json file at "${metadataFilePath}"`, error)
|
||||
return null
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Check authors that were removed from a book and remove them if they no longer have any books
|
||||
* keep authors without books that have a asin, description or imagePath
|
||||
* @param {string} libraryId
|
||||
* @param {import('./ScanLogger')} scanLogger
|
||||
* @param {string} libraryId
|
||||
* @param {import('./ScanLogger')} scanLogger
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async checkAuthorsRemovedFromBooks(libraryId, scanLogger) {
|
||||
const bookAuthorsToRemove = (await Database.authorModel.findAll({
|
||||
where: [
|
||||
{
|
||||
id: scanLogger.authorsRemovedFromBooks,
|
||||
asin: {
|
||||
[sequelize.Op.or]: [null, ""]
|
||||
const bookAuthorsToRemove = (
|
||||
await Database.authorModel.findAll({
|
||||
where: [
|
||||
{
|
||||
id: scanLogger.authorsRemovedFromBooks,
|
||||
asin: {
|
||||
[sequelize.Op.or]: [null, '']
|
||||
},
|
||||
description: {
|
||||
[sequelize.Op.or]: [null, '']
|
||||
},
|
||||
imagePath: {
|
||||
[sequelize.Op.or]: [null, '']
|
||||
}
|
||||
},
|
||||
description: {
|
||||
[sequelize.Op.or]: [null, ""]
|
||||
},
|
||||
imagePath: {
|
||||
[sequelize.Op.or]: [null, ""]
|
||||
}
|
||||
},
|
||||
sequelize.where(sequelize.literal('(SELECT count(*) FROM bookAuthors ba WHERE ba.authorId = author.id)'), 0)
|
||||
],
|
||||
attributes: ['id'],
|
||||
raw: true
|
||||
})).map(au => au.id)
|
||||
sequelize.where(sequelize.literal('(SELECT count(*) FROM bookAuthors ba WHERE ba.authorId = author.id)'), 0)
|
||||
],
|
||||
attributes: ['id'],
|
||||
raw: true
|
||||
})
|
||||
).map((au) => au.id)
|
||||
if (bookAuthorsToRemove.length) {
|
||||
await Database.authorModel.destroy({
|
||||
where: {
|
||||
@@ -907,21 +914,23 @@ class BookScanner {
|
||||
|
||||
/**
|
||||
* Check series that were removed from books and remove them if they no longer have any books
|
||||
* @param {string} libraryId
|
||||
* @param {import('./ScanLogger')} scanLogger
|
||||
* @param {string} libraryId
|
||||
* @param {import('./ScanLogger')} scanLogger
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async checkSeriesRemovedFromBooks(libraryId, scanLogger) {
|
||||
const bookSeriesToRemove = (await Database.seriesModel.findAll({
|
||||
where: [
|
||||
{
|
||||
id: scanLogger.seriesRemovedFromBooks
|
||||
},
|
||||
sequelize.where(sequelize.literal('(SELECT count(*) FROM bookSeries bs WHERE bs.seriesId = series.id)'), 0)
|
||||
],
|
||||
attributes: ['id'],
|
||||
raw: true
|
||||
})).map(se => se.id)
|
||||
const bookSeriesToRemove = (
|
||||
await Database.seriesModel.findAll({
|
||||
where: [
|
||||
{
|
||||
id: scanLogger.seriesRemovedFromBooks
|
||||
},
|
||||
sequelize.where(sequelize.literal('(SELECT count(*) FROM bookSeries bs WHERE bs.seriesId = series.id)'), 0)
|
||||
],
|
||||
attributes: ['id'],
|
||||
raw: true
|
||||
})
|
||||
).map((se) => se.id)
|
||||
if (bookSeriesToRemove.length) {
|
||||
await Database.seriesModel.destroy({
|
||||
where: {
|
||||
@@ -938,11 +947,11 @@ class BookScanner {
|
||||
|
||||
/**
|
||||
* Search cover provider for matching cover
|
||||
* @param {string} libraryItemId
|
||||
* @param {string} libraryItemId
|
||||
* @param {string} libraryItemPath null if book isFile
|
||||
* @param {string} title
|
||||
* @param {string} author
|
||||
* @param {LibraryScan} libraryScan
|
||||
* @param {string} title
|
||||
* @param {string} author
|
||||
* @param {LibraryScan} libraryScan
|
||||
* @returns {Promise<string>} path to downloaded cover or null if no cover found
|
||||
*/
|
||||
async searchForCover(libraryItemId, libraryItemPath, title, author, libraryScan) {
|
||||
@@ -956,7 +965,6 @@ class BookScanner {
|
||||
|
||||
// If the first cover result fails, attempt to download the second
|
||||
for (let i = 0; i < results.length && i < 2; i++) {
|
||||
|
||||
// Downloads and updates the book cover
|
||||
const result = await CoverManager.downloadCoverFromUrlNew(results[i], libraryItemId, libraryItemPath)
|
||||
|
||||
@@ -970,4 +978,4 @@ class BookScanner {
|
||||
return null
|
||||
}
|
||||
}
|
||||
module.exports = new BookScanner()
|
||||
module.exports = new BookScanner()
|
||||
|
||||
@@ -8,7 +8,6 @@ const { findMatchingEpisodesInFeed, getPodcastFeed } = require('../utils/podcast
|
||||
const BookFinder = require('../finders/BookFinder')
|
||||
const PodcastFinder = require('../finders/PodcastFinder')
|
||||
const LibraryScan = require('./LibraryScan')
|
||||
const Series = require('../objects/entities/Series')
|
||||
const LibraryScanner = require('./LibraryScanner')
|
||||
const CoverManager = require('../managers/CoverManager')
|
||||
const TaskManager = require('../managers/TaskManager')
|
||||
@@ -209,6 +208,7 @@ class Scanner {
|
||||
if (!author) {
|
||||
author = await Database.authorModel.create({
|
||||
name: authorName,
|
||||
lastFirst: Database.authorModel.getLastFirst(authorName),
|
||||
libraryId: libraryItem.libraryId
|
||||
})
|
||||
SocketAuthority.emitter('author_added', author.toOldJSON())
|
||||
@@ -225,14 +225,16 @@ class Scanner {
|
||||
if (!Array.isArray(matchData.series)) matchData.series = [{ series: matchData.series, sequence: matchData.sequence }]
|
||||
const seriesPayload = []
|
||||
for (const seriesMatchItem of matchData.series) {
|
||||
let seriesItem = await Database.seriesModel.getOldByNameAndLibrary(seriesMatchItem.series, libraryItem.libraryId)
|
||||
let seriesItem = await Database.seriesModel.getByNameAndLibrary(seriesMatchItem.series, libraryItem.libraryId)
|
||||
if (!seriesItem) {
|
||||
seriesItem = new Series()
|
||||
seriesItem.setData({ name: seriesMatchItem.series }, libraryItem.libraryId)
|
||||
await Database.createSeries(seriesItem)
|
||||
seriesItem = await Database.seriesModel.create({
|
||||
name: seriesMatchItem.series,
|
||||
nameIgnorePrefix: getTitleIgnorePrefix(seriesMatchItem.series),
|
||||
libraryId
|
||||
})
|
||||
// Update filter data
|
||||
Database.addSeriesToFilterData(libraryItem.libraryId, seriesItem.name, seriesItem.id)
|
||||
SocketAuthority.emitter('series_added', seriesItem.toJSON())
|
||||
SocketAuthority.emitter('series_added', seriesItem.toOldJSON())
|
||||
}
|
||||
seriesPayload.push(seriesItem.toJSONMinimal(seriesMatchItem.sequence))
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ module.exports = {
|
||||
* @param {import('../../models/User')} user
|
||||
* @param {string[]} include
|
||||
* @param {number} limit
|
||||
* @returns {{ series:import('../../objects/entities/Series')[], count:number}}
|
||||
* @returns {{ series:any[], count:number}}
|
||||
*/
|
||||
async getSeriesMostRecentlyAdded(library, user, include, limit) {
|
||||
if (!library.isBook) return { series: [], count: 0 }
|
||||
@@ -276,7 +276,7 @@ module.exports = {
|
||||
|
||||
const allOldSeries = []
|
||||
for (const s of series) {
|
||||
const oldSeries = s.getOldSeries().toJSON()
|
||||
const oldSeries = s.toOldJSON()
|
||||
|
||||
if (s.feeds?.length) {
|
||||
oldSeries.rssFeed = Database.feedModel.getOldFeed(s.feeds[0]).toJSONMinified()
|
||||
|
||||
@@ -954,12 +954,12 @@ module.exports = {
|
||||
|
||||
/**
|
||||
* Get library items for series
|
||||
* @param {import('../../objects/entities/Series')} oldSeries
|
||||
* @param {import('../../models/Series')} series
|
||||
* @param {import('../../models/User')} [user]
|
||||
* @returns {Promise<import('../../objects/LibraryItem')[]>}
|
||||
*/
|
||||
async getLibraryItemsForSeries(oldSeries, user) {
|
||||
const { libraryItems } = await this.getFilteredLibraryItems(oldSeries.libraryId, user, 'series', oldSeries.id, null, null, false, [], null, null)
|
||||
async getLibraryItemsForSeries(series, user) {
|
||||
const { libraryItems } = await this.getFilteredLibraryItems(series.libraryId, user, 'series', series.id, null, null, false, [], null, null)
|
||||
return libraryItems.map((li) => Database.libraryItemModel.getOldLibraryItem(li))
|
||||
},
|
||||
|
||||
@@ -1130,7 +1130,7 @@ module.exports = {
|
||||
return Database.libraryItemModel.getOldLibraryItem(libraryItem).toJSON()
|
||||
})
|
||||
seriesMatches.push({
|
||||
series: series.getOldSeries().toJSON(),
|
||||
series: series.toOldJSON(),
|
||||
books
|
||||
})
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ module.exports = {
|
||||
// Map series to old series
|
||||
const allOldSeries = []
|
||||
for (const s of series) {
|
||||
const oldSeries = s.getOldSeries().toJSON()
|
||||
const oldSeries = s.toOldJSON()
|
||||
|
||||
if (s.dataValues.totalDuration) {
|
||||
oldSeries.totalDuration = s.dataValues.totalDuration
|
||||
|
||||
Reference in New Issue
Block a user