mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-04 01:40:40 +02:00
Compare commits
170 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c7d8021a16 | |||
| 9e83ad25b9 | |||
| 2eccb9465c | |||
| 599b6bd6ad | |||
| e01ac489fb | |||
| 271dbc4764 | |||
| 84c2931434 | |||
| 38483c9269 | |||
| b2e97d70df | |||
| 78aafe038d | |||
| 34f7ddfdd7 | |||
| 0e9777feec | |||
| 6351fd8d7b | |||
| b7591abd06 | |||
| 2b36caf096 | |||
| f87a0bfc2f | |||
| b109b2edee | |||
| 7795bf25d0 | |||
| 3d5c02ae7c | |||
| 373d14a49e | |||
| a17127f078 | |||
| 20f812403f | |||
| a864c6bcc6 | |||
| 6c0e42db49 | |||
| 364ccd85fe | |||
| d6b58c2f10 | |||
| 64185b7519 | |||
| e1b3b657c4 | |||
| 4662fc5244 | |||
| 13c20e0cdd | |||
| 19a65dba98 | |||
| 799879d67d | |||
| 452d354b52 | |||
| 9d7f44f73a | |||
| e8b60defb6 | |||
| 0cc2e39367 | |||
| a34b01fcb4 | |||
| 7919a8b581 | |||
| 565eb423ee | |||
| 42b0e31b4a | |||
| 97a8959bf8 | |||
| b5b99cbaca | |||
| f04ef320aa | |||
| 4e33059ac8 | |||
| 699644322b | |||
| 49ba364b2a | |||
| adb3967f89 | |||
| cfdcac9475 | |||
| b1d57bc0b3 | |||
| f7cea8ca12 | |||
| 293440006b | |||
| 45f7f54b6c | |||
| bb5e16157c | |||
| 2e8cb46c57 | |||
| f9c0e52f18 | |||
| 6290cfaeb1 | |||
| fd3d4f5fcf | |||
| 9f9bee2ddc | |||
| 568bf0254d | |||
| 79f4db5ff3 | |||
| 7038f5730f | |||
| 0a8186cbda | |||
| 659164003f | |||
| de5d8650e8 | |||
| bacefb5f6f | |||
| 0169bf5518 | |||
| 8f192b1b17 | |||
| 21343b5aa0 | |||
| a5508cdc4c | |||
| bd4f48ec39 | |||
| cb9fc3e0d1 | |||
| 707533df8f | |||
| 2e48ec0dde | |||
| f1e46a351b | |||
| da8fd2d9d5 | |||
| f1de307bf9 | |||
| 7282afcfde | |||
| e2f1aeed75 | |||
| 23a750214f | |||
| 6a7418ad41 | |||
| 8b00c16062 | |||
| 8ee5646d79 | |||
| 373551fb74 | |||
| d9b206fe1c | |||
| fe4e0145c9 | |||
| c4d99a118f | |||
| b96226966b | |||
| 5ca12eee19 | |||
| f460297daf | |||
| ebdf377fc1 | |||
| 808d23561c | |||
| a34813b3ab | |||
| 725192fbc0 | |||
| 2915c072b5 | |||
| 03a1d7da32 | |||
| 1be1ce6f87 | |||
| 21b27c432c | |||
| cbe5e3db8a | |||
| 08b4d4d7a2 | |||
| ac8324e595 | |||
| a14c6a3a8b | |||
| 74b35ea9d1 | |||
| 78d8c83e6d | |||
| bf795d3662 | |||
| 1fbd090441 | |||
| 70621e72e8 | |||
| d30a09f503 | |||
| 39567c6c22 | |||
| ed3af5bdcd | |||
| 9e54b4f7ca | |||
| ec65376569 | |||
| 4e8cd6fba0 | |||
| 1a3d70d041 | |||
| 14e92435ec | |||
| 0ccb88904a | |||
| 4cc300d6e9 | |||
| 068ba84a8c | |||
| 36ef675556 | |||
| 0dd57a8912 | |||
| ef45f844e5 | |||
| 9a261195b7 | |||
| 3d08a35aa0 | |||
| a13143245b | |||
| 52bb28669a | |||
| 25ae6dd59a | |||
| a37fe3c3d2 | |||
| 59bcbe0dfa | |||
| b5e69630de | |||
| 0bba709124 | |||
| e93bb5cb07 | |||
| 3f7af8acfb | |||
| 5e5a604d03 | |||
| 201e12ecc3 | |||
| 24d6e390f0 | |||
| 0cf7a6abec | |||
| 76ac0d001b | |||
| 00343a953b | |||
| 82ab95ab02 | |||
| a1d8ebc01b | |||
| eeaae5f934 | |||
| 4464276a6e | |||
| 3465790fe9 | |||
| 5fa4c5a2c3 | |||
| 13f353596b | |||
| 3d9100e5b8 | |||
| b62309ead2 | |||
| 1fce94ad4a | |||
| 9abd6698ae | |||
| 88c10ad619 | |||
| c62a6fbffd | |||
| 989388d3ed | |||
| 4cc97a22f6 | |||
| 8bd336a4ba | |||
| 437c8dd09c | |||
| f82697cbbf | |||
| 74c87a0bbd | |||
| 35eb5bcfc0 | |||
| 0a29b549df | |||
| a38a92b948 | |||
| d245c93da4 | |||
| bcf8f6b732 | |||
| 40e11db5e5 | |||
| aebb3ff413 | |||
| a58d486c44 | |||
| 4a76ba0226 | |||
| 7afff57b5e | |||
| 2e13c5bd50 | |||
| 344de941ff | |||
| 5c0cd98cb3 | |||
| 8974c582fc |
@@ -99,6 +99,7 @@ export default {
|
|||||||
this.$store.commit('showEditModal', libraryItem)
|
this.$store.commit('showEditModal', libraryItem)
|
||||||
},
|
},
|
||||||
editEpisode({ libraryItem, episode }) {
|
editEpisode({ libraryItem, episode }) {
|
||||||
|
this.$store.commit('setEpisodeTableEpisodeIds', [episode.id])
|
||||||
this.$store.commit('setSelectedLibraryItem', libraryItem)
|
this.$store.commit('setSelectedLibraryItem', libraryItem)
|
||||||
this.$store.commit('globals/setSelectedEpisode', episode)
|
this.$store.commit('globals/setSelectedEpisode', episode)
|
||||||
this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)
|
this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)
|
||||||
|
|||||||
@@ -19,6 +19,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-else-if="!totalShelves && initialized" class="w-full py-16">
|
<div v-else-if="!totalShelves && initialized" class="w-full py-16">
|
||||||
<p class="text-xl text-center">{{ emptyMessage }}</p>
|
<p class="text-xl text-center">{{ emptyMessage }}</p>
|
||||||
|
<div v-if="entityName === 'collections' || entityName === 'playlists'" class="flex justify-center mt-4">
|
||||||
|
{{ emptyMessageHelp }}
|
||||||
|
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
|
||||||
|
<a href="https://www.audiobookshelf.org/guides/collections" target="_blank" class="inline-flex">
|
||||||
|
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
|
||||||
|
</a>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
<!-- Clear filter only available on Library bookshelf -->
|
<!-- Clear filter only available on Library bookshelf -->
|
||||||
<div v-if="entityName === 'items'" class="flex justify-center mt-2">
|
<div v-if="entityName === 'items'" class="flex justify-center mt-2">
|
||||||
<ui-btn v-if="hasFilter" color="primary" @click="clearFilter">{{ $strings.ButtonClearFilter }}</ui-btn>
|
<ui-btn v-if="hasFilter" color="primary" @click="clearFilter">{{ $strings.ButtonClearFilter }}</ui-btn>
|
||||||
@@ -109,6 +117,11 @@ export default {
|
|||||||
}
|
}
|
||||||
return this.$strings.MessageNoResults
|
return this.$strings.MessageNoResults
|
||||||
},
|
},
|
||||||
|
emptyMessageHelp() {
|
||||||
|
if (this.page === 'collections') return this.$strings.MessageBookshelfNoCollectionsHelp
|
||||||
|
if (this.page === 'playlists') return this.$strings.MessageNoUserPlaylistsHelp
|
||||||
|
return ''
|
||||||
|
},
|
||||||
entityName() {
|
entityName() {
|
||||||
if (!this.page) return 'items'
|
if (!this.page) return 'items'
|
||||||
return this.page
|
return this.page
|
||||||
@@ -406,7 +419,7 @@ export default {
|
|||||||
|
|
||||||
this.postScrollTimeout = setTimeout(this.postScroll, 500)
|
this.postScrollTimeout = setTimeout(this.postScroll, 500)
|
||||||
},
|
},
|
||||||
async resetEntities() {
|
async resetEntities(scrollPositionToRestore) {
|
||||||
if (this.isFetchingEntities) {
|
if (this.isFetchingEntities) {
|
||||||
this.pendingReset = true
|
this.pendingReset = true
|
||||||
return
|
return
|
||||||
@@ -424,6 +437,12 @@ export default {
|
|||||||
await this.loadPage(0)
|
await this.loadPage(0)
|
||||||
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
|
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
|
||||||
this.mountEntities(0, lastBookIndex)
|
this.mountEntities(0, lastBookIndex)
|
||||||
|
|
||||||
|
if (scrollPositionToRestore) {
|
||||||
|
if (window.bookshelf) {
|
||||||
|
window.bookshelf.scrollTop = scrollPositionToRestore
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
async rebuild() {
|
async rebuild() {
|
||||||
this.initSizeData()
|
this.initSizeData()
|
||||||
@@ -431,9 +450,8 @@ export default {
|
|||||||
var lastBookIndex = Math.min(this.totalEntities, this.booksPerFetch)
|
var lastBookIndex = Math.min(this.totalEntities, this.booksPerFetch)
|
||||||
this.destroyEntityComponents()
|
this.destroyEntityComponents()
|
||||||
await this.loadPage(0)
|
await this.loadPage(0)
|
||||||
var bookshelfEl = document.getElementById('bookshelf')
|
if (window.bookshelf) {
|
||||||
if (bookshelfEl) {
|
window.bookshelf.scrollTop = 0
|
||||||
bookshelfEl.scrollTop = 0
|
|
||||||
}
|
}
|
||||||
this.mountEntities(0, lastBookIndex)
|
this.mountEntities(0, lastBookIndex)
|
||||||
},
|
},
|
||||||
@@ -534,6 +552,15 @@ export default {
|
|||||||
if (this.entityName === 'items' || this.entityName === 'series-books') {
|
if (this.entityName === 'items' || this.entityName === 'series-books') {
|
||||||
var indexOf = this.entities.findIndex((ent) => ent && ent.id === libraryItem.id)
|
var indexOf = this.entities.findIndex((ent) => ent && ent.id === libraryItem.id)
|
||||||
if (indexOf >= 0) {
|
if (indexOf >= 0) {
|
||||||
|
if (this.entityName === 'items' && this.orderBy === 'media.metadata.title') {
|
||||||
|
const curTitle = this.entities[indexOf].media.metadata?.title
|
||||||
|
const newTitle = libraryItem.media.metadata?.title
|
||||||
|
if (curTitle != newTitle) {
|
||||||
|
console.log('Title changed. Re-sorting...')
|
||||||
|
this.resetEntities(this.currScrollTop)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
this.entities[indexOf] = libraryItem
|
this.entities[indexOf] = libraryItem
|
||||||
if (this.entityComponentRefs[indexOf]) {
|
if (this.entityComponentRefs[indexOf]) {
|
||||||
this.entityComponentRefs[indexOf].setEntity(libraryItem)
|
this.entityComponentRefs[indexOf].setEntity(libraryItem)
|
||||||
@@ -541,6 +568,18 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
routeToBookshelfIfLastIssueRemoved() {
|
||||||
|
if (this.totalEntities === 0) {
|
||||||
|
const currentRouteQuery = this.$route.query
|
||||||
|
if (currentRouteQuery?.filter && currentRouteQuery.filter === 'issues') {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
console.log('Last issue removed. Redirecting to library bookshelf')
|
||||||
|
this.$router.push(`/library/${this.currentLibraryId}/bookshelf`)
|
||||||
|
this.$store.dispatch('libraries/fetch', this.currentLibraryId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
libraryItemRemoved(libraryItem) {
|
libraryItemRemoved(libraryItem) {
|
||||||
if (this.entityName === 'items' || this.entityName === 'series-books') {
|
if (this.entityName === 'items' || this.entityName === 'series-books') {
|
||||||
var indexOf = this.entities.findIndex((ent) => ent && ent.id === libraryItem.id)
|
var indexOf = this.entities.findIndex((ent) => ent && ent.id === libraryItem.id)
|
||||||
@@ -551,6 +590,7 @@ export default {
|
|||||||
this.executeRebuild()
|
this.executeRebuild()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.routeToBookshelfIfLastIssueRemoved()
|
||||||
},
|
},
|
||||||
libraryItemsAdded(libraryItems) {
|
libraryItemsAdded(libraryItems) {
|
||||||
console.log('items added', libraryItems)
|
console.log('items added', libraryItems)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="text-gray-400 flex items-center w-1/2 sm:w-4/5 lg:w-2/5">
|
<div class="text-gray-400 flex items-center w-1/2 sm:w-4/5 lg:w-2/5">
|
||||||
<span class="material-symbols text-sm">person</span>
|
<span class="material-symbols text-sm">person</span>
|
||||||
<div v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ podcastAuthor }}</div>
|
<div v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base truncate">{{ podcastAuthor }}</div>
|
||||||
<div v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base truncate">
|
<div v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base truncate">
|
||||||
<nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">, </span></nuxt-link>
|
<nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">, </span></nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
@@ -85,7 +85,8 @@ export default {
|
|||||||
displayTitle: null,
|
displayTitle: null,
|
||||||
currentPlaybackRate: 1,
|
currentPlaybackRate: 1,
|
||||||
syncFailedToast: null,
|
syncFailedToast: null,
|
||||||
coverAspectRatio: 1
|
coverAspectRatio: 1,
|
||||||
|
lastChapterId: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -236,12 +237,16 @@ export default {
|
|||||||
}
|
}
|
||||||
}, 1000)
|
}, 1000)
|
||||||
},
|
},
|
||||||
checkChapterEnd(time) {
|
checkChapterEnd() {
|
||||||
if (!this.currentChapter) return
|
if (!this.currentChapter) return
|
||||||
const chapterEndTime = this.currentChapter.end
|
|
||||||
const tolerance = 0.75
|
// Track chapter transitions by comparing current chapter with last chapter
|
||||||
if (time >= chapterEndTime - tolerance) {
|
if (this.lastChapterId !== this.currentChapter.id) {
|
||||||
this.sleepTimerEnd()
|
// Chapter changed - if we had a previous chapter, this means we crossed a boundary
|
||||||
|
if (this.lastChapterId) {
|
||||||
|
this.sleepTimerEnd()
|
||||||
|
}
|
||||||
|
this.lastChapterId = this.currentChapter.id
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
sleepTimerEnd() {
|
sleepTimerEnd() {
|
||||||
@@ -301,7 +306,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.sleepTimerType === this.$constants.SleepTimerTypes.CHAPTER && this.sleepTimerSet) {
|
if (this.sleepTimerType === this.$constants.SleepTimerTypes.CHAPTER && this.sleepTimerSet) {
|
||||||
this.checkChapterEnd(time)
|
this.checkChapterEnd()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setDuration(duration) {
|
setDuration(duration) {
|
||||||
|
|||||||
@@ -31,13 +31,6 @@
|
|||||||
<p cy-id="placeholderAuthorText" aria-hidden="true" class="text-center" style="color: rgb(247 223 187); opacity: 0.75" :style="{ fontSize: authorFontSize + 'em' }">{{ authorCleaned }}</p>
|
<p cy-id="placeholderAuthorText" aria-hidden="true" class="text-center" style="color: rgb(247 223 187); opacity: 0.75" :style="{ fontSize: authorFontSize + 'em' }">{{ authorCleaned }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="seriesSequenceList" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20 text-right" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `${0.1}em ${0.25}em` }" style="background-color: #78350f">
|
|
||||||
<p :style="{ fontSize: 0.8 + 'em' }">#{{ seriesSequenceList }}</p>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="booksInSeries" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `${0.1}em ${0.25}em` }" style="background-color: #cd9d49dd">
|
|
||||||
<p :style="{ fontSize: 0.8 + 'em' }">{{ booksInSeries }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- No progress shown for podcasts (unless showing podcast episode) -->
|
<!-- No progress shown for podcasts (unless showing podcast episode) -->
|
||||||
<div cy-id="progressBar" v-if="!isPodcast || episodeProgress" class="absolute bottom-0 left-0 h-1e max-w-full z-20 rounded-b box-shadow-progressbar" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: coverWidth * userProgressPercent + 'px' }"></div>
|
<div cy-id="progressBar" v-if="!isPodcast || episodeProgress" class="absolute bottom-0 left-0 h-1e max-w-full z-20 rounded-b box-shadow-progressbar" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: coverWidth * userProgressPercent + 'px' }"></div>
|
||||||
|
|
||||||
@@ -244,6 +237,7 @@ export default {
|
|||||||
return this.mediaMetadata.series
|
return this.mediaMetadata.series
|
||||||
},
|
},
|
||||||
seriesName() {
|
seriesName() {
|
||||||
|
if (this.collapsedSeries?.name) return this.collapsedSeries.name
|
||||||
return this.series?.name || null
|
return this.series?.name || null
|
||||||
},
|
},
|
||||||
seriesSequence() {
|
seriesSequence() {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div ref="container" class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
||||||
<div v-if="show" class="w-full h-full py-4">
|
<div v-if="show" class="w-full h-full py-4">
|
||||||
<div class="w-full overflow-y-auto overflow-x-hidden max-h-96">
|
<div class="w-full overflow-y-auto overflow-x-hidden max-h-96">
|
||||||
<div class="flex px-8 items-center py-2">
|
<div class="flex px-8 items-center py-2">
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div ref="container" class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
||||||
<div v-if="show" class="w-full h-full">
|
<div v-if="show" class="w-full h-full">
|
||||||
<div class="py-4 px-4">
|
<div class="py-4 px-4">
|
||||||
<h1 v-if="!showBatchCollectionModal" class="text-2xl">{{ $strings.LabelAddToCollection }}</h1>
|
<h1 v-if="!showBatchCollectionModal" class="text-2xl">{{ $strings.LabelAddToCollection }}</h1>
|
||||||
@@ -19,9 +19,20 @@
|
|||||||
</template>
|
</template>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!collections.length" class="flex h-32 items-center justify-center">
|
<div v-if="!collections.length" class="flex h-32 items-center justify-center text-center px-2">
|
||||||
<p class="text-xl">{{ $strings.MessageNoCollections }}</p>
|
<div>
|
||||||
|
<p class="text-xl mb-2">{{ $strings.MessageNoCollections }}</p>
|
||||||
|
<div class="text-sm flex items-center justify-center text-gray-200">
|
||||||
|
<p>{{ $strings.MessageBookshelfNoCollectionsHelp }}</p>
|
||||||
|
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
|
||||||
|
<a href="https://www.audiobookshelf.org/guides/collections" target="_blank" class="inline-flex">
|
||||||
|
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
|
||||||
|
</a>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full h-px bg-white bg-opacity-10" />
|
<div class="w-full h-px bg-white bg-opacity-10" />
|
||||||
<form @submit.prevent="submitCreateCollection">
|
<form @submit.prevent="submitCreateCollection">
|
||||||
<div class="flex px-4 py-2 items-center text-center border-b border-white border-opacity-10 text-white text-opacity-80">
|
<div class="flex px-4 py-2 items-center text-center border-b border-white border-opacity-10 text-white text-opacity-80">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex items-center px-4 py-2 justify-start relative hover:bg-bg" @mouseover="mouseover" @mouseleave="mouseleave">
|
<div class="flex items-center px-4 py-2 justify-start relative hover:bg-black-400" @mouseover="mouseover" @mouseleave="mouseleave">
|
||||||
<div v-if="isBookIncluded" class="absolute top-0 left-0 h-full w-1 bg-success z-10" />
|
<div v-if="isBookIncluded" class="absolute top-0 left-0 h-full w-1 bg-success z-10" />
|
||||||
<div class="w-20 max-w-20 text-center">
|
<div class="w-20 max-w-20 text-center">
|
||||||
<covers-collection-cover :book-items="books" :width="80" :height="40 * bookCoverAspectRatio" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-collection-cover :book-items="books" :width="80" :height="40 * bookCoverAspectRatio" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
|
|||||||
@@ -196,6 +196,9 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
async goPrevBook() {
|
async goPrevBook() {
|
||||||
if (this.currentBookshelfIndex - 1 < 0) return
|
if (this.currentBookshelfIndex - 1 < 0) return
|
||||||
|
// Remove focus from active input
|
||||||
|
document.activeElement?.blur?.()
|
||||||
|
|
||||||
var prevBookId = this.bookshelfBookIds[this.currentBookshelfIndex - 1]
|
var prevBookId = this.bookshelfBookIds[this.currentBookshelfIndex - 1]
|
||||||
this.processing = true
|
this.processing = true
|
||||||
var prevBook = await this.$axios.$get(`/api/items/${prevBookId}?expanded=1`).catch((error) => {
|
var prevBook = await this.$axios.$get(`/api/items/${prevBookId}?expanded=1`).catch((error) => {
|
||||||
@@ -215,6 +218,9 @@ export default {
|
|||||||
},
|
},
|
||||||
async goNextBook() {
|
async goNextBook() {
|
||||||
if (this.currentBookshelfIndex >= this.bookshelfBookIds.length - 1) return
|
if (this.currentBookshelfIndex >= this.bookshelfBookIds.length - 1) return
|
||||||
|
// Remove focus from active input
|
||||||
|
document.activeElement?.blur?.()
|
||||||
|
|
||||||
this.processing = true
|
this.processing = true
|
||||||
var nextBookId = this.bookshelfBookIds[this.currentBookshelfIndex + 1]
|
var nextBookId = this.bookshelfBookIds[this.currentBookshelfIndex + 1]
|
||||||
var nextBook = await this.$axios.$get(`/api/items/${nextBookId}?expanded=1`).catch((error) => {
|
var nextBook = await this.$axios.$get(`/api/items/${nextBookId}?expanded=1`).catch((error) => {
|
||||||
@@ -300,4 +306,4 @@ export default {
|
|||||||
.tab.tab-selected {
|
.tab.tab-selected {
|
||||||
height: 41px;
|
height: 41px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full overflow-hidden overflow-y-auto px-2 sm:px-4 py-6 relative">
|
<div class="w-full h-full overflow-hidden overflow-y-auto px-2 sm:px-4 py-6 relative">
|
||||||
<div class="flex flex-col sm:flex-row mb-4">
|
<div class="flex flex-col sm:flex-row mb-4">
|
||||||
<div class="relative self-center">
|
<div class="relative self-center md:self-start">
|
||||||
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId, libraryItemUpdatedAt, true)" :width="120" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId, libraryItemUpdatedAt, true)" :width="120" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
|
|
||||||
<!-- book cover overlay -->
|
<!-- book cover overlay -->
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
<ui-btn small @click="showLocalCovers = !showLocalCovers">{{ showLocalCovers ? $strings.ButtonHide : $strings.ButtonShow }}</ui-btn>
|
<ui-btn small @click="showLocalCovers = !showLocalCovers">{{ showLocalCovers ? $strings.ButtonHide : $strings.ButtonShow }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="showLocalCovers" class="flex items-center justify-center pb-2">
|
<div v-if="showLocalCovers" class="flex items-center justify-center flex-wrap pb-2">
|
||||||
<template v-for="localCoverFile in localCovers">
|
<template v-for="localCoverFile in localCovers">
|
||||||
<div :key="localCoverFile.ino" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="localCoverFile.metadata.path === coverPath ? 'border-yellow-300' : ''" @click="setCover(localCoverFile)">
|
<div :key="localCoverFile.ino" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="localCoverFile.metadata.path === coverPath ? 'border-yellow-300' : ''" @click="setCover(localCoverFile)">
|
||||||
<div class="h-24 bg-primary" :style="{ width: 96 / bookCoverAspectRatio + 'px' }">
|
<div class="h-24 bg-primary" :style="{ width: 96 / bookCoverAspectRatio + 'px' }">
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div ref="container" class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
||||||
<div v-if="show" class="w-full h-full">
|
<div v-if="show" class="w-full h-full">
|
||||||
<div class="py-4 px-4">
|
<div class="py-4 px-4">
|
||||||
<h1 v-if="!isBatch" class="text-2xl">{{ $strings.LabelAddToPlaylist }}</h1>
|
<h1 v-if="!isBatch" class="text-2xl">{{ $strings.LabelAddToPlaylist }}</h1>
|
||||||
@@ -19,8 +19,18 @@
|
|||||||
</template>
|
</template>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!playlists.length" class="flex h-32 items-center justify-center">
|
<div v-if="!playlists.length" class="flex h-32 items-center justify-center text-center px-2">
|
||||||
<p class="text-xl">{{ $strings.MessageNoUserPlaylists }}</p>
|
<div>
|
||||||
|
<p class="text-xl mb-2">{{ $strings.MessageNoUserPlaylists }}</p>
|
||||||
|
<div class="text-sm flex items-center justify-center text-gray-200">
|
||||||
|
<p>{{ $strings.MessageNoUserPlaylistsHelp }}</p>
|
||||||
|
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
|
||||||
|
<a href="https://www.audiobookshelf.org/guides/collections" target="_blank" class="inline-flex">
|
||||||
|
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
|
||||||
|
</a>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full h-px bg-white bg-opacity-10" />
|
<div class="w-full h-px bg-white bg-opacity-10" />
|
||||||
<form @submit.prevent="submitCreatePlaylist">
|
<form @submit.prevent="submitCreatePlaylist">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex items-center px-4 py-2 justify-start relative hover:bg-bg" @mouseover="mouseover" @mouseleave="mouseleave">
|
<div class="flex items-center px-4 py-2 justify-start relative hover:bg-black-400" @mouseover="mouseover" @mouseleave="mouseleave">
|
||||||
<div v-if="isItemIncluded" class="absolute top-0 left-0 h-full w-1 bg-success z-10" />
|
<div v-if="isItemIncluded" class="absolute top-0 left-0 h-full w-1 bg-success z-10" />
|
||||||
<div class="w-16 max-w-16 text-center">
|
<div class="w-16 max-w-16 text-center">
|
||||||
<covers-playlist-cover :items="items" :width="64" :height="64" />
|
<covers-playlist-cover :items="items" :width="64" :height="64" />
|
||||||
|
|||||||
@@ -117,8 +117,12 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
async goPrevEpisode() {
|
async goPrevEpisode() {
|
||||||
if (this.currentEpisodeIndex - 1 < 0) return
|
if (this.currentEpisodeIndex - 1 < 0) return
|
||||||
|
// Remove focus from active input
|
||||||
|
document.activeElement?.blur?.()
|
||||||
|
|
||||||
const prevEpisodeId = this.episodeTableEpisodeIds[this.currentEpisodeIndex - 1]
|
const prevEpisodeId = this.episodeTableEpisodeIds[this.currentEpisodeIndex - 1]
|
||||||
this.processing = true
|
this.processing = true
|
||||||
|
|
||||||
const prevEpisode = await this.$axios.$get(`/api/podcasts/${this.libraryItem.id}/episode/${prevEpisodeId}`).catch((error) => {
|
const prevEpisode = await this.$axios.$get(`/api/podcasts/${this.libraryItem.id}/episode/${prevEpisodeId}`).catch((error) => {
|
||||||
const errorMsg = error.response && error.response.data ? error.response.data : 'Failed to fetch episode'
|
const errorMsg = error.response && error.response.data ? error.response.data : 'Failed to fetch episode'
|
||||||
this.$toast.error(errorMsg)
|
this.$toast.error(errorMsg)
|
||||||
@@ -134,8 +138,12 @@ export default {
|
|||||||
},
|
},
|
||||||
async goNextEpisode() {
|
async goNextEpisode() {
|
||||||
if (this.currentEpisodeIndex >= this.episodeTableEpisodeIds.length - 1) return
|
if (this.currentEpisodeIndex >= this.episodeTableEpisodeIds.length - 1) return
|
||||||
|
// Remove focus from active input
|
||||||
|
document.activeElement?.blur?.()
|
||||||
|
|
||||||
this.processing = true
|
this.processing = true
|
||||||
const nextEpisodeId = this.episodeTableEpisodeIds[this.currentEpisodeIndex + 1]
|
const nextEpisodeId = this.episodeTableEpisodeIds[this.currentEpisodeIndex + 1]
|
||||||
|
|
||||||
const nextEpisode = await this.$axios.$get(`/api/podcasts/${this.libraryItem.id}/episode/${nextEpisodeId}`).catch((error) => {
|
const nextEpisode = await this.$axios.$get(`/api/podcasts/${this.libraryItem.id}/episode/${nextEpisodeId}`).catch((error) => {
|
||||||
const errorMsg = error.response && error.response.data ? error.response.data : 'Failed to fetch book'
|
const errorMsg = error.response && error.response.data ? error.response.data : 'Failed to fetch book'
|
||||||
this.$toast.error(errorMsg)
|
this.$toast.error(errorMsg)
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="flex flex-wrap">
|
<div class="flex flex-wrap">
|
||||||
<div class="w-1/5 p-1">
|
<div class="w-1/5 p-1">
|
||||||
<ui-text-input-with-label v-model="newEpisode.season" :label="$strings.LabelSeason" />
|
<ui-text-input-with-label v-model="newEpisode.season" trim-whitespace :label="$strings.LabelSeason" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/5 p-1">
|
<div class="w-1/5 p-1">
|
||||||
<ui-text-input-with-label v-model="newEpisode.episode" :label="$strings.LabelEpisode" />
|
<ui-text-input-with-label v-model="newEpisode.episode" trim-whitespace :label="$strings.LabelEpisode" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/5 p-1">
|
<div class="w-1/5 p-1">
|
||||||
<ui-dropdown v-model="newEpisode.episodeType" :label="$strings.LabelEpisodeType" :items="episodeTypes" small />
|
<ui-dropdown v-model="newEpisode.episodeType" :label="$strings.LabelEpisodeType" :items="episodeTypes" small />
|
||||||
@@ -14,10 +14,10 @@
|
|||||||
<ui-text-input-with-label v-model="pubDateInput" ref="pubdate" type="datetime-local" :label="$strings.LabelPubDate" @input="updatePubDate" />
|
<ui-text-input-with-label v-model="pubDateInput" ref="pubdate" type="datetime-local" :label="$strings.LabelPubDate" @input="updatePubDate" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full p-1">
|
<div class="w-full p-1">
|
||||||
<ui-text-input-with-label v-model="newEpisode.title" :label="$strings.LabelTitle" />
|
<ui-text-input-with-label v-model="newEpisode.title" :label="$strings.LabelTitle" trim-whitespace />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full p-1">
|
<div class="w-full p-1">
|
||||||
<ui-textarea-with-label v-model="newEpisode.subtitle" :label="$strings.LabelSubtitle" :rows="3" />
|
<ui-textarea-with-label v-model="newEpisode.subtitle" :label="$strings.LabelSubtitle" :rows="3" trim-whitespace />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full p-1">
|
<div class="w-full p-1">
|
||||||
<ui-rich-text-editor :label="$strings.LabelDescription" v-model="newEpisode.description" />
|
<ui-rich-text-editor :label="$strings.LabelDescription" v-model="newEpisode.description" />
|
||||||
|
|||||||
@@ -215,6 +215,10 @@ export default {
|
|||||||
inputBlur() {
|
inputBlur() {
|
||||||
if (!this.isFocused) return
|
if (!this.isFocused) return
|
||||||
|
|
||||||
|
if (typeof this.textInput === 'string') {
|
||||||
|
this.textInput = this.textInput.trim()
|
||||||
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (document.activeElement === this.$refs.input) {
|
if (document.activeElement === this.$refs.input) {
|
||||||
return
|
return
|
||||||
@@ -231,6 +235,11 @@ export default {
|
|||||||
},
|
},
|
||||||
forceBlur() {
|
forceBlur() {
|
||||||
this.isFocused = false
|
this.isFocused = false
|
||||||
|
|
||||||
|
if (typeof this.textInput === 'string') {
|
||||||
|
this.textInput = this.textInput.trim()
|
||||||
|
}
|
||||||
|
|
||||||
if (this.textInput) this.submitForm()
|
if (this.textInput) this.submitForm()
|
||||||
if (this.$refs.input) this.$refs.input.blur()
|
if (this.$refs.input) this.$refs.input.blur()
|
||||||
},
|
},
|
||||||
@@ -289,11 +298,12 @@ export default {
|
|||||||
this.selectedMenuItemIndex = null
|
this.selectedMenuItemIndex = null
|
||||||
},
|
},
|
||||||
submitForm() {
|
submitForm() {
|
||||||
if (!this.textInput) return
|
if (!this.textInput || !this.textInput.trim?.()) return
|
||||||
|
|
||||||
|
this.textInput = this.textInput.trim()
|
||||||
|
|
||||||
const cleaned = this.textInput.trim()
|
|
||||||
const matchesItem = this.items.find((i) => {
|
const matchesItem = this.items.find((i) => {
|
||||||
return i.name === cleaned
|
return i.name === this.textInput
|
||||||
})
|
})
|
||||||
|
|
||||||
if (matchesItem) {
|
if (matchesItem) {
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ export default {
|
|||||||
showCopy: Boolean,
|
showCopy: Boolean,
|
||||||
step: [String, Number],
|
step: [String, Number],
|
||||||
min: [String, Number],
|
min: [String, Number],
|
||||||
customInputClass: String
|
customInputClass: String,
|
||||||
|
trimWhitespace: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -101,9 +102,13 @@ export default {
|
|||||||
this.$emit('focus')
|
this.$emit('focus')
|
||||||
},
|
},
|
||||||
blurred() {
|
blurred() {
|
||||||
|
if (this.trimWhitespace && typeof this.inputValue === 'string') {
|
||||||
|
this.inputValue = this.inputValue.trim()
|
||||||
|
}
|
||||||
this.isFocused = false
|
this.isFocused = false
|
||||||
this.$emit('blur')
|
this.$emit('blur')
|
||||||
},
|
},
|
||||||
|
|
||||||
change(e) {
|
change(e) {
|
||||||
this.$emit('change', e.target.value)
|
this.$emit('change', e.target.value)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em>
|
<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em>
|
||||||
</label>
|
</label>
|
||||||
</slot>
|
</slot>
|
||||||
<ui-text-input :placeholder="placeholder || label" :inputId="identifier" ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" :show-copy="showCopy" class="w-full" :class="inputClass" @blur="inputBlurred" />
|
<ui-text-input :placeholder="placeholder || label" :inputId="identifier" ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" :show-copy="showCopy" class="w-full" :class="inputClass" :trim-whitespace="trimWhitespace" @blur="inputBlurred" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -24,7 +24,8 @@ export default {
|
|||||||
readonly: Boolean,
|
readonly: Boolean,
|
||||||
disabled: Boolean,
|
disabled: Boolean,
|
||||||
inputClass: String,
|
inputClass: String,
|
||||||
showCopy: Boolean
|
showCopy: Boolean,
|
||||||
|
trimWhitespace: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
|
|||||||
@@ -351,8 +351,10 @@ export default {
|
|||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
trix-editor {
|
trix-editor {
|
||||||
max-height: calc(4 * 1lh);
|
height: calc(4 * 1lh);
|
||||||
|
min-height: calc(4 * 1lh);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
resize: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
trix-editor * {
|
trix-editor * {
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
<form class="w-full h-full px-2 md:px-4 py-6" @submit.prevent="submitForm">
|
<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="flex flex-wrap -mx-1">
|
||||||
<div class="w-full md:w-1/2 px-1">
|
<div class="w-full md:w-1/2 px-1">
|
||||||
<ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" @input="handleInputChange" />
|
<ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" trim-whitespace @input="handleInputChange" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-1 mt-2 md:mt-0">
|
<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" @input="handleInputChange" />
|
<ui-text-input-with-label ref="subtitleInput" v-model="details.subtitle" :label="$strings.LabelSubtitle" trim-whitespace @input="handleInputChange" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -42,19 +42,19 @@
|
|||||||
<ui-multi-select ref="narratorsSelect" v-model="details.narrators" :label="$strings.LabelNarrators" :items="narrators" @input="handleInputChange" />
|
<ui-multi-select ref="narratorsSelect" v-model="details.narrators" :label="$strings.LabelNarrators" :items="narrators" @input="handleInputChange" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
|
<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" @input="handleInputChange" />
|
<ui-text-input-with-label ref="isbnInput" v-model="details.isbn" label="ISBN" trim-whitespace @input="handleInputChange" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
|
<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" @input="handleInputChange" />
|
<ui-text-input-with-label ref="asinInput" v-model="details.asin" label="ASIN" trim-whitespace @input="handleInputChange" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap mt-2 -mx-1">
|
<div class="flex flex-wrap mt-2 -mx-1">
|
||||||
<div class="w-full md:w-1/4 px-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" @input="handleInputChange" />
|
<ui-text-input-with-label ref="publisherInput" v-model="details.publisher" :label="$strings.LabelPublisher" trim-whitespace @input="handleInputChange" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
|
<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" @input="handleInputChange" />
|
<ui-text-input-with-label ref="languageInput" v-model="details.language" :label="$strings.LabelLanguage" trim-whitespace @input="handleInputChange" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-1 pt-6 mt-2 md:mt-0">
|
<div class="flex-grow px-1 pt-6 mt-2 md:mt-0">
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ export default {
|
|||||||
this.updateSelectionMode(false)
|
this.updateSelectionMode(false)
|
||||||
},
|
},
|
||||||
editEpisode({ libraryItem, episode }) {
|
editEpisode({ libraryItem, episode }) {
|
||||||
|
this.$store.commit('setEpisodeTableEpisodeIds', [episode.id])
|
||||||
this.$store.commit('setSelectedLibraryItem', libraryItem)
|
this.$store.commit('setSelectedLibraryItem', libraryItem)
|
||||||
this.$store.commit('globals/setSelectedEpisode', episode)
|
this.$store.commit('globals/setSelectedEpisode', episode)
|
||||||
this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)
|
this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
<ui-tooltip v-if="tasksRunning" :text="$strings.LabelTasks" direction="bottom" class="flex items-center">
|
<ui-tooltip v-if="tasksRunning" :text="$strings.LabelTasks" direction="bottom" class="flex items-center">
|
||||||
<widgets-loading-spinner />
|
<widgets-loading-spinner />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
<ui-tooltip v-else text="Activities" direction="bottom" class="flex items-center">
|
<ui-tooltip v-else :text="$strings.LabelActivities" direction="bottom" class="flex items-center">
|
||||||
<span class="material-symbols text-1.5xl" aria-label="Activities" role="button">notifications</span>
|
<span class="material-symbols text-1.5xl" :aria-label="$strings.LabelActivities" role="button">notifications</span>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showUnseenSuccessIndicator" class="w-2 h-2 rounded-full bg-success pointer-events-none absolute -top-1 -right-0.5" />
|
<div v-if="showUnseenSuccessIndicator" class="w-2 h-2 rounded-full bg-success pointer-events-none absolute -top-1 -right-0.5" />
|
||||||
|
|||||||
@@ -3,14 +3,14 @@
|
|||||||
<form class="w-full h-full px-4 py-6" @submit.prevent="submitForm">
|
<form class="w-full h-full px-4 py-6" @submit.prevent="submitForm">
|
||||||
<div class="flex -mx-1">
|
<div class="flex -mx-1">
|
||||||
<div class="w-1/2 px-1">
|
<div class="w-1/2 px-1">
|
||||||
<ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" @input="handleInputChange" />
|
<ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" trim-whitespace @input="handleInputChange" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-1">
|
<div class="flex-grow px-1">
|
||||||
<ui-text-input-with-label ref="authorInput" v-model="details.author" :label="$strings.LabelAuthor" @input="handleInputChange" />
|
<ui-text-input-with-label ref="authorInput" v-model="details.author" :label="$strings.LabelAuthor" trim-whitespace @input="handleInputChange" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ui-text-input-with-label ref="feedUrlInput" v-model="details.feedUrl" :label="$strings.LabelRSSFeedURL" class="mt-2" @input="handleInputChange" />
|
<ui-text-input-with-label ref="feedUrlInput" v-model="details.feedUrl" :label="$strings.LabelRSSFeedURL" trim-whitespace class="mt-2" @input="handleInputChange" />
|
||||||
|
|
||||||
<ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" :label="$strings.LabelDescription" class="mt-2" @input="handleInputChange" />
|
<ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" :label="$strings.LabelDescription" class="mt-2" @input="handleInputChange" />
|
||||||
|
|
||||||
@@ -25,13 +25,13 @@
|
|||||||
|
|
||||||
<div class="flex mt-2 -mx-1">
|
<div class="flex mt-2 -mx-1">
|
||||||
<div class="w-1/4 px-1">
|
<div class="w-1/4 px-1">
|
||||||
<ui-text-input-with-label ref="releaseDateInput" v-model="details.releaseDate" :label="$strings.LabelReleaseDate" @input="handleInputChange" />
|
<ui-text-input-with-label ref="releaseDateInput" v-model="details.releaseDate" :label="$strings.LabelReleaseDate" trim-whitespace @input="handleInputChange" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/4 px-1">
|
<div class="w-1/4 px-1">
|
||||||
<ui-text-input-with-label ref="itunesIdInput" v-model="details.itunesId" label="iTunes ID" @input="handleInputChange" />
|
<ui-text-input-with-label ref="itunesIdInput" v-model="details.itunesId" label="iTunes ID" trim-whitespace @input="handleInputChange" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/4 px-1">
|
<div class="w-1/4 px-1">
|
||||||
<ui-text-input-with-label ref="languageInput" v-model="details.language" :label="$strings.LabelLanguage" @input="handleInputChange" />
|
<ui-text-input-with-label ref="languageInput" v-model="details.language" :label="$strings.LabelLanguage" trim-whitespace @input="handleInputChange" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-1 pt-6">
|
<div class="flex-grow px-1 pt-6">
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const pkg = require('./package.json')
|
const pkg = require('./package.json')
|
||||||
|
|
||||||
const routerBasePath = process.env.ROUTER_BASE_PATH || '/audiobookshelf'
|
const routerBasePath = process.env.ROUTER_BASE_PATH ?? '/audiobookshelf'
|
||||||
const serverHostUrl = process.env.NODE_ENV === 'production' ? '' : 'http://localhost:3333'
|
const serverHostUrl = process.env.NODE_ENV === 'production' ? '' : 'http://localhost:3333'
|
||||||
const serverPaths = ['api/', 'public/', 'hls/', 'auth/', 'feed/', 'status', 'login', 'logout', 'init']
|
const serverPaths = ['api/', 'public/', 'hls/', 'auth/', 'feed/', 'status', 'login', 'logout', 'init']
|
||||||
const proxy = Object.fromEntries(serverPaths.map((path) => [`${routerBasePath}/${path}`, { target: process.env.NODE_ENV !== 'production' ? serverHostUrl : '/' }]))
|
const proxy = Object.fromEntries(serverPaths.map((path) => [`${routerBasePath}/${path}`, { target: process.env.NODE_ENV !== 'production' ? serverHostUrl : '/' }]))
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.18.1",
|
"version": "2.19.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.18.1",
|
"version": "2.19.5",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/axios": "^5.13.6",
|
"@nuxtjs/axios": "^5.13.6",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.18.1",
|
"version": "2.19.5",
|
||||||
"buildNumber": 1,
|
"buildNumber": 1,
|
||||||
"description": "Self-hosted audiobook and podcast client",
|
"description": "Self-hosted audiobook and podcast client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
|||||||
@@ -414,11 +414,8 @@ export default {
|
|||||||
|
|
||||||
const audioEl = this.audioEl || document.createElement('audio')
|
const audioEl = this.audioEl || document.createElement('audio')
|
||||||
var src = audioTrack.contentUrl + `?token=${this.userToken}`
|
var src = audioTrack.contentUrl + `?token=${this.userToken}`
|
||||||
if (this.$isDev) {
|
|
||||||
src = `${process.env.serverUrl}${src}`
|
|
||||||
}
|
|
||||||
|
|
||||||
audioEl.src = src
|
audioEl.src = `${process.env.serverUrl}${src}`
|
||||||
audioEl.id = 'chapter-audio'
|
audioEl.id = 'chapter-audio'
|
||||||
document.body.appendChild(audioEl)
|
document.body.appendChild(audioEl)
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<div v-if="openMapOptions" class="flex flex-wrap">
|
<div v-if="openMapOptions" class="flex flex-wrap">
|
||||||
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 h-18 w-1/2">
|
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 h-18 w-1/2">
|
||||||
<ui-checkbox v-model="selectedBatchUsage.subtitle" />
|
<ui-checkbox v-model="selectedBatchUsage.subtitle" />
|
||||||
<ui-text-input-with-label ref="subtitleInput" v-model="batchDetails.subtitle" :disabled="!selectedBatchUsage.subtitle" :label="$strings.LabelSubtitle" class="mb-5 ml-4" />
|
<ui-text-input-with-label ref="subtitleInput" v-model="batchDetails.subtitle" :disabled="!selectedBatchUsage.subtitle" :label="$strings.LabelSubtitle" trim-whitespace class="mb-5 ml-4" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isPodcastLibrary" class="flex items-center px-4 h-18 w-1/2">
|
<div v-if="!isPodcastLibrary" class="flex items-center px-4 h-18 w-1/2">
|
||||||
<ui-checkbox v-model="selectedBatchUsage.authors" />
|
<ui-checkbox v-model="selectedBatchUsage.authors" />
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 h-18 w-1/2">
|
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 h-18 w-1/2">
|
||||||
<ui-checkbox v-model="selectedBatchUsage.publishedYear" />
|
<ui-checkbox v-model="selectedBatchUsage.publishedYear" />
|
||||||
<ui-text-input-with-label ref="publishedYearInput" v-model="batchDetails.publishedYear" :disabled="!selectedBatchUsage.publishedYear" :label="$strings.LabelPublishYear" class="mb-5 ml-4" />
|
<ui-text-input-with-label ref="publishedYearInput" v-model="batchDetails.publishedYear" :disabled="!selectedBatchUsage.publishedYear" :label="$strings.LabelPublishYear" trim-whitespace class="mb-5 ml-4" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isPodcastLibrary" class="flex items-center px-4 h-18 w-1/2">
|
<div v-if="!isPodcastLibrary" class="flex items-center px-4 h-18 w-1/2">
|
||||||
<ui-checkbox v-model="selectedBatchUsage.series" />
|
<ui-checkbox v-model="selectedBatchUsage.series" />
|
||||||
@@ -51,11 +51,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 h-18 w-1/2">
|
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 h-18 w-1/2">
|
||||||
<ui-checkbox v-model="selectedBatchUsage.publisher" />
|
<ui-checkbox v-model="selectedBatchUsage.publisher" />
|
||||||
<ui-text-input-with-label ref="publisherInput" v-model="batchDetails.publisher" :disabled="!selectedBatchUsage.publisher" :label="$strings.LabelPublisher" class="mb-5 ml-4" />
|
<ui-text-input-with-label ref="publisherInput" v-model="batchDetails.publisher" :disabled="!selectedBatchUsage.publisher" :label="$strings.LabelPublisher" trim-whitespace class="mb-5 ml-4" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isMapAppend" class="flex items-center px-4 h-18 w-1/2">
|
<div v-if="!isMapAppend" class="flex items-center px-4 h-18 w-1/2">
|
||||||
<ui-checkbox v-model="selectedBatchUsage.language" />
|
<ui-checkbox v-model="selectedBatchUsage.language" />
|
||||||
<ui-text-input-with-label ref="languageInput" v-model="batchDetails.language" :disabled="!selectedBatchUsage.language" :label="$strings.LabelLanguage" class="mb-5 ml-4" />
|
<ui-text-input-with-label ref="languageInput" v-model="batchDetails.language" :disabled="!selectedBatchUsage.language" :label="$strings.LabelLanguage" trim-whitespace class="mb-5 ml-4" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isMapAppend" class="flex items-center px-4 h-18 w-1/2">
|
<div v-if="!isMapAppend" class="flex items-center px-4 h-18 w-1/2">
|
||||||
<ui-checkbox v-model="selectedBatchUsage.explicit" />
|
<ui-checkbox v-model="selectedBatchUsage.explicit" />
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ export default {
|
|||||||
},
|
},
|
||||||
scheduleDescription() {
|
scheduleDescription() {
|
||||||
if (!this.cronExpression) return ''
|
if (!this.cronExpression) return ''
|
||||||
const parsed = this.$parseCronExpression(this.cronExpression)
|
const parsed = this.$parseCronExpression(this.cronExpression, this)
|
||||||
return parsed ? parsed.description : `${this.$strings.LabelCustomCronExpression} ${this.cronExpression}`
|
return parsed ? parsed.description : `${this.$strings.LabelCustomCronExpression} ${this.cronExpression}`
|
||||||
},
|
},
|
||||||
nextBackupDate() {
|
nextBackupDate() {
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="newServerSettings.scannerFindCovers" class="w-44 ml-14 mb-2">
|
<div v-if="newServerSettings.scannerFindCovers" class="w-44 ml-14 mb-2">
|
||||||
<ui-dropdown v-model="newServerSettings.scannerCoverProvider" small :items="providers" label="Cover Provider" @input="updateScannerCoverProvider" :disabled="updatingServerSettings" />
|
<ui-dropdown v-model="newServerSettings.scannerCoverProvider" small :items="providers" :label="$strings.LabelCoverProvider" @input="updateScannerCoverProvider" :disabled="updatingServerSettings" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div role="article" :aria-label="$strings.LabelSettingsPreferMatchedMetadataHelp" class="flex items-center py-2">
|
<div role="article" :aria-label="$strings.LabelSettingsPreferMatchedMetadataHelp" class="flex items-center py-2">
|
||||||
|
|||||||
@@ -137,7 +137,16 @@ export default {
|
|||||||
this.$toast.error(this.$strings.ToastFailedToLoadData)
|
this.$toast.error(this.$strings.ToastFailedToLoadData)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.feeds = data.feeds
|
this.feeds = data.feeds.map((feed) => ({
|
||||||
|
...feed,
|
||||||
|
episodes: [...feed.episodes].sort((a, b) => {
|
||||||
|
if (!a.pubDate) return 1 // null dates sort to end
|
||||||
|
if (!b.pubDate) return -1
|
||||||
|
const dateA = new Date(a.pubDate)
|
||||||
|
const dateB = new Date(b.pubDate)
|
||||||
|
return dateA - dateB
|
||||||
|
})
|
||||||
|
}))
|
||||||
},
|
},
|
||||||
init() {
|
init() {
|
||||||
this.loadFeeds()
|
this.loadFeeds()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export default class AudioTrack {
|
export default class AudioTrack {
|
||||||
constructor(track, userToken) {
|
constructor(track, userToken, routerBasePath) {
|
||||||
this.index = track.index || 0
|
this.index = track.index || 0
|
||||||
this.startOffset = track.startOffset || 0 // Total time of all previous tracks
|
this.startOffset = track.startOffset || 0 // Total time of all previous tracks
|
||||||
this.duration = track.duration || 0
|
this.duration = track.duration || 0
|
||||||
@@ -9,20 +9,27 @@ export default class AudioTrack {
|
|||||||
this.metadata = track.metadata || {}
|
this.metadata = track.metadata || {}
|
||||||
|
|
||||||
this.userToken = userToken
|
this.userToken = userToken
|
||||||
|
this.routerBasePath = routerBasePath || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for CastPlayer
|
||||||
|
*/
|
||||||
get fullContentUrl() {
|
get fullContentUrl() {
|
||||||
if (!this.contentUrl || this.contentUrl.startsWith('http')) return this.contentUrl
|
if (!this.contentUrl || this.contentUrl.startsWith('http')) return this.contentUrl
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
return `${process.env.serverUrl}${this.contentUrl}?token=${this.userToken}`
|
return `${process.env.serverUrl}${this.contentUrl}?token=${this.userToken}`
|
||||||
}
|
}
|
||||||
return `${window.location.origin}${this.contentUrl}?token=${this.userToken}`
|
return `${window.location.origin}${this.routerBasePath}${this.contentUrl}?token=${this.userToken}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for LocalPlayer
|
||||||
|
*/
|
||||||
get relativeContentUrl() {
|
get relativeContentUrl() {
|
||||||
if (!this.contentUrl || this.contentUrl.startsWith('http')) return this.contentUrl
|
if (!this.contentUrl || this.contentUrl.startsWith('http')) return this.contentUrl
|
||||||
|
|
||||||
return this.contentUrl + `?token=${this.userToken}`
|
return `${this.routerBasePath}${this.contentUrl}?token=${this.userToken}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ export default class PlayerHandler {
|
|||||||
|
|
||||||
console.log('[PlayerHandler] Preparing Session', session)
|
console.log('[PlayerHandler] Preparing Session', session)
|
||||||
|
|
||||||
var audioTracks = session.audioTracks.map((at) => new AudioTrack(at, this.userToken))
|
var audioTracks = session.audioTracks.map((at) => new AudioTrack(at, this.userToken, this.ctx.$config.routerBasePath))
|
||||||
|
|
||||||
this.ctx.playerLoading = true
|
this.ctx.playerLoading = true
|
||||||
this.isHlsTranscode = true
|
this.isHlsTranscode = true
|
||||||
|
|||||||
@@ -107,6 +107,19 @@ Vue.prototype.$formatNumber = (num) => {
|
|||||||
return Intl.NumberFormat(Vue.prototype.$languageCodes.current).format(num)
|
return Intl.NumberFormat(Vue.prototype.$languageCodes.current).format(num)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the days of the week for the current language
|
||||||
|
* Starts with Sunday
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
Vue.prototype.$getDaysOfWeek = () => {
|
||||||
|
const days = []
|
||||||
|
for (let i = 0; i < 7; i++) {
|
||||||
|
days.push(new Date(2025, 0, 5 + i).toLocaleString(Vue.prototype.$languageCodes.current, { weekday: 'long' }))
|
||||||
|
}
|
||||||
|
return days
|
||||||
|
}
|
||||||
|
|
||||||
const translations = {
|
const translations = {
|
||||||
[defaultCode]: enUsStrings
|
[defaultCode]: enUsStrings
|
||||||
}
|
}
|
||||||
@@ -148,6 +161,7 @@ async function loadi18n(code) {
|
|||||||
Vue.prototype.$setDateFnsLocale(languageCodeMap[code].dateFnsLocale)
|
Vue.prototype.$setDateFnsLocale(languageCodeMap[code].dateFnsLocale)
|
||||||
|
|
||||||
this?.$eventBus?.$emit('change-lang', code)
|
this?.$eventBus?.$emit('change-lang', code)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-10
@@ -93,7 +93,7 @@ Vue.prototype.$elapsedPrettyExtended = (seconds, useDays = true, showSeconds = t
|
|||||||
return strs.join(' ')
|
return strs.join(' ')
|
||||||
}
|
}
|
||||||
|
|
||||||
Vue.prototype.$parseCronExpression = (expression) => {
|
Vue.prototype.$parseCronExpression = (expression, context) => {
|
||||||
if (!expression) return null
|
if (!expression) return null
|
||||||
const pieces = expression.split(' ')
|
const pieces = expression.split(' ')
|
||||||
if (pieces.length !== 5) {
|
if (pieces.length !== 5) {
|
||||||
@@ -102,31 +102,31 @@ Vue.prototype.$parseCronExpression = (expression) => {
|
|||||||
|
|
||||||
const commonPatterns = [
|
const commonPatterns = [
|
||||||
{
|
{
|
||||||
text: 'Every 12 hours',
|
text: context.$strings.LabelIntervalEvery12Hours,
|
||||||
value: '0 */12 * * *'
|
value: '0 */12 * * *'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Every 6 hours',
|
text: context.$strings.LabelIntervalEvery6Hours,
|
||||||
value: '0 */6 * * *'
|
value: '0 */6 * * *'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Every 2 hours',
|
text: context.$strings.LabelIntervalEvery2Hours,
|
||||||
value: '0 */2 * * *'
|
value: '0 */2 * * *'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Every hour',
|
text: context.$strings.LabelIntervalEveryHour,
|
||||||
value: '0 * * * *'
|
value: '0 * * * *'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Every 30 minutes',
|
text: context.$strings.LabelIntervalEvery30Minutes,
|
||||||
value: '*/30 * * * *'
|
value: '*/30 * * * *'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Every 15 minutes',
|
text: context.$strings.LabelIntervalEvery15Minutes,
|
||||||
value: '*/15 * * * *'
|
value: '*/15 * * * *'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Every minute',
|
text: context.$strings.LabelIntervalEveryMinute,
|
||||||
value: '* * * * *'
|
value: '* * * * *'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -147,7 +147,7 @@ Vue.prototype.$parseCronExpression = (expression) => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
|
const weekdays = context.$getDaysOfWeek()
|
||||||
var weekdayText = 'day'
|
var weekdayText = 'day'
|
||||||
if (pieces[4] !== '*')
|
if (pieces[4] !== '*')
|
||||||
weekdayText = pieces[4]
|
weekdayText = pieces[4]
|
||||||
@@ -156,7 +156,7 @@ Vue.prototype.$parseCronExpression = (expression) => {
|
|||||||
.join(', ')
|
.join(', ')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
description: `Run every ${weekdayText} at ${pieces[1]}:${pieces[0].padStart(2, '0')}`
|
description: context.$getString('MessageScheduleRunEveryWeekdayAtTime', [weekdayText, `${pieces[1]}:${pieces[0].padStart(2, '0')}`])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+227
-3
@@ -10,6 +10,7 @@
|
|||||||
"ButtonApplyChapters": "Ужыць раздзелы",
|
"ButtonApplyChapters": "Ужыць раздзелы",
|
||||||
"ButtonAuthors": "Аўтары",
|
"ButtonAuthors": "Аўтары",
|
||||||
"ButtonBack": "Назад",
|
"ButtonBack": "Назад",
|
||||||
|
"ButtonBatchEditPopulateFromExisting": "Запоўніць з існуючага",
|
||||||
"ButtonBrowseForFolder": "Знайсці тэчку",
|
"ButtonBrowseForFolder": "Знайсці тэчку",
|
||||||
"ButtonCancel": "Адмяніць",
|
"ButtonCancel": "Адмяніць",
|
||||||
"ButtonCancelEncode": "Адмяніць кадзіраванне",
|
"ButtonCancelEncode": "Адмяніць кадзіраванне",
|
||||||
@@ -35,14 +36,18 @@
|
|||||||
"ButtonForceReScan": "Прымусовае паўторнае сканаванне",
|
"ButtonForceReScan": "Прымусовае паўторнае сканаванне",
|
||||||
"ButtonFullPath": "Поўны шлях",
|
"ButtonFullPath": "Поўны шлях",
|
||||||
"ButtonHide": "Схаваць",
|
"ButtonHide": "Схаваць",
|
||||||
|
"ButtonHome": "Галоўная",
|
||||||
"ButtonIssues": "Праблемы",
|
"ButtonIssues": "Праблемы",
|
||||||
"ButtonJumpBackward": "Перайсці назад",
|
"ButtonJumpBackward": "Перайсці назад",
|
||||||
"ButtonJumpForward": "Перайсці наперад",
|
"ButtonJumpForward": "Перайсці наперад",
|
||||||
|
"ButtonLatest": "Апошняе",
|
||||||
"ButtonLibrary": "Бібліятэка",
|
"ButtonLibrary": "Бібліятэка",
|
||||||
"ButtonLogout": "Выйсці",
|
"ButtonLogout": "Выйсці",
|
||||||
"ButtonLookup": "",
|
"ButtonLookup": "Пошук",
|
||||||
|
"ButtonManageTracks": "Кіраванне дарожкамі",
|
||||||
"ButtonMapChapterTitles": "Супаставіць назвы раздзелаў",
|
"ButtonMapChapterTitles": "Супаставіць назвы раздзелаў",
|
||||||
"ButtonMatchAllAuthors": "Супадзенне ўсіх аўтараў",
|
"ButtonMatchAllAuthors": "Супадзенне ўсіх аўтараў",
|
||||||
|
"ButtonMatchBooks": "Падбор кніг",
|
||||||
"ButtonNevermind": "Няважна",
|
"ButtonNevermind": "Няважна",
|
||||||
"ButtonNext": "Далей",
|
"ButtonNext": "Далей",
|
||||||
"ButtonNextChapter": "Наступны раздзел",
|
"ButtonNextChapter": "Наступны раздзел",
|
||||||
@@ -71,6 +76,9 @@
|
|||||||
"ButtonRemove": "Выдаліць",
|
"ButtonRemove": "Выдаліць",
|
||||||
"ButtonRemoveAll": "Выдаліць усе",
|
"ButtonRemoveAll": "Выдаліць усе",
|
||||||
"ButtonRemoveAllLibraryItems": "Выдаліць усе элементы бібліятэкі",
|
"ButtonRemoveAllLibraryItems": "Выдаліць усе элементы бібліятэкі",
|
||||||
|
"ButtonRemoveFromContinueListening": "Выдаліць з Працягваць слухаць",
|
||||||
|
"ButtonRemoveFromContinueReading": "Выдаліць з Працягваць чытанне",
|
||||||
|
"ButtonRemoveSeriesFromContinueSeries": "Выдаліць серыю з Працягваць серыю",
|
||||||
"ButtonReset": "Скінуць",
|
"ButtonReset": "Скінуць",
|
||||||
"ButtonResetToDefault": "Скінуць па змаўчанні",
|
"ButtonResetToDefault": "Скінуць па змаўчанні",
|
||||||
"ButtonRestore": "Аднавіць",
|
"ButtonRestore": "Аднавіць",
|
||||||
@@ -100,9 +108,14 @@
|
|||||||
"ButtonUserEdit": "Рэдагаваць карыстальніка {0}",
|
"ButtonUserEdit": "Рэдагаваць карыстальніка {0}",
|
||||||
"ButtonViewAll": "Прагледзець усе",
|
"ButtonViewAll": "Прагледзець усе",
|
||||||
"ButtonYes": "Так",
|
"ButtonYes": "Так",
|
||||||
|
"ErrorUploadFetchMetadataAPI": "Памылка пры атрыманні метададзеных",
|
||||||
|
"ErrorUploadFetchMetadataNoResults": "Не ўдалося атрымаць метададзеныя – паспрабуйце абнавіць назву і/або аўтара",
|
||||||
|
"ErrorUploadLacksTitle": "Павінна быць назва",
|
||||||
"HeaderAccount": "Уліковы запіс",
|
"HeaderAccount": "Уліковы запіс",
|
||||||
"HeaderAddCustomMetadataProvider": "Дадаць карыстальніцкага пастаўшчыка метаданных",
|
"HeaderAddCustomMetadataProvider": "Дадаць карыстальніцкага пастаўшчыка метаданных",
|
||||||
|
"HeaderAdvanced": "Дадаткова",
|
||||||
"HeaderAppriseNotificationSettings": "Налады апавяшчэнняў Apprise",
|
"HeaderAppriseNotificationSettings": "Налады апавяшчэнняў Apprise",
|
||||||
|
"HeaderAudioTracks": "Аўдыядарожкі",
|
||||||
"HeaderAudiobookTools": "Сродкі кіравання файламі аўдыякніг",
|
"HeaderAudiobookTools": "Сродкі кіравання файламі аўдыякніг",
|
||||||
"HeaderAuthentication": "Аўтэнтыфікацыя",
|
"HeaderAuthentication": "Аўтэнтыфікацыя",
|
||||||
"HeaderBackups": "Рэзервовыя копіі",
|
"HeaderBackups": "Рэзервовыя копіі",
|
||||||
@@ -112,6 +125,217 @@
|
|||||||
"HeaderCollection": "Калекцыя",
|
"HeaderCollection": "Калекцыя",
|
||||||
"HeaderCollectionItems": "Элементы калекцыі",
|
"HeaderCollectionItems": "Элементы калекцыі",
|
||||||
"HeaderCover": "Вокладка",
|
"HeaderCover": "Вокладка",
|
||||||
"HeaderCurrentDownloads": "Бягучыя загрузкі",
|
"HeaderCurrentDownloads": "Бягучыя спампоўкі",
|
||||||
"HeaderCustomMessageOnLogin": "Карыстальніцкае паведамленне пры ўваходзе"
|
"HeaderCustomMessageOnLogin": "Карыстальніцкае паведамленне пры ўваходзе",
|
||||||
|
"HeaderCustomMetadataProviders": "Карыстальніцкія крыніцы метададзеных",
|
||||||
|
"HeaderDetails": "Падрабязнасці",
|
||||||
|
"HeaderDownloadQueue": "Чарга спамповак",
|
||||||
|
"HeaderEbookFiles": "Файлы электронных кніг",
|
||||||
|
"HeaderEmail": "Электронная пошта",
|
||||||
|
"HeaderEmailSettings": "Налады электроннай пошты",
|
||||||
|
"HeaderEpisodes": "Эпізоды",
|
||||||
|
"HeaderEreaderDevices": "Прылады для чытання",
|
||||||
|
"HeaderEreaderSettings": "Налады прылады для чытання",
|
||||||
|
"HeaderFiles": "Файлы",
|
||||||
|
"HeaderFindChapters": "Знайсці раздзелы",
|
||||||
|
"HeaderIgnoredFiles": "Ігнараваныя файлы",
|
||||||
|
"HeaderItemFiles": "Файлы элементаў",
|
||||||
|
"HeaderItemMetadataUtils": "Утыліты для метададзеных элементаў",
|
||||||
|
"HeaderLastListeningSession": "Апошні сеанс праслухоўвання",
|
||||||
|
"HeaderLatestEpisodes": "Апошнія эпізоды",
|
||||||
|
"HeaderLibraries": "Бібліятэкі",
|
||||||
|
"HeaderLibraryFiles": "Файлы бібліятэкі",
|
||||||
|
"HeaderLibraryStats": "Статыстыка бібліятэкі",
|
||||||
|
"HeaderListeningSessions": "Сеансы праслухоўвання",
|
||||||
|
"HeaderListeningStats": "Статыстыка праслухоўвання",
|
||||||
|
"HeaderLogin": "Уваход",
|
||||||
|
"HeaderLogs": "Журналы",
|
||||||
|
"HeaderManageGenres": "Кіраванне жанрамі",
|
||||||
|
"HeaderManageTags": "Кіраванне тэгамі",
|
||||||
|
"HeaderMapDetails": "Падрабязнасці адлюстравання",
|
||||||
|
"HeaderNewAccount": "Новы ўліковы запіс",
|
||||||
|
"HeaderNewLibrary": "Новая бібліятэка",
|
||||||
|
"HeaderNotificationCreate": "Стварыць апавяшчэнне",
|
||||||
|
"HeaderNotificationUpdate": "Абнавіць апавяшчэнне",
|
||||||
|
"HeaderNotifications": "Апавяшчэнні",
|
||||||
|
"HeaderOpenListeningSessions": "Адкрыць сеансы праслухоўвання",
|
||||||
|
"HeaderOpenRSSFeed": "Адкрыць RSS-стужку",
|
||||||
|
"HeaderPlaylist": "Плэйліст",
|
||||||
|
"HeaderPlaylistItems": "Элементы плэйліста",
|
||||||
|
"HeaderRSSFeedGeneral": "Падрабязнасці RSS",
|
||||||
|
"HeaderRSSFeedIsOpen": "RSS-стужка адкрыта",
|
||||||
|
"HeaderRSSFeeds": "RSS-стужкі",
|
||||||
|
"HeaderRemoveEpisode": "Выдаліць эпізод",
|
||||||
|
"HeaderRemoveEpisodes": "Выдаліць {0} эпізодаў",
|
||||||
|
"HeaderSavedMediaProgress": "Захаваны прагрэс медыя",
|
||||||
|
"HeaderScheduleEpisodeDownloads": "Расклад аўтаматычных спамповак эпізодаў",
|
||||||
|
"HeaderSettings": "Налады",
|
||||||
|
"HeaderSettingsDisplay": "Дысплей",
|
||||||
|
"HeaderSettingsExperimental": "Эксперыментальныя функцыі",
|
||||||
|
"HeaderSettingsGeneral": "Агульныя",
|
||||||
|
"HeaderSettingsScanner": "Сканер",
|
||||||
|
"HeaderSettingsWebClient": "Вэб-кліент",
|
||||||
|
"HeaderSleepTimer": "Таймер сну",
|
||||||
|
"HeaderStatsMinutesListeningChart": "Хвіліны праслухоўвання (апошнія 7 дзён)",
|
||||||
|
"HeaderStatsTop10Authors": "10 лепшых аўтараў",
|
||||||
|
"HeaderStatsTop5Genres": "5 лепшых жанраў",
|
||||||
|
"HeaderTableOfContents": "Змест",
|
||||||
|
"HeaderTools": "Інструменты",
|
||||||
|
"HeaderUpdateAccount": "Абнавіць уліковы запіс",
|
||||||
|
"HeaderYourStats": "Ваша статыстыка",
|
||||||
|
"LabelAccountType": "Тып уліковага запіса",
|
||||||
|
"LabelAccountTypeAdmin": "Адміністратар",
|
||||||
|
"LabelAccountTypeGuest": "Госць",
|
||||||
|
"LabelAccountTypeUser": "Карыстальнік",
|
||||||
|
"LabelAddToPlaylist": "Дадаць у плэйліст",
|
||||||
|
"LabelAddedDate": "Дададзена {0}",
|
||||||
|
"LabelAll": "Усе",
|
||||||
|
"LabelAudioBitrate": "Бітрэйт аўдыё (напрыклад, 128к)",
|
||||||
|
"LabelAudioChannels": "Аўдыёканалы (1 або 2)",
|
||||||
|
"LabelAudioCodec": "Аўдыёкодэк",
|
||||||
|
"LabelAuthor": "Аўтар",
|
||||||
|
"LabelAuthorFirstLast": "Аўтар (Імя Прозвішча)",
|
||||||
|
"LabelAuthorLastFirst": "Аўтар (Прозвішча, Імя)",
|
||||||
|
"LabelAuthors": "Аўтары",
|
||||||
|
"LabelAutoDownloadEpisodes": "Аўтаматычнае спампаванне эпізодаў",
|
||||||
|
"LabelBackupAudioFiles": "Рэзервовае капіраванне аўдыёфайлаў",
|
||||||
|
"LabelBooks": "Кнігі",
|
||||||
|
"LabelChapters": "Раздзелы",
|
||||||
|
"LabelClosePlayer": "Зачыніць прайгравальнік",
|
||||||
|
"LabelCollapseSeries": "Згарнуць серыі",
|
||||||
|
"LabelComplete": "Завершана",
|
||||||
|
"LabelContinueListening": "Працягваць слухаць",
|
||||||
|
"LabelContinueReading": "Працягнуць чытанне",
|
||||||
|
"LabelContinueSeries": "Працягнуць серыі",
|
||||||
|
"LabelDescription": "Апісанне",
|
||||||
|
"LabelDiscover": "Знайсці",
|
||||||
|
"LabelDownload": "Спампаваць",
|
||||||
|
"LabelDownloadNEpisodes": "Спампована {0} эпізодаў",
|
||||||
|
"LabelDownloadable": "Спампоўваецца",
|
||||||
|
"LabelDuration": "Працягласць",
|
||||||
|
"LabelEbook": "Электронная кніга",
|
||||||
|
"LabelEbooks": "Электронныя кнігі",
|
||||||
|
"LabelEnable": "Уключыць",
|
||||||
|
"LabelEncodingBackupLocation": "Рэзервовая копія вашых арыгінальных аўдыёфайлаў будзе захавана ў:",
|
||||||
|
"LabelEncodingChaptersNotEmbedded": "Раздзелы не ўбудаваны ў шматдарожкавыя аўдыякнігі.",
|
||||||
|
"LabelEncodingFinishedM4B": "Гатовы файл M4B будзе змешчаны ў вашу тэчку з аўдыякнігамі па адрасе:",
|
||||||
|
"LabelEncodingInfoEmbedded": "Метаданыя будуць убудаваны ў аўдыядарожкі ўнутры вашай тэчкі з аўдыякнігамі.",
|
||||||
|
"LabelEnd": "Канец",
|
||||||
|
"LabelEndOfChapter": "Канец раздзела",
|
||||||
|
"LabelEpisode": "Эпізод",
|
||||||
|
"LabelEpisodeNotLinkedToRssFeed": "Эпізод не звязаны з RSS-стужкай",
|
||||||
|
"LabelEpisodeUrlFromRssFeed": "URL эпізоду з RSS-стужкі",
|
||||||
|
"LabelFeedURL": "URL стужкі",
|
||||||
|
"LabelFile": "Файл",
|
||||||
|
"LabelFileBirthtime": "Час стварэння файла",
|
||||||
|
"LabelFileModified": "Час змянення файла",
|
||||||
|
"LabelFilename": "Імя файла",
|
||||||
|
"LabelFinished": "Скончана",
|
||||||
|
"LabelFolder": "Тэчка",
|
||||||
|
"LabelFontBoldness": "Таўшчыня шрыфта",
|
||||||
|
"LabelFontScale": "Памер шрыфту",
|
||||||
|
"LabelGenre": "Жанр",
|
||||||
|
"LabelGenres": "Жанры",
|
||||||
|
"LabelHasEbook": "Мае электронную кнігу",
|
||||||
|
"LabelHasSupplementaryEbook": "Мае дадатковую электронную кнігу",
|
||||||
|
"LabelHost": "Хост",
|
||||||
|
"LabelInProgress": "У працэсе",
|
||||||
|
"LabelIncomplete": "Незавершана",
|
||||||
|
"LabelLanguage": "Мова",
|
||||||
|
"LabelLayoutSinglePage": "Аднабаковы",
|
||||||
|
"LabelLineSpacing": "Міжрадковы інтэрвал",
|
||||||
|
"LabelListenAgain": "Паслухаць зноў",
|
||||||
|
"LabelMaxEpisodesToDownload": "Максімальная колькасць эпізодаў для спампоўкі. Выкарыстоўвайце 0 для неабмежаванай колькасці.",
|
||||||
|
"LabelMaxEpisodesToDownloadPerCheck": "Максімальная колькасць новых эпізодаў для спампоўкі за праверку",
|
||||||
|
"LabelMaxEpisodesToKeepHelp": "Значэнне 0 не ўстанаўлівае максімальнага абмежавання. Пасля аўтаматычнай спампоўкі новага эпізоду будзе выдалены самы стары эпізод, калі ў вас больш за X эпізодаў. Пры кожнай новай спампоўцы будзе выдаляцца толькі 1 эпізод.",
|
||||||
|
"LabelMediaPlayer": "Медыяплэер",
|
||||||
|
"LabelMediaType": "Тып медыя",
|
||||||
|
"LabelMissing": "Адсутнічае",
|
||||||
|
"LabelMore": "Больш",
|
||||||
|
"LabelMoreInfo": "Больш інфармацыі",
|
||||||
|
"LabelName": "Імя",
|
||||||
|
"LabelNarrator": "Чытальнік",
|
||||||
|
"LabelNarrators": "Чытальнікі",
|
||||||
|
"LabelOpenRSSFeed": "Адкрыць RSS-стужку",
|
||||||
|
"LabelPermissionsDownload": "Можна спампаваць",
|
||||||
|
"LabelPreventIndexing": "Прадухіліць індэксацыю вашай стужкі каталогамі падкастаў iTunes і Google",
|
||||||
|
"LabelRSSFeedCustomOwnerEmail": "Карыстальніцкая электронная пошта ўладальніка",
|
||||||
|
"LabelRSSFeedCustomOwnerName": "Карыстальніцкае імя ўладальніка",
|
||||||
|
"LabelRSSFeedOpen": "RSS-стужка адкрытая",
|
||||||
|
"LabelRSSFeedPreventIndexing": "Прадухіліць індэксацыю",
|
||||||
|
"LabelRSSFeedURL": "URL RSS-стужкі",
|
||||||
|
"LabelReAddSeriesToContinueListening": "Дадаць серыю зноў у Працягваць слухаць",
|
||||||
|
"LabelRecentSeries": "Апошнія серыі",
|
||||||
|
"LabelSeries": "Серыі",
|
||||||
|
"LabelSetEbookAsPrimary": "Зрабіць асноўным",
|
||||||
|
"LabelSetEbookAsSupplementary": "Зрабіць дадатковым",
|
||||||
|
"LabelSettingsExperimentalFeaturesHelp": "Функцыі ў распрацоўцы, для якіх вашы водгукі і дапамога ў тэставанні будуць карыснымі. Націсніце, каб адкрыць абмеркаванне на GitHub.",
|
||||||
|
"LabelSettingsLibraryMarkAsFinishedWhen": "Пазначыць элемент медыя як скончаны, калі",
|
||||||
|
"LabelShareDownloadableHelp": "Дазваляе карыстальнікам, якія маюць спасылку на доступ, спампаваць ZIP-файл элемента бібліятэкі.",
|
||||||
|
"LabelShowAll": "Паказаць усё",
|
||||||
|
"LabelSize": "Памер",
|
||||||
|
"LabelStatsAudioTracks": "Аўдыядарожкі",
|
||||||
|
"LabelTracks": "Дарожкі",
|
||||||
|
"MessageBookshelfNoRSSFeeds": "Няма адкрытых RSS-стужак",
|
||||||
|
"MessageConfirmCloseFeed": "Вы ўпэўнены, што жадаеце закрыць гэтую стужку?",
|
||||||
|
"MessageConfirmRemoveListeningSessions": "Вы ўпэўнены, што жадаеце выдаліць {0} сеансаў праслухоўвання?",
|
||||||
|
"MessageDownloadingEpisode": "Спампоўка эпізоду",
|
||||||
|
"MessageEpisodesQueuedForDownload": "{0} эпізод(аў) у чарзе для спампоўкі",
|
||||||
|
"MessageFeedURLWillBe": "URL стужкі будзе {0}",
|
||||||
|
"MessageNoChapters": "Няма раздзелаў",
|
||||||
|
"MessageNoDownloadsInProgress": "Зараз няма актыўных спамповак",
|
||||||
|
"MessageNoDownloadsQueued": "Няма спамповак у чарзе",
|
||||||
|
"MessageNoListeningSessions": "Няма сеансаў праслухоўвання",
|
||||||
|
"MessageNoMediaProgress": "Няма прагрэсу медыя",
|
||||||
|
"MessageNoPodcastFeed": "Няправільны падкаст: Няма стужкі",
|
||||||
|
"MessageOpmlPreviewNote": "Заўвага: гэта папярэдні прагляд разабранага OPML-файла. Фактычная назва падкаста будзе ўзятая з RSS-стужкі.",
|
||||||
|
"MessagePodcastHasNoRSSFeedForMatching": "У падкаста няма URL RSS-стужкі для супадзення",
|
||||||
|
"MessagePodcastSearchField": "Увядзіце пошукавы запыт або URL RSS-стужкі",
|
||||||
|
"MessageTaskDownloadingEpisodeDescription": "Спампоўка эпізоду \"{0}\"",
|
||||||
|
"MessageTaskOpmlImportDescription": "Стварэнне падкастаў з {0} RSS-стужак",
|
||||||
|
"MessageTaskOpmlImportFeed": "Імпарт стужкі з OPML",
|
||||||
|
"MessageTaskOpmlImportFeedDescription": "Імпартаванне RSS-стужкі \"{0}\"",
|
||||||
|
"MessageTaskOpmlImportFeedFailed": "Не ўдалося атрымаць стужку падкаста",
|
||||||
|
"MessageTaskOpmlImportFeedPodcastDescription": "Стварэнне падкаста \"{0}\"",
|
||||||
|
"MessageTaskOpmlImportFeedPodcastExists": "Падкаст ужо існуе па гэтым шляху",
|
||||||
|
"MessageTaskOpmlImportFeedPodcastFailed": "Не ўдалося стварыць падкаст",
|
||||||
|
"MessageTaskOpmlParseNoneFound": "У OPML-файле не знойдзена стужак",
|
||||||
|
"NoteRSSFeedPodcastAppsHttps": "Папярэджанне: большасць праграм для падкастаў патрабуюць, каб URL RSS-стужкі выкарыстоўваў HTTPS",
|
||||||
|
"NoteRSSFeedPodcastAppsPubDate": "Папярэджанне: адзін ці больш вашых эпізодаў не маюць даты публікацыі. Некаторыя праграмы для падкастаў патрабуюць гэтага.",
|
||||||
|
"NoteUploaderFoldersWithMediaFiles": "Тэчкі з медыяфайламі будуць апрацоўвацца як асобныя элементы бібліятэкі.",
|
||||||
|
"NotificationOnEpisodeDownloadedDescription": "Выклікаецца, калі эпізод падкаста аўтаматычна спампоўваецца",
|
||||||
|
"ToastAccountUpdateSuccess": "Уліковы запіс абноўлены",
|
||||||
|
"ToastEpisodeDownloadQueueClearFailed": "Не ўдалося ачысціць чаргу",
|
||||||
|
"ToastEpisodeDownloadQueueClearSuccess": "Чарга спампоўкі эпізодаў ачышчана",
|
||||||
|
"ToastInvalidMaxEpisodesToDownload": "Няправільная максімальная колькасць эпізодаў для спампоўкі",
|
||||||
|
"ToastItemMarkedAsFinishedFailed": "Не ўдалося пазначыць як Скончана",
|
||||||
|
"ToastItemMarkedAsFinishedSuccess": "Элемент пазначаны як Завершаны",
|
||||||
|
"ToastItemMarkedAsNotFinishedFailed": "Не ўдалося пазначыць як Незавершанае",
|
||||||
|
"ToastItemMarkedAsNotFinishedSuccess": "Элемент пазначаны як Незавершаны",
|
||||||
|
"ToastItemUpdateSuccess": "Элемент абноўлены",
|
||||||
|
"ToastLibraryCreateFailed": "Не ўдалося стварыць бібліятэку",
|
||||||
|
"ToastLibraryCreateSuccess": "Бібліятэка \"{0}\" створана",
|
||||||
|
"ToastLibraryDeleteFailed": "Не ўдалося выдаліць бібліятэку",
|
||||||
|
"ToastLibraryDeleteSuccess": "Бібліятэка выдалена",
|
||||||
|
"ToastLibraryScanFailedToStart": "Не ўдалося запусціць сканаванне",
|
||||||
|
"ToastLibraryScanStarted": "Сканаванне бібліятэкі запушчана",
|
||||||
|
"ToastLibraryUpdateSuccess": "Бібліятэка \"{0}\" абноўлена",
|
||||||
|
"ToastMatchAllAuthorsFailed": "Не ўдалося знайсці адпаведнасць для ўсіх аўтараў",
|
||||||
|
"ToastMetadataFilesRemovedError": "Памылка пры выдаленні metadata.{0} файлаў",
|
||||||
|
"ToastMetadataFilesRemovedNoneFound": "У бібліятэцы не знойдзены metadata.{0} файлаў",
|
||||||
|
"ToastMetadataFilesRemovedNoneRemoved": "Не выдалена metadata.{0} файлаў",
|
||||||
|
"ToastMetadataFilesRemovedSuccess": "{0} metadata.{1} файлаў выдалена",
|
||||||
|
"ToastMustHaveAtLeastOnePath": "Павінен быць хаця б адзін шлях",
|
||||||
|
"ToastNameEmailRequired": "Імя і электронная пошта абавязковыя",
|
||||||
|
"ToastNameRequired": "Імя абавязковае",
|
||||||
|
"ToastNewUserCreatedFailed": "Не ўдалося стварыць уліковы запіс: \"{0}\"",
|
||||||
|
"ToastNewUserCreatedSuccess": "Новы ўліковы запіс створаны",
|
||||||
|
"ToastNoRSSFeed": "У падкаста няма RSS-стужкі",
|
||||||
|
"ToastPodcastGetFeedFailed": "Не ўдалося атрымаць стужку падкаста",
|
||||||
|
"ToastPodcastNoEpisodesInFeed": "У RSS-стужцы не знойдзена эпізодаў",
|
||||||
|
"ToastPodcastNoRssFeed": "У падкаста няма RSS-стужкі",
|
||||||
|
"ToastRSSFeedCloseFailed": "Не ўдалося закрыць RSS-стужку",
|
||||||
|
"ToastRSSFeedCloseSuccess": "RSS-стужка закрыта",
|
||||||
|
"ToastUserPasswordMustChange": "Новы пароль не можа супадаць са старым",
|
||||||
|
"ToastUserRootRequireName": "Неабходна ўвесці імя карыстальніка адміністратара"
|
||||||
}
|
}
|
||||||
|
|||||||
+186
-108
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"ButtonAdd": "Добави",
|
"ButtonAdd": "Създай",
|
||||||
"ButtonAddChapters": "Добави Глави",
|
"ButtonAddChapters": "Добави Глави",
|
||||||
"ButtonAddDevice": "Добави Устройство",
|
"ButtonAddDevice": "Добави Устройство",
|
||||||
"ButtonAddLibrary": "Добави Библиотека",
|
"ButtonAddLibrary": "Добави Библиотека",
|
||||||
@@ -10,15 +10,18 @@
|
|||||||
"ButtonApplyChapters": "Приложи Глави",
|
"ButtonApplyChapters": "Приложи Глави",
|
||||||
"ButtonAuthors": "Автори",
|
"ButtonAuthors": "Автори",
|
||||||
"ButtonBack": "Назад",
|
"ButtonBack": "Назад",
|
||||||
|
"ButtonBatchEditPopulateFromExisting": "Попълни от съществуващи",
|
||||||
|
"ButtonBatchEditPopulateMapDetails": "Попълни подробности за картата",
|
||||||
"ButtonBrowseForFolder": "Прегледай за папка",
|
"ButtonBrowseForFolder": "Прегледай за папка",
|
||||||
"ButtonCancel": "Откажи",
|
"ButtonCancel": "Отказ",
|
||||||
"ButtonCancelEncode": "Откажи закодирането",
|
"ButtonCancelEncode": "Откажи закодирането",
|
||||||
"ButtonChangeRootPassword": "Промени паролата за Root",
|
"ButtonChangeRootPassword": "Промени паролата за Root",
|
||||||
"ButtonCheckAndDownloadNewEpisodes": "Провери и Свали Нови Епизоди",
|
"ButtonCheckAndDownloadNewEpisodes": "Провери и Свали Нови Епизоди",
|
||||||
"ButtonChooseAFolder": "Избери Папка",
|
"ButtonChooseAFolder": "Избери Папка",
|
||||||
"ButtonChooseFiles": "Избери Файлове",
|
"ButtonChooseFiles": "Избери Файлове",
|
||||||
"ButtonClearFilter": "Изчисти Филтър",
|
"ButtonClearFilter": "Изчисти филтър",
|
||||||
"ButtonCloseFeed": "Затвори Feed",
|
"ButtonCloseFeed": "Затвори стената",
|
||||||
|
"ButtonCloseSession": "Затвори отворената сесия",
|
||||||
"ButtonCollections": "Колекции",
|
"ButtonCollections": "Колекции",
|
||||||
"ButtonConfigureScanner": "Конфигурирай Скенера",
|
"ButtonConfigureScanner": "Конфигурирай Скенера",
|
||||||
"ButtonCreate": "Създай",
|
"ButtonCreate": "Създай",
|
||||||
@@ -28,6 +31,9 @@
|
|||||||
"ButtonEdit": "Редактирай",
|
"ButtonEdit": "Редактирай",
|
||||||
"ButtonEditChapters": "Редактирай Глави",
|
"ButtonEditChapters": "Редактирай Глави",
|
||||||
"ButtonEditPodcast": "Редактирай Подкаст",
|
"ButtonEditPodcast": "Редактирай Подкаст",
|
||||||
|
"ButtonEnable": "Активирай",
|
||||||
|
"ButtonFireAndFail": "Задействай и неуспей",
|
||||||
|
"ButtonFireOnTest": "Задействай събитие onTest",
|
||||||
"ButtonForceReScan": "Принудително Пресканиране",
|
"ButtonForceReScan": "Принудително Пресканиране",
|
||||||
"ButtonFullPath": "Пълен Път",
|
"ButtonFullPath": "Пълен Път",
|
||||||
"ButtonHide": "Скрий",
|
"ButtonHide": "Скрий",
|
||||||
@@ -44,24 +50,31 @@
|
|||||||
"ButtonMatchAllAuthors": "Съвпадение на Всички Автори",
|
"ButtonMatchAllAuthors": "Съвпадение на Всички Автори",
|
||||||
"ButtonMatchBooks": "Съвпадение на Книги",
|
"ButtonMatchBooks": "Съвпадение на Книги",
|
||||||
"ButtonNevermind": "Няма значение",
|
"ButtonNevermind": "Няма значение",
|
||||||
|
"ButtonNext": "Следващо",
|
||||||
"ButtonNextChapter": "Следваща Глава",
|
"ButtonNextChapter": "Следваща Глава",
|
||||||
"ButtonOk": "Добре",
|
"ButtonNextItemInQueue": "Следващият елемент в опашката",
|
||||||
"ButtonOpenFeed": "Отвори Feed",
|
"ButtonOk": "Приемам",
|
||||||
|
"ButtonOpenFeed": "Отвори стената",
|
||||||
"ButtonOpenManager": "Отвори Мениджър",
|
"ButtonOpenManager": "Отвори Мениджър",
|
||||||
"ButtonPause": "Пауза",
|
"ButtonPause": "Паузирай",
|
||||||
"ButtonPlay": "Пусни",
|
"ButtonPlay": "Пусни",
|
||||||
|
"ButtonPlayAll": "Пусни всички",
|
||||||
"ButtonPlaying": "Пуска се",
|
"ButtonPlaying": "Пуска се",
|
||||||
"ButtonPlaylists": "Плейлисти",
|
"ButtonPlaylists": "Плейлисти",
|
||||||
|
"ButtonPrevious": "Предишен",
|
||||||
"ButtonPreviousChapter": "Предишна Глава",
|
"ButtonPreviousChapter": "Предишна Глава",
|
||||||
|
"ButtonProbeAudioFile": "Провери аудио файла",
|
||||||
"ButtonPurgeAllCache": "Изчисти Всички Кешове",
|
"ButtonPurgeAllCache": "Изчисти Всички Кешове",
|
||||||
"ButtonPurgeItemsCache": "Изчисти Кеша на Елементи",
|
"ButtonPurgeItemsCache": "Изчисти Кеша на Елементи",
|
||||||
"ButtonQueueAddItem": "Добави към опашката",
|
"ButtonQueueAddItem": "Добави към опашката",
|
||||||
"ButtonQueueRemoveItem": "Премахни от опашката",
|
"ButtonQueueRemoveItem": "Премахни от опашката",
|
||||||
|
"ButtonQuickEmbed": "Бързо вграждане",
|
||||||
|
"ButtonQuickEmbedMetadata": "Бързо вграждане метадата",
|
||||||
"ButtonQuickMatch": "Бързо Съпоставяне",
|
"ButtonQuickMatch": "Бързо Съпоставяне",
|
||||||
"ButtonReScan": "Пресканирай",
|
"ButtonReScan": "Пресканирай",
|
||||||
"ButtonRead": "Прочети",
|
"ButtonRead": "Прочети",
|
||||||
"ButtonReadLess": "Покажи по-малко",
|
"ButtonReadLess": "Изчети по-малко",
|
||||||
"ButtonReadMore": "Покажи повече",
|
"ButtonReadMore": "Прочети дълго",
|
||||||
"ButtonRefresh": "Обнови",
|
"ButtonRefresh": "Обнови",
|
||||||
"ButtonRemove": "Премахни",
|
"ButtonRemove": "Премахни",
|
||||||
"ButtonRemoveAll": "Премахни Всички",
|
"ButtonRemoveAll": "Премахни Всички",
|
||||||
@@ -77,7 +90,9 @@
|
|||||||
"ButtonSaveTracklist": "Запази Списък с Канали",
|
"ButtonSaveTracklist": "Запази Списък с Канали",
|
||||||
"ButtonScan": "Сканирай",
|
"ButtonScan": "Сканирай",
|
||||||
"ButtonScanLibrary": "Сканирай Библиотека",
|
"ButtonScanLibrary": "Сканирай Библиотека",
|
||||||
"ButtonSearch": "Търси",
|
"ButtonScrollLeft": "Скролни наляво",
|
||||||
|
"ButtonScrollRight": "Скролни надясно",
|
||||||
|
"ButtonSearch": "Търси в",
|
||||||
"ButtonSelectFolderPath": "Избери Път на Папка",
|
"ButtonSelectFolderPath": "Избери Път на Папка",
|
||||||
"ButtonSeries": "Серии",
|
"ButtonSeries": "Серии",
|
||||||
"ButtonSetChaptersFromTracks": "Задай Глави от Песни",
|
"ButtonSetChaptersFromTracks": "Задай Глави от Песни",
|
||||||
@@ -86,8 +101,10 @@
|
|||||||
"ButtonShow": "Покажи",
|
"ButtonShow": "Покажи",
|
||||||
"ButtonStartM4BEncode": "Започни M4B Кодиране",
|
"ButtonStartM4BEncode": "Започни M4B Кодиране",
|
||||||
"ButtonStartMetadataEmbed": "Започни Вграждане на Метаданни",
|
"ButtonStartMetadataEmbed": "Започни Вграждане на Метаданни",
|
||||||
|
"ButtonStats": "Статистики",
|
||||||
"ButtonSubmit": "Изпрати",
|
"ButtonSubmit": "Изпрати",
|
||||||
"ButtonTest": "Тест",
|
"ButtonTest": "Тест",
|
||||||
|
"ButtonUnlinkOpenId": "Премахни връзката с OpenID",
|
||||||
"ButtonUpload": "Качи",
|
"ButtonUpload": "Качи",
|
||||||
"ButtonUploadBackup": "Качи Backup",
|
"ButtonUploadBackup": "Качи Backup",
|
||||||
"ButtonUploadCover": "Качи Корица",
|
"ButtonUploadCover": "Качи Корица",
|
||||||
@@ -100,9 +117,10 @@
|
|||||||
"ErrorUploadFetchMetadataNoResults": "Метаданните не могат да бъдат взети - опитайте да обновите заглавието и/или автора",
|
"ErrorUploadFetchMetadataNoResults": "Метаданните не могат да бъдат взети - опитайте да обновите заглавието и/или автора",
|
||||||
"ErrorUploadLacksTitle": "Трябва да има Заглавие",
|
"ErrorUploadLacksTitle": "Трябва да има Заглавие",
|
||||||
"HeaderAccount": "Профил",
|
"HeaderAccount": "Профил",
|
||||||
"HeaderAdvanced": "Разширени",
|
"HeaderAddCustomMetadataProvider": "Добави персонализиран доставчик на метаданни",
|
||||||
|
"HeaderAdvanced": "Разширени настройки",
|
||||||
"HeaderAppriseNotificationSettings": "Apprise Notification Опции",
|
"HeaderAppriseNotificationSettings": "Apprise Notification Опции",
|
||||||
"HeaderAudioTracks": "Звуков Канал",
|
"HeaderAudioTracks": "Песни",
|
||||||
"HeaderAudiobookTools": "Инструмент за Менижиране на Аудиокниги",
|
"HeaderAudiobookTools": "Инструмент за Менижиране на Аудиокниги",
|
||||||
"HeaderAuthentication": "Аутентикация",
|
"HeaderAuthentication": "Аутентикация",
|
||||||
"HeaderBackups": "Архив",
|
"HeaderBackups": "Архив",
|
||||||
@@ -110,26 +128,26 @@
|
|||||||
"HeaderChapters": "Глави",
|
"HeaderChapters": "Глави",
|
||||||
"HeaderChooseAFolder": "Избети Папка",
|
"HeaderChooseAFolder": "Избети Папка",
|
||||||
"HeaderCollection": "Колекция",
|
"HeaderCollection": "Колекция",
|
||||||
"HeaderCollectionItems": "Елементи на Колекция",
|
"HeaderCollectionItems": "Елемент в колекция",
|
||||||
"HeaderCover": "Корица",
|
"HeaderCover": "Корица",
|
||||||
"HeaderCurrentDownloads": "Текущи Сваляния",
|
"HeaderCurrentDownloads": "Текущи Сваляния",
|
||||||
"HeaderCustomMessageOnLogin": "Потребителско съобщение при влизане",
|
"HeaderCustomMessageOnLogin": "Потребителско съобщение при влизане",
|
||||||
"HeaderCustomMetadataProviders": "Потребителски Доставчици на Метаданни",
|
"HeaderCustomMetadataProviders": "Потребителски Доставчици на Метаданни",
|
||||||
"HeaderDetails": "Детайли",
|
"HeaderDetails": "Детайли",
|
||||||
"HeaderDownloadQueue": "Опашка за Сваляне",
|
"HeaderDownloadQueue": "Опашка за Сваляне",
|
||||||
"HeaderEbookFiles": "Файлове на Електронни книги",
|
"HeaderEbookFiles": "Е-книги файлове",
|
||||||
"HeaderEmail": "Емейл",
|
"HeaderEmail": "Емейл",
|
||||||
"HeaderEmailSettings": "Настройки Емайл",
|
"HeaderEmailSettings": "Настройки Емайл",
|
||||||
"HeaderEpisodes": "Епизоди",
|
"HeaderEpisodes": "Епизоди",
|
||||||
"HeaderEreaderDevices": "Елктронни Четци",
|
"HeaderEreaderDevices": "Елктронни Четци",
|
||||||
"HeaderEreaderSettings": "Настройки на Електронни Четци",
|
"HeaderEreaderSettings": "Настройки на Е-четецът",
|
||||||
"HeaderFiles": "Файлове",
|
"HeaderFiles": "Файлове",
|
||||||
"HeaderFindChapters": "Намери Глави",
|
"HeaderFindChapters": "Намери Глави",
|
||||||
"HeaderIgnoredFiles": "Игнорирани Файлове",
|
"HeaderIgnoredFiles": "Игнорирани Файлове",
|
||||||
"HeaderItemFiles": "Файлове на Елемент",
|
"HeaderItemFiles": "Файлове на Елемент",
|
||||||
"HeaderItemMetadataUtils": "Инструменти за Метаданни на Елемент",
|
"HeaderItemMetadataUtils": "Инструменти за Метаданни на Елемент",
|
||||||
"HeaderLastListeningSession": "Последна Сесия на Слушане",
|
"HeaderLastListeningSession": "Последна Сесия на Слушане",
|
||||||
"HeaderLatestEpisodes": "Последни Епизоди",
|
"HeaderLatestEpisodes": "Последни епизоди",
|
||||||
"HeaderLibraries": "Библиотеки",
|
"HeaderLibraries": "Библиотеки",
|
||||||
"HeaderLibraryFiles": "Файлове на Библиотека",
|
"HeaderLibraryFiles": "Файлове на Библиотека",
|
||||||
"HeaderLibraryStats": "Статистика на Библиотека",
|
"HeaderLibraryStats": "Статистика на Библиотека",
|
||||||
@@ -145,24 +163,29 @@
|
|||||||
"HeaderMetadataToEmbed": "Метаданни за Вграждане",
|
"HeaderMetadataToEmbed": "Метаданни за Вграждане",
|
||||||
"HeaderNewAccount": "Нов Профил",
|
"HeaderNewAccount": "Нов Профил",
|
||||||
"HeaderNewLibrary": "Нова Библиотека",
|
"HeaderNewLibrary": "Нова Библиотека",
|
||||||
|
"HeaderNotificationCreate": "Създай нотификация",
|
||||||
|
"HeaderNotificationUpdate": "Обнови нотификация",
|
||||||
"HeaderNotifications": "Известия",
|
"HeaderNotifications": "Известия",
|
||||||
"HeaderOpenIDConnectAuthentication": "OpenID Connect Аутентикация",
|
"HeaderOpenIDConnectAuthentication": "OpenID Connect Аутентикация",
|
||||||
"HeaderOpenRSSFeed": "Отвори RSS Feed",
|
"HeaderOpenListeningSessions": "Отвори сесия",
|
||||||
|
"HeaderOpenRSSFeed": "Отвори RSS емисията",
|
||||||
"HeaderOtherFiles": "Други Файлове",
|
"HeaderOtherFiles": "Други Файлове",
|
||||||
"HeaderPasswordAuthentication": "Паролна Аутентикация",
|
"HeaderPasswordAuthentication": "Паролна Аутентикация",
|
||||||
"HeaderPermissions": "Права",
|
"HeaderPermissions": "Права",
|
||||||
"HeaderPlayerQueue": "Опашка на Плейъра",
|
"HeaderPlayerQueue": "Опашка на Плейъра",
|
||||||
|
"HeaderPlayerSettings": "Настройки на плейъра",
|
||||||
"HeaderPlaylist": "Плейлист",
|
"HeaderPlaylist": "Плейлист",
|
||||||
"HeaderPlaylistItems": "Елементи на Плейлист",
|
"HeaderPlaylistItems": "Елементи от плейлист",
|
||||||
"HeaderPodcastsToAdd": "Подкасти за Добавяне",
|
"HeaderPodcastsToAdd": "Подкасти за Добавяне",
|
||||||
"HeaderPreviewCover": "Преглед на Корица",
|
"HeaderPreviewCover": "Преглед на Корица",
|
||||||
"HeaderRSSFeedGeneral": "RSS Детайли",
|
"HeaderRSSFeedGeneral": "RSS подробности",
|
||||||
"HeaderRSSFeedIsOpen": "RSS Feed е Отворен",
|
"HeaderRSSFeedIsOpen": "RSS емисията е отворена",
|
||||||
"HeaderRSSFeeds": "RSS Feed-ове",
|
"HeaderRSSFeeds": "RSS Feed-ове",
|
||||||
"HeaderRemoveEpisode": "Премахни Епизод",
|
"HeaderRemoveEpisode": "Премахни Епизод",
|
||||||
"HeaderRemoveEpisodes": "Премахни {0} Епизоди",
|
"HeaderRemoveEpisodes": "Премахни {0} Епизоди",
|
||||||
"HeaderSavedMediaProgress": "Запазен Прогрес на Медията",
|
"HeaderSavedMediaProgress": "Запазен Прогрес на Медията",
|
||||||
"HeaderSchedule": "График",
|
"HeaderSchedule": "График",
|
||||||
|
"HeaderScheduleEpisodeDownloads": "Планирай автоматично изтегляне на епизоди",
|
||||||
"HeaderScheduleLibraryScans": "График за Автоматично Сканиране на Библиотека",
|
"HeaderScheduleLibraryScans": "График за Автоматично Сканиране на Библиотека",
|
||||||
"HeaderSession": "Сесия",
|
"HeaderSession": "Сесия",
|
||||||
"HeaderSetBackupSchedule": "Задай График за Backup",
|
"HeaderSetBackupSchedule": "Задай График за Backup",
|
||||||
@@ -171,11 +194,12 @@
|
|||||||
"HeaderSettingsExperimental": "Експериментални Функции",
|
"HeaderSettingsExperimental": "Експериментални Функции",
|
||||||
"HeaderSettingsGeneral": "Общи",
|
"HeaderSettingsGeneral": "Общи",
|
||||||
"HeaderSettingsScanner": "Скенер",
|
"HeaderSettingsScanner": "Скенер",
|
||||||
"HeaderSleepTimer": "Таймер за Сън",
|
"HeaderSettingsWebClient": "Уеб клиент",
|
||||||
|
"HeaderSleepTimer": "Таймер за заспиване",
|
||||||
"HeaderStatsLargestItems": "Най-Големите Елементи",
|
"HeaderStatsLargestItems": "Най-Големите Елементи",
|
||||||
"HeaderStatsLongestItems": "Най-Дългите Елементи (часове)",
|
"HeaderStatsLongestItems": "Най-Дългите Елементи (часове)",
|
||||||
"HeaderStatsMinutesListeningChart": "Минути на Слушане (последни 7 дни)",
|
"HeaderStatsMinutesListeningChart": "Изслушани минути (последните 7 дни)",
|
||||||
"HeaderStatsRecentSessions": "Скорошни Сесии",
|
"HeaderStatsRecentSessions": "Последни сесии",
|
||||||
"HeaderStatsTop10Authors": "Топ 10 Автори",
|
"HeaderStatsTop10Authors": "Топ 10 Автори",
|
||||||
"HeaderStatsTop5Genres": "Топ 5 Жанрове",
|
"HeaderStatsTop5Genres": "Топ 5 Жанрове",
|
||||||
"HeaderTableOfContents": "Съдържание",
|
"HeaderTableOfContents": "Съдържание",
|
||||||
@@ -186,7 +210,7 @@
|
|||||||
"HeaderUpdateLibrary": "Обнови Библиотека",
|
"HeaderUpdateLibrary": "Обнови Библиотека",
|
||||||
"HeaderUsers": "Потребители",
|
"HeaderUsers": "Потребители",
|
||||||
"HeaderYearReview": "Преглед на {0} Година",
|
"HeaderYearReview": "Преглед на {0} Година",
|
||||||
"HeaderYourStats": "Твоята Статистика",
|
"HeaderYourStats": "Вашата статистика",
|
||||||
"LabelAbridged": "Съкратен",
|
"LabelAbridged": "Съкратен",
|
||||||
"LabelAbridgedChecked": "Съкратена (отбелязано)",
|
"LabelAbridgedChecked": "Съкратена (отбелязано)",
|
||||||
"LabelAbridgedUnchecked": "Несъкратена (не отбелязано)",
|
"LabelAbridgedUnchecked": "Несъкратена (не отбелязано)",
|
||||||
@@ -198,21 +222,26 @@
|
|||||||
"LabelActivity": "Дейност",
|
"LabelActivity": "Дейност",
|
||||||
"LabelAddToCollection": "Добави в Колекция",
|
"LabelAddToCollection": "Добави в Колекция",
|
||||||
"LabelAddToCollectionBatch": "Добави {0} Книги в Колекция",
|
"LabelAddToCollectionBatch": "Добави {0} Книги в Колекция",
|
||||||
"LabelAddToPlaylist": "Добави в Плейлист",
|
"LabelAddToPlaylist": "Добави в плейлист",
|
||||||
"LabelAddToPlaylistBatch": "Добави {0} Елемент в Плейлист",
|
"LabelAddToPlaylistBatch": "Добави {0} Елемент в Плейлист",
|
||||||
"LabelAddedAt": "Добавени На",
|
"LabelAddedAt": "Добавено в",
|
||||||
|
"LabelAddedDate": "Добавено",
|
||||||
"LabelAdminUsersOnly": "Само за Администратори",
|
"LabelAdminUsersOnly": "Само за Администратори",
|
||||||
"LabelAll": "Всички",
|
"LabelAll": "Всичко",
|
||||||
"LabelAllUsers": "Всички Потребители",
|
"LabelAllUsers": "Всички Потребители",
|
||||||
"LabelAllUsersExcludingGuests": "Всички потребители без гости",
|
"LabelAllUsersExcludingGuests": "Всички потребители без гости",
|
||||||
"LabelAllUsersIncludingGuests": "Всички потребители включително гости",
|
"LabelAllUsersIncludingGuests": "Всички потребители включително гости",
|
||||||
"LabelAlreadyInYourLibrary": "Вече е в твоята библиотека",
|
"LabelAlreadyInYourLibrary": "Вече е в твоята библиотека",
|
||||||
|
"LabelApiToken": "АПИ Токен",
|
||||||
"LabelAppend": "Добави",
|
"LabelAppend": "Добави",
|
||||||
|
"LabelAudioBitrate": "Аудио битрейт (напр. 128k)",
|
||||||
|
"LabelAudioChannels": "Аудио канали (1 или 2)",
|
||||||
|
"LabelAudioCodec": "Аудио кодек",
|
||||||
"LabelAuthor": "Автор",
|
"LabelAuthor": "Автор",
|
||||||
"LabelAuthorFirstLast": "Автор (Първо Име, Фамилия)",
|
"LabelAuthorFirstLast": "Автор (Първи, Последен)",
|
||||||
"LabelAuthorLastFirst": "Автор (Фамилия, Първо Име)",
|
"LabelAuthorLastFirst": "Автор (Последен, Първи)",
|
||||||
"LabelAuthors": "Автори",
|
"LabelAuthors": "Автори",
|
||||||
"LabelAutoDownloadEpisodes": "Автоматично Сваляне на Епизоди",
|
"LabelAutoDownloadEpisodes": "Автоматично изтегляне на епизоди",
|
||||||
"LabelAutoFetchMetadata": "Автоматично Взимане на Метаданни",
|
"LabelAutoFetchMetadata": "Автоматично Взимане на Метаданни",
|
||||||
"LabelAutoFetchMetadataHelp": "Взима метаданни за заглвие, автор и серии за да опрости качването. Допълнителни метаданни може да трябва да бъде взера след качване.",
|
"LabelAutoFetchMetadataHelp": "Взима метаданни за заглвие, автор и серии за да опрости качването. Допълнителни метаданни може да трябва да бъде взера след качване.",
|
||||||
"LabelAutoLaunch": "Автоматично Стартиране",
|
"LabelAutoLaunch": "Автоматично Стартиране",
|
||||||
@@ -220,6 +249,7 @@
|
|||||||
"LabelAutoRegister": "Автоматична Регистрация",
|
"LabelAutoRegister": "Автоматична Регистрация",
|
||||||
"LabelAutoRegisterDescription": "Автоматично създаване на нови потребители след вход",
|
"LabelAutoRegisterDescription": "Автоматично създаване на нови потребители след вход",
|
||||||
"LabelBackToUser": "Обратно към Потребител",
|
"LabelBackToUser": "Обратно към Потребител",
|
||||||
|
"LabelBackupAudioFiles": "Създай резервно копие на аудио файлове",
|
||||||
"LabelBackupLocation": "Местоположение на Архив",
|
"LabelBackupLocation": "Местоположение на Архив",
|
||||||
"LabelBackupsEnableAutomaticBackups": "Включи автоматично архивиране",
|
"LabelBackupsEnableAutomaticBackups": "Включи автоматично архивиране",
|
||||||
"LabelBackupsEnableAutomaticBackupsHelp": "Архиви запазени в /metadata/backups",
|
"LabelBackupsEnableAutomaticBackupsHelp": "Архиви запазени в /metadata/backups",
|
||||||
@@ -228,31 +258,38 @@
|
|||||||
"LabelBackupsNumberToKeep": "Брой архиви за запазване",
|
"LabelBackupsNumberToKeep": "Брой архиви за запазване",
|
||||||
"LabelBackupsNumberToKeepHelp": "Само 1 архив ще бъде премахнат на веднъж, така че ако вече имате повече архиви от това трябва да ги премахнете ръчно.",
|
"LabelBackupsNumberToKeepHelp": "Само 1 архив ще бъде премахнат на веднъж, така че ако вече имате повече архиви от това трябва да ги премахнете ръчно.",
|
||||||
"LabelBitrate": "Битрейт",
|
"LabelBitrate": "Битрейт",
|
||||||
|
"LabelBonus": "Бонус",
|
||||||
"LabelBooks": "Книги",
|
"LabelBooks": "Книги",
|
||||||
"LabelButtonText": "Текст на Бутон",
|
"LabelButtonText": "Текст на Бутон",
|
||||||
|
"LabelByAuthor": "от {0}",
|
||||||
"LabelChangePassword": "Промени Парола",
|
"LabelChangePassword": "Промени Парола",
|
||||||
"LabelChannels": "Канали",
|
"LabelChannels": "Канали",
|
||||||
|
"LabelChapterCount": "{0} Глави",
|
||||||
"LabelChapterTitle": "Заглавие на Глава",
|
"LabelChapterTitle": "Заглавие на Глава",
|
||||||
"LabelChapters": "Глави",
|
"LabelChapters": "Глави",
|
||||||
"LabelChaptersFound": "намерени глави",
|
"LabelChaptersFound": "намерени глави",
|
||||||
"LabelClickForMoreInfo": "Кликни за повече информация",
|
"LabelClickForMoreInfo": "Кликни за повече информация",
|
||||||
"LabelClosePlayer": "Затвори Плейъра",
|
"LabelClickToUseCurrentValue": "Натисни да ползваш сегашната стойност",
|
||||||
|
"LabelClosePlayer": "Затвори",
|
||||||
"LabelCodec": "Кодек",
|
"LabelCodec": "Кодек",
|
||||||
"LabelCollapseSeries": "Свий Серия",
|
"LabelCollapseSeries": "Скрий сериите",
|
||||||
|
"LabelCollapseSubSeries": "Свий подсерии",
|
||||||
"LabelCollection": "Колекция",
|
"LabelCollection": "Колекция",
|
||||||
"LabelCollections": "Колекции",
|
"LabelCollections": "Колекции",
|
||||||
"LabelComplete": "Завършено",
|
"LabelComplete": "Приключено",
|
||||||
"LabelConfirmPassword": "Потвърди Парола",
|
"LabelConfirmPassword": "Потвърди Парола",
|
||||||
"LabelContinueListening": "Продължи Слушане",
|
"LabelContinueListening": "Продължи слушане",
|
||||||
"LabelContinueReading": "Продължи Четене",
|
"LabelContinueReading": "Продължи четене",
|
||||||
"LabelContinueSeries": "Продължи Серия",
|
"LabelContinueSeries": "Продължи серии",
|
||||||
"LabelCover": "Корица",
|
"LabelCover": "Корица",
|
||||||
"LabelCoverImageURL": "URL на Корица",
|
"LabelCoverImageURL": "URL на Корица",
|
||||||
"LabelCreatedAt": "Създадено на",
|
"LabelCreatedAt": "Създадено на",
|
||||||
|
"LabelCronExpression": "Cron израз",
|
||||||
"LabelCurrent": "Текущо",
|
"LabelCurrent": "Текущо",
|
||||||
"LabelCurrently": "Текущо:",
|
"LabelCurrently": "Текущо:",
|
||||||
"LabelCustomCronExpression": "Потребителски Cron Expression:",
|
"LabelCustomCronExpression": "Потребителски Cron Expression:",
|
||||||
"LabelDatetime": "Дата и Време",
|
"LabelDatetime": "Дата и Време",
|
||||||
|
"LabelDays": "Дни",
|
||||||
"LabelDeleteFromFileSystemCheckbox": "Изтрий от файловата система (отмени за да бъдат премахни само от базата данни)",
|
"LabelDeleteFromFileSystemCheckbox": "Изтрий от файловата система (отмени за да бъдат премахни само от базата данни)",
|
||||||
"LabelDescription": "Описание",
|
"LabelDescription": "Описание",
|
||||||
"LabelDeselectAll": "Премахни всички",
|
"LabelDeselectAll": "Премахни всички",
|
||||||
@@ -263,16 +300,18 @@
|
|||||||
"LabelDiscFromFilename": "Диск от Име на Файл",
|
"LabelDiscFromFilename": "Диск от Име на Файл",
|
||||||
"LabelDiscFromMetadata": "Диск от Метаданни",
|
"LabelDiscFromMetadata": "Диск от Метаданни",
|
||||||
"LabelDiscover": "Открий",
|
"LabelDiscover": "Открий",
|
||||||
"LabelDownload": "Сваляне",
|
"LabelDownload": "Свали",
|
||||||
"LabelDownloadNEpisodes": "Свали {0} епизоди",
|
"LabelDownloadNEpisodes": "Свали {0} епизоди",
|
||||||
|
"LabelDownloadable": "Може да се изтегли",
|
||||||
"LabelDuration": "Продължителност",
|
"LabelDuration": "Продължителност",
|
||||||
"LabelDurationComparisonExactMatch": "(точно съвпадение)",
|
"LabelDurationComparisonExactMatch": "(точно съвпадение)",
|
||||||
"LabelDurationComparisonLonger": "({0} по-дълго)",
|
"LabelDurationComparisonLonger": "({0} по-дълго)",
|
||||||
"LabelDurationComparisonShorter": "({0} по-късо)",
|
"LabelDurationComparisonShorter": "({0} по-късо)",
|
||||||
"LabelDurationFound": "Намерена продължителност:",
|
"LabelDurationFound": "Намерена продължителност:",
|
||||||
"LabelEbook": "Електронна книга",
|
"LabelEbook": "Е-Книга",
|
||||||
"LabelEbooks": "Електронни книги",
|
"LabelEbooks": "Е-книги",
|
||||||
"LabelEdit": "Редакция",
|
"LabelEdit": "Редакция",
|
||||||
|
"LabelEmail": "Имейл",
|
||||||
"LabelEmailSettingsFromAddress": "От Адрес",
|
"LabelEmailSettingsFromAddress": "От Адрес",
|
||||||
"LabelEmailSettingsRejectUnauthorized": "Отхвърли неавторизирани сертификати",
|
"LabelEmailSettingsRejectUnauthorized": "Отхвърли неавторизирани сертификати",
|
||||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Спирането на валидацията на SSL сертификате може да изложи връзката ви на рискове, като man-in-the-middle атака. Спираите тази опция само ако знете имоликацийте от това и се доверявате на mail сървъра към който се свързвате.",
|
"LabelEmailSettingsRejectUnauthorizedHelp": "Спирането на валидацията на SSL сертификате може да изложи връзката ви на рискове, като man-in-the-middle атака. Спираите тази опция само ако знете имоликацийте от това и се доверявате на mail сървъра към който се свързвате.",
|
||||||
@@ -280,41 +319,53 @@
|
|||||||
"LabelEmailSettingsSecureHelp": "Ако е вярно възката ще изполва TLS когате се свързва със сървъра. Ако не е то TLS ще се използва ако сървъра поддържа разширението STARTTLS. В повечето случаи задайте тази стойност на истина ако се свързвате към порт 465. За порт 587 или 25 оставете я на лъжа. (от nodemailer.com/smtp/#authentication)",
|
"LabelEmailSettingsSecureHelp": "Ако е вярно възката ще изполва TLS когате се свързва със сървъра. Ако не е то TLS ще се използва ако сървъра поддържа разширението STARTTLS. В повечето случаи задайте тази стойност на истина ако се свързвате към порт 465. За порт 587 или 25 оставете я на лъжа. (от nodemailer.com/smtp/#authentication)",
|
||||||
"LabelEmailSettingsTestAddress": "Тестов Адрес",
|
"LabelEmailSettingsTestAddress": "Тестов Адрес",
|
||||||
"LabelEmbeddedCover": "Вградена Корица",
|
"LabelEmbeddedCover": "Вградена Корица",
|
||||||
"LabelEnable": "Включи",
|
"LabelEnable": "Активирай",
|
||||||
|
"LabelEncodingBackupLocation": "Резервно копие на вашите оригинални аудио файлове ще бъде съхранено в:",
|
||||||
|
"LabelEncodingChaptersNotEmbedded": "Главите не са вградени в аудиокнигите с множество тракове.",
|
||||||
|
"LabelEncodingClearItemCache": "Уверете се, че периодично изчиствате кеша на елементите.",
|
||||||
|
"LabelEncodingFinishedM4B": "Завършеният M4B файл ще бъде поставен в папката на вашите аудиокниги на:",
|
||||||
|
"LabelEncodingInfoEmbedded": "Метаданните ще бъдат вградени в аудио траковете в папката на вашите аудиокниги.",
|
||||||
"LabelEnd": "Край",
|
"LabelEnd": "Край",
|
||||||
|
"LabelEndOfChapter": "Край на глава",
|
||||||
"LabelEpisode": "Епизод",
|
"LabelEpisode": "Епизод",
|
||||||
"LabelEpisodeTitle": "Заглавие на Епизод",
|
"LabelEpisodeTitle": "Заглавие на Епизод",
|
||||||
"LabelEpisodeType": "Тип на Епизод",
|
"LabelEpisodeType": "Тип на Епизод",
|
||||||
"LabelExample": "Пример",
|
"LabelExample": "Пример",
|
||||||
"LabelExplicit": "Експлицитно",
|
"LabelExpandSeries": "Покажи сериите",
|
||||||
|
"LabelExpandSubSeries": "Покажи съб сериите",
|
||||||
|
"LabelExplicit": "С нецензурно съдържание",
|
||||||
|
"LabelExplicitChecked": "С нецензурно съдържание (проверено)",
|
||||||
|
"LabelExplicitUnchecked": "Без нецензурно съдържание (непроверено)",
|
||||||
|
"LabelExportOPML": "Експортирай OPML",
|
||||||
|
"LabelFeedURL": "URL на емисия",
|
||||||
"LabelFetchingMetadata": "Взимане на Метаданни",
|
"LabelFetchingMetadata": "Взимане на Метаданни",
|
||||||
"LabelFile": "Файл",
|
"LabelFile": "Файл",
|
||||||
"LabelFileBirthtime": "Дата на създаване на файла",
|
"LabelFileBirthtime": "Дата на създаване на файла",
|
||||||
"LabelFileModified": "Файлът променен",
|
"LabelFileModified": "Дата на модификация на файла",
|
||||||
"LabelFilename": "Име на Файл",
|
"LabelFilename": "Име на файла",
|
||||||
"LabelFilterByUser": "Филтриране по Потребител",
|
"LabelFilterByUser": "Филтриране по Потребител",
|
||||||
"LabelFindEpisodes": "Намери Епизоди",
|
"LabelFindEpisodes": "Намери Епизоди",
|
||||||
"LabelFinished": "Завършено",
|
"LabelFinished": "Дата на приключване",
|
||||||
"LabelFolder": "Папка",
|
"LabelFolder": "Папка",
|
||||||
"LabelFolders": "Папки",
|
"LabelFolders": "Папки",
|
||||||
"LabelFontBold": "Получерно",
|
"LabelFontBold": "Получерно",
|
||||||
"LabelFontBoldness": "Плътност на шрифта",
|
"LabelFontBoldness": "Дебелина на шрифта",
|
||||||
"LabelFontFamily": "Шрифт",
|
"LabelFontFamily": "Шрифт",
|
||||||
"LabelFontItalic": "Курсив",
|
"LabelFontItalic": "Курсив",
|
||||||
"LabelFontScale": "Мащаб на Шрифта",
|
"LabelFontScale": "Мащаб на шрифта",
|
||||||
"LabelFontStrikethrough": "Зачертан",
|
"LabelFontStrikethrough": "Зачертан",
|
||||||
"LabelFormat": "Формат",
|
"LabelFormat": "Формат",
|
||||||
"LabelGenre": "Жанр",
|
"LabelGenre": "Жанр",
|
||||||
"LabelGenres": "Жанрове",
|
"LabelGenres": "Жанрове",
|
||||||
"LabelHardDeleteFile": "Пълно Изтриване на Файл",
|
"LabelHardDeleteFile": "Пълно Изтриване на Файл",
|
||||||
"LabelHasEbook": "Има електронна книга",
|
"LabelHasEbook": "Има е-книга",
|
||||||
"LabelHasSupplementaryEbook": "Има допълнителна електронна книга",
|
"LabelHasSupplementaryEbook": "Има допълнителна е-книга",
|
||||||
"LabelHighestPriority": "Най-висок Приоритет",
|
"LabelHighestPriority": "Най-висок Приоритет",
|
||||||
"LabelHost": "Хост",
|
"LabelHost": "Хост",
|
||||||
"LabelHour": "Час",
|
"LabelHour": "Час",
|
||||||
"LabelIcon": "Икона",
|
"LabelIcon": "Икона",
|
||||||
"LabelImageURLFromTheWeb": "URL на Изображение от Интернет",
|
"LabelImageURLFromTheWeb": "URL на Изображение от Интернет",
|
||||||
"LabelInProgress": "В Прогрес",
|
"LabelInProgress": "В процес на изпълнение",
|
||||||
"LabelIncludeInTracklist": "Включи в Списъка с Канали",
|
"LabelIncludeInTracklist": "Включи в Списъка с Канали",
|
||||||
"LabelIncomplete": "Незавършено",
|
"LabelIncomplete": "Незавършено",
|
||||||
"LabelInterval": "Интервал",
|
"LabelInterval": "Интервал",
|
||||||
@@ -337,7 +388,7 @@
|
|||||||
"LabelLastTime": "Последно Време",
|
"LabelLastTime": "Последно Време",
|
||||||
"LabelLastUpdate": "Последно Обновяване",
|
"LabelLastUpdate": "Последно Обновяване",
|
||||||
"LabelLayout": "Оформление",
|
"LabelLayout": "Оформление",
|
||||||
"LabelLayoutSinglePage": "Една Страница",
|
"LabelLayoutSinglePage": "Единична страница",
|
||||||
"LabelLayoutSplitPage": "Разделена Страница",
|
"LabelLayoutSplitPage": "Разделена Страница",
|
||||||
"LabelLess": "По-малко",
|
"LabelLess": "По-малко",
|
||||||
"LabelLibrariesAccessibleToUser": "Библиотеки Достъпни за Потребителя",
|
"LabelLibrariesAccessibleToUser": "Библиотеки Достъпни за Потребителя",
|
||||||
@@ -345,8 +396,8 @@
|
|||||||
"LabelLibraryItem": "Елемент на Библиотека",
|
"LabelLibraryItem": "Елемент на Библиотека",
|
||||||
"LabelLibraryName": "Име на Библиотека",
|
"LabelLibraryName": "Име на Библиотека",
|
||||||
"LabelLimit": "Лимит",
|
"LabelLimit": "Лимит",
|
||||||
"LabelLineSpacing": "Линейно Разстояние",
|
"LabelLineSpacing": "Междуредие",
|
||||||
"LabelListenAgain": "Слушай Отново",
|
"LabelListenAgain": "Слушай отново",
|
||||||
"LabelLogLevelDebug": "Дебъг",
|
"LabelLogLevelDebug": "Дебъг",
|
||||||
"LabelLogLevelInfo": "Информация",
|
"LabelLogLevelInfo": "Информация",
|
||||||
"LabelLogLevelWarn": "Предупреждение",
|
"LabelLogLevelWarn": "Предупреждение",
|
||||||
@@ -355,7 +406,7 @@
|
|||||||
"LabelMatchExistingUsersBy": "Съпостави съществуващи потребители по",
|
"LabelMatchExistingUsersBy": "Съпостави съществуващи потребители по",
|
||||||
"LabelMatchExistingUsersByDescription": "Използва се за свързване на съществуващи потребители. След свързване потребителите ще бъдат съпоставени по уникален идентификатор от вашия доставчик на SSO",
|
"LabelMatchExistingUsersByDescription": "Използва се за свързване на съществуващи потребители. След свързване потребителите ще бъдат съпоставени по уникален идентификатор от вашия доставчик на SSO",
|
||||||
"LabelMediaPlayer": "Медия Плейър",
|
"LabelMediaPlayer": "Медия Плейър",
|
||||||
"LabelMediaType": "Тип на Медията",
|
"LabelMediaType": "Тип медия",
|
||||||
"LabelMetaTag": "Мета Таг",
|
"LabelMetaTag": "Мета Таг",
|
||||||
"LabelMetaTags": "Мета Тагове",
|
"LabelMetaTags": "Мета Тагове",
|
||||||
"LabelMetadataOrderOfPrecedenceDescription": "По-високите източници на метаданни ще заменят по-ниските",
|
"LabelMetadataOrderOfPrecedenceDescription": "По-високите източници на метаданни ще заменят по-ниските",
|
||||||
@@ -367,19 +418,19 @@
|
|||||||
"LabelMobileRedirectURIs": "Позволени URI за Мобилно Пренасочване",
|
"LabelMobileRedirectURIs": "Позволени URI за Мобилно Пренасочване",
|
||||||
"LabelMobileRedirectURIsDescription": "Това е whitelist на валидни URI за пренасочване за мобилни приложения. По подразбиране е <code>audiobookshelf://oauth</code>, който може да премахнете или допълните с допълнителни URI за интеграция на приложения от трети страни. Използването на звезда (<code>*</code>) като единствен запис позволява всеки URI.",
|
"LabelMobileRedirectURIsDescription": "Това е whitelist на валидни URI за пренасочване за мобилни приложения. По подразбиране е <code>audiobookshelf://oauth</code>, който може да премахнете или допълните с допълнителни URI за интеграция на приложения от трети страни. Използването на звезда (<code>*</code>) като единствен запис позволява всеки URI.",
|
||||||
"LabelMore": "Повече",
|
"LabelMore": "Повече",
|
||||||
"LabelMoreInfo": "Повече Информация",
|
"LabelMoreInfo": "Повече информация",
|
||||||
"LabelName": "Име",
|
"LabelName": "Име",
|
||||||
"LabelNarrator": "Разказвач",
|
"LabelNarrator": "Разказвач",
|
||||||
"LabelNarrators": "Разказвачи",
|
"LabelNarrators": "Разказвачи",
|
||||||
"LabelNew": "Нови",
|
"LabelNew": "Нови",
|
||||||
"LabelNewPassword": "Нова Парола",
|
"LabelNewPassword": "Нова Парола",
|
||||||
"LabelNewestAuthors": "Най-нови Автори",
|
"LabelNewestAuthors": "Най-новите автори",
|
||||||
"LabelNewestEpisodes": "Най-нови Епизоди",
|
"LabelNewestEpisodes": "Най-новите епизоди",
|
||||||
"LabelNextBackupDate": "Следваща Дата на Архивиране",
|
"LabelNextBackupDate": "Следваща Дата на Архивиране",
|
||||||
"LabelNextScheduledRun": "Следващо Планирано Изпълнение",
|
"LabelNextScheduledRun": "Следващо Планирано Изпълнение",
|
||||||
"LabelNoCustomMetadataProviders": "Няма потребителски доставчици на метаданни",
|
"LabelNoCustomMetadataProviders": "Няма потребителски доставчици на метаданни",
|
||||||
"LabelNoEpisodesSelected": "Няма избрани епизоди",
|
"LabelNoEpisodesSelected": "Няма избрани епизоди",
|
||||||
"LabelNotFinished": "Не е завършено",
|
"LabelNotFinished": "Не е приключено",
|
||||||
"LabelNotStarted": "Не е започнато",
|
"LabelNotStarted": "Не е започнато",
|
||||||
"LabelNotes": "Бележки",
|
"LabelNotes": "Бележки",
|
||||||
"LabelNotificationAppriseURL": "Apprise URL-и",
|
"LabelNotificationAppriseURL": "Apprise URL-и",
|
||||||
@@ -392,7 +443,10 @@
|
|||||||
"LabelNotificationsMaxQueueSize": "Максимален размер на опашката за известия",
|
"LabelNotificationsMaxQueueSize": "Максимален размер на опашката за известия",
|
||||||
"LabelNotificationsMaxQueueSizeHelp": "Събитията са ограничени до изстрелване на 1 на секунда. Събитията ще бъдат игнорирани ако опашката е на максимален размер. Това предотвратява спамирането на известия.",
|
"LabelNotificationsMaxQueueSizeHelp": "Събитията са ограничени до изстрелване на 1 на секунда. Събитията ще бъдат игнорирани ако опашката е на максимален размер. Това предотвратява спамирането на известия.",
|
||||||
"LabelNumberOfBooks": "Брой на Книги",
|
"LabelNumberOfBooks": "Брой на Книги",
|
||||||
"LabelNumberOfEpisodes": "# Епизоди",
|
"LabelNumberOfEpisodes": "Брой епизоди",
|
||||||
|
"LabelOpenIDAdvancedPermsClaimDescription": "Име на OpenID твърдението, което съдържа разширени права за достъп до потребителски действия в приложението, които ще се прилагат за роли, различни от администраторските (<b>ако е конфигурирано</b>). Ако твърдението липсва в отговора, достъпът до ABS ще бъде отказан. Ако липсва една опция, тя ще се третира като <code>false</code>. Уверете се, че твърдението на доставчика на идентичност съответства на очакваната структура:",
|
||||||
|
"LabelOpenIDClaims": "Оставете следните опции празни, за да деактивирате разширеното присвояване на групи, като автоматично ще бъде присвоена групата 'Потребител'.",
|
||||||
|
"LabelOpenIDGroupClaimDescription": "Име на OpenID твърдението, което съдържа списък с групите на потребителя. Обикновено се нарича <code>groups</code>. <b>Ако е конфигурирано</b>, приложението автоматично ще присвоява роли въз основа на членството на потребителя в групи, при условие че тези групи са наименувани без чувствителност към регистъра като 'admin', 'user' или 'guest' в твърдението. Твърдението трябва да съдържа списък и ако потребителят принадлежи към множество групи, приложението ще присвои ролята, съответстваща на най-високото ниво на достъп. Ако няма съвпадение с група, достъпът ще бъде отказан.",
|
||||||
"LabelOpenRSSFeed": "Отвори RSS Feed",
|
"LabelOpenRSSFeed": "Отвори RSS Feed",
|
||||||
"LabelOverwrite": "Презапиши",
|
"LabelOverwrite": "Презапиши",
|
||||||
"LabelPassword": "Парола",
|
"LabelPassword": "Парола",
|
||||||
@@ -414,24 +468,27 @@
|
|||||||
"LabelPodcasts": "Подкасти",
|
"LabelPodcasts": "Подкасти",
|
||||||
"LabelPort": "Порт",
|
"LabelPort": "Порт",
|
||||||
"LabelPrefixesToIgnore": "Префикси за Игнориране (без значение за главни/малки букви)",
|
"LabelPrefixesToIgnore": "Префикси за Игнориране (без значение за главни/малки букви)",
|
||||||
"LabelPreventIndexing": "Предотврати индексирането на вашия feed от iTunes и Google podcast директории",
|
"LabelPreventIndexing": "Предотвратете индексирането на вашата емисия от директориите на iTunes и Google за подкасти",
|
||||||
"LabelPrimaryEbook": "Основна Електронна Книга",
|
"LabelPrimaryEbook": "Основна Електронна Книга",
|
||||||
"LabelProgress": "Прогрес",
|
"LabelProgress": "Прогрес",
|
||||||
"LabelProvider": "Доставчик",
|
"LabelProvider": "Доставчик",
|
||||||
"LabelPubDate": "Дата на Издаване",
|
"LabelPubDate": "Дата на публикуване",
|
||||||
"LabelPublishYear": "Година на Издаване",
|
"LabelPublishYear": "Година на публикуване",
|
||||||
|
"LabelPublishedDate": "Публикувани {0}",
|
||||||
"LabelPublisher": "Издател",
|
"LabelPublisher": "Издател",
|
||||||
"LabelPublishers": "Издателство",
|
"LabelPublishers": "Издателство",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Потребителски собственик Email",
|
"LabelRSSFeedCustomOwnerEmail": "Персонализиран имейл на собственика",
|
||||||
"LabelRSSFeedCustomOwnerName": "Потребителски собственик Име",
|
"LabelRSSFeedCustomOwnerName": "Персонализирано име на собственика",
|
||||||
"LabelRSSFeedOpen": "RSS Feed Оптворен",
|
"LabelRSSFeedOpen": "RSS Feed Оптворен",
|
||||||
"LabelRSSFeedPreventIndexing": "Предотврати индексиране",
|
"LabelRSSFeedPreventIndexing": "Предотвратете индексиране",
|
||||||
"LabelRSSFeedSlug": "RSS Feed слъг",
|
"LabelRSSFeedSlug": "идентификатор на RSS емисия",
|
||||||
|
"LabelRSSFeedURL": "URL на RSS емисия",
|
||||||
|
"LabelRandomly": "Случайно",
|
||||||
"LabelRead": "Прочети",
|
"LabelRead": "Прочети",
|
||||||
"LabelReadAgain": "Прочети Отново",
|
"LabelReadAgain": "Прочети отново",
|
||||||
"LabelReadEbookWithoutProgress": "Прочети електронната книга без записване прогрес",
|
"LabelReadEbookWithoutProgress": "Прочети електронната книга без записване прогрес",
|
||||||
"LabelRecentSeries": "Скорошни Серии",
|
"LabelRecentSeries": "Скорошни серии",
|
||||||
"LabelRecentlyAdded": "Наскоро Добавени",
|
"LabelRecentlyAdded": "Скорошно добавени",
|
||||||
"LabelRecommended": "Препоръчано",
|
"LabelRecommended": "Препоръчано",
|
||||||
"LabelRedo": "Повтори",
|
"LabelRedo": "Повтори",
|
||||||
"LabelRegion": "Регион",
|
"LabelRegion": "Регион",
|
||||||
@@ -448,12 +505,12 @@
|
|||||||
"LabelSelectUsers": "Избери Потребители",
|
"LabelSelectUsers": "Избери Потребители",
|
||||||
"LabelSendEbookToDevice": "Изпрати електронна книга до ...",
|
"LabelSendEbookToDevice": "Изпрати електронна книга до ...",
|
||||||
"LabelSequence": "Последователност",
|
"LabelSequence": "Последователност",
|
||||||
"LabelSeries": "Серия",
|
"LabelSeries": "От сериите",
|
||||||
"LabelSeriesName": "Име на Серия",
|
"LabelSeriesName": "Име на Серия",
|
||||||
"LabelSeriesProgress": "Прогрес на Серия",
|
"LabelSeriesProgress": "Прогрес на Серия",
|
||||||
"LabelServerYearReview": "Преглед на годината на сървъра ({0})",
|
"LabelServerYearReview": "Преглед на годината на сървъра ({0})",
|
||||||
"LabelSetEbookAsPrimary": "Задай като основна",
|
"LabelSetEbookAsPrimary": "Направи главен",
|
||||||
"LabelSetEbookAsSupplementary": "Задай като допълнителна",
|
"LabelSetEbookAsSupplementary": "Направи второстепенен",
|
||||||
"LabelSettingsAudiobooksOnly": "Само аудиокниги",
|
"LabelSettingsAudiobooksOnly": "Само аудиокниги",
|
||||||
"LabelSettingsAudiobooksOnlyHelp": "Активирането на тази настройка ще игнорира файловете на електронни книги, освен ако не са в папка с аудиокниги, в което случай ще бъдат зададени като допълнителни електронни книги",
|
"LabelSettingsAudiobooksOnlyHelp": "Активирането на тази настройка ще игнорира файловете на електронни книги, освен ако не са в папка с аудиокниги, в което случай ще бъдат зададени като допълнителни електронни книги",
|
||||||
"LabelSettingsBookshelfViewHelp": "Скеуморфен дизайн с дървени рафтове",
|
"LabelSettingsBookshelfViewHelp": "Скеуморфен дизайн с дървени рафтове",
|
||||||
@@ -476,6 +533,7 @@
|
|||||||
"LabelSettingsHomePageBookshelfView": "Начална страница изглед на рафт",
|
"LabelSettingsHomePageBookshelfView": "Начална страница изглед на рафт",
|
||||||
"LabelSettingsLibraryBookshelfView": "Библиотека изглед на рафт",
|
"LabelSettingsLibraryBookshelfView": "Библиотека изглед на рафт",
|
||||||
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Пропусни предишни книги в Продължи Поредица",
|
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Пропусни предишни книги в Продължи Поредица",
|
||||||
|
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Рафтът на началната страница 'Продължи поредицата' показва първата книга, която не е започната в поредици, в които има поне една завършена книга и няма книги в процес на четене. Активирането на тази настройка ще продължи поредицата от най-далечната завършена книга вместо от първата незапочната книга.",
|
||||||
"LabelSettingsParseSubtitles": "Извлечи подзаглавия",
|
"LabelSettingsParseSubtitles": "Извлечи подзаглавия",
|
||||||
"LabelSettingsParseSubtitlesHelp": "Извлича подзаглавия от имената на папките на аудиокнигите.<br>Подзаглавията трябва да бъдат разделени с \" - \"<br>например \"Заглавие на Книга - Тук е Подзаглавито\" има подзаглавие \"Тук е Подзаглавито\"",
|
"LabelSettingsParseSubtitlesHelp": "Извлича подзаглавия от имената на папките на аудиокнигите.<br>Подзаглавията трябва да бъдат разделени с \" - \"<br>например \"Заглавие на Книга - Тук е Подзаглавито\" има подзаглавие \"Тук е Подзаглавито\"",
|
||||||
"LabelSettingsPreferMatchedMetadata": "Предпочети съвпадащи метаданни",
|
"LabelSettingsPreferMatchedMetadata": "Предпочети съвпадащи метаданни",
|
||||||
@@ -491,9 +549,10 @@
|
|||||||
"LabelSettingsStoreMetadataWithItem": "Запази метаданните с елемента",
|
"LabelSettingsStoreMetadataWithItem": "Запази метаданните с елемента",
|
||||||
"LabelSettingsStoreMetadataWithItemHelp": "По подразбиране метаданните се съхраняват в /metadata/items, като активирате тази настройка метаданните ще се съхраняват в папката на елемента на вашата библиотека",
|
"LabelSettingsStoreMetadataWithItemHelp": "По подразбиране метаданните се съхраняват в /metadata/items, като активирате тази настройка метаданните ще се съхраняват в папката на елемента на вашата библиотека",
|
||||||
"LabelSettingsTimeFormat": "Формат на Време",
|
"LabelSettingsTimeFormat": "Формат на Време",
|
||||||
"LabelShowAll": "Покажи Всички",
|
"LabelShowAll": "Покажи всички",
|
||||||
|
"LabelShowSeconds": "Покажи секунди",
|
||||||
"LabelSize": "Размер",
|
"LabelSize": "Размер",
|
||||||
"LabelSleepTimer": "Таймер за Сън",
|
"LabelSleepTimer": "Таймер за изключване",
|
||||||
"LabelSlug": "Слъг",
|
"LabelSlug": "Слъг",
|
||||||
"LabelStart": "Старт",
|
"LabelStart": "Старт",
|
||||||
"LabelStartTime": "Начално Време",
|
"LabelStartTime": "Начално Време",
|
||||||
@@ -501,19 +560,19 @@
|
|||||||
"LabelStartedAt": "Стартирано на",
|
"LabelStartedAt": "Стартирано на",
|
||||||
"LabelStatsAudioTracks": "Аудио Канали",
|
"LabelStatsAudioTracks": "Аудио Канали",
|
||||||
"LabelStatsAuthors": "Автори",
|
"LabelStatsAuthors": "Автори",
|
||||||
"LabelStatsBestDay": "Най-добър Ден",
|
"LabelStatsBestDay": "Най-добър ден",
|
||||||
"LabelStatsDailyAverage": "Дневна Средна Стойност",
|
"LabelStatsDailyAverage": "Средно дневно",
|
||||||
"LabelStatsDays": "Дни",
|
"LabelStatsDays": "Общо дни",
|
||||||
"LabelStatsDaysListened": "Дни Слушани",
|
"LabelStatsDaysListened": "Общо слушани дни",
|
||||||
"LabelStatsHours": "Часове",
|
"LabelStatsHours": "Часове",
|
||||||
"LabelStatsInARow": "подред",
|
"LabelStatsInARow": "последователно",
|
||||||
"LabelStatsItemsFinished": "Завършени Елементи",
|
"LabelStatsItemsFinished": "Приключени елементи",
|
||||||
"LabelStatsItemsInLibrary": "Елементи в Библиотеката",
|
"LabelStatsItemsInLibrary": "Елементи в Библиотеката",
|
||||||
"LabelStatsMinutes": "минути",
|
"LabelStatsMinutes": "минути",
|
||||||
"LabelStatsMinutesListening": "Минути Слушани",
|
"LabelStatsMinutesListening": "Общо слушани минути",
|
||||||
"LabelStatsOverallDays": "Общо Дни",
|
"LabelStatsOverallDays": "Общо Дни",
|
||||||
"LabelStatsOverallHours": "Общо Часове",
|
"LabelStatsOverallHours": "Общо Часове",
|
||||||
"LabelStatsWeekListening": "Седмица Слушане",
|
"LabelStatsWeekListening": "Общо слушани седмици",
|
||||||
"LabelSubtitle": "Подзаглавие",
|
"LabelSubtitle": "Подзаглавие",
|
||||||
"LabelSupportedFileTypes": "Поддържани Типове Файлове",
|
"LabelSupportedFileTypes": "Поддържани Типове Файлове",
|
||||||
"LabelTag": "Таг",
|
"LabelTag": "Таг",
|
||||||
@@ -531,7 +590,7 @@
|
|||||||
"LabelTimeBase": "Времева Основа",
|
"LabelTimeBase": "Времева Основа",
|
||||||
"LabelTimeListened": "Време Слушано",
|
"LabelTimeListened": "Време Слушано",
|
||||||
"LabelTimeListenedToday": "Време Слушано Днес",
|
"LabelTimeListenedToday": "Време Слушано Днес",
|
||||||
"LabelTimeRemaining": "{0} оставащо време",
|
"LabelTimeRemaining": "{0} оставащи",
|
||||||
"LabelTimeToShift": "Време за изместване в секунди",
|
"LabelTimeToShift": "Време за изместване в секунди",
|
||||||
"LabelTitle": "Заглавие",
|
"LabelTitle": "Заглавие",
|
||||||
"LabelToolsEmbedMetadata": "Вграждане на Метаданни",
|
"LabelToolsEmbedMetadata": "Вграждане на Метаданни",
|
||||||
@@ -544,14 +603,14 @@
|
|||||||
"LabelTotalTimeListened": "Общо Време Слушано",
|
"LabelTotalTimeListened": "Общо Време Слушано",
|
||||||
"LabelTrackFromFilename": "Канал от Име на Файл",
|
"LabelTrackFromFilename": "Канал от Име на Файл",
|
||||||
"LabelTrackFromMetadata": "Канал от Метаданни",
|
"LabelTrackFromMetadata": "Канал от Метаданни",
|
||||||
"LabelTracks": "Канали",
|
"LabelTracks": "Тракове",
|
||||||
"LabelTracksMultiTrack": "Многоканален",
|
"LabelTracksMultiTrack": "Многоканален",
|
||||||
"LabelTracksNone": "Няма канали",
|
"LabelTracksNone": "Няма канали",
|
||||||
"LabelTracksSingleTrack": "Единичен канал",
|
"LabelTracksSingleTrack": "Единичен канал",
|
||||||
"LabelType": "Тип",
|
"LabelType": "Тип",
|
||||||
"LabelUnabridged": "Несъкратен",
|
"LabelUnabridged": "Несъкратен",
|
||||||
"LabelUndo": "Отмени",
|
"LabelUndo": "Отмени",
|
||||||
"LabelUnknown": "Неизвестно",
|
"LabelUnknown": "Неизвестен",
|
||||||
"LabelUpdateCover": "Обнови Корица",
|
"LabelUpdateCover": "Обнови Корица",
|
||||||
"LabelUpdateCoverHelp": "Позволи презаписване на съществуващите корици за избраните книги, когато се намери съвпадение",
|
"LabelUpdateCoverHelp": "Позволи презаписване на съществуващите корици за избраните книги, когато се намери съвпадение",
|
||||||
"LabelUpdateDetails": "Обнови Детайли",
|
"LabelUpdateDetails": "Обнови Детайли",
|
||||||
@@ -563,7 +622,7 @@
|
|||||||
"LabelUseChapterTrack": "Използвай канал за глава",
|
"LabelUseChapterTrack": "Използвай канал за глава",
|
||||||
"LabelUseFullTrack": "Използвай пълен канал",
|
"LabelUseFullTrack": "Използвай пълен канал",
|
||||||
"LabelUser": "Потребител",
|
"LabelUser": "Потребител",
|
||||||
"LabelUsername": "Потребителско Име",
|
"LabelUsername": "Потребителско име",
|
||||||
"LabelValue": "Стойност",
|
"LabelValue": "Стойност",
|
||||||
"LabelVersion": "Версия",
|
"LabelVersion": "Версия",
|
||||||
"LabelViewBookmarks": "Виж Отметки",
|
"LabelViewBookmarks": "Виж Отметки",
|
||||||
@@ -571,16 +630,20 @@
|
|||||||
"LabelViewQueue": "Виж Опашка",
|
"LabelViewQueue": "Виж Опашка",
|
||||||
"LabelVolume": "Сила на Звука",
|
"LabelVolume": "Сила на Звука",
|
||||||
"LabelWeekdaysToRun": "Делници за изпълнение",
|
"LabelWeekdaysToRun": "Делници за изпълнение",
|
||||||
|
"LabelYearReviewHide": "Скрий ревю на годината ти",
|
||||||
|
"LabelYearReviewShow": "Виж ревю на годината ти",
|
||||||
"LabelYourAudiobookDuration": "Продължителност на вашата аудиокнига",
|
"LabelYourAudiobookDuration": "Продължителност на вашата аудиокнига",
|
||||||
"LabelYourBookmarks": "Вашите Отметки",
|
"LabelYourBookmarks": "Твойте отметки",
|
||||||
"LabelYourPlaylists": "Вашите Плейлисти",
|
"LabelYourPlaylists": "Вашите Плейлисти",
|
||||||
"LabelYourProgress": "Вашият Прогрес",
|
"LabelYourProgress": "Твоят прогрес",
|
||||||
"MessageAddToPlayerQueue": "Добави към опашката на плейъра",
|
"MessageAddToPlayerQueue": "Добави към опашката на плейъра",
|
||||||
"MessageAppriseDescription": "За да ползвате тази функция трябва да имате активна инстанция на <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> или на друго АПИ което да обработва тези заявки. <br />The Apprise API Url-а трябва дае пълния URL път за изпращане на известията, например, ако вашето АПИ ве подава от <code>http://192.168.1.1:8337</code> трябва да сложитев <code>http://192.168.1.1:8337/notify</code>.",
|
"MessageAppriseDescription": "За да ползвате тази функция трябва да имате активна инстанция на <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> или на друго АПИ което да обработва тези заявки. <br />The Apprise API Url-а трябва дае пълния URL път за изпращане на известията, например, ако вашето АПИ ве подава от <code>http://192.168.1.1:8337</code> трябва да сложитев <code>http://192.168.1.1:8337/notify</code>.",
|
||||||
|
"MessageBackupsDescription": "Резервните копия включват потребители, напредък на потребителите, подробности за елементите в библиотеката, настройки на сървъра и изображения, съхранени в <code>/metadata/items</code> и <code>/metadata/authors</code>. Резервните копия <strong>не</strong> включват никакви файлове, съхранени в папките на вашата библиотека.",
|
||||||
"MessageBatchQuickMatchDescription": "Бързото Съпоставяне ще опита да добави липсващи корици и метаданни за избраните елементи. Активирайте опциите по-долу, за да позволите на Бързото съпоставяне да презапише съществуващите корици и/или метаданни.",
|
"MessageBatchQuickMatchDescription": "Бързото Съпоставяне ще опита да добави липсващи корици и метаданни за избраните елементи. Активирайте опциите по-долу, за да позволите на Бързото съпоставяне да презапише съществуващите корици и/или метаданни.",
|
||||||
"MessageBookshelfNoCollections": "Все още нямате създадени колекции",
|
"MessageBookshelfNoCollections": "Все още нямате създадени колекции",
|
||||||
"MessageBookshelfNoRSSFeeds": "Няма отворени RSS feed-ове",
|
"MessageBookshelfNoRSSFeeds": "Няма отворени RSS feed-ове",
|
||||||
"MessageBookshelfNoResultsForFilter": "Няма резултат за филтер \"{0}: {1}\"",
|
"MessageBookshelfNoResultsForFilter": "Няма резултат за филтер \"{0}: {1}\"",
|
||||||
|
"MessageBookshelfNoResultsForQuery": "Няма резултати от заявката",
|
||||||
"MessageBookshelfNoSeries": "Нямаш сеЗЙ",
|
"MessageBookshelfNoSeries": "Нямаш сеЗЙ",
|
||||||
"MessageChapterEndIsAfter": "Краят на главата е след края на вашата аудиокнига",
|
"MessageChapterEndIsAfter": "Краят на главата е след края на вашата аудиокнига",
|
||||||
"MessageChapterErrorFirstNotZero": "Първата глава трябва да започва от 0",
|
"MessageChapterErrorFirstNotZero": "Първата глава трябва да започва от 0",
|
||||||
@@ -600,6 +663,8 @@
|
|||||||
"MessageConfirmMarkAllEpisodesNotFinished": "Сигурни ли сте, че искате да маркирате всички епизоди като незавършени?",
|
"MessageConfirmMarkAllEpisodesNotFinished": "Сигурни ли сте, че искате да маркирате всички епизоди като незавършени?",
|
||||||
"MessageConfirmMarkSeriesFinished": "Сигурни ли сте, че искате да маркирате всички книги в тази серия като завършени?",
|
"MessageConfirmMarkSeriesFinished": "Сигурни ли сте, че искате да маркирате всички книги в тази серия като завършени?",
|
||||||
"MessageConfirmMarkSeriesNotFinished": "Сигурни ли сте, че искате да маркирате всички книги в тази серия като незавършени?",
|
"MessageConfirmMarkSeriesNotFinished": "Сигурни ли сте, че искате да маркирате всички книги в тази серия като незавършени?",
|
||||||
|
"MessageConfirmPurgeCache": "Изчистването на кеша ще изтрие цялата директория в <code>/metadata/cache</code>. <br /><br />Сигурни ли сте, че искате да премахнете директорията на кеша?",
|
||||||
|
"MessageConfirmPurgeItemsCache": "Изчистването на кеша на елементите ще изтрие цялата директория в <code>/metadata/cache/items</code>. <br />Сигурни ли сте?",
|
||||||
"MessageConfirmQuickEmbed": "Внимание! Бързото вграждане няма да архивира вашите аудио файлове. Уверете се, че имате резервно копие на вашите аудио файлове. <br><br>Искате ли да продължите?",
|
"MessageConfirmQuickEmbed": "Внимание! Бързото вграждане няма да архивира вашите аудио файлове. Уверете се, че имате резервно копие на вашите аудио файлове. <br><br>Искате ли да продължите?",
|
||||||
"MessageConfirmReScanLibraryItems": "Сигурни ли сте, че искате да сканирате отново {0} елемента?",
|
"MessageConfirmReScanLibraryItems": "Сигурни ли сте, че искате да сканирате отново {0} елемента?",
|
||||||
"MessageConfirmRemoveAllChapters": "Сигурни ли сте, че искате да премахнете всички глави?",
|
"MessageConfirmRemoveAllChapters": "Сигурни ли сте, че искате да премахнете всички глави?",
|
||||||
@@ -617,34 +682,36 @@
|
|||||||
"MessageConfirmRenameTagMergeNote": "Забележка: Този таг вече съществува и ще бъде слято.",
|
"MessageConfirmRenameTagMergeNote": "Забележка: Този таг вече съществува и ще бъде слято.",
|
||||||
"MessageConfirmRenameTagWarning": "Внимание! Вече съществува подобен таг с различно писане \"{0}\".",
|
"MessageConfirmRenameTagWarning": "Внимание! Вече съществува подобен таг с различно писане \"{0}\".",
|
||||||
"MessageConfirmSendEbookToDevice": "Сигурни ли сте, че искате да изпратите {0} електронна книга \"{1}\" до устройство \"{2}\"?",
|
"MessageConfirmSendEbookToDevice": "Сигурни ли сте, че искате да изпратите {0} електронна книга \"{1}\" до устройство \"{2}\"?",
|
||||||
"MessageDownloadingEpisode": "Изтегляне на епизод",
|
"MessageDownloadingEpisode": "Сваля епизод",
|
||||||
"MessageDragFilesIntoTrackOrder": "Плъзнете файлове в правилния ред на каналите",
|
"MessageDragFilesIntoTrackOrder": "Плъзнете файлове в правилния ред на каналите",
|
||||||
"MessageEmbedFinished": "Вграждането завърши!",
|
"MessageEmbedFinished": "Вграждането завърши!",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} епизод(и) в опашка за изтегляне",
|
"MessageEpisodesQueuedForDownload": "{0} Епизод(и) са сложени за сваляне",
|
||||||
"MessageFeedURLWillBe": "Feed URL-a ще бъде {0}",
|
"MessageEreaderDevices": "За да осигурите доставката на е-книги, може да се наложи да добавите горепосочения имейл адрес като валиден подател за всяко устройство, изброено по-долу.",
|
||||||
"MessageFetching": "Взимане...",
|
"MessageFeedURLWillBe": "Адресът на емисията ще бъде {0}",
|
||||||
|
"MessageFetching": "Извличане...",
|
||||||
"MessageForceReScanDescription": "ще сканира всички файлове отново като прясно сканиране. Аудио файлове ID3 тагове, OPF файлове и текстови файлове ще бъдат сканирани като нови.",
|
"MessageForceReScanDescription": "ще сканира всички файлове отново като прясно сканиране. Аудио файлове ID3 тагове, OPF файлове и текстови файлове ще бъдат сканирани като нови.",
|
||||||
"MessageImportantNotice": "Важно Съобщение!",
|
"MessageImportantNotice": "Важно Съобщение!",
|
||||||
"MessageInsertChapterBelow": "Вмъкни глава под",
|
"MessageInsertChapterBelow": "Вмъкни глава под",
|
||||||
"MessageItemsSelected": "{0} избрани",
|
"MessageItemsSelected": "{0} избрани",
|
||||||
"MessageItemsUpdated": "{0} елемента обновени",
|
"MessageItemsUpdated": "{0} елемента обновени",
|
||||||
"MessageJoinUsOn": "Присъединете се към нас",
|
"MessageJoinUsOn": "Присъединете се към нас",
|
||||||
"MessageLoading": "Зареждане...",
|
"MessageLoading": "Зарежда...",
|
||||||
"MessageLoadingFolders": "Зареждане на Папки...",
|
"MessageLoadingFolders": "Зареждане на Папки...",
|
||||||
|
"MessageLogsDescription": "Логовете се съхраняват в <code>/metadata/logs</code> като JSON файлове. Дневниците за сривове се съхраняват в <code>/metadata/logs/crash_logs.txt</code>.",
|
||||||
"MessageM4BFailed": "M4B Провалено!",
|
"MessageM4BFailed": "M4B Провалено!",
|
||||||
"MessageM4BFinished": "M4B Завършено!",
|
"MessageM4BFinished": "M4B Завършено!",
|
||||||
"MessageMapChapterTitles": "Съпостави заглавията на главите със съществуващите глави на аудиокнигата без да променяш времената",
|
"MessageMapChapterTitles": "Съпостави заглавията на главите със съществуващите глави на аудиокнигата без да променяш времената",
|
||||||
"MessageMarkAllEpisodesFinished": "Маркирай всички епизоди като завършени",
|
"MessageMarkAllEpisodesFinished": "Маркирай всички епизоди като завършени",
|
||||||
"MessageMarkAllEpisodesNotFinished": "Маркирай всички епизоди като незавършени",
|
"MessageMarkAllEpisodesNotFinished": "Маркирай всички епизоди като незавършени",
|
||||||
"MessageMarkAsFinished": "Маркирай като Завършено",
|
"MessageMarkAsFinished": "Маркирай като завършено",
|
||||||
"MessageMarkAsNotFinished": "Маркирай като Незавършено",
|
"MessageMarkAsNotFinished": "Маркирай като Незавършено",
|
||||||
"MessageMatchBooksDescription": "ще се опита да съпостави книги в библиотеката с книга от избрания доставчик за търсене и ще попълни празни детайли и корици. Не презаписва детайлите.",
|
"MessageMatchBooksDescription": "ще се опита да съпостави книги в библиотеката с книга от избрания доставчик за търсене и ще попълни празни детайли и корици. Не презаписва детайлите.",
|
||||||
"MessageNoAudioTracks": "Няма аудио канали",
|
"MessageNoAudioTracks": "Няма аудио канали",
|
||||||
"MessageNoAuthors": "Няма Автори",
|
"MessageNoAuthors": "Няма Автори",
|
||||||
"MessageNoBackups": "Няма архиви",
|
"MessageNoBackups": "Няма архиви",
|
||||||
"MessageNoBookmarks": "Няма Отметки",
|
"MessageNoBookmarks": "Няма отметки",
|
||||||
"MessageNoChapters": "Няма Глави",
|
"MessageNoChapters": "Няма глави",
|
||||||
"MessageNoCollections": "Няма Колекции",
|
"MessageNoCollections": "Няма колекции",
|
||||||
"MessageNoCoversFound": "Не са намерени корици",
|
"MessageNoCoversFound": "Не са намерени корици",
|
||||||
"MessageNoDescription": "Няма описание",
|
"MessageNoDescription": "Няма описание",
|
||||||
"MessageNoDownloadsInProgress": "Няма изтегляния в прогрес",
|
"MessageNoDownloadsInProgress": "Няма изтегляния в прогрес",
|
||||||
@@ -654,9 +721,9 @@
|
|||||||
"MessageNoFoldersAvailable": "Няма налични папки",
|
"MessageNoFoldersAvailable": "Няма налични папки",
|
||||||
"MessageNoGenres": "Няма Жанрове",
|
"MessageNoGenres": "Няма Жанрове",
|
||||||
"MessageNoIssues": "Няма проблеми",
|
"MessageNoIssues": "Няма проблеми",
|
||||||
"MessageNoItems": "Няма Елементи",
|
"MessageNoItems": "Няма елементи",
|
||||||
"MessageNoItemsFound": "Няма намерени елементи",
|
"MessageNoItemsFound": "Няма намерени елементи",
|
||||||
"MessageNoListeningSessions": "Няма слушателски сесии",
|
"MessageNoListeningSessions": "Няма сесии за слушане",
|
||||||
"MessageNoLogs": "Няма логове",
|
"MessageNoLogs": "Няма логове",
|
||||||
"MessageNoMediaProgress": "Няма прогрес на медията",
|
"MessageNoMediaProgress": "Няма прогрес на медията",
|
||||||
"MessageNoNotifications": "Няма известия",
|
"MessageNoNotifications": "Няма известия",
|
||||||
@@ -666,20 +733,21 @@
|
|||||||
"MessageNoSeries": "Няма Серии",
|
"MessageNoSeries": "Няма Серии",
|
||||||
"MessageNoTags": "Няма Тагове",
|
"MessageNoTags": "Няма Тагове",
|
||||||
"MessageNoTasksRunning": "Няма вършещи се задачи",
|
"MessageNoTasksRunning": "Няма вършещи се задачи",
|
||||||
"MessageNoUpdatesWereNecessary": "Не бяха необходими обновления",
|
"MessageNoUpdatesWereNecessary": "Няма нужда от обновяване",
|
||||||
"MessageNoUserPlaylists": "Няма плейлисти на потребителя",
|
"MessageNoUserPlaylists": "Нямате създадени плейлисти",
|
||||||
"MessageNotYetImplemented": "Още не е изпълнено",
|
"MessageNotYetImplemented": "Още не е изпълнено",
|
||||||
"MessageOr": "или",
|
"MessageOr": "или",
|
||||||
"MessagePauseChapter": "Пауза на глава",
|
"MessagePauseChapter": "Пауза на глава",
|
||||||
"MessagePlayChapter": "Пусни налчалото на глава",
|
"MessagePlayChapter": "Пусни налчалото на глава",
|
||||||
"MessagePlaylistCreateFromCollection": "Създай плейлист от колекция",
|
"MessagePlaylistCreateFromCollection": "Създай плейлист от колекция",
|
||||||
"MessagePodcastHasNoRSSFeedForMatching": "Подкастът няма URL адрес на RSS feed за използване за съпоставяне",
|
"MessagePodcastHasNoRSSFeedForMatching": "Подкастът няма URL адрес на RSS feed за използване за съпоставяне",
|
||||||
|
"MessagePodcastSearchField": "Въведи какво да търся или RSS емисия адрес",
|
||||||
"MessageQuickMatchDescription": "Попълни празните детайли и корици с първия резултат от '{0}'. Не презаписва детайлите, освен ако не е активирана настройката 'Предпочети съвпадащи метаданни' на сървъра.",
|
"MessageQuickMatchDescription": "Попълни празните детайли и корици с първия резултат от '{0}'. Не презаписва детайлите, освен ако не е активирана настройката 'Предпочети съвпадащи метаданни' на сървъра.",
|
||||||
"MessageRemoveChapter": "Премахни глава",
|
"MessageRemoveChapter": "Премахни глава",
|
||||||
"MessageRemoveEpisodes": "Премахни {0} епизод(и)",
|
"MessageRemoveEpisodes": "Премахни {0} епизод(и)",
|
||||||
"MessageRemoveFromPlayerQueue": "Премахни от опашката на плейъра",
|
"MessageRemoveFromPlayerQueue": "Премахни от опашката на плейъра",
|
||||||
"MessageRemoveUserWarning": "Сигурни ли сте, че искате да изтриете потребител \"{0}\" завинаги?",
|
"MessageRemoveUserWarning": "Сигурни ли сте, че искате да изтриете потребител \"{0}\" завинаги?",
|
||||||
"MessageReportBugsAndContribute": "Съобщавайте за грешки, заявявайте функции и допринасяйте на",
|
"MessageReportBugsAndContribute": "Докладвайте грешки, поискайте нови функции и допринасяйте на",
|
||||||
"MessageResetChaptersConfirm": "Сигурни ли сте, че искате да нулирате главите и да отмените промените, които сте направили?",
|
"MessageResetChaptersConfirm": "Сигурни ли сте, че искате да нулирате главите и да отмените промените, които сте направили?",
|
||||||
"MessageRestoreBackupConfirm": "Сигурни ли сте, че искате да възстановите архива създаден на",
|
"MessageRestoreBackupConfirm": "Сигурни ли сте, че искате да възстановите архива създаден на",
|
||||||
"MessageRestoreBackupWarning": "Възстановяването на архив ще презапише цялата база данни, намираща се в /config и кориците в /metadata/items & /metadata/authors.<br /><br />Архивите не променят файловете в папките на вашата библиотека. Ако сте активирали настройките на сървъра за съхранение на корици и метаданни в папките на вашата библиотека, те няма да бъдат архивирани или презаписани.<br /><br />Всички клиенти, използващи вашия сървър, ще бъдат автоматично обновени.",
|
"MessageRestoreBackupWarning": "Възстановяването на архив ще презапише цялата база данни, намираща се в /config и кориците в /metadata/items & /metadata/authors.<br /><br />Архивите не променят файловете в папките на вашата библиотека. Ако сте активирали настройките на сървъра за съхранение на корици и метаданни в папките на вашата библиотека, те няма да бъдат архивирани или презаписани.<br /><br />Всички клиенти, използващи вашия сървър, ще бъдат автоматично обновени.",
|
||||||
@@ -700,8 +768,8 @@
|
|||||||
"NoteChangeRootPassword": "Root потребителят е единственият потребител, който може да има празна парола",
|
"NoteChangeRootPassword": "Root потребителят е единственият потребител, който може да има празна парола",
|
||||||
"NoteChapterEditorTimes": "Забележка: Първото време на начало на главата трябва да остане на 0:00, а последното време на начало на главата не може да надвишава продължителността на тази аудиокнига.",
|
"NoteChapterEditorTimes": "Забележка: Първото време на начало на главата трябва да остане на 0:00, а последното време на начало на главата не може да надвишава продължителността на тази аудиокнига.",
|
||||||
"NoteFolderPicker": "Забележка: папките, които вече са картографирани, няма да бъдат показани",
|
"NoteFolderPicker": "Забележка: папките, които вече са картографирани, няма да бъдат показани",
|
||||||
"NoteRSSFeedPodcastAppsHttps": "Внимание: Повечето приложения за подкасти изискват URL адреса на RSS feed да използва HTTPS",
|
"NoteRSSFeedPodcastAppsHttps": "Предупреждение: Повечето приложения за подкасти изискват URL адресът на RSS емисията да използва HTTPS",
|
||||||
"NoteRSSFeedPodcastAppsPubDate": "Внимание: 1 или повече от вашите епизоди нямат дата на публикуване. Някои приложения за подкасти изискват това",
|
"NoteRSSFeedPodcastAppsPubDate": "Предупреждение: Един или повече от вашите епизоди нямат дата на публикуване. Някои приложения за подкасти изискват това.",
|
||||||
"NoteUploaderFoldersWithMediaFiles": "Папките с медийни файлове ще бъдат обработени като отделни елементи на библиотеката.",
|
"NoteUploaderFoldersWithMediaFiles": "Папките с медийни файлове ще бъдат обработени като отделни елементи на библиотеката.",
|
||||||
"NoteUploaderOnlyAudioFiles": "Ако качвате само аудио файлове, то всеки аудио файл ще бъде обработен като отделна аудиокнига.",
|
"NoteUploaderOnlyAudioFiles": "Ако качвате само аудио файлове, то всеки аудио файл ще бъде обработен като отделна аудиокнига.",
|
||||||
"NoteUploaderUnsupportedFiles": "Неподдържаните файлове се игнорират. При избор или пускане на папка, други файлове, които не са в папка на елемент, се игнорират.",
|
"NoteUploaderUnsupportedFiles": "Неподдържаните файлове се игнорират. При избор или пускане на папка, други файлове, които не са в папка на елемент, се игнорират.",
|
||||||
@@ -722,18 +790,25 @@
|
|||||||
"ToastBackupRestoreFailed": "Неуспешно възстановяване на архив",
|
"ToastBackupRestoreFailed": "Неуспешно възстановяване на архив",
|
||||||
"ToastBackupUploadFailed": "Неуспешно качване на архив",
|
"ToastBackupUploadFailed": "Неуспешно качване на архив",
|
||||||
"ToastBackupUploadSuccess": "Архивът е качен",
|
"ToastBackupUploadSuccess": "Архивът е качен",
|
||||||
|
"ToastBatchUpdateFailed": "Неуспешно групово актуализиране",
|
||||||
|
"ToastBatchUpdateSuccess": "Успешно групово актуализиране",
|
||||||
"ToastBookmarkCreateFailed": "Неуспешно създаване на отметка",
|
"ToastBookmarkCreateFailed": "Неуспешно създаване на отметка",
|
||||||
"ToastBookmarkCreateSuccess": "Отметката е създадена",
|
"ToastBookmarkCreateSuccess": "Отметката е създадена",
|
||||||
"ToastBookmarkRemoveSuccess": "Отметката е премахната",
|
"ToastBookmarkRemoveSuccess": "Отметката е премахната",
|
||||||
|
"ToastCachePurgeFailed": "Неуспешно изчистване на кеша",
|
||||||
|
"ToastCachePurgeSuccess": "Успешно изчистване на кеша",
|
||||||
"ToastChaptersHaveErrors": "Главите имат грешки",
|
"ToastChaptersHaveErrors": "Главите имат грешки",
|
||||||
"ToastChaptersMustHaveTitles": "Главите трябва да имат заглавия",
|
"ToastChaptersMustHaveTitles": "Главите трябва да имат заглавия",
|
||||||
"ToastCollectionRemoveSuccess": "Колекцията е премахната",
|
"ToastCollectionRemoveSuccess": "Колекцията е премахната",
|
||||||
"ToastCollectionUpdateSuccess": "Колекцията е обновена",
|
"ToastCollectionUpdateSuccess": "Колекцията е обновена",
|
||||||
|
"ToastDeleteFileFailed": "Неуспешно изтриване на файла",
|
||||||
|
"ToastDeleteFileSuccess": "Успешно изтриване на файла",
|
||||||
|
"ToastFailedToLoadData": "Неуспешно зареждане на данни",
|
||||||
"ToastItemCoverUpdateSuccess": "Корицата на елемента е обновена",
|
"ToastItemCoverUpdateSuccess": "Корицата на елемента е обновена",
|
||||||
"ToastItemDetailsUpdateSuccess": "Детайлите на елемента са обновени",
|
"ToastItemDetailsUpdateSuccess": "Детайлите на елемента са обновени",
|
||||||
"ToastItemMarkedAsFinishedFailed": "Неуспешно маркиране като завършено",
|
"ToastItemMarkedAsFinishedFailed": "Неуспешно маркиране като Завършено",
|
||||||
"ToastItemMarkedAsFinishedSuccess": "Елементът е маркиран като завършен",
|
"ToastItemMarkedAsFinishedSuccess": "Елементът е маркиран като завършен",
|
||||||
"ToastItemMarkedAsNotFinishedFailed": "Неуспешно маркиране като незавършено",
|
"ToastItemMarkedAsNotFinishedFailed": "Неуспешно маркиране като Незавършено",
|
||||||
"ToastItemMarkedAsNotFinishedSuccess": "Елементът е маркиран като незавършен",
|
"ToastItemMarkedAsNotFinishedSuccess": "Елементът е маркиран като незавършен",
|
||||||
"ToastLibraryCreateFailed": "Неуспешно създаване на библиотека",
|
"ToastLibraryCreateFailed": "Неуспешно създаване на библиотека",
|
||||||
"ToastLibraryCreateSuccess": "Библиотеката \"{0}\" е създадена",
|
"ToastLibraryCreateSuccess": "Библиотеката \"{0}\" е създадена",
|
||||||
@@ -747,20 +822,23 @@
|
|||||||
"ToastPlaylistRemoveSuccess": "Плейлистът е премахнат",
|
"ToastPlaylistRemoveSuccess": "Плейлистът е премахнат",
|
||||||
"ToastPlaylistUpdateSuccess": "Плейлистът е обновен",
|
"ToastPlaylistUpdateSuccess": "Плейлистът е обновен",
|
||||||
"ToastPodcastCreateFailed": "Неуспешно създаване на подкаст",
|
"ToastPodcastCreateFailed": "Неуспешно създаване на подкаст",
|
||||||
"ToastPodcastCreateSuccess": "Подкастът е създаден",
|
"ToastPodcastCreateSuccess": "Подкаст успешно създаден",
|
||||||
"ToastRSSFeedCloseFailed": "Неуспешно затваряне на RSS feed",
|
"ToastRSSFeedCloseFailed": "Неуспешно затваряне на RSS емисията",
|
||||||
"ToastRSSFeedCloseSuccess": "RSS feed затворен",
|
"ToastRSSFeedCloseSuccess": "RSS емисията е затворена",
|
||||||
"ToastRemoveItemFromCollectionFailed": "Неуспешно премахване на елемент от колекция",
|
"ToastRemoveItemFromCollectionFailed": "Неуспешно премахване на елемент от колекция",
|
||||||
"ToastRemoveItemFromCollectionSuccess": "Елементът е премахнат от колекция",
|
"ToastRemoveItemFromCollectionSuccess": "Елементът е премахнат от колекция",
|
||||||
"ToastSendEbookToDeviceFailed": "Неуспешно изпращане на електронна книга до устройство",
|
"ToastSendEbookToDeviceFailed": "Неуспешно изпращане на електронна книга до устройство",
|
||||||
"ToastSendEbookToDeviceSuccess": "Електронната книга е изпратена до устройство \"{0}\"",
|
"ToastSendEbookToDeviceSuccess": "Електронната книга е изпратена до устройство \"{0}\"",
|
||||||
"ToastSeriesUpdateFailed": "Неуспешно обновяване на серия",
|
"ToastSeriesUpdateFailed": "Неуспешно обновяване на серия",
|
||||||
"ToastSeriesUpdateSuccess": "Серията е обновена",
|
"ToastSeriesUpdateSuccess": "Серията е обновена",
|
||||||
|
"ToastServerSettingsUpdateSuccess": "Настройките на сървъра са актуализирани",
|
||||||
"ToastSessionDeleteFailed": "Неуспешно изтриване на сесия",
|
"ToastSessionDeleteFailed": "Неуспешно изтриване на сесия",
|
||||||
"ToastSessionDeleteSuccess": "Сесията е изтрита",
|
"ToastSessionDeleteSuccess": "Сесията е изтрита",
|
||||||
"ToastSocketConnected": "Свързан сокет",
|
"ToastSocketConnected": "Свързан сокет",
|
||||||
"ToastSocketDisconnected": "Сокетът е прекъснат",
|
"ToastSocketDisconnected": "Сокетът е прекъснат",
|
||||||
"ToastSocketFailedToConnect": "Неуспешно свързване на сокет",
|
"ToastSocketFailedToConnect": "Неуспешно свързване на сокет",
|
||||||
|
"ToastSortingPrefixesEmptyError": "Трябва да има поне 1 префикс за сортиране",
|
||||||
|
"ToastSortingPrefixesUpdateSuccess": "Префиксите за сортиране са актуализирани ({0} елемента)",
|
||||||
"ToastUserDeleteFailed": "Неуспешно изтриване на потребител",
|
"ToastUserDeleteFailed": "Неуспешно изтриване на потребител",
|
||||||
"ToastUserDeleteSuccess": "Потребителят е изтрит"
|
"ToastUserDeleteSuccess": "Потребителят е изтрит"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -217,6 +217,7 @@
|
|||||||
"LabelAccountTypeAdmin": "Správce",
|
"LabelAccountTypeAdmin": "Správce",
|
||||||
"LabelAccountTypeGuest": "Host",
|
"LabelAccountTypeGuest": "Host",
|
||||||
"LabelAccountTypeUser": "Uživatel",
|
"LabelAccountTypeUser": "Uživatel",
|
||||||
|
"LabelActivities": "Aktivity",
|
||||||
"LabelActivity": "Aktivita",
|
"LabelActivity": "Aktivita",
|
||||||
"LabelAddToCollection": "Přidat do kolekce",
|
"LabelAddToCollection": "Přidat do kolekce",
|
||||||
"LabelAddToCollectionBatch": "Přidat {0} knihy do kolekce",
|
"LabelAddToCollectionBatch": "Přidat {0} knihy do kolekce",
|
||||||
@@ -389,6 +390,7 @@
|
|||||||
"LabelIntervalEvery6Hours": "Každých 6 hodin",
|
"LabelIntervalEvery6Hours": "Každých 6 hodin",
|
||||||
"LabelIntervalEveryDay": "Každý den",
|
"LabelIntervalEveryDay": "Každý den",
|
||||||
"LabelIntervalEveryHour": "Každou hodinu",
|
"LabelIntervalEveryHour": "Každou hodinu",
|
||||||
|
"LabelIntervalEveryMinute": "Každou minutu",
|
||||||
"LabelInvert": "Invertovat",
|
"LabelInvert": "Invertovat",
|
||||||
"LabelItem": "Položka",
|
"LabelItem": "Položka",
|
||||||
"LabelJumpBackwardAmount": "Přeskočit zpět o",
|
"LabelJumpBackwardAmount": "Přeskočit zpět o",
|
||||||
@@ -484,6 +486,7 @@
|
|||||||
"LabelPersonalYearReview": "Váš přehled roku ({0})",
|
"LabelPersonalYearReview": "Váš přehled roku ({0})",
|
||||||
"LabelPhotoPathURL": "Cesta k fotografii/URL",
|
"LabelPhotoPathURL": "Cesta k fotografii/URL",
|
||||||
"LabelPlayMethod": "Metoda přehrávání",
|
"LabelPlayMethod": "Metoda přehrávání",
|
||||||
|
"LabelPlaybackRateIncrementDecrement": "Velikost kroku pro změnu rychlosti přehrávání",
|
||||||
"LabelPlayerChapterNumberMarker": "{0} z {1}",
|
"LabelPlayerChapterNumberMarker": "{0} z {1}",
|
||||||
"LabelPlaylists": "Seznamy skladeb",
|
"LabelPlaylists": "Seznamy skladeb",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
@@ -706,6 +709,7 @@
|
|||||||
"MessageBackupsLocationPathEmpty": "Umístění záloh nemůže být prázdné",
|
"MessageBackupsLocationPathEmpty": "Umístění záloh nemůže být prázdné",
|
||||||
"MessageBatchQuickMatchDescription": "Rychlá párování se pokusí přidat chybějící obálky a metadata pro vybrané položky. Povolením níže uvedených možností umožníte funkci Rychlé párování přepsat stávající obálky a/nebo metadata.",
|
"MessageBatchQuickMatchDescription": "Rychlá párování se pokusí přidat chybějící obálky a metadata pro vybrané položky. Povolením níže uvedených možností umožníte funkci Rychlé párování přepsat stávající obálky a/nebo metadata.",
|
||||||
"MessageBookshelfNoCollections": "Ještě jste nevytvořili žádnou sbírku",
|
"MessageBookshelfNoCollections": "Ještě jste nevytvořili žádnou sbírku",
|
||||||
|
"MessageBookshelfNoCollectionsHelp": "Kolekce jsou veřejné. Mohou je zobrazit všichni uživatelé s přístupem do knihovny.",
|
||||||
"MessageBookshelfNoRSSFeeds": "Nejsou otevřeny žádné RSS kanály",
|
"MessageBookshelfNoRSSFeeds": "Nejsou otevřeny žádné RSS kanály",
|
||||||
"MessageBookshelfNoResultsForFilter": "Filtr \"{0}: {1}\"",
|
"MessageBookshelfNoResultsForFilter": "Filtr \"{0}: {1}\"",
|
||||||
"MessageBookshelfNoResultsForQuery": "Žádné výsledky pro dotaz",
|
"MessageBookshelfNoResultsForQuery": "Žádné výsledky pro dotaz",
|
||||||
@@ -816,6 +820,7 @@
|
|||||||
"MessageNoTasksRunning": "Nejsou spuštěny žádné úlohy",
|
"MessageNoTasksRunning": "Nejsou spuštěny žádné úlohy",
|
||||||
"MessageNoUpdatesWereNecessary": "Nebyly nutné žádné aktualizace",
|
"MessageNoUpdatesWereNecessary": "Nebyly nutné žádné aktualizace",
|
||||||
"MessageNoUserPlaylists": "Nemáte žádné seznamy skladeb",
|
"MessageNoUserPlaylists": "Nemáte žádné seznamy skladeb",
|
||||||
|
"MessageNoUserPlaylistsHelp": "Seznamy skladeb jsou soukromé. Zobrazit je může pouze uživatel, který je vytvořil.",
|
||||||
"MessageNotYetImplemented": "Ještě není implementováno",
|
"MessageNotYetImplemented": "Ještě není implementováno",
|
||||||
"MessageOpmlPreviewNote": "Poznámka: Toto je náhled načteného OMPL souboru. Aktuální název podcastu bude načten z RSS feedu.",
|
"MessageOpmlPreviewNote": "Poznámka: Toto je náhled načteného OMPL souboru. Aktuální název podcastu bude načten z RSS feedu.",
|
||||||
"MessageOr": "nebo",
|
"MessageOr": "nebo",
|
||||||
|
|||||||
+17
-10
@@ -5,11 +5,13 @@
|
|||||||
"ButtonAddLibrary": "Tilføj Bibliotek",
|
"ButtonAddLibrary": "Tilføj Bibliotek",
|
||||||
"ButtonAddPodcasts": "Tilføj podcasts",
|
"ButtonAddPodcasts": "Tilføj podcasts",
|
||||||
"ButtonAddUser": "Tilføj bruger",
|
"ButtonAddUser": "Tilføj bruger",
|
||||||
"ButtonAddYourFirstLibrary": "Tilføj din første bibliotek",
|
"ButtonAddYourFirstLibrary": "Tilføj dit første bibliotek",
|
||||||
"ButtonApply": "Anvend",
|
"ButtonApply": "Anvend",
|
||||||
"ButtonApplyChapters": "Anvend kapitler",
|
"ButtonApplyChapters": "Anvend kapitler",
|
||||||
"ButtonAuthors": "Forfattere",
|
"ButtonAuthors": "Forfattere",
|
||||||
"ButtonBack": "Tilbage",
|
"ButtonBack": "Tilbage",
|
||||||
|
"ButtonBatchEditPopulateFromExisting": "Opret fra eksisterende",
|
||||||
|
"ButtonBatchEditPopulateMapDetails": "Opret fra kortlægnings detaljer",
|
||||||
"ButtonBrowseForFolder": "Gennemse mappe",
|
"ButtonBrowseForFolder": "Gennemse mappe",
|
||||||
"ButtonCancel": "Annuller",
|
"ButtonCancel": "Annuller",
|
||||||
"ButtonCancelEncode": "Annuller kodning",
|
"ButtonCancelEncode": "Annuller kodning",
|
||||||
@@ -91,7 +93,7 @@
|
|||||||
"ButtonScrollLeft": "Rul til Venstre",
|
"ButtonScrollLeft": "Rul til Venstre",
|
||||||
"ButtonScrollRight": "Rul til Højre",
|
"ButtonScrollRight": "Rul til Højre",
|
||||||
"ButtonSearch": "Søg",
|
"ButtonSearch": "Søg",
|
||||||
"ButtonSelectFolderPath": "Vælg Mappen Sti",
|
"ButtonSelectFolderPath": "Vælg Mappe Sti",
|
||||||
"ButtonSeries": "Serier",
|
"ButtonSeries": "Serier",
|
||||||
"ButtonSetChaptersFromTracks": "Sæt kapitler fra spor",
|
"ButtonSetChaptersFromTracks": "Sæt kapitler fra spor",
|
||||||
"ButtonShare": "Del",
|
"ButtonShare": "Del",
|
||||||
@@ -213,7 +215,7 @@
|
|||||||
"LabelAbridgedChecked": "Forkortet (kontrolleret)",
|
"LabelAbridgedChecked": "Forkortet (kontrolleret)",
|
||||||
"LabelAbridgedUnchecked": "Uforkortet (ikke kontrolleret)",
|
"LabelAbridgedUnchecked": "Uforkortet (ikke kontrolleret)",
|
||||||
"LabelAccessibleBy": "Tilgængelig af",
|
"LabelAccessibleBy": "Tilgængelig af",
|
||||||
"LabelAccountType": "Kontotype",
|
"LabelAccountType": "Brugertype",
|
||||||
"LabelAccountTypeAdmin": "Administrator",
|
"LabelAccountTypeAdmin": "Administrator",
|
||||||
"LabelAccountTypeGuest": "Gæst",
|
"LabelAccountTypeGuest": "Gæst",
|
||||||
"LabelAccountTypeUser": "Bruger",
|
"LabelAccountTypeUser": "Bruger",
|
||||||
@@ -224,7 +226,7 @@
|
|||||||
"LabelAddToPlaylistBatch": "Tilføj {0} Elementer til Afspilningsliste",
|
"LabelAddToPlaylistBatch": "Tilføj {0} Elementer til Afspilningsliste",
|
||||||
"LabelAddedAt": "Tilføjet",
|
"LabelAddedAt": "Tilføjet",
|
||||||
"LabelAddedDate": "Tilføjet {0}",
|
"LabelAddedDate": "Tilføjet {0}",
|
||||||
"LabelAdminUsersOnly": "Kun Administratorbrugere",
|
"LabelAdminUsersOnly": "Kun Administratorer",
|
||||||
"LabelAll": "Alle",
|
"LabelAll": "Alle",
|
||||||
"LabelAllUsers": "Alle Brugere",
|
"LabelAllUsers": "Alle Brugere",
|
||||||
"LabelAllUsersExcludingGuests": "Alle bruger eksklusiv gæster",
|
"LabelAllUsersExcludingGuests": "Alle bruger eksklusiv gæster",
|
||||||
@@ -443,7 +445,7 @@
|
|||||||
"LabelNarrator": "Fortæller",
|
"LabelNarrator": "Fortæller",
|
||||||
"LabelNarrators": "Fortællere",
|
"LabelNarrators": "Fortællere",
|
||||||
"LabelNew": "Ny",
|
"LabelNew": "Ny",
|
||||||
"LabelNewPassword": "Nyt kodeord",
|
"LabelNewPassword": "Ny adgangskode",
|
||||||
"LabelNewestAuthors": "Nyeste forfattere",
|
"LabelNewestAuthors": "Nyeste forfattere",
|
||||||
"LabelNewestEpisodes": "Nyeste episoder",
|
"LabelNewestEpisodes": "Nyeste episoder",
|
||||||
"LabelNextBackupDate": "Næste sikkerhedskopi dato",
|
"LabelNextBackupDate": "Næste sikkerhedskopi dato",
|
||||||
@@ -465,12 +467,12 @@
|
|||||||
"LabelNumberOfBooks": "Antal bøger",
|
"LabelNumberOfBooks": "Antal bøger",
|
||||||
"LabelNumberOfEpisodes": "# afsnit",
|
"LabelNumberOfEpisodes": "# afsnit",
|
||||||
"LabelOpenIDAdvancedPermsClaimDescription": "Navnet af OpenID claimet som indeholder avancerede brugerhandlinger inden i applikationen som vil gælde for ikke administrative roller (<b>hvis konfigureret</b>). Hvis et claim mangler fra svaret vil adgang til ABS blive nægtet. Hvis en enkelt indstilling/option mangler, vil det bliver behandlet som <code>false</code>. Sørg for at identity provider's claim matcher den forventede struktur:",
|
"LabelOpenIDAdvancedPermsClaimDescription": "Navnet af OpenID claimet som indeholder avancerede brugerhandlinger inden i applikationen som vil gælde for ikke administrative roller (<b>hvis konfigureret</b>). Hvis et claim mangler fra svaret vil adgang til ABS blive nægtet. Hvis en enkelt indstilling/option mangler, vil det bliver behandlet som <code>false</code>. Sørg for at identity provider's claim matcher den forventede struktur:",
|
||||||
"LabelOpenIDClaims": "Efterlad de følgende indstillinger tomme for at deaktivere avancerede grupper og adgangsstyring for automatisk at tilføje dem til 'User' gruppen.",
|
"LabelOpenIDClaims": "Efterlad de følgende indstillinger tomme for at deaktivere avanceret gruppe og adgangsindstilling, ved automatisk at assigne 'Bruger' grupper.",
|
||||||
"LabelOpenIDGroupClaimDescription": "Navnet af det OpenID claim som skal indeholde brugerens grupper. Mest kendt som <code>groups</code>. <b>hvis konfigureret</b>, vil applikationen automatiske tildele roller baseret p[ brugerens gruppemedlemsskaber, givet disse grupper er navngivet (uden forbehold for store og små bogstaver) 'admin', 'user' eller 'guest' i claimet. Claimet burde indeholde en liste (og hvis brugeren tilhøre flere grupper) som applikationen vil tildele roller med højeste adgangsnvieau. Hvis ingen grupper matcher vil adgang blive nægtet.",
|
"LabelOpenIDGroupClaimDescription": "Navnet af det OpenID claim som skal indeholde brugerens grupper. Mest kendt som <code>groups</code>. <b>hvis konfigureret</b>, vil applikationen automatiske tildele roller baseret p[ brugerens gruppemedlemsskaber, givet disse grupper er navngivet (uden forbehold for store og små bogstaver) 'admin', 'user' eller 'guest' i claimet. Claimet burde indeholde en liste (og hvis brugeren tilhøre flere grupper) som applikationen vil tildele roller med højeste adgangsnvieau. Hvis ingen grupper matcher vil adgang blive nægtet.",
|
||||||
"LabelOpenRSSFeed": "Åbn RSS-feed",
|
"LabelOpenRSSFeed": "Åbn RSS-feed",
|
||||||
"LabelOverwrite": "Overskriv",
|
"LabelOverwrite": "Overskriv",
|
||||||
"LabelPaginationPageXOfY": "Side {0} af {1}",
|
"LabelPaginationPageXOfY": "Side {0} af {1}",
|
||||||
"LabelPassword": "Kodeord",
|
"LabelPassword": "Adgangskode",
|
||||||
"LabelPath": "Sti",
|
"LabelPath": "Sti",
|
||||||
"LabelPermanent": "Permanent",
|
"LabelPermanent": "Permanent",
|
||||||
"LabelPermissionsAccessAllLibraries": "Kan få adgang til alle biblioteker",
|
"LabelPermissionsAccessAllLibraries": "Kan få adgang til alle biblioteker",
|
||||||
@@ -484,6 +486,7 @@
|
|||||||
"LabelPersonalYearReview": "Dit år i review ({0})",
|
"LabelPersonalYearReview": "Dit år i review ({0})",
|
||||||
"LabelPhotoPathURL": "Foto sti/URL",
|
"LabelPhotoPathURL": "Foto sti/URL",
|
||||||
"LabelPlayMethod": "Afspilningsmetode",
|
"LabelPlayMethod": "Afspilningsmetode",
|
||||||
|
"LabelPlaybackRateIncrementDecrement": "Afspilningshastighed øges/sænkes med",
|
||||||
"LabelPlayerChapterNumberMarker": "{0} af {1}",
|
"LabelPlayerChapterNumberMarker": "{0} af {1}",
|
||||||
"LabelPlaylists": "Afspilningslister",
|
"LabelPlaylists": "Afspilningslister",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
@@ -573,12 +576,12 @@
|
|||||||
"LabelSettingsLibraryMarkAsFinishedWhen": "Marker medie indhold som færdigt når",
|
"LabelSettingsLibraryMarkAsFinishedWhen": "Marker medie indhold som færdigt når",
|
||||||
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Spring til tidligere bøger i Fortsæt serie",
|
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Spring til tidligere bøger i Fortsæt serie",
|
||||||
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Fortsæt Serien siden hylde viser de første bøger som ikke er startet i serier med mindst en bog som ikke er startet og ingen bøger i gang. Aktivering af denne indstilling vil fortsætte serien fra den sidst gennemførte bog modsat den først ikke startede bog.",
|
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Fortsæt Serien siden hylde viser de første bøger som ikke er startet i serier med mindst en bog som ikke er startet og ingen bøger i gang. Aktivering af denne indstilling vil fortsætte serien fra den sidst gennemførte bog modsat den først ikke startede bog.",
|
||||||
"LabelSettingsParseSubtitles": "Fortolk undertekster",
|
"LabelSettingsParseSubtitles": "Fortolk undertitler",
|
||||||
"LabelSettingsParseSubtitlesHelp": "Udtræk undertekster fra lydbogsmappenavne.<br>Undertitler skal adskilles af \" - \"<br>f.eks. \"Bogtitel - En undertitel her\" har undertitlen \"En undertitel her\"",
|
"LabelSettingsParseSubtitlesHelp": "Udtræk undertekster fra lydbogsmappenavne.<br>Undertitler skal adskilles af \" - \"<br>f.eks. \"Bogtitel - En undertitel her\" har undertitlen \"En undertitel her\"",
|
||||||
"LabelSettingsPreferMatchedMetadata": "Foretræk matchede metadata",
|
"LabelSettingsPreferMatchedMetadata": "Foretræk matchede metadata",
|
||||||
"LabelSettingsPreferMatchedMetadataHelp": "Matchede data vil tilsidesætte elementdetaljer ved brug af Hurtig Match. Som standard udfylder Hurtig Match kun manglende detaljer.",
|
"LabelSettingsPreferMatchedMetadataHelp": "Matchede data vil tilsidesætte elementdetaljer ved brug af Hurtig Match. Som standard udfylder Hurtig Match kun manglende detaljer.",
|
||||||
"LabelSettingsSkipMatchingBooksWithASIN": "Spring over matchende bøger, der allerede har en ASIN",
|
"LabelSettingsSkipMatchingBooksWithASIN": "Spring over matchende bøger, der allerede har en ASIN",
|
||||||
"LabelSettingsSkipMatchingBooksWithISBN": "Spring over matchende bøger, der allerede har en ISBN",
|
"LabelSettingsSkipMatchingBooksWithISBN": "Spring matchende bøger over, som allerede har et ISBN-nummer",
|
||||||
"LabelSettingsSortingIgnorePrefixes": "Ignorer præfikser ved sortering",
|
"LabelSettingsSortingIgnorePrefixes": "Ignorer præfikser ved sortering",
|
||||||
"LabelSettingsSortingIgnorePrefixesHelp": "f.eks. for præfikset \"the\" vil bogtitlen \"The Book Title\" blive sorteret som \"Book Title, The\"",
|
"LabelSettingsSortingIgnorePrefixesHelp": "f.eks. for præfikset \"the\" vil bogtitlen \"The Book Title\" blive sorteret som \"Book Title, The\"",
|
||||||
"LabelSettingsSquareBookCovers": "Brug kvadratiske bogomslag",
|
"LabelSettingsSquareBookCovers": "Brug kvadratiske bogomslag",
|
||||||
@@ -662,7 +665,7 @@
|
|||||||
"LabelTrailer": "Trailer",
|
"LabelTrailer": "Trailer",
|
||||||
"LabelType": "Type",
|
"LabelType": "Type",
|
||||||
"LabelUnabridged": "Uforkortet",
|
"LabelUnabridged": "Uforkortet",
|
||||||
"LabelUndo": "Undo",
|
"LabelUndo": "Fortryd",
|
||||||
"LabelUnknown": "Ukendt",
|
"LabelUnknown": "Ukendt",
|
||||||
"LabelUnknownPublishDate": "Ukendt publiceringsdato",
|
"LabelUnknownPublishDate": "Ukendt publiceringsdato",
|
||||||
"LabelUpdateCover": "Opdater omslag",
|
"LabelUpdateCover": "Opdater omslag",
|
||||||
@@ -704,8 +707,11 @@
|
|||||||
"MessageBackupsLocationEditNote": "Note: Opdatering af backup sti vil ikke fjerne eller modificere eksisterende backups",
|
"MessageBackupsLocationEditNote": "Note: Opdatering af backup sti vil ikke fjerne eller modificere eksisterende backups",
|
||||||
"MessageBackupsLocationNoEditNote": "Note: Backup sti er sat igennem miljøvariabel og kan ikke ændres her.",
|
"MessageBackupsLocationNoEditNote": "Note: Backup sti er sat igennem miljøvariabel og kan ikke ændres her.",
|
||||||
"MessageBackupsLocationPathEmpty": "Backup sti kan ikke være tom",
|
"MessageBackupsLocationPathEmpty": "Backup sti kan ikke være tom",
|
||||||
|
"MessageBatchEditPopulateMapDetailsAllHelp": "Opret felter slået til med data fra alle genstande. Felter med flere værdier vil blive sammenflettet",
|
||||||
|
"MessageBatchEditPopulateMapDetailsItemHelp": "Opret kort med værdier der er slået til fra felter med data fra denne genstand",
|
||||||
"MessageBatchQuickMatchDescription": "Quick Match vil forsøge at tilføje manglende omslag og metadata til de valgte elementer. Aktivér indstillingerne nedenfor for at tillade Quick Match at overskrive eksisterende omslag og/eller metadata.",
|
"MessageBatchQuickMatchDescription": "Quick Match vil forsøge at tilføje manglende omslag og metadata til de valgte elementer. Aktivér indstillingerne nedenfor for at tillade Quick Match at overskrive eksisterende omslag og/eller metadata.",
|
||||||
"MessageBookshelfNoCollections": "Du har ikke oprettet nogen samlinger endnu",
|
"MessageBookshelfNoCollections": "Du har ikke oprettet nogen samlinger endnu",
|
||||||
|
"MessageBookshelfNoCollectionsHelp": "Samlinger er offentlige. Alle brugere med adgang til biblioteket kan se dem.",
|
||||||
"MessageBookshelfNoRSSFeeds": "Ingen RSS-feeds er åbne",
|
"MessageBookshelfNoRSSFeeds": "Ingen RSS-feeds er åbne",
|
||||||
"MessageBookshelfNoResultsForFilter": "Ingen resultater for filter \"{0}: {1}\"",
|
"MessageBookshelfNoResultsForFilter": "Ingen resultater for filter \"{0}: {1}\"",
|
||||||
"MessageBookshelfNoResultsForQuery": "Intet resultat for query",
|
"MessageBookshelfNoResultsForQuery": "Intet resultat for query",
|
||||||
@@ -816,6 +822,7 @@
|
|||||||
"MessageNoTasksRunning": "Ingen opgaver kører",
|
"MessageNoTasksRunning": "Ingen opgaver kører",
|
||||||
"MessageNoUpdatesWereNecessary": "Ingen opdateringer var nødvendige",
|
"MessageNoUpdatesWereNecessary": "Ingen opdateringer var nødvendige",
|
||||||
"MessageNoUserPlaylists": "Du har ingen afspilningslister",
|
"MessageNoUserPlaylists": "Du har ingen afspilningslister",
|
||||||
|
"MessageNoUserPlaylistsHelp": "Playlister er private. Kun brugere som opretter dem kan se dem.",
|
||||||
"MessageNotYetImplemented": "Endnu ikke implementeret",
|
"MessageNotYetImplemented": "Endnu ikke implementeret",
|
||||||
"MessageOpmlPreviewNote": "Note: Dette er en forhåndsvisning af den indlæste OPML fil. Podcast titel vil blive taget fra RSS feedet.",
|
"MessageOpmlPreviewNote": "Note: Dette er en forhåndsvisning af den indlæste OPML fil. Podcast titel vil blive taget fra RSS feedet.",
|
||||||
"MessageOr": "eller",
|
"MessageOr": "eller",
|
||||||
|
|||||||
+12
-2
@@ -10,6 +10,8 @@
|
|||||||
"ButtonApplyChapters": "Kapitel anwenden",
|
"ButtonApplyChapters": "Kapitel anwenden",
|
||||||
"ButtonAuthors": "Autoren",
|
"ButtonAuthors": "Autoren",
|
||||||
"ButtonBack": "Zurück",
|
"ButtonBack": "Zurück",
|
||||||
|
"ButtonBatchEditPopulateFromExisting": "Auffüllen aus vorhandenem",
|
||||||
|
"ButtonBatchEditPopulateMapDetails": "Kartendetails auffüllen",
|
||||||
"ButtonBrowseForFolder": "Ordnersuche",
|
"ButtonBrowseForFolder": "Ordnersuche",
|
||||||
"ButtonCancel": "Abbrechen",
|
"ButtonCancel": "Abbrechen",
|
||||||
"ButtonCancelEncode": "Codierung abbrechen",
|
"ButtonCancelEncode": "Codierung abbrechen",
|
||||||
@@ -217,7 +219,8 @@
|
|||||||
"LabelAccountTypeAdmin": "Admin",
|
"LabelAccountTypeAdmin": "Admin",
|
||||||
"LabelAccountTypeGuest": "Gast",
|
"LabelAccountTypeGuest": "Gast",
|
||||||
"LabelAccountTypeUser": "Benutzer",
|
"LabelAccountTypeUser": "Benutzer",
|
||||||
"LabelActivity": "Aktivitäten",
|
"LabelActivities": "Aktivitäten",
|
||||||
|
"LabelActivity": "Aktivität",
|
||||||
"LabelAddToCollection": "Zur Sammlung hinzufügen",
|
"LabelAddToCollection": "Zur Sammlung hinzufügen",
|
||||||
"LabelAddToCollectionBatch": "Füge {0} Hörbüch(er)/Podcast(s) der Sammlung hinzu",
|
"LabelAddToCollectionBatch": "Füge {0} Hörbüch(er)/Podcast(s) der Sammlung hinzu",
|
||||||
"LabelAddToPlaylist": "Zur Wiedergabeliste hinzufügen",
|
"LabelAddToPlaylist": "Zur Wiedergabeliste hinzufügen",
|
||||||
@@ -281,6 +284,7 @@
|
|||||||
"LabelContinueSeries": "Serien fortsetzen",
|
"LabelContinueSeries": "Serien fortsetzen",
|
||||||
"LabelCover": "Titelbild",
|
"LabelCover": "Titelbild",
|
||||||
"LabelCoverImageURL": "URL des Titelbildes",
|
"LabelCoverImageURL": "URL des Titelbildes",
|
||||||
|
"LabelCoverProvider": "Titelbildanbieter",
|
||||||
"LabelCreatedAt": "Erstellt am",
|
"LabelCreatedAt": "Erstellt am",
|
||||||
"LabelCronExpression": "Cron-Ausdruck",
|
"LabelCronExpression": "Cron-Ausdruck",
|
||||||
"LabelCurrent": "Aktuell",
|
"LabelCurrent": "Aktuell",
|
||||||
@@ -389,6 +393,7 @@
|
|||||||
"LabelIntervalEvery6Hours": "Alle 6 Stunden",
|
"LabelIntervalEvery6Hours": "Alle 6 Stunden",
|
||||||
"LabelIntervalEveryDay": "Jeden Tag",
|
"LabelIntervalEveryDay": "Jeden Tag",
|
||||||
"LabelIntervalEveryHour": "Jede Stunde",
|
"LabelIntervalEveryHour": "Jede Stunde",
|
||||||
|
"LabelIntervalEveryMinute": "Jede Minute",
|
||||||
"LabelInvert": "Umkehren",
|
"LabelInvert": "Umkehren",
|
||||||
"LabelItem": "Medium",
|
"LabelItem": "Medium",
|
||||||
"LabelJumpBackwardAmount": "Zurückspringen Zeit",
|
"LabelJumpBackwardAmount": "Zurückspringen Zeit",
|
||||||
@@ -484,6 +489,7 @@
|
|||||||
"LabelPersonalYearReview": "Dein Jahr in Übersicht ({0})",
|
"LabelPersonalYearReview": "Dein Jahr in Übersicht ({0})",
|
||||||
"LabelPhotoPathURL": "Foto Pfad/URL",
|
"LabelPhotoPathURL": "Foto Pfad/URL",
|
||||||
"LabelPlayMethod": "Abspielmethode",
|
"LabelPlayMethod": "Abspielmethode",
|
||||||
|
"LabelPlaybackRateIncrementDecrement": "Wiedergaberate der Erhöhung/Verminderung",
|
||||||
"LabelPlayerChapterNumberMarker": "{0} von {1}",
|
"LabelPlayerChapterNumberMarker": "{0} von {1}",
|
||||||
"LabelPlaylists": "Wiedergabelisten",
|
"LabelPlaylists": "Wiedergabelisten",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
@@ -645,7 +651,7 @@
|
|||||||
"LabelTimeToShift": "Zeit bis zum Wechsel in Sekunden",
|
"LabelTimeToShift": "Zeit bis zum Wechsel in Sekunden",
|
||||||
"LabelTitle": "Titel",
|
"LabelTitle": "Titel",
|
||||||
"LabelToolsEmbedMetadata": "Metadaten einbetten",
|
"LabelToolsEmbedMetadata": "Metadaten einbetten",
|
||||||
"LabelToolsEmbedMetadataDescription": "Bettet die Metadaten einschließlich des Titelbildes und der Kapitel in die Audiodatein ein.",
|
"LabelToolsEmbedMetadataDescription": "Bettet die Metadaten einschließlich des Titelbildes und der Kapitel in die Audiodateien ein.",
|
||||||
"LabelToolsM4bEncoder": "M4B Kodierer",
|
"LabelToolsM4bEncoder": "M4B Kodierer",
|
||||||
"LabelToolsMakeM4b": "M4B-Datei erstellen",
|
"LabelToolsMakeM4b": "M4B-Datei erstellen",
|
||||||
"LabelToolsMakeM4bDescription": "Erstellt eine M4B-Datei (Endung \".m4b\") welche mehrere mp3-Dateien in einer einzigen Datei inkl. derer Metadaten (Beschreibung, Titelbild, Kapitel, ...) zusammenfasst. M4B-Datei können darüber hinaus Lesezeichen speichern und mit einem Abspielschutz (Passwort) versehen werden.",
|
"LabelToolsMakeM4bDescription": "Erstellt eine M4B-Datei (Endung \".m4b\") welche mehrere mp3-Dateien in einer einzigen Datei inkl. derer Metadaten (Beschreibung, Titelbild, Kapitel, ...) zusammenfasst. M4B-Datei können darüber hinaus Lesezeichen speichern und mit einem Abspielschutz (Passwort) versehen werden.",
|
||||||
@@ -704,8 +710,10 @@
|
|||||||
"MessageBackupsLocationEditNote": "Hinweis: Durch das Aktualisieren des Backup-Speicherorts werden vorhandene Sicherungen nicht verschoben oder geändert",
|
"MessageBackupsLocationEditNote": "Hinweis: Durch das Aktualisieren des Backup-Speicherorts werden vorhandene Sicherungen nicht verschoben oder geändert",
|
||||||
"MessageBackupsLocationNoEditNote": "Hinweis: Der Sicherungsspeicherort wird über eine Umgebungsvariable festgelegt und kann hier nicht geändert werden.",
|
"MessageBackupsLocationNoEditNote": "Hinweis: Der Sicherungsspeicherort wird über eine Umgebungsvariable festgelegt und kann hier nicht geändert werden.",
|
||||||
"MessageBackupsLocationPathEmpty": "Der Backup-Pfad darf nicht leer sein",
|
"MessageBackupsLocationPathEmpty": "Der Backup-Pfad darf nicht leer sein",
|
||||||
|
"MessageBatchEditPopulateMapDetailsAllHelp": "Fülle die aktivierten Felder mit Daten aus allen Elementen. Felder mit mehreren Werten werden zusammengeführt",
|
||||||
"MessageBatchQuickMatchDescription": "Der Schnellabgleich versucht, fehlende Titelbilder und Metadaten für die ausgewählten Artikel hinzuzufügen. Aktiviere die nachstehenden Optionen, damit der Schnellabgleich vorhandene Titelbilder und/oder Metadaten überschreiben kann.",
|
"MessageBatchQuickMatchDescription": "Der Schnellabgleich versucht, fehlende Titelbilder und Metadaten für die ausgewählten Artikel hinzuzufügen. Aktiviere die nachstehenden Optionen, damit der Schnellabgleich vorhandene Titelbilder und/oder Metadaten überschreiben kann.",
|
||||||
"MessageBookshelfNoCollections": "Es wurden noch keine Sammlungen erstellt",
|
"MessageBookshelfNoCollections": "Es wurden noch keine Sammlungen erstellt",
|
||||||
|
"MessageBookshelfNoCollectionsHelp": "Sammlungen sind öffentlich. Alle Benutzer mit Zugriff auf die Bibliothek können sie sehen.",
|
||||||
"MessageBookshelfNoRSSFeeds": "Keine RSS-Feeds geöffnet",
|
"MessageBookshelfNoRSSFeeds": "Keine RSS-Feeds geöffnet",
|
||||||
"MessageBookshelfNoResultsForFilter": "Keine Ergebnisse für Filter \"{0}: {1}\"",
|
"MessageBookshelfNoResultsForFilter": "Keine Ergebnisse für Filter \"{0}: {1}\"",
|
||||||
"MessageBookshelfNoResultsForQuery": "Keine Ergebnisse für die Abfrage",
|
"MessageBookshelfNoResultsForQuery": "Keine Ergebnisse für die Abfrage",
|
||||||
@@ -816,6 +824,7 @@
|
|||||||
"MessageNoTasksRunning": "Keine laufenden Aufgaben",
|
"MessageNoTasksRunning": "Keine laufenden Aufgaben",
|
||||||
"MessageNoUpdatesWereNecessary": "Keine Aktualisierungen waren notwendig",
|
"MessageNoUpdatesWereNecessary": "Keine Aktualisierungen waren notwendig",
|
||||||
"MessageNoUserPlaylists": "Keine Wiedergabelisten vorhanden",
|
"MessageNoUserPlaylists": "Keine Wiedergabelisten vorhanden",
|
||||||
|
"MessageNoUserPlaylistsHelp": "Wiedergabelisten sind privat. Nur der Benutzer, der sie erstellt hat, kann sie sehen.",
|
||||||
"MessageNotYetImplemented": "Noch nicht implementiert",
|
"MessageNotYetImplemented": "Noch nicht implementiert",
|
||||||
"MessageOpmlPreviewNote": "Hinweis: Dies ist nur eine Vorschau der geparsten OPML Datei. Der eigentliche Podcast-Titel wird aus dem RSS-Feed übernommen.",
|
"MessageOpmlPreviewNote": "Hinweis: Dies ist nur eine Vorschau der geparsten OPML Datei. Der eigentliche Podcast-Titel wird aus dem RSS-Feed übernommen.",
|
||||||
"MessageOr": "Oder",
|
"MessageOr": "Oder",
|
||||||
@@ -838,6 +847,7 @@
|
|||||||
"MessageRestoreBackupConfirm": "Bist du dir sicher, dass du die Sicherung wiederherstellen willst, welche am",
|
"MessageRestoreBackupConfirm": "Bist du dir sicher, dass du die Sicherung wiederherstellen willst, welche am",
|
||||||
"MessageRestoreBackupWarning": "Bei der Wiederherstellung einer Sicherung wird die gesamte Datenbank unter /config und die Titelbilder in /metadata/items und /metadata/authors überschrieben.<br /><br />Bei der Sicherung werden keine Dateien in deinen Bibliotheksordnern verändert. Wenn du die Servereinstellungen aktiviert hast, um Cover und Metadaten in deinen Bibliotheksordnern zu speichern, werden diese nicht gesichert oder überschrieben.<br /><br />Alle Clients, die Ihren Server nutzen, werden automatisch aktualisiert.",
|
"MessageRestoreBackupWarning": "Bei der Wiederherstellung einer Sicherung wird die gesamte Datenbank unter /config und die Titelbilder in /metadata/items und /metadata/authors überschrieben.<br /><br />Bei der Sicherung werden keine Dateien in deinen Bibliotheksordnern verändert. Wenn du die Servereinstellungen aktiviert hast, um Cover und Metadaten in deinen Bibliotheksordnern zu speichern, werden diese nicht gesichert oder überschrieben.<br /><br />Alle Clients, die Ihren Server nutzen, werden automatisch aktualisiert.",
|
||||||
"MessageScheduleLibraryScanNote": "Für die meisten Nutzer wird empfohlen, diese Funktion deaktiviert zu lassen und stattdessen die Ordnerüberwachung aktiviert zu lassen. Die Ordnerüberwachung erkennt automatisch Änderungen in deinen Bibliotheksordnern. Da die Ordnerüberwachung jedoch nicht mit jedem Dateisystem (z.B. NFS) funktioniert, können alternativ hier geplante Bibliotheks-Scans aktiviert werden.",
|
"MessageScheduleLibraryScanNote": "Für die meisten Nutzer wird empfohlen, diese Funktion deaktiviert zu lassen und stattdessen die Ordnerüberwachung aktiviert zu lassen. Die Ordnerüberwachung erkennt automatisch Änderungen in deinen Bibliotheksordnern. Da die Ordnerüberwachung jedoch nicht mit jedem Dateisystem (z.B. NFS) funktioniert, können alternativ hier geplante Bibliotheks-Scans aktiviert werden.",
|
||||||
|
"MessageScheduleRunEveryWeekdayAtTime": "Immer {0} um {1} ausführen",
|
||||||
"MessageSearchResultsFor": "Suchergebnisse für",
|
"MessageSearchResultsFor": "Suchergebnisse für",
|
||||||
"MessageSelected": "{0} ausgewählt",
|
"MessageSelected": "{0} ausgewählt",
|
||||||
"MessageServerCouldNotBeReached": "Server kann nicht erreicht werden",
|
"MessageServerCouldNotBeReached": "Server kann nicht erreicht werden",
|
||||||
|
|||||||
@@ -219,6 +219,7 @@
|
|||||||
"LabelAccountTypeAdmin": "Admin",
|
"LabelAccountTypeAdmin": "Admin",
|
||||||
"LabelAccountTypeGuest": "Guest",
|
"LabelAccountTypeGuest": "Guest",
|
||||||
"LabelAccountTypeUser": "User",
|
"LabelAccountTypeUser": "User",
|
||||||
|
"LabelActivities": "Activities",
|
||||||
"LabelActivity": "Activity",
|
"LabelActivity": "Activity",
|
||||||
"LabelAddToCollection": "Add to Collection",
|
"LabelAddToCollection": "Add to Collection",
|
||||||
"LabelAddToCollectionBatch": "Add {0} Books to Collection",
|
"LabelAddToCollectionBatch": "Add {0} Books to Collection",
|
||||||
@@ -283,6 +284,7 @@
|
|||||||
"LabelContinueSeries": "Continue Series",
|
"LabelContinueSeries": "Continue Series",
|
||||||
"LabelCover": "Cover",
|
"LabelCover": "Cover",
|
||||||
"LabelCoverImageURL": "Cover Image URL",
|
"LabelCoverImageURL": "Cover Image URL",
|
||||||
|
"LabelCoverProvider": "Cover Provider",
|
||||||
"LabelCreatedAt": "Created At",
|
"LabelCreatedAt": "Created At",
|
||||||
"LabelCronExpression": "Cron Expression",
|
"LabelCronExpression": "Cron Expression",
|
||||||
"LabelCurrent": "Current",
|
"LabelCurrent": "Current",
|
||||||
@@ -391,6 +393,7 @@
|
|||||||
"LabelIntervalEvery6Hours": "Every 6 hours",
|
"LabelIntervalEvery6Hours": "Every 6 hours",
|
||||||
"LabelIntervalEveryDay": "Every day",
|
"LabelIntervalEveryDay": "Every day",
|
||||||
"LabelIntervalEveryHour": "Every hour",
|
"LabelIntervalEveryHour": "Every hour",
|
||||||
|
"LabelIntervalEveryMinute": "Every minute",
|
||||||
"LabelInvert": "Invert",
|
"LabelInvert": "Invert",
|
||||||
"LabelItem": "Item",
|
"LabelItem": "Item",
|
||||||
"LabelJumpBackwardAmount": "Jump backward amount",
|
"LabelJumpBackwardAmount": "Jump backward amount",
|
||||||
@@ -711,6 +714,7 @@
|
|||||||
"MessageBatchEditPopulateMapDetailsItemHelp": "Populate enabled map details fields with data from this item",
|
"MessageBatchEditPopulateMapDetailsItemHelp": "Populate enabled map details fields with data from this item",
|
||||||
"MessageBatchQuickMatchDescription": "Quick Match will attempt to add missing covers and metadata for the selected items. Enable the options below to allow Quick Match to overwrite existing covers and/or metadata.",
|
"MessageBatchQuickMatchDescription": "Quick Match will attempt to add missing covers and metadata for the selected items. Enable the options below to allow Quick Match to overwrite existing covers and/or metadata.",
|
||||||
"MessageBookshelfNoCollections": "You haven't made any collections yet",
|
"MessageBookshelfNoCollections": "You haven't made any collections yet",
|
||||||
|
"MessageBookshelfNoCollectionsHelp": "Collections are public. All users with access to the library can see them.",
|
||||||
"MessageBookshelfNoRSSFeeds": "No RSS feeds are open",
|
"MessageBookshelfNoRSSFeeds": "No RSS feeds are open",
|
||||||
"MessageBookshelfNoResultsForFilter": "No results for filter \"{0}: {1}\"",
|
"MessageBookshelfNoResultsForFilter": "No results for filter \"{0}: {1}\"",
|
||||||
"MessageBookshelfNoResultsForQuery": "No results for query",
|
"MessageBookshelfNoResultsForQuery": "No results for query",
|
||||||
@@ -821,6 +825,7 @@
|
|||||||
"MessageNoTasksRunning": "No Tasks Running",
|
"MessageNoTasksRunning": "No Tasks Running",
|
||||||
"MessageNoUpdatesWereNecessary": "No updates were necessary",
|
"MessageNoUpdatesWereNecessary": "No updates were necessary",
|
||||||
"MessageNoUserPlaylists": "You have no playlists",
|
"MessageNoUserPlaylists": "You have no playlists",
|
||||||
|
"MessageNoUserPlaylistsHelp": "Playlists are private. Only the user who creates them can see them.",
|
||||||
"MessageNotYetImplemented": "Not yet implemented",
|
"MessageNotYetImplemented": "Not yet implemented",
|
||||||
"MessageOpmlPreviewNote": "Note: This is a preview of the parsed OPML file. The actual podcast title will be taken from the RSS feed.",
|
"MessageOpmlPreviewNote": "Note: This is a preview of the parsed OPML file. The actual podcast title will be taken from the RSS feed.",
|
||||||
"MessageOr": "or",
|
"MessageOr": "or",
|
||||||
@@ -843,6 +848,7 @@
|
|||||||
"MessageRestoreBackupConfirm": "Are you sure you want to restore the backup created on",
|
"MessageRestoreBackupConfirm": "Are you sure you want to restore the backup created on",
|
||||||
"MessageRestoreBackupWarning": "Restoring a backup will overwrite the entire database located at /config and cover images in /metadata/items & /metadata/authors.<br /><br />Backups do not modify any files in your library folders. If you have enabled server settings to store cover art and metadata in your library folders then those are not backed up or overwritten.<br /><br />All clients using your server will be automatically refreshed.",
|
"MessageRestoreBackupWarning": "Restoring a backup will overwrite the entire database located at /config and cover images in /metadata/items & /metadata/authors.<br /><br />Backups do not modify any files in your library folders. If you have enabled server settings to store cover art and metadata in your library folders then those are not backed up or overwritten.<br /><br />All clients using your server will be automatically refreshed.",
|
||||||
"MessageScheduleLibraryScanNote": "For most users, it is recommended to leave this feature disabled and keep the folder watcher setting enabled. The folder watcher will automatically detect changes in your library folders. The folder watcher doesn't work for every file system (like NFS) so scheduled library scans can be used instead.",
|
"MessageScheduleLibraryScanNote": "For most users, it is recommended to leave this feature disabled and keep the folder watcher setting enabled. The folder watcher will automatically detect changes in your library folders. The folder watcher doesn't work for every file system (like NFS) so scheduled library scans can be used instead.",
|
||||||
|
"MessageScheduleRunEveryWeekdayAtTime": "Run every {0} at {1}",
|
||||||
"MessageSearchResultsFor": "Search results for",
|
"MessageSearchResultsFor": "Search results for",
|
||||||
"MessageSelected": "{0} selected",
|
"MessageSelected": "{0} selected",
|
||||||
"MessageServerCouldNotBeReached": "Server could not be reached",
|
"MessageServerCouldNotBeReached": "Server could not be reached",
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
"ButtonApplyChapters": "Appliquer aux chapitres",
|
"ButtonApplyChapters": "Appliquer aux chapitres",
|
||||||
"ButtonAuthors": "Auteurs",
|
"ButtonAuthors": "Auteurs",
|
||||||
"ButtonBack": "Retour",
|
"ButtonBack": "Retour",
|
||||||
|
"ButtonBatchEditPopulateFromExisting": "Remplir à partir de l'existant",
|
||||||
|
"ButtonBatchEditPopulateMapDetails": "Remplir les détails de la carte",
|
||||||
"ButtonBrowseForFolder": "Naviguer vers le répertoire",
|
"ButtonBrowseForFolder": "Naviguer vers le répertoire",
|
||||||
"ButtonCancel": "Annuler",
|
"ButtonCancel": "Annuler",
|
||||||
"ButtonCancelEncode": "Annuler l’encodage",
|
"ButtonCancelEncode": "Annuler l’encodage",
|
||||||
@@ -484,6 +486,7 @@
|
|||||||
"LabelPersonalYearReview": "Bilan de l’année ({0})",
|
"LabelPersonalYearReview": "Bilan de l’année ({0})",
|
||||||
"LabelPhotoPathURL": "Chemin / URL des photos",
|
"LabelPhotoPathURL": "Chemin / URL des photos",
|
||||||
"LabelPlayMethod": "Méthode d’écoute",
|
"LabelPlayMethod": "Méthode d’écoute",
|
||||||
|
"LabelPlaybackRateIncrementDecrement": "Augmentation/Diminition de la vitesse de lecture",
|
||||||
"LabelPlayerChapterNumberMarker": "{0} sur {1}",
|
"LabelPlayerChapterNumberMarker": "{0} sur {1}",
|
||||||
"LabelPlaylists": "Listes de lecture",
|
"LabelPlaylists": "Listes de lecture",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
@@ -704,6 +707,7 @@
|
|||||||
"MessageBackupsLocationEditNote": "Remarque : Mettre à jour l'emplacement de sauvegarde ne déplacera pas ou ne modifiera pas les sauvegardes existantes",
|
"MessageBackupsLocationEditNote": "Remarque : Mettre à jour l'emplacement de sauvegarde ne déplacera pas ou ne modifiera pas les sauvegardes existantes",
|
||||||
"MessageBackupsLocationNoEditNote": "Remarque : l’emplacement de sauvegarde est défini via une variable d’environnement et ne peut pas être modifié ici.",
|
"MessageBackupsLocationNoEditNote": "Remarque : l’emplacement de sauvegarde est défini via une variable d’environnement et ne peut pas être modifié ici.",
|
||||||
"MessageBackupsLocationPathEmpty": "L'emplacement de secours ne peut pas être vide",
|
"MessageBackupsLocationPathEmpty": "L'emplacement de secours ne peut pas être vide",
|
||||||
|
"MessageBatchEditPopulateMapDetailsAllHelp": "Remplir les champs disponibles avec les données de tous les éléments. Les champs avec des valeurs multiples seront fusionnés.",
|
||||||
"MessageBatchQuickMatchDescription": "La recherche par correspondance rapide tentera d’ajouter les couvertures et métadonnées manquantes pour les éléments sélectionnés. Activez les options ci-dessous pour permettre la Recherche par correspondance d’écraser les couvertures et/ou métadonnées existantes.",
|
"MessageBatchQuickMatchDescription": "La recherche par correspondance rapide tentera d’ajouter les couvertures et métadonnées manquantes pour les éléments sélectionnés. Activez les options ci-dessous pour permettre la Recherche par correspondance d’écraser les couvertures et/ou métadonnées existantes.",
|
||||||
"MessageBookshelfNoCollections": "Vous n’avez pas encore de collections",
|
"MessageBookshelfNoCollections": "Vous n’avez pas encore de collections",
|
||||||
"MessageBookshelfNoRSSFeeds": "Aucun flux RSS n’est ouvert",
|
"MessageBookshelfNoRSSFeeds": "Aucun flux RSS n’est ouvert",
|
||||||
|
|||||||
+22
-11
@@ -10,11 +10,13 @@
|
|||||||
"ButtonApplyChapters": "Primijeni poglavlja",
|
"ButtonApplyChapters": "Primijeni poglavlja",
|
||||||
"ButtonAuthors": "Autori",
|
"ButtonAuthors": "Autori",
|
||||||
"ButtonBack": "Natrag",
|
"ButtonBack": "Natrag",
|
||||||
|
"ButtonBatchEditPopulateFromExisting": "Popuni iz postojećeg",
|
||||||
|
"ButtonBatchEditPopulateMapDetails": "Popuni mapirane pojedinosti",
|
||||||
"ButtonBrowseForFolder": "Pronađi mapu",
|
"ButtonBrowseForFolder": "Pronađi mapu",
|
||||||
"ButtonCancel": "Odustani",
|
"ButtonCancel": "Odustani",
|
||||||
"ButtonCancelEncode": "Otkaži kodiranje",
|
"ButtonCancelEncode": "Otkaži kodiranje",
|
||||||
"ButtonChangeRootPassword": "Promijeni zaporku root korisnika",
|
"ButtonChangeRootPassword": "Promijeni zaporku root korisnika",
|
||||||
"ButtonCheckAndDownloadNewEpisodes": "Provjeri i preuzmi nove epizode",
|
"ButtonCheckAndDownloadNewEpisodes": "Provjeri i preuzmi nove nastavke",
|
||||||
"ButtonChooseAFolder": "Odaberi mapu",
|
"ButtonChooseAFolder": "Odaberi mapu",
|
||||||
"ButtonChooseFiles": "Odaberi datoteke",
|
"ButtonChooseFiles": "Odaberi datoteke",
|
||||||
"ButtonClearFilter": "Poništi filter",
|
"ButtonClearFilter": "Poništi filter",
|
||||||
@@ -217,6 +219,7 @@
|
|||||||
"LabelAccountTypeAdmin": "Administrator",
|
"LabelAccountTypeAdmin": "Administrator",
|
||||||
"LabelAccountTypeGuest": "Gost",
|
"LabelAccountTypeGuest": "Gost",
|
||||||
"LabelAccountTypeUser": "Korisnik",
|
"LabelAccountTypeUser": "Korisnik",
|
||||||
|
"LabelActivities": "Aktivnosti",
|
||||||
"LabelActivity": "Aktivnost",
|
"LabelActivity": "Aktivnost",
|
||||||
"LabelAddToCollection": "Dodaj u zbirku",
|
"LabelAddToCollection": "Dodaj u zbirku",
|
||||||
"LabelAddToCollectionBatch": "Dodaj {0} knjiga u zbirku",
|
"LabelAddToCollectionBatch": "Dodaj {0} knjiga u zbirku",
|
||||||
@@ -281,6 +284,7 @@
|
|||||||
"LabelContinueSeries": "Nastavi serijal",
|
"LabelContinueSeries": "Nastavi serijal",
|
||||||
"LabelCover": "Naslovnica",
|
"LabelCover": "Naslovnica",
|
||||||
"LabelCoverImageURL": "URL naslovnice",
|
"LabelCoverImageURL": "URL naslovnice",
|
||||||
|
"LabelCoverProvider": "Pružatelj naslovnica",
|
||||||
"LabelCreatedAt": "Izrađen",
|
"LabelCreatedAt": "Izrađen",
|
||||||
"LabelCronExpression": "Cron izraz",
|
"LabelCronExpression": "Cron izraz",
|
||||||
"LabelCurrent": "Trenutan",
|
"LabelCurrent": "Trenutan",
|
||||||
@@ -288,7 +292,7 @@
|
|||||||
"LabelCustomCronExpression": "Prilagođeni CRON izraz:",
|
"LabelCustomCronExpression": "Prilagođeni CRON izraz:",
|
||||||
"LabelDatetime": "Datum i vrijeme",
|
"LabelDatetime": "Datum i vrijeme",
|
||||||
"LabelDays": "Dani",
|
"LabelDays": "Dani",
|
||||||
"LabelDeleteFromFileSystemCheckbox": "Izbriši datoteke (uklonite oznaku ako stavku želite izbrisati samo iz baze podataka)",
|
"LabelDeleteFromFileSystemCheckbox": "Izbriši datoteke (uklonite kvačicu ako stavku želite izbrisati samo iz baze podataka)",
|
||||||
"LabelDescription": "Opis",
|
"LabelDescription": "Opis",
|
||||||
"LabelDeselectAll": "Odznači sve",
|
"LabelDeselectAll": "Odznači sve",
|
||||||
"LabelDevice": "Uređaj",
|
"LabelDevice": "Uređaj",
|
||||||
@@ -353,7 +357,7 @@
|
|||||||
"LabelFileModifiedDate": "Izmijenjeno {0}",
|
"LabelFileModifiedDate": "Izmijenjeno {0}",
|
||||||
"LabelFilename": "Naziv datoteke",
|
"LabelFilename": "Naziv datoteke",
|
||||||
"LabelFilterByUser": "Filtriraj po korisniku",
|
"LabelFilterByUser": "Filtriraj po korisniku",
|
||||||
"LabelFindEpisodes": "Pronađi epizode",
|
"LabelFindEpisodes": "Pronađi nastavke",
|
||||||
"LabelFinished": "Dovršeno",
|
"LabelFinished": "Dovršeno",
|
||||||
"LabelFolder": "Mapa",
|
"LabelFolder": "Mapa",
|
||||||
"LabelFolders": "Mape",
|
"LabelFolders": "Mape",
|
||||||
@@ -389,6 +393,7 @@
|
|||||||
"LabelIntervalEvery6Hours": "Svakih 6 sati",
|
"LabelIntervalEvery6Hours": "Svakih 6 sati",
|
||||||
"LabelIntervalEveryDay": "Svaki dan",
|
"LabelIntervalEveryDay": "Svaki dan",
|
||||||
"LabelIntervalEveryHour": "Svaki sat",
|
"LabelIntervalEveryHour": "Svaki sat",
|
||||||
|
"LabelIntervalEveryMinute": "Svaku minutu",
|
||||||
"LabelInvert": "Obrni",
|
"LabelInvert": "Obrni",
|
||||||
"LabelItem": "Stavka",
|
"LabelItem": "Stavka",
|
||||||
"LabelJumpBackwardAmount": "Dužina skoka unatrag",
|
"LabelJumpBackwardAmount": "Dužina skoka unatrag",
|
||||||
@@ -398,8 +403,8 @@
|
|||||||
"LabelLanguages": "Jezici",
|
"LabelLanguages": "Jezici",
|
||||||
"LabelLastBookAdded": "Zadnja dodana knjiga",
|
"LabelLastBookAdded": "Zadnja dodana knjiga",
|
||||||
"LabelLastBookUpdated": "Zadnja ažurirana knjiga",
|
"LabelLastBookUpdated": "Zadnja ažurirana knjiga",
|
||||||
"LabelLastSeen": "Zadnji puta viđen",
|
"LabelLastSeen": "Zadnje gledano",
|
||||||
"LabelLastTime": "Zadnji puta",
|
"LabelLastTime": "Vrijeme zadnjeg slušanja",
|
||||||
"LabelLastUpdate": "Zadnje ažuriranje",
|
"LabelLastUpdate": "Zadnje ažuriranje",
|
||||||
"LabelLayout": "Prikaz",
|
"LabelLayout": "Prikaz",
|
||||||
"LabelLayoutSinglePage": "Jedna stranica",
|
"LabelLayoutSinglePage": "Jedna stranica",
|
||||||
@@ -416,7 +421,7 @@
|
|||||||
"LabelLogLevelDebug": "Debug",
|
"LabelLogLevelDebug": "Debug",
|
||||||
"LabelLogLevelInfo": "Info",
|
"LabelLogLevelInfo": "Info",
|
||||||
"LabelLogLevelWarn": "Warn",
|
"LabelLogLevelWarn": "Warn",
|
||||||
"LabelLookForNewEpisodesAfterDate": "Traži nove epizode nakon ovog datuma",
|
"LabelLookForNewEpisodesAfterDate": "Traži nove nastavke nakon ovog datuma",
|
||||||
"LabelLowestPriority": "Najniži prioritet",
|
"LabelLowestPriority": "Najniži prioritet",
|
||||||
"LabelMatchExistingUsersBy": "Prepoznaj postojeće korisnike pomoću",
|
"LabelMatchExistingUsersBy": "Prepoznaj postojeće korisnike pomoću",
|
||||||
"LabelMatchExistingUsersByDescription": "Rabi se za povezivanje postojećih korisnika. Nakon što se spoje, korisnike se prepoznaje temeljem jedinstvene oznake vašeg pružatelja SSO usluga",
|
"LabelMatchExistingUsersByDescription": "Rabi se za povezivanje postojećih korisnika. Nakon što se spoje, korisnike se prepoznaje temeljem jedinstvene oznake vašeg pružatelja SSO usluga",
|
||||||
@@ -445,7 +450,7 @@
|
|||||||
"LabelNew": "Novo",
|
"LabelNew": "Novo",
|
||||||
"LabelNewPassword": "Nova zaporka",
|
"LabelNewPassword": "Nova zaporka",
|
||||||
"LabelNewestAuthors": "Najnoviji autori",
|
"LabelNewestAuthors": "Najnoviji autori",
|
||||||
"LabelNewestEpisodes": "Najnovije epizode",
|
"LabelNewestEpisodes": "Najnoviji nastavci",
|
||||||
"LabelNextBackupDate": "Sljedeća izrada sigurnosne kopije",
|
"LabelNextBackupDate": "Sljedeća izrada sigurnosne kopije",
|
||||||
"LabelNextScheduledRun": "Sljedeće zakazano izvođenje",
|
"LabelNextScheduledRun": "Sljedeće zakazano izvođenje",
|
||||||
"LabelNoCustomMetadataProviders": "Nema prilagođenih pružatelja meta-podataka",
|
"LabelNoCustomMetadataProviders": "Nema prilagođenih pružatelja meta-podataka",
|
||||||
@@ -484,6 +489,7 @@
|
|||||||
"LabelPersonalYearReview": "Vaš godišnji pregled ({0})",
|
"LabelPersonalYearReview": "Vaš godišnji pregled ({0})",
|
||||||
"LabelPhotoPathURL": "Putanja ili URL fotografije",
|
"LabelPhotoPathURL": "Putanja ili URL fotografije",
|
||||||
"LabelPlayMethod": "Način reprodukcije",
|
"LabelPlayMethod": "Način reprodukcije",
|
||||||
|
"LabelPlaybackRateIncrementDecrement": "Korak povećanja/smanjenja brzine reprodukcije",
|
||||||
"LabelPlayerChapterNumberMarker": "{0} od {1}",
|
"LabelPlayerChapterNumberMarker": "{0} od {1}",
|
||||||
"LabelPlaylists": "Popisi za izvođenje",
|
"LabelPlaylists": "Popisi za izvođenje",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
@@ -638,10 +644,10 @@
|
|||||||
"LabelTimeDurationXMinutes": "{0} minuta",
|
"LabelTimeDurationXMinutes": "{0} minuta",
|
||||||
"LabelTimeDurationXSeconds": "{0} sekundi",
|
"LabelTimeDurationXSeconds": "{0} sekundi",
|
||||||
"LabelTimeInMinutes": "Vrijeme u minutama",
|
"LabelTimeInMinutes": "Vrijeme u minutama",
|
||||||
"LabelTimeLeft": "{0} preostalo",
|
"LabelTimeLeft": "preostalo {0}",
|
||||||
"LabelTimeListened": "Vremena odslušano",
|
"LabelTimeListened": "Vremena odslušano",
|
||||||
"LabelTimeListenedToday": "Vremena odslušano danas",
|
"LabelTimeListenedToday": "Vremena odslušano danas",
|
||||||
"LabelTimeRemaining": "{0} preostalo",
|
"LabelTimeRemaining": "preostalo {0}",
|
||||||
"LabelTimeToShift": "Vrijeme za pomjeriti u sekundama",
|
"LabelTimeToShift": "Vrijeme za pomjeriti u sekundama",
|
||||||
"LabelTitle": "Naslov",
|
"LabelTitle": "Naslov",
|
||||||
"LabelToolsEmbedMetadata": "Ugradi meta-podatke",
|
"LabelToolsEmbedMetadata": "Ugradi meta-podatke",
|
||||||
@@ -675,7 +681,7 @@
|
|||||||
"LabelUploaderDropFiles": "Ispusti datoteke",
|
"LabelUploaderDropFiles": "Ispusti datoteke",
|
||||||
"LabelUploaderItemFetchMetadataHelp": "Automatski dohvati naslov, autora i serijal",
|
"LabelUploaderItemFetchMetadataHelp": "Automatski dohvati naslov, autora i serijal",
|
||||||
"LabelUseAdvancedOptions": "Koristi se naprednim opcijama",
|
"LabelUseAdvancedOptions": "Koristi se naprednim opcijama",
|
||||||
"LabelUseChapterTrack": "Koristi zvučni zapis poglavlja",
|
"LabelUseChapterTrack": "Upravljaj trakom poglavlja",
|
||||||
"LabelUseFullTrack": "Koristi cijeli zvučni zapis",
|
"LabelUseFullTrack": "Koristi cijeli zvučni zapis",
|
||||||
"LabelUseZeroForUnlimited": "0 za neograničeno",
|
"LabelUseZeroForUnlimited": "0 za neograničeno",
|
||||||
"LabelUser": "Korisnik",
|
"LabelUser": "Korisnik",
|
||||||
@@ -704,8 +710,11 @@
|
|||||||
"MessageBackupsLocationEditNote": "Napomena: Uređivanje lokacije za sigurnosne kopije ne premješta ili mijenja postojeće sigurnosne kopije",
|
"MessageBackupsLocationEditNote": "Napomena: Uređivanje lokacije za sigurnosne kopije ne premješta ili mijenja postojeće sigurnosne kopije",
|
||||||
"MessageBackupsLocationNoEditNote": "Napomena: Lokacija za sigurnosne kopije zadana je kroz varijablu okoline i ovdje se ne može izmijeniti.",
|
"MessageBackupsLocationNoEditNote": "Napomena: Lokacija za sigurnosne kopije zadana je kroz varijablu okoline i ovdje se ne može izmijeniti.",
|
||||||
"MessageBackupsLocationPathEmpty": "Putanja do lokacije za sigurnosne kopije ne može ostati prazna",
|
"MessageBackupsLocationPathEmpty": "Putanja do lokacije za sigurnosne kopije ne može ostati prazna",
|
||||||
|
"MessageBatchEditPopulateMapDetailsAllHelp": "Nadopunjuje omogućena polja podatcima iz svih stavki. Polja s višestrukim podatcima će se spojiti",
|
||||||
|
"MessageBatchEditPopulateMapDetailsItemHelp": "Popuni omogućena polja mapiranih pojedinosti s podatcima iz ove stavke",
|
||||||
"MessageBatchQuickMatchDescription": "Brzo prepoznavanje za odabrane će stavke pokušati dodati naslovnice i meta-podatke koji nedostaju. Uključite donje opcije ako želite da Brzo prepoznavanje prepiše postojeće naslovnice i/ili meta-podatke.",
|
"MessageBatchQuickMatchDescription": "Brzo prepoznavanje za odabrane će stavke pokušati dodati naslovnice i meta-podatke koji nedostaju. Uključite donje opcije ako želite da Brzo prepoznavanje prepiše postojeće naslovnice i/ili meta-podatke.",
|
||||||
"MessageBookshelfNoCollections": "Niste izradili niti jednu zbirku",
|
"MessageBookshelfNoCollections": "Niste izradili niti jednu zbirku",
|
||||||
|
"MessageBookshelfNoCollectionsHelp": "Zbirke su javne. Svi korisnici s pristupom knjižnici mogu ih vidjeti.",
|
||||||
"MessageBookshelfNoRSSFeeds": "Nema otvorenih RSS izvora",
|
"MessageBookshelfNoRSSFeeds": "Nema otvorenih RSS izvora",
|
||||||
"MessageBookshelfNoResultsForFilter": "Nema rezultata za filter \"{0}: {1}\"",
|
"MessageBookshelfNoResultsForFilter": "Nema rezultata za filter \"{0}: {1}\"",
|
||||||
"MessageBookshelfNoResultsForQuery": "Vaš upit nema rezultata",
|
"MessageBookshelfNoResultsForQuery": "Vaš upit nema rezultata",
|
||||||
@@ -721,7 +730,7 @@
|
|||||||
"MessageConfirmDeleteDevice": "Sigurno želite izbrisati e-čitač \"{0}\"?",
|
"MessageConfirmDeleteDevice": "Sigurno želite izbrisati e-čitač \"{0}\"?",
|
||||||
"MessageConfirmDeleteFile": "Ovo će izbrisati datoteke s datotečnog sustava. Jeste li sigurni?",
|
"MessageConfirmDeleteFile": "Ovo će izbrisati datoteke s datotečnog sustava. Jeste li sigurni?",
|
||||||
"MessageConfirmDeleteLibrary": "Sigurno želite trajno izbrisati knjižnicu \"{0}\"?",
|
"MessageConfirmDeleteLibrary": "Sigurno želite trajno izbrisati knjižnicu \"{0}\"?",
|
||||||
"MessageConfirmDeleteLibraryItem": "Ovo će izbrisati knjižničku stavku iz datoteke i vašeg datotečnog sustava. Jeste li sigurni?",
|
"MessageConfirmDeleteLibraryItem": "Ovo će izbrisati knjižničku stavku iz baze podataka i s datotečnog sustava. Jeste li sigurni?",
|
||||||
"MessageConfirmDeleteLibraryItems": "Ovo će izbrisati {0} knjižničkih stavki iz baze podataka i datotečnog sustava. Jeste li sigurni?",
|
"MessageConfirmDeleteLibraryItems": "Ovo će izbrisati {0} knjižničkih stavki iz baze podataka i datotečnog sustava. Jeste li sigurni?",
|
||||||
"MessageConfirmDeleteMetadataProvider": "Sigurno želite izbrisati prilagođenog pružatelja meta-podataka \"{0}\"?",
|
"MessageConfirmDeleteMetadataProvider": "Sigurno želite izbrisati prilagođenog pružatelja meta-podataka \"{0}\"?",
|
||||||
"MessageConfirmDeleteNotification": "Sigurno želite izbrisati ovu obavijest?",
|
"MessageConfirmDeleteNotification": "Sigurno želite izbrisati ovu obavijest?",
|
||||||
@@ -816,6 +825,7 @@
|
|||||||
"MessageNoTasksRunning": "Nema zadataka koji se izvode",
|
"MessageNoTasksRunning": "Nema zadataka koji se izvode",
|
||||||
"MessageNoUpdatesWereNecessary": "Ažuriranje nije bilo potrebno",
|
"MessageNoUpdatesWereNecessary": "Ažuriranje nije bilo potrebno",
|
||||||
"MessageNoUserPlaylists": "Nemate popisa za izvođenje",
|
"MessageNoUserPlaylists": "Nemate popisa za izvođenje",
|
||||||
|
"MessageNoUserPlaylistsHelp": "Popisi za izvođenje su privatni. Može ih vidjeti samo korisnik koji ih je izradio.",
|
||||||
"MessageNotYetImplemented": "Još nije implementirano",
|
"MessageNotYetImplemented": "Još nije implementirano",
|
||||||
"MessageOpmlPreviewNote": "Napomena: Ovo je pretpregled raščlanjene OPML datoteke. Stvarni naslov podcasta preuzet će se iz RSS izvora.",
|
"MessageOpmlPreviewNote": "Napomena: Ovo je pretpregled raščlanjene OPML datoteke. Stvarni naslov podcasta preuzet će se iz RSS izvora.",
|
||||||
"MessageOr": "ili",
|
"MessageOr": "ili",
|
||||||
@@ -838,6 +848,7 @@
|
|||||||
"MessageRestoreBackupConfirm": "Sigurno želite vratiti sigurnosnu kopiju izrađenu",
|
"MessageRestoreBackupConfirm": "Sigurno želite vratiti sigurnosnu kopiju izrađenu",
|
||||||
"MessageRestoreBackupWarning": "Vraćanjem sigurnosne kopije prepisat ćete cijelu bazu podataka koja se nalazi u /config i slike naslovnice u /metadata/items i /metadata/authors.<br /><br />Sigurnosne kopije ne mijenjaju datoteke koje se nalaze u mapama vaših knjižnica. Ako ste u postavkama poslužitelja uključili mogućnost spremanja naslovnica i meta-podataka u mape knjižnice, te se datoteke neće niti sigurnosno pohraniti niti prepisati. <br /><br />Svi klijenti koji se spajaju na vaš poslužitelj automatski će se osvježiti.",
|
"MessageRestoreBackupWarning": "Vraćanjem sigurnosne kopije prepisat ćete cijelu bazu podataka koja se nalazi u /config i slike naslovnice u /metadata/items i /metadata/authors.<br /><br />Sigurnosne kopije ne mijenjaju datoteke koje se nalaze u mapama vaših knjižnica. Ako ste u postavkama poslužitelja uključili mogućnost spremanja naslovnica i meta-podataka u mape knjižnice, te se datoteke neće niti sigurnosno pohraniti niti prepisati. <br /><br />Svi klijenti koji se spajaju na vaš poslužitelj automatski će se osvježiti.",
|
||||||
"MessageScheduleLibraryScanNote": "Za većinu korisnika se preporučuje ostaviti ovu funkciju deaktiviranom i ostaviti postavku promatrača mape aktiviranom. Promatrač mapa će automatski otkriti promjene u mapama vaše knjižnice. Promatrač mapa ne radi na svakom datotečnom sustavu (kao što je NFS) pa se umjesto njega mogu koristiti planirana pretraživanja knjižnice.",
|
"MessageScheduleLibraryScanNote": "Za većinu korisnika se preporučuje ostaviti ovu funkciju deaktiviranom i ostaviti postavku promatrača mape aktiviranom. Promatrač mapa će automatski otkriti promjene u mapama vaše knjižnice. Promatrač mapa ne radi na svakom datotečnom sustavu (kao što je NFS) pa se umjesto njega mogu koristiti planirana pretraživanja knjižnice.",
|
||||||
|
"MessageScheduleRunEveryWeekdayAtTime": "Pokreni svaki {0} u {1}",
|
||||||
"MessageSearchResultsFor": "Rezultati pretrage za",
|
"MessageSearchResultsFor": "Rezultati pretrage za",
|
||||||
"MessageSelected": "{0} odabrano",
|
"MessageSelected": "{0} odabrano",
|
||||||
"MessageServerCouldNotBeReached": "Nije moguće pristupiti poslužitelju",
|
"MessageServerCouldNotBeReached": "Nije moguće pristupiti poslužitelju",
|
||||||
|
|||||||
+25
-1
@@ -10,6 +10,8 @@
|
|||||||
"ButtonApplyChapters": "Applica",
|
"ButtonApplyChapters": "Applica",
|
||||||
"ButtonAuthors": "Autori",
|
"ButtonAuthors": "Autori",
|
||||||
"ButtonBack": "Indietro",
|
"ButtonBack": "Indietro",
|
||||||
|
"ButtonBatchEditPopulateFromExisting": "Popola da esistente",
|
||||||
|
"ButtonBatchEditPopulateMapDetails": "Inserisci i dettagli della mappa",
|
||||||
"ButtonBrowseForFolder": "Per Cartella",
|
"ButtonBrowseForFolder": "Per Cartella",
|
||||||
"ButtonCancel": "Cancella",
|
"ButtonCancel": "Cancella",
|
||||||
"ButtonCancelEncode": "Ferma la codifica",
|
"ButtonCancelEncode": "Ferma la codifica",
|
||||||
@@ -88,6 +90,8 @@
|
|||||||
"ButtonSaveTracklist": "Salva Tracklist",
|
"ButtonSaveTracklist": "Salva Tracklist",
|
||||||
"ButtonScan": "Scansiona",
|
"ButtonScan": "Scansiona",
|
||||||
"ButtonScanLibrary": "Scansiona Libreria",
|
"ButtonScanLibrary": "Scansiona Libreria",
|
||||||
|
"ButtonScrollLeft": "Scorri verso sinistra",
|
||||||
|
"ButtonScrollRight": "Scorri verso destra",
|
||||||
"ButtonSearch": "Cerca",
|
"ButtonSearch": "Cerca",
|
||||||
"ButtonSelectFolderPath": "Seleziona percorso cartella",
|
"ButtonSelectFolderPath": "Seleziona percorso cartella",
|
||||||
"ButtonSeries": "Serie",
|
"ButtonSeries": "Serie",
|
||||||
@@ -190,6 +194,7 @@
|
|||||||
"HeaderSettingsExperimental": "Opzioni Sperimentali",
|
"HeaderSettingsExperimental": "Opzioni Sperimentali",
|
||||||
"HeaderSettingsGeneral": "Generale",
|
"HeaderSettingsGeneral": "Generale",
|
||||||
"HeaderSettingsScanner": "Scanner",
|
"HeaderSettingsScanner": "Scanner",
|
||||||
|
"HeaderSettingsWebClient": "Web Client",
|
||||||
"HeaderSleepTimer": "Sveglia",
|
"HeaderSleepTimer": "Sveglia",
|
||||||
"HeaderStatsLargestItems": "File pesanti",
|
"HeaderStatsLargestItems": "File pesanti",
|
||||||
"HeaderStatsLongestItems": "libri più lunghi (ore)",
|
"HeaderStatsLongestItems": "libri più lunghi (ore)",
|
||||||
@@ -214,6 +219,7 @@
|
|||||||
"LabelAccountTypeAdmin": "Amministratore",
|
"LabelAccountTypeAdmin": "Amministratore",
|
||||||
"LabelAccountTypeGuest": "Ospite",
|
"LabelAccountTypeGuest": "Ospite",
|
||||||
"LabelAccountTypeUser": "Utente",
|
"LabelAccountTypeUser": "Utente",
|
||||||
|
"LabelActivities": "Attività",
|
||||||
"LabelActivity": "Attività",
|
"LabelActivity": "Attività",
|
||||||
"LabelAddToCollection": "Aggiungi alla Raccolta",
|
"LabelAddToCollection": "Aggiungi alla Raccolta",
|
||||||
"LabelAddToCollectionBatch": "Aggiungi {0} Libri alla Raccolta",
|
"LabelAddToCollectionBatch": "Aggiungi {0} Libri alla Raccolta",
|
||||||
@@ -278,6 +284,7 @@
|
|||||||
"LabelContinueSeries": "Continua serie",
|
"LabelContinueSeries": "Continua serie",
|
||||||
"LabelCover": "Copertina",
|
"LabelCover": "Copertina",
|
||||||
"LabelCoverImageURL": "Indirizzo della cover URL",
|
"LabelCoverImageURL": "Indirizzo della cover URL",
|
||||||
|
"LabelCoverProvider": "Cover Provider",
|
||||||
"LabelCreatedAt": "Creato A",
|
"LabelCreatedAt": "Creato A",
|
||||||
"LabelCronExpression": "Espressione Cron",
|
"LabelCronExpression": "Espressione Cron",
|
||||||
"LabelCurrent": "Attuale",
|
"LabelCurrent": "Attuale",
|
||||||
@@ -386,6 +393,7 @@
|
|||||||
"LabelIntervalEvery6Hours": "Ogni 6 ore",
|
"LabelIntervalEvery6Hours": "Ogni 6 ore",
|
||||||
"LabelIntervalEveryDay": "Ogni Giorno",
|
"LabelIntervalEveryDay": "Ogni Giorno",
|
||||||
"LabelIntervalEveryHour": "Ogni ora",
|
"LabelIntervalEveryHour": "Ogni ora",
|
||||||
|
"LabelIntervalEveryMinute": "Ogni minuto",
|
||||||
"LabelInvert": "Inverti",
|
"LabelInvert": "Inverti",
|
||||||
"LabelItem": "Oggetti",
|
"LabelItem": "Oggetti",
|
||||||
"LabelJumpBackwardAmount": "secondi di avvolgimento",
|
"LabelJumpBackwardAmount": "secondi di avvolgimento",
|
||||||
@@ -429,7 +437,7 @@
|
|||||||
"LabelMetadataProvider": "Metadata Provider",
|
"LabelMetadataProvider": "Metadata Provider",
|
||||||
"LabelMinute": "Minuto",
|
"LabelMinute": "Minuto",
|
||||||
"LabelMinutes": "Minuti",
|
"LabelMinutes": "Minuti",
|
||||||
"LabelMissing": "Altro",
|
"LabelMissing": "Mancante",
|
||||||
"LabelMissingEbook": "Non ha libri digitali",
|
"LabelMissingEbook": "Non ha libri digitali",
|
||||||
"LabelMissingSupplementaryEbook": "Non ha un libro digitale supplementare",
|
"LabelMissingSupplementaryEbook": "Non ha un libro digitale supplementare",
|
||||||
"LabelMobileRedirectURIs": "URI di reindirizzamento mobile consentiti",
|
"LabelMobileRedirectURIs": "URI di reindirizzamento mobile consentiti",
|
||||||
@@ -481,6 +489,7 @@
|
|||||||
"LabelPersonalYearReview": "Il tuo anno in rassegna ({0})",
|
"LabelPersonalYearReview": "Il tuo anno in rassegna ({0})",
|
||||||
"LabelPhotoPathURL": "foto Path/URL",
|
"LabelPhotoPathURL": "foto Path/URL",
|
||||||
"LabelPlayMethod": "Metodo di riproduzione",
|
"LabelPlayMethod": "Metodo di riproduzione",
|
||||||
|
"LabelPlaybackRateIncrementDecrement": "Valore incremento/decremento velocità di riproduzione",
|
||||||
"LabelPlayerChapterNumberMarker": "{0} di {1}",
|
"LabelPlayerChapterNumberMarker": "{0} di {1}",
|
||||||
"LabelPlaylists": "Playlist",
|
"LabelPlaylists": "Playlist",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
@@ -543,6 +552,7 @@
|
|||||||
"LabelServerYearReview": "Anno del server in sintesi({0})",
|
"LabelServerYearReview": "Anno del server in sintesi({0})",
|
||||||
"LabelSetEbookAsPrimary": "Imposta come primario",
|
"LabelSetEbookAsPrimary": "Imposta come primario",
|
||||||
"LabelSetEbookAsSupplementary": "Imposta come suplementare",
|
"LabelSetEbookAsSupplementary": "Imposta come suplementare",
|
||||||
|
"LabelSettingsAllowIframe": "Consenti l'incorporamento in un iframe",
|
||||||
"LabelSettingsAudiobooksOnly": "Solo Audiolibri",
|
"LabelSettingsAudiobooksOnly": "Solo Audiolibri",
|
||||||
"LabelSettingsAudiobooksOnlyHelp": "L'abilitazione di questa impostazione ignorerà i file di libro digitale a meno che non si trovino all'interno di una cartella di audiolibri, nel qual caso verranno impostati come libri digitali supplementari",
|
"LabelSettingsAudiobooksOnlyHelp": "L'abilitazione di questa impostazione ignorerà i file di libro digitale a meno che non si trovino all'interno di una cartella di audiolibri, nel qual caso verranno impostati come libri digitali supplementari",
|
||||||
"LabelSettingsBookshelfViewHelp": "Design con scaffali in legno",
|
"LabelSettingsBookshelfViewHelp": "Design con scaffali in legno",
|
||||||
@@ -585,6 +595,7 @@
|
|||||||
"LabelSettingsStoreMetadataWithItemHelp": "Di default, i metadati sono salvati dentro /metadata/items, abilitando questa opzione si memorizzeranno i metadata nella cartella della libreria",
|
"LabelSettingsStoreMetadataWithItemHelp": "Di default, i metadati sono salvati dentro /metadata/items, abilitando questa opzione si memorizzeranno i metadata nella cartella della libreria",
|
||||||
"LabelSettingsTimeFormat": "Formato Ora",
|
"LabelSettingsTimeFormat": "Formato Ora",
|
||||||
"LabelShare": "Condividi",
|
"LabelShare": "Condividi",
|
||||||
|
"LabelShareDownloadableHelp": "Consente agli utenti dotati del link di condivisione di scaricare un file zip dell'elemento della libreria.",
|
||||||
"LabelShareOpen": "Apri Condivisioni",
|
"LabelShareOpen": "Apri Condivisioni",
|
||||||
"LabelShareURL": "Condividi URL",
|
"LabelShareURL": "Condividi URL",
|
||||||
"LabelShowAll": "Mostra tutto",
|
"LabelShowAll": "Mostra tutto",
|
||||||
@@ -593,6 +604,8 @@
|
|||||||
"LabelSize": "Dimensione",
|
"LabelSize": "Dimensione",
|
||||||
"LabelSleepTimer": "Temporizzatore",
|
"LabelSleepTimer": "Temporizzatore",
|
||||||
"LabelSlug": "Lento",
|
"LabelSlug": "Lento",
|
||||||
|
"LabelSortAscending": "Crescente",
|
||||||
|
"LabelSortDescending": "Discendente",
|
||||||
"LabelStart": "Inizo",
|
"LabelStart": "Inizo",
|
||||||
"LabelStartTime": "Tempo di inizio",
|
"LabelStartTime": "Tempo di inizio",
|
||||||
"LabelStarted": "Iniziato",
|
"LabelStarted": "Iniziato",
|
||||||
@@ -664,6 +677,7 @@
|
|||||||
"LabelUpdateDetailsHelp": "Consenti la sovrascrittura dei dettagli esistenti per i libri selezionati quando viene individuata una corrispondenza",
|
"LabelUpdateDetailsHelp": "Consenti la sovrascrittura dei dettagli esistenti per i libri selezionati quando viene individuata una corrispondenza",
|
||||||
"LabelUpdatedAt": "Aggiornato alle",
|
"LabelUpdatedAt": "Aggiornato alle",
|
||||||
"LabelUploaderDragAndDrop": "Drag & drop file o Cartelle",
|
"LabelUploaderDragAndDrop": "Drag & drop file o Cartelle",
|
||||||
|
"LabelUploaderDragAndDropFilesOnly": "Drag & drop files",
|
||||||
"LabelUploaderDropFiles": "Elimina file",
|
"LabelUploaderDropFiles": "Elimina file",
|
||||||
"LabelUploaderItemFetchMetadataHelp": "Recupera automaticamente titolo, autore e serie",
|
"LabelUploaderItemFetchMetadataHelp": "Recupera automaticamente titolo, autore e serie",
|
||||||
"LabelUseAdvancedOptions": "Usa le opzioni avanzate",
|
"LabelUseAdvancedOptions": "Usa le opzioni avanzate",
|
||||||
@@ -679,6 +693,8 @@
|
|||||||
"LabelViewPlayerSettings": "Mostra Impostazioni player",
|
"LabelViewPlayerSettings": "Mostra Impostazioni player",
|
||||||
"LabelViewQueue": "Visualizza coda",
|
"LabelViewQueue": "Visualizza coda",
|
||||||
"LabelVolume": "Volume",
|
"LabelVolume": "Volume",
|
||||||
|
"LabelWebRedirectURLsDescription": "Autorizza questi URL nel tuo provider OAuth per consentire il reindirizzamento all'app Web dopo l'accesso:",
|
||||||
|
"LabelWebRedirectURLsSubfolder": "Sottocartella per URL di reindirizzamento",
|
||||||
"LabelWeekdaysToRun": "Giorni feriali da eseguire",
|
"LabelWeekdaysToRun": "Giorni feriali da eseguire",
|
||||||
"LabelXBooks": "{0} libri",
|
"LabelXBooks": "{0} libri",
|
||||||
"LabelXItems": "{0} oggetti",
|
"LabelXItems": "{0} oggetti",
|
||||||
@@ -694,8 +710,11 @@
|
|||||||
"MessageBackupsLocationEditNote": "Nota: l'aggiornamento della posizione di backup non sposterà o modificherà i backup esistenti",
|
"MessageBackupsLocationEditNote": "Nota: l'aggiornamento della posizione di backup non sposterà o modificherà i backup esistenti",
|
||||||
"MessageBackupsLocationNoEditNote": "Nota: la posizione del backup viene impostata tramite una variabile di ambiente e non può essere modificata qui.",
|
"MessageBackupsLocationNoEditNote": "Nota: la posizione del backup viene impostata tramite una variabile di ambiente e non può essere modificata qui.",
|
||||||
"MessageBackupsLocationPathEmpty": "Il percorso del backup non può essere vuoto",
|
"MessageBackupsLocationPathEmpty": "Il percorso del backup non può essere vuoto",
|
||||||
|
"MessageBatchEditPopulateMapDetailsAllHelp": "Popola i campi abilitati con i dati di tutti gli elementi. I campi con più valori verranno uniti",
|
||||||
|
"MessageBatchEditPopulateMapDetailsItemHelp": "Compila i campi dei dettagli della mappa abilitati con i dati di questo elemento",
|
||||||
"MessageBatchQuickMatchDescription": "Quick Match tenterà di aggiungere copertine e metadati mancanti per gli elementi selezionati. Attiva l'opzione per consentire a Quick Match di sovrascrivere copertine e/o metadati esistenti.",
|
"MessageBatchQuickMatchDescription": "Quick Match tenterà di aggiungere copertine e metadati mancanti per gli elementi selezionati. Attiva l'opzione per consentire a Quick Match di sovrascrivere copertine e/o metadati esistenti.",
|
||||||
"MessageBookshelfNoCollections": "Non hai ancora creato nessuna raccolta",
|
"MessageBookshelfNoCollections": "Non hai ancora creato nessuna raccolta",
|
||||||
|
"MessageBookshelfNoCollectionsHelp": "le collezioni sono pubbliche. Tutti gli utenti con accesso alla biblioteca possono vederle.",
|
||||||
"MessageBookshelfNoRSSFeeds": "Nessun RSS feeds aperto",
|
"MessageBookshelfNoRSSFeeds": "Nessun RSS feeds aperto",
|
||||||
"MessageBookshelfNoResultsForFilter": "Nessun risultato per il filtro \"{0}: {1}\"",
|
"MessageBookshelfNoResultsForFilter": "Nessun risultato per il filtro \"{0}: {1}\"",
|
||||||
"MessageBookshelfNoResultsForQuery": "Nessun risultato per la query",
|
"MessageBookshelfNoResultsForQuery": "Nessun risultato per la query",
|
||||||
@@ -748,6 +767,7 @@
|
|||||||
"MessageConfirmResetProgress": "Vuoi davvero azzerare i tuoi progressi?",
|
"MessageConfirmResetProgress": "Vuoi davvero azzerare i tuoi progressi?",
|
||||||
"MessageConfirmSendEbookToDevice": "Sei sicuro/sicura di voler inviare {0} libro «{1}» al dispositivo «{2}»?",
|
"MessageConfirmSendEbookToDevice": "Sei sicuro/sicura di voler inviare {0} libro «{1}» al dispositivo «{2}»?",
|
||||||
"MessageConfirmUnlinkOpenId": "Vuoi davvero scollegare questo utente da OpenID?",
|
"MessageConfirmUnlinkOpenId": "Vuoi davvero scollegare questo utente da OpenID?",
|
||||||
|
"MessageDaysListenedInTheLastYear": "{0} giorni ascoltati nell'ultimo anno",
|
||||||
"MessageDownloadingEpisode": "Scaricamento dell’episodio in corso",
|
"MessageDownloadingEpisode": "Scaricamento dell’episodio in corso",
|
||||||
"MessageDragFilesIntoTrackOrder": "Trascina i file nell'ordine di traccia corretto",
|
"MessageDragFilesIntoTrackOrder": "Trascina i file nell'ordine di traccia corretto",
|
||||||
"MessageEmbedFailed": "Incorporamento non riuscito!",
|
"MessageEmbedFailed": "Incorporamento non riuscito!",
|
||||||
@@ -805,6 +825,7 @@
|
|||||||
"MessageNoTasksRunning": "Nessun processo in esecuzione",
|
"MessageNoTasksRunning": "Nessun processo in esecuzione",
|
||||||
"MessageNoUpdatesWereNecessary": "Nessun aggiornamento necessario",
|
"MessageNoUpdatesWereNecessary": "Nessun aggiornamento necessario",
|
||||||
"MessageNoUserPlaylists": "non hai nessuna Playlist",
|
"MessageNoUserPlaylists": "non hai nessuna Playlist",
|
||||||
|
"MessageNoUserPlaylistsHelp": "Le playlist sono private. Solo l'utente che le crea può vederle.",
|
||||||
"MessageNotYetImplemented": "Non Ancora Implementato",
|
"MessageNotYetImplemented": "Non Ancora Implementato",
|
||||||
"MessageOpmlPreviewNote": "Nota: questa è un'anteprima del file OPML analizzato. Il titolo effettivo del podcast verrà preso dal feed RSS.",
|
"MessageOpmlPreviewNote": "Nota: questa è un'anteprima del file OPML analizzato. Il titolo effettivo del podcast verrà preso dal feed RSS.",
|
||||||
"MessageOr": "o",
|
"MessageOr": "o",
|
||||||
@@ -826,6 +847,7 @@
|
|||||||
"MessageResetChaptersConfirm": "Sei sicuro di voler reimpostare i capitoli e annullare le modifiche ?",
|
"MessageResetChaptersConfirm": "Sei sicuro di voler reimpostare i capitoli e annullare le modifiche ?",
|
||||||
"MessageRestoreBackupConfirm": "Sei sicuro di voler ripristinare il backup creato su",
|
"MessageRestoreBackupConfirm": "Sei sicuro di voler ripristinare il backup creato su",
|
||||||
"MessageRestoreBackupWarning": "Il ripristino di un backup sovrascriverà l'intero database situato in /config e sovrascrive le immagini in /metadata/items & /metadata/authors.<br /><br />I backup non modificano alcun file nelle cartelle della libreria. Se hai abilitato le impostazioni del server per archiviare copertine e metadati nelle cartelle della libreria, questi non vengono sottoposti a backup o sovrascritti.<br /><br />Tutti i client che utilizzano il tuo server verranno aggiornati automaticamente.",
|
"MessageRestoreBackupWarning": "Il ripristino di un backup sovrascriverà l'intero database situato in /config e sovrascrive le immagini in /metadata/items & /metadata/authors.<br /><br />I backup non modificano alcun file nelle cartelle della libreria. Se hai abilitato le impostazioni del server per archiviare copertine e metadati nelle cartelle della libreria, questi non vengono sottoposti a backup o sovrascritti.<br /><br />Tutti i client che utilizzano il tuo server verranno aggiornati automaticamente.",
|
||||||
|
"MessageScheduleLibraryScanNote": "Per la maggior parte degli utenti, si consiglia di lasciare questa funzionalità disabilitata e di mantenere abilitata l'impostazione di folder watcher. Il folder watcher rileverà automaticamente le modifiche nelle cartelle della libreria. Il folder watcher non funziona per ogni file system (come NFS), quindi è possibile utilizzare le scansioni pianificate della libreria.",
|
||||||
"MessageSearchResultsFor": "cerca risultati per",
|
"MessageSearchResultsFor": "cerca risultati per",
|
||||||
"MessageSelected": "{0} selezionati",
|
"MessageSelected": "{0} selezionati",
|
||||||
"MessageServerCouldNotBeReached": "Impossibile raggiungere il server",
|
"MessageServerCouldNotBeReached": "Impossibile raggiungere il server",
|
||||||
@@ -952,6 +974,7 @@
|
|||||||
"ToastCollectionRemoveSuccess": "Collezione rimossa",
|
"ToastCollectionRemoveSuccess": "Collezione rimossa",
|
||||||
"ToastCollectionUpdateSuccess": "Raccolta aggiornata",
|
"ToastCollectionUpdateSuccess": "Raccolta aggiornata",
|
||||||
"ToastCoverUpdateFailed": "Aggiornamento cover fallito",
|
"ToastCoverUpdateFailed": "Aggiornamento cover fallito",
|
||||||
|
"ToastDateTimeInvalidOrIncomplete": "Data e ora non sono valide o incomplete",
|
||||||
"ToastDeleteFileFailed": "Impossibile eliminare il file",
|
"ToastDeleteFileFailed": "Impossibile eliminare il file",
|
||||||
"ToastDeleteFileSuccess": "File eliminato",
|
"ToastDeleteFileSuccess": "File eliminato",
|
||||||
"ToastDeviceAddFailed": "Aggiunta dispositivo fallita",
|
"ToastDeviceAddFailed": "Aggiunta dispositivo fallita",
|
||||||
@@ -1004,6 +1027,7 @@
|
|||||||
"ToastNewUserTagError": "Devi selezionare almeno un tag",
|
"ToastNewUserTagError": "Devi selezionare almeno un tag",
|
||||||
"ToastNewUserUsernameError": "Inserisci un nome utente",
|
"ToastNewUserUsernameError": "Inserisci un nome utente",
|
||||||
"ToastNoNewEpisodesFound": "Nessun nuovo episodio trovato",
|
"ToastNoNewEpisodesFound": "Nessun nuovo episodio trovato",
|
||||||
|
"ToastNoRSSFeed": "Il podcast non ha un feed RSS",
|
||||||
"ToastNoUpdatesNecessary": "Nessun aggiornamento necessario",
|
"ToastNoUpdatesNecessary": "Nessun aggiornamento necessario",
|
||||||
"ToastNotificationCreateFailed": "Impossibile creare la notifica",
|
"ToastNotificationCreateFailed": "Impossibile creare la notifica",
|
||||||
"ToastNotificationDeleteFailed": "Impossibile eliminare la notifica",
|
"ToastNotificationDeleteFailed": "Impossibile eliminare la notifica",
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
{}
|
{
|
||||||
|
"ButtonAdd": "追加"
|
||||||
|
}
|
||||||
|
|||||||
@@ -484,6 +484,7 @@
|
|||||||
"LabelPersonalYearReview": "Jouw jaar in review ({0})",
|
"LabelPersonalYearReview": "Jouw jaar in review ({0})",
|
||||||
"LabelPhotoPathURL": "Foto pad/URL",
|
"LabelPhotoPathURL": "Foto pad/URL",
|
||||||
"LabelPlayMethod": "Afspeelwijze",
|
"LabelPlayMethod": "Afspeelwijze",
|
||||||
|
"LabelPlaybackRateIncrementDecrement": "Afspeel Snelheid Vermeerderen/Verminderen",
|
||||||
"LabelPlayerChapterNumberMarker": "{0} van {1}",
|
"LabelPlayerChapterNumberMarker": "{0} van {1}",
|
||||||
"LabelPlaylists": "Afspeellijsten",
|
"LabelPlaylists": "Afspeellijsten",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
@@ -704,8 +705,11 @@
|
|||||||
"MessageBackupsLocationEditNote": "Let op: het bijwerken van de back-uplocatie zal bestaande back-ups niet verplaatsen of wijzigen",
|
"MessageBackupsLocationEditNote": "Let op: het bijwerken van de back-uplocatie zal bestaande back-ups niet verplaatsen of wijzigen",
|
||||||
"MessageBackupsLocationNoEditNote": "Let op: De back-uplocatie wordt ingesteld via een omgevingsvariabele en kan hier niet worden gewijzigd.",
|
"MessageBackupsLocationNoEditNote": "Let op: De back-uplocatie wordt ingesteld via een omgevingsvariabele en kan hier niet worden gewijzigd.",
|
||||||
"MessageBackupsLocationPathEmpty": "Backup locatie pad kan niet leeg zijn",
|
"MessageBackupsLocationPathEmpty": "Backup locatie pad kan niet leeg zijn",
|
||||||
|
"MessageBatchEditPopulateMapDetailsAllHelp": "Vul actieve velden in met data van alle items. Velden met meerdere waarden zullen worden samengevoegd",
|
||||||
|
"MessageBatchEditPopulateMapDetailsItemHelp": "Vul actieve folder detail velden met de data van dit item",
|
||||||
"MessageBatchQuickMatchDescription": "Quick Match zal proberen ontbrekende covers en metadata voor de geselecteerde onderdelen te matchten. Schakel de opties hieronder in om Quick Match toe te staan bestaande covers en/of metadata te overschrijven.",
|
"MessageBatchQuickMatchDescription": "Quick Match zal proberen ontbrekende covers en metadata voor de geselecteerde onderdelen te matchten. Schakel de opties hieronder in om Quick Match toe te staan bestaande covers en/of metadata te overschrijven.",
|
||||||
"MessageBookshelfNoCollections": "Je hebt nog geen collecties gemaakt",
|
"MessageBookshelfNoCollections": "Je hebt nog geen collecties gemaakt",
|
||||||
|
"MessageBookshelfNoCollectionsHelp": "Collecties zijn publiekelijk. Alle gebruikers met toegang tot de bibliotheek kunnen ze zien.",
|
||||||
"MessageBookshelfNoRSSFeeds": "Geen RSS-feeds geopend",
|
"MessageBookshelfNoRSSFeeds": "Geen RSS-feeds geopend",
|
||||||
"MessageBookshelfNoResultsForFilter": "Geen resultaten voor filter \"{0}: {1}\"",
|
"MessageBookshelfNoResultsForFilter": "Geen resultaten voor filter \"{0}: {1}\"",
|
||||||
"MessageBookshelfNoResultsForQuery": "Geen resultaten voor query",
|
"MessageBookshelfNoResultsForQuery": "Geen resultaten voor query",
|
||||||
@@ -816,6 +820,7 @@
|
|||||||
"MessageNoTasksRunning": "Geen lopende taken",
|
"MessageNoTasksRunning": "Geen lopende taken",
|
||||||
"MessageNoUpdatesWereNecessary": "Geen bijwerkingen waren noodzakelijk",
|
"MessageNoUpdatesWereNecessary": "Geen bijwerkingen waren noodzakelijk",
|
||||||
"MessageNoUserPlaylists": "Je hebt geen afspeellijsten",
|
"MessageNoUserPlaylists": "Je hebt geen afspeellijsten",
|
||||||
|
"MessageNoUserPlaylistsHelp": "Afspeellijsten zijn privaat. Alleen de gebruikers die ze hebben gemaakt kunnen ze zien.",
|
||||||
"MessageNotYetImplemented": "Nog niet geimplementeerd",
|
"MessageNotYetImplemented": "Nog niet geimplementeerd",
|
||||||
"MessageOpmlPreviewNote": "Let op: Dit is een preview van het geparseerde OPML-bestand. De werkelijke podcasttitel wordt overgenomen uit de RSS-feed.",
|
"MessageOpmlPreviewNote": "Let op: Dit is een preview van het geparseerde OPML-bestand. De werkelijke podcasttitel wordt overgenomen uit de RSS-feed.",
|
||||||
"MessageOr": "of",
|
"MessageOr": "of",
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
"ButtonApplyChapters": "Zatwierdź rozdziały",
|
"ButtonApplyChapters": "Zatwierdź rozdziały",
|
||||||
"ButtonAuthors": "Autorzy",
|
"ButtonAuthors": "Autorzy",
|
||||||
"ButtonBack": "Wstecz",
|
"ButtonBack": "Wstecz",
|
||||||
|
"ButtonBatchEditPopulateFromExisting": "Powiel z poprzednich",
|
||||||
|
"ButtonBatchEditPopulateMapDetails": "Powiel szczegóły mapy",
|
||||||
"ButtonBrowseForFolder": "Wyszukaj folder",
|
"ButtonBrowseForFolder": "Wyszukaj folder",
|
||||||
"ButtonCancel": "Anuluj",
|
"ButtonCancel": "Anuluj",
|
||||||
"ButtonCancelEncode": "Anuluj enkodowanie",
|
"ButtonCancelEncode": "Anuluj enkodowanie",
|
||||||
@@ -31,6 +33,7 @@
|
|||||||
"ButtonEditPodcast": "Edytuj podcast",
|
"ButtonEditPodcast": "Edytuj podcast",
|
||||||
"ButtonEnable": "Włącz",
|
"ButtonEnable": "Włącz",
|
||||||
"ButtonFireAndFail": "Fail start",
|
"ButtonFireAndFail": "Fail start",
|
||||||
|
"ButtonFireOnTest": "Uruchom po zdarzeniu testowym",
|
||||||
"ButtonForceReScan": "Wymuś ponowne skanowanie",
|
"ButtonForceReScan": "Wymuś ponowne skanowanie",
|
||||||
"ButtonFullPath": "Pełna ścieżka",
|
"ButtonFullPath": "Pełna ścieżka",
|
||||||
"ButtonHide": "Ukryj",
|
"ButtonHide": "Ukryj",
|
||||||
@@ -87,6 +90,8 @@
|
|||||||
"ButtonSaveTracklist": "Zapisz listę odtwarzania",
|
"ButtonSaveTracklist": "Zapisz listę odtwarzania",
|
||||||
"ButtonScan": "Zeskanuj",
|
"ButtonScan": "Zeskanuj",
|
||||||
"ButtonScanLibrary": "Skanuj bibliotekę",
|
"ButtonScanLibrary": "Skanuj bibliotekę",
|
||||||
|
"ButtonScrollLeft": "Przewiń w lewo",
|
||||||
|
"ButtonScrollRight": "Przewiń w prawo",
|
||||||
"ButtonSearch": "Szukaj",
|
"ButtonSearch": "Szukaj",
|
||||||
"ButtonSelectFolderPath": "Wybierz ścieżkę folderu",
|
"ButtonSelectFolderPath": "Wybierz ścieżkę folderu",
|
||||||
"ButtonSeries": "Seria",
|
"ButtonSeries": "Seria",
|
||||||
@@ -155,13 +160,14 @@
|
|||||||
"HeaderMapDetails": "Szczegóły mapowania",
|
"HeaderMapDetails": "Szczegóły mapowania",
|
||||||
"HeaderMatch": "Dopasuj",
|
"HeaderMatch": "Dopasuj",
|
||||||
"HeaderMetadataOrderOfPrecedence": "Kolejność metadanych",
|
"HeaderMetadataOrderOfPrecedence": "Kolejność metadanych",
|
||||||
"HeaderMetadataToEmbed": "Osadź metadane",
|
"HeaderMetadataToEmbed": "Metadane do osadzenia",
|
||||||
"HeaderNewAccount": "Nowe konto",
|
"HeaderNewAccount": "Nowe konto",
|
||||||
"HeaderNewLibrary": "Nowa biblioteka",
|
"HeaderNewLibrary": "Nowa biblioteka",
|
||||||
"HeaderNotificationCreate": "Utwórz powiadomienie",
|
"HeaderNotificationCreate": "Utwórz powiadomienie",
|
||||||
"HeaderNotificationUpdate": "Zaktualizuj powiadomienie",
|
"HeaderNotificationUpdate": "Zaktualizuj powiadomienie",
|
||||||
"HeaderNotifications": "Powiadomienia",
|
"HeaderNotifications": "Powiadomienia",
|
||||||
"HeaderOpenIDConnectAuthentication": "Uwierzytelnianie OpenID Connect",
|
"HeaderOpenIDConnectAuthentication": "Uwierzytelnianie OpenID Connect",
|
||||||
|
"HeaderOpenListeningSessions": "Otwarte sesje słuchania",
|
||||||
"HeaderOpenRSSFeed": "Utwórz kanał RSS",
|
"HeaderOpenRSSFeed": "Utwórz kanał RSS",
|
||||||
"HeaderOtherFiles": "Inne pliki",
|
"HeaderOtherFiles": "Inne pliki",
|
||||||
"HeaderPasswordAuthentication": "Uwierzytelnianie hasłem",
|
"HeaderPasswordAuthentication": "Uwierzytelnianie hasłem",
|
||||||
@@ -188,6 +194,7 @@
|
|||||||
"HeaderSettingsExperimental": "Funkcje eksperymentalne",
|
"HeaderSettingsExperimental": "Funkcje eksperymentalne",
|
||||||
"HeaderSettingsGeneral": "Ogólne",
|
"HeaderSettingsGeneral": "Ogólne",
|
||||||
"HeaderSettingsScanner": "Skanowanie",
|
"HeaderSettingsScanner": "Skanowanie",
|
||||||
|
"HeaderSettingsWebClient": "Klient webowy",
|
||||||
"HeaderSleepTimer": "Wyłącznik czasowy",
|
"HeaderSleepTimer": "Wyłącznik czasowy",
|
||||||
"HeaderStatsLargestItems": "Największe pozycje",
|
"HeaderStatsLargestItems": "Największe pozycje",
|
||||||
"HeaderStatsLongestItems": "Najdłuższe pozycje (godziny)",
|
"HeaderStatsLongestItems": "Najdłuższe pozycje (godziny)",
|
||||||
@@ -438,7 +445,7 @@
|
|||||||
"LabelNotificationsMaxQueueSize": "Maksymalny rozmiar kolejki dla powiadomień",
|
"LabelNotificationsMaxQueueSize": "Maksymalny rozmiar kolejki dla powiadomień",
|
||||||
"LabelNotificationsMaxQueueSizeHelp": "Zdarzenia są ograniczone do 1 na sekundę. Zdarzenia będą ignorowane jeśli kolejka ma maksymalny rozmiar. Zapobiega to spamowaniu powiadomieniami.",
|
"LabelNotificationsMaxQueueSizeHelp": "Zdarzenia są ograniczone do 1 na sekundę. Zdarzenia będą ignorowane jeśli kolejka ma maksymalny rozmiar. Zapobiega to spamowaniu powiadomieniami.",
|
||||||
"LabelNumberOfBooks": "Liczba książek",
|
"LabelNumberOfBooks": "Liczba książek",
|
||||||
"LabelNumberOfEpisodes": "# odcinków",
|
"LabelNumberOfEpisodes": "# Odcinków",
|
||||||
"LabelOpenRSSFeed": "Otwórz kanał RSS",
|
"LabelOpenRSSFeed": "Otwórz kanał RSS",
|
||||||
"LabelOverwrite": "Nadpisz",
|
"LabelOverwrite": "Nadpisz",
|
||||||
"LabelPassword": "Hasło",
|
"LabelPassword": "Hasło",
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
+11
-4
@@ -10,6 +10,8 @@
|
|||||||
"ButtonApplyChapters": "Применить главы",
|
"ButtonApplyChapters": "Применить главы",
|
||||||
"ButtonAuthors": "Авторы",
|
"ButtonAuthors": "Авторы",
|
||||||
"ButtonBack": "Назад",
|
"ButtonBack": "Назад",
|
||||||
|
"ButtonBatchEditPopulateFromExisting": "Заполнить из существующих",
|
||||||
|
"ButtonBatchEditPopulateMapDetails": "Заполнить данные карты",
|
||||||
"ButtonBrowseForFolder": "Выбрать папку",
|
"ButtonBrowseForFolder": "Выбрать папку",
|
||||||
"ButtonCancel": "Отмена",
|
"ButtonCancel": "Отмена",
|
||||||
"ButtonCancelEncode": "Отменить кодирование",
|
"ButtonCancelEncode": "Отменить кодирование",
|
||||||
@@ -301,7 +303,7 @@
|
|||||||
"LabelDownload": "Скачать",
|
"LabelDownload": "Скачать",
|
||||||
"LabelDownloadNEpisodes": "Скачать {0} эпизодов",
|
"LabelDownloadNEpisodes": "Скачать {0} эпизодов",
|
||||||
"LabelDownloadable": "Загружаемый",
|
"LabelDownloadable": "Загружаемый",
|
||||||
"LabelDuration": "Длина",
|
"LabelDuration": "Продолжительность",
|
||||||
"LabelDurationComparisonExactMatch": "(точное совпадение)",
|
"LabelDurationComparisonExactMatch": "(точное совпадение)",
|
||||||
"LabelDurationComparisonLonger": "({0} дольше)",
|
"LabelDurationComparisonLonger": "({0} дольше)",
|
||||||
"LabelDurationComparisonShorter": "({0} короче)",
|
"LabelDurationComparisonShorter": "({0} короче)",
|
||||||
@@ -432,7 +434,7 @@
|
|||||||
"LabelMetadataProvider": "Провайдер",
|
"LabelMetadataProvider": "Провайдер",
|
||||||
"LabelMinute": "Минуты",
|
"LabelMinute": "Минуты",
|
||||||
"LabelMinutes": "Минуты",
|
"LabelMinutes": "Минуты",
|
||||||
"LabelMissing": "Потеряно",
|
"LabelMissing": "Отсутствует",
|
||||||
"LabelMissingEbook": "Нет e-книги",
|
"LabelMissingEbook": "Нет e-книги",
|
||||||
"LabelMissingSupplementaryEbook": "Нет дополнительной e-книги",
|
"LabelMissingSupplementaryEbook": "Нет дополнительной e-книги",
|
||||||
"LabelMobileRedirectURIs": "Разрешенные URI перенаправления с мобильных устройств",
|
"LabelMobileRedirectURIs": "Разрешенные URI перенаправления с мобильных устройств",
|
||||||
@@ -463,7 +465,7 @@
|
|||||||
"LabelNotificationsMaxQueueSize": "Макс. размер очереди для событий уведомлений",
|
"LabelNotificationsMaxQueueSize": "Макс. размер очереди для событий уведомлений",
|
||||||
"LabelNotificationsMaxQueueSizeHelp": "События ограничены 1 в секунду. События будут игнорированы если в очереди максимальное количество. Это предотвращает спам сообщениями.",
|
"LabelNotificationsMaxQueueSizeHelp": "События ограничены 1 в секунду. События будут игнорированы если в очереди максимальное количество. Это предотвращает спам сообщениями.",
|
||||||
"LabelNumberOfBooks": "Количество книг",
|
"LabelNumberOfBooks": "Количество книг",
|
||||||
"LabelNumberOfEpisodes": "# Эпизодов",
|
"LabelNumberOfEpisodes": "# из эпизодов",
|
||||||
"LabelOpenIDAdvancedPermsClaimDescription": "Имя утверждения OpenID, содержащего расширенные разрешения на действия пользователя в приложении, которые будут применяться к ролям, не являющимся администраторами (<b>если они настроены</b>). Если утверждение отсутствует в ответе, в доступе к ABS будет отказано. Если одна опция отсутствует, она будет рассматриваться как <code>false</code>. Убедитесь, что утверждение поставщика удостоверений соответствует ожидаемой структуре:",
|
"LabelOpenIDAdvancedPermsClaimDescription": "Имя утверждения OpenID, содержащего расширенные разрешения на действия пользователя в приложении, которые будут применяться к ролям, не являющимся администраторами (<b>если они настроены</b>). Если утверждение отсутствует в ответе, в доступе к ABS будет отказано. Если одна опция отсутствует, она будет рассматриваться как <code>false</code>. Убедитесь, что утверждение поставщика удостоверений соответствует ожидаемой структуре:",
|
||||||
"LabelOpenIDClaims": "Оставьте следующие параметры пустыми, чтобы отключить расширенное назначение групп и разрешений, будет автоматически присвоена группа «Пользователь».",
|
"LabelOpenIDClaims": "Оставьте следующие параметры пустыми, чтобы отключить расширенное назначение групп и разрешений, будет автоматически присвоена группа «Пользователь».",
|
||||||
"LabelOpenIDGroupClaimDescription": "Имя утверждения OpenID, содержащего список групп пользователя. Обычно их называют <code>groups</code>. <b>Если эта настройка</b> настроена, приложение будет автоматически назначать роли на основе членства пользователя в группах при условии, что эти группы названы в утверждении без учета регистра \"admin\", \"user\" или \"guest\". Утверждение должно содержать список, и если пользователь принадлежит к нескольким группам, то приложение назначит роль, соответствующую самому высокому уровню доступа. Если ни одна из групп не совпадает, доступ будет запрещен.",
|
"LabelOpenIDGroupClaimDescription": "Имя утверждения OpenID, содержащего список групп пользователя. Обычно их называют <code>groups</code>. <b>Если эта настройка</b> настроена, приложение будет автоматически назначать роли на основе членства пользователя в группах при условии, что эти группы названы в утверждении без учета регистра \"admin\", \"user\" или \"guest\". Утверждение должно содержать список, и если пользователь принадлежит к нескольким группам, то приложение назначит роль, соответствующую самому высокому уровню доступа. Если ни одна из групп не совпадает, доступ будет запрещен.",
|
||||||
@@ -484,6 +486,7 @@
|
|||||||
"LabelPersonalYearReview": "Итоги прошедшего года ({0})",
|
"LabelPersonalYearReview": "Итоги прошедшего года ({0})",
|
||||||
"LabelPhotoPathURL": "Путь к фото/URL",
|
"LabelPhotoPathURL": "Путь к фото/URL",
|
||||||
"LabelPlayMethod": "Метод воспроизведения",
|
"LabelPlayMethod": "Метод воспроизведения",
|
||||||
|
"LabelPlaybackRateIncrementDecrement": "Величина увеличения/уменьшения скорости воспроизведения",
|
||||||
"LabelPlayerChapterNumberMarker": "{0} из {1}",
|
"LabelPlayerChapterNumberMarker": "{0} из {1}",
|
||||||
"LabelPlaylists": "Плейлисты",
|
"LabelPlaylists": "Плейлисты",
|
||||||
"LabelPodcast": "Подкаст",
|
"LabelPodcast": "Подкаст",
|
||||||
@@ -651,7 +654,7 @@
|
|||||||
"LabelToolsMakeM4bDescription": "Создает .M4B файл аудиокниги с встроенными метаданными, обложкой и главами.",
|
"LabelToolsMakeM4bDescription": "Создает .M4B файл аудиокниги с встроенными метаданными, обложкой и главами.",
|
||||||
"LabelToolsSplitM4b": "Разделить M4B на MP3 файлы",
|
"LabelToolsSplitM4b": "Разделить M4B на MP3 файлы",
|
||||||
"LabelToolsSplitM4bDescription": "Создает MP3 файла из M4B, разделяет на главы с встроенными метаданными, обложкой и главами.",
|
"LabelToolsSplitM4bDescription": "Создает MP3 файла из M4B, разделяет на главы с встроенными метаданными, обложкой и главами.",
|
||||||
"LabelTotalDuration": "Общая длина",
|
"LabelTotalDuration": "Общая продолжительность",
|
||||||
"LabelTotalTimeListened": "Всего прослушано",
|
"LabelTotalTimeListened": "Всего прослушано",
|
||||||
"LabelTrackFromFilename": "Трек из Имени файла",
|
"LabelTrackFromFilename": "Трек из Имени файла",
|
||||||
"LabelTrackFromMetadata": "Трек из Метаданных",
|
"LabelTrackFromMetadata": "Трек из Метаданных",
|
||||||
@@ -704,8 +707,11 @@
|
|||||||
"MessageBackupsLocationEditNote": "Примечание: Обновление местоположения резервной копии не приведет к перемещению или изменению существующих резервных копий",
|
"MessageBackupsLocationEditNote": "Примечание: Обновление местоположения резервной копии не приведет к перемещению или изменению существующих резервных копий",
|
||||||
"MessageBackupsLocationNoEditNote": "Примечание: Местоположение резервного копирования задается с помощью переменной среды и не может быть изменено здесь.",
|
"MessageBackupsLocationNoEditNote": "Примечание: Местоположение резервного копирования задается с помощью переменной среды и не может быть изменено здесь.",
|
||||||
"MessageBackupsLocationPathEmpty": "Путь к расположению резервной копии не может быть пустым",
|
"MessageBackupsLocationPathEmpty": "Путь к расположению резервной копии не может быть пустым",
|
||||||
|
"MessageBatchEditPopulateMapDetailsAllHelp": "Заполнить включенные поля данными из всех элементов. Поля с несколькими значениями будут объединены",
|
||||||
|
"MessageBatchEditPopulateMapDetailsItemHelp": "Заполнить активированные поля сведений о карте данными из этого элемента",
|
||||||
"MessageBatchQuickMatchDescription": "Быстрый Поиск попытается добавить отсутствующие обложки и метаданные для выбранных элементов. Включите параметры ниже, чтобы разрешить Быстрому Поиску перезаписывать существующие обложки и/или метаданные.",
|
"MessageBatchQuickMatchDescription": "Быстрый Поиск попытается добавить отсутствующие обложки и метаданные для выбранных элементов. Включите параметры ниже, чтобы разрешить Быстрому Поиску перезаписывать существующие обложки и/или метаданные.",
|
||||||
"MessageBookshelfNoCollections": "Вы еще не создали ни одной коллекции",
|
"MessageBookshelfNoCollections": "Вы еще не создали ни одной коллекции",
|
||||||
|
"MessageBookshelfNoCollectionsHelp": "Коллекции являются общедоступными. Все пользователи, имеющие доступ к библиотеке, могут их просматривать.",
|
||||||
"MessageBookshelfNoRSSFeeds": "Нет открытых RSS-каналов",
|
"MessageBookshelfNoRSSFeeds": "Нет открытых RSS-каналов",
|
||||||
"MessageBookshelfNoResultsForFilter": "Нет Результатов для фильтра \"{0}: {1}\"",
|
"MessageBookshelfNoResultsForFilter": "Нет Результатов для фильтра \"{0}: {1}\"",
|
||||||
"MessageBookshelfNoResultsForQuery": "Нет результатов для запроса",
|
"MessageBookshelfNoResultsForQuery": "Нет результатов для запроса",
|
||||||
@@ -816,6 +822,7 @@
|
|||||||
"MessageNoTasksRunning": "Нет выполняемых задач",
|
"MessageNoTasksRunning": "Нет выполняемых задач",
|
||||||
"MessageNoUpdatesWereNecessary": "Обновления не требовались",
|
"MessageNoUpdatesWereNecessary": "Обновления не требовались",
|
||||||
"MessageNoUserPlaylists": "У вас нет плейлистов",
|
"MessageNoUserPlaylists": "У вас нет плейлистов",
|
||||||
|
"MessageNoUserPlaylistsHelp": "Списки воспроизведения являются конфиденциальными. Только пользователь, который их создает, может их видеть.",
|
||||||
"MessageNotYetImplemented": "Пока не реализовано",
|
"MessageNotYetImplemented": "Пока не реализовано",
|
||||||
"MessageOpmlPreviewNote": "Примечание: Это предварительный просмотр разобранного файла OPML. Фактическое название подкаста будет взято из RSS-канала.",
|
"MessageOpmlPreviewNote": "Примечание: Это предварительный просмотр разобранного файла OPML. Фактическое название подкаста будет взято из RSS-канала.",
|
||||||
"MessageOr": "или",
|
"MessageOr": "или",
|
||||||
|
|||||||
@@ -434,7 +434,7 @@
|
|||||||
"LabelMetadataProvider": "Ponudnik metapodatkov",
|
"LabelMetadataProvider": "Ponudnik metapodatkov",
|
||||||
"LabelMinute": "Minuta",
|
"LabelMinute": "Minuta",
|
||||||
"LabelMinutes": "Minute",
|
"LabelMinutes": "Minute",
|
||||||
"LabelMissing": "Manjkajoče",
|
"LabelMissing": "Manjka",
|
||||||
"LabelMissingEbook": "Nima nobene e-knjige",
|
"LabelMissingEbook": "Nima nobene e-knjige",
|
||||||
"LabelMissingSupplementaryEbook": "Nima nobene dodatne e-knjige",
|
"LabelMissingSupplementaryEbook": "Nima nobene dodatne e-knjige",
|
||||||
"LabelMobileRedirectURIs": "Dovoljeni mobilni preusmeritveni URI-ji",
|
"LabelMobileRedirectURIs": "Dovoljeni mobilni preusmeritveni URI-ji",
|
||||||
@@ -486,6 +486,7 @@
|
|||||||
"LabelPersonalYearReview": "Pregled tvojega leta ({0})",
|
"LabelPersonalYearReview": "Pregled tvojega leta ({0})",
|
||||||
"LabelPhotoPathURL": "Slika pot/URL",
|
"LabelPhotoPathURL": "Slika pot/URL",
|
||||||
"LabelPlayMethod": "Metoda predvajanja",
|
"LabelPlayMethod": "Metoda predvajanja",
|
||||||
|
"LabelPlaybackRateIncrementDecrement": "Korak povečanja/zmanjšanja hitrosti predvajanja",
|
||||||
"LabelPlayerChapterNumberMarker": "{0} od {1}",
|
"LabelPlayerChapterNumberMarker": "{0} od {1}",
|
||||||
"LabelPlaylists": "Seznami predvajanja",
|
"LabelPlaylists": "Seznami predvajanja",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
@@ -710,6 +711,7 @@
|
|||||||
"MessageBatchEditPopulateMapDetailsItemHelp": "Napolni omogočena polja s podrobnostmi zemljevida s podatki iz tega elementa",
|
"MessageBatchEditPopulateMapDetailsItemHelp": "Napolni omogočena polja s podrobnostmi zemljevida s podatki iz tega elementa",
|
||||||
"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.",
|
"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",
|
"MessageBookshelfNoCollections": "Ustvaril nisi še nobene zbirke",
|
||||||
|
"MessageBookshelfNoCollectionsHelp": "Zbirke so javne. Vsi uporabniki z dostopom do knjižnice jih lahko vidijo.",
|
||||||
"MessageBookshelfNoRSSFeeds": "Noben vir RSS ni odprt",
|
"MessageBookshelfNoRSSFeeds": "Noben vir RSS ni odprt",
|
||||||
"MessageBookshelfNoResultsForFilter": "Ni rezultatov za filter \"{0}: {1}\"",
|
"MessageBookshelfNoResultsForFilter": "Ni rezultatov za filter \"{0}: {1}\"",
|
||||||
"MessageBookshelfNoResultsForQuery": "Ni rezultatov za poizvedbo",
|
"MessageBookshelfNoResultsForQuery": "Ni rezultatov za poizvedbo",
|
||||||
@@ -820,6 +822,7 @@
|
|||||||
"MessageNoTasksRunning": "Nobeno opravili ne teče",
|
"MessageNoTasksRunning": "Nobeno opravili ne teče",
|
||||||
"MessageNoUpdatesWereNecessary": "Posodobitve niso bile potrebne",
|
"MessageNoUpdatesWereNecessary": "Posodobitve niso bile potrebne",
|
||||||
"MessageNoUserPlaylists": "Nimate seznamov predvajanja",
|
"MessageNoUserPlaylists": "Nimate seznamov predvajanja",
|
||||||
|
"MessageNoUserPlaylistsHelp": "Seznami predvajanj so zasebni. Samo uporabniki, ki jih ustvarijo, jih lahko vidijo.",
|
||||||
"MessageNotYetImplemented": "Še ni implementirano",
|
"MessageNotYetImplemented": "Še ni implementirano",
|
||||||
"MessageOpmlPreviewNote": "Opomba: To je predogled razčlenjene datoteke OPML. Dejanski naslov podcasta bo vzet iz vira RSS.",
|
"MessageOpmlPreviewNote": "Opomba: To je predogled razčlenjene datoteke OPML. Dejanski naslov podcasta bo vzet iz vira RSS.",
|
||||||
"MessageOr": "ali",
|
"MessageOr": "ali",
|
||||||
|
|||||||
+184
-85
@@ -10,11 +10,13 @@
|
|||||||
"ButtonApplyChapters": "Tillämpa kapitel",
|
"ButtonApplyChapters": "Tillämpa kapitel",
|
||||||
"ButtonAuthors": "Författare",
|
"ButtonAuthors": "Författare",
|
||||||
"ButtonBack": "Tillbaka",
|
"ButtonBack": "Tillbaka",
|
||||||
|
"ButtonBatchEditPopulateFromExisting": "Hämta befintlig information",
|
||||||
|
"ButtonBatchEditPopulateMapDetails": "Addera befintliga information",
|
||||||
"ButtonBrowseForFolder": "Bläddra efter mapp",
|
"ButtonBrowseForFolder": "Bläddra efter mapp",
|
||||||
"ButtonCancel": "Avbryt",
|
"ButtonCancel": "Avbryt",
|
||||||
"ButtonCancelEncode": "Avbryt omkodning",
|
"ButtonCancelEncode": "Avbryt omkodning",
|
||||||
"ButtonChangeRootPassword": "Ändra lösenordet för root",
|
"ButtonChangeRootPassword": "Ändra lösenordet för root",
|
||||||
"ButtonCheckAndDownloadNewEpisodes": "Kontrollera och ladda ner nya avsnitt",
|
"ButtonCheckAndDownloadNewEpisodes": "Sök & Hämta nya avsnitt",
|
||||||
"ButtonChooseAFolder": "Välj en mapp",
|
"ButtonChooseAFolder": "Välj en mapp",
|
||||||
"ButtonChooseFiles": "Välj filer",
|
"ButtonChooseFiles": "Välj filer",
|
||||||
"ButtonClearFilter": "Rensa filter",
|
"ButtonClearFilter": "Rensa filter",
|
||||||
@@ -30,7 +32,7 @@
|
|||||||
"ButtonEditChapters": "Redigera kapitel",
|
"ButtonEditChapters": "Redigera kapitel",
|
||||||
"ButtonEditPodcast": "Redigera podcast",
|
"ButtonEditPodcast": "Redigera podcast",
|
||||||
"ButtonEnable": "Aktivera",
|
"ButtonEnable": "Aktivera",
|
||||||
"ButtonForceReScan": "Tvinga omstart",
|
"ButtonForceReScan": "Starta ny skanning",
|
||||||
"ButtonFullPath": "Fullständig sökväg",
|
"ButtonFullPath": "Fullständig sökväg",
|
||||||
"ButtonHide": "Dölj",
|
"ButtonHide": "Dölj",
|
||||||
"ButtonHome": "Hem",
|
"ButtonHome": "Hem",
|
||||||
@@ -64,8 +66,8 @@
|
|||||||
"ButtonPurgeItemsCache": "Rensa cache för föremål",
|
"ButtonPurgeItemsCache": "Rensa cache för föremål",
|
||||||
"ButtonQueueAddItem": "Lägg till i kön",
|
"ButtonQueueAddItem": "Lägg till i kön",
|
||||||
"ButtonQueueRemoveItem": "Ta bort från kön",
|
"ButtonQueueRemoveItem": "Ta bort från kön",
|
||||||
"ButtonQuickMatch": "Snabb matchning",
|
"ButtonQuickMatch": "Snabbmatchning",
|
||||||
"ButtonReScan": "Omstart",
|
"ButtonReScan": "Ny skanning",
|
||||||
"ButtonRead": "Läs",
|
"ButtonRead": "Läs",
|
||||||
"ButtonReadLess": "Visa mindre",
|
"ButtonReadLess": "Visa mindre",
|
||||||
"ButtonReadMore": "Visa mer",
|
"ButtonReadMore": "Visa mer",
|
||||||
@@ -73,8 +75,8 @@
|
|||||||
"ButtonRemove": "Ta bort",
|
"ButtonRemove": "Ta bort",
|
||||||
"ButtonRemoveAll": "Ta bort alla",
|
"ButtonRemoveAll": "Ta bort alla",
|
||||||
"ButtonRemoveAllLibraryItems": "Ta bort alla objekt i biblioteket",
|
"ButtonRemoveAllLibraryItems": "Ta bort alla objekt i biblioteket",
|
||||||
"ButtonRemoveFromContinueListening": "Radera från 'Fortsätt läsa/lyssna'",
|
"ButtonRemoveFromContinueListening": "Radera från 'Fortsätt att lyssna'",
|
||||||
"ButtonRemoveFromContinueReading": "Ta bort från Fortsätt läsa",
|
"ButtonRemoveFromContinueReading": "Radera från 'Fortsätt att läsa'",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "Radera från 'Fortsätt med serien'",
|
"ButtonRemoveSeriesFromContinueSeries": "Radera från 'Fortsätt med serien'",
|
||||||
"ButtonReset": "Tillbaka",
|
"ButtonReset": "Tillbaka",
|
||||||
"ButtonResetToDefault": "Återställ till standard",
|
"ButtonResetToDefault": "Återställ till standard",
|
||||||
@@ -97,8 +99,8 @@
|
|||||||
"ButtonSubmit": "Spara",
|
"ButtonSubmit": "Spara",
|
||||||
"ButtonTest": "Testa",
|
"ButtonTest": "Testa",
|
||||||
"ButtonUpload": "Ladda upp",
|
"ButtonUpload": "Ladda upp",
|
||||||
"ButtonUploadBackup": "Ladda upp säkerhetskopia",
|
"ButtonUploadBackup": "Läs in säkerhetskopia",
|
||||||
"ButtonUploadCover": "Ladda upp bokomslag",
|
"ButtonUploadCover": "Ladda upp omslag",
|
||||||
"ButtonUploadOPMLFile": "Ladda upp OPML-fil",
|
"ButtonUploadOPMLFile": "Ladda upp OPML-fil",
|
||||||
"ButtonUserDelete": "Radera användare {0}",
|
"ButtonUserDelete": "Radera användare {0}",
|
||||||
"ButtonUserEdit": "Redigera användare {0}",
|
"ButtonUserEdit": "Redigera användare {0}",
|
||||||
@@ -110,7 +112,7 @@
|
|||||||
"HeaderAccount": "Konto",
|
"HeaderAccount": "Konto",
|
||||||
"HeaderAddCustomMetadataProvider": "Addera egen källa för metadata",
|
"HeaderAddCustomMetadataProvider": "Addera egen källa för metadata",
|
||||||
"HeaderAdvanced": "Avancerad",
|
"HeaderAdvanced": "Avancerad",
|
||||||
"HeaderAppriseNotificationSettings": "Apprise Meddelandeinställningar",
|
"HeaderAppriseNotificationSettings": "Inställningar av meddelanden med Apprise",
|
||||||
"HeaderAudioTracks": "Ljudspår",
|
"HeaderAudioTracks": "Ljudspår",
|
||||||
"HeaderAudiobookTools": "Hantering av ljudboksfil",
|
"HeaderAudiobookTools": "Hantering av ljudboksfil",
|
||||||
"HeaderAuthentication": "Autentisering",
|
"HeaderAuthentication": "Autentisering",
|
||||||
@@ -120,7 +122,7 @@
|
|||||||
"HeaderChooseAFolder": "Välj en mapp",
|
"HeaderChooseAFolder": "Välj en mapp",
|
||||||
"HeaderCollection": "Samling",
|
"HeaderCollection": "Samling",
|
||||||
"HeaderCollectionItems": "Böcker i samlingen",
|
"HeaderCollectionItems": "Böcker i samlingen",
|
||||||
"HeaderCover": "Bokomslag",
|
"HeaderCover": "Omslag",
|
||||||
"HeaderCurrentDownloads": "Aktuella nedladdningar",
|
"HeaderCurrentDownloads": "Aktuella nedladdningar",
|
||||||
"HeaderCustomMetadataProviders": "Egen källa för metadata",
|
"HeaderCustomMetadataProviders": "Egen källa för metadata",
|
||||||
"HeaderDetails": "Detaljer",
|
"HeaderDetails": "Detaljer",
|
||||||
@@ -134,8 +136,8 @@
|
|||||||
"HeaderFiles": "Filer",
|
"HeaderFiles": "Filer",
|
||||||
"HeaderFindChapters": "Hitta kapitel",
|
"HeaderFindChapters": "Hitta kapitel",
|
||||||
"HeaderIgnoredFiles": "Ignorerade filer",
|
"HeaderIgnoredFiles": "Ignorerade filer",
|
||||||
"HeaderItemFiles": "Föremålsfiler",
|
"HeaderItemFiles": "Filer",
|
||||||
"HeaderItemMetadataUtils": "Metadataverktyg för föremål",
|
"HeaderItemMetadataUtils": "Metadataverktyg",
|
||||||
"HeaderLastListeningSession": "Senaste lyssningstillfället",
|
"HeaderLastListeningSession": "Senaste lyssningstillfället",
|
||||||
"HeaderLatestEpisodes": "Senaste avsnitten",
|
"HeaderLatestEpisodes": "Senaste avsnitten",
|
||||||
"HeaderLibraries": "Bibliotek",
|
"HeaderLibraries": "Bibliotek",
|
||||||
@@ -147,12 +149,13 @@
|
|||||||
"HeaderLogs": "Loggar",
|
"HeaderLogs": "Loggar",
|
||||||
"HeaderManageGenres": "Hantera kategorier",
|
"HeaderManageGenres": "Hantera kategorier",
|
||||||
"HeaderManageTags": "Hantera taggar",
|
"HeaderManageTags": "Hantera taggar",
|
||||||
"HeaderMapDetails": "Karta detaljer",
|
"HeaderMapDetails": "Gemensam information för samtliga objekt",
|
||||||
"HeaderMatch": "Matcha",
|
"HeaderMatch": "Matcha",
|
||||||
"HeaderMetadataOrderOfPrecedence": "Prioriteringsordning vid inläsning av metadata",
|
"HeaderMetadataOrderOfPrecedence": "Prioriteringsordning vid inläsning av metadata",
|
||||||
"HeaderMetadataToEmbed": "Metadata som kommer att adderas",
|
"HeaderMetadataToEmbed": "Metadata som kommer att adderas",
|
||||||
"HeaderNewAccount": "Nytt konto",
|
"HeaderNewAccount": "Nytt konto",
|
||||||
"HeaderNewLibrary": "Nytt bibliotek",
|
"HeaderNewLibrary": "Nytt bibliotek",
|
||||||
|
"HeaderNotificationCreate": "Addera ett meddelande",
|
||||||
"HeaderNotifications": "Meddelanden",
|
"HeaderNotifications": "Meddelanden",
|
||||||
"HeaderOpenRSSFeed": "Öppna RSS-flöde",
|
"HeaderOpenRSSFeed": "Öppna RSS-flöde",
|
||||||
"HeaderOtherFiles": "Andra filer",
|
"HeaderOtherFiles": "Andra filer",
|
||||||
@@ -163,15 +166,15 @@
|
|||||||
"HeaderPlaylist": "Spellista",
|
"HeaderPlaylist": "Spellista",
|
||||||
"HeaderPlaylistItems": "Böcker i spellistan",
|
"HeaderPlaylistItems": "Böcker i spellistan",
|
||||||
"HeaderPodcastsToAdd": "Podcaster att lägga till",
|
"HeaderPodcastsToAdd": "Podcaster att lägga till",
|
||||||
"HeaderPreviewCover": "Förhandsgranska bokomslag",
|
"HeaderPreviewCover": "Förhandsgranska omslag",
|
||||||
"HeaderRSSFeedGeneral": "RSS-information",
|
"HeaderRSSFeedGeneral": "RSS-information",
|
||||||
"HeaderRSSFeedIsOpen": "RSS-flödet är öppet",
|
"HeaderRSSFeedIsOpen": "RSS-flödet är öppet",
|
||||||
"HeaderRSSFeeds": "RSS-flöden",
|
"HeaderRSSFeeds": "RSS-flöden",
|
||||||
"HeaderRemoveEpisode": "Ta bort avsnitt",
|
"HeaderRemoveEpisode": "Radera avsnitt",
|
||||||
"HeaderRemoveEpisodes": "Ta bort {0} avsnitt",
|
"HeaderRemoveEpisodes": "Radera {0} avsnitt",
|
||||||
"HeaderSavedMediaProgress": "Sparad historik",
|
"HeaderSavedMediaProgress": "Sparad historik",
|
||||||
"HeaderSchedule": "Schema",
|
"HeaderSchedule": "Schema",
|
||||||
"HeaderScheduleEpisodeDownloads": "Schemalägg automatiska avsnittsnedladdningar",
|
"HeaderScheduleEpisodeDownloads": "Schemalägg automatiska nedladdning av avsnitt",
|
||||||
"HeaderScheduleLibraryScans": "Schema för skanning av biblioteket",
|
"HeaderScheduleLibraryScans": "Schema för skanning av biblioteket",
|
||||||
"HeaderSession": "Tillfälle",
|
"HeaderSession": "Tillfälle",
|
||||||
"HeaderSetBackupSchedule": "Ange schemaläggning för säkerhetskopia",
|
"HeaderSetBackupSchedule": "Ange schemaläggning för säkerhetskopia",
|
||||||
@@ -197,15 +200,16 @@
|
|||||||
"HeaderUsers": "Användare",
|
"HeaderUsers": "Användare",
|
||||||
"HeaderYearReview": "Sammanställning av {0}",
|
"HeaderYearReview": "Sammanställning av {0}",
|
||||||
"HeaderYourStats": "Din statistik",
|
"HeaderYourStats": "Din statistik",
|
||||||
"LabelAbridged": "Förkortad",
|
"LabelAbridged": "Förkortad version",
|
||||||
"LabelAccessibleBy": "Tillgänglig för",
|
"LabelAccessibleBy": "Tillgänglig för",
|
||||||
"LabelAccountType": "Kontotyp",
|
"LabelAccountType": "Kontotyp",
|
||||||
"LabelAccountTypeAdmin": "Administratör",
|
"LabelAccountTypeAdmin": "Administratör",
|
||||||
"LabelAccountTypeGuest": "Gäst",
|
"LabelAccountTypeGuest": "Gäst",
|
||||||
"LabelAccountTypeUser": "Användare",
|
"LabelAccountTypeUser": "Användare",
|
||||||
|
"LabelActivities": "Aktiviteter",
|
||||||
"LabelActivity": "Aktivitet",
|
"LabelActivity": "Aktivitet",
|
||||||
"LabelAddToCollection": "Lägg till i en samling",
|
"LabelAddToCollection": "Lägg till i en samling",
|
||||||
"LabelAddToCollectionBatch": "Lägg till {0} böcker i en Samling",
|
"LabelAddToCollectionBatch": "Lägg till {0} böcker i samlingen",
|
||||||
"LabelAddToPlaylist": "Lägg till i en spellista",
|
"LabelAddToPlaylist": "Lägg till i en spellista",
|
||||||
"LabelAddToPlaylistBatch": "Lägg till {0} objekt i Spellistan",
|
"LabelAddToPlaylistBatch": "Lägg till {0} objekt i Spellistan",
|
||||||
"LabelAddedAt": "Datum adderad",
|
"LabelAddedAt": "Datum adderad",
|
||||||
@@ -215,7 +219,7 @@
|
|||||||
"LabelAllUsers": "Alla användare",
|
"LabelAllUsers": "Alla användare",
|
||||||
"LabelAllUsersExcludingGuests": "Alla användare utom gäster",
|
"LabelAllUsersExcludingGuests": "Alla användare utom gäster",
|
||||||
"LabelAllUsersIncludingGuests": "Alla användare inklusive gäster",
|
"LabelAllUsersIncludingGuests": "Alla användare inklusive gäster",
|
||||||
"LabelAlreadyInYourLibrary": "Redan i din samling",
|
"LabelAlreadyInYourLibrary": "Finns redan i samlingen",
|
||||||
"LabelApiToken": "API-token",
|
"LabelApiToken": "API-token",
|
||||||
"LabelAppend": "Lägg till",
|
"LabelAppend": "Lägg till",
|
||||||
"LabelAudioBitrate": "Bitrate för ljud (t.ex. 128k)",
|
"LabelAudioBitrate": "Bitrate för ljud (t.ex. 128k)",
|
||||||
@@ -228,18 +232,19 @@
|
|||||||
"LabelAutoDownloadEpisodes": "Automatisk nedladdning av avsnitt",
|
"LabelAutoDownloadEpisodes": "Automatisk nedladdning av avsnitt",
|
||||||
"LabelAutoFetchMetadata": "Automatisk nedladdning av metadata",
|
"LabelAutoFetchMetadata": "Automatisk nedladdning av metadata",
|
||||||
"LabelAutoFetchMetadataHelp": "Hämtar metadata för titel, författare och serier. Kompletterande metadata kan manuellt adderas efter uppladdningen.",
|
"LabelAutoFetchMetadataHelp": "Hämtar metadata för titel, författare och serier. Kompletterande metadata kan manuellt adderas efter uppladdningen.",
|
||||||
|
"LabelAutoLaunch": "Automatisk start",
|
||||||
"LabelAutoRegisterDescription": "Skapa automatiskt nya användare efter inloggning",
|
"LabelAutoRegisterDescription": "Skapa automatiskt nya användare efter inloggning",
|
||||||
"LabelBackToUser": "Tillbaka till användaren",
|
"LabelBackToUser": "Tillbaka till användaren",
|
||||||
"LabelBackupAudioFiles": "Säkerhetskopiera ljudfiler",
|
"LabelBackupAudioFiles": "Säkerhetskopiera ljudfiler",
|
||||||
"LabelBackupLocation": "Plats för säkerhetskopia",
|
"LabelBackupLocation": "Plats för säkerhetskopia",
|
||||||
"LabelBackupsEnableAutomaticBackups": "Aktivera automatisk säkerhetskopiering",
|
"LabelBackupsEnableAutomaticBackups": "Aktivera automatisk säkerhetskopiering",
|
||||||
"LabelBackupsEnableAutomaticBackupsHelp": "Säkerhetskopior sparas i \"/metadata/backups\"",
|
"LabelBackupsEnableAutomaticBackupsHelp": "Säkerhetskopior sparas i \"/metadata/backups\"",
|
||||||
"LabelBackupsMaxBackupSize": "Maximal storlek på säkerhetskopia (i GB) (0 = obegränsad)",
|
"LabelBackupsMaxBackupSize": "Maximal storlek på säkerhetskopia i GigaByte (0 = obegränsad)",
|
||||||
"LabelBackupsMaxBackupSizeHelp": "Som ett skydd mot en felaktig konfiguration kommer säkerhetskopior inte att genomföras om de överskrider den konfigurerade storleken.",
|
"LabelBackupsMaxBackupSizeHelp": "Som ett skydd mot en felaktig konfiguration kommer säkerhetskopior inte att genomföras om de överskrider den konfigurerade storleken.",
|
||||||
"LabelBackupsNumberToKeep": "Antal säkerhetskopior att behålla",
|
"LabelBackupsNumberToKeep": "Antal säkerhetskopior att behålla",
|
||||||
"LabelBackupsNumberToKeepHelp": "Endast en gammal säkerhetskopia tas bort åt gången, så om du redan har fler säkerhetskopior än det angivna beloppet bör du ta bort dem manuellt.",
|
"LabelBackupsNumberToKeepHelp": "Endast en gammal säkerhetskopia tas bort åt gången, så om du redan har fler säkerhetskopior än det angivna värdet bör du ta bort dem manuellt.",
|
||||||
"LabelBitrate": "Bitfrekvens",
|
"LabelBitrate": "Bitfrekvens",
|
||||||
"LabelBonus": "Bonus",
|
"LabelBonus": "Bonusavsnitt",
|
||||||
"LabelBooks": "Böcker",
|
"LabelBooks": "Böcker",
|
||||||
"LabelButtonText": "Knapptext",
|
"LabelButtonText": "Knapptext",
|
||||||
"LabelByAuthor": "av {0}",
|
"LabelByAuthor": "av {0}",
|
||||||
@@ -261,8 +266,9 @@
|
|||||||
"LabelContinueListening": "Fortsätt att lyssna",
|
"LabelContinueListening": "Fortsätt att lyssna",
|
||||||
"LabelContinueReading": "Fortsätt att läsa",
|
"LabelContinueReading": "Fortsätt att läsa",
|
||||||
"LabelContinueSeries": "Fortsätt med serien",
|
"LabelContinueSeries": "Fortsätt med serien",
|
||||||
"LabelCover": "Bokomslag",
|
"LabelCover": "Omslag",
|
||||||
"LabelCoverImageURL": "URL till omslagsbild",
|
"LabelCoverImageURL": "URL till omslagsbild",
|
||||||
|
"LabelCoverProvider": "Källa för omslag",
|
||||||
"LabelCreatedAt": "Skapad",
|
"LabelCreatedAt": "Skapad",
|
||||||
"LabelCronExpression": "Schemaläggning med hjälp av Cron (Cron Expression)",
|
"LabelCronExpression": "Schemaläggning med hjälp av Cron (Cron Expression)",
|
||||||
"LabelCurrent": "Nuvarande",
|
"LabelCurrent": "Nuvarande",
|
||||||
@@ -296,7 +302,7 @@
|
|||||||
"LabelEmailSettingsSecure": "Säker",
|
"LabelEmailSettingsSecure": "Säker",
|
||||||
"LabelEmailSettingsSecureHelp": "Om sant kommer anslutningen att använda TLS vid anslutning till servern. Om falskt används TLS om servern stöder STARTTLS-tillägget. I de flesta fall, om du ansluter till port 465, bör du ställa in detta värde till sant. För port 587 eller 25, låt det vara falskt. (från nodemailer.com/smtp/#authentication)",
|
"LabelEmailSettingsSecureHelp": "Om sant kommer anslutningen att använda TLS vid anslutning till servern. Om falskt används TLS om servern stöder STARTTLS-tillägget. I de flesta fall, om du ansluter till port 465, bör du ställa in detta värde till sant. För port 587 eller 25, låt det vara falskt. (från nodemailer.com/smtp/#authentication)",
|
||||||
"LabelEmailSettingsTestAddress": "E-postadress för test",
|
"LabelEmailSettingsTestAddress": "E-postadress för test",
|
||||||
"LabelEmbeddedCover": "Inbäddat bokomslag",
|
"LabelEmbeddedCover": "Infogat omslag",
|
||||||
"LabelEnable": "Aktivera",
|
"LabelEnable": "Aktivera",
|
||||||
"LabelEncodingBackupLocation": "En säkerhetskopia av dina orginalljudfiler kommer att placeras i katalogen:",
|
"LabelEncodingBackupLocation": "En säkerhetskopia av dina orginalljudfiler kommer att placeras i katalogen:",
|
||||||
"LabelEncodingClearItemCache": "Kom ihåg att regelbundet radera cachen för föremål. Du hittar funktionen längst ner på sidan 'Inställningar'.",
|
"LabelEncodingClearItemCache": "Kom ihåg att regelbundet radera cachen för föremål. Du hittar funktionen längst ner på sidan 'Inställningar'.",
|
||||||
@@ -309,28 +315,39 @@
|
|||||||
"LabelEnd": "Slut",
|
"LabelEnd": "Slut",
|
||||||
"LabelEndOfChapter": "Slut av kapitel",
|
"LabelEndOfChapter": "Slut av kapitel",
|
||||||
"LabelEpisode": "Avsnitt",
|
"LabelEpisode": "Avsnitt",
|
||||||
"LabelEpisodeTitle": "Avsnittsrubrik",
|
"LabelEpisodeNotLinkedToRssFeed": "Avsnittet är inte knutet till ett RSS-flöde",
|
||||||
"LabelEpisodeType": "Avsnittstyp",
|
"LabelEpisodeNumber": "Avsnitt #{0}",
|
||||||
|
"LabelEpisodeTitle": "Titel på avsnittet",
|
||||||
|
"LabelEpisodeType": "Typ av avsnitt",
|
||||||
|
"LabelEpisodeUrlFromRssFeed": "URL-adress till avsnittet i RSS-flödet",
|
||||||
|
"LabelEpisodes": "Avsnitt",
|
||||||
|
"LabelEpisodic": "Uppdelad i avsnitt",
|
||||||
"LabelExample": "Exempel",
|
"LabelExample": "Exempel",
|
||||||
"LabelExpandSeries": "Expandera serier",
|
"LabelExpandSeries": "Expandera serier",
|
||||||
"LabelFeedURL": "Flödes-URL",
|
"LabelExplicit": "Explicit version",
|
||||||
|
"LabelExplicitChecked": "Explicit version (markerad)",
|
||||||
|
"LabelExplicitUnchecked": "Ej Explicit version (ej markerad)",
|
||||||
|
"LabelExportOPML": "Exportera OPML-information",
|
||||||
|
"LabelFeedURL": "URL-adress för flödet",
|
||||||
"LabelFetchingMetadata": "Hämtar metadata",
|
"LabelFetchingMetadata": "Hämtar metadata",
|
||||||
"LabelFile": "Fil",
|
"LabelFile": "Fil",
|
||||||
"LabelFileBirthtime": "Tidpunkt, filen skapades",
|
"LabelFileBirthtime": "Tidpunkt, fil skapad",
|
||||||
"LabelFileModified": "Tidpunkt, filen ändrades",
|
"LabelFileBornDate": "Skapad {0}",
|
||||||
|
"LabelFileModified": "Tidpunkt, fil ändrad",
|
||||||
"LabelFileModifiedDate": "Ändrad {0}",
|
"LabelFileModifiedDate": "Ändrad {0}",
|
||||||
"LabelFilename": "Filnamn",
|
"LabelFilename": "Filnamn",
|
||||||
"LabelFilterByUser": "Välj användare",
|
"LabelFilterByUser": "Välj användare",
|
||||||
"LabelFindEpisodes": "Hitta avsnitt",
|
"LabelFindEpisodes": "Sök avsnitt",
|
||||||
"LabelFinished": "Avslutad",
|
"LabelFinished": "Avslutad",
|
||||||
"LabelFolder": "Mapp",
|
"LabelFolder": "Mapp",
|
||||||
"LabelFolders": "Mappar",
|
"LabelFolders": "Mappar",
|
||||||
"LabelFontBold": "Fetstil",
|
"LabelFontBold": "Fetstil",
|
||||||
"LabelFontBoldness": "Fetstil",
|
"LabelFontBoldness": "Fetstil",
|
||||||
"LabelFontFamily": "Typsnittsfamilj",
|
"LabelFontFamily": "Typsnittsfamilj",
|
||||||
"LabelFontItalic": "Kursiverad",
|
"LabelFontItalic": "Kursiv",
|
||||||
"LabelFontScale": "Skala på typsnitt",
|
"LabelFontScale": "Skala på typsnitt",
|
||||||
"LabelFontStrikethrough": "Genomstruken",
|
"LabelFontStrikethrough": "Genomstruken",
|
||||||
|
"LabelFull": "Komplett",
|
||||||
"LabelGenre": "Kategori",
|
"LabelGenre": "Kategori",
|
||||||
"LabelGenres": "Kategorier",
|
"LabelGenres": "Kategorier",
|
||||||
"LabelHardDeleteFile": "Hård radering av fil",
|
"LabelHardDeleteFile": "Hård radering av fil",
|
||||||
@@ -345,7 +362,7 @@
|
|||||||
"LabelImageURLFromTheWeb": "Skriv URL-adressen till bilden på webben",
|
"LabelImageURLFromTheWeb": "Skriv URL-adressen till bilden på webben",
|
||||||
"LabelInProgress": "Pågående",
|
"LabelInProgress": "Pågående",
|
||||||
"LabelIncludeInTracklist": "Inkludera i spårlista",
|
"LabelIncludeInTracklist": "Inkludera i spårlista",
|
||||||
"LabelIncomplete": "Ofullständig",
|
"LabelIncomplete": "Ofullständigt",
|
||||||
"LabelInterval": "Intervall",
|
"LabelInterval": "Intervall",
|
||||||
"LabelIntervalCustomDailyWeekly": "Anpassad daglig/veckovis",
|
"LabelIntervalCustomDailyWeekly": "Anpassad daglig/veckovis",
|
||||||
"LabelIntervalEvery12Hours": "Var 12:e timme",
|
"LabelIntervalEvery12Hours": "Var 12:e timme",
|
||||||
@@ -355,6 +372,7 @@
|
|||||||
"LabelIntervalEvery6Hours": "Var 6:e timme",
|
"LabelIntervalEvery6Hours": "Var 6:e timme",
|
||||||
"LabelIntervalEveryDay": "Varje dag",
|
"LabelIntervalEveryDay": "Varje dag",
|
||||||
"LabelIntervalEveryHour": "Varje timme",
|
"LabelIntervalEveryHour": "Varje timme",
|
||||||
|
"LabelIntervalEveryMinute": "Varje minut",
|
||||||
"LabelInvert": "Invertera",
|
"LabelInvert": "Invertera",
|
||||||
"LabelItem": "Objekt",
|
"LabelItem": "Objekt",
|
||||||
"LabelJumpBackwardAmount": "Inställning för \"hopp bakåt\"",
|
"LabelJumpBackwardAmount": "Inställning för \"hopp bakåt\"",
|
||||||
@@ -362,7 +380,7 @@
|
|||||||
"LabelLanguage": "Språk",
|
"LabelLanguage": "Språk",
|
||||||
"LabelLanguageDefaultServer": "Standardspråk för server",
|
"LabelLanguageDefaultServer": "Standardspråk för server",
|
||||||
"LabelLanguages": "Språk",
|
"LabelLanguages": "Språk",
|
||||||
"LabelLastBookAdded": "Bok senast tillagd",
|
"LabelLastBookAdded": "Bok senast adderad",
|
||||||
"LabelLastBookUpdated": "Bok senast uppdaterad",
|
"LabelLastBookUpdated": "Bok senast uppdaterad",
|
||||||
"LabelLastSeen": "Senast inloggad",
|
"LabelLastSeen": "Senast inloggad",
|
||||||
"LabelLastTime": "Senaste tillfället",
|
"LabelLastTime": "Senaste tillfället",
|
||||||
@@ -377,12 +395,16 @@
|
|||||||
"LabelLibraryName": "Biblioteksnamn",
|
"LabelLibraryName": "Biblioteksnamn",
|
||||||
"LabelLimit": "Begränsning",
|
"LabelLimit": "Begränsning",
|
||||||
"LabelLineSpacing": "Radavstånd",
|
"LabelLineSpacing": "Radavstånd",
|
||||||
"LabelListenAgain": "Läs/Lyssna igen",
|
"LabelListenAgain": "Lyssna igen",
|
||||||
"LabelLogLevelDebug": "Felsökning",
|
"LabelLogLevelDebug": "Felsökning",
|
||||||
"LabelLogLevelInfo": "Information",
|
"LabelLogLevelInfo": "Information",
|
||||||
"LabelLogLevelWarn": "Varningar",
|
"LabelLogLevelWarn": "Varningar",
|
||||||
"LabelLookForNewEpisodesAfterDate": "Sök efter nya avsnitt efter detta datum",
|
"LabelLookForNewEpisodesAfterDate": "Sök efter nya avsnitt efter detta datum",
|
||||||
"LabelLowestPriority": "Lägst prioritet",
|
"LabelLowestPriority": "Lägst prioritet",
|
||||||
|
"LabelMaxEpisodesToDownload": "Maximalt antal avsnitt att ladda ner (0 = obegränsat).",
|
||||||
|
"LabelMaxEpisodesToDownloadPerCheck": "Maximalt antal nya avsnitt att ladda ner per tillfälle",
|
||||||
|
"LabelMaxEpisodesToKeep": "Maximalt antal avsnitt att behålla",
|
||||||
|
"LabelMaxEpisodesToKeepHelp": "'0' innebär obegränsat antal avsnitt. Efter att nya avsnitt laddats ner raderas det äldsta avsnittet om du har mer än maximalt antal avsnitt. Endast ett avsnitt kommer att raderas per tillfälle.",
|
||||||
"LabelMediaPlayer": "Mediaspelare",
|
"LabelMediaPlayer": "Mediaspelare",
|
||||||
"LabelMediaType": "Mediatyp",
|
"LabelMediaType": "Mediatyp",
|
||||||
"LabelMetaTag": "Metadata",
|
"LabelMetaTag": "Metadata",
|
||||||
@@ -402,11 +424,11 @@
|
|||||||
"LabelNew": "Nytt",
|
"LabelNew": "Nytt",
|
||||||
"LabelNewPassword": "Nytt lösenord",
|
"LabelNewPassword": "Nytt lösenord",
|
||||||
"LabelNewestAuthors": "Senaste författarna",
|
"LabelNewestAuthors": "Senaste författarna",
|
||||||
"LabelNewestEpisodes": "Senast tillagda avsnitt",
|
"LabelNewestEpisodes": "Senaste avsnitten",
|
||||||
"LabelNextBackupDate": "Nästa datum för säkerhetskopiering",
|
"LabelNextBackupDate": "Nästa tillfälle för säkerhetskopiering",
|
||||||
"LabelNextScheduledRun": "Nästa schemalagda körning",
|
"LabelNextScheduledRun": "Nästa schemalagda körning",
|
||||||
"LabelNoCustomMetadataProviders": "Ingen egen källa för metadata",
|
"LabelNoCustomMetadataProviders": "Ingen egen källa för metadata",
|
||||||
"LabelNoEpisodesSelected": "Inga avsnitt valda",
|
"LabelNoEpisodesSelected": "Inga avsnitt har valts",
|
||||||
"LabelNotFinished": "Ej avslutad",
|
"LabelNotFinished": "Ej avslutad",
|
||||||
"LabelNotStarted": "Ej påbörjad",
|
"LabelNotStarted": "Ej påbörjad",
|
||||||
"LabelNotes": "Anteckningar",
|
"LabelNotes": "Anteckningar",
|
||||||
@@ -428,7 +450,7 @@
|
|||||||
"LabelPath": "Sökväg",
|
"LabelPath": "Sökväg",
|
||||||
"LabelPermissionsAccessAllLibraries": "Kan komma åt alla bibliotek",
|
"LabelPermissionsAccessAllLibraries": "Kan komma åt alla bibliotek",
|
||||||
"LabelPermissionsAccessAllTags": "Kan komma åt alla taggar",
|
"LabelPermissionsAccessAllTags": "Kan komma åt alla taggar",
|
||||||
"LabelPermissionsAccessExplicitContent": "Kan komma åt explicit innehåll",
|
"LabelPermissionsAccessExplicitContent": "Kan komma åt explicit version",
|
||||||
"LabelPermissionsCreateEreader": "Kan addera e-läsarenhet",
|
"LabelPermissionsCreateEreader": "Kan addera e-läsarenhet",
|
||||||
"LabelPermissionsDelete": "Kan radera",
|
"LabelPermissionsDelete": "Kan radera",
|
||||||
"LabelPermissionsDownload": "Kan ladda ner",
|
"LabelPermissionsDownload": "Kan ladda ner",
|
||||||
@@ -441,36 +463,39 @@
|
|||||||
"LabelPlaylists": "Spellistor",
|
"LabelPlaylists": "Spellistor",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcastSearchRegion": "Podcast-sökområde",
|
"LabelPodcastSearchRegion": "Podcast-sökområde",
|
||||||
"LabelPodcastType": "Podcasttyp",
|
"LabelPodcastType": "Typ av postcast",
|
||||||
"LabelPodcasts": "Podcasts",
|
"LabelPodcasts": "Podcasts",
|
||||||
"LabelPort": "Port",
|
"LabelPort": "Port",
|
||||||
"LabelPrefixesToIgnore": "Prefix att ignorera (skiftlägesokänsligt)",
|
"LabelPrefixesToIgnore": "Prefix att ignorera (skiftlägesokänsligt)",
|
||||||
"LabelPreventIndexing": "Förhindra att ditt flöde indexeras av iTunes och Google-podcastsökmotorer",
|
"LabelPreventIndexing": "Förhindra att ditt flöde indexeras av sökmotorer från iTunes och Google",
|
||||||
"LabelPrimaryEbook": "Primär e-bok",
|
"LabelPrimaryEbook": "Primär e-bok",
|
||||||
"LabelProgress": "Framsteg",
|
"LabelProgress": "Framsteg",
|
||||||
"LabelProvider": "Källa",
|
"LabelProvider": "Källa",
|
||||||
"LabelPubDate": "Publiceringsdatum",
|
"LabelPubDate": "Publiceringsdatum",
|
||||||
"LabelPublishYear": "Publiceringsår",
|
"LabelPublishYear": "Publiceringsår",
|
||||||
|
"LabelPublishedDate": "Publicerad {0}",
|
||||||
"LabelPublishedDecade": "Årtionde för publicering",
|
"LabelPublishedDecade": "Årtionde för publicering",
|
||||||
"LabelPublisher": "Utgivare",
|
"LabelPublisher": "Utgivare",
|
||||||
|
"LabelPublishers": "Utgivare",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Anpassad ägarens e-post",
|
"LabelRSSFeedCustomOwnerEmail": "Anpassad ägarens e-post",
|
||||||
"LabelRSSFeedCustomOwnerName": "Anpassat ägarnamn",
|
"LabelRSSFeedCustomOwnerName": "Anpassat ägarnamn",
|
||||||
"LabelRSSFeedOpen": "Öppna RSS-flöde",
|
"LabelRSSFeedOpen": "Öppna RSS-flöde",
|
||||||
"LabelRSSFeedPreventIndexing": "Förhindra indexering",
|
"LabelRSSFeedPreventIndexing": "Förhindra indexering",
|
||||||
"LabelRSSFeedSlug": "RSS-flödesslag",
|
"LabelRSSFeedSlug": "RSS-flödesslag",
|
||||||
"LabelRSSFeedURL": "RSS-flöde URL",
|
"LabelRSSFeedURL": "URL-adress för RSS-flödet",
|
||||||
"LabelRandomly": "Slumpartat",
|
"LabelRandomly": "Slumpartat",
|
||||||
"LabelRead": "Läst",
|
"LabelRead": "Läst",
|
||||||
"LabelReadAgain": "Läs igen",
|
"LabelReadAgain": "Läs igen",
|
||||||
"LabelReadEbookWithoutProgress": "Läs e-bok utan att behålla framsteg",
|
"LabelReadEbookWithoutProgress": "Läs e-bok utan att behålla framsteg",
|
||||||
"LabelRecentSeries": "Nyaste serierna",
|
"LabelRecentSeries": "Senaste serierna",
|
||||||
"LabelRecentlyAdded": "Nyligen tillagda",
|
"LabelRecentlyAdded": "Nyligen adderade",
|
||||||
"LabelRecommended": "Rekommenderad",
|
"LabelRecommended": "Rekommenderad",
|
||||||
|
"LabelRedo": "Gör om",
|
||||||
"LabelRegion": "Region",
|
"LabelRegion": "Region",
|
||||||
"LabelReleaseDate": "Utgivningsdatum",
|
"LabelReleaseDate": "Utgivningsdatum",
|
||||||
"LabelRemoveAllMetadataAbs": "Radera alla 'metadata.abs' filer",
|
"LabelRemoveAllMetadataAbs": "Radera alla 'metadata.abs' filer",
|
||||||
"LabelRemoveAllMetadataJson": "Radera alla 'metadata.json' filer",
|
"LabelRemoveAllMetadataJson": "Radera alla 'metadata.json' filer",
|
||||||
"LabelRemoveCover": "Ta bort bokomslag",
|
"LabelRemoveCover": "Ta bort omslag",
|
||||||
"LabelRemoveMetadataFile": "Radera metadata-filer i alla mappar i biblioteket",
|
"LabelRemoveMetadataFile": "Radera metadata-filer i alla mappar i biblioteket",
|
||||||
"LabelRemoveMetadataFileHelp": "Radera alla 'metadata.json' och 'metadata.abs' filer i dina {0} mappar.",
|
"LabelRemoveMetadataFileHelp": "Radera alla 'metadata.json' och 'metadata.abs' filer i dina {0} mappar.",
|
||||||
"LabelRowsPerPage": "Antal rader per sida",
|
"LabelRowsPerPage": "Antal rader per sida",
|
||||||
@@ -478,12 +503,13 @@
|
|||||||
"LabelSearchTitle": "Titel",
|
"LabelSearchTitle": "Titel",
|
||||||
"LabelSearchTitleOrASIN": "Sök titel eller ASIN-kod",
|
"LabelSearchTitleOrASIN": "Sök titel eller ASIN-kod",
|
||||||
"LabelSeason": "Säsong",
|
"LabelSeason": "Säsong",
|
||||||
|
"LabelSeasonNumber": "Säsong #{0}",
|
||||||
"LabelSelectAll": "Välj alla",
|
"LabelSelectAll": "Välj alla",
|
||||||
"LabelSelectAllEpisodes": "Välj alla avsnitt",
|
"LabelSelectAllEpisodes": "Välj alla avsnitt",
|
||||||
"LabelSelectEpisodesShowing": "Välj {0} avsnitt som visas",
|
"LabelSelectEpisodesShowing": "Välj {0} avsnitt som visas",
|
||||||
"LabelSelectUsers": "Välj användare",
|
"LabelSelectUsers": "Välj användare",
|
||||||
"LabelSendEbookToDevice": "Skicka e-bok till...",
|
"LabelSendEbookToDevice": "Skicka e-bok till...",
|
||||||
"LabelSequence": "Sekvens",
|
"LabelSequence": "Sekvensnummer",
|
||||||
"LabelSeries": "Serier",
|
"LabelSeries": "Serier",
|
||||||
"LabelSeriesName": "Serienamn",
|
"LabelSeriesName": "Serienamn",
|
||||||
"LabelSeriesProgress": "Status för serier",
|
"LabelSeriesProgress": "Status för serier",
|
||||||
@@ -499,16 +525,16 @@
|
|||||||
"LabelSettingsDateFormat": "Datumformat",
|
"LabelSettingsDateFormat": "Datumformat",
|
||||||
"LabelSettingsDisableWatcher": "Inaktivera Watcher",
|
"LabelSettingsDisableWatcher": "Inaktivera Watcher",
|
||||||
"LabelSettingsDisableWatcherForLibrary": "Inaktivera bevakning med Watcher för biblioteket",
|
"LabelSettingsDisableWatcherForLibrary": "Inaktivera bevakning med Watcher för biblioteket",
|
||||||
"LabelSettingsDisableWatcherHelp": "Inaktiverar automatik att addera/uppdatera objekt<br>när ändringar av filer genomförs.<br>OBS: Kräver en omstart av servern",
|
"LabelSettingsDisableWatcherHelp": "Inaktiverar automatik att addera/uppdatera<br> objekt när ändringar av filer genomförs.<br>OBS: Kräver en omstart av servern",
|
||||||
"LabelSettingsEnableWatcher": "Aktivera Watcher",
|
"LabelSettingsEnableWatcher": "Aktivera Watcher",
|
||||||
"LabelSettingsEnableWatcherForLibrary": "Aktivera bevakning med Watcher för biblioteket",
|
"LabelSettingsEnableWatcherForLibrary": "Aktivera bevakning med Watcher för biblioteket",
|
||||||
"LabelSettingsEnableWatcherHelp": "Aktiverar automatik att addera/uppdatera objekt<br>när ändringar av filer genomförs.<br>OBS: Kräver en omstart av servern",
|
"LabelSettingsEnableWatcherHelp": "Aktiverar automatik att addera/uppdatera<br> objekt när ändringar av filer genomförs.<br>OBS: Kräver en omstart av servern",
|
||||||
"LabelSettingsEpubsAllowScriptedContent": "Tillåt e-böcker i epubs-format som innehåller script",
|
"LabelSettingsEpubsAllowScriptedContent": "Tillåt e-böcker i epubs-format som innehåller script",
|
||||||
"LabelSettingsEpubsAllowScriptedContentHelp": "Tillåt att epub-filer får använda script.<br>Det rekommenderas att denna inställning är<br>avstängd när du inte litar på källan för epub-filerna.",
|
"LabelSettingsEpubsAllowScriptedContentHelp": "Tillåt att epub-filer får använda script.<br>Det rekommenderas att denna inställning är<br>avstängd när du inte litar på källan för epub-filerna.",
|
||||||
"LabelSettingsExperimentalFeatures": "Experimentella funktioner",
|
"LabelSettingsExperimentalFeatures": "Experimentella funktioner",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "Funktioner under utveckling som behöver din feedback och hjälp med testning. Klicka för att öppna diskussionen på GitHub.",
|
"LabelSettingsExperimentalFeaturesHelp": "Funktioner under utveckling som behöver din feedback och hjälp med testning. Klicka för att öppna diskussionen på GitHub.",
|
||||||
"LabelSettingsFindCovers": "Hitta ett bokomslag",
|
"LabelSettingsFindCovers": "Hitta ett omslag",
|
||||||
"LabelSettingsFindCoversHelp": "Om din bok inte har ett bokomslag inbäddat i filen eller en fil med bokomslaget i mappen kommer skannern att försöka hitta ett omslag. OBS: Detta kommer att förlänga inläsningstiden",
|
"LabelSettingsFindCoversHelp": "Om din bok INTE har ett omslag inbäddat i filen eller en fil med omslaget i mappen kommer skannern att försöka hitta ett omslag.<br>OBS: Detta kommer att förlänga inläsningstiden",
|
||||||
"LabelSettingsHideSingleBookSeries": "Dölj serier som endast innehåller en bok",
|
"LabelSettingsHideSingleBookSeries": "Dölj serier som endast innehåller en bok",
|
||||||
"LabelSettingsHideSingleBookSeriesHelp": "Serier som endast har en bok kommer att<br>döljas från sidan 'Serier' och hyllorna på startsidan.",
|
"LabelSettingsHideSingleBookSeriesHelp": "Serier som endast har en bok kommer att<br>döljas från sidan 'Serier' och hyllorna på startsidan.",
|
||||||
"LabelSettingsHomePageBookshelfView": "Använd vy liknande en bokhylla på startsidan",
|
"LabelSettingsHomePageBookshelfView": "Använd vy liknande en bokhylla på startsidan",
|
||||||
@@ -519,21 +545,22 @@
|
|||||||
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Hoppa över tidigare böcker i en serie",
|
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Hoppa över tidigare böcker i en serie",
|
||||||
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Sektionen 'Fortsätt med serien' på startsidan visar \"nästa bok\" i serien,<br>där åtminstone en bok avslutats, och ingen bok i serien har påbörjats.<br>Om detta alternativ aktiveras kommer efterföljande bok till den<br>avslutade att föreslås - istället för den första ej avslutade boken i serien.",
|
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Sektionen 'Fortsätt med serien' på startsidan visar \"nästa bok\" i serien,<br>där åtminstone en bok avslutats, och ingen bok i serien har påbörjats.<br>Om detta alternativ aktiveras kommer efterföljande bok till den<br>avslutade att föreslås - istället för den första ej avslutade boken i serien.",
|
||||||
"LabelSettingsParseSubtitles": "Hämta undertitel från bokens mapp",
|
"LabelSettingsParseSubtitles": "Hämta undertitel från bokens mapp",
|
||||||
"LabelSettingsParseSubtitlesHelp": "Hämtar undertiteln från namnet på mappen där boken lagras.<br>Undertiteln måste vara åtskilda med ett bindestreck ' - '.<br>En mapp med namnet 'Boktitel - Bokens undertitel'<br> får undertiteln \"Bokens undertitel\"",
|
"LabelSettingsParseSubtitlesHelp": "Hämtar undertiteln från namnet<br> på mappen där boken lagras.<br>Undertiteln måste vara åtskilda med ett bindestreck ' - '.<br>En mapp med namnet 'Boktitel - Bokens undertitel'<br> får undertiteln \"Bokens undertitel\"",
|
||||||
"LabelSettingsPreferMatchedMetadata": "Prioritera matchad metadata",
|
"LabelSettingsPreferMatchedMetadata": "Prioritera matchad metadata",
|
||||||
"LabelSettingsPreferMatchedMetadataHelp": "Matchad data kommer att åsidosätta objektdetaljer vid snabbmatchning. Som standard kommer snabbmatchning endast att fylla i saknade detaljer.",
|
"LabelSettingsPreferMatchedMetadataHelp": "Matchad data kommer att ersätta befintliga uppgifter vid en snabbmatchning. Som standard kommer en snabbmatchning endast att fylla i saknade detaljer.",
|
||||||
"LabelSettingsSkipMatchingBooksWithASIN": "Hoppa över matchande böcker som har en ASIN-kod",
|
"LabelSettingsSkipMatchingBooksWithASIN": "Hoppa över matchande böcker som har en ASIN-kod",
|
||||||
"LabelSettingsSkipMatchingBooksWithISBN": "Hoppa över matchande böcker som har en ISBN-kod",
|
"LabelSettingsSkipMatchingBooksWithISBN": "Hoppa över matchande böcker som har en ISBN-kod",
|
||||||
"LabelSettingsSortingIgnorePrefixes": "Ignorera prefix vid sortering",
|
"LabelSettingsSortingIgnorePrefixes": "Ignorera prefix vid sortering",
|
||||||
"LabelSettingsSortingIgnorePrefixesHelp": "För prefix som t.ex. \"the\" kommer boktiteln \"The Book Title\" att sorteras som \"Book Title, The\"",
|
"LabelSettingsSortingIgnorePrefixesHelp": "För prefix som t.ex. \"the\" kommer boktiteln \"The Book Title\" att sorteras som \"Book Title, The\"",
|
||||||
"LabelSettingsSquareBookCovers": "Använd kvadratiska bokomslag",
|
"LabelSettingsSquareBookCovers": "Använd kvadratiska omslag",
|
||||||
"LabelSettingsSquareBookCoversHelp": "Föredrar att använda kvadratiska bokomslag<br>före standardformatet 1.6:1",
|
"LabelSettingsSquareBookCoversHelp": "Föredrar att använda kvadratiska omslag<br>före standardformatet 1.6:1",
|
||||||
"LabelSettingsStoreCoversWithItem": "Lagra bokomslag med objektet",
|
"LabelSettingsStoreCoversWithItem": "Lagra omslag med objektet",
|
||||||
"LabelSettingsStoreCoversWithItemHelp": "Som standard lagras bokomslag i mappen '/metadata/items'.<br>Genom att aktivera detta alternativ kommer<br>omslagen att lagra i din biblioteksmapp.<br>Endast en fil med namnet 'cover' kommer att behållas",
|
"LabelSettingsStoreCoversWithItemHelp": "Som standard lagras omslag i mappen '/metadata/items'.<br>Genom att aktivera detta alternativ kommer<br>omslagen att lagra i din biblioteksmapp.<br>Endast en fil med namnet 'cover' kommer att behållas",
|
||||||
"LabelSettingsStoreMetadataWithItem": "Lagra metadata med objektet",
|
"LabelSettingsStoreMetadataWithItem": "Lagra metadata med objektet",
|
||||||
"LabelSettingsStoreMetadataWithItemHelp": "Som standard lagras metadatafiler i mappen '/metadata/items'. Genom att aktivera detta alternativ kommer metadatafilerna att lagra i din biblioteksmapp",
|
"LabelSettingsStoreMetadataWithItemHelp": "Som standard lagras metadatafiler i mappen '/metadata/items'. Genom att aktivera detta alternativ kommer metadatafilerna att lagra i din biblioteksmapp",
|
||||||
"LabelSettingsTimeFormat": "Tidsformat",
|
"LabelSettingsTimeFormat": "Tidsformat",
|
||||||
"LabelShare": "Dela",
|
"LabelShare": "Dela",
|
||||||
|
"LabelShareURL": "Dela URL-länk",
|
||||||
"LabelShowAll": "Visa alla",
|
"LabelShowAll": "Visa alla",
|
||||||
"LabelShowSeconds": "Visa sekunder",
|
"LabelShowSeconds": "Visa sekunder",
|
||||||
"LabelShowSubtitles": "Visa underrubriker",
|
"LabelShowSubtitles": "Visa underrubriker",
|
||||||
@@ -568,6 +595,7 @@
|
|||||||
"LabelTagsNotAccessibleToUser": "Taggar inte tillgängliga för användaren",
|
"LabelTagsNotAccessibleToUser": "Taggar inte tillgängliga för användaren",
|
||||||
"LabelTasks": "Pågående aktivitet",
|
"LabelTasks": "Pågående aktivitet",
|
||||||
"LabelTextEditorBulletedList": "Punktlista",
|
"LabelTextEditorBulletedList": "Punktlista",
|
||||||
|
"LabelTextEditorLink": "Länk",
|
||||||
"LabelTextEditorNumberedList": "Numrerad lista",
|
"LabelTextEditorNumberedList": "Numrerad lista",
|
||||||
"LabelTheme": "Utseende",
|
"LabelTheme": "Utseende",
|
||||||
"LabelThemeDark": "Mörkt",
|
"LabelThemeDark": "Mörkt",
|
||||||
@@ -603,8 +631,8 @@
|
|||||||
"LabelUndo": "Ångra",
|
"LabelUndo": "Ångra",
|
||||||
"LabelUnknown": "Okänd",
|
"LabelUnknown": "Okänd",
|
||||||
"LabelUnknownPublishDate": "Okänt publiceringsdatum",
|
"LabelUnknownPublishDate": "Okänt publiceringsdatum",
|
||||||
"LabelUpdateCover": "Uppdatera bokomslag",
|
"LabelUpdateCover": "Uppdatera omslag",
|
||||||
"LabelUpdateCoverHelp": "Tillåt att befintliga bokomslag för de valda böckerna ersätts när en matchning hittas",
|
"LabelUpdateCoverHelp": "Tillåt att befintliga omslag för de valda böckerna ersätts när en matchning hittas",
|
||||||
"LabelUpdateDetails": "Uppdatera detaljer",
|
"LabelUpdateDetails": "Uppdatera detaljer",
|
||||||
"LabelUpdateDetailsHelp": "Tillåt att befintliga detaljer för de valda böckerna ersätts när en matchning hittas",
|
"LabelUpdateDetailsHelp": "Tillåt att befintliga detaljer för de valda böckerna ersätts när en matchning hittas",
|
||||||
"LabelUpdatedAt": "Uppdaterades",
|
"LabelUpdatedAt": "Uppdaterades",
|
||||||
@@ -636,12 +664,15 @@
|
|||||||
"LabelYourProgress": "Framsteg",
|
"LabelYourProgress": "Framsteg",
|
||||||
"MessageAddToPlayerQueue": "Lägg till i spellistan",
|
"MessageAddToPlayerQueue": "Lägg till i spellistan",
|
||||||
"MessageAppriseDescription": "För att använda den här funktionen behöver du ha en instans av <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> igång eller en API som hanterar dessa begäranden. <br />Apprise API-urlen bör vara hela URL-sökvägen för att skicka meddelandet, t.ex., om din API-instans är tillgänglig på <code>http://192.168.1.1:8337</code>, bör du ange <code>http://192.168.1.1:8337/notify</code>.",
|
"MessageAppriseDescription": "För att använda den här funktionen behöver du ha en instans av <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> igång eller en API som hanterar dessa begäranden. <br />Apprise API-urlen bör vara hela URL-sökvägen för att skicka meddelandet, t.ex., om din API-instans är tillgänglig på <code>http://192.168.1.1:8337</code>, bör du ange <code>http://192.168.1.1:8337/notify</code>.",
|
||||||
"MessageBackupsDescription": "Säkerhetskopior inkluderar användare, användarnas framsteg, biblioteksobjekt, serverinställningar<br>och bilder lagrade i <code>/metadata/items</code> & <code>/metadata/authors</code>.<br>De inkluderar <strong>INTE</strong> några filer lagrade i dina biblioteksmappar.",
|
"MessageBackupsDescription": "Säkerhetskopior inkluderar användare, användarnas framsteg, biblioteksobjekt,<br>serverinställningar och bilder lagrade i <code>/metadata/items</code> & <code>/metadata/authors</code>.<br>De inkluderar <strong>INTE</strong> några filer lagrade i dina biblioteksmappar.",
|
||||||
"MessageBackupsLocationEditNote": "OBS: När du ändrar plats för säkerhetskopiorna så flyttas INTE gamla säkerhetskopior dit",
|
"MessageBackupsLocationEditNote": "OBS: När du ändrar plats för säkerhetskopiorna så flyttas INTE gamla säkerhetskopior dit.",
|
||||||
"MessageBackupsLocationNoEditNote": "OBS: Platsen där säkerhetskopiorna lagras bestäms av en central inställning och kan inte ändras här.",
|
"MessageBackupsLocationNoEditNote": "OBS: Platsen där säkerhetskopiorna lagras bestäms av en central inställning och kan inte ändras här.",
|
||||||
"MessageBackupsLocationPathEmpty": "Uppgiften om platsen för lagring av säkerhetskopior kan inte lämnas tom",
|
"MessageBackupsLocationPathEmpty": "Uppgiften om platsen för lagring av säkerhetskopior kan inte lämnas tom",
|
||||||
|
"MessageBatchEditPopulateMapDetailsAllHelp": "Adderar information från alla objekt nedan i de fält som aktiverats. Om fälten innehåller olika uppgifter kommer informationen att slås samman.",
|
||||||
|
"MessageBatchEditPopulateMapDetailsItemHelp": "Addera information från detta objekt i aktiva fält ovan",
|
||||||
"MessageBatchQuickMatchDescription": "Quick Match kommer försöka lägga till saknade omslag och metadata för de valda föremålen. Aktivera alternativen nedan för att tillåta Quick Match att överskriva befintliga omslag och/eller metadata.",
|
"MessageBatchQuickMatchDescription": "Quick Match kommer försöka lägga till saknade omslag och metadata för de valda föremålen. Aktivera alternativen nedan för att tillåta Quick Match att överskriva befintliga omslag och/eller metadata.",
|
||||||
"MessageBookshelfNoCollections": "Du har ännu inte skapat några samlingar",
|
"MessageBookshelfNoCollections": "Du har ännu inte skapat några samlingar",
|
||||||
|
"MessageBookshelfNoCollectionsHelp": "Samlingar är privata. Endast den användare som skapat en samling kan se den.",
|
||||||
"MessageBookshelfNoRSSFeeds": "Inga RSS-flöden är öppna",
|
"MessageBookshelfNoRSSFeeds": "Inga RSS-flöden är öppna",
|
||||||
"MessageBookshelfNoResultsForFilter": "Inga resultat för filter \"{0}: {1}\"",
|
"MessageBookshelfNoResultsForFilter": "Inga resultat för filter \"{0}: {1}\"",
|
||||||
"MessageBookshelfNoResultsForQuery": "Sökningen gav inget resultat",
|
"MessageBookshelfNoResultsForQuery": "Sökningen gav inget resultat",
|
||||||
@@ -660,8 +691,10 @@
|
|||||||
"MessageConfirmDeleteLibraryItem": "Detta kommer att radera objektet från databasen och ditt filsystem. Är du säker?",
|
"MessageConfirmDeleteLibraryItem": "Detta kommer att radera objektet från databasen och ditt filsystem. Är du säker?",
|
||||||
"MessageConfirmDeleteLibraryItems": "Detta kommer att radera {0} biblioteksobjekt från databasen och ditt filsystem. Är du säker?",
|
"MessageConfirmDeleteLibraryItems": "Detta kommer att radera {0} biblioteksobjekt från databasen och ditt filsystem. Är du säker?",
|
||||||
"MessageConfirmDeleteMetadataProvider": "Är du säker på att du vill radera din egen källa för metadata \"{0}\"?",
|
"MessageConfirmDeleteMetadataProvider": "Är du säker på att du vill radera din egen källa för metadata \"{0}\"?",
|
||||||
|
"MessageConfirmDeleteNotification": "Är du säker på att du vill radera detta meddelande?",
|
||||||
"MessageConfirmDeleteSession": "Är du säker på att du vill radera detta lyssningstillfälle?",
|
"MessageConfirmDeleteSession": "Är du säker på att du vill radera detta lyssningstillfälle?",
|
||||||
"MessageConfirmForceReScan": "Är du säker på att du vill tvinga omgenomsökning?",
|
"MessageConfirmEmbedMetadataInAudioFiles": "Är du säker på att du vill infoga metadata i {0} ljudfiler?",
|
||||||
|
"MessageConfirmForceReScan": "Är du säker på att du vill starta en ny skanning?",
|
||||||
"MessageConfirmMarkAllEpisodesFinished": "Är du säker på att du vill markera alla avsnitt som avslutade?",
|
"MessageConfirmMarkAllEpisodesFinished": "Är du säker på att du vill markera alla avsnitt som avslutade?",
|
||||||
"MessageConfirmMarkAllEpisodesNotFinished": "Är du säker på att du vill markera alla avsnitt som ej avslutade?",
|
"MessageConfirmMarkAllEpisodesNotFinished": "Är du säker på att du vill markera alla avsnitt som ej avslutade?",
|
||||||
"MessageConfirmMarkItemFinished": "Är du säker på att du vill markera \"{0}\" som avslutad?",
|
"MessageConfirmMarkItemFinished": "Är du säker på att du vill markera \"{0}\" som avslutad?",
|
||||||
@@ -671,19 +704,20 @@
|
|||||||
"MessageConfirmPurgeCache": "När du rensar cashen kommer katalogen <code>/metadata/cache</code> att raderas. <br /><br />Är du säker på att du vill radera katalogen?",
|
"MessageConfirmPurgeCache": "När du rensar cashen kommer katalogen <code>/metadata/cache</code> att raderas. <br /><br />Är du säker på att du vill radera katalogen?",
|
||||||
"MessageConfirmPurgeItemsCache": "När du rensar cashen för föremål kommer katalogen <code>/metadata/cache/items</code> att raderas. <br /><br />Är du säker på att du vill radera katalogen?",
|
"MessageConfirmPurgeItemsCache": "När du rensar cashen för föremål kommer katalogen <code>/metadata/cache/items</code> att raderas. <br /><br />Är du säker på att du vill radera katalogen?",
|
||||||
"MessageConfirmQuickEmbed": "VARNING! Quick embed kommer inte att säkerhetskopiera dina ljudfiler. Se till att du har en säkerhetskopia av dina ljudfiler. <br><br>Vill du fortsätta?",
|
"MessageConfirmQuickEmbed": "VARNING! Quick embed kommer inte att säkerhetskopiera dina ljudfiler. Se till att du har en säkerhetskopia av dina ljudfiler. <br><br>Vill du fortsätta?",
|
||||||
"MessageConfirmReScanLibraryItems": "Är du säker på att du vill göra en ny genomsökning för {0} objekt?",
|
"MessageConfirmQuickMatchEpisodes": "Snabbmatchning av avsnitt kommer att ersätta befintlig information vid en träff. Endast omatchade avsnitt kommer att uppdateras. Vill du fortsätta?",
|
||||||
|
"MessageConfirmReScanLibraryItems": "Är du säker på att du vill göra en ny skanning för {0} objekt?",
|
||||||
"MessageConfirmRemoveAllChapters": "Är du säker på att du vill ta bort alla kapitel?",
|
"MessageConfirmRemoveAllChapters": "Är du säker på att du vill ta bort alla kapitel?",
|
||||||
"MessageConfirmRemoveAuthor": "Är du säker på att du vill ta bort författaren \"{0}\"?",
|
"MessageConfirmRemoveAuthor": "Är du säker på att du vill ta bort författaren \"{0}\"?",
|
||||||
"MessageConfirmRemoveCollection": "Är du säker på att du vill ta bort samlingen \"{0}\"?",
|
"MessageConfirmRemoveCollection": "Är du säker på att du vill ta bort samlingen \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisode": "Är du säker på att du vill ta bort avsnittet \"{0}\"?",
|
"MessageConfirmRemoveEpisode": "Är du säker på att du vill radera avsnittet \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisodes": "Är du säker på att du vill ta bort {0} avsnitt?",
|
"MessageConfirmRemoveEpisodes": "Är du säker på att du vill radera {0} avsnitt?",
|
||||||
"MessageConfirmRemoveListeningSessions": "Är du säker på att du vill radera {0} lyssningstillfällen?",
|
"MessageConfirmRemoveListeningSessions": "Är du säker på att du vill radera {0} lyssningstillfällen?",
|
||||||
"MessageConfirmRemoveMetadataFiles": "Är du säker på att du vill radera 'metadata.{0}' filerna i alla mappar i ditt bibliotek?",
|
"MessageConfirmRemoveMetadataFiles": "Är du säker på att du vill radera filerna 'metadata.{0}' i alla mappar i ditt bibliotek?",
|
||||||
"MessageConfirmRemoveNarrator": "Är du säker på att du vill ta bort uppläsaren \"{0}\"?",
|
"MessageConfirmRemoveNarrator": "Är du säker på att du vill ta bort uppläsaren \"{0}\"?",
|
||||||
"MessageConfirmRemovePlaylist": "Är du säker på att du vill ta bort din spellista \"{0}\"?",
|
"MessageConfirmRemovePlaylist": "Är du säker på att du vill ta bort din spellista \"{0}\"?",
|
||||||
"MessageConfirmRenameGenre": "Är du säker på att du vill byta namn på kategorin \"{0}\" till \"{1}\" för alla objekt?",
|
"MessageConfirmRenameGenre": "Är du säker på att du vill byta namn på kategorin \"{0}\" till \"{1}\" för alla objekt?",
|
||||||
"MessageConfirmRenameGenreMergeNote": "OBS: Den här kategorin finns redan, så de kommer att slås samman.",
|
"MessageConfirmRenameGenreMergeNote": "OBS: Den här kategorin finns redan, så de kommer att slås samman.",
|
||||||
"MessageConfirmRenameGenreWarning": "Varning! En liknande kategori med annat skrivsätt finns redan \"{0}\".",
|
"MessageConfirmRenameGenreWarning": "VARNING! En liknande kategori med annat skrivsätt finns redan \"{0}\".",
|
||||||
"MessageConfirmRenameTag": "Är du säker på att du vill byta namn på taggen \"{0}\" till \"{1}\" för alla objekt?",
|
"MessageConfirmRenameTag": "Är du säker på att du vill byta namn på taggen \"{0}\" till \"{1}\" för alla objekt?",
|
||||||
"MessageConfirmRenameTagMergeNote": "OBS: Den här taggen finns redan, så de kommer att slås samman.",
|
"MessageConfirmRenameTagMergeNote": "OBS: Den här taggen finns redan, så de kommer att slås samman.",
|
||||||
"MessageConfirmRenameTagWarning": "VARNING! En liknande tagg med annat skrivsätt finns redan \"{0}\".",
|
"MessageConfirmRenameTagWarning": "VARNING! En liknande tagg med annat skrivsätt finns redan \"{0}\".",
|
||||||
@@ -694,7 +728,7 @@
|
|||||||
"MessageDragFilesIntoTrackOrder": "Dra filer till rätt spårordning",
|
"MessageDragFilesIntoTrackOrder": "Dra filer till rätt spårordning",
|
||||||
"MessageEmbedFinished": "Inbäddning genomförd!",
|
"MessageEmbedFinished": "Inbäddning genomförd!",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} avsnitt i kö för nedladdning",
|
"MessageEpisodesQueuedForDownload": "{0} avsnitt i kö för nedladdning",
|
||||||
"MessageEreaderDevices": "För att säkerställa överföring av e-böcker kan du bli tvungen<br>att addera ovanstående e-postadress som godkänd<br>avsändare för varje enhet angiven nedan.",
|
"MessageEreaderDevices": "För att säkerställa överföring av e-böcker kan du bli tvungen<br>att addera ovanstående e-postadress som godkänd avsändare<br>för varje enhet angiven nedan.",
|
||||||
"MessageFeedURLWillBe": "Flödes-URL kommer att vara {0}",
|
"MessageFeedURLWillBe": "Flödes-URL kommer att vara {0}",
|
||||||
"MessageFetching": "Hämtar...",
|
"MessageFetching": "Hämtar...",
|
||||||
"MessageForceReScanDescription": "kommer att göra en omgångssökning av alla filer som en färsk sökning. ID3-taggar för ljudfiler, OPF-filer och textfiler kommer att sökas som nya.",
|
"MessageForceReScanDescription": "kommer att göra en omgångssökning av alla filer som en färsk sökning. ID3-taggar för ljudfiler, OPF-filer och textfiler kommer att sökas som nya.",
|
||||||
@@ -713,19 +747,19 @@
|
|||||||
"MessageMarkAllEpisodesNotFinished": "Markera alla avsnitt som ej avslutade",
|
"MessageMarkAllEpisodesNotFinished": "Markera alla avsnitt som ej avslutade",
|
||||||
"MessageMarkAsFinished": "Markera som avslutad",
|
"MessageMarkAsFinished": "Markera som avslutad",
|
||||||
"MessageMarkAsNotFinished": "Markera som ej avslutad",
|
"MessageMarkAsNotFinished": "Markera som ej avslutad",
|
||||||
"MessageMatchBooksDescription": "kommer att försöka matcha böcker i biblioteket med en bok från<br>den valda källan och fylla i uppgifter som saknas och bokomslag.<br>Inga befintliga uppgifter kommer att ersättas.",
|
"MessageMatchBooksDescription": "kommer att försöka matcha böcker i biblioteket med en bok från den valda källan och fylla i uppgifter som saknas och omslag. Inga befintliga uppgifter kommer att ersättas.",
|
||||||
"MessageNoAudioTracks": "Inga ljudspår har hittats",
|
"MessageNoAudioTracks": "Inga ljudspår har hittats",
|
||||||
"MessageNoAuthors": "Inga författare",
|
"MessageNoAuthors": "Inga författare",
|
||||||
"MessageNoBackups": "Inga säkerhetskopior",
|
"MessageNoBackups": "Inga säkerhetskopior",
|
||||||
"MessageNoBookmarks": "Inga bokmärken",
|
"MessageNoBookmarks": "Inga bokmärken",
|
||||||
"MessageNoChapters": "Inga kapitel",
|
"MessageNoChapters": "Inga kapitel",
|
||||||
"MessageNoCollections": "Inga samlingar",
|
"MessageNoCollections": "Inga samlingar",
|
||||||
"MessageNoCoversFound": "Inga bokomslag hittades",
|
"MessageNoCoversFound": "Inga omslag hittades",
|
||||||
"MessageNoDescription": "Ingen beskrivning",
|
"MessageNoDescription": "Ingen beskrivning",
|
||||||
"MessageNoDevices": "Inga enheter angivna",
|
"MessageNoDevices": "Inga enheter angivna",
|
||||||
"MessageNoDownloadsInProgress": "Inga nedladdningar pågår för närvarande",
|
"MessageNoDownloadsInProgress": "Inga nedladdningar pågår för närvarande",
|
||||||
"MessageNoDownloadsQueued": "Inga nedladdningar i kö",
|
"MessageNoDownloadsQueued": "Inga nedladdningar i kö",
|
||||||
"MessageNoEpisodeMatchesFound": "Inga matchande avsnitt hittades",
|
"MessageNoEpisodeMatchesFound": "Inga matchande avsnitt kunde hittas",
|
||||||
"MessageNoEpisodes": "Inga avsnitt",
|
"MessageNoEpisodes": "Inga avsnitt",
|
||||||
"MessageNoFoldersAvailable": "Inga mappar tillgängliga",
|
"MessageNoFoldersAvailable": "Inga mappar tillgängliga",
|
||||||
"MessageNoGenres": "Inga kategorier",
|
"MessageNoGenres": "Inga kategorier",
|
||||||
@@ -744,54 +778,83 @@
|
|||||||
"MessageNoTasksRunning": "Inga pågående uppgifter",
|
"MessageNoTasksRunning": "Inga pågående uppgifter",
|
||||||
"MessageNoUpdatesWereNecessary": "Inga uppdateringar var nödvändiga",
|
"MessageNoUpdatesWereNecessary": "Inga uppdateringar var nödvändiga",
|
||||||
"MessageNoUserPlaylists": "Du har inga spellistor",
|
"MessageNoUserPlaylists": "Du har inga spellistor",
|
||||||
|
"MessageNoUserPlaylistsHelp": "Spellistor är privata. Endast den användare som skapat listan kan se den.",
|
||||||
"MessageNotYetImplemented": "Ännu inte implementerad",
|
"MessageNotYetImplemented": "Ännu inte implementerad",
|
||||||
"MessageOr": "eller",
|
"MessageOr": "eller",
|
||||||
"MessagePauseChapter": "Pausa kapiteluppspelning",
|
"MessagePauseChapter": "Pausa kapiteluppspelning",
|
||||||
"MessagePlayChapter": "Lyssna på kapitlets början",
|
"MessagePlayChapter": "Lyssna på kapitlets början",
|
||||||
"MessagePlaylistCreateFromCollection": "Skapa spellista från samling",
|
"MessagePlaylistCreateFromCollection": "Skapa en spellista från samlingen",
|
||||||
"MessagePleaseWait": "Vänta ett ögonblick...",
|
"MessagePleaseWait": "Vänta ett ögonblick...",
|
||||||
"MessagePodcastHasNoRSSFeedForMatching": "Podcasten har ingen RSS-flödes-URL att använda för matchning",
|
"MessagePodcastHasNoRSSFeedForMatching": "Podcasten har ingen RSS-flödes-URL att använda för matchning",
|
||||||
|
"MessagePodcastSearchField": "Skriv sökfrågan eller URL-adressen för RSS-flödet",
|
||||||
|
"MessageQuickMatchAllEpisodes": "Snabbmatchning av alla avsnitt",
|
||||||
"MessageQuickMatchDescription": "Adderar uppgifter som saknas samt en omslagsbild från<br>första träffen i resultatet vid sökningen från '{0}'.<br>Skriver inte över befintliga uppgifter om inte<br>inställningen 'Prioritera matchad metadata' är aktiverad.",
|
"MessageQuickMatchDescription": "Adderar uppgifter som saknas samt en omslagsbild från<br>första träffen i resultatet vid sökningen från '{0}'.<br>Skriver inte över befintliga uppgifter om inte<br>inställningen 'Prioritera matchad metadata' är aktiverad.",
|
||||||
"MessageRemoveChapter": "Ta bort kapitel",
|
"MessageRemoveChapter": "Ta bort kapitel",
|
||||||
"MessageRemoveEpisodes": "Ta bort {0} avsnitt",
|
"MessageRemoveEpisodes": "Radera {0} avsnitt",
|
||||||
"MessageRemoveFromPlayerQueue": "Ta bort från spellistan",
|
"MessageRemoveFromPlayerQueue": "Ta bort från spellistan",
|
||||||
"MessageRemoveUserWarning": "Är du säker på att du vill radera användaren \"{0}\"?",
|
"MessageRemoveUserWarning": "Är du säker på att du vill radera användaren \"{0}\"?",
|
||||||
"MessageReportBugsAndContribute": "Rapportera buggar, begär funktioner och bidra på",
|
"MessageReportBugsAndContribute": "Rapportera buggar, begär funktioner och bidra på",
|
||||||
"MessageResetChaptersConfirm": "Är du säker på att du vill återställa alla kapitel och ångra de ändringarna du gjort?",
|
"MessageResetChaptersConfirm": "Är du säker på att du vill återställa alla kapitel och ångra de ändringarna du gjort?",
|
||||||
"MessageRestoreBackupConfirm": "Är du säker på att du vill läsa in säkerhetskopian som skapades den",
|
"MessageRestoreBackupConfirm": "Är du säker på att du vill läsa in säkerhetskopian som skapades den",
|
||||||
"MessageRestoreBackupWarning": "Att återställa en säkerhetskopia kommer att skriva över hela databasen som finns i /config och omslagsbilder i /metadata/items & /metadata/authors.<br /><br />Säkerhetskopior ändrar inte några filer i dina biblioteksmappar. Om du har aktiverat serverinställningar för att lagra omslagskonst och metadata i dina biblioteksmappar säkerhetskopieras eller skrivs de inte över.<br /><br />Alla klienter som använder din server kommer att uppdateras automatiskt.",
|
"MessageRestoreBackupWarning": "Att återställa en säkerhetskopia kommer att skriva över hela databasen som finns i /config och omslagsbilder i /metadata/items & /metadata/authors.<br /><br />Säkerhetskopior ändrar inte några filer i dina biblioteksmappar. Om du har aktiverat serverinställningar för att lagra omslagskonst och metadata i dina biblioteksmappar säkerhetskopieras eller skrivs de inte över.<br /><br />Alla klienter som använder din server kommer att uppdateras automatiskt.",
|
||||||
|
"MessageScheduleLibraryScanNote": "För de flesta användare rekommenderas att denna funktion ej aktiveras. Istället bör funktionen 'Watcher' vara aktiverad. Watcher kommer då automatiskt identifiera förändringar i biblioteket. För vissa filsystem (som t.ex. NFS) fungerar inte Watcher. Då kan schemalagda skanningar av biblioteken användas istället.",
|
||||||
|
"MessageScheduleRunEveryWeekdayAtTime": "Startar varje {0} klockan {1}",
|
||||||
"MessageSearchResultsFor": "Sökresultat för",
|
"MessageSearchResultsFor": "Sökresultat för",
|
||||||
"MessageSelected": "{0} valda",
|
"MessageSelected": "{0} valda",
|
||||||
"MessageServerCouldNotBeReached": "Servern kunde inte nås",
|
"MessageServerCouldNotBeReached": "Servern kunde inte nås",
|
||||||
"MessageSetChaptersFromTracksDescription": "Ställ in kapitel med varje ljudfil som ett kapitel och kapitelrubrik som ljudfilens namn",
|
"MessageSetChaptersFromTracksDescription": "Ställ in kapitel med varje ljudfil som ett kapitel och kapitelrubrik som ljudfilens namn",
|
||||||
"MessageStartPlaybackAtTime": "Starta uppspelning av \"{0}\" vid tidpunkt {1}?",
|
"MessageStartPlaybackAtTime": "Starta uppspelning av \"{0}\" vid tidpunkt {1}?",
|
||||||
|
"MessageTaskAudioFileNotWritable": "Det går inte att skriva till ljudfilen \"{0}\"",
|
||||||
"MessageTaskCanceledByUser": "Uppgiften avslutades av användaren",
|
"MessageTaskCanceledByUser": "Uppgiften avslutades av användaren",
|
||||||
|
"MessageTaskDownloadingEpisodeDescription": "Laddar ner avsnitt \"{0}\"",
|
||||||
|
"MessageTaskEmbeddingMetadata": "Infogar metadata",
|
||||||
|
"MessageTaskEmbeddingMetadataDescription": "Infogar metadata i ljudboken \"{0}\"",
|
||||||
"MessageTaskEncodingM4bDescription": "Omkodning av ljudbok \"{0}\" till en M4B-fil",
|
"MessageTaskEncodingM4bDescription": "Omkodning av ljudbok \"{0}\" till en M4B-fil",
|
||||||
"MessageTaskFailed": "Misslyckades",
|
"MessageTaskFailed": "Misslyckades",
|
||||||
|
"MessageTaskFailedToBackupAudioFile": "Misslyckades med att göra backup på ljudfil \"{0}\"",
|
||||||
"MessageTaskFailedToCreateCacheDirectory": "Misslyckades med att skapa bibliotek för cachen",
|
"MessageTaskFailedToCreateCacheDirectory": "Misslyckades med att skapa bibliotek för cachen",
|
||||||
|
"MessageTaskFailedToEmbedMetadataInFile": "Misslyckades med att infoga metadata i \"{0}\"",
|
||||||
|
"MessageTaskFailedToMergeAudioFiles": "Misslyckades med att sammanfoga ljudfilerna",
|
||||||
|
"MessageTaskFailedToMoveM4bFile": "Misslyckades med att flytta M4B-filen",
|
||||||
|
"MessageTaskFailedToWriteMetadataFile": "Misslyckades med att skapa filen med metadata",
|
||||||
"MessageTaskMatchingBooksInLibrary": "Matchar böcker i biblioteket \"{0}\"",
|
"MessageTaskMatchingBooksInLibrary": "Matchar böcker i biblioteket \"{0}\"",
|
||||||
|
"MessageTaskNoFilesToScan": "Inga filer finns tillgängliga för skanning",
|
||||||
|
"MessageTaskOpmlImportDescription": "Skapar podcasts från {0} RSS-flöden",
|
||||||
|
"MessageTaskOpmlImportFeedDescription": "Importerar RSS-flödet \"{0}\"",
|
||||||
|
"MessageTaskOpmlImportFeedPodcastDescription": "Skapar podcast \"{0}\"",
|
||||||
|
"MessageTaskOpmlImportFeedPodcastExists": "En podcast finns redan med den adressen",
|
||||||
|
"MessageTaskOpmlImportFeedPodcastFailed": "Misslyckades med att skapa podcast",
|
||||||
|
"MessageTaskOpmlImportFinished": "Adderade {0} podcasts",
|
||||||
|
"MessageTaskOpmlParseFailed": "Misslyckades att tolka OPML-filen",
|
||||||
|
"MessageTaskOpmlParseFastFail": "Felaktig OPML-fil. Ingen <opml> tag eller <outline> tag finns i filen",
|
||||||
|
"MessageTaskOpmlParseNoneFound": "Inget flöde finns angivet i OPML-filen",
|
||||||
"MessageTaskScanItemsAdded": "{0} adderades",
|
"MessageTaskScanItemsAdded": "{0} adderades",
|
||||||
|
"MessageTaskScanItemsMissing": "{0} saknades",
|
||||||
"MessageTaskScanItemsUpdated": "{0} uppdaterades",
|
"MessageTaskScanItemsUpdated": "{0} uppdaterades",
|
||||||
"MessageTaskScanNoChangesNeeded": "Inget adderades eller uppdaterades",
|
"MessageTaskScanNoChangesNeeded": "Inget adderades eller uppdaterades",
|
||||||
"MessageTaskScanningLibrary": "Biblioteket \"{0}\" har skannats",
|
"MessageTaskScanningLibrary": "Biblioteket \"{0}\" har skannats",
|
||||||
|
"MessageTaskTargetDirectoryNotWritable": "Det är inte tillåtet att skriva i den angivna katalogen",
|
||||||
"MessageThinking": "Tänker...",
|
"MessageThinking": "Tänker...",
|
||||||
"MessageUploaderItemFailed": "Misslyckades med att ladda upp",
|
"MessageUploaderItemFailed": "Misslyckades med att ladda upp",
|
||||||
"MessageUploaderItemSuccess": "Uppladdning lyckades!",
|
"MessageUploaderItemSuccess": "har blivit uppladdad!",
|
||||||
"MessageUploading": "Laddar upp...",
|
"MessageUploading": "Laddar upp...",
|
||||||
"MessageValidCronExpression": "Giltigt cron-uttryck",
|
"MessageValidCronExpression": "Giltigt cron-uttryck",
|
||||||
"MessageWatcherIsDisabledGlobally": "Watcher är inaktiverad centralt under rubriken 'Inställningar'",
|
"MessageWatcherIsDisabledGlobally": "Watcher är inaktiverad centralt under rubriken 'Inställningar'",
|
||||||
"MessageXLibraryIsEmpty": "Biblioteket {0} är tomt!",
|
"MessageXLibraryIsEmpty": "Biblioteket {0} är tomt!",
|
||||||
"MessageYourAudiobookDurationIsLonger": "Varaktigheten på din ljudbok är längre än den hittade varaktigheten",
|
"MessageYourAudiobookDurationIsLonger": "Varaktigheten på din ljudbok är längre än den hittade varaktigheten",
|
||||||
"MessageYourAudiobookDurationIsShorter": "Varaktigheten på din ljudbok är kortare än den hittade varaktigheten",
|
"MessageYourAudiobookDurationIsShorter": "Varaktigheten på din ljudbok är kortare än den hittade varaktigheten",
|
||||||
"NoteChangeRootPassword": "Rotanvändaren är den enda användaren som kan ha ett tomt lösenord",
|
"NoteChangeRootPassword": "Användaren 'root' är den enda användaren som kan vara utan lösenord",
|
||||||
"NoteChapterEditorTimes": "OBS: Starttiden för första kapitlet måste vara 0:00 och starttiden för det sista kapitlet får inte överstiga ljudbokens totala varaktighet.",
|
"NoteChapterEditorTimes": "OBS: Starttiden för första kapitlet måste vara 0:00 och starttiden för det sista kapitlet får inte överstiga ljudbokens totala varaktighet.",
|
||||||
"NoteFolderPicker": "Obs: Mappar som redan är kartlagda kommer inte att visas",
|
"NoteFolderPicker": "OBS: Mappar som redan är kopplade kommer inte att visas",
|
||||||
"NoteRSSFeedPodcastAppsHttps": "Varning: De flesta podcastappar kräver att RSS-flödets URL används med HTTPS",
|
"NoteRSSFeedPodcastAppsHttps": "VARNING: De flesta applikationer för podcasts kräver att URL:en för RSS-flödet använder HTTPS",
|
||||||
"NoteRSSFeedPodcastAppsPubDate": "Varning: 1 eller flera av dina avsnitt har inte ett publiceringsdatum. Vissa podcastappar kräver detta.",
|
"NoteRSSFeedPodcastAppsPubDate": "VARNING: Ett eller flera av dina avsnitt saknar publiceringsdatum. Vissa applikationer för podcasts kräver detta.",
|
||||||
"NoteUploaderFoldersWithMediaFiles": "Mappar med flera mediefiler hanteras som separata objekt i biblioteket.",
|
"NoteUploaderFoldersWithMediaFiles": "Mappar som innehåller mediefiler hanteras som separata objekt i biblioteket.",
|
||||||
"NoteUploaderOnlyAudioFiles": "Om du bara laddar upp ljudfiler kommer varje ljudfil att hanteras som en separat ljudbok.",
|
"NoteUploaderOnlyAudioFiles": "Om du bara laddar upp ljudfiler kommer varje ljudfil att hanteras som en separat ljudbok.",
|
||||||
"NoteUploaderUnsupportedFiles": "Oaccepterade filer ignoreras. När du väljer eller släpper en mapp ignoreras andra filer som inte finns i ett objektmapp.",
|
"NoteUploaderUnsupportedFiles": "Oaccepterade filer ignoreras. När du väljer eller släpper en mapp ignoreras andra filer som inte finns i ett objektmapp.",
|
||||||
"PlaceholderNewCollection": "Nytt samlingsnamn",
|
"NotificationOnBackupCompletedDescription": "Aktiveras när en backup är genomförd",
|
||||||
|
"NotificationOnBackupFailedDescription": "Aktiveras när en backup misslyckas",
|
||||||
|
"NotificationOnEpisodeDownloadedDescription": "Aktiveras när avsnitt i en podcast automatiskt har hämtats",
|
||||||
|
"PlaceholderNewCollection": "Nytt namn på samlingen",
|
||||||
"PlaceholderNewFolderPath": "Nytt sökväg till mappen",
|
"PlaceholderNewFolderPath": "Nytt sökväg till mappen",
|
||||||
"PlaceholderNewPlaylist": "Nytt namn på spellistan",
|
"PlaceholderNewPlaylist": "Nytt namn på spellistan",
|
||||||
"PlaceholderSearch": "Sök...",
|
"PlaceholderSearch": "Sök...",
|
||||||
@@ -832,6 +895,7 @@
|
|||||||
"ToastBackupRestoreFailed": "Det gick inte att återställa säkerhetskopian",
|
"ToastBackupRestoreFailed": "Det gick inte att återställa säkerhetskopian",
|
||||||
"ToastBackupUploadFailed": "Det gick inte att ladda upp säkerhetskopian",
|
"ToastBackupUploadFailed": "Det gick inte att ladda upp säkerhetskopian",
|
||||||
"ToastBackupUploadSuccess": "Säkerhetskopian uppladdad",
|
"ToastBackupUploadSuccess": "Säkerhetskopian uppladdad",
|
||||||
|
"ToastBatchQuickMatchStarted": "Snabbmatchning av {0} böcker har påbörjats!",
|
||||||
"ToastBatchUpdateFailed": "Batchuppdateringen misslyckades",
|
"ToastBatchUpdateFailed": "Batchuppdateringen misslyckades",
|
||||||
"ToastBatchUpdateSuccess": "Batchuppdateringen lyckades",
|
"ToastBatchUpdateSuccess": "Batchuppdateringen lyckades",
|
||||||
"ToastBookmarkCreateFailed": "Det gick inte att skapa bokmärket",
|
"ToastBookmarkCreateFailed": "Det gick inte att skapa bokmärket",
|
||||||
@@ -841,28 +905,39 @@
|
|||||||
"ToastCachePurgeSuccess": "Rensning av cachen har genomförts",
|
"ToastCachePurgeSuccess": "Rensning av cachen har genomförts",
|
||||||
"ToastChaptersHaveErrors": "Kapitlen har fel",
|
"ToastChaptersHaveErrors": "Kapitlen har fel",
|
||||||
"ToastChaptersMustHaveTitles": "Kapitel måste ha titlar",
|
"ToastChaptersMustHaveTitles": "Kapitel måste ha titlar",
|
||||||
|
"ToastChaptersRemoved": "Kapitlen har raderats",
|
||||||
|
"ToastChaptersUpdated": "Kapitlen har uppdaterats",
|
||||||
"ToastCollectionItemsAddFailed": "Misslyckades med att addera böcker till samlingen",
|
"ToastCollectionItemsAddFailed": "Misslyckades med att addera böcker till samlingen",
|
||||||
"ToastCollectionRemoveSuccess": "Samlingen har raderats",
|
"ToastCollectionRemoveSuccess": "Samlingen har raderats",
|
||||||
"ToastCollectionUpdateSuccess": "Samlingen har uppdaterats",
|
"ToastCollectionUpdateSuccess": "Samlingen har uppdaterats",
|
||||||
"ToastCoverUpdateFailed": "Uppdatering av bokomslag misslyckades",
|
"ToastCoverUpdateFailed": "Uppdatering av omslag misslyckades",
|
||||||
"ToastDateTimeInvalidOrIncomplete": "Datum och klockslag är felaktigt eller ej komplett",
|
"ToastDateTimeInvalidOrIncomplete": "Datum och klockslag är felaktigt eller ej komplett",
|
||||||
"ToastDeleteFileFailed": "Misslyckades att radera filen",
|
"ToastDeleteFileFailed": "Misslyckades att radera filen",
|
||||||
"ToastDeleteFileSuccess": "Filen har raderats",
|
"ToastDeleteFileSuccess": "Filen har raderats",
|
||||||
|
"ToastDeviceAddFailed": "Misslyckades med att addera enheten",
|
||||||
|
"ToastDeviceNameAlreadyExists": "En enhet för att läsa e-böcker med det namnet finns redan",
|
||||||
"ToastDeviceTestEmailFailed": "Misslyckades med att skicka ett testmail",
|
"ToastDeviceTestEmailFailed": "Misslyckades med att skicka ett testmail",
|
||||||
"ToastDeviceTestEmailSuccess": "Ett testmail har skickats",
|
"ToastDeviceTestEmailSuccess": "Ett testmail har skickats",
|
||||||
"ToastEmailSettingsUpdateSuccess": "Inställningarna av e-post har uppdaterats",
|
"ToastEmailSettingsUpdateSuccess": "Inställningarna av e-post har uppdaterats",
|
||||||
|
"ToastEncodeCancelFailed": "Misslyckades med att avbryta omkodningen",
|
||||||
|
"ToastEncodeCancelSucces": "Omkodningen avbruten",
|
||||||
|
"ToastEpisodeDownloadQueueClearFailed": "Misslyckades med att tömma kön",
|
||||||
|
"ToastEpisodeDownloadQueueClearSuccess": "Kö för nedladdning av avsnitt har tömts",
|
||||||
|
"ToastEpisodeUpdateSuccess": "{0} avsnitt uppdaterades",
|
||||||
"ToastFailedToLoadData": "Misslyckades med att ladda data",
|
"ToastFailedToLoadData": "Misslyckades med att ladda data",
|
||||||
|
"ToastFailedToUpdate": "Misslyckades med att uppdatera",
|
||||||
"ToastInvalidImageUrl": "Felaktig URL-adress till omslagsbilden",
|
"ToastInvalidImageUrl": "Felaktig URL-adress till omslagsbilden",
|
||||||
"ToastInvalidMaxEpisodesToDownload": "Ogiltigt maximalt antal avsnitt att ladda ner",
|
"ToastInvalidMaxEpisodesToDownload": "Ogiltigt maximalt antal avsnitt att ladda ner",
|
||||||
"ToastInvalidUrl": "Felaktig URL-adress",
|
"ToastInvalidUrl": "Felaktig URL-adress",
|
||||||
"ToastItemCoverUpdateSuccess": "Objektets bokomslag har uppdaterats",
|
"ToastItemCoverUpdateSuccess": "Objektets omslag har uppdaterats",
|
||||||
"ToastItemDeletedFailed": "Misslyckades med att radera objektet",
|
"ToastItemDeletedFailed": "Misslyckades med att radera objektet",
|
||||||
"ToastItemDeletedSuccess": "Objektet har raderats",
|
"ToastItemDeletedSuccess": "Objektet har raderats",
|
||||||
"ToastItemDetailsUpdateSuccess": "Detaljerna om boken har uppdaterats",
|
"ToastItemDetailsUpdateSuccess": "Informationen om objektet har uppdaterats",
|
||||||
"ToastItemMarkedAsFinishedFailed": "Misslyckades med att markera den som avslutad",
|
"ToastItemMarkedAsFinishedFailed": "Misslyckades med att markera den som avslutad",
|
||||||
"ToastItemMarkedAsFinishedSuccess": "Den har markerat som avslutad",
|
"ToastItemMarkedAsFinishedSuccess": "Den har markerat som avslutad",
|
||||||
"ToastItemMarkedAsNotFinishedFailed": "Misslyckades med att markera den som ej avslutad",
|
"ToastItemMarkedAsNotFinishedFailed": "Misslyckades med att markera den som ej avslutad",
|
||||||
"ToastItemMarkedAsNotFinishedSuccess": "Den har markerats som ej avslutad",
|
"ToastItemMarkedAsNotFinishedSuccess": "Den har markerats som ej avslutad",
|
||||||
|
"ToastItemUpdateSuccess": "Objektet har uppdaterats",
|
||||||
"ToastLibraryCreateFailed": "Det gick inte att skapa biblioteket",
|
"ToastLibraryCreateFailed": "Det gick inte att skapa biblioteket",
|
||||||
"ToastLibraryCreateSuccess": "Biblioteket \"{0}\" har skapats",
|
"ToastLibraryCreateSuccess": "Biblioteket \"{0}\" har skapats",
|
||||||
"ToastLibraryDeleteFailed": "Det gick inte att ta bort biblioteket",
|
"ToastLibraryDeleteFailed": "Det gick inte att ta bort biblioteket",
|
||||||
@@ -870,28 +945,51 @@
|
|||||||
"ToastLibraryScanFailedToStart": "Misslyckades med att starta skanningen",
|
"ToastLibraryScanFailedToStart": "Misslyckades med att starta skanningen",
|
||||||
"ToastLibraryScanStarted": "Skanning av biblioteket påbörjad",
|
"ToastLibraryScanStarted": "Skanning av biblioteket påbörjad",
|
||||||
"ToastLibraryUpdateSuccess": "Biblioteket \"{0}\" har uppdaterats",
|
"ToastLibraryUpdateSuccess": "Biblioteket \"{0}\" har uppdaterats",
|
||||||
|
"ToastMatchAllAuthorsFailed": "Misslyckades med att matcha alla författare",
|
||||||
"ToastMetadataFilesRemovedError": "Misslyckades med att radera 'metadata.{0}' filerna",
|
"ToastMetadataFilesRemovedError": "Misslyckades med att radera 'metadata.{0}' filerna",
|
||||||
"ToastMetadataFilesRemovedNoneFound": "Inga 'metadata.{0}' filer hittades i biblioteket",
|
"ToastMetadataFilesRemovedNoneFound": "Inga 'metadata.{0}' filer hittades i biblioteket",
|
||||||
"ToastMetadataFilesRemovedNoneRemoved": "Inga 'metadata.{0}' filer raderades",
|
"ToastMetadataFilesRemovedNoneRemoved": "Inga 'metadata.{0}' filer raderades",
|
||||||
"ToastMetadataFilesRemovedSuccess": "{0} 'metadata.{1}' raderades",
|
"ToastMetadataFilesRemovedSuccess": "{0} 'metadata.{1}' raderades",
|
||||||
"ToastNameEmailRequired": "Ett namn och en e-postadress måste anges",
|
"ToastNameEmailRequired": "Ett namn och en e-postadress måste anges",
|
||||||
"ToastNameRequired": "Ett namn måste anges",
|
"ToastNameRequired": "Ett namn måste anges",
|
||||||
|
"ToastNewEpisodesFound": "Hittade {0} nya avsnitt",
|
||||||
"ToastNewUserCreatedFailed": "Misslyckades med att skapa kontot \"{0}\"",
|
"ToastNewUserCreatedFailed": "Misslyckades med att skapa kontot \"{0}\"",
|
||||||
"ToastNewUserCreatedSuccess": "Ett nytt konto har skapats",
|
"ToastNewUserCreatedSuccess": "Ett nytt konto har skapats",
|
||||||
|
"ToastNewUserLibraryError": "Minst ett bibliotek måste anges",
|
||||||
|
"ToastNewUserPasswordError": "Ett lösenord måste anges. Endast användaren 'root' kan vara utan lösenord.",
|
||||||
|
"ToastNewUserTagError": "Minst en tagg måste läggas till",
|
||||||
|
"ToastNewUserUsernameError": "Ange ett användarnamn",
|
||||||
|
"ToastNoNewEpisodesFound": "Inga nya avsnitt kunde hittas",
|
||||||
|
"ToastNoRSSFeed": "Denna podcast har ingen RSS-flöde",
|
||||||
"ToastNoUpdatesNecessary": "Inga uppdateringar var nödvändiga",
|
"ToastNoUpdatesNecessary": "Inga uppdateringar var nödvändiga",
|
||||||
|
"ToastNotificationCreateFailed": "Misslyckades med att skapa meddelandet",
|
||||||
|
"ToastNotificationDeleteFailed": "Misslyckades med att radera meddelandet",
|
||||||
|
"ToastNotificationUpdateSuccess": "Meddelandet har uppdaterats",
|
||||||
"ToastPlaylistCreateFailed": "Det gick inte att skapa spellistan",
|
"ToastPlaylistCreateFailed": "Det gick inte att skapa spellistan",
|
||||||
"ToastPlaylistCreateSuccess": "Spellistan skapad",
|
"ToastPlaylistCreateSuccess": "Spellistan skapad",
|
||||||
"ToastPlaylistRemoveSuccess": "Spellistan har tagits bort",
|
"ToastPlaylistRemoveSuccess": "Spellistan har tagits bort",
|
||||||
"ToastPlaylistUpdateSuccess": "Spellistan uppdaterad",
|
"ToastPlaylistUpdateSuccess": "Spellistan har uppdaterats",
|
||||||
"ToastPodcastCreateFailed": "Misslyckades med att skapa podcasten",
|
"ToastPodcastCreateFailed": "Misslyckades med att skapa podcasten",
|
||||||
"ToastPodcastCreateSuccess": "Podcasten skapad framgångsrikt",
|
"ToastPodcastCreateSuccess": "Podcasten skapades framgångsrikt",
|
||||||
|
"ToastPodcastNoEpisodesInFeed": "Inga avsnitt finns i RSS-flödet",
|
||||||
|
"ToastPodcastNoRssFeed": "Denna podcast har ingen RSS-flöde",
|
||||||
"ToastProviderCreatedFailed": "Misslyckades med att addera en källa",
|
"ToastProviderCreatedFailed": "Misslyckades med att addera en källa",
|
||||||
"ToastProviderCreatedSuccess": "En ny källa har adderats",
|
"ToastProviderCreatedSuccess": "En ny källa har adderats",
|
||||||
|
"ToastProviderNameAndUrlRequired": "Ett namn och en URL-adress krävs",
|
||||||
"ToastProviderRemoveSuccess": "Källan har tagits bort",
|
"ToastProviderRemoveSuccess": "Källan har tagits bort",
|
||||||
"ToastRSSFeedCloseFailed": "Misslyckades med att stänga RSS-flödet",
|
"ToastRSSFeedCloseFailed": "Misslyckades med att stänga RSS-flödet",
|
||||||
"ToastRSSFeedCloseSuccess": "RSS-flödet stängt",
|
"ToastRSSFeedCloseSuccess": "RSS-flödet stängt",
|
||||||
|
"ToastRemoveFailed": "Misslyckades med att radera",
|
||||||
"ToastRemoveItemFromCollectionFailed": "Misslyckades med att ta bort objektet från samlingen",
|
"ToastRemoveItemFromCollectionFailed": "Misslyckades med att ta bort objektet från samlingen",
|
||||||
"ToastRemoveItemFromCollectionSuccess": "Objektet borttaget från samlingen",
|
"ToastRemoveItemFromCollectionSuccess": "Objektet borttaget från samlingen",
|
||||||
|
"ToastRemoveItemsWithIssuesFailed": "Misslyckades med att radera objekt med problem",
|
||||||
|
"ToastRemoveItemsWithIssuesSuccess": "Raderade objekt med problem",
|
||||||
|
"ToastRenameFailed": "Misslyckades med att ändra namn",
|
||||||
|
"ToastRescanFailed": "Skanningen misslyckades för {0}",
|
||||||
|
"ToastRescanRemoved": "Skanningen har genomförts - objektet har raderats",
|
||||||
|
"ToastRescanUpToDate": "Skanningen har genomförts - objektet behövde inte uppdateras",
|
||||||
|
"ToastRescanUpdated": "Skanningen har genomförts - objektet har uppdaterats",
|
||||||
|
"ToastScanFailed": "Misslyckades med att skanna biblioteket",
|
||||||
"ToastSelectAtLeastOneUser": "Åtminstone en användare måste väljas",
|
"ToastSelectAtLeastOneUser": "Åtminstone en användare måste väljas",
|
||||||
"ToastSendEbookToDeviceFailed": "Misslyckades med att skicka e-boken till enheten",
|
"ToastSendEbookToDeviceFailed": "Misslyckades med att skicka e-boken till enheten",
|
||||||
"ToastSendEbookToDeviceSuccess": "E-boken skickad till enheten \"{0}\"",
|
"ToastSendEbookToDeviceSuccess": "E-boken skickad till enheten \"{0}\"",
|
||||||
@@ -912,5 +1010,6 @@
|
|||||||
"ToastUserDeleteSuccess": "Användaren borttagen",
|
"ToastUserDeleteSuccess": "Användaren borttagen",
|
||||||
"ToastUserPasswordChangeSuccess": "Lösenordet har ändrats",
|
"ToastUserPasswordChangeSuccess": "Lösenordet har ändrats",
|
||||||
"ToastUserPasswordMismatch": "Lösenorden är inte identiska",
|
"ToastUserPasswordMismatch": "Lösenorden är inte identiska",
|
||||||
"ToastUserPasswordMustChange": "Det nya lösenordet kan inte vara samma som det gamla"
|
"ToastUserPasswordMustChange": "Det nya lösenordet kan inte vara samma som det gamla",
|
||||||
|
"ToastUserRootRequireName": "Ett användarnamn för 'root' måste anges"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,209 @@
|
|||||||
|
{
|
||||||
|
"ButtonAdd": "Ekle",
|
||||||
|
"ButtonAddChapters": "Bölüm Ekle",
|
||||||
|
"ButtonAddDevice": "Cihaz Ekle",
|
||||||
|
"ButtonAddLibrary": "Kütüphane Ekle",
|
||||||
|
"ButtonAddPodcasts": "Podcast Ekle",
|
||||||
|
"ButtonAddUser": "Kullanıcı Ekle",
|
||||||
|
"ButtonAddYourFirstLibrary": "İlk kütüphaneni ekle",
|
||||||
|
"ButtonApply": "Uygula",
|
||||||
|
"ButtonApplyChapters": "Bölümleri Uygula",
|
||||||
|
"ButtonAuthors": "Yazarlar",
|
||||||
|
"ButtonBack": "Geri",
|
||||||
|
"ButtonBatchEditPopulateFromExisting": "Mevcut olandan çoğalt",
|
||||||
|
"ButtonBatchEditPopulateMapDetails": "Harita detaylarını çoğalt",
|
||||||
|
"ButtonBrowseForFolder": "Klasör için göz at",
|
||||||
|
"ButtonCancel": "İptal",
|
||||||
|
"ButtonCancelEncode": "Kodlamayı Durdur",
|
||||||
|
"ButtonChangeRootPassword": "Root Şifresini Değiştir",
|
||||||
|
"ButtonCheckAndDownloadNewEpisodes": "Yeni Bölümleri Kontrol Et & İndir",
|
||||||
|
"ButtonChooseAFolder": "Klasör seç",
|
||||||
|
"ButtonChooseFiles": "Dosya seç",
|
||||||
|
"ButtonClearFilter": "Filtreyi Temizle",
|
||||||
|
"ButtonCloseFeed": "Akışı Kapat",
|
||||||
|
"ButtonCloseSession": "Acık Oturumu Kapat",
|
||||||
|
"ButtonCollections": "Koleksiyonlar",
|
||||||
|
"ButtonConfigureScanner": "Tarayıcı Ayarları",
|
||||||
|
"ButtonCreate": "Oluştur",
|
||||||
|
"ButtonCreateBackup": "Yedek Oluştur",
|
||||||
|
"ButtonDelete": "Sil",
|
||||||
|
"ButtonDownloadQueue": "Sıra",
|
||||||
|
"ButtonEdit": "Düzenle",
|
||||||
|
"ButtonEditChapters": "Bölümleri Düzenle",
|
||||||
|
"ButtonEditPodcast": "Podcast Düzenle",
|
||||||
|
"ButtonEnable": "Etkinleştir",
|
||||||
|
"ButtonFireAndFail": "Gönder ve hata al",
|
||||||
|
"ButtonFireOnTest": "onTest Gönder",
|
||||||
|
"ButtonForceReScan": "Zorla Yeniden Tara",
|
||||||
|
"ButtonFullPath": "Tam Dosya Yolu",
|
||||||
|
"ButtonHide": "Gizle",
|
||||||
|
"ButtonHome": "Ana sayfa",
|
||||||
|
"ButtonIssues": "Sorunlar",
|
||||||
|
"ButtonJumpBackward": "Geri Sar",
|
||||||
|
"ButtonJumpForward": "İleri Sar",
|
||||||
|
"ButtonLatest": "En yeni",
|
||||||
|
"ButtonLibrary": "Kütüphane",
|
||||||
|
"ButtonLogout": "Çıkış Yap",
|
||||||
|
"ButtonLookup": "Sorgula",
|
||||||
|
"ButtonManageTracks": "Parçaları Yönet",
|
||||||
|
"ButtonMapChapterTitles": "Bölüm Başlıklarını Haritalandır",
|
||||||
|
"ButtonNevermind": "Vazgeç",
|
||||||
|
"ButtonNext": "Sonraki",
|
||||||
|
"ButtonNextChapter": "Sonraki Bölüm",
|
||||||
|
"ButtonNextItemInQueue": "Sıradaki Sonraki Öğe",
|
||||||
|
"ButtonOk": "Tamam",
|
||||||
|
"ButtonOpenFeed": "Akışı Aç",
|
||||||
|
"ButtonOpenManager": "Yöneticiyi Aç",
|
||||||
|
"ButtonPause": "Durdur",
|
||||||
|
"ButtonPlay": "Oynat",
|
||||||
|
"ButtonPlayAll": "Hepsini Oynat",
|
||||||
|
"ButtonPlaying": "Oynatılıyor",
|
||||||
|
"ButtonPlaylists": "Oynatma listeleri",
|
||||||
|
"ButtonPrevious": "Önceki",
|
||||||
|
"ButtonPreviousChapter": "Önceki Bölüm",
|
||||||
|
"ButtonProbeAudioFile": "Ses Dosyasını Yokla",
|
||||||
|
"ButtonPurgeAllCache": "Bütün Önbelleği Temizle",
|
||||||
|
"ButtonPurgeItemsCache": "Öğenin Önbelleğini Temizle",
|
||||||
|
"ButtonQueueAddItem": "Sıraya ekle",
|
||||||
|
"ButtonQueueRemoveItem": "Sıradan çıkar",
|
||||||
|
"ButtonReScan": "Yeniden Tara",
|
||||||
|
"ButtonRead": "Oku",
|
||||||
|
"ButtonReadLess": "Daha az göster",
|
||||||
|
"ButtonReadMore": "Daha fazla göster",
|
||||||
|
"ButtonRefresh": "Yenile",
|
||||||
|
"ButtonRemove": "Kaldır",
|
||||||
|
"ButtonRemoveAll": "Hepsini Sil",
|
||||||
|
"ButtonRemoveAllLibraryItems": "Bütün Kütüphane Öğelerini Sil",
|
||||||
|
"ButtonSave": "Kaydet",
|
||||||
|
"ButtonSearch": "Ara",
|
||||||
|
"ButtonSeries": "Dizi",
|
||||||
|
"ButtonSubmit": "Gönder",
|
||||||
|
"ButtonYes": "Evet",
|
||||||
|
"HeaderAccount": "Hesap",
|
||||||
|
"HeaderAdvanced": "Gelişmiş",
|
||||||
|
"HeaderAudioTracks": "Ses Kanalları",
|
||||||
|
"HeaderChapters": "Bölümler",
|
||||||
|
"HeaderCollection": "Koleksiyon",
|
||||||
|
"HeaderCollectionItems": "Koleksiyon Öğeleri",
|
||||||
|
"HeaderDetails": "Detaylar",
|
||||||
|
"HeaderEbookFiles": "Ebook Dosyaları",
|
||||||
|
"HeaderEpisodes": "Bölümler",
|
||||||
|
"HeaderEreaderSettings": "Ereader Ayarları",
|
||||||
|
"HeaderLatestEpisodes": "En son bölümler",
|
||||||
|
"HeaderLibraries": "Kütüphaneler",
|
||||||
|
"HeaderOpenRSSFeed": "RSS Akışını Aç",
|
||||||
|
"HeaderPlaylist": "Oynatma listesi",
|
||||||
|
"HeaderPlaylistItems": "Oynatma Listesi Öğeleri",
|
||||||
|
"HeaderRSSFeedGeneral": "RSS Detayları",
|
||||||
|
"HeaderRSSFeedIsOpen": "RSS Akışı Açık",
|
||||||
|
"HeaderSettings": "Ayarlar",
|
||||||
|
"HeaderSleepTimer": "Uyku Zamanlayıcısı",
|
||||||
|
"HeaderStatsMinutesListeningChart": "Dinlenilen Dakika (son 7 gün)",
|
||||||
|
"HeaderStatsRecentSessions": "Geçmiş Oturumlar",
|
||||||
|
"HeaderTableOfContents": "İçindekiler",
|
||||||
|
"HeaderYourStats": "İstatistiklerin",
|
||||||
|
"LabelAddToPlaylist": "Oynatma Listesine Ekle",
|
||||||
|
"LabelAddedAt": "Eklenme Zamanı",
|
||||||
|
"LabelAddedDate": "Eklendi {0}",
|
||||||
|
"LabelAll": "Hepsi",
|
||||||
|
"LabelAuthor": "Yazar",
|
||||||
|
"LabelAuthorFirstLast": "Yazar (İlk Son)",
|
||||||
|
"LabelAuthorLastFirst": "Yazar (Son, İlk)",
|
||||||
|
"LabelAuthors": "Yazarlar",
|
||||||
|
"LabelAutoDownloadEpisodes": "Bölümleri Otomatik Olarak İndir",
|
||||||
|
"LabelBooks": "Kitaplar",
|
||||||
|
"LabelChapters": "Bölümler",
|
||||||
|
"LabelClosePlayer": "Oynatıcıyı kapat",
|
||||||
|
"LabelCollapseSeries": "Seriyi Daralt",
|
||||||
|
"LabelComplete": "Tamamlandı",
|
||||||
|
"LabelContinueListening": "Dinlemeye Devam Et",
|
||||||
|
"LabelContinueReading": "Okumaya Devam Et",
|
||||||
|
"LabelContinueSeries": "Seriye Devam Et",
|
||||||
|
"LabelDescription": "Açıklama",
|
||||||
|
"LabelDiscover": "Keşfet",
|
||||||
|
"LabelDownload": "İndir",
|
||||||
|
"LabelDuration": "Süre",
|
||||||
|
"LabelEbook": "Ekitap",
|
||||||
|
"LabelEbooks": "Ekitaplar",
|
||||||
|
"LabelEnable": "Etkinleştir",
|
||||||
|
"LabelEnd": "Son",
|
||||||
|
"LabelEndOfChapter": "Bölüm Sonu",
|
||||||
|
"LabelEpisode": "Bölüm",
|
||||||
|
"LabelFeedURL": "Akış URLsi",
|
||||||
|
"LabelFile": "Dosya",
|
||||||
|
"LabelFileBirthtime": "Dosya Oluşum Zamanı",
|
||||||
|
"LabelFileModified": "Dosya Düzenlendi",
|
||||||
|
"LabelFilename": "Dosya İsmi",
|
||||||
|
"LabelFinished": "Tamamlandı",
|
||||||
|
"LabelFolder": "Klasör",
|
||||||
|
"LabelFontBoldness": "Font Kalınlığı",
|
||||||
|
"LabelFontScale": "Font büyüklüğü",
|
||||||
|
"LabelGenre": "Tür",
|
||||||
|
"LabelGenres": "Türler",
|
||||||
|
"LabelHasEbook": "Ekitabı var",
|
||||||
|
"LabelHasSupplementaryEbook": "İlave ekitabı var",
|
||||||
|
"LabelHost": "Sunucu",
|
||||||
|
"LabelInProgress": "İlerleme Halinde",
|
||||||
|
"LabelIncomplete": "Tamamlanmamış",
|
||||||
|
"LabelLanguage": "Dil",
|
||||||
|
"LabelLayout": "Düzen",
|
||||||
|
"LabelLayoutSinglePage": "Tek sayfa",
|
||||||
|
"LabelLineSpacing": "Satır aralığı",
|
||||||
|
"LabelListenAgain": "Tekrar Dinle",
|
||||||
|
"LabelMediaType": "Medya Türü",
|
||||||
|
"LabelMissing": "Kayıp",
|
||||||
|
"LabelMore": "Daha fazla",
|
||||||
|
"LabelMoreInfo": "Daha fazla bilgi",
|
||||||
|
"LabelName": "İsim",
|
||||||
|
"LabelNarrator": "Anlatıcı",
|
||||||
|
"LabelNarrators": "Anlatıcılar",
|
||||||
|
"LabelNewestAuthors": "En Yeni Yazarlar",
|
||||||
|
"LabelNewestEpisodes": "En Yeni Bölümler",
|
||||||
|
"LabelNotFinished": "Tamamlanmadı",
|
||||||
|
"LabelNotStarted": "Başlanmadı",
|
||||||
|
"LabelNumberOfEpisodes": "Bölüm Sayısı",
|
||||||
|
"LabelPassword": "Şifre",
|
||||||
|
"LabelPath": "Yol",
|
||||||
|
"LabelPodcast": "Podcast",
|
||||||
|
"LabelPodcasts": "Podcastler",
|
||||||
|
"LabelPreventIndexing": "Akışınızın iTunes ve Google podcast dizinleri tarafından dizinlenmesini önleyin",
|
||||||
|
"LabelProgress": "İlerleme",
|
||||||
|
"LabelPubDate": "Yay. Tarihi",
|
||||||
|
"LabelPublishYear": "Yayım Yılı",
|
||||||
|
"LabelPublishedDate": "Yayımlandı {0}",
|
||||||
|
"LabelRSSFeedCustomOwnerEmail": "Özelleştirilmiş sahip Emaili",
|
||||||
|
"LabelRSSFeedCustomOwnerName": "Özelleştirilmis sahip İsmi",
|
||||||
|
"LabelRSSFeedPreventIndexing": "Dizinlemeyi Önle",
|
||||||
|
"LabelRandomly": "Rastgele",
|
||||||
|
"LabelRead": "Oku",
|
||||||
|
"LabelReadAgain": "Tekrar Oku",
|
||||||
|
"LabelRecentlyAdded": "Yakınlarda Eklenmiş",
|
||||||
|
"LabelSeason": "Sezon",
|
||||||
|
"LabelSetEbookAsPrimary": "Birincil olarak ayarla",
|
||||||
|
"LabelSetEbookAsSupplementary": "Yedek olarak ayarla",
|
||||||
|
"LabelShowAll": "Hepsini Göster",
|
||||||
|
"LabelSize": "Boyut",
|
||||||
|
"LabelSleepTimer": "Uyku Zamanlayıcısı",
|
||||||
|
"LabelStart": "Başla",
|
||||||
|
"LabelStatsBestDay": "En İyi Gün",
|
||||||
|
"LabelStatsDailyAverage": "Günlük Ortalama",
|
||||||
|
"LabelStatsDays": "Günler",
|
||||||
|
"LabelStatsDaysListened": "Dinlenen Günler",
|
||||||
|
"LabelStatsInARow": "art arda",
|
||||||
|
"LabelStatsItemsFinished": "Bitirilen Öğeler",
|
||||||
|
"LabelStatsMinutes": "dakika",
|
||||||
|
"LabelStatsMinutesListening": "Dinlenen Dakika",
|
||||||
|
"LabelTag": "Etiket",
|
||||||
|
"LabelTags": "Etiketler",
|
||||||
|
"LabelTheme": "Tema",
|
||||||
|
"LabelThemeDark": "Koyu",
|
||||||
|
"LabelThemeLight": "Açık",
|
||||||
|
"LabelTimeRemaining": "{0} kalan",
|
||||||
|
"LabelTitle": "Başlık",
|
||||||
|
"LabelTracks": "Parçalar",
|
||||||
|
"LabelType": "Tür",
|
||||||
|
"LabelUnknown": "Bilinmeyen",
|
||||||
|
"LabelUser": "Kullanıcı",
|
||||||
|
"LabelUsername": "Kullanıcı Adı",
|
||||||
|
"LabelYourBookmarks": "Yer İşaretleriniz"
|
||||||
|
}
|
||||||
@@ -219,6 +219,7 @@
|
|||||||
"LabelAccountTypeAdmin": "Адміністратор",
|
"LabelAccountTypeAdmin": "Адміністратор",
|
||||||
"LabelAccountTypeGuest": "Гість",
|
"LabelAccountTypeGuest": "Гість",
|
||||||
"LabelAccountTypeUser": "Користувач",
|
"LabelAccountTypeUser": "Користувач",
|
||||||
|
"LabelActivities": "Діяльність",
|
||||||
"LabelActivity": "Активність",
|
"LabelActivity": "Активність",
|
||||||
"LabelAddToCollection": "Додати у добірку",
|
"LabelAddToCollection": "Додати у добірку",
|
||||||
"LabelAddToCollectionBatch": "Додати книги до добірки: {0}",
|
"LabelAddToCollectionBatch": "Додати книги до добірки: {0}",
|
||||||
@@ -283,6 +284,7 @@
|
|||||||
"LabelContinueSeries": "Продовжити серії",
|
"LabelContinueSeries": "Продовжити серії",
|
||||||
"LabelCover": "Обкладинка",
|
"LabelCover": "Обкладинка",
|
||||||
"LabelCoverImageURL": "URL-адреса обкладинки",
|
"LabelCoverImageURL": "URL-адреса обкладинки",
|
||||||
|
"LabelCoverProvider": "Постачальник покриття",
|
||||||
"LabelCreatedAt": "Дата створення",
|
"LabelCreatedAt": "Дата створення",
|
||||||
"LabelCronExpression": "Команда cron",
|
"LabelCronExpression": "Команда cron",
|
||||||
"LabelCurrent": "Поточне",
|
"LabelCurrent": "Поточне",
|
||||||
@@ -391,6 +393,7 @@
|
|||||||
"LabelIntervalEvery6Hours": "Кожні 6 годин",
|
"LabelIntervalEvery6Hours": "Кожні 6 годин",
|
||||||
"LabelIntervalEveryDay": "Щодня",
|
"LabelIntervalEveryDay": "Щодня",
|
||||||
"LabelIntervalEveryHour": "Щогодини",
|
"LabelIntervalEveryHour": "Щогодини",
|
||||||
|
"LabelIntervalEveryMinute": "Кожну хвилину",
|
||||||
"LabelInvert": "Інвертувати",
|
"LabelInvert": "Інвертувати",
|
||||||
"LabelItem": "Елемент",
|
"LabelItem": "Елемент",
|
||||||
"LabelJumpBackwardAmount": "Час переходу назад",
|
"LabelJumpBackwardAmount": "Час переходу назад",
|
||||||
@@ -434,7 +437,7 @@
|
|||||||
"LabelMetadataProvider": "Джерело метаданих",
|
"LabelMetadataProvider": "Джерело метаданих",
|
||||||
"LabelMinute": "Хвилина",
|
"LabelMinute": "Хвилина",
|
||||||
"LabelMinutes": "Хвилини",
|
"LabelMinutes": "Хвилини",
|
||||||
"LabelMissing": "Бракує",
|
"LabelMissing": "Відсутня",
|
||||||
"LabelMissingEbook": "Без електронної книги",
|
"LabelMissingEbook": "Без електронної книги",
|
||||||
"LabelMissingSupplementaryEbook": "Без додаткової електронної книги",
|
"LabelMissingSupplementaryEbook": "Без додаткової електронної книги",
|
||||||
"LabelMobileRedirectURIs": "Дозволені адреси перенаправлення",
|
"LabelMobileRedirectURIs": "Дозволені адреси перенаправлення",
|
||||||
@@ -486,6 +489,7 @@
|
|||||||
"LabelPersonalYearReview": "Ваші підсумки року ({0})",
|
"LabelPersonalYearReview": "Ваші підсумки року ({0})",
|
||||||
"LabelPhotoPathURL": "Шлях/URL фото",
|
"LabelPhotoPathURL": "Шлях/URL фото",
|
||||||
"LabelPlayMethod": "Метод відтворення",
|
"LabelPlayMethod": "Метод відтворення",
|
||||||
|
"LabelPlaybackRateIncrementDecrement": "Величина збільшення/зменшення швидкості відтворення",
|
||||||
"LabelPlayerChapterNumberMarker": "{0} з {1}",
|
"LabelPlayerChapterNumberMarker": "{0} з {1}",
|
||||||
"LabelPlaylists": "Списки відтворення",
|
"LabelPlaylists": "Списки відтворення",
|
||||||
"LabelPodcast": "Подкаст",
|
"LabelPodcast": "Подкаст",
|
||||||
@@ -710,6 +714,7 @@
|
|||||||
"MessageBatchEditPopulateMapDetailsItemHelp": "Заповніть увімкнені поля деталей карти даними з цього елемента",
|
"MessageBatchEditPopulateMapDetailsItemHelp": "Заповніть увімкнені поля деталей карти даними з цього елемента",
|
||||||
"MessageBatchQuickMatchDescription": "Швидкий пошук спробує знайти відсутні обкладинки та метадані обраних елементів. Увімкніть налаштування нижче, аби дозволити заміну наявних обкладинок та/або метаданих під час швидкого пошуку.",
|
"MessageBatchQuickMatchDescription": "Швидкий пошук спробує знайти відсутні обкладинки та метадані обраних елементів. Увімкніть налаштування нижче, аби дозволити заміну наявних обкладинок та/або метаданих під час швидкого пошуку.",
|
||||||
"MessageBookshelfNoCollections": "Ви не створили жодної добірки",
|
"MessageBookshelfNoCollections": "Ви не створили жодної добірки",
|
||||||
|
"MessageBookshelfNoCollectionsHelp": "Колекції публічні. Їх можуть бачити всі користувачі, які мають доступ до бібліотеки.",
|
||||||
"MessageBookshelfNoRSSFeeds": "Немає відкритих RSS-каналів",
|
"MessageBookshelfNoRSSFeeds": "Немає відкритих RSS-каналів",
|
||||||
"MessageBookshelfNoResultsForFilter": "Немає результатів з фільтром \"{0}: {1}\"",
|
"MessageBookshelfNoResultsForFilter": "Немає результатів з фільтром \"{0}: {1}\"",
|
||||||
"MessageBookshelfNoResultsForQuery": "Немає результатів за запитом",
|
"MessageBookshelfNoResultsForQuery": "Немає результатів за запитом",
|
||||||
@@ -820,6 +825,7 @@
|
|||||||
"MessageNoTasksRunning": "Немає активних завдань",
|
"MessageNoTasksRunning": "Немає активних завдань",
|
||||||
"MessageNoUpdatesWereNecessary": "Оновлень не потрібно",
|
"MessageNoUpdatesWereNecessary": "Оновлень не потрібно",
|
||||||
"MessageNoUserPlaylists": "У вас немає списків відтворення",
|
"MessageNoUserPlaylists": "У вас немає списків відтворення",
|
||||||
|
"MessageNoUserPlaylistsHelp": "Списки відтворення приватні. Лише користувач, який їх створює, може бачити їх.",
|
||||||
"MessageNotYetImplemented": "Ще не реалізовано",
|
"MessageNotYetImplemented": "Ще не реалізовано",
|
||||||
"MessageOpmlPreviewNote": "Примітка: це попередній перегляд OPML-файлу. Актуальна назва подкасту буде завантажена з RSS-каналу.",
|
"MessageOpmlPreviewNote": "Примітка: це попередній перегляд OPML-файлу. Актуальна назва подкасту буде завантажена з RSS-каналу.",
|
||||||
"MessageOr": "або",
|
"MessageOr": "або",
|
||||||
@@ -842,6 +848,7 @@
|
|||||||
"MessageRestoreBackupConfirm": "Ви дійсно бажаєте відновити резервну копію від",
|
"MessageRestoreBackupConfirm": "Ви дійсно бажаєте відновити резервну копію від",
|
||||||
"MessageRestoreBackupWarning": "Відновлення резервної копії перезапише всю базу даних, розташовану в /config, і зображення обкладинок в /metadata/items та /metadata/authors.<br /><br />Резервні копії не змінюють жодних файлів у теках бібліотеки. Якщо у налаштуваннях сервера увімкнено збереження обкладинок і метаданих у теках бібліотеки, вони не створюються під час резервного копіювання і не перезаписуються..<br /><br />Всі клієнти, що користуються вашим сервером, будуть автоматично оновлені.",
|
"MessageRestoreBackupWarning": "Відновлення резервної копії перезапише всю базу даних, розташовану в /config, і зображення обкладинок в /metadata/items та /metadata/authors.<br /><br />Резервні копії не змінюють жодних файлів у теках бібліотеки. Якщо у налаштуваннях сервера увімкнено збереження обкладинок і метаданих у теках бібліотеки, вони не створюються під час резервного копіювання і не перезаписуються..<br /><br />Всі клієнти, що користуються вашим сервером, будуть автоматично оновлені.",
|
||||||
"MessageScheduleLibraryScanNote": "Для більшості користувачів рекомендується залишити цю функцію вимкненою та залишити параметр перегляду папок увімкненим. Засіб спостереження за папками автоматично виявить зміни в папках вашої бібліотеки. Засіб спостереження за папками не працює для кожної файлової системи (наприклад, NFS), тому замість нього можна використовувати сканування бібліотек за розкладом.",
|
"MessageScheduleLibraryScanNote": "Для більшості користувачів рекомендується залишити цю функцію вимкненою та залишити параметр перегляду папок увімкненим. Засіб спостереження за папками автоматично виявить зміни в папках вашої бібліотеки. Засіб спостереження за папками не працює для кожної файлової системи (наприклад, NFS), тому замість нього можна використовувати сканування бібліотек за розкладом.",
|
||||||
|
"MessageScheduleRunEveryWeekdayAtTime": "Запуск кожні {0} о {1}",
|
||||||
"MessageSearchResultsFor": "Результати пошуку для",
|
"MessageSearchResultsFor": "Результати пошуку для",
|
||||||
"MessageSelected": "Вибрано: {0}",
|
"MessageSelected": "Вибрано: {0}",
|
||||||
"MessageServerCouldNotBeReached": "Не вдалося підключитися до сервера",
|
"MessageServerCouldNotBeReached": "Не вдалося підключитися до сервера",
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
"ButtonApplyChapters": "应用到章节",
|
"ButtonApplyChapters": "应用到章节",
|
||||||
"ButtonAuthors": "作者",
|
"ButtonAuthors": "作者",
|
||||||
"ButtonBack": "返回",
|
"ButtonBack": "返回",
|
||||||
|
"ButtonBatchEditPopulateFromExisting": "用现有内容填充",
|
||||||
|
"ButtonBatchEditPopulateMapDetails": "填充地图详细信息",
|
||||||
"ButtonBrowseForFolder": "浏览文件夹",
|
"ButtonBrowseForFolder": "浏览文件夹",
|
||||||
"ButtonCancel": "取消",
|
"ButtonCancel": "取消",
|
||||||
"ButtonCancelEncode": "取消编码",
|
"ButtonCancelEncode": "取消编码",
|
||||||
@@ -432,7 +434,7 @@
|
|||||||
"LabelMetadataProvider": "元数据提供商",
|
"LabelMetadataProvider": "元数据提供商",
|
||||||
"LabelMinute": "分钟",
|
"LabelMinute": "分钟",
|
||||||
"LabelMinutes": "分钟",
|
"LabelMinutes": "分钟",
|
||||||
"LabelMissing": "丢失",
|
"LabelMissing": "丢失的",
|
||||||
"LabelMissingEbook": "没有电子书",
|
"LabelMissingEbook": "没有电子书",
|
||||||
"LabelMissingSupplementaryEbook": "没有补充电子书",
|
"LabelMissingSupplementaryEbook": "没有补充电子书",
|
||||||
"LabelMobileRedirectURIs": "允许移动应用重定向 URI",
|
"LabelMobileRedirectURIs": "允许移动应用重定向 URI",
|
||||||
@@ -484,6 +486,7 @@
|
|||||||
"LabelPersonalYearReview": "你的年度回顾 ({0})",
|
"LabelPersonalYearReview": "你的年度回顾 ({0})",
|
||||||
"LabelPhotoPathURL": "图片路径或 URL",
|
"LabelPhotoPathURL": "图片路径或 URL",
|
||||||
"LabelPlayMethod": "播放方法",
|
"LabelPlayMethod": "播放方法",
|
||||||
|
"LabelPlaybackRateIncrementDecrement": "播放速率增加/减少量",
|
||||||
"LabelPlayerChapterNumberMarker": "{0} 于 {1}",
|
"LabelPlayerChapterNumberMarker": "{0} 于 {1}",
|
||||||
"LabelPlaylists": "播放列表",
|
"LabelPlaylists": "播放列表",
|
||||||
"LabelPodcast": "播客",
|
"LabelPodcast": "播客",
|
||||||
@@ -704,8 +707,11 @@
|
|||||||
"MessageBackupsLocationEditNote": "注意: 更新备份位置不会移动或修改现有备份",
|
"MessageBackupsLocationEditNote": "注意: 更新备份位置不会移动或修改现有备份",
|
||||||
"MessageBackupsLocationNoEditNote": "注意: 备份位置是通过环境变量设置的, 不能在此处更改.",
|
"MessageBackupsLocationNoEditNote": "注意: 备份位置是通过环境变量设置的, 不能在此处更改.",
|
||||||
"MessageBackupsLocationPathEmpty": "备份位置路径不能为空",
|
"MessageBackupsLocationPathEmpty": "备份位置路径不能为空",
|
||||||
|
"MessageBatchEditPopulateMapDetailsAllHelp": "使用所有项目的数据填充已启用的字段. 具有多个值的字段将被合并",
|
||||||
|
"MessageBatchEditPopulateMapDetailsItemHelp": "使用此项目的数据填充已启用的地图详细信息字段",
|
||||||
"MessageBatchQuickMatchDescription": "快速匹配将尝试为所选项目添加缺少的封面和元数据. 启用以下选项以允许快速匹配覆盖现有封面和或元数据.",
|
"MessageBatchQuickMatchDescription": "快速匹配将尝试为所选项目添加缺少的封面和元数据. 启用以下选项以允许快速匹配覆盖现有封面和或元数据.",
|
||||||
"MessageBookshelfNoCollections": "你尚未进行任何收藏",
|
"MessageBookshelfNoCollections": "你尚未进行任何收藏",
|
||||||
|
"MessageBookshelfNoCollectionsHelp": "收藏是公开的. 所有有权访问图书馆的用户都可以看到它们.",
|
||||||
"MessageBookshelfNoRSSFeeds": "没有打开的 RSS 源",
|
"MessageBookshelfNoRSSFeeds": "没有打开的 RSS 源",
|
||||||
"MessageBookshelfNoResultsForFilter": "过滤器无结果 \"{0}: {1}\"",
|
"MessageBookshelfNoResultsForFilter": "过滤器无结果 \"{0}: {1}\"",
|
||||||
"MessageBookshelfNoResultsForQuery": "没有可查询的结果",
|
"MessageBookshelfNoResultsForQuery": "没有可查询的结果",
|
||||||
@@ -816,6 +822,7 @@
|
|||||||
"MessageNoTasksRunning": "没有正在运行的任务",
|
"MessageNoTasksRunning": "没有正在运行的任务",
|
||||||
"MessageNoUpdatesWereNecessary": "无需更新",
|
"MessageNoUpdatesWereNecessary": "无需更新",
|
||||||
"MessageNoUserPlaylists": "你没有播放列表",
|
"MessageNoUserPlaylists": "你没有播放列表",
|
||||||
|
"MessageNoUserPlaylistsHelp": "播放列表是私密的. 只有创建播放列表的用户才能看到.",
|
||||||
"MessageNotYetImplemented": "尚未实施",
|
"MessageNotYetImplemented": "尚未实施",
|
||||||
"MessageOpmlPreviewNote": "注意: 这是解析的OPML文件的预览. 实际的播客标题将从 RSS 提要中获取.",
|
"MessageOpmlPreviewNote": "注意: 这是解析的OPML文件的预览. 实际的播客标题将从 RSS 提要中获取.",
|
||||||
"MessageOr": "或",
|
"MessageOr": "或",
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ if (isDev) {
|
|||||||
if (devEnv.AllowIframe) process.env.ALLOW_IFRAME = '1'
|
if (devEnv.AllowIframe) process.env.ALLOW_IFRAME = '1'
|
||||||
if (devEnv.BackupPath) process.env.BACKUP_PATH = devEnv.BackupPath
|
if (devEnv.BackupPath) process.env.BACKUP_PATH = devEnv.BackupPath
|
||||||
process.env.SOURCE = 'local'
|
process.env.SOURCE = 'local'
|
||||||
process.env.ROUTER_BASE_PATH = devEnv.RouterBasePath || ''
|
process.env.ROUTER_BASE_PATH = devEnv.RouterBasePath ?? '/audiobookshelf'
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputConfig = options.config ? Path.resolve(options.config) : null
|
const inputConfig = options.config ? Path.resolve(options.config) : null
|
||||||
@@ -41,7 +41,7 @@ const CONFIG_PATH = inputConfig || process.env.CONFIG_PATH || Path.resolve('conf
|
|||||||
const METADATA_PATH = inputMetadata || process.env.METADATA_PATH || Path.resolve('metadata')
|
const METADATA_PATH = inputMetadata || process.env.METADATA_PATH || Path.resolve('metadata')
|
||||||
const SOURCE = options.source || process.env.SOURCE || 'debian'
|
const SOURCE = options.source || process.env.SOURCE || 'debian'
|
||||||
|
|
||||||
const ROUTER_BASE_PATH = process.env.ROUTER_BASE_PATH || '/audiobookshelf'
|
const ROUTER_BASE_PATH = process.env.ROUTER_BASE_PATH ?? '/audiobookshelf'
|
||||||
|
|
||||||
console.log(`Running in ${process.env.NODE_ENV} mode.`)
|
console.log(`Running in ${process.env.NODE_ENV} mode.`)
|
||||||
console.log(`Options: CONFIG_PATH=${CONFIG_PATH}, METADATA_PATH=${METADATA_PATH}, PORT=${PORT}, HOST=${HOST}, SOURCE=${SOURCE}, ROUTER_BASE_PATH=${ROUTER_BASE_PATH}`)
|
console.log(`Options: CONFIG_PATH=${CONFIG_PATH}, METADATA_PATH=${METADATA_PATH}, PORT=${PORT}, HOST=${HOST}, SOURCE=${SOURCE}, ROUTER_BASE_PATH=${ROUTER_BASE_PATH}`)
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.18.1",
|
"version": "2.19.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.18.1",
|
"version": "2.19.5",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.18.1",
|
"version": "2.19.5",
|
||||||
"buildNumber": 1,
|
"buildNumber": 1,
|
||||||
"description": "Self-hosted audiobook and podcast server",
|
"description": "Self-hosted audiobook and podcast server",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const CONFIG_PATH = inputConfig || process.env.CONFIG_PATH || Path.resolve('conf
|
|||||||
const METADATA_PATH = inputMetadata || process.env.METADATA_PATH || Path.resolve('metadata')
|
const METADATA_PATH = inputMetadata || process.env.METADATA_PATH || Path.resolve('metadata')
|
||||||
const SOURCE = options.source || process.env.SOURCE || 'debian'
|
const SOURCE = options.source || process.env.SOURCE || 'debian'
|
||||||
|
|
||||||
const ROUTER_BASE_PATH = process.env.ROUTER_BASE_PATH || '/audiobookshelf'
|
const ROUTER_BASE_PATH = process.env.ROUTER_BASE_PATH ?? '/audiobookshelf'
|
||||||
|
|
||||||
console.log(process.env.NODE_ENV, 'Config', CONFIG_PATH, METADATA_PATH)
|
console.log(process.env.NODE_ENV, 'Config', CONFIG_PATH, METADATA_PATH)
|
||||||
|
|
||||||
|
|||||||
+7
-2
@@ -10,6 +10,7 @@ const ExtractJwt = require('passport-jwt').ExtractJwt
|
|||||||
const OpenIDClient = require('openid-client')
|
const OpenIDClient = require('openid-client')
|
||||||
const Database = require('./Database')
|
const Database = require('./Database')
|
||||||
const Logger = require('./Logger')
|
const Logger = require('./Logger')
|
||||||
|
const { escapeRegExp } = require('./utils')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @class Class for handling all the authentication related functionality.
|
* @class Class for handling all the authentication related functionality.
|
||||||
@@ -18,7 +19,11 @@ class Auth {
|
|||||||
constructor() {
|
constructor() {
|
||||||
// Map of openId sessions indexed by oauth2 state-variable
|
// Map of openId sessions indexed by oauth2 state-variable
|
||||||
this.openIdAuthSession = new Map()
|
this.openIdAuthSession = new Map()
|
||||||
this.ignorePatterns = [/\/api\/items\/[^/]+\/cover/, /\/api\/authors\/[^/]+\/image/]
|
const escapedRouterBasePath = escapeRegExp(global.RouterBasePath)
|
||||||
|
this.ignorePatterns = [
|
||||||
|
new RegExp(`^(${escapedRouterBasePath}/api)?/items/[^/]+/cover$`),
|
||||||
|
new RegExp(`^(${escapedRouterBasePath}/api)?/authors/[^/]+/image$`)
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,7 +33,7 @@ class Auth {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
authNotNeeded(req) {
|
authNotNeeded(req) {
|
||||||
return req.method === 'GET' && this.ignorePatterns.some((pattern) => pattern.test(req.originalUrl))
|
return req.method === 'GET' && this.ignorePatterns.some((pattern) => pattern.test(req.path))
|
||||||
}
|
}
|
||||||
|
|
||||||
ifAuthNeeded(middleware) {
|
ifAuthNeeded(middleware) {
|
||||||
|
|||||||
@@ -190,7 +190,13 @@ class Database {
|
|||||||
await this.buildModels(force)
|
await this.buildModels(force)
|
||||||
Logger.info(`[Database] Db initialized with models:`, Object.keys(this.sequelize.models).join(', '))
|
Logger.info(`[Database] Db initialized with models:`, Object.keys(this.sequelize.models).join(', '))
|
||||||
|
|
||||||
|
await this.addTriggers()
|
||||||
|
|
||||||
await this.loadData()
|
await this.loadData()
|
||||||
|
|
||||||
|
Logger.info(`[Database] running ANALYZE`)
|
||||||
|
await this.sequelize.query('ANALYZE')
|
||||||
|
Logger.info(`[Database] ANALYZE completed`)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -767,6 +773,43 @@ class Database {
|
|||||||
return textQuery
|
return textQuery
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is used to create necessary triggers for new databases.
|
||||||
|
* It adds triggers to update libraryItems.title[IgnorePrefix] when (books|podcasts).title[IgnorePrefix] is updated
|
||||||
|
*/
|
||||||
|
async addTriggers() {
|
||||||
|
await this.addTriggerIfNotExists('books', 'title', 'id', 'libraryItems', 'title', 'mediaId')
|
||||||
|
await this.addTriggerIfNotExists('books', 'titleIgnorePrefix', 'id', 'libraryItems', 'titleIgnorePrefix', 'mediaId')
|
||||||
|
await this.addTriggerIfNotExists('podcasts', 'title', 'id', 'libraryItems', 'title', 'mediaId')
|
||||||
|
await this.addTriggerIfNotExists('podcasts', 'titleIgnorePrefix', 'id', 'libraryItems', 'titleIgnorePrefix', 'mediaId')
|
||||||
|
}
|
||||||
|
|
||||||
|
async addTriggerIfNotExists(sourceTable, sourceColumn, sourceIdColumn, targetTable, targetColumn, targetIdColumn) {
|
||||||
|
const action = `update_${targetTable}_${targetColumn}`
|
||||||
|
const fromSource = sourceTable === 'books' ? '' : `_from_${sourceTable}_${sourceColumn}`
|
||||||
|
const triggerName = this.convertToSnakeCase(`${action}${fromSource}`)
|
||||||
|
|
||||||
|
const [[{ count }]] = await this.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='${triggerName}'`)
|
||||||
|
if (count > 0) return // Trigger already exists
|
||||||
|
|
||||||
|
Logger.info(`[Database] Adding trigger ${triggerName}`)
|
||||||
|
|
||||||
|
await this.sequelize.query(`
|
||||||
|
CREATE TRIGGER ${triggerName}
|
||||||
|
AFTER UPDATE OF ${sourceColumn} ON ${sourceTable}
|
||||||
|
FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
UPDATE ${targetTable}
|
||||||
|
SET ${targetColumn} = NEW.${sourceColumn}
|
||||||
|
WHERE ${targetTable}.${targetIdColumn} = NEW.${sourceIdColumn};
|
||||||
|
END;
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
convertToSnakeCase(str) {
|
||||||
|
return str.replace(/([A-Z])/g, '_$1').toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
TextSearchQuery = class {
|
TextSearchQuery = class {
|
||||||
constructor(sequelize, supportsUnaccent, query) {
|
constructor(sequelize, supportsUnaccent, query) {
|
||||||
this.sequelize = sequelize
|
this.sequelize = sequelize
|
||||||
|
|||||||
+5
-10
@@ -5,7 +5,7 @@ const Logger = require('./Logger')
|
|||||||
const Task = require('./objects/Task')
|
const Task = require('./objects/Task')
|
||||||
const TaskManager = require('./managers/TaskManager')
|
const TaskManager = require('./managers/TaskManager')
|
||||||
|
|
||||||
const { filePathToPOSIX, isSameOrSubPath, getFileMTimeMs } = require('./utils/fileUtils')
|
const { filePathToPOSIX, isSameOrSubPath, getFileMTimeMs, shouldIgnoreFile } = require('./utils/fileUtils')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef PendingFileUpdate
|
* @typedef PendingFileUpdate
|
||||||
@@ -286,15 +286,10 @@ class FolderWatcher extends EventEmitter {
|
|||||||
|
|
||||||
const relPath = path.replace(folderPath, '')
|
const relPath = path.replace(folderPath, '')
|
||||||
|
|
||||||
if (Path.extname(relPath).toLowerCase() === '.part') {
|
// Check for ignored extensions or directories, such as dotfiles and hidden directories
|
||||||
Logger.debug(`[Watcher] Ignoring .part file "${relPath}"`)
|
const shouldIgnore = shouldIgnoreFile(relPath)
|
||||||
return false
|
if (shouldIgnore) {
|
||||||
}
|
Logger.debug(`[Watcher] Ignoring ${shouldIgnore} - "${relPath}"`)
|
||||||
|
|
||||||
// Ignore files/folders starting with "."
|
|
||||||
const hasDotPath = relPath.split('/').find((p) => p.startsWith('.'))
|
|
||||||
if (hasDotPath) {
|
|
||||||
Logger.debug(`[Watcher] Ignoring dot path "${relPath}" | Piece "${hasDotPath}"`)
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -251,6 +251,7 @@ class CollectionController {
|
|||||||
/**
|
/**
|
||||||
* DELETE: /api/collections/:id/book/:bookId
|
* DELETE: /api/collections/:id/book/:bookId
|
||||||
* Remove a single book from a collection. Re-order books
|
* Remove a single book from a collection. Re-order books
|
||||||
|
* Users with update permission can remove books from collections
|
||||||
* TODO: bookId is actually libraryItemId. Clients need updating to use bookId
|
* TODO: bookId is actually libraryItemId. Clients need updating to use bookId
|
||||||
*
|
*
|
||||||
* @param {CollectionControllerRequest} req
|
* @param {CollectionControllerRequest} req
|
||||||
@@ -427,7 +428,8 @@ class CollectionController {
|
|||||||
req.collection = collection
|
req.collection = collection
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.method == 'DELETE' && !req.user.canDelete) {
|
// Users with update permission can remove books from collections
|
||||||
|
if (req.method == 'DELETE' && !req.params.bookId && !req.user.canDelete) {
|
||||||
Logger.warn(`[CollectionController] User "${req.user.username}" attempted to delete without permission`)
|
Logger.warn(`[CollectionController] User "${req.user.username}" attempted to delete without permission`)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
} else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) {
|
} else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) {
|
||||||
|
|||||||
@@ -254,6 +254,11 @@ class LibraryController {
|
|||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async update(req, res) {
|
async update(req, res) {
|
||||||
|
if (!req.user.isAdminOrUp) {
|
||||||
|
Logger.error(`[LibraryController] Non-admin user "${req.user.username}" attempted to update library`)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
// Validation
|
// Validation
|
||||||
const updatePayload = {}
|
const updatePayload = {}
|
||||||
const keysToCheck = ['name', 'provider', 'mediaType', 'icon']
|
const keysToCheck = ['name', 'provider', 'mediaType', 'icon']
|
||||||
@@ -519,6 +524,11 @@ class LibraryController {
|
|||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async delete(req, res) {
|
async delete(req, res) {
|
||||||
|
if (!req.user.isAdminOrUp) {
|
||||||
|
Logger.error(`[LibraryController] Non-admin user "${req.user.username}" attempted to delete library`)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
// Remove library watcher
|
// Remove library watcher
|
||||||
Watcher.removeLibrary(req.library)
|
Watcher.removeLibrary(req.library)
|
||||||
|
|
||||||
@@ -639,6 +649,11 @@ class LibraryController {
|
|||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async removeLibraryItemsWithIssues(req, res) {
|
async removeLibraryItemsWithIssues(req, res) {
|
||||||
|
if (!req.user.isAdminOrUp) {
|
||||||
|
Logger.error(`[LibraryController] Non-admin user "${req.user.username}" attempted to delete library items missing or invalid`)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
const libraryItemsWithIssues = await Database.libraryItemModel.findAll({
|
const libraryItemsWithIssues = await Database.libraryItemModel.findAll({
|
||||||
where: {
|
where: {
|
||||||
libraryId: req.library.id,
|
libraryId: req.library.id,
|
||||||
|
|||||||
@@ -107,7 +107,9 @@ class PodcastController {
|
|||||||
libraryFiles: [],
|
libraryFiles: [],
|
||||||
extraData: {},
|
extraData: {},
|
||||||
libraryId: library.id,
|
libraryId: library.id,
|
||||||
libraryFolderId: folder.id
|
libraryFolderId: folder.id,
|
||||||
|
title: podcast.title,
|
||||||
|
titleIgnorePrefix: podcast.titleIgnorePrefix
|
||||||
},
|
},
|
||||||
{ transaction }
|
{ transaction }
|
||||||
)
|
)
|
||||||
@@ -498,6 +500,10 @@ class PodcastController {
|
|||||||
req.libraryItem.changed('libraryFiles', true)
|
req.libraryItem.changed('libraryFiles', true)
|
||||||
await req.libraryItem.save()
|
await req.libraryItem.save()
|
||||||
|
|
||||||
|
// update number of episodes
|
||||||
|
req.libraryItem.media.numEpisodes = req.libraryItem.media.podcastEpisodes.length
|
||||||
|
await req.libraryItem.media.save()
|
||||||
|
|
||||||
SocketAuthority.emitter('item_updated', req.libraryItem.toOldJSONExpanded())
|
SocketAuthority.emitter('item_updated', req.libraryItem.toOldJSONExpanded())
|
||||||
res.json(req.libraryItem.toOldJSON())
|
res.json(req.libraryItem.toOldJSON())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -232,6 +232,11 @@ class PodcastManager {
|
|||||||
|
|
||||||
await libraryItem.save()
|
await libraryItem.save()
|
||||||
|
|
||||||
|
if (libraryItem.media.numEpisodes !== libraryItem.media.podcastEpisodes.length) {
|
||||||
|
libraryItem.media.numEpisodes = libraryItem.media.podcastEpisodes.length
|
||||||
|
await libraryItem.media.save()
|
||||||
|
}
|
||||||
|
|
||||||
SocketAuthority.emitter('item_updated', libraryItem.toOldJSONExpanded())
|
SocketAuthority.emitter('item_updated', libraryItem.toOldJSONExpanded())
|
||||||
const podcastEpisodeExpanded = podcastEpisode.toOldJSONExpanded(libraryItem.id)
|
const podcastEpisodeExpanded = podcastEpisode.toOldJSONExpanded(libraryItem.id)
|
||||||
podcastEpisodeExpanded.libraryItem = libraryItem.toOldJSONExpanded()
|
podcastEpisodeExpanded.libraryItem = libraryItem.toOldJSONExpanded()
|
||||||
@@ -622,7 +627,9 @@ class PodcastManager {
|
|||||||
libraryFiles: [],
|
libraryFiles: [],
|
||||||
extraData: {},
|
extraData: {},
|
||||||
libraryId: folder.libraryId,
|
libraryId: folder.libraryId,
|
||||||
libraryFolderId: folder.id
|
libraryFolderId: folder.id,
|
||||||
|
title: podcast.title,
|
||||||
|
titleIgnorePrefix: podcast.titleIgnorePrefix
|
||||||
},
|
},
|
||||||
{ transaction }
|
{ transaction }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -246,6 +246,15 @@ class RssFeedManager {
|
|||||||
const extname = Path.extname(feed.coverPath).toLowerCase().slice(1)
|
const extname = Path.extname(feed.coverPath).toLowerCase().slice(1)
|
||||||
res.type(`image/${extname}`)
|
res.type(`image/${extname}`)
|
||||||
const readStream = fs.createReadStream(feed.coverPath)
|
const readStream = fs.createReadStream(feed.coverPath)
|
||||||
|
|
||||||
|
readStream.on('error', (error) => {
|
||||||
|
Logger.error(`[RssFeedManager] Error streaming cover image: ${error.message}`)
|
||||||
|
// Only send error if headers haven't been sent yet
|
||||||
|
if (!res.headersSent) {
|
||||||
|
res.sendStatus(404)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
readStream.pipe(res)
|
readStream.pipe(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,3 +13,5 @@ Please add a record of every database migration that you create to this file. Th
|
|||||||
| v2.17.5 | v2.17.5-remove-host-from-feed-urls | removes the host (serverAddress) from URL columns in the feeds and feedEpisodes tables |
|
| v2.17.5 | v2.17.5-remove-host-from-feed-urls | removes the host (serverAddress) from URL columns in the feeds and feedEpisodes tables |
|
||||||
| v2.17.6 | v2.17.6-share-add-isdownloadable | Adds the isDownloadable column to the mediaItemShares table |
|
| v2.17.6 | v2.17.6-share-add-isdownloadable | Adds the isDownloadable column to the mediaItemShares table |
|
||||||
| v2.17.7 | v2.17.7-add-indices | Adds indices to the libraryItems and books tables to reduce query times |
|
| v2.17.7 | v2.17.7-add-indices | Adds indices to the libraryItems and books tables to reduce query times |
|
||||||
|
| v2.19.1 | v2.19.1-copy-title-to-library-items | Copies title and titleIgnorePrefix to the libraryItems table, creates update triggers and indices |
|
||||||
|
| v2.19.4 | v2.19.4-improve-podcast-queries | Adds numEpisodes to podcasts, adds podcastId to mediaProgresses, copies podcast title to libraryItems |
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
const util = require('util')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef MigrationContext
|
||||||
|
* @property {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
|
||||||
|
* @property {import('../Logger')} logger - a Logger object.
|
||||||
|
*
|
||||||
|
* @typedef MigrationOptions
|
||||||
|
* @property {MigrationContext} context - an object containing the migration context.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const migrationVersion = '2.19.1'
|
||||||
|
const migrationName = `${migrationVersion}-copy-title-to-library-items`
|
||||||
|
const loggerPrefix = `[${migrationVersion} migration]`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This upward migration adds a title column to the libraryItems table, copies the title from the book to the libraryItem,
|
||||||
|
* and creates a new index on the title column. In addition it sets a trigger on the books table to update the title column
|
||||||
|
* in the libraryItems table when a book is updated.
|
||||||
|
*
|
||||||
|
* @param {MigrationOptions} options - an object containing the migration context.
|
||||||
|
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||||
|
*/
|
||||||
|
async function up({ context: { queryInterface, logger } }) {
|
||||||
|
// Upwards migration script
|
||||||
|
logger.info(`${loggerPrefix} UPGRADE BEGIN: ${migrationName}`)
|
||||||
|
|
||||||
|
await addColumn(queryInterface, logger, 'libraryItems', 'title', { type: queryInterface.sequelize.Sequelize.STRING, allowNull: true })
|
||||||
|
await copyColumn(queryInterface, logger, 'books', 'title', 'id', 'libraryItems', 'title', 'mediaId')
|
||||||
|
await addTrigger(queryInterface, logger, 'books', 'title', 'id', 'libraryItems', 'title', 'mediaId')
|
||||||
|
await addIndex(queryInterface, logger, 'libraryItems', ['libraryId', 'mediaType', { name: 'title', collate: 'NOCASE' }])
|
||||||
|
|
||||||
|
await addColumn(queryInterface, logger, 'libraryItems', 'titleIgnorePrefix', { type: queryInterface.sequelize.Sequelize.STRING, allowNull: true })
|
||||||
|
await copyColumn(queryInterface, logger, 'books', 'titleIgnorePrefix', 'id', 'libraryItems', 'titleIgnorePrefix', 'mediaId')
|
||||||
|
await addTrigger(queryInterface, logger, 'books', 'titleIgnorePrefix', 'id', 'libraryItems', 'titleIgnorePrefix', 'mediaId')
|
||||||
|
await addIndex(queryInterface, logger, 'libraryItems', ['libraryId', 'mediaType', { name: 'titleIgnorePrefix', collate: 'NOCASE' }])
|
||||||
|
|
||||||
|
await addIndex(queryInterface, logger, 'libraryItems', ['libraryId', 'mediaType', 'createdAt'])
|
||||||
|
|
||||||
|
logger.info(`${loggerPrefix} UPGRADE END: ${migrationName}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This downward migration script removes the title column from the libraryItems table, removes the trigger on the books table,
|
||||||
|
* and removes the index on the title column.
|
||||||
|
*
|
||||||
|
* @param {MigrationOptions} options - an object containing the migration context.
|
||||||
|
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||||
|
*/
|
||||||
|
async function down({ context: { queryInterface, logger } }) {
|
||||||
|
// Downward migration script
|
||||||
|
logger.info(`${loggerPrefix} DOWNGRADE BEGIN: ${migrationName}`)
|
||||||
|
|
||||||
|
await removeIndex(queryInterface, logger, 'libraryItems', ['libraryId', 'mediaType', 'title'])
|
||||||
|
await removeTrigger(queryInterface, logger, 'libraryItems', 'title')
|
||||||
|
await removeColumn(queryInterface, logger, 'libraryItems', 'title')
|
||||||
|
|
||||||
|
await removeIndex(queryInterface, logger, 'libraryItems', ['libraryId', 'mediaType', 'titleIgnorePrefix'])
|
||||||
|
await removeTrigger(queryInterface, logger, 'libraryItems', 'titleIgnorePrefix')
|
||||||
|
await removeColumn(queryInterface, logger, 'libraryItems', 'titleIgnorePrefix')
|
||||||
|
|
||||||
|
await removeIndex(queryInterface, logger, 'libraryItems', ['libraryId', 'mediaType', 'createdAt'])
|
||||||
|
|
||||||
|
logger.info(`${loggerPrefix} DOWNGRADE END: ${migrationName}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to add an index to a table. If the index already z`exists, it logs a message and continues.
|
||||||
|
*
|
||||||
|
* @param {import('sequelize').QueryInterface} queryInterface
|
||||||
|
* @param {import ('../Logger')} logger
|
||||||
|
* @param {string} tableName
|
||||||
|
* @param {string[]} columns
|
||||||
|
*/
|
||||||
|
async function addIndex(queryInterface, logger, tableName, columns) {
|
||||||
|
const columnString = columns.map((column) => util.inspect(column)).join(', ')
|
||||||
|
const indexName = convertToSnakeCase(`${tableName}_${columns.map((column) => (typeof column === 'string' ? column : column.name)).join('_')}`)
|
||||||
|
try {
|
||||||
|
logger.info(`${loggerPrefix} adding index on [${columnString}] to table ${tableName}. index name: ${indexName}"`)
|
||||||
|
await queryInterface.addIndex(tableName, columns)
|
||||||
|
logger.info(`${loggerPrefix} added index on [${columnString}] to table ${tableName}. index name: ${indexName}"`)
|
||||||
|
} catch (error) {
|
||||||
|
if (error.name === 'SequelizeDatabaseError' && error.message.includes('already exists')) {
|
||||||
|
logger.info(`${loggerPrefix} index [${columnString}] for table "${tableName}" already exists`)
|
||||||
|
} else {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to remove an index from a table.
|
||||||
|
* Sequelize implemets it using DROP INDEX IF EXISTS, so it won't throw an error if the index doesn't exist.
|
||||||
|
*
|
||||||
|
* @param {import('sequelize').QueryInterface} queryInterface
|
||||||
|
* @param {import ('../Logger')} logger
|
||||||
|
* @param {string} tableName
|
||||||
|
* @param {string[]} columns
|
||||||
|
*/
|
||||||
|
async function removeIndex(queryInterface, logger, tableName, columns) {
|
||||||
|
logger.info(`${loggerPrefix} removing index [${columns.join(', ')}] from table "${tableName}"`)
|
||||||
|
await queryInterface.removeIndex(tableName, columns)
|
||||||
|
logger.info(`${loggerPrefix} removed index [${columns.join(', ')}] from table "${tableName}"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addColumn(queryInterface, logger, table, column, options) {
|
||||||
|
logger.info(`${loggerPrefix} adding column "${column}" to table "${table}"`)
|
||||||
|
const tableDescription = await queryInterface.describeTable(table)
|
||||||
|
if (!tableDescription[column]) {
|
||||||
|
await queryInterface.addColumn(table, column, options)
|
||||||
|
logger.info(`${loggerPrefix} added column "${column}" to table "${table}"`)
|
||||||
|
} else {
|
||||||
|
logger.info(`${loggerPrefix} column "${column}" already exists in table "${table}"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeColumn(queryInterface, logger, table, column) {
|
||||||
|
logger.info(`${loggerPrefix} removing column "${column}" from table "${table}"`)
|
||||||
|
await queryInterface.removeColumn(table, column)
|
||||||
|
logger.info(`${loggerPrefix} removed column "${column}" from table "${table}"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyColumn(queryInterface, logger, sourceTable, sourceColumn, sourceIdColumn, targetTable, targetColumn, targetIdColumn) {
|
||||||
|
logger.info(`${loggerPrefix} copying column "${sourceColumn}" from table "${sourceTable}" to table "${targetTable}"`)
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
UPDATE ${targetTable}
|
||||||
|
SET ${targetColumn} = ${sourceTable}.${sourceColumn}
|
||||||
|
FROM ${sourceTable}
|
||||||
|
WHERE ${targetTable}.${targetIdColumn} = ${sourceTable}.${sourceIdColumn}
|
||||||
|
`)
|
||||||
|
logger.info(`${loggerPrefix} copied column "${sourceColumn}" from table "${sourceTable}" to table "${targetTable}"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addTrigger(queryInterface, logger, sourceTable, sourceColumn, sourceIdColumn, targetTable, targetColumn, targetIdColumn) {
|
||||||
|
logger.info(`${loggerPrefix} adding trigger to update ${targetTable}.${targetColumn} when ${sourceTable}.${sourceColumn} is updated`)
|
||||||
|
const triggerName = convertToSnakeCase(`update_${targetTable}_${targetColumn}`)
|
||||||
|
|
||||||
|
await queryInterface.sequelize.query(`DROP TRIGGER IF EXISTS ${triggerName}`)
|
||||||
|
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
CREATE TRIGGER ${triggerName}
|
||||||
|
AFTER UPDATE OF ${sourceColumn} ON ${sourceTable}
|
||||||
|
FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
UPDATE ${targetTable}
|
||||||
|
SET ${targetColumn} = NEW.${sourceColumn}
|
||||||
|
WHERE ${targetTable}.${targetIdColumn} = NEW.${sourceIdColumn};
|
||||||
|
END;
|
||||||
|
`)
|
||||||
|
logger.info(`${loggerPrefix} added trigger to update ${targetTable}.${targetColumn} when ${sourceTable}.${sourceColumn} is updated`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeTrigger(queryInterface, logger, targetTable, targetColumn) {
|
||||||
|
logger.info(`${loggerPrefix} removing trigger to update ${targetTable}.${targetColumn}`)
|
||||||
|
const triggerName = convertToSnakeCase(`update_${targetTable}_${targetColumn}`)
|
||||||
|
await queryInterface.sequelize.query(`DROP TRIGGER IF EXISTS ${triggerName}`)
|
||||||
|
logger.info(`${loggerPrefix} removed trigger to update ${targetTable}.${targetColumn}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertToSnakeCase(str) {
|
||||||
|
return str.replace(/([A-Z])/g, '_$1').toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { up, down }
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
const util = require('util')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef MigrationContext
|
||||||
|
* @property {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
|
||||||
|
* @property {import('../Logger')} logger - a Logger object.
|
||||||
|
*
|
||||||
|
* @typedef MigrationOptions
|
||||||
|
* @property {MigrationContext} context - an object containing the migration context.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const migrationVersion = '2.19.4'
|
||||||
|
const migrationName = `${migrationVersion}-improve-podcast-queries`
|
||||||
|
const loggerPrefix = `[${migrationVersion} migration]`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This upward migration adds a numEpisodes column to the podcasts table and populates it.
|
||||||
|
* It also adds a podcastId column to the mediaProgresses table and populates it.
|
||||||
|
* It also copies the title and titleIgnorePrefix columns from the podcasts table to the libraryItems table,
|
||||||
|
* and adds triggers to update them when the corresponding columns in the podcasts table are updated.
|
||||||
|
*
|
||||||
|
* @param {MigrationOptions} options - an object containing the migration context.
|
||||||
|
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||||
|
*/
|
||||||
|
async function up({ context: { queryInterface, logger } }) {
|
||||||
|
// Upwards migration script
|
||||||
|
logger.info(`${loggerPrefix} UPGRADE BEGIN: ${migrationName}`)
|
||||||
|
|
||||||
|
// Add numEpisodes column to podcasts table
|
||||||
|
await addColumn(queryInterface, logger, 'podcasts', 'numEpisodes', { type: queryInterface.sequelize.Sequelize.INTEGER, allowNull: false, defaultValue: 0 })
|
||||||
|
|
||||||
|
// Populate numEpisodes column with the number of episodes for each podcast
|
||||||
|
await populateNumEpisodes(queryInterface, logger)
|
||||||
|
|
||||||
|
// Add podcastId column to mediaProgresses table
|
||||||
|
await addColumn(queryInterface, logger, 'mediaProgresses', 'podcastId', { type: queryInterface.sequelize.Sequelize.UUID, allowNull: true })
|
||||||
|
|
||||||
|
// Populate podcastId column with the podcastId for each mediaProgress
|
||||||
|
await populatePodcastId(queryInterface, logger)
|
||||||
|
|
||||||
|
// Copy title and titleIgnorePrefix columns from podcasts to libraryItems
|
||||||
|
await copyColumn(queryInterface, logger, 'podcasts', 'title', 'id', 'libraryItems', 'title', 'mediaId')
|
||||||
|
await copyColumn(queryInterface, logger, 'podcasts', 'titleIgnorePrefix', 'id', 'libraryItems', 'titleIgnorePrefix', 'mediaId')
|
||||||
|
|
||||||
|
// Add triggers to update title and titleIgnorePrefix in libraryItems
|
||||||
|
await addTrigger(queryInterface, logger, 'podcasts', 'title', 'id', 'libraryItems', 'title', 'mediaId')
|
||||||
|
await addTrigger(queryInterface, logger, 'podcasts', 'titleIgnorePrefix', 'id', 'libraryItems', 'titleIgnorePrefix', 'mediaId')
|
||||||
|
|
||||||
|
logger.info(`${loggerPrefix} UPGRADE END: ${migrationName}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This downward migration removes the triggers on the podcasts table,
|
||||||
|
* the numEpisodes column from the podcasts table, and the podcastId column from the mediaProgresses table.
|
||||||
|
*
|
||||||
|
* @param {MigrationOptions} options - an object containing the migration context.
|
||||||
|
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||||
|
*/
|
||||||
|
async function down({ context: { queryInterface, logger } }) {
|
||||||
|
// Downward migration script
|
||||||
|
logger.info(`${loggerPrefix} DOWNGRADE BEGIN: ${migrationName}`)
|
||||||
|
|
||||||
|
// Remove triggers from libraryItems
|
||||||
|
await removeTrigger(queryInterface, logger, 'podcasts', 'title', 'libraryItems', 'title')
|
||||||
|
await removeTrigger(queryInterface, logger, 'podcasts', 'titleIgnorePrefix', 'libraryItems', 'titleIgnorePrefix')
|
||||||
|
|
||||||
|
// Remove numEpisodes column from podcasts table
|
||||||
|
await removeColumn(queryInterface, logger, 'podcasts', 'numEpisodes')
|
||||||
|
|
||||||
|
// Remove podcastId column from mediaProgresses table
|
||||||
|
await removeColumn(queryInterface, logger, 'mediaProgresses', 'podcastId')
|
||||||
|
|
||||||
|
logger.info(`${loggerPrefix} DOWNGRADE END: ${migrationName}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function populateNumEpisodes(queryInterface, logger) {
|
||||||
|
logger.info(`${loggerPrefix} populating numEpisodes column in podcasts table`)
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
UPDATE podcasts
|
||||||
|
SET numEpisodes = (SELECT COUNT(*) FROM podcastEpisodes WHERE podcastEpisodes.podcastId = podcasts.id)
|
||||||
|
`)
|
||||||
|
logger.info(`${loggerPrefix} populated numEpisodes column in podcasts table`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function populatePodcastId(queryInterface, logger) {
|
||||||
|
logger.info(`${loggerPrefix} populating podcastId column in mediaProgresses table`)
|
||||||
|
// bulk update podcastId to the podcastId of the podcastEpisode if the mediaItemType is podcastEpisode
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
UPDATE mediaProgresses
|
||||||
|
SET podcastId = (SELECT podcastId FROM podcastEpisodes WHERE podcastEpisodes.id = mediaProgresses.mediaItemId)
|
||||||
|
WHERE mediaItemType = 'podcastEpisode'
|
||||||
|
`)
|
||||||
|
logger.info(`${loggerPrefix} populated podcastId column in mediaProgresses table`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to add a column to a table. If the column already exists, it logs a message and continues.
|
||||||
|
*
|
||||||
|
* @param {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
|
||||||
|
* @param {import('../Logger')} logger - a Logger object.
|
||||||
|
* @param {string} table - the name of the table to add the column to.
|
||||||
|
* @param {string} column - the name of the column to add.
|
||||||
|
* @param {Object} options - the options for the column.
|
||||||
|
*/
|
||||||
|
async function addColumn(queryInterface, logger, table, column, options) {
|
||||||
|
logger.info(`${loggerPrefix} adding column "${column}" to table "${table}"`)
|
||||||
|
const tableDescription = await queryInterface.describeTable(table)
|
||||||
|
if (!tableDescription[column]) {
|
||||||
|
await queryInterface.addColumn(table, column, options)
|
||||||
|
logger.info(`${loggerPrefix} added column "${column}" to table "${table}"`)
|
||||||
|
} else {
|
||||||
|
logger.info(`${loggerPrefix} column "${column}" already exists in table "${table}"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to remove a column from a table. If the column does not exist, it logs a message and continues.
|
||||||
|
*
|
||||||
|
* @param {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
|
||||||
|
* @param {import('../Logger')} logger - a Logger object.
|
||||||
|
* @param {string} table - the name of the table to remove the column from.
|
||||||
|
* @param {string} column - the name of the column to remove.
|
||||||
|
*/
|
||||||
|
async function removeColumn(queryInterface, logger, table, column) {
|
||||||
|
logger.info(`${loggerPrefix} removing column "${column}" from table "${table}"`)
|
||||||
|
const tableDescription = await queryInterface.describeTable(table)
|
||||||
|
if (tableDescription[column]) {
|
||||||
|
await queryInterface.sequelize.query(`ALTER TABLE ${table} DROP COLUMN ${column}`)
|
||||||
|
logger.info(`${loggerPrefix} removed column "${column}" from table "${table}"`)
|
||||||
|
} else {
|
||||||
|
logger.info(`${loggerPrefix} column "${column}" does not exist in table "${table}"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to add a trigger to update a column in a target table when a column in a source table is updated.
|
||||||
|
* If the trigger already exists, it drops it and creates a new one.
|
||||||
|
* sourceIdColumn and targetIdColumn are used to match the source and target rows.
|
||||||
|
*
|
||||||
|
* @param {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
|
||||||
|
* @param {import('../Logger')} logger - a Logger object.
|
||||||
|
* @param {string} sourceTable - the name of the source table.
|
||||||
|
* @param {string} sourceColumn - the name of the column to update.
|
||||||
|
* @param {string} sourceIdColumn - the name of the id column of the source table.
|
||||||
|
* @param {string} targetTable - the name of the target table.
|
||||||
|
* @param {string} targetColumn - the name of the column to update.
|
||||||
|
* @param {string} targetIdColumn - the name of the id column of the target table.
|
||||||
|
*/
|
||||||
|
async function addTrigger(queryInterface, logger, sourceTable, sourceColumn, sourceIdColumn, targetTable, targetColumn, targetIdColumn) {
|
||||||
|
logger.info(`${loggerPrefix} adding trigger to update ${targetTable}.${targetColumn} when ${sourceTable}.${sourceColumn} is updated`)
|
||||||
|
const triggerName = convertToSnakeCase(`update_${targetTable}_${targetColumn}_from_${sourceTable}_${sourceColumn}`)
|
||||||
|
|
||||||
|
await queryInterface.sequelize.query(`DROP TRIGGER IF EXISTS ${triggerName}`)
|
||||||
|
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
CREATE TRIGGER ${triggerName}
|
||||||
|
AFTER UPDATE OF ${sourceColumn} ON ${sourceTable}
|
||||||
|
FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
UPDATE ${targetTable}
|
||||||
|
SET ${targetColumn} = NEW.${sourceColumn}
|
||||||
|
WHERE ${targetTable}.${targetIdColumn} = NEW.${sourceIdColumn};
|
||||||
|
END;
|
||||||
|
`)
|
||||||
|
logger.info(`${loggerPrefix} added trigger to update ${targetTable}.${targetColumn} when ${sourceTable}.${sourceColumn} is updated`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to remove an update trigger from a table.
|
||||||
|
*
|
||||||
|
* @param {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
|
||||||
|
* @param {import('../Logger')} logger - a Logger object.
|
||||||
|
* @param {string} sourceTable - the name of the source table.
|
||||||
|
* @param {string} sourceColumn - the name of the column to update.
|
||||||
|
* @param {string} targetTable - the name of the target table.
|
||||||
|
* @param {string} targetColumn - the name of the column to update.
|
||||||
|
*/
|
||||||
|
async function removeTrigger(queryInterface, logger, sourceTable, sourceColumn, targetTable, targetColumn) {
|
||||||
|
logger.info(`${loggerPrefix} removing trigger to update ${targetTable}.${targetColumn}`)
|
||||||
|
const triggerName = convertToSnakeCase(`update_${targetTable}_${targetColumn}_from_${sourceTable}_${sourceColumn}`)
|
||||||
|
await queryInterface.sequelize.query(`DROP TRIGGER IF EXISTS ${triggerName}`)
|
||||||
|
logger.info(`${loggerPrefix} removed trigger to update ${targetTable}.${targetColumn}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to copy a column from a source table to a target table.
|
||||||
|
* sourceIdColumn and targetIdColumn are used to match the source and target rows.
|
||||||
|
*
|
||||||
|
* @param {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
|
||||||
|
* @param {import('../Logger')} logger - a Logger object.
|
||||||
|
* @param {string} sourceTable - the name of the source table.
|
||||||
|
* @param {string} sourceColumn - the name of the column to copy.
|
||||||
|
* @param {string} sourceIdColumn - the name of the id column of the source table.
|
||||||
|
* @param {string} targetTable - the name of the target table.
|
||||||
|
* @param {string} targetColumn - the name of the column to copy to.
|
||||||
|
* @param {string} targetIdColumn - the name of the id column of the target table.
|
||||||
|
*/
|
||||||
|
async function copyColumn(queryInterface, logger, sourceTable, sourceColumn, sourceIdColumn, targetTable, targetColumn, targetIdColumn) {
|
||||||
|
logger.info(`${loggerPrefix} copying column "${sourceColumn}" from table "${sourceTable}" to table "${targetTable}"`)
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
UPDATE ${targetTable}
|
||||||
|
SET ${targetColumn} = ${sourceTable}.${sourceColumn}
|
||||||
|
FROM ${sourceTable}
|
||||||
|
WHERE ${targetTable}.${targetIdColumn} = ${sourceTable}.${sourceIdColumn}
|
||||||
|
`)
|
||||||
|
logger.info(`${loggerPrefix} copied column "${sourceColumn}" from table "${sourceTable}" to table "${targetTable}"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to convert a string to snake case, e.g. "titleIgnorePrefix" -> "title_ignore_prefix"
|
||||||
|
*
|
||||||
|
* @param {string} str - the string to convert to snake case.
|
||||||
|
* @returns {string} - the string in snake case.
|
||||||
|
*/
|
||||||
|
function convertToSnakeCase(str) {
|
||||||
|
return str.replace(/([A-Z])/g, '_$1').toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { up, down }
|
||||||
+11
-2
@@ -3,6 +3,7 @@ const Logger = require('../Logger')
|
|||||||
const { getTitlePrefixAtEnd, getTitleIgnorePrefix } = require('../utils')
|
const { getTitlePrefixAtEnd, getTitleIgnorePrefix } = require('../utils')
|
||||||
const parseNameString = require('../utils/parsers/parseNameString')
|
const parseNameString = require('../utils/parsers/parseNameString')
|
||||||
const htmlSanitizer = require('../utils/htmlSanitizer')
|
const htmlSanitizer = require('../utils/htmlSanitizer')
|
||||||
|
const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef EBookFileObject
|
* @typedef EBookFileObject
|
||||||
@@ -192,6 +193,14 @@ class Book extends Model {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Book.addHook('afterDestroy', async (instance) => {
|
||||||
|
libraryItemsBookFilters.clearCountCache('afterDestroy')
|
||||||
|
})
|
||||||
|
|
||||||
|
Book.addHook('afterCreate', async (instance) => {
|
||||||
|
libraryItemsBookFilters.clearCountCache('afterCreate')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -286,7 +295,7 @@ class Book extends Model {
|
|||||||
const track = structuredClone(af)
|
const track = structuredClone(af)
|
||||||
track.title = af.metadata.filename
|
track.title = af.metadata.filename
|
||||||
track.startOffset = startOffset
|
track.startOffset = startOffset
|
||||||
track.contentUrl = `${global.RouterBasePath}/api/items/${libraryItemId}/file/${track.ino}`
|
track.contentUrl = `/api/items/${libraryItemId}/file/${track.ino}`
|
||||||
startOffset += track.duration
|
startOffset += track.duration
|
||||||
return track
|
return track
|
||||||
})
|
})
|
||||||
@@ -365,7 +374,7 @@ class Book extends Model {
|
|||||||
if (payload.metadata) {
|
if (payload.metadata) {
|
||||||
const metadataStringKeys = ['title', 'subtitle', 'publishedYear', 'publishedDate', 'publisher', 'description', 'isbn', 'asin', 'language']
|
const metadataStringKeys = ['title', 'subtitle', 'publishedYear', 'publishedDate', 'publisher', 'description', 'isbn', 'asin', 'language']
|
||||||
metadataStringKeys.forEach((key) => {
|
metadataStringKeys.forEach((key) => {
|
||||||
if (typeof payload.metadata[key] === 'string' && this[key] !== payload.metadata[key]) {
|
if ((typeof payload.metadata[key] === 'string' || payload.metadata[key] === null) && this[key] !== payload.metadata[key]) {
|
||||||
this[key] = payload.metadata[key] || null
|
this[key] = payload.metadata[key] || null
|
||||||
|
|
||||||
if (key === 'title') {
|
if (key === 'title') {
|
||||||
|
|||||||
@@ -73,6 +73,10 @@ class LibraryItem extends Model {
|
|||||||
|
|
||||||
/** @type {Book.BookExpanded|Podcast.PodcastExpanded} - only set when expanded */
|
/** @type {Book.BookExpanded|Podcast.PodcastExpanded} - only set when expanded */
|
||||||
this.media
|
this.media
|
||||||
|
/** @type {string} */
|
||||||
|
this.title // Only used for sorting
|
||||||
|
/** @type {string} */
|
||||||
|
this.titleIgnorePrefix // Only used for sorting
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -99,7 +103,7 @@ class LibraryItem extends Model {
|
|||||||
{
|
{
|
||||||
model: this.sequelize.models.series,
|
model: this.sequelize.models.series,
|
||||||
through: {
|
through: {
|
||||||
attributes: ['sequence', 'createdAt']
|
attributes: ['id', 'sequence', 'createdAt']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -677,7 +681,9 @@ class LibraryItem extends Model {
|
|||||||
lastScan: DataTypes.DATE,
|
lastScan: DataTypes.DATE,
|
||||||
lastScanVersion: DataTypes.STRING,
|
lastScanVersion: DataTypes.STRING,
|
||||||
libraryFiles: DataTypes.JSON,
|
libraryFiles: DataTypes.JSON,
|
||||||
extraData: DataTypes.JSON
|
extraData: DataTypes.JSON,
|
||||||
|
title: DataTypes.STRING,
|
||||||
|
titleIgnorePrefix: DataTypes.STRING
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sequelize,
|
sequelize,
|
||||||
@@ -695,6 +701,15 @@ class LibraryItem extends Model {
|
|||||||
{
|
{
|
||||||
fields: ['libraryId', 'mediaType', 'size']
|
fields: ['libraryId', 'mediaType', 'size']
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
fields: ['libraryId', 'mediaType', 'createdAt']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fields: ['libraryId', 'mediaType', { name: 'title', collate: 'NOCASE' }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fields: ['libraryId', 'mediaType', { name: 'titleIgnorePrefix', collate: 'NOCASE' }]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
fields: ['libraryId', 'mediaId', 'mediaType']
|
fields: ['libraryId', 'mediaId', 'mediaType']
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ class MediaProgress extends Model {
|
|||||||
this.updatedAt
|
this.updatedAt
|
||||||
/** @type {Date} */
|
/** @type {Date} */
|
||||||
this.createdAt
|
this.createdAt
|
||||||
|
/** @type {UUIDV4} */
|
||||||
|
this.podcastId
|
||||||
}
|
}
|
||||||
|
|
||||||
static removeById(mediaProgressId) {
|
static removeById(mediaProgressId) {
|
||||||
@@ -69,7 +71,8 @@ class MediaProgress extends Model {
|
|||||||
ebookLocation: DataTypes.STRING,
|
ebookLocation: DataTypes.STRING,
|
||||||
ebookProgress: DataTypes.FLOAT,
|
ebookProgress: DataTypes.FLOAT,
|
||||||
finishedAt: DataTypes.DATE,
|
finishedAt: DataTypes.DATE,
|
||||||
extraData: DataTypes.JSON
|
extraData: DataTypes.JSON,
|
||||||
|
podcastId: DataTypes.UUID
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sequelize,
|
sequelize,
|
||||||
@@ -123,6 +126,16 @@ class MediaProgress extends Model {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// make sure to call the afterDestroy hook for each instance
|
||||||
|
MediaProgress.addHook('beforeBulkDestroy', (options) => {
|
||||||
|
options.individualHooks = true
|
||||||
|
})
|
||||||
|
|
||||||
|
// update the potentially cached user after destroying the media progress
|
||||||
|
MediaProgress.addHook('afterDestroy', (instance) => {
|
||||||
|
user.mediaProgressRemoved(instance)
|
||||||
|
})
|
||||||
|
|
||||||
user.hasMany(MediaProgress, {
|
user.hasMany(MediaProgress, {
|
||||||
onDelete: 'CASCADE'
|
onDelete: 'CASCADE'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const { DataTypes, Model } = require('sequelize')
|
const { DataTypes, Model } = require('sequelize')
|
||||||
const { getTitlePrefixAtEnd, getTitleIgnorePrefix } = require('../utils')
|
const { getTitlePrefixAtEnd, getTitleIgnorePrefix } = require('../utils')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
const libraryItemsPodcastFilters = require('../utils/queries/libraryItemsPodcastFilters')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef PodcastExpandedProperties
|
* @typedef PodcastExpandedProperties
|
||||||
@@ -61,6 +62,8 @@ class Podcast extends Model {
|
|||||||
this.createdAt
|
this.createdAt
|
||||||
/** @type {Date} */
|
/** @type {Date} */
|
||||||
this.updatedAt
|
this.updatedAt
|
||||||
|
/** @type {number} */
|
||||||
|
this.numEpisodes
|
||||||
|
|
||||||
/** @type {import('./PodcastEpisode')[]} */
|
/** @type {import('./PodcastEpisode')[]} */
|
||||||
this.podcastEpisodes
|
this.podcastEpisodes
|
||||||
@@ -138,13 +141,22 @@ class Podcast extends Model {
|
|||||||
maxNewEpisodesToDownload: DataTypes.INTEGER,
|
maxNewEpisodesToDownload: DataTypes.INTEGER,
|
||||||
coverPath: DataTypes.STRING,
|
coverPath: DataTypes.STRING,
|
||||||
tags: DataTypes.JSON,
|
tags: DataTypes.JSON,
|
||||||
genres: DataTypes.JSON
|
genres: DataTypes.JSON,
|
||||||
|
numEpisodes: DataTypes.INTEGER
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sequelize,
|
sequelize,
|
||||||
modelName: 'podcast'
|
modelName: 'podcast'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Podcast.addHook('afterDestroy', async (instance) => {
|
||||||
|
libraryItemsPodcastFilters.clearCountCache('podcast', 'afterDestroy')
|
||||||
|
})
|
||||||
|
|
||||||
|
Podcast.addHook('afterCreate', async (instance) => {
|
||||||
|
libraryItemsPodcastFilters.clearCountCache('podcast', 'afterCreate')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasMediaFiles() {
|
get hasMediaFiles() {
|
||||||
@@ -202,8 +214,9 @@ class Podcast extends Model {
|
|||||||
} else if (key === 'itunesPageUrl') {
|
} else if (key === 'itunesPageUrl') {
|
||||||
newKey = 'itunesPageURL'
|
newKey = 'itunesPageURL'
|
||||||
}
|
}
|
||||||
if (typeof payload.metadata[key] === 'string' && payload.metadata[key] !== this[newKey]) {
|
if ((typeof payload.metadata[key] === 'string' || payload.metadata[key] === null) && payload.metadata[key] !== this[newKey]) {
|
||||||
this[newKey] = payload.metadata[key]
|
this[newKey] = payload.metadata[key] || null
|
||||||
|
|
||||||
if (key === 'title') {
|
if (key === 'title') {
|
||||||
this.titleIgnorePrefix = getTitleIgnorePrefix(this.title)
|
this.titleIgnorePrefix = getTitleIgnorePrefix(this.title)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const { DataTypes, Model } = require('sequelize')
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
const libraryItemsPodcastFilters = require('../utils/queries/libraryItemsPodcastFilters')
|
||||||
/**
|
/**
|
||||||
* @typedef ChapterObject
|
* @typedef ChapterObject
|
||||||
* @property {number} id
|
* @property {number} id
|
||||||
@@ -132,6 +132,14 @@ class PodcastEpisode extends Model {
|
|||||||
onDelete: 'CASCADE'
|
onDelete: 'CASCADE'
|
||||||
})
|
})
|
||||||
PodcastEpisode.belongsTo(podcast)
|
PodcastEpisode.belongsTo(podcast)
|
||||||
|
|
||||||
|
PodcastEpisode.addHook('afterDestroy', async (instance) => {
|
||||||
|
libraryItemsPodcastFilters.clearCountCache('podcastEpisode', 'afterDestroy')
|
||||||
|
})
|
||||||
|
|
||||||
|
PodcastEpisode.addHook('afterCreate', async (instance) => {
|
||||||
|
libraryItemsPodcastFilters.clearCountCache('podcastEpisode', 'afterCreate')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
get size() {
|
get size() {
|
||||||
@@ -169,7 +177,7 @@ class PodcastEpisode extends Model {
|
|||||||
const track = structuredClone(this.audioFile)
|
const track = structuredClone(this.audioFile)
|
||||||
track.startOffset = 0
|
track.startOffset = 0
|
||||||
track.title = this.audioFile.metadata.filename
|
track.title = this.audioFile.metadata.filename
|
||||||
track.contentUrl = `${global.RouterBasePath}/api/items/${libraryItemId}/file/${track.ino}`
|
track.contentUrl = `/api/items/${libraryItemId}/file/${track.ino}`
|
||||||
return track
|
return track
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -404,6 +404,14 @@ class User extends Model {
|
|||||||
return count > 0
|
return count > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static mediaProgressRemoved(mediaProgress) {
|
||||||
|
const cachedUser = userCache.getById(mediaProgress.userId)
|
||||||
|
if (cachedUser) {
|
||||||
|
Logger.debug(`[User] mediaProgressRemoved: ${mediaProgress.id} from user ${cachedUser.id}`)
|
||||||
|
cachedUser.mediaProgresses = cachedUser.mediaProgresses.filter((mp) => mp.id !== mediaProgress.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize model
|
* Initialize model
|
||||||
* @param {import('../Database').sequelize} sequelize
|
* @param {import('../Database').sequelize} sequelize
|
||||||
@@ -626,6 +634,7 @@ class User extends Model {
|
|||||||
/** @type {import('./MediaProgress')|null} */
|
/** @type {import('./MediaProgress')|null} */
|
||||||
let mediaProgress = null
|
let mediaProgress = null
|
||||||
let mediaItemId = null
|
let mediaItemId = null
|
||||||
|
let podcastId = null
|
||||||
if (progressPayload.episodeId) {
|
if (progressPayload.episodeId) {
|
||||||
const podcastEpisode = await this.sequelize.models.podcastEpisode.findByPk(progressPayload.episodeId, {
|
const podcastEpisode = await this.sequelize.models.podcastEpisode.findByPk(progressPayload.episodeId, {
|
||||||
attributes: ['id', 'podcastId'],
|
attributes: ['id', 'podcastId'],
|
||||||
@@ -654,6 +663,7 @@ class User extends Model {
|
|||||||
}
|
}
|
||||||
mediaItemId = podcastEpisode.id
|
mediaItemId = podcastEpisode.id
|
||||||
mediaProgress = podcastEpisode.mediaProgresses?.[0]
|
mediaProgress = podcastEpisode.mediaProgresses?.[0]
|
||||||
|
podcastId = podcastEpisode.podcastId
|
||||||
} else {
|
} else {
|
||||||
const libraryItem = await this.sequelize.models.libraryItem.findByPk(progressPayload.libraryItemId, {
|
const libraryItem = await this.sequelize.models.libraryItem.findByPk(progressPayload.libraryItemId, {
|
||||||
attributes: ['id', 'mediaId', 'mediaType'],
|
attributes: ['id', 'mediaId', 'mediaType'],
|
||||||
@@ -686,6 +696,7 @@ class User extends Model {
|
|||||||
const newMediaProgressPayload = {
|
const newMediaProgressPayload = {
|
||||||
userId: this.id,
|
userId: this.id,
|
||||||
mediaItemId,
|
mediaItemId,
|
||||||
|
podcastId,
|
||||||
mediaItemType: progressPayload.episodeId ? 'podcastEpisode' : 'book',
|
mediaItemType: progressPayload.episodeId ? 'podcastEpisode' : 'book',
|
||||||
duration: isNullOrNaN(progressPayload.duration) ? 0 : Number(progressPayload.duration),
|
duration: isNullOrNaN(progressPayload.duration) ? 0 : Number(progressPayload.duration),
|
||||||
currentTime: isNullOrNaN(progressPayload.currentTime) ? 0 : Number(progressPayload.currentTime),
|
currentTime: isNullOrNaN(progressPayload.currentTime) ? 0 : Number(progressPayload.currentTime),
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class AudioTrack {
|
|||||||
this.duration = audioFile.duration
|
this.duration = audioFile.duration
|
||||||
this.title = audioFile.metadata.filename || ''
|
this.title = audioFile.metadata.filename || ''
|
||||||
|
|
||||||
this.contentUrl = `${global.RouterBasePath}/api/items/${itemId}/file/${audioFile.ino}`
|
this.contentUrl = `/api/items/${itemId}/file/${audioFile.ino}`
|
||||||
this.mimeType = audioFile.mimeType
|
this.mimeType = audioFile.mimeType
|
||||||
this.codec = audioFile.codec || null
|
this.codec = audioFile.codec || null
|
||||||
this.metadata = audioFile.metadata.clone()
|
this.metadata = audioFile.metadata.clone()
|
||||||
@@ -44,4 +44,4 @@ class AudioTrack {
|
|||||||
this.mimeType = 'application/vnd.apple.mpegurl'
|
this.mimeType = 'application/vnd.apple.mpegurl'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = AudioTrack
|
module.exports = AudioTrack
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ class CustomProviderAdapter {
|
|||||||
}
|
}
|
||||||
const queryString = new URLSearchParams(queryObj).toString()
|
const queryString = new URLSearchParams(queryObj).toString()
|
||||||
|
|
||||||
|
const url = `${provider.url}/search?${queryString}`
|
||||||
|
Logger.debug(`[CustomMetadataProvider] Search url: ${url}`)
|
||||||
|
|
||||||
// Setup headers
|
// Setup headers
|
||||||
const axiosOptions = {
|
const axiosOptions = {
|
||||||
timeout
|
timeout
|
||||||
@@ -52,7 +55,7 @@ class CustomProviderAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const matches = await axios
|
const matches = await axios
|
||||||
.get(`${provider.url}/search?${queryString}`, axiosOptions)
|
.get(url, axiosOptions)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (!res?.data || !Array.isArray(res.data.matches)) return null
|
if (!res?.data || !Array.isArray(res.data.matches)) return null
|
||||||
return res.data.matches
|
return res.data.matches
|
||||||
|
|||||||
@@ -521,6 +521,8 @@ class BookScanner {
|
|||||||
libraryItemObj.isMissing = false
|
libraryItemObj.isMissing = false
|
||||||
libraryItemObj.isInvalid = false
|
libraryItemObj.isInvalid = false
|
||||||
libraryItemObj.extraData = {}
|
libraryItemObj.extraData = {}
|
||||||
|
libraryItemObj.title = bookMetadata.title
|
||||||
|
libraryItemObj.titleIgnorePrefix = getTitleIgnorePrefix(bookMetadata.title)
|
||||||
|
|
||||||
// Set isSupplementary flag on ebook library files
|
// Set isSupplementary flag on ebook library files
|
||||||
for (const libraryFile of libraryItemObj.libraryFiles) {
|
for (const libraryFile of libraryItemObj.libraryFiles) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const uuidv4 = require("uuid").v4
|
const uuidv4 = require('uuid').v4
|
||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const { LogLevel } = require('../utils/constants')
|
const { LogLevel } = require('../utils/constants')
|
||||||
const { getTitleIgnorePrefix } = require('../utils/index')
|
const { getTitleIgnorePrefix } = require('../utils/index')
|
||||||
@@ -8,9 +8,9 @@ const { filePathToPOSIX, getFileTimestampsWithIno } = require('../utils/fileUtil
|
|||||||
const AudioFile = require('../objects/files/AudioFile')
|
const AudioFile = require('../objects/files/AudioFile')
|
||||||
const CoverManager = require('../managers/CoverManager')
|
const CoverManager = require('../managers/CoverManager')
|
||||||
const LibraryFile = require('../objects/files/LibraryFile')
|
const LibraryFile = require('../objects/files/LibraryFile')
|
||||||
const fsExtra = require("../libs/fsExtra")
|
const fsExtra = require('../libs/fsExtra')
|
||||||
const PodcastEpisode = require("../models/PodcastEpisode")
|
const PodcastEpisode = require('../models/PodcastEpisode')
|
||||||
const AbsMetadataFileScanner = require("./AbsMetadataFileScanner")
|
const AbsMetadataFileScanner = require('./AbsMetadataFileScanner')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Metadata for podcasts pulled from files
|
* Metadata for podcasts pulled from files
|
||||||
@@ -32,13 +32,13 @@ const AbsMetadataFileScanner = require("./AbsMetadataFileScanner")
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
class PodcastScanner {
|
class PodcastScanner {
|
||||||
constructor() { }
|
constructor() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('../models/LibraryItem')} existingLibraryItem
|
* @param {import('../models/LibraryItem')} existingLibraryItem
|
||||||
* @param {import('./LibraryItemScanData')} libraryItemData
|
* @param {import('./LibraryItemScanData')} libraryItemData
|
||||||
* @param {import('../models/Library').LibrarySettingsObject} librarySettings
|
* @param {import('../models/Library').LibrarySettingsObject} librarySettings
|
||||||
* @param {import('./LibraryScan')} libraryScan
|
* @param {import('./LibraryScan')} libraryScan
|
||||||
* @returns {Promise<{libraryItem:import('../models/LibraryItem'), wasUpdated:boolean}>}
|
* @returns {Promise<{libraryItem:import('../models/LibraryItem'), wasUpdated:boolean}>}
|
||||||
*/
|
*/
|
||||||
async rescanExistingPodcastLibraryItem(existingLibraryItem, libraryItemData, librarySettings, libraryScan) {
|
async rescanExistingPodcastLibraryItem(existingLibraryItem, libraryItemData, librarySettings, libraryScan) {
|
||||||
@@ -59,28 +59,34 @@ class PodcastScanner {
|
|||||||
|
|
||||||
if (libraryItemData.hasAudioFileChanges || libraryItemData.audioLibraryFiles.length !== existingPodcastEpisodes.length) {
|
if (libraryItemData.hasAudioFileChanges || libraryItemData.audioLibraryFiles.length !== existingPodcastEpisodes.length) {
|
||||||
// Filter out and destroy episodes that were removed
|
// Filter out and destroy episodes that were removed
|
||||||
existingPodcastEpisodes = await Promise.all(existingPodcastEpisodes.filter(async ep => {
|
existingPodcastEpisodes = await Promise.all(
|
||||||
if (libraryItemData.checkAudioFileRemoved(ep.audioFile)) {
|
existingPodcastEpisodes.filter(async (ep) => {
|
||||||
libraryScan.addLog(LogLevel.INFO, `Podcast episode "${ep.title}" audio file was removed`)
|
if (libraryItemData.checkAudioFileRemoved(ep.audioFile)) {
|
||||||
// TODO: Should clean up other data linked to this episode
|
libraryScan.addLog(LogLevel.INFO, `Podcast episode "${ep.title}" audio file was removed`)
|
||||||
await ep.destroy()
|
// TODO: Should clean up other data linked to this episode
|
||||||
return false
|
await ep.destroy()
|
||||||
}
|
return false
|
||||||
return true
|
}
|
||||||
}))
|
return true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
// Update audio files that were modified
|
// Update audio files that were modified
|
||||||
if (libraryItemData.audioLibraryFilesModified.length) {
|
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)
|
||||||
|
)
|
||||||
|
|
||||||
for (const podcastEpisode of existingPodcastEpisodes) {
|
for (const podcastEpisode of existingPodcastEpisodes) {
|
||||||
let matchedScannedAudioFile = scannedAudioFiles.find(saf => saf.metadata.path === podcastEpisode.audioFile.metadata.path)
|
let matchedScannedAudioFile = scannedAudioFiles.find((saf) => saf.metadata.path === podcastEpisode.audioFile.metadata.path)
|
||||||
if (!matchedScannedAudioFile) {
|
if (!matchedScannedAudioFile) {
|
||||||
matchedScannedAudioFile = scannedAudioFiles.find(saf => saf.ino === podcastEpisode.audioFile.ino)
|
matchedScannedAudioFile = scannedAudioFiles.find((saf) => saf.ino === podcastEpisode.audioFile.ino)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matchedScannedAudioFile) {
|
if (matchedScannedAudioFile) {
|
||||||
scannedAudioFiles = scannedAudioFiles.filter(saf => saf !== matchedScannedAudioFile)
|
scannedAudioFiles = scannedAudioFiles.filter((saf) => saf !== matchedScannedAudioFile)
|
||||||
const audioFile = new AudioFile(podcastEpisode.audioFile)
|
const audioFile = new AudioFile(podcastEpisode.audioFile)
|
||||||
audioFile.updateFromScan(matchedScannedAudioFile)
|
audioFile.updateFromScan(matchedScannedAudioFile)
|
||||||
podcastEpisode.audioFile = audioFile.toJSON()
|
podcastEpisode.audioFile = audioFile.toJSON()
|
||||||
@@ -131,15 +137,20 @@ class PodcastScanner {
|
|||||||
|
|
||||||
let hasMediaChanges = false
|
let hasMediaChanges = false
|
||||||
|
|
||||||
|
if (existingPodcastEpisodes.length !== media.numEpisodes) {
|
||||||
|
media.numEpisodes = existingPodcastEpisodes.length
|
||||||
|
hasMediaChanges = true
|
||||||
|
}
|
||||||
|
|
||||||
// Check if cover was removed
|
// Check if cover was removed
|
||||||
if (media.coverPath && libraryItemData.imageLibraryFilesRemoved.some(lf => lf.metadata.path === media.coverPath)) {
|
if (media.coverPath && libraryItemData.imageLibraryFilesRemoved.some((lf) => lf.metadata.path === media.coverPath)) {
|
||||||
media.coverPath = null
|
media.coverPath = null
|
||||||
hasMediaChanges = true
|
hasMediaChanges = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update cover if it was modified
|
// Update cover if it was modified
|
||||||
if (media.coverPath && libraryItemData.imageLibraryFilesModified.length) {
|
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) {
|
if (coverMatch) {
|
||||||
const coverPath = coverMatch.new.metadata.path
|
const coverPath = coverMatch.new.metadata.path
|
||||||
if (coverPath !== media.coverPath) {
|
if (coverPath !== media.coverPath) {
|
||||||
@@ -154,7 +165,7 @@ class PodcastScanner {
|
|||||||
// Check if cover is not set and image files were found
|
// Check if cover is not set and image files were found
|
||||||
if (!media.coverPath && libraryItemData.imageLibraryFiles.length) {
|
if (!media.coverPath && libraryItemData.imageLibraryFiles.length) {
|
||||||
// Prefer using a cover image with the name "cover" otherwise use the first image
|
// 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
|
media.coverPath = coverMatch?.metadata.path || libraryItemData.imageLibraryFiles[0].metadata.path
|
||||||
hasMediaChanges = true
|
hasMediaChanges = true
|
||||||
}
|
}
|
||||||
@@ -167,7 +178,7 @@ class PodcastScanner {
|
|||||||
|
|
||||||
if (key === 'genres') {
|
if (key === 'genres') {
|
||||||
const existingGenres = media.genres || []
|
const existingGenres = media.genres || []
|
||||||
if (podcastMetadata.genres.some(g => !existingGenres.includes(g)) || existingGenres.some(g => !podcastMetadata.genres.includes(g))) {
|
if (podcastMetadata.genres.some((g) => !existingGenres.includes(g)) || existingGenres.some((g) => !podcastMetadata.genres.includes(g))) {
|
||||||
libraryScan.addLog(LogLevel.DEBUG, `Updating podcast genres "${existingGenres.join(',')}" => "${podcastMetadata.genres.join(',')}" for podcast "${podcastMetadata.title}"`)
|
libraryScan.addLog(LogLevel.DEBUG, `Updating podcast genres "${existingGenres.join(',')}" => "${podcastMetadata.genres.join(',')}" for podcast "${podcastMetadata.title}"`)
|
||||||
media.genres = podcastMetadata.genres
|
media.genres = podcastMetadata.genres
|
||||||
media.changed('genres', true)
|
media.changed('genres', true)
|
||||||
@@ -175,7 +186,7 @@ class PodcastScanner {
|
|||||||
}
|
}
|
||||||
} else if (key === 'tags') {
|
} else if (key === 'tags') {
|
||||||
const existingTags = media.tags || []
|
const existingTags = media.tags || []
|
||||||
if (podcastMetadata.tags.some(t => !existingTags.includes(t)) || existingTags.some(t => !podcastMetadata.tags.includes(t))) {
|
if (podcastMetadata.tags.some((t) => !existingTags.includes(t)) || existingTags.some((t) => !podcastMetadata.tags.includes(t))) {
|
||||||
libraryScan.addLog(LogLevel.DEBUG, `Updating podcast tags "${existingTags.join(',')}" => "${podcastMetadata.tags.join(',')}" for podcast "${podcastMetadata.title}"`)
|
libraryScan.addLog(LogLevel.DEBUG, `Updating podcast tags "${existingTags.join(',')}" => "${podcastMetadata.tags.join(',')}" for podcast "${podcastMetadata.title}"`)
|
||||||
media.tags = podcastMetadata.tags
|
media.tags = podcastMetadata.tags
|
||||||
media.changed('tags', true)
|
media.changed('tags', true)
|
||||||
@@ -190,7 +201,7 @@ class PodcastScanner {
|
|||||||
|
|
||||||
// If no cover then extract cover from audio file if available
|
// If no cover then extract cover from audio file if available
|
||||||
if (!media.coverPath && existingPodcastEpisodes.length) {
|
if (!media.coverPath && existingPodcastEpisodes.length) {
|
||||||
const audioFiles = existingPodcastEpisodes.map(ep => ep.audioFile)
|
const audioFiles = existingPodcastEpisodes.map((ep) => ep.audioFile)
|
||||||
const extractedCoverPath = await CoverManager.saveEmbeddedCoverArt(audioFiles, existingLibraryItem.id, existingLibraryItem.path)
|
const extractedCoverPath = await CoverManager.saveEmbeddedCoverArt(audioFiles, existingLibraryItem.id, existingLibraryItem.path)
|
||||||
if (extractedCoverPath) {
|
if (extractedCoverPath) {
|
||||||
libraryScan.addLog(LogLevel.DEBUG, `Updating podcast "${podcastMetadata.title}" extracted embedded cover art from audio file to path "${extractedCoverPath}"`)
|
libraryScan.addLog(LogLevel.DEBUG, `Updating podcast "${podcastMetadata.title}" extracted embedded cover art from audio file to path "${extractedCoverPath}"`)
|
||||||
@@ -222,10 +233,10 @@ class PodcastScanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {import('./LibraryItemScanData')} libraryItemData
|
* @param {import('./LibraryItemScanData')} libraryItemData
|
||||||
* @param {import('../models/Library').LibrarySettingsObject} librarySettings
|
* @param {import('../models/Library').LibrarySettingsObject} librarySettings
|
||||||
* @param {import('./LibraryScan')} libraryScan
|
* @param {import('./LibraryScan')} libraryScan
|
||||||
* @returns {Promise<import('../models/LibraryItem')>}
|
* @returns {Promise<import('../models/LibraryItem')>}
|
||||||
*/
|
*/
|
||||||
async scanNewPodcastLibraryItem(libraryItemData, librarySettings, libraryScan) {
|
async scanNewPodcastLibraryItem(libraryItemData, librarySettings, libraryScan) {
|
||||||
@@ -267,7 +278,7 @@ class PodcastScanner {
|
|||||||
// Set cover image from library file
|
// Set cover image from library file
|
||||||
if (libraryItemData.imageLibraryFiles.length) {
|
if (libraryItemData.imageLibraryFiles.length) {
|
||||||
// Prefer using a cover image with the name "cover" otherwise use the first image
|
// 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))
|
||||||
podcastMetadata.coverPath = coverMatch?.metadata.path || libraryItemData.imageLibraryFiles[0].metadata.path
|
podcastMetadata.coverPath = coverMatch?.metadata.path || libraryItemData.imageLibraryFiles[0].metadata.path
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,7 +294,8 @@ class PodcastScanner {
|
|||||||
lastEpisodeCheck: 0,
|
lastEpisodeCheck: 0,
|
||||||
maxEpisodesToKeep: 0,
|
maxEpisodesToKeep: 0,
|
||||||
maxNewEpisodesToDownload: 3,
|
maxNewEpisodesToDownload: 3,
|
||||||
podcastEpisodes: newPodcastEpisodes
|
podcastEpisodes: newPodcastEpisodes,
|
||||||
|
numEpisodes: newPodcastEpisodes.length
|
||||||
}
|
}
|
||||||
|
|
||||||
const libraryItemObj = libraryItemData.libraryItemObject
|
const libraryItemObj = libraryItemData.libraryItemObject
|
||||||
@@ -291,6 +303,8 @@ class PodcastScanner {
|
|||||||
libraryItemObj.isMissing = false
|
libraryItemObj.isMissing = false
|
||||||
libraryItemObj.isInvalid = false
|
libraryItemObj.isInvalid = false
|
||||||
libraryItemObj.extraData = {}
|
libraryItemObj.extraData = {}
|
||||||
|
libraryItemObj.title = podcastObject.title
|
||||||
|
libraryItemObj.titleIgnorePrefix = getTitleIgnorePrefix(podcastObject.title)
|
||||||
|
|
||||||
// If cover was not found in folder then check embedded covers in audio files
|
// If cover was not found in folder then check embedded covers in audio files
|
||||||
if (!podcastObject.coverPath && scannedAudioFiles.length) {
|
if (!podcastObject.coverPath && scannedAudioFiles.length) {
|
||||||
@@ -324,10 +338,10 @@ class PodcastScanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {PodcastEpisode[]} podcastEpisodes Not the models for new podcasts
|
* @param {PodcastEpisode[]} podcastEpisodes Not the models for new podcasts
|
||||||
* @param {import('./LibraryItemScanData')} libraryItemData
|
* @param {import('./LibraryItemScanData')} libraryItemData
|
||||||
* @param {import('./LibraryScan')} libraryScan
|
* @param {import('./LibraryScan')} libraryScan
|
||||||
* @param {string} [existingLibraryItemId]
|
* @param {string} [existingLibraryItemId]
|
||||||
* @returns {Promise<PodcastMetadataObject>}
|
* @returns {Promise<PodcastMetadataObject>}
|
||||||
*/
|
*/
|
||||||
@@ -364,8 +378,8 @@ class PodcastScanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {import('../models/LibraryItem')} libraryItem
|
* @param {import('../models/LibraryItem')} libraryItem
|
||||||
* @param {import('./LibraryScan')} libraryScan
|
* @param {import('./LibraryScan')} libraryScan
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
@@ -399,41 +413,44 @@ class PodcastScanner {
|
|||||||
explicit: !!libraryItem.media.explicit,
|
explicit: !!libraryItem.media.explicit,
|
||||||
podcastType: libraryItem.media.podcastType
|
podcastType: libraryItem.media.podcastType
|
||||||
}
|
}
|
||||||
return fsExtra.writeFile(metadataFilePath, JSON.stringify(jsonObject, null, 2)).then(async () => {
|
return fsExtra
|
||||||
// Add metadata.json to libraryFiles array if it is new
|
.writeFile(metadataFilePath, JSON.stringify(jsonObject, null, 2))
|
||||||
let metadataLibraryFile = libraryItem.libraryFiles.find(lf => lf.metadata.path === filePathToPOSIX(metadataFilePath))
|
.then(async () => {
|
||||||
if (storeMetadataWithItem) {
|
// Add metadata.json to libraryFiles array if it is new
|
||||||
if (!metadataLibraryFile) {
|
let metadataLibraryFile = libraryItem.libraryFiles.find((lf) => lf.metadata.path === filePathToPOSIX(metadataFilePath))
|
||||||
const newLibraryFile = new LibraryFile()
|
if (storeMetadataWithItem) {
|
||||||
await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`)
|
if (!metadataLibraryFile) {
|
||||||
metadataLibraryFile = newLibraryFile.toJSON()
|
const newLibraryFile = new LibraryFile()
|
||||||
libraryItem.libraryFiles.push(metadataLibraryFile)
|
await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`)
|
||||||
} else {
|
metadataLibraryFile = newLibraryFile.toJSON()
|
||||||
const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath)
|
libraryItem.libraryFiles.push(metadataLibraryFile)
|
||||||
if (fileTimestamps) {
|
} else {
|
||||||
metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs
|
const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath)
|
||||||
metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs
|
if (fileTimestamps) {
|
||||||
metadataLibraryFile.metadata.size = fileTimestamps.size
|
metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs
|
||||||
metadataLibraryFile.ino = fileTimestamps.ino
|
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
|
return metadataLibraryFile
|
||||||
}).catch((error) => {
|
})
|
||||||
libraryScan.addLog(LogLevel.ERROR, `Failed to save json file at "${metadataFilePath}"`, error)
|
.catch((error) => {
|
||||||
return null
|
libraryScan.addLog(LogLevel.ERROR, `Failed to save json file at "${metadataFilePath}"`, error)
|
||||||
})
|
return null
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = new PodcastScanner()
|
module.exports = new PodcastScanner()
|
||||||
|
|||||||
@@ -48,13 +48,7 @@ class Scanner {
|
|||||||
let updatePayload = {}
|
let updatePayload = {}
|
||||||
let hasUpdated = false
|
let hasUpdated = false
|
||||||
|
|
||||||
let existingAuthors = [] // Used for checking if authors or series are now empty
|
|
||||||
let existingSeries = []
|
|
||||||
|
|
||||||
if (libraryItem.isBook) {
|
if (libraryItem.isBook) {
|
||||||
existingAuthors = libraryItem.media.authors.map((a) => a.id)
|
|
||||||
existingSeries = libraryItem.media.series.map((s) => s.id)
|
|
||||||
|
|
||||||
const searchISBN = options.isbn || libraryItem.media.isbn
|
const searchISBN = options.isbn || libraryItem.media.isbn
|
||||||
const searchASIN = options.asin || libraryItem.media.asin
|
const searchASIN = options.asin || libraryItem.media.asin
|
||||||
|
|
||||||
|
|||||||
+52
-10
@@ -131,6 +131,40 @@ async function readTextFile(path) {
|
|||||||
}
|
}
|
||||||
module.exports.readTextFile = readTextFile
|
module.exports.readTextFile = readTextFile
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if file or directory should be ignored. Returns a string of the reason to ignore, or null if not ignored
|
||||||
|
*
|
||||||
|
* @param {string} path
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
module.exports.shouldIgnoreFile = (path) => {
|
||||||
|
// Check if directory or file name starts with "."
|
||||||
|
if (Path.basename(path).startsWith('.')) {
|
||||||
|
return 'dotfile'
|
||||||
|
}
|
||||||
|
if (path.split('/').find((p) => p.startsWith('.'))) {
|
||||||
|
return 'dotpath'
|
||||||
|
}
|
||||||
|
|
||||||
|
// If these strings exist anywhere in the filename or directory name, ignore. Vendor specific hidden directories
|
||||||
|
const includeAnywhereIgnore = ['@eaDir']
|
||||||
|
const filteredInclude = includeAnywhereIgnore.filter((str) => path.includes(str))
|
||||||
|
if (filteredInclude.length) {
|
||||||
|
return `${filteredInclude[0]} directory`
|
||||||
|
}
|
||||||
|
|
||||||
|
const extensionIgnores = ['.part', '.tmp', '.crdownload', '.download', '.bak', '.old', '.temp', '.tempfile', '.tempfile~']
|
||||||
|
|
||||||
|
// Check extension
|
||||||
|
if (extensionIgnores.includes(Path.extname(path).toLowerCase())) {
|
||||||
|
// Return the extension that is ignored
|
||||||
|
return `${Path.extname(path)} file`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should not ignore this file or directory
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef FilePathItem
|
* @typedef FilePathItem
|
||||||
* @property {string} name - file name e.g. "audiofile.m4b"
|
* @property {string} name - file name e.g. "audiofile.m4b"
|
||||||
@@ -147,7 +181,7 @@ module.exports.readTextFile = readTextFile
|
|||||||
* @param {string} [relPathToReplace]
|
* @param {string} [relPathToReplace]
|
||||||
* @returns {FilePathItem[]}
|
* @returns {FilePathItem[]}
|
||||||
*/
|
*/
|
||||||
async function recurseFiles(path, relPathToReplace = null) {
|
module.exports.recurseFiles = async (path, relPathToReplace = null) => {
|
||||||
path = filePathToPOSIX(path)
|
path = filePathToPOSIX(path)
|
||||||
if (!path.endsWith('/')) path = path + '/'
|
if (!path.endsWith('/')) path = path + '/'
|
||||||
|
|
||||||
@@ -197,14 +231,10 @@ async function recurseFiles(path, relPathToReplace = null) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.extension === '.part') {
|
// Check for ignored extensions or directories
|
||||||
Logger.debug(`[fileUtils] Ignoring .part file "${relpath}"`)
|
const shouldIgnore = this.shouldIgnoreFile(relpath)
|
||||||
return false
|
if (shouldIgnore) {
|
||||||
}
|
Logger.debug(`[fileUtils] Ignoring ${shouldIgnore} - "${relpath}"`)
|
||||||
|
|
||||||
// Ignore any file if a directory or the filename starts with "."
|
|
||||||
if (relpath.split('/').find((p) => p.startsWith('.'))) {
|
|
||||||
Logger.debug(`[fileUtils] Ignoring path has . "${relpath}"`)
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,7 +265,6 @@ async function recurseFiles(path, relPathToReplace = null) {
|
|||||||
|
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
module.exports.recurseFiles = recurseFiles
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -286,10 +315,23 @@ module.exports.downloadFile = (url, filepath, contentTypeFilter = null) => {
|
|||||||
return reject(new Error(`Invalid content type "${response.headers?.['content-type'] || ''}"`))
|
return reject(new Error(`Invalid content type "${response.headers?.['content-type'] || ''}"`))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const totalSize = parseInt(response.headers['content-length'], 10)
|
||||||
|
let downloadedSize = 0
|
||||||
|
|
||||||
// Write to filepath
|
// Write to filepath
|
||||||
const writer = fs.createWriteStream(filepath)
|
const writer = fs.createWriteStream(filepath)
|
||||||
response.data.pipe(writer)
|
response.data.pipe(writer)
|
||||||
|
|
||||||
|
let lastProgress = 0
|
||||||
|
response.data.on('data', (chunk) => {
|
||||||
|
downloadedSize += chunk.length
|
||||||
|
const progress = totalSize ? Math.round((downloadedSize / totalSize) * 100) : 0
|
||||||
|
if (progress >= lastProgress + 5) {
|
||||||
|
Logger.debug(`[fileUtils] File "${Path.basename(filepath)}" download progress: ${progress}% (${downloadedSize}/${totalSize} bytes)`)
|
||||||
|
lastProgress = progress
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
writer.on('finish', resolve)
|
writer.on('finish', resolve)
|
||||||
writer.on('error', reject)
|
writer.on('error', reject)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -35,11 +35,18 @@ module.exports.nameToLastFirst = (firstLast) => {
|
|||||||
return `${nameObj.last_name}, ${nameObj.first_name}`
|
return `${nameObj.last_name}, ${nameObj.first_name}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle any name string
|
/**
|
||||||
|
* Parses a name string into an array of names
|
||||||
|
*
|
||||||
|
* @param {string} nameString - The name string to parse
|
||||||
|
* @returns {{ names: string[] }} Array of names
|
||||||
|
*/
|
||||||
module.exports.parse = (nameString) => {
|
module.exports.parse = (nameString) => {
|
||||||
if (!nameString) return null
|
if (!nameString) return null
|
||||||
|
|
||||||
var splitNames = []
|
let splitNames = []
|
||||||
|
const isCommaSeparated = nameString.includes(',')
|
||||||
|
|
||||||
// Example &LF: Friedman, Milton & Friedman, Rose
|
// Example &LF: Friedman, Milton & Friedman, Rose
|
||||||
if (nameString.includes('&')) {
|
if (nameString.includes('&')) {
|
||||||
nameString.split('&').forEach((asa) => (splitNames = splitNames.concat(asa.split(','))))
|
nameString.split('&').forEach((asa) => (splitNames = splitNames.concat(asa.split(','))))
|
||||||
@@ -59,17 +66,18 @@ module.exports.parse = (nameString) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var names = []
|
let names = []
|
||||||
|
|
||||||
// 1 name FIRST LAST
|
// 1 name FIRST LAST
|
||||||
if (splitNames.length === 1) {
|
if (splitNames.length === 1) {
|
||||||
names.push(parseName(nameString))
|
names.push(parseName(nameString))
|
||||||
} else {
|
} else {
|
||||||
var firstChunkIsALastName = checkIsALastName(splitNames[0])
|
// Determines whether this is formatted as last, first or first last (only if using comma separator)
|
||||||
var isEvenNum = splitNames.length % 2 === 0
|
// Example: "Smith; James Jones" -> ["Smith", "James Jones"]
|
||||||
|
let firstChunkIsALastName = !isCommaSeparated ? false : checkIsALastName(splitNames[0])
|
||||||
|
let isEvenNum = splitNames.length % 2 === 0
|
||||||
|
|
||||||
if (!isEvenNum && firstChunkIsALastName) {
|
if (!isEvenNum && firstChunkIsALastName) {
|
||||||
// console.error('Multi-name LAST,FIRST entry has a straggler (could be roman numerals or a suffix), ignore it')
|
|
||||||
splitNames = splitNames.slice(0, splitNames.length - 1)
|
splitNames = splitNames.slice(0, splitNames.length - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -145,15 +145,15 @@ function extractEpisodeData(item) {
|
|||||||
|
|
||||||
if (item.enclosure?.[0]?.['$']?.url) {
|
if (item.enclosure?.[0]?.['$']?.url) {
|
||||||
enclosure = item.enclosure[0]['$']
|
enclosure = item.enclosure[0]['$']
|
||||||
} else if(item['media:content']?.find(c => c?.['$']?.url && (c?.['$']?.type ?? "").startsWith("audio"))) {
|
} else if (item['media:content']?.find((c) => c?.['$']?.url && (c?.['$']?.type ?? '').startsWith('audio'))) {
|
||||||
enclosure = item['media:content'].find(c => (c['$']?.type ?? "").startsWith("audio"))['$']
|
enclosure = item['media:content'].find((c) => (c['$']?.type ?? '').startsWith('audio'))['$']
|
||||||
} else {
|
} else {
|
||||||
Logger.error(`[podcastUtils] Invalid podcast episode data`)
|
Logger.error(`[podcastUtils] Invalid podcast episode data`)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const episode = {
|
const episode = {
|
||||||
enclosure: enclosure,
|
enclosure: enclosure
|
||||||
}
|
}
|
||||||
|
|
||||||
episode.enclosure.url = episode.enclosure.url.trim()
|
episode.enclosure.url = episode.enclosure.url.trim()
|
||||||
@@ -311,6 +311,7 @@ module.exports.getPodcastFeed = (feedUrl, excludeEpisodeMetadata = false) => {
|
|||||||
responseType: 'arraybuffer',
|
responseType: 'arraybuffer',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/rss+xml, application/xhtml+xml, application/xml, */*;q=0.8',
|
Accept: 'application/rss+xml, application/xhtml+xml, application/xml, */*;q=0.8',
|
||||||
|
'Accept-Encoding': 'gzip, compress, deflate',
|
||||||
'User-Agent': userAgent
|
'User-Agent': userAgent
|
||||||
},
|
},
|
||||||
httpAgent: global.DisableSsrfRequestFilter?.(feedUrl) ? null : ssrfFilter(feedUrl),
|
httpAgent: global.DisableSsrfRequestFilter?.(feedUrl) ? null : ssrfFilter(feedUrl),
|
||||||
@@ -342,6 +343,14 @@ module.exports.getPodcastFeed = (feedUrl, excludeEpisodeMetadata = false) => {
|
|||||||
return payload.podcast
|
return payload.podcast
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
// Check for failures due to redirecting from http to https. If original url was http, upgrade to https and try again
|
||||||
|
if (error.code === 'ERR_FR_REDIRECTION_FAILURE' && error.cause.code === 'ERR_INVALID_PROTOCOL') {
|
||||||
|
if (feedUrl.startsWith('http://') && error.request._options.protocol === 'https:') {
|
||||||
|
Logger.info('Redirection from http to https detected. Upgrading Request', error.request._options.href)
|
||||||
|
feedUrl = feedUrl.replace('http://', 'https://')
|
||||||
|
return this.getPodcastFeed(feedUrl, excludeEpisodeMetadata)
|
||||||
|
}
|
||||||
|
}
|
||||||
Logger.error('[podcastUtils] getPodcastFeed Error', error)
|
Logger.error('[podcastUtils] getPodcastFeed Error', error)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
const { performance, createHistogram } = require('perf_hooks')
|
||||||
|
const util = require('util')
|
||||||
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
|
const histograms = new Map()
|
||||||
|
|
||||||
|
function profile(asyncFunc, isFindQuery = true, funcName = asyncFunc.name) {
|
||||||
|
if (!histograms.has(funcName)) {
|
||||||
|
const histogram = createHistogram()
|
||||||
|
histogram.values = []
|
||||||
|
histograms.set(funcName, histogram)
|
||||||
|
}
|
||||||
|
const histogram = histograms.get(funcName)
|
||||||
|
|
||||||
|
return async (...args) => {
|
||||||
|
if (isFindQuery) {
|
||||||
|
const findOptions = args[0]
|
||||||
|
Logger.info(`[${funcName}] findOptions:`, util.inspect(findOptions, { depth: null }))
|
||||||
|
findOptions.logging = (query, time) => Logger.info(`[${funcName}] ${query} Elapsed time: ${time}ms`)
|
||||||
|
findOptions.benchmark = true
|
||||||
|
}
|
||||||
|
const start = performance.now()
|
||||||
|
try {
|
||||||
|
const result = await asyncFunc(...args)
|
||||||
|
return result
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(`[${funcName}] failed`)
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
const end = performance.now()
|
||||||
|
const duration = Math.round(end - start)
|
||||||
|
histogram.record(duration)
|
||||||
|
histogram.values.push(duration)
|
||||||
|
Logger.info(`[${funcName}] duration: ${duration}ms`)
|
||||||
|
Logger.info(`[${funcName}] histogram values:`, histogram.values)
|
||||||
|
Logger.info(`[${funcName}] histogram:`, histogram)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { profile }
|
||||||
@@ -4,6 +4,7 @@ const Database = require('../../Database')
|
|||||||
const libraryItemsBookFilters = require('./libraryItemsBookFilters')
|
const libraryItemsBookFilters = require('./libraryItemsBookFilters')
|
||||||
const libraryItemsPodcastFilters = require('./libraryItemsPodcastFilters')
|
const libraryItemsPodcastFilters = require('./libraryItemsPodcastFilters')
|
||||||
const { createNewSortInstance } = require('../../libs/fastSort')
|
const { createNewSortInstance } = require('../../libs/fastSort')
|
||||||
|
const { profile } = require('../../utils/profiler')
|
||||||
const naturalSort = createNewSortInstance({
|
const naturalSort = createNewSortInstance({
|
||||||
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
|
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
|
||||||
})
|
})
|
||||||
@@ -474,7 +475,8 @@ module.exports = {
|
|||||||
// Check how many podcasts are in library to determine if we need to load all of the data
|
// Check how many podcasts are in library to determine if we need to load all of the data
|
||||||
// This is done to handle the edge case of podcasts having been deleted and not having
|
// This is done to handle the edge case of podcasts having been deleted and not having
|
||||||
// an updatedAt timestamp to trigger a reload of the filter data
|
// an updatedAt timestamp to trigger a reload of the filter data
|
||||||
const podcastCountFromDatabase = await Database.podcastModel.count({
|
const podcastModelCount = process.env.QUERY_PROFILING ? profile(Database.podcastModel.count.bind(Database.podcastModel)) : Database.podcastModel.count.bind(Database.podcastModel)
|
||||||
|
const podcastCountFromDatabase = await podcastModelCount({
|
||||||
include: {
|
include: {
|
||||||
model: Database.libraryItemModel,
|
model: Database.libraryItemModel,
|
||||||
attributes: [],
|
attributes: [],
|
||||||
@@ -489,7 +491,7 @@ module.exports = {
|
|||||||
// data was loaded. If so, we can skip loading all of the data.
|
// data was loaded. If so, we can skip loading all of the data.
|
||||||
// Because many items could change, just check the count of items instead
|
// Because many items could change, just check the count of items instead
|
||||||
// of actually loading the data twice
|
// of actually loading the data twice
|
||||||
const changedPodcasts = await Database.podcastModel.count({
|
const changedPodcasts = await podcastModelCount({
|
||||||
include: {
|
include: {
|
||||||
model: Database.libraryItemModel,
|
model: Database.libraryItemModel,
|
||||||
attributes: [],
|
attributes: [],
|
||||||
@@ -520,7 +522,8 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Something has changed in the podcasts table, so reload all of the filter data for library
|
// Something has changed in the podcasts table, so reload all of the filter data for library
|
||||||
const podcasts = await Database.podcastModel.findAll({
|
const findAll = process.env.QUERY_PROFILING ? profile(Database.podcastModel.findAll.bind(Database.podcastModel)) : Database.podcastModel.findAll.bind(Database.podcastModel)
|
||||||
|
const podcasts = await findAll({
|
||||||
include: {
|
include: {
|
||||||
model: Database.libraryItemModel,
|
model: Database.libraryItemModel,
|
||||||
attributes: [],
|
attributes: [],
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ const Logger = require('../../Logger')
|
|||||||
const authorFilters = require('./authorFilters')
|
const authorFilters = require('./authorFilters')
|
||||||
|
|
||||||
const ShareManager = require('../../managers/ShareManager')
|
const ShareManager = require('../../managers/ShareManager')
|
||||||
|
const { profile } = require('../profiler')
|
||||||
|
const stringifySequelizeQuery = require('../stringifySequelizeQuery')
|
||||||
|
const countCache = new Map()
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
/**
|
/**
|
||||||
@@ -270,9 +273,9 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (global.ServerSettings.sortingIgnorePrefix) {
|
if (global.ServerSettings.sortingIgnorePrefix) {
|
||||||
return [[Sequelize.literal('titleIgnorePrefix COLLATE NOCASE'), dir]]
|
return [[Sequelize.literal('`libraryItem`.`titleIgnorePrefix` COLLATE NOCASE'), dir]]
|
||||||
} else {
|
} else {
|
||||||
return [[Sequelize.literal('`book`.`title` COLLATE NOCASE'), dir]]
|
return [[Sequelize.literal('`libraryItem`.`title` COLLATE NOCASE'), dir]]
|
||||||
}
|
}
|
||||||
} else if (sortBy === 'sequence') {
|
} else if (sortBy === 'sequence') {
|
||||||
const nullDir = sortDesc ? 'DESC NULLS FIRST' : 'ASC NULLS LAST'
|
const nullDir = sortDesc ? 'DESC NULLS FIRST' : 'ASC NULLS LAST'
|
||||||
@@ -336,6 +339,35 @@ module.exports = {
|
|||||||
return { booksToExclude, bookSeriesToInclude }
|
return { booksToExclude, bookSeriesToInclude }
|
||||||
},
|
},
|
||||||
|
|
||||||
|
clearCountCache(hook) {
|
||||||
|
Logger.debug(`[LibraryItemsBookFilters] book.${hook}: Clearing count cache`)
|
||||||
|
countCache.clear()
|
||||||
|
},
|
||||||
|
|
||||||
|
async findAndCountAll(findOptions, limit, offset, useCountCache) {
|
||||||
|
const model = Database.bookModel
|
||||||
|
if (useCountCache) {
|
||||||
|
const countCacheKey = stringifySequelizeQuery(findOptions)
|
||||||
|
Logger.debug(`[LibraryItemsBookFilters] countCacheKey: ${countCacheKey}`)
|
||||||
|
if (!countCache.has(countCacheKey)) {
|
||||||
|
const count = await model.count(findOptions)
|
||||||
|
countCache.set(countCacheKey, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
findOptions.limit = limit || null
|
||||||
|
findOptions.offset = offset
|
||||||
|
|
||||||
|
const rows = await model.findAll(findOptions)
|
||||||
|
|
||||||
|
return { rows, count: countCache.get(countCacheKey) }
|
||||||
|
}
|
||||||
|
|
||||||
|
findOptions.limit = limit || null
|
||||||
|
findOptions.offset = offset
|
||||||
|
|
||||||
|
return await model.findAndCountAll(findOptions)
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get library items for book media type using filter and sort
|
* Get library items for book media type using filter and sort
|
||||||
* @param {string} libraryId
|
* @param {string} libraryId
|
||||||
@@ -408,18 +440,17 @@ module.exports = {
|
|||||||
|
|
||||||
const libraryItemIncludes = []
|
const libraryItemIncludes = []
|
||||||
const bookIncludes = []
|
const bookIncludes = []
|
||||||
if (includeRSSFeed) {
|
|
||||||
|
if (filterGroup === 'feed-open' || includeRSSFeed) {
|
||||||
|
const rssFeedRequired = filterGroup === 'feed-open'
|
||||||
libraryItemIncludes.push({
|
libraryItemIncludes.push({
|
||||||
model: Database.feedModel,
|
model: Database.feedModel,
|
||||||
required: filterGroup === 'feed-open'
|
required: rssFeedRequired,
|
||||||
|
separate: !rssFeedRequired
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (filterGroup === 'feed-open' && !includeRSSFeed) {
|
|
||||||
libraryItemIncludes.push({
|
if (filterGroup === 'share-open') {
|
||||||
model: Database.feedModel,
|
|
||||||
required: true
|
|
||||||
})
|
|
||||||
} else if (filterGroup === 'share-open') {
|
|
||||||
bookIncludes.push({
|
bookIncludes.push({
|
||||||
model: Database.mediaItemShareModel,
|
model: Database.mediaItemShareModel,
|
||||||
required: true
|
required: true
|
||||||
@@ -554,13 +585,13 @@ module.exports = {
|
|||||||
// When collapsing series and sorting by title then use the series name instead of the book title
|
// When collapsing series and sorting by title then use the series name instead of the book title
|
||||||
// for this set an attribute "display_title" to use in sorting
|
// for this set an attribute "display_title" to use in sorting
|
||||||
if (global.ServerSettings.sortingIgnorePrefix) {
|
if (global.ServerSettings.sortingIgnorePrefix) {
|
||||||
bookAttributes.include.push([Sequelize.literal(`IFNULL((SELECT s.nameIgnorePrefix FROM bookSeries AS bs, series AS s WHERE bs.seriesId = s.id AND bs.bookId = book.id AND bs.id IN (${bookSeriesToInclude.map((v) => `"${v.id}"`).join(', ')})), titleIgnorePrefix)`), 'display_title'])
|
bookAttributes.include.push([Sequelize.literal(`IFNULL((SELECT s.nameIgnorePrefix FROM bookSeries AS bs, series AS s WHERE bs.seriesId = s.id AND bs.bookId = book.id AND bs.id IN (${bookSeriesToInclude.map((v) => `"${v.id}"`).join(', ')})), \`libraryItem\`.\`titleIgnorePrefix\`)`), 'display_title'])
|
||||||
} else {
|
} else {
|
||||||
bookAttributes.include.push([Sequelize.literal(`IFNULL((SELECT s.name FROM bookSeries AS bs, series AS s WHERE bs.seriesId = s.id AND bs.bookId = book.id AND bs.id IN (${bookSeriesToInclude.map((v) => `"${v.id}"`).join(', ')})), \`book\`.\`title\`)`), 'display_title'])
|
bookAttributes.include.push([Sequelize.literal(`IFNULL((SELECT s.name FROM bookSeries AS bs, series AS s WHERE bs.seriesId = s.id AND bs.bookId = book.id AND bs.id IN (${bookSeriesToInclude.map((v) => `"${v.id}"`).join(', ')})), \`libraryItem\`.\`title\`)`), 'display_title'])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { rows: books, count } = await Database.bookModel.findAndCountAll({
|
const findOptions = {
|
||||||
where: bookWhere,
|
where: bookWhere,
|
||||||
distinct: true,
|
distinct: true,
|
||||||
attributes: bookAttributes,
|
attributes: bookAttributes,
|
||||||
@@ -577,10 +608,11 @@ module.exports = {
|
|||||||
...bookIncludes
|
...bookIncludes
|
||||||
],
|
],
|
||||||
order: sortOrder,
|
order: sortOrder,
|
||||||
subQuery: false,
|
subQuery: false
|
||||||
limit: limit || null,
|
}
|
||||||
offset
|
|
||||||
})
|
const findAndCountAll = process.env.QUERY_PROFILING ? profile(this.findAndCountAll) : this.findAndCountAll
|
||||||
|
const { rows: books, count } = await findAndCountAll(findOptions, limit, offset, !filterGroup)
|
||||||
|
|
||||||
const libraryItems = books.map((bookExpanded) => {
|
const libraryItems = books.map((bookExpanded) => {
|
||||||
const libraryItem = bookExpanded.libraryItem
|
const libraryItem = bookExpanded.libraryItem
|
||||||
@@ -1008,8 +1040,8 @@ module.exports = {
|
|||||||
|
|
||||||
const textSearchQuery = await Database.createTextSearchQuery(query)
|
const textSearchQuery = await Database.createTextSearchQuery(query)
|
||||||
|
|
||||||
const matchTitle = textSearchQuery.matchExpression('title')
|
const matchTitle = textSearchQuery.matchExpression('book.title')
|
||||||
const matchSubtitle = textSearchQuery.matchExpression('subtitle')
|
const matchSubtitle = textSearchQuery.matchExpression('book.subtitle')
|
||||||
|
|
||||||
// Search title, subtitle, asin, isbn
|
// Search title, subtitle, asin, isbn
|
||||||
const books = await Database.bookModel.findAll({
|
const books = await Database.bookModel.findAll({
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
const Sequelize = require('sequelize')
|
const Sequelize = require('sequelize')
|
||||||
const Database = require('../../Database')
|
const Database = require('../../Database')
|
||||||
const Logger = require('../../Logger')
|
const Logger = require('../../Logger')
|
||||||
|
const { profile } = require('../../utils/profiler')
|
||||||
|
const stringifySequelizeQuery = require('../stringifySequelizeQuery')
|
||||||
|
|
||||||
|
const countCache = new Map()
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
/**
|
/**
|
||||||
@@ -84,9 +88,9 @@ module.exports = {
|
|||||||
return [[Sequelize.literal(`\`podcast\`.\`author\` COLLATE NOCASE ${nullDir}`)]]
|
return [[Sequelize.literal(`\`podcast\`.\`author\` COLLATE NOCASE ${nullDir}`)]]
|
||||||
} else if (sortBy === 'media.metadata.title') {
|
} else if (sortBy === 'media.metadata.title') {
|
||||||
if (global.ServerSettings.sortingIgnorePrefix) {
|
if (global.ServerSettings.sortingIgnorePrefix) {
|
||||||
return [[Sequelize.literal('titleIgnorePrefix COLLATE NOCASE'), dir]]
|
return [[Sequelize.literal('`libraryItem`.`titleIgnorePrefix` COLLATE NOCASE'), dir]]
|
||||||
} else {
|
} else {
|
||||||
return [[Sequelize.literal('`podcast`.`title` COLLATE NOCASE'), dir]]
|
return [[Sequelize.literal('`libraryItem`.`title` COLLATE NOCASE'), dir]]
|
||||||
}
|
}
|
||||||
} else if (sortBy === 'media.numTracks') {
|
} else if (sortBy === 'media.numTracks') {
|
||||||
return [['numEpisodes', dir]]
|
return [['numEpisodes', dir]]
|
||||||
@@ -96,6 +100,34 @@ module.exports = {
|
|||||||
return []
|
return []
|
||||||
},
|
},
|
||||||
|
|
||||||
|
clearCountCache(model, hook) {
|
||||||
|
Logger.debug(`[LibraryItemsPodcastFilters] ${model}.${hook}: Clearing count cache`)
|
||||||
|
countCache.clear()
|
||||||
|
},
|
||||||
|
|
||||||
|
async findAndCountAll(findOptions, model, limit, offset, useCountCache) {
|
||||||
|
if (useCountCache) {
|
||||||
|
const countCacheKey = stringifySequelizeQuery(findOptions)
|
||||||
|
Logger.debug(`[LibraryItemsPodcastFilters] countCacheKey: ${countCacheKey}`)
|
||||||
|
if (!countCache.has(countCacheKey)) {
|
||||||
|
const count = await model.count(findOptions)
|
||||||
|
countCache.set(countCacheKey, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
findOptions.limit = limit || null
|
||||||
|
findOptions.offset = offset
|
||||||
|
|
||||||
|
const rows = await model.findAll(findOptions)
|
||||||
|
|
||||||
|
return { rows, count: countCache.get(countCacheKey) }
|
||||||
|
}
|
||||||
|
|
||||||
|
findOptions.limit = limit || null
|
||||||
|
findOptions.offset = offset
|
||||||
|
|
||||||
|
return await model.findAndCountAll(findOptions)
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get library items for podcast media type using filter and sort
|
* Get library items for podcast media type using filter and sort
|
||||||
* @param {string} libraryId
|
* @param {string} libraryId
|
||||||
@@ -120,7 +152,8 @@ module.exports = {
|
|||||||
if (includeRSSFeed) {
|
if (includeRSSFeed) {
|
||||||
libraryItemIncludes.push({
|
libraryItemIncludes.push({
|
||||||
model: Database.feedModel,
|
model: Database.feedModel,
|
||||||
required: filterGroup === 'feed-open'
|
required: filterGroup === 'feed-open',
|
||||||
|
separate: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (filterGroup === 'issues') {
|
if (filterGroup === 'issues') {
|
||||||
@@ -139,9 +172,6 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const podcastIncludes = []
|
const podcastIncludes = []
|
||||||
if (includeNumEpisodesIncomplete) {
|
|
||||||
podcastIncludes.push([Sequelize.literal(`(SELECT count(*) FROM podcastEpisodes pe LEFT OUTER JOIN mediaProgresses mp ON mp.mediaItemId = pe.id AND mp.userId = :userId WHERE pe.podcastId = podcast.id AND (mp.isFinished = 0 OR mp.isFinished IS NULL))`), 'numEpisodesIncomplete'])
|
|
||||||
}
|
|
||||||
|
|
||||||
let { mediaWhere, replacements } = this.getMediaGroupQuery(filterGroup, filterValue)
|
let { mediaWhere, replacements } = this.getMediaGroupQuery(filterGroup, filterValue)
|
||||||
replacements.userId = user.id
|
replacements.userId = user.id
|
||||||
@@ -153,12 +183,12 @@ module.exports = {
|
|||||||
replacements = { ...replacements, ...userPermissionPodcastWhere.replacements }
|
replacements = { ...replacements, ...userPermissionPodcastWhere.replacements }
|
||||||
podcastWhere.push(...userPermissionPodcastWhere.podcastWhere)
|
podcastWhere.push(...userPermissionPodcastWhere.podcastWhere)
|
||||||
|
|
||||||
const { rows: podcasts, count } = await Database.podcastModel.findAndCountAll({
|
const findOptions = {
|
||||||
where: podcastWhere,
|
where: podcastWhere,
|
||||||
replacements,
|
replacements,
|
||||||
distinct: true,
|
distinct: true,
|
||||||
attributes: {
|
attributes: {
|
||||||
include: [[Sequelize.literal(`(SELECT count(*) FROM podcastEpisodes pe WHERE pe.podcastId = podcast.id)`), 'numEpisodes'], ...podcastIncludes]
|
include: [...podcastIncludes]
|
||||||
},
|
},
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
@@ -169,10 +199,12 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
order: this.getOrder(sortBy, sortDesc),
|
order: this.getOrder(sortBy, sortDesc),
|
||||||
subQuery: false,
|
subQuery: false
|
||||||
limit: limit || null,
|
}
|
||||||
offset
|
|
||||||
})
|
const findAndCountAll = process.env.QUERY_PROFILING ? profile(this.findAndCountAll) : this.findAndCountAll
|
||||||
|
|
||||||
|
const { rows: podcasts, count } = await findAndCountAll(findOptions, Database.podcastModel, limit, offset, !filterGroup)
|
||||||
|
|
||||||
const libraryItems = podcasts.map((podcastExpanded) => {
|
const libraryItems = podcasts.map((podcastExpanded) => {
|
||||||
const libraryItem = podcastExpanded.libraryItem
|
const libraryItem = podcastExpanded.libraryItem
|
||||||
@@ -183,11 +215,15 @@ module.exports = {
|
|||||||
if (libraryItem.feeds?.length) {
|
if (libraryItem.feeds?.length) {
|
||||||
libraryItem.rssFeed = libraryItem.feeds[0]
|
libraryItem.rssFeed = libraryItem.feeds[0]
|
||||||
}
|
}
|
||||||
if (podcast.dataValues.numEpisodesIncomplete) {
|
|
||||||
libraryItem.numEpisodesIncomplete = podcast.dataValues.numEpisodesIncomplete
|
if (includeNumEpisodesIncomplete) {
|
||||||
}
|
const numEpisodesComplete = user.mediaProgresses.reduce((acc, mp) => {
|
||||||
if (podcast.dataValues.numEpisodes) {
|
if (mp.podcastId === podcast.id && mp.isFinished) {
|
||||||
podcast.numEpisodes = podcast.dataValues.numEpisodes
|
acc += 1
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, 0)
|
||||||
|
libraryItem.numEpisodesIncomplete = podcast.numEpisodes - numEpisodesComplete
|
||||||
}
|
}
|
||||||
|
|
||||||
libraryItem.media = podcast
|
libraryItem.media = podcast
|
||||||
@@ -268,28 +304,31 @@ module.exports = {
|
|||||||
|
|
||||||
const userPermissionPodcastWhere = this.getUserPermissionPodcastWhereQuery(user)
|
const userPermissionPodcastWhere = this.getUserPermissionPodcastWhereQuery(user)
|
||||||
|
|
||||||
const { rows: podcastEpisodes, count } = await Database.podcastEpisodeModel.findAndCountAll({
|
const findOptions = {
|
||||||
where: podcastEpisodeWhere,
|
where: podcastEpisodeWhere,
|
||||||
replacements: userPermissionPodcastWhere.replacements,
|
replacements: userPermissionPodcastWhere.replacements,
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: Database.podcastModel,
|
model: Database.podcastModel,
|
||||||
|
required: true,
|
||||||
where: userPermissionPodcastWhere.podcastWhere,
|
where: userPermissionPodcastWhere.podcastWhere,
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: Database.libraryItemModel,
|
model: Database.libraryItemModel,
|
||||||
|
required: true,
|
||||||
where: libraryItemWhere
|
where: libraryItemWhere
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
...podcastEpisodeIncludes
|
...podcastEpisodeIncludes
|
||||||
],
|
],
|
||||||
distinct: true,
|
|
||||||
subQuery: false,
|
subQuery: false,
|
||||||
order: podcastEpisodeOrder,
|
order: podcastEpisodeOrder
|
||||||
limit,
|
}
|
||||||
offset
|
|
||||||
})
|
const findAndCountAll = process.env.QUERY_PROFILING ? profile(this.findAndCountAll) : this.findAndCountAll
|
||||||
|
|
||||||
|
const { rows: podcastEpisodes, count } = await findAndCountAll(findOptions, Database.podcastEpisodeModel, limit, offset, !filterGroup)
|
||||||
|
|
||||||
const libraryItems = podcastEpisodes.map((ep) => {
|
const libraryItems = podcastEpisodes.map((ep) => {
|
||||||
const libraryItem = ep.podcast.libraryItem
|
const libraryItem = ep.podcast.libraryItem
|
||||||
@@ -321,8 +360,8 @@ module.exports = {
|
|||||||
|
|
||||||
const textSearchQuery = await Database.createTextSearchQuery(query)
|
const textSearchQuery = await Database.createTextSearchQuery(query)
|
||||||
|
|
||||||
const matchTitle = textSearchQuery.matchExpression('title')
|
const matchTitle = textSearchQuery.matchExpression('podcast.title')
|
||||||
const matchAuthor = textSearchQuery.matchExpression('author')
|
const matchAuthor = textSearchQuery.matchExpression('podcast.author')
|
||||||
|
|
||||||
// Search title, author, itunesId, itunesArtistId
|
// Search title, author, itunesId, itunesArtistId
|
||||||
const podcasts = await Database.podcastModel.findAll({
|
const podcasts = await Database.podcastModel.findAll({
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
function stringifySequelizeQuery(findOptions) {
|
||||||
|
function isClass(func) {
|
||||||
|
return typeof func === 'function' && /^class\s/.test(func.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
function replacer(key, value) {
|
||||||
|
if (typeof value === 'object' && value !== null) {
|
||||||
|
const symbols = Object.getOwnPropertySymbols(value).reduce((acc, sym) => {
|
||||||
|
acc[sym.toString()] = value[sym]
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
return { ...value, ...symbols }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isClass(value)) {
|
||||||
|
return `${value.name}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(findOptions, replacer)
|
||||||
|
}
|
||||||
|
module.exports = stringifySequelizeQuery
|
||||||
@@ -129,9 +129,9 @@ describe('migration-v2.15.0-series-column-unique', () => {
|
|||||||
{ id: series1Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() },
|
{ id: series1Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() },
|
||||||
{ id: series2Id, name: 'Series 2', libraryId: library2Id, createdAt: new Date(), updatedAt: new Date() },
|
{ id: series2Id, name: 'Series 2', libraryId: library2Id, createdAt: new Date(), updatedAt: new Date() },
|
||||||
{ id: series3Id, name: 'Series 3', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() },
|
{ id: series3Id, name: 'Series 3', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() },
|
||||||
{ id: series1Id_dup, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() },
|
{ id: series1Id_dup, name: 'Series 1', libraryId: library1Id, createdAt: new Date(0), updatedAt: new Date(0) },
|
||||||
{ id: series3Id_dup, name: 'Series 3', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() },
|
{ id: series3Id_dup, name: 'Series 3', libraryId: library1Id, createdAt: new Date(0), updatedAt: new Date(0) },
|
||||||
{ id: series1Id_dup2, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() }
|
{ id: series1Id_dup2, name: 'Series 1', libraryId: library1Id, createdAt: new Date(0), updatedAt: new Date(0) }
|
||||||
])
|
])
|
||||||
// Add some entries to the BookSeries table
|
// Add some entries to the BookSeries table
|
||||||
await queryInterface.bulkInsert('BookSeries', [
|
await queryInterface.bulkInsert('BookSeries', [
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
const chai = require('chai')
|
||||||
|
const sinon = require('sinon')
|
||||||
|
const { expect } = chai
|
||||||
|
|
||||||
|
const { DataTypes, Sequelize } = require('sequelize')
|
||||||
|
const Logger = require('../../../server/Logger')
|
||||||
|
|
||||||
|
const { up, down } = require('../../../server/migrations/v2.19.1-copy-title-to-library-items')
|
||||||
|
|
||||||
|
describe('Migration v2.19.1-copy-title-to-library-items', () => {
|
||||||
|
let sequelize
|
||||||
|
let queryInterface
|
||||||
|
let loggerInfoStub
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
|
||||||
|
queryInterface = sequelize.getQueryInterface()
|
||||||
|
loggerInfoStub = sinon.stub(Logger, 'info')
|
||||||
|
|
||||||
|
await queryInterface.createTable('books', {
|
||||||
|
id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, unique: true },
|
||||||
|
title: { type: DataTypes.STRING, allowNull: true },
|
||||||
|
titleIgnorePrefix: { type: DataTypes.STRING, allowNull: true }
|
||||||
|
})
|
||||||
|
|
||||||
|
await queryInterface.createTable('libraryItems', {
|
||||||
|
id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, unique: true },
|
||||||
|
libraryId: { type: DataTypes.INTEGER, allowNull: false },
|
||||||
|
mediaType: { type: DataTypes.STRING, allowNull: false },
|
||||||
|
mediaId: { type: DataTypes.INTEGER, allowNull: false },
|
||||||
|
createdAt: { type: DataTypes.DATE, allowNull: false }
|
||||||
|
})
|
||||||
|
|
||||||
|
await queryInterface.bulkInsert('books', [
|
||||||
|
{ id: 1, title: 'The Book 1', titleIgnorePrefix: 'Book 1, The' },
|
||||||
|
{ id: 2, title: 'Book 2', titleIgnorePrefix: 'Book 2' }
|
||||||
|
])
|
||||||
|
|
||||||
|
await queryInterface.bulkInsert('libraryItems', [
|
||||||
|
{ id: 1, libraryId: 1, mediaType: 'book', mediaId: 1, createdAt: '2025-01-01 00:00:00.000 +00:00' },
|
||||||
|
{ id: 2, libraryId: 2, mediaType: 'book', mediaId: 2, createdAt: '2025-01-02 00:00:00.000 +00:00' }
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
sinon.restore()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('up', () => {
|
||||||
|
it('should copy title and titleIgnorePrefix to libraryItems', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [libraryItems] = await queryInterface.sequelize.query('SELECT * FROM libraryItems')
|
||||||
|
expect(libraryItems).to.deep.equal([
|
||||||
|
{ id: 1, libraryId: 1, mediaType: 'book', mediaId: 1, title: 'The Book 1', titleIgnorePrefix: 'Book 1, The', createdAt: '2025-01-01 00:00:00.000 +00:00' },
|
||||||
|
{ id: 2, libraryId: 2, mediaType: 'book', mediaId: 2, title: 'Book 2', titleIgnorePrefix: 'Book 2', createdAt: '2025-01-02 00:00:00.000 +00:00' }
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should add index on title to libraryItems', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='index' AND name='library_items_library_id_media_type_title_ignore_prefix'`)
|
||||||
|
expect(count).to.equal(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should add trigger to books.title to update libraryItems.title', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_title'`)
|
||||||
|
expect(count).to.equal(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should add index on titleIgnorePrefix to libraryItems', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='index' AND name='library_items_library_id_media_type_title_ignore_prefix'`)
|
||||||
|
expect(count).to.equal(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should add trigger to books.titleIgnorePrefix to update libraryItems.titleIgnorePrefix', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_title_ignore_prefix'`)
|
||||||
|
expect(count).to.equal(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should add index on createdAt to libraryItems', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='index' AND name='library_items_library_id_media_type_created_at'`)
|
||||||
|
expect(count).to.equal(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('down', () => {
|
||||||
|
it('should remove title and titleIgnorePrefix from libraryItems', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
await down({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [libraryItems] = await queryInterface.sequelize.query('SELECT * FROM libraryItems')
|
||||||
|
expect(libraryItems).to.deep.equal([
|
||||||
|
{ id: 1, libraryId: 1, mediaType: 'book', mediaId: 1, createdAt: '2025-01-01 00:00:00.000 +00:00' },
|
||||||
|
{ id: 2, libraryId: 2, mediaType: 'book', mediaId: 2, createdAt: '2025-01-02 00:00:00.000 +00:00' }
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should remove title trigger from books', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
await down({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_title'`)
|
||||||
|
expect(count).to.equal(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should remove titleIgnorePrefix trigger from books', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
await down({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_title_ignore_prefix'`)
|
||||||
|
expect(count).to.equal(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should remove index on titleIgnorePrefix from libraryItems', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
await down({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='index' AND name='library_items_library_id_media_type_title_ignore_prefix'`)
|
||||||
|
expect(count).to.equal(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should remove index on title from libraryItems', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
await down({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='index' AND name='library_items_library_id_media_type_title'`)
|
||||||
|
expect(count).to.equal(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should remove index on createdAt from libraryItems', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
await down({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='index' AND name='library_items_library_id_media_type_created_at'`)
|
||||||
|
expect(count).to.equal(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
const chai = require('chai')
|
||||||
|
const sinon = require('sinon')
|
||||||
|
const { expect } = chai
|
||||||
|
|
||||||
|
const { DataTypes, Sequelize } = require('sequelize')
|
||||||
|
const Logger = require('../../../server/Logger')
|
||||||
|
|
||||||
|
const { up, down } = require('../../../server/migrations/v2.19.4-improve-podcast-queries')
|
||||||
|
|
||||||
|
describe('Migration v2.19.4-improve-podcast-queries', () => {
|
||||||
|
let sequelize
|
||||||
|
let queryInterface
|
||||||
|
let loggerInfoStub
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
|
||||||
|
queryInterface = sequelize.getQueryInterface()
|
||||||
|
loggerInfoStub = sinon.stub(Logger, 'info')
|
||||||
|
|
||||||
|
await queryInterface.createTable('libraryItems', {
|
||||||
|
id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, unique: true },
|
||||||
|
mediaId: { type: DataTypes.INTEGER, allowNull: false },
|
||||||
|
title: { type: DataTypes.STRING, allowNull: true },
|
||||||
|
titleIgnorePrefix: { type: DataTypes.STRING, allowNull: true }
|
||||||
|
})
|
||||||
|
await queryInterface.createTable('podcasts', {
|
||||||
|
id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, unique: true },
|
||||||
|
title: { type: DataTypes.STRING, allowNull: false },
|
||||||
|
titleIgnorePrefix: { type: DataTypes.STRING, allowNull: false }
|
||||||
|
})
|
||||||
|
|
||||||
|
await queryInterface.createTable('podcastEpisodes', {
|
||||||
|
id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, unique: true },
|
||||||
|
podcastId: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'podcasts', key: 'id', onDelete: 'CASCADE' } }
|
||||||
|
})
|
||||||
|
|
||||||
|
await queryInterface.createTable('mediaProgresses', {
|
||||||
|
id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, unique: true },
|
||||||
|
userId: { type: DataTypes.INTEGER, allowNull: false },
|
||||||
|
mediaItemId: { type: DataTypes.INTEGER, allowNull: false },
|
||||||
|
mediaItemType: { type: DataTypes.STRING, allowNull: false },
|
||||||
|
isFinished: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false }
|
||||||
|
})
|
||||||
|
|
||||||
|
await queryInterface.bulkInsert('libraryItems', [
|
||||||
|
{ id: 1, mediaId: 1, title: null, titleIgnorePrefix: null },
|
||||||
|
{ id: 2, mediaId: 2, title: null, titleIgnorePrefix: null }
|
||||||
|
])
|
||||||
|
|
||||||
|
await queryInterface.bulkInsert('podcasts', [
|
||||||
|
{ id: 1, title: 'The Podcast 1', titleIgnorePrefix: 'Podcast 1, The' },
|
||||||
|
{ id: 2, title: 'The Podcast 2', titleIgnorePrefix: 'Podcast 2, The' }
|
||||||
|
])
|
||||||
|
|
||||||
|
await queryInterface.bulkInsert('podcastEpisodes', [
|
||||||
|
{ id: 1, podcastId: 1 },
|
||||||
|
{ id: 2, podcastId: 1 },
|
||||||
|
{ id: 3, podcastId: 2 }
|
||||||
|
])
|
||||||
|
|
||||||
|
await queryInterface.bulkInsert('mediaProgresses', [
|
||||||
|
{ id: 1, userId: 1, mediaItemId: 1, mediaItemType: 'podcastEpisode', isFinished: 1 },
|
||||||
|
{ id: 2, userId: 1, mediaItemId: 2, mediaItemType: 'podcastEpisode', isFinished: 0 },
|
||||||
|
{ id: 3, userId: 1, mediaItemId: 3, mediaItemType: 'podcastEpisode', isFinished: 1 },
|
||||||
|
{ id: 4, userId: 2, mediaItemId: 1, mediaItemType: 'podcastEpisode', isFinished: 0 },
|
||||||
|
{ id: 5, userId: 2, mediaItemId: 2, mediaItemType: 'podcastEpisode', isFinished: 1 },
|
||||||
|
{ id: 6, userId: 2, mediaItemId: 3, mediaItemType: 'podcastEpisode', isFinished: 0 },
|
||||||
|
{ id: 7, userId: 1, mediaItemId: 1, mediaItemType: 'book', isFinished: 1 },
|
||||||
|
{ id: 8, userId: 1, mediaItemId: 2, mediaItemType: 'book', isFinished: 0 }
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
sinon.restore()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('up', () => {
|
||||||
|
it('should add numEpisodes column to podcasts', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [podcasts] = await queryInterface.sequelize.query('SELECT * FROM podcasts')
|
||||||
|
expect(podcasts).to.deep.equal([
|
||||||
|
{ id: 1, numEpisodes: 2, title: 'The Podcast 1', titleIgnorePrefix: 'Podcast 1, The' },
|
||||||
|
{ id: 2, numEpisodes: 1, title: 'The Podcast 2', titleIgnorePrefix: 'Podcast 2, The' }
|
||||||
|
])
|
||||||
|
|
||||||
|
// Make sure podcastEpisodes are not affected due to ON DELETE CASCADE
|
||||||
|
const [podcastEpisodes] = await queryInterface.sequelize.query('SELECT * FROM podcastEpisodes')
|
||||||
|
expect(podcastEpisodes).to.deep.equal([
|
||||||
|
{ id: 1, podcastId: 1 },
|
||||||
|
{ id: 2, podcastId: 1 },
|
||||||
|
{ id: 3, podcastId: 2 }
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should add podcastId column to mediaProgresses', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [mediaProgresses] = await queryInterface.sequelize.query('SELECT * FROM mediaProgresses')
|
||||||
|
expect(mediaProgresses).to.deep.equal([
|
||||||
|
{ id: 1, userId: 1, mediaItemId: 1, mediaItemType: 'podcastEpisode', podcastId: 1, isFinished: 1 },
|
||||||
|
{ id: 2, userId: 1, mediaItemId: 2, mediaItemType: 'podcastEpisode', podcastId: 1, isFinished: 0 },
|
||||||
|
{ id: 3, userId: 1, mediaItemId: 3, mediaItemType: 'podcastEpisode', podcastId: 2, isFinished: 1 },
|
||||||
|
{ id: 4, userId: 2, mediaItemId: 1, mediaItemType: 'podcastEpisode', podcastId: 1, isFinished: 0 },
|
||||||
|
{ id: 5, userId: 2, mediaItemId: 2, mediaItemType: 'podcastEpisode', podcastId: 1, isFinished: 1 },
|
||||||
|
{ id: 6, userId: 2, mediaItemId: 3, mediaItemType: 'podcastEpisode', podcastId: 2, isFinished: 0 },
|
||||||
|
{ id: 7, userId: 1, mediaItemId: 1, mediaItemType: 'book', podcastId: null, isFinished: 1 },
|
||||||
|
{ id: 8, userId: 1, mediaItemId: 2, mediaItemType: 'book', podcastId: null, isFinished: 0 }
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should copy title and titleIgnorePrefix from podcasts to libraryItems', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [libraryItems] = await queryInterface.sequelize.query('SELECT * FROM libraryItems')
|
||||||
|
expect(libraryItems).to.deep.equal([
|
||||||
|
{ id: 1, mediaId: 1, title: 'The Podcast 1', titleIgnorePrefix: 'Podcast 1, The' },
|
||||||
|
{ id: 2, mediaId: 2, title: 'The Podcast 2', titleIgnorePrefix: 'Podcast 2, The' }
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should add trigger to update title in libraryItems', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_title_from_podcasts_title'`)
|
||||||
|
expect(count).to.equal(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should add trigger to update titleIgnorePrefix in libraryItems', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_title_ignore_prefix_from_podcasts_title_ignore_prefix'`)
|
||||||
|
expect(count).to.equal(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should be idempotent', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [podcasts] = await queryInterface.sequelize.query('SELECT * FROM podcasts')
|
||||||
|
expect(podcasts).to.deep.equal([
|
||||||
|
{ id: 1, numEpisodes: 2, title: 'The Podcast 1', titleIgnorePrefix: 'Podcast 1, The' },
|
||||||
|
{ id: 2, numEpisodes: 1, title: 'The Podcast 2', titleIgnorePrefix: 'Podcast 2, The' }
|
||||||
|
])
|
||||||
|
|
||||||
|
const [mediaProgresses] = await queryInterface.sequelize.query('SELECT * FROM mediaProgresses')
|
||||||
|
expect(mediaProgresses).to.deep.equal([
|
||||||
|
{ id: 1, userId: 1, mediaItemId: 1, mediaItemType: 'podcastEpisode', podcastId: 1, isFinished: 1 },
|
||||||
|
{ id: 2, userId: 1, mediaItemId: 2, mediaItemType: 'podcastEpisode', podcastId: 1, isFinished: 0 },
|
||||||
|
{ id: 3, userId: 1, mediaItemId: 3, mediaItemType: 'podcastEpisode', podcastId: 2, isFinished: 1 },
|
||||||
|
{ id: 4, userId: 2, mediaItemId: 1, mediaItemType: 'podcastEpisode', podcastId: 1, isFinished: 0 },
|
||||||
|
{ id: 5, userId: 2, mediaItemId: 2, mediaItemType: 'podcastEpisode', podcastId: 1, isFinished: 1 },
|
||||||
|
{ id: 6, userId: 2, mediaItemId: 3, mediaItemType: 'podcastEpisode', podcastId: 2, isFinished: 0 },
|
||||||
|
{ id: 7, userId: 1, mediaItemId: 1, mediaItemType: 'book', podcastId: null, isFinished: 1 },
|
||||||
|
{ id: 8, userId: 1, mediaItemId: 2, mediaItemType: 'book', podcastId: null, isFinished: 0 }
|
||||||
|
])
|
||||||
|
|
||||||
|
const [libraryItems] = await queryInterface.sequelize.query('SELECT * FROM libraryItems')
|
||||||
|
expect(libraryItems).to.deep.equal([
|
||||||
|
{ id: 1, mediaId: 1, title: 'The Podcast 1', titleIgnorePrefix: 'Podcast 1, The' },
|
||||||
|
{ id: 2, mediaId: 2, title: 'The Podcast 2', titleIgnorePrefix: 'Podcast 2, The' }
|
||||||
|
])
|
||||||
|
|
||||||
|
const [[{ count: count1 }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_title_from_podcasts_title'`)
|
||||||
|
expect(count1).to.equal(1)
|
||||||
|
|
||||||
|
const [[{ count: count2 }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_title_ignore_prefix_from_podcasts_title_ignore_prefix'`)
|
||||||
|
expect(count2).to.equal(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('down', () => {
|
||||||
|
it('should remove numEpisodes column from podcasts', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
try {
|
||||||
|
await down({ context: { queryInterface, logger: Logger } })
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
const [podcasts] = await queryInterface.sequelize.query('SELECT * FROM podcasts')
|
||||||
|
expect(podcasts).to.deep.equal([
|
||||||
|
{ id: 1, title: 'The Podcast 1', titleIgnorePrefix: 'Podcast 1, The' },
|
||||||
|
{ id: 2, title: 'The Podcast 2', titleIgnorePrefix: 'Podcast 2, The' }
|
||||||
|
])
|
||||||
|
|
||||||
|
// Make sure podcastEpisodes are not affected due to ON DELETE CASCADE
|
||||||
|
const [podcastEpisodes] = await queryInterface.sequelize.query('SELECT * FROM podcastEpisodes')
|
||||||
|
expect(podcastEpisodes).to.deep.equal([
|
||||||
|
{ id: 1, podcastId: 1 },
|
||||||
|
{ id: 2, podcastId: 1 },
|
||||||
|
{ id: 3, podcastId: 2 }
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should remove podcastId column from mediaProgresses', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
await down({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [mediaProgresses] = await queryInterface.sequelize.query('SELECT * FROM mediaProgresses')
|
||||||
|
expect(mediaProgresses).to.deep.equal([
|
||||||
|
{ id: 1, userId: 1, mediaItemId: 1, mediaItemType: 'podcastEpisode', isFinished: 1 },
|
||||||
|
{ id: 2, userId: 1, mediaItemId: 2, mediaItemType: 'podcastEpisode', isFinished: 0 },
|
||||||
|
{ id: 3, userId: 1, mediaItemId: 3, mediaItemType: 'podcastEpisode', isFinished: 1 },
|
||||||
|
{ id: 4, userId: 2, mediaItemId: 1, mediaItemType: 'podcastEpisode', isFinished: 0 },
|
||||||
|
{ id: 5, userId: 2, mediaItemId: 2, mediaItemType: 'podcastEpisode', isFinished: 1 },
|
||||||
|
{ id: 6, userId: 2, mediaItemId: 3, mediaItemType: 'podcastEpisode', isFinished: 0 },
|
||||||
|
{ id: 7, userId: 1, mediaItemId: 1, mediaItemType: 'book', isFinished: 1 },
|
||||||
|
{ id: 8, userId: 1, mediaItemId: 2, mediaItemType: 'book', isFinished: 0 }
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should remove trigger to update title in libraryItems', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
await down({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_title_from_podcasts_title'`)
|
||||||
|
expect(count).to.equal(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should remove trigger to update titleIgnorePrefix in libraryItems', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
await down({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [[{ count }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_title_ignore_prefix_from_podcasts_title_ignore_prefix'`)
|
||||||
|
expect(count).to.equal(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should be idempotent', async () => {
|
||||||
|
await up({ context: { queryInterface, logger: Logger } })
|
||||||
|
await down({ context: { queryInterface, logger: Logger } })
|
||||||
|
await down({ context: { queryInterface, logger: Logger } })
|
||||||
|
|
||||||
|
const [podcasts] = await queryInterface.sequelize.query('SELECT * FROM podcasts')
|
||||||
|
expect(podcasts).to.deep.equal([
|
||||||
|
{ id: 1, title: 'The Podcast 1', titleIgnorePrefix: 'Podcast 1, The' },
|
||||||
|
{ id: 2, title: 'The Podcast 2', titleIgnorePrefix: 'Podcast 2, The' }
|
||||||
|
])
|
||||||
|
|
||||||
|
const [mediaProgresses] = await queryInterface.sequelize.query('SELECT * FROM mediaProgresses')
|
||||||
|
expect(mediaProgresses).to.deep.equal([
|
||||||
|
{ id: 1, userId: 1, mediaItemId: 1, mediaItemType: 'podcastEpisode', isFinished: 1 },
|
||||||
|
{ id: 2, userId: 1, mediaItemId: 2, mediaItemType: 'podcastEpisode', isFinished: 0 },
|
||||||
|
{ id: 3, userId: 1, mediaItemId: 3, mediaItemType: 'podcastEpisode', isFinished: 1 },
|
||||||
|
{ id: 4, userId: 2, mediaItemId: 1, mediaItemType: 'podcastEpisode', isFinished: 0 },
|
||||||
|
{ id: 5, userId: 2, mediaItemId: 2, mediaItemType: 'podcastEpisode', isFinished: 1 },
|
||||||
|
{ id: 6, userId: 2, mediaItemId: 3, mediaItemType: 'podcastEpisode', isFinished: 0 },
|
||||||
|
{ id: 7, userId: 1, mediaItemId: 1, mediaItemType: 'book', isFinished: 1 },
|
||||||
|
{ id: 8, userId: 1, mediaItemId: 2, mediaItemType: 'book', isFinished: 0 }
|
||||||
|
])
|
||||||
|
|
||||||
|
const [libraryItems] = await queryInterface.sequelize.query('SELECT * FROM libraryItems')
|
||||||
|
expect(libraryItems).to.deep.equal([
|
||||||
|
{ id: 1, mediaId: 1, title: 'The Podcast 1', titleIgnorePrefix: 'Podcast 1, The' },
|
||||||
|
{ id: 2, mediaId: 2, title: 'The Podcast 2', titleIgnorePrefix: 'Podcast 2, The' }
|
||||||
|
])
|
||||||
|
|
||||||
|
const [[{ count: count1 }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_title_from_podcasts_title'`)
|
||||||
|
expect(count1).to.equal(0)
|
||||||
|
|
||||||
|
const [[{ count: count2 }]] = await queryInterface.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='update_library_items_title_ignore_prefix_from_podcasts_title_ignore_prefix'`)
|
||||||
|
expect(count2).to.equal(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
const chai = require('chai')
|
||||||
|
const expect = chai.expect
|
||||||
|
const sinon = require('sinon')
|
||||||
|
const fileUtils = require('../../../server/utils/fileUtils')
|
||||||
|
const fs = require('fs')
|
||||||
|
const Logger = require('../../../server/Logger')
|
||||||
|
|
||||||
|
describe('fileUtils', () => {
|
||||||
|
it('shouldIgnoreFile', () => {
|
||||||
|
global.isWin = process.platform === 'win32'
|
||||||
|
|
||||||
|
const testCases = [
|
||||||
|
{ path: 'test.txt', expected: null },
|
||||||
|
{ path: 'folder/test.mp3', expected: null },
|
||||||
|
{ path: 'normal/path/file.m4b', expected: null },
|
||||||
|
{ path: 'test.txt.part', expected: '.part file' },
|
||||||
|
{ path: 'test.txt.tmp', expected: '.tmp file' },
|
||||||
|
{ path: 'test.txt.crdownload', expected: '.crdownload file' },
|
||||||
|
{ path: 'test.txt.download', expected: '.download file' },
|
||||||
|
{ path: 'test.txt.bak', expected: '.bak file' },
|
||||||
|
{ path: 'test.txt.old', expected: '.old file' },
|
||||||
|
{ path: 'test.txt.temp', expected: '.temp file' },
|
||||||
|
{ path: 'test.txt.tempfile', expected: '.tempfile file' },
|
||||||
|
{ path: 'test.txt.tempfile~', expected: '.tempfile~ file' },
|
||||||
|
{ path: '.gitignore', expected: 'dotfile' },
|
||||||
|
{ path: 'folder/.hidden', expected: 'dotfile' },
|
||||||
|
{ path: '.git/config', expected: 'dotpath' },
|
||||||
|
{ path: 'path/.hidden/file.txt', expected: 'dotpath' },
|
||||||
|
{ path: '@eaDir', expected: '@eaDir directory' },
|
||||||
|
{ path: 'folder/@eaDir', expected: '@eaDir directory' },
|
||||||
|
{ path: 'path/@eaDir/file.txt', expected: '@eaDir directory' },
|
||||||
|
{ path: '.hidden/test.tmp', expected: 'dotpath' },
|
||||||
|
{ path: '@eaDir/test.part', expected: '@eaDir directory' }
|
||||||
|
]
|
||||||
|
|
||||||
|
testCases.forEach(({ path, expected }) => {
|
||||||
|
const result = fileUtils.shouldIgnoreFile(path)
|
||||||
|
expect(result).to.equal(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('recurseFiles', () => {
|
||||||
|
let readdirStub, realpathStub, statStub
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
global.isWin = process.platform === 'win32'
|
||||||
|
|
||||||
|
// Mock file structure with normalized paths
|
||||||
|
const mockDirContents = new Map([
|
||||||
|
['/test', ['file1.mp3', 'subfolder', 'ignoreme', 'temp.mp3.tmp']],
|
||||||
|
['/test/subfolder', ['file2.m4b']],
|
||||||
|
['/test/ignoreme', ['.ignore', 'ignored.mp3']]
|
||||||
|
])
|
||||||
|
|
||||||
|
const mockStats = new Map([
|
||||||
|
['/test/file1.mp3', { isDirectory: () => false, size: 1024, mtimeMs: Date.now(), ino: '1' }],
|
||||||
|
['/test/subfolder', { isDirectory: () => true, size: 0, mtimeMs: Date.now(), ino: '2' }],
|
||||||
|
['/test/subfolder/file2.m4b', { isDirectory: () => false, size: 1024, mtimeMs: Date.now(), ino: '3' }],
|
||||||
|
['/test/ignoreme', { isDirectory: () => true, size: 0, mtimeMs: Date.now(), ino: '4' }],
|
||||||
|
['/test/ignoreme/.ignore', { isDirectory: () => false, size: 0, mtimeMs: Date.now(), ino: '5' }],
|
||||||
|
['/test/ignoreme/ignored.mp3', { isDirectory: () => false, size: 1024, mtimeMs: Date.now(), ino: '6' }],
|
||||||
|
['/test/temp.mp3.tmp', { isDirectory: () => false, size: 1024, mtimeMs: Date.now(), ino: '7' }]
|
||||||
|
])
|
||||||
|
|
||||||
|
// Stub fs.readdir
|
||||||
|
readdirStub = sinon.stub(fs, 'readdir')
|
||||||
|
readdirStub.callsFake((path, callback) => {
|
||||||
|
const contents = mockDirContents.get(path)
|
||||||
|
if (contents) {
|
||||||
|
callback(null, contents)
|
||||||
|
} else {
|
||||||
|
callback(new Error(`ENOENT: no such file or directory, scandir '${path}'`))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Stub fs.realpath
|
||||||
|
realpathStub = sinon.stub(fs, 'realpath')
|
||||||
|
realpathStub.callsFake((path, callback) => {
|
||||||
|
// Return normalized path
|
||||||
|
callback(null, fileUtils.filePathToPOSIX(path).replace(/\/$/, ''))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Stub fs.stat
|
||||||
|
statStub = sinon.stub(fs, 'stat')
|
||||||
|
statStub.callsFake((path, callback) => {
|
||||||
|
const normalizedPath = fileUtils.filePathToPOSIX(path).replace(/\/$/, '')
|
||||||
|
const stats = mockStats.get(normalizedPath)
|
||||||
|
if (stats) {
|
||||||
|
callback(null, stats)
|
||||||
|
} else {
|
||||||
|
callback(new Error(`ENOENT: no such file or directory, stat '${normalizedPath}'`))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Stub Logger
|
||||||
|
sinon.stub(Logger, 'debug')
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
sinon.restore()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return filtered file list', async () => {
|
||||||
|
const files = await fileUtils.recurseFiles('/test')
|
||||||
|
expect(files).to.be.an('array')
|
||||||
|
expect(files).to.have.lengthOf(2)
|
||||||
|
|
||||||
|
expect(files[0]).to.deep.equal({
|
||||||
|
name: 'file1.mp3',
|
||||||
|
path: 'file1.mp3',
|
||||||
|
reldirpath: '',
|
||||||
|
fullpath: '/test/file1.mp3',
|
||||||
|
extension: '.mp3',
|
||||||
|
deep: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(files[1]).to.deep.equal({
|
||||||
|
name: 'file2.m4b',
|
||||||
|
path: 'subfolder/file2.m4b',
|
||||||
|
reldirpath: 'subfolder',
|
||||||
|
fullpath: '/test/subfolder/file2.m4b',
|
||||||
|
extension: '.m4b',
|
||||||
|
deep: 1
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
const chai = require('chai')
|
||||||
|
const expect = chai.expect
|
||||||
|
const { parse, nameToLastFirst } = require('../../../../server/utils/parsers/parseNameString')
|
||||||
|
|
||||||
|
describe('parseNameString', () => {
|
||||||
|
describe('parse', () => {
|
||||||
|
it('returns null if nameString is empty', () => {
|
||||||
|
const result = parse('')
|
||||||
|
expect(result).to.be.null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('parses single name in First Last format', () => {
|
||||||
|
const result = parse('John Smith')
|
||||||
|
expect(result.names).to.deep.equal(['John Smith'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('parses single name in Last, First format', () => {
|
||||||
|
const result = parse('Smith, John')
|
||||||
|
expect(result.names).to.deep.equal(['John Smith'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('parses multiple names separated by &', () => {
|
||||||
|
const result = parse('John Smith & Jane Doe')
|
||||||
|
expect(result.names).to.deep.equal(['John Smith', 'Jane Doe'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('parses multiple names separated by "and"', () => {
|
||||||
|
const result = parse('John Smith and Jane Doe')
|
||||||
|
expect(result.names).to.deep.equal(['John Smith', 'Jane Doe'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('parses multiple names separated by comma and "and"', () => {
|
||||||
|
const result = parse('John Smith, Jane Doe and John Doe')
|
||||||
|
expect(result.names).to.deep.equal(['John Smith', 'Jane Doe', 'John Doe'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('parses multiple names separated by semicolon', () => {
|
||||||
|
const result = parse('John Smith; Jane Doe')
|
||||||
|
expect(result.names).to.deep.equal(['John Smith', 'Jane Doe'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('parses multiple names in Last, First format', () => {
|
||||||
|
const result = parse('Smith, John, Doe, Jane')
|
||||||
|
expect(result.names).to.deep.equal(['John Smith', 'Jane Doe'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('parses multiple names with single word name', () => {
|
||||||
|
const result = parse('John Smith, Jones, James Doe, Ludwig von Mises')
|
||||||
|
expect(result.names).to.deep.equal(['John Smith', 'Jones', 'James Doe', 'Ludwig von Mises'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('parses multiple names with single word name listed first (semicolon separator)', () => {
|
||||||
|
const result = parse('Jones; John Smith; James Doe; Ludwig von Mises')
|
||||||
|
expect(result.names).to.deep.equal(['Jones', 'John Smith', 'James Doe', 'Ludwig von Mises'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles names with suffixes', () => {
|
||||||
|
const result = parse('Smith, John Jr.')
|
||||||
|
expect(result.names).to.deep.equal(['John Jr. Smith'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles compound last names', () => {
|
||||||
|
const result = parse('von Mises, Ludwig')
|
||||||
|
expect(result.names).to.deep.equal(['Ludwig von Mises'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles Chinese/Japanese/Korean names', () => {
|
||||||
|
const result = parse('张三, 李四')
|
||||||
|
expect(result.names).to.deep.equal(['张三', '李四'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('removes duplicate names', () => {
|
||||||
|
const result = parse('John Smith & John Smith')
|
||||||
|
expect(result.names).to.deep.equal(['John Smith'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('filters out empty names', () => {
|
||||||
|
const result = parse('John Smith,')
|
||||||
|
expect(result.names).to.deep.equal(['John Smith'])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('nameToLastFirst', () => {
|
||||||
|
it('converts First Last to Last, First format', () => {
|
||||||
|
const result = nameToLastFirst('John Smith')
|
||||||
|
expect(result).to.equal('Smith, John')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns last name only when no first name', () => {
|
||||||
|
const result = nameToLastFirst('Smith')
|
||||||
|
expect(result).to.equal('Smith')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles names with middle names', () => {
|
||||||
|
const result = nameToLastFirst('John Middle Smith')
|
||||||
|
expect(result).to.equal('Smith, John Middle')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
const { expect } = require('chai')
|
||||||
|
const stringifySequelizeQuery = require('../../../server/utils/stringifySequelizeQuery')
|
||||||
|
const Sequelize = require('sequelize')
|
||||||
|
|
||||||
|
class DummyClass {}
|
||||||
|
|
||||||
|
describe('stringifySequelizeQuery', () => {
|
||||||
|
it('should stringify a sequelize query containing an op', () => {
|
||||||
|
const query = {
|
||||||
|
where: {
|
||||||
|
name: 'John',
|
||||||
|
age: {
|
||||||
|
[Sequelize.Op.gt]: 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = stringifySequelizeQuery(query)
|
||||||
|
expect(result).to.equal('{"where":{"name":"John","age":{"Symbol(gt)":20}}}')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should stringify a sequelize query containing a literal', () => {
|
||||||
|
const query = {
|
||||||
|
order: [[Sequelize.literal('libraryItem.title'), 'ASC']]
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = stringifySequelizeQuery(query)
|
||||||
|
expect(result).to.equal('{"order":{"0":{"0":{"val":"libraryItem.title"},"1":"ASC"}}}')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should stringify a sequelize query containing a class', () => {
|
||||||
|
const query = {
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: DummyClass
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = stringifySequelizeQuery(query)
|
||||||
|
expect(result).to.equal('{"include":{"0":{"model":"DummyClass"}}}')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should ignore non-class functions', () => {
|
||||||
|
const query = {
|
||||||
|
logging: (query) => console.log(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = stringifySequelizeQuery(query)
|
||||||
|
expect(result).to.equal('{}')
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user