mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-05 10:12:44 +02:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 104cadb0b3 | |||
| 6814adffcc | |||
| 20c11e381e | |||
| b5952f16eb | |||
| 5b6878e5de | |||
| 89a25bcf39 | |||
| d0cd512be8 | |||
| 3543dea0fb | |||
| 1949e25ccb | |||
| b715ef3bfc | |||
| 954050df81 | |||
| e4aa7f10fa | |||
| 2afd0e2acd | |||
| 0829237166 | |||
| 541975f038 | |||
| 01bf58ab97 | |||
| d99b2c25e8 | |||
| a31df5ff81 | |||
| 63e5cf2e60 | |||
| 7beca048e7 | |||
| ec998dc1ac | |||
| ddc54c8811 | |||
| 72e306935f | |||
| 96a7c7f4d1 | |||
| 9c65d655b8 | |||
| b108f2241b | |||
| 9439acf300 | |||
| d181e66d83 | |||
| a87c3f2c77 | |||
| 2834f6077e | |||
| 918013ccb3 | |||
| 4c4672c6c1 | |||
| b3991574c7 | |||
| 47b9ee557e |
@@ -10,6 +10,7 @@ FROM sandreas/tone:v0.1.5 AS tone
|
|||||||
FROM node:16-alpine
|
FROM node:16-alpine
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
RUN apk update && \
|
RUN apk update && \
|
||||||
apk add --no-cache --update \
|
apk add --no-cache --update \
|
||||||
curl \
|
curl \
|
||||||
@@ -28,6 +29,8 @@ RUN npm ci --only=production
|
|||||||
|
|
||||||
RUN apk del make python3 g++
|
RUN apk del make python3 g++
|
||||||
|
|
||||||
|
ENV NODE_OPTIONS=--max-old-space-size=8192
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
HEALTHCHECK \
|
HEALTHCHECK \
|
||||||
--interval=30s \
|
--interval=30s \
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ export default {
|
|||||||
},
|
},
|
||||||
async fetchCategories() {
|
async fetchCategories() {
|
||||||
const categories = await this.$axios
|
const categories = await this.$axios
|
||||||
.$get(`/api/libraries/${this.currentLibraryId}/personalized?include=rssfeed`)
|
.$get(`/api/libraries/${this.currentLibraryId}/personalized?include=rssfeed,numEpisodesIncomplete`)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
return data
|
return data
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -315,10 +315,10 @@ export default {
|
|||||||
|
|
||||||
const entityPath = this.entityName === 'series-books' ? 'items' : this.entityName
|
const entityPath = this.entityName === 'series-books' ? 'items' : this.entityName
|
||||||
const sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : ''
|
const sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : ''
|
||||||
const fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1&include=rssfeed`
|
const fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1&include=rssfeed,numEpisodesIncomplete`
|
||||||
|
|
||||||
const payload = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/${entityPath}${fullQueryString}`).catch((error) => {
|
const payload = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/${entityPath}${fullQueryString}`).catch((error) => {
|
||||||
console.error('failed to fetch books', error)
|
console.error('failed to fetch items', error)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -116,9 +116,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Podcast Num Episodes -->
|
<!-- Podcast Num Episodes -->
|
||||||
<div v-else-if="numEpisodes && !isHovering && !isSelectionMode" class="absolute rounded-full bg-black bg-opacity-90 box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', width: 1.25 * sizeMultiplier + 'rem', height: 1.25 * sizeMultiplier + 'rem' }">
|
<div v-else-if="!numEpisodesIncomplete && numEpisodes && !isHovering && !isSelectionMode" class="absolute rounded-full bg-black bg-opacity-90 box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', width: 1.25 * sizeMultiplier + 'rem', height: 1.25 * sizeMultiplier + 'rem' }">
|
||||||
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ numEpisodes }}</p>
|
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ numEpisodes }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Podcast Num Episodes -->
|
||||||
|
<div v-else-if="numEpisodesIncomplete && !isHovering && !isSelectionMode" class="absolute rounded-full bg-yellow-400 text-black font-semibold box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', width: 1.25 * sizeMultiplier + 'rem', height: 1.25 * sizeMultiplier + 'rem' }">
|
||||||
|
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ numEpisodesIncomplete }}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -227,9 +232,11 @@ export default {
|
|||||||
return this.media.numTracks || 0 // toJSONMinified
|
return this.media.numTracks || 0 // toJSONMinified
|
||||||
},
|
},
|
||||||
numEpisodes() {
|
numEpisodes() {
|
||||||
if (!this.isPodcast) return 0
|
|
||||||
return this.media.numEpisodes || 0
|
return this.media.numEpisodes || 0
|
||||||
},
|
},
|
||||||
|
numEpisodesIncomplete() {
|
||||||
|
return this._libraryItem.numEpisodesIncomplete || 0
|
||||||
|
},
|
||||||
processingBatch() {
|
processingBatch() {
|
||||||
return this.store.state.processingBatch
|
return this.store.state.processingBatch
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div v-if="narrators?.length" class="flex py-0.5 mt-4">
|
<div v-if="narrators?.length" class="flex py-0.5 mt-4">
|
||||||
<div class="w-32">
|
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelNarrators }}</span>
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelNarrators }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="max-w-[calc(100vw-10rem)] overflow-hidden overflow-ellipsis">
|
<div class="max-w-[calc(100vw-10rem)] overflow-hidden overflow-ellipsis">
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="publishedYear" class="flex py-0.5">
|
<div v-if="publishedYear" class="flex py-0.5">
|
||||||
<div class="w-32">
|
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPublishYear }}</span>
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPublishYear }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -20,15 +20,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="publisher" class="flex py-0.5">
|
<div v-if="publisher" class="flex py-0.5">
|
||||||
<div class="w-32">
|
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPublisher }}</span>
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPublisher }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{{ publisher }}
|
<nuxt-link :to="`/library/${libraryId}/bookshelf?filter=publishers.${$encode(publisher)}`" class="hover:underline">{{ publisher }}</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="musicAlbum" class="flex py-0.5">
|
<div v-if="musicAlbum" class="flex py-0.5">
|
||||||
<div class="w-32">
|
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">Album</span>
|
<span class="text-white text-opacity-60 uppercase text-sm">Album</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="musicAlbumArtist" class="flex py-0.5">
|
<div v-if="musicAlbumArtist" class="flex py-0.5">
|
||||||
<div class="w-32">
|
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">Album Artist</span>
|
<span class="text-white text-opacity-60 uppercase text-sm">Album Artist</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="musicTrackPretty" class="flex py-0.5">
|
<div v-if="musicTrackPretty" class="flex py-0.5">
|
||||||
<div class="w-32">
|
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">Track</span>
|
<span class="text-white text-opacity-60 uppercase text-sm">Track</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="musicDiscPretty" class="flex py-0.5">
|
<div v-if="musicDiscPretty" class="flex py-0.5">
|
||||||
<div class="w-32">
|
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">Disc</span>
|
<span class="text-white text-opacity-60 uppercase text-sm">Disc</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="podcastType" class="flex py-0.5">
|
<div v-if="podcastType" class="flex py-0.5">
|
||||||
<div class="w-32">
|
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPodcastType }}</span>
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPodcastType }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="capitalize">
|
<div class="capitalize">
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex py-0.5" v-if="genres.length">
|
<div class="flex py-0.5" v-if="genres.length">
|
||||||
<div class="w-32">
|
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelGenres }}</span>
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelGenres }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="max-w-[calc(100vw-10rem)] overflow-hidden overflow-ellipsis">
|
<div class="max-w-[calc(100vw-10rem)] overflow-hidden overflow-ellipsis">
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex py-0.5" v-if="tags.length">
|
<div class="flex py-0.5" v-if="tags.length">
|
||||||
<div class="w-32">
|
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelTags }}</span>
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelTags }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="max-w-[calc(100vw-10rem)] overflow-hidden overflow-ellipsis">
|
<div class="max-w-[calc(100vw-10rem)] overflow-hidden overflow-ellipsis">
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="tracks.length || audioFile || (isPodcast && totalPodcastDuration)" class="flex py-0.5">
|
<div v-if="tracks.length || audioFile || (isPodcast && totalPodcastDuration)" class="flex py-0.5">
|
||||||
<div class="w-32">
|
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelDuration }}</span>
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelDuration }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex py-0.5">
|
<div class="flex py-0.5">
|
||||||
<div class="w-32">
|
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelSize }}</span>
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelSize }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -124,6 +124,11 @@ export default {
|
|||||||
value: 'narrators',
|
value: 'narrators',
|
||||||
sublist: true
|
sublist: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelPublisher,
|
||||||
|
value: 'publishers',
|
||||||
|
sublist: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: this.$strings.LabelLanguage,
|
text: this.$strings.LabelLanguage,
|
||||||
value: 'languages',
|
value: 'languages',
|
||||||
@@ -167,6 +172,11 @@ export default {
|
|||||||
value: 'narrators',
|
value: 'narrators',
|
||||||
sublist: true
|
sublist: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelPublisher,
|
||||||
|
value: 'publishers',
|
||||||
|
sublist: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: this.$strings.LabelLanguage,
|
text: this.$strings.LabelLanguage,
|
||||||
value: 'languages',
|
value: 'languages',
|
||||||
@@ -313,6 +323,9 @@ export default {
|
|||||||
languages() {
|
languages() {
|
||||||
return this.filterData.languages || []
|
return this.filterData.languages || []
|
||||||
},
|
},
|
||||||
|
publishers() {
|
||||||
|
return this.filterData.publishers || []
|
||||||
|
},
|
||||||
progress() {
|
progress() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full overflow-hidden overflow-y-auto px-2 sm:px-4 py-6 relative">
|
<div class="w-full h-full overflow-hidden overflow-y-auto px-2 sm:px-4 py-6 relative">
|
||||||
<div class="flex flex-wrap">
|
<div class="flex flex-wrap mb-4">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId, null, true)" :width="120" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId, libraryItemUpdatedAt, true)" :width="120" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
|
|
||||||
<!-- book cover overlay -->
|
<!-- book cover overlay -->
|
||||||
<div v-if="media.coverPath" class="absolute top-0 left-0 w-full h-full z-10 opacity-0 hover:opacity-100 transition-opacity duration-100">
|
<div v-if="media.coverPath" class="absolute top-0 left-0 w-full h-full z-10 opacity-0 hover:opacity-100 transition-opacity duration-100">
|
||||||
@@ -139,16 +139,19 @@ export default {
|
|||||||
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
libraryItemId() {
|
libraryItemId() {
|
||||||
return this.libraryItem ? this.libraryItem.id : null
|
return this.libraryItem?.id || null
|
||||||
|
},
|
||||||
|
libraryItemUpdatedAt() {
|
||||||
|
return this.libraryItem?.updatedAt || null
|
||||||
},
|
},
|
||||||
mediaType() {
|
mediaType() {
|
||||||
return this.libraryItem ? this.libraryItem.mediaType : null
|
return this.libraryItem?.mediaType || null
|
||||||
},
|
},
|
||||||
isPodcast() {
|
isPodcast() {
|
||||||
return this.mediaType == 'podcast'
|
return this.mediaType == 'podcast'
|
||||||
},
|
},
|
||||||
media() {
|
media() {
|
||||||
return this.libraryItem ? this.libraryItem.media || {} : {}
|
return this.libraryItem?.media || {}
|
||||||
},
|
},
|
||||||
coverPath() {
|
coverPath() {
|
||||||
return this.media.coverPath
|
return this.media.coverPath
|
||||||
@@ -157,7 +160,7 @@ export default {
|
|||||||
return this.media.metadata || {}
|
return this.media.metadata || {}
|
||||||
},
|
},
|
||||||
libraryFiles() {
|
libraryFiles() {
|
||||||
return this.libraryItem ? this.libraryItem.libraryFiles || [] : []
|
return this.libraryItem?.libraryFiles || []
|
||||||
},
|
},
|
||||||
userCanUpload() {
|
userCanUpload() {
|
||||||
return this.$store.getters['user/getUserCanUpload']
|
return this.$store.getters['user/getUserCanUpload']
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ export default {
|
|||||||
selectedEpisodes: [],
|
selectedEpisodes: [],
|
||||||
episodesToRemove: [],
|
episodesToRemove: [],
|
||||||
processing: false,
|
processing: false,
|
||||||
quickMatchingEpisodes: false,
|
|
||||||
search: null,
|
search: null,
|
||||||
searchTimeout: null,
|
searchTimeout: null,
|
||||||
searchText: null
|
searchText: null
|
||||||
@@ -78,6 +77,10 @@ export default {
|
|||||||
{
|
{
|
||||||
text: 'Quick match all episodes',
|
text: 'Quick match all episodes',
|
||||||
action: 'quick-match-episodes'
|
action: 'quick-match-episodes'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.allEpisodesFinished ? this.$strings.MessageMarkAllEpisodesNotFinished : this.$strings.MessageMarkAllEpisodesFinished,
|
||||||
|
action: 'batch-mark-as-finished'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -169,9 +172,15 @@ export default {
|
|||||||
},
|
},
|
||||||
selectedIsFinished() {
|
selectedIsFinished() {
|
||||||
// Find an item that is not finished, if none then all items finished
|
// Find an item that is not finished, if none then all items finished
|
||||||
return !this.selectedEpisodes.find((episode) => {
|
return !this.selectedEpisodes.some((episode) => {
|
||||||
var itemProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItem.id, episode.id)
|
const itemProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItem.id, episode.id)
|
||||||
return !itemProgress || !itemProgress.isFinished
|
return !itemProgress?.isFinished
|
||||||
|
})
|
||||||
|
},
|
||||||
|
allEpisodesFinished() {
|
||||||
|
return !this.episodesSorted.some((episode) => {
|
||||||
|
const itemProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItem.id, episode.id)
|
||||||
|
return !itemProgress?.isFinished
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
dateFormat() {
|
dateFormat() {
|
||||||
@@ -194,17 +203,34 @@ export default {
|
|||||||
},
|
},
|
||||||
contextMenuAction({ action }) {
|
contextMenuAction({ action }) {
|
||||||
if (action === 'quick-match-episodes') {
|
if (action === 'quick-match-episodes') {
|
||||||
if (this.quickMatchingEpisodes) return
|
if (this.processing) return
|
||||||
|
|
||||||
this.quickMatchAllEpisodes()
|
this.quickMatchAllEpisodes()
|
||||||
|
} else if (action === 'batch-mark-as-finished') {
|
||||||
|
if (this.processing) return
|
||||||
|
|
||||||
|
this.markAllEpisodesFinished()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
markAllEpisodesFinished() {
|
||||||
|
const newIsFinished = !this.allEpisodesFinished
|
||||||
|
const payload = {
|
||||||
|
message: newIsFinished ? this.$strings.MessageConfirmMarkAllEpisodesFinished : this.$strings.MessageConfirmMarkAllEpisodesNotFinished,
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.batchUpdateEpisodesFinished(this.episodesSorted, newIsFinished)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
},
|
||||||
quickMatchAllEpisodes() {
|
quickMatchAllEpisodes() {
|
||||||
if (!this.mediaMetadata.feedUrl) {
|
if (!this.mediaMetadata.feedUrl) {
|
||||||
this.$toast.error(this.$strings.MessagePodcastHasNoRSSFeedForMatching)
|
this.$toast.error(this.$strings.MessagePodcastHasNoRSSFeedForMatching)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.quickMatchingEpisodes = true
|
this.processing = true
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
message: 'Quick matching episodes will overwrite details if a match is found. Only unmatched episodes will be updated. Are you sure?',
|
message: 'Quick matching episodes will overwrite details if a match is found. Only unmatched episodes will be updated. Are you sure?',
|
||||||
@@ -224,7 +250,7 @@ export default {
|
|||||||
this.$toast.error('Failed to match episodes')
|
this.$toast.error('Failed to match episodes')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
this.quickMatchingEpisodes = false
|
this.processing = false
|
||||||
},
|
},
|
||||||
type: 'yesNo'
|
type: 'yesNo'
|
||||||
}
|
}
|
||||||
@@ -248,17 +274,19 @@ export default {
|
|||||||
this.$store.commit('addItemToQueue', queueItem)
|
this.$store.commit('addItemToQueue', queueItem)
|
||||||
},
|
},
|
||||||
toggleBatchFinished() {
|
toggleBatchFinished() {
|
||||||
|
this.batchUpdateEpisodesFinished(this.selectedEpisodes, !this.selectedIsFinished)
|
||||||
|
},
|
||||||
|
batchUpdateEpisodesFinished(episodes, newIsFinished) {
|
||||||
this.processing = true
|
this.processing = true
|
||||||
var newIsFinished = !this.selectedIsFinished
|
|
||||||
var updateProgressPayloads = this.selectedEpisodes.map((episode) => {
|
const updateProgressPayloads = episodes.map((episode) => {
|
||||||
return {
|
return {
|
||||||
libraryItemId: this.libraryItem.id,
|
libraryItemId: this.libraryItem.id,
|
||||||
episodeId: episode.id,
|
episodeId: episode.id,
|
||||||
isFinished: newIsFinished
|
isFinished: newIsFinished
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
return this.$axios
|
||||||
this.$axios
|
|
||||||
.patch(`/api/me/progress/batch/update`, updateProgressPayloads)
|
.patch(`/api/me/progress/batch/update`, updateProgressPayloads)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$toast.success(this.$strings.ToastBatchUpdateSuccess)
|
this.$toast.success(this.$strings.ToastBatchUpdateSuccess)
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.23",
|
"version": "2.3.2",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.23",
|
"version": "2.3.2",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/axios": "^5.13.6",
|
"@nuxtjs/axios": "^5.13.6",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.23",
|
"version": "2.3.2",
|
||||||
"description": "Self-hosted audiobook and podcast client",
|
"description": "Self-hosted audiobook and podcast client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -83,8 +83,8 @@ export const getters = {
|
|||||||
getLibraryItemCoverSrc: (state, getters, rootState, rootGetters) => (libraryItem, placeholder = null) => {
|
getLibraryItemCoverSrc: (state, getters, rootState, rootGetters) => (libraryItem, placeholder = null) => {
|
||||||
if (!placeholder) placeholder = `${rootState.routerBasePath}/book_placeholder.jpg`
|
if (!placeholder) placeholder = `${rootState.routerBasePath}/book_placeholder.jpg`
|
||||||
if (!libraryItem) return placeholder
|
if (!libraryItem) return placeholder
|
||||||
var media = libraryItem.media
|
const media = libraryItem.media
|
||||||
if (!media || !media.coverPath || media.coverPath === placeholder) return placeholder
|
if (!media?.coverPath || media.coverPath === placeholder) return placeholder
|
||||||
|
|
||||||
// Absolute URL covers (should no longer be used)
|
// Absolute URL covers (should no longer be used)
|
||||||
if (media.coverPath.startsWith('http:') || media.coverPath.startsWith('https:')) return media.coverPath
|
if (media.coverPath.startsWith('http:') || media.coverPath.startsWith('https:')) return media.coverPath
|
||||||
@@ -99,14 +99,14 @@ export const getters = {
|
|||||||
|
|
||||||
return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}&ts=${lastUpdate}`
|
return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}&ts=${lastUpdate}`
|
||||||
},
|
},
|
||||||
getLibraryItemCoverSrcById: (state, getters, rootState, rootGetters) => (libraryItemId, placeholder = null, raw = false) => {
|
getLibraryItemCoverSrcById: (state, getters, rootState, rootGetters) => (libraryItemId, timestamp = null, raw = false) => {
|
||||||
if (!placeholder) placeholder = `${rootState.routerBasePath}/book_placeholder.jpg`
|
const placeholder = `${rootState.routerBasePath}/book_placeholder.jpg`
|
||||||
if (!libraryItemId) return placeholder
|
if (!libraryItemId) return placeholder
|
||||||
var userToken = rootGetters['user/getToken']
|
const userToken = rootGetters['user/getToken']
|
||||||
if (process.env.NODE_ENV !== 'production') { // Testing
|
if (process.env.NODE_ENV !== 'production') { // Testing
|
||||||
return `http://localhost:3333${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}${raw ? '&raw=1' : ''}`
|
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' : ''}`
|
return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}${raw ? '&raw=1' : ''}${timestamp ? `&ts=${timestamp}` : ''}`
|
||||||
},
|
},
|
||||||
getIsBatchSelectingMediaItems: (state) => {
|
getIsBatchSelectingMediaItems: (state) => {
|
||||||
return state.selectedMediaItems.length
|
return state.selectedMediaItems.length
|
||||||
|
|||||||
+19
-13
@@ -238,21 +238,23 @@ export const mutations = {
|
|||||||
if (!libraryItem || !state.filterData) return
|
if (!libraryItem || !state.filterData) return
|
||||||
if (state.currentLibraryId !== libraryItem.libraryId) return
|
if (state.currentLibraryId !== libraryItem.libraryId) return
|
||||||
/*
|
/*
|
||||||
var data = {
|
structure of filterData:
|
||||||
|
{
|
||||||
authors: [],
|
authors: [],
|
||||||
genres: [],
|
genres: [],
|
||||||
tags: [],
|
tags: [],
|
||||||
series: [],
|
series: [],
|
||||||
narrators: [],
|
narrators: [],
|
||||||
languages: []
|
languages: [],
|
||||||
|
publishers: []
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
var mediaMetadata = libraryItem.media.metadata
|
const mediaMetadata = libraryItem.media.metadata
|
||||||
|
|
||||||
// Add/update book authors
|
// Add/update book authors
|
||||||
if (mediaMetadata.authors && mediaMetadata.authors.length) {
|
if (mediaMetadata.authors?.length) {
|
||||||
mediaMetadata.authors.forEach((author) => {
|
mediaMetadata.authors.forEach((author) => {
|
||||||
var indexOf = state.filterData.authors.findIndex(au => au.id === author.id)
|
const indexOf = state.filterData.authors.findIndex(au => au.id === author.id)
|
||||||
if (indexOf >= 0) {
|
if (indexOf >= 0) {
|
||||||
state.filterData.authors.splice(indexOf, 1, author)
|
state.filterData.authors.splice(indexOf, 1, author)
|
||||||
} else {
|
} else {
|
||||||
@@ -263,9 +265,9 @@ export const mutations = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add/update series
|
// Add/update series
|
||||||
if (mediaMetadata.series && mediaMetadata.series.length) {
|
if (mediaMetadata.series?.length) {
|
||||||
mediaMetadata.series.forEach((series) => {
|
mediaMetadata.series.forEach((series) => {
|
||||||
var indexOf = state.filterData.series.findIndex(se => se.id === series.id)
|
const indexOf = state.filterData.series.findIndex(se => se.id === series.id)
|
||||||
if (indexOf >= 0) {
|
if (indexOf >= 0) {
|
||||||
state.filterData.series.splice(indexOf, 1, { id: series.id, name: series.name })
|
state.filterData.series.splice(indexOf, 1, { id: series.id, name: series.name })
|
||||||
} else {
|
} else {
|
||||||
@@ -276,7 +278,7 @@ export const mutations = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add genres
|
// Add genres
|
||||||
if (mediaMetadata.genres && mediaMetadata.genres.length) {
|
if (mediaMetadata.genres?.length) {
|
||||||
mediaMetadata.genres.forEach((genre) => {
|
mediaMetadata.genres.forEach((genre) => {
|
||||||
if (!state.filterData.genres.includes(genre)) {
|
if (!state.filterData.genres.includes(genre)) {
|
||||||
state.filterData.genres.push(genre)
|
state.filterData.genres.push(genre)
|
||||||
@@ -286,7 +288,7 @@ export const mutations = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add tags
|
// Add tags
|
||||||
if (libraryItem.media.tags && libraryItem.media.tags.length) {
|
if (libraryItem.media.tags?.length) {
|
||||||
libraryItem.media.tags.forEach((tag) => {
|
libraryItem.media.tags.forEach((tag) => {
|
||||||
if (!state.filterData.tags.includes(tag)) {
|
if (!state.filterData.tags.includes(tag)) {
|
||||||
state.filterData.tags.push(tag)
|
state.filterData.tags.push(tag)
|
||||||
@@ -296,7 +298,7 @@ export const mutations = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add narrators
|
// Add narrators
|
||||||
if (mediaMetadata.narrators && mediaMetadata.narrators.length) {
|
if (mediaMetadata.narrators?.length) {
|
||||||
mediaMetadata.narrators.forEach((narrator) => {
|
mediaMetadata.narrators.forEach((narrator) => {
|
||||||
if (!state.filterData.narrators.includes(narrator)) {
|
if (!state.filterData.narrators.includes(narrator)) {
|
||||||
state.filterData.narrators.push(narrator)
|
state.filterData.narrators.push(narrator)
|
||||||
@@ -305,13 +307,17 @@ export const mutations = {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add publishers
|
||||||
|
if (mediaMetadata.publisher && !state.filterData.publishers.includes(mediaMetadata.publisher)) {
|
||||||
|
state.filterData.publishers.push(mediaMetadata.publisher)
|
||||||
|
state.filterData.publishers.sort((a, b) => a.localeCompare(b))
|
||||||
|
}
|
||||||
|
|
||||||
// Add language
|
// Add language
|
||||||
if (mediaMetadata.language) {
|
if (mediaMetadata.language && !state.filterData.languages.includes(mediaMetadata.language)) {
|
||||||
if (!state.filterData.languages.includes(mediaMetadata.language)) {
|
|
||||||
state.filterData.languages.push(mediaMetadata.language)
|
state.filterData.languages.push(mediaMetadata.language)
|
||||||
state.filterData.languages.sort((a, b) => a.localeCompare(b))
|
state.filterData.languages.sort((a, b) => a.localeCompare(b))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
setCollections(state, collections) {
|
setCollections(state, collections) {
|
||||||
state.collections = collections
|
state.collections = collections
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export const actions = {
|
|||||||
if (state.settings.orderBy == 'media.metadata.publishedYear') {
|
if (state.settings.orderBy == 'media.metadata.publishedYear') {
|
||||||
settingsUpdate.orderBy = 'media.metadata.title'
|
settingsUpdate.orderBy = 'media.metadata.title'
|
||||||
}
|
}
|
||||||
const invalidFilters = ['series', 'authors', 'narrators', 'languages', 'progress', 'issues', 'ebooks', 'abridged']
|
const invalidFilters = ['series', 'authors', 'narrators', 'publishers', 'languages', 'progress', 'issues', 'ebooks', 'abridged']
|
||||||
const filterByFirstPart = (state.settings.filterBy || '').split('.').shift()
|
const filterByFirstPart = (state.settings.filterBy || '').split('.').shift()
|
||||||
if (invalidFilters.includes(filterByFirstPart)) {
|
if (invalidFilters.includes(filterByFirstPart)) {
|
||||||
settingsUpdate.filterBy = 'all'
|
settingsUpdate.filterBy = 'all'
|
||||||
|
|||||||
+45
-41
@@ -3,7 +3,7 @@
|
|||||||
"ButtonAddChapters": "Kapitel hinzufügen",
|
"ButtonAddChapters": "Kapitel hinzufügen",
|
||||||
"ButtonAddPodcasts": "Podcasts hinzufügen",
|
"ButtonAddPodcasts": "Podcasts hinzufügen",
|
||||||
"ButtonAddYourFirstLibrary": "Erstelle deine erste Bibliothek",
|
"ButtonAddYourFirstLibrary": "Erstelle deine erste Bibliothek",
|
||||||
"ButtonApply": "Anwenden",
|
"ButtonApply": "Übernehmen",
|
||||||
"ButtonApplyChapters": "Kapitel anwenden",
|
"ButtonApplyChapters": "Kapitel anwenden",
|
||||||
"ButtonAuthors": "Autoren",
|
"ButtonAuthors": "Autoren",
|
||||||
"ButtonBrowseForFolder": "Ordnersuche",
|
"ButtonBrowseForFolder": "Ordnersuche",
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
"ButtonMapChapterTitles": "Kapitelüberschriften zuordnen",
|
"ButtonMapChapterTitles": "Kapitelüberschriften zuordnen",
|
||||||
"ButtonMatchAllAuthors": "Online Metadaten-Abgleich (alle Autoren)",
|
"ButtonMatchAllAuthors": "Online Metadaten-Abgleich (alle Autoren)",
|
||||||
"ButtonMatchBooks": "Online Metadaten-Abgleich (alle Medien)",
|
"ButtonMatchBooks": "Online Metadaten-Abgleich (alle Medien)",
|
||||||
"ButtonNevermind": "Vergiss es",
|
"ButtonNevermind": "Abbrechen",
|
||||||
"ButtonOk": "Ok",
|
"ButtonOk": "Ok",
|
||||||
"ButtonOpenFeed": "Feed öffnen",
|
"ButtonOpenFeed": "Feed öffnen",
|
||||||
"ButtonOpenManager": "Manager öffnen",
|
"ButtonOpenManager": "Manager öffnen",
|
||||||
@@ -98,12 +98,12 @@
|
|||||||
"HeaderCurrentDownloads": "Aktuelle Downloads",
|
"HeaderCurrentDownloads": "Aktuelle Downloads",
|
||||||
"HeaderDetails": "Details",
|
"HeaderDetails": "Details",
|
||||||
"HeaderDownloadQueue": "Download Warteschlange",
|
"HeaderDownloadQueue": "Download Warteschlange",
|
||||||
"HeaderEbookFiles": "Ebook Files",
|
"HeaderEbookFiles": "E-Book Dateien",
|
||||||
"HeaderEmail": "Email",
|
"HeaderEmail": "Email",
|
||||||
"HeaderEmailSettings": "Email Settings",
|
"HeaderEmailSettings": "Email Einstellungen",
|
||||||
"HeaderEpisodes": "Episoden",
|
"HeaderEpisodes": "Episoden",
|
||||||
"HeaderEreaderDevices": "Ereader Devices",
|
"HeaderEreaderDevices": "Ereader Geräte",
|
||||||
"HeaderEreaderSettings": "Ereader Settings",
|
"HeaderEreaderSettings": "Ereader Einstellungen",
|
||||||
"HeaderFiles": "Dateien",
|
"HeaderFiles": "Dateien",
|
||||||
"HeaderFindChapters": "Kapitel suchen",
|
"HeaderFindChapters": "Kapitel suchen",
|
||||||
"HeaderIgnoredFiles": "Ignorierte Dateien",
|
"HeaderIgnoredFiles": "Ignorierte Dateien",
|
||||||
@@ -155,7 +155,7 @@
|
|||||||
"HeaderStatsRecentSessions": "Neueste Ereignisse",
|
"HeaderStatsRecentSessions": "Neueste Ereignisse",
|
||||||
"HeaderStatsTop10Authors": "Top 10 Autoren",
|
"HeaderStatsTop10Authors": "Top 10 Autoren",
|
||||||
"HeaderStatsTop5Genres": "Top 5 Kategorien",
|
"HeaderStatsTop5Genres": "Top 5 Kategorien",
|
||||||
"HeaderTableOfContents": "Table of Contents",
|
"HeaderTableOfContents": "Inhaltsverzeichnis",
|
||||||
"HeaderTools": "Werkzeuge",
|
"HeaderTools": "Werkzeuge",
|
||||||
"HeaderUpdateAccount": "Konto aktualisieren",
|
"HeaderUpdateAccount": "Konto aktualisieren",
|
||||||
"HeaderUpdateAuthor": "Autor aktualisieren",
|
"HeaderUpdateAuthor": "Autor aktualisieren",
|
||||||
@@ -195,7 +195,7 @@
|
|||||||
"LabelBooks": "Bücher",
|
"LabelBooks": "Bücher",
|
||||||
"LabelChangePassword": "Passwort ändern",
|
"LabelChangePassword": "Passwort ändern",
|
||||||
"LabelChannels": "Kanäle",
|
"LabelChannels": "Kanäle",
|
||||||
"LabelChapters": "Chapters",
|
"LabelChapters": "Kapitel",
|
||||||
"LabelChaptersFound": "gefundene Kapitel",
|
"LabelChaptersFound": "gefundene Kapitel",
|
||||||
"LabelChapterTitle": "Kapitelüberschrift",
|
"LabelChapterTitle": "Kapitelüberschrift",
|
||||||
"LabelClosePlayer": "Player schließen",
|
"LabelClosePlayer": "Player schließen",
|
||||||
@@ -205,7 +205,7 @@
|
|||||||
"LabelComplete": "Vollständig",
|
"LabelComplete": "Vollständig",
|
||||||
"LabelConfirmPassword": "Passwort bestätigen",
|
"LabelConfirmPassword": "Passwort bestätigen",
|
||||||
"LabelContinueListening": "Weiterhören",
|
"LabelContinueListening": "Weiterhören",
|
||||||
"LabelContinueReading": "Continue Reading",
|
"LabelContinueReading": "Lesen fortsetzen",
|
||||||
"LabelContinueSeries": "Serien fortsetzen",
|
"LabelContinueSeries": "Serien fortsetzen",
|
||||||
"LabelCover": "Titelbild",
|
"LabelCover": "Titelbild",
|
||||||
"LabelCoverImageURL": "URL des Titelbildes",
|
"LabelCoverImageURL": "URL des Titelbildes",
|
||||||
@@ -226,14 +226,14 @@
|
|||||||
"LabelDownloadNEpisodes": "Download {0} episodes",
|
"LabelDownloadNEpisodes": "Download {0} episodes",
|
||||||
"LabelDuration": "Laufzeit",
|
"LabelDuration": "Laufzeit",
|
||||||
"LabelDurationFound": "Gefundene Laufzeit:",
|
"LabelDurationFound": "Gefundene Laufzeit:",
|
||||||
"LabelEbook": "Ebook",
|
"LabelEbook": "E-Book",
|
||||||
"LabelEbooks": "Ebooks",
|
"LabelEbooks": "E-Books",
|
||||||
"LabelEdit": "Bearbeiten",
|
"LabelEdit": "Bearbeiten",
|
||||||
"LabelEmail": "Email",
|
"LabelEmail": "Email",
|
||||||
"LabelEmailSettingsFromAddress": "From Address",
|
"LabelEmailSettingsFromAddress": "Von Address",
|
||||||
"LabelEmailSettingsSecure": "Secure",
|
"LabelEmailSettingsSecure": "Sicherheit",
|
||||||
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
|
"LabelEmailSettingsSecureHelp": "Wenn \"true\", verwendet die Verbindung TLS, wenn sie eine Verbindung zum Server herstellt. Bei \"false\" wird TLS verwendet, wenn der Server die STARTTLS-Erweiterung unterstützt. In den meisten Fällen setzen Sie diesen Wert auf \"true\", wenn Sie eine Verbindung zu Port 465 herstellen. Für Port 587 oder 25 behalten Sie den Wert \"false\" bei. (von nodemailer.com/smtp/#authentication)",
|
||||||
"LabelEmailSettingsTestAddress": "Test Address",
|
"LabelEmailSettingsTestAddress": "Test Addresse",
|
||||||
"LabelEmbeddedCover": "Eingebettetes Cover",
|
"LabelEmbeddedCover": "Eingebettetes Cover",
|
||||||
"LabelEnable": "Aktivieren",
|
"LabelEnable": "Aktivieren",
|
||||||
"LabelEnd": "Ende",
|
"LabelEnd": "Ende",
|
||||||
@@ -244,7 +244,7 @@
|
|||||||
"LabelExplicit": "Explizit (Altersbeschränkung)",
|
"LabelExplicit": "Explizit (Altersbeschränkung)",
|
||||||
"LabelFeedURL": "Feed URL",
|
"LabelFeedURL": "Feed URL",
|
||||||
"LabelFile": "Datei",
|
"LabelFile": "Datei",
|
||||||
"LabelFileBirthtime": "Datei Geburtsdatum",
|
"LabelFileBirthtime": "Datei erstellt",
|
||||||
"LabelFileModified": "Datei geändert",
|
"LabelFileModified": "Datei geändert",
|
||||||
"LabelFilename": "Dateiname",
|
"LabelFilename": "Dateiname",
|
||||||
"LabelFilterByUser": "Nach Benutzern filtern",
|
"LabelFilterByUser": "Nach Benutzern filtern",
|
||||||
@@ -252,13 +252,13 @@
|
|||||||
"LabelFinished": "beendet",
|
"LabelFinished": "beendet",
|
||||||
"LabelFolder": "Ordner",
|
"LabelFolder": "Ordner",
|
||||||
"LabelFolders": "Verzeichnisse",
|
"LabelFolders": "Verzeichnisse",
|
||||||
"LabelFontScale": "Font scale",
|
"LabelFontScale": "Schriftgröße",
|
||||||
"LabelFormat": "Format",
|
"LabelFormat": "Format",
|
||||||
"LabelGenre": "Kategorie",
|
"LabelGenre": "Kategorie",
|
||||||
"LabelGenres": "Kategorien",
|
"LabelGenres": "Kategorien",
|
||||||
"LabelHardDeleteFile": "Datei dauerhaft löschen",
|
"LabelHardDeleteFile": "Datei dauerhaft löschen",
|
||||||
"LabelHasEbook": "Has ebook",
|
"LabelHasEbook": "mit E-Book",
|
||||||
"LabelHasSupplementaryEbook": "Has supplementary ebook",
|
"LabelHasSupplementaryEbook": "mit zusätlichem E-Book",
|
||||||
"LabelHost": "Host",
|
"LabelHost": "Host",
|
||||||
"LabelHour": "Stunde",
|
"LabelHour": "Stunde",
|
||||||
"LabelIcon": "Symbol",
|
"LabelIcon": "Symbol",
|
||||||
@@ -275,7 +275,7 @@
|
|||||||
"LabelIntervalEveryDay": "Jeden Tag",
|
"LabelIntervalEveryDay": "Jeden Tag",
|
||||||
"LabelIntervalEveryHour": "Jede Stunde",
|
"LabelIntervalEveryHour": "Jede Stunde",
|
||||||
"LabelInvalidParts": "Ungültige Teile",
|
"LabelInvalidParts": "Ungültige Teile",
|
||||||
"LabelInvert": "Invert",
|
"LabelInvert": "Umkehren",
|
||||||
"LabelItem": "Medium",
|
"LabelItem": "Medium",
|
||||||
"LabelLanguage": "Sprache",
|
"LabelLanguage": "Sprache",
|
||||||
"LabelLanguageDefaultServer": "Standard-Server-Sprache",
|
"LabelLanguageDefaultServer": "Standard-Server-Sprache",
|
||||||
@@ -285,15 +285,15 @@
|
|||||||
"LabelLastTime": "Letztes Mal",
|
"LabelLastTime": "Letztes Mal",
|
||||||
"LabelLastUpdate": "Letzte Aktualisierung",
|
"LabelLastUpdate": "Letzte Aktualisierung",
|
||||||
"LabelLayout": "Layout",
|
"LabelLayout": "Layout",
|
||||||
"LabelLayoutSinglePage": "Single page",
|
"LabelLayoutSinglePage": "Eine Seite",
|
||||||
"LabelLayoutSplitPage": "Split page",
|
"LabelLayoutSplitPage": "Geteilte Seite",
|
||||||
"LabelLess": "Weniger",
|
"LabelLess": "Weniger",
|
||||||
"LabelLibrariesAccessibleToUser": "Für Benutzer zugängliche Bibliotheken",
|
"LabelLibrariesAccessibleToUser": "Für Benutzer zugängliche Bibliotheken",
|
||||||
"LabelLibrary": "Bibliothek",
|
"LabelLibrary": "Bibliothek",
|
||||||
"LabelLibraryItem": "Bibliothekseintrag",
|
"LabelLibraryItem": "Bibliothekseintrag",
|
||||||
"LabelLibraryName": "Bibliotheksname",
|
"LabelLibraryName": "Bibliotheksname",
|
||||||
"LabelLimit": "Begrenzung",
|
"LabelLimit": "Begrenzung",
|
||||||
"LabelLineSpacing": "Line spacing",
|
"LabelLineSpacing": "Zeilenabstand",
|
||||||
"LabelListenAgain": "Erneut anhören",
|
"LabelListenAgain": "Erneut anhören",
|
||||||
"LabelLogLevelDebug": "Fehlersuche",
|
"LabelLogLevelDebug": "Fehlersuche",
|
||||||
"LabelLogLevelInfo": "Informationen",
|
"LabelLogLevelInfo": "Informationen",
|
||||||
@@ -308,7 +308,7 @@
|
|||||||
"LabelMissing": "Fehlend",
|
"LabelMissing": "Fehlend",
|
||||||
"LabelMissingParts": "Fehlende Teile",
|
"LabelMissingParts": "Fehlende Teile",
|
||||||
"LabelMore": "Mehr",
|
"LabelMore": "Mehr",
|
||||||
"LabelMoreInfo": "More Info",
|
"LabelMoreInfo": "Mehr Info",
|
||||||
"LabelName": "Name",
|
"LabelName": "Name",
|
||||||
"LabelNarrator": "Erzähler",
|
"LabelNarrator": "Erzähler",
|
||||||
"LabelNarrators": "Erzähler",
|
"LabelNarrators": "Erzähler",
|
||||||
@@ -318,7 +318,7 @@
|
|||||||
"LabelNewPassword": "Neues Passwort",
|
"LabelNewPassword": "Neues Passwort",
|
||||||
"LabelNextBackupDate": "Nächstes Sicherungsdatum",
|
"LabelNextBackupDate": "Nächstes Sicherungsdatum",
|
||||||
"LabelNextScheduledRun": "Nächster planmäßiger Durchlauf",
|
"LabelNextScheduledRun": "Nächster planmäßiger Durchlauf",
|
||||||
"LabelNoEpisodesSelected": "No episodes selected",
|
"LabelNoEpisodesSelected": "Keine Episoden ausgewählt",
|
||||||
"LabelNotes": "Hinweise",
|
"LabelNotes": "Hinweise",
|
||||||
"LabelNotFinished": "nicht beendet",
|
"LabelNotFinished": "nicht beendet",
|
||||||
"LabelNotificationAppriseURL": "Apprise URL(s)",
|
"LabelNotificationAppriseURL": "Apprise URL(s)",
|
||||||
@@ -353,15 +353,15 @@
|
|||||||
"LabelPort": "Port",
|
"LabelPort": "Port",
|
||||||
"LabelPrefixesToIgnore": "Zu ignorierende(s) Vorwort(e) (Groß- und Kleinschreibung wird nicht berücksichtigt)",
|
"LabelPrefixesToIgnore": "Zu ignorierende(s) Vorwort(e) (Groß- und Kleinschreibung wird nicht berücksichtigt)",
|
||||||
"LabelPreventIndexing": "Verhindere, dass dein Feed von iTunes- und Google-Podcast-Verzeichnissen indiziert wird",
|
"LabelPreventIndexing": "Verhindere, dass dein Feed von iTunes- und Google-Podcast-Verzeichnissen indiziert wird",
|
||||||
"LabelPrimaryEbook": "Primary ebook",
|
"LabelPrimaryEbook": "Haupt-E-Book",
|
||||||
"LabelProgress": "Fortschritt",
|
"LabelProgress": "Fortschritt",
|
||||||
"LabelProvider": "Anbieter",
|
"LabelProvider": "Anbieter",
|
||||||
"LabelPubDate": "Veröffentlichungsdatum",
|
"LabelPubDate": "Veröffentlichungsdatum",
|
||||||
"LabelPublisher": "Herausgeber",
|
"LabelPublisher": "Herausgeber",
|
||||||
"LabelPublishYear": "Jahr",
|
"LabelPublishYear": "Jahr",
|
||||||
"LabelRead": "Read",
|
"LabelRead": "Lesen",
|
||||||
"LabelReadAgain": "Read Again",
|
"LabelReadAgain": "Nocheinmal Lesen",
|
||||||
"LabelReadEbookWithoutProgress": "Read ebook without keeping progress",
|
"LabelReadEbookWithoutProgress": "E-Book lesen und Fortschritt verwerfen",
|
||||||
"LabelRecentlyAdded": "Kürzlich hinzugefügt",
|
"LabelRecentlyAdded": "Kürzlich hinzugefügt",
|
||||||
"LabelRecentSeries": "Aktuelle Serien",
|
"LabelRecentSeries": "Aktuelle Serien",
|
||||||
"LabelRecommended": "Empfohlen",
|
"LabelRecommended": "Empfohlen",
|
||||||
@@ -378,17 +378,17 @@
|
|||||||
"LabelSearchTitle": "Titel",
|
"LabelSearchTitle": "Titel",
|
||||||
"LabelSearchTitleOrASIN": "Titel oder ASIN",
|
"LabelSearchTitleOrASIN": "Titel oder ASIN",
|
||||||
"LabelSeason": "Staffel",
|
"LabelSeason": "Staffel",
|
||||||
"LabelSelectAllEpisodes": "Select all episodes",
|
"LabelSelectAllEpisodes": "Alle Episoden auswählen",
|
||||||
"LabelSelectEpisodesShowing": "Select {0} episodes showing",
|
"LabelSelectEpisodesShowing": "{0} ausgewählte Episoden werden angezeigt",
|
||||||
"LabelSendEbookToDevice": "Send Ebook to...",
|
"LabelSendEbookToDevice": "E-Book senden an...",
|
||||||
"LabelSequence": "Reihenfolge",
|
"LabelSequence": "Reihenfolge",
|
||||||
"LabelSeries": "Serien",
|
"LabelSeries": "Serien",
|
||||||
"LabelSeriesName": "Serienname",
|
"LabelSeriesName": "Serienname",
|
||||||
"LabelSeriesProgress": "Serienfortschritt",
|
"LabelSeriesProgress": "Serienfortschritt",
|
||||||
"LabelSetEbookAsPrimary": "Set as primary",
|
"LabelSetEbookAsPrimary": "Setzen als Hauptbuch",
|
||||||
"LabelSetEbookAsSupplementary": "Set as supplementary",
|
"LabelSetEbookAsSupplementary": "Setzen als Ergänzung",
|
||||||
"LabelSettingsAudiobooksOnly": "Audiobooks only",
|
"LabelSettingsAudiobooksOnly": "nur Hörbücher",
|
||||||
"LabelSettingsAudiobooksOnlyHelp": "Enabling this setting will ignore ebook files unless they are inside an audiobook folder in which case they will be set as supplementary ebooks",
|
"LabelSettingsAudiobooksOnlyHelp": "Wenn Sie diese Einstellung aktivieren, werden E-Book-Dateien ignoriert, es sei denn, sie befinden sich in einem Hörbuchordner. In diesem Fall werden sie als zusätzliche E-Books festgelegt",
|
||||||
"LabelSettingsBookshelfViewHelp": "Skeumorphes Design mit Holzeinlegeböden",
|
"LabelSettingsBookshelfViewHelp": "Skeumorphes Design mit Holzeinlegeböden",
|
||||||
"LabelSettingsChromecastSupport": "Chromecastunterstützung",
|
"LabelSettingsChromecastSupport": "Chromecastunterstützung",
|
||||||
"LabelSettingsDateFormat": "Datumsformat",
|
"LabelSettingsDateFormat": "Datumsformat",
|
||||||
@@ -399,8 +399,8 @@
|
|||||||
"LabelSettingsExperimentalFeaturesHelp": "Funktionen welche sich in der Entwicklung befinden, benötigen Ihr Feedback und Ihre Hilfe beim Testen. Klicken Sie hier, um die Github-Diskussion zu öffnen.",
|
"LabelSettingsExperimentalFeaturesHelp": "Funktionen welche sich in der Entwicklung befinden, benötigen Ihr Feedback und Ihre Hilfe beim Testen. Klicken Sie hier, um die Github-Diskussion zu öffnen.",
|
||||||
"LabelSettingsFindCovers": "Suche Titelbilder",
|
"LabelSettingsFindCovers": "Suche Titelbilder",
|
||||||
"LabelSettingsFindCoversHelp": "Wenn Ihr Medium kein eingebettetes Titelbild oder kein Titelbild im Ordner hat, versucht der Scanner, ein Titelbild online zu finden.<br>Hinweis: Dies verlängert die Scandauer",
|
"LabelSettingsFindCoversHelp": "Wenn Ihr Medium kein eingebettetes Titelbild oder kein Titelbild im Ordner hat, versucht der Scanner, ein Titelbild online zu finden.<br>Hinweis: Dies verlängert die Scandauer",
|
||||||
"LabelSettingsHideSingleBookSeries": "Hide single book series",
|
"LabelSettingsHideSingleBookSeries": "Ausblenden einzelzne Bücher",
|
||||||
"LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.",
|
"LabelSettingsHideSingleBookSeriesHelp": "Serien, die ein einzelnes Buch enthalten, werden in den Regalen der Serienseite und der Startseite ausgeblendet.",
|
||||||
"LabelSettingsHomePageBookshelfView": "Startseite verwendet die Bücherregalansicht",
|
"LabelSettingsHomePageBookshelfView": "Startseite verwendet die Bücherregalansicht",
|
||||||
"LabelSettingsLibraryBookshelfView": "Bibliothek verwendet die Bücherregalansicht",
|
"LabelSettingsLibraryBookshelfView": "Bibliothek verwendet die Bücherregalansicht",
|
||||||
"LabelSettingsOverdriveMediaMarkers": "Verwende Overdrive Media Marker für Kapitel",
|
"LabelSettingsOverdriveMediaMarkers": "Verwende Overdrive Media Marker für Kapitel",
|
||||||
@@ -519,6 +519,8 @@
|
|||||||
"MessageConfirmDeleteLibrary": "Sind Sie sicher, dass Sie die Bibliothek \"{0}\" dauerhaft löschen wollen?",
|
"MessageConfirmDeleteLibrary": "Sind Sie sicher, dass Sie die Bibliothek \"{0}\" dauerhaft löschen wollen?",
|
||||||
"MessageConfirmDeleteSession": "Sind Sie sicher, dass Sie diese Sitzung löschen möchten?",
|
"MessageConfirmDeleteSession": "Sind Sie sicher, dass Sie diese Sitzung löschen möchten?",
|
||||||
"MessageConfirmForceReScan": "Sind Sie sicher, dass Sie einen erneuten Scanvorgang erzwingen wollen?",
|
"MessageConfirmForceReScan": "Sind Sie sicher, dass Sie einen erneuten Scanvorgang erzwingen wollen?",
|
||||||
|
"MessageConfirmMarkAllEpisodesFinished": "Sind Sie sicher, dass Sie alle Episoden als abgeschlossen markieren möchten?",
|
||||||
|
"MessageConfirmMarkAllEpisodesNotFinished": "Sind Sie sicher, dass Sie alle Episoden als nicht abgeschlossen markieren möchten?",
|
||||||
"MessageConfirmMarkSeriesFinished": "Sind Sie sicher, dass Sie alle Medien dieser Reihe als abgeschlossen markieren wollen?",
|
"MessageConfirmMarkSeriesFinished": "Sind Sie sicher, dass Sie alle Medien dieser Reihe als abgeschlossen markieren wollen?",
|
||||||
"MessageConfirmMarkSeriesNotFinished": "Sind Sie sicher, dass Sie alle Medien dieser Reihe als nicht abgeschlossen markieren wollen?",
|
"MessageConfirmMarkSeriesNotFinished": "Sind Sie sicher, dass Sie alle Medien dieser Reihe als nicht abgeschlossen markieren wollen?",
|
||||||
"MessageConfirmRemoveAllChapters": "Sind Sie sicher, dass Sie alle Kapitel entfernen möchten?",
|
"MessageConfirmRemoveAllChapters": "Sind Sie sicher, dass Sie alle Kapitel entfernen möchten?",
|
||||||
@@ -533,7 +535,7 @@
|
|||||||
"MessageConfirmRenameTag": "Sind Sie sicher, dass Sie den Tag \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts umbenennen wollen?",
|
"MessageConfirmRenameTag": "Sind Sie sicher, dass Sie den Tag \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts umbenennen wollen?",
|
||||||
"MessageConfirmRenameTagMergeNote": "Hinweis: Tag existiert bereits -> Tags werden zusammengelegt.",
|
"MessageConfirmRenameTagMergeNote": "Hinweis: Tag existiert bereits -> Tags werden zusammengelegt.",
|
||||||
"MessageConfirmRenameTagWarning": "Warnung! Ein ähnlicher Tag mit einem anderen Wortlaut existiert bereits: \"{0}\".",
|
"MessageConfirmRenameTagWarning": "Warnung! Ein ähnlicher Tag mit einem anderen Wortlaut existiert bereits: \"{0}\".",
|
||||||
"MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
|
"MessageConfirmSendEbookToDevice": "Sind Sie sicher, dass sie {0} ebook \"{1}\" auf das Gerät \"{2}\" senden wollen?",
|
||||||
"MessageDownloadingEpisode": "Episode herunterladen",
|
"MessageDownloadingEpisode": "Episode herunterladen",
|
||||||
"MessageDragFilesIntoTrackOrder": "Verschieben Sie die Dateien in die richtige Reihenfolge",
|
"MessageDragFilesIntoTrackOrder": "Verschieben Sie die Dateien in die richtige Reihenfolge",
|
||||||
"MessageEmbedFinished": "Einbettung abgeschlossen!",
|
"MessageEmbedFinished": "Einbettung abgeschlossen!",
|
||||||
@@ -552,6 +554,8 @@
|
|||||||
"MessageM4BFailed": "M4B fehlgeschlagen!",
|
"MessageM4BFailed": "M4B fehlgeschlagen!",
|
||||||
"MessageM4BFinished": "M4B beendet!",
|
"MessageM4BFinished": "M4B beendet!",
|
||||||
"MessageMapChapterTitles": "Zuordnen von Kapiteltiteln zu Ihren vorhandenen Medienkapiteln ohne Anpassung der Zeitangaben",
|
"MessageMapChapterTitles": "Zuordnen von Kapiteltiteln zu Ihren vorhandenen Medienkapiteln ohne Anpassung der Zeitangaben",
|
||||||
|
"MessageMarkAllEpisodesFinished": "Alle Episoden als beendet markieren",
|
||||||
|
"MessageMarkAllEpisodesNotFinished": "Alle Episoden als ungehört markieren",
|
||||||
"MessageMarkAsFinished": "Als beendet markieren",
|
"MessageMarkAsFinished": "Als beendet markieren",
|
||||||
"MessageMarkAsNotFinished": "Als nicht abgeschlossen markieren",
|
"MessageMarkAsNotFinished": "Als nicht abgeschlossen markieren",
|
||||||
"MessageMatchBooksDescription": "versucht, Bücher in der Bibliothek mit einem Buch des ausgewählten Suchanbieters abzugleichen und leere Details und das Titelbild auszufüllen. Details werden nicht überschrieben.",
|
"MessageMatchBooksDescription": "versucht, Bücher in der Bibliothek mit einem Buch des ausgewählten Suchanbieters abzugleichen und leere Details und das Titelbild auszufüllen. Details werden nicht überschrieben.",
|
||||||
@@ -687,8 +691,8 @@
|
|||||||
"ToastRemoveItemFromCollectionSuccess": "Medium aus der Sammlung gelöscht",
|
"ToastRemoveItemFromCollectionSuccess": "Medium aus der Sammlung gelöscht",
|
||||||
"ToastRSSFeedCloseFailed": "RSS-Feed konnte nicht geschlossen werden",
|
"ToastRSSFeedCloseFailed": "RSS-Feed konnte nicht geschlossen werden",
|
||||||
"ToastRSSFeedCloseSuccess": "RSS-Feed geschlossen",
|
"ToastRSSFeedCloseSuccess": "RSS-Feed geschlossen",
|
||||||
"ToastSendEbookToDeviceFailed": "Failed to send ebook to device",
|
"ToastSendEbookToDeviceFailed": "E-Book konnte nicht auf Gerät übertragen werden",
|
||||||
"ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"",
|
"ToastSendEbookToDeviceSuccess": "E-Book an Gerät senden \"{0}\"",
|
||||||
"ToastSeriesUpdateFailed": "Aktualisierung der Serien fehlgeschlagen",
|
"ToastSeriesUpdateFailed": "Aktualisierung der Serien fehlgeschlagen",
|
||||||
"ToastSeriesUpdateSuccess": "Serien aktualisiert",
|
"ToastSeriesUpdateSuccess": "Serien aktualisiert",
|
||||||
"ToastSessionDeleteFailed": "Sitzung konnte nicht gelöscht werden",
|
"ToastSessionDeleteFailed": "Sitzung konnte nicht gelöscht werden",
|
||||||
|
|||||||
@@ -519,6 +519,8 @@
|
|||||||
"MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?",
|
"MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?",
|
||||||
"MessageConfirmDeleteSession": "Are you sure you want to delete this session?",
|
"MessageConfirmDeleteSession": "Are you sure you want to delete this session?",
|
||||||
"MessageConfirmForceReScan": "Are you sure you want to force re-scan?",
|
"MessageConfirmForceReScan": "Are you sure you want to force re-scan?",
|
||||||
|
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
|
||||||
|
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
|
||||||
"MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
|
"MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
|
||||||
"MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
|
"MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
|
||||||
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
|
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
|
||||||
@@ -552,6 +554,8 @@
|
|||||||
"MessageM4BFailed": "M4B Failed!",
|
"MessageM4BFailed": "M4B Failed!",
|
||||||
"MessageM4BFinished": "M4B Finished!",
|
"MessageM4BFinished": "M4B Finished!",
|
||||||
"MessageMapChapterTitles": "Map chapter titles to your existing audiobook chapters without adjusting timestamps",
|
"MessageMapChapterTitles": "Map chapter titles to your existing audiobook chapters without adjusting timestamps",
|
||||||
|
"MessageMarkAllEpisodesFinished": "Mark all episodes finished",
|
||||||
|
"MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
|
||||||
"MessageMarkAsFinished": "Mark as Finished",
|
"MessageMarkAsFinished": "Mark as Finished",
|
||||||
"MessageMarkAsNotFinished": "Mark as Not Finished",
|
"MessageMarkAsNotFinished": "Mark as Not Finished",
|
||||||
"MessageMatchBooksDescription": "will attempt to match books in the library with a book from the selected search provider and fill in empty details and cover art. Does not overwrite details.",
|
"MessageMatchBooksDescription": "will attempt to match books in the library with a book from the selected search provider and fill in empty details and cover art. Does not overwrite details.",
|
||||||
|
|||||||
@@ -519,6 +519,8 @@
|
|||||||
"MessageConfirmDeleteLibrary": "Esta seguro que desea eliminar permanentemente la biblioteca \"{0}\"?",
|
"MessageConfirmDeleteLibrary": "Esta seguro que desea eliminar permanentemente la biblioteca \"{0}\"?",
|
||||||
"MessageConfirmDeleteSession": "Esta seguro que desea eliminar esta session?",
|
"MessageConfirmDeleteSession": "Esta seguro que desea eliminar esta session?",
|
||||||
"MessageConfirmForceReScan": "Esta seguro que desea forzar re-escanear?",
|
"MessageConfirmForceReScan": "Esta seguro que desea forzar re-escanear?",
|
||||||
|
"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?",
|
||||||
"MessageConfirmMarkSeriesFinished": "Esta seguro que desea marcar todos los libros en esta serie como terminados?",
|
"MessageConfirmMarkSeriesFinished": "Esta seguro que desea marcar todos los libros en esta serie como terminados?",
|
||||||
"MessageConfirmMarkSeriesNotFinished": "Esta seguro que desea marcar todos los libros en esta serie como no terminados?",
|
"MessageConfirmMarkSeriesNotFinished": "Esta seguro que desea marcar todos los libros en esta serie como no terminados?",
|
||||||
"MessageConfirmRemoveAllChapters": "Esta seguro que desea remover todos los capitulos?",
|
"MessageConfirmRemoveAllChapters": "Esta seguro que desea remover todos los capitulos?",
|
||||||
@@ -552,6 +554,8 @@
|
|||||||
"MessageM4BFailed": "M4B Fallo!",
|
"MessageM4BFailed": "M4B Fallo!",
|
||||||
"MessageM4BFinished": "M4B Terminado!",
|
"MessageM4BFinished": "M4B Terminado!",
|
||||||
"MessageMapChapterTitles": "Map chapter titles to your existing audiobook chapters without adjusting timestamps",
|
"MessageMapChapterTitles": "Map chapter titles to your existing audiobook chapters without adjusting timestamps",
|
||||||
|
"MessageMarkAllEpisodesFinished": "Mark all episodes finished",
|
||||||
|
"MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
|
||||||
"MessageMarkAsFinished": "Marcar como Terminado",
|
"MessageMarkAsFinished": "Marcar como Terminado",
|
||||||
"MessageMarkAsNotFinished": "Marcar como No Terminado",
|
"MessageMarkAsNotFinished": "Marcar como No Terminado",
|
||||||
"MessageMatchBooksDescription": "intentará hacer coincidir los libros de la biblioteca con un libro del proveedor de búsqueda seleccionado y rellenará los detalles vacíos y la portada. No sobrescribe los detalles.",
|
"MessageMatchBooksDescription": "intentará hacer coincidir los libros de la biblioteca con un libro del proveedor de búsqueda seleccionado y rellenará los detalles vacíos y la portada. No sobrescribe los detalles.",
|
||||||
|
|||||||
+36
-32
@@ -97,13 +97,13 @@
|
|||||||
"HeaderCover": "Couverture",
|
"HeaderCover": "Couverture",
|
||||||
"HeaderCurrentDownloads": "Téléchargements en cours",
|
"HeaderCurrentDownloads": "Téléchargements en cours",
|
||||||
"HeaderDetails": "Détails",
|
"HeaderDetails": "Détails",
|
||||||
"HeaderDownloadQueue": "File d'attente de téléchargements",
|
"HeaderDownloadQueue": "File d’attente de téléchargements",
|
||||||
"HeaderEbookFiles": "Ebook Files",
|
"HeaderEbookFiles": "Fichier des livres numériques",
|
||||||
"HeaderEmail": "E-mails",
|
"HeaderEmail": "Courriels",
|
||||||
"HeaderEmailSettings": "Configuration des e-mails",
|
"HeaderEmailSettings": "Configuration des courriels",
|
||||||
"HeaderEpisodes": "Épisodes",
|
"HeaderEpisodes": "Épisodes",
|
||||||
"HeaderEreaderDevices": "Lecteurs d'e-books",
|
"HeaderEreaderDevices": "Lecteur de livres numériques",
|
||||||
"HeaderEreaderSettings": "Ereader Settings",
|
"HeaderEreaderSettings": "Options Ereader",
|
||||||
"HeaderFiles": "Fichiers",
|
"HeaderFiles": "Fichiers",
|
||||||
"HeaderFindChapters": "Trouver les chapitres",
|
"HeaderFindChapters": "Trouver les chapitres",
|
||||||
"HeaderIgnoredFiles": "Fichiers Ignorés",
|
"HeaderIgnoredFiles": "Fichiers Ignorés",
|
||||||
@@ -223,16 +223,16 @@
|
|||||||
"LabelDiscFromFilename": "Disque depuis le fichier",
|
"LabelDiscFromFilename": "Disque depuis le fichier",
|
||||||
"LabelDiscFromMetadata": "Disque depuis les métadonnées",
|
"LabelDiscFromMetadata": "Disque depuis les métadonnées",
|
||||||
"LabelDownload": "Téléchargement",
|
"LabelDownload": "Téléchargement",
|
||||||
"LabelDownloadNEpisodes": "Download {0} episodes",
|
"LabelDownloadNEpisodes": "Télécharger {0} épisode(s)",
|
||||||
"LabelDuration": "Durée",
|
"LabelDuration": "Durée",
|
||||||
"LabelDurationFound": "Durée trouvée :",
|
"LabelDurationFound": "Durée trouvée :",
|
||||||
"LabelEbook": "E-book",
|
"LabelEbook": "Livre numérique",
|
||||||
"LabelEbooks": "Ebooks",
|
"LabelEbooks": "Livres numériques",
|
||||||
"LabelEdit": "Modifier",
|
"LabelEdit": "Modifier",
|
||||||
"LabelEmail": "E-mail",
|
"LabelEmail": "Courriel",
|
||||||
"LabelEmailSettingsFromAddress": "Expéditeur",
|
"LabelEmailSettingsFromAddress": "Expéditeur",
|
||||||
"LabelEmailSettingsSecure": "Sécurisé",
|
"LabelEmailSettingsSecure": "Sécurisé",
|
||||||
"LabelEmailSettingsSecureHelp": "Si coché, la connexion utilisera TLS lors de la connexion au serveur. Sinon TLS est utilisé si le serveur prend en charge l'extension STARTTLS. Dans la plupart des cas, cochez si vous vous connectez au port 465. Décochez pour le port 587 ou 25. (source: nodemailer.com/smtp/#authentication)",
|
"LabelEmailSettingsSecureHelp": "Si coché, la connexion utilisera TLS lors de la connexion au serveur. Sinon TLS est utilisé si le serveur prend en charge l’extension STARTTLS. Dans la plupart des cas, cochez si vous vous connectez au port 465. Décochez pour le port 587 ou 25. (source: nodemailer.com/smtp/#authentication)",
|
||||||
"LabelEmailSettingsTestAddress": "Test Address",
|
"LabelEmailSettingsTestAddress": "Test Address",
|
||||||
"LabelEmbeddedCover": "Couverture du livre intégrée",
|
"LabelEmbeddedCover": "Couverture du livre intégrée",
|
||||||
"LabelEnable": "Activer",
|
"LabelEnable": "Activer",
|
||||||
@@ -257,8 +257,8 @@
|
|||||||
"LabelGenre": "Genre",
|
"LabelGenre": "Genre",
|
||||||
"LabelGenres": "Genres",
|
"LabelGenres": "Genres",
|
||||||
"LabelHardDeleteFile": "Suppression du fichier",
|
"LabelHardDeleteFile": "Suppression du fichier",
|
||||||
"LabelHasEbook": "Has ebook",
|
"LabelHasEbook": "Dispose d’un livre numérique",
|
||||||
"LabelHasSupplementaryEbook": "Has supplementary ebook",
|
"LabelHasSupplementaryEbook": "Dispose d’un livre numérique supplémentaire",
|
||||||
"LabelHost": "Hôte",
|
"LabelHost": "Hôte",
|
||||||
"LabelHour": "Heure",
|
"LabelHour": "Heure",
|
||||||
"LabelIcon": "Icone",
|
"LabelIcon": "Icone",
|
||||||
@@ -353,22 +353,22 @@
|
|||||||
"LabelPort": "Port",
|
"LabelPort": "Port",
|
||||||
"LabelPrefixesToIgnore": "Préfixes à Ignorer (Insensible à la Casse)",
|
"LabelPrefixesToIgnore": "Préfixes à Ignorer (Insensible à la Casse)",
|
||||||
"LabelPreventIndexing": "Empêcher l’indexation de votre flux par les bases de données iTunes et Google podcast",
|
"LabelPreventIndexing": "Empêcher l’indexation de votre flux par les bases de données iTunes et Google podcast",
|
||||||
"LabelPrimaryEbook": "Primary ebook",
|
"LabelPrimaryEbook": "Premier livre numérique",
|
||||||
"LabelProgress": "Progression",
|
"LabelProgress": "Progression",
|
||||||
"LabelProvider": "Fournisseur",
|
"LabelProvider": "Fournisseur",
|
||||||
"LabelPubDate": "Date de publication",
|
"LabelPubDate": "Date de publication",
|
||||||
"LabelPublisher": "Éditeur",
|
"LabelPublisher": "Éditeur",
|
||||||
"LabelPublishYear": "Année d’édition",
|
"LabelPublishYear": "Année d’édition",
|
||||||
"LabelRead": "Read",
|
"LabelRead": "Lire",
|
||||||
"LabelReadAgain": "Read Again",
|
"LabelReadAgain": "Lire à nouveau",
|
||||||
"LabelReadEbookWithoutProgress": "Read ebook without keeping progress",
|
"LabelReadEbookWithoutProgress": "Lire le livre numérique sans sauvegarder la progression",
|
||||||
"LabelRecentlyAdded": "Derniers ajouts",
|
"LabelRecentlyAdded": "Derniers ajouts",
|
||||||
"LabelRecentSeries": "Séries récentes",
|
"LabelRecentSeries": "Séries récentes",
|
||||||
"LabelRecommended": "Recommandé",
|
"LabelRecommended": "Recommandé",
|
||||||
"LabelRegion": "Région",
|
"LabelRegion": "Région",
|
||||||
"LabelReleaseDate": "Date de parution",
|
"LabelReleaseDate": "Date de parution",
|
||||||
"LabelRemoveCover": "Supprimer la couverture",
|
"LabelRemoveCover": "Supprimer la couverture",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "E-mail propriétaire personnalisé",
|
"LabelRSSFeedCustomOwnerEmail": "Courriel du propriétaire personnalisé",
|
||||||
"LabelRSSFeedCustomOwnerName": "Nom propriétaire personnalisé",
|
"LabelRSSFeedCustomOwnerName": "Nom propriétaire personnalisé",
|
||||||
"LabelRSSFeedOpen": "Flux RSS ouvert",
|
"LabelRSSFeedOpen": "Flux RSS ouvert",
|
||||||
"LabelRSSFeedPreventIndexing": "Empêcher l’indexation",
|
"LabelRSSFeedPreventIndexing": "Empêcher l’indexation",
|
||||||
@@ -379,16 +379,16 @@
|
|||||||
"LabelSearchTitleOrASIN": "Recherche du titre ou ASIN",
|
"LabelSearchTitleOrASIN": "Recherche du titre ou ASIN",
|
||||||
"LabelSeason": "Saison",
|
"LabelSeason": "Saison",
|
||||||
"LabelSelectAllEpisodes": "Select all episodes",
|
"LabelSelectAllEpisodes": "Select all episodes",
|
||||||
"LabelSelectEpisodesShowing": "Select {0} episodes showing",
|
"LabelSelectEpisodesShowing": "Sélectionner {0} episode(s) en cours",
|
||||||
"LabelSendEbookToDevice": "Envoyer l'e-book à...",
|
"LabelSendEbookToDevice": "Envoyer le livre numérique à...",
|
||||||
"LabelSequence": "Séquence",
|
"LabelSequence": "Séquence",
|
||||||
"LabelSeries": "Séries",
|
"LabelSeries": "Séries",
|
||||||
"LabelSeriesName": "Nom de la série",
|
"LabelSeriesName": "Nom de la série",
|
||||||
"LabelSeriesProgress": "Progression de séries",
|
"LabelSeriesProgress": "Progression de séries",
|
||||||
"LabelSetEbookAsPrimary": "Set as primary",
|
"LabelSetEbookAsPrimary": "Définir comme principale",
|
||||||
"LabelSetEbookAsSupplementary": "Set as supplementary",
|
"LabelSetEbookAsSupplementary": "Définir comme supplémentaire",
|
||||||
"LabelSettingsAudiobooksOnly": "Audiobooks only",
|
"LabelSettingsAudiobooksOnly": "Livres audios seulement",
|
||||||
"LabelSettingsAudiobooksOnlyHelp": "Enabling this setting will ignore ebook files unless they are inside an audiobook folder in which case they will be set as supplementary ebooks",
|
"LabelSettingsAudiobooksOnlyHelp": "L’activation de ce paramètre ignorera les fichiers “ ebook ”, à moins qu’ils ne se trouvent dans un dossier de livres audio, auquel cas ils seront définis comme des livres numériques supplémentaires.",
|
||||||
"LabelSettingsBookshelfViewHelp": "Interface skeuomorphique avec une étagère en bois",
|
"LabelSettingsBookshelfViewHelp": "Interface skeuomorphique avec une étagère en bois",
|
||||||
"LabelSettingsChromecastSupport": "Support du Chromecast",
|
"LabelSettingsChromecastSupport": "Support du Chromecast",
|
||||||
"LabelSettingsDateFormat": "Format de date",
|
"LabelSettingsDateFormat": "Format de date",
|
||||||
@@ -399,8 +399,8 @@
|
|||||||
"LabelSettingsExperimentalFeaturesHelp": "Fonctionnalités en cours de développement sur lesquelles nous attendons votre retour et expérience. Cliquez pour ouvrir la discussion Github.",
|
"LabelSettingsExperimentalFeaturesHelp": "Fonctionnalités en cours de développement sur lesquelles nous attendons votre retour et expérience. Cliquez pour ouvrir la discussion Github.",
|
||||||
"LabelSettingsFindCovers": "Chercher des couvertures de livre",
|
"LabelSettingsFindCovers": "Chercher des couvertures de livre",
|
||||||
"LabelSettingsFindCoversHelp": "Si votre livre audio ne possède pas de couverture intégrée ou une image de couverture dans le dossier, l’analyseur tentera de récupérer une couverture.<br>Attention, cela peut augmenter le temps d’analyse.",
|
"LabelSettingsFindCoversHelp": "Si votre livre audio ne possède pas de couverture intégrée ou une image de couverture dans le dossier, l’analyseur tentera de récupérer une couverture.<br>Attention, cela peut augmenter le temps d’analyse.",
|
||||||
"LabelSettingsHideSingleBookSeries": "Hide single book series",
|
"LabelSettingsHideSingleBookSeries": "Masquer les séries de livres uniques",
|
||||||
"LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.",
|
"LabelSettingsHideSingleBookSeriesHelp": "Les séries qui ne comportent qu’un seul livre seront masquées sur la page de la série et sur les étagères de la page d’accueil.",
|
||||||
"LabelSettingsHomePageBookshelfView": "La page d’accueil utilise la vue étagère",
|
"LabelSettingsHomePageBookshelfView": "La page d’accueil utilise la vue étagère",
|
||||||
"LabelSettingsLibraryBookshelfView": "La bibliothèque utilise la vue étagère",
|
"LabelSettingsLibraryBookshelfView": "La bibliothèque utilise la vue étagère",
|
||||||
"LabelSettingsOverdriveMediaMarkers": "Utiliser Overdrive Media Marker pour les chapitres",
|
"LabelSettingsOverdriveMediaMarkers": "Utiliser Overdrive Media Marker pour les chapitres",
|
||||||
@@ -512,13 +512,15 @@
|
|||||||
"MessageChapterErrorFirstNotZero": "Le premier capitre doit débuter à 0",
|
"MessageChapterErrorFirstNotZero": "Le premier capitre doit débuter à 0",
|
||||||
"MessageChapterErrorStartGteDuration": "Horodatage invalide car il doit débuter avant la fin du livre",
|
"MessageChapterErrorStartGteDuration": "Horodatage invalide car il doit débuter avant la fin du livre",
|
||||||
"MessageChapterErrorStartLtPrev": "Horodatage invalide car il doit débuter au moins après le précédent chapitre",
|
"MessageChapterErrorStartLtPrev": "Horodatage invalide car il doit débuter au moins après le précédent chapitre",
|
||||||
"MessageChapterStartIsAfter": "Le Chapitre Début est situé au début de votre Livre Audio",
|
"MessageChapterStartIsAfter": "Le premier chapitre est situé au début de votre livre audio",
|
||||||
"MessageCheckingCron": "Vérification du cron…",
|
"MessageCheckingCron": "Vérification du cron…",
|
||||||
"MessageConfirmDeleteBackup": "Êtes-vous sûr de vouloir supprimer la Sauvegarde de {0} ?",
|
"MessageConfirmDeleteBackup": "Êtes-vous sûr de vouloir supprimer la Sauvegarde de {0} ?",
|
||||||
"MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?",
|
"MessageConfirmDeleteFile": "Cela Le fichier sera supprimer de votre système. Êtes-vous sûr ?",
|
||||||
"MessageConfirmDeleteLibrary": "Êtes-vous sûr de vouloir supprimer définitivement la bibliothèque « {0} » ?",
|
"MessageConfirmDeleteLibrary": "Êtes-vous sûr de vouloir supprimer définitivement la bibliothèque « {0} » ?",
|
||||||
"MessageConfirmDeleteSession": "Êtes-vous sûr de vouloir supprimer cette session ?",
|
"MessageConfirmDeleteSession": "Êtes-vous sûr de vouloir supprimer cette session ?",
|
||||||
"MessageConfirmForceReScan": "Êtes-vous sûr de vouloir lancer une Analyse Forcée ?",
|
"MessageConfirmForceReScan": "Êtes-vous sûr de vouloir lancer une Analyse Forcée ?",
|
||||||
|
"MessageConfirmMarkAllEpisodesFinished": "Êtes-vous sûr de marquer tous les épisodes comme terminés ?",
|
||||||
|
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
|
||||||
"MessageConfirmMarkSeriesFinished": "Êtes-vous sûr de vouloir marquer comme terminé tous les livres de cette série ?",
|
"MessageConfirmMarkSeriesFinished": "Êtes-vous sûr de vouloir marquer comme terminé tous les livres de cette série ?",
|
||||||
"MessageConfirmMarkSeriesNotFinished": "Êtes-vous sûr de vouloir marquer comme non terminé tous les livres de cette série ?",
|
"MessageConfirmMarkSeriesNotFinished": "Êtes-vous sûr de vouloir marquer comme non terminé tous les livres de cette série ?",
|
||||||
"MessageConfirmRemoveAllChapters": "Êtes-vous sûr de vouloir supprimer tous les chapitres ?",
|
"MessageConfirmRemoveAllChapters": "Êtes-vous sûr de vouloir supprimer tous les chapitres ?",
|
||||||
@@ -533,7 +535,7 @@
|
|||||||
"MessageConfirmRenameTag": "Êtes-vous sûr de vouloir renommer l’étiquette « {0} » en « {1} » pour tous les articles ?",
|
"MessageConfirmRenameTag": "Êtes-vous sûr de vouloir renommer l’étiquette « {0} » en « {1} » pour tous les articles ?",
|
||||||
"MessageConfirmRenameTagMergeNote": "Information: Cette étiquette existe déjà et sera fusionnée.",
|
"MessageConfirmRenameTagMergeNote": "Information: Cette étiquette existe déjà et sera fusionnée.",
|
||||||
"MessageConfirmRenameTagWarning": "Attention ! Une étiquette similaire avec une casse différente existe déjà « {0} ».",
|
"MessageConfirmRenameTagWarning": "Attention ! Une étiquette similaire avec une casse différente existe déjà « {0} ».",
|
||||||
"MessageConfirmSendEbookToDevice": "Êtes-vous sûr de vouloir envoyer l'ebook {0} \"{1}\" à l'appareil \"{2}\"?",
|
"MessageConfirmSendEbookToDevice": "Êtes-vous sûr de vouloir envoyer le livre numérique {0} \"{1}\" à l’appareil \"{2}\"?",
|
||||||
"MessageDownloadingEpisode": "Téléchargement de l’épisode",
|
"MessageDownloadingEpisode": "Téléchargement de l’épisode",
|
||||||
"MessageDragFilesIntoTrackOrder": "Faire glisser les fichiers dans l’ordre correct",
|
"MessageDragFilesIntoTrackOrder": "Faire glisser les fichiers dans l’ordre correct",
|
||||||
"MessageEmbedFinished": "Intégration Terminée !",
|
"MessageEmbedFinished": "Intégration Terminée !",
|
||||||
@@ -552,8 +554,10 @@
|
|||||||
"MessageM4BFailed": "M4B en échec !",
|
"MessageM4BFailed": "M4B en échec !",
|
||||||
"MessageM4BFinished": "M4B terminé !",
|
"MessageM4BFinished": "M4B terminé !",
|
||||||
"MessageMapChapterTitles": "Faire correspondre les titres des chapitres aux chapitres existants de votre livre audio sans ajuster l’horodatage.",
|
"MessageMapChapterTitles": "Faire correspondre les titres des chapitres aux chapitres existants de votre livre audio sans ajuster l’horodatage.",
|
||||||
|
"MessageMarkAllEpisodesFinished": "Marquer tous les épisodes terminés",
|
||||||
|
"MessageMarkAllEpisodesNotFinished": "Marquer tous les épisodes non terminés",
|
||||||
"MessageMarkAsFinished": "Marquer comme terminé",
|
"MessageMarkAsFinished": "Marquer comme terminé",
|
||||||
"MessageMarkAsNotFinished": "Marquer comme non Terminé",
|
"MessageMarkAsNotFinished": "Marquer comme non terminé",
|
||||||
"MessageMatchBooksDescription": "tentera de faire correspondre les livres de la bibliothèque avec les livres du fournisseur sélectionné pour combler les détails et couverture manquants. N’écrase pas les données existantes.",
|
"MessageMatchBooksDescription": "tentera de faire correspondre les livres de la bibliothèque avec les livres du fournisseur sélectionné pour combler les détails et couverture manquants. N’écrase pas les données existantes.",
|
||||||
"MessageNoAudioTracks": "Aucune piste audio",
|
"MessageNoAudioTracks": "Aucune piste audio",
|
||||||
"MessageNoAuthors": "Aucun auteur",
|
"MessageNoAuthors": "Aucun auteur",
|
||||||
@@ -687,8 +691,8 @@
|
|||||||
"ToastRemoveItemFromCollectionSuccess": "Article supprimé de la collection",
|
"ToastRemoveItemFromCollectionSuccess": "Article supprimé de la collection",
|
||||||
"ToastRSSFeedCloseFailed": "Échec de la fermeture du flux RSS",
|
"ToastRSSFeedCloseFailed": "Échec de la fermeture du flux RSS",
|
||||||
"ToastRSSFeedCloseSuccess": "Flux RSS fermé",
|
"ToastRSSFeedCloseSuccess": "Flux RSS fermé",
|
||||||
"ToastSendEbookToDeviceFailed": "Échec de l'envoi de l'e-book à l'appareil",
|
"ToastSendEbookToDeviceFailed": "Échec de l’envoi du livre numérique à l’appareil",
|
||||||
"ToastSendEbookToDeviceSuccess": "E-book envoyé à l'appareil \"{0}\"",
|
"ToastSendEbookToDeviceSuccess": "Livre numérique envoyé à l’appareil : {0}",
|
||||||
"ToastSeriesUpdateFailed": "Échec de la mise à jour de la série",
|
"ToastSeriesUpdateFailed": "Échec de la mise à jour de la série",
|
||||||
"ToastSeriesUpdateSuccess": "Mise à jour de la série réussie",
|
"ToastSeriesUpdateSuccess": "Mise à jour de la série réussie",
|
||||||
"ToastSessionDeleteFailed": "Échec de la suppression de session",
|
"ToastSessionDeleteFailed": "Échec de la suppression de session",
|
||||||
|
|||||||
@@ -519,6 +519,8 @@
|
|||||||
"MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?",
|
"MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?",
|
||||||
"MessageConfirmDeleteSession": "Are you sure you want to delete this session?",
|
"MessageConfirmDeleteSession": "Are you sure you want to delete this session?",
|
||||||
"MessageConfirmForceReScan": "Are you sure you want to force re-scan?",
|
"MessageConfirmForceReScan": "Are you sure you want to force re-scan?",
|
||||||
|
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
|
||||||
|
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
|
||||||
"MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
|
"MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
|
||||||
"MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
|
"MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
|
||||||
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
|
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
|
||||||
@@ -552,6 +554,8 @@
|
|||||||
"MessageM4BFailed": "M4B Failed!",
|
"MessageM4BFailed": "M4B Failed!",
|
||||||
"MessageM4BFinished": "M4B Finished!",
|
"MessageM4BFinished": "M4B Finished!",
|
||||||
"MessageMapChapterTitles": "Map chapter titles to your existing audiobook chapters without adjusting timestamps",
|
"MessageMapChapterTitles": "Map chapter titles to your existing audiobook chapters without adjusting timestamps",
|
||||||
|
"MessageMarkAllEpisodesFinished": "Mark all episodes finished",
|
||||||
|
"MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
|
||||||
"MessageMarkAsFinished": "Mark as Finished",
|
"MessageMarkAsFinished": "Mark as Finished",
|
||||||
"MessageMarkAsNotFinished": "Mark as Not Finished",
|
"MessageMarkAsNotFinished": "Mark as Not Finished",
|
||||||
"MessageMatchBooksDescription": "will attempt to match books in the library with a book from the selected search provider and fill in empty details and cover art. Does not overwrite details.",
|
"MessageMatchBooksDescription": "will attempt to match books in the library with a book from the selected search provider and fill in empty details and cover art. Does not overwrite details.",
|
||||||
|
|||||||
@@ -519,6 +519,8 @@
|
|||||||
"MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?",
|
"MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?",
|
||||||
"MessageConfirmDeleteSession": "Are you sure you want to delete this session?",
|
"MessageConfirmDeleteSession": "Are you sure you want to delete this session?",
|
||||||
"MessageConfirmForceReScan": "Are you sure you want to force re-scan?",
|
"MessageConfirmForceReScan": "Are you sure you want to force re-scan?",
|
||||||
|
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
|
||||||
|
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
|
||||||
"MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
|
"MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
|
||||||
"MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
|
"MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
|
||||||
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
|
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
|
||||||
@@ -552,6 +554,8 @@
|
|||||||
"MessageM4BFailed": "M4B Failed!",
|
"MessageM4BFailed": "M4B Failed!",
|
||||||
"MessageM4BFinished": "M4B Finished!",
|
"MessageM4BFinished": "M4B Finished!",
|
||||||
"MessageMapChapterTitles": "Map chapter titles to your existing audiobook chapters without adjusting timestamps",
|
"MessageMapChapterTitles": "Map chapter titles to your existing audiobook chapters without adjusting timestamps",
|
||||||
|
"MessageMarkAllEpisodesFinished": "Mark all episodes finished",
|
||||||
|
"MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
|
||||||
"MessageMarkAsFinished": "Mark as Finished",
|
"MessageMarkAsFinished": "Mark as Finished",
|
||||||
"MessageMarkAsNotFinished": "Mark as Not Finished",
|
"MessageMarkAsNotFinished": "Mark as Not Finished",
|
||||||
"MessageMatchBooksDescription": "will attempt to match books in the library with a book from the selected search provider and fill in empty details and cover art. Does not overwrite details.",
|
"MessageMatchBooksDescription": "will attempt to match books in the library with a book from the selected search provider and fill in empty details and cover art. Does not overwrite details.",
|
||||||
|
|||||||
@@ -519,6 +519,8 @@
|
|||||||
"MessageConfirmDeleteLibrary": "Jeste li sigurni da želite trajno obrisati biblioteku \"{0}\"?",
|
"MessageConfirmDeleteLibrary": "Jeste li sigurni da želite trajno obrisati biblioteku \"{0}\"?",
|
||||||
"MessageConfirmDeleteSession": "Jeste li sigurni da želite obrisati ovu sesiju?",
|
"MessageConfirmDeleteSession": "Jeste li sigurni da želite obrisati ovu sesiju?",
|
||||||
"MessageConfirmForceReScan": "Jeste li sigurni da želite ponovno skenirati?",
|
"MessageConfirmForceReScan": "Jeste li sigurni da želite ponovno skenirati?",
|
||||||
|
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
|
||||||
|
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
|
||||||
"MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
|
"MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
|
||||||
"MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
|
"MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
|
||||||
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
|
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
|
||||||
@@ -552,6 +554,8 @@
|
|||||||
"MessageM4BFailed": "M4B neuspješan!",
|
"MessageM4BFailed": "M4B neuspješan!",
|
||||||
"MessageM4BFinished": "M4B završio!",
|
"MessageM4BFinished": "M4B završio!",
|
||||||
"MessageMapChapterTitles": "Mapiraj imena poglavlja u postoječa poglavlja bez izmijene timestampova.",
|
"MessageMapChapterTitles": "Mapiraj imena poglavlja u postoječa poglavlja bez izmijene timestampova.",
|
||||||
|
"MessageMarkAllEpisodesFinished": "Mark all episodes finished",
|
||||||
|
"MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
|
||||||
"MessageMarkAsFinished": "Označi kao završeno",
|
"MessageMarkAsFinished": "Označi kao završeno",
|
||||||
"MessageMarkAsNotFinished": "Označi kao nezavršeno",
|
"MessageMarkAsNotFinished": "Označi kao nezavršeno",
|
||||||
"MessageMatchBooksDescription": "će probati matchati knjige iz biblioteke sa knjigom od odabranog poslužitelja i popuniti prazne detalje i cover. Ne briše postojeće detalje.",
|
"MessageMatchBooksDescription": "će probati matchati knjige iz biblioteke sa knjigom od odabranog poslužitelja i popuniti prazne detalje i cover. Ne briše postojeće detalje.",
|
||||||
|
|||||||
+54
-50
@@ -55,7 +55,7 @@
|
|||||||
"ButtonRemoveAll": "Rimuovi Tutto",
|
"ButtonRemoveAll": "Rimuovi Tutto",
|
||||||
"ButtonRemoveAllLibraryItems": "Rimuovi tutto il contenuto della libreria",
|
"ButtonRemoveAllLibraryItems": "Rimuovi tutto il contenuto della libreria",
|
||||||
"ButtonRemoveFromContinueListening": "Rimuovi per proseguire l'ascolto",
|
"ButtonRemoveFromContinueListening": "Rimuovi per proseguire l'ascolto",
|
||||||
"ButtonRemoveFromContinueReading": "Remove from Continue Reading",
|
"ButtonRemoveFromContinueReading": "Rimuovi per proseguire la lettura",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "Rimuovi la Serie per Continuarla",
|
"ButtonRemoveSeriesFromContinueSeries": "Rimuovi la Serie per Continuarla",
|
||||||
"ButtonReScan": "Ri-scansiona",
|
"ButtonReScan": "Ri-scansiona",
|
||||||
"ButtonReset": "Reset",
|
"ButtonReset": "Reset",
|
||||||
@@ -95,15 +95,15 @@
|
|||||||
"HeaderCollection": "Raccolta",
|
"HeaderCollection": "Raccolta",
|
||||||
"HeaderCollectionItems": "Elementi della Raccolta",
|
"HeaderCollectionItems": "Elementi della Raccolta",
|
||||||
"HeaderCover": "Cover",
|
"HeaderCover": "Cover",
|
||||||
"HeaderCurrentDownloads": "Current Downloads",
|
"HeaderCurrentDownloads": "Download Correnti",
|
||||||
"HeaderDetails": "Dettagli",
|
"HeaderDetails": "Dettagli",
|
||||||
"HeaderDownloadQueue": "Download Queue",
|
"HeaderDownloadQueue": "Download Queue",
|
||||||
"HeaderEbookFiles": "Ebook Files",
|
"HeaderEbookFiles": "Ebook Files",
|
||||||
"HeaderEmail": "Email",
|
"HeaderEmail": "Email",
|
||||||
"HeaderEmailSettings": "Email Settings",
|
"HeaderEmailSettings": "Email Settings",
|
||||||
"HeaderEpisodes": "Episodi",
|
"HeaderEpisodes": "Episodi",
|
||||||
"HeaderEreaderDevices": "Ereader Devices",
|
"HeaderEreaderDevices": "Dispositivo Ereader",
|
||||||
"HeaderEreaderSettings": "Ereader Settings",
|
"HeaderEreaderSettings": "Impostazioni Ereader",
|
||||||
"HeaderFiles": "File",
|
"HeaderFiles": "File",
|
||||||
"HeaderFindChapters": "Trova Capitoli",
|
"HeaderFindChapters": "Trova Capitoli",
|
||||||
"HeaderIgnoredFiles": "File Ignorati",
|
"HeaderIgnoredFiles": "File Ignorati",
|
||||||
@@ -149,13 +149,13 @@
|
|||||||
"HeaderSettingsGeneral": "Generale",
|
"HeaderSettingsGeneral": "Generale",
|
||||||
"HeaderSettingsScanner": "Scanner",
|
"HeaderSettingsScanner": "Scanner",
|
||||||
"HeaderSleepTimer": "Sveglia",
|
"HeaderSleepTimer": "Sveglia",
|
||||||
"HeaderStatsLargestItems": "Largest Items",
|
"HeaderStatsLargestItems": "Oggetti Grandi",
|
||||||
"HeaderStatsLongestItems": "libri più lunghi (ore)",
|
"HeaderStatsLongestItems": "libri più lunghi (ore)",
|
||||||
"HeaderStatsMinutesListeningChart": "Minuti ascoltati (Ultimi 7 Giorni)",
|
"HeaderStatsMinutesListeningChart": "Minuti ascoltati (Ultimi 7 Giorni)",
|
||||||
"HeaderStatsRecentSessions": "Sessioni Recenti",
|
"HeaderStatsRecentSessions": "Sessioni Recenti",
|
||||||
"HeaderStatsTop10Authors": "Top 10 Autori",
|
"HeaderStatsTop10Authors": "Top 10 Autori",
|
||||||
"HeaderStatsTop5Genres": "Top 5 Generi",
|
"HeaderStatsTop5Genres": "Top 5 Generi",
|
||||||
"HeaderTableOfContents": "Table of Contents",
|
"HeaderTableOfContents": "Tabellla dei Contenuti",
|
||||||
"HeaderTools": "Strumenti",
|
"HeaderTools": "Strumenti",
|
||||||
"HeaderUpdateAccount": "Aggiorna Account",
|
"HeaderUpdateAccount": "Aggiorna Account",
|
||||||
"HeaderUpdateAuthor": "Aggiorna Autore",
|
"HeaderUpdateAuthor": "Aggiorna Autore",
|
||||||
@@ -163,13 +163,13 @@
|
|||||||
"HeaderUpdateLibrary": "Aggiorna Libreria",
|
"HeaderUpdateLibrary": "Aggiorna Libreria",
|
||||||
"HeaderUsers": "Utenti",
|
"HeaderUsers": "Utenti",
|
||||||
"HeaderYourStats": "Statistiche Personali",
|
"HeaderYourStats": "Statistiche Personali",
|
||||||
"LabelAbridged": "Abridged",
|
"LabelAbridged": "Abbreviato",
|
||||||
"LabelAccountType": "Tipo di Account",
|
"LabelAccountType": "Tipo di Account",
|
||||||
"LabelAccountTypeAdmin": "Admin",
|
"LabelAccountTypeAdmin": "Admin",
|
||||||
"LabelAccountTypeGuest": "Ospite",
|
"LabelAccountTypeGuest": "Ospite",
|
||||||
"LabelAccountTypeUser": "Utente",
|
"LabelAccountTypeUser": "Utente",
|
||||||
"LabelActivity": "Attività",
|
"LabelActivity": "Attività",
|
||||||
"LabelAdded": "Added",
|
"LabelAdded": "Aggiunto",
|
||||||
"LabelAddedAt": "Aggiunto il",
|
"LabelAddedAt": "Aggiunto il",
|
||||||
"LabelAddToCollection": "Aggiungi alla Raccolta",
|
"LabelAddToCollection": "Aggiungi alla Raccolta",
|
||||||
"LabelAddToCollectionBatch": "Aggiungi {0} Libri alla Raccolta",
|
"LabelAddToCollectionBatch": "Aggiungi {0} Libri alla Raccolta",
|
||||||
@@ -194,8 +194,8 @@
|
|||||||
"LabelBitrate": "Bitrate",
|
"LabelBitrate": "Bitrate",
|
||||||
"LabelBooks": "Libri",
|
"LabelBooks": "Libri",
|
||||||
"LabelChangePassword": "Cambia Password",
|
"LabelChangePassword": "Cambia Password",
|
||||||
"LabelChannels": "Channels",
|
"LabelChannels": "Canali",
|
||||||
"LabelChapters": "Chapters",
|
"LabelChapters": "Capitoli",
|
||||||
"LabelChaptersFound": "Capitoli Trovati",
|
"LabelChaptersFound": "Capitoli Trovati",
|
||||||
"LabelChapterTitle": "Titoli dei Capitoli",
|
"LabelChapterTitle": "Titoli dei Capitoli",
|
||||||
"LabelClosePlayer": "Chiudi player",
|
"LabelClosePlayer": "Chiudi player",
|
||||||
@@ -205,7 +205,7 @@
|
|||||||
"LabelComplete": "Completo",
|
"LabelComplete": "Completo",
|
||||||
"LabelConfirmPassword": "Conferma Password",
|
"LabelConfirmPassword": "Conferma Password",
|
||||||
"LabelContinueListening": "Continua ad Ascoltare",
|
"LabelContinueListening": "Continua ad Ascoltare",
|
||||||
"LabelContinueReading": "Continue Reading",
|
"LabelContinueReading": "Continua la Lettura",
|
||||||
"LabelContinueSeries": "Continua Serie",
|
"LabelContinueSeries": "Continua Serie",
|
||||||
"LabelCover": "Cover",
|
"LabelCover": "Cover",
|
||||||
"LabelCoverImageURL": "Cover Image URL",
|
"LabelCoverImageURL": "Cover Image URL",
|
||||||
@@ -230,17 +230,17 @@
|
|||||||
"LabelEbooks": "Ebooks",
|
"LabelEbooks": "Ebooks",
|
||||||
"LabelEdit": "Modifica",
|
"LabelEdit": "Modifica",
|
||||||
"LabelEmail": "Email",
|
"LabelEmail": "Email",
|
||||||
"LabelEmailSettingsFromAddress": "From Address",
|
"LabelEmailSettingsFromAddress": "Da Indirizzo",
|
||||||
"LabelEmailSettingsSecure": "Secure",
|
"LabelEmailSettingsSecure": "Secure",
|
||||||
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
|
"LabelEmailSettingsSecureHelp": "Se vero, la connessione utilizzerà TLS durante la connessione al server. Se false, viene utilizzato TLS se il server supporta l'estensione STARTTLS. Nella maggior parte dei casi impostare questo valore su true se ci si connette alla porta 465. Per la porta 587 o 25 mantenerlo false. (da nodemailer.com/smtp/#authentication)",
|
||||||
"LabelEmailSettingsTestAddress": "Test Address",
|
"LabelEmailSettingsTestAddress": "Test Indirizzo",
|
||||||
"LabelEmbeddedCover": "Embedded Cover",
|
"LabelEmbeddedCover": "Cover Integrata",
|
||||||
"LabelEnable": "Abilita",
|
"LabelEnable": "Abilita",
|
||||||
"LabelEnd": "Fine",
|
"LabelEnd": "Fine",
|
||||||
"LabelEpisode": "Episodio",
|
"LabelEpisode": "Episodio",
|
||||||
"LabelEpisodeTitle": "Titolo Episodio",
|
"LabelEpisodeTitle": "Titolo Episodio",
|
||||||
"LabelEpisodeType": "Tipo Episodio",
|
"LabelEpisodeType": "Tipo Episodio",
|
||||||
"LabelExample": "Example",
|
"LabelExample": "Esempio",
|
||||||
"LabelExplicit": "Esplicito",
|
"LabelExplicit": "Esplicito",
|
||||||
"LabelFeedURL": "Feed URL",
|
"LabelFeedURL": "Feed URL",
|
||||||
"LabelFile": "File",
|
"LabelFile": "File",
|
||||||
@@ -252,13 +252,13 @@
|
|||||||
"LabelFinished": "Finita",
|
"LabelFinished": "Finita",
|
||||||
"LabelFolder": "Cartella",
|
"LabelFolder": "Cartella",
|
||||||
"LabelFolders": "Cartelle",
|
"LabelFolders": "Cartelle",
|
||||||
"LabelFontScale": "Font scale",
|
"LabelFontScale": "Dimensione Font",
|
||||||
"LabelFormat": "Format",
|
"LabelFormat": "Formato",
|
||||||
"LabelGenre": "Genere",
|
"LabelGenre": "Genere",
|
||||||
"LabelGenres": "Generi",
|
"LabelGenres": "Generi",
|
||||||
"LabelHardDeleteFile": "Elimina Definitivamente",
|
"LabelHardDeleteFile": "Elimina Definitivamente",
|
||||||
"LabelHasEbook": "Has ebook",
|
"LabelHasEbook": "Un ebook",
|
||||||
"LabelHasSupplementaryEbook": "Has supplementary ebook",
|
"LabelHasSupplementaryEbook": "Un ebook Supplementare",
|
||||||
"LabelHost": "Host",
|
"LabelHost": "Host",
|
||||||
"LabelHour": "Ora",
|
"LabelHour": "Ora",
|
||||||
"LabelIcon": "Icona",
|
"LabelIcon": "Icona",
|
||||||
@@ -275,18 +275,18 @@
|
|||||||
"LabelIntervalEveryDay": "Ogni Giorno",
|
"LabelIntervalEveryDay": "Ogni Giorno",
|
||||||
"LabelIntervalEveryHour": "Ogni ora",
|
"LabelIntervalEveryHour": "Ogni ora",
|
||||||
"LabelInvalidParts": "Parti Invalide",
|
"LabelInvalidParts": "Parti Invalide",
|
||||||
"LabelInvert": "Invert",
|
"LabelInvert": "Inverti",
|
||||||
"LabelItem": "Oggetti",
|
"LabelItem": "Oggetti",
|
||||||
"LabelLanguage": "Lingua",
|
"LabelLanguage": "Lingua",
|
||||||
"LabelLanguageDefaultServer": "Lingua di Default",
|
"LabelLanguageDefaultServer": "Lingua di Default",
|
||||||
"LabelLastBookAdded": "Last Book Added",
|
"LabelLastBookAdded": "Ultimo Libro Aggiunto",
|
||||||
"LabelLastBookUpdated": "Last Book Updated",
|
"LabelLastBookUpdated": "Ultimo Libro Aggiornato",
|
||||||
"LabelLastSeen": "Ultimi Visti",
|
"LabelLastSeen": "Ultimi Visti",
|
||||||
"LabelLastTime": "Ultima Volta",
|
"LabelLastTime": "Ultima Volta",
|
||||||
"LabelLastUpdate": "Ultimo Aggiornamento",
|
"LabelLastUpdate": "Ultimo Aggiornamento",
|
||||||
"LabelLayout": "Layout",
|
"LabelLayout": "Layout",
|
||||||
"LabelLayoutSinglePage": "Single page",
|
"LabelLayoutSinglePage": "Pagina Singola",
|
||||||
"LabelLayoutSplitPage": "Split page",
|
"LabelLayoutSplitPage": "DIvidi Pagina",
|
||||||
"LabelLess": "Poco",
|
"LabelLess": "Poco",
|
||||||
"LabelLibrariesAccessibleToUser": "Librerie Accessibili agli Utenti",
|
"LabelLibrariesAccessibleToUser": "Librerie Accessibili agli Utenti",
|
||||||
"LabelLibrary": "Libreria",
|
"LabelLibrary": "Libreria",
|
||||||
@@ -308,7 +308,7 @@
|
|||||||
"LabelMissing": "Altro",
|
"LabelMissing": "Altro",
|
||||||
"LabelMissingParts": "Parti rimantenti",
|
"LabelMissingParts": "Parti rimantenti",
|
||||||
"LabelMore": "Molto",
|
"LabelMore": "Molto",
|
||||||
"LabelMoreInfo": "More Info",
|
"LabelMoreInfo": "Più Info",
|
||||||
"LabelName": "Nome",
|
"LabelName": "Nome",
|
||||||
"LabelNarrator": "Narratore",
|
"LabelNarrator": "Narratore",
|
||||||
"LabelNarrators": "Narratori",
|
"LabelNarrators": "Narratori",
|
||||||
@@ -318,7 +318,7 @@
|
|||||||
"LabelNewPassword": "Nuova Password",
|
"LabelNewPassword": "Nuova Password",
|
||||||
"LabelNextBackupDate": "Data Prossimo Backup",
|
"LabelNextBackupDate": "Data Prossimo Backup",
|
||||||
"LabelNextScheduledRun": "Data prossima esecuzione schedulata",
|
"LabelNextScheduledRun": "Data prossima esecuzione schedulata",
|
||||||
"LabelNoEpisodesSelected": "No episodes selected",
|
"LabelNoEpisodesSelected": "Nessun Episodio Selezionato",
|
||||||
"LabelNotes": "Note",
|
"LabelNotes": "Note",
|
||||||
"LabelNotFinished": "Da Completare",
|
"LabelNotFinished": "Da Completare",
|
||||||
"LabelNotificationAppriseURL": "Apprendi URL(s)",
|
"LabelNotificationAppriseURL": "Apprendi URL(s)",
|
||||||
@@ -349,19 +349,19 @@
|
|||||||
"LabelPlayMethod": "Metodo di riproduzione",
|
"LabelPlayMethod": "Metodo di riproduzione",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcasts",
|
"LabelPodcasts": "Podcasts",
|
||||||
"LabelPodcastType": "Timo di Podcast",
|
"LabelPodcastType": "Tipo di Podcast",
|
||||||
"LabelPort": "Port",
|
"LabelPort": "Port",
|
||||||
"LabelPrefixesToIgnore": "Suffissi da ignorare (specificando maiuscole e minuscole)",
|
"LabelPrefixesToIgnore": "Suffissi da ignorare (specificando maiuscole e minuscole)",
|
||||||
"LabelPreventIndexing": "Impedisci che il tuo feed venga indicizzato da iTunes e dalle directory dei podcast di Google",
|
"LabelPreventIndexing": "Impedisci che il tuo feed venga indicizzato da iTunes e dalle directory dei podcast di Google",
|
||||||
"LabelPrimaryEbook": "Primary ebook",
|
"LabelPrimaryEbook": "Libri Principlae",
|
||||||
"LabelProgress": "Cominciati",
|
"LabelProgress": "Cominciati",
|
||||||
"LabelProvider": "Provider",
|
"LabelProvider": "Provider",
|
||||||
"LabelPubDate": "Data Pubblicazione",
|
"LabelPubDate": "Data Pubblicazione",
|
||||||
"LabelPublisher": "Editore",
|
"LabelPublisher": "Editore",
|
||||||
"LabelPublishYear": "Anno Pubblicazione",
|
"LabelPublishYear": "Anno Pubblicazione",
|
||||||
"LabelRead": "Read",
|
"LabelRead": "Leggi",
|
||||||
"LabelReadAgain": "Read Again",
|
"LabelReadAgain": "Leggi Ancora",
|
||||||
"LabelReadEbookWithoutProgress": "Read ebook without keeping progress",
|
"LabelReadEbookWithoutProgress": "Leggi l'ebook senza mantenere i progressi",
|
||||||
"LabelRecentlyAdded": "Aggiunti Recentemente",
|
"LabelRecentlyAdded": "Aggiunti Recentemente",
|
||||||
"LabelRecentSeries": "Serie Recenti",
|
"LabelRecentSeries": "Serie Recenti",
|
||||||
"LabelRecommended": "Raccomandati",
|
"LabelRecommended": "Raccomandati",
|
||||||
@@ -378,17 +378,17 @@
|
|||||||
"LabelSearchTitle": "Cerca Titolo",
|
"LabelSearchTitle": "Cerca Titolo",
|
||||||
"LabelSearchTitleOrASIN": "Cerca titolo o ASIN",
|
"LabelSearchTitleOrASIN": "Cerca titolo o ASIN",
|
||||||
"LabelSeason": "Stagione",
|
"LabelSeason": "Stagione",
|
||||||
"LabelSelectAllEpisodes": "Select all episodes",
|
"LabelSelectAllEpisodes": "Seleziona tutti gli Episodi",
|
||||||
"LabelSelectEpisodesShowing": "Select {0} episodes showing",
|
"LabelSelectEpisodesShowing": "Episodi {0} selezionati ",
|
||||||
"LabelSendEbookToDevice": "Send Ebook to...",
|
"LabelSendEbookToDevice": "Invia ebook a...",
|
||||||
"LabelSequence": "Sequenza",
|
"LabelSequence": "Sequenza",
|
||||||
"LabelSeries": "Serie",
|
"LabelSeries": "Serie",
|
||||||
"LabelSeriesName": "Nome Serie",
|
"LabelSeriesName": "Nome Serie",
|
||||||
"LabelSeriesProgress": "Cominciato",
|
"LabelSeriesProgress": "Cominciato",
|
||||||
"LabelSetEbookAsPrimary": "Set as primary",
|
"LabelSetEbookAsPrimary": "Immposta come Primario",
|
||||||
"LabelSetEbookAsSupplementary": "Set as supplementary",
|
"LabelSetEbookAsSupplementary": "Imposta come Suplementare",
|
||||||
"LabelSettingsAudiobooksOnly": "Audiobooks only",
|
"LabelSettingsAudiobooksOnly": "Solo Audiolibri",
|
||||||
"LabelSettingsAudiobooksOnlyHelp": "Enabling this setting will ignore ebook files unless they are inside an audiobook folder in which case they will be set as supplementary ebooks",
|
"LabelSettingsAudiobooksOnlyHelp": "L'abilitazione di questa impostazione ignorerà i file di ebook a meno che non si trovino all'interno di una cartella di audiolibri, nel qual caso verranno impostati come ebook supplementari",
|
||||||
"LabelSettingsBookshelfViewHelp": "Design con scaffali in legno",
|
"LabelSettingsBookshelfViewHelp": "Design con scaffali in legno",
|
||||||
"LabelSettingsChromecastSupport": "Supporto a Chromecast",
|
"LabelSettingsChromecastSupport": "Supporto a Chromecast",
|
||||||
"LabelSettingsDateFormat": "Formato Data",
|
"LabelSettingsDateFormat": "Formato Data",
|
||||||
@@ -399,8 +399,8 @@
|
|||||||
"LabelSettingsExperimentalFeaturesHelp": "Funzionalità in fase di sviluppo che potrebbero utilizzare i tuoi feedback e aiutare i test. Fare clic per aprire la discussione github.",
|
"LabelSettingsExperimentalFeaturesHelp": "Funzionalità in fase di sviluppo che potrebbero utilizzare i tuoi feedback e aiutare i test. Fare clic per aprire la discussione github.",
|
||||||
"LabelSettingsFindCovers": "Trova covers",
|
"LabelSettingsFindCovers": "Trova covers",
|
||||||
"LabelSettingsFindCoversHelp": "Se il tuo audiolibro non ha una copertina incorporata o un'immagine di copertina all'interno della cartella, questa funzione tenterà di trovare una copertina.<br>Nota: aumenta il tempo di scansione",
|
"LabelSettingsFindCoversHelp": "Se il tuo audiolibro non ha una copertina incorporata o un'immagine di copertina all'interno della cartella, questa funzione tenterà di trovare una copertina.<br>Nota: aumenta il tempo di scansione",
|
||||||
"LabelSettingsHideSingleBookSeries": "Hide single book series",
|
"LabelSettingsHideSingleBookSeries": "Nascondi una singola serie di libri",
|
||||||
"LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.",
|
"LabelSettingsHideSingleBookSeriesHelp": "Le serie che hanno un solo libro saranno nascoste dalla pagina della serie e dagli scaffali della home page.",
|
||||||
"LabelSettingsHomePageBookshelfView": "Home page con sfondo legno",
|
"LabelSettingsHomePageBookshelfView": "Home page con sfondo legno",
|
||||||
"LabelSettingsLibraryBookshelfView": "Libreria con sfondo legno",
|
"LabelSettingsLibraryBookshelfView": "Libreria con sfondo legno",
|
||||||
"LabelSettingsOverdriveMediaMarkers": "Usa Overdrive Media Markers per i capitoli",
|
"LabelSettingsOverdriveMediaMarkers": "Usa Overdrive Media Markers per i capitoli",
|
||||||
@@ -451,9 +451,9 @@
|
|||||||
"LabelTag": "Tag",
|
"LabelTag": "Tag",
|
||||||
"LabelTags": "Tags",
|
"LabelTags": "Tags",
|
||||||
"LabelTagsAccessibleToUser": "Tags permessi agli Utenti",
|
"LabelTagsAccessibleToUser": "Tags permessi agli Utenti",
|
||||||
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
"LabelTagsNotAccessibleToUser": "Tags non accessibile agli Utenti",
|
||||||
"LabelTasks": "Processi in esecuzione",
|
"LabelTasks": "Processi in esecuzione",
|
||||||
"LabelTheme": "Theme",
|
"LabelTheme": "Tema",
|
||||||
"LabelThemeDark": "Dark",
|
"LabelThemeDark": "Dark",
|
||||||
"LabelThemeLight": "Light",
|
"LabelThemeLight": "Light",
|
||||||
"LabelTimeBase": "Time Base",
|
"LabelTimeBase": "Time Base",
|
||||||
@@ -476,7 +476,7 @@
|
|||||||
"LabelTracksMultiTrack": "Multi-traccia",
|
"LabelTracksMultiTrack": "Multi-traccia",
|
||||||
"LabelTracksSingleTrack": "Traccia-singola",
|
"LabelTracksSingleTrack": "Traccia-singola",
|
||||||
"LabelType": "Tipo",
|
"LabelType": "Tipo",
|
||||||
"LabelUnabridged": "Unabridged",
|
"LabelUnabridged": "Integrale",
|
||||||
"LabelUnknown": "Sconosciuto",
|
"LabelUnknown": "Sconosciuto",
|
||||||
"LabelUpdateCover": "Aggiornamento Cover",
|
"LabelUpdateCover": "Aggiornamento Cover",
|
||||||
"LabelUpdateCoverHelp": "Consenti la sovrascrittura delle copertine esistenti per i libri selezionati quando viene trovata una corrispondenza",
|
"LabelUpdateCoverHelp": "Consenti la sovrascrittura delle copertine esistenti per i libri selezionati quando viene trovata una corrispondenza",
|
||||||
@@ -515,17 +515,19 @@
|
|||||||
"MessageChapterStartIsAfter": "L'inizio del capitolo è dopo la fine del tuo audiolibro",
|
"MessageChapterStartIsAfter": "L'inizio del capitolo è dopo la fine del tuo audiolibro",
|
||||||
"MessageCheckingCron": "Controllo cron...",
|
"MessageCheckingCron": "Controllo cron...",
|
||||||
"MessageConfirmDeleteBackup": "Sei sicuro di voler eliminare il backup {0}?",
|
"MessageConfirmDeleteBackup": "Sei sicuro di voler eliminare il backup {0}?",
|
||||||
"MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?",
|
"MessageConfirmDeleteFile": "Questo eliminerà il file dal tuo file system. Sei sicuro?",
|
||||||
"MessageConfirmDeleteLibrary": "Sei sicuro di voler eliminare definitivamente la libreria \"{0}\"?",
|
"MessageConfirmDeleteLibrary": "Sei sicuro di voler eliminare definitivamente la libreria \"{0}\"?",
|
||||||
"MessageConfirmDeleteSession": "Sei sicuro di voler eliminare questa sessione?",
|
"MessageConfirmDeleteSession": "Sei sicuro di voler eliminare questa sessione?",
|
||||||
"MessageConfirmForceReScan": "Sei sicuro di voler forzare una nuova scansione?",
|
"MessageConfirmForceReScan": "Sei sicuro di voler forzare una nuova scansione?",
|
||||||
|
"MessageConfirmMarkAllEpisodesFinished": "Sei sicuro di voler contrassegnare tutti gli episodi come finiti?",
|
||||||
|
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
|
||||||
"MessageConfirmMarkSeriesFinished": "Sei sicuro di voler contrassegnare tutti i libri di questa serie come completati?",
|
"MessageConfirmMarkSeriesFinished": "Sei sicuro di voler contrassegnare tutti i libri di questa serie come completati?",
|
||||||
"MessageConfirmMarkSeriesNotFinished": "Sei sicuro di voler contrassegnare tutti i libri di questa serie come non completati?",
|
"MessageConfirmMarkSeriesNotFinished": "Sei sicuro di voler contrassegnare tutti i libri di questa serie come non completati?",
|
||||||
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
|
"MessageConfirmRemoveAllChapters": "Sei sicuro di voler rimuovere tutti i capitoli?",
|
||||||
"MessageConfirmRemoveCollection": "Sei sicuro di voler rimuovere la Raccolta \"{0}\"?",
|
"MessageConfirmRemoveCollection": "Sei sicuro di voler rimuovere la Raccolta \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisode": "Sei sicuro di voler rimuovere l'episodio \"{0}\"?",
|
"MessageConfirmRemoveEpisode": "Sei sicuro di voler rimuovere l'episodio \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisodes": "Sei sicuro di voler rimuovere {0} episodi?",
|
"MessageConfirmRemoveEpisodes": "Sei sicuro di voler rimuovere {0} episodi?",
|
||||||
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
|
"MessageConfirmRemoveNarrator": "Sei sicuro di voler rimuovere il narratore \"{0}\"?",
|
||||||
"MessageConfirmRemovePlaylist": "Sei sicuro di voler rimuovere la tua playlist \"{0}\"?",
|
"MessageConfirmRemovePlaylist": "Sei sicuro di voler rimuovere la tua playlist \"{0}\"?",
|
||||||
"MessageConfirmRenameGenre": "Sei sicuro di voler rinominare il genere \"{0}\" in \"{1}\" per tutti gli oggetti?",
|
"MessageConfirmRenameGenre": "Sei sicuro di voler rinominare il genere \"{0}\" in \"{1}\" per tutti gli oggetti?",
|
||||||
"MessageConfirmRenameGenreMergeNote": "Note: Questo genere esiste già quindi verra unito.",
|
"MessageConfirmRenameGenreMergeNote": "Note: Questo genere esiste già quindi verra unito.",
|
||||||
@@ -533,7 +535,7 @@
|
|||||||
"MessageConfirmRenameTag": "Sei sicuro di voler rinominare il tag \"{0}\" in \"{1}\" per tutti gli oggetti?",
|
"MessageConfirmRenameTag": "Sei sicuro di voler rinominare il tag \"{0}\" in \"{1}\" per tutti gli oggetti?",
|
||||||
"MessageConfirmRenameTagMergeNote": "Nota: Questo tag esiste già e verrà unito nel vecchio.",
|
"MessageConfirmRenameTagMergeNote": "Nota: Questo tag esiste già e verrà unito nel vecchio.",
|
||||||
"MessageConfirmRenameTagWarning": "Avvertimento! Esiste già un tag simile con un nome simile \"{0}\".",
|
"MessageConfirmRenameTagWarning": "Avvertimento! Esiste già un tag simile con un nome simile \"{0}\".",
|
||||||
"MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
|
"MessageConfirmSendEbookToDevice": "Sei sicuro di voler inviare {0} ebook \"{1}\" al Device \"{2}\"?",
|
||||||
"MessageDownloadingEpisode": "Download episodio in corso",
|
"MessageDownloadingEpisode": "Download episodio in corso",
|
||||||
"MessageDragFilesIntoTrackOrder": "Trascina i file nell'ordine di traccia corretto",
|
"MessageDragFilesIntoTrackOrder": "Trascina i file nell'ordine di traccia corretto",
|
||||||
"MessageEmbedFinished": "Incorporamento finito!",
|
"MessageEmbedFinished": "Incorporamento finito!",
|
||||||
@@ -552,6 +554,8 @@
|
|||||||
"MessageM4BFailed": "M4B Fallito!",
|
"MessageM4BFailed": "M4B Fallito!",
|
||||||
"MessageM4BFinished": "M4B Finito!",
|
"MessageM4BFinished": "M4B Finito!",
|
||||||
"MessageMapChapterTitles": "Associa i titoli dei capitoli ai capitoli dell'audiolibro esistente senza modificare i timestamp",
|
"MessageMapChapterTitles": "Associa i titoli dei capitoli ai capitoli dell'audiolibro esistente senza modificare i timestamp",
|
||||||
|
"MessageMarkAllEpisodesFinished": "Segna tutti gli episodi come finiti",
|
||||||
|
"MessageMarkAllEpisodesNotFinished": "Segna tutti gli episodi come non finiti",
|
||||||
"MessageMarkAsFinished": "Segna come finito",
|
"MessageMarkAsFinished": "Segna come finito",
|
||||||
"MessageMarkAsNotFinished": "Segna come da completare",
|
"MessageMarkAsNotFinished": "Segna come da completare",
|
||||||
"MessageMatchBooksDescription": "tenterà di abbinare i libri nella biblioteca con un libro del provider di ricerca selezionato e inserirà i dettagli vuoti e la copertina. Non sovrascrive i dettagli.",
|
"MessageMatchBooksDescription": "tenterà di abbinare i libri nella biblioteca con un libro del provider di ricerca selezionato e inserirà i dettagli vuoti e la copertina. Non sovrascrive i dettagli.",
|
||||||
@@ -687,8 +691,8 @@
|
|||||||
"ToastRemoveItemFromCollectionSuccess": "Oggetto rimosso dalla Raccolta",
|
"ToastRemoveItemFromCollectionSuccess": "Oggetto rimosso dalla Raccolta",
|
||||||
"ToastRSSFeedCloseFailed": "Errore chiusura RSS feed",
|
"ToastRSSFeedCloseFailed": "Errore chiusura RSS feed",
|
||||||
"ToastRSSFeedCloseSuccess": "RSS feed chiuso",
|
"ToastRSSFeedCloseSuccess": "RSS feed chiuso",
|
||||||
"ToastSendEbookToDeviceFailed": "Failed to Send Ebook to device",
|
"ToastSendEbookToDeviceFailed": "Impossibile inviare l'ebook al dispositivo",
|
||||||
"ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"",
|
"ToastSendEbookToDeviceSuccess": "Ebook inviato al dispositivo \"{0}\"",
|
||||||
"ToastSeriesUpdateFailed": "Aggiornaento Serie Fallito",
|
"ToastSeriesUpdateFailed": "Aggiornaento Serie Fallito",
|
||||||
"ToastSeriesUpdateSuccess": "Serie Aggornate",
|
"ToastSeriesUpdateSuccess": "Serie Aggornate",
|
||||||
"ToastSessionDeleteFailed": "Errore eliminazione sessione",
|
"ToastSessionDeleteFailed": "Errore eliminazione sessione",
|
||||||
|
|||||||
@@ -519,6 +519,8 @@
|
|||||||
"MessageConfirmDeleteLibrary": "Weet je zeker dat je de bibliotheek \"{0}\" permanent wil verwijderen?",
|
"MessageConfirmDeleteLibrary": "Weet je zeker dat je de bibliotheek \"{0}\" permanent wil verwijderen?",
|
||||||
"MessageConfirmDeleteSession": "Weet je zeker dat je deze sessie wil verwijderen?",
|
"MessageConfirmDeleteSession": "Weet je zeker dat je deze sessie wil verwijderen?",
|
||||||
"MessageConfirmForceReScan": "Weet je zeker dat je geforceerd opnieuw wil scannen?",
|
"MessageConfirmForceReScan": "Weet je zeker dat je geforceerd opnieuw wil scannen?",
|
||||||
|
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
|
||||||
|
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
|
||||||
"MessageConfirmMarkSeriesFinished": "Weet je zeker dat je alle boeken in deze serie wil markeren als voltooid?",
|
"MessageConfirmMarkSeriesFinished": "Weet je zeker dat je alle boeken in deze serie wil markeren als voltooid?",
|
||||||
"MessageConfirmMarkSeriesNotFinished": "Weet je zeker dat je alle boeken in deze serie wil markeren als niet voltooid?",
|
"MessageConfirmMarkSeriesNotFinished": "Weet je zeker dat je alle boeken in deze serie wil markeren als niet voltooid?",
|
||||||
"MessageConfirmRemoveAllChapters": "Weet je zeker dat je alle hoofdstukken wil verwijderen?",
|
"MessageConfirmRemoveAllChapters": "Weet je zeker dat je alle hoofdstukken wil verwijderen?",
|
||||||
@@ -552,6 +554,8 @@
|
|||||||
"MessageM4BFailed": "M4B mislukt!",
|
"MessageM4BFailed": "M4B mislukt!",
|
||||||
"MessageM4BFinished": "M4B voltooid!",
|
"MessageM4BFinished": "M4B voltooid!",
|
||||||
"MessageMapChapterTitles": "Map hoofdstuktitels naar je bestaande audioboekhoofdstukken zonder aanpassing van tijden",
|
"MessageMapChapterTitles": "Map hoofdstuktitels naar je bestaande audioboekhoofdstukken zonder aanpassing van tijden",
|
||||||
|
"MessageMarkAllEpisodesFinished": "Mark all episodes finished",
|
||||||
|
"MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
|
||||||
"MessageMarkAsFinished": "Markeer als Voltooid",
|
"MessageMarkAsFinished": "Markeer als Voltooid",
|
||||||
"MessageMarkAsNotFinished": "Markeer als Niet Voltooid",
|
"MessageMarkAsNotFinished": "Markeer als Niet Voltooid",
|
||||||
"MessageMatchBooksDescription": "zal proberen boeken in de bibliotheek te matchen met een boek uit de geselecteerde bron en lege details en coverafbeelding te vullen. Overschrijft details niet.",
|
"MessageMatchBooksDescription": "zal proberen boeken in de bibliotheek te matchen met een boek uit de geselecteerde bron en lege details en coverafbeelding te vullen. Overschrijft details niet.",
|
||||||
|
|||||||
@@ -519,6 +519,8 @@
|
|||||||
"MessageConfirmDeleteLibrary": "Czy na pewno chcesz trwale usunąć bibliotekę \"{0}\"?",
|
"MessageConfirmDeleteLibrary": "Czy na pewno chcesz trwale usunąć bibliotekę \"{0}\"?",
|
||||||
"MessageConfirmDeleteSession": "Czy na pewno chcesz usunąć tę sesję?",
|
"MessageConfirmDeleteSession": "Czy na pewno chcesz usunąć tę sesję?",
|
||||||
"MessageConfirmForceReScan": "Czy na pewno chcesz wymusić ponowne skanowanie?",
|
"MessageConfirmForceReScan": "Czy na pewno chcesz wymusić ponowne skanowanie?",
|
||||||
|
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
|
||||||
|
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
|
||||||
"MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
|
"MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
|
||||||
"MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
|
"MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
|
||||||
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
|
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
|
||||||
@@ -552,6 +554,8 @@
|
|||||||
"MessageM4BFailed": "Tworzenie pliku M4B nie powiodło się",
|
"MessageM4BFailed": "Tworzenie pliku M4B nie powiodło się",
|
||||||
"MessageM4BFinished": "Tworzenie pliku M4B zakończyło się!",
|
"MessageM4BFinished": "Tworzenie pliku M4B zakończyło się!",
|
||||||
"MessageMapChapterTitles": "Mapowanie tytułów rozdziałów do istniejących rozdziałów audiobooka bez dostosowywania znaczników czasu",
|
"MessageMapChapterTitles": "Mapowanie tytułów rozdziałów do istniejących rozdziałów audiobooka bez dostosowywania znaczników czasu",
|
||||||
|
"MessageMarkAllEpisodesFinished": "Mark all episodes finished",
|
||||||
|
"MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
|
||||||
"MessageMarkAsFinished": "Oznacz jako ukończone",
|
"MessageMarkAsFinished": "Oznacz jako ukończone",
|
||||||
"MessageMarkAsNotFinished": "Oznacz jako nieukończone",
|
"MessageMarkAsNotFinished": "Oznacz jako nieukończone",
|
||||||
"MessageMatchBooksDescription": "spróbuje dopasować książki w bibliotece bez plików audio, korzystając z wybranego dostawcy wyszukiwania i wypełnić puste szczegóły i okładki. Nie nadpisuje informacji.",
|
"MessageMatchBooksDescription": "spróbuje dopasować książki w bibliotece bez plików audio, korzystając z wybranego dostawcy wyszukiwania i wypełnić puste szczegóły i okładki. Nie nadpisuje informacji.",
|
||||||
|
|||||||
@@ -519,6 +519,8 @@
|
|||||||
"MessageConfirmDeleteLibrary": "Вы уверены, что хотите навсегда удалить библиотеку \"{0}\"?",
|
"MessageConfirmDeleteLibrary": "Вы уверены, что хотите навсегда удалить библиотеку \"{0}\"?",
|
||||||
"MessageConfirmDeleteSession": "Вы уверены, что хотите удалить этот сеанс?",
|
"MessageConfirmDeleteSession": "Вы уверены, что хотите удалить этот сеанс?",
|
||||||
"MessageConfirmForceReScan": "Вы уверены, что хотите принудительно выполнить повторное сканирование?",
|
"MessageConfirmForceReScan": "Вы уверены, что хотите принудительно выполнить повторное сканирование?",
|
||||||
|
"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?",
|
||||||
"MessageConfirmMarkSeriesFinished": "Вы уверены, что хотите отметить все книги этой серии как законченные?",
|
"MessageConfirmMarkSeriesFinished": "Вы уверены, что хотите отметить все книги этой серии как законченные?",
|
||||||
"MessageConfirmMarkSeriesNotFinished": "Вы уверены, что хотите отметить все книги этой серии как незаконченные?",
|
"MessageConfirmMarkSeriesNotFinished": "Вы уверены, что хотите отметить все книги этой серии как незаконченные?",
|
||||||
"MessageConfirmRemoveAllChapters": "Вы уверены, что хотите удалить все главы?",
|
"MessageConfirmRemoveAllChapters": "Вы уверены, что хотите удалить все главы?",
|
||||||
@@ -552,6 +554,8 @@
|
|||||||
"MessageM4BFailed": "M4B Ошибка!",
|
"MessageM4BFailed": "M4B Ошибка!",
|
||||||
"MessageM4BFinished": "M4B Завершено!",
|
"MessageM4BFinished": "M4B Завершено!",
|
||||||
"MessageMapChapterTitles": "Сопоставление названий глав с существующими главами аудиокниги без корректировки временных меток",
|
"MessageMapChapterTitles": "Сопоставление названий глав с существующими главами аудиокниги без корректировки временных меток",
|
||||||
|
"MessageMarkAllEpisodesFinished": "Mark all episodes finished",
|
||||||
|
"MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
|
||||||
"MessageMarkAsFinished": "Отметить, как завершенную",
|
"MessageMarkAsFinished": "Отметить, как завершенную",
|
||||||
"MessageMarkAsNotFinished": "Отметить, как не завершенную",
|
"MessageMarkAsNotFinished": "Отметить, как не завершенную",
|
||||||
"MessageMatchBooksDescription": "попытается сопоставить книги в библиотеке с книгой из выбранного поставщика поиска и заполнить пустые детали и обложку. Не перезаписывает сведения.",
|
"MessageMatchBooksDescription": "попытается сопоставить книги в библиотеке с книгой из выбранного поставщика поиска и заполнить пустые детали и обложку. Не перезаписывает сведения.",
|
||||||
|
|||||||
@@ -519,6 +519,8 @@
|
|||||||
"MessageConfirmDeleteLibrary": "你确定要永久删除媒体库 \"{0}\"?",
|
"MessageConfirmDeleteLibrary": "你确定要永久删除媒体库 \"{0}\"?",
|
||||||
"MessageConfirmDeleteSession": "你确定要删除此会话吗?",
|
"MessageConfirmDeleteSession": "你确定要删除此会话吗?",
|
||||||
"MessageConfirmForceReScan": "你确定要强制重新扫描吗?",
|
"MessageConfirmForceReScan": "你确定要强制重新扫描吗?",
|
||||||
|
"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?",
|
||||||
"MessageConfirmMarkSeriesFinished": "你确定要将此系列中的所有书籍都标记为已听完吗?",
|
"MessageConfirmMarkSeriesFinished": "你确定要将此系列中的所有书籍都标记为已听完吗?",
|
||||||
"MessageConfirmMarkSeriesNotFinished": "你确定要将此系列中的所有书籍都标记为未听完吗?",
|
"MessageConfirmMarkSeriesNotFinished": "你确定要将此系列中的所有书籍都标记为未听完吗?",
|
||||||
"MessageConfirmRemoveAllChapters": "你确定要移除所有章节吗?",
|
"MessageConfirmRemoveAllChapters": "你确定要移除所有章节吗?",
|
||||||
@@ -552,6 +554,8 @@
|
|||||||
"MessageM4BFailed": "M4B 失败!",
|
"MessageM4BFailed": "M4B 失败!",
|
||||||
"MessageM4BFinished": "M4B 完成!",
|
"MessageM4BFinished": "M4B 完成!",
|
||||||
"MessageMapChapterTitles": "将章节标题映射到现有的有声读物章节, 无需调整时间戳",
|
"MessageMapChapterTitles": "将章节标题映射到现有的有声读物章节, 无需调整时间戳",
|
||||||
|
"MessageMarkAllEpisodesFinished": "Mark all episodes finished",
|
||||||
|
"MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
|
||||||
"MessageMarkAsFinished": "标记为已听完",
|
"MessageMarkAsFinished": "标记为已听完",
|
||||||
"MessageMarkAsNotFinished": "标记为未听完",
|
"MessageMarkAsNotFinished": "标记为未听完",
|
||||||
"MessageMatchBooksDescription": "尝试将媒体库中的图书与所选搜索提供商的图书进行匹配, 并填写空白的详细信息和封面. 不覆盖详细信息.",
|
"MessageMatchBooksDescription": "尝试将媒体库中的图书与所选搜索提供商的图书进行匹配, 并填写空白的详细信息和封面. 不覆盖详细信息.",
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.2.23",
|
"version": "2.3.2",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.2.23",
|
"version": "2.3.2",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.2.23",
|
"version": "2.3.2",
|
||||||
"description": "Self-hosted audiobook and podcast server",
|
"description": "Self-hosted audiobook and podcast server",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+18
-13
@@ -23,7 +23,6 @@ class Database {
|
|||||||
this.playlists = []
|
this.playlists = []
|
||||||
this.authors = []
|
this.authors = []
|
||||||
this.series = []
|
this.series = []
|
||||||
this.feeds = []
|
|
||||||
|
|
||||||
this.serverSettings = null
|
this.serverSettings = null
|
||||||
this.notificationSettings = null
|
this.notificationSettings = null
|
||||||
@@ -128,6 +127,18 @@ class Database {
|
|||||||
|
|
||||||
const startTime = Date.now()
|
const startTime = Date.now()
|
||||||
|
|
||||||
|
const settingsData = await this.models.setting.getOldSettings()
|
||||||
|
this.settings = settingsData.settings
|
||||||
|
this.emailSettings = settingsData.emailSettings
|
||||||
|
this.serverSettings = settingsData.serverSettings
|
||||||
|
this.notificationSettings = settingsData.notificationSettings
|
||||||
|
global.ServerSettings = this.serverSettings.toJSON()
|
||||||
|
|
||||||
|
// Version specific migrations
|
||||||
|
if (this.serverSettings.version === '2.3.0' && packageJson.version !== '2.3.0') {
|
||||||
|
await dbMigration.migrationPatch(this)
|
||||||
|
}
|
||||||
|
|
||||||
this.libraryItems = await this.models.libraryItem.getAllOldLibraryItems()
|
this.libraryItems = await this.models.libraryItem.getAllOldLibraryItems()
|
||||||
this.users = await this.models.user.getOldUsers()
|
this.users = await this.models.user.getOldUsers()
|
||||||
this.libraries = await this.models.library.getAllOldLibraries()
|
this.libraries = await this.models.library.getAllOldLibraries()
|
||||||
@@ -135,14 +146,6 @@ class Database {
|
|||||||
this.playlists = await this.models.playlist.getOldPlaylists()
|
this.playlists = await this.models.playlist.getOldPlaylists()
|
||||||
this.authors = await this.models.author.getOldAuthors()
|
this.authors = await this.models.author.getOldAuthors()
|
||||||
this.series = await this.models.series.getAllOldSeries()
|
this.series = await this.models.series.getAllOldSeries()
|
||||||
this.feeds = await this.models.feed.getOldFeeds()
|
|
||||||
|
|
||||||
const settingsData = await this.models.setting.getOldSettings()
|
|
||||||
this.settings = settingsData.settings
|
|
||||||
this.emailSettings = settingsData.emailSettings
|
|
||||||
this.serverSettings = settingsData.serverSettings
|
|
||||||
this.notificationSettings = settingsData.notificationSettings
|
|
||||||
global.ServerSettings = this.serverSettings.toJSON()
|
|
||||||
|
|
||||||
Logger.info(`[Database] Db data loaded in ${Date.now() - startTime}ms`)
|
Logger.info(`[Database] Db data loaded in ${Date.now() - startTime}ms`)
|
||||||
|
|
||||||
@@ -357,7 +360,11 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getLibraryItem(libraryItemId) {
|
getLibraryItem(libraryItemId) {
|
||||||
if (!this.sequelize) return false
|
if (!this.sequelize || !libraryItemId) return false
|
||||||
|
|
||||||
|
// Temp support for old library item ids from mobile
|
||||||
|
if (libraryItemId.startsWith('li_')) return this.libraryItems.find(li => li.oldLibraryItemId === libraryItemId)
|
||||||
|
|
||||||
return this.libraryItems.find(li => li.id === libraryItemId)
|
return this.libraryItems.find(li => li.id === libraryItemId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,7 +406,6 @@ class Database {
|
|||||||
async createFeed(oldFeed) {
|
async createFeed(oldFeed) {
|
||||||
if (!this.sequelize) return false
|
if (!this.sequelize) return false
|
||||||
await this.models.feed.fullCreateFromOld(oldFeed)
|
await this.models.feed.fullCreateFromOld(oldFeed)
|
||||||
this.feeds.push(oldFeed)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFeed(oldFeed) {
|
updateFeed(oldFeed) {
|
||||||
@@ -410,7 +416,6 @@ class Database {
|
|||||||
async removeFeed(feedId) {
|
async removeFeed(feedId) {
|
||||||
if (!this.sequelize) return false
|
if (!this.sequelize) return false
|
||||||
await this.models.feed.removeById(feedId)
|
await this.models.feed.removeById(feedId)
|
||||||
this.feeds = this.feeds.filter(f => f.id !== feedId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSeries(oldSeries) {
|
updateSeries(oldSeries) {
|
||||||
@@ -438,7 +443,7 @@ class Database {
|
|||||||
|
|
||||||
async createAuthor(oldAuthor) {
|
async createAuthor(oldAuthor) {
|
||||||
if (!this.sequelize) return false
|
if (!this.sequelize) return false
|
||||||
await this.models.createFromOld(oldAuthor)
|
await this.models.author.createFromOld(oldAuthor)
|
||||||
this.authors.push(oldAuthor)
|
this.authors.push(oldAuthor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,14 +26,14 @@ class CollectionController {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
findOne(req, res) {
|
async findOne(req, res) {
|
||||||
const includeEntities = (req.query.include || '').split(',')
|
const includeEntities = (req.query.include || '').split(',')
|
||||||
|
|
||||||
const collectionExpanded = req.collection.toJSONExpanded(Database.libraryItems)
|
const collectionExpanded = req.collection.toJSONExpanded(Database.libraryItems)
|
||||||
|
|
||||||
if (includeEntities.includes('rssfeed')) {
|
if (includeEntities.includes('rssfeed')) {
|
||||||
const feedData = this.rssFeedManager.findFeedForEntityId(collectionExpanded.id)
|
const feedData = await this.rssFeedManager.findFeedForEntityId(collectionExpanded.id)
|
||||||
collectionExpanded.rssFeed = feedData ? feedData.toJSONMinified() : null
|
collectionExpanded.rssFeed = feedData?.toJSONMinified() || null
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json(collectionExpanded)
|
res.json(collectionExpanded)
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ class LibraryController {
|
|||||||
|
|
||||||
// api/libraries/:id/items
|
// api/libraries/:id/items
|
||||||
// TODO: Optimize this method, items are iterated through several times but can be combined
|
// TODO: Optimize this method, items are iterated through several times but can be combined
|
||||||
getLibraryItems(req, res) {
|
async getLibraryItems(req, res) {
|
||||||
let libraryItems = req.libraryItems
|
let libraryItems = req.libraryItems
|
||||||
|
|
||||||
const include = (req.query.include || '').split(',').map(v => v.trim().toLowerCase()).filter(v => !!v)
|
const include = (req.query.include || '').split(',').map(v => v.trim().toLowerCase()).filter(v => !!v)
|
||||||
@@ -198,11 +198,12 @@ class LibraryController {
|
|||||||
include: include.join(',')
|
include: include.join(',')
|
||||||
}
|
}
|
||||||
const mediaIsBook = payload.mediaType === 'book'
|
const mediaIsBook = payload.mediaType === 'book'
|
||||||
|
const mediaIsPodcast = payload.mediaType === 'podcast'
|
||||||
|
|
||||||
// Step 1 - Filter the retrieved library items
|
// Step 1 - Filter the retrieved library items
|
||||||
let filterSeries = null
|
let filterSeries = null
|
||||||
if (payload.filterBy) {
|
if (payload.filterBy) {
|
||||||
libraryItems = libraryHelpers.getFilteredLibraryItems(libraryItems, payload.filterBy, req.user, Database.feeds)
|
libraryItems = await libraryHelpers.getFilteredLibraryItems(libraryItems, payload.filterBy, req.user)
|
||||||
payload.total = libraryItems.length
|
payload.total = libraryItems.length
|
||||||
|
|
||||||
// Determining if we are filtering titles by a series, and if so, which series
|
// Determining if we are filtering titles by a series, and if so, which series
|
||||||
@@ -236,7 +237,6 @@ class LibraryController {
|
|||||||
const sortArray = []
|
const sortArray = []
|
||||||
|
|
||||||
// When on the series page, sort by sequence only
|
// When on the series page, sort by sequence only
|
||||||
if (payload.sortBy === 'book.volumeNumber') payload.sortBy = null // TODO: Remove temp fix after mobile release 0.9.60
|
|
||||||
if (filterSeries && !payload.sortBy) {
|
if (filterSeries && !payload.sortBy) {
|
||||||
sortArray.push({ asc: (li) => li.media.metadata.getSeries(filterSeries).sequence })
|
sortArray.push({ asc: (li) => li.media.metadata.getSeries(filterSeries).sequence })
|
||||||
// If no series sequence then fallback to sorting by title (or collapsed series name for sub-series)
|
// If no series sequence then fallback to sorting by title (or collapsed series name for sub-series)
|
||||||
@@ -319,7 +319,7 @@ class LibraryController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 4 - Transform the items to pass to the client side
|
// Step 4 - Transform the items to pass to the client side
|
||||||
payload.results = libraryItems.map(li => {
|
payload.results = await Promise.all(libraryItems.map(async li => {
|
||||||
const json = payload.minified ? li.toJSONMinified() : li.toJSON()
|
const json = payload.minified ? li.toJSONMinified() : li.toJSON()
|
||||||
|
|
||||||
if (li.collapsedSeries) {
|
if (li.collapsedSeries) {
|
||||||
@@ -356,10 +356,15 @@ class LibraryController {
|
|||||||
} else {
|
} else {
|
||||||
// add rssFeed object if "include=rssfeed" was put in query string (only for non-collapsed series)
|
// add rssFeed object if "include=rssfeed" was put in query string (only for non-collapsed series)
|
||||||
if (include.includes('rssfeed')) {
|
if (include.includes('rssfeed')) {
|
||||||
const feedData = this.rssFeedManager.findFeedForEntityId(json.id)
|
const feedData = await this.rssFeedManager.findFeedForEntityId(json.id)
|
||||||
json.rssFeed = feedData ? feedData.toJSONMinified() : null
|
json.rssFeed = feedData ? feedData.toJSONMinified() : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add numEpisodesIncomplete if "include=numEpisodesIncomplete" was put in query string (only for podcasts)
|
||||||
|
if (mediaIsPodcast && include.includes('numepisodesincomplete')) {
|
||||||
|
json.numEpisodesIncomplete = req.user.getNumEpisodesIncompleteForPodcast(li)
|
||||||
|
}
|
||||||
|
|
||||||
if (filterSeries) {
|
if (filterSeries) {
|
||||||
// If filtering by series, make sure to include the series metadata
|
// If filtering by series, make sure to include the series metadata
|
||||||
json.media.metadata.series = li.media.metadata.getSeries(filterSeries)
|
json.media.metadata.series = li.media.metadata.getSeries(filterSeries)
|
||||||
@@ -367,7 +372,7 @@ class LibraryController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return json
|
return json
|
||||||
})
|
}))
|
||||||
|
|
||||||
res.json(payload)
|
res.json(payload)
|
||||||
}
|
}
|
||||||
@@ -444,11 +449,11 @@ class LibraryController {
|
|||||||
|
|
||||||
// add rssFeed when "include=rssfeed" is in query string
|
// add rssFeed when "include=rssfeed" is in query string
|
||||||
if (include.includes('rssfeed')) {
|
if (include.includes('rssfeed')) {
|
||||||
series = series.map((se) => {
|
series = await Promise.all(series.map(async (se) => {
|
||||||
const feedData = this.rssFeedManager.findFeedForEntityId(se.id)
|
const feedData = await this.rssFeedManager.findFeedForEntityId(se.id)
|
||||||
se.rssFeed = feedData?.toJSONMinified() || null
|
se.rssFeed = feedData?.toJSONMinified() || null
|
||||||
return se
|
return se
|
||||||
})
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
payload.results = series
|
payload.results = series
|
||||||
@@ -484,7 +489,7 @@ class LibraryController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (include.includes('rssfeed')) {
|
if (include.includes('rssfeed')) {
|
||||||
const feedObj = this.rssFeedManager.findFeedForEntityId(seriesJson.id)
|
const feedObj = await this.rssFeedManager.findFeedForEntityId(seriesJson.id)
|
||||||
seriesJson.rssFeed = feedObj?.toJSONMinified() || null
|
seriesJson.rssFeed = feedObj?.toJSONMinified() || null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -509,19 +514,21 @@ class LibraryController {
|
|||||||
include: include.join(',')
|
include: include.join(',')
|
||||||
}
|
}
|
||||||
|
|
||||||
let collections = Database.collections.filter(c => c.libraryId === req.library.id).map(c => {
|
let collections = await Promise.all(Database.collections.filter(c => c.libraryId === req.library.id).map(async c => {
|
||||||
const expanded = c.toJSONExpanded(libraryItems, payload.minified)
|
const expanded = c.toJSONExpanded(libraryItems, payload.minified)
|
||||||
|
|
||||||
// If all books restricted to user in this collection then hide this collection
|
// If all books restricted to user in this collection then hide this collection
|
||||||
if (!expanded.books.length && c.books.length) return null
|
if (!expanded.books.length && c.books.length) return null
|
||||||
|
|
||||||
if (include.includes('rssfeed')) {
|
if (include.includes('rssfeed')) {
|
||||||
const feedData = this.rssFeedManager.findFeedForEntityId(c.id)
|
const feedData = await this.rssFeedManager.findFeedForEntityId(c.id)
|
||||||
expanded.rssFeed = feedData?.toJSONMinified() || null
|
expanded.rssFeed = feedData?.toJSONMinified() || null
|
||||||
}
|
}
|
||||||
|
|
||||||
return expanded
|
return expanded
|
||||||
}).filter(c => !!c)
|
}))
|
||||||
|
|
||||||
|
collections = collections.filter(c => !!c)
|
||||||
|
|
||||||
payload.total = collections.length
|
payload.total = collections.length
|
||||||
|
|
||||||
@@ -590,7 +597,7 @@ class LibraryController {
|
|||||||
const limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) || 10 : 10
|
const limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) || 10 : 10
|
||||||
const include = (req.query.include || '').split(',').map(v => v.trim().toLowerCase()).filter(v => !!v)
|
const include = (req.query.include || '').split(',').map(v => v.trim().toLowerCase()).filter(v => !!v)
|
||||||
|
|
||||||
const categories = libraryHelpers.buildPersonalizedShelves(this, req.user, req.libraryItems, req.library, limitPerShelf, include)
|
const categories = await libraryHelpers.buildPersonalizedShelves(this, req.user, req.libraryItems, req.library, limitPerShelf, include)
|
||||||
res.json(categories)
|
res.json(categories)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class LibraryItemController {
|
|||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
// Example expand with authors: api/items/:id?expanded=1&include=authors
|
// Example expand with authors: api/items/:id?expanded=1&include=authors
|
||||||
findOne(req, res) {
|
async findOne(req, res) {
|
||||||
const includeEntities = (req.query.include || '').split(',')
|
const includeEntities = (req.query.include || '').split(',')
|
||||||
if (req.query.expanded == 1) {
|
if (req.query.expanded == 1) {
|
||||||
var item = req.libraryItem.toJSONExpanded()
|
var item = req.libraryItem.toJSONExpanded()
|
||||||
@@ -25,8 +25,8 @@ class LibraryItemController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (includeEntities.includes('rssfeed')) {
|
if (includeEntities.includes('rssfeed')) {
|
||||||
const feedData = this.rssFeedManager.findFeedForEntityId(item.id)
|
const feedData = await this.rssFeedManager.findFeedForEntityId(item.id)
|
||||||
item.rssFeed = feedData ? feedData.toJSONMinified() : null
|
item.rssFeed = feedData?.toJSONMinified() || null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.mediaType == 'book') {
|
if (item.mediaType == 'book') {
|
||||||
@@ -88,7 +88,9 @@ class LibraryItemController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const libraryItemPath = req.libraryItem.path
|
const libraryItemPath = req.libraryItem.path
|
||||||
const filename = `${req.libraryItem.media.metadata.title}.zip`
|
const itemTitle = req.libraryItem.media.metadata.title
|
||||||
|
Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${itemTitle}" at "${libraryItemPath}"`)
|
||||||
|
const filename = `${itemTitle}.zip`
|
||||||
zipHelpers.zipDirectoryPipe(libraryItemPath, filename, res)
|
zipHelpers.zipDirectoryPipe(libraryItemPath, filename, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,6 +100,7 @@ class LibraryItemController {
|
|||||||
async updateMedia(req, res) {
|
async updateMedia(req, res) {
|
||||||
const libraryItem = req.libraryItem
|
const libraryItem = req.libraryItem
|
||||||
const mediaPayload = req.body
|
const mediaPayload = req.body
|
||||||
|
|
||||||
// Item has cover and update is removing cover so purge it from cache
|
// Item has cover and update is removing cover so purge it from cache
|
||||||
if (libraryItem.media.coverPath && (mediaPayload.coverPath === '' || mediaPayload.coverPath === null)) {
|
if (libraryItem.media.coverPath && (mediaPayload.coverPath === '' || mediaPayload.coverPath === null)) {
|
||||||
await this.cacheManager.purgeCoverCache(libraryItem.id)
|
await this.cacheManager.purgeCoverCache(libraryItem.id)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class RSSFeedController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check that this slug is not being used for another feed (slug will also be the Feed id)
|
// Check that this slug is not being used for another feed (slug will also be the Feed id)
|
||||||
if (this.rssFeedManager.findFeedBySlug(options.slug)) {
|
if (await this.rssFeedManager.findFeedBySlug(options.slug)) {
|
||||||
Logger.error(`[RSSFeedController] Cannot open RSS feed because slug "${options.slug}" is already in use`)
|
Logger.error(`[RSSFeedController] Cannot open RSS feed because slug "${options.slug}" is already in use`)
|
||||||
return res.status(400).send('Slug already in use')
|
return res.status(400).send('Slug already in use')
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ class RSSFeedController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check that this slug is not being used for another feed (slug will also be the Feed id)
|
// Check that this slug is not being used for another feed (slug will also be the Feed id)
|
||||||
if (this.rssFeedManager.findFeedBySlug(options.slug)) {
|
if (await this.rssFeedManager.findFeedBySlug(options.slug)) {
|
||||||
Logger.error(`[RSSFeedController] Cannot open RSS feed because slug "${options.slug}" is already in use`)
|
Logger.error(`[RSSFeedController] Cannot open RSS feed because slug "${options.slug}" is already in use`)
|
||||||
return res.status(400).send('Slug already in use')
|
return res.status(400).send('Slug already in use')
|
||||||
}
|
}
|
||||||
@@ -89,7 +89,7 @@ class RSSFeedController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check that this slug is not being used for another feed (slug will also be the Feed id)
|
// Check that this slug is not being used for another feed (slug will also be the Feed id)
|
||||||
if (this.rssFeedManager.findFeedBySlug(options.slug)) {
|
if (await this.rssFeedManager.findFeedBySlug(options.slug)) {
|
||||||
Logger.error(`[RSSFeedController] Cannot open RSS feed because slug "${options.slug}" is already in use`)
|
Logger.error(`[RSSFeedController] Cannot open RSS feed because slug "${options.slug}" is already in use`)
|
||||||
return res.status(400).send('Slug already in use')
|
return res.status(400).send('Slug already in use')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class SeriesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (include.includes('rssfeed')) {
|
if (include.includes('rssfeed')) {
|
||||||
const feedObj = this.rssFeedManager.findFeedForEntityId(seriesJson.id)
|
const feedObj = await this.rssFeedManager.findFeedForEntityId(seriesJson.id)
|
||||||
seriesJson.rssFeed = feedObj?.toJSONMinified() || null
|
seriesJson.rssFeed = feedObj?.toJSONMinified() || null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ class SeriesController {
|
|||||||
* Filter out any library items not accessible to user
|
* Filter out any library items not accessible to user
|
||||||
*/
|
*/
|
||||||
const libraryItems = Database.libraryItems.filter(li => li.media.metadata.hasSeries?.(series.id))
|
const libraryItems = Database.libraryItems.filter(li => li.media.metadata.hasSeries?.(series.id))
|
||||||
const libraryItemsAccessible = libraryItems.filter(req.user.checkCanAccessLibraryItem)
|
const libraryItemsAccessible = libraryItems.filter(li => req.user.checkCanAccessLibraryItem(li))
|
||||||
if (libraryItems.length && !libraryItemsAccessible.length) {
|
if (libraryItems.length && !libraryItemsAccessible.length) {
|
||||||
Logger.warn(`[SeriesController] User attempted to access series "${series.id}" without access to any of the books`, req.user)
|
Logger.warn(`[SeriesController] User attempted to access series "${series.id}" without access to any of the books`, req.user)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ class SessionController {
|
|||||||
|
|
||||||
// POST: api/session/local
|
// POST: api/session/local
|
||||||
syncLocal(req, res) {
|
syncLocal(req, res) {
|
||||||
this.playbackSessionManager.syncLocalSessionRequest(req.user, req.body, res)
|
this.playbackSessionManager.syncLocalSessionRequest(req, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST: api/session/local-all
|
// POST: api/session/local-all
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const uuidv4 = require("uuid").v4
|
||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const serverVersion = require('../../package.json').version
|
const serverVersion = require('../../package.json').version
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
@@ -19,6 +20,7 @@ class PlaybackSessionManager {
|
|||||||
constructor() {
|
constructor() {
|
||||||
this.StreamsPath = Path.join(global.MetadataPath, 'streams')
|
this.StreamsPath = Path.join(global.MetadataPath, 'streams')
|
||||||
|
|
||||||
|
this.oldPlaybackSessionMap = {} // TODO: Remove after updated mobile versions
|
||||||
this.sessions = []
|
this.sessions = []
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,13 +76,14 @@ class PlaybackSessionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async syncLocalSessionsRequest(req, res) {
|
async syncLocalSessionsRequest(req, res) {
|
||||||
|
const deviceInfo = await this.getDeviceInfo(req)
|
||||||
const user = req.user
|
const user = req.user
|
||||||
const sessions = req.body.sessions || []
|
const sessions = req.body.sessions || []
|
||||||
|
|
||||||
const syncResults = []
|
const syncResults = []
|
||||||
for (const sessionJson of sessions) {
|
for (const sessionJson of sessions) {
|
||||||
Logger.info(`[PlaybackSessionManager] Syncing local session "${sessionJson.displayTitle}" (${sessionJson.id})`)
|
Logger.info(`[PlaybackSessionManager] Syncing local session "${sessionJson.displayTitle}" (${sessionJson.id})`)
|
||||||
const result = await this.syncLocalSession(user, sessionJson)
|
const result = await this.syncLocalSession(user, sessionJson, deviceInfo)
|
||||||
syncResults.push(result)
|
syncResults.push(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +92,7 @@ class PlaybackSessionManager {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async syncLocalSession(user, sessionJson) {
|
async syncLocalSession(user, sessionJson, deviceInfo) {
|
||||||
const libraryItem = Database.getLibraryItem(sessionJson.libraryItemId)
|
const libraryItem = Database.getLibraryItem(sessionJson.libraryItemId)
|
||||||
const episode = (sessionJson.episodeId && libraryItem && libraryItem.isPodcast) ? libraryItem.media.getEpisode(sessionJson.episodeId) : null
|
const episode = (sessionJson.episodeId && libraryItem && libraryItem.isPodcast) ? libraryItem.media.getEpisode(sessionJson.episodeId) : null
|
||||||
if (!libraryItem || (libraryItem.isPodcast && !episode)) {
|
if (!libraryItem || (libraryItem.isPodcast && !episode)) {
|
||||||
@@ -101,10 +104,40 @@ class PlaybackSessionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sessionJson.userId = user.id
|
||||||
|
sessionJson.serverVersion = serverVersion
|
||||||
|
|
||||||
|
// TODO: Temp update local playback session id to uuidv4 & library item/book/episode ids
|
||||||
|
if (sessionJson.id?.startsWith('play_local_')) {
|
||||||
|
if (!this.oldPlaybackSessionMap[sessionJson.id]) {
|
||||||
|
const newSessionId = uuidv4()
|
||||||
|
this.oldPlaybackSessionMap[sessionJson.id] = newSessionId
|
||||||
|
sessionJson.id = newSessionId
|
||||||
|
} else {
|
||||||
|
sessionJson.id = this.oldPlaybackSessionMap[sessionJson.id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sessionJson.libraryItemId !== libraryItem.id) {
|
||||||
|
Logger.info(`[PlaybackSessionManager] Mapped old libraryItemId "${sessionJson.libraryItemId}" to ${libraryItem.id}`)
|
||||||
|
sessionJson.libraryItemId = libraryItem.id
|
||||||
|
sessionJson.bookId = episode ? null : libraryItem.media.id
|
||||||
|
}
|
||||||
|
if (!sessionJson.bookId && !episode) {
|
||||||
|
sessionJson.bookId = libraryItem.media.id
|
||||||
|
}
|
||||||
|
if (episode && sessionJson.episodeId !== episode.id) {
|
||||||
|
Logger.info(`[PlaybackSessionManager] Mapped old episodeId "${sessionJson.episodeId}" to ${episode.id}`)
|
||||||
|
sessionJson.episodeId = episode.id
|
||||||
|
}
|
||||||
|
if (sessionJson.libraryId !== libraryItem.libraryId) {
|
||||||
|
sessionJson.libraryId = libraryItem.libraryId
|
||||||
|
}
|
||||||
|
|
||||||
let session = await Database.getPlaybackSession(sessionJson.id)
|
let session = await Database.getPlaybackSession(sessionJson.id)
|
||||||
if (!session) {
|
if (!session) {
|
||||||
// New session from local
|
// New session from local
|
||||||
session = new PlaybackSession(sessionJson)
|
session = new PlaybackSession(sessionJson)
|
||||||
|
session.deviceInfo = deviceInfo
|
||||||
Logger.debug(`[PlaybackSessionManager] Inserting new session for "${session.displayTitle}" (${session.id})`)
|
Logger.debug(`[PlaybackSessionManager] Inserting new session for "${session.displayTitle}" (${session.id})`)
|
||||||
await Database.createPlaybackSession(session)
|
await Database.createPlaybackSession(session)
|
||||||
} else {
|
} else {
|
||||||
@@ -152,8 +185,11 @@ class PlaybackSessionManager {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
async syncLocalSessionRequest(user, sessionJson, res) {
|
async syncLocalSessionRequest(req, res) {
|
||||||
const result = await this.syncLocalSession(user, sessionJson)
|
const deviceInfo = await this.getDeviceInfo(req)
|
||||||
|
const user = req.user
|
||||||
|
const sessionJson = req.body
|
||||||
|
const result = await this.syncLocalSession(user, sessionJson, deviceInfo)
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
res.status(500).send(result.error)
|
res.status(500).send(result.error)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -35,8 +35,12 @@ class RssFeedManager {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate all feeds and remove invalid
|
||||||
|
*/
|
||||||
async init() {
|
async init() {
|
||||||
for (const feed of Database.feeds) {
|
const feeds = await Database.models.feed.getOldFeeds()
|
||||||
|
for (const feed of feeds) {
|
||||||
// Remove invalid feeds
|
// Remove invalid feeds
|
||||||
if (!this.validateFeedEntity(feed)) {
|
if (!this.validateFeedEntity(feed)) {
|
||||||
await Database.removeFeed(feed.id)
|
await Database.removeFeed(feed.id)
|
||||||
@@ -44,20 +48,35 @@ class RssFeedManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find open feed for an entity (e.g. collection id, playlist id, library item id)
|
||||||
|
* @param {string} entityId
|
||||||
|
* @returns {Promise<objects.Feed>} oldFeed
|
||||||
|
*/
|
||||||
findFeedForEntityId(entityId) {
|
findFeedForEntityId(entityId) {
|
||||||
return Database.feeds.find(feed => feed.entityId === entityId)
|
return Database.models.feed.findOneOld({ entityId })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find open feed for a slug
|
||||||
|
* @param {string} slug
|
||||||
|
* @returns {Promise<objects.Feed>} oldFeed
|
||||||
|
*/
|
||||||
findFeedBySlug(slug) {
|
findFeedBySlug(slug) {
|
||||||
return Database.feeds.find(feed => feed.slug === slug)
|
return Database.models.feed.findOneOld({ slug })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find open feed for a slug
|
||||||
|
* @param {string} slug
|
||||||
|
* @returns {Promise<objects.Feed>} oldFeed
|
||||||
|
*/
|
||||||
findFeed(id) {
|
findFeed(id) {
|
||||||
return Database.feeds.find(feed => feed.id === id)
|
return Database.models.feed.findByPkOld(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFeed(req, res) {
|
async getFeed(req, res) {
|
||||||
const feed = this.findFeedBySlug(req.params.slug)
|
const feed = await this.findFeedBySlug(req.params.slug)
|
||||||
if (!feed) {
|
if (!feed) {
|
||||||
Logger.warn(`[RssFeedManager] Feed not found ${req.params.slug}`)
|
Logger.warn(`[RssFeedManager] Feed not found ${req.params.slug}`)
|
||||||
res.sendStatus(404)
|
res.sendStatus(404)
|
||||||
@@ -134,8 +153,8 @@ class RssFeedManager {
|
|||||||
res.send(xml)
|
res.send(xml)
|
||||||
}
|
}
|
||||||
|
|
||||||
getFeedItem(req, res) {
|
async getFeedItem(req, res) {
|
||||||
const feed = this.findFeedBySlug(req.params.slug)
|
const feed = await this.findFeedBySlug(req.params.slug)
|
||||||
if (!feed) {
|
if (!feed) {
|
||||||
Logger.debug(`[RssFeedManager] Feed not found ${req.params.slug}`)
|
Logger.debug(`[RssFeedManager] Feed not found ${req.params.slug}`)
|
||||||
res.sendStatus(404)
|
res.sendStatus(404)
|
||||||
@@ -150,8 +169,8 @@ class RssFeedManager {
|
|||||||
res.sendFile(episodePath)
|
res.sendFile(episodePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
getFeedCover(req, res) {
|
async getFeedCover(req, res) {
|
||||||
const feed = this.findFeedBySlug(req.params.slug)
|
const feed = await this.findFeedBySlug(req.params.slug)
|
||||||
if (!feed) {
|
if (!feed) {
|
||||||
Logger.debug(`[RssFeedManager] Feed not found ${req.params.slug}`)
|
Logger.debug(`[RssFeedManager] Feed not found ${req.params.slug}`)
|
||||||
res.sendStatus(404)
|
res.sendStatus(404)
|
||||||
@@ -225,7 +244,7 @@ class RssFeedManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async closeRssFeed(req, res) {
|
async closeRssFeed(req, res) {
|
||||||
const feed = this.findFeed(req.params.id)
|
const feed = await this.findFeed(req.params.id)
|
||||||
if (!feed) {
|
if (!feed) {
|
||||||
Logger.error(`[RssFeedManager] RSS feed not found with id "${req.params.id}"`)
|
Logger.error(`[RssFeedManager] RSS feed not found with id "${req.params.id}"`)
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
@@ -234,8 +253,8 @@ class RssFeedManager {
|
|||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
closeFeedForEntityId(entityId) {
|
async closeFeedForEntityId(entityId) {
|
||||||
const feed = this.findFeedForEntityId(entityId)
|
const feed = await this.findFeedForEntityId(entityId)
|
||||||
if (!feed) return
|
if (!feed) return
|
||||||
return this.handleCloseFeed(feed)
|
return this.handleCloseFeed(feed)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,53 @@ module.exports = (sequelize) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all library item ids that have an open feed (used in library filter)
|
||||||
|
* @returns {Promise<Array<String>>} array of library item ids
|
||||||
|
*/
|
||||||
|
static async findAllLibraryItemIds() {
|
||||||
|
const feeds = await this.findAll({
|
||||||
|
attributes: ['entityId'],
|
||||||
|
where: {
|
||||||
|
entityType: 'libraryItem'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return feeds.map(f => f.entityId).filter(f => f) || []
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find feed where and return oldFeed
|
||||||
|
* @param {object} where sequelize where object
|
||||||
|
* @returns {Promise<objects.Feed>} oldFeed
|
||||||
|
*/
|
||||||
|
static async findOneOld(where) {
|
||||||
|
if (!where) return null
|
||||||
|
const feedExpanded = await this.findOne({
|
||||||
|
where,
|
||||||
|
include: {
|
||||||
|
model: sequelize.models.feedEpisode
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!feedExpanded) return null
|
||||||
|
return this.getOldFeed(feedExpanded)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find feed and return oldFeed
|
||||||
|
* @param {string} id
|
||||||
|
* @returns {Promise<objects.Feed>} oldFeed
|
||||||
|
*/
|
||||||
|
static async findByPkOld(id) {
|
||||||
|
if (!id) return null
|
||||||
|
const feedExpanded = await this.findByPk(id, {
|
||||||
|
include: {
|
||||||
|
model: sequelize.models.feedEpisode
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!feedExpanded) return null
|
||||||
|
return this.getOldFeed(feedExpanded)
|
||||||
|
}
|
||||||
|
|
||||||
static async fullCreateFromOld(oldFeed) {
|
static async fullCreateFromOld(oldFeed) {
|
||||||
const feedObj = this.getFromOld(oldFeed)
|
const feedObj = this.getFromOld(oldFeed)
|
||||||
const newFeed = await this.create(feedObj)
|
const newFeed = await this.create(feedObj)
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ module.exports = (sequelize) => {
|
|||||||
class Library extends Model {
|
class Library extends Model {
|
||||||
static async getAllOldLibraries() {
|
static async getAllOldLibraries() {
|
||||||
const libraries = await this.findAll({
|
const libraries = await this.findAll({
|
||||||
include: sequelize.models.libraryFolder
|
include: sequelize.models.libraryFolder,
|
||||||
|
order: [['displayOrder', 'ASC']]
|
||||||
})
|
})
|
||||||
return libraries.map(lib => this.getOldLibrary(lib))
|
return libraries.map(lib => this.getOldLibrary(lib))
|
||||||
}
|
}
|
||||||
@@ -22,6 +23,7 @@ module.exports = (sequelize) => {
|
|||||||
})
|
})
|
||||||
return new oldLibrary({
|
return new oldLibrary({
|
||||||
id: libraryExpanded.id,
|
id: libraryExpanded.id,
|
||||||
|
oldLibraryId: libraryExpanded.extraData?.oldLibraryId || null,
|
||||||
name: libraryExpanded.name,
|
name: libraryExpanded.name,
|
||||||
folders,
|
folders,
|
||||||
displayOrder: libraryExpanded.displayOrder,
|
displayOrder: libraryExpanded.displayOrder,
|
||||||
@@ -92,6 +94,10 @@ module.exports = (sequelize) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static getFromOld(oldLibrary) {
|
static getFromOld(oldLibrary) {
|
||||||
|
const extraData = {}
|
||||||
|
if (oldLibrary.oldLibraryId) {
|
||||||
|
extraData.oldLibraryId = oldLibrary.oldLibraryId
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
id: oldLibrary.id,
|
id: oldLibrary.id,
|
||||||
name: oldLibrary.name,
|
name: oldLibrary.name,
|
||||||
@@ -101,7 +107,8 @@ module.exports = (sequelize) => {
|
|||||||
provider: oldLibrary.provider,
|
provider: oldLibrary.provider,
|
||||||
settings: oldLibrary.settings?.toJSON() || {},
|
settings: oldLibrary.settings?.toJSON() || {},
|
||||||
createdAt: oldLibrary.createdAt,
|
createdAt: oldLibrary.createdAt,
|
||||||
updatedAt: oldLibrary.lastUpdate
|
updatedAt: oldLibrary.lastUpdate,
|
||||||
|
extraData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +134,8 @@ module.exports = (sequelize) => {
|
|||||||
provider: DataTypes.STRING,
|
provider: DataTypes.STRING,
|
||||||
lastScan: DataTypes.DATE,
|
lastScan: DataTypes.DATE,
|
||||||
lastScanVersion: DataTypes.STRING,
|
lastScanVersion: DataTypes.STRING,
|
||||||
settings: DataTypes.JSON
|
settings: DataTypes.JSON,
|
||||||
|
extraData: DataTypes.JSON
|
||||||
}, {
|
}, {
|
||||||
sequelize,
|
sequelize,
|
||||||
modelName: 'library'
|
modelName: 'library'
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ module.exports = (sequelize) => {
|
|||||||
return new oldLibraryItem({
|
return new oldLibraryItem({
|
||||||
id: libraryItemExpanded.id,
|
id: libraryItemExpanded.id,
|
||||||
ino: libraryItemExpanded.ino,
|
ino: libraryItemExpanded.ino,
|
||||||
|
oldLibraryItemId: libraryItemExpanded.extraData?.oldLibraryItemId || null,
|
||||||
libraryId: libraryItemExpanded.libraryId,
|
libraryId: libraryItemExpanded.libraryId,
|
||||||
folderId: libraryItemExpanded.libraryFolderId,
|
folderId: libraryItemExpanded.libraryFolderId,
|
||||||
path: libraryItemExpanded.path,
|
path: libraryItemExpanded.path,
|
||||||
@@ -118,7 +119,7 @@ module.exports = (sequelize) => {
|
|||||||
{
|
{
|
||||||
model: sequelize.models.series,
|
model: sequelize.models.series,
|
||||||
through: {
|
through: {
|
||||||
attributes: ['sequence']
|
attributes: ['id', 'sequence']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -219,7 +220,7 @@ module.exports = (sequelize) => {
|
|||||||
hasUpdates = true
|
hasUpdates = true
|
||||||
} else if (existingSeriesMatch.bookSeries.sequence !== updatedSeries.sequence) {
|
} else if (existingSeriesMatch.bookSeries.sequence !== updatedSeries.sequence) {
|
||||||
Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${updatedSeries.name}" sequence was updated from "${existingSeriesMatch.bookSeries.sequence}" to "${updatedSeries.sequence}"`)
|
Logger.dev(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${updatedSeries.name}" sequence was updated from "${existingSeriesMatch.bookSeries.sequence}" to "${updatedSeries.sequence}"`)
|
||||||
await existingSeriesMatch.bookSeries.update({ sequence: updatedSeries.sequence })
|
await existingSeriesMatch.bookSeries.update({ id: updatedSeries.id, sequence: updatedSeries.sequence })
|
||||||
hasUpdates = true
|
hasUpdates = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -261,6 +262,10 @@ module.exports = (sequelize) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static getFromOld(oldLibraryItem) {
|
static getFromOld(oldLibraryItem) {
|
||||||
|
const extraData = {}
|
||||||
|
if (oldLibraryItem.oldLibraryItemId) {
|
||||||
|
extraData.oldLibraryItemId = oldLibraryItem.oldLibraryItemId
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
id: oldLibraryItem.id,
|
id: oldLibraryItem.id,
|
||||||
ino: oldLibraryItem.ino,
|
ino: oldLibraryItem.ino,
|
||||||
@@ -278,7 +283,8 @@ module.exports = (sequelize) => {
|
|||||||
lastScanVersion: oldLibraryItem.scanVersion,
|
lastScanVersion: oldLibraryItem.scanVersion,
|
||||||
libraryId: oldLibraryItem.libraryId,
|
libraryId: oldLibraryItem.libraryId,
|
||||||
libraryFolderId: oldLibraryItem.folderId,
|
libraryFolderId: oldLibraryItem.folderId,
|
||||||
libraryFiles: oldLibraryItem.libraryFiles?.map(lf => lf.toJSON()) || []
|
libraryFiles: oldLibraryItem.libraryFiles?.map(lf => lf.toJSON()) || [],
|
||||||
|
extraData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,7 +323,8 @@ module.exports = (sequelize) => {
|
|||||||
birthtime: DataTypes.DATE(6),
|
birthtime: DataTypes.DATE(6),
|
||||||
lastScan: DataTypes.DATE,
|
lastScan: DataTypes.DATE,
|
||||||
lastScanVersion: DataTypes.STRING,
|
lastScanVersion: DataTypes.STRING,
|
||||||
libraryFiles: DataTypes.JSON
|
libraryFiles: DataTypes.JSON,
|
||||||
|
extraData: DataTypes.JSON
|
||||||
}, {
|
}, {
|
||||||
sequelize,
|
sequelize,
|
||||||
modelName: 'libraryItem'
|
modelName: 'libraryItem'
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ module.exports = (sequelize) => {
|
|||||||
libraryItemId: libraryItemId || null,
|
libraryItemId: libraryItemId || null,
|
||||||
podcastId: this.podcastId,
|
podcastId: this.podcastId,
|
||||||
id: this.id,
|
id: this.id,
|
||||||
|
oldEpisodeId: this.extraData?.oldEpisodeId || null,
|
||||||
index: this.index,
|
index: this.index,
|
||||||
season: this.season,
|
season: this.season,
|
||||||
episode: this.episode,
|
episode: this.episode,
|
||||||
@@ -38,6 +39,10 @@ module.exports = (sequelize) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static getFromOld(oldEpisode) {
|
static getFromOld(oldEpisode) {
|
||||||
|
const extraData = {}
|
||||||
|
if (oldEpisode.oldEpisodeId) {
|
||||||
|
extraData.oldEpisodeId = oldEpisode.oldEpisodeId
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
id: oldEpisode.id,
|
id: oldEpisode.id,
|
||||||
index: oldEpisode.index,
|
index: oldEpisode.index,
|
||||||
@@ -54,7 +59,8 @@ module.exports = (sequelize) => {
|
|||||||
publishedAt: oldEpisode.publishedAt,
|
publishedAt: oldEpisode.publishedAt,
|
||||||
podcastId: oldEpisode.podcastId,
|
podcastId: oldEpisode.podcastId,
|
||||||
audioFile: oldEpisode.audioFile?.toJSON() || null,
|
audioFile: oldEpisode.audioFile?.toJSON() || null,
|
||||||
chapters: oldEpisode.chapters
|
chapters: oldEpisode.chapters,
|
||||||
|
extraData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,7 +85,8 @@ module.exports = (sequelize) => {
|
|||||||
publishedAt: DataTypes.DATE,
|
publishedAt: DataTypes.DATE,
|
||||||
|
|
||||||
audioFile: DataTypes.JSON,
|
audioFile: DataTypes.JSON,
|
||||||
chapters: DataTypes.JSON
|
chapters: DataTypes.JSON,
|
||||||
|
extraData: DataTypes.JSON
|
||||||
}, {
|
}, {
|
||||||
sequelize,
|
sequelize,
|
||||||
modelName: 'podcastEpisode'
|
modelName: 'podcastEpisode'
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const { filePathToPOSIX } = require('../utils/fileUtils')
|
|||||||
class Library {
|
class Library {
|
||||||
constructor(library = null) {
|
constructor(library = null) {
|
||||||
this.id = null
|
this.id = null
|
||||||
|
this.oldLibraryId = null // TODO: Temp
|
||||||
this.name = null
|
this.name = null
|
||||||
this.folders = []
|
this.folders = []
|
||||||
this.displayOrder = 1
|
this.displayOrder = 1
|
||||||
@@ -39,6 +40,7 @@ class Library {
|
|||||||
|
|
||||||
construct(library) {
|
construct(library) {
|
||||||
this.id = library.id
|
this.id = library.id
|
||||||
|
this.oldLibraryId = library.oldLibraryId
|
||||||
this.name = library.name
|
this.name = library.name
|
||||||
this.folders = (library.folders || []).map(f => new Folder(f))
|
this.folders = (library.folders || []).map(f => new Folder(f))
|
||||||
this.displayOrder = library.displayOrder || 1
|
this.displayOrder = library.displayOrder || 1
|
||||||
@@ -74,6 +76,7 @@ class Library {
|
|||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
|
oldLibraryId: this.oldLibraryId,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
folders: (this.folders || []).map(f => f.toJSON()),
|
folders: (this.folders || []).map(f => f.toJSON()),
|
||||||
displayOrder: this.displayOrder,
|
displayOrder: this.displayOrder,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class LibraryItem {
|
|||||||
constructor(libraryItem = null) {
|
constructor(libraryItem = null) {
|
||||||
this.id = null
|
this.id = null
|
||||||
this.ino = null // Inode
|
this.ino = null // Inode
|
||||||
|
this.oldLibraryItemId = null
|
||||||
|
|
||||||
this.libraryId = null
|
this.libraryId = null
|
||||||
this.folderId = null
|
this.folderId = null
|
||||||
@@ -52,6 +53,7 @@ class LibraryItem {
|
|||||||
construct(libraryItem) {
|
construct(libraryItem) {
|
||||||
this.id = libraryItem.id
|
this.id = libraryItem.id
|
||||||
this.ino = libraryItem.ino || null
|
this.ino = libraryItem.ino || null
|
||||||
|
this.oldLibraryItemId = libraryItem.oldLibraryItemId
|
||||||
this.libraryId = libraryItem.libraryId
|
this.libraryId = libraryItem.libraryId
|
||||||
this.folderId = libraryItem.folderId
|
this.folderId = libraryItem.folderId
|
||||||
this.path = libraryItem.path
|
this.path = libraryItem.path
|
||||||
@@ -97,6 +99,7 @@ class LibraryItem {
|
|||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
ino: this.ino,
|
ino: this.ino,
|
||||||
|
oldLibraryItemId: this.oldLibraryItemId,
|
||||||
libraryId: this.libraryId,
|
libraryId: this.libraryId,
|
||||||
folderId: this.folderId,
|
folderId: this.folderId,
|
||||||
path: this.path,
|
path: this.path,
|
||||||
@@ -121,6 +124,7 @@ class LibraryItem {
|
|||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
ino: this.ino,
|
ino: this.ino,
|
||||||
|
oldLibraryItemId: this.oldLibraryItemId,
|
||||||
libraryId: this.libraryId,
|
libraryId: this.libraryId,
|
||||||
folderId: this.folderId,
|
folderId: this.folderId,
|
||||||
path: this.path,
|
path: this.path,
|
||||||
@@ -145,6 +149,7 @@ class LibraryItem {
|
|||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
ino: this.ino,
|
ino: this.ino,
|
||||||
|
oldLibraryItemId: this.oldLibraryItemId,
|
||||||
libraryId: this.libraryId,
|
libraryId: this.libraryId,
|
||||||
folderId: this.folderId,
|
folderId: this.folderId,
|
||||||
path: this.path,
|
path: this.path,
|
||||||
|
|||||||
@@ -115,13 +115,24 @@ class PlaybackSession {
|
|||||||
this.userId = session.userId
|
this.userId = session.userId
|
||||||
this.libraryId = session.libraryId || null
|
this.libraryId = session.libraryId || null
|
||||||
this.libraryItemId = session.libraryItemId
|
this.libraryItemId = session.libraryItemId
|
||||||
this.bookId = session.bookId
|
this.bookId = session.bookId || null
|
||||||
this.episodeId = session.episodeId
|
this.episodeId = session.episodeId
|
||||||
this.mediaType = session.mediaType
|
this.mediaType = session.mediaType
|
||||||
this.duration = session.duration
|
this.duration = session.duration
|
||||||
this.playMethod = session.playMethod
|
this.playMethod = session.playMethod
|
||||||
this.mediaPlayer = session.mediaPlayer || null
|
this.mediaPlayer = session.mediaPlayer || null
|
||||||
|
|
||||||
|
// Temp do not store old IDs
|
||||||
|
if (this.libraryId?.startsWith('lib_')) {
|
||||||
|
this.libraryId = null
|
||||||
|
}
|
||||||
|
if (this.libraryItemId?.startsWith('li_') || this.libraryItemId?.startsWith('local_')) {
|
||||||
|
this.libraryItemId = null
|
||||||
|
}
|
||||||
|
if (this.episodeId?.startsWith('ep_') || this.episodeId?.startsWith('local_')) {
|
||||||
|
this.episodeId = null
|
||||||
|
}
|
||||||
|
|
||||||
if (session.deviceInfo instanceof DeviceInfo) {
|
if (session.deviceInfo instanceof DeviceInfo) {
|
||||||
this.deviceInfo = new DeviceInfo(session.deviceInfo.toJSON())
|
this.deviceInfo = new DeviceInfo(session.deviceInfo.toJSON())
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class PodcastEpisode {
|
|||||||
this.libraryItemId = null
|
this.libraryItemId = null
|
||||||
this.podcastId = null
|
this.podcastId = null
|
||||||
this.id = null
|
this.id = null
|
||||||
|
this.oldEpisodeId = null
|
||||||
this.index = null
|
this.index = null
|
||||||
|
|
||||||
this.season = null
|
this.season = null
|
||||||
@@ -36,6 +37,7 @@ class PodcastEpisode {
|
|||||||
this.libraryItemId = episode.libraryItemId
|
this.libraryItemId = episode.libraryItemId
|
||||||
this.podcastId = episode.podcastId
|
this.podcastId = episode.podcastId
|
||||||
this.id = episode.id
|
this.id = episode.id
|
||||||
|
this.oldEpisodeId = episode.oldEpisodeId
|
||||||
this.index = episode.index
|
this.index = episode.index
|
||||||
this.season = episode.season
|
this.season = episode.season
|
||||||
this.episode = episode.episode
|
this.episode = episode.episode
|
||||||
@@ -59,6 +61,7 @@ class PodcastEpisode {
|
|||||||
libraryItemId: this.libraryItemId,
|
libraryItemId: this.libraryItemId,
|
||||||
podcastId: this.podcastId,
|
podcastId: this.podcastId,
|
||||||
id: this.id,
|
id: this.id,
|
||||||
|
oldEpisodeId: this.oldEpisodeId,
|
||||||
index: this.index,
|
index: this.index,
|
||||||
season: this.season,
|
season: this.season,
|
||||||
episode: this.episode,
|
episode: this.episode,
|
||||||
@@ -81,6 +84,7 @@ class PodcastEpisode {
|
|||||||
libraryItemId: this.libraryItemId,
|
libraryItemId: this.libraryItemId,
|
||||||
podcastId: this.podcastId,
|
podcastId: this.podcastId,
|
||||||
id: this.id,
|
id: this.id,
|
||||||
|
oldEpisodeId: this.oldEpisodeId,
|
||||||
index: this.index,
|
index: this.index,
|
||||||
season: this.season,
|
season: this.season,
|
||||||
episode: this.episode,
|
episode: this.episode,
|
||||||
|
|||||||
@@ -335,6 +335,11 @@ class Podcast {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getEpisode(episodeId) {
|
getEpisode(episodeId) {
|
||||||
|
if (!episodeId) return null
|
||||||
|
|
||||||
|
// Support old episode ids for mobile downloads
|
||||||
|
if (episodeId.startsWith('ep_')) return this.episodes.find(ep => ep.oldEpisodeId == episodeId)
|
||||||
|
|
||||||
return this.episodes.find(ep => ep.id == episodeId)
|
return this.episodes.find(ep => ep.id == episodeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -416,5 +416,23 @@ class User {
|
|||||||
if (!progress) return false
|
if (!progress) return false
|
||||||
return progress.removeFromContinueListening()
|
return progress.removeFromContinueListening()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of podcast episodes not finished for library item
|
||||||
|
* Note: libraryItem passed in from libraryHelpers is not a LibraryItem class instance
|
||||||
|
* @param {LibraryItem|object} libraryItem
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
getNumEpisodesIncompleteForPodcast(libraryItem) {
|
||||||
|
if (!libraryItem?.media.episodes) return 0
|
||||||
|
let numEpisodesIncomplete = 0
|
||||||
|
for (const episode of libraryItem.media.episodes) {
|
||||||
|
const mediaProgress = this.getMediaProgress(libraryItem.id, episode.id)
|
||||||
|
if (!mediaProgress?.isFinished) {
|
||||||
|
numEpisodesIncomplete++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return numEpisodesIncomplete
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = User
|
module.exports = User
|
||||||
@@ -11,10 +11,10 @@ module.exports = {
|
|||||||
return Buffer.from(decodeURIComponent(text), 'base64').toString()
|
return Buffer.from(decodeURIComponent(text), 'base64').toString()
|
||||||
},
|
},
|
||||||
|
|
||||||
getFilteredLibraryItems(libraryItems, filterBy, user, feedsArray) {
|
async getFilteredLibraryItems(libraryItems, filterBy, user) {
|
||||||
let filtered = libraryItems
|
let filtered = libraryItems
|
||||||
|
|
||||||
const searchGroups = ['genres', 'tags', 'series', 'authors', 'progress', 'narrators', 'missing', 'languages', 'tracks', 'ebooks']
|
const searchGroups = ['genres', 'tags', 'series', 'authors', 'progress', 'narrators', 'publishers', 'missing', 'languages', 'tracks', 'ebooks']
|
||||||
const group = searchGroups.find(_group => filterBy.startsWith(_group + '.'))
|
const group = searchGroups.find(_group => filterBy.startsWith(_group + '.'))
|
||||||
if (group) {
|
if (group) {
|
||||||
const filterVal = filterBy.replace(`${group}.`, '')
|
const filterVal = filterBy.replace(`${group}.`, '')
|
||||||
@@ -29,6 +29,7 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
else if (group === 'authors') filtered = filtered.filter(li => li.isBook && li.media.metadata.hasAuthor(filter))
|
else if (group === 'authors') filtered = filtered.filter(li => li.isBook && li.media.metadata.hasAuthor(filter))
|
||||||
else if (group === 'narrators') filtered = filtered.filter(li => li.isBook && li.media.metadata.hasNarrator(filter))
|
else if (group === 'narrators') filtered = filtered.filter(li => li.isBook && li.media.metadata.hasNarrator(filter))
|
||||||
|
else if (group === 'publishers') filtered = filtered.filter(li => li.isBook && li.media.metadata.publisher === filter)
|
||||||
else if (group === 'progress') {
|
else if (group === 'progress') {
|
||||||
filtered = filtered.filter(li => {
|
filtered = filtered.filter(li => {
|
||||||
const itemProgress = user.getMediaProgress(li.id)
|
const itemProgress = user.getMediaProgress(li.id)
|
||||||
@@ -70,7 +71,9 @@ module.exports = {
|
|||||||
} else if (filterBy === 'issues') {
|
} else if (filterBy === 'issues') {
|
||||||
filtered = filtered.filter(li => li.hasIssues)
|
filtered = filtered.filter(li => li.hasIssues)
|
||||||
} else if (filterBy === 'feed-open') {
|
} else if (filterBy === 'feed-open') {
|
||||||
filtered = filtered.filter(li => feedsArray.some(feed => feed.entityId === li.id))
|
const libraryItemIdsWithFeed = await Database.models.feed.findAllLibraryItemIds()
|
||||||
|
filtered = filtered.filter(li => libraryItemIdsWithFeed.includes(li.id))
|
||||||
|
// filtered = filtered.filter(li => feedsArray.some(feed => feed.entityId === li.id))
|
||||||
} else if (filterBy === 'abridged') {
|
} else if (filterBy === 'abridged') {
|
||||||
filtered = filtered.filter(li => !!li.media.metadata?.abridged)
|
filtered = filtered.filter(li => !!li.media.metadata?.abridged)
|
||||||
} else if (filterBy === 'ebook') {
|
} else if (filterBy === 'ebook') {
|
||||||
@@ -82,16 +85,17 @@ module.exports = {
|
|||||||
|
|
||||||
// Returns false if should be filtered out
|
// Returns false if should be filtered out
|
||||||
checkFilterForSeriesLibraryItem(libraryItem, filterBy) {
|
checkFilterForSeriesLibraryItem(libraryItem, filterBy) {
|
||||||
var searchGroups = ['genres', 'tags', 'authors', 'progress', 'narrators', 'languages']
|
const searchGroups = ['genres', 'tags', 'authors', 'progress', 'narrators', 'publishers', 'languages']
|
||||||
var group = searchGroups.find(_group => filterBy.startsWith(_group + '.'))
|
const group = searchGroups.find(_group => filterBy.startsWith(_group + '.'))
|
||||||
if (group) {
|
if (group) {
|
||||||
var filterVal = filterBy.replace(`${group}.`, '')
|
const filterVal = filterBy.replace(`${group}.`, '')
|
||||||
var filter = this.decode(filterVal)
|
const filter = this.decode(filterVal)
|
||||||
|
|
||||||
if (group === 'genres') return libraryItem.media.metadata.genres.includes(filter)
|
if (group === 'genres') return libraryItem.media.metadata.genres.includes(filter)
|
||||||
else if (group === 'tags') return libraryItem.media.tags.includes(filter)
|
else if (group === 'tags') return libraryItem.media.tags.includes(filter)
|
||||||
else if (group === 'authors') return libraryItem.isBook && libraryItem.media.metadata.hasAuthor(filter)
|
else if (group === 'authors') return libraryItem.isBook && libraryItem.media.metadata.hasAuthor(filter)
|
||||||
else if (group === 'narrators') return libraryItem.isBook && libraryItem.media.metadata.hasNarrator(filter)
|
else if (group === 'narrators') return libraryItem.isBook && libraryItem.media.metadata.hasNarrator(filter)
|
||||||
|
else if (group === 'publishers') return libraryItem.isBook && libraryItem.media.metadata.publisher === filter
|
||||||
else if (group === 'languages') {
|
else if (group === 'languages') {
|
||||||
return libraryItem.media.metadata.language === filter
|
return libraryItem.media.metadata.language === filter
|
||||||
}
|
}
|
||||||
@@ -123,27 +127,28 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getDistinctFilterDataNew(libraryItems) {
|
getDistinctFilterDataNew(libraryItems) {
|
||||||
var data = {
|
const data = {
|
||||||
authors: [],
|
authors: [],
|
||||||
genres: [],
|
genres: [],
|
||||||
tags: [],
|
tags: [],
|
||||||
series: [],
|
series: [],
|
||||||
narrators: [],
|
narrators: [],
|
||||||
languages: []
|
languages: [],
|
||||||
|
publishers: []
|
||||||
}
|
}
|
||||||
libraryItems.forEach((li) => {
|
libraryItems.forEach((li) => {
|
||||||
var mediaMetadata = li.media.metadata
|
const mediaMetadata = li.media.metadata
|
||||||
if (mediaMetadata.authors && mediaMetadata.authors.length) {
|
if (mediaMetadata.authors?.length) {
|
||||||
mediaMetadata.authors.forEach((author) => {
|
mediaMetadata.authors.forEach((author) => {
|
||||||
if (author && !data.authors.find(au => au.id === author.id)) data.authors.push({ id: author.id, name: author.name })
|
if (author && !data.authors.some(au => au.id === author.id)) data.authors.push({ id: author.id, name: author.name })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (mediaMetadata.series && mediaMetadata.series.length) {
|
if (mediaMetadata.series?.length) {
|
||||||
mediaMetadata.series.forEach((series) => {
|
mediaMetadata.series.forEach((series) => {
|
||||||
if (series && !data.series.find(se => se.id === series.id)) data.series.push({ id: series.id, name: series.name })
|
if (series && !data.series.some(se => se.id === series.id)) data.series.push({ id: series.id, name: series.name })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (mediaMetadata.genres && mediaMetadata.genres.length) {
|
if (mediaMetadata.genres?.length) {
|
||||||
mediaMetadata.genres.forEach((genre) => {
|
mediaMetadata.genres.forEach((genre) => {
|
||||||
if (genre && !data.genres.includes(genre)) data.genres.push(genre)
|
if (genre && !data.genres.includes(genre)) data.genres.push(genre)
|
||||||
})
|
})
|
||||||
@@ -153,18 +158,24 @@ module.exports = {
|
|||||||
if (tag && !data.tags.includes(tag)) data.tags.push(tag)
|
if (tag && !data.tags.includes(tag)) data.tags.push(tag)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (mediaMetadata.narrators && mediaMetadata.narrators.length) {
|
if (mediaMetadata.narrators?.length) {
|
||||||
mediaMetadata.narrators.forEach((narrator) => {
|
mediaMetadata.narrators.forEach((narrator) => {
|
||||||
if (narrator && !data.narrators.includes(narrator)) data.narrators.push(narrator)
|
if (narrator && !data.narrators.includes(narrator)) data.narrators.push(narrator)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (mediaMetadata.language && !data.languages.includes(mediaMetadata.language)) data.languages.push(mediaMetadata.language)
|
if (mediaMetadata.publisher && !data.publishers.includes(mediaMetadata.publisher)) {
|
||||||
|
data.publishers.push(mediaMetadata.publisher)
|
||||||
|
}
|
||||||
|
if (mediaMetadata.language && !data.languages.includes(mediaMetadata.language)) {
|
||||||
|
data.languages.push(mediaMetadata.language)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
data.authors = naturalSort(data.authors).asc(au => au.name)
|
data.authors = naturalSort(data.authors).asc(au => au.name)
|
||||||
data.genres = naturalSort(data.genres).asc()
|
data.genres = naturalSort(data.genres).asc()
|
||||||
data.tags = naturalSort(data.tags).asc()
|
data.tags = naturalSort(data.tags).asc()
|
||||||
data.series = naturalSort(data.series).asc(se => se.name)
|
data.series = naturalSort(data.series).asc(se => se.name)
|
||||||
data.narrators = naturalSort(data.narrators).asc()
|
data.narrators = naturalSort(data.narrators).asc()
|
||||||
|
data.publishers = naturalSort(data.publishers).asc()
|
||||||
data.languages = naturalSort(data.languages).asc()
|
data.languages = naturalSort(data.languages).asc()
|
||||||
return data
|
return data
|
||||||
},
|
},
|
||||||
@@ -347,10 +358,11 @@ module.exports = {
|
|||||||
return filteredLibraryItems
|
return filteredLibraryItems
|
||||||
},
|
},
|
||||||
|
|
||||||
buildPersonalizedShelves(ctx, user, libraryItems, library, maxEntitiesPerShelf, include) {
|
async buildPersonalizedShelves(ctx, user, libraryItems, library, maxEntitiesPerShelf, include) {
|
||||||
const mediaType = library.mediaType
|
const mediaType = library.mediaType
|
||||||
const isPodcastLibrary = mediaType === 'podcast'
|
const isPodcastLibrary = mediaType === 'podcast'
|
||||||
const includeRssFeed = include.includes('rssfeed')
|
const includeRssFeed = include.includes('rssfeed')
|
||||||
|
const includeNumEpisodesIncomplete = include.includes('numepisodesincomplete') // Podcasts only
|
||||||
const hideSingleBookSeries = library.settings.hideSingleBookSeries
|
const hideSingleBookSeries = library.settings.hideSingleBookSeries
|
||||||
|
|
||||||
const shelves = [
|
const shelves = [
|
||||||
@@ -447,12 +459,18 @@ module.exports = {
|
|||||||
|
|
||||||
for (const libraryItem of libraryItems) {
|
for (const libraryItem of libraryItems) {
|
||||||
if (libraryItem.addedAt > categoryMap['recently-added'].smallest) {
|
if (libraryItem.addedAt > categoryMap['recently-added'].smallest) {
|
||||||
|
const libraryItemObj = libraryItem.toJSONMinified()
|
||||||
|
|
||||||
|
// add numEpisodesIncomplete if "include=numEpisodesIncomplete" was put in query string (only for podcasts)
|
||||||
|
if (includeNumEpisodesIncomplete && libraryItem.isPodcast) {
|
||||||
|
libraryItemObj.numEpisodesIncomplete = user.getNumEpisodesIncompleteForPodcast(libraryItem)
|
||||||
|
}
|
||||||
|
|
||||||
const indexToPut = categoryMap['recently-added'].items.findIndex(i => libraryItem.addedAt > i.addedAt)
|
const indexToPut = categoryMap['recently-added'].items.findIndex(i => libraryItem.addedAt > i.addedAt)
|
||||||
if (indexToPut >= 0) {
|
if (indexToPut >= 0) {
|
||||||
categoryMap['recently-added'].items.splice(indexToPut, 0, libraryItem.toJSONMinified())
|
categoryMap['recently-added'].items.splice(indexToPut, 0, libraryItemObj)
|
||||||
} else {
|
} else {
|
||||||
categoryMap['recently-added'].items.push(libraryItem.toJSONMinified())
|
categoryMap['recently-added'].items.push(libraryItemObj)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (categoryMap['recently-added'].items.length > maxEntitiesPerShelf) {
|
if (categoryMap['recently-added'].items.length > maxEntitiesPerShelf) {
|
||||||
@@ -830,27 +848,30 @@ module.exports = {
|
|||||||
|
|
||||||
const categoriesWithItems = Object.values(categoryMap).filter(cat => cat.items.length)
|
const categoriesWithItems = Object.values(categoryMap).filter(cat => cat.items.length)
|
||||||
|
|
||||||
return categoriesWithItems.map(cat => {
|
const finalShelves = []
|
||||||
const shelf = shelves.find(s => s.id === cat.id)
|
for (const categoryWithItems of categoriesWithItems) {
|
||||||
shelf.entities = cat.items
|
const shelf = shelves.find(s => s.id === categoryWithItems.id)
|
||||||
|
shelf.entities = categoryWithItems.items
|
||||||
|
|
||||||
// Add rssFeed to entities if query string "include=rssfeed" was on request
|
// Add rssFeed to entities if query string "include=rssfeed" was on request
|
||||||
if (includeRssFeed) {
|
if (includeRssFeed) {
|
||||||
if (shelf.type === 'book' || shelf.type === 'podcast') {
|
if (shelf.type === 'book' || shelf.type === 'podcast') {
|
||||||
shelf.entities = shelf.entities.map((item) => {
|
shelf.entities = await Promise.all(shelf.entities.map(async (item) => {
|
||||||
item.rssFeed = ctx.rssFeedManager.findFeedForEntityId(item.id)?.toJSONMinified() || null
|
const feed = await ctx.rssFeedManager.findFeedForEntityId(item.id)
|
||||||
|
item.rssFeed = feed?.toJSONMinified() || null
|
||||||
return item
|
return item
|
||||||
})
|
}))
|
||||||
} else if (shelf.type === 'series') {
|
} else if (shelf.type === 'series') {
|
||||||
shelf.entities = shelf.entities.map((series) => {
|
shelf.entities = await Promise.all(shelf.entities.map(async (series) => {
|
||||||
series.rssFeed = ctx.rssFeedManager.findFeedForEntityId(series.id)?.toJSONMinified() || null
|
const feed = await ctx.rssFeedManager.findFeedForEntityId(series.id)
|
||||||
|
series.rssFeed = feed?.toJSONMinified() || null
|
||||||
return series
|
return series
|
||||||
})
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
finalShelves.push(shelf)
|
||||||
return shelf
|
}
|
||||||
})
|
return finalShelves
|
||||||
},
|
},
|
||||||
|
|
||||||
groupMusicLibraryItemsIntoAlbums(libraryItems) {
|
groupMusicLibraryItemsIntoAlbums(libraryItems) {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const { DataTypes, QueryInterface } = require('sequelize')
|
||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const uuidv4 = require("uuid").v4
|
const uuidv4 = require("uuid").v4
|
||||||
const Logger = require('../../Logger')
|
const Logger = require('../../Logger')
|
||||||
@@ -17,29 +18,6 @@ const oldDbIdMap = {
|
|||||||
podcasts: {}, // key is library item id
|
podcasts: {}, // key is library item id
|
||||||
devices: {} // key is a json stringify of the old DeviceInfo data OR deviceId if it exists
|
devices: {} // key is a json stringify of the old DeviceInfo data OR deviceId if it exists
|
||||||
}
|
}
|
||||||
const newRecords = {
|
|
||||||
user: [],
|
|
||||||
library: [],
|
|
||||||
libraryFolder: [],
|
|
||||||
author: [],
|
|
||||||
book: [],
|
|
||||||
podcast: [],
|
|
||||||
libraryItem: [],
|
|
||||||
bookAuthor: [],
|
|
||||||
series: [],
|
|
||||||
bookSeries: [],
|
|
||||||
podcastEpisode: [],
|
|
||||||
mediaProgress: [],
|
|
||||||
device: [],
|
|
||||||
playbackSession: [],
|
|
||||||
collection: [],
|
|
||||||
collectionBook: [],
|
|
||||||
playlist: [],
|
|
||||||
playlistMediaItem: [],
|
|
||||||
feed: [],
|
|
||||||
feedEpisode: [],
|
|
||||||
setting: []
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDeviceInfoString(deviceInfo, UserId) {
|
function getDeviceInfoString(deviceInfo, UserId) {
|
||||||
if (!deviceInfo) return null
|
if (!deviceInfo) return null
|
||||||
@@ -60,9 +38,22 @@ function getDeviceInfoString(deviceInfo, UserId) {
|
|||||||
return 'temp-' + Buffer.from(keys.join('-'), 'utf-8').toString('base64')
|
return 'temp-' + Buffer.from(keys.join('-'), 'utf-8').toString('base64')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate oldLibraryItem.media to Book model
|
||||||
|
* Migrate BookSeries and BookAuthor
|
||||||
|
* @param {objects.LibraryItem} oldLibraryItem
|
||||||
|
* @param {object} LibraryItem models.LibraryItem object
|
||||||
|
* @returns {object} { book: object, bookSeries: [], bookAuthor: [] }
|
||||||
|
*/
|
||||||
function migrateBook(oldLibraryItem, LibraryItem) {
|
function migrateBook(oldLibraryItem, LibraryItem) {
|
||||||
const oldBook = oldLibraryItem.media
|
const oldBook = oldLibraryItem.media
|
||||||
|
|
||||||
|
const _newRecords = {
|
||||||
|
book: null,
|
||||||
|
bookSeries: [],
|
||||||
|
bookAuthor: []
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Migrate Book
|
// Migrate Book
|
||||||
//
|
//
|
||||||
@@ -91,17 +82,23 @@ function migrateBook(oldLibraryItem, LibraryItem) {
|
|||||||
tags: oldBook.tags,
|
tags: oldBook.tags,
|
||||||
genres: oldBook.metadata.genres
|
genres: oldBook.metadata.genres
|
||||||
}
|
}
|
||||||
newRecords.book.push(Book)
|
_newRecords.book = Book
|
||||||
oldDbIdMap.books[oldLibraryItem.id] = Book.id
|
oldDbIdMap.books[oldLibraryItem.id] = Book.id
|
||||||
|
|
||||||
//
|
//
|
||||||
// Migrate BookAuthors
|
// Migrate BookAuthors
|
||||||
//
|
//
|
||||||
|
const bookAuthorsInserted = []
|
||||||
for (const oldBookAuthor of oldBook.metadata.authors) {
|
for (const oldBookAuthor of oldBook.metadata.authors) {
|
||||||
if (oldDbIdMap.authors[LibraryItem.libraryId][oldBookAuthor.id]) {
|
if (oldDbIdMap.authors[LibraryItem.libraryId][oldBookAuthor.id]) {
|
||||||
newRecords.bookAuthor.push({
|
const authorId = oldDbIdMap.authors[LibraryItem.libraryId][oldBookAuthor.id]
|
||||||
|
|
||||||
|
if (bookAuthorsInserted.includes(authorId)) continue // Duplicate prevention
|
||||||
|
bookAuthorsInserted.push(authorId)
|
||||||
|
|
||||||
|
_newRecords.bookAuthor.push({
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
authorId: oldDbIdMap.authors[LibraryItem.libraryId][oldBookAuthor.id],
|
authorId,
|
||||||
bookId: Book.id
|
bookId: Book.id
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@@ -112,22 +109,40 @@ function migrateBook(oldLibraryItem, LibraryItem) {
|
|||||||
//
|
//
|
||||||
// Migrate BookSeries
|
// Migrate BookSeries
|
||||||
//
|
//
|
||||||
|
const bookSeriesInserted = []
|
||||||
for (const oldBookSeries of oldBook.metadata.series) {
|
for (const oldBookSeries of oldBook.metadata.series) {
|
||||||
if (oldDbIdMap.series[LibraryItem.libraryId][oldBookSeries.id]) {
|
if (oldDbIdMap.series[LibraryItem.libraryId][oldBookSeries.id]) {
|
||||||
const BookSeries = {
|
const seriesId = oldDbIdMap.series[LibraryItem.libraryId][oldBookSeries.id]
|
||||||
|
|
||||||
|
if (bookSeriesInserted.includes(seriesId)) continue // Duplicate prevention
|
||||||
|
bookSeriesInserted.push(seriesId)
|
||||||
|
|
||||||
|
_newRecords.bookSeries.push({
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
sequence: oldBookSeries.sequence,
|
sequence: oldBookSeries.sequence,
|
||||||
seriesId: oldDbIdMap.series[LibraryItem.libraryId][oldBookSeries.id],
|
seriesId: oldDbIdMap.series[LibraryItem.libraryId][oldBookSeries.id],
|
||||||
bookId: Book.id
|
bookId: Book.id
|
||||||
}
|
})
|
||||||
newRecords.bookSeries.push(BookSeries)
|
|
||||||
} else {
|
} else {
|
||||||
Logger.warn(`[dbMigration] migrateBook: Series not found "${oldBookSeries.name}"`)
|
Logger.warn(`[dbMigration] migrateBook: Series not found "${oldBookSeries.name}"`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return _newRecords
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate oldLibraryItem.media to Podcast model
|
||||||
|
* Migrate PodcastEpisode
|
||||||
|
* @param {objects.LibraryItem} oldLibraryItem
|
||||||
|
* @param {object} LibraryItem models.LibraryItem object
|
||||||
|
* @returns {object} { podcast: object, podcastEpisode: [] }
|
||||||
|
*/
|
||||||
function migratePodcast(oldLibraryItem, LibraryItem) {
|
function migratePodcast(oldLibraryItem, LibraryItem) {
|
||||||
|
const _newRecords = {
|
||||||
|
podcast: null,
|
||||||
|
podcastEpisode: []
|
||||||
|
}
|
||||||
|
|
||||||
const oldPodcast = oldLibraryItem.media
|
const oldPodcast = oldLibraryItem.media
|
||||||
const oldPodcastMetadata = oldPodcast.metadata
|
const oldPodcastMetadata = oldPodcast.metadata
|
||||||
|
|
||||||
@@ -161,7 +176,7 @@ function migratePodcast(oldLibraryItem, LibraryItem) {
|
|||||||
tags: oldPodcast.tags,
|
tags: oldPodcast.tags,
|
||||||
genres: oldPodcastMetadata.genres
|
genres: oldPodcastMetadata.genres
|
||||||
}
|
}
|
||||||
newRecords.podcast.push(Podcast)
|
_newRecords.podcast = Podcast
|
||||||
oldDbIdMap.podcasts[oldLibraryItem.id] = Podcast.id
|
oldDbIdMap.podcasts[oldLibraryItem.id] = Podcast.id
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -173,6 +188,7 @@ function migratePodcast(oldLibraryItem, LibraryItem) {
|
|||||||
|
|
||||||
const PodcastEpisode = {
|
const PodcastEpisode = {
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
|
oldEpisodeId: oldEpisode.id,
|
||||||
index: oldEpisode.index,
|
index: oldEpisode.index,
|
||||||
season: oldEpisode.season || null,
|
season: oldEpisode.season || null,
|
||||||
episode: oldEpisode.episode || null,
|
episode: oldEpisode.episode || null,
|
||||||
@@ -191,12 +207,26 @@ function migratePodcast(oldLibraryItem, LibraryItem) {
|
|||||||
audioFile: oldEpisode.audioFile,
|
audioFile: oldEpisode.audioFile,
|
||||||
chapters: oldEpisode.chapters || []
|
chapters: oldEpisode.chapters || []
|
||||||
}
|
}
|
||||||
newRecords.podcastEpisode.push(PodcastEpisode)
|
_newRecords.podcastEpisode.push(PodcastEpisode)
|
||||||
oldDbIdMap.podcastEpisodes[oldEpisode.id] = PodcastEpisode.id
|
oldDbIdMap.podcastEpisodes[oldEpisode.id] = PodcastEpisode.id
|
||||||
}
|
}
|
||||||
|
return _newRecords
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate libraryItems to LibraryItem, Book, Podcast models
|
||||||
|
* @param {Array<objects.LibraryItem>} oldLibraryItems
|
||||||
|
* @returns {object} { libraryItem: [], book: [], podcast: [], podcastEpisode: [], bookSeries: [], bookAuthor: [] }
|
||||||
|
*/
|
||||||
function migrateLibraryItems(oldLibraryItems) {
|
function migrateLibraryItems(oldLibraryItems) {
|
||||||
|
const _newRecords = {
|
||||||
|
book: [],
|
||||||
|
podcast: [],
|
||||||
|
podcastEpisode: [],
|
||||||
|
bookSeries: [],
|
||||||
|
bookAuthor: [],
|
||||||
|
libraryItem: []
|
||||||
|
}
|
||||||
for (const oldLibraryItem of oldLibraryItems) {
|
for (const oldLibraryItem of oldLibraryItems) {
|
||||||
const libraryFolderId = oldDbIdMap.libraryFolders[oldLibraryItem.folderId]
|
const libraryFolderId = oldDbIdMap.libraryFolders[oldLibraryItem.folderId]
|
||||||
if (!libraryFolderId) {
|
if (!libraryFolderId) {
|
||||||
@@ -218,6 +248,7 @@ function migrateLibraryItems(oldLibraryItems) {
|
|||||||
//
|
//
|
||||||
const LibraryItem = {
|
const LibraryItem = {
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
|
oldLibraryItemId: oldLibraryItem.id,
|
||||||
ino: oldLibraryItem.ino,
|
ino: oldLibraryItem.ino,
|
||||||
path: oldLibraryItem.path,
|
path: oldLibraryItem.path,
|
||||||
relPath: oldLibraryItem.relPath,
|
relPath: oldLibraryItem.relPath,
|
||||||
@@ -241,22 +272,39 @@ function migrateLibraryItems(oldLibraryItems) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
oldDbIdMap.libraryItems[oldLibraryItem.id] = LibraryItem.id
|
oldDbIdMap.libraryItems[oldLibraryItem.id] = LibraryItem.id
|
||||||
newRecords.libraryItem.push(LibraryItem)
|
_newRecords.libraryItem.push(LibraryItem)
|
||||||
|
|
||||||
//
|
//
|
||||||
// Migrate Book/Podcast
|
// Migrate Book/Podcast
|
||||||
//
|
//
|
||||||
if (oldLibraryItem.mediaType === 'book') {
|
if (oldLibraryItem.mediaType === 'book') {
|
||||||
migrateBook(oldLibraryItem, LibraryItem)
|
const bookRecords = migrateBook(oldLibraryItem, LibraryItem)
|
||||||
|
_newRecords.book.push(bookRecords.book)
|
||||||
|
_newRecords.bookAuthor.push(...bookRecords.bookAuthor)
|
||||||
|
_newRecords.bookSeries.push(...bookRecords.bookSeries)
|
||||||
|
|
||||||
LibraryItem.mediaId = oldDbIdMap.books[oldLibraryItem.id]
|
LibraryItem.mediaId = oldDbIdMap.books[oldLibraryItem.id]
|
||||||
} else if (oldLibraryItem.mediaType === 'podcast') {
|
} else if (oldLibraryItem.mediaType === 'podcast') {
|
||||||
migratePodcast(oldLibraryItem, LibraryItem)
|
const podcastRecords = migratePodcast(oldLibraryItem, LibraryItem)
|
||||||
|
_newRecords.podcast.push(podcastRecords.podcast)
|
||||||
|
_newRecords.podcastEpisode.push(...podcastRecords.podcastEpisode)
|
||||||
|
|
||||||
LibraryItem.mediaId = oldDbIdMap.podcasts[oldLibraryItem.id]
|
LibraryItem.mediaId = oldDbIdMap.podcasts[oldLibraryItem.id]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return _newRecords
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate Library and LibraryFolder
|
||||||
|
* @param {Array<objects.Library>} oldLibraries
|
||||||
|
* @returns {object} { library: [], libraryFolder: [] }
|
||||||
|
*/
|
||||||
function migrateLibraries(oldLibraries) {
|
function migrateLibraries(oldLibraries) {
|
||||||
|
const _newRecords = {
|
||||||
|
library: [],
|
||||||
|
libraryFolder: []
|
||||||
|
}
|
||||||
for (const oldLibrary of oldLibraries) {
|
for (const oldLibrary of oldLibraries) {
|
||||||
if (!['book', 'podcast'].includes(oldLibrary.mediaType)) {
|
if (!['book', 'podcast'].includes(oldLibrary.mediaType)) {
|
||||||
Logger.error(`[dbMigration] migrateLibraries: Not migrating library with mediaType=${oldLibrary.mediaType}`)
|
Logger.error(`[dbMigration] migrateLibraries: Not migrating library with mediaType=${oldLibrary.mediaType}`)
|
||||||
@@ -268,6 +316,7 @@ function migrateLibraries(oldLibraries) {
|
|||||||
//
|
//
|
||||||
const Library = {
|
const Library = {
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
|
oldLibraryId: oldLibrary.id,
|
||||||
name: oldLibrary.name,
|
name: oldLibrary.name,
|
||||||
displayOrder: oldLibrary.displayOrder,
|
displayOrder: oldLibrary.displayOrder,
|
||||||
icon: oldLibrary.icon || null,
|
icon: oldLibrary.icon || null,
|
||||||
@@ -278,7 +327,7 @@ function migrateLibraries(oldLibraries) {
|
|||||||
updatedAt: oldLibrary.lastUpdate
|
updatedAt: oldLibrary.lastUpdate
|
||||||
}
|
}
|
||||||
oldDbIdMap.libraries[oldLibrary.id] = Library.id
|
oldDbIdMap.libraries[oldLibrary.id] = Library.id
|
||||||
newRecords.library.push(Library)
|
_newRecords.library.push(Library)
|
||||||
|
|
||||||
//
|
//
|
||||||
// Migrate LibraryFolders
|
// Migrate LibraryFolders
|
||||||
@@ -292,12 +341,21 @@ function migrateLibraries(oldLibraries) {
|
|||||||
libraryId: Library.id
|
libraryId: Library.id
|
||||||
}
|
}
|
||||||
oldDbIdMap.libraryFolders[oldFolder.id] = LibraryFolder.id
|
oldDbIdMap.libraryFolders[oldFolder.id] = LibraryFolder.id
|
||||||
newRecords.libraryFolder.push(LibraryFolder)
|
_newRecords.libraryFolder.push(LibraryFolder)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return _newRecords
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate Author
|
||||||
|
* Previously Authors were shared between libraries, this will ensure every author has one library
|
||||||
|
* @param {Array<objects.entities.Author>} oldAuthors
|
||||||
|
* @param {Array<objects.LibraryItem>} oldLibraryItems
|
||||||
|
* @returns {Array<object>} Array of Author model objs
|
||||||
|
*/
|
||||||
function migrateAuthors(oldAuthors, oldLibraryItems) {
|
function migrateAuthors(oldAuthors, oldLibraryItems) {
|
||||||
|
const _newRecords = []
|
||||||
for (const oldAuthor of oldAuthors) {
|
for (const oldAuthor of oldAuthors) {
|
||||||
// Get an array of NEW library ids that have this author
|
// Get an array of NEW library ids that have this author
|
||||||
const librariesWithThisAuthor = [...new Set(oldLibraryItems.map(li => {
|
const librariesWithThisAuthor = [...new Set(oldLibraryItems.map(li => {
|
||||||
@@ -325,12 +383,21 @@ function migrateAuthors(oldAuthors, oldLibraryItems) {
|
|||||||
}
|
}
|
||||||
if (!oldDbIdMap.authors[libraryId]) oldDbIdMap.authors[libraryId] = {}
|
if (!oldDbIdMap.authors[libraryId]) oldDbIdMap.authors[libraryId] = {}
|
||||||
oldDbIdMap.authors[libraryId][oldAuthor.id] = Author.id
|
oldDbIdMap.authors[libraryId][oldAuthor.id] = Author.id
|
||||||
newRecords.author.push(Author)
|
_newRecords.push(Author)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return _newRecords
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate Series
|
||||||
|
* Previously Series were shared between libraries, this will ensure every series has one library
|
||||||
|
* @param {Array<objects.entities.Series>} oldSerieses
|
||||||
|
* @param {Array<objects.LibraryItem>} oldLibraryItems
|
||||||
|
* @returns {Array<object>} Array of Series model objs
|
||||||
|
*/
|
||||||
function migrateSeries(oldSerieses, oldLibraryItems) {
|
function migrateSeries(oldSerieses, oldLibraryItems) {
|
||||||
|
const _newRecords = []
|
||||||
// Originaly series were shared between libraries if they had the same name
|
// Originaly series were shared between libraries if they had the same name
|
||||||
// Series will be separate between libraries
|
// Series will be separate between libraries
|
||||||
for (const oldSeries of oldSerieses) {
|
for (const oldSeries of oldSerieses) {
|
||||||
@@ -355,16 +422,47 @@ function migrateSeries(oldSerieses, oldLibraryItems) {
|
|||||||
}
|
}
|
||||||
if (!oldDbIdMap.series[libraryId]) oldDbIdMap.series[libraryId] = {}
|
if (!oldDbIdMap.series[libraryId]) oldDbIdMap.series[libraryId] = {}
|
||||||
oldDbIdMap.series[libraryId][oldSeries.id] = Series.id
|
oldDbIdMap.series[libraryId][oldSeries.id] = Series.id
|
||||||
newRecords.series.push(Series)
|
_newRecords.push(Series)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return _newRecords
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate users to User and MediaProgress models
|
||||||
|
* @param {Array<objects.User>} oldUsers
|
||||||
|
* @returns {object} { user: [], mediaProgress: [] }
|
||||||
|
*/
|
||||||
function migrateUsers(oldUsers) {
|
function migrateUsers(oldUsers) {
|
||||||
|
const _newRecords = {
|
||||||
|
user: [],
|
||||||
|
mediaProgress: []
|
||||||
|
}
|
||||||
for (const oldUser of oldUsers) {
|
for (const oldUser of oldUsers) {
|
||||||
//
|
//
|
||||||
// Migrate User
|
// Migrate User
|
||||||
//
|
//
|
||||||
|
// Convert old library ids to new ids
|
||||||
|
const librariesAccessible = (oldUser.librariesAccessible || []).map((lid) => oldDbIdMap.libraries[lid]).filter(li => li)
|
||||||
|
|
||||||
|
// Convert old library item ids to new ids
|
||||||
|
const bookmarks = (oldUser.bookmarks || []).map(bm => {
|
||||||
|
bm.libraryItemId = oldDbIdMap.libraryItems[bm.libraryItemId]
|
||||||
|
return bm
|
||||||
|
}).filter(bm => bm.libraryItemId)
|
||||||
|
|
||||||
|
// Convert old series ids to new
|
||||||
|
const seriesHideFromContinueListening = (oldUser.seriesHideFromContinueListening || []).map(oldSeriesId => {
|
||||||
|
// Series were split to be per library
|
||||||
|
// This will use the first series it finds
|
||||||
|
for (const libraryId in oldDbIdMap.series) {
|
||||||
|
if (oldDbIdMap.series[libraryId][oldSeriesId]) {
|
||||||
|
return oldDbIdMap.series[libraryId][oldSeriesId]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}).filter(se => se)
|
||||||
|
|
||||||
const User = {
|
const User = {
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
username: oldUser.username,
|
username: oldUser.username,
|
||||||
@@ -374,19 +472,19 @@ function migrateUsers(oldUsers) {
|
|||||||
isActive: !!oldUser.isActive,
|
isActive: !!oldUser.isActive,
|
||||||
lastSeen: oldUser.lastSeen || null,
|
lastSeen: oldUser.lastSeen || null,
|
||||||
extraData: {
|
extraData: {
|
||||||
seriesHideFromContinueListening: oldUser.seriesHideFromContinueListening || [],
|
seriesHideFromContinueListening,
|
||||||
oldUserId: oldUser.id // Used to keep old tokens
|
oldUserId: oldUser.id // Used to keep old tokens
|
||||||
},
|
},
|
||||||
createdAt: oldUser.createdAt || Date.now(),
|
createdAt: oldUser.createdAt || Date.now(),
|
||||||
permissions: {
|
permissions: {
|
||||||
...oldUser.permissions,
|
...oldUser.permissions,
|
||||||
librariesAccessible: oldUser.librariesAccessible || [],
|
librariesAccessible,
|
||||||
itemTagsSelected: oldUser.itemTagsSelected || []
|
itemTagsSelected: oldUser.itemTagsSelected || []
|
||||||
},
|
},
|
||||||
bookmarks: oldUser.bookmarks
|
bookmarks
|
||||||
}
|
}
|
||||||
oldDbIdMap.users[oldUser.id] = User.id
|
oldDbIdMap.users[oldUser.id] = User.id
|
||||||
newRecords.user.push(User)
|
_newRecords.user.push(User)
|
||||||
|
|
||||||
//
|
//
|
||||||
// Migrate MediaProgress
|
// Migrate MediaProgress
|
||||||
@@ -425,12 +523,23 @@ function migrateUsers(oldUsers) {
|
|||||||
progress: oldMediaProgress.progress
|
progress: oldMediaProgress.progress
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
newRecords.mediaProgress.push(MediaProgress)
|
_newRecords.mediaProgress.push(MediaProgress)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return _newRecords
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate playbackSessions to PlaybackSession and Device models
|
||||||
|
* @param {Array<objects.PlaybackSession>} oldSessions
|
||||||
|
* @returns {object} { playbackSession: [], device: [] }
|
||||||
|
*/
|
||||||
function migrateSessions(oldSessions) {
|
function migrateSessions(oldSessions) {
|
||||||
|
const _newRecords = {
|
||||||
|
device: [],
|
||||||
|
playbackSession: []
|
||||||
|
}
|
||||||
|
|
||||||
for (const oldSession of oldSessions) {
|
for (const oldSession of oldSessions) {
|
||||||
const userId = oldDbIdMap.users[oldSession.userId]
|
const userId = oldDbIdMap.users[oldSession.userId]
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
@@ -495,12 +604,12 @@ function migrateSessions(oldSessions) {
|
|||||||
userId,
|
userId,
|
||||||
extraData
|
extraData
|
||||||
}
|
}
|
||||||
newRecords.device.push(Device)
|
deviceId = Device.id
|
||||||
|
_newRecords.device.push(Device)
|
||||||
oldDbIdMap.devices[deviceDeviceId] = Device.id
|
oldDbIdMap.devices[deviceDeviceId] = Device.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Migrate PlaybackSession
|
// Migrate PlaybackSession
|
||||||
//
|
//
|
||||||
@@ -528,7 +637,7 @@ function migrateSessions(oldSessions) {
|
|||||||
serverVersion: oldSession.deviceInfo?.serverVersion || null,
|
serverVersion: oldSession.deviceInfo?.serverVersion || null,
|
||||||
createdAt: oldSession.startedAt,
|
createdAt: oldSession.startedAt,
|
||||||
updatedAt: oldSession.updatedAt,
|
updatedAt: oldSession.updatedAt,
|
||||||
userId, // Can be null
|
userId,
|
||||||
deviceId,
|
deviceId,
|
||||||
timeListening: oldSession.timeListening,
|
timeListening: oldSession.timeListening,
|
||||||
coverPath: oldSession.coverPath,
|
coverPath: oldSession.coverPath,
|
||||||
@@ -539,11 +648,21 @@ function migrateSessions(oldSessions) {
|
|||||||
libraryItemId: oldDbIdMap.libraryItems[oldSession.libraryItemId]
|
libraryItemId: oldDbIdMap.libraryItems[oldSession.libraryItemId]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
newRecords.playbackSession.push(PlaybackSession)
|
_newRecords.playbackSession.push(PlaybackSession)
|
||||||
}
|
}
|
||||||
|
return _newRecords
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate collections to Collection & CollectionBook
|
||||||
|
* @param {Array<objects.Collection>} oldCollections
|
||||||
|
* @returns {object} { collection: [], collectionBook: [] }
|
||||||
|
*/
|
||||||
function migrateCollections(oldCollections) {
|
function migrateCollections(oldCollections) {
|
||||||
|
const _newRecords = {
|
||||||
|
collection: [],
|
||||||
|
collectionBook: []
|
||||||
|
}
|
||||||
for (const oldCollection of oldCollections) {
|
for (const oldCollection of oldCollections) {
|
||||||
const libraryId = oldDbIdMap.libraries[oldCollection.libraryId]
|
const libraryId = oldDbIdMap.libraries[oldCollection.libraryId]
|
||||||
if (!libraryId) {
|
if (!libraryId) {
|
||||||
@@ -566,7 +685,7 @@ function migrateCollections(oldCollections) {
|
|||||||
libraryId
|
libraryId
|
||||||
}
|
}
|
||||||
oldDbIdMap.collections[oldCollection.id] = Collection.id
|
oldDbIdMap.collections[oldCollection.id] = Collection.id
|
||||||
newRecords.collection.push(Collection)
|
_newRecords.collection.push(Collection)
|
||||||
|
|
||||||
let order = 1
|
let order = 1
|
||||||
BookIds.forEach((bookId) => {
|
BookIds.forEach((bookId) => {
|
||||||
@@ -577,12 +696,22 @@ function migrateCollections(oldCollections) {
|
|||||||
collectionId: Collection.id,
|
collectionId: Collection.id,
|
||||||
order: order++
|
order: order++
|
||||||
}
|
}
|
||||||
newRecords.collectionBook.push(CollectionBook)
|
_newRecords.collectionBook.push(CollectionBook)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
return _newRecords
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate playlists to Playlist and PlaylistMediaItem
|
||||||
|
* @param {Array<objects.Playlist>} oldPlaylists
|
||||||
|
* @returns {object} { playlist: [], playlistMediaItem: [] }
|
||||||
|
*/
|
||||||
function migratePlaylists(oldPlaylists) {
|
function migratePlaylists(oldPlaylists) {
|
||||||
|
const _newRecords = {
|
||||||
|
playlist: [],
|
||||||
|
playlistMediaItem: []
|
||||||
|
}
|
||||||
for (const oldPlaylist of oldPlaylists) {
|
for (const oldPlaylist of oldPlaylists) {
|
||||||
const libraryId = oldDbIdMap.libraries[oldPlaylist.libraryId]
|
const libraryId = oldDbIdMap.libraries[oldPlaylist.libraryId]
|
||||||
if (!libraryId) {
|
if (!libraryId) {
|
||||||
@@ -622,7 +751,7 @@ function migratePlaylists(oldPlaylists) {
|
|||||||
userId,
|
userId,
|
||||||
libraryId
|
libraryId
|
||||||
}
|
}
|
||||||
newRecords.playlist.push(Playlist)
|
_newRecords.playlist.push(Playlist)
|
||||||
|
|
||||||
let order = 1
|
let order = 1
|
||||||
MediaItemIds.forEach((mediaItemId) => {
|
MediaItemIds.forEach((mediaItemId) => {
|
||||||
@@ -634,12 +763,22 @@ function migratePlaylists(oldPlaylists) {
|
|||||||
playlistId: Playlist.id,
|
playlistId: Playlist.id,
|
||||||
order: order++
|
order: order++
|
||||||
}
|
}
|
||||||
newRecords.playlistMediaItem.push(PlaylistMediaItem)
|
_newRecords.playlistMediaItem.push(PlaylistMediaItem)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
return _newRecords
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate feeds to Feed and FeedEpisode models
|
||||||
|
* @param {Array<objects.Feed>} oldFeeds
|
||||||
|
* @returns {object} { feed: [], feedEpisode: [] }
|
||||||
|
*/
|
||||||
function migrateFeeds(oldFeeds) {
|
function migrateFeeds(oldFeeds) {
|
||||||
|
const _newRecords = {
|
||||||
|
feed: [],
|
||||||
|
feedEpisode: []
|
||||||
|
}
|
||||||
for (const oldFeed of oldFeeds) {
|
for (const oldFeed of oldFeeds) {
|
||||||
if (!oldFeed.episodes?.length) {
|
if (!oldFeed.episodes?.length) {
|
||||||
continue
|
continue
|
||||||
@@ -698,7 +837,7 @@ function migrateFeeds(oldFeeds) {
|
|||||||
updatedAt: oldFeed.updatedAt,
|
updatedAt: oldFeed.updatedAt,
|
||||||
userId
|
userId
|
||||||
}
|
}
|
||||||
newRecords.feed.push(Feed)
|
_newRecords.feed.push(Feed)
|
||||||
|
|
||||||
//
|
//
|
||||||
// Migrate FeedEpisodes
|
// Migrate FeedEpisodes
|
||||||
@@ -724,65 +863,227 @@ function migrateFeeds(oldFeeds) {
|
|||||||
updatedAt: oldFeed.updatedAt,
|
updatedAt: oldFeed.updatedAt,
|
||||||
feedId: Feed.id
|
feedId: Feed.id
|
||||||
}
|
}
|
||||||
newRecords.feedEpisode.push(FeedEpisode)
|
_newRecords.feedEpisode.push(FeedEpisode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return _newRecords
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate ServerSettings, NotificationSettings and EmailSettings to Setting model
|
||||||
|
* @param {Array<objects.settings.*>} oldSettings
|
||||||
|
* @returns {Array<object>} Array of Setting model objs
|
||||||
|
*/
|
||||||
function migrateSettings(oldSettings) {
|
function migrateSettings(oldSettings) {
|
||||||
|
const _newRecords = []
|
||||||
const serverSettings = oldSettings.find(s => s.id === 'server-settings')
|
const serverSettings = oldSettings.find(s => s.id === 'server-settings')
|
||||||
const notificationSettings = oldSettings.find(s => s.id === 'notification-settings')
|
const notificationSettings = oldSettings.find(s => s.id === 'notification-settings')
|
||||||
const emailSettings = oldSettings.find(s => s.id === 'email-settings')
|
const emailSettings = oldSettings.find(s => s.id === 'email-settings')
|
||||||
|
|
||||||
if (serverSettings) {
|
if (serverSettings) {
|
||||||
newRecords.setting.push({
|
_newRecords.push({
|
||||||
key: 'server-settings',
|
key: 'server-settings',
|
||||||
value: serverSettings
|
value: serverSettings
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (notificationSettings) {
|
if (notificationSettings) {
|
||||||
newRecords.setting.push({
|
_newRecords.push({
|
||||||
key: 'notification-settings',
|
key: 'notification-settings',
|
||||||
value: notificationSettings
|
value: notificationSettings
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (emailSettings) {
|
if (emailSettings) {
|
||||||
newRecords.setting.push({
|
_newRecords.push({
|
||||||
key: 'email-settings',
|
key: 'email-settings',
|
||||||
value: emailSettings
|
value: emailSettings
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
return _newRecords
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load old libraries and bulkCreate new Library and LibraryFolder rows
|
||||||
|
* @param {Map<string,Model>} DatabaseModels
|
||||||
|
*/
|
||||||
|
async function handleMigrateLibraries(DatabaseModels) {
|
||||||
|
const oldLibraries = await oldDbFiles.loadOldData('libraries')
|
||||||
|
const newLibraryRecords = migrateLibraries(oldLibraries)
|
||||||
|
for (const model in newLibraryRecords) {
|
||||||
|
Logger.info(`[dbMigration] Inserting ${newLibraryRecords[model].length} ${model} rows`)
|
||||||
|
await DatabaseModels[model].bulkCreate(newLibraryRecords[model])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load old EmailSettings, NotificationSettings and ServerSettings and bulkCreate new Setting rows
|
||||||
|
* @param {Map<string,Model>} DatabaseModels
|
||||||
|
*/
|
||||||
|
async function handleMigrateSettings(DatabaseModels) {
|
||||||
|
const oldSettings = await oldDbFiles.loadOldData('settings')
|
||||||
|
const newSettings = migrateSettings(oldSettings)
|
||||||
|
Logger.info(`[dbMigration] Inserting ${newSettings.length} setting rows`)
|
||||||
|
await DatabaseModels.setting.bulkCreate(newSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load old authors and bulkCreate new Author rows
|
||||||
|
* @param {Map<string,Model>} DatabaseModels
|
||||||
|
* @param {Array<objects.LibraryItem>} oldLibraryItems
|
||||||
|
*/
|
||||||
|
async function handleMigrateAuthors(DatabaseModels, oldLibraryItems) {
|
||||||
|
const oldAuthors = await oldDbFiles.loadOldData('authors')
|
||||||
|
const newAuthors = migrateAuthors(oldAuthors, oldLibraryItems)
|
||||||
|
Logger.info(`[dbMigration] Inserting ${newAuthors.length} author rows`)
|
||||||
|
await DatabaseModels.author.bulkCreate(newAuthors)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load old series and bulkCreate new Series rows
|
||||||
|
* @param {Map<string,Model>} DatabaseModels
|
||||||
|
* @param {Array<objects.LibraryItem>} oldLibraryItems
|
||||||
|
*/
|
||||||
|
async function handleMigrateSeries(DatabaseModels, oldLibraryItems) {
|
||||||
|
const oldSeries = await oldDbFiles.loadOldData('series')
|
||||||
|
const newSeries = migrateSeries(oldSeries, oldLibraryItems)
|
||||||
|
Logger.info(`[dbMigration] Inserting ${newSeries.length} series rows`)
|
||||||
|
await DatabaseModels.series.bulkCreate(newSeries)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* bulkCreate new LibraryItem, Book and Podcast rows
|
||||||
|
* @param {Map<string,Model>} DatabaseModels
|
||||||
|
* @param {Array<objects.LibraryItem>} oldLibraryItems
|
||||||
|
*/
|
||||||
|
async function handleMigrateLibraryItems(DatabaseModels, oldLibraryItems) {
|
||||||
|
const newItemsBooksPodcasts = migrateLibraryItems(oldLibraryItems)
|
||||||
|
for (const model in newItemsBooksPodcasts) {
|
||||||
|
Logger.info(`[dbMigration] Inserting ${newItemsBooksPodcasts[model].length} ${model} rows`)
|
||||||
|
await DatabaseModels[model].bulkCreate(newItemsBooksPodcasts[model])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate authors, series then library items in chunks
|
||||||
|
* Authors and series require old library items loaded first
|
||||||
|
* @param {Map<string,Model>} DatabaseModels
|
||||||
|
*/
|
||||||
|
async function handleMigrateAuthorsSeriesAndLibraryItems(DatabaseModels) {
|
||||||
|
const oldLibraryItems = await oldDbFiles.loadOldData('libraryItems')
|
||||||
|
await handleMigrateAuthors(DatabaseModels, oldLibraryItems)
|
||||||
|
|
||||||
|
await handleMigrateSeries(DatabaseModels, oldLibraryItems)
|
||||||
|
|
||||||
|
// Migrate library items in chunks of 1000
|
||||||
|
const numChunks = Math.ceil(oldLibraryItems.length / 1000)
|
||||||
|
for (let i = 0; i < numChunks; i++) {
|
||||||
|
let start = i * 1000
|
||||||
|
await handleMigrateLibraryItems(DatabaseModels, oldLibraryItems.slice(start, start + 1000))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load old users and bulkCreate new User rows
|
||||||
|
* @param {Map<string,Model>} DatabaseModels
|
||||||
|
*/
|
||||||
|
async function handleMigrateUsers(DatabaseModels) {
|
||||||
|
const oldUsers = await oldDbFiles.loadOldData('users')
|
||||||
|
const newUserRecords = migrateUsers(oldUsers)
|
||||||
|
for (const model in newUserRecords) {
|
||||||
|
Logger.info(`[dbMigration] Inserting ${newUserRecords[model].length} ${model} rows`)
|
||||||
|
await DatabaseModels[model].bulkCreate(newUserRecords[model])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load old sessions and bulkCreate new PlaybackSession & Device rows
|
||||||
|
* @param {Map<string,Model>} DatabaseModels
|
||||||
|
*/
|
||||||
|
async function handleMigrateSessions(DatabaseModels) {
|
||||||
|
const oldSessions = await oldDbFiles.loadOldData('sessions')
|
||||||
|
|
||||||
|
let chunkSize = 1000
|
||||||
|
let numChunks = Math.ceil(oldSessions.length / chunkSize)
|
||||||
|
|
||||||
|
for (let i = 0; i < numChunks; i++) {
|
||||||
|
let start = i * chunkSize
|
||||||
|
const newSessionRecords = migrateSessions(oldSessions.slice(start, start + chunkSize))
|
||||||
|
for (const model in newSessionRecords) {
|
||||||
|
Logger.info(`[dbMigration] Inserting ${newSessionRecords[model].length} ${model} rows`)
|
||||||
|
await DatabaseModels[model].bulkCreate(newSessionRecords[model])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load old collections and bulkCreate new Collection, CollectionBook models
|
||||||
|
* @param {Map<string,Model>} DatabaseModels
|
||||||
|
*/
|
||||||
|
async function handleMigrateCollections(DatabaseModels) {
|
||||||
|
const oldCollections = await oldDbFiles.loadOldData('collections')
|
||||||
|
const newCollectionRecords = migrateCollections(oldCollections)
|
||||||
|
for (const model in newCollectionRecords) {
|
||||||
|
Logger.info(`[dbMigration] Inserting ${newCollectionRecords[model].length} ${model} rows`)
|
||||||
|
await DatabaseModels[model].bulkCreate(newCollectionRecords[model])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load old playlists and bulkCreate new Playlist, PlaylistMediaItem models
|
||||||
|
* @param {Map<string,Model>} DatabaseModels
|
||||||
|
*/
|
||||||
|
async function handleMigratePlaylists(DatabaseModels) {
|
||||||
|
const oldPlaylists = await oldDbFiles.loadOldData('playlists')
|
||||||
|
const newPlaylistRecords = migratePlaylists(oldPlaylists)
|
||||||
|
for (const model in newPlaylistRecords) {
|
||||||
|
Logger.info(`[dbMigration] Inserting ${newPlaylistRecords[model].length} ${model} rows`)
|
||||||
|
await DatabaseModels[model].bulkCreate(newPlaylistRecords[model])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load old feeds and bulkCreate new Feed, FeedEpisode models
|
||||||
|
* @param {Map<string,Model>} DatabaseModels
|
||||||
|
*/
|
||||||
|
async function handleMigrateFeeds(DatabaseModels) {
|
||||||
|
const oldFeeds = await oldDbFiles.loadOldData('feeds')
|
||||||
|
const newFeedRecords = migrateFeeds(oldFeeds)
|
||||||
|
for (const model in newFeedRecords) {
|
||||||
|
Logger.info(`[dbMigration] Inserting ${newFeedRecords[model].length} ${model} rows`)
|
||||||
|
await DatabaseModels[model].bulkCreate(newFeedRecords[model])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.migrate = async (DatabaseModels) => {
|
module.exports.migrate = async (DatabaseModels) => {
|
||||||
Logger.info(`[dbMigration] Starting migration`)
|
Logger.info(`[dbMigration] Starting migration`)
|
||||||
|
|
||||||
const data = await oldDbFiles.init()
|
|
||||||
|
|
||||||
const start = Date.now()
|
const start = Date.now()
|
||||||
migrateSettings(data.settings)
|
|
||||||
migrateLibraries(data.libraries)
|
|
||||||
migrateAuthors(data.authors, data.libraryItems)
|
|
||||||
migrateSeries(data.series, data.libraryItems)
|
|
||||||
migrateLibraryItems(data.libraryItems)
|
|
||||||
migrateUsers(data.users)
|
|
||||||
migrateSessions(data.sessions)
|
|
||||||
migrateCollections(data.collections)
|
|
||||||
migratePlaylists(data.playlists)
|
|
||||||
migrateFeeds(data.feeds)
|
|
||||||
|
|
||||||
let totalRecords = 0
|
// Migrate to Library and LibraryFolder models
|
||||||
for (const model in newRecords) {
|
await handleMigrateLibraries(DatabaseModels)
|
||||||
Logger.info(`[dbMigration] Inserting ${newRecords[model].length} ${model} rows`)
|
|
||||||
if (newRecords[model].length) {
|
|
||||||
await DatabaseModels[model].bulkCreate(newRecords[model])
|
|
||||||
totalRecords += newRecords[model].length
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const elapsed = Date.now() - start
|
// Migrate EmailSettings, NotificationSettings and ServerSettings to Setting model
|
||||||
|
await handleMigrateSettings(DatabaseModels)
|
||||||
|
|
||||||
|
// Migrate Series, Author, LibraryItem, Book, Podcast
|
||||||
|
await handleMigrateAuthorsSeriesAndLibraryItems(DatabaseModels)
|
||||||
|
|
||||||
|
// Migrate User, MediaProgress
|
||||||
|
await handleMigrateUsers(DatabaseModels)
|
||||||
|
|
||||||
|
// Migrate PlaybackSession, Device
|
||||||
|
await handleMigrateSessions(DatabaseModels)
|
||||||
|
|
||||||
|
// Migrate Collection, CollectionBook
|
||||||
|
await handleMigrateCollections(DatabaseModels)
|
||||||
|
|
||||||
|
// Migrate Playlist, PlaylistMediaItem
|
||||||
|
await handleMigratePlaylists(DatabaseModels)
|
||||||
|
|
||||||
|
// Migrate Feed, FeedEpisode
|
||||||
|
await handleMigrateFeeds(DatabaseModels)
|
||||||
|
|
||||||
// Purge author images and cover images from cache
|
// Purge author images and cover images from cache
|
||||||
try {
|
try {
|
||||||
@@ -796,7 +1097,8 @@ module.exports.migrate = async (DatabaseModels) => {
|
|||||||
// Put all old db folders into a zipfile oldDb.zip
|
// Put all old db folders into a zipfile oldDb.zip
|
||||||
await oldDbFiles.zipWrapOldDb()
|
await oldDbFiles.zipWrapOldDb()
|
||||||
|
|
||||||
Logger.info(`[dbMigration] Migration complete. ${totalRecords} rows. Elapsed ${(elapsed / 1000).toFixed(2)}s`)
|
const elapsed = Date.now() - start
|
||||||
|
Logger.info(`[dbMigration] Migration complete. Elapsed ${(elapsed / 1000).toFixed(2)}s`)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -806,3 +1108,204 @@ module.exports.checkShouldMigrate = async () => {
|
|||||||
if (await oldDbFiles.checkHasOldDb()) return true
|
if (await oldDbFiles.checkHasOldDb()) return true
|
||||||
return oldDbFiles.checkHasOldDbZip()
|
return oldDbFiles.checkHasOldDbZip()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migration from 2.3.0 to 2.3.1 - create extraData columns in LibraryItem and PodcastEpisode
|
||||||
|
* @param {QueryInterface} queryInterface
|
||||||
|
*/
|
||||||
|
async function migrationPatchNewColumns(queryInterface) {
|
||||||
|
try {
|
||||||
|
return queryInterface.sequelize.transaction(t => {
|
||||||
|
return Promise.all([
|
||||||
|
queryInterface.addColumn('libraryItems', 'extraData', {
|
||||||
|
type: DataTypes.JSON
|
||||||
|
}, { transaction: t }),
|
||||||
|
queryInterface.addColumn('podcastEpisodes', 'extraData', {
|
||||||
|
type: DataTypes.JSON
|
||||||
|
}, { transaction: t }),
|
||||||
|
queryInterface.addColumn('libraries', 'extraData', {
|
||||||
|
type: DataTypes.JSON
|
||||||
|
}, { transaction: t })
|
||||||
|
])
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(`[dbMigration] Migration from 2.3.0+ column creation failed`, error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migration from 2.3.0 to 2.3.1 - old library item ids
|
||||||
|
* @param {/src/Database} ctx
|
||||||
|
*/
|
||||||
|
async function handleOldLibraryItems(ctx) {
|
||||||
|
const oldLibraryItems = await oldDbFiles.loadOldData('libraryItems')
|
||||||
|
const libraryItems = await ctx.models.libraryItem.getAllOldLibraryItems()
|
||||||
|
|
||||||
|
const bulkUpdateItems = []
|
||||||
|
const bulkUpdateEpisodes = []
|
||||||
|
|
||||||
|
for (const libraryItem of libraryItems) {
|
||||||
|
// Find matching old library item by ino
|
||||||
|
const matchingOldLibraryItem = oldLibraryItems.find(oli => oli.ino === libraryItem.ino)
|
||||||
|
if (matchingOldLibraryItem) {
|
||||||
|
oldDbIdMap.libraryItems[matchingOldLibraryItem.id] = libraryItem.id
|
||||||
|
|
||||||
|
bulkUpdateItems.push({
|
||||||
|
id: libraryItem.id,
|
||||||
|
extraData: {
|
||||||
|
oldLibraryItemId: matchingOldLibraryItem.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (libraryItem.media.episodes?.length && matchingOldLibraryItem.media.episodes?.length) {
|
||||||
|
for (const podcastEpisode of libraryItem.media.episodes) {
|
||||||
|
// Find matching old episode by audio file ino
|
||||||
|
const matchingOldPodcastEpisode = matchingOldLibraryItem.media.episodes.find(oep => oep.audioFile?.ino && oep.audioFile.ino === podcastEpisode.audioFile?.ino)
|
||||||
|
if (matchingOldPodcastEpisode) {
|
||||||
|
oldDbIdMap.podcastEpisodes[matchingOldPodcastEpisode.id] = podcastEpisode.id
|
||||||
|
|
||||||
|
bulkUpdateEpisodes.push({
|
||||||
|
id: podcastEpisode.id,
|
||||||
|
extraData: {
|
||||||
|
oldEpisodeId: matchingOldPodcastEpisode.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bulkUpdateEpisodes.length) {
|
||||||
|
await ctx.models.podcastEpisode.bulkCreate(bulkUpdateEpisodes, {
|
||||||
|
updateOnDuplicate: ['extraData']
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bulkUpdateItems.length) {
|
||||||
|
await ctx.models.libraryItem.bulkCreate(bulkUpdateItems, {
|
||||||
|
updateOnDuplicate: ['extraData']
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.info(`[dbMigration] Migration 2.3.0+: Updated ${bulkUpdateItems.length} library items & ${bulkUpdateEpisodes.length} episodes`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migration from 2.3.0 to 2.3.1 - updating oldLibraryId
|
||||||
|
* @param {/src/Database} ctx
|
||||||
|
*/
|
||||||
|
async function handleOldLibraries(ctx) {
|
||||||
|
const oldLibraries = await oldDbFiles.loadOldData('libraries')
|
||||||
|
const libraries = await ctx.models.library.getAllOldLibraries()
|
||||||
|
|
||||||
|
let librariesUpdated = 0
|
||||||
|
for (const library of libraries) {
|
||||||
|
// Find matching old library using exact match on folder paths, exact match on library name
|
||||||
|
const matchingOldLibrary = oldLibraries.find(ol => {
|
||||||
|
if (ol.name !== library.name) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const folderPaths = ol.folders?.map(f => f.fullPath) || []
|
||||||
|
return folderPaths.join(',') === library.folderPaths.join(',')
|
||||||
|
})
|
||||||
|
|
||||||
|
if (matchingOldLibrary) {
|
||||||
|
library.oldLibraryId = matchingOldLibrary.id
|
||||||
|
oldDbIdMap.libraries[library.oldLibraryId] = library.id
|
||||||
|
await ctx.models.library.updateFromOld(library)
|
||||||
|
librariesUpdated++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Logger.info(`[dbMigration] Migration 2.3.0+: Updated ${librariesUpdated} libraries`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migration from 2.3.0 to 2.3.1 - fixing librariesAccessible and bookmarks
|
||||||
|
* @param {/src/Database} ctx
|
||||||
|
*/
|
||||||
|
async function handleOldUsers(ctx) {
|
||||||
|
const users = await ctx.models.user.getOldUsers()
|
||||||
|
|
||||||
|
let usersUpdated = 0
|
||||||
|
for (const user of users) {
|
||||||
|
let hasUpdates = false
|
||||||
|
if (user.bookmarks?.length) {
|
||||||
|
user.bookmarks = user.bookmarks.map(bm => {
|
||||||
|
// Only update if this is not the old id format
|
||||||
|
if (!bm.libraryItemId.startsWith('li_')) return bm
|
||||||
|
|
||||||
|
bm.libraryItemId = oldDbIdMap.libraryItems[bm.libraryItemId]
|
||||||
|
hasUpdates = true
|
||||||
|
return bm
|
||||||
|
}).filter(bm => bm.libraryItemId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert old library ids to new library ids
|
||||||
|
if (user.librariesAccessible?.length) {
|
||||||
|
user.librariesAccessible = user.librariesAccessible.map(lid => {
|
||||||
|
if (!lid.startsWith('lib_') && lid !== 'main') return lid // Already not an old library id so dont change
|
||||||
|
hasUpdates = true
|
||||||
|
return oldDbIdMap.libraries[lid]
|
||||||
|
}).filter(lid => lid)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.seriesHideFromContinueListening?.length) {
|
||||||
|
user.seriesHideFromContinueListening = user.seriesHideFromContinueListening.map((seriesId) => {
|
||||||
|
if (seriesId.startsWith('se_')) {
|
||||||
|
hasUpdates = true
|
||||||
|
return null // Filter out old series ids
|
||||||
|
}
|
||||||
|
return seriesId
|
||||||
|
}).filter(se => se)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasUpdates) {
|
||||||
|
await ctx.models.user.updateFromOld(user)
|
||||||
|
usersUpdated++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Logger.info(`[dbMigration] Migration 2.3.0+: Updated ${usersUpdated} users`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migration from 2.3.0 to 2.3.1
|
||||||
|
* @param {/src/Database} ctx
|
||||||
|
*/
|
||||||
|
module.exports.migrationPatch = async (ctx) => {
|
||||||
|
const queryInterface = ctx.sequelize.getQueryInterface()
|
||||||
|
const librariesTableDescription = await queryInterface.describeTable('libraries')
|
||||||
|
|
||||||
|
if (librariesTableDescription?.extraData) {
|
||||||
|
Logger.info(`[dbMigration] Migration patch 2.3.0+ - extraData columns already on model`)
|
||||||
|
} else {
|
||||||
|
const migrationResult = await migrationPatchNewColumns(queryInterface)
|
||||||
|
if (migrationResult === false) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldDbPath = Path.join(global.ConfigPath, 'oldDb.zip')
|
||||||
|
if (!await fs.pathExists(oldDbPath)) {
|
||||||
|
Logger.info(`[dbMigration] Migration patch 2.3.0+ unnecessary - no oldDb.zip found`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const migrationStart = Date.now()
|
||||||
|
Logger.info(`[dbMigration] Applying migration patch from 2.3.0+`)
|
||||||
|
|
||||||
|
// Extract from oldDb.zip
|
||||||
|
if (!await oldDbFiles.checkExtractItemsUsersAndLibraries()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await handleOldLibraryItems(ctx)
|
||||||
|
await handleOldLibraries(ctx)
|
||||||
|
await handleOldUsers(ctx)
|
||||||
|
|
||||||
|
await oldDbFiles.removeOldItemsUsersAndLibrariesFolders()
|
||||||
|
|
||||||
|
const elapsed = Date.now() - migrationStart
|
||||||
|
Logger.info(`[dbMigration] Migration patch 2.3.0+ finished. Elapsed ${(elapsed / 1000).toFixed(2)}s`)
|
||||||
|
}
|
||||||
@@ -71,27 +71,11 @@ async function loadDbData(dbpath) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.init = async () => {
|
module.exports.loadOldData = async (dbName) => {
|
||||||
const dbs = {
|
const dbPath = Path.join(global.ConfigPath, dbName, 'data')
|
||||||
libraryItems: Path.join(global.ConfigPath, 'libraryItems', 'data'),
|
const dbData = await loadDbData(dbPath) || []
|
||||||
users: Path.join(global.ConfigPath, 'users', 'data'),
|
Logger.info(`[oldDbFiles] ${dbData.length} ${dbName} loaded`)
|
||||||
sessions: Path.join(global.ConfigPath, 'sessions', 'data'),
|
return dbData
|
||||||
libraries: Path.join(global.ConfigPath, 'libraries', 'data'),
|
|
||||||
settings: Path.join(global.ConfigPath, 'settings', 'data'),
|
|
||||||
collections: Path.join(global.ConfigPath, 'collections', 'data'),
|
|
||||||
playlists: Path.join(global.ConfigPath, 'playlists', 'data'),
|
|
||||||
authors: Path.join(global.ConfigPath, 'authors', 'data'),
|
|
||||||
series: Path.join(global.ConfigPath, 'series', 'data'),
|
|
||||||
feeds: Path.join(global.ConfigPath, 'feeds', 'data')
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = {}
|
|
||||||
for (const key in dbs) {
|
|
||||||
data[key] = await loadDbData(dbs[key])
|
|
||||||
Logger.info(`[oldDbFiles] ${data[key].length} ${key} loaded`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return data
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.zipWrapOldDb = async () => {
|
module.exports.zipWrapOldDb = async () => {
|
||||||
@@ -184,6 +168,59 @@ module.exports.checkHasOldDbZip = async () => {
|
|||||||
// Extract oldDb.zip
|
// Extract oldDb.zip
|
||||||
const zip = new StreamZip.async({ file: oldDbPath })
|
const zip = new StreamZip.async({ file: oldDbPath })
|
||||||
await zip.extract(null, global.ConfigPath)
|
await zip.extract(null, global.ConfigPath)
|
||||||
|
await zip.close()
|
||||||
|
|
||||||
return this.checkHasOldDb()
|
return this.checkHasOldDb()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for migration from 2.3.0 -> 2.3.1
|
||||||
|
* @returns {boolean} true if extracted
|
||||||
|
*/
|
||||||
|
module.exports.checkExtractItemsUsersAndLibraries = async () => {
|
||||||
|
const oldDbPath = Path.join(global.ConfigPath, 'oldDb.zip')
|
||||||
|
|
||||||
|
const zip = new StreamZip.async({ file: oldDbPath })
|
||||||
|
const libraryItemsPath = Path.join(global.ConfigPath, 'libraryItems')
|
||||||
|
await zip.extract('libraryItems/', libraryItemsPath)
|
||||||
|
|
||||||
|
if (!await fs.pathExists(libraryItemsPath)) {
|
||||||
|
Logger.error(`[oldDbFiles] Failed to extract old libraryItems from oldDb.zip`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const usersPath = Path.join(global.ConfigPath, 'users')
|
||||||
|
await zip.extract('users/', usersPath)
|
||||||
|
|
||||||
|
if (!await fs.pathExists(usersPath)) {
|
||||||
|
Logger.error(`[oldDbFiles] Failed to extract old users from oldDb.zip`)
|
||||||
|
await fs.remove(libraryItemsPath) // Remove old library items folder
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const librariesPath = Path.join(global.ConfigPath, 'libraries')
|
||||||
|
await zip.extract('libraries/', librariesPath)
|
||||||
|
|
||||||
|
if (!await fs.pathExists(librariesPath)) {
|
||||||
|
Logger.error(`[oldDbFiles] Failed to extract old libraries from oldDb.zip`)
|
||||||
|
await fs.remove(usersPath) // Remove old users folder
|
||||||
|
await fs.remove(libraryItemsPath) // Remove old library items folder
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
await zip.close()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for migration from 2.3.0 -> 2.3.1
|
||||||
|
*/
|
||||||
|
module.exports.removeOldItemsUsersAndLibrariesFolders = async () => {
|
||||||
|
const libraryItemsPath = Path.join(global.ConfigPath, 'libraryItems')
|
||||||
|
const usersPath = Path.join(global.ConfigPath, 'users')
|
||||||
|
const librariesPath = Path.join(global.ConfigPath, 'libraries')
|
||||||
|
await fs.remove(libraryItemsPath)
|
||||||
|
await fs.remove(usersPath)
|
||||||
|
await fs.remove(librariesPath)
|
||||||
|
}
|
||||||
@@ -43,6 +43,8 @@ module.exports.parse = (nameString) => {
|
|||||||
// Example &LF: Friedman, Milton & Friedman, Rose
|
// Example &LF: Friedman, Milton & Friedman, Rose
|
||||||
if (nameString.includes('&')) {
|
if (nameString.includes('&')) {
|
||||||
nameString.split('&').forEach((asa) => splitNames = splitNames.concat(asa.split(',')))
|
nameString.split('&').forEach((asa) => splitNames = splitNames.concat(asa.split(',')))
|
||||||
|
} else if (nameString.includes(' and ')) {
|
||||||
|
nameString.split(' and ').forEach((asa) => splitNames = splitNames.concat(asa.split(',')))
|
||||||
} else if (nameString.includes(';')) {
|
} else if (nameString.includes(';')) {
|
||||||
nameString.split(';').forEach((asa) => splitNames = splitNames.concat(asa.split(',')))
|
nameString.split(';').forEach((asa) => splitNames = splitNames.concat(asa.split(',')))
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ function parseCreators(metadata) {
|
|||||||
|
|
||||||
function fetchCreators(creators, role) {
|
function fetchCreators(creators, role) {
|
||||||
if (!creators || !creators.length) return null
|
if (!creators || !creators.length) return null
|
||||||
return creators.filter(c => c.role === role).map(c => c.value)
|
return [...new Set(creators.filter(c => c.role === role).map(c => c.value))]
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchTagString(metadata, tag) {
|
function fetchTagString(metadata, tag) {
|
||||||
@@ -92,7 +92,7 @@ function fetchDescription(metadata) {
|
|||||||
|
|
||||||
function fetchGenres(metadata) {
|
function fetchGenres(metadata) {
|
||||||
if (!metadata['dc:subject'] || !metadata['dc:subject'].length) return []
|
if (!metadata['dc:subject'] || !metadata['dc:subject'].length) return []
|
||||||
return metadata['dc:subject'].map(g => typeof g === 'string' ? g : null).filter(g => !!g)
|
return [...new Set(metadata['dc:subject'].filter(g => typeof g === 'string'))]
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchLanguage(metadata) {
|
function fetchLanguage(metadata) {
|
||||||
@@ -122,7 +122,7 @@ function fetchNarrators(creators, metadata) {
|
|||||||
|
|
||||||
function fetchTags(metadata) {
|
function fetchTags(metadata) {
|
||||||
if (!metadata['dc:tag'] || !metadata['dc:tag'].length) return []
|
if (!metadata['dc:tag'] || !metadata['dc:tag'].length) return []
|
||||||
return metadata['dc:tag'].filter(tag => (typeof tag === 'string'))
|
return [...new Set(metadata['dc:tag'].filter(tag => typeof tag === 'string'))]
|
||||||
}
|
}
|
||||||
|
|
||||||
function stripPrefix(str) {
|
function stripPrefix(str) {
|
||||||
|
|||||||
@@ -139,15 +139,16 @@ function isNullOrNaN(val) {
|
|||||||
*/
|
*/
|
||||||
function parseChapters(chapters) {
|
function parseChapters(chapters) {
|
||||||
if (!chapters) return []
|
if (!chapters) return []
|
||||||
|
let index = 0
|
||||||
return chapters.map(chap => {
|
return chapters.map(chap => {
|
||||||
var title = chap['TAG:title'] || chap.title || ''
|
let title = chap['TAG:title'] || chap.title || ''
|
||||||
if (!title && chap.tags && chap.tags.title) title = chap.tags.title
|
if (!title && chap.tags?.title) title = chap.tags.title
|
||||||
|
|
||||||
var timebase = chap.time_base && chap.time_base.includes('/') ? Number(chap.time_base.split('/')[1]) : 1
|
const timebase = chap.time_base?.includes('/') ? Number(chap.time_base.split('/')[1]) : 1
|
||||||
var start = !isNullOrNaN(chap.start_time) ? Number(chap.start_time) : !isNullOrNaN(chap.start) ? Number(chap.start) / timebase : 0
|
const start = !isNullOrNaN(chap.start_time) ? Number(chap.start_time) : !isNullOrNaN(chap.start) ? Number(chap.start) / timebase : 0
|
||||||
var end = !isNullOrNaN(chap.end_time) ? Number(chap.end_time) : !isNullOrNaN(chap.end) ? Number(chap.end) / timebase : 0
|
const end = !isNullOrNaN(chap.end_time) ? Number(chap.end_time) : !isNullOrNaN(chap.end) ? Number(chap.end) / timebase : 0
|
||||||
return {
|
return {
|
||||||
id: chap.id,
|
id: index++,
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
title
|
title
|
||||||
|
|||||||
Reference in New Issue
Block a user