mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-01 16:30:39 +02:00
Compare commits
113 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d80752cc9d | |||
| b764e848c7 | |||
| b037c4e8a3 | |||
| 6ba2360790 | |||
| ca4eb507f0 | |||
| 965b094470 | |||
| 0fe313ecfd | |||
| 35a2f8d44f | |||
| 50797879d5 | |||
| 9327331ee9 | |||
| 1c15007e32 | |||
| 2151ffa114 | |||
| 49ed208a54 | |||
| d668462529 | |||
| f2102a0a23 | |||
| 5efc6b82c1 | |||
| 1e4e9768da | |||
| cc5109c305 | |||
| e858d6a1d5 | |||
| b4cd5d2862 | |||
| 0633a44cfb | |||
| 5748126b83 | |||
| 06375743a3 | |||
| 2a41c186aa | |||
| af51b7254c | |||
| f63dfd769f | |||
| a1512f3174 | |||
| 245751e2ce | |||
| 37001d9425 | |||
| 9d1f51c6ba | |||
| cb234fe1fc | |||
| cb85e0255b | |||
| 61b4cfdab7 | |||
| d2c405c126 | |||
| cbca560f92 | |||
| 2d7b63b4cf | |||
| 217038b085 | |||
| 13dd4edd6a | |||
| a7288b4fbf | |||
| 3020e8104e | |||
| 8fdeeaaf38 | |||
| 42616b59de | |||
| bf16681bea | |||
| 027190b5a4 | |||
| 241c02be30 | |||
| dd87268848 | |||
| f2ac24e623 | |||
| 80e0cac474 | |||
| 37273dd51c | |||
| 926a85fff0 | |||
| 70273ba2ba | |||
| 158cdeed57 | |||
| ba9595a1be | |||
| 347e3ff674 | |||
| 2b6fb46cdb | |||
| 465775bd55 | |||
| 44e82fc454 | |||
| c4963d0de8 | |||
| ff81d70cb1 | |||
| d7a543e143 | |||
| cba547083d | |||
| 47b1d2a2c2 | |||
| abc378954c | |||
| fdf871af17 | |||
| 83fcb0efdc | |||
| 0c43f3d15a | |||
| 88e087d50f | |||
| a9fb6eb8bc | |||
| 08acfdcd24 | |||
| 576eb9106f | |||
| ddd2c0ae4e | |||
| e58d7db03b | |||
| 1cac42aec5 | |||
| f94449a659 | |||
| df6afc957f | |||
| 99ffd3050c | |||
| 69dd82d329 | |||
| 076f71d490 | |||
| 33eae1e03a | |||
| 8a20510cde | |||
| c33b470fca | |||
| 29db5f1990 | |||
| f98f78a5bd | |||
| d258b42e01 | |||
| a6da32430f | |||
| cfae607310 | |||
| 7653e72e88 | |||
| f38b6636e3 | |||
| e42db121ea | |||
| 0adceaa3f0 | |||
| e6db1495ab | |||
| e6e494a92c | |||
| 549f95b259 | |||
| d92626071e | |||
| a7ac82b023 | |||
| 64b78b5822 | |||
| 8ba17db877 | |||
| 6820d9ae4e | |||
| 0bdc2fb05e | |||
| 5154e31c1c | |||
| c67b5e950e | |||
| 8a7b5cc87d | |||
| 66b290577c | |||
| 8b95dd65d9 | |||
| 691ed88096 | |||
| 836d772cd4 | |||
| 999ada03d1 | |||
| fa451f362b | |||
| 868659a2f1 | |||
| 8ae62da138 | |||
| bedba39af9 | |||
| c163f84aec | |||
| 2711b989e1 |
+2
-3
@@ -16,7 +16,6 @@ RUN apk update && \
|
||||
tzdata \
|
||||
ffmpeg \
|
||||
make \
|
||||
gcompat \
|
||||
python3 \
|
||||
g++ \
|
||||
tini \
|
||||
@@ -33,9 +32,9 @@ ENV NUSQLITE3_PATH="${NUSQLITE3_DIR}/libnusqlite3.so"
|
||||
|
||||
RUN case "$TARGETPLATFORM" in \
|
||||
"linux/amd64") \
|
||||
curl -L -o /tmp/library.zip "https://github.com/mikiher/nunicode-sqlite/releases/download/v1.1/libnusqlite3-linux-x64.zip" ;; \
|
||||
curl -L -o /tmp/library.zip "https://github.com/mikiher/nunicode-sqlite/releases/download/v1.2/libnusqlite3-linux-musl-x64.zip" ;; \
|
||||
"linux/arm64") \
|
||||
curl -L -o /tmp/library.zip "https://github.com/mikiher/nunicode-sqlite/releases/download/v1.1/libnusqlite3-linux-arm64.zip" ;; \
|
||||
curl -L -o /tmp/library.zip "https://github.com/mikiher/nunicode-sqlite/releases/download/v1.2/libnusqlite3-linux-musl-arm64.zip" ;; \
|
||||
*) echo "Unsupported platform: $TARGETPLATFORM" && exit 1 ;; \
|
||||
esac && \
|
||||
unzip /tmp/library.zip -d $NUSQLITE3_DIR && \
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</div>
|
||||
<div v-if="shelf.type === 'authors'" class="flex items-center">
|
||||
<template v-for="entity in shelf.entities">
|
||||
<cards-author-card :key="entity.id" :author="entity" @hook:updated="updatedBookCard" class="mx-2e" @edit="editAuthor" />
|
||||
<cards-author-card :key="entity.id" :authorMount="entity" @hook:updated="updatedBookCard" class="mx-2e" @edit="editAuthor" />
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="shelf.type === 'narrators'" class="flex items-center">
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<p v-if="isCollectionsPage" class="text-sm">{{ $strings.ButtonCollections }}</p>
|
||||
<span v-else class="material-symbols text-lg"></span>
|
||||
</nuxt-link>
|
||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/authors`" class="flex-grow h-full flex justify-center items-center" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/authors`" class="flex-grow h-full flex justify-center items-center" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||
<p v-if="isAuthorsPage" class="text-sm">{{ $strings.ButtonAuthors }}</p>
|
||||
<svg v-else class="w-5 h-5" viewBox="0 0 24 24">
|
||||
<path
|
||||
@@ -62,7 +62,7 @@
|
||||
<ui-context-menu-dropdown v-if="!isBatchSelecting && seriesContextMenuItems.length" :items="seriesContextMenuItems" class="mx-px" @action="seriesContextMenuAction" />
|
||||
</template>
|
||||
<!-- library & collections page -->
|
||||
<template v-else-if="page !== 'search' && page !== 'podcast-search' && page !== 'recent-episodes' && !isHome">
|
||||
<template v-else-if="page !== 'search' && page !== 'podcast-search' && page !== 'recent-episodes' && !isHome && !isAuthorsPage">
|
||||
<p class="hidden md:block">{{ $formatNumber(numShowing) }} {{ entityName }}</p>
|
||||
|
||||
<div class="flex-grow hidden sm:inline-block" />
|
||||
@@ -92,12 +92,14 @@
|
||||
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" :menu-width="110" class="ml-2" @action="contextMenuAction" />
|
||||
</template>
|
||||
<!-- authors page -->
|
||||
<template v-else-if="page === 'authors'">
|
||||
<div class="flex-grow" />
|
||||
<ui-btn v-if="userCanUpdate && authors?.length && !isBatchSelecting" :loading="processingAuthors" color="primary" small @click="matchAllAuthors">{{ $strings.ButtonMatchAllAuthors }}</ui-btn>
|
||||
<template v-else-if="isAuthorsPage">
|
||||
<p class="hidden md:block">{{ $formatNumber(numShowing) }} {{ entityName }}</p>
|
||||
|
||||
<div class="flex-grow hidden sm:inline-block" />
|
||||
<ui-btn v-if="userCanUpdate && !isBatchSelecting" :loading="processingAuthors" color="primary" small @click="matchAllAuthors">{{ $strings.ButtonMatchAllAuthors }}</ui-btn>
|
||||
|
||||
<!-- author sort select -->
|
||||
<controls-sort-select v-if="authors?.length" v-model="settings.authorSortBy" :descending.sync="settings.authorSortDesc" :items="authorSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateAuthorSort" />
|
||||
<controls-sort-select v-model="settings.authorSortBy" :descending.sync="settings.authorSortDesc" :items="authorSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateAuthorSort" />
|
||||
</template>
|
||||
<!-- home page -->
|
||||
<template v-else-if="isHome">
|
||||
@@ -117,11 +119,7 @@ export default {
|
||||
type: Object,
|
||||
default: () => null
|
||||
},
|
||||
searchQuery: String,
|
||||
authors: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
searchQuery: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -268,7 +266,7 @@ export default {
|
||||
return this.$route.name === 'library-library-podcast-latest'
|
||||
},
|
||||
isAuthorsPage() {
|
||||
return this.$route.name === 'library-library-authors'
|
||||
return this.page === 'authors'
|
||||
},
|
||||
isAlbumsPage() {
|
||||
return this.page === 'albums'
|
||||
@@ -284,6 +282,7 @@ export default {
|
||||
if (this.isSeriesPage) return this.$strings.LabelSeries
|
||||
if (this.isCollectionsPage) return this.$strings.LabelCollections
|
||||
if (this.isPlaylistsPage) return this.$strings.LabelPlaylists
|
||||
if (this.isAuthorsPage) return this.$strings.LabelAuthors
|
||||
return ''
|
||||
},
|
||||
seriesId() {
|
||||
@@ -479,36 +478,48 @@ export default {
|
||||
this.processingSeries = false
|
||||
})
|
||||
},
|
||||
async fetchAllAuthors() {
|
||||
// fetch all authors from the server, in the order that they are currently displayed
|
||||
const response = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/authors?sort=${this.settings.authorSortBy}&desc=${this.settings.authorSortDesc}`)
|
||||
return response.authors
|
||||
},
|
||||
async matchAllAuthors() {
|
||||
this.processingAuthors = true
|
||||
|
||||
for (const author of this.authors) {
|
||||
const payload = {}
|
||||
if (author.asin) payload.asin = author.asin
|
||||
else payload.q = author.name
|
||||
try {
|
||||
const authors = await this.fetchAllAuthors()
|
||||
|
||||
payload.region = 'us'
|
||||
if (this.libraryProvider.startsWith('audible.')) {
|
||||
payload.region = this.libraryProvider.split('.').pop() || 'us'
|
||||
for (const author of authors) {
|
||||
const payload = {}
|
||||
if (author.asin) payload.asin = author.asin
|
||||
else payload.q = author.name
|
||||
|
||||
payload.region = 'us'
|
||||
if (this.libraryProvider.startsWith('audible.')) {
|
||||
payload.region = this.libraryProvider.split('.').pop() || 'us'
|
||||
}
|
||||
|
||||
this.$eventBus.$emit(`searching-author-${author.id}`, true)
|
||||
|
||||
var response = await this.$axios.$post(`/api/authors/${author.id}/match`, payload).catch((error) => {
|
||||
console.error('Failed', error)
|
||||
return null
|
||||
})
|
||||
if (!response) {
|
||||
console.error(`Author ${author.name} not found`)
|
||||
this.$toast.error(this.$getString('ToastAuthorNotFound', [author.name]))
|
||||
} else if (response.updated) {
|
||||
if (response.author.imagePath) console.log(`Author ${response.author.name} was updated`)
|
||||
else console.log(`Author ${response.author.name} was updated (no image found)`)
|
||||
} else {
|
||||
console.log(`No updates were made for Author ${response.author.name}`)
|
||||
}
|
||||
|
||||
this.$eventBus.$emit(`searching-author-${author.id}`, false)
|
||||
}
|
||||
|
||||
this.$eventBus.$emit(`searching-author-${author.id}`, true)
|
||||
|
||||
var response = await this.$axios.$post(`/api/authors/${author.id}/match`, payload).catch((error) => {
|
||||
console.error('Failed', error)
|
||||
return null
|
||||
})
|
||||
if (!response) {
|
||||
console.error(`Author ${author.name} not found`)
|
||||
this.$toast.error(this.$getString('ToastAuthorNotFound', [author.name]))
|
||||
} else if (response.updated) {
|
||||
if (response.author.imagePath) console.log(`Author ${response.author.name} was updated`)
|
||||
else console.log(`Author ${response.author.name} was updated (no image found)`)
|
||||
} else {
|
||||
console.log(`No updates were made for Author ${response.author.name}`)
|
||||
}
|
||||
|
||||
this.$eventBus.$emit(`searching-author-${author.id}`, false)
|
||||
} catch (error) {
|
||||
console.error('Failed to match all authors', error)
|
||||
this.$toast.error(this.$strings.ToastMatchAllAuthorsFailed)
|
||||
}
|
||||
this.processingAuthors = false
|
||||
},
|
||||
|
||||
@@ -91,6 +91,7 @@ export default {
|
||||
if (this.page === 'series') return this.$strings.MessageBookshelfNoSeries
|
||||
if (this.page === 'collections') return this.$strings.MessageBookshelfNoCollections
|
||||
if (this.page === 'playlists') return this.$strings.MessageNoUserPlaylists
|
||||
if (this.page === 'authors') return this.$strings.MessageNoAuthors
|
||||
if (this.hasFilter) {
|
||||
if (this.filterName === 'Issues') return this.$strings.MessageNoIssues
|
||||
else if (this.filterName === 'Feed-open') return this.$strings.MessageBookshelfNoRSSFeeds
|
||||
@@ -111,6 +112,12 @@ export default {
|
||||
seriesFilterBy() {
|
||||
return this.$store.getters['user/getUserSetting']('seriesFilterBy')
|
||||
},
|
||||
authorSortBy() {
|
||||
return this.$store.getters['user/getUserSetting']('authorSortBy')
|
||||
},
|
||||
authorSortDesc() {
|
||||
return !!this.$store.getters['user/getUserSetting']('authorSortDesc')
|
||||
},
|
||||
orderBy() {
|
||||
return this.$store.getters['user/getUserSetting']('orderBy')
|
||||
},
|
||||
@@ -217,6 +224,8 @@ export default {
|
||||
this.$store.commit('globals/setEditCollection', entity)
|
||||
} else if (this.entityName === 'playlists') {
|
||||
this.$store.commit('globals/setEditPlaylist', entity)
|
||||
} else if (this.entityName === 'authors') {
|
||||
this.$store.commit('globals/showEditAuthorModal', entity)
|
||||
}
|
||||
},
|
||||
clearSelectedEntities() {
|
||||
@@ -457,6 +466,9 @@ export default {
|
||||
if (this.collapseBookSeries) {
|
||||
searchParams.set('collapseseries', 1)
|
||||
}
|
||||
} else if (this.page === 'authors') {
|
||||
searchParams.set('sort', this.authorSortBy)
|
||||
searchParams.set('desc', this.authorSortDesc ? 1 : 0)
|
||||
} else {
|
||||
if (this.filterBy && this.filterBy !== 'all') {
|
||||
searchParams.set('filter', this.filterBy)
|
||||
@@ -601,6 +613,34 @@ export default {
|
||||
this.executeRebuild()
|
||||
}
|
||||
},
|
||||
authorAdded(author) {
|
||||
if (this.entityName !== 'authors') return
|
||||
console.log(`[LazyBookshelf] authorAdded ${author.id}`, author)
|
||||
this.resetEntities()
|
||||
},
|
||||
authorUpdated(author) {
|
||||
if (this.entityName !== 'authors') return
|
||||
console.log(`[LazyBookshelf] authorUpdated ${author.id}`, author)
|
||||
const indexOf = this.entities.findIndex((ent) => ent && ent.id === author.id)
|
||||
if (indexOf >= 0) {
|
||||
this.entities[indexOf] = author
|
||||
if (this.entityComponentRefs[indexOf]) {
|
||||
this.entityComponentRefs[indexOf].setEntity(author)
|
||||
}
|
||||
}
|
||||
},
|
||||
authorRemoved(author) {
|
||||
if (this.entityName !== 'authors') return
|
||||
console.log(`[LazyBookshelf] authorRemoved ${author.id}`, author)
|
||||
const indexOf = this.entities.findIndex((ent) => ent && ent.id === author.id)
|
||||
if (indexOf >= 0) {
|
||||
this.entities = this.entities.filter((ent) => ent.id !== author.id)
|
||||
this.totalEntities--
|
||||
this.$eventBus.$emit('bookshelf-total-entities', this.totalEntities)
|
||||
this.executeRebuild()
|
||||
}
|
||||
},
|
||||
|
||||
shareOpen(mediaItemShare) {
|
||||
if (this.entityName === 'items' || this.entityName === 'series-books') {
|
||||
var indexOf = this.entities.findIndex((ent) => ent?.media?.id === mediaItemShare.mediaItemId)
|
||||
@@ -727,6 +767,9 @@ export default {
|
||||
this.$root.socket.on('playlist_added', this.playlistAdded)
|
||||
this.$root.socket.on('playlist_updated', this.playlistUpdated)
|
||||
this.$root.socket.on('playlist_removed', this.playlistRemoved)
|
||||
this.$root.socket.on('author_added', this.authorAdded)
|
||||
this.$root.socket.on('author_updated', this.authorUpdated)
|
||||
this.$root.socket.on('author_removed', this.authorRemoved)
|
||||
this.$root.socket.on('share_open', this.shareOpen)
|
||||
this.$root.socket.on('share_closed', this.shareClosed)
|
||||
} else {
|
||||
@@ -756,6 +799,9 @@ export default {
|
||||
this.$root.socket.off('playlist_added', this.playlistAdded)
|
||||
this.$root.socket.off('playlist_updated', this.playlistUpdated)
|
||||
this.$root.socket.off('playlist_removed', this.playlistRemoved)
|
||||
this.$root.socket.off('author_added', this.authorAdded)
|
||||
this.$root.socket.off('author_updated', this.authorUpdated)
|
||||
this.$root.socket.off('author_removed', this.authorRemoved)
|
||||
this.$root.socket.off('share_open', this.shareOpen)
|
||||
this.$root.socket.off('share_closed', this.shareClosed)
|
||||
} else {
|
||||
|
||||
@@ -167,7 +167,7 @@ export default {
|
||||
},
|
||||
podcastAuthor() {
|
||||
if (!this.isPodcast) return null
|
||||
return this.mediaMetadata.author || 'Unknown'
|
||||
return this.mediaMetadata.author || this.$strings.LabelUnknown
|
||||
},
|
||||
hasNextItemInQueue() {
|
||||
return this.currentPlayerQueueIndex < this.playerQueueItems.length - 1
|
||||
@@ -251,7 +251,7 @@ export default {
|
||||
sleepTimerEnd() {
|
||||
this.clearSleepTimer()
|
||||
this.playerHandler.pause()
|
||||
this.$toast.info('Sleep Timer Done.. zZzzZz')
|
||||
this.$toast.info(this.$strings.ToastSleepTimerDone)
|
||||
},
|
||||
cancelSleepTimer() {
|
||||
this.showSleepTimerModal = false
|
||||
@@ -525,7 +525,7 @@ export default {
|
||||
},
|
||||
showFailedProgressSyncs() {
|
||||
if (!isNaN(this.syncFailedToast)) this.$toast.dismiss(this.syncFailedToast)
|
||||
this.syncFailedToast = this.$toast('Progress is not being synced. Restart playback', { timeout: false, type: 'error' })
|
||||
this.syncFailedToast = this.$toast(this.$strings.ToastProgressIsNotBeingSynced, { timeout: false, type: 'error' })
|
||||
},
|
||||
sessionClosedEvent(sessionId) {
|
||||
if (this.playerHandler.currentSessionId === sessionId) {
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
<div v-show="isPlaylistsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/authors`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/authors`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<svg class="w-6 h-6" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
@@ -180,7 +180,7 @@ export default {
|
||||
return this.$route.name === 'library-library-series-id' || this.paramId === 'series'
|
||||
},
|
||||
isAuthorsPage() {
|
||||
return this.$route.name === 'library-library-authors'
|
||||
return this.libraryBookshelfPage && this.paramId === 'authors'
|
||||
},
|
||||
isNarratorsPage() {
|
||||
return this.$route.name === 'library-library-narrators'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div :style="{ minWidth: cardWidth + 'px', maxWidth: cardWidth + 'px' }">
|
||||
<nuxt-link :to="`/author/${author.id}`">
|
||||
<div class="pb-3e" :style="{ minWidth: cardWidth + 'px', maxWidth: cardWidth + 'px' }">
|
||||
<nuxt-link :to="`/author/${author?.id}`">
|
||||
<div cy-id="card" @mouseover="mouseover" @mouseleave="mouseleave">
|
||||
<div cy-id="imageArea" :style="{ height: cardHeight + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
|
||||
<!-- Image or placeholder -->
|
||||
@@ -40,7 +40,7 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
author: {
|
||||
authorMount: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
@@ -57,7 +57,8 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
searching: false,
|
||||
isHovering: false
|
||||
isHovering: false,
|
||||
author: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -68,34 +69,37 @@ export default {
|
||||
return this.height * this.sizeMultiplier
|
||||
},
|
||||
userToken() {
|
||||
return this.$store.getters['user/getToken']
|
||||
return this.store.getters['user/getToken']
|
||||
},
|
||||
_author() {
|
||||
return this.author || {}
|
||||
},
|
||||
authorId() {
|
||||
return this._author.id
|
||||
return this._author?.id || ''
|
||||
},
|
||||
name() {
|
||||
return this._author.name || ''
|
||||
return this._author?.name || ''
|
||||
},
|
||||
asin() {
|
||||
return this._author.asin || ''
|
||||
return this._author?.asin || ''
|
||||
},
|
||||
numBooks() {
|
||||
return this._author.numBooks || 0
|
||||
return this._author?.numBooks || 0
|
||||
},
|
||||
store() {
|
||||
return this.$store || this.$nuxt.$store
|
||||
},
|
||||
userCanUpdate() {
|
||||
return this.$store.getters['user/getUserCanUpdate']
|
||||
return this.store.getters['user/getUserCanUpdate']
|
||||
},
|
||||
currentLibraryId() {
|
||||
return this.$store.state.libraries.currentLibraryId
|
||||
return this.store.state.libraries.currentLibraryId
|
||||
},
|
||||
libraryProvider() {
|
||||
return this.$store.getters['libraries/getLibraryProvider'](this.currentLibraryId) || 'google'
|
||||
return this.store.getters['libraries/getLibraryProvider'](this.currentLibraryId) || 'google'
|
||||
},
|
||||
sizeMultiplier() {
|
||||
return this.$store.getters['user/getSizeMultiplier']
|
||||
return this.store.getters['user/getSizeMultiplier']
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -121,24 +125,54 @@ export default {
|
||||
return null
|
||||
})
|
||||
if (!response) {
|
||||
this.$toast.error(`Author ${this.name} not found`)
|
||||
this.$toast.error(this.$getString('ToastAuthorNotFound', [this.name]))
|
||||
} else if (response.updated) {
|
||||
if (response.author.imagePath) this.$toast.success(`Author ${response.author.name} was updated`)
|
||||
else this.$toast.success(`Author ${response.author.name} was updated (no image found)`)
|
||||
if (response.author.imagePath) {
|
||||
this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
|
||||
} else {
|
||||
this.$toast.success(this.$strings.ToastAuthorUpdateSuccessNoImageFound)
|
||||
}
|
||||
} else {
|
||||
this.$toast.info(`No updates were made for Author ${response.author.name}`)
|
||||
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||
}
|
||||
this.searching = false
|
||||
},
|
||||
setSearching(isSearching) {
|
||||
this.searching = isSearching
|
||||
}
|
||||
},
|
||||
setEntity(author) {
|
||||
this.removeListeners()
|
||||
this.author = author
|
||||
this.addListeners()
|
||||
},
|
||||
addListeners() {
|
||||
if (this.author) {
|
||||
this.$eventBus.$on(`searching-author-${this.authorId}`, this.setSearching)
|
||||
}
|
||||
},
|
||||
removeListeners() {
|
||||
if (this.author) {
|
||||
this.$eventBus.$off(`searching-author-${this.authorId}`, this.setSearching)
|
||||
}
|
||||
},
|
||||
destroy() {
|
||||
// destroy the vue listeners, etc
|
||||
this.$destroy()
|
||||
|
||||
// remove the element from the DOM
|
||||
if (this.$el && this.$el.parentNode) {
|
||||
this.$el.parentNode.removeChild(this.$el)
|
||||
} else if (this.$el && this.$el.remove) {
|
||||
this.$el.remove()
|
||||
}
|
||||
},
|
||||
setSelectionMode(val) {}
|
||||
},
|
||||
mounted() {
|
||||
this.$eventBus.$on(`searching-author-${this.authorId}`, this.setSearching)
|
||||
if (this.authorMount) this.setEntity(this.authorMount)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$eventBus.$off(`searching-author-${this.authorId}`, this.setSearching)
|
||||
this.removeListeners()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -325,7 +325,7 @@ export default {
|
||||
},
|
||||
displaySubtitle() {
|
||||
if (!this.libraryItem) return '\u00A0'
|
||||
if (this.collapsedSeries) return this.collapsedSeries.numBooks === 1 ? '1 book' : `${this.collapsedSeries.numBooks} books`
|
||||
if (this.collapsedSeries) return `${this.collapsedSeries.numBooks} ${this.$strings.LabelBooks}`
|
||||
if (this.mediaMetadata.subtitle) return this.mediaMetadata.subtitle
|
||||
if (this.mediaMetadata.seriesName) return this.mediaMetadata.seriesName
|
||||
return ''
|
||||
|
||||
@@ -189,6 +189,12 @@ export default {
|
||||
value: 'publishers',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelPublishedDecade,
|
||||
textPlural: this.$strings.LabelPublishedDecades,
|
||||
value: 'publishedDecades',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelLanguage,
|
||||
textPlural: this.$strings.LabelLanguages,
|
||||
@@ -338,6 +344,9 @@ export default {
|
||||
publishers() {
|
||||
return this.filterData.publishers || []
|
||||
},
|
||||
publishedDecades() {
|
||||
return this.filterData.publishedDecades || []
|
||||
},
|
||||
progress() {
|
||||
return [
|
||||
{
|
||||
@@ -404,21 +413,17 @@ export default {
|
||||
id: 'isbn',
|
||||
name: 'ISBN'
|
||||
},
|
||||
{
|
||||
id: 'subtitle',
|
||||
name: this.$strings.LabelSubtitle
|
||||
},
|
||||
{
|
||||
id: 'authors',
|
||||
name: this.$strings.LabelAuthor
|
||||
},
|
||||
{
|
||||
id: 'publishedYear',
|
||||
name: this.$strings.LabelPublishYear
|
||||
id: 'chapters',
|
||||
name: this.$strings.LabelChapters
|
||||
},
|
||||
{
|
||||
id: 'series',
|
||||
name: this.$strings.LabelSeries
|
||||
id: 'cover',
|
||||
name: this.$strings.LabelCover
|
||||
},
|
||||
{
|
||||
id: 'description',
|
||||
@@ -429,24 +434,32 @@ export default {
|
||||
name: this.$strings.LabelGenres
|
||||
},
|
||||
{
|
||||
id: 'tags',
|
||||
name: this.$strings.LabelTags
|
||||
id: 'language',
|
||||
name: this.$strings.LabelLanguage
|
||||
},
|
||||
{
|
||||
id: 'narrators',
|
||||
name: this.$strings.LabelNarrator
|
||||
},
|
||||
{
|
||||
id: 'publishedYear',
|
||||
name: this.$strings.LabelPublishYear
|
||||
},
|
||||
{
|
||||
id: 'publisher',
|
||||
name: this.$strings.LabelPublisher
|
||||
},
|
||||
{
|
||||
id: 'language',
|
||||
name: this.$strings.LabelLanguage
|
||||
id: 'series',
|
||||
name: this.$strings.LabelSeries
|
||||
},
|
||||
{
|
||||
id: 'cover',
|
||||
name: this.$strings.LabelCover
|
||||
id: 'subtitle',
|
||||
name: this.$strings.LabelSubtitle
|
||||
},
|
||||
{
|
||||
id: 'tags',
|
||||
name: this.$strings.LabelTags
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -56,24 +56,15 @@ export default {
|
||||
},
|
||||
imgSrc() {
|
||||
if (!this.imagePath) return null
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
// Testing
|
||||
return `http://localhost:3333${this.$config.routerBasePath}/api/authors/${this.authorId}/image?token=${this.userToken}&ts=${this.updatedAt}`
|
||||
}
|
||||
return `/api/authors/${this.authorId}/image?token=${this.userToken}&ts=${this.updatedAt}`
|
||||
return `${this.$config.routerBasePath}/api/authors/${this.authorId}/image?token=${this.userToken}&ts=${this.updatedAt}`
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
imageLoaded() {
|
||||
var aspectRatio = 1.25
|
||||
if (this.$refs.wrapper) {
|
||||
aspectRatio = this.$refs.wrapper.clientHeight / this.$refs.wrapper.clientWidth
|
||||
}
|
||||
if (this.$refs.img) {
|
||||
var { naturalWidth, naturalHeight } = this.$refs.img
|
||||
var imgAr = naturalHeight / naturalWidth
|
||||
var arDiff = Math.abs(imgAr - aspectRatio)
|
||||
if (arDiff > 0.15) {
|
||||
if (imgAr < 0.5 || imgAr > 2) {
|
||||
this.showCoverBg = true
|
||||
} else {
|
||||
this.showCoverBg = false
|
||||
|
||||
@@ -116,10 +116,10 @@ export default {
|
||||
libraryItemIds: this.selectedBookIds
|
||||
})
|
||||
.then(() => {
|
||||
this.$toast.info('Batch quick match of ' + this.selectedBookIds.length + ' books started!')
|
||||
this.$toast.info(this.$getString('ToastBatchQuickMatchStarted', [this.selectedBookIds.length]))
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toast.error('Batch quick match failed')
|
||||
this.$toast.error(this.$strings.ToastBatchQuickMatchFailed)
|
||||
console.error('Failed to batch quick match', error)
|
||||
})
|
||||
.finally(() => {
|
||||
|
||||
@@ -112,11 +112,11 @@ export default {
|
||||
return this.$store.state.user.user
|
||||
},
|
||||
demoShareUrl() {
|
||||
return `${window.origin}/share/${this.newShareSlug}`
|
||||
return `${window.origin}${this.$config.routerBasePath}/share/${this.newShareSlug}`
|
||||
},
|
||||
currentShareUrl() {
|
||||
if (!this.currentShare) return ''
|
||||
return `${window.origin}/share/${this.currentShare.slug}`
|
||||
return `${window.origin}${this.$config.routerBasePath}/share/${this.currentShare.slug}`
|
||||
},
|
||||
currentShareTimeRemaining() {
|
||||
if (!this.currentShare) return 'Error'
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<ui-text-input-with-label ref="maxEpisodesInput" v-model="maxEpisodesToDownload" :disabled="checkingNewEpisodes" type="number" :label="$strings.LabelLimit" class="w-16 mr-2" input-class="h-10">
|
||||
<div class="flex -mb-0.5">
|
||||
<p class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': checkingNewEpisodes }">{{ $strings.LabelLimit }}</p>
|
||||
<ui-tooltip direction="top" text="Max # of episodes to download. Use 0 for unlimited.">
|
||||
<ui-tooltip direction="top" :text="$strings.LabelMaxEpisodesToDownload">
|
||||
<span class="material-symbols text-base">info</span>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
@@ -99,7 +99,7 @@ export default {
|
||||
|
||||
if (this.maxEpisodesToDownload < 0) {
|
||||
this.maxEpisodesToDownload = 3
|
||||
this.$toast.error('Invalid max episodes to download')
|
||||
this.$toast.error(this.$strings.ToastInvalidMaxEpisodesToDownload)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -120,9 +120,9 @@ export default {
|
||||
.then((response) => {
|
||||
if (response.episodes && response.episodes.length) {
|
||||
console.log('New episodes', response.episodes.length)
|
||||
this.$toast.success(`${response.episodes.length} new episodes found!`)
|
||||
this.$toast.success(this.$getString('ToastNewEpisodesFound', [response.episodes.length]))
|
||||
} else {
|
||||
this.$toast.info('No new episodes found')
|
||||
this.$toast.info(this.$strings.ToastNoNewEpisodesFound)
|
||||
}
|
||||
this.checkingNewEpisodes = false
|
||||
})
|
||||
@@ -141,4 +141,4 @@ export default {
|
||||
this.setLastEpisodeCheckInput()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
<div class="flex-grow ml-4">
|
||||
<ui-text-input-with-label v-model="selectedMatch.title" :disabled="!selectedMatchUsage.title" :label="$strings.LabelTitle" />
|
||||
<p v-if="mediaMetadata.title" class="text-xs ml-1 text-white text-opacity-60">
|
||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('title', mediaMetadata.title)">{{ mediaMetadata.title || '' }}</a>
|
||||
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('title', mediaMetadata.title)">{{ mediaMetadata.title || '' }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,7 +69,7 @@
|
||||
<div class="flex-grow ml-4">
|
||||
<ui-text-input-with-label v-model="selectedMatch.subtitle" :disabled="!selectedMatchUsage.subtitle" :label="$strings.LabelSubtitle" />
|
||||
<p v-if="mediaMetadata.subtitle" class="text-xs ml-1 text-white text-opacity-60">
|
||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('subtitle', mediaMetadata.subtitle)">{{ mediaMetadata.subtitle }}</a>
|
||||
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('subtitle', mediaMetadata.subtitle)">{{ mediaMetadata.subtitle }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -78,7 +78,7 @@
|
||||
<div class="flex-grow ml-4">
|
||||
<ui-text-input-with-label v-model="selectedMatch.author" :disabled="!selectedMatchUsage.author" :label="$strings.LabelAuthor" />
|
||||
<p v-if="mediaMetadata.authorName" class="text-xs ml-1 text-white text-opacity-60">
|
||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('author', mediaMetadata.authorName)">{{ mediaMetadata.authorName }}</a>
|
||||
{{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('author', mediaMetadata.authorName)">{{ mediaMetadata.authorName }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -87,7 +87,7 @@
|
||||
<div class="flex-grow ml-4">
|
||||
<ui-multi-select v-model="selectedMatch.narrator" :items="narrators" :disabled="!selectedMatchUsage.narrator" :label="$strings.LabelNarrators" />
|
||||
<p v-if="mediaMetadata.narratorName" class="text-xs ml-1 text-white text-opacity-60">
|
||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('narrator', mediaMetadata.narrators)">{{ mediaMetadata.narratorName }}</a>
|
||||
{{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('narrator', mediaMetadata.narrators)">{{ mediaMetadata.narratorName }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -96,7 +96,7 @@
|
||||
<div class="flex-grow ml-4">
|
||||
<ui-textarea-with-label v-model="selectedMatch.description" :rows="3" :disabled="!selectedMatchUsage.description" :label="$strings.LabelDescription" />
|
||||
<p v-if="mediaMetadata.description" class="text-xs ml-1 text-white text-opacity-60">
|
||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('description', mediaMetadata.description)">{{ mediaMetadata.description.substr(0, 100) + (mediaMetadata.description.length > 100 ? '...' : '') }}</a>
|
||||
{{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('description', mediaMetadata.description)">{{ mediaMetadata.description.substr(0, 100) + (mediaMetadata.description.length > 100 ? '...' : '') }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -105,7 +105,7 @@
|
||||
<div class="flex-grow ml-4">
|
||||
<ui-text-input-with-label v-model="selectedMatch.publisher" :disabled="!selectedMatchUsage.publisher" :label="$strings.LabelPublisher" />
|
||||
<p v-if="mediaMetadata.publisher" class="text-xs ml-1 text-white text-opacity-60">
|
||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('publisher', mediaMetadata.publisher)">{{ mediaMetadata.publisher }}</a>
|
||||
{{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('publisher', mediaMetadata.publisher)">{{ mediaMetadata.publisher }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -114,7 +114,7 @@
|
||||
<div class="flex-grow ml-4">
|
||||
<ui-text-input-with-label v-model="selectedMatch.publishedYear" :disabled="!selectedMatchUsage.publishedYear" :label="$strings.LabelPublishYear" />
|
||||
<p v-if="mediaMetadata.publishedYear" class="text-xs ml-1 text-white text-opacity-60">
|
||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('publishedYear', mediaMetadata.publishedYear)">{{ mediaMetadata.publishedYear }}</a>
|
||||
{{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('publishedYear', mediaMetadata.publishedYear)">{{ mediaMetadata.publishedYear }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -124,7 +124,7 @@
|
||||
<div class="flex-grow ml-4">
|
||||
<widgets-series-input-widget v-model="selectedMatch.series" :disabled="!selectedMatchUsage.series" />
|
||||
<p v-if="mediaMetadata.seriesName" class="text-xs ml-1 text-white text-opacity-60">
|
||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('series', mediaMetadata.series)">{{ mediaMetadata.seriesName }}</a>
|
||||
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('series', mediaMetadata.series)">{{ mediaMetadata.seriesName }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -133,7 +133,7 @@
|
||||
<div class="flex-grow ml-4">
|
||||
<ui-multi-select v-model="selectedMatch.genres" :items="genres" :disabled="!selectedMatchUsage.genres" :label="$strings.LabelGenres" />
|
||||
<p v-if="mediaMetadata.genres?.length" class="text-xs ml-1 text-white text-opacity-60">
|
||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('genres', mediaMetadata.genres)">{{ mediaMetadata.genres.join(', ') }}</a>
|
||||
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('genres', mediaMetadata.genres)">{{ mediaMetadata.genres.join(', ') }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -142,7 +142,7 @@
|
||||
<div class="flex-grow ml-4">
|
||||
<ui-multi-select v-model="selectedMatch.tags" :items="tags" :disabled="!selectedMatchUsage.tags" :label="$strings.LabelTags" />
|
||||
<p v-if="media.tags?.length" class="text-xs ml-1 text-white text-opacity-60">
|
||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('tags', media.tags)">{{ media.tags.join(', ') }}</a>
|
||||
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('tags', media.tags)">{{ media.tags.join(', ') }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -151,7 +151,7 @@
|
||||
<div class="flex-grow ml-4">
|
||||
<ui-text-input-with-label v-model="selectedMatch.language" :disabled="!selectedMatchUsage.language" :label="$strings.LabelLanguage" />
|
||||
<p v-if="mediaMetadata.language" class="text-xs ml-1 text-white text-opacity-60">
|
||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('language', mediaMetadata.language)">{{ mediaMetadata.language }}</a>
|
||||
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('language', mediaMetadata.language)">{{ mediaMetadata.language }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -160,7 +160,7 @@
|
||||
<div class="flex-grow ml-4">
|
||||
<ui-text-input-with-label v-model="selectedMatch.isbn" :disabled="!selectedMatchUsage.isbn" label="ISBN" />
|
||||
<p v-if="mediaMetadata.isbn" class="text-xs ml-1 text-white text-opacity-60">
|
||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('isbn', mediaMetadata.isbn)">{{ mediaMetadata.isbn }}</a>
|
||||
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('isbn', mediaMetadata.isbn)">{{ mediaMetadata.isbn }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -169,7 +169,7 @@
|
||||
<div class="flex-grow ml-4">
|
||||
<ui-text-input-with-label v-model="selectedMatch.asin" :disabled="!selectedMatchUsage.asin" label="ASIN" />
|
||||
<p v-if="mediaMetadata.asin" class="text-xs ml-1 text-white text-opacity-60">
|
||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('asin', mediaMetadata.asin)">{{ mediaMetadata.asin }}</a>
|
||||
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('asin', mediaMetadata.asin)">{{ mediaMetadata.asin }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -179,7 +179,7 @@
|
||||
<div class="flex-grow ml-4">
|
||||
<ui-text-input-with-label v-model="selectedMatch.itunesId" type="number" :disabled="!selectedMatchUsage.itunesId" label="iTunes ID" />
|
||||
<p v-if="mediaMetadata.itunesId" class="text-xs ml-1 text-white text-opacity-60">
|
||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('itunesId', mediaMetadata.itunesId)">{{ mediaMetadata.itunesId }}</a>
|
||||
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('itunesId', mediaMetadata.itunesId)">{{ mediaMetadata.itunesId }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -188,7 +188,7 @@
|
||||
<div class="flex-grow ml-4">
|
||||
<ui-text-input-with-label v-model="selectedMatch.feedUrl" :disabled="!selectedMatchUsage.feedUrl" label="RSS Feed URL" />
|
||||
<p v-if="mediaMetadata.feedUrl" class="text-xs ml-1 text-white text-opacity-60">
|
||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('feedUrl', mediaMetadata.feedUrl)">{{ mediaMetadata.feedUrl }}</a>
|
||||
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('feedUrl', mediaMetadata.feedUrl)">{{ mediaMetadata.feedUrl }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -197,7 +197,7 @@
|
||||
<div class="flex-grow ml-4">
|
||||
<ui-text-input-with-label v-model="selectedMatch.itunesPageUrl" :disabled="!selectedMatchUsage.itunesPageUrl" label="iTunes Page URL" />
|
||||
<p v-if="mediaMetadata.itunesPageUrl" class="text-xs ml-1 text-white text-opacity-60">
|
||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('itunesPageUrl', mediaMetadata.itunesPageUrl)">{{ mediaMetadata.itunesPageUrl }}</a>
|
||||
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('itunesPageUrl', mediaMetadata.itunesPageUrl)">{{ mediaMetadata.itunesPageUrl }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -206,7 +206,7 @@
|
||||
<div class="flex-grow ml-4">
|
||||
<ui-text-input-with-label v-model="selectedMatch.releaseDate" :disabled="!selectedMatchUsage.releaseDate" :label="$strings.LabelReleaseDate" />
|
||||
<p v-if="mediaMetadata.releaseDate" class="text-xs ml-1 text-white text-opacity-60">
|
||||
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('releaseDate', mediaMetadata.releaseDate)">{{ mediaMetadata.releaseDate }}</a>
|
||||
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('releaseDate', mediaMetadata.releaseDate)">{{ mediaMetadata.releaseDate }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,28 +2,28 @@
|
||||
<div class="w-full h-full relative">
|
||||
<div id="scheduleWrapper" class="w-full overflow-y-auto px-2 py-4 md:px-6 md:py-6">
|
||||
<template v-if="!feedUrl">
|
||||
<widgets-alert type="warning" class="text-base mb-4">No RSS feed URL is set for this podcast</widgets-alert>
|
||||
<widgets-alert type="warning" class="text-base mb-4">{{ $strings.ToastPodcastNoRssFeed }}</widgets-alert>
|
||||
</template>
|
||||
<template v-if="feedUrl || autoDownloadEpisodes">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<p class="text-base md:text-xl font-semibold">Schedule Automatic Episode Downloads</p>
|
||||
<ui-checkbox v-model="enableAutoDownloadEpisodes" label="Enable" medium checkbox-bg="bg" label-class="pl-2 text-base md:text-lg" />
|
||||
<p class="text-base md:text-xl font-semibold">{{ $strings.HeaderScheduleEpisodeDownloads }}</p>
|
||||
<ui-checkbox v-model="enableAutoDownloadEpisodes" :label="$strings.LabelEnable" medium checkbox-bg="bg" label-class="pl-2 text-base md:text-lg" />
|
||||
</div>
|
||||
|
||||
<div v-if="enableAutoDownloadEpisodes" class="flex items-center py-2">
|
||||
<ui-text-input ref="maxEpisodesInput" type="number" v-model="newMaxEpisodesToKeep" no-spinner :padding-x="1" text-center class="w-10 text-base" @change="updatedMaxEpisodesToKeep" />
|
||||
<ui-tooltip text="Value of 0 sets no max limit. After a new episode is auto-downloaded this will delete the oldest episode if you have more than X episodes. <br>This will only delete 1 episode per new download.">
|
||||
<ui-tooltip :text="$strings.LabelMaxEpisodesToKeepHelp">
|
||||
<p class="pl-4 text-base">
|
||||
Max episodes to keep
|
||||
{{ $strings.LabelMaxEpisodesToKeep }}
|
||||
<span class="material-symbols icon-text">info</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
<div v-if="enableAutoDownloadEpisodes" class="flex items-center py-2">
|
||||
<ui-text-input ref="maxEpisodesToDownloadInput" type="number" v-model="newMaxNewEpisodesToDownload" no-spinner :padding-x="1" text-center class="w-10 text-base" @change="updateMaxNewEpisodesToDownload" />
|
||||
<ui-tooltip text="Value of 0 sets no max limit. When checking for new episodes this is the max number of episodes that will be downloaded.">
|
||||
<ui-tooltip :text="$strings.LabelUseZeroForUnlimited">
|
||||
<p class="pl-4 text-base">
|
||||
Max new episodes to download per check
|
||||
{{ $strings.LabelMaxEpisodesToDownloadPerCheck }}
|
||||
<span class="material-symbols icon-text">info</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
@@ -36,7 +36,7 @@
|
||||
<div v-if="feedUrl || autoDownloadEpisodes" class="absolute bottom-0 left-0 w-full py-2 md:py-4 bg-bg border-t border-white border-opacity-5">
|
||||
<div class="flex items-center px-2 md:px-4">
|
||||
<div class="flex-grow" />
|
||||
<ui-btn @click="save" :disabled="!isUpdated" :color="isUpdated ? 'success' : 'primary'" class="mx-2">{{ isUpdated ? 'Save' : 'No update necessary' }}</ui-btn>
|
||||
<ui-btn @click="save" :disabled="!isUpdated" :color="isUpdated ? 'success' : 'primary'" class="mx-2">{{ isUpdated ? $strings.ButtonSave : $strings.MessageNoUpdatesWereNecessary }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -33,18 +33,18 @@
|
||||
<span class="material-symbols text-lg ml-2">launch</span>
|
||||
</ui-btn>
|
||||
|
||||
<ui-btn v-if="!isMetadataEmbedQueued && !isEmbedTaskRunning" class="w-full mt-4" small @click.stop="quickEmbed">Quick Embed</ui-btn>
|
||||
<ui-btn v-if="!isMetadataEmbedQueued && !isEmbedTaskRunning" class="w-full mt-4" small @click.stop="quickEmbed">{{ $strings.ButtonQuickEmbed }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- queued alert -->
|
||||
<widgets-alert v-if="isMetadataEmbedQueued" type="warning" class="mt-4">
|
||||
<p class="text-lg">Queued for metadata embed ({{ queuedEmbedLIds.length }} in queue)</p>
|
||||
<p class="text-lg">{{ $getString('MessageQuickEmbedQueue', [queuedEmbedLIds.length]) }}</p>
|
||||
</widgets-alert>
|
||||
|
||||
<!-- processing alert -->
|
||||
<widgets-alert v-if="isEmbedTaskRunning" type="warning" class="mt-4">
|
||||
<p class="text-lg">Currently embedding metadata</p>
|
||||
<p class="text-lg">{{ $strings.MessageQuickEmbedInProgress }}</p>
|
||||
</widgets-alert>
|
||||
</div>
|
||||
|
||||
@@ -113,7 +113,7 @@ export default {
|
||||
methods: {
|
||||
quickEmbed() {
|
||||
const payload = {
|
||||
message: 'Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?',
|
||||
message: this.$strings.MessageConfirmQuickEmbed,
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.$axios
|
||||
|
||||
@@ -160,7 +160,7 @@ export default {
|
||||
return false
|
||||
}
|
||||
if (!this.libraryCopy.folders.length) {
|
||||
this.$toast.error('Library must have at least 1 path')
|
||||
this.$toast.error(this.$strings.ToastMustHaveAtLeastOnePath)
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
<div class="w-full border border-black-200 p-4 my-8">
|
||||
<div class="flex flex-wrap items-center">
|
||||
<div>
|
||||
<p class="text-lg">Remove metadata files in library item folders</p>
|
||||
<p class="max-w-sm text-sm pt-2 text-gray-300">Remove all metadata.json or metadata.abs files in your {{ mediaType }} folders</p>
|
||||
<p class="text-lg">{{ $strings.LabelRemoveMetadataFile }}</p>
|
||||
<p class="max-w-sm text-sm pt-2 text-gray-300">{{ $getString('LabelRemoveMetadataFileHelp', [mediaType]) }}</p>
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
<div>
|
||||
<ui-btn class="mb-4 block" @click.stop="removeAllMetadataClick('json')">Remove all metadata.json</ui-btn>
|
||||
<ui-btn @click.stop="removeAllMetadataClick('abs')">Remove all metadata.abs</ui-btn>
|
||||
<ui-btn class="mb-4 block" @click.stop="removeAllMetadataClick('json')">{{ $strings.LabelRemoveAllMetadataJson }}</ui-btn>
|
||||
<ui-btn @click.stop="removeAllMetadataClick('abs')">{{ $strings.LabelRemoveAllMetadataAbs }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -43,7 +43,7 @@ export default {
|
||||
methods: {
|
||||
removeAllMetadataClick(ext) {
|
||||
const payload = {
|
||||
message: `Are you sure you want to remove all metadata.${ext} files in your library item folders?`,
|
||||
message: this.$getString('MessageConfirmRemoveMetadataFiles', [ext]),
|
||||
persistent: true,
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
@@ -60,16 +60,16 @@ export default {
|
||||
.$post(`/api/libraries/${this.libraryId}/remove-metadata?ext=${ext}`)
|
||||
.then((data) => {
|
||||
if (!data.found) {
|
||||
this.$toast.info(`No metadata.${ext} files were found in library`)
|
||||
this.$toast.info(this.$getString('ToastMetadataFilesRemovedNoneFound', [ext]))
|
||||
} else if (!data.removed) {
|
||||
this.$toast.success(`No metadata.${ext} files removed`)
|
||||
this.$toast.success(this.$getString('ToastMetadataFilesRemovedNoneRemoved', [ext]))
|
||||
} else {
|
||||
this.$toast.success(`Successfully removed ${data.removed} metadata.${ext} files`)
|
||||
this.$toast.success(this.$getString('ToastMetadataFilesRemovedSuccess', [data.removed, ext]))
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to remove metadata files', error)
|
||||
this.$toast.error('Failed to remove metadata files')
|
||||
this.$toast.error(this.$getString('ToastMetadataFilesRemovedError', [ext]))
|
||||
})
|
||||
.finally(() => {
|
||||
this.$emit('update:processing', false)
|
||||
|
||||
@@ -77,7 +77,13 @@ export default {
|
||||
return this.notificationData.events || []
|
||||
},
|
||||
eventOptions() {
|
||||
return this.notificationEvents.map((e) => ({ value: e.name, text: e.name, subtext: e.description }))
|
||||
return this.notificationEvents.map((e) => {
|
||||
return {
|
||||
value: e.name,
|
||||
text: e.name,
|
||||
subtext: this.$strings[e.descriptionKey] || e.description
|
||||
}
|
||||
})
|
||||
},
|
||||
selectedEventData() {
|
||||
return this.notificationEvents.find((e) => e.name === this.newNotification.eventName)
|
||||
|
||||
@@ -156,7 +156,12 @@ export default {
|
||||
return this.selectedFolder.fullPath
|
||||
},
|
||||
podcastTypes() {
|
||||
return this.$store.state.globals.podcastTypes || []
|
||||
return this.$store.state.globals.podcastTypes.map((e) => {
|
||||
return {
|
||||
text: this.$strings[e.descriptionKey] || e.text,
|
||||
value: e.value
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -33,11 +33,11 @@
|
||||
</div>
|
||||
<div v-if="enclosureUrl" class="pb-4 pt-6">
|
||||
<ui-text-input-with-label :value="enclosureUrl" readonly class="text-xs">
|
||||
<label class="px-1 text-xs text-gray-200 font-semibold">Episode URL from RSS feed</label>
|
||||
<label class="px-1 text-xs text-gray-200 font-semibold">{{ $strings.LabelEpisodeUrlFromRssFeed }}</label>
|
||||
</ui-text-input-with-label>
|
||||
</div>
|
||||
<div v-else class="py-4">
|
||||
<p class="text-xs text-gray-300 font-semibold">Episode not linked to RSS feed episode</p>
|
||||
<p class="text-xs text-gray-300 font-semibold">{{ $strings.LabelEpisodeNotLinkedToRssFeed }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -97,7 +97,12 @@ export default {
|
||||
return this.enclosure.url
|
||||
},
|
||||
episodeTypes() {
|
||||
return this.$store.state.globals.episodeTypes || []
|
||||
return this.$store.state.globals.episodeTypes.map((e) => {
|
||||
return {
|
||||
text: this.$strings[e.descriptionKey] || e.text,
|
||||
value: e.value
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -152,14 +157,14 @@ export default {
|
||||
const updateResult = await this.$axios.$patch(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}`, updatedDetails).catch((error) => {
|
||||
console.error('Failed update episode', error)
|
||||
this.isProcessing = false
|
||||
this.$toast.error(error?.response?.data || 'Failed to update episode')
|
||||
this.$toast.error(error?.response?.data || this.$strings.ToastFailedToUpdate)
|
||||
return false
|
||||
})
|
||||
|
||||
this.isProcessing = false
|
||||
if (updateResult) {
|
||||
if (updateResult) {
|
||||
this.$toast.success('Podcast episode updated')
|
||||
this.$toast.success(this.$strings.ToastItemUpdateSuccess)
|
||||
return true
|
||||
} else {
|
||||
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
|
||||
|
||||
@@ -139,7 +139,7 @@ export default {
|
||||
slug: this.newFeedSlug,
|
||||
metadataDetails: this.metadataDetails
|
||||
}
|
||||
if (this.$isDev) payload.serverAddress = `http://localhost:3333${this.$config.routerBasePath}`
|
||||
if (this.$isDev) payload.serverAddress = process.env.serverUrl
|
||||
|
||||
console.log('Payload', payload)
|
||||
this.$axios
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
</div>
|
||||
<div class="h-8 flex items-center">
|
||||
<div class="w-full inline-flex justify-between max-w-xl">
|
||||
<p v-if="episode?.season" class="text-sm text-gray-300">Season #{{ episode.season }}</p>
|
||||
<p v-if="episode?.episode" class="text-sm text-gray-300">Episode #{{ episode.episode }}</p>
|
||||
<p v-if="episode?.chapters?.length" class="text-sm text-gray-300">{{ episode.chapters.length }} Chapters</p>
|
||||
<p v-if="publishedAt" class="text-sm text-gray-300">Published {{ $formatDate(publishedAt, dateFormat) }}</p>
|
||||
<p v-if="episode?.season" class="text-sm text-gray-300">{{ $getString('LabelSeasonNumber', [episode.season]) }}</p>
|
||||
<p v-if="episode?.episode" class="text-sm text-gray-300">{{ $getString('LabelEpisodeNumber', [episode.episode]) }}</p>
|
||||
<p v-if="episode?.chapters?.length" class="text-sm text-gray-300">{{ $getString('LabelChapterCount', [episode.chapters.length]) }}</p>
|
||||
<p v-if="publishedAt" class="text-sm text-gray-300">{{ $getString('LabelPublishedDate', [$formatDate(publishedAt, dateFormat)]) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -132,13 +132,13 @@ export default {
|
||||
return this.store.state.streamIsPlaying && this.isStreaming
|
||||
},
|
||||
timeRemaining() {
|
||||
if (this.streamIsPlaying) return 'Playing'
|
||||
if (this.streamIsPlaying) return this.$strings.ButtonPlaying
|
||||
if (!this.itemProgress) return this.$elapsedPretty(this.episode?.duration || 0)
|
||||
if (this.userIsFinished) return 'Finished'
|
||||
if (this.userIsFinished) return this.$strings.LabelFinished
|
||||
|
||||
const duration = this.itemProgress.duration || this.episode?.duration || 0
|
||||
const remaining = Math.floor(duration - this.itemProgress.currentTime)
|
||||
return `${this.$elapsedPretty(remaining)} left`
|
||||
return this.$getString('LabelTimeLeft', [this.$elapsedPretty(remaining)])
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -182,7 +182,7 @@ export default {
|
||||
toggleFinished(confirmed = false) {
|
||||
if (!this.userIsFinished && this.itemProgressPercent > 0 && !confirmed) {
|
||||
const payload = {
|
||||
message: `Are you sure you want to mark "${this.episodeTitle}" as finished?`,
|
||||
message: this.$getString('MessageConfirmMarkItemFinished', [this.episodeTitle]),
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.toggleFinished(true)
|
||||
|
||||
@@ -93,17 +93,18 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
contextMenuItems() {
|
||||
if (!this.userIsAdminOrUp) return []
|
||||
return [
|
||||
{
|
||||
text: 'Quick match all episodes',
|
||||
const menuItems = []
|
||||
if (this.userIsAdminOrUp) {
|
||||
menuItems.push({
|
||||
text: this.$strings.MessageQuickMatchAllEpisodes,
|
||||
action: 'quick-match-episodes'
|
||||
},
|
||||
{
|
||||
text: this.allEpisodesFinished ? this.$strings.MessageMarkAllEpisodesNotFinished : this.$strings.MessageMarkAllEpisodesFinished,
|
||||
action: 'batch-mark-as-finished'
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
menuItems.push({
|
||||
text: this.allEpisodesFinished ? this.$strings.MessageMarkAllEpisodesNotFinished : this.$strings.MessageMarkAllEpisodesFinished,
|
||||
action: 'batch-mark-as-finished'
|
||||
})
|
||||
return menuItems
|
||||
},
|
||||
sortItems() {
|
||||
return [
|
||||
@@ -261,21 +262,21 @@ export default {
|
||||
this.processing = true
|
||||
|
||||
const payload = {
|
||||
message: 'Quick matching episodes will overwrite details if a match is found. Only unmatched episodes will be updated. Are you sure?',
|
||||
message: this.$strings.MessageConfirmQuickMatchEpisodes,
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.$axios
|
||||
.$post(`/api/podcasts/${this.libraryItem.id}/match-episodes?override=1`)
|
||||
.then((data) => {
|
||||
if (data.numEpisodesUpdated) {
|
||||
this.$toast.success(`${data.numEpisodesUpdated} episodes updated`)
|
||||
this.$toast.success(this.$getString('ToastEpisodeUpdateSuccess', [data.numEpisodesUpdated]))
|
||||
} else {
|
||||
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to request match episodes', error)
|
||||
this.$toast.error('Failed to match episodes')
|
||||
this.$toast.error(this.$strings.ToastFailedToMatch)
|
||||
})
|
||||
}
|
||||
this.processing = false
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
<template>
|
||||
<ui-tooltip v-if="alreadyInLibrary" :text="$strings.LabelAlreadyInYourLibrary" direction="top">
|
||||
<span class="material-symbols ml-1 text-success" style="font-size: 0.8rem">check_circle</span>
|
||||
<ui-tooltip :text="$strings.LabelAlreadyInYourLibrary" direction="top" class="inline-flex">
|
||||
<span class="material-symbols ml-1 text-sm text-success">check_circle</span>
|
||||
</ui-tooltip>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
alreadyInLibrary: Boolean
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
|
||||
@@ -65,7 +65,7 @@ export default {
|
||||
},
|
||||
authors: {
|
||||
component: 'cards-author-card',
|
||||
itemPropName: 'author',
|
||||
itemPropName: 'author-mount',
|
||||
itemIdFunc: (item) => item.id
|
||||
},
|
||||
narrators: {
|
||||
|
||||
@@ -101,7 +101,12 @@ export default {
|
||||
return this.$store.state.libraries.filterData || {}
|
||||
},
|
||||
podcastTypes() {
|
||||
return this.$store.state.globals.podcastTypes || []
|
||||
return this.$store.state.globals.podcastTypes.map((e) => {
|
||||
return {
|
||||
text: this.$strings[e.descriptionKey] || e.text,
|
||||
value: e.value
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -5,14 +5,14 @@ import Tooltip from '@/components/ui/Tooltip.vue'
|
||||
import LoadingSpinner from '@/components/widgets/LoadingSpinner.vue'
|
||||
|
||||
describe('AuthorCard', () => {
|
||||
const author = {
|
||||
const authorMount = {
|
||||
id: 1,
|
||||
name: 'John Doe',
|
||||
numBooks: 5
|
||||
}
|
||||
|
||||
const propsData = {
|
||||
author,
|
||||
authorMount,
|
||||
nameBelow: false
|
||||
}
|
||||
|
||||
|
||||
@@ -357,7 +357,8 @@ export default {
|
||||
teardown: false,
|
||||
transports: ['websocket'],
|
||||
upgrade: false,
|
||||
reconnection: true
|
||||
reconnection: true,
|
||||
path: `${this.$config.routerBasePath}/socket.io`
|
||||
})
|
||||
this.$root.socket = this.socket
|
||||
console.log('Socket initialized')
|
||||
|
||||
@@ -4,6 +4,7 @@ import LazySeriesCard from '@/components/cards/LazySeriesCard'
|
||||
import LazyCollectionCard from '@/components/cards/LazyCollectionCard'
|
||||
import LazyPlaylistCard from '@/components/cards/LazyPlaylistCard'
|
||||
import LazyAlbumCard from '@/components/cards/LazyAlbumCard'
|
||||
import AuthorCard from '@/components/cards/AuthorCard'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
@@ -20,6 +21,7 @@ export default {
|
||||
if (this.entityName === 'collections') return Vue.extend(LazyCollectionCard)
|
||||
if (this.entityName === 'playlists') return Vue.extend(LazyPlaylistCard)
|
||||
if (this.entityName === 'albums') return Vue.extend(LazyAlbumCard)
|
||||
if (this.entityName === 'authors') return Vue.extend(AuthorCard)
|
||||
return Vue.extend(LazyBookCard)
|
||||
},
|
||||
getComponentName() {
|
||||
@@ -27,6 +29,7 @@ export default {
|
||||
if (this.entityName === 'collections') return 'cards-lazy-collection-card'
|
||||
if (this.entityName === 'playlists') return 'cards-lazy-playlist-card'
|
||||
if (this.entityName === 'albums') return 'cards-lazy-album-card'
|
||||
if (this.entityName === 'authors') return 'cards-author-card'
|
||||
return 'cards-lazy-book-card'
|
||||
},
|
||||
async setCardSize() {
|
||||
@@ -46,13 +49,14 @@ export default {
|
||||
props.orderBy = this.seriesSortBy
|
||||
}
|
||||
const instance = new ComponentClass({
|
||||
propsData: props
|
||||
propsData: props,
|
||||
parent: this
|
||||
})
|
||||
instance.$mount()
|
||||
this.resizeObserver = new ResizeObserver((entries) => {
|
||||
for (let entry of entries) {
|
||||
this.cardWidth = entry.contentRect.width
|
||||
this.cardHeight = entry.contentRect.height
|
||||
this.cardWidth = entry.borderBoxSize[0].inlineSize
|
||||
this.cardHeight = entry.borderBoxSize[0].blockSize
|
||||
this.resizeObserver.disconnect()
|
||||
this.$refs.bookshelf.removeChild(instance.$el)
|
||||
}
|
||||
@@ -72,7 +76,7 @@ export default {
|
||||
})
|
||||
const timeAfter = performance.now()
|
||||
},
|
||||
async mountEntityCard(index) {
|
||||
mountEntityCard(index) {
|
||||
var shelf = Math.floor(index / this.entitiesPerShelf)
|
||||
var shelfEl = document.getElementById(`shelf-${shelf}`)
|
||||
if (!shelfEl) {
|
||||
@@ -114,6 +118,7 @@ export default {
|
||||
const _this = this
|
||||
const instance = new ComponentClass({
|
||||
propsData: props,
|
||||
parent: this,
|
||||
created() {
|
||||
this.$on('edit', (entity) => {
|
||||
if (_this.editEntity) _this.editEntity(entity)
|
||||
|
||||
+34
-49
@@ -1,19 +1,24 @@
|
||||
const pkg = require('./package.json')
|
||||
|
||||
const routerBasePath = process.env.ROUTER_BASE_PATH || ''
|
||||
const serverHostUrl = process.env.NODE_ENV === 'production' ? '' : 'http://localhost:3333'
|
||||
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 : '/' }]))
|
||||
|
||||
module.exports = {
|
||||
// Disable server-side rendering: https://go.nuxtjs.dev/ssr-mode
|
||||
ssr: false,
|
||||
target: 'static',
|
||||
dev: process.env.NODE_ENV !== 'production',
|
||||
env: {
|
||||
serverUrl: process.env.NODE_ENV === 'production' ? process.env.ROUTER_BASE_PATH || '' : 'http://localhost:3333',
|
||||
serverUrl: serverHostUrl + routerBasePath,
|
||||
chromecastReceiver: 'FD1F76C5'
|
||||
},
|
||||
telemetry: false,
|
||||
|
||||
publicRuntimeConfig: {
|
||||
version: pkg.version,
|
||||
routerBasePath: process.env.ROUTER_BASE_PATH || ''
|
||||
routerBasePath
|
||||
},
|
||||
|
||||
// Global page headers: https://go.nuxtjs.dev/config-head
|
||||
@@ -22,38 +27,23 @@ module.exports = {
|
||||
htmlAttrs: {
|
||||
lang: 'en'
|
||||
},
|
||||
meta: [
|
||||
{ charset: 'utf-8' },
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
||||
{ hid: 'description', name: 'description', content: '' },
|
||||
{ hid: 'robots', name: 'robots', content: 'noindex' }
|
||||
],
|
||||
meta: [{ charset: 'utf-8' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' }, { hid: 'description', name: 'description', content: '' }, { hid: 'robots', name: 'robots', content: 'noindex' }],
|
||||
script: [],
|
||||
link: [
|
||||
{ rel: 'icon', type: 'image/x-icon', href: (process.env.ROUTER_BASE_PATH || '') + '/favicon.ico' },
|
||||
{ rel: 'apple-touch-icon', href: (process.env.ROUTER_BASE_PATH || '') + '/ios_icon.png' }
|
||||
{ rel: 'icon', type: 'image/x-icon', href: routerBasePath + '/favicon.ico' },
|
||||
{ rel: 'apple-touch-icon', href: routerBasePath + '/ios_icon.png' }
|
||||
]
|
||||
},
|
||||
|
||||
router: {
|
||||
base: process.env.ROUTER_BASE_PATH || ''
|
||||
base: routerBasePath
|
||||
},
|
||||
|
||||
// Global CSS: https://go.nuxtjs.dev/config-css
|
||||
css: [
|
||||
'@/assets/tailwind.css',
|
||||
'@/assets/app.css'
|
||||
],
|
||||
css: ['@/assets/tailwind.css', '@/assets/app.css'],
|
||||
|
||||
// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
|
||||
plugins: [
|
||||
'@/plugins/constants.js',
|
||||
'@/plugins/init.client.js',
|
||||
'@/plugins/axios.js',
|
||||
'@/plugins/toast.js',
|
||||
'@/plugins/utils.js',
|
||||
'@/plugins/i18n.js'
|
||||
],
|
||||
plugins: ['@/plugins/constants.js', '@/plugins/init.client.js', '@/plugins/axios.js', '@/plugins/toast.js', '@/plugins/utils.js', '@/plugins/i18n.js'],
|
||||
|
||||
// Auto import components: https://go.nuxtjs.dev/config-components
|
||||
components: true,
|
||||
@@ -65,30 +55,25 @@ module.exports = {
|
||||
],
|
||||
|
||||
// Modules: https://go.nuxtjs.dev/config-modules
|
||||
modules: [
|
||||
'nuxt-socket-io',
|
||||
'@nuxtjs/axios',
|
||||
'@nuxtjs/proxy'
|
||||
],
|
||||
modules: ['nuxt-socket-io', '@nuxtjs/axios', '@nuxtjs/proxy'],
|
||||
|
||||
proxy: {
|
||||
'/api/': { target: process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '/' },
|
||||
'/dev/': { target: 'http://localhost:3333', pathRewrite: { '^/dev/': '' } }
|
||||
},
|
||||
proxy,
|
||||
|
||||
io: {
|
||||
sockets: [{
|
||||
name: 'dev',
|
||||
url: 'http://localhost:3333'
|
||||
},
|
||||
{
|
||||
name: 'prod'
|
||||
}]
|
||||
sockets: [
|
||||
{
|
||||
name: 'dev',
|
||||
url: serverHostUrl
|
||||
},
|
||||
{
|
||||
name: 'prod'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// Axios module configuration: https://go.nuxtjs.dev/config-axios
|
||||
axios: {
|
||||
baseURL: process.env.ROUTER_BASE_PATH || ''
|
||||
baseURL: routerBasePath
|
||||
},
|
||||
|
||||
// nuxt/pwa https://pwa.nuxtjs.org
|
||||
@@ -108,11 +93,11 @@ module.exports = {
|
||||
background_color: '#232323',
|
||||
icons: [
|
||||
{
|
||||
src: (process.env.ROUTER_BASE_PATH || '') + '/icon.svg',
|
||||
src: routerBasePath + '/icon.svg',
|
||||
sizes: 'any'
|
||||
},
|
||||
{
|
||||
src: (process.env.ROUTER_BASE_PATH || '') + '/icon192.png',
|
||||
src: routerBasePath + '/icon192.png',
|
||||
type: 'image/png',
|
||||
sizes: 'any'
|
||||
}
|
||||
@@ -132,7 +117,7 @@ module.exports = {
|
||||
postcssOptions: {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
autoprefixer: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -149,12 +134,12 @@ module.exports = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Temporary workaround for @nuxt-community/tailwindcss-module.
|
||||
*
|
||||
* Reported: 2022-05-23
|
||||
* See: [Issue tracker](https://github.com/nuxt-community/tailwindcss-module/issues/480)
|
||||
*/
|
||||
* Temporary workaround for @nuxt-community/tailwindcss-module.
|
||||
*
|
||||
* Reported: 2022-05-23
|
||||
* See: [Issue tracker](https://github.com/nuxt-community/tailwindcss-module/issues/480)
|
||||
*/
|
||||
devServerHandlers: [],
|
||||
|
||||
ignore: ["**/*.test.*", "**/*.cy.*"]
|
||||
ignore: ['**/*.test.*', '**/*.cy.*']
|
||||
}
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.14.0",
|
||||
"version": "2.15.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.14.0",
|
||||
"version": "2.15.1",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@nuxtjs/axios": "^5.13.6",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.14.0",
|
||||
"version": "2.15.1",
|
||||
"buildNumber": 1,
|
||||
"description": "Self-hosted audiobook and podcast client",
|
||||
"main": "index.js",
|
||||
|
||||
@@ -415,7 +415,7 @@ export default {
|
||||
const audioEl = this.audioEl || document.createElement('audio')
|
||||
var src = audioTrack.contentUrl + `?token=${this.userToken}`
|
||||
if (this.$isDev) {
|
||||
src = `http://localhost:3333${this.$config.routerBasePath}${src}`
|
||||
src = `${process.env.serverUrl}${src}`
|
||||
}
|
||||
|
||||
audioEl.src = src
|
||||
@@ -486,7 +486,7 @@ export default {
|
||||
.then((data) => {
|
||||
this.saving = false
|
||||
if (data.updated) {
|
||||
this.$toast.success('Chapters updated')
|
||||
this.$toast.success(this.$strings.ToastChaptersUpdated)
|
||||
if (this.previousRoute) {
|
||||
this.$router.push(this.previousRoute)
|
||||
} else {
|
||||
@@ -533,7 +533,7 @@ export default {
|
||||
},
|
||||
findChapters() {
|
||||
if (!this.asinInput) {
|
||||
this.$toast.error('Must input an ASIN')
|
||||
this.$toast.error(this.$strings.ToastAsinRequired)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -63,11 +63,11 @@
|
||||
<div class="w-full max-w-4xl mx-auto">
|
||||
<!-- queued alert -->
|
||||
<widgets-alert v-if="isMetadataEmbedQueued" type="warning" class="mb-4">
|
||||
<p class="text-lg">Audiobook is queued for metadata embed ({{ queuedEmbedLIds.length }} in queue)</p>
|
||||
<p class="text-lg">{{ $getString('MessageEmbedQueue', [queuedEmbedLIds.length]) }}</p>
|
||||
</widgets-alert>
|
||||
<!-- metadata embed action buttons -->
|
||||
<div v-else-if="isEmbedTool" class="w-full flex justify-end items-center mb-4">
|
||||
<ui-checkbox v-if="!isTaskFinished" v-model="shouldBackupAudioFiles" :disabled="processing" label="Backup audio files" medium checkbox-bg="bg" label-class="pl-2 text-base md:text-lg" @input="toggleBackupAudioFiles" />
|
||||
<ui-checkbox v-if="!isTaskFinished" v-model="shouldBackupAudioFiles" :disabled="processing" :label="$strings.LabelBackupAudioFiles" medium checkbox-bg="bg" label-class="pl-2 text-base md:text-lg" @input="toggleBackupAudioFiles" />
|
||||
|
||||
<div class="flex-grow" />
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
<!-- m4b embed action buttons -->
|
||||
<div v-else class="w-full flex items-center mb-4">
|
||||
<button :disabled="processing" class="text-sm uppercase text-gray-200 flex items-center pt-px pl-1 pr-2 hover:bg-white/5 rounded-md" @click="showEncodeOptions = !showEncodeOptions">
|
||||
<span class="material-symbols text-xl">{{ showEncodeOptions || usingCustomEncodeOptions ? 'check_box' : 'check_box_outline_blank' }}</span> <span class="pl-1">Use Advanced Options</span>
|
||||
<span class="material-symbols text-xl">{{ showEncodeOptions || usingCustomEncodeOptions ? 'check_box' : 'check_box_outline_blank' }}</span> <span class="pl-1">{{ $strings.LabelUseAdvancedOptions }}</span>
|
||||
</button>
|
||||
|
||||
<div class="flex-grow" />
|
||||
@@ -94,11 +94,11 @@
|
||||
<transition name="slide">
|
||||
<div v-if="showEncodeOptions || usingCustomEncodeOptions" class="mb-4 pb-4 border-b border-white/10">
|
||||
<div class="flex flex-wrap -mx-2">
|
||||
<ui-text-input-with-label ref="bitrateInput" v-model="encodingOptions.bitrate" :disabled="processing || isTaskFinished" :label="'Audio Bitrate (e.g. 128k)'" class="m-2 max-w-40" @input="bitrateChanged" />
|
||||
<ui-text-input-with-label ref="channelsInput" v-model="encodingOptions.channels" :disabled="processing || isTaskFinished" :label="'Audio Channels (1 or 2)'" class="m-2 max-w-40" @input="channelsChanged" />
|
||||
<ui-text-input-with-label ref="codecInput" v-model="encodingOptions.codec" :disabled="processing || isTaskFinished" :label="'Audio Codec'" class="m-2 max-w-40" @input="codecChanged" />
|
||||
<ui-text-input-with-label ref="bitrateInput" v-model="encodingOptions.bitrate" :disabled="processing || isTaskFinished" :label="$strings.LabelAudioBitrate" class="m-2 max-w-40" @input="bitrateChanged" />
|
||||
<ui-text-input-with-label ref="channelsInput" v-model="encodingOptions.channels" :disabled="processing || isTaskFinished" :label="$strings.LabelAudioChannels" class="m-2 max-w-40" @input="channelsChanged" />
|
||||
<ui-text-input-with-label ref="codecInput" v-model="encodingOptions.codec" :disabled="processing || isTaskFinished" :label="$strings.LabelAudioCodec" class="m-2 max-w-40" @input="codecChanged" />
|
||||
</div>
|
||||
<p class="text-sm text-warning">Warning: Do not update these settings unless you are familiar with ffmpeg encoding options.</p>
|
||||
<p class="text-sm text-warning">{{ $strings.LabelEncodingWarningAdvancedSettings }}</p>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
@@ -106,36 +106,36 @@
|
||||
<div class="mb-4">
|
||||
<div v-if="isEmbedTool" class="flex items-start mb-2">
|
||||
<span class="material-symbols text-base text-warning pt-1">star</span>
|
||||
<p class="text-gray-200 ml-2">Metadata will be embedded in the audio tracks inside your audiobook folder.</p>
|
||||
<p class="text-gray-200 ml-2">{{ $strings.LabelEncodingInfoEmbedded }}</p>
|
||||
</div>
|
||||
<div v-else class="flex items-start mb-2">
|
||||
<span class="material-symbols text-base text-warning pt-1">star</span>
|
||||
<p class="text-gray-200 ml-2">
|
||||
Finished M4B will be put into your audiobook folder at <span class="rounded-md bg-neutral-600 text-sm text-white py-0.5 px-1 font-mono">.../{{ libraryItemRelPath }}/</span>.
|
||||
{{ $strings.LabelEncodingFinishedM4B }} <span class="rounded-md bg-neutral-600 text-sm text-white py-0.5 px-1 font-mono">.../{{ libraryItemRelPath }}/</span>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="shouldBackupAudioFiles || isM4BTool" class="flex items-start mb-2">
|
||||
<span class="material-symbols text-base text-warning pt-1">star</span>
|
||||
<p class="text-gray-200 ml-2">
|
||||
A backup of your original audio files will be stored in <span class="rounded-md bg-neutral-600 text-sm text-white py-0.5 px-1 font-mono">/metadata/cache/items/{{ libraryItemId }}/</span>. Make sure to periodically purge items cache.
|
||||
{{ $strings.LabelEncodingBackupLocation }} <span class="rounded-md bg-neutral-600 text-sm text-white py-0.5 px-1 font-mono">/metadata/cache/items/{{ libraryItemId }}/</span>. {{ $strings.LabelEncodingClearItemCache }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="isEmbedTool && audioFiles.length > 1" class="flex items-start mb-2">
|
||||
<span class="material-symbols text-base text-warning pt-1">star</span>
|
||||
<p class="text-gray-200 ml-2">Chapters are not embedded in multi-track audiobooks.</p>
|
||||
<p class="text-gray-200 ml-2">{{ $strings.LabelEncodingChaptersNotEmbedded }}</p>
|
||||
</div>
|
||||
<div v-if="isM4BTool" class="flex items-start mb-2">
|
||||
<span class="material-symbols text-base text-warning pt-1">star</span>
|
||||
<p class="text-gray-200 ml-2">Encoding can take up to 30 minutes.</p>
|
||||
<p class="text-gray-200 ml-2">{{ $strings.LabelEncodingTimeWarning }}</p>
|
||||
</div>
|
||||
<div v-if="isM4BTool" class="flex items-start mb-2">
|
||||
<span class="material-symbols text-base text-warning pt-1">star</span>
|
||||
<p class="text-gray-200 ml-2">If you have the watcher disabled you will need to re-scan this audiobook afterwards.</p>
|
||||
<p class="text-gray-200 ml-2">{{ $strings.LabelEncodingWatcherDisabled }}</p>
|
||||
</div>
|
||||
<div class="flex items-start mb-2">
|
||||
<span class="material-symbols text-base text-warning pt-1">star</span>
|
||||
<p class="text-gray-200 ml-2">Once the task is started you can navigate away from this page.</p>
|
||||
<p class="text-gray-200 ml-2">{{ $strings.LabelEncodingStartedNavigation }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -269,11 +269,11 @@ export default {
|
||||
},
|
||||
availableTools() {
|
||||
if (this.isSingleM4b) {
|
||||
return [{ value: 'embed', text: 'Embed Metadata' }]
|
||||
return [{ value: 'embed', text: this.$strings.LabelToolsEmbedMetadata }]
|
||||
} else {
|
||||
return [
|
||||
{ value: 'embed', text: 'Embed Metadata' },
|
||||
{ value: 'm4b', text: 'M4B Encoder' }
|
||||
{ value: 'embed', text: this.$strings.LabelToolsEmbedMetadata },
|
||||
{ value: 'm4b', text: this.$strings.LabelToolsM4bEncoder }
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -370,7 +370,7 @@ export default {
|
||||
},
|
||||
embedClick() {
|
||||
const payload = {
|
||||
message: `Are you sure you want to embed metadata in ${this.audioFiles.length} audio files?`,
|
||||
message: this.$getString('MessageConfirmEmbedMetadataInAudioFiles', [this.audioFiles.length]),
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.updateAudioFileMetadata()
|
||||
|
||||
@@ -53,7 +53,7 @@ export default {
|
||||
})
|
||||
|
||||
if (!author) {
|
||||
return redirect(`/library/${store.state.libraries.currentLibraryId}/authors`)
|
||||
return redirect(`/library/${store.state.libraries.currentLibraryId}/bookshelf/authors`)
|
||||
}
|
||||
|
||||
if (store.state.libraries.currentLibraryId !== author.libraryId || !store.state.libraries.filterData) {
|
||||
@@ -109,7 +109,7 @@ export default {
|
||||
authorRemoved(author) {
|
||||
if (author.id === this.author.id) {
|
||||
console.warn('Author was removed')
|
||||
this.$router.replace(`/library/${this.currentLibraryId}/authors`)
|
||||
this.$router.replace(`/library/${this.currentLibraryId}/bookshelf/authors`)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
</template>
|
||||
|
||||
<div class="flex justify-between mb-2 place-items-end">
|
||||
<ui-text-input ref="input" v-model="search" placeholder="Search filter.." @input="inputUpdate" clearable class="w-full sm:w-40 h-8 text-sm sm:mb-0" />
|
||||
<ui-text-input ref="input" v-model="search" :placeholder="$strings.PlaceholderSearch" @input="inputUpdate" clearable class="w-full sm:w-40 h-8 text-sm sm:mb-0" />
|
||||
|
||||
<ui-dropdown v-model="newServerSettings.logLevel" label="Server Log Level" :items="logLevelItems" @input="logLevelUpdated" class="w-full sm:w-44" />
|
||||
<ui-dropdown v-model="newServerSettings.logLevel" :label="$strings.LabelServerLogLevel" :items="logLevelItems" @input="logLevelUpdated" class="w-full sm:w-44" />
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
<template>
|
||||
<div class="page" :class="streamLibraryItem ? 'streaming' : ''">
|
||||
<app-book-shelf-toolbar page="authors" is-home :authors="authors" />
|
||||
<div id="bookshelf" class="w-full h-full p-8e overflow-y-auto" :style="{ fontSize: sizeMultiplier + 'rem' }">
|
||||
<!-- Cover size widget -->
|
||||
<widgets-cover-size-widget class="fixed right-4 z-50" :style="{ bottom: streamLibraryItem ? '181px' : '16px' }" />
|
||||
<div class="flex flex-wrap justify-center">
|
||||
<template v-for="author in authorsSorted">
|
||||
<cards-author-card :key="author.id" :author="author" class="p-3e" @edit="editAuthor" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
async asyncData({ store, params, redirect, query, app }) {
|
||||
var libraryId = params.library
|
||||
var libraryData = await store.dispatch('libraries/fetch', libraryId)
|
||||
if (!libraryData) {
|
||||
return redirect('/oops?message=Library not found')
|
||||
}
|
||||
|
||||
const library = libraryData.library
|
||||
if (library.mediaType === 'podcast') {
|
||||
return redirect(`/library/${libraryId}`)
|
||||
}
|
||||
|
||||
return {
|
||||
libraryId
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
authors: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
sizeMultiplier() {
|
||||
return this.$store.getters['user/getSizeMultiplier']
|
||||
},
|
||||
streamLibraryItem() {
|
||||
return this.$store.state.streamLibraryItem
|
||||
},
|
||||
currentLibraryId() {
|
||||
return this.$store.state.libraries.currentLibraryId
|
||||
},
|
||||
selectedAuthor() {
|
||||
return this.$store.state.globals.selectedAuthor
|
||||
},
|
||||
authorSortBy() {
|
||||
return this.$store.getters['user/getUserSetting']('authorSortBy') || 'name'
|
||||
},
|
||||
authorSortDesc() {
|
||||
return !!this.$store.getters['user/getUserSetting']('authorSortDesc')
|
||||
},
|
||||
authorsSorted() {
|
||||
const sortProp = this.authorSortBy
|
||||
const bDesc = this.authorSortDesc ? -1 : 1
|
||||
return this.authors.sort((a, b) => {
|
||||
if (typeof a[sortProp] === 'number' && typeof b[sortProp] === 'number') {
|
||||
// Fallback to name sort if equal
|
||||
if (a[sortProp] === b[sortProp]) return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }) * bDesc
|
||||
return a[sortProp] > b[sortProp] ? bDesc : -bDesc
|
||||
}
|
||||
return a[sortProp]?.localeCompare(b[sortProp], undefined, { sensitivity: 'base' }) * bDesc
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async init() {
|
||||
this.authors = await this.$axios
|
||||
.$get(`/api/libraries/${this.currentLibraryId}/authors`)
|
||||
.then((response) => response.authors)
|
||||
.catch((error) => {
|
||||
console.error('Failed to load authors', error)
|
||||
return []
|
||||
})
|
||||
this.loading = false
|
||||
},
|
||||
authorAdded(author) {
|
||||
if (!this.authors.some((au) => au.id === author.id)) {
|
||||
this.authors.push(author)
|
||||
}
|
||||
},
|
||||
authorUpdated(author) {
|
||||
this.authors = this.authors.map((au) => {
|
||||
if (au.id === author.id) {
|
||||
return author
|
||||
}
|
||||
return au
|
||||
})
|
||||
},
|
||||
authorRemoved(author) {
|
||||
this.authors = this.authors.filter((au) => au.id !== author.id)
|
||||
},
|
||||
editAuthor(author) {
|
||||
this.$store.commit('globals/showEditAuthorModal', author)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.init()
|
||||
this.$root.socket.on('author_added', this.authorAdded)
|
||||
this.$root.socket.on('author_updated', this.authorUpdated)
|
||||
this.$root.socket.on('author_removed', this.authorRemoved)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$root.socket.off('author_added', this.authorAdded)
|
||||
this.$root.socket.off('author_updated', this.authorUpdated)
|
||||
this.$root.socket.off('author_removed', this.authorRemoved)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -27,7 +27,7 @@ export default {
|
||||
|
||||
// Redirect podcast libraries
|
||||
const library = libraryData.library
|
||||
if (library.mediaType === 'podcast' && (params.id === 'collections' || params.id === 'series')) {
|
||||
if (library.mediaType === 'podcast' && (params.id === 'collections' || params.id === 'series' || params.id === 'authors')) {
|
||||
return redirect(`/library/${libraryId}`)
|
||||
}
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ export default {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to updated narrator', error)
|
||||
this.$toast.error('Failed to update narrator')
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div id="bookshelf" class="w-full overflow-y-auto px-2 py-6 sm:px-4 md:p-12 relative">
|
||||
<div class="w-full max-w-4xl mx-auto flex">
|
||||
<form @submit.prevent="submit" class="flex flex-grow">
|
||||
<ui-text-input v-model="searchInput" type="search" :disabled="processing" placeholder="Enter search term or RSS feed URL" class="flex-grow mr-2 text-sm md:text-base" />
|
||||
<ui-text-input v-model="searchInput" type="search" :disabled="processing" :placeholder="$strings.MessagePodcastSearchField" class="flex-grow mr-2 text-sm md:text-base" />
|
||||
<ui-btn type="submit" :disabled="processing" class="hidden md:block">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||
<ui-btn type="submit" :disabled="processing" class="block md:hidden" small>{{ $strings.ButtonSubmit }}</ui-btn>
|
||||
</form>
|
||||
@@ -22,7 +22,7 @@
|
||||
<div class="flex items-center">
|
||||
<a :href="podcast.pageUrl" class="text-base md:text-lg text-gray-200 hover:underline" target="_blank" @click.stop>{{ podcast.title }}</a>
|
||||
<widgets-explicit-indicator v-if="podcast.explicit" />
|
||||
<widgets-already-in-library-indicator :already-in-library="podcast.alreadyInLibrary" />
|
||||
<widgets-already-in-library-indicator v-if="podcast.alreadyInLibrary" />
|
||||
</div>
|
||||
<p class="text-sm md:text-base text-gray-300 whitespace-nowrap truncate">{{ $getString('LabelByAuthor', [podcast.artistName]) }}</p>
|
||||
<p class="text-xs text-gray-400 leading-5">{{ podcast.genres.join(', ') }}</p>
|
||||
@@ -108,7 +108,7 @@ export default {
|
||||
|
||||
if (!txt || !txt.includes('<opml') || !txt.includes('<outline ')) {
|
||||
// Quick lazy check for valid OPML
|
||||
this.$toast.error('Invalid OPML file <opml> tag not found OR an <outline> tag was not found')
|
||||
this.$toast.error(this.$strings.MessageTaskOpmlParseFastFail)
|
||||
this.processing = false
|
||||
return
|
||||
}
|
||||
@@ -117,7 +117,7 @@ export default {
|
||||
.$post(`/api/podcasts/opml/parse`, { opmlText: txt })
|
||||
.then((data) => {
|
||||
if (!data.feeds?.length) {
|
||||
this.$toast.error('No feeds found in OPML file')
|
||||
this.$toast.error(this.$strings.MessageTaskOpmlParseNoneFound)
|
||||
} else {
|
||||
this.opmlFeeds = data.feeds || []
|
||||
this.showOPMLFeedsModal = true
|
||||
@@ -125,7 +125,7 @@ export default {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed', error)
|
||||
this.$toast.error('Failed to parse OPML file')
|
||||
this.$toast.error(this.$strings.MessageTaskOpmlParseFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
@@ -191,7 +191,7 @@ export default {
|
||||
return
|
||||
}
|
||||
if (!podcast.feedUrl) {
|
||||
this.$toast.error('Invalid podcast - no feed')
|
||||
this.$toast.error(this.$strings.MessageNoPodcastFeed)
|
||||
return
|
||||
}
|
||||
this.processing = true
|
||||
@@ -211,15 +211,15 @@ export default {
|
||||
async fetchExistentPodcastsInYourLibrary() {
|
||||
this.processing = true
|
||||
|
||||
const podcasts = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/items?page=0&minified=1`).catch((error) => {
|
||||
const podcastsResponse = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/podcast-titles`).catch((error) => {
|
||||
console.error('Failed to fetch podcasts', error)
|
||||
return []
|
||||
})
|
||||
this.existentPodcasts = podcasts.results.map((p) => {
|
||||
this.existentPodcasts = podcastsResponse.podcasts.map((p) => {
|
||||
return {
|
||||
title: p.media.metadata.title.toLowerCase(),
|
||||
itunesId: p.media.metadata.itunesId,
|
||||
id: p.id
|
||||
title: p.title.toLowerCase(),
|
||||
itunesId: p.itunesId,
|
||||
id: p.libraryItemId
|
||||
}
|
||||
})
|
||||
this.processing = false
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<p v-if="mediaItemShare.playbackSession.displayAuthor" class="text-lg lg:text-xl text-slate-400 font-semibold text-center mb-1 truncate">{{ mediaItemShare.playbackSession.displayAuthor }}</p>
|
||||
|
||||
<div class="w-full pt-16">
|
||||
<player-ui ref="audioPlayer" :chapters="chapters" :paused="isPaused" :loading="!hasLoaded" :is-podcast="false" hide-bookmarks hide-sleep-timer @playPause="playPause" @jumpForward="jumpForward" @jumpBackward="jumpBackward" @setVolume="setVolume" @setPlaybackRate="setPlaybackRate" @seek="seek" />
|
||||
<player-ui ref="audioPlayer" :chapters="chapters" :current-chapter="currentChapter" :paused="isPaused" :loading="!hasLoaded" :is-podcast="false" hide-bookmarks hide-sleep-timer @playPause="playPause" @jumpForward="jumpForward" @jumpBackward="jumpBackward" @setVolume="setVolume" @setPlaybackRate="setPlaybackRate" @seek="seek" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -51,7 +51,8 @@ export default {
|
||||
windowHeight: 0,
|
||||
listeningTimeSinceSync: 0,
|
||||
coverRgb: null,
|
||||
coverBgIsLight: false
|
||||
coverBgIsLight: false,
|
||||
currentTime: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -60,16 +61,10 @@ export default {
|
||||
},
|
||||
coverUrl() {
|
||||
if (!this.playbackSession.coverPath) return `${this.$config.routerBasePath}/book_placeholder.jpg`
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return `http://localhost:3333/public/share/${this.mediaItemShare.slug}/cover`
|
||||
}
|
||||
return `/public/share/${this.mediaItemShare.slug}/cover`
|
||||
return `${this.$config.routerBasePath}/public/share/${this.mediaItemShare.slug}/cover`
|
||||
},
|
||||
audioTracks() {
|
||||
return (this.playbackSession.audioTracks || []).map((track) => {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
track.contentUrl = `${process.env.serverUrl}${track.contentUrl}`
|
||||
}
|
||||
track.relativeContentUrl = track.contentUrl
|
||||
return track
|
||||
})
|
||||
@@ -83,6 +78,9 @@ export default {
|
||||
chapters() {
|
||||
return this.playbackSession.chapters || []
|
||||
},
|
||||
currentChapter() {
|
||||
return this.chapters.find((chapter) => chapter.start <= this.currentTime && this.currentTime < chapter.end)
|
||||
},
|
||||
coverAspectRatio() {
|
||||
const coverAspectRatio = this.playbackSession.coverAspectRatio
|
||||
return coverAspectRatio === this.$constants.BookCoverAspectRatio.STANDARD ? 1.6 : 1
|
||||
@@ -154,6 +152,7 @@ export default {
|
||||
|
||||
// Update UI
|
||||
this.$refs.audioPlayer.setCurrentTime(time)
|
||||
this.currentTime = time
|
||||
},
|
||||
setDuration() {
|
||||
if (!this.localAudioPlayer) return
|
||||
|
||||
@@ -23,10 +23,6 @@ export default class AudioTrack {
|
||||
get relativeContentUrl() {
|
||||
if (!this.contentUrl || this.contentUrl.startsWith('http')) return this.contentUrl
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return `${process.env.serverUrl}${this.contentUrl}?token=${this.userToken}`
|
||||
}
|
||||
|
||||
return this.contentUrl + `?token=${this.userToken}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export default function ({ $axios, store, $config }) {
|
||||
$axios.onRequest(config => {
|
||||
$axios.onRequest((config) => {
|
||||
if (!config.url) {
|
||||
console.error('Axios request invalid config', config)
|
||||
return
|
||||
@@ -13,14 +13,13 @@ export default function ({ $axios, store, $config }) {
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
config.url = `/dev${config.url}`
|
||||
console.log('Making request to ' + config.url)
|
||||
}
|
||||
})
|
||||
|
||||
$axios.onError(error => {
|
||||
$axios.onError((error) => {
|
||||
const code = parseInt(error.response && error.response.status)
|
||||
const message = error.response ? error.response.data || 'Unknown Error' : 'Unknown Error'
|
||||
console.error('Axios error', code, message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+5
-15
@@ -72,13 +72,13 @@ export const state = () => ({
|
||||
}
|
||||
],
|
||||
podcastTypes: [
|
||||
{ text: 'Episodic', value: 'episodic' },
|
||||
{ text: 'Serial', value: 'serial' }
|
||||
{ text: 'Episodic', value: 'episodic', descriptionKey: 'LabelEpisodic' },
|
||||
{ text: 'Serial', value: 'serial', descriptionKey: 'LabelSerial' }
|
||||
],
|
||||
episodeTypes: [
|
||||
{ text: 'Full', value: 'full' },
|
||||
{ text: 'Trailer', value: 'trailer' },
|
||||
{ text: 'Bonus', value: 'bonus' }
|
||||
{ text: 'Full', value: 'full', descriptionKey: 'LabelFull' },
|
||||
{ text: 'Trailer', value: 'trailer', descriptionKey: 'LabelTrailer' },
|
||||
{ text: 'Bonus', value: 'bonus', descriptionKey: 'LabelBonus' }
|
||||
],
|
||||
libraryIcons: ['database', 'audiobookshelf', 'books-1', 'books-2', 'book-1', 'microphone-1', 'microphone-3', 'radio', 'podcast', 'rss', 'headphones', 'music', 'file-picture', 'rocket', 'power', 'star', 'heart']
|
||||
})
|
||||
@@ -98,12 +98,6 @@ export const getters = {
|
||||
const userToken = rootGetters['user/getToken']
|
||||
const lastUpdate = libraryItem.updatedAt || Date.now()
|
||||
const libraryItemId = libraryItem.libraryItemId || libraryItem.id // Workaround for /users/:id page showing media progress covers
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
// Testing
|
||||
return `http://localhost:3333${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}&ts=${lastUpdate}${raw ? '&raw=1' : ''}`
|
||||
}
|
||||
|
||||
return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}&ts=${lastUpdate}${raw ? '&raw=1' : ''}`
|
||||
},
|
||||
getLibraryItemCoverSrcById:
|
||||
@@ -112,10 +106,6 @@ export const getters = {
|
||||
const placeholder = `${rootState.routerBasePath}/book_placeholder.jpg`
|
||||
if (!libraryItemId) return placeholder
|
||||
const userToken = rootGetters['user/getToken']
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
// Testing
|
||||
return `http://localhost:3333${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}${raw ? '&raw=1' : ''}${timestamp ? `&ts=${timestamp}` : ''}`
|
||||
}
|
||||
return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}${raw ? '&raw=1' : ''}${timestamp ? `&ts=${timestamp}` : ''}`
|
||||
},
|
||||
getIsBatchSelectingMediaItems: (state) => {
|
||||
|
||||
@@ -240,7 +240,8 @@ export const mutations = {
|
||||
series: [],
|
||||
narrators: [],
|
||||
languages: [],
|
||||
publishers: []
|
||||
publishers: [],
|
||||
publishedDecades: []
|
||||
}
|
||||
*/
|
||||
const mediaMetadata = libraryItem.media.metadata
|
||||
@@ -307,6 +308,16 @@ export const mutations = {
|
||||
state.filterData.publishers.sort((a, b) => a.localeCompare(b))
|
||||
}
|
||||
|
||||
// Add publishedDecades
|
||||
if (mediaMetadata.publishedYear && !isNaN(mediaMetadata.publishedYear)) {
|
||||
const publishedYear = parseInt(mediaMetadata.publishedYear, 10)
|
||||
const decade = (Math.floor(publishedYear / 10) * 10).toString()
|
||||
if (!state.filterData.publishedDecades.includes(decade)) {
|
||||
state.filterData.publishedDecades.push(decade)
|
||||
state.filterData.publishedDecades.sort((a, b) => a - b)
|
||||
}
|
||||
}
|
||||
|
||||
// Add language
|
||||
if (mediaMetadata.language && !state.filterData.languages.includes(mediaMetadata.language)) {
|
||||
state.filterData.languages.push(mediaMetadata.language)
|
||||
|
||||
@@ -90,7 +90,7 @@ export const actions = {
|
||||
if (state.settings.orderBy == 'media.metadata.publishedYear') {
|
||||
settingsUpdate.orderBy = 'media.metadata.title'
|
||||
}
|
||||
const invalidFilters = ['series', 'authors', 'narrators', 'publishers', 'languages', 'progress', 'issues', 'ebooks', 'abridged']
|
||||
const invalidFilters = ['series', 'authors', 'narrators', 'publishers', 'publishedDecades', 'languages', 'progress', 'issues', 'ebooks', 'abridged']
|
||||
const filterByFirstPart = (state.settings.filterBy || '').split('.').shift()
|
||||
if (invalidFilters.includes(filterByFirstPart)) {
|
||||
settingsUpdate.filterBy = 'all'
|
||||
|
||||
@@ -550,7 +550,7 @@
|
||||
"LabelSleepTimer": "স্লিপ টাইমার",
|
||||
"LabelSlug": "স্লাগ",
|
||||
"LabelStart": "শুরু",
|
||||
"LabelStartTime": "শুরু করার সময়",
|
||||
"LabelStartTime": "শুরুর সময়",
|
||||
"LabelStarted": "শুরু হয়েছে",
|
||||
"LabelStartedAt": "এতে শুরু হয়েছে",
|
||||
"LabelStatsAudioTracks": "অডিও ট্র্যাক",
|
||||
@@ -901,6 +901,7 @@
|
||||
"ToastErrorCannotShare": "এই ডিভাইসে স্থানীয়ভাবে শেয়ার করা যাবে না",
|
||||
"ToastFailedToLoadData": "ডেটা লোড করা যায়নি",
|
||||
"ToastFailedToShare": "শেয়ার করতে ব্যর্থ",
|
||||
"ToastFailedToUpdate": "আপডেট করতে ব্যর্থ হয়েছে",
|
||||
"ToastInvalidImageUrl": "অকার্যকর ছবির ইউআরএল",
|
||||
"ToastInvalidUrl": "অকার্যকর ইউআরএল",
|
||||
"ToastItemCoverUpdateSuccess": "আইটেম কভার আপডেট করা হয়েছে",
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
"ButtonPurgeItemsCache": "Lösche Medien-Cache",
|
||||
"ButtonQueueAddItem": "Zur Warteschlange hinzufügen",
|
||||
"ButtonQueueRemoveItem": "Aus der Warteschlange entfernen",
|
||||
"ButtonQuickEmbed": "Schnelles Hinzufügen",
|
||||
"ButtonQuickEmbedMetadata": "Schnelles Hinzufügen von Metadaten",
|
||||
"ButtonQuickMatch": "Schnellabgleich",
|
||||
"ButtonReScan": "Neu scannen",
|
||||
@@ -225,6 +226,9 @@
|
||||
"LabelAllUsersIncludingGuests": "Alle Benutzer und Gäste",
|
||||
"LabelAlreadyInYourLibrary": "Bereits in der Bibliothek",
|
||||
"LabelAppend": "Anhängen",
|
||||
"LabelAudioBitrate": "Audiobitrate (z. B. 128 kbit/s)",
|
||||
"LabelAudioChannels": "Audiokanäle (1 oder 2)",
|
||||
"LabelAudioCodec": "Audiocodec",
|
||||
"LabelAuthor": "Autor",
|
||||
"LabelAuthorFirstLast": "Autor (Vorname Nachname)",
|
||||
"LabelAuthorLastFirst": "Autor (Nachname, Vorname)",
|
||||
@@ -237,6 +241,7 @@
|
||||
"LabelAutoRegister": "Automatische Registrierung",
|
||||
"LabelAutoRegisterDescription": "Automatische neue Neutzer anlegen nach dem Registrieren",
|
||||
"LabelBackToUser": "Zurück zum Benutzer",
|
||||
"LabelBackupAudioFiles": "Audio-Dateien sichern",
|
||||
"LabelBackupLocation": "Backup-Ort",
|
||||
"LabelBackupsEnableAutomaticBackups": "Automatische Sicherung aktivieren",
|
||||
"LabelBackupsEnableAutomaticBackupsHelp": "Backups werden in /metadata/backups gespeichert",
|
||||
@@ -303,6 +308,14 @@
|
||||
"LabelEmailSettingsTestAddress": "Test-Adresse",
|
||||
"LabelEmbeddedCover": "Eingebettetes Cover",
|
||||
"LabelEnable": "Aktivieren",
|
||||
"LabelEncodingBackupLocation": "Eine Sicherungskopie der originalen Audiodateien wird gespeichert in:",
|
||||
"LabelEncodingChaptersNotEmbedded": "Kapitel sind in mehrspurigen Hörbüchern nicht eingebettet.",
|
||||
"LabelEncodingClearItemCache": "Stelle sicher, dass der Cache regelmäßig geleert wird.",
|
||||
"LabelEncodingFinishedM4B": "Die fertige M4B-Datei wird im Hörbuch-Ordner unter folgendem Pfad abgelegt:",
|
||||
"LabelEncodingInfoEmbedded": "Metadaten werden in die Audiodateien innerhalb des Audiobook Ordners eingebunden.",
|
||||
"LabelEncodingStartedNavigation": "Sobald die Aufgabe gestartet ist, kann die Seite verlassen werden.",
|
||||
"LabelEncodingTimeWarning": "Kodierung kann bis zu 30 Minuten dauern.",
|
||||
"LabelEncodingWarningAdvancedSettings": "Achtung: Ändere diese Einstellungen nur, wenn du dich mit ffmpeg Kodierung auskennst.",
|
||||
"LabelEnd": "Ende",
|
||||
"LabelEndOfChapter": "Ende des Kapitels",
|
||||
"LabelEpisode": "Episode",
|
||||
@@ -465,6 +478,8 @@
|
||||
"LabelPubDate": "Veröffentlichungsdatum",
|
||||
"LabelPublishYear": "Jahr",
|
||||
"LabelPublishedDate": "Veröffentlicht {0}",
|
||||
"LabelPublishedDecade": "Jahrzehnt",
|
||||
"LabelPublishedDecades": "Jahrzehnte",
|
||||
"LabelPublisher": "Herausgeber",
|
||||
"LabelPublishers": "Herausgeber",
|
||||
"LabelRSSFeedCustomOwnerEmail": "Benutzerdefinierte Eigentümer-E-Mail",
|
||||
@@ -499,6 +514,7 @@
|
||||
"LabelSeries": "Serien",
|
||||
"LabelSeriesName": "Serienname",
|
||||
"LabelSeriesProgress": "Serienfortschritt",
|
||||
"LabelServerLogLevel": "Server Log Level",
|
||||
"LabelServerYearReview": "Server Jahr in Übersicht ({0})",
|
||||
"LabelSetEbookAsPrimary": "Als Hauptbuch setzen",
|
||||
"LabelSetEbookAsSupplementary": "Als Ergänzung setzen",
|
||||
@@ -594,6 +610,7 @@
|
||||
"LabelTitle": "Titel",
|
||||
"LabelToolsEmbedMetadata": "Metadaten einbetten",
|
||||
"LabelToolsEmbedMetadataDescription": "Bettet die Metadaten einschließlich des Titelbildes und der Kapitel in die Audiodatein ein.",
|
||||
"LabelToolsM4bEncoder": "M4B Kodierer",
|
||||
"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.",
|
||||
"LabelToolsSplitM4b": "M4B in MP3s aufteilen",
|
||||
@@ -619,6 +636,7 @@
|
||||
"LabelUploaderDragAndDrop": "Ziehen und Ablegen von Dateien oder Ordnern",
|
||||
"LabelUploaderDropFiles": "Dateien löschen",
|
||||
"LabelUploaderItemFetchMetadataHelp": "Automatisches Aktualisieren von Titel, Autor und Serie",
|
||||
"LabelUseAdvancedOptions": "Nutze Erweiterte Optionen",
|
||||
"LabelUseChapterTrack": "Kapiteldatei verwenden",
|
||||
"LabelUseFullTrack": "Gesamte Datei verwenden",
|
||||
"LabelUser": "Benutzer",
|
||||
@@ -667,6 +685,7 @@
|
||||
"MessageConfirmDeleteMetadataProvider": "Möchtest du den benutzerdefinierten Metadatenanbieter \"{0}\" wirklich löschen?",
|
||||
"MessageConfirmDeleteNotification": "Möchtest du diese Benachrichtigung wirklich löschen?",
|
||||
"MessageConfirmDeleteSession": "Sitzung wird gelöscht! Bist du dir sicher?",
|
||||
"MessageConfirmEmbedMetadataInAudioFiles": "Bist du dir sicher, dass die Metadaten in {0} Audiodateien eingebettet werden sollen?",
|
||||
"MessageConfirmForceReScan": "Scanvorgang erzwingen! Bist du dir sicher?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Alle Episoden werden als abgeschlossen markiert! Bist du dir sicher?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Alle Episoden werden als nicht abgeschlossen markiert! Bist du dir sicher?",
|
||||
@@ -744,6 +763,7 @@
|
||||
"MessageNoLogs": "Keine Protokolle",
|
||||
"MessageNoMediaProgress": "Kein Medienfortschritt",
|
||||
"MessageNoNotifications": "Keine Benachrichtigungen",
|
||||
"MessageNoPodcastFeed": "Ungültiger Podcast: Kein Feed",
|
||||
"MessageNoPodcastsFound": "Keine Podcasts gefunden",
|
||||
"MessageNoResults": "Keine Ergebnisse",
|
||||
"MessageNoSearchResultsFor": "Keine Suchergebnisse für \"{0}\"",
|
||||
@@ -791,17 +811,24 @@
|
||||
"MessageTaskFailedToMergeAudioFiles": "Fehler beim zusammenführen der Audiodateien",
|
||||
"MessageTaskFailedToMoveM4bFile": "Fehler beim verschieben der m4b Datei",
|
||||
"MessageTaskFailedToWriteMetadataFile": "Fehler beim schreiben der Metadaten-Datei",
|
||||
"MessageTaskMatchingBooksInLibrary": "Vergleiche Bücher in Bibliothek \"{0}\"",
|
||||
"MessageTaskNoFilesToScan": "Keine Dateien zum scannen",
|
||||
"MessageTaskOpmlImport": "OPML-Import",
|
||||
"MessageTaskOpmlImportDescription": "Podcasts von {0} RSS-Feeds werden ersrtellt",
|
||||
"MessageTaskOpmlImportFeed": "OPML-Feed importieren",
|
||||
"MessageTaskOpmlImportFeedDescription": "RSS-Feed \"{0}\" wird importiert",
|
||||
"MessageTaskOpmlImportFeedFailed": "Podcast Feed konnte nicht geladen werden",
|
||||
"MessageTaskOpmlImportFeedPodcastDescription": "Podcast \"{0}\" wird erstellt",
|
||||
"MessageTaskOpmlImportFeedPodcastExists": "Der Podcast ist bereits im Pfad vorhanden",
|
||||
"MessageTaskOpmlImportFeedPodcastFailed": "Erstellen des Podcasts fehlgeschlagen",
|
||||
"MessageTaskOpmlImportFinished": "{0} Podcasts hinzugefügt",
|
||||
"MessageTaskScanItemsAdded": "{0} hinzugefügt",
|
||||
"MessageTaskScanItemsMissing": "{0} fehlend",
|
||||
"MessageTaskScanItemsUpdated": "{0} aktualisiert",
|
||||
"MessageTaskScanNoChangesNeeded": "Keine Änderungen nötig",
|
||||
"MessageTaskScanningFileChanges": "Überprüfe \"{0}\" nach geänderten Dateien",
|
||||
"MessageTaskScanningLibrary": "Bibliothek \"{0}\" wird durchsucht",
|
||||
"MessageTaskTargetDirectoryNotWritable": "Das Zielverzeichnis ist schreibgeschützt",
|
||||
"MessageThinking": "Nachdenken...",
|
||||
"MessageUploaderItemFailed": "Hochladen fehlgeschlagen",
|
||||
"MessageUploaderItemSuccess": "Erfolgreich hochgeladen!",
|
||||
@@ -894,6 +921,7 @@
|
||||
"ToastErrorCannotShare": "Das kann nicht nativ auf diesem Gerät freigegeben werden",
|
||||
"ToastFailedToLoadData": "Daten laden fehlgeschlagen",
|
||||
"ToastFailedToShare": "Fehler beim Teilen",
|
||||
"ToastFailedToUpdate": "Aktualisierung ist fehlgeschlagen",
|
||||
"ToastInvalidImageUrl": "Ungültiger Bild URL",
|
||||
"ToastInvalidUrl": "Ungültiger URL",
|
||||
"ToastItemCoverUpdateSuccess": "Titelbild aktualisiert",
|
||||
@@ -912,6 +940,7 @@
|
||||
"ToastLibraryScanFailedToStart": "Scan konnte nicht gestartet werden",
|
||||
"ToastLibraryScanStarted": "Bibliotheksscan gestartet",
|
||||
"ToastLibraryUpdateSuccess": "Bibliothek \"{0}\" aktualisiert",
|
||||
"ToastMatchAllAuthorsFailed": "Nicht alle Autoren konnten zugeordnet werden",
|
||||
"ToastNameEmailRequired": "Name und E-Mail sind erforderlich",
|
||||
"ToastNameRequired": "Name ist erforderlich",
|
||||
"ToastNewUserCreatedFailed": "Fehler beim erstellen des Accounts: \"{ 0}\"",
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
"ButtonPurgeItemsCache": "Purge Items Cache",
|
||||
"ButtonQueueAddItem": "Add to queue",
|
||||
"ButtonQueueRemoveItem": "Remove from queue",
|
||||
"ButtonQuickEmbed": "Quick Embed",
|
||||
"ButtonQuickEmbedMetadata": "Quick Embed Metadata",
|
||||
"ButtonQuickMatch": "Quick Match",
|
||||
"ButtonReScan": "Re-Scan",
|
||||
@@ -179,6 +180,7 @@
|
||||
"HeaderRemoveEpisodes": "Remove {0} Episodes",
|
||||
"HeaderSavedMediaProgress": "Saved Media Progress",
|
||||
"HeaderSchedule": "Schedule",
|
||||
"HeaderScheduleEpisodeDownloads": "Schedule Automatic Episode Downloads",
|
||||
"HeaderScheduleLibraryScans": "Schedule Automatic Library Scans",
|
||||
"HeaderSession": "Session",
|
||||
"HeaderSetBackupSchedule": "Set Backup Schedule",
|
||||
@@ -225,6 +227,9 @@
|
||||
"LabelAllUsersIncludingGuests": "All users including guests",
|
||||
"LabelAlreadyInYourLibrary": "Already in your library",
|
||||
"LabelAppend": "Append",
|
||||
"LabelAudioBitrate": "Audio Bitrate (e.g. 128k)",
|
||||
"LabelAudioChannels": "Audio Channels (1 or 2)",
|
||||
"LabelAudioCodec": "Audio Codec",
|
||||
"LabelAuthor": "Author",
|
||||
"LabelAuthorFirstLast": "Author (First Last)",
|
||||
"LabelAuthorLastFirst": "Author (Last, First)",
|
||||
@@ -237,6 +242,7 @@
|
||||
"LabelAutoRegister": "Auto Register",
|
||||
"LabelAutoRegisterDescription": "Automatically create new users after logging in",
|
||||
"LabelBackToUser": "Back to User",
|
||||
"LabelBackupAudioFiles": "Backup Audio Files",
|
||||
"LabelBackupLocation": "Backup Location",
|
||||
"LabelBackupsEnableAutomaticBackups": "Enable automatic backups",
|
||||
"LabelBackupsEnableAutomaticBackupsHelp": "Backups saved in /metadata/backups",
|
||||
@@ -245,15 +251,18 @@
|
||||
"LabelBackupsNumberToKeep": "Number of backups to keep",
|
||||
"LabelBackupsNumberToKeepHelp": "Only 1 backup will be removed at a time so if you already have more backups than this you should manually remove them.",
|
||||
"LabelBitrate": "Bitrate",
|
||||
"LabelBonus": "Bonus",
|
||||
"LabelBooks": "Books",
|
||||
"LabelButtonText": "Button Text",
|
||||
"LabelByAuthor": "by {0}",
|
||||
"LabelChangePassword": "Change Password",
|
||||
"LabelChannels": "Channels",
|
||||
"LabelChapterCount": "{0} Chapters",
|
||||
"LabelChapterTitle": "Chapter Title",
|
||||
"LabelChapters": "Chapters",
|
||||
"LabelChaptersFound": "chapters found",
|
||||
"LabelClickForMoreInfo": "Click for more info",
|
||||
"LabelClickToUseCurrentValue": "Click to use current value",
|
||||
"LabelClosePlayer": "Close player",
|
||||
"LabelCodec": "Codec",
|
||||
"LabelCollapseSeries": "Collapse Series",
|
||||
@@ -303,12 +312,25 @@
|
||||
"LabelEmailSettingsTestAddress": "Test Address",
|
||||
"LabelEmbeddedCover": "Embedded Cover",
|
||||
"LabelEnable": "Enable",
|
||||
"LabelEncodingBackupLocation": "A backup of your original audio files will be stored in:",
|
||||
"LabelEncodingChaptersNotEmbedded": "Chapters are not embedded in multi-track audiobooks.",
|
||||
"LabelEncodingClearItemCache": "Make sure to periodically purge items cache.",
|
||||
"LabelEncodingFinishedM4B": "Finished M4B will be put into your audiobook folder at:",
|
||||
"LabelEncodingInfoEmbedded": "Metadata will be embedded in the audio tracks inside your audiobook folder.",
|
||||
"LabelEncodingStartedNavigation": "Once the task is started you can navigate away from this page.",
|
||||
"LabelEncodingTimeWarning": "Encoding can take up to 30 minutes.",
|
||||
"LabelEncodingWarningAdvancedSettings": "Warning: Do not update these settings unless you are familiar with ffmpeg encoding options.",
|
||||
"LabelEncodingWatcherDisabled": "If you have the watcher disabled you will need to re-scan this audiobook afterwards.",
|
||||
"LabelEnd": "End",
|
||||
"LabelEndOfChapter": "End of Chapter",
|
||||
"LabelEpisode": "Episode",
|
||||
"LabelEpisodeNotLinkedToRssFeed": "Episode not linked to RSS feed",
|
||||
"LabelEpisodeNumber": "Episode #{0}",
|
||||
"LabelEpisodeTitle": "Episode Title",
|
||||
"LabelEpisodeType": "Episode Type",
|
||||
"LabelEpisodeUrlFromRssFeed": "Episode URL from RSS feed",
|
||||
"LabelEpisodes": "Episodes",
|
||||
"LabelEpisodic": "Episodic",
|
||||
"LabelExample": "Example",
|
||||
"LabelExpandSeries": "Expand Series",
|
||||
"LabelExpandSubSeries": "Expand Sub Series",
|
||||
@@ -336,6 +358,7 @@
|
||||
"LabelFontScale": "Font scale",
|
||||
"LabelFontStrikethrough": "Strikethrough",
|
||||
"LabelFormat": "Format",
|
||||
"LabelFull": "Full",
|
||||
"LabelGenre": "Genre",
|
||||
"LabelGenres": "Genres",
|
||||
"LabelHardDeleteFile": "Hard delete file",
|
||||
@@ -391,6 +414,10 @@
|
||||
"LabelLowestPriority": "Lowest Priority",
|
||||
"LabelMatchExistingUsersBy": "Match existing users by",
|
||||
"LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider",
|
||||
"LabelMaxEpisodesToDownload": "Max # of episodes to download. Use 0 for unlimited.",
|
||||
"LabelMaxEpisodesToDownloadPerCheck": "Max # of new episodes to download per check",
|
||||
"LabelMaxEpisodesToKeep": "Max # of episodes to keep",
|
||||
"LabelMaxEpisodesToKeepHelp": "Value of 0 sets no max limit. After a new episode is auto-downloaded this will delete the oldest episode if you have more than X episodes. This will only delete 1 episode per new download.",
|
||||
"LabelMediaPlayer": "Media Player",
|
||||
"LabelMediaType": "Media Type",
|
||||
"LabelMetaTag": "Meta Tag",
|
||||
@@ -465,6 +492,8 @@
|
||||
"LabelPubDate": "Pub Date",
|
||||
"LabelPublishYear": "Publish Year",
|
||||
"LabelPublishedDate": "Published {0}",
|
||||
"LabelPublishedDecade": "Published Decade",
|
||||
"LabelPublishedDecades": "Published Decades",
|
||||
"LabelPublisher": "Publisher",
|
||||
"LabelPublishers": "Publishers",
|
||||
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
|
||||
@@ -484,21 +513,28 @@
|
||||
"LabelRedo": "Redo",
|
||||
"LabelRegion": "Region",
|
||||
"LabelReleaseDate": "Release Date",
|
||||
"LabelRemoveAllMetadataAbs": "Remove all metadata.abs files",
|
||||
"LabelRemoveAllMetadataJson": "Remove all metadata.json files",
|
||||
"LabelRemoveCover": "Remove cover",
|
||||
"LabelRemoveMetadataFile": "Remove metadata files in library item folders",
|
||||
"LabelRemoveMetadataFileHelp": "Remove all metadata.json and metadata.abs files in your {0} folders.",
|
||||
"LabelRowsPerPage": "Rows per page",
|
||||
"LabelSearchTerm": "Search Term",
|
||||
"LabelSearchTitle": "Search Title",
|
||||
"LabelSearchTitleOrASIN": "Search Title or ASIN",
|
||||
"LabelSeason": "Season",
|
||||
"LabelSeasonNumber": "Season #{0}",
|
||||
"LabelSelectAll": "Select all",
|
||||
"LabelSelectAllEpisodes": "Select all episodes",
|
||||
"LabelSelectEpisodesShowing": "Select {0} episodes showing",
|
||||
"LabelSelectUsers": "Select users",
|
||||
"LabelSendEbookToDevice": "Send Ebook to...",
|
||||
"LabelSequence": "Sequence",
|
||||
"LabelSerial": "Serial",
|
||||
"LabelSeries": "Series",
|
||||
"LabelSeriesName": "Series Name",
|
||||
"LabelSeriesProgress": "Series Progress",
|
||||
"LabelServerLogLevel": "Server Log Level",
|
||||
"LabelServerYearReview": "Server Year in Review ({0})",
|
||||
"LabelSetEbookAsPrimary": "Set as primary",
|
||||
"LabelSetEbookAsSupplementary": "Set as supplementary",
|
||||
@@ -587,6 +623,7 @@
|
||||
"LabelTimeDurationXMinutes": "{0} minutes",
|
||||
"LabelTimeDurationXSeconds": "{0} seconds",
|
||||
"LabelTimeInMinutes": "Time in minutes",
|
||||
"LabelTimeLeft": "{0} left",
|
||||
"LabelTimeListened": "Time Listened",
|
||||
"LabelTimeListenedToday": "Time Listened Today",
|
||||
"LabelTimeRemaining": "{0} remaining",
|
||||
@@ -594,6 +631,7 @@
|
||||
"LabelTitle": "Title",
|
||||
"LabelToolsEmbedMetadata": "Embed Metadata",
|
||||
"LabelToolsEmbedMetadataDescription": "Embed metadata into audio files including cover image and chapters.",
|
||||
"LabelToolsM4bEncoder": "M4B Encoder",
|
||||
"LabelToolsMakeM4b": "Make M4B Audiobook File",
|
||||
"LabelToolsMakeM4bDescription": "Generate a .M4B audiobook file with embedded metadata, cover image, and chapters.",
|
||||
"LabelToolsSplitM4b": "Split M4B to MP3's",
|
||||
@@ -606,6 +644,7 @@
|
||||
"LabelTracksMultiTrack": "Multi-track",
|
||||
"LabelTracksNone": "No tracks",
|
||||
"LabelTracksSingleTrack": "Single-track",
|
||||
"LabelTrailer": "Trailer",
|
||||
"LabelType": "Type",
|
||||
"LabelUnabridged": "Unabridged",
|
||||
"LabelUndo": "Undo",
|
||||
@@ -619,8 +658,10 @@
|
||||
"LabelUploaderDragAndDrop": "Drag & drop files or folders",
|
||||
"LabelUploaderDropFiles": "Drop files",
|
||||
"LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
|
||||
"LabelUseAdvancedOptions": "Use Advanced Options",
|
||||
"LabelUseChapterTrack": "Use chapter track",
|
||||
"LabelUseFullTrack": "Use full track",
|
||||
"LabelUseZeroForUnlimited": "Use 0 for unlimited",
|
||||
"LabelUser": "User",
|
||||
"LabelUsername": "Username",
|
||||
"LabelValue": "Value",
|
||||
@@ -667,6 +708,7 @@
|
||||
"MessageConfirmDeleteMetadataProvider": "Are you sure you want to delete custom metadata provider \"{0}\"?",
|
||||
"MessageConfirmDeleteNotification": "Are you sure you want to delete this notification?",
|
||||
"MessageConfirmDeleteSession": "Are you sure you want to delete this session?",
|
||||
"MessageConfirmEmbedMetadataInAudioFiles": "Are you sure you want to embed metadata in {0} audio files?",
|
||||
"MessageConfirmForceReScan": "Are you sure you want to force re-scan?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
|
||||
@@ -678,6 +720,7 @@
|
||||
"MessageConfirmPurgeCache": "Purge cache will delete the entire directory at <code>/metadata/cache</code>. <br /><br />Are you sure you want to remove the cache directory?",
|
||||
"MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at <code>/metadata/cache/items</code>.<br />Are you sure?",
|
||||
"MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?",
|
||||
"MessageConfirmQuickMatchEpisodes": "Quick matching episodes will overwrite details if a match is found. Only unmatched episodes will be updated. Are you sure?",
|
||||
"MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?",
|
||||
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
|
||||
"MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?",
|
||||
@@ -685,6 +728,7 @@
|
||||
"MessageConfirmRemoveEpisode": "Are you sure you want to remove episode \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} episodes?",
|
||||
"MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
|
||||
"MessageConfirmRemoveMetadataFiles": "Are you sure you want to remove all metadata.{0} files in your library item folders?",
|
||||
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
|
||||
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
|
||||
"MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?",
|
||||
@@ -700,6 +744,7 @@
|
||||
"MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
|
||||
"MessageEmbedFailed": "Embed Failed!",
|
||||
"MessageEmbedFinished": "Embed Finished!",
|
||||
"MessageEmbedQueue": "Queued for metadata embed ({0} in queue)",
|
||||
"MessageEpisodesQueuedForDownload": "{0} Episode(s) queued for download",
|
||||
"MessageEreaderDevices": "To ensure delivery of ebooks, you may need to add the above email address as a valid sender for each device listed below.",
|
||||
"MessageFeedURLWillBe": "Feed URL will be {0}",
|
||||
@@ -744,6 +789,7 @@
|
||||
"MessageNoLogs": "No Logs",
|
||||
"MessageNoMediaProgress": "No Media Progress",
|
||||
"MessageNoNotifications": "No Notifications",
|
||||
"MessageNoPodcastFeed": "Invalid podcast: No Feed",
|
||||
"MessageNoPodcastsFound": "No podcasts found",
|
||||
"MessageNoResults": "No Results",
|
||||
"MessageNoSearchResultsFor": "No search results for \"{0}\"",
|
||||
@@ -760,6 +806,10 @@
|
||||
"MessagePlaylistCreateFromCollection": "Create playlist from collection",
|
||||
"MessagePleaseWait": "Please wait...",
|
||||
"MessagePodcastHasNoRSSFeedForMatching": "Podcast has no RSS feed url to use for matching",
|
||||
"MessagePodcastSearchField": "Enter search term or RSS feed URL",
|
||||
"MessageQuickEmbedInProgress": "Quick embed in progress",
|
||||
"MessageQuickEmbedQueue": "Queued for quick embed ({0} in queue)",
|
||||
"MessageQuickMatchAllEpisodes": "Quick Match All Episodes",
|
||||
"MessageQuickMatchDescription": "Populate empty item details & cover with first match result from '{0}'. Does not overwrite details unless 'Prefer matched metadata' server setting is enabled.",
|
||||
"MessageRemoveChapter": "Remove chapter",
|
||||
"MessageRemoveEpisodes": "Remove {0} episode(s)",
|
||||
@@ -802,6 +852,9 @@
|
||||
"MessageTaskOpmlImportFeedPodcastExists": "Podcast already exists at path",
|
||||
"MessageTaskOpmlImportFeedPodcastFailed": "Failed to create podcast",
|
||||
"MessageTaskOpmlImportFinished": "Added {0} podcasts",
|
||||
"MessageTaskOpmlParseFailed": "Failed to parse OPML file",
|
||||
"MessageTaskOpmlParseFastFail": "Invalid OPML file <opml> tag not found OR an <outline> tag was not found",
|
||||
"MessageTaskOpmlParseNoneFound": "No feeds found in OPML file",
|
||||
"MessageTaskScanItemsAdded": "{0} added",
|
||||
"MessageTaskScanItemsMissing": "{0} missing",
|
||||
"MessageTaskScanItemsUpdated": "{0} updated",
|
||||
@@ -826,6 +879,10 @@
|
||||
"NoteUploaderFoldersWithMediaFiles": "Folders with media files will be handled as separate library items.",
|
||||
"NoteUploaderOnlyAudioFiles": "If uploading only audio files then each audio file will be handled as a separate audiobook.",
|
||||
"NoteUploaderUnsupportedFiles": "Unsupported files are ignored. When choosing or dropping a folder, other files that are not in an item folder are ignored.",
|
||||
"NotificationOnBackupCompletedDescription": "Triggered when a backup is completed",
|
||||
"NotificationOnBackupFailedDescription": "Triggered when a backup fails",
|
||||
"NotificationOnEpisodeDownloadedDescription": "Triggered when a podcast episode is auto-downloaded",
|
||||
"NotificationOnTestDescription": "Event for testing the notification system",
|
||||
"PlaceholderNewCollection": "New collection name",
|
||||
"PlaceholderNewFolderPath": "New folder path",
|
||||
"PlaceholderNewPlaylist": "New playlist name",
|
||||
@@ -851,6 +908,7 @@
|
||||
"StatsYearInReview": "YEAR IN REVIEW",
|
||||
"ToastAccountUpdateSuccess": "Account updated",
|
||||
"ToastAppriseUrlRequired": "Must enter an Apprise URL",
|
||||
"ToastAsinRequired": "ASIN is required",
|
||||
"ToastAuthorImageRemoveSuccess": "Author image removed",
|
||||
"ToastAuthorNotFound": "Author \"{0}\" not found",
|
||||
"ToastAuthorRemoveSuccess": "Author removed",
|
||||
@@ -870,6 +928,8 @@
|
||||
"ToastBackupUploadSuccess": "Backup uploaded",
|
||||
"ToastBatchDeleteFailed": "Batch delete failed",
|
||||
"ToastBatchDeleteSuccess": "Batch delete success",
|
||||
"ToastBatchQuickMatchFailed": "Batch Quick Match failed!",
|
||||
"ToastBatchQuickMatchStarted": "Batch Quick Match of {0} books started!",
|
||||
"ToastBatchUpdateFailed": "Batch update failed",
|
||||
"ToastBatchUpdateSuccess": "Batch update success",
|
||||
"ToastBookmarkCreateFailed": "Failed to create bookmark",
|
||||
@@ -881,6 +941,7 @@
|
||||
"ToastChaptersHaveErrors": "Chapters have errors",
|
||||
"ToastChaptersMustHaveTitles": "Chapters must have titles",
|
||||
"ToastChaptersRemoved": "Chapters removed",
|
||||
"ToastChaptersUpdated": "Chapters updated",
|
||||
"ToastCollectionItemsAddFailed": "Item(s) added to collection failed",
|
||||
"ToastCollectionItemsAddSuccess": "Item(s) added to collection success",
|
||||
"ToastCollectionItemsRemoveSuccess": "Item(s) removed from collection",
|
||||
@@ -898,11 +959,14 @@
|
||||
"ToastEncodeCancelSucces": "Encode canceled",
|
||||
"ToastEpisodeDownloadQueueClearFailed": "Failed to clear queue",
|
||||
"ToastEpisodeDownloadQueueClearSuccess": "Episode download queue cleared",
|
||||
"ToastEpisodeUpdateSuccess": "{0} episodes updated",
|
||||
"ToastErrorCannotShare": "Cannot share natively on this device",
|
||||
"ToastFailedToLoadData": "Failed to load data",
|
||||
"ToastFailedToMatch": "Failed to match",
|
||||
"ToastFailedToShare": "Failed to share",
|
||||
"ToastFailedToUpdate": "Failed to update",
|
||||
"ToastInvalidImageUrl": "Invalid image URL",
|
||||
"ToastInvalidMaxEpisodesToDownload": "Invalid max episodes to download",
|
||||
"ToastInvalidUrl": "Invalid URL",
|
||||
"ToastItemCoverUpdateSuccess": "Item cover updated",
|
||||
"ToastItemDeletedFailed": "Failed to delete item",
|
||||
@@ -920,14 +984,22 @@
|
||||
"ToastLibraryScanFailedToStart": "Failed to start scan",
|
||||
"ToastLibraryScanStarted": "Library scan started",
|
||||
"ToastLibraryUpdateSuccess": "Library \"{0}\" updated",
|
||||
"ToastMatchAllAuthorsFailed": "Failed to match all authors",
|
||||
"ToastMetadataFilesRemovedError": "Error removing metadata.{0} files",
|
||||
"ToastMetadataFilesRemovedNoneFound": "No metadata.{0} files found in library",
|
||||
"ToastMetadataFilesRemovedNoneRemoved": "No metadata.{0} files removed",
|
||||
"ToastMetadataFilesRemovedSuccess": "{0} metadata.{1} files removed",
|
||||
"ToastMustHaveAtLeastOnePath": "Must have at least one path",
|
||||
"ToastNameEmailRequired": "Name and email are required",
|
||||
"ToastNameRequired": "Name is required",
|
||||
"ToastNewEpisodesFound": "{0} new episodes found",
|
||||
"ToastNewUserCreatedFailed": "Failed to create account: \"{0}\"",
|
||||
"ToastNewUserCreatedSuccess": "New account created",
|
||||
"ToastNewUserLibraryError": "Must select at least one library",
|
||||
"ToastNewUserPasswordError": "Must have a password, only root user can have an empty password",
|
||||
"ToastNewUserTagError": "Must select at least one tag",
|
||||
"ToastNewUserUsernameError": "Enter a username",
|
||||
"ToastNoNewEpisodesFound": "No new episodes found",
|
||||
"ToastNoUpdatesNecessary": "No updates necessary",
|
||||
"ToastNotificationCreateFailed": "Failed to create notification",
|
||||
"ToastNotificationDeleteFailed": "Failed to delete notification",
|
||||
@@ -946,6 +1018,7 @@
|
||||
"ToastPodcastGetFeedFailed": "Failed to get podcast feed",
|
||||
"ToastPodcastNoEpisodesInFeed": "No episodes found in RSS feed",
|
||||
"ToastPodcastNoRssFeed": "Podcast does not have an RSS feed",
|
||||
"ToastProgressIsNotBeingSynced": "Progress is not being synced, restart playback",
|
||||
"ToastProviderCreatedFailed": "Failed to add provider",
|
||||
"ToastProviderCreatedSuccess": "New provider added",
|
||||
"ToastProviderNameAndUrlRequired": "Name and Url required",
|
||||
@@ -972,6 +1045,7 @@
|
||||
"ToastSessionCloseFailed": "Failed to close session",
|
||||
"ToastSessionDeleteFailed": "Failed to delete session",
|
||||
"ToastSessionDeleteSuccess": "Session deleted",
|
||||
"ToastSleepTimerDone": "Sleep timer done... zZzzZz",
|
||||
"ToastSlugMustChange": "Slug contains invalid characters",
|
||||
"ToastSlugRequired": "Slug is required",
|
||||
"ToastSocketConnected": "Socket connected",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
+42
-1
@@ -66,6 +66,7 @@
|
||||
"ButtonPurgeItemsCache": "Purgar Elementos de Cache",
|
||||
"ButtonQueueAddItem": "Agregar a la Fila",
|
||||
"ButtonQueueRemoveItem": "Remover de la Fila",
|
||||
"ButtonQuickEmbed": "Inserción rápida",
|
||||
"ButtonQuickEmbedMetadata": "Agregue metadatos rápidamente",
|
||||
"ButtonQuickMatch": "Encontrar Rápido",
|
||||
"ButtonReScan": "Re-Escanear",
|
||||
@@ -179,6 +180,7 @@
|
||||
"HeaderRemoveEpisodes": "Remover {0} Episodios",
|
||||
"HeaderSavedMediaProgress": "Guardar Progreso de Multimedia",
|
||||
"HeaderSchedule": "Horario",
|
||||
"HeaderScheduleEpisodeDownloads": "Programar descargas automáticas de episodios",
|
||||
"HeaderScheduleLibraryScans": "Programar Escaneo Automático de Biblioteca",
|
||||
"HeaderSession": "Sesión",
|
||||
"HeaderSetBackupSchedule": "Programar Respaldo",
|
||||
@@ -225,6 +227,9 @@
|
||||
"LabelAllUsersIncludingGuests": "Todos los usuarios e invitados",
|
||||
"LabelAlreadyInYourLibrary": "Ya existe en la Biblioteca",
|
||||
"LabelAppend": "Adjuntar",
|
||||
"LabelAudioBitrate": "Tasa de bits del audio (por ejemplo, 128k)",
|
||||
"LabelAudioChannels": "Canales de audio (1 o 2)",
|
||||
"LabelAudioCodec": "Códec de audio",
|
||||
"LabelAuthor": "Autor",
|
||||
"LabelAuthorFirstLast": "Autor (Nombre Apellido)",
|
||||
"LabelAuthorLastFirst": "Autor (Apellido, Nombre)",
|
||||
@@ -237,6 +242,7 @@
|
||||
"LabelAutoRegister": "Registro automático",
|
||||
"LabelAutoRegisterDescription": "Crear usuarios automáticamente tras iniciar sesión",
|
||||
"LabelBackToUser": "Regresar a Usuario",
|
||||
"LabelBackupAudioFiles": "Copia de seguridad de archivos de audio",
|
||||
"LabelBackupLocation": "Ubicación del Respaldo",
|
||||
"LabelBackupsEnableAutomaticBackups": "Habilitar Respaldo Automático",
|
||||
"LabelBackupsEnableAutomaticBackupsHelp": "Respaldo Guardado en /metadata/backups",
|
||||
@@ -250,10 +256,12 @@
|
||||
"LabelByAuthor": "por {0}",
|
||||
"LabelChangePassword": "Cambiar Contraseña",
|
||||
"LabelChannels": "Canales",
|
||||
"LabelChapterCount": "{0} capítulos",
|
||||
"LabelChapterTitle": "Titulo del Capítulo",
|
||||
"LabelChapters": "Capítulos",
|
||||
"LabelChaptersFound": "Capítulo Encontrado",
|
||||
"LabelClickForMoreInfo": "Click para más información",
|
||||
"LabelClickToUseCurrentValue": "Haz clic para utilizar el valor actual",
|
||||
"LabelClosePlayer": "Cerrar reproductor",
|
||||
"LabelCodec": "Codec",
|
||||
"LabelCollapseSeries": "Colapsar serie",
|
||||
@@ -303,11 +311,23 @@
|
||||
"LabelEmailSettingsTestAddress": "Probar Dirección",
|
||||
"LabelEmbeddedCover": "Portada Integrada",
|
||||
"LabelEnable": "Habilitar",
|
||||
"LabelEncodingBackupLocation": "Se guardará una copia de seguridad de tus archivos de audio originales en:",
|
||||
"LabelEncodingChaptersNotEmbedded": "Los capítulos no se incrustan en los audiolibros multipista.",
|
||||
"LabelEncodingClearItemCache": "Asegúrese de purgar periódicamente la caché.",
|
||||
"LabelEncodingFinishedM4B": "El M4B terminado se colocará en su carpeta de audiolibros en:",
|
||||
"LabelEncodingInfoEmbedded": "Los metadatos se integrarán en las pistas de audio dentro de la carpeta de audiolibros.",
|
||||
"LabelEncodingStartedNavigation": "Una vez iniciada la tarea, puedes salir de esta página.",
|
||||
"LabelEncodingTimeWarning": "La codificación puede tardar hasta 30 minutos.",
|
||||
"LabelEncodingWarningAdvancedSettings": "Advertencia: No actualice esta configuración a menos que esté familiarizado con las opciones de codificación de ffmpeg.",
|
||||
"LabelEncodingWatcherDisabled": "Si ha desactivado la supervisión de los archivos, deberá volver a escanear este audiolibro más adelante.",
|
||||
"LabelEnd": "Fin",
|
||||
"LabelEndOfChapter": "Fin del capítulo",
|
||||
"LabelEpisode": "Episodio",
|
||||
"LabelEpisodeNotLinkedToRssFeed": "Episodio no enlazado al feed RSS",
|
||||
"LabelEpisodeNumber": "Episodio #{0}",
|
||||
"LabelEpisodeTitle": "Titulo de Episodio",
|
||||
"LabelEpisodeType": "Tipo de Episodio",
|
||||
"LabelEpisodeUrlFromRssFeed": "URL del episodio del feed RSS",
|
||||
"LabelEpisodes": "Episodios",
|
||||
"LabelExample": "Ejemplo",
|
||||
"LabelExpandSeries": "Ampliar serie",
|
||||
@@ -336,6 +356,7 @@
|
||||
"LabelFontScale": "Tamaño de fuente",
|
||||
"LabelFontStrikethrough": "Tachado",
|
||||
"LabelFormat": "Formato",
|
||||
"LabelFull": "Completo",
|
||||
"LabelGenre": "Genero",
|
||||
"LabelGenres": "Géneros",
|
||||
"LabelHardDeleteFile": "Eliminar Definitivamente",
|
||||
@@ -391,6 +412,7 @@
|
||||
"LabelLowestPriority": "Menor prioridad",
|
||||
"LabelMatchExistingUsersBy": "Emparejar a los usuarios existentes por",
|
||||
"LabelMatchExistingUsersByDescription": "Se utiliza para conectar usuarios existentes. Una vez conectados, los usuarios serán emparejados por un identificador único de su proveedor de SSO",
|
||||
"LabelMaxEpisodesToDownload": "Número máximo # de episodios para descargar. Usa 0 para descargar una cantidad ilimitada.",
|
||||
"LabelMediaPlayer": "Reproductor de Medios",
|
||||
"LabelMediaType": "Tipo de multimedia",
|
||||
"LabelMetaTag": "Metaetiqueta",
|
||||
@@ -465,6 +487,8 @@
|
||||
"LabelPubDate": "Fecha de publicación",
|
||||
"LabelPublishYear": "Año de publicación",
|
||||
"LabelPublishedDate": "Publicado {0}",
|
||||
"LabelPublishedDecade": "Década de publicación",
|
||||
"LabelPublishedDecades": "Décadas publicadas",
|
||||
"LabelPublisher": "Editor",
|
||||
"LabelPublishers": "Editores",
|
||||
"LabelRSSFeedCustomOwnerEmail": "Correo electrónico de dueño personalizado",
|
||||
@@ -499,6 +523,7 @@
|
||||
"LabelSeries": "Series",
|
||||
"LabelSeriesName": "Nombre de la Serie",
|
||||
"LabelSeriesProgress": "Progreso de la Serie",
|
||||
"LabelServerLogLevel": "Nivel de registro del servidor",
|
||||
"LabelServerYearReview": "Resumen del año del servidor ({0})",
|
||||
"LabelSetEbookAsPrimary": "Establecer como primario",
|
||||
"LabelSetEbookAsSupplementary": "Establecer como suplementario",
|
||||
@@ -594,6 +619,7 @@
|
||||
"LabelTitle": "Título",
|
||||
"LabelToolsEmbedMetadata": "Incrustar Metadatos",
|
||||
"LabelToolsEmbedMetadataDescription": "Incrusta metadatos en los archivos de audio, incluyendo la portada y capítulos.",
|
||||
"LabelToolsM4bEncoder": "Codificador M4B",
|
||||
"LabelToolsMakeM4b": "Hacer Archivo de Audiolibro M4B",
|
||||
"LabelToolsMakeM4bDescription": "Generar archivo de audiolibro .M4B con metadatos, imágenes de portada y capítulos incorporados.",
|
||||
"LabelToolsSplitM4b": "Dividir M4B en Archivos MP3",
|
||||
@@ -619,6 +645,7 @@
|
||||
"LabelUploaderDragAndDrop": "Arrastre y suelte archivos o carpetas",
|
||||
"LabelUploaderDropFiles": "Suelte los Archivos",
|
||||
"LabelUploaderItemFetchMetadataHelp": "Buscar título, autor y series automáticamente",
|
||||
"LabelUseAdvancedOptions": "Usar opciones avanzadas",
|
||||
"LabelUseChapterTrack": "Usar pista por capitulo",
|
||||
"LabelUseFullTrack": "Usar pista completa",
|
||||
"LabelUser": "Usuario",
|
||||
@@ -667,6 +694,7 @@
|
||||
"MessageConfirmDeleteMetadataProvider": "¿Estás seguro de que deseas eliminar el proveedor de metadatos personalizado \"{0}\"?",
|
||||
"MessageConfirmDeleteNotification": "¿Estás seguro de que deseas eliminar esta notificación?",
|
||||
"MessageConfirmDeleteSession": "¿Está seguro de que desea eliminar esta sesión?",
|
||||
"MessageConfirmEmbedMetadataInAudioFiles": "¿Está seguro de que desea incrustar metadatos en {0} archivos de audio?",
|
||||
"MessageConfirmForceReScan": "¿Está seguro de que desea forzar un re-escaneo?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "¿Está seguro de que desea marcar todos los episodios como terminados?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "¿Está seguro de que desea marcar todos los episodios como no terminados?",
|
||||
@@ -700,6 +728,7 @@
|
||||
"MessageDragFilesIntoTrackOrder": "Arrastra los archivos al orden correcto de las pistas",
|
||||
"MessageEmbedFailed": "¡Error al insertar!",
|
||||
"MessageEmbedFinished": "Incrustación Terminada!",
|
||||
"MessageEmbedQueue": "En cola para incrustar metadatos ({0} en cola)",
|
||||
"MessageEpisodesQueuedForDownload": "{0} Episodio(s) en cola para descargar",
|
||||
"MessageEreaderDevices": "Para garantizar la entrega de libros electrónicos, es posible que tenga que agregar la dirección de correo electrónico anterior como remitente válido para cada dispositivo enumerado a continuación.",
|
||||
"MessageFeedURLWillBe": "URL de la fuente será {0}",
|
||||
@@ -744,6 +773,7 @@
|
||||
"MessageNoLogs": "No hay logs",
|
||||
"MessageNoMediaProgress": "Multimedia sin Progreso",
|
||||
"MessageNoNotifications": "Ninguna Notificación",
|
||||
"MessageNoPodcastFeed": "Podcast no válido: Sin feed",
|
||||
"MessageNoPodcastsFound": "Ningún podcast encontrado",
|
||||
"MessageNoResults": "Sin Resultados",
|
||||
"MessageNoSearchResultsFor": "No hay resultados para la búsqueda \"{0}\"",
|
||||
@@ -760,6 +790,9 @@
|
||||
"MessagePlaylistCreateFromCollection": "Crear una lista de reproducción a partir de una colección",
|
||||
"MessagePleaseWait": "Por favor, espera...",
|
||||
"MessagePodcastHasNoRSSFeedForMatching": "El podcast no tiene una URL de fuente RSS que pueda usar",
|
||||
"MessagePodcastSearchField": "Introduzca el término de búsqueda o la URL de la fuente RSS",
|
||||
"MessageQuickEmbedInProgress": "Integración rápida en proceso",
|
||||
"MessageQuickEmbedQueue": "En cola para inserción rápida ({0} en cola)",
|
||||
"MessageQuickMatchDescription": "Rellenar detalles de elementos vacíos y portada con los primeros resultados de '{0}'. No sobrescribe los detalles a menos que la opción \"Preferir Metadatos Encontrados\" del servidor esté habilitada.",
|
||||
"MessageRemoveChapter": "Remover capítulos",
|
||||
"MessageRemoveEpisodes": "Remover {0} episodio(s)",
|
||||
@@ -802,6 +835,9 @@
|
||||
"MessageTaskOpmlImportFeedPodcastExists": "Podcast ya existe en la ruta",
|
||||
"MessageTaskOpmlImportFeedPodcastFailed": "Error al crear podcast",
|
||||
"MessageTaskOpmlImportFinished": "Añadido {0} podcasts",
|
||||
"MessageTaskOpmlParseFailed": "No se pudo analizar el archivo OPML",
|
||||
"MessageTaskOpmlParseFastFail": "No se encontró la etiqueta <opml> del archivo OPML no válido O no se encontró la etiqueta <outline>",
|
||||
"MessageTaskOpmlParseNoneFound": "No se encontraron fuentes en el archivo OPML",
|
||||
"MessageTaskScanItemsAdded": "{0} añadido",
|
||||
"MessageTaskScanItemsMissing": "Falta {0}",
|
||||
"MessageTaskScanItemsUpdated": "{0} actualizado",
|
||||
@@ -826,6 +862,10 @@
|
||||
"NoteUploaderFoldersWithMediaFiles": "Las carpetas con archivos multimedia se manejarán como elementos separados en la biblioteca.",
|
||||
"NoteUploaderOnlyAudioFiles": "Si sube solamente archivos de audio, cada archivo se manejará como un audiolibro por separado.",
|
||||
"NoteUploaderUnsupportedFiles": "Se ignorarán los archivos no soportados. Al elegir o arrastrar una carpeta, los archivos que no estén dentro de una subcarpeta serán ignorados.",
|
||||
"NotificationOnBackupCompletedDescription": "Se activa cuando se completa una copia de seguridad",
|
||||
"NotificationOnBackupFailedDescription": "Se activa cuando falla una copia de seguridad",
|
||||
"NotificationOnEpisodeDownloadedDescription": "Se activa cuando se descarga automáticamente un episodio de un podcast",
|
||||
"NotificationOnTestDescription": "Evento para probar el sistema de notificaciones",
|
||||
"PlaceholderNewCollection": "Nuevo nombre de la colección",
|
||||
"PlaceholderNewFolderPath": "Nueva ruta de carpeta",
|
||||
"PlaceholderNewPlaylist": "Nuevo nombre de la lista de reproducción",
|
||||
@@ -920,7 +960,8 @@
|
||||
"ToastLibraryScanFailedToStart": "Error al iniciar el escaneo",
|
||||
"ToastLibraryScanStarted": "Se inició el escaneo de la biblioteca",
|
||||
"ToastLibraryUpdateSuccess": "Biblioteca \"{0}\" actualizada",
|
||||
"ToastNameEmailRequired": "Nombre y correo electrónico obligatorios",
|
||||
"ToastMatchAllAuthorsFailed": "No coincide con todos los autores",
|
||||
"ToastNameEmailRequired": "Son obligatorios el nombre y el correo electrónico",
|
||||
"ToastNameRequired": "Nombre obligatorio",
|
||||
"ToastNewUserCreatedFailed": "Error al crear la cuenta: \"{0}\"",
|
||||
"ToastNewUserCreatedSuccess": "Nueva cuenta creada",
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
"ButtonPurgeItemsCache": "Purger le cache des éléments",
|
||||
"ButtonQueueAddItem": "Ajouter à la liste de lecture",
|
||||
"ButtonQueueRemoveItem": "Supprimer de la liste de lecture",
|
||||
"ButtonQuickEmbed": "Intégration rapide",
|
||||
"ButtonQuickEmbedMetadata": "Ajouter rapidement des métadonnées",
|
||||
"ButtonQuickMatch": "Recherche rapide",
|
||||
"ButtonReScan": "Nouvelle analyse",
|
||||
@@ -225,6 +226,9 @@
|
||||
"LabelAllUsersIncludingGuests": "Tous les utilisateurs, y compris les invités",
|
||||
"LabelAlreadyInYourLibrary": "Déjà dans la bibliothèque",
|
||||
"LabelAppend": "Ajouter",
|
||||
"LabelAudioBitrate": "Débit audio (par exemple 128k)",
|
||||
"LabelAudioChannels": "Canaux audio (1 ou 2)",
|
||||
"LabelAudioCodec": "Codec audio",
|
||||
"LabelAuthor": "Auteur",
|
||||
"LabelAuthorFirstLast": "Auteur (Prénom Nom)",
|
||||
"LabelAuthorLastFirst": "Auteur (Nom, Prénom)",
|
||||
@@ -237,6 +241,7 @@
|
||||
"LabelAutoRegister": "Enregistrement automatique",
|
||||
"LabelAutoRegisterDescription": "Créer automatiquement de nouveaux utilisateurs après la connexion",
|
||||
"LabelBackToUser": "Retour à l’utilisateur",
|
||||
"LabelBackupAudioFiles": "Sauvegarder les fichiers audio",
|
||||
"LabelBackupLocation": "Emplacement de la sauvegarde",
|
||||
"LabelBackupsEnableAutomaticBackups": "Activer les sauvegardes automatiques",
|
||||
"LabelBackupsEnableAutomaticBackupsHelp": "Sauvegardes enregistrées dans /metadata/backups",
|
||||
@@ -303,6 +308,15 @@
|
||||
"LabelEmailSettingsTestAddress": "Adresse de test",
|
||||
"LabelEmbeddedCover": "Couverture du livre intégrée",
|
||||
"LabelEnable": "Activer",
|
||||
"LabelEncodingBackupLocation": "Une sauvegarde de vos fichiers audio originaux sera stockée dans :",
|
||||
"LabelEncodingChaptersNotEmbedded": "Les chapitres ne sont pas intégrés dans les livres audio multipistes.",
|
||||
"LabelEncodingClearItemCache": "Assurez-vous de purger périodiquement le cache des éléments.",
|
||||
"LabelEncodingFinishedM4B": "Le fichier M4B terminé sera placé dans votre dossier de livre audio à l'adresse suivante :",
|
||||
"LabelEncodingInfoEmbedded": "Les métadonnées seront intégrées dans les pistes audio de votre dossier de livre audio.",
|
||||
"LabelEncodingStartedNavigation": "Une fois la tâche démarrée, vous pouvez quitter cette page.",
|
||||
"LabelEncodingTimeWarning": "L’encodage peut prendre jusqu’à 30 minutes.",
|
||||
"LabelEncodingWarningAdvancedSettings": "Avertissement : ne mettez pas à jour ces paramètres à moins que vous ne soyez familier avec les options d'encodage « ffmpeg ».",
|
||||
"LabelEncodingWatcherDisabled": "Si l'observateur est désactivé, vous devrez ensuite réanalyser ce livre audio.",
|
||||
"LabelEnd": "Fin",
|
||||
"LabelEndOfChapter": "Fin du chapitre",
|
||||
"LabelEpisode": "Épisode",
|
||||
@@ -465,6 +479,8 @@
|
||||
"LabelPubDate": "Date de publication",
|
||||
"LabelPublishYear": "Année de publication",
|
||||
"LabelPublishedDate": "Publié en {0}",
|
||||
"LabelPublishedDecade": "Décennie de publication",
|
||||
"LabelPublishedDecades": "Décennies de publication",
|
||||
"LabelPublisher": "Éditeur",
|
||||
"LabelPublishers": "Éditeurs",
|
||||
"LabelRSSFeedCustomOwnerEmail": "Courriel personnalisée du propriétaire",
|
||||
@@ -499,6 +515,7 @@
|
||||
"LabelSeries": "Séries",
|
||||
"LabelSeriesName": "Nom de la série",
|
||||
"LabelSeriesProgress": "Progression de séries",
|
||||
"LabelServerLogLevel": "Niveau de journalisation du serveur",
|
||||
"LabelServerYearReview": "Bilan de l’année du serveur ({0})",
|
||||
"LabelSetEbookAsPrimary": "Définir comme principale",
|
||||
"LabelSetEbookAsSupplementary": "Définir comme supplémentaire",
|
||||
@@ -594,6 +611,7 @@
|
||||
"LabelTitle": "Titre",
|
||||
"LabelToolsEmbedMetadata": "Métadonnées intégrées",
|
||||
"LabelToolsEmbedMetadataDescription": "Intègre les métadonnées au fichier audio avec la couverture et les chapitres.",
|
||||
"LabelToolsM4bEncoder": "Encodeur M4B",
|
||||
"LabelToolsMakeM4b": "Créer un fichier livre audio M4B",
|
||||
"LabelToolsMakeM4bDescription": "Générer un fichier de livre audio .M4B avec des métadonnées intégrées, une image de couverture et des chapitres.",
|
||||
"LabelToolsSplitM4b": "Scinde le fichier M4B en fichiers MP3",
|
||||
@@ -619,6 +637,7 @@
|
||||
"LabelUploaderDragAndDrop": "Glisser et déposer des fichiers ou dossiers",
|
||||
"LabelUploaderDropFiles": "Déposer des fichiers",
|
||||
"LabelUploaderItemFetchMetadataHelp": "Récupérer automatiquement le titre, l’auteur et la série",
|
||||
"LabelUseAdvancedOptions": "Utiliser les options avancées",
|
||||
"LabelUseChapterTrack": "Utiliser la piste du chapitre",
|
||||
"LabelUseFullTrack": "Utiliser la piste complète",
|
||||
"LabelUser": "Utilisateur",
|
||||
@@ -667,6 +686,7 @@
|
||||
"MessageConfirmDeleteMetadataProvider": "Êtes-vous sûr·e de vouloir supprimer le fournisseur de métadonnées personnalisées « {0} » ?",
|
||||
"MessageConfirmDeleteNotification": "Êtes-vous sûr·e de vouloir supprimer cette notification ?",
|
||||
"MessageConfirmDeleteSession": "Êtes-vous sûr·e de vouloir supprimer cette session ?",
|
||||
"MessageConfirmEmbedMetadataInAudioFiles": "Souhaitez-vous vraiment intégrer des métadonnées dans {0} fichiers audio ?",
|
||||
"MessageConfirmForceReScan": "Êtes-vous sûr·e de vouloir lancer une analyse forcée ?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Êtes-vous sûr·e de marquer tous les épisodes comme terminés ?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Êtes-vous sûr·e de vouloir marquer tous les épisodes comme non terminés ?",
|
||||
@@ -700,6 +720,7 @@
|
||||
"MessageDragFilesIntoTrackOrder": "Faites glisser les fichiers dans l’ordre correct des pistes",
|
||||
"MessageEmbedFailed": "Échec de l’intégration !",
|
||||
"MessageEmbedFinished": "Intégration terminée !",
|
||||
"MessageEmbedQueue": "En file d'attente pour l'intégration des métadonnées ({0} dans la file d'attente)",
|
||||
"MessageEpisodesQueuedForDownload": "{0} épisode(s) mis en file pour téléchargement",
|
||||
"MessageEreaderDevices": "Pour garantir l’envoi des livres électroniques, vous devrez peut-être ajouter le courriel ci-dessus comme expéditeur valide pour chaque appareil répertorié ci-dessous.",
|
||||
"MessageFeedURLWillBe": "L’URL du flux sera {0}",
|
||||
@@ -744,6 +765,7 @@
|
||||
"MessageNoLogs": "Aucun journaux",
|
||||
"MessageNoMediaProgress": "Aucun média en cours",
|
||||
"MessageNoNotifications": "Aucune notification",
|
||||
"MessageNoPodcastFeed": "Podcast invalide : pas de flux",
|
||||
"MessageNoPodcastsFound": "Aucun podcast trouvé",
|
||||
"MessageNoResults": "Aucun résultat",
|
||||
"MessageNoSearchResultsFor": "Aucun résultat pour la recherche « {0} »",
|
||||
@@ -760,6 +782,9 @@
|
||||
"MessagePlaylistCreateFromCollection": "Créer une liste de lecture depuis la collection",
|
||||
"MessagePleaseWait": "Merci de patienter…",
|
||||
"MessagePodcastHasNoRSSFeedForMatching": "Le Podcast n’a pas d’URL de flux RSS à utiliser pour la correspondance",
|
||||
"MessagePodcastSearchField": "Saisissez le terme de recherche ou l'URL du flux RSS",
|
||||
"MessageQuickEmbedInProgress": "Intégration rapide en cours",
|
||||
"MessageQuickEmbedQueue": "En file d'attente pour une intégration rapide ({0} dans la file d'attente)",
|
||||
"MessageQuickMatchDescription": "Renseigne les détails manquants ainsi que la couverture avec la première correspondance de « {0} ». N’écrase pas les données présentes à moins que le paramètre « Préférer les Métadonnées par correspondance » soit activé.",
|
||||
"MessageRemoveChapter": "Supprimer le chapitre",
|
||||
"MessageRemoveEpisodes": "Suppression de {0} épisode(s)",
|
||||
@@ -802,6 +827,9 @@
|
||||
"MessageTaskOpmlImportFeedPodcastExists": "Le podcast existe déjà à cet emplacement",
|
||||
"MessageTaskOpmlImportFeedPodcastFailed": "Échec de la création du podcast",
|
||||
"MessageTaskOpmlImportFinished": "Ajout de {0} podcasts",
|
||||
"MessageTaskOpmlParseFailed": "Échec de l'analyse du fichier OPML",
|
||||
"MessageTaskOpmlParseFastFail": "Balise <opml> de fichier OPML non valide introuvable OU une balise <outline> n’a pas été trouvée",
|
||||
"MessageTaskOpmlParseNoneFound": "Aucun flux trouvé dans le fichier OPML",
|
||||
"MessageTaskScanItemsAdded": "{0} ajouté",
|
||||
"MessageTaskScanItemsMissing": "{0} manquant",
|
||||
"MessageTaskScanItemsUpdated": "{0} mis à jour",
|
||||
@@ -826,6 +854,10 @@
|
||||
"NoteUploaderFoldersWithMediaFiles": "Les dossiers contenant des fichiers multimédias seront traités comme des éléments distincts de la bibliothèque.",
|
||||
"NoteUploaderOnlyAudioFiles": "Si vous téléversez uniquement des fichiers audio, chaque fichier audio sera traité comme un livre audio distinct.",
|
||||
"NoteUploaderUnsupportedFiles": "Les fichiers non pris en charge sont ignorés. Lorsque vous choisissez ou déposez un dossier, les autres fichiers qui ne sont pas dans un dossier d’élément sont ignorés.",
|
||||
"NotificationOnBackupCompletedDescription": "Déclenché lorsqu’une sauvegarde est terminée",
|
||||
"NotificationOnBackupFailedDescription": "Déclenché lorsqu'une sauvegarde échoue",
|
||||
"NotificationOnEpisodeDownloadedDescription": "Déclenché lorsqu’un épisode de podcast est téléchargé automatiquement",
|
||||
"NotificationOnTestDescription": "Événement pour tester le système de notification",
|
||||
"PlaceholderNewCollection": "Nom de la nouvelle collection",
|
||||
"PlaceholderNewFolderPath": "Nouveau chemin de dossier",
|
||||
"PlaceholderNewPlaylist": "Nouveau nom de liste de lecture",
|
||||
@@ -901,6 +933,7 @@
|
||||
"ToastErrorCannotShare": "Impossible de partager nativement sur cet appareil",
|
||||
"ToastFailedToLoadData": "Échec du chargement des données",
|
||||
"ToastFailedToShare": "Échec du partage",
|
||||
"ToastFailedToUpdate": "Échec de la mise à jour",
|
||||
"ToastInvalidImageUrl": "URL de l'image invalide",
|
||||
"ToastInvalidUrl": "URL invalide",
|
||||
"ToastItemCoverUpdateSuccess": "Couverture mise à jour",
|
||||
@@ -919,6 +952,7 @@
|
||||
"ToastLibraryScanFailedToStart": "Échec du démarrage de l’analyse",
|
||||
"ToastLibraryScanStarted": "Analyse de la bibliothèque démarrée",
|
||||
"ToastLibraryUpdateSuccess": "Bibliothèque « {0} » mise à jour",
|
||||
"ToastMatchAllAuthorsFailed": "Tous les auteurs et autrices n’ont pas pu être classés",
|
||||
"ToastNameEmailRequired": "Le nom et le courriel sont requis",
|
||||
"ToastNameRequired": "Le nom est requis",
|
||||
"ToastNewUserCreatedFailed": "La création du compte à échouée : « {0} »",
|
||||
|
||||
+79
-6
@@ -66,6 +66,7 @@
|
||||
"ButtonPurgeItemsCache": "Isprazni predmemoriju stavki",
|
||||
"ButtonQueueAddItem": "Dodaj u red",
|
||||
"ButtonQueueRemoveItem": "Ukloni iz reda",
|
||||
"ButtonQuickEmbed": "Brzo ugrađivanje",
|
||||
"ButtonQuickEmbedMetadata": "Brzo ugrađivanje meta-podataka",
|
||||
"ButtonQuickMatch": "Brzo prepoznavanje",
|
||||
"ButtonReScan": "Ponovno skeniraj",
|
||||
@@ -81,7 +82,7 @@
|
||||
"ButtonRemoveSeriesFromContinueSeries": "Ukloni seriju iz Nastavi seriju",
|
||||
"ButtonReset": "Poništi",
|
||||
"ButtonResetToDefault": "Vrati na početne postavke",
|
||||
"ButtonRestore": "Povrati",
|
||||
"ButtonRestore": "Vraćanje",
|
||||
"ButtonSave": "Spremi",
|
||||
"ButtonSaveAndClose": "Spremi i zatvori",
|
||||
"ButtonSaveTracklist": "Spremi popis zvučnih zapisa",
|
||||
@@ -179,6 +180,7 @@
|
||||
"HeaderRemoveEpisodes": "Ukloni {0} nastavaka",
|
||||
"HeaderSavedMediaProgress": "Spremljen napredak medija",
|
||||
"HeaderSchedule": "Zakazivanje",
|
||||
"HeaderScheduleEpisodeDownloads": "Zakazivanje automatskog preuzimanja nastavaka",
|
||||
"HeaderScheduleLibraryScans": "Zakaži automatsko skeniranje knjižnice",
|
||||
"HeaderSession": "Sesija",
|
||||
"HeaderSetBackupSchedule": "Zakazivanje sigurnosne pohrane",
|
||||
@@ -225,6 +227,9 @@
|
||||
"LabelAllUsersIncludingGuests": "Svi korisnici uključujući i goste",
|
||||
"LabelAlreadyInYourLibrary": "Već u vašoj knjižnici",
|
||||
"LabelAppend": "Pridodaj",
|
||||
"LabelAudioBitrate": "Kvaliteta zvučnog zapisa (npr. 128k)",
|
||||
"LabelAudioChannels": "Broj zvučnih kanala (1 ili 2)",
|
||||
"LabelAudioCodec": "Zvučni kodek",
|
||||
"LabelAuthor": "Autor",
|
||||
"LabelAuthorFirstLast": "Autor (Ime Prezime)",
|
||||
"LabelAuthorLastFirst": "Autor (Prezime, Ime)",
|
||||
@@ -237,23 +242,27 @@
|
||||
"LabelAutoRegister": "Automatska registracija",
|
||||
"LabelAutoRegisterDescription": "Automatski izradi nove korisnike nakon prijave",
|
||||
"LabelBackToUser": "Povratak na korisnika",
|
||||
"LabelBackupAudioFiles": "Sigurnosno kopiranje zvučnih datoteka",
|
||||
"LabelBackupLocation": "Lokacija sigurnosnih kopija",
|
||||
"LabelBackupsEnableAutomaticBackups": "Uključi automatsku izradu sigurnosnih kopija",
|
||||
"LabelBackupsEnableAutomaticBackups": "Omogući automatsku izradu sigurnosnih kopija",
|
||||
"LabelBackupsEnableAutomaticBackupsHelp": "Sigurnosne kopije spremaju se u /metadata/backups",
|
||||
"LabelBackupsMaxBackupSize": "Maksimalna veličina sigurnosne kopije (u GB) (0 za neograničeno)",
|
||||
"LabelBackupsMaxBackupSizeHelp": "U svrhu sprečavanja izrade krive konfiguracije, sigurnosne kopije neće se izraditi ako su veće od zadane veličine.",
|
||||
"LabelBackupsNumberToKeep": "Broj sigurnosnih kopija za čuvanje",
|
||||
"LabelBackupsNumberToKeepHelp": "Moguće je izbrisati samo jednu po jednu sigurnosnu kopiju, ako ih već imate više trebat ćete ih ručno ukloniti.",
|
||||
"LabelBitrate": "Protok",
|
||||
"LabelBonus": "Bonus",
|
||||
"LabelBooks": "knjiga/e",
|
||||
"LabelButtonText": "Tekst gumba",
|
||||
"LabelByAuthor": "po {0}",
|
||||
"LabelChangePassword": "Promijeni zaporku",
|
||||
"LabelChannels": "Kanali",
|
||||
"LabelChapterCount": "{0} Poglavlje/a",
|
||||
"LabelChapterTitle": "Naslov poglavlja",
|
||||
"LabelChapters": "Poglavlja",
|
||||
"LabelChaptersFound": "poglavlja pronađeno",
|
||||
"LabelClickForMoreInfo": "Kliknite za više informacija",
|
||||
"LabelClickToUseCurrentValue": "Kliknite za trenutnu vrijednost",
|
||||
"LabelClosePlayer": "Zatvori reproduktor",
|
||||
"LabelCodec": "Kodek",
|
||||
"LabelCollapseSeries": "Serijale prikaži sažeto",
|
||||
@@ -303,12 +312,25 @@
|
||||
"LabelEmailSettingsTestAddress": "Probna adresa",
|
||||
"LabelEmbeddedCover": "Ugrađena naslovnica",
|
||||
"LabelEnable": "Omogući",
|
||||
"LabelEncodingBackupLocation": "Sigurnosna kopija vaših izvornih zvučnih datoteka čuvat će se u mapi:",
|
||||
"LabelEncodingChaptersNotEmbedded": "Poglavlja se ne ugrađuju u zvučne knjige koje se sastoje od više zvučnih zapisa.",
|
||||
"LabelEncodingClearItemCache": "Svakako redovito praznite predmemoriju stavki.",
|
||||
"LabelEncodingFinishedM4B": "Stvorene M4B datoteke spremit će se u vašu mapu sa zvučnim knjigama:",
|
||||
"LabelEncodingInfoEmbedded": "Meta-podatci će se ugraditi u zvučne zapise u vašoj mapi sa zvučnim knjigama.",
|
||||
"LabelEncodingStartedNavigation": "Nakon pokretanja zadatka možete napustiti ovu stranicu.",
|
||||
"LabelEncodingTimeWarning": "Kodiranje može potrajati do 30 minuta.",
|
||||
"LabelEncodingWarningAdvancedSettings": "Pažnja: Ne mijenjajte ove postavke ako niste temeljito upoznati s opcijama kodiranja u ffmpegu.",
|
||||
"LabelEncodingWatcherDisabled": "Ako vam je onemogućeno praćenje mape, ovu ćete zvučnu knjigu poslije morati ponovno skenirati.",
|
||||
"LabelEnd": "Kraj",
|
||||
"LabelEndOfChapter": "Kraj poglavlja",
|
||||
"LabelEpisode": "Nastavak",
|
||||
"LabelEpisodeNotLinkedToRssFeed": "Nastavak nije povezan s RSS izvorom",
|
||||
"LabelEpisodeNumber": "{0}. nastavak",
|
||||
"LabelEpisodeTitle": "Naslov nastavka",
|
||||
"LabelEpisodeType": "Vrsta nastavka",
|
||||
"LabelEpisodeUrlFromRssFeed": "URL nastavka iz RSS izvora",
|
||||
"LabelEpisodes": "Nastavci",
|
||||
"LabelEpisodic": "U nastavcima",
|
||||
"LabelExample": "Primjer",
|
||||
"LabelExpandSeries": "Serijal prikaži prošireno",
|
||||
"LabelExpandSubSeries": "Podserijal prikaži prošireno",
|
||||
@@ -391,6 +413,10 @@
|
||||
"LabelLowestPriority": "Najniži prioritet",
|
||||
"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",
|
||||
"LabelMaxEpisodesToDownload": "Najveći broj nastavaka za preuzimanje. 0 za neograničeno.",
|
||||
"LabelMaxEpisodesToDownloadPerCheck": "Najviše novih nastavaka za preuzimanje po provjeri",
|
||||
"LabelMaxEpisodesToKeep": "Najviše nastavaka za čuvanje",
|
||||
"LabelMaxEpisodesToKeepHelp": "Ako je vrijednost 0, nema ograničenja broja. Nakon automatskog preuzimanja novog nastavka ova funkcija briše najstariji nastavak ako ih ima više od zadanog broja. Ovo briše samo jedan nastavak po novom preuzetom nastavku.",
|
||||
"LabelMediaPlayer": "Reproduktor medijskih sadržaja",
|
||||
"LabelMediaType": "Vrsta medija",
|
||||
"LabelMetaTag": "Meta oznaka",
|
||||
@@ -413,7 +439,7 @@
|
||||
"LabelNewPassword": "Nova zaporka",
|
||||
"LabelNewestAuthors": "Najnoviji autori",
|
||||
"LabelNewestEpisodes": "Najnovije epizode",
|
||||
"LabelNextBackupDate": "Sljedeće izrada sigurnosne kopije",
|
||||
"LabelNextBackupDate": "Sljedeća izrada sigurnosne kopije",
|
||||
"LabelNextScheduledRun": "Sljedeće zakazano izvođenje",
|
||||
"LabelNoCustomMetadataProviders": "Nema prilagođenih pružatelja meta-podataka",
|
||||
"LabelNoEpisodesSelected": "Nema odabranih nastavaka",
|
||||
@@ -441,7 +467,7 @@
|
||||
"LabelPermanent": "Trajno",
|
||||
"LabelPermissionsAccessAllLibraries": "Ima pristup svim knjižnicama",
|
||||
"LabelPermissionsAccessAllTags": "Ima pristup svim oznakama",
|
||||
"LabelPermissionsAccessExplicitContent": "Ima pristup eksplicitnom sadržzaju",
|
||||
"LabelPermissionsAccessExplicitContent": "Ima pristup eksplicitnom sadržaju",
|
||||
"LabelPermissionsDelete": "Smije brisati",
|
||||
"LabelPermissionsDownload": "Smije preuzimati",
|
||||
"LabelPermissionsUpdate": "Smije ažurirati",
|
||||
@@ -463,8 +489,10 @@
|
||||
"LabelProvider": "Dobavljač",
|
||||
"LabelProviderAuthorizationValue": "Vrijednost autorizacijskog zaglavlja",
|
||||
"LabelPubDate": "Datum izdavanja",
|
||||
"LabelPublishYear": "Godina izdavanja",
|
||||
"LabelPublishYear": "Godina objavljivanja",
|
||||
"LabelPublishedDate": "Objavljeno {0}",
|
||||
"LabelPublishedDecade": "Desetljeće objavljivanja",
|
||||
"LabelPublishedDecades": "Desetljeća objavljivanja",
|
||||
"LabelPublisher": "Izdavač",
|
||||
"LabelPublishers": "Izdavači",
|
||||
"LabelRSSFeedCustomOwnerEmail": "Prilagođena adresa e-pošte vlasnika",
|
||||
@@ -484,21 +512,28 @@
|
||||
"LabelRedo": "Ponovi",
|
||||
"LabelRegion": "Regija",
|
||||
"LabelReleaseDate": "Datum izlaska",
|
||||
"LabelRemoveAllMetadataAbs": "Ukloni sve datoteke metadata.abs",
|
||||
"LabelRemoveAllMetadataJson": "Ukloni sve datoteke metadata.json",
|
||||
"LabelRemoveCover": "Ukloni naslovnicu",
|
||||
"LabelRemoveMetadataFile": "Ukloni datoteke s meta-podatcima iz mapa knjižničkih stavki",
|
||||
"LabelRemoveMetadataFileHelp": "Uklanjanje svih datoteka metadata.json i metadata.abs u vaših {0} mapa.",
|
||||
"LabelRowsPerPage": "Redaka po stranici",
|
||||
"LabelSearchTerm": "Traži pojam",
|
||||
"LabelSearchTitle": "Traži naslov",
|
||||
"LabelSearchTitleOrASIN": "Traži naslov ili ASIN",
|
||||
"LabelSeason": "Sezona",
|
||||
"LabelSeasonNumber": "{0}. sezona",
|
||||
"LabelSelectAll": "Označi sve",
|
||||
"LabelSelectAllEpisodes": "Označi sve nastavke",
|
||||
"LabelSelectEpisodesShowing": "Prikazujem {0} odabranih nastavaka",
|
||||
"LabelSelectUsers": "Označi korisnike",
|
||||
"LabelSendEbookToDevice": "Pošalji e-knjigu",
|
||||
"LabelSequence": "Slijed",
|
||||
"LabelSerial": "Serijal",
|
||||
"LabelSeries": "Serijal/a",
|
||||
"LabelSeriesName": "Ime serijala",
|
||||
"LabelSeriesProgress": "Napredak u serijalu",
|
||||
"LabelServerLogLevel": "Razina zapisa poslužitelja",
|
||||
"LabelServerYearReview": "Godišnji pregled poslužitelja ({0})",
|
||||
"LabelSetEbookAsPrimary": "Postavi kao primarno",
|
||||
"LabelSetEbookAsSupplementary": "Postavi kao dopunsko",
|
||||
@@ -587,6 +622,7 @@
|
||||
"LabelTimeDurationXMinutes": "{0} minuta",
|
||||
"LabelTimeDurationXSeconds": "{0} sekundi",
|
||||
"LabelTimeInMinutes": "Vrijeme u minutama",
|
||||
"LabelTimeLeft": "{0} preostalo",
|
||||
"LabelTimeListened": "Vremena odslušano",
|
||||
"LabelTimeListenedToday": "Vremena odslušano danas",
|
||||
"LabelTimeRemaining": "{0} preostalo",
|
||||
@@ -594,6 +630,7 @@
|
||||
"LabelTitle": "Naslov",
|
||||
"LabelToolsEmbedMetadata": "Ugradi meta-podatke",
|
||||
"LabelToolsEmbedMetadataDescription": "Ugradi meta-podatke u zvučne datoteke zajedno s naslovnicom i poglavljima.",
|
||||
"LabelToolsM4bEncoder": "M4B kodiranje",
|
||||
"LabelToolsMakeM4b": "Stvori M4B datoteku audioknjige",
|
||||
"LabelToolsMakeM4bDescription": "Izrađuje zvučnu knjigu u .M4B formatu s ugrađenim meta-podatcima, naslovnicom i poglavljima.",
|
||||
"LabelToolsSplitM4b": "Podijeli M4B datoteke u MP3 datoteke",
|
||||
@@ -606,6 +643,7 @@
|
||||
"LabelTracksMultiTrack": "Više zvučnih zapisa",
|
||||
"LabelTracksNone": "Nema zapisa",
|
||||
"LabelTracksSingleTrack": "Jedan zvučni zapis",
|
||||
"LabelTrailer": "Najava",
|
||||
"LabelType": "Vrsta",
|
||||
"LabelUnabridged": "Neskraćeno",
|
||||
"LabelUndo": "Vrati",
|
||||
@@ -619,8 +657,10 @@
|
||||
"LabelUploaderDragAndDrop": "Pritisni i prevuci datoteke ili mape",
|
||||
"LabelUploaderDropFiles": "Ispusti datoteke",
|
||||
"LabelUploaderItemFetchMetadataHelp": "Automatski dohvati naslov, autora i serijal",
|
||||
"LabelUseAdvancedOptions": "Koristi se naprednim opcijama",
|
||||
"LabelUseChapterTrack": "Koristi zvučni zapis poglavlja",
|
||||
"LabelUseFullTrack": "Koristi cijeli zvučni zapis",
|
||||
"LabelUseZeroForUnlimited": "0 za neograničeno",
|
||||
"LabelUser": "Korisnik",
|
||||
"LabelUsername": "Korisničko ime",
|
||||
"LabelValue": "Vrijednost",
|
||||
@@ -641,7 +681,7 @@
|
||||
"LabelYourProgress": "Vaš napredak",
|
||||
"MessageAddToPlayerQueue": "Dodaj u redoslijed izvođenja",
|
||||
"MessageAppriseDescription": "Da biste se koristili ovom značajkom, treba vam instanca <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API-ja</a> ili API koji može rukovati istom vrstom zahtjeva.<br />The Adresa Apprise API-ja treba biti puna URL putanja za slanje obavijesti, npr. ako vam se API instanca poslužuje na adresi <code>http://192.168.1.1:8337</code> trebate upisati <code>http://192.168.1.1:8337/notify</code>.",
|
||||
"MessageBackupsDescription": "Backups uključuju korisnike, korisnikov napredak, detalje stavki iz biblioteke, postavke server i slike iz <code>/metadata/items</code> & <code>/metadata/authors</code>. Backups ne uključuju nijedne datoteke koje su u folderima biblioteke.",
|
||||
"MessageBackupsDescription": "Sigurnosne kopije sadrže korisnike, korisnikov napredak medija, pojedinosti knjižničke građe, postavke poslužitelja i slike koje se spremaju u <code>/metadata/items</code> & <code>/metadata/authors</code>. Sigurnosne kopije ne sadrže niti jednu datoteku iz mapa knjižnice.",
|
||||
"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.",
|
||||
"MessageBackupsLocationPathEmpty": "Putanja do lokacije za sigurnosne kopije ne može ostati prazna",
|
||||
@@ -667,6 +707,7 @@
|
||||
"MessageConfirmDeleteMetadataProvider": "Sigurno želite izbrisati prilagođenog pružatelja meta-podataka \"{0}\"?",
|
||||
"MessageConfirmDeleteNotification": "Sigurno želite izbrisati ovu obavijest?",
|
||||
"MessageConfirmDeleteSession": "Sigurno želite obrisati ovu sesiju?",
|
||||
"MessageConfirmEmbedMetadataInAudioFiles": "Sigurno želite ugraditi meta-podatke u {0} zvučnih datoteka?",
|
||||
"MessageConfirmForceReScan": "Sigurno želite ponovno pokrenuti skeniranje?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Sigurno želite označiti sve nastavke dovršenima?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Sigurno želite označiti sve nastavke nedovršenima?",
|
||||
@@ -678,6 +719,7 @@
|
||||
"MessageConfirmPurgeCache": "Brisanje predmemorije izbrisat će cijelu mapu <code>/metadata/cache</code>. <br /><br />Sigurno želite izbrisati mapu predmemorije?",
|
||||
"MessageConfirmPurgeItemsCache": "Brisanje predmemorije stavki izbrisat će cijelu mapu <code>/metadata/cache/items</code>.<br />Jeste li sigurni?",
|
||||
"MessageConfirmQuickEmbed": "Pažnja! Funkcija brzog ugrađivanja ne stvara sigurnosne kopije vaših zvučnih datoteka. Provjerite imate li sigurnosnu kopiju. <br><br>Želite li nastaviti?",
|
||||
"MessageConfirmQuickMatchEpisodes": "Brzo prepoznavanje nastavaka prepisat će pojedinosti ukoliko se pronađe podudaranje. Neprepoznati nastavci će se ažurirati. Jeste li sigurni?",
|
||||
"MessageConfirmReScanLibraryItems": "Sigurno želite ponovno skenirati {0} stavki?",
|
||||
"MessageConfirmRemoveAllChapters": "Sigurno želite ukloniti sva poglavlja?",
|
||||
"MessageConfirmRemoveAuthor": "Sigurno želite ukloniti autora \"{0}\"?",
|
||||
@@ -685,6 +727,7 @@
|
||||
"MessageConfirmRemoveEpisode": "Sigurno želite ukloniti nastavak \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisodes": "Sigurno želite ukloniti {0} nastavaka?",
|
||||
"MessageConfirmRemoveListeningSessions": "Sigurno želite ukloniti {0} sesija slušanja?",
|
||||
"MessageConfirmRemoveMetadataFiles": "Sigurno želite ukloniti sve datoteke metadata.{0} u mapama vaših knjižničkih stavki?",
|
||||
"MessageConfirmRemoveNarrator": "Sigurno želite ukloniti pripovjedača \"{0}\"?",
|
||||
"MessageConfirmRemovePlaylist": "Sigurno želite ukloniti vaš popis za izvođenje \"{0}\"?",
|
||||
"MessageConfirmRenameGenre": "Sigurno želite preimenovati žanr \"{0}\" u \"{1}\" za sve stavke?",
|
||||
@@ -700,6 +743,7 @@
|
||||
"MessageDragFilesIntoTrackOrder": "Ispravi redoslijed zapisa prevlačenje datoteka",
|
||||
"MessageEmbedFailed": "Ugrađivanje nije uspjelo!",
|
||||
"MessageEmbedFinished": "Ugrađivanje je dovršeno!",
|
||||
"MessageEmbedQueue": "Ugrađivanje meta-podataka dodano u red obrade ({0} u redu)",
|
||||
"MessageEpisodesQueuedForDownload": "{0} nastavak(a) u redu za preuzimanje",
|
||||
"MessageEreaderDevices": "Da biste osigurali isporuku e-knjiga, možda ćete morati gornju adresu e-pošte dodati kao dopuštenog pošiljatelja za svaki od donjih uređaja.",
|
||||
"MessageFeedURLWillBe": "URL izvora bit će {0}",
|
||||
@@ -744,6 +788,7 @@
|
||||
"MessageNoLogs": "Nema zapisnika",
|
||||
"MessageNoMediaProgress": "Nema podataka o započetim medijima",
|
||||
"MessageNoNotifications": "Nema obavijesti",
|
||||
"MessageNoPodcastFeed": "Neispravan podcast: Nema izvora",
|
||||
"MessageNoPodcastsFound": "Nije pronađen niti jedan podcast",
|
||||
"MessageNoResults": "Nema rezultata",
|
||||
"MessageNoSearchResultsFor": "Nema rezultata pretrage za \"{0}\"",
|
||||
@@ -760,6 +805,10 @@
|
||||
"MessagePlaylistCreateFromCollection": "Stvori popis za izvođenje od zbirke",
|
||||
"MessagePleaseWait": "Molimo pričekajte...",
|
||||
"MessagePodcastHasNoRSSFeedForMatching": "Podcast nema adresu RSS izvora za prepoznavanje",
|
||||
"MessagePodcastSearchField": "Unesite upit za pretragu ili URL RSS izvora",
|
||||
"MessageQuickEmbedInProgress": "Brzo ugrađivanje u tijeku",
|
||||
"MessageQuickEmbedQueue": "Dodano u red za brzo ugrađivanje ({0} u redu izvođenja)",
|
||||
"MessageQuickMatchAllEpisodes": "Brzo prepoznavanje svih nastavaka",
|
||||
"MessageQuickMatchDescription": "Popuni pojedinosti i naslovnice koji nedostaju prvim pronađenim rezultatom za '{0}'. Ne prepisuje podatke osim ako ne uključite mogućnost 'Daj prednost meta-podatcima prepoznatih stavki'.",
|
||||
"MessageRemoveChapter": "Ukloni poglavlje",
|
||||
"MessageRemoveEpisodes": "Ukloni {0} nastavaka",
|
||||
@@ -802,6 +851,9 @@
|
||||
"MessageTaskOpmlImportFeedPodcastExists": "Podcast već postoji u putanji",
|
||||
"MessageTaskOpmlImportFeedPodcastFailed": "Stvaranje podcasta nije uspjelo",
|
||||
"MessageTaskOpmlImportFinished": "Dodano {0} podcasta",
|
||||
"MessageTaskOpmlParseFailed": "Raščlanjivanje OPML datoteke nije uspjelo",
|
||||
"MessageTaskOpmlParseFastFail": "Neispravna OPML datoteka, oznaka <opml> nije pronađena ILI oznaka <outline> nije pronađena",
|
||||
"MessageTaskOpmlParseNoneFound": "U OPML datoteci nisu pronađeni izvori",
|
||||
"MessageTaskScanItemsAdded": "{0} dodan(o)",
|
||||
"MessageTaskScanItemsMissing": "{0} nedostaje",
|
||||
"MessageTaskScanItemsUpdated": "{0} ažurirano",
|
||||
@@ -826,6 +878,10 @@
|
||||
"NoteUploaderFoldersWithMediaFiles": "Mape s medijskim datotekama smatrat će se zasebnim stavkama knjižnice.",
|
||||
"NoteUploaderOnlyAudioFiles": "Ako učitavate samo zvučne datoteke svaka će se zvučna datoteka uvesti kao zasebna zvučna knjiga.",
|
||||
"NoteUploaderUnsupportedFiles": "Nepodržane vrste datoteka zanemaruju se. Kada odabirete datoteke ili ispuštate mapu, sve datoteke koje nisu u mapi stavke zanemarit će se.",
|
||||
"NotificationOnBackupCompletedDescription": "Pokreće se po završetku sigurnosnog kopiranja",
|
||||
"NotificationOnBackupFailedDescription": "Pokreće se kada sigurnosno kopiranje ne uspije",
|
||||
"NotificationOnEpisodeDownloadedDescription": "Pokreće se kada se nastavak podcasta automatski preuzme",
|
||||
"NotificationOnTestDescription": "Događaj za testiranje sustava obavijesti",
|
||||
"PlaceholderNewCollection": "Ime nove zbirke",
|
||||
"PlaceholderNewFolderPath": "Nova putanja mape",
|
||||
"PlaceholderNewPlaylist": "Naziv novog popisa za izvođenje",
|
||||
@@ -851,6 +907,7 @@
|
||||
"StatsYearInReview": "PREGLED GODINE",
|
||||
"ToastAccountUpdateSuccess": "Račun ažuriran",
|
||||
"ToastAppriseUrlRequired": "Obavezno upisati Apprise URL",
|
||||
"ToastAsinRequired": "ASIN je obvezan",
|
||||
"ToastAuthorImageRemoveSuccess": "Slika autora uklonjena",
|
||||
"ToastAuthorNotFound": "Autor \"{0}\" nije pronađen",
|
||||
"ToastAuthorRemoveSuccess": "Autor uklonjen",
|
||||
@@ -870,6 +927,8 @@
|
||||
"ToastBackupUploadSuccess": "Sigurnosna kopija učitana",
|
||||
"ToastBatchDeleteFailed": "Grupno brisanje nije uspjelo",
|
||||
"ToastBatchDeleteSuccess": "Grupno brisanje je uspješno dovršeno",
|
||||
"ToastBatchQuickMatchFailed": "Grupno brzo prepoznavanje nije uspjelo!",
|
||||
"ToastBatchQuickMatchStarted": "Započelo je brzo prepoznavanje {0} knjiga!",
|
||||
"ToastBatchUpdateFailed": "Skupno ažuriranje nije uspjelo",
|
||||
"ToastBatchUpdateSuccess": "Skupno ažuriranje uspješno dovršeno",
|
||||
"ToastBookmarkCreateFailed": "Izrada knjižne oznake nije uspjela",
|
||||
@@ -881,6 +940,7 @@
|
||||
"ToastChaptersHaveErrors": "Poglavlja imaju pogreške",
|
||||
"ToastChaptersMustHaveTitles": "Poglavlja moraju imati naslove",
|
||||
"ToastChaptersRemoved": "Poglavlja uklonjena",
|
||||
"ToastChaptersUpdated": "Poglavlja su ažurirana",
|
||||
"ToastCollectionItemsAddFailed": "Neuspješno dodavanje stavki u zbirku",
|
||||
"ToastCollectionItemsAddSuccess": "Uspješno dodavanje stavki u zbirku",
|
||||
"ToastCollectionItemsRemoveSuccess": "Stavke izbrisane iz zbirke",
|
||||
@@ -898,11 +958,14 @@
|
||||
"ToastEncodeCancelSucces": "Kodiranje otkazano",
|
||||
"ToastEpisodeDownloadQueueClearFailed": "Redoslijed izvođenja nije uspješno očišćen",
|
||||
"ToastEpisodeDownloadQueueClearSuccess": "Redoslijed preuzimanja nastavaka očišćen",
|
||||
"ToastEpisodeUpdateSuccess": "{0} nastavak/a ažurirano",
|
||||
"ToastErrorCannotShare": "Dijeljenje na ovaj uređaj nije moguće",
|
||||
"ToastFailedToLoadData": "Učitavanje podataka nije uspjelo",
|
||||
"ToastFailedToMatch": "Nije prepoznato",
|
||||
"ToastFailedToShare": "Dijeljenje nije uspjelo",
|
||||
"ToastFailedToUpdate": "Ažuriranje nije uspjelo",
|
||||
"ToastInvalidImageUrl": "Neispravan URL slike",
|
||||
"ToastInvalidMaxEpisodesToDownload": "Neispravan unos maksimalnog broja nastavaka",
|
||||
"ToastInvalidUrl": "Neispravan URL",
|
||||
"ToastItemCoverUpdateSuccess": "Naslovnica stavke ažurirana",
|
||||
"ToastItemDeletedFailed": "Brisanje stavke nije uspjelo",
|
||||
@@ -920,14 +983,22 @@
|
||||
"ToastLibraryScanFailedToStart": "Skeniranje nije uspjelo",
|
||||
"ToastLibraryScanStarted": "Skeniranje knjižnice započelo",
|
||||
"ToastLibraryUpdateSuccess": "Knjižnica \"{0}\" ažurirana",
|
||||
"ToastMatchAllAuthorsFailed": "Nisu prepoznati svi autori",
|
||||
"ToastMetadataFilesRemovedError": "Pogreška kod uklanjanja datoteka metadata.{0}",
|
||||
"ToastMetadataFilesRemovedNoneFound": "U knjižnici nisu pronađene datoteke metadata.{0}",
|
||||
"ToastMetadataFilesRemovedNoneRemoved": "Datoteke metadata.{0} nisu uklonjenje",
|
||||
"ToastMetadataFilesRemovedSuccess": "uklonjeno {0} datoteka metadata.{1}",
|
||||
"ToastMustHaveAtLeastOnePath": "Mora postojati barem jedna putanja",
|
||||
"ToastNameEmailRequired": "Ime i adresa e-pošte su obavezni",
|
||||
"ToastNameRequired": "Ime je obavezno",
|
||||
"ToastNewEpisodesFound": "pronađeno {0} novih nastavaka",
|
||||
"ToastNewUserCreatedFailed": "Račun \"{0}\" nije uspješno izrađen",
|
||||
"ToastNewUserCreatedSuccess": "Novi račun izrađen",
|
||||
"ToastNewUserLibraryError": "Treba odabrati barem jednu knjižnicu",
|
||||
"ToastNewUserPasswordError": "Mora imati zaporku, samo korisnik root može imati praznu zaporku",
|
||||
"ToastNewUserTagError": "Potrebno je odabrati najmanje jednu oznaku",
|
||||
"ToastNewUserUsernameError": "Upišite korisničko ime",
|
||||
"ToastNoNewEpisodesFound": "Nisu pronađeni novi nastavci",
|
||||
"ToastNoUpdatesNecessary": "Ažuriranja nisu potrebna",
|
||||
"ToastNotificationCreateFailed": "Stvaranje obavijesti nije uspjelo",
|
||||
"ToastNotificationDeleteFailed": "Brisanje obavijesti nije uspjelo",
|
||||
@@ -946,6 +1017,7 @@
|
||||
"ToastPodcastGetFeedFailed": "Dohvat izvora podcasta nije uspio",
|
||||
"ToastPodcastNoEpisodesInFeed": "U RSS izvoru nisu pronađeni nastavci",
|
||||
"ToastPodcastNoRssFeed": "Podcast nema RSS izvor",
|
||||
"ToastProgressIsNotBeingSynced": "Napredak se ne sinkronizira, ponovno pokrenite reprodukciju",
|
||||
"ToastProviderCreatedFailed": "Dodavanje pružatelja nije uspjelo",
|
||||
"ToastProviderCreatedSuccess": "Novi pružatelj dodan",
|
||||
"ToastProviderNameAndUrlRequired": "Ime i URL su obavezni",
|
||||
@@ -972,6 +1044,7 @@
|
||||
"ToastSessionCloseFailed": "Zatvaranje sesije nije uspjelo",
|
||||
"ToastSessionDeleteFailed": "Neuspješno brisanje serije",
|
||||
"ToastSessionDeleteSuccess": "Sesija obrisana",
|
||||
"ToastSleepTimerDone": "Timer za spavanje istječe... zZzzZz",
|
||||
"ToastSlugMustChange": "Slug sadrži nedozvoljene znakove",
|
||||
"ToastSlugRequired": "Slug je obavezan",
|
||||
"ToastSocketConnected": "Socket priključen",
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
"ButtonPurgeItemsCache": "Elimina la Cache selezionata",
|
||||
"ButtonQueueAddItem": "Aggiungi alla Coda",
|
||||
"ButtonQueueRemoveItem": "Rimuovi dalla Coda",
|
||||
"ButtonQuickEmbed": "Quick Embed",
|
||||
"ButtonQuickEmbedMetadata": "Incorporamento rapido Metadati",
|
||||
"ButtonQuickMatch": "Controlla Metadata Auto",
|
||||
"ButtonReScan": "Ri-scansiona",
|
||||
@@ -225,6 +226,9 @@
|
||||
"LabelAllUsersIncludingGuests": "Tutti gli Utenti Inclusi gli ospiti",
|
||||
"LabelAlreadyInYourLibrary": "Già esistente nella libreria",
|
||||
"LabelAppend": "Appese",
|
||||
"LabelAudioBitrate": "Audio Bitrate (es. 128k)",
|
||||
"LabelAudioChannels": "Canali Audio (1 o 2)",
|
||||
"LabelAudioCodec": "Codec Audio",
|
||||
"LabelAuthor": "Autore",
|
||||
"LabelAuthorFirstLast": "Autore (Per Nome)",
|
||||
"LabelAuthorLastFirst": "Autori (Per Cognome)",
|
||||
@@ -237,6 +241,7 @@
|
||||
"LabelAutoRegister": "Auto Registrazione",
|
||||
"LabelAutoRegisterDescription": "Crea automaticamente nuovi utenti dopo aver effettuato l'accesso",
|
||||
"LabelBackToUser": "Torna a Utenti",
|
||||
"LabelBackupAudioFiles": "Backup file Audio",
|
||||
"LabelBackupLocation": "Percorso del Backup",
|
||||
"LabelBackupsEnableAutomaticBackups": "Abilita backup Automatico",
|
||||
"LabelBackupsEnableAutomaticBackupsHelp": "I Backup saranno salvati in /metadata/backups",
|
||||
@@ -303,6 +308,15 @@
|
||||
"LabelEmailSettingsTestAddress": "Indirizzo di test",
|
||||
"LabelEmbeddedCover": "Cover Integrata",
|
||||
"LabelEnable": "Abilita",
|
||||
"LabelEncodingBackupLocation": "il backup dei file audio verrà archiviato in:",
|
||||
"LabelEncodingChaptersNotEmbedded": "Negli audiolibri multitraccia i capitoli non sono incorporati.",
|
||||
"LabelEncodingClearItemCache": "Assicurati di svuotare periodicamente la cache degli oggetti.",
|
||||
"LabelEncodingFinishedM4B": "L'M4B completato verrà inserito nella cartella:",
|
||||
"LabelEncodingInfoEmbedded": "I metadati verranno incorporati nelle tracce audio all'interno della cartella dell'audiolibro.",
|
||||
"LabelEncodingStartedNavigation": "Una volta avviata l'attività, è possibile uscire da questa pagina.",
|
||||
"LabelEncodingTimeWarning": "La codifica può richiedere fino a 30 minuti.",
|
||||
"LabelEncodingWarningAdvancedSettings": "Attenzione: non aggiornare queste impostazioni se non hai familiarità con le opzioni di codifica ffmpeg.",
|
||||
"LabelEncodingWatcherDisabled": "Se hai disabilitato l'opzione Watcher, dovrai eseguire nuovamente la scansione dell'audiolibro in seguito.",
|
||||
"LabelEnd": "Fine",
|
||||
"LabelEndOfChapter": "Fine Capitolo",
|
||||
"LabelEpisode": "Episodio",
|
||||
@@ -465,6 +479,8 @@
|
||||
"LabelPubDate": "Data di pubblicazione",
|
||||
"LabelPublishYear": "Anno di pubblicazione",
|
||||
"LabelPublishedDate": "{0} pubblicati",
|
||||
"LabelPublishedDecade": "Decennio di pubblicazione",
|
||||
"LabelPublishedDecades": "Decenni di pubblicazione",
|
||||
"LabelPublisher": "Editore",
|
||||
"LabelPublishers": "Editori",
|
||||
"LabelRSSFeedCustomOwnerEmail": "E-mail del proprietario personalizzato",
|
||||
@@ -499,6 +515,7 @@
|
||||
"LabelSeries": "Serie",
|
||||
"LabelSeriesName": "Nome Serie",
|
||||
"LabelSeriesProgress": "Cominciato",
|
||||
"LabelServerLogLevel": "Server Log Level",
|
||||
"LabelServerYearReview": "Anno del server in sintesi({0})",
|
||||
"LabelSetEbookAsPrimary": "Imposta come primario",
|
||||
"LabelSetEbookAsSupplementary": "Imposta come suplementare",
|
||||
@@ -594,6 +611,7 @@
|
||||
"LabelTitle": "Titolo",
|
||||
"LabelToolsEmbedMetadata": "Incorpora Metadata",
|
||||
"LabelToolsEmbedMetadataDescription": "Incorpora i metadati nei file audio, inclusi l'immagine di copertina e i capitoli.",
|
||||
"LabelToolsM4bEncoder": "M4B Encoder",
|
||||
"LabelToolsMakeM4b": "Crea un file M4B",
|
||||
"LabelToolsMakeM4bDescription": "Genera un file audiolibro M4B con metadati incorporati, immagine di copertina e capitoli.",
|
||||
"LabelToolsSplitM4b": "Converti M4B in MP3",
|
||||
@@ -619,6 +637,7 @@
|
||||
"LabelUploaderDragAndDrop": "Drag & drop file o Cartelle",
|
||||
"LabelUploaderDropFiles": "Elimina file",
|
||||
"LabelUploaderItemFetchMetadataHelp": "Recupera automaticamente titolo, autore e serie",
|
||||
"LabelUseAdvancedOptions": "Usa le opzioni avanzate",
|
||||
"LabelUseChapterTrack": "Usa il Capitolo della Traccia",
|
||||
"LabelUseFullTrack": "Usa la traccia totale",
|
||||
"LabelUser": "Utente",
|
||||
@@ -667,6 +686,7 @@
|
||||
"MessageConfirmDeleteMetadataProvider": "Sei sicuro/sicura di voler eliminare il fornitore di metadati personalizzato {0}?",
|
||||
"MessageConfirmDeleteNotification": "Sei sicuro/sicura di voler eliminare questa notifica?",
|
||||
"MessageConfirmDeleteSession": "Sei sicuro di voler eliminare questa sessione?",
|
||||
"MessageConfirmEmbedMetadataInAudioFiles": "Sei sicuro di voler incorporare i metadati nei file audio {0}?",
|
||||
"MessageConfirmForceReScan": "Sei sicuro di voler forzare una nuova scansione?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Sei sicuro di voler contrassegnare tutti gli episodi come finiti?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Sei sicuro di voler contrassegnare tutti gli episodi come non completati?",
|
||||
@@ -700,6 +720,7 @@
|
||||
"MessageDragFilesIntoTrackOrder": "Trascina i file nell'ordine di traccia corretto",
|
||||
"MessageEmbedFailed": "Incorporamento non riuscito!",
|
||||
"MessageEmbedFinished": "Incorporamento finito!",
|
||||
"MessageEmbedQueue": "In coda per l'incorporamento dei metadati ({0} in coda)",
|
||||
"MessageEpisodesQueuedForDownload": "{0} episodio(i) in coda per lo scaricamento",
|
||||
"MessageEreaderDevices": "Per garantire la consegna dei libri digitali, potrebbe essere necessario aggiungere l'indirizzo e-mail sopra indicato come mittente valido per ciascun dispositivo elencato di seguito.",
|
||||
"MessageFeedURLWillBe": "l’URL del flusso sarà {0}",
|
||||
@@ -744,6 +765,7 @@
|
||||
"MessageNoLogs": "Nessun Logs",
|
||||
"MessageNoMediaProgress": "Nessun progresso multimediale",
|
||||
"MessageNoNotifications": "Nessuna notifica",
|
||||
"MessageNoPodcastFeed": "Podcast non valido: nessun feed",
|
||||
"MessageNoPodcastsFound": "Nessun podcast trovato",
|
||||
"MessageNoResults": "Nessun Risultato",
|
||||
"MessageNoSearchResultsFor": "Nessun risultato per \"{0}\"",
|
||||
@@ -760,6 +782,9 @@
|
||||
"MessagePlaylistCreateFromCollection": "Crea playlist da una Raccolta",
|
||||
"MessagePleaseWait": "Attendi...",
|
||||
"MessagePodcastHasNoRSSFeedForMatching": "Podcast non ha l'URL del feed RSS da utilizzare per il match",
|
||||
"MessagePodcastSearchField": "Inserisci il termine di ricerca o l'URL del feed RSS",
|
||||
"MessageQuickEmbedInProgress": "Incorporamento rapido in corso",
|
||||
"MessageQuickEmbedQueue": "In coda per incorporamento rapido ({0} in coda)",
|
||||
"MessageQuickMatchDescription": "Compila i dettagli dell'articolo vuoto e copri con il risultato della prima corrispondenza di '{0}'. Non sovrascrive i dettagli a meno che non sia abilitata l'impostazione del server \"Preferisci metadati corrispondenti\".",
|
||||
"MessageRemoveChapter": "Rimuovi Capitolo",
|
||||
"MessageRemoveEpisodes": "rimuovi {0} episodio(i)",
|
||||
@@ -777,6 +802,41 @@
|
||||
"MessageShareExpiresIn": "Scade in {0}",
|
||||
"MessageShareURLWillBe": "L'indirizzo sarà: <strong>{0}</strong>",
|
||||
"MessageStartPlaybackAtTime": "Avvia la riproduzione per \"{0}\" a {1}?",
|
||||
"MessageTaskAudioFileNotWritable": "Il file audio «{0}» non è scrivibile",
|
||||
"MessageTaskCanceledByUser": "Attività annullata dall'utente",
|
||||
"MessageTaskDownloadingEpisodeDescription": "Scaricamento dell'episodio «{0}»",
|
||||
"MessageTaskEmbeddingMetadata": "Metadati integrati",
|
||||
"MessageTaskEmbeddingMetadataDescription": "Integrazione dei metadati nell'audiolibro «{0}»",
|
||||
"MessageTaskEncodingM4b": "Codifica M4B",
|
||||
"MessageTaskEncodingM4bDescription": "Codifica dell'audiolibro «{0}» in un singolo file m4b",
|
||||
"MessageTaskFailed": "Fallimento",
|
||||
"MessageTaskFailedToBackupAudioFile": "Non riuscita a eseguire il backup del file audio «{0}»",
|
||||
"MessageTaskFailedToCreateCacheDirectory": "Non riuscita a creare la cartella della cache",
|
||||
"MessageTaskFailedToEmbedMetadataInFile": "Non ha inserito i metadati nel file «{0}»",
|
||||
"MessageTaskFailedToMergeAudioFiles": "Non è riuscito a fondere i file audio",
|
||||
"MessageTaskFailedToMoveM4bFile": "Non è riuscito a spostare il file m4b",
|
||||
"MessageTaskFailedToWriteMetadataFile": "Non è riuscito a scrivere file di metadati",
|
||||
"MessageTaskMatchingBooksInLibrary": "Libri di corrispondenza in biblioteca «{0}»",
|
||||
"MessageTaskNoFilesToScan": "Nessun file per la scansione",
|
||||
"MessageTaskOpmlImport": "Importazione OPML",
|
||||
"MessageTaskOpmlImportDescription": "Creazione di podcast da {0} flusso RSS",
|
||||
"MessageTaskOpmlImportFeed": "Flusso di importazione OPML",
|
||||
"MessageTaskOpmlImportFeedDescription": "Importazione del flusso RSS «{0}»",
|
||||
"MessageTaskOpmlImportFeedFailed": "Impossibile ottenere il flusso del podcast",
|
||||
"MessageTaskOpmlImportFeedPodcastDescription": "Creazione di podcast «{0}»",
|
||||
"MessageTaskOpmlImportFeedPodcastExists": "Il podcast esiste già nel percorso",
|
||||
"MessageTaskOpmlImportFeedPodcastFailed": "Errore durante la creazione del podcast",
|
||||
"MessageTaskOpmlImportFinished": "{0} podcast aggiunti",
|
||||
"MessageTaskOpmlParseFailed": "Impossibile analizzare il file OPML",
|
||||
"MessageTaskOpmlParseFastFail": "File OPML non valido. Tag <opml> non trovato OPPURE non è stato trovato un tag <outline>",
|
||||
"MessageTaskOpmlParseNoneFound": "Nessun feed trovato nel file OPML",
|
||||
"MessageTaskScanItemsAdded": "{0} aggiunti",
|
||||
"MessageTaskScanItemsMissing": "{0} mancanti",
|
||||
"MessageTaskScanItemsUpdated": "{0} aggiornati",
|
||||
"MessageTaskScanNoChangesNeeded": "Nessuna modifica necessaria",
|
||||
"MessageTaskScanningFileChanges": "Cambiamenti di file di scansione in «{0}»",
|
||||
"MessageTaskScanningLibrary": "Scansione della biblioteca «{0}»",
|
||||
"MessageTaskTargetDirectoryNotWritable": "La cartella di destinazione non è scrivibile",
|
||||
"MessageThinking": "Elaborazione...",
|
||||
"MessageUploaderItemFailed": "Caricamento Fallito",
|
||||
"MessageUploaderItemSuccess": "Caricato con successo!",
|
||||
@@ -794,6 +854,10 @@
|
||||
"NoteUploaderFoldersWithMediaFiles": "Le cartelle con file multimediali verranno gestite come elementi della libreria separati.",
|
||||
"NoteUploaderOnlyAudioFiles": "Se carichi solo file audio, ogni file audio verrà gestito come un audiolibro separato.",
|
||||
"NoteUploaderUnsupportedFiles": "I file non supportati vengono ignorati. Quando si sceglie o si elimina una cartella, gli altri file che non si trovano in una cartella di elementi vengono ignorati.",
|
||||
"NotificationOnBackupCompletedDescription": "Attivato al completamento di un backup",
|
||||
"NotificationOnBackupFailedDescription": "Attivato quando un backup fallisce",
|
||||
"NotificationOnEpisodeDownloadedDescription": "Attivato quando un episodio di podcast viene scaricato automaticamente",
|
||||
"NotificationOnTestDescription": "test il sistema di notifica",
|
||||
"PlaceholderNewCollection": "Nome Nuova Raccolta",
|
||||
"PlaceholderNewFolderPath": "Nuovo Percorso Cartella",
|
||||
"PlaceholderNewPlaylist": "Nome nuova playlist",
|
||||
@@ -869,6 +933,7 @@
|
||||
"ToastErrorCannotShare": "Impossibile condividere in modo nativo su questo dispositivo",
|
||||
"ToastFailedToLoadData": "Impossibile caricare i dati",
|
||||
"ToastFailedToShare": "Impossibile condividere",
|
||||
"ToastFailedToUpdate": "Non aggiornato",
|
||||
"ToastInvalidImageUrl": "URL dell'immagine non valido",
|
||||
"ToastInvalidUrl": "URL non valido",
|
||||
"ToastItemCoverUpdateSuccess": "Cover aggiornata",
|
||||
@@ -887,6 +952,7 @@
|
||||
"ToastLibraryScanFailedToStart": "Errore inizio scansione",
|
||||
"ToastLibraryScanStarted": "Scansione Libreria iniziata",
|
||||
"ToastLibraryUpdateSuccess": "Libreria \"{0}\" aggiornata",
|
||||
"ToastMatchAllAuthorsFailed": "Tutti gli autori non sono potuti essere classificati",
|
||||
"ToastNameEmailRequired": "Nome ed email sono obbligatori",
|
||||
"ToastNameRequired": "Il nome è obbligatorio",
|
||||
"ToastNewUserCreatedFailed": "Impossibile creare l'account: \"{0}\"",
|
||||
|
||||
+32
-3
@@ -19,6 +19,7 @@
|
||||
"ButtonChooseFiles": "Pasirinkite failus",
|
||||
"ButtonClearFilter": "Valyti filtrą",
|
||||
"ButtonCloseFeed": "Uždaryti srautą",
|
||||
"ButtonCloseSession": "Uždaryti Atidarytą sesiją",
|
||||
"ButtonCollections": "Kolekcijos",
|
||||
"ButtonConfigureScanner": "Konfigūruoti skenerį",
|
||||
"ButtonCreate": "Kurti",
|
||||
@@ -28,11 +29,14 @@
|
||||
"ButtonEdit": "Redaguoti",
|
||||
"ButtonEditChapters": "Redaguoti skyrius",
|
||||
"ButtonEditPodcast": "Redaguoti tinklalaidę",
|
||||
"ButtonEnable": "Įjungti",
|
||||
"ButtonForceReScan": "Priverstinai nuskaityti iš naujo",
|
||||
"ButtonFullPath": "Visas kelias",
|
||||
"ButtonHide": "Slėpti",
|
||||
"ButtonHome": "Pradžia",
|
||||
"ButtonIssues": "Problemos",
|
||||
"ButtonJumpBackward": "Peršokti atgal",
|
||||
"ButtonJumpForward": "Peršokti į priekį",
|
||||
"ButtonLatest": "Naujausias",
|
||||
"ButtonLibrary": "Biblioteka",
|
||||
"ButtonLogout": "Atsijungti",
|
||||
@@ -42,12 +46,19 @@
|
||||
"ButtonMatchAllAuthors": "Pritaikyti visus autorius",
|
||||
"ButtonMatchBooks": "Pritaikyti knygas",
|
||||
"ButtonNevermind": "Nesvarbu",
|
||||
"ButtonNext": "Kitas",
|
||||
"ButtonNextChapter": "Kitas Skyrius",
|
||||
"ButtonNextItemInQueue": "Kitas eilėje",
|
||||
"ButtonOk": "Ok",
|
||||
"ButtonOpenFeed": "Atidaryti srautą",
|
||||
"ButtonOpenManager": "Atidaryti tvarkyklę",
|
||||
"ButtonPause": "Pauzė",
|
||||
"ButtonPlay": "Groti",
|
||||
"ButtonPlayAll": "Groti Visus",
|
||||
"ButtonPlaying": "Grojama",
|
||||
"ButtonPlaylists": "Grojaraščiai",
|
||||
"ButtonPrevious": "Praeitas",
|
||||
"ButtonPreviousChapter": "Praeitas Skyrius",
|
||||
"ButtonPurgeAllCache": "Valyti visą saugyklą",
|
||||
"ButtonPurgeItemsCache": "Valyti elementų saugyklą",
|
||||
"ButtonQueueAddItem": "Pridėti į eilę",
|
||||
@@ -55,6 +66,9 @@
|
||||
"ButtonQuickMatch": "Greitas pritaikymas",
|
||||
"ButtonReScan": "Iš naujo nuskaityti",
|
||||
"ButtonRead": "Skaityti",
|
||||
"ButtonReadLess": "Mažiau",
|
||||
"ButtonReadMore": "Daugiau",
|
||||
"ButtonRefresh": "Atnaujinti",
|
||||
"ButtonRemove": "Pašalinti",
|
||||
"ButtonRemoveAll": "Pašalinti viską",
|
||||
"ButtonRemoveAllLibraryItems": "Pašalinti visus bibliotekos elementus",
|
||||
@@ -72,12 +86,15 @@
|
||||
"ButtonSelectFolderPath": "Pasirinkti aplanko kelią",
|
||||
"ButtonSeries": "Serijos",
|
||||
"ButtonSetChaptersFromTracks": "Nustatyti skyrius iš takelių",
|
||||
"ButtonShare": "Dalintis",
|
||||
"ButtonShiftTimes": "Perstumti laikus",
|
||||
"ButtonShow": "Rodyti",
|
||||
"ButtonStartM4BEncode": "Pradėti M4B kodavimą",
|
||||
"ButtonStartMetadataEmbed": "Pradėti metaduomenų įterpimą",
|
||||
"ButtonStats": "Statistika",
|
||||
"ButtonSubmit": "Pateikti",
|
||||
"ButtonTest": "Testuoti",
|
||||
"ButtonUnlinkOpenId": "Atsieti OpenID",
|
||||
"ButtonUpload": "Įkelti",
|
||||
"ButtonUploadBackup": "Įkelti atsarginę kopiją",
|
||||
"ButtonUploadCover": "Įkelti viršelį",
|
||||
@@ -86,11 +103,15 @@
|
||||
"ButtonUserEdit": "Redaguoti naudotoją {0}",
|
||||
"ButtonViewAll": "Peržiūrėti visus",
|
||||
"ButtonYes": "Taip",
|
||||
"ErrorUploadFetchMetadataAPI": "Klaida gaunant metaduomenis",
|
||||
"ErrorUploadFetchMetadataNoResults": "Nepavyko gauti metaduomenų - pabandykite atnaujinti pavadinimą ir/ar autorių.",
|
||||
"ErrorUploadLacksTitle": "Pavadinimas yra privalomas",
|
||||
"HeaderAccount": "Paskyra",
|
||||
"HeaderAdvanced": "Papildomi",
|
||||
"HeaderAppriseNotificationSettings": "Apprise pranešimo nustatymai",
|
||||
"HeaderAudioTracks": "Garso takeliai",
|
||||
"HeaderAudiobookTools": "Audioknygų failų valdymo įrankiai",
|
||||
"HeaderAuthentication": "Autentifikacija",
|
||||
"HeaderBackups": "Atsarginės kopijos",
|
||||
"HeaderChangePassword": "Pakeisti slaptažodį",
|
||||
"HeaderChapters": "Skyriai",
|
||||
@@ -99,6 +120,7 @@
|
||||
"HeaderCollectionItems": "Kolekcijos elementai",
|
||||
"HeaderCover": "Viršelis",
|
||||
"HeaderCurrentDownloads": "Dabartiniai parsisiuntimai",
|
||||
"HeaderCustomMessageOnLogin": "Pritaikyta prisijungimo žinutė",
|
||||
"HeaderDetails": "Detalės",
|
||||
"HeaderDownloadQueue": "Parsisiuntimo eilė",
|
||||
"HeaderEbookFiles": "Eknygos failai",
|
||||
@@ -189,7 +211,7 @@
|
||||
"LabelBackToUser": "Grįžti į naudotoją",
|
||||
"LabelBackupsEnableAutomaticBackups": "Įjungti automatinį atsarginių kopijų kūrimą",
|
||||
"LabelBackupsEnableAutomaticBackupsHelp": "Atsarginės kopijos bus išsaugotos /metadata/backups aplanke",
|
||||
"LabelBackupsMaxBackupSize": "Maksimalus atsarginių kopijų dydis (GB)",
|
||||
"LabelBackupsMaxBackupSize": "Maksimalus atsarginių kopijų dydis (GB) (0 - neribotai)",
|
||||
"LabelBackupsMaxBackupSizeHelp": "Jei konfigūruotas dydis viršijamas, atsarginės kopijos nebus sukurtos, kad būtų išvengta klaidingų konfigūracijų.",
|
||||
"LabelBackupsNumberToKeep": "Laikytinų atsarginių kopijų skaičius",
|
||||
"LabelBackupsNumberToKeepHelp": "Tik viena atsarginė kopija bus pašalinta vienu metu, todėl jei jau turite daugiau atsarginių kopijų nei nurodyta, turite jas pašalinti rankiniu būdu.",
|
||||
@@ -397,7 +419,7 @@
|
||||
"LabelSettingsExperimentalFeatures": "Eksperimentiniai funkcionalumai",
|
||||
"LabelSettingsExperimentalFeaturesHelp": "Funkcijos, kurios yra kuriamos ir laukiami jūsų komentarai. Spustelėkite, kad atidarytumėte „GitHub“ diskusiją.",
|
||||
"LabelSettingsFindCovers": "Rasti viršelius",
|
||||
"LabelSettingsFindCoversHelp": "Jei jūsų audioknyga neturi įterpto viršelio arba viršelio paveikslėlio aplanko, skeneris bandys rasti viršelį.<br>Pastaba: Tai padidins skenavimo trukmę.",
|
||||
"LabelSettingsFindCoversHelp": "Jei jūsų audioknyga neturi įterpto viršelio arba viršelio paveikslėlio aplanke, bandyti rasti viršelį.<br>Pastaba: Tai padidins skenavimo trukmę.",
|
||||
"LabelSettingsHideSingleBookSeries": "Slėpti serijas, turinčias tik vieną knygą",
|
||||
"LabelSettingsHideSingleBookSeriesHelp": "Serijos, turinčios tik vieną knygą, bus paslėptos nuo serijų puslapio ir pagrindinio puslapio lentynų.",
|
||||
"LabelSettingsHomePageBookshelfView": "Naudoti pagrindinio puslapio knygų lentynų vaizdą",
|
||||
@@ -413,7 +435,7 @@
|
||||
"LabelSettingsSquareBookCovers": "Naudoti kvadratinius knygos viršelius",
|
||||
"LabelSettingsSquareBookCoversHelp": "Naudoti kvadratinius viršelius vietoj standartinių 1.6:1 knygų viršelių",
|
||||
"LabelSettingsStoreCoversWithItem": "Saugoti viršelius su elementu",
|
||||
"LabelSettingsStoreCoversWithItemHelp": "Pagal nutylėjimą viršeliai saugomi /metadata/items aplanke, įjungus šią parinktį viršeliai bus saugomi jūsų bibliotekos elemento aplanke. Bus išsaugotas tik vienas „cover“ pavadinimo failas.",
|
||||
"LabelSettingsStoreCoversWithItemHelp": "Pagal nutylėjimą viršeliai saugomi /metadata/items aplanke, įjungus šią parinktį viršeliai bus saugomi jūsų bibliotekos elemento aplanke. Bus išsaugotas tik vienas failas su \"cover\" pavadinimu.",
|
||||
"LabelSettingsStoreMetadataWithItem": "Saugoti metaduomenis su elementu",
|
||||
"LabelSettingsStoreMetadataWithItemHelp": "Pagal nutylėjimą metaduomenų failai saugomi /metadata/items aplanke, įjungus šią parinktį metaduomenų failai bus saugomi jūsų bibliotekos elemento aplanke",
|
||||
"LabelSettingsTimeFormat": "Laiko formatas",
|
||||
@@ -642,10 +664,17 @@
|
||||
"ToastBookmarkUpdateSuccess": "Žyma atnaujinta",
|
||||
"ToastChaptersHaveErrors": "Skyriai turi klaidų",
|
||||
"ToastChaptersMustHaveTitles": "Skyriai turi turėti pavadinimus",
|
||||
"ToastChaptersRemoved": "Skyriai pašalinti",
|
||||
"ToastCollectionItemsAddFailed": "Nepavyko pridėti į kolekciją",
|
||||
"ToastCollectionItemsAddSuccess": "Pridėta į kolekciją",
|
||||
"ToastCollectionItemsRemoveSuccess": "Elementai pašalinti iš kolekcijos",
|
||||
"ToastCollectionRemoveSuccess": "Kolekcija pašalinta",
|
||||
"ToastCollectionUpdateSuccess": "Kolekcija atnaujinta",
|
||||
"ToastCoverUpdateFailed": "Viršelio atnaujinimas nepavyko",
|
||||
"ToastDeviceTestEmailSuccess": "Bandomasis el. laiškas išsiųstas",
|
||||
"ToastItemCoverUpdateSuccess": "Elemento viršelis atnaujintas",
|
||||
"ToastItemDeletedFailed": "Nepavyko ištrinti",
|
||||
"ToastItemDeletedSuccess": "Ištrinta",
|
||||
"ToastItemDetailsUpdateSuccess": "Elemento detalės atnaujintos",
|
||||
"ToastItemMarkedAsFinishedFailed": "Pažymėti kaip Baigta nepavyko",
|
||||
"ToastItemMarkedAsFinishedSuccess": "Elementas pažymėtas kaip Baigta",
|
||||
|
||||
+45
-12
@@ -19,6 +19,7 @@
|
||||
"ButtonChooseFiles": "Bestanden kiezen",
|
||||
"ButtonClearFilter": "Filter verwijderen",
|
||||
"ButtonCloseFeed": "Feed sluiten",
|
||||
"ButtonCloseSession": "Sluit Sessie",
|
||||
"ButtonCollections": "Collecties",
|
||||
"ButtonConfigureScanner": "Configureer scanner",
|
||||
"ButtonCreate": "Creëer",
|
||||
@@ -28,9 +29,11 @@
|
||||
"ButtonEdit": "Wijzig",
|
||||
"ButtonEditChapters": "Hoofdstukken wijzigen",
|
||||
"ButtonEditPodcast": "Podcast wijzigen",
|
||||
"ButtonEnable": "Aanzetten",
|
||||
"ButtonForceReScan": "Forceer nieuwe scan",
|
||||
"ButtonFullPath": "Volledig pad",
|
||||
"ButtonHide": "Verberg",
|
||||
"ButtonHome": "Thuis",
|
||||
"ButtonIssues": "Problemen",
|
||||
"ButtonJumpBackward": "Spring achteruit",
|
||||
"ButtonJumpForward": "Spring vooruit",
|
||||
@@ -45,18 +48,23 @@
|
||||
"ButtonNevermind": "Laat maar",
|
||||
"ButtonNext": "Volgende",
|
||||
"ButtonNextChapter": "Volgend hoofdstuk",
|
||||
"ButtonNextItemInQueue": "Volgend Item in Wachtrij",
|
||||
"ButtonOk": "Ok",
|
||||
"ButtonOpenFeed": "Feed openen",
|
||||
"ButtonOpenManager": "Manager openen",
|
||||
"ButtonPause": "Pauze",
|
||||
"ButtonPlay": "Afspelen",
|
||||
"ButtonPlayAll": "Alles Afspelen",
|
||||
"ButtonPlaying": "Speelt",
|
||||
"ButtonPlaylists": "Afspeellijsten",
|
||||
"ButtonPrevious": "Vorige",
|
||||
"ButtonPreviousChapter": "Vorig hoofdstuk",
|
||||
"ButtonProbeAudioFile": "Onderzoek Audio Bestand",
|
||||
"ButtonPurgeAllCache": "Volledige cache legen",
|
||||
"ButtonPurgeItemsCache": "Onderdelen-cache legen",
|
||||
"ButtonQueueAddItem": "In wachtrij zetten",
|
||||
"ButtonQueueRemoveItem": "Uit wachtrij verwijderen",
|
||||
"ButtonQuickEmbed": "Snel Embedden",
|
||||
"ButtonQuickMatch": "Snelle match",
|
||||
"ButtonReScan": "Nieuwe scan",
|
||||
"ButtonRead": "Lees",
|
||||
@@ -69,20 +77,26 @@
|
||||
"ButtonRemoveFromContinueListening": "Vewijder uit Verder luisteren",
|
||||
"ButtonRemoveFromContinueReading": "Verwijder van Verder luisteren",
|
||||
"ButtonRemoveSeriesFromContinueSeries": "Verwijder serie uit Serie vervolgen",
|
||||
"ButtonReset": "Opnieuw Instellen",
|
||||
"ButtonResetToDefault": "Standaardwaarden Terugzetten",
|
||||
"ButtonRestore": "Herstel",
|
||||
"ButtonSave": "Opslaan",
|
||||
"ButtonSaveAndClose": "Opslaan & sluiten",
|
||||
"ButtonSaveTracklist": "Afspeellijst opslaan",
|
||||
"ButtonScan": "Scannen",
|
||||
"ButtonScanLibrary": "Scan bibliotheek",
|
||||
"ButtonSearch": "Zoeken",
|
||||
"ButtonSelectFolderPath": "Maplocatie selecteren",
|
||||
"ButtonSeries": "Series",
|
||||
"ButtonSetChaptersFromTracks": "Maak hoofdstukken op basis van tracks",
|
||||
"ButtonShare": "Deel",
|
||||
"ButtonShiftTimes": "Tijden verschuiven",
|
||||
"ButtonShow": "Toon",
|
||||
"ButtonStartM4BEncode": "Start M4B-encoding",
|
||||
"ButtonStartMetadataEmbed": "Start insluiten metadata",
|
||||
"ButtonStats": "Statistieken",
|
||||
"ButtonSubmit": "Indienen",
|
||||
"ButtonTest": "Testen",
|
||||
"ButtonUploadBackup": "Upload back-up",
|
||||
"ButtonUploadCover": "Upload cover",
|
||||
"ButtonUploadOPMLFile": "Upload OPML-bestand",
|
||||
@@ -93,6 +107,7 @@
|
||||
"ErrorUploadFetchMetadataAPI": "Error metadata ophalen",
|
||||
"ErrorUploadFetchMetadataNoResults": "Kan metadata niet ophalen - probeer de titel en/of auteur te updaten",
|
||||
"ErrorUploadLacksTitle": "Moet een titel hebben",
|
||||
"HeaderAccount": "Account",
|
||||
"HeaderAdvanced": "Geavanceerd",
|
||||
"HeaderAppriseNotificationSettings": "Apprise-notificatie instellingen",
|
||||
"HeaderAudioTracks": "Audiotracks",
|
||||
@@ -105,6 +120,7 @@
|
||||
"HeaderCollectionItems": "Collectie-objecten",
|
||||
"HeaderCover": "Omslag",
|
||||
"HeaderCurrentDownloads": "Huidige downloads",
|
||||
"HeaderDetails": "Details",
|
||||
"HeaderDownloadQueue": "Download-wachtrij",
|
||||
"HeaderEbookFiles": "Ebook bestanden",
|
||||
"HeaderEmail": "E-mail",
|
||||
@@ -207,9 +223,9 @@
|
||||
"LabelCollections": "Collecties",
|
||||
"LabelComplete": "Compleet",
|
||||
"LabelConfirmPassword": "Bevestig wachtwoord",
|
||||
"LabelContinueListening": "Verder luisteren",
|
||||
"LabelContinueReading": "Verder luisteren",
|
||||
"LabelContinueSeries": "Ga verder met serie",
|
||||
"LabelContinueListening": "Verder Luisteren",
|
||||
"LabelContinueReading": "Verder lezen",
|
||||
"LabelContinueSeries": "Doorgaan met Serie",
|
||||
"LabelCoverImageURL": "Coverafbeelding URL",
|
||||
"LabelCreatedAt": "Gecreëerd op",
|
||||
"LabelCronExpression": "Cron-uitdrukking",
|
||||
@@ -225,9 +241,12 @@
|
||||
"LabelDirectory": "Map",
|
||||
"LabelDiscFromFilename": "Schijf uit bestandsnaam",
|
||||
"LabelDiscFromMetadata": "Schijf uit metadata",
|
||||
"LabelDiscover": "Ontdek",
|
||||
"LabelDiscover": "Ontdekken",
|
||||
"LabelDownload": "Download",
|
||||
"LabelDuration": "Duur",
|
||||
"LabelDurationFound": "Gevonden duur:",
|
||||
"LabelEbook": "Ebook",
|
||||
"LabelEbooks": "Eboeken",
|
||||
"LabelEdit": "Wijzig",
|
||||
"LabelEmailSettingsFromAddress": "Van-adres",
|
||||
"LabelEmailSettingsSecure": "Veilig",
|
||||
@@ -236,11 +255,13 @@
|
||||
"LabelEmbeddedCover": "Ingesloten cover",
|
||||
"LabelEnable": "Inschakelen",
|
||||
"LabelEnd": "Einde",
|
||||
"LabelEndOfChapter": "Einde van het Hoofdstuk",
|
||||
"LabelEpisode": "Aflevering",
|
||||
"LabelEpisodeTitle": "Afleveringtitel",
|
||||
"LabelEpisodeType": "Afleveringtype",
|
||||
"LabelExample": "Voorbeeld",
|
||||
"LabelExplicit": "Expliciet",
|
||||
"LabelFeedURL": "Feed URL",
|
||||
"LabelFetchingMetadata": "Metadata ophalen",
|
||||
"LabelFile": "Bestand",
|
||||
"LabelFileBirthtime": "Aanmaaktijd bestand",
|
||||
@@ -252,12 +273,16 @@
|
||||
"LabelFolder": "Map",
|
||||
"LabelFolders": "Mappen",
|
||||
"LabelFontBold": "Vetgedrukt",
|
||||
"LabelFontBoldness": "Font Boldness",
|
||||
"LabelFontFamily": "Lettertypefamilie",
|
||||
"LabelFontScale": "Lettertype schaal",
|
||||
"LabelFormat": "Formaat",
|
||||
"LabelGenre": "Genre",
|
||||
"LabelGenres": "Genres",
|
||||
"LabelHardDeleteFile": "Hard-delete bestand",
|
||||
"LabelHasEbook": "Heeft ebook",
|
||||
"LabelHasSupplementaryEbook": "Heeft supplementair ebook",
|
||||
"LabelHasEbook": "Heeft Ebook",
|
||||
"LabelHasSupplementaryEbook": "Heeft aanvullend Ebook",
|
||||
"LabelHost": "Host",
|
||||
"LabelHour": "Uur",
|
||||
"LabelHours": "Uren",
|
||||
"LabelIcon": "Icoon",
|
||||
@@ -281,6 +306,7 @@
|
||||
"LabelLastSeen": "Laatst gezien",
|
||||
"LabelLastTime": "Laatste keer",
|
||||
"LabelLastUpdate": "Laatste update",
|
||||
"LabelLayout": "Layout",
|
||||
"LabelLayoutSinglePage": "Enkele pagina",
|
||||
"LabelLayoutSplitPage": "Gesplitste pagina",
|
||||
"LabelLess": "Minder",
|
||||
@@ -290,7 +316,7 @@
|
||||
"LabelLibraryName": "Bibliotheeknaam",
|
||||
"LabelLimit": "Limiet",
|
||||
"LabelLineSpacing": "Regelruimte",
|
||||
"LabelListenAgain": "Luister opnieuw",
|
||||
"LabelListenAgain": "Opnieuw Beluisteren",
|
||||
"LabelLogLevelWarn": "Waarschuwing",
|
||||
"LabelLookForNewEpisodesAfterDate": "Zoek naar nieuwe afleveringen na deze datum",
|
||||
"LabelMediaPlayer": "Mediaspeler",
|
||||
@@ -307,8 +333,8 @@
|
||||
"LabelNarrators": "Vertellers",
|
||||
"LabelNew": "Nieuw",
|
||||
"LabelNewPassword": "Nieuw wachtwoord",
|
||||
"LabelNewestAuthors": "Nieuwste auteurs",
|
||||
"LabelNewestEpisodes": "Nieuwste afleveringen",
|
||||
"LabelNewestAuthors": "Nieuwste Auteurs",
|
||||
"LabelNewestEpisodes": "Nieuwste Afleveringen",
|
||||
"LabelNextBackupDate": "Volgende back-up datum",
|
||||
"LabelNextScheduledRun": "Volgende geplande run",
|
||||
"LabelNoEpisodesSelected": "Geen afleveringen geselecteerd",
|
||||
@@ -339,6 +365,7 @@
|
||||
"LabelPhotoPathURL": "Foto pad/URL",
|
||||
"LabelPlayMethod": "Afspeelwijze",
|
||||
"LabelPlaylists": "Afspeellijsten",
|
||||
"LabelPodcast": "Podcast",
|
||||
"LabelPodcastSearchRegion": "Podcast zoekregio",
|
||||
"LabelPodcastType": "Podcasttype",
|
||||
"LabelPort": "Poort",
|
||||
@@ -356,11 +383,12 @@
|
||||
"LabelRSSFeedPreventIndexing": "Voorkom indexering",
|
||||
"LabelRSSFeedSlug": "RSS-feed slug",
|
||||
"LabelRSSFeedURL": "RSS-feed URL",
|
||||
"LabelRandomly": "Willekeurig",
|
||||
"LabelRead": "Lees",
|
||||
"LabelReadAgain": "Lees opnieuw",
|
||||
"LabelReadAgain": "Opnieuw Lezen",
|
||||
"LabelReadEbookWithoutProgress": "Lees ebook zonder voortgang bij te houden",
|
||||
"LabelRecentSeries": "Recente series",
|
||||
"LabelRecentlyAdded": "Recent toegevoegd",
|
||||
"LabelRecentSeries": "Recente Serie",
|
||||
"LabelRecentlyAdded": "Recent Toegevoegd",
|
||||
"LabelRecommended": "Aangeraden",
|
||||
"LabelRegion": "Regio",
|
||||
"LabelReleaseDate": "Verschijningsdatum",
|
||||
@@ -414,6 +442,7 @@
|
||||
"LabelShowAll": "Toon alle",
|
||||
"LabelSize": "Grootte",
|
||||
"LabelSleepTimer": "Slaaptimer",
|
||||
"LabelStart": "Start",
|
||||
"LabelStartTime": "Starttijd",
|
||||
"LabelStarted": "Gestart",
|
||||
"LabelStartedAt": "Gestart op",
|
||||
@@ -434,6 +463,8 @@
|
||||
"LabelStatsWeekListening": "Week luisterend",
|
||||
"LabelSubtitle": "Subtitel",
|
||||
"LabelSupportedFileTypes": "Ondersteunde bestandstypes",
|
||||
"LabelTag": "Tag",
|
||||
"LabelTags": "Tags",
|
||||
"LabelTagsAccessibleToUser": "Tags toegankelijk voor de gebruiker",
|
||||
"LabelTagsNotAccessibleToUser": "Tags niet toegankelijk voor de gebruiker",
|
||||
"LabelTasks": "Lopende taken",
|
||||
@@ -456,8 +487,10 @@
|
||||
"LabelTotalTimeListened": "Totale tijd geluisterd",
|
||||
"LabelTrackFromFilename": "Track vanuit bestandsnaam",
|
||||
"LabelTrackFromMetadata": "Track vanuit metadata",
|
||||
"LabelTracks": "Audiosporen",
|
||||
"LabelTracksNone": "Geen tracks",
|
||||
"LabelTracksSingleTrack": "Enkele track",
|
||||
"LabelType": "Type",
|
||||
"LabelUnabridged": "Onverkort",
|
||||
"LabelUndo": "Ongedaan maken",
|
||||
"LabelUnknown": "Onbekend",
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
"ButtonStartM4BEncode": "Eksportuj jako plik M4B",
|
||||
"ButtonStartMetadataEmbed": "Osadź metadane",
|
||||
"ButtonStats": "Statystyki",
|
||||
"ButtonSubmit": "Zaloguj",
|
||||
"ButtonSubmit": "Pobierz",
|
||||
"ButtonTest": "Test",
|
||||
"ButtonUnlinkOpenId": "Odłącz OpenID",
|
||||
"ButtonUpload": "Wgraj",
|
||||
@@ -462,7 +462,7 @@
|
||||
"LabelReadAgain": "Czytaj ponownie",
|
||||
"LabelReadEbookWithoutProgress": "Czytaj książkę bez zapamiętywania postępu",
|
||||
"LabelRecentSeries": "Ostatnie serie",
|
||||
"LabelRecentlyAdded": "Niedawno dodany",
|
||||
"LabelRecentlyAdded": "Niedawno dodane",
|
||||
"LabelRecommended": "Polecane",
|
||||
"LabelRedo": "Wycofaj",
|
||||
"LabelReleaseDate": "Data wydania",
|
||||
|
||||
+115
-41
@@ -66,6 +66,7 @@
|
||||
"ButtonPurgeItemsCache": "Počisti predpomnilnik elementov",
|
||||
"ButtonQueueAddItem": "Dodaj v čakalno vrsto",
|
||||
"ButtonQueueRemoveItem": "Odstrani iz čakalne vrste",
|
||||
"ButtonQuickEmbed": "Hitra vdelava",
|
||||
"ButtonQuickEmbedMetadata": "Hitra vdelava metapodatkov",
|
||||
"ButtonQuickMatch": "Hitro ujemanje",
|
||||
"ButtonReScan": "Ponovno pregledovanje",
|
||||
@@ -134,7 +135,7 @@
|
||||
"HeaderEmail": "E-pošta",
|
||||
"HeaderEmailSettings": "Nastavitve e-pošte",
|
||||
"HeaderEpisodes": "Epizode",
|
||||
"HeaderEreaderDevices": "Ebralne naprave",
|
||||
"HeaderEreaderDevices": "E-bralniki",
|
||||
"HeaderEreaderSettings": "Nastavitve ebralnika",
|
||||
"HeaderFiles": "Datoteke",
|
||||
"HeaderFindChapters": "Najdi poglavja",
|
||||
@@ -146,7 +147,7 @@
|
||||
"HeaderLibraries": "Knjižnice",
|
||||
"HeaderLibraryFiles": "Datoteke knjižnice",
|
||||
"HeaderLibraryStats": "Statistika knjižnice",
|
||||
"HeaderListeningSessions": "Seje poslušanja",
|
||||
"HeaderListeningSessions": "Sej poslušanja",
|
||||
"HeaderListeningStats": "Statistika poslušanja",
|
||||
"HeaderLogin": "Prijava",
|
||||
"HeaderLogs": "Dnevniki",
|
||||
@@ -161,10 +162,10 @@
|
||||
"HeaderNotificationCreate": "Ustvari obvestilo",
|
||||
"HeaderNotificationUpdate": "Posodobi obvestilo",
|
||||
"HeaderNotifications": "Obvestila",
|
||||
"HeaderOpenIDConnectAuthentication": "Preverjanje pristnosti OpenID Connect",
|
||||
"HeaderOpenIDConnectAuthentication": "Prijava z OpenID Connect",
|
||||
"HeaderOpenRSSFeed": "Odpri vir RSS",
|
||||
"HeaderOtherFiles": "Ostale datoteke",
|
||||
"HeaderPasswordAuthentication": "Preverjanje pristnosti gesla",
|
||||
"HeaderPasswordAuthentication": "Preverjanje pristnosti z geslom",
|
||||
"HeaderPermissions": "Dovoljenja",
|
||||
"HeaderPlayerQueue": "Čakalna vrsta predvajalnika",
|
||||
"HeaderPlayerSettings": "Nastavitve predvajalnika",
|
||||
@@ -179,6 +180,7 @@
|
||||
"HeaderRemoveEpisodes": "Odstrani {0} epizod",
|
||||
"HeaderSavedMediaProgress": "Shranjen napredek predstavnosti",
|
||||
"HeaderSchedule": "Načrtovanje",
|
||||
"HeaderScheduleEpisodeDownloads": "Načrtovanje samodejnega prenosa epizod",
|
||||
"HeaderScheduleLibraryScans": "Načrtuj samodejno pregledovanje knjižnice",
|
||||
"HeaderSession": "Seja",
|
||||
"HeaderSetBackupSchedule": "Nastavite urnik varnostnega kopiranja",
|
||||
@@ -186,7 +188,7 @@
|
||||
"HeaderSettingsDisplay": "Zaslon",
|
||||
"HeaderSettingsExperimental": "Eksperimentalne funkcije",
|
||||
"HeaderSettingsGeneral": "Splošno",
|
||||
"HeaderSettingsScanner": "Skener",
|
||||
"HeaderSettingsScanner": "Pregledovalnik",
|
||||
"HeaderSleepTimer": "Časovnik za izklop",
|
||||
"HeaderStatsLargestItems": "Največji elementi",
|
||||
"HeaderStatsLongestItems": "Najdaljši elementi (ure)",
|
||||
@@ -219,12 +221,15 @@
|
||||
"LabelAddedAt": "Dodano ob",
|
||||
"LabelAddedDate": "Dodano {0}",
|
||||
"LabelAdminUsersOnly": "Samo administratorji",
|
||||
"LabelAll": "Vsi",
|
||||
"LabelAll": "Vse",
|
||||
"LabelAllUsers": "Vsi uporabniki",
|
||||
"LabelAllUsersExcludingGuests": "Vsi uporabniki razen gosti",
|
||||
"LabelAllUsersIncludingGuests": "Vsi uporabniki vključno z gosti",
|
||||
"LabelAlreadyInYourLibrary": "Že v tvoji knjižnici",
|
||||
"LabelAppend": "Priloži",
|
||||
"LabelAudioBitrate": "Avdio bitna hitrost (npr. 128k)",
|
||||
"LabelAudioChannels": "Avdio kanali (1 ali 2)",
|
||||
"LabelAudioCodec": "Avdio kodek",
|
||||
"LabelAuthor": "Avtor",
|
||||
"LabelAuthorFirstLast": "Avtor (ime priimek)",
|
||||
"LabelAuthorLastFirst": "Avtor (priimek, ime)",
|
||||
@@ -237,6 +242,7 @@
|
||||
"LabelAutoRegister": "Samodejna registracija",
|
||||
"LabelAutoRegisterDescription": "Po prijavi samodejno ustvari nove uporabnike",
|
||||
"LabelBackToUser": "Nazaj na uporabnika",
|
||||
"LabelBackupAudioFiles": "Varnostno kopiranje zvočnih datotek",
|
||||
"LabelBackupLocation": "Lokacija rezervne kopije",
|
||||
"LabelBackupsEnableAutomaticBackups": "Omogoči samodejno varnostno kopiranje",
|
||||
"LabelBackupsEnableAutomaticBackupsHelp": "Varnostne kopije shranjene v /metadata/backups",
|
||||
@@ -245,15 +251,18 @@
|
||||
"LabelBackupsNumberToKeep": "Število varnostnih kopij, ki jih je treba hraniti",
|
||||
"LabelBackupsNumberToKeepHelp": "Naenkrat bo odstranjena samo ena varnostna kopija, če že imate več varnostnih kopij, jih odstranite ročno.",
|
||||
"LabelBitrate": "Bitna hitrost",
|
||||
"LabelBooks": "Knjige",
|
||||
"LabelBonus": "Bonus",
|
||||
"LabelBooks": "knjig",
|
||||
"LabelButtonText": "Besedilo gumba",
|
||||
"LabelByAuthor": "od {0}",
|
||||
"LabelChangePassword": "Spremeni geslo",
|
||||
"LabelChannels": "Kanali",
|
||||
"LabelChapterCount": "{0} poglavij",
|
||||
"LabelChapterTitle": "Naslov poglavja",
|
||||
"LabelChapters": "Poglavja",
|
||||
"LabelChaptersFound": "najdenih poglavij",
|
||||
"LabelClickForMoreInfo": "Klikni za več informacij",
|
||||
"LabelClickToUseCurrentValue": "Klikni za uporabo trenutne vrednosti",
|
||||
"LabelClosePlayer": "Zapri predvajalnik",
|
||||
"LabelCodec": "Kodek",
|
||||
"LabelCollapseSeries": "Strni serije",
|
||||
@@ -303,12 +312,25 @@
|
||||
"LabelEmailSettingsTestAddress": "Testiraj naslov",
|
||||
"LabelEmbeddedCover": "Vdelana naslovnica",
|
||||
"LabelEnable": "Omogoči",
|
||||
"LabelEncodingBackupLocation": "Varnostna kopija vaših izvirnih zvočnih datotek bo shranjena v:",
|
||||
"LabelEncodingChaptersNotEmbedded": "Poglavja niso vdelana v zvočne knjige z večimi sledmi.",
|
||||
"LabelEncodingClearItemCache": "Občasno počistite predpomnilnik elementov.",
|
||||
"LabelEncodingFinishedM4B": "Končana M4B datoteka bo shranjena v vaši mapi z zvočnimi knjigami:",
|
||||
"LabelEncodingInfoEmbedded": "Metapodatki bodo vdelani v zvočne posnetke znotraj vaše mape zvočne knjige.",
|
||||
"LabelEncodingStartedNavigation": "Ko se opravilo začne, lahko zapustite to stran.",
|
||||
"LabelEncodingTimeWarning": "Enkodiranje lahko traja tudi do 30 minut.",
|
||||
"LabelEncodingWarningAdvancedSettings": "Opozorilo: Ne posodabljajte teh nastavitev, razen če poznate možnosti ekodiranja s programom ffmpeg.",
|
||||
"LabelEncodingWatcherDisabled": "Če ste spremljanje datotečnega sistema onemogočili, boste morali pozneje ponovno pregledati to zvočno knjigo.",
|
||||
"LabelEnd": "Konec",
|
||||
"LabelEndOfChapter": "Konec poglavja",
|
||||
"LabelEpisode": "Epizoda",
|
||||
"LabelEpisodeNotLinkedToRssFeed": "Epizoda ni povezana z virom RSS",
|
||||
"LabelEpisodeNumber": "Epizoda #{0}",
|
||||
"LabelEpisodeTitle": "Naslov epizode",
|
||||
"LabelEpisodeType": "Tip epizode",
|
||||
"LabelEpisodeUrlFromRssFeed": "URL epizode iz vira RSS",
|
||||
"LabelEpisodes": "Epizode",
|
||||
"LabelEpisodic": "Epizodično",
|
||||
"LabelExample": "Primer",
|
||||
"LabelExpandSeries": "Razširi serije",
|
||||
"LabelExpandSubSeries": "Razširi podserije",
|
||||
@@ -336,6 +358,7 @@
|
||||
"LabelFontScale": "Merilo pisave",
|
||||
"LabelFontStrikethrough": "Prečrtano",
|
||||
"LabelFormat": "Oblika",
|
||||
"LabelFull": "Polno",
|
||||
"LabelGenre": "Žanr",
|
||||
"LabelGenres": "Žanri",
|
||||
"LabelHardDeleteFile": "Trdo brisanje datoteke",
|
||||
@@ -350,7 +373,7 @@
|
||||
"LabelImageURLFromTheWeb": "URL slike iz spleta",
|
||||
"LabelInProgress": "V teku",
|
||||
"LabelIncludeInTracklist": "Vključi v seznam skladb",
|
||||
"LabelIncomplete": "Nepopolno",
|
||||
"LabelIncomplete": "Nedokončano",
|
||||
"LabelInterval": "Interval",
|
||||
"LabelIntervalCustomDailyWeekly": "Dnevno/tedensko po meri",
|
||||
"LabelIntervalEvery12Hours": "Vsakih 12 ur",
|
||||
@@ -391,6 +414,10 @@
|
||||
"LabelLowestPriority": "Najnižja prioriteta",
|
||||
"LabelMatchExistingUsersBy": "Poveži obstoječe uporabnike po",
|
||||
"LabelMatchExistingUsersByDescription": "Uporablja se za povezovanje obstoječih uporabnikov. Ko se vzpostavi povezava, se bodo uporabniki ujemali z enoličnim ID-jem vašega ponudnika SSO",
|
||||
"LabelMaxEpisodesToDownload": "Največje število epizod za prenos. Uporabite 0 za neomejeno.",
|
||||
"LabelMaxEpisodesToDownloadPerCheck": "Največje število novih epizod za prenos ob preverjanju",
|
||||
"LabelMaxEpisodesToKeep": "Največje število epizod, ki jih lahko obdržite",
|
||||
"LabelMaxEpisodesToKeepHelp": "Vrednost 0 ne omejuje navišjega števila. Ko se nova epizoda samodejno prenese, se bo izbrisala najstarejša epizoda, če imate več kot X epizod. S tem boste izbrisali samo 1 epizodo na nov prenos.",
|
||||
"LabelMediaPlayer": "Medijski predvajalnik",
|
||||
"LabelMediaType": "Vrsta medija",
|
||||
"LabelMetaTag": "Meta oznaka",
|
||||
@@ -400,8 +427,8 @@
|
||||
"LabelMinute": "Minuta",
|
||||
"LabelMinutes": "Minute",
|
||||
"LabelMissing": "Manjkajoče",
|
||||
"LabelMissingEbook": "Nima nobene eknjige",
|
||||
"LabelMissingSupplementaryEbook": "Nima nobene dodatne eknjige",
|
||||
"LabelMissingEbook": "Nima nobene e-knjige",
|
||||
"LabelMissingSupplementaryEbook": "Nima nobene dodatne e-knjige",
|
||||
"LabelMobileRedirectURIs": "Dovoljeni mobilni preusmeritveni URI-ji",
|
||||
"LabelMobileRedirectURIsDescription": "To je seznam dovoljenih veljavnih preusmeritvenih URI-jev za mobilne aplikacije. Privzeti je <code>audiobookshelf://oauth</code>, ki ga lahko odstranite ali dopolnite z dodatnimi URI-ji za integracijo aplikacij tretjih oseb. Uporaba zvezdice (<code>*</code>) kot edinega vnosa dovoljuje kateri koli URI.",
|
||||
"LabelMore": "Več",
|
||||
@@ -422,7 +449,7 @@
|
||||
"LabelNotes": "Opombe",
|
||||
"LabelNotificationAppriseURL": "Apprise URL(ji)",
|
||||
"LabelNotificationAvailableVariables": "Razpoložljive spremenljivke",
|
||||
"LabelNotificationBodyTemplate": "Predloga telesa",
|
||||
"LabelNotificationBodyTemplate": "Predloga vsebime",
|
||||
"LabelNotificationEvent": "Dogodek obvestila",
|
||||
"LabelNotificationTitleTemplate": "Predloga naslova",
|
||||
"LabelNotificationsMaxFailedAttempts": "Najvišje število neuspelih poskusov",
|
||||
@@ -463,10 +490,12 @@
|
||||
"LabelProvider": "Ponudnik",
|
||||
"LabelProviderAuthorizationValue": "Vrednost glave avtorizacije",
|
||||
"LabelPubDate": "Datum objave",
|
||||
"LabelPublishYear": "Leto objave",
|
||||
"LabelPublishedDate": "Objavljeno {0}",
|
||||
"LabelPublisher": "Založnik",
|
||||
"LabelPublishers": "Založniki",
|
||||
"LabelPublishYear": "Leto izdaje",
|
||||
"LabelPublishedDate": "Izdano {0}",
|
||||
"LabelPublishedDecade": "Desetletje izdaje",
|
||||
"LabelPublishedDecades": "Desetletja izdaje",
|
||||
"LabelPublisher": "Izdajatelj",
|
||||
"LabelPublishers": "Izdajatelji",
|
||||
"LabelRSSFeedCustomOwnerEmail": "E-pošta lastnika po meri",
|
||||
"LabelRSSFeedCustomOwnerName": "Ime lastnika po meri",
|
||||
"LabelRSSFeedOpen": "Odprt vir RSS",
|
||||
@@ -484,21 +513,28 @@
|
||||
"LabelRedo": "Ponovi",
|
||||
"LabelRegion": "Regija",
|
||||
"LabelReleaseDate": "Datum izdaje",
|
||||
"LabelRemoveAllMetadataAbs": "Odstrani vse datoteke metadata.abs",
|
||||
"LabelRemoveAllMetadataJson": "Odstrani vse datoteke metadata.json",
|
||||
"LabelRemoveCover": "Odstrani naslovnico",
|
||||
"LabelRemoveMetadataFile": "Odstrani datoteke z metapodatki v mapah elementov knjižnice",
|
||||
"LabelRemoveMetadataFileHelp": "Odstrani vse datoteke metadata.json in metadata.abs v svojih mapah {0}.",
|
||||
"LabelRowsPerPage": "Vrstic na stran",
|
||||
"LabelSearchTerm": "Iskalni pojem",
|
||||
"LabelSearchTitle": "Naslov iskanja",
|
||||
"LabelSearchTitleOrASIN": "Naslov iskanja ali ASIN",
|
||||
"LabelSeason": "Sezona",
|
||||
"LabelSeasonNumber": "Sezona #{0}",
|
||||
"LabelSelectAll": "Izberite vse",
|
||||
"LabelSelectAllEpisodes": "Izberite vse epizode",
|
||||
"LabelSelectEpisodesShowing": "Izberi {0} prikazanih epizod",
|
||||
"LabelSelectUsers": "Izberite uporabnike",
|
||||
"LabelSendEbookToDevice": "Pošlji eknjigo k...",
|
||||
"LabelSequence": "Zaporedje",
|
||||
"LabelSerial": "Serija",
|
||||
"LabelSeries": "Serije",
|
||||
"LabelSeriesName": "Ime serije",
|
||||
"LabelSeriesProgress": "Napredek serije",
|
||||
"LabelServerLogLevel": "Raven dnevnika strežnika",
|
||||
"LabelServerYearReview": "Pregled leta strežnika ({0})",
|
||||
"LabelSetEbookAsPrimary": "Nastavi kot primarno",
|
||||
"LabelSetEbookAsSupplementary": "Nastavi kot dodatno",
|
||||
@@ -507,11 +543,11 @@
|
||||
"LabelSettingsBookshelfViewHelp": "Skeuomorfna oblika z lesenimi policami",
|
||||
"LabelSettingsChromecastSupport": "Podpora za Chromecast",
|
||||
"LabelSettingsDateFormat": "Oblika datuma",
|
||||
"LabelSettingsDisableWatcher": "Onemogoči pregledovalca",
|
||||
"LabelSettingsDisableWatcherForLibrary": "Onemogoči pregledovalca map za knjižnico",
|
||||
"LabelSettingsDisableWatcher": "Onemogoči spremljanje datotečnega sistema",
|
||||
"LabelSettingsDisableWatcherForLibrary": "Onemogoči spremljanje map za knjižnico",
|
||||
"LabelSettingsDisableWatcherHelp": "Onemogoči samodejno dodajanje/posodabljanje elementov, ko so zaznane spremembe datoteke. *Potreben je ponovni zagon strežnika",
|
||||
"LabelSettingsEnableWatcher": "Omogoči pregledovalca",
|
||||
"LabelSettingsEnableWatcherForLibrary": "Omogoči pregledovalca map za knjižnico",
|
||||
"LabelSettingsEnableWatcher": "Omogoči spremljanje sprememb",
|
||||
"LabelSettingsEnableWatcherForLibrary": "Omogoči spremljanje sprememb v mapi knjižnice",
|
||||
"LabelSettingsEnableWatcherHelp": "Omogoča samodejno dodajanje/posodabljanje elementov, ko so zaznane spremembe datoteke. *Potreben je ponovni zagon strežnika",
|
||||
"LabelSettingsEpubsAllowScriptedContent": "Dovoli skriptirano vsebino v epubih",
|
||||
"LabelSettingsEpubsAllowScriptedContentHelp": "Dovoli datotekam epub izvajanje skript. Priporočljivo je, da to nastavitev pustite onemogočeno, razen če zaupate viru datotek epub.",
|
||||
@@ -526,12 +562,12 @@
|
||||
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Preskoči prejšnje knjige v nadaljevanju serije",
|
||||
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Polica z domačo stranjo Nadaljuj serijo prikazuje prvo nezačeto knjigo v seriji, ki ima vsaj eno dokončano knjigo in ni nobene knjige v teku. Če omogočite to nastavitev, se bo serija nadaljevala od najbolj dokončane knjige namesto od prve nezačete knjige.",
|
||||
"LabelSettingsParseSubtitles": "Uporabi podnapise",
|
||||
"LabelSettingsParseSubtitlesHelp": "Izvleci podnapise iz imen map zvočnih knjig.<br>Podnaslov mora biti ločen z \" - \"<br>npr. »Naslov knjige – Tu podnapis« ima podnaslov »Tu podnapis«",
|
||||
"LabelSettingsParseSubtitlesHelp": "Izvleci podnapise iz imen map zvočnih knjig.<br>Podnapis mora biti ločen z \" - \"<br>npr. \"Naslov knjige – tu podnapis\" ima podnapis \"tu podnapis\"",
|
||||
"LabelSettingsPreferMatchedMetadata": "Prednost imajo ujemajoči se metapodatki",
|
||||
"LabelSettingsPreferMatchedMetadataHelp": "Pri uporabi hitrega ujemanja bodo ujemajoči se podatki preglasili podrobnosti artikla. Hitro ujemanje bo privzeto izpolnil samo manjkajoče podrobnosti.",
|
||||
"LabelSettingsSkipMatchingBooksWithASIN": "Preskoči ujemajoče se knjige, ki že imajo ASIN",
|
||||
"LabelSettingsSkipMatchingBooksWithISBN": "Preskoči ujemajoče se knjige, ki že imajo oznako ISBN",
|
||||
"LabelSettingsSortingIgnorePrefixes": "Pri razvrščanju ne upoštevajte predpon",
|
||||
"LabelSettingsSortingIgnorePrefixes": "Pri razvrščanju ne upoštevaj predpon",
|
||||
"LabelSettingsSortingIgnorePrefixesHelp": "npr. za naslov knjige s predpono \"the\" bi se \"The Book Title\" razvrstil kot \"Book Title, The\"",
|
||||
"LabelSettingsSquareBookCovers": "Uporabi kvadratne platnice knjig",
|
||||
"LabelSettingsSquareBookCoversHelp": "Raje uporabi kvadratne platnice kot standardne knjižne platnice 1.6:1",
|
||||
@@ -558,15 +594,15 @@
|
||||
"LabelStatsBestDay": "Najboljši dan",
|
||||
"LabelStatsDailyAverage": "Dnevno povprečje",
|
||||
"LabelStatsDays": "Dnevi",
|
||||
"LabelStatsDaysListened": "Poslušani dnevi",
|
||||
"LabelStatsDaysListened": "Dnevi poslušanja",
|
||||
"LabelStatsHours": "Ure",
|
||||
"LabelStatsInARow": "v vrsti",
|
||||
"LabelStatsItemsFinished": "Končani elementi",
|
||||
"LabelStatsItemsInLibrary": "Elementi v knjižnici",
|
||||
"LabelStatsMinutes": "minute",
|
||||
"LabelStatsMinutesListening": "Poslušane minute",
|
||||
"LabelStatsMinutesListening": "Minut poslušanja",
|
||||
"LabelStatsOverallDays": "Skupaj dnevi",
|
||||
"LabelStatsOverallHours": "Skupaj ure",
|
||||
"LabelStatsOverallHours": "Skupaj ur",
|
||||
"LabelStatsWeekListening": "Tednov poslušanja",
|
||||
"LabelSubtitle": "Podnapis",
|
||||
"LabelSupportedFileTypes": "Podprte vrste datotek",
|
||||
@@ -587,6 +623,7 @@
|
||||
"LabelTimeDurationXMinutes": "{0} minut",
|
||||
"LabelTimeDurationXSeconds": "{0} sekund",
|
||||
"LabelTimeInMinutes": "Čas v minutah",
|
||||
"LabelTimeLeft": "{0} še preostane",
|
||||
"LabelTimeListened": "Čas poslušanja",
|
||||
"LabelTimeListenedToday": "Čas poslušanja danes",
|
||||
"LabelTimeRemaining": "Še {0}",
|
||||
@@ -594,8 +631,9 @@
|
||||
"LabelTitle": "Naslov",
|
||||
"LabelToolsEmbedMetadata": "Vdelaj metapodatke",
|
||||
"LabelToolsEmbedMetadataDescription": "Vdelajte metapodatke v zvočne datoteke, vključno s sliko naslovnice in poglavji.",
|
||||
"LabelToolsMakeM4b": "Ustvari datoteko zvočne knjige M4B",
|
||||
"LabelToolsMakeM4bDescription": "Ustvarite datoteko zvočne knjige .M4B z vdelanimi metapodatki, sliko naslovnice in poglavji.",
|
||||
"LabelToolsM4bEncoder": "M4B enkoder",
|
||||
"LabelToolsMakeM4b": "Ustvari M4B datoteko zvočne knjige",
|
||||
"LabelToolsMakeM4bDescription": "Ustvari zvočno knjigo v .M4B obliki z vdelanimi metapodatki, sliko naslovnice in poglavji.",
|
||||
"LabelToolsSplitM4b": "Razdeli M4B v MP3 datoteke",
|
||||
"LabelToolsSplitM4bDescription": "Ustvarite MP3 datoteke iz datoteke M4B, razdeljene po poglavjih z vdelanimi metapodatki, naslovno sliko in poglavji.",
|
||||
"LabelTotalDuration": "Skupno trajanje",
|
||||
@@ -606,11 +644,12 @@
|
||||
"LabelTracksMultiTrack": "Več posnetkov",
|
||||
"LabelTracksNone": "Brez posnetka",
|
||||
"LabelTracksSingleTrack": "Enojni posnetek",
|
||||
"LabelTrailer": "Napovednik",
|
||||
"LabelType": "Vrsta",
|
||||
"LabelUnabridged": "Neskrajšano",
|
||||
"LabelUndo": "Razveljavi",
|
||||
"LabelUnknown": "Neznano",
|
||||
"LabelUnknownPublishDate": "Neznan datum objave",
|
||||
"LabelUnknownPublishDate": "Neznan datum izdaje",
|
||||
"LabelUpdateCover": "Posodobi naslovnico",
|
||||
"LabelUpdateCoverHelp": "Dovoli prepisovanje obstoječih naslovnic za izbrane knjige, ko se najde ujemanje",
|
||||
"LabelUpdateDetails": "Posodobi podrobnosti",
|
||||
@@ -619,8 +658,10 @@
|
||||
"LabelUploaderDragAndDrop": "Povleci in spusti datoteke ali mape",
|
||||
"LabelUploaderDropFiles": "Spusti datoteke",
|
||||
"LabelUploaderItemFetchMetadataHelp": "Samodejno pridobi naslov, avtorja in serijo",
|
||||
"LabelUseAdvancedOptions": "Uporabi napredne možnosti",
|
||||
"LabelUseChapterTrack": "Uporabi posnetek poglavij",
|
||||
"LabelUseFullTrack": "Uporabi celoten posnetek",
|
||||
"LabelUseZeroForUnlimited": "Uporabi 0 za neomejeno",
|
||||
"LabelUser": "Uporabnik",
|
||||
"LabelUsername": "Uporabniško ime",
|
||||
"LabelValue": "Vrednost",
|
||||
@@ -640,7 +681,7 @@
|
||||
"LabelYourPlaylists": "Tvoje seznami predvajanj",
|
||||
"LabelYourProgress": "Tvoj napredek",
|
||||
"MessageAddToPlayerQueue": "Dodaj v čakalno vrsto predvajalnika",
|
||||
"MessageAppriseDescription": "Če želite uporabljati to funkcijo, morate imeti zagnan primerek <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">API Apprise</a> ali API, ki bo obravnaval te iste zahteve. <br />Url API-ja Apprise mora biti celotna pot URL-ja za pošiljanje obvestila, npr. če je vaš primerek API-ja postrežen na <code>http://192.168.1.1:8337</code>, bi morali vnesti <code >http://192.168.1.1:8337/notify</code>.",
|
||||
"MessageAppriseDescription": "Če želite uporabljati to funkcijo, morate imeti zagnano namestitev <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">API Apprise</a> ali API, ki bo obravnavala te iste zahteve. <br />Url API-ja Apprise mora biti celotna pot URL-ja za pošiljanje obvestila, npr. če je vaša namestitev API-ja postrežena na <code>http://192.168.1.1:8337</code>, bi morali vnesti <code >http://192.168.1.1:8337/notify</code>.",
|
||||
"MessageBackupsDescription": "Varnostne kopije vključujejo uporabnike, napredek uporabnikov, podrobnosti elementov knjižnice, nastavitve strežnika in slike, shranjene v <code>/metadata/items</code> & <code>/metadata/authors</code>. Varnostne kopije <strong>ne</strong> vključujejo datotek, shranjenih v mapah vaše knjižnice.",
|
||||
"MessageBackupsLocationEditNote": "Opomba: Posodabljanje lokacije varnostne kopije ne bo premaknilo ali spremenilo obstoječih varnostnih kopij",
|
||||
"MessageBackupsLocationNoEditNote": "Opomba: Lokacija varnostne kopije je nastavljena s spremenljivko okolja in je tu ni mogoče spremeniti.",
|
||||
@@ -651,9 +692,9 @@
|
||||
"MessageBookshelfNoResultsForFilter": "Ni rezultatov za filter \"{0}: {1}\"",
|
||||
"MessageBookshelfNoResultsForQuery": "Ni rezultatov za poizvedbo",
|
||||
"MessageBookshelfNoSeries": "Nimate serij",
|
||||
"MessageChapterEndIsAfter": "Konec poglavja je za koncem vaše zvočne knjige",
|
||||
"MessageChapterEndIsAfter": "Konec poglavja je po koncu zvočne knjige",
|
||||
"MessageChapterErrorFirstNotZero": "Prvo poglavje se mora začeti pri 0",
|
||||
"MessageChapterErrorStartGteDuration": "Neveljaven začetni čas mora biti krajši od trajanja zvočne knjige",
|
||||
"MessageChapterErrorStartGteDuration": "Neveljaven začetni čas, mora biti krajši od trajanja zvočne knjige",
|
||||
"MessageChapterErrorStartLtPrev": "Neveljaven začetni čas mora biti večji od ali enak začetnemu času prejšnjega poglavja",
|
||||
"MessageChapterStartIsAfter": "Začetek poglavja je po koncu vaše zvočne knjige",
|
||||
"MessageCheckingCron": "Preverjam cron...",
|
||||
@@ -667,7 +708,8 @@
|
||||
"MessageConfirmDeleteMetadataProvider": "Ali ste prepričani, da želite izbrisati ponudnika metapodatkov po meri \"{0}\"?",
|
||||
"MessageConfirmDeleteNotification": "Ali ste prepričani, da želite izbrisati to obvestilo?",
|
||||
"MessageConfirmDeleteSession": "Ali ste prepričani, da želite izbrisati to sejo?",
|
||||
"MessageConfirmForceReScan": "Ali ste prepričani, da želite vsiliti ponovno iskanje?",
|
||||
"MessageConfirmEmbedMetadataInAudioFiles": "Ali ste prepričani, da želite vdelati metapodatke v {0} zvočnih datotek?",
|
||||
"MessageConfirmForceReScan": "Ali ste prepričani, da želite vsiliti ponovno pregledovanje?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Ali ste prepričani, da želite označiti vse epizode kot dokončane?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Ali ste prepričani, da želite vse epizode označiti kot nedokončane?",
|
||||
"MessageConfirmMarkItemFinished": "Ali ste prepričani, da želite \"{0}\" označiti kot dokončanega?",
|
||||
@@ -678,13 +720,15 @@
|
||||
"MessageConfirmPurgeCache": "Čiščenje predpomnilnika bo izbrisalo celoten imenik v <code>/metadata/cache</code>. <br /><br />Ali ste prepričani, da želite odstraniti imenik predpomnilnika?",
|
||||
"MessageConfirmPurgeItemsCache": "Čiščenje predpomnilnika elementov bo izbrisalo celoten imenik na <code>/metadata/cache/items</code>.<br />Ste prepričani?",
|
||||
"MessageConfirmQuickEmbed": "Opozorilo! Hitra vdelava ne bo varnostno kopirala vaših zvočnih datotek. Prepričajte se, da imate varnostno kopijo zvočnih datotek. <br><br>Ali želite nadaljevati?",
|
||||
"MessageConfirmReScanLibraryItems": "Ali ste prepričani, da želite ponovno poiskati {0} elementov?",
|
||||
"MessageConfirmQuickMatchEpisodes": "Hitro ujemanja epizod bo prepisalo podrobnosti, če se najde ujemanje. Posodobljene bodo samo epizode, ki se ne ujemajo. Ste prepričani?",
|
||||
"MessageConfirmReScanLibraryItems": "Ali ste prepričani, da želite ponovno pregledati {0} elementov?",
|
||||
"MessageConfirmRemoveAllChapters": "Ali ste prepričani, da želite odstraniti vsa poglavja?",
|
||||
"MessageConfirmRemoveAuthor": "Ali ste prepričani, da želite odstraniti avtorja \"{0}\"?",
|
||||
"MessageConfirmRemoveCollection": "Ali ste prepričani, da želite odstraniti zbirko \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisode": "Ali ste prepričani, da želite odstraniti epizodo \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisodes": "Ali ste prepričani, da želite odstraniti {0} epizod?",
|
||||
"MessageConfirmRemoveListeningSessions": "Ali ste prepričani, da želite odstraniti {0} sej poslušanja?",
|
||||
"MessageConfirmRemoveMetadataFiles": "Ali ste prepričani, da želite odstraniti vse metapodatke.{0} v mapah elementov knjižnice?",
|
||||
"MessageConfirmRemoveNarrator": "Ali ste prepričani, da želite odstraniti bralca \"{0}\"?",
|
||||
"MessageConfirmRemovePlaylist": "Ali ste prepričani, da želite odstraniti svoj seznam predvajanja \"{0}\"?",
|
||||
"MessageConfirmRenameGenre": "Ali ste prepričani, da želite preimenovati žanr \"{0}\" v \"{1}\" za vse elemente?",
|
||||
@@ -700,11 +744,12 @@
|
||||
"MessageDragFilesIntoTrackOrder": "Povlecite datoteke v pravilen vrstni red posnetkov",
|
||||
"MessageEmbedFailed": "Vdelava ni uspela!",
|
||||
"MessageEmbedFinished": "Vdelava končana!",
|
||||
"MessageEmbedQueue": "V čakalni vrsta za vdelavo metapodatkov ({0} v čakalni vrsti)",
|
||||
"MessageEpisodesQueuedForDownload": "{0} epizod v čakalni vrsti za prenos",
|
||||
"MessageEreaderDevices": "Da zagotovite dostavo e-knjig, boste morda morali dodati zgornji e-poštni naslov kot veljavnega pošiljatelja za vsako spodaj navedeno napravo.",
|
||||
"MessageFeedURLWillBe": "URL vira bo {0}",
|
||||
"MessageFetching": "Pridobivam...",
|
||||
"MessageForceReScanDescription": "bo znova pregledal vse datoteke kot nov pregled. Oznake ID3 zvočnih datotek, datoteke OPF in besedilne datoteke bodo pregledane kot nove.",
|
||||
"MessageForceReScanDescription": "bo znova pregledal vse datoteke kot pregled od začetka. Oznake ID3 zvočnih datotek, datoteke OPF in besedilne datoteke bodo pregledane kot nove.",
|
||||
"MessageImportantNotice": "Pomembno obvestilo!",
|
||||
"MessageInsertChapterBelow": "Spodaj vstavite poglavje",
|
||||
"MessageItemsSelected": "{0} izbranih elementov",
|
||||
@@ -716,12 +761,12 @@
|
||||
"MessageLogsDescription": "Dnevniki so shranjeni v <code>/metadata/logs</code> kot datoteke JSON. Dnevniki zrušitev so shranjeni v <code>/metadata/logs/crash_logs.txt</code>.",
|
||||
"MessageM4BFailed": "M4B ni uspel!",
|
||||
"MessageM4BFinished": "M4B končan!",
|
||||
"MessageMapChapterTitles": "Preslikajte naslove poglavij v obstoječa poglavja zvočne knjige brez prilagajanja časovnih žigov",
|
||||
"MessageMapChapterTitles": "Preslikaj naslove poglavij v obstoječa poglavja zvočne knjige brez prilagajanja časovnih indentifikatorjev",
|
||||
"MessageMarkAllEpisodesFinished": "Označi vse epizode kot končane",
|
||||
"MessageMarkAllEpisodesNotFinished": "Označi vse epizode kot nedokončane",
|
||||
"MessageMarkAsFinished": "Označi kot dokončano",
|
||||
"MessageMarkAsNotFinished": "Označi kot nedokončano",
|
||||
"MessageMatchBooksDescription": "bo poskušal povezati knjige v knjižnici s knjigo izbranega ponudnika iskanja in izpolniti prazne podatke in naslovnico. Ne prepisujejo se pa podrobnosti.",
|
||||
"MessageMatchBooksDescription": "bo poskušal povezati knjige v knjižnici s knjigo izbranega ponudnika iskanja in izpolniti prazne podatke in naslovnico. Ne prepisuje čez obstoječe podatke.",
|
||||
"MessageNoAudioTracks": "Ni zvočnih posnetkov",
|
||||
"MessageNoAuthors": "Brez avtorjev",
|
||||
"MessageNoBackups": "Brez varnostnih kopij",
|
||||
@@ -744,6 +789,7 @@
|
||||
"MessageNoLogs": "Ni dnevnikov",
|
||||
"MessageNoMediaProgress": "Ni medijskega napredka",
|
||||
"MessageNoNotifications": "Ni obvestil",
|
||||
"MessageNoPodcastFeed": "Neveljaven podcast: Ni vira",
|
||||
"MessageNoPodcastsFound": "Ni podcastov",
|
||||
"MessageNoResults": "Ni rezultatov",
|
||||
"MessageNoSearchResultsFor": "Ni rezultatov iskanja za \"{0}\"",
|
||||
@@ -760,6 +806,10 @@
|
||||
"MessagePlaylistCreateFromCollection": "Ustvari seznam predvajanja iz zbirke",
|
||||
"MessagePleaseWait": "Prosim počakajte...",
|
||||
"MessagePodcastHasNoRSSFeedForMatching": "Podcast nima URL-ja vira RSS, ki bi ga lahko uporabil za ujemanje",
|
||||
"MessagePodcastSearchField": "Vnesite iskalni izraz ali URL vira RSS",
|
||||
"MessageQuickEmbedInProgress": "Hitra vdelava je v teku",
|
||||
"MessageQuickEmbedQueue": "V čakalni vrsti za hitro vdelavo ({0} v čakalni vrsti)",
|
||||
"MessageQuickMatchAllEpisodes": "Hitro ujemanje vseh epizod",
|
||||
"MessageQuickMatchDescription": "Izpolni prazne podrobnosti elementa in naslovnico s prvim rezultatom ujemanja iz '{0}'. Ne prepiše podrobnosti, razen če je omogočena nastavitev strežnika 'Prednostno ujemajoči se metapodatki'.",
|
||||
"MessageRemoveChapter": "Odstrani poglavje",
|
||||
"MessageRemoveEpisodes": "Odstrani toliko epizod: {0}",
|
||||
@@ -791,7 +841,7 @@
|
||||
"MessageTaskFailedToMergeAudioFiles": "Zvočnih datotek ni bilo mogoče združiti",
|
||||
"MessageTaskFailedToMoveM4bFile": "Datoteke m4b ni bilo mogoče premakniti",
|
||||
"MessageTaskFailedToWriteMetadataFile": "Metapodatke ni bilo mogoče zapisati v datoteke",
|
||||
"MessageTaskMatchingBooksInLibrary": "Ujemam knjige v knjižnici \"{0}\"",
|
||||
"MessageTaskMatchingBooksInLibrary": "Prepoznavam knjige v knjižnici \"{0}\"",
|
||||
"MessageTaskNoFilesToScan": "Ni datotek za pregledovanje",
|
||||
"MessageTaskOpmlImport": "Uvoz OPML",
|
||||
"MessageTaskOpmlImportDescription": "Ustvarjanje podcastov iz {0} virov RSS",
|
||||
@@ -802,19 +852,22 @@
|
||||
"MessageTaskOpmlImportFeedPodcastExists": "Podcast že obstaja na tej poti",
|
||||
"MessageTaskOpmlImportFeedPodcastFailed": "Podcasta ni bilo mogoče ustvariti",
|
||||
"MessageTaskOpmlImportFinished": "Dodanih {0} podcastov",
|
||||
"MessageTaskOpmlParseFailed": "Datoteke OPML ni bilo mogoče razčleniti",
|
||||
"MessageTaskOpmlParseFastFail": "Neveljavna OPMPL datoteka, oznake <opml> ni bilo mogoče najti ALI oznake <outline> ni bilo mogoče najti",
|
||||
"MessageTaskOpmlParseNoneFound": "V datoteki OPML ni virov",
|
||||
"MessageTaskScanItemsAdded": "{0} dodano",
|
||||
"MessageTaskScanItemsMissing": "{0} manjka",
|
||||
"MessageTaskScanItemsUpdated": "{0} posodobljeno",
|
||||
"MessageTaskScanNoChangesNeeded": "Spremembe niso potrebne",
|
||||
"MessageTaskScanningFileChanges": "Pregledovanje sprememb v datoteki \"{0}\"",
|
||||
"MessageTaskScanningLibrary": "Pregled knjižnice \"{0}\"",
|
||||
"MessageTaskScanningLibrary": "Pregledujem knjižnico \"{0}\"",
|
||||
"MessageTaskTargetDirectoryNotWritable": "Ciljni imenik ni zapisljiv",
|
||||
"MessageThinking": "Razmišljam...",
|
||||
"MessageUploaderItemFailed": "Nalaganje ni uspelo",
|
||||
"MessageUploaderItemSuccess": "Uspešno naloženo!",
|
||||
"MessageUploading": "Nalaganje...",
|
||||
"MessageValidCronExpression": "Veljaven cron izraz",
|
||||
"MessageWatcherIsDisabledGlobally": "Pregledovalec je globalno onemogočen v nastavitvah strežnika",
|
||||
"MessageWatcherIsDisabledGlobally": "Spremljanje sprememb datotek je globalno onemogočeno v nastavitvah strežnika",
|
||||
"MessageXLibraryIsEmpty": "{0} Knjižnica je prazna!",
|
||||
"MessageYourAudiobookDurationIsLonger": "Trajanje vaše zvočne knjige je daljše od ugotovljenega trajanja",
|
||||
"MessageYourAudiobookDurationIsShorter": "Trajanje vaše zvočne knjige je krajše od ugotovljenega trajanja",
|
||||
@@ -826,6 +879,10 @@
|
||||
"NoteUploaderFoldersWithMediaFiles": "Mape z predstavnostnimi datotekami bodo obravnavane kot ločene postavke knjižnice.",
|
||||
"NoteUploaderOnlyAudioFiles": "Če nalagate samo zvočne datoteke, bo vsaka zvočna datoteka obravnavana kot ločena zvočna knjiga.",
|
||||
"NoteUploaderUnsupportedFiles": "Nepodprte datoteke so prezrte. Ko izberete ali spustite mapo, se druge datoteke, ki niso v mapi elementov, prezrejo.",
|
||||
"NotificationOnBackupCompletedDescription": "Sproži se, ko je varnostno kopiranje končano",
|
||||
"NotificationOnBackupFailedDescription": "Sproži se, ko varnostno kopiranje ne uspe",
|
||||
"NotificationOnEpisodeDownloadedDescription": "Sproži se, ko se epizoda podcasta samodejno prenese",
|
||||
"NotificationOnTestDescription": "Dogodek za testiranje sistema obveščanja",
|
||||
"PlaceholderNewCollection": "Novo ime zbirke",
|
||||
"PlaceholderNewFolderPath": "Pot nove mape",
|
||||
"PlaceholderNewPlaylist": "Novo ime seznama predvajanja",
|
||||
@@ -834,11 +891,11 @@
|
||||
"StatsAuthorsAdded": "dodanih avtorjev",
|
||||
"StatsBooksAdded": "dodanih knjig",
|
||||
"StatsBooksAdditional": "Nekateri dodatki vključujejo…",
|
||||
"StatsBooksFinished": "končane knjige",
|
||||
"StatsBooksFinished": "končanih knjig",
|
||||
"StatsBooksFinishedThisYear": "Nekaj knjig, ki so bile dokončane letos…",
|
||||
"StatsBooksListenedTo": "poslušane knjige",
|
||||
"StatsBooksListenedTo": "poslušanih knjig",
|
||||
"StatsCollectionGrewTo": "Vaša zbirka knjig se je povečala na …",
|
||||
"StatsSessions": "seje",
|
||||
"StatsSessions": "sej",
|
||||
"StatsSpentListening": "porabil za poslušanje",
|
||||
"StatsTopAuthor": "TOP AVTOR",
|
||||
"StatsTopAuthors": "TOP AVTORJI",
|
||||
@@ -851,6 +908,7 @@
|
||||
"StatsYearInReview": "PREGLED LETA",
|
||||
"ToastAccountUpdateSuccess": "Račun posodobljen",
|
||||
"ToastAppriseUrlRequired": "Vnesti morate Apprise URL",
|
||||
"ToastAsinRequired": "ASIN koda je obvezen podatek",
|
||||
"ToastAuthorImageRemoveSuccess": "Slika avtorja je odstranjena",
|
||||
"ToastAuthorNotFound": "Avtor \"{0}\" ni bil najden",
|
||||
"ToastAuthorRemoveSuccess": "Avtor odstranjen",
|
||||
@@ -870,6 +928,8 @@
|
||||
"ToastBackupUploadSuccess": "Varnostna kopija je naložena",
|
||||
"ToastBatchDeleteFailed": "Paketno brisanje ni uspelo",
|
||||
"ToastBatchDeleteSuccess": "Paketno brisanje je bilo uspešno",
|
||||
"ToastBatchQuickMatchFailed": "Paketno hitro ujemanje ni uspelo!",
|
||||
"ToastBatchQuickMatchStarted": "Paketno hitro ujemanje {0} knjig se je začelo!",
|
||||
"ToastBatchUpdateFailed": "Paketna posodobitev ni uspela",
|
||||
"ToastBatchUpdateSuccess": "Paketna posodobitev je uspela",
|
||||
"ToastBookmarkCreateFailed": "Zaznamka ni bilo mogoče ustvariti",
|
||||
@@ -881,6 +941,7 @@
|
||||
"ToastChaptersHaveErrors": "Poglavja imajo napake",
|
||||
"ToastChaptersMustHaveTitles": "Poglavja morajo imeti naslove",
|
||||
"ToastChaptersRemoved": "Poglavja so odstranjena",
|
||||
"ToastChaptersUpdated": "Poglavja so posodobljena",
|
||||
"ToastCollectionItemsAddFailed": "Dodajanje elementov v zbirko ni uspelo",
|
||||
"ToastCollectionItemsAddSuccess": "Dodajanje elementov v zbirko je bilo uspešno",
|
||||
"ToastCollectionItemsRemoveSuccess": "Elementi so bili odstranjeni iz zbirke",
|
||||
@@ -898,11 +959,14 @@
|
||||
"ToastEncodeCancelSucces": "Prekodiranje prekinjeno",
|
||||
"ToastEpisodeDownloadQueueClearFailed": "Čiščenje čakalne vrste ni uspelo",
|
||||
"ToastEpisodeDownloadQueueClearSuccess": "Čakalna vrsta za prenos epizod je počiščena",
|
||||
"ToastEpisodeUpdateSuccess": "Število posodobljenih epizod: {0}",
|
||||
"ToastErrorCannotShare": "V tej napravi ni mogoče dati v skupno rabo",
|
||||
"ToastFailedToLoadData": "Podatkov ni bilo mogoče naložiti",
|
||||
"ToastFailedToMatch": "Ujemanje ni uspelo",
|
||||
"ToastFailedToShare": "Skupna raba ni uspela",
|
||||
"ToastFailedToUpdate": "Napaka pri posodobitvi",
|
||||
"ToastInvalidImageUrl": "Neveljaven URL slike",
|
||||
"ToastInvalidMaxEpisodesToDownload": "Neveljavno največje število epizod za prenos",
|
||||
"ToastInvalidUrl": "Neveljaven URL",
|
||||
"ToastItemCoverUpdateSuccess": "Naslovnica elementa je bila posodobljena",
|
||||
"ToastItemDeletedFailed": "Elementa ni bilo mogoče izbrisati",
|
||||
@@ -920,14 +984,22 @@
|
||||
"ToastLibraryScanFailedToStart": "Pregleda ni bilo mogoče začeti",
|
||||
"ToastLibraryScanStarted": "Pregled knjižnice se je začel",
|
||||
"ToastLibraryUpdateSuccess": "Knjižnica \"{0}\" je bila posodobljena",
|
||||
"ToastMatchAllAuthorsFailed": "Ujemanje vseh avtorjev ni bilo uspešno",
|
||||
"ToastMetadataFilesRemovedError": "Napaka pri odstranjevanju metapodatkov.{0} datotek",
|
||||
"ToastMetadataFilesRemovedNoneFound": "Ni metapodatkov.{0} datotek, najdenih v knjižnici",
|
||||
"ToastMetadataFilesRemovedNoneRemoved": "Ni metapodatkov.{0} datotek odstranjenih",
|
||||
"ToastMetadataFilesRemovedSuccess": "{0} metapodatki.{1} datotek odstranjenih",
|
||||
"ToastMustHaveAtLeastOnePath": "Imeti mora vsaj eno pot",
|
||||
"ToastNameEmailRequired": "Ime in e-pošta sta obvezna",
|
||||
"ToastNameRequired": "Ime je obvezno",
|
||||
"ToastNewEpisodesFound": "Število najdenih novih epizod: {0}",
|
||||
"ToastNewUserCreatedFailed": "Računa ni bilo mogoče ustvariti: \"{0}\"",
|
||||
"ToastNewUserCreatedSuccess": "Nov račun je bil ustvarjen",
|
||||
"ToastNewUserLibraryError": "Izbrati morate vsaj eno knjižnico",
|
||||
"ToastNewUserPasswordError": "Mora imeti geslo, samo korenski uporabnik ima lahko prazno geslo",
|
||||
"ToastNewUserTagError": "Izbrati morate vsaj eno oznako",
|
||||
"ToastNewUserUsernameError": "Vnesite uporabniško ime",
|
||||
"ToastNoNewEpisodesFound": "Ni novih epizod",
|
||||
"ToastNoUpdatesNecessary": "Posodobitve niso potrebne",
|
||||
"ToastNotificationCreateFailed": "Obvestila ni bilo mogoče ustvariti",
|
||||
"ToastNotificationDeleteFailed": "Brisanje obvestila ni uspelo",
|
||||
@@ -946,6 +1018,7 @@
|
||||
"ToastPodcastGetFeedFailed": "Vira podcasta ni bilo mogoče pridobiti",
|
||||
"ToastPodcastNoEpisodesInFeed": "V viru RSS ni bilo mogoče najti nobene epizode",
|
||||
"ToastPodcastNoRssFeed": "Podcast nima vira RSS",
|
||||
"ToastProgressIsNotBeingSynced": "Napredek se ne sinhronizira, znova zaženite predvajanje",
|
||||
"ToastProviderCreatedFailed": "Ponudnika ni bilo mogoče dodati",
|
||||
"ToastProviderCreatedSuccess": "Dodan je bil nov ponudnik",
|
||||
"ToastProviderNameAndUrlRequired": "Obvezen podatek sta ime in URL",
|
||||
@@ -972,6 +1045,7 @@
|
||||
"ToastSessionCloseFailed": "Seje ni bilo mogoče zapreti",
|
||||
"ToastSessionDeleteFailed": "Brisanje seje ni uspelo",
|
||||
"ToastSessionDeleteSuccess": "Seja je bila izbrisana",
|
||||
"ToastSleepTimerDone": "Časovnik za spanje se je končal... zZzzZz",
|
||||
"ToastSlugMustChange": "Slug vsebuje neveljavne znake",
|
||||
"ToastSlugRequired": "Slug je obvezen podatek",
|
||||
"ToastSocketConnected": "Omrežna povezava je priklopljena",
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
"ButtonPurgeItemsCache": "清理项目缓存",
|
||||
"ButtonQueueAddItem": "添加到队列",
|
||||
"ButtonQueueRemoveItem": "从队列中移除",
|
||||
"ButtonQuickEmbed": "快速嵌入",
|
||||
"ButtonQuickEmbedMetadata": "快速嵌入元数据",
|
||||
"ButtonQuickMatch": "快速匹配",
|
||||
"ButtonReScan": "重新扫描",
|
||||
@@ -179,6 +180,7 @@
|
||||
"HeaderRemoveEpisodes": "移除 {0} 剧集",
|
||||
"HeaderSavedMediaProgress": "保存媒体进度",
|
||||
"HeaderSchedule": "计划任务",
|
||||
"HeaderScheduleEpisodeDownloads": "设置自动下载剧集",
|
||||
"HeaderScheduleLibraryScans": "自动扫描媒体库",
|
||||
"HeaderSession": "会话",
|
||||
"HeaderSetBackupSchedule": "设置备份计划任务",
|
||||
@@ -225,6 +227,9 @@
|
||||
"LabelAllUsersIncludingGuests": "包括访客的所有用户",
|
||||
"LabelAlreadyInYourLibrary": "已存在你的库中",
|
||||
"LabelAppend": "附加",
|
||||
"LabelAudioBitrate": "音频比特率 (例如: 128k)",
|
||||
"LabelAudioChannels": "音频通道 (1 或 2)",
|
||||
"LabelAudioCodec": "音频编解码器",
|
||||
"LabelAuthor": "作者",
|
||||
"LabelAuthorFirstLast": "作者 (姓 名)",
|
||||
"LabelAuthorLastFirst": "作者 (名, 姓)",
|
||||
@@ -237,6 +242,7 @@
|
||||
"LabelAutoRegister": "自动注册",
|
||||
"LabelAutoRegisterDescription": "登录后自动创建新用户",
|
||||
"LabelBackToUser": "返回到用户",
|
||||
"LabelBackupAudioFiles": "备份音频文件",
|
||||
"LabelBackupLocation": "备份位置",
|
||||
"LabelBackupsEnableAutomaticBackups": "启用自动备份",
|
||||
"LabelBackupsEnableAutomaticBackupsHelp": "备份保存到 /metadata/backups",
|
||||
@@ -245,15 +251,18 @@
|
||||
"LabelBackupsNumberToKeep": "要保留的备份个数",
|
||||
"LabelBackupsNumberToKeepHelp": "一次只能删除一个备份, 因此如果你已经有超过此数量的备份, 则应手动删除它们.",
|
||||
"LabelBitrate": "比特率",
|
||||
"LabelBonus": "额外",
|
||||
"LabelBooks": "图书",
|
||||
"LabelButtonText": "按钮文本",
|
||||
"LabelByAuthor": "由 {0}",
|
||||
"LabelChangePassword": "修改密码",
|
||||
"LabelChannels": "声道",
|
||||
"LabelChapterCount": "{0} 章节",
|
||||
"LabelChapterTitle": "章节标题",
|
||||
"LabelChapters": "章节",
|
||||
"LabelChaptersFound": "找到的章节",
|
||||
"LabelClickForMoreInfo": "点击了解更多信息",
|
||||
"LabelClickToUseCurrentValue": "点击使用当前值",
|
||||
"LabelClosePlayer": "关闭播放器",
|
||||
"LabelCodec": "编解码",
|
||||
"LabelCollapseSeries": "折叠系列",
|
||||
@@ -303,12 +312,25 @@
|
||||
"LabelEmailSettingsTestAddress": "测试地址",
|
||||
"LabelEmbeddedCover": "嵌入封面",
|
||||
"LabelEnable": "启用",
|
||||
"LabelEncodingBackupLocation": "你的原始音频文件的备份将存储在:",
|
||||
"LabelEncodingChaptersNotEmbedded": "多轨有声读物中未嵌入章节.",
|
||||
"LabelEncodingClearItemCache": "确保定期清除项目缓存.",
|
||||
"LabelEncodingFinishedM4B": "完成的 M4B 将被放入你的有声读物文件夹中:",
|
||||
"LabelEncodingInfoEmbedded": "元数据将嵌入有声读物文件夹内的音轨中.",
|
||||
"LabelEncodingStartedNavigation": "一旦任务开始, 你就可以离开此页面.",
|
||||
"LabelEncodingTimeWarning": "编码最多可能需要 30 分钟.",
|
||||
"LabelEncodingWarningAdvancedSettings": "警告: 除非你熟悉 ffmpeg 编码选项, 否则请不要更新这些设置.",
|
||||
"LabelEncodingWatcherDisabled": "如果你禁用了监视器, 则随后需要重新扫描此有声读物.",
|
||||
"LabelEnd": "结束",
|
||||
"LabelEndOfChapter": "章节结束",
|
||||
"LabelEpisode": "剧集",
|
||||
"LabelEpisodeNotLinkedToRssFeed": "剧集没有链接到RSS源",
|
||||
"LabelEpisodeNumber": "剧集 #{0}",
|
||||
"LabelEpisodeTitle": "剧集标题",
|
||||
"LabelEpisodeType": "剧集类型",
|
||||
"LabelEpisodeUrlFromRssFeed": "来自 RSS 订阅的剧集 URL",
|
||||
"LabelEpisodes": "剧集",
|
||||
"LabelEpisodic": "剧集",
|
||||
"LabelExample": "示例",
|
||||
"LabelExpandSeries": "展开系列",
|
||||
"LabelExpandSubSeries": "展开子系列",
|
||||
@@ -336,6 +358,7 @@
|
||||
"LabelFontScale": "字体比例",
|
||||
"LabelFontStrikethrough": "删除线",
|
||||
"LabelFormat": "编码格式",
|
||||
"LabelFull": "完整",
|
||||
"LabelGenre": "流派",
|
||||
"LabelGenres": "流派",
|
||||
"LabelHardDeleteFile": "完全删除文件",
|
||||
@@ -391,6 +414,10 @@
|
||||
"LabelLowestPriority": "最低优先级",
|
||||
"LabelMatchExistingUsersBy": "匹配现有用户",
|
||||
"LabelMatchExistingUsersByDescription": "用于连接现有用户. 连接后, 用户将通过 SSO 提供商提供的唯一 id 进行匹配",
|
||||
"LabelMaxEpisodesToDownload": "可下载的最大集数. 输入 0 表示无限制.",
|
||||
"LabelMaxEpisodesToDownloadPerCheck": "每次检查最多可下载新剧集数",
|
||||
"LabelMaxEpisodesToKeep": "要保留的最大剧集数",
|
||||
"LabelMaxEpisodesToKeepHelp": "值为 0 时, 不设置最大限制. 自动下载新剧集后, 如果您有超过 X 个剧集, 它将删除最旧的剧集. 每次新下载时, 只会删除 1 个剧集.",
|
||||
"LabelMediaPlayer": "媒体播放器",
|
||||
"LabelMediaType": "媒体类型",
|
||||
"LabelMetaTag": "元数据标签",
|
||||
@@ -465,6 +492,8 @@
|
||||
"LabelPubDate": "出版日期",
|
||||
"LabelPublishYear": "发布年份",
|
||||
"LabelPublishedDate": "已发布 {0}",
|
||||
"LabelPublishedDecade": "出版年代",
|
||||
"LabelPublishedDecades": "出版年代",
|
||||
"LabelPublisher": "出版商",
|
||||
"LabelPublishers": "出版商",
|
||||
"LabelRSSFeedCustomOwnerEmail": "自定义所有者电子邮件",
|
||||
@@ -484,21 +513,28 @@
|
||||
"LabelRedo": "重做",
|
||||
"LabelRegion": "区域",
|
||||
"LabelReleaseDate": "发布日期",
|
||||
"LabelRemoveAllMetadataAbs": "删除所有 metadata.abs 文件",
|
||||
"LabelRemoveAllMetadataJson": "删除所有 metadata.json 文件",
|
||||
"LabelRemoveCover": "移除封面",
|
||||
"LabelRemoveMetadataFile": "删除库项目文件夹中的元数据文件",
|
||||
"LabelRemoveMetadataFileHelp": "删除 {0} 文件夹中的所有 metadata.json 和 metadata.abs 文件.",
|
||||
"LabelRowsPerPage": "每页行数",
|
||||
"LabelSearchTerm": "搜索项",
|
||||
"LabelSearchTitle": "搜索标题",
|
||||
"LabelSearchTitleOrASIN": "搜索标题或 ASIN",
|
||||
"LabelSeason": "季",
|
||||
"LabelSeasonNumber": "第 {0} 季",
|
||||
"LabelSelectAll": "全选",
|
||||
"LabelSelectAllEpisodes": "选择所有剧集",
|
||||
"LabelSelectEpisodesShowing": "选择正在播放的 {0} 剧集",
|
||||
"LabelSelectUsers": "选择用户",
|
||||
"LabelSendEbookToDevice": "发送电子书到...",
|
||||
"LabelSequence": "序列",
|
||||
"LabelSerial": "系列",
|
||||
"LabelSeries": "系列",
|
||||
"LabelSeriesName": "系列名称",
|
||||
"LabelSeriesProgress": "系列进度",
|
||||
"LabelServerLogLevel": "服务器日志级别",
|
||||
"LabelServerYearReview": "服务器年度回顾 ({0})",
|
||||
"LabelSetEbookAsPrimary": "设置为主",
|
||||
"LabelSetEbookAsSupplementary": "设置为补充",
|
||||
@@ -587,6 +623,7 @@
|
||||
"LabelTimeDurationXMinutes": "{0} 分钟",
|
||||
"LabelTimeDurationXSeconds": "{0} 秒",
|
||||
"LabelTimeInMinutes": "时间 (分钟)",
|
||||
"LabelTimeLeft": "剩余 {0}",
|
||||
"LabelTimeListened": "收听时间",
|
||||
"LabelTimeListenedToday": "今日收听的时间",
|
||||
"LabelTimeRemaining": "剩余 {0}",
|
||||
@@ -594,6 +631,7 @@
|
||||
"LabelTitle": "标题",
|
||||
"LabelToolsEmbedMetadata": "嵌入元数据",
|
||||
"LabelToolsEmbedMetadataDescription": "将元数据嵌入音频文件, 包括封面图像和章节.",
|
||||
"LabelToolsM4bEncoder": "M4B 编码器",
|
||||
"LabelToolsMakeM4b": "制作 M4B 有声读物文件",
|
||||
"LabelToolsMakeM4bDescription": "生成带有嵌入元数据, 封面图像和章节的 .M4B 有声读物文件.",
|
||||
"LabelToolsSplitM4b": "将 M4B 文件拆分为 MP3 文件",
|
||||
@@ -606,6 +644,7 @@
|
||||
"LabelTracksMultiTrack": "多轨",
|
||||
"LabelTracksNone": "没有音轨",
|
||||
"LabelTracksSingleTrack": "单轨",
|
||||
"LabelTrailer": "预告",
|
||||
"LabelType": "类型",
|
||||
"LabelUnabridged": "未删节",
|
||||
"LabelUndo": "撤消",
|
||||
@@ -619,8 +658,10 @@
|
||||
"LabelUploaderDragAndDrop": "拖放文件或文件夹",
|
||||
"LabelUploaderDropFiles": "删除文件",
|
||||
"LabelUploaderItemFetchMetadataHelp": "自动获取标题, 作者和系列",
|
||||
"LabelUseAdvancedOptions": "使用高级选项",
|
||||
"LabelUseChapterTrack": "使用章节音轨",
|
||||
"LabelUseFullTrack": "使用完整音轨",
|
||||
"LabelUseZeroForUnlimited": "使用 0 表示无限制",
|
||||
"LabelUser": "用户",
|
||||
"LabelUsername": "用户名",
|
||||
"LabelValue": "值",
|
||||
@@ -659,25 +700,27 @@
|
||||
"MessageCheckingCron": "检查计划任务...",
|
||||
"MessageConfirmCloseFeed": "你确定要关闭此订阅源吗?",
|
||||
"MessageConfirmDeleteBackup": "你确定要删除备份 {0}?",
|
||||
"MessageConfirmDeleteDevice": "您确定要删除电子阅读器设备 \"{0}\" 吗?",
|
||||
"MessageConfirmDeleteDevice": "你确定要删除电子阅读器设备 \"{0}\" 吗?",
|
||||
"MessageConfirmDeleteFile": "这将从文件系统中删除该文件. 你确定吗?",
|
||||
"MessageConfirmDeleteLibrary": "你确定要永久删除媒体库 \"{0}\"?",
|
||||
"MessageConfirmDeleteLibraryItem": "这将从数据库和文件系统中删除库项目. 你确定吗?",
|
||||
"MessageConfirmDeleteLibraryItems": "这将从数据库和文件系统中删除 {0} 个库项目. 你确定吗?",
|
||||
"MessageConfirmDeleteMetadataProvider": "是否确实要删除自定义元数据提供商 \"{0}\" ?",
|
||||
"MessageConfirmDeleteNotification": "您确定要删除此通知吗?",
|
||||
"MessageConfirmDeleteNotification": "你确定要删除此通知吗?",
|
||||
"MessageConfirmDeleteSession": "你确定要删除此会话吗?",
|
||||
"MessageConfirmEmbedMetadataInAudioFiles": "你确定要将元数据嵌入到 {0} 个音频文件中吗?",
|
||||
"MessageConfirmForceReScan": "你确定要强制重新扫描吗?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "你确定要将所有剧集都标记为已完成吗?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "你确定要将所有剧集都标记为未完成吗?",
|
||||
"MessageConfirmMarkItemFinished": "您确定要将 \"{0}\" 标记为已完成吗?",
|
||||
"MessageConfirmMarkItemNotFinished": "您确定要将 \"{0}\" 标记为未完成吗?",
|
||||
"MessageConfirmMarkItemFinished": "你确定要将 \"{0}\" 标记为已完成吗?",
|
||||
"MessageConfirmMarkItemNotFinished": "你确定要将 \"{0}\" 标记为未完成吗?",
|
||||
"MessageConfirmMarkSeriesFinished": "你确定要将此系列中的所有书籍都标记为已听完吗?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "你确定要将此系列中的所有书籍都标记为未听完吗?",
|
||||
"MessageConfirmNotificationTestTrigger": "使用测试数据触发此通知吗?",
|
||||
"MessageConfirmPurgeCache": "清除缓存将删除 <code>/metadata/cache</code> 整个目录. <br /><br />你确定要删除缓存目录吗?",
|
||||
"MessageConfirmPurgeItemsCache": "清除项目缓存将删除 <code>/metadata/cache/items</code> 整个目录.<br />你确定吗?",
|
||||
"MessageConfirmQuickEmbed": "警告! 快速嵌入不会备份你的音频文件. 确保你有音频文件的备份. <br><br>你是否想继续吗?",
|
||||
"MessageConfirmQuickMatchEpisodes": "如果找到匹配项, 快速匹配的剧集将覆盖详细信息. 只有不匹配的剧集才会更新. 你确定吗?",
|
||||
"MessageConfirmReScanLibraryItems": "你确定要重新扫描 {0} 个项目吗?",
|
||||
"MessageConfirmRemoveAllChapters": "你确定要移除所有章节吗?",
|
||||
"MessageConfirmRemoveAuthor": "你确定要删除作者 \"{0}\"?",
|
||||
@@ -685,6 +728,7 @@
|
||||
"MessageConfirmRemoveEpisode": "你确定要移除剧集 \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisodes": "你确定要移除 {0} 剧集?",
|
||||
"MessageConfirmRemoveListeningSessions": "你确定要移除 {0} 收听会话吗?",
|
||||
"MessageConfirmRemoveMetadataFiles": "你确实要删除库项目文件夹中的所有 metadata.{0} 文件吗?",
|
||||
"MessageConfirmRemoveNarrator": "你确定要删除演播者 \"{0}\"?",
|
||||
"MessageConfirmRemovePlaylist": "你确定要移除播放列表 \"{0}\"?",
|
||||
"MessageConfirmRenameGenre": "你确定要将所有项目流派 \"{0}\" 重命名到 \"{1}\"?",
|
||||
@@ -695,11 +739,12 @@
|
||||
"MessageConfirmRenameTagWarning": "警告! 已经存在有大小写不同的类似标签 \"{0}\".",
|
||||
"MessageConfirmResetProgress": "你确定要重置进度吗?",
|
||||
"MessageConfirmSendEbookToDevice": "你确定要发送 {0} 电子书 \"{1}\" 到设备 \"{2}\"?",
|
||||
"MessageConfirmUnlinkOpenId": "您确定要取消该用户与 OpenID 的链接吗?",
|
||||
"MessageConfirmUnlinkOpenId": "你确定要取消该用户与 OpenID 的链接吗?",
|
||||
"MessageDownloadingEpisode": "正在下载剧集",
|
||||
"MessageDragFilesIntoTrackOrder": "将文件拖动到正确的音轨顺序",
|
||||
"MessageEmbedFailed": "嵌入失败!",
|
||||
"MessageEmbedFinished": "嵌入完成!",
|
||||
"MessageEmbedQueue": "已排队等待元数据嵌入 (队列中有 {0} 个)",
|
||||
"MessageEpisodesQueuedForDownload": "{0} 个剧集排队等待下载",
|
||||
"MessageEreaderDevices": "为了确保电子书的送达, 你可能需要将上述电子邮件地址添加为下列每台设备的有效发件人.",
|
||||
"MessageFeedURLWillBe": "源 URL 将改为 {0}",
|
||||
@@ -744,6 +789,7 @@
|
||||
"MessageNoLogs": "无日志",
|
||||
"MessageNoMediaProgress": "无媒体进度",
|
||||
"MessageNoNotifications": "无通知",
|
||||
"MessageNoPodcastFeed": "无效播客: 无源",
|
||||
"MessageNoPodcastsFound": "未找到播客",
|
||||
"MessageNoResults": "无结果",
|
||||
"MessageNoSearchResultsFor": "没有搜索到结果 \"{0}\"",
|
||||
@@ -760,6 +806,10 @@
|
||||
"MessagePlaylistCreateFromCollection": "从收藏中创建播放列表",
|
||||
"MessagePleaseWait": "请稍等...",
|
||||
"MessagePodcastHasNoRSSFeedForMatching": "播客没有可用于匹配 RSS 源的 url",
|
||||
"MessagePodcastSearchField": "输入搜索词或 RSS 源 URL",
|
||||
"MessageQuickEmbedInProgress": "正在进行快速嵌入",
|
||||
"MessageQuickEmbedQueue": "已排队等待快速嵌入 (队列中有 {0} 个)",
|
||||
"MessageQuickMatchAllEpisodes": "快速匹配所有剧集",
|
||||
"MessageQuickMatchDescription": "使用来自 '{0}' 的第一个匹配结果填充空白详细信息和封面. 除非启用 '首选匹配元数据' 服务器设置, 否则不会覆盖详细信息.",
|
||||
"MessageRemoveChapter": "移除章节",
|
||||
"MessageRemoveEpisodes": "移除 {0} 剧集",
|
||||
@@ -802,6 +852,9 @@
|
||||
"MessageTaskOpmlImportFeedPodcastExists": "播客已存在于路径中",
|
||||
"MessageTaskOpmlImportFeedPodcastFailed": "无法创建播客",
|
||||
"MessageTaskOpmlImportFinished": "已添加 {0} 播客",
|
||||
"MessageTaskOpmlParseFailed": "无法解析 OPML 文件",
|
||||
"MessageTaskOpmlParseFastFail": "未找到无效的 OPML 文件 <opml> 标签或未找到 <outline> 标签",
|
||||
"MessageTaskOpmlParseNoneFound": "OPML 文件中未找到任何信息",
|
||||
"MessageTaskScanItemsAdded": "{0} 已添加",
|
||||
"MessageTaskScanItemsMissing": "{0} 已缺失",
|
||||
"MessageTaskScanItemsUpdated": "{0} 已更新",
|
||||
@@ -826,6 +879,10 @@
|
||||
"NoteUploaderFoldersWithMediaFiles": "包含媒体文件的文件夹将作为单独的媒体库项目处理.",
|
||||
"NoteUploaderOnlyAudioFiles": "如果只上传音频文件, 则每个音频文件将作为单独的有声读物处理.",
|
||||
"NoteUploaderUnsupportedFiles": "不支持的文件将被忽略. 选择或删除文件夹时, 将忽略不在项目文件夹中的其他文件.",
|
||||
"NotificationOnBackupCompletedDescription": "备份完成时触发",
|
||||
"NotificationOnBackupFailedDescription": "备份失败时触发",
|
||||
"NotificationOnEpisodeDownloadedDescription": "当播客节目自动下载时触发",
|
||||
"NotificationOnTestDescription": "测试通知系统的事件",
|
||||
"PlaceholderNewCollection": "输入收藏夹名称",
|
||||
"PlaceholderNewFolderPath": "输入文件夹路径",
|
||||
"PlaceholderNewPlaylist": "输入播放列表名称",
|
||||
@@ -837,7 +894,7 @@
|
||||
"StatsBooksFinished": "已完成书籍",
|
||||
"StatsBooksFinishedThisYear": "今年完成的一些书…",
|
||||
"StatsBooksListenedTo": "听过的书",
|
||||
"StatsCollectionGrewTo": "您的藏书已增长到…",
|
||||
"StatsCollectionGrewTo": "你的藏书已增长到…",
|
||||
"StatsSessions": "会话",
|
||||
"StatsSpentListening": "花时间聆听",
|
||||
"StatsTopAuthor": "热门作者",
|
||||
@@ -851,6 +908,7 @@
|
||||
"StatsYearInReview": "年度回顾",
|
||||
"ToastAccountUpdateSuccess": "帐户已更新",
|
||||
"ToastAppriseUrlRequired": "必须输入 Apprise URL",
|
||||
"ToastAsinRequired": "需要 ASIN",
|
||||
"ToastAuthorImageRemoveSuccess": "作者图像已删除",
|
||||
"ToastAuthorNotFound": "未找到作者 \"{0}\"",
|
||||
"ToastAuthorRemoveSuccess": "作者已删除",
|
||||
@@ -870,6 +928,8 @@
|
||||
"ToastBackupUploadSuccess": "备份已上传",
|
||||
"ToastBatchDeleteFailed": "批量删除失败",
|
||||
"ToastBatchDeleteSuccess": "批量删除成功",
|
||||
"ToastBatchQuickMatchFailed": "批量快速匹配失败!",
|
||||
"ToastBatchQuickMatchStarted": "批量快速匹配 {0} 图书已开始!",
|
||||
"ToastBatchUpdateFailed": "批量更新失败",
|
||||
"ToastBatchUpdateSuccess": "批量更新成功",
|
||||
"ToastBookmarkCreateFailed": "创建书签失败",
|
||||
@@ -881,6 +941,7 @@
|
||||
"ToastChaptersHaveErrors": "章节有错误",
|
||||
"ToastChaptersMustHaveTitles": "章节必须有标题",
|
||||
"ToastChaptersRemoved": "已删除章节",
|
||||
"ToastChaptersUpdated": "章节已更新",
|
||||
"ToastCollectionItemsAddFailed": "项目添加到收藏夹失败",
|
||||
"ToastCollectionItemsAddSuccess": "项目添加到收藏夹成功",
|
||||
"ToastCollectionItemsRemoveSuccess": "项目从收藏夹移除",
|
||||
@@ -898,11 +959,14 @@
|
||||
"ToastEncodeCancelSucces": "编码已取消",
|
||||
"ToastEpisodeDownloadQueueClearFailed": "无法清除队列",
|
||||
"ToastEpisodeDownloadQueueClearSuccess": "剧集下载队列已清空",
|
||||
"ToastEpisodeUpdateSuccess": "已更新 {0} 剧集",
|
||||
"ToastErrorCannotShare": "无法在此设备上本地共享",
|
||||
"ToastFailedToLoadData": "加载数据失败",
|
||||
"ToastFailedToMatch": "匹配失败",
|
||||
"ToastFailedToShare": "分享失败",
|
||||
"ToastFailedToUpdate": "更新失败",
|
||||
"ToastInvalidImageUrl": "图片网址无效",
|
||||
"ToastInvalidMaxEpisodesToDownload": "可下载的最大集数无效",
|
||||
"ToastInvalidUrl": "网址无效",
|
||||
"ToastItemCoverUpdateSuccess": "项目封面已更新",
|
||||
"ToastItemDeletedFailed": "删除项目失败",
|
||||
@@ -920,14 +984,22 @@
|
||||
"ToastLibraryScanFailedToStart": "无法启动扫描",
|
||||
"ToastLibraryScanStarted": "媒体库扫描已启动",
|
||||
"ToastLibraryUpdateSuccess": "媒体库 \"{0}\" 已更新",
|
||||
"ToastMatchAllAuthorsFailed": "无法匹配所有作者",
|
||||
"ToastMetadataFilesRemovedError": "删除 metadata.{0} 文件时出错",
|
||||
"ToastMetadataFilesRemovedNoneFound": "在库中没有找到 metadata.{0} 文件",
|
||||
"ToastMetadataFilesRemovedNoneRemoved": "没有 metadata.{0} 文件被删除",
|
||||
"ToastMetadataFilesRemovedSuccess": "{0} 个 metadata.{1} 文件被删除",
|
||||
"ToastMustHaveAtLeastOnePath": "必须至少有一个路径",
|
||||
"ToastNameEmailRequired": "姓名和电子邮件为必填项",
|
||||
"ToastNameRequired": "姓名为必填项",
|
||||
"ToastNewEpisodesFound": "找到 {0} 个新剧集",
|
||||
"ToastNewUserCreatedFailed": "无法创建帐户: \"{0}\"",
|
||||
"ToastNewUserCreatedSuccess": "已创建新帐户",
|
||||
"ToastNewUserLibraryError": "必须至少选择一个图书馆",
|
||||
"ToastNewUserPasswordError": "必须有密码, 只有root用户可以有空密码",
|
||||
"ToastNewUserTagError": "必须至少选择一个标签",
|
||||
"ToastNewUserUsernameError": "输入用户名",
|
||||
"ToastNoNewEpisodesFound": "没有找到新剧集",
|
||||
"ToastNoUpdatesNecessary": "无需更新",
|
||||
"ToastNotificationCreateFailed": "无法创建通知",
|
||||
"ToastNotificationDeleteFailed": "删除通知失败",
|
||||
@@ -946,6 +1018,7 @@
|
||||
"ToastPodcastGetFeedFailed": "无法获取播客信息",
|
||||
"ToastPodcastNoEpisodesInFeed": "RSS 订阅中未找到任何剧集",
|
||||
"ToastPodcastNoRssFeed": "播客没有 RSS 源",
|
||||
"ToastProgressIsNotBeingSynced": "进度未同步, 请重新开始播放",
|
||||
"ToastProviderCreatedFailed": "无法添加提供商",
|
||||
"ToastProviderCreatedSuccess": "已添加新提供商",
|
||||
"ToastProviderNameAndUrlRequired": "名称和网址必需填写",
|
||||
@@ -972,6 +1045,7 @@
|
||||
"ToastSessionCloseFailed": "关闭会话失败",
|
||||
"ToastSessionDeleteFailed": "删除会话失败",
|
||||
"ToastSessionDeleteSuccess": "会话已删除",
|
||||
"ToastSleepTimerDone": "睡眠定时完成... zZzzZz",
|
||||
"ToastSlugMustChange": "Slug 包含无效字符",
|
||||
"ToastSlugRequired": "Slug 是必填项",
|
||||
"ToastSocketConnected": "网络已连接",
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.14.0",
|
||||
"version": "2.15.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.14.0",
|
||||
"version": "2.15.1",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"axios": "^0.27.2",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.14.0",
|
||||
"version": "2.15.1",
|
||||
"buildNumber": 1,
|
||||
"description": "Self-hosted audiobook and podcast server",
|
||||
"main": "index.js",
|
||||
|
||||
@@ -92,29 +92,33 @@ Toggle websockets support.
|
||||
Add this to the site config file on your nginx server after you have changed the relevant parts in the <> brackets, and inserted your certificate paths.
|
||||
|
||||
```bash
|
||||
server
|
||||
{
|
||||
listen 443 ssl;
|
||||
server_name <sub>.<domain>.<tld>;
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name <sub>.<domain>.<tld>;
|
||||
|
||||
access_log /var/log/nginx/audiobookshelf.access.log;
|
||||
error_log /var/log/nginx/audiobookshelf.error.log;
|
||||
access_log /var/log/nginx/audiobookshelf.access.log;
|
||||
error_log /var/log/nginx/audiobookshelf.error.log;
|
||||
|
||||
ssl_certificate /path/to/certificate;
|
||||
ssl_certificate_key /path/to/key;
|
||||
ssl_certificate /path/to/certificate;
|
||||
ssl_certificate_key /path/to/key;
|
||||
|
||||
location / {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
location / {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_pass http://<URL_to_forward_to>;
|
||||
proxy_redirect http:// https://;
|
||||
}
|
||||
proxy_pass http://<URL_to_forward_to>;
|
||||
proxy_redirect http:// https://;
|
||||
|
||||
# Prevent 413 Request Entity Too Large error
|
||||
# by increasing the maximum allowed size of the client request body
|
||||
# For example, set it to 10 GiB
|
||||
client_max_body_size 10240M;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -607,6 +607,11 @@ class Database {
|
||||
this.libraryFilterData[libraryId].publishers.push(publisher)
|
||||
}
|
||||
|
||||
addPublishedDecadeToFilterData(libraryId, decade) {
|
||||
if (!this.libraryFilterData[libraryId] || !decade || this.libraryFilterData[libraryId].publishedDecades.includes(decade)) return
|
||||
this.libraryFilterData[libraryId].publishedDecades.push(decade)
|
||||
}
|
||||
|
||||
addLanguageToFilterData(libraryId, language) {
|
||||
if (!this.libraryFilterData[libraryId] || !language || this.libraryFilterData[libraryId].languages.includes(language)) return
|
||||
this.libraryFilterData[libraryId].languages.push(language)
|
||||
|
||||
+10
-1
@@ -243,6 +243,15 @@ class Server {
|
||||
await this.auth.initPassportJs()
|
||||
|
||||
const router = express.Router()
|
||||
// if RouterBasePath is set, modify all requests to include the base path
|
||||
if (global.RouterBasePath) {
|
||||
app.use((req, res, next) => {
|
||||
if (!req.url.startsWith(global.RouterBasePath)) {
|
||||
req.url = `${global.RouterBasePath}${req.url}`
|
||||
}
|
||||
next()
|
||||
})
|
||||
}
|
||||
app.use(global.RouterBasePath, router)
|
||||
app.disable('x-powered-by')
|
||||
|
||||
@@ -340,7 +349,7 @@ class Server {
|
||||
Logger.info('Received ping')
|
||||
res.json({ success: true })
|
||||
})
|
||||
app.get('/healthcheck', (req, res) => res.sendStatus(200))
|
||||
router.get('/healthcheck', (req, res) => res.sendStatus(200))
|
||||
|
||||
this.server.listen(this.Port, this.Host, () => {
|
||||
if (this.Host) Logger.info(`Listening on http://${this.Host}:${this.Port}`)
|
||||
|
||||
@@ -103,7 +103,8 @@ class SocketAuthority {
|
||||
cors: {
|
||||
origin: '*',
|
||||
methods: ['GET', 'POST']
|
||||
}
|
||||
},
|
||||
path: `${global.RouterBasePath}/socket.io`
|
||||
})
|
||||
|
||||
this.io.on('connection', (socket) => {
|
||||
|
||||
@@ -9,7 +9,6 @@ const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilter
|
||||
const libraryItemFilters = require('../utils/queries/libraryItemFilters')
|
||||
const seriesFilters = require('../utils/queries/seriesFilters')
|
||||
const fileUtils = require('../utils/fileUtils')
|
||||
const { asciiOnlyToLowerCase } = require('../utils/index')
|
||||
const { createNewSortInstance } = require('../libs/fastSort')
|
||||
const naturalSort = createNewSortInstance({
|
||||
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
|
||||
@@ -493,8 +492,8 @@ class LibraryController {
|
||||
const payload = {
|
||||
results: [],
|
||||
total: undefined,
|
||||
limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0,
|
||||
page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0,
|
||||
limit: req.query.limit || 0,
|
||||
page: req.query.page || 0,
|
||||
sortBy: req.query.sort,
|
||||
sortDesc: req.query.desc === '1',
|
||||
filterBy: req.query.filter,
|
||||
@@ -504,13 +503,6 @@ class LibraryController {
|
||||
include: include.join(',')
|
||||
}
|
||||
|
||||
if (!Number.isInteger(payload.limit) || payload.limit < 0) {
|
||||
return res.status(400).send('Invalid request. Limit must be a positive integer')
|
||||
}
|
||||
if (!Number.isInteger(payload.page) || payload.page < 0) {
|
||||
return res.status(400).send('Invalid request. Page must be a positive integer')
|
||||
}
|
||||
|
||||
payload.offset = payload.page * payload.limit
|
||||
|
||||
// TODO: Temporary way of handling collapse sub-series. Either remove feature or handle through sql queries
|
||||
@@ -602,8 +594,8 @@ class LibraryController {
|
||||
const payload = {
|
||||
results: [],
|
||||
total: 0,
|
||||
limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0,
|
||||
page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0,
|
||||
limit: req.query.limit || 0,
|
||||
page: req.query.page || 0,
|
||||
sortBy: req.query.sort,
|
||||
sortDesc: req.query.desc === '1',
|
||||
filterBy: req.query.filter,
|
||||
@@ -674,8 +666,8 @@ class LibraryController {
|
||||
const payload = {
|
||||
results: [],
|
||||
total: 0,
|
||||
limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0,
|
||||
page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0,
|
||||
limit: req.query.limit || 0,
|
||||
page: req.query.page || 0,
|
||||
sortBy: req.query.sort,
|
||||
sortDesc: req.query.desc === '1',
|
||||
filterBy: req.query.filter,
|
||||
@@ -710,8 +702,8 @@ class LibraryController {
|
||||
const payload = {
|
||||
results: [],
|
||||
total: playlistsForUser.length,
|
||||
limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0,
|
||||
page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0
|
||||
limit: req.query.limit || 0,
|
||||
page: req.query.page || 0
|
||||
}
|
||||
|
||||
if (payload.limit) {
|
||||
@@ -742,7 +734,7 @@ class LibraryController {
|
||||
* @param {Response} res
|
||||
*/
|
||||
async getUserPersonalizedShelves(req, res) {
|
||||
const limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) || 10 : 10
|
||||
const limitPerShelf = req.query.limit || 10
|
||||
const include = (req.query.include || '')
|
||||
.split(',')
|
||||
.map((v) => v.trim().toLowerCase())
|
||||
@@ -815,8 +807,8 @@ class LibraryController {
|
||||
return res.status(400).send('Invalid request. Query param "q" must be a string')
|
||||
}
|
||||
|
||||
const limit = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12
|
||||
const query = asciiOnlyToLowerCase(req.query.q.trim())
|
||||
const limit = req.query.limit || 12
|
||||
const query = req.query.q.trim()
|
||||
|
||||
const matches = await libraryItemFilters.search(req.user, req.library, query, limit)
|
||||
res.json(matches)
|
||||
@@ -873,8 +865,40 @@ class LibraryController {
|
||||
* @param {Response} res
|
||||
*/
|
||||
async getAuthors(req, res) {
|
||||
const isPaginated = req.query.limit && !isNaN(req.query.limit) && !isNaN(req.query.page)
|
||||
|
||||
const payload = {
|
||||
results: [],
|
||||
total: 0,
|
||||
limit: isPaginated ? Number(req.query.limit) : 0,
|
||||
page: isPaginated ? Number(req.query.page) : 0,
|
||||
sortBy: req.query.sort,
|
||||
sortDesc: req.query.desc === '1',
|
||||
filterBy: req.query.filter,
|
||||
minified: req.query.minified === '1',
|
||||
include: req.query.include
|
||||
}
|
||||
|
||||
// create order, limit and offset for pagination
|
||||
let offset = isPaginated ? payload.page * payload.limit : undefined
|
||||
let limit = isPaginated ? payload.limit : undefined
|
||||
let order = undefined
|
||||
const direction = payload.sortDesc ? 'DESC' : 'ASC'
|
||||
if (payload.sortBy === 'name') {
|
||||
order = [[Sequelize.literal('name COLLATE NOCASE'), direction]]
|
||||
} else if (payload.sortBy === 'lastFirst') {
|
||||
order = [[Sequelize.literal('lastFirst COLLATE NOCASE'), direction]]
|
||||
} else if (payload.sortBy === 'addedAt') {
|
||||
order = [['createdAt', direction]]
|
||||
} else if (payload.sortBy === 'updatedAt') {
|
||||
order = [['updatedAt', direction]]
|
||||
} else if (payload.sortBy === 'numBooks') {
|
||||
offset = undefined
|
||||
limit = undefined
|
||||
}
|
||||
|
||||
const { bookWhere, replacements } = libraryItemsBookFilters.getUserPermissionBookWhereQuery(req.user)
|
||||
const authors = await Database.authorModel.findAll({
|
||||
const { rows: authors, count } = await Database.authorModel.findAndCountAll({
|
||||
where: {
|
||||
libraryId: req.library.id
|
||||
},
|
||||
@@ -888,10 +912,13 @@ class LibraryController {
|
||||
attributes: []
|
||||
}
|
||||
},
|
||||
order: [[Sequelize.literal('name COLLATE NOCASE'), 'ASC']]
|
||||
order: order,
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
distinct: true
|
||||
})
|
||||
|
||||
const oldAuthors = []
|
||||
let oldAuthors = []
|
||||
|
||||
for (const author of authors) {
|
||||
const oldAuthor = author.toOldJSONExpanded(author.books.length)
|
||||
@@ -899,9 +926,25 @@ class LibraryController {
|
||||
oldAuthors.push(oldAuthor)
|
||||
}
|
||||
|
||||
res.json({
|
||||
authors: oldAuthors
|
||||
})
|
||||
// numBooks sort is handled post-query
|
||||
if (payload.sortBy === 'numBooks') {
|
||||
oldAuthors.sort((a, b) => (payload.sortDesc ? b.numBooks - a.numBooks : a.numBooks - b.numBooks))
|
||||
if (isPaginated) {
|
||||
const startIndex = payload.page * payload.limit
|
||||
const endIndex = startIndex + payload.limit
|
||||
oldAuthors = oldAuthors.slice(startIndex, endIndex)
|
||||
}
|
||||
}
|
||||
|
||||
payload.results = oldAuthors
|
||||
if (isPaginated) {
|
||||
payload.total = count
|
||||
res.json(payload)
|
||||
} else {
|
||||
res.json({
|
||||
authors: payload.results
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1096,8 +1139,8 @@ class LibraryController {
|
||||
|
||||
const payload = {
|
||||
episodes: [],
|
||||
limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0,
|
||||
page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0
|
||||
limit: req.query.limit || 0,
|
||||
page: req.query.page || 0
|
||||
}
|
||||
|
||||
const offset = payload.page * payload.limit
|
||||
@@ -1183,6 +1226,44 @@ class LibraryController {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* GET: /api/libraries/:id/podcast-titles
|
||||
*
|
||||
* Get podcast titles with itunesId and libraryItemId for library
|
||||
* Used on the podcast add page in order to check if a podcast is already in the library and redirect to it
|
||||
*
|
||||
* @param {LibraryControllerRequest} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async getPodcastTitles(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[LibraryController] Non-admin user "${req.user.username}" attempted to get podcast titles`)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
const podcasts = await Database.podcastModel.findAll({
|
||||
attributes: ['id', 'title', 'itunesId'],
|
||||
include: {
|
||||
model: Database.libraryItemModel,
|
||||
attributes: ['id', 'libraryId'],
|
||||
where: {
|
||||
libraryId: req.library.id
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
res.json({
|
||||
podcasts: podcasts.map((p) => {
|
||||
return {
|
||||
title: p.title,
|
||||
itunesId: p.itunesId,
|
||||
libraryItemId: p.libraryItem.id,
|
||||
libraryId: p.libraryItem.libraryId
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {RequestWithUser} req
|
||||
@@ -1200,6 +1281,17 @@ class LibraryController {
|
||||
return res.status(404).send('Library not found')
|
||||
}
|
||||
req.library = library
|
||||
|
||||
// Ensure pagination query params are positive integers
|
||||
for (const queryKey of ['limit', 'page']) {
|
||||
if (req.query[queryKey] !== undefined) {
|
||||
req.query[queryKey] = !isNaN(req.query[queryKey]) ? Number(req.query[queryKey]) : 0
|
||||
if (!Number.isInteger(req.query[queryKey]) || req.query[queryKey] < 0) {
|
||||
return res.status(400).send(`Invalid request. ${queryKey} must be a positive integer`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,7 +324,7 @@ class BinaryManager {
|
||||
defaultRequiredBinaries = [
|
||||
new Binary('ffmpeg', 'executable', 'FFMPEG_PATH', ['5.1'], ffbinaries), // ffmpeg executable
|
||||
new Binary('ffprobe', 'executable', 'FFPROBE_PATH', ['5.1'], ffbinaries), // ffprobe executable
|
||||
new Binary('libnusqlite3', 'library', 'NUSQLITE3_PATH', ['1.1'], nunicode, false) // nunicode sqlite3 extension
|
||||
new Binary('libnusqlite3', 'library', 'NUSQLITE3_PATH', ['1.2'], nunicode, false) // nunicode sqlite3 extension
|
||||
]
|
||||
|
||||
constructor(requiredBinaries = this.defaultRequiredBinaries) {
|
||||
|
||||
@@ -38,6 +38,7 @@ class MigrationManager {
|
||||
if (!(await fs.pathExists(this.configPath))) throw new Error(`Config path does not exist: ${this.configPath}`)
|
||||
|
||||
this.migrationsDir = path.join(this.configPath, 'migrations')
|
||||
await fs.ensureDir(this.migrationsDir)
|
||||
|
||||
this.serverVersion = this.extractVersionFromTag(serverVersion)
|
||||
if (!this.serverVersion) throw new Error(`Invalid server version: ${serverVersion}. Expected a version tag like v1.2.3.`)
|
||||
@@ -129,7 +130,7 @@ class MigrationManager {
|
||||
|
||||
async initUmzug(umzugStorage = new SequelizeStorage({ sequelize: this.sequelize })) {
|
||||
// This check is for dependency injection in tests
|
||||
const files = (await fs.readdir(this.migrationsDir)).map((file) => path.join(this.migrationsDir, file))
|
||||
const files = (await fs.readdir(this.migrationsDir)).filter((file) => !file.startsWith('.')).map((file) => path.join(this.migrationsDir, file))
|
||||
|
||||
const parent = new Umzug({
|
||||
migrations: {
|
||||
@@ -222,8 +223,6 @@ class MigrationManager {
|
||||
}
|
||||
|
||||
async copyMigrationsToConfigDir() {
|
||||
await fs.ensureDir(this.migrationsDir) // Ensure the target directory exists
|
||||
|
||||
if (!(await fs.pathExists(this.migrationsSourceDir))) return
|
||||
|
||||
const files = await fs.readdir(this.migrationsSourceDir)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
Please add a record of every database migration that you create to this file. This will help us keep track of changes to the database schema over time.
|
||||
|
||||
| Server Version | Migration Script Name | Description |
|
||||
| -------------- | --------------------- | ----------- |
|
||||
| | | |
|
||||
| Server Version | Migration Script Name | Description |
|
||||
| -------------- | ---------------------------- | ------------------------------------------------------------------------------------ |
|
||||
| v2.15.0 | v2.15.0-series-column-unique | Series must have unique names in the same library |
|
||||
| v2.15.1 | v2.15.1-reindex-nocase | Fix potential db corruption issues due to bad sqlite extension introduced in v2.12.0 |
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This upward migration script cleans any duplicate series in the `Series` table and
|
||||
* adds a unique index on the `name` and `libraryId` columns.
|
||||
*
|
||||
* @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('[2.15.0 migration] UPGRADE BEGIN: 2.15.0-series-column-unique ')
|
||||
|
||||
// Run reindex nocase to fix potential corruption issues due to the bad sqlite extension introduced in v2.12.0
|
||||
logger.info('[2.15.0 migration] Reindexing NOCASE indices to fix potential hidden corruption issues')
|
||||
await queryInterface.sequelize.query('REINDEX NOCASE;')
|
||||
|
||||
// Check if the unique index already exists
|
||||
const seriesIndexes = await queryInterface.showIndex('Series')
|
||||
if (seriesIndexes.some((index) => index.name === 'unique_series_name_per_library')) {
|
||||
logger.info('[2.15.0 migration] Unique index on Series.name and Series.libraryId already exists')
|
||||
logger.info('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique ')
|
||||
return
|
||||
}
|
||||
|
||||
// The steps taken to deduplicate the series are as follows:
|
||||
// 1. Find all duplicate series in the `Series` table.
|
||||
// 2. Iterate over the duplicate series and find all book IDs that are associated with the duplicate series in `bookSeries` table.
|
||||
// 2.a For each book ID, check if the ID occurs multiple times for the duplicate series.
|
||||
// 2.b If so, keep only one of the rows that has this bookId and seriesId.
|
||||
// 3. Update `bookSeries` table to point to the most recent series.
|
||||
// 4. Delete the older series.
|
||||
|
||||
// Use the queryInterface to get the series table and find duplicates in the `name` and `libraryId` column
|
||||
const [duplicates] = await queryInterface.sequelize.query(`
|
||||
SELECT name, libraryId
|
||||
FROM Series
|
||||
GROUP BY name, libraryId
|
||||
HAVING COUNT(name) > 1
|
||||
`)
|
||||
|
||||
// Print out how many duplicates were found
|
||||
logger.info(`[2.15.0 migration] Found ${duplicates.length} duplicate series`)
|
||||
|
||||
// Iterate over each duplicate series
|
||||
for (const duplicate of duplicates) {
|
||||
// Report the series name that is being deleted
|
||||
logger.info(`[2.15.0 migration] Deduplicating series "${duplicate.name}" in library ${duplicate.libraryId}`)
|
||||
|
||||
// Determine any duplicate book IDs in the `bookSeries` table for the same series
|
||||
const [duplicateBookIds] = await queryInterface.sequelize.query(
|
||||
`
|
||||
SELECT bookId
|
||||
FROM BookSeries
|
||||
WHERE seriesId IN (
|
||||
SELECT id
|
||||
FROM Series
|
||||
WHERE name = :name AND libraryId = :libraryId
|
||||
)
|
||||
GROUP BY bookId
|
||||
HAVING COUNT(bookId) > 1
|
||||
`,
|
||||
{
|
||||
replacements: {
|
||||
name: duplicate.name,
|
||||
libraryId: duplicate.libraryId
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Iterate over the duplicate book IDs if there is at least one and only keep the first row that has this bookId and seriesId
|
||||
for (const { bookId } of duplicateBookIds) {
|
||||
logger.info(`[2.15.0 migration] Deduplicating bookId ${bookId} in series "${duplicate.name}" of library ${duplicate.libraryId}`)
|
||||
// Get all rows of `BookSeries` table that have the same `bookId` and `seriesId`. Sort by `sequence` with nulls sorted last
|
||||
const [duplicateBookSeries] = await queryInterface.sequelize.query(
|
||||
`
|
||||
SELECT id
|
||||
FROM BookSeries
|
||||
WHERE bookId = :bookId
|
||||
AND seriesId IN (
|
||||
SELECT id
|
||||
FROM Series
|
||||
WHERE name = :name AND libraryId = :libraryId
|
||||
)
|
||||
ORDER BY sequence NULLS LAST
|
||||
`,
|
||||
{
|
||||
replacements: {
|
||||
bookId,
|
||||
name: duplicate.name,
|
||||
libraryId: duplicate.libraryId
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// remove the first element from the array
|
||||
duplicateBookSeries.shift()
|
||||
|
||||
// Delete the remaining duplicate rows
|
||||
if (duplicateBookSeries.length > 0) {
|
||||
const [deletedBookSeries] = await queryInterface.sequelize.query(
|
||||
`
|
||||
DELETE FROM BookSeries
|
||||
WHERE id IN (:ids)
|
||||
`,
|
||||
{
|
||||
replacements: {
|
||||
ids: duplicateBookSeries.map((row) => row.id)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
logger.info(`[2.15.0 migration] Finished cleanup of bookId ${bookId} in series "${duplicate.name}" of library ${duplicate.libraryId}`)
|
||||
}
|
||||
|
||||
// Get all the most recent series which matches the `name` and `libraryId`
|
||||
const [mostRecentSeries] = await queryInterface.sequelize.query(
|
||||
`
|
||||
SELECT id
|
||||
FROM Series
|
||||
WHERE name = :name AND libraryId = :libraryId
|
||||
ORDER BY updatedAt DESC
|
||||
LIMIT 1
|
||||
`,
|
||||
{
|
||||
replacements: {
|
||||
name: duplicate.name,
|
||||
libraryId: duplicate.libraryId
|
||||
},
|
||||
type: queryInterface.sequelize.QueryTypes.SELECT
|
||||
}
|
||||
)
|
||||
|
||||
if (mostRecentSeries) {
|
||||
// Update all BookSeries records for this series to point to the most recent series
|
||||
const [seriesUpdated] = await queryInterface.sequelize.query(
|
||||
`
|
||||
UPDATE BookSeries
|
||||
SET seriesId = :mostRecentSeriesId
|
||||
WHERE seriesId IN (
|
||||
SELECT id
|
||||
FROM Series
|
||||
WHERE name = :name AND libraryId = :libraryId
|
||||
AND id != :mostRecentSeriesId
|
||||
)
|
||||
`,
|
||||
{
|
||||
replacements: {
|
||||
name: duplicate.name,
|
||||
libraryId: duplicate.libraryId,
|
||||
mostRecentSeriesId: mostRecentSeries.id
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Delete the older series
|
||||
const seriesDeleted = await queryInterface.sequelize.query(
|
||||
`
|
||||
DELETE FROM Series
|
||||
WHERE name = :name AND libraryId = :libraryId
|
||||
AND id != :mostRecentSeriesId
|
||||
`,
|
||||
{
|
||||
replacements: {
|
||||
name: duplicate.name,
|
||||
libraryId: duplicate.libraryId,
|
||||
mostRecentSeriesId: mostRecentSeries.id
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`[2.15.0 migration] Deduplication complete`)
|
||||
|
||||
// Create a unique index based on the name and library ID for the `Series` table
|
||||
await queryInterface.addIndex('Series', ['name', 'libraryId'], {
|
||||
unique: true,
|
||||
name: 'unique_series_name_per_library'
|
||||
})
|
||||
logger.info('[2.15.0 migration] Added unique index on Series.name and Series.libraryId')
|
||||
|
||||
logger.info('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique ')
|
||||
}
|
||||
|
||||
/**
|
||||
* This removes the unique index on the `Series` 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('[2.15.0 migration] DOWNGRADE BEGIN: 2.15.0-series-column-unique ')
|
||||
|
||||
// Remove the unique index
|
||||
await queryInterface.removeIndex('Series', 'unique_series_name_per_library')
|
||||
logger.info('[2.15.0 migration] Removed unique index on Series.name and Series.libraryId')
|
||||
|
||||
logger.info('[2.15.0 migration] DOWNGRADE END: 2.15.0-series-column-unique ')
|
||||
}
|
||||
|
||||
module.exports = { up, down }
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This upward migration script fixes old database corruptions due to the a bad sqlite extension introduced in v2.12.0.
|
||||
*
|
||||
* @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('[2.15.1 migration] UPGRADE BEGIN: 2.15.1-reindex-nocase ')
|
||||
|
||||
// Run reindex nocase to fix potential corruption issues due to the bad sqlite extension introduced in v2.12.0
|
||||
logger.info('[2.15.1 migration] Reindexing NOCASE indices to fix potential hidden corruption issues')
|
||||
await queryInterface.sequelize.query('REINDEX NOCASE;')
|
||||
|
||||
logger.info('[2.15.1 migration] UPGRADE END: 2.15.1-reindex-nocase ')
|
||||
}
|
||||
|
||||
/**
|
||||
* This downward migration script is a no-op.
|
||||
*
|
||||
* @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('[2.15.1 migration] DOWNGRADE BEGIN: 2.15.1-reindex-nocase ')
|
||||
|
||||
// This migration is a no-op
|
||||
logger.info('[2.15.1 migration] No action required for downgrade')
|
||||
|
||||
logger.info('[2.15.1 migration] DOWNGRADE END: 2.15.1-reindex-nocase ')
|
||||
}
|
||||
|
||||
module.exports = { up, down }
|
||||
@@ -1,6 +1,5 @@
|
||||
const { DataTypes, Model, where, fn, col } = require('sequelize')
|
||||
const parseNameString = require('../utils/parsers/parseNameString')
|
||||
const { asciiOnlyToLowerCase } = require('../utils/index')
|
||||
|
||||
class Author extends Model {
|
||||
constructor(values, options) {
|
||||
@@ -56,7 +55,7 @@ class Author extends Model {
|
||||
static async getByNameAndLibrary(authorName, libraryId) {
|
||||
return this.findOne({
|
||||
where: [
|
||||
where(fn('lower', col('name')), asciiOnlyToLowerCase(authorName)),
|
||||
where(fn('lower', col('name')), authorName.toLowerCase()),
|
||||
{
|
||||
libraryId
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const { DataTypes, Model, where, fn, col } = require('sequelize')
|
||||
|
||||
const { getTitlePrefixAtEnd } = require('../utils/index')
|
||||
const { asciiOnlyToLowerCase } = require('../utils/index')
|
||||
|
||||
class Series extends Model {
|
||||
constructor(values, options) {
|
||||
@@ -42,7 +41,7 @@ class Series extends Model {
|
||||
static async getByNameAndLibrary(seriesName, libraryId) {
|
||||
return this.findOne({
|
||||
where: [
|
||||
where(fn('lower', col('name')), asciiOnlyToLowerCase(seriesName)),
|
||||
where(fn('lower', col('name')), seriesName.toLowerCase()),
|
||||
{
|
||||
libraryId
|
||||
}
|
||||
@@ -84,6 +83,12 @@ class Series extends Model {
|
||||
// collate: 'NOCASE'
|
||||
// }]
|
||||
// },
|
||||
{
|
||||
// unique constraint on name and libraryId
|
||||
fields: ['name', 'libraryId'],
|
||||
unique: true,
|
||||
name: 'unique_series_name_per_library'
|
||||
},
|
||||
{
|
||||
fields: ['libraryId']
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@ class ApiRouter {
|
||||
this.router.get('/libraries/:id/opml', LibraryController.middleware.bind(this), LibraryController.getOPMLFile.bind(this))
|
||||
this.router.post('/libraries/order', LibraryController.reorder.bind(this))
|
||||
this.router.post('/libraries/:id/remove-metadata', LibraryController.middleware.bind(this), LibraryController.removeAllMetadataFiles.bind(this))
|
||||
this.router.get('/libraries/:id/podcast-titles', LibraryController.middleware.bind(this), LibraryController.getPodcastTitles.bind(this))
|
||||
|
||||
//
|
||||
// Item Routes
|
||||
|
||||
@@ -590,6 +590,10 @@ class BookScanner {
|
||||
Database.addPublisherToFilterData(libraryItemData.libraryId, libraryItem.book.publisher)
|
||||
Database.addLanguageToFilterData(libraryItemData.libraryId, libraryItem.book.language)
|
||||
|
||||
const publishedYear = libraryItem.book.publishedYear
|
||||
const decade = publishedYear ? `${Math.floor(publishedYear / 10) * 10}` : null
|
||||
Database.addPublishedDecadeToFilterData(libraryItemData.libraryId, decade)
|
||||
|
||||
// Load for emitting to client
|
||||
libraryItem.media = await libraryItem.getMedia({
|
||||
include: [
|
||||
|
||||
@@ -55,7 +55,7 @@ async function extractCoverArt(filepath, outputpath) {
|
||||
return new Promise((resolve) => {
|
||||
/** @type {import('../libs/fluentFfmpeg/index').FfmpegCommand} */
|
||||
var ffmpeg = Ffmpeg(filepath)
|
||||
ffmpeg.addOption(['-map 0:v', '-frames:v 1'])
|
||||
ffmpeg.addOption(['-map 0:v:0', '-frames:v 1'])
|
||||
ffmpeg.output(outputpath)
|
||||
|
||||
ffmpeg.on('start', (cmd) => {
|
||||
|
||||
@@ -194,29 +194,6 @@ module.exports.getTitlePrefixAtEnd = (title) => {
|
||||
return prefix ? `${sort}, ${prefix}` : title
|
||||
}
|
||||
|
||||
/**
|
||||
* to lower case for only ascii characters
|
||||
* used to handle sqlite that doesnt support unicode lower
|
||||
* @see https://github.com/advplyr/audiobookshelf/issues/2187
|
||||
*
|
||||
* @param {string} str
|
||||
* @returns {string}
|
||||
*/
|
||||
module.exports.asciiOnlyToLowerCase = (str) => {
|
||||
if (!str) return ''
|
||||
|
||||
let temp = ''
|
||||
for (let chars of str) {
|
||||
let value = chars.charCodeAt()
|
||||
if (value >= 65 && value <= 90) {
|
||||
temp += String.fromCharCode(value + 32)
|
||||
} else {
|
||||
temp += chars
|
||||
}
|
||||
}
|
||||
return temp
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape string used in RegExp
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
|
||||
|
||||
@@ -7,6 +7,7 @@ module.exports.notificationData = {
|
||||
requiresLibrary: true,
|
||||
libraryMediaType: 'podcast',
|
||||
description: 'Triggered when a podcast episode is auto-downloaded',
|
||||
descriptionKey: 'NotificationOnEpisodeDownloadedDescription',
|
||||
variables: ['libraryItemId', 'libraryId', 'podcastTitle', 'podcastAuthor', 'podcastDescription', 'podcastGenres', 'episodeTitle', 'episodeSubtitle', 'episodeDescription', 'libraryName', 'episodeId', 'mediaTags'],
|
||||
defaults: {
|
||||
title: 'New {{podcastTitle}} Episode!',
|
||||
@@ -31,6 +32,7 @@ module.exports.notificationData = {
|
||||
name: 'onBackupCompleted',
|
||||
requiresLibrary: false,
|
||||
description: 'Triggered when a backup is completed',
|
||||
descriptionKey: 'NotificationOnBackupCompletedDescription',
|
||||
variables: ['completionTime', 'backupPath', 'backupSize', 'backupCount', 'removedOldest'],
|
||||
defaults: {
|
||||
title: 'Backup Completed',
|
||||
@@ -48,6 +50,7 @@ module.exports.notificationData = {
|
||||
name: 'onBackupFailed',
|
||||
requiresLibrary: false,
|
||||
description: 'Triggered when a backup fails',
|
||||
descriptionKey: 'NotificationOnBackupFailedDescription',
|
||||
variables: ['errorMsg'],
|
||||
defaults: {
|
||||
title: 'Backup Failed',
|
||||
@@ -61,6 +64,7 @@ module.exports.notificationData = {
|
||||
name: 'onTest',
|
||||
requiresLibrary: false,
|
||||
description: 'Event for testing the notification system',
|
||||
descriptionKey: 'NotificationOnTestDescription',
|
||||
variables: ['version'],
|
||||
defaults: {
|
||||
title: 'Test Notification on Abs {{version}}',
|
||||
|
||||
@@ -26,7 +26,7 @@ module.exports = {
|
||||
let filterValue = null
|
||||
let filterGroup = null
|
||||
if (filterBy) {
|
||||
const searchGroups = ['genres', 'tags', 'series', 'authors', 'progress', 'narrators', 'publishers', 'missing', 'languages', 'tracks', 'ebooks']
|
||||
const searchGroups = ['genres', 'tags', 'series', 'authors', 'progress', 'narrators', 'publishers', 'publishedDecades', 'missing', 'languages', 'tracks', 'ebooks']
|
||||
const group = searchGroups.find((_group) => filterBy.startsWith(_group + '.'))
|
||||
filterGroup = group || filterBy
|
||||
filterValue = group ? this.decode(filterBy.replace(`${group}.`, '')) : null
|
||||
@@ -458,6 +458,7 @@ module.exports = {
|
||||
narrators: new Set(),
|
||||
languages: new Set(),
|
||||
publishers: new Set(),
|
||||
publishedDecades: new Set(),
|
||||
numIssues: 0
|
||||
}
|
||||
|
||||
@@ -492,7 +493,7 @@ module.exports = {
|
||||
libraryId: libraryId
|
||||
}
|
||||
},
|
||||
attributes: ['tags', 'genres', 'publisher', 'narrators', 'language']
|
||||
attributes: ['tags', 'genres', 'publisher', 'publishedYear', 'narrators', 'language']
|
||||
})
|
||||
for (const book of books) {
|
||||
if (book.libraryItem.isMissing || book.libraryItem.isInvalid) data.numIssues++
|
||||
@@ -506,6 +507,11 @@ module.exports = {
|
||||
book.narrators.forEach((narrator) => data.narrators.add(narrator))
|
||||
}
|
||||
if (book.publisher) data.publishers.add(book.publisher)
|
||||
// Check if published year exists and is valid
|
||||
if (book.publishedYear && !isNaN(book.publishedYear) && book.publishedYear > 0 && book.publishedYear < 3000) {
|
||||
const decade = (Math.floor(book.publishedYear / 10) * 10).toString()
|
||||
data.publishedDecades.add(decade)
|
||||
}
|
||||
if (book.language) data.languages.add(book.language)
|
||||
}
|
||||
|
||||
@@ -532,6 +538,7 @@ module.exports = {
|
||||
data.series = naturalSort(data.series).asc((se) => se.name)
|
||||
data.narrators = naturalSort([...data.narrators]).asc()
|
||||
data.publishers = naturalSort([...data.publishers]).asc()
|
||||
data.publishedDecades = naturalSort([...data.publishedDecades]).asc()
|
||||
data.languages = naturalSort([...data.languages]).asc()
|
||||
data.loadedAt = Date.now()
|
||||
Database.libraryFilterData[libraryId] = data
|
||||
|
||||
@@ -219,7 +219,7 @@ module.exports = {
|
||||
mediaWhere[key] = {
|
||||
[Sequelize.Op.or]: [null, '']
|
||||
}
|
||||
} else if (['genres', 'tags', 'narrators'].includes(value)) {
|
||||
} else if (['genres', 'tags', 'narrators', 'chapters'].includes(value)) {
|
||||
mediaWhere[value] = {
|
||||
[Sequelize.Op.or]: [null, Sequelize.where(Sequelize.fn('json_array_length', Sequelize.col(value)), 0)]
|
||||
}
|
||||
@@ -228,6 +228,12 @@ module.exports = {
|
||||
} else if (value === 'series') {
|
||||
mediaWhere['$series.id$'] = null
|
||||
}
|
||||
} else if (group === 'publishedDecades') {
|
||||
const startYear = parseInt(value)
|
||||
const endYear = parseInt(value, 10) + 9
|
||||
mediaWhere = Sequelize.where(Sequelize.literal('CAST(`book`.`publishedYear` AS INTEGER)'), {
|
||||
[Sequelize.Op.between]: [startYear, endYear]
|
||||
})
|
||||
}
|
||||
|
||||
return { mediaWhere, replacements }
|
||||
@@ -499,7 +505,6 @@ module.exports = {
|
||||
}
|
||||
|
||||
let { mediaWhere, replacements } = this.getMediaGroupQuery(filterGroup, filterValue)
|
||||
|
||||
let bookWhere = Array.isArray(mediaWhere) ? mediaWhere : [mediaWhere]
|
||||
|
||||
// User permissions
|
||||
|
||||
@@ -63,6 +63,8 @@ describe('MigrationManager', () => {
|
||||
await migrationManager.init(serverVersion)
|
||||
|
||||
// Assert
|
||||
expect(fsEnsureDirStub.calledOnce).to.be.true
|
||||
expect(fsEnsureDirStub.calledWith(migrationManager.migrationsDir)).to.be.true
|
||||
expect(migrationManager.serverVersion).to.equal(serverVersion)
|
||||
expect(migrationManager.sequelize).to.equal(sequelizeStub)
|
||||
expect(migrationManager.migrationsDir).to.equal(path.join(__dirname, 'migrations'))
|
||||
@@ -353,8 +355,6 @@ describe('MigrationManager', () => {
|
||||
await migrationManager.copyMigrationsToConfigDir()
|
||||
|
||||
// Assert
|
||||
expect(fsEnsureDirStub.calledOnce).to.be.true
|
||||
expect(fsEnsureDirStub.calledWith(targetDir)).to.be.true
|
||||
expect(readdirStub.calledOnce).to.be.true
|
||||
expect(readdirStub.calledWith(migrationsSourceDir)).to.be.true
|
||||
expect(fsCopyStub.calledTwice).to.be.true
|
||||
@@ -382,8 +382,6 @@ describe('MigrationManager', () => {
|
||||
} catch (error) {}
|
||||
|
||||
// Assert
|
||||
expect(fsEnsureDirStub.calledOnce).to.be.true
|
||||
expect(fsEnsureDirStub.calledWith(targetDir)).to.be.true
|
||||
expect(readdirStub.calledOnce).to.be.true
|
||||
expect(readdirStub.calledWith(migrationsSourceDir)).to.be.true
|
||||
expect(fsCopyStub.calledTwice).to.be.true
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
const { expect } = require('chai')
|
||||
const sinon = require('sinon')
|
||||
const { up, down } = require('../../../server/migrations/v2.15.0-series-column-unique')
|
||||
const { Sequelize } = require('sequelize')
|
||||
const Logger = require('../../../server/Logger')
|
||||
const { query } = require('express')
|
||||
const { logger } = require('sequelize/lib/utils/logger')
|
||||
const e = require('express')
|
||||
|
||||
describe('migration-v2.15.0-series-column-unique', () => {
|
||||
let sequelize
|
||||
let queryInterface
|
||||
let loggerInfoStub
|
||||
let series1Id
|
||||
let series2Id
|
||||
let series3Id
|
||||
let series1Id_dup
|
||||
let series3Id_dup
|
||||
let series1Id_dup2
|
||||
let book1Id
|
||||
let book2Id
|
||||
let book3Id
|
||||
let book4Id
|
||||
let book5Id
|
||||
let book6Id
|
||||
let library1Id
|
||||
let library2Id
|
||||
let bookSeries1Id
|
||||
let bookSeries2Id
|
||||
let bookSeries3Id
|
||||
let bookSeries1Id_dup
|
||||
let bookSeries3Id_dup
|
||||
let bookSeries1Id_dup2
|
||||
|
||||
beforeEach(() => {
|
||||
sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
|
||||
queryInterface = sequelize.getQueryInterface()
|
||||
loggerInfoStub = sinon.stub(Logger, 'info')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
describe('up', () => {
|
||||
beforeEach(async () => {
|
||||
await queryInterface.createTable('Series', {
|
||||
id: { type: Sequelize.UUID, primaryKey: true },
|
||||
name: { type: Sequelize.STRING, allowNull: false },
|
||||
libraryId: { type: Sequelize.UUID, allowNull: false },
|
||||
createdAt: { type: Sequelize.DATE, allowNull: false },
|
||||
updatedAt: { type: Sequelize.DATE, allowNull: false }
|
||||
})
|
||||
// Create a table for BookSeries, with a unique constraint of bookId and seriesId
|
||||
await queryInterface.createTable(
|
||||
'BookSeries',
|
||||
{
|
||||
id: { type: Sequelize.UUID, primaryKey: true },
|
||||
sequence: { type: Sequelize.STRING, allowNull: true },
|
||||
bookId: { type: Sequelize.UUID, allowNull: false },
|
||||
seriesId: { type: Sequelize.UUID, allowNull: false }
|
||||
},
|
||||
{ uniqueKeys: { book_series_unique: { fields: ['bookId', 'seriesId'] } } }
|
||||
)
|
||||
// Set UUIDs for the tests
|
||||
series1Id = 'fc086255-3fd2-4a95-8a28-840d9206501b'
|
||||
series2Id = '70f46ac2-ee48-4b3c-9822-933cc15c29bd'
|
||||
series3Id = '01cac008-142b-4e15-b0ff-cf7cc2c5b64e'
|
||||
series1Id_dup = 'ad0b3b3b-4b3b-4b3b-4b3b-4b3b4b3b4b3b'
|
||||
series3Id_dup = '4b3b4b3b-4b3b-4b3b-4b3b-4b3b4b3b4b3b'
|
||||
series1Id_dup2 = '0123456a-4b3b-4b3b-4b3b-4b3b4b3b4b3b'
|
||||
book1Id = '4a38b6e5-0ae4-4de4-b119-4e33891bd63f'
|
||||
book2Id = '8bc2e61d-47f6-42ef-a3f4-93cf2f1de82f'
|
||||
book3Id = 'ec9bbaaf-1e55-457f-b59c-bd2bd955a404'
|
||||
book4Id = '876f3b3b-4b3b-4b3b-4b3b-4b3b4b3b4b3b'
|
||||
book5Id = '4e5b4b3b-4b3b-4b3b-4b3b-4b3b4b3b4b3b'
|
||||
book6Id = 'abcda123-4b3b-4b3b-4b3b-4b3b4b3b4b3b'
|
||||
library1Id = '3a5a1c7c-a914-472e-88b0-b871ceae63e7'
|
||||
library2Id = 'fd6c324a-4f3a-4bb0-99d6-7a330e765e7e'
|
||||
bookSeries1Id = 'eca24687-2241-4ffa-a9b3-02a0ba03c763'
|
||||
bookSeries2Id = '56f56105-813b-4395-9689-fd04198e7d5d'
|
||||
bookSeries3Id = '404a1761-c710-4d86-9d78-68d9a9c0fb6b'
|
||||
bookSeries1Id_dup = '8bea3b3b-4b3b-4b3b-4b3b-4b3b4b3b4b3b'
|
||||
bookSeries3Id_dup = '89656a3b-4b3b-4b3b-4b3b-4b3b4b3b4b3b'
|
||||
bookSeries1Id_dup2 = '9bea3b3b-4b3b-4b3b-4b3b-4b3b4b3b4b3b'
|
||||
})
|
||||
afterEach(async () => {
|
||||
await queryInterface.dropTable('Series')
|
||||
await queryInterface.dropTable('BookSeries')
|
||||
})
|
||||
it('upgrade with no duplicate series', async () => {
|
||||
// Add some entries to the Series table using the UUID for the ids
|
||||
await queryInterface.bulkInsert('Series', [
|
||||
{ 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: series3Id, name: 'Series 3', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() }
|
||||
])
|
||||
// Add some entries to the BookSeries table
|
||||
await queryInterface.bulkInsert('BookSeries', [
|
||||
{ id: bookSeries1Id, sequence: '1', bookId: book1Id, seriesId: series1Id },
|
||||
{ id: bookSeries2Id, bookId: book2Id, seriesId: series2Id },
|
||||
{ id: bookSeries3Id, sequence: '1', bookId: book3Id, seriesId: series3Id }
|
||||
])
|
||||
|
||||
await up({ context: { queryInterface, logger: Logger } })
|
||||
|
||||
expect(loggerInfoStub.callCount).to.equal(6)
|
||||
expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.15.0 migration] UPGRADE BEGIN: 2.15.0-series-column-unique '))).to.be.true
|
||||
expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.15.0 migration] Reindexing NOCASE indices to fix potential hidden corruption issues'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.15.0 migration] Found 0 duplicate series'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.15.0 migration] Deduplication complete'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.15.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique '))).to.be.true
|
||||
// Validate rows in tables
|
||||
const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT })
|
||||
expect(series).to.have.length(3)
|
||||
expect(series).to.deep.include({ id: series1Id, name: 'Series 1', libraryId: library1Id })
|
||||
expect(series).to.deep.include({ id: series2Id, name: 'Series 2', libraryId: library2Id })
|
||||
expect(series).to.deep.include({ id: series3Id, name: 'Series 3', libraryId: library1Id })
|
||||
const bookSeries = await queryInterface.sequelize.query('SELECT "id", "sequence", "bookId", "seriesId" FROM BookSeries', { type: queryInterface.sequelize.QueryTypes.SELECT })
|
||||
expect(bookSeries).to.have.length(3)
|
||||
expect(bookSeries).to.deep.include({ id: bookSeries1Id, sequence: '1', bookId: book1Id, seriesId: series1Id })
|
||||
expect(bookSeries).to.deep.include({ id: bookSeries2Id, sequence: null, bookId: book2Id, seriesId: series2Id })
|
||||
expect(bookSeries).to.deep.include({ id: bookSeries3Id, sequence: '1', bookId: book3Id, seriesId: series3Id })
|
||||
})
|
||||
it('upgrade with duplicate series and no sequence', async () => {
|
||||
// Add some entries to the Series table using the UUID for the ids
|
||||
await queryInterface.bulkInsert('Series', [
|
||||
{ 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: 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: series3Id_dup, name: 'Series 3', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() },
|
||||
{ id: series1Id_dup2, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() }
|
||||
])
|
||||
// Add some entries to the BookSeries table
|
||||
await queryInterface.bulkInsert('BookSeries', [
|
||||
{ id: bookSeries1Id, bookId: book1Id, seriesId: series1Id },
|
||||
{ id: bookSeries2Id, bookId: book2Id, seriesId: series2Id },
|
||||
{ id: bookSeries3Id, bookId: book3Id, seriesId: series3Id },
|
||||
{ id: bookSeries1Id_dup, bookId: book4Id, seriesId: series1Id_dup },
|
||||
{ id: bookSeries3Id_dup, bookId: book5Id, seriesId: series3Id_dup },
|
||||
{ id: bookSeries1Id_dup2, bookId: book6Id, seriesId: series1Id_dup2 }
|
||||
])
|
||||
|
||||
await up({ context: { queryInterface, logger: Logger } })
|
||||
|
||||
expect(loggerInfoStub.callCount).to.equal(8)
|
||||
expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.15.0 migration] UPGRADE BEGIN: 2.15.0-series-column-unique '))).to.be.true
|
||||
expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.15.0 migration] Reindexing NOCASE indices to fix potential hidden corruption issues'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.15.0 migration] Found 2 duplicate series'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.15.0 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.15.0 migration] Deduplicating series "Series 3" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.15.0 migration] Deduplication complete'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.15.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(7).calledWith(sinon.match('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique '))).to.be.true
|
||||
// Validate rows
|
||||
const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT })
|
||||
expect(series).to.have.length(3)
|
||||
expect(series).to.deep.include({ id: series1Id, name: 'Series 1', libraryId: library1Id })
|
||||
expect(series).to.deep.include({ id: series2Id, name: 'Series 2', libraryId: library2Id })
|
||||
expect(series).to.deep.include({ id: series3Id, name: 'Series 3', libraryId: library1Id })
|
||||
const bookSeries = await queryInterface.sequelize.query('SELECT "id", "bookId", "seriesId" FROM BookSeries', { type: queryInterface.sequelize.QueryTypes.SELECT })
|
||||
expect(bookSeries).to.have.length(6)
|
||||
expect(bookSeries).to.deep.include({ id: bookSeries1Id, bookId: book1Id, seriesId: series1Id })
|
||||
expect(bookSeries).to.deep.include({ id: bookSeries2Id, bookId: book2Id, seriesId: series2Id })
|
||||
expect(bookSeries).to.deep.include({ id: bookSeries3Id, bookId: book3Id, seriesId: series3Id })
|
||||
expect(bookSeries).to.deep.include({ id: bookSeries1Id_dup, bookId: book4Id, seriesId: series1Id })
|
||||
expect(bookSeries).to.deep.include({ id: bookSeries3Id_dup, bookId: book5Id, seriesId: series3Id })
|
||||
expect(bookSeries).to.deep.include({ id: bookSeries1Id_dup2, bookId: book6Id, seriesId: series1Id })
|
||||
})
|
||||
it('upgrade with same series name in different libraries', async () => {
|
||||
// Add some entries to the Series table using the UUID for the ids
|
||||
await queryInterface.bulkInsert('Series', [
|
||||
{ id: series1Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() },
|
||||
{ id: series2Id, name: 'Series 1', libraryId: library2Id, createdAt: new Date(), updatedAt: new Date() }
|
||||
])
|
||||
// Add some entries to the BookSeries table
|
||||
await queryInterface.bulkInsert('BookSeries', [
|
||||
{ id: bookSeries1Id, bookId: book1Id, seriesId: series1Id },
|
||||
{ id: bookSeries2Id, bookId: book2Id, seriesId: series2Id }
|
||||
])
|
||||
|
||||
await up({ context: { queryInterface, logger: Logger } })
|
||||
|
||||
expect(loggerInfoStub.callCount).to.equal(6)
|
||||
expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.15.0 migration] UPGRADE BEGIN: 2.15.0-series-column-unique '))).to.be.true
|
||||
expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.15.0 migration] Reindexing NOCASE indices to fix potential hidden corruption issues'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.15.0 migration] Found 0 duplicate series'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.15.0 migration] Deduplication complete'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.15.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique '))).to.be.true
|
||||
// Validate rows
|
||||
const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT })
|
||||
expect(series).to.have.length(2)
|
||||
expect(series).to.deep.include({ id: series1Id, name: 'Series 1', libraryId: library1Id })
|
||||
expect(series).to.deep.include({ id: series2Id, name: 'Series 1', libraryId: library2Id })
|
||||
const bookSeries = await queryInterface.sequelize.query('SELECT "id", "bookId", "seriesId" FROM BookSeries', { type: queryInterface.sequelize.QueryTypes.SELECT })
|
||||
expect(bookSeries).to.have.length(2)
|
||||
expect(bookSeries).to.deep.include({ id: bookSeries1Id, bookId: book1Id, seriesId: series1Id })
|
||||
expect(bookSeries).to.deep.include({ id: bookSeries2Id, bookId: book2Id, seriesId: series2Id })
|
||||
})
|
||||
it('upgrade with one book in two of the same series, both sequence are null', async () => {
|
||||
// Create two different series with the same name in the same library
|
||||
await queryInterface.bulkInsert('Series', [
|
||||
{ id: series1Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() },
|
||||
{ id: series2Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() }
|
||||
])
|
||||
// Create a book that is in both series
|
||||
await queryInterface.bulkInsert('BookSeries', [
|
||||
{ id: bookSeries1Id, bookId: book1Id, seriesId: series1Id },
|
||||
{ id: bookSeries2Id, bookId: book1Id, seriesId: series2Id }
|
||||
])
|
||||
|
||||
await up({ context: { queryInterface, logger: Logger } })
|
||||
|
||||
expect(loggerInfoStub.callCount).to.equal(9)
|
||||
expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.15.0 migration] UPGRADE BEGIN: 2.15.0-series-column-unique '))).to.be.true
|
||||
expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.15.0 migration] Reindexing NOCASE indices to fix potential hidden corruption issues'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.15.0 migration] Found 1 duplicate series'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.15.0 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.15.0 migration] Deduplicating bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.15.0 migration] Finished cleanup of bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.15.0 migration] Deduplication complete'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(7).calledWith(sinon.match('[2.15.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(8).calledWith(sinon.match('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique '))).to.be.true
|
||||
// validate rows
|
||||
const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT })
|
||||
expect(series).to.have.length(1)
|
||||
expect(series).to.deep.include({ id: series1Id, name: 'Series 1', libraryId: library1Id })
|
||||
const bookSeries = await queryInterface.sequelize.query('SELECT "id", "sequence", "bookId", "seriesId" FROM BookSeries', { type: queryInterface.sequelize.QueryTypes.SELECT })
|
||||
expect(bookSeries).to.have.length(1)
|
||||
// Keep BookSeries 2 because it was edited last from cleaning up duplicate books
|
||||
expect(bookSeries).to.deep.include({ id: bookSeries2Id, sequence: null, bookId: book1Id, seriesId: series1Id })
|
||||
})
|
||||
it('upgrade with one book in two of the same series, one sequence is null', async () => {
|
||||
// Create two different series with the same name in the same library
|
||||
await queryInterface.bulkInsert('Series', [
|
||||
{ id: series1Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() },
|
||||
{ id: series2Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() }
|
||||
])
|
||||
// Create a book that is in both series
|
||||
await queryInterface.bulkInsert('BookSeries', [
|
||||
{ id: bookSeries1Id, sequence: '1', bookId: book1Id, seriesId: series1Id },
|
||||
{ id: bookSeries2Id, bookId: book1Id, seriesId: series2Id }
|
||||
])
|
||||
|
||||
await up({ context: { queryInterface, logger: Logger } })
|
||||
|
||||
expect(loggerInfoStub.callCount).to.equal(9)
|
||||
expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.15.0 migration] UPGRADE BEGIN: 2.15.0-series-column-unique '))).to.be.true
|
||||
expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.15.0 migration] Reindexing NOCASE indices to fix potential hidden corruption issues'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.15.0 migration] Found 1 duplicate series'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.15.0 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.15.0 migration] Deduplicating bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.15.0 migration] Finished cleanup of bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.15.0 migration] Deduplication complete'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(7).calledWith(sinon.match('[2.15.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(8).calledWith(sinon.match('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique '))).to.be.true
|
||||
// validate rows
|
||||
const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT })
|
||||
expect(series).to.have.length(1)
|
||||
expect(series).to.deep.include({ id: series1Id, name: 'Series 1', libraryId: library1Id })
|
||||
const bookSeries = await queryInterface.sequelize.query('SELECT "id", "sequence", "bookId", "seriesId" FROM BookSeries', { type: queryInterface.sequelize.QueryTypes.SELECT })
|
||||
expect(bookSeries).to.have.length(1)
|
||||
expect(bookSeries).to.deep.include({ id: bookSeries1Id, sequence: '1', bookId: book1Id, seriesId: series1Id })
|
||||
})
|
||||
it('upgrade with one book in two of the same series, both sequence are not null', async () => {
|
||||
// Create two different series with the same name in the same library
|
||||
await queryInterface.bulkInsert('Series', [
|
||||
{ id: series1Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() },
|
||||
{ id: series2Id, name: 'Series 1', libraryId: library1Id, createdAt: new Date(), updatedAt: new Date() }
|
||||
])
|
||||
// Create a book that is in both series
|
||||
await queryInterface.bulkInsert('BookSeries', [
|
||||
{ id: bookSeries1Id, sequence: '3', bookId: book1Id, seriesId: series1Id },
|
||||
{ id: bookSeries2Id, sequence: '2', bookId: book1Id, seriesId: series2Id }
|
||||
])
|
||||
|
||||
await up({ context: { queryInterface, logger: Logger } })
|
||||
|
||||
expect(loggerInfoStub.callCount).to.equal(9)
|
||||
expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.15.0 migration] UPGRADE BEGIN: 2.15.0-series-column-unique '))).to.be.true
|
||||
expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.15.0 migration] Reindexing NOCASE indices to fix potential hidden corruption issues'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.15.0 migration] Found 1 duplicate series'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.15.0 migration] Deduplicating series "Series 1" in library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.15.0 migration] Deduplicating bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.15.0 migration] Finished cleanup of bookId 4a38b6e5-0ae4-4de4-b119-4e33891bd63f in series "Series 1" of library 3a5a1c7c-a914-472e-88b0-b871ceae63e7'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.15.0 migration] Deduplication complete'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(7).calledWith(sinon.match('[2.15.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(8).calledWith(sinon.match('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique '))).to.be.true
|
||||
// validate rows
|
||||
const series = await queryInterface.sequelize.query('SELECT "id", "name", "libraryId" FROM Series', { type: queryInterface.sequelize.QueryTypes.SELECT })
|
||||
expect(series).to.have.length(1)
|
||||
expect(series).to.deep.include({ id: series1Id, name: 'Series 1', libraryId: library1Id })
|
||||
const bookSeries = await queryInterface.sequelize.query('SELECT "id", "sequence", "bookId", "seriesId" FROM BookSeries', { type: queryInterface.sequelize.QueryTypes.SELECT })
|
||||
expect(bookSeries).to.have.length(1)
|
||||
// Keep BookSeries 2 because it is the lower sequence number
|
||||
expect(bookSeries).to.deep.include({ id: bookSeries2Id, sequence: '2', bookId: book1Id, seriesId: series1Id })
|
||||
})
|
||||
})
|
||||
|
||||
describe('down', () => {
|
||||
beforeEach(async () => {
|
||||
await queryInterface.createTable('Series', {
|
||||
id: { type: Sequelize.UUID, primaryKey: true },
|
||||
name: { type: Sequelize.STRING, allowNull: false },
|
||||
libraryId: { type: Sequelize.UUID, allowNull: false },
|
||||
createdAt: { type: Sequelize.DATE, allowNull: false },
|
||||
updatedAt: { type: Sequelize.DATE, allowNull: false }
|
||||
})
|
||||
// Create a table for BookSeries, with a unique constraint of bookId and seriesId
|
||||
await queryInterface.createTable(
|
||||
'BookSeries',
|
||||
{
|
||||
id: { type: Sequelize.UUID, primaryKey: true },
|
||||
bookId: { type: Sequelize.UUID, allowNull: false },
|
||||
seriesId: { type: Sequelize.UUID, allowNull: false }
|
||||
},
|
||||
{ uniqueKeys: { book_series_unique: { fields: ['bookId', 'seriesId'] } } }
|
||||
)
|
||||
})
|
||||
it('should not have unique constraint on series name and libraryId', async () => {
|
||||
await up({ context: { queryInterface, logger: Logger } })
|
||||
await down({ context: { queryInterface, logger: Logger } })
|
||||
|
||||
expect(loggerInfoStub.callCount).to.equal(9)
|
||||
expect(loggerInfoStub.getCall(0).calledWith(sinon.match('[2.15.0 migration] UPGRADE BEGIN: 2.15.0-series-column-unique '))).to.be.true
|
||||
expect(loggerInfoStub.getCall(1).calledWith(sinon.match('[2.15.0 migration] Reindexing NOCASE indices to fix potential hidden corruption issues'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(2).calledWith(sinon.match('[2.15.0 migration] Found 0 duplicate series'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(3).calledWith(sinon.match('[2.15.0 migration] Deduplication complete'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(4).calledWith(sinon.match('[2.15.0 migration] Added unique index on Series.name and Series.libraryId'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(5).calledWith(sinon.match('[2.15.0 migration] UPGRADE END: 2.15.0-series-column-unique '))).to.be.true
|
||||
expect(loggerInfoStub.getCall(6).calledWith(sinon.match('[2.15.0 migration] DOWNGRADE BEGIN: 2.15.0-series-column-unique '))).to.be.true
|
||||
expect(loggerInfoStub.getCall(7).calledWith(sinon.match('[2.15.0 migration] Removed unique index on Series.name and Series.libraryId'))).to.be.true
|
||||
expect(loggerInfoStub.getCall(8).calledWith(sinon.match('[2.15.0 migration] DOWNGRADE END: 2.15.0-series-column-unique '))).to.be.true
|
||||
// Ensure index does not exist
|
||||
const indexes = await queryInterface.showIndex('Series')
|
||||
expect(indexes).to.not.deep.include({ tableName: 'Series', unique: true, fields: ['name', 'libraryId'], name: 'unique_series_name_per_library' })
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user