mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-04 01:40:40 +02:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ca5f781531 | |||
| 53c96b2540 | |||
| 9712bdf5f0 | |||
| 0678c26627 | |||
| b52e240025 | |||
| 2fa73f7a8d | |||
| 2cc23b6d6b | |||
| 9a617226b3 | |||
| fbfc015d92 | |||
| 3e4c94e2b4 | |||
| 1da471e136 | |||
| 4dba95c000 | |||
| 36477a832c | |||
| b4aa8f0c9a | |||
| 6a974d5ef0 | |||
| 304eda9f8c | |||
| 581f2e3d15 | |||
| be2d317325 | |||
| 9f6bfeb839 | |||
| f4f5f79af7 | |||
| 92bb2fb23d | |||
| 3c406c12b4 | |||
| 81d4ac3ed2 | |||
| 32bdae31a8 | |||
| 84c16c4a39 | |||
| b8b3d05f5e | |||
| bac09de23d | |||
| b0bf9604bb | |||
| 688531f0a7 | |||
| dfc7877f69 | |||
| e00116a0e3 | |||
| 2ab287e2a9 | |||
| 1e0da09b2f | |||
| 0e7a5649cc | |||
| 30009e45da |
@@ -232,6 +232,20 @@ Bookshelf Label
|
|||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.episode-subtitle-long {
|
||||||
|
word-break: break-word;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
line-height: 16px;
|
||||||
|
/* fallback */
|
||||||
|
max-height: 72px;
|
||||||
|
/* fallback */
|
||||||
|
-webkit-line-clamp: 6;
|
||||||
|
/* number of lines to show */
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Padding for toastification toasts in the top right to not cover appbar/toolbar */
|
/* Padding for toastification toasts in the top right to not cover appbar/toolbar */
|
||||||
.app-bar-and-toolbar .Vue-Toastification__container.top-right {
|
.app-bar-and-toolbar .Vue-Toastification__container.top-right {
|
||||||
|
|||||||
@@ -15,8 +15,6 @@
|
|||||||
<controls-global-search v-if="currentLibrary" class="mr-1 sm:mr-0" />
|
<controls-global-search v-if="currentLibrary" class="mr-1 sm:mr-0" />
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|
||||||
<widgets-notification-widget class="hidden md:block" />
|
|
||||||
|
|
||||||
<ui-tooltip v-if="isChromecastInitialized && !isHttps" direction="bottom" text="Casting requires a secure connection" class="flex items-center">
|
<ui-tooltip v-if="isChromecastInitialized && !isHttps" direction="bottom" text="Casting requires a secure connection" class="flex items-center">
|
||||||
<span class="material-icons-outlined text-2xl text-warning text-opacity-50"> cast </span>
|
<span class="material-icons-outlined text-2xl text-warning text-opacity-50"> cast </span>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
@@ -24,6 +22,8 @@
|
|||||||
<google-cast-launcher></google-cast-launcher>
|
<google-cast-launcher></google-cast-launcher>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<widgets-notification-widget class="hidden md:block" />
|
||||||
|
|
||||||
<nuxt-link v-if="currentLibrary" to="/config/stats" class="hover:text-gray-200 cursor-pointer w-8 h-8 hidden sm:flex items-center justify-center mx-1">
|
<nuxt-link v-if="currentLibrary" to="/config/stats" class="hover:text-gray-200 cursor-pointer w-8 h-8 hidden sm:flex items-center justify-center mx-1">
|
||||||
<ui-tooltip :text="$strings.HeaderYourStats" direction="bottom" class="flex items-center">
|
<ui-tooltip :text="$strings.HeaderYourStats" direction="bottom" class="flex items-center">
|
||||||
<span class="material-icons text-2xl" aria-label="User Stats" role="button">equalizer</span>
|
<span class="material-icons text-2xl" aria-label="User Stats" role="button">equalizer</span>
|
||||||
@@ -178,6 +178,11 @@ export default {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
options.push({
|
||||||
|
text: 'Re-Scan',
|
||||||
|
action: 'rescan'
|
||||||
|
})
|
||||||
|
|
||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -211,8 +216,34 @@ export default {
|
|||||||
this.requestBatchQuickEmbed()
|
this.requestBatchQuickEmbed()
|
||||||
} else if (action === 'quick-match') {
|
} else if (action === 'quick-match') {
|
||||||
this.batchAutoMatchClick()
|
this.batchAutoMatchClick()
|
||||||
|
} else if (action === 'rescan') {
|
||||||
|
this.batchRescan()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async batchRescan() {
|
||||||
|
const payload = {
|
||||||
|
message: `Are you sure you want to re-scan ${this.selectedMediaItems.length} items?`,
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.$axios
|
||||||
|
.$post(`/api/items/batch/scan`, {
|
||||||
|
libraryItemIds: this.selectedMediaItems.map((i) => i.id)
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
console.log('Batch Re-Scan started')
|
||||||
|
this.cancelSelectionMode()
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Batch Re-Scan failed', error)
|
||||||
|
const errorMsg = error.response.data || 'Failed to batch re-scan'
|
||||||
|
this.$toast.error(errorMsg)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
},
|
||||||
async playSelectedItems() {
|
async playSelectedItems() {
|
||||||
this.$store.commit('setProcessingBatch', true)
|
this.$store.commit('setProcessingBatch', true)
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<!-- Alternate plain view -->
|
<!-- Alternate plain view -->
|
||||||
<div v-else-if="isAlternativeBookshelfView" class="w-full mb-24">
|
<div v-else-if="isAlternativeBookshelfView" class="w-full mb-24">
|
||||||
<template v-for="(shelf, index) in shelves">
|
<template v-for="(shelf, index) in shelves">
|
||||||
<widgets-item-slider v-if="shelf.type === 'book' || shelf.type === 'podcast'" :shelf-id="shelf.id" :key="index + '.'" :items="shelf.entities" :continue-listening-shelf="shelf.id === 'continue-listening'" :height="232 * sizeMultiplier" class="bookshelf-row pl-8 my-6" @selectEntity="(payload) => selectEntity(payload, index)">
|
<widgets-item-slider v-if="shelf.type === 'book' || shelf.type === 'podcast'" :shelf-id="shelf.id" :key="index + '.'" :items="shelf.entities" :continue-listening-shelf="shelf.id === 'continue-listening' || shelf.id === 'continue-reading'" :height="232 * sizeMultiplier" class="bookshelf-row pl-8 my-6" @selectEntity="(payload) => selectEntity(payload, index)">
|
||||||
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ $strings[shelf.labelStringKey] }}</p>
|
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ $strings[shelf.labelStringKey] }}</p>
|
||||||
</widgets-item-slider>
|
</widgets-item-slider>
|
||||||
<widgets-episode-slider v-else-if="shelf.type === 'episode'" :key="index + '.'" :items="shelf.entities" :continue-listening-shelf="shelf.id === 'continue-listening'" :height="232 * sizeMultiplier" class="bookshelf-row pl-8 my-6" @selectEntity="(payload) => selectEntity(payload, index)">
|
<widgets-episode-slider v-else-if="shelf.type === 'episode'" :key="index + '.'" :items="shelf.entities" :continue-listening-shelf="shelf.id === 'continue-listening'" :height="232 * sizeMultiplier" class="bookshelf-row pl-8 my-6" @selectEntity="(payload) => selectEntity(payload, index)">
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
<!-- Regular bookshelf view -->
|
<!-- Regular bookshelf view -->
|
||||||
<div v-else class="w-full">
|
<div v-else class="w-full">
|
||||||
<template v-for="(shelf, index) in shelves">
|
<template v-for="(shelf, index) in shelves">
|
||||||
<app-book-shelf-row :key="index" :index="index" :shelf="shelf" :size-multiplier="sizeMultiplier" :book-cover-width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" :continue-listening-shelf="shelf.id === 'continue-listening'" @selectEntity="(payload) => selectEntity(payload, index)" />
|
<app-book-shelf-row :key="index" :index="index" :shelf="shelf" :size-multiplier="sizeMultiplier" :book-cover-width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" :continue-listening-shelf="shelf.id === 'continue-listening' || shelf.id === 'continue-reading'" @selectEntity="(payload) => selectEntity(payload, index)" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -286,7 +286,8 @@ export default {
|
|||||||
}
|
}
|
||||||
if (user.mediaProgress.length) {
|
if (user.mediaProgress.length) {
|
||||||
const mediaProgressToHide = user.mediaProgress.filter((mp) => mp.hideFromContinueListening)
|
const mediaProgressToHide = user.mediaProgress.filter((mp) => mp.hideFromContinueListening)
|
||||||
this.removeItemsFromContinueListening(mediaProgressToHide)
|
this.removeItemsFromContinueListeningReading(mediaProgressToHide, 'continue-listening')
|
||||||
|
this.removeItemsFromContinueListeningReading(mediaProgressToHide, 'continue-reading')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
libraryItemAdded(libraryItem) {
|
libraryItemAdded(libraryItem) {
|
||||||
@@ -336,8 +337,9 @@ export default {
|
|||||||
},
|
},
|
||||||
libraryItemsAdded(libraryItems) {
|
libraryItemsAdded(libraryItems) {
|
||||||
console.log('libraryItems added', libraryItems)
|
console.log('libraryItems added', libraryItems)
|
||||||
// TODO: Check if audiobook would be on this shelf
|
|
||||||
if (!this.search) {
|
const isThisLibrary = !libraryItems.some((li) => li.libraryId !== this.currentLibraryId)
|
||||||
|
if (!this.search && isThisLibrary) {
|
||||||
this.fetchCategories()
|
this.fetchCategories()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -346,6 +348,14 @@ export default {
|
|||||||
this.libraryItemUpdated(li)
|
this.libraryItemUpdated(li)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
episodeAdded(episodeWithLibraryItem) {
|
||||||
|
console.log('Podcast episode added', episodeWithLibraryItem)
|
||||||
|
|
||||||
|
const isThisLibrary = episodeWithLibraryItem.libraryItem?.libraryId === this.currentLibraryId
|
||||||
|
if (!this.search && isThisLibrary) {
|
||||||
|
this.fetchCategories()
|
||||||
|
}
|
||||||
|
},
|
||||||
removeAllSeriesFromContinueSeries(seriesIds) {
|
removeAllSeriesFromContinueSeries(seriesIds) {
|
||||||
this.shelves.forEach((shelf) => {
|
this.shelves.forEach((shelf) => {
|
||||||
if (shelf.type == 'book' && shelf.id == 'continue-series') {
|
if (shelf.type == 'book' && shelf.id == 'continue-series') {
|
||||||
@@ -357,8 +367,8 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
removeItemsFromContinueListening(mediaProgressItems) {
|
removeItemsFromContinueListeningReading(mediaProgressItems, categoryId) {
|
||||||
const continueListeningShelf = this.shelves.find((s) => s.id === 'continue-listening')
|
const continueListeningShelf = this.shelves.find((s) => s.id === categoryId)
|
||||||
if (continueListeningShelf) {
|
if (continueListeningShelf) {
|
||||||
if (continueListeningShelf.type === 'book') {
|
if (continueListeningShelf.type === 'book') {
|
||||||
continueListeningShelf.entities = continueListeningShelf.entities.filter((ent) => {
|
continueListeningShelf.entities = continueListeningShelf.entities.filter((ent) => {
|
||||||
@@ -373,17 +383,6 @@ export default {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// this.shelves.forEach((shelf) => {
|
|
||||||
// if (shelf.id == 'continue-listening') {
|
|
||||||
// if (shelf.type == 'book') {
|
|
||||||
// // Filter out books from continue listening shelf
|
|
||||||
// shelf.entities = shelf.entities.filter((ent) => {
|
|
||||||
// if (mediaProgressItems.some(mp => mp.libraryItemId === ent.id)) return false
|
|
||||||
// return true
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
},
|
},
|
||||||
authorUpdated(author) {
|
authorUpdated(author) {
|
||||||
this.shelves.forEach((shelf) => {
|
this.shelves.forEach((shelf) => {
|
||||||
@@ -417,6 +416,7 @@ export default {
|
|||||||
this.$root.socket.on('item_removed', this.libraryItemRemoved)
|
this.$root.socket.on('item_removed', this.libraryItemRemoved)
|
||||||
this.$root.socket.on('items_updated', this.libraryItemsUpdated)
|
this.$root.socket.on('items_updated', this.libraryItemsUpdated)
|
||||||
this.$root.socket.on('items_added', this.libraryItemsAdded)
|
this.$root.socket.on('items_added', this.libraryItemsAdded)
|
||||||
|
this.$root.socket.on('episode_added', this.episodeAdded)
|
||||||
} else {
|
} else {
|
||||||
console.error('Error socket not initialized')
|
console.error('Error socket not initialized')
|
||||||
}
|
}
|
||||||
@@ -431,6 +431,7 @@ export default {
|
|||||||
this.$root.socket.off('item_removed', this.libraryItemRemoved)
|
this.$root.socket.off('item_removed', this.libraryItemRemoved)
|
||||||
this.$root.socket.off('items_updated', this.libraryItemsUpdated)
|
this.$root.socket.off('items_updated', this.libraryItemsUpdated)
|
||||||
this.$root.socket.off('items_added', this.libraryItemsAdded)
|
this.$root.socket.off('items_added', this.libraryItemsAdded)
|
||||||
|
this.$root.socket.off('episode_added', this.episodeAdded)
|
||||||
} else {
|
} else {
|
||||||
console.error('Error socket not initialized')
|
console.error('Error socket not initialized')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<div v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ podcastAuthor }}</div>
|
<div v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ podcastAuthor }}</div>
|
||||||
<div v-else-if="musicArtists" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ musicArtists }}</div>
|
<div v-else-if="musicArtists" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ musicArtists }}</div>
|
||||||
<div v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base">
|
<div v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base">
|
||||||
<nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">, </span></nuxt-link>
|
<nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}?library=${libraryId}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">, </span></nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-xs sm:text-base cursor-pointer pl-1 sm:pl-1.5">{{ $strings.LabelUnknown }}</div>
|
<div v-else class="text-xs sm:text-base cursor-pointer pl-1 sm:pl-1.5">{{ $strings.LabelUnknown }}</div>
|
||||||
<widgets-explicit-indicator :explicit="isExplicit"></widgets-explicit-indicator>
|
<widgets-explicit-indicator :explicit="isExplicit"></widgets-explicit-indicator>
|
||||||
@@ -268,6 +268,10 @@ export default {
|
|||||||
seek(time) {
|
seek(time) {
|
||||||
this.playerHandler.seek(time)
|
this.playerHandler.seek(time)
|
||||||
},
|
},
|
||||||
|
playbackTimeUpdate(time) {
|
||||||
|
// When updating progress from another session
|
||||||
|
this.playerHandler.seek(time, false)
|
||||||
|
},
|
||||||
setCurrentTime(time) {
|
setCurrentTime(time) {
|
||||||
this.currentTime = time
|
this.currentTime = time
|
||||||
if (this.$refs.audioPlayer) {
|
if (this.$refs.audioPlayer) {
|
||||||
@@ -366,9 +370,8 @@ export default {
|
|||||||
navigator.mediaSession.setActionHandler('seekbackward', this.mediaSessionSeekBackward)
|
navigator.mediaSession.setActionHandler('seekbackward', this.mediaSessionSeekBackward)
|
||||||
navigator.mediaSession.setActionHandler('seekforward', this.mediaSessionSeekForward)
|
navigator.mediaSession.setActionHandler('seekforward', this.mediaSessionSeekForward)
|
||||||
navigator.mediaSession.setActionHandler('seekto', this.mediaSessionSeekTo)
|
navigator.mediaSession.setActionHandler('seekto', this.mediaSessionSeekTo)
|
||||||
navigator.mediaSession.setActionHandler('previoustrack', this.mediaSessionPreviousTrack)
|
navigator.mediaSession.setActionHandler('previoustrack', this.mediaSessionSeekBackward)
|
||||||
const hasNextChapter = this.$refs.audioPlayer && this.$refs.audioPlayer.hasNextChapter
|
navigator.mediaSession.setActionHandler('nexttrack', this.mediaSessionSeekForward)
|
||||||
navigator.mediaSession.setActionHandler('nexttrack', hasNextChapter ? this.mediaSessionNextTrack : null)
|
|
||||||
} else {
|
} else {
|
||||||
console.warn('Media session not available')
|
console.warn('Media session not available')
|
||||||
}
|
}
|
||||||
@@ -478,12 +481,14 @@ export default {
|
|||||||
mounted() {
|
mounted() {
|
||||||
this.$eventBus.$on('cast-session-active', this.castSessionActive)
|
this.$eventBus.$on('cast-session-active', this.castSessionActive)
|
||||||
this.$eventBus.$on('playback-seek', this.seek)
|
this.$eventBus.$on('playback-seek', this.seek)
|
||||||
|
this.$eventBus.$on('playback-time-update', this.playbackTimeUpdate)
|
||||||
this.$eventBus.$on('play-item', this.playLibraryItem)
|
this.$eventBus.$on('play-item', this.playLibraryItem)
|
||||||
this.$eventBus.$on('pause-item', this.pauseItem)
|
this.$eventBus.$on('pause-item', this.pauseItem)
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.$eventBus.$off('cast-session-active', this.castSessionActive)
|
this.$eventBus.$off('cast-session-active', this.castSessionActive)
|
||||||
this.$eventBus.$off('playback-seek', this.seek)
|
this.$eventBus.$off('playback-seek', this.seek)
|
||||||
|
this.$eventBus.$off('playback-time-update', this.playbackTimeUpdate)
|
||||||
this.$eventBus.$off('play-item', this.playLibraryItem)
|
this.$eventBus.$off('play-item', this.playLibraryItem)
|
||||||
this.$eventBus.$off('pause-item', this.pauseItem)
|
this.$eventBus.$off('pause-item', this.pauseItem)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<nuxt-link :to="`/author/${author.id}`">
|
<nuxt-link :to="`/author/${author.id}?library=${currentLibraryId}`">
|
||||||
<div @mouseover="mouseover" @mouseleave="mouseleave">
|
<div @mouseover="mouseover" @mouseleave="mouseleave">
|
||||||
<div :style="{ width: width + 'px', height: height + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
|
<div :style="{ width: width + 'px', height: height + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
|
||||||
<!-- Image or placeholder -->
|
<!-- Image or placeholder -->
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<p v-if="matchKey !== 'title'" class="truncate text-sm">{{ title }}</p>
|
<p v-if="matchKey !== 'title'" class="truncate text-sm">{{ title }}</p>
|
||||||
<p v-else class="truncate text-sm" v-html="matchHtml" />
|
<p v-else class="truncate text-sm" v-html="matchHtml" />
|
||||||
|
|
||||||
<p v-if="matchKey === 'subtitle'" class="truncate text-xs text-gray-300">{{ matchHtml }}</p>
|
<p v-if="matchKey === 'subtitle'" class="truncate text-xs text-gray-300" v-html="matchHtml" />
|
||||||
|
|
||||||
<p v-if="matchKey !== 'authors'" class="text-xs text-gray-200 truncate">by {{ authorName }}</p>
|
<p v-if="matchKey !== 'authors'" class="text-xs text-gray-200 truncate">by {{ authorName }}</p>
|
||||||
<p v-else class="truncate text-xs text-gray-200" v-html="matchHtml" />
|
<p v-else class="truncate text-xs text-gray-200" v-html="matchHtml" />
|
||||||
@@ -61,7 +61,6 @@ export default {
|
|||||||
},
|
},
|
||||||
matchHtml() {
|
matchHtml() {
|
||||||
if (!this.matchText || !this.search) return ''
|
if (!this.matchText || !this.search) return ''
|
||||||
if (this.matchKey === 'subtitle') return ''
|
|
||||||
|
|
||||||
// This used to highlight the part of the search found
|
// This used to highlight the part of the search found
|
||||||
// but with removing commas periods etc this is no longer plausible
|
// but with removing commas periods etc this is no longer plausible
|
||||||
@@ -69,6 +68,7 @@ export default {
|
|||||||
|
|
||||||
if (this.matchKey === 'episode') return `<p class="truncate">${this.$strings.LabelEpisode}: ${html}</p>`
|
if (this.matchKey === 'episode') return `<p class="truncate">${this.$strings.LabelEpisode}: ${html}</p>`
|
||||||
if (this.matchKey === 'tags') return `<p class="truncate">${this.$strings.LabelTags}: ${html}</p>`
|
if (this.matchKey === 'tags') return `<p class="truncate">${this.$strings.LabelTags}: ${html}</p>`
|
||||||
|
if (this.matchKey === 'subtitle') return `<p class="truncate">${html}</p>`
|
||||||
if (this.matchKey === 'authors') return `by ${html}`
|
if (this.matchKey === 'authors') return `by ${html}`
|
||||||
if (this.matchKey === 'isbn') return `<p class="truncate">ISBN: ${html}</p>`
|
if (this.matchKey === 'isbn') return `<p class="truncate">ISBN: ${html}</p>`
|
||||||
if (this.matchKey === 'asin') return `<p class="truncate">ASIN: ${html}</p>`
|
if (this.matchKey === 'asin') return `<p class="truncate">ASIN: ${html}</p>`
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex items-center h-full px-1 overflow-hidden">
|
<div class="flex items-center px-1 overflow-hidden">
|
||||||
<div class="h-5 w-5 min-w-5 text-lg mr-1.5 flex items-center justify-center">
|
<div class="w-8 flex items-center justify-center">
|
||||||
<span v-if="isFinished" :class="taskIconStatus" class="material-icons text-base">{{actionIcon}}</span>
|
<!-- <div class="text-lg"> -->
|
||||||
|
<span v-if="isFinished" :class="taskIconStatus" class="material-icons text-base">{{ actionIcon }}</span>
|
||||||
<widgets-loading-spinner v-else />
|
<widgets-loading-spinner v-else />
|
||||||
|
<!-- </div> -->
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-2 taskRunningCardContent">
|
<div class="flex-grow px-2 taskRunningCardContent">
|
||||||
<p class="truncate text-sm">{{ title }}</p>
|
<p class="truncate text-sm">{{ title }}</p>
|
||||||
@@ -36,10 +38,13 @@ export default {
|
|||||||
return this.task.details || 'Unknown'
|
return this.task.details || 'Unknown'
|
||||||
},
|
},
|
||||||
isFinished() {
|
isFinished() {
|
||||||
return this.task.isFinished || false
|
return !!this.task.isFinished
|
||||||
},
|
},
|
||||||
isFailed() {
|
isFailed() {
|
||||||
return this.task.isFailed || false
|
return !!this.task.isFailed
|
||||||
|
},
|
||||||
|
isSuccess() {
|
||||||
|
return this.isFinished && !this.isFailed
|
||||||
},
|
},
|
||||||
failedMessage() {
|
failedMessage() {
|
||||||
return this.task.error || ''
|
return this.task.error || ''
|
||||||
@@ -48,6 +53,11 @@ export default {
|
|||||||
return this.task.action || ''
|
return this.task.action || ''
|
||||||
},
|
},
|
||||||
actionIcon() {
|
actionIcon() {
|
||||||
|
if (this.isFailed) {
|
||||||
|
return 'error'
|
||||||
|
} else if (this.isSuccess) {
|
||||||
|
return 'done'
|
||||||
|
}
|
||||||
switch (this.action) {
|
switch (this.action) {
|
||||||
case 'download-podcast-episode':
|
case 'download-podcast-episode':
|
||||||
return 'cloud_download'
|
return 'cloud_download'
|
||||||
@@ -68,16 +78,15 @@ export default {
|
|||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {},
|
||||||
},
|
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.taskRunningCardContent {
|
.taskRunningCardContent {
|
||||||
width: calc(100% - 80px);
|
width: calc(100% - 84px);
|
||||||
height: 75px;
|
height: 60px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ export default {
|
|||||||
var files = this.item.itemFiles.concat(this.item.otherFiles)
|
var files = this.item.itemFiles.concat(this.item.otherFiles)
|
||||||
return {
|
return {
|
||||||
index: this.item.index,
|
index: this.item.index,
|
||||||
|
directory: this.directory,
|
||||||
...this.itemData,
|
...this.itemData,
|
||||||
files
|
files
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,10 @@
|
|||||||
<div ref="moreIcon" v-show="!isSelectionMode && moreMenuItems.length" class="hidden md:block absolute cursor-pointer hover:text-yellow-300 300 hover:scale-125 transform duration-150" :style="{ bottom: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickShowMore">
|
<div ref="moreIcon" v-show="!isSelectionMode && moreMenuItems.length" class="hidden md:block absolute cursor-pointer hover:text-yellow-300 300 hover:scale-125 transform duration-150" :style="{ bottom: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickShowMore">
|
||||||
<span class="material-icons" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">more_vert</span>
|
<span class="material-icons" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">more_vert</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="ebookFormat" class="absolute" :style="{ bottom: 0.375 * sizeMultiplier + 'rem', left: 0.375 * sizeMultiplier + 'rem' }">
|
||||||
|
<span class="text-white/80" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ ebookFormat }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Processing/loading spinner overlay -->
|
<!-- Processing/loading spinner overlay -->
|
||||||
@@ -221,7 +225,7 @@ export default {
|
|||||||
libraryId() {
|
libraryId() {
|
||||||
return this._libraryItem.libraryId
|
return this._libraryItem.libraryId
|
||||||
},
|
},
|
||||||
hasEbook() {
|
ebookFormat() {
|
||||||
return this.media.ebookFormat
|
return this.media.ebookFormat
|
||||||
},
|
},
|
||||||
numTracks() {
|
numTracks() {
|
||||||
@@ -252,14 +256,14 @@ export default {
|
|||||||
},
|
},
|
||||||
booksInSeries() {
|
booksInSeries() {
|
||||||
// Only added to item object when collapseSeries is enabled
|
// Only added to item object when collapseSeries is enabled
|
||||||
return this.collapsedSeries ? this.collapsedSeries.numBooks : 0
|
return this.collapsedSeries?.numBooks || 0
|
||||||
},
|
},
|
||||||
seriesSequenceList() {
|
seriesSequenceList() {
|
||||||
return this.collapsedSeries ? this.collapsedSeries.seriesSequenceList : null
|
return this.collapsedSeries?.seriesSequenceList || null
|
||||||
},
|
},
|
||||||
libraryItemIdsInSeries() {
|
libraryItemIdsInSeries() {
|
||||||
// Only added to item object when collapseSeries is enabled
|
// Only added to item object when collapseSeries is enabled
|
||||||
return this.collapsedSeries ? this.collapsedSeries.libraryItemIds || [] : []
|
return this.collapsedSeries?.libraryItemIds || []
|
||||||
},
|
},
|
||||||
hasCover() {
|
hasCover() {
|
||||||
return !!this.media.coverPath
|
return !!this.media.coverPath
|
||||||
@@ -325,6 +329,9 @@ export default {
|
|||||||
if (this.episodeProgress) return this.episodeProgress
|
if (this.episodeProgress) return this.episodeProgress
|
||||||
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
||||||
},
|
},
|
||||||
|
isEBookOnly() {
|
||||||
|
return !this.numTracks && this.ebookFormat
|
||||||
|
},
|
||||||
useEBookProgress() {
|
useEBookProgress() {
|
||||||
if (!this.userProgress || this.userProgress.progress) return false
|
if (!this.userProgress || this.userProgress.progress) return false
|
||||||
return this.userProgress.ebookProgress > 0
|
return this.userProgress.ebookProgress > 0
|
||||||
@@ -360,13 +367,13 @@ export default {
|
|||||||
return this.store.getters['getIsStreamingFromDifferentLibrary']
|
return this.store.getters['getIsStreamingFromDifferentLibrary']
|
||||||
},
|
},
|
||||||
showReadButton() {
|
showReadButton() {
|
||||||
return !this.isSelectionMode && !this.showPlayButton && this.hasEbook && (this.showExperimentalFeatures || this.enableEReader)
|
return !this.isSelectionMode && !this.showPlayButton && this.ebookFormat && (this.showExperimentalFeatures || this.enableEReader)
|
||||||
},
|
},
|
||||||
showPlayButton() {
|
showPlayButton() {
|
||||||
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && !this.isStreaming && (this.numTracks || this.recentEpisode || this.isMusic)
|
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && !this.isStreaming && (this.numTracks || this.recentEpisode || this.isMusic)
|
||||||
},
|
},
|
||||||
showSmallEBookIcon() {
|
showSmallEBookIcon() {
|
||||||
return !this.isSelectionMode && this.hasEbook && (this.showExperimentalFeatures || this.enableEReader)
|
return !this.isSelectionMode && this.ebookFormat && (this.showExperimentalFeatures || this.enableEReader)
|
||||||
},
|
},
|
||||||
isMissing() {
|
isMissing() {
|
||||||
return this._libraryItem.isMissing
|
return this._libraryItem.isMissing
|
||||||
@@ -441,6 +448,7 @@ export default {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
if (this.continueListeningShelf) {
|
if (this.continueListeningShelf) {
|
||||||
|
|
||||||
items.push({
|
items.push({
|
||||||
func: 'removeFromContinueListening',
|
func: 'removeFromContinueListening',
|
||||||
text: this.$strings.ButtonRemoveFromContinueListening
|
text: this.$strings.ButtonRemoveFromContinueListening
|
||||||
@@ -508,7 +516,7 @@ export default {
|
|||||||
if (this.continueListeningShelf) {
|
if (this.continueListeningShelf) {
|
||||||
items.push({
|
items.push({
|
||||||
func: 'removeFromContinueListening',
|
func: 'removeFromContinueListening',
|
||||||
text: this.$strings.ButtonRemoveFromContinueListening
|
text: this.isEBookOnly ? this.$strings.ButtonRemoveFromContinueReading : this.$strings.ButtonRemoveFromContinueListening
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (!this.isPodcast) {
|
if (!this.isPodcast) {
|
||||||
@@ -865,7 +873,8 @@ export default {
|
|||||||
this.createMoreMenu()
|
this.createMoreMenu()
|
||||||
},
|
},
|
||||||
async clickReadEBook() {
|
async clickReadEBook() {
|
||||||
var libraryItem = await this.$axios.$get(`/api/items/${this.libraryItemId}?expanded=1`).catch((error) => {
|
const axios = this.$axios || this.$nuxt.$axios
|
||||||
|
var libraryItem = await axios.$get(`/api/items/${this.libraryItemId}?expanded=1`).catch((error) => {
|
||||||
console.error('Failed to get lirbary item', this.libraryItemId)
|
console.error('Failed to get lirbary item', this.libraryItemId)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<form class="flex items-center justify-center px-6 py-3" @submit.prevent="submitCustomTime">
|
<form class="flex items-center justify-center px-6 py-3" @submit.prevent="submitCustomTime">
|
||||||
<ui-text-input v-model="customTime" type="number" step="any" min="0.1" placeholder="Time in minutes" class="w-48" />
|
<ui-text-input v-model="customTime" type="number" step="any" min="0.1" placeholder="Time in minutes" class="w-48" />
|
||||||
<ui-btn color="success" type="submit" padding-x="0" class="h-9 w-12 flex items-center justify-center ml-1">Set</ui-btn>
|
<ui-btn color="success" type="submit" :padding-x="0" class="h-9 w-12 flex items-center justify-center ml-1">Set</ui-btn>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="w-full p-4">
|
<div v-else class="w-full p-4">
|
||||||
|
|||||||
@@ -74,6 +74,9 @@ export default {
|
|||||||
this.$store.commit('setEditModalTab', val)
|
this.$store.commit('setEditModalTab', val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
height() {
|
||||||
|
return Math.min(this.availableHeight, 650)
|
||||||
|
},
|
||||||
tabs() {
|
tabs() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -136,6 +139,18 @@ export default {
|
|||||||
userIsAdminOrUp() {
|
userIsAdminOrUp() {
|
||||||
return this.$store.getters['user/getIsAdminOrUp']
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
},
|
},
|
||||||
|
selectedLibraryItem() {
|
||||||
|
return this.$store.state.selectedLibraryItem || {}
|
||||||
|
},
|
||||||
|
selectedLibraryItemId() {
|
||||||
|
return this.selectedLibraryItem.id
|
||||||
|
},
|
||||||
|
media() {
|
||||||
|
return this.libraryItem?.media || {}
|
||||||
|
},
|
||||||
|
mediaMetadata() {
|
||||||
|
return this.media.metadata || {}
|
||||||
|
},
|
||||||
availableTabs() {
|
availableTabs() {
|
||||||
if (!this.userCanUpdate && !this.userCanDownload) return []
|
if (!this.userCanUpdate && !this.userCanDownload) return []
|
||||||
return this.tabs.filter((tab) => {
|
return this.tabs.filter((tab) => {
|
||||||
@@ -144,6 +159,7 @@ export default {
|
|||||||
if (tab.admin && !this.userIsAdminOrUp) return false
|
if (tab.admin && !this.userIsAdminOrUp) return false
|
||||||
|
|
||||||
if (tab.id === 'tools' && this.isMissing) return false
|
if (tab.id === 'tools' && this.isMissing) return false
|
||||||
|
if (tab.id === 'chapters' && this.isEBookOnly) return false
|
||||||
|
|
||||||
if ((tab.id === 'tools' || tab.id === 'files') && this.userCanDownload) return true
|
if ((tab.id === 'tools' || tab.id === 'files') && this.userCanDownload) return true
|
||||||
if (tab.id !== 'tools' && tab.id !== 'files' && this.userCanUpdate) return true
|
if (tab.id !== 'tools' && tab.id !== 'files' && this.userCanUpdate) return true
|
||||||
@@ -151,9 +167,6 @@ export default {
|
|||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
height() {
|
|
||||||
return Math.min(this.availableHeight, 650)
|
|
||||||
},
|
|
||||||
tabName() {
|
tabName() {
|
||||||
var _tab = this.tabs.find((t) => t.id === this.selectedTab)
|
var _tab = this.tabs.find((t) => t.id === this.selectedTab)
|
||||||
return _tab ? _tab.component : ''
|
return _tab ? _tab.component : ''
|
||||||
@@ -161,20 +174,11 @@ export default {
|
|||||||
isMissing() {
|
isMissing() {
|
||||||
return this.selectedLibraryItem.isMissing
|
return this.selectedLibraryItem.isMissing
|
||||||
},
|
},
|
||||||
selectedLibraryItem() {
|
isEBookOnly() {
|
||||||
return this.$store.state.selectedLibraryItem || {}
|
return this.media.ebookFile && !this.media.tracks?.length
|
||||||
},
|
|
||||||
selectedLibraryItemId() {
|
|
||||||
return this.selectedLibraryItem.id
|
|
||||||
},
|
|
||||||
media() {
|
|
||||||
return this.libraryItem ? this.libraryItem.media || {} : {}
|
|
||||||
},
|
|
||||||
mediaMetadata() {
|
|
||||||
return this.media.metadata || {}
|
|
||||||
},
|
},
|
||||||
mediaType() {
|
mediaType() {
|
||||||
return this.libraryItem ? this.libraryItem.mediaType : null
|
return this.libraryItem?.mediaType || null
|
||||||
},
|
},
|
||||||
title() {
|
title() {
|
||||||
return this.mediaMetadata.title || 'No Title'
|
return this.mediaMetadata.title || 'No Title'
|
||||||
|
|||||||
@@ -17,10 +17,10 @@
|
|||||||
<div class="flex-grow sm:pl-2 md:pl-6 sm:pr-2 mt-2 md:mt-0">
|
<div class="flex-grow sm:pl-2 md:pl-6 sm:pr-2 mt-2 md:mt-0">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div v-if="userCanUpload" class="w-10 md:w-40 pr-2 pt-4 md:min-w-32">
|
<div v-if="userCanUpload" class="w-10 md:w-40 pr-2 pt-4 md:min-w-32">
|
||||||
<ui-file-input ref="fileInput" @change="fileUploadSelected"
|
<ui-file-input ref="fileInput" @change="fileUploadSelected">
|
||||||
><span class="hidden md:inline-block">{{ $strings.ButtonUploadCover }}</span
|
<span class="hidden md:inline-block">{{ $strings.ButtonUploadCover }}</span>
|
||||||
><span class="material-icons text-2xl inline-block md:!hidden">upload</span></ui-file-input
|
<span class="material-icons text-2xl inline-block md:!hidden">upload</span>
|
||||||
>
|
</ui-file-input>
|
||||||
</div>
|
</div>
|
||||||
<form @submit.prevent="submitForm" class="flex flex-grow">
|
<form @submit.prevent="submitForm" class="flex flex-grow">
|
||||||
<ui-text-input-with-label v-model="imageUrl" :label="$strings.LabelCoverImageURL" />
|
<ui-text-input-with-label v-model="imageUrl" :label="$strings.LabelCoverImageURL" />
|
||||||
@@ -128,7 +128,7 @@ export default {
|
|||||||
},
|
},
|
||||||
providers() {
|
providers() {
|
||||||
if (this.isPodcast) return this.$store.state.scanners.podcastProviders
|
if (this.isPodcast) return this.$store.state.scanners.podcastProviders
|
||||||
return [...this.$store.state.scanners.providers, ...this.$store.state.scanners.coverOnlyProviders]
|
return [{ text: 'All', value: 'all' }, ...this.$store.state.scanners.providers, ...this.$store.state.scanners.coverOnlyProviders]
|
||||||
},
|
},
|
||||||
searchTitleLabel() {
|
searchTitleLabel() {
|
||||||
if (this.provider.startsWith('audible')) return this.$strings.LabelSearchTitleOrASIN
|
if (this.provider.startsWith('audible')) return this.$strings.LabelSearchTitleOrASIN
|
||||||
@@ -223,7 +223,7 @@ export default {
|
|||||||
this.searchTitle = this.mediaMetadata.title || ''
|
this.searchTitle = this.mediaMetadata.title || ''
|
||||||
this.searchAuthor = this.mediaMetadata.authorName || ''
|
this.searchAuthor = this.mediaMetadata.authorName || ''
|
||||||
if (this.isPodcast) this.provider = 'itunes'
|
if (this.isPodcast) this.provider = 'itunes'
|
||||||
else this.provider = localStorage.getItem('book-provider') || 'google'
|
else this.provider = localStorage.getItem('book-cover-provider') || localStorage.getItem('book-provider') || 'google'
|
||||||
},
|
},
|
||||||
removeCover() {
|
removeCover() {
|
||||||
if (!this.media.coverPath) {
|
if (!this.media.coverPath) {
|
||||||
@@ -288,13 +288,13 @@ export default {
|
|||||||
},
|
},
|
||||||
getSearchQuery() {
|
getSearchQuery() {
|
||||||
var searchQuery = `provider=${this.provider}&title=${this.searchTitle}`
|
var searchQuery = `provider=${this.provider}&title=${this.searchTitle}`
|
||||||
if (this.searchAuthor) searchQuery += `&author=${this.searchAuthor}`
|
if (this.searchAuthor) searchQuery += `&author=${this.searchAuthor || ''}`
|
||||||
if (this.isPodcast) searchQuery += '&podcast=1'
|
if (this.isPodcast) searchQuery += '&podcast=1'
|
||||||
return searchQuery
|
return searchQuery
|
||||||
},
|
},
|
||||||
persistProvider() {
|
persistProvider() {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem('book-provider', this.provider)
|
localStorage.setItem('book-cover-provider', this.provider)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('PersistProvider', error)
|
console.error('PersistProvider', error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full">
|
<div class="w-full h-full">
|
||||||
<div v-show="showPageMenu" v-click-outside="clickOutside" class="pagemenu absolute top-9 right-20 rounded-md overflow-y-auto bg-bg shadow-lg z-20 border border-gray-400 w-52">
|
<div v-show="showPageMenu" v-click-outside="clickOutside" class="pagemenu absolute top-9 left-8 rounded-md overflow-y-auto bg-bg shadow-lg z-20 border border-gray-400 w-52">
|
||||||
<div v-for="(file, index) in pages" :key="file" class="w-full cursor-pointer hover:bg-black-200 px-2 py-1" :class="page === index ? 'bg-black-200' : ''" @click="setPage(index)">
|
<div v-for="(file, index) in pages" :key="file" class="w-full cursor-pointer hover:bg-black-200 px-2 py-1" :class="page === index ? 'bg-black-200' : ''" @click="setPage(index)">
|
||||||
<p class="text-sm truncate">{{ file }}</p>
|
<p class="text-sm truncate">{{ file }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="showInfoMenu" v-click-outside="clickOutside" class="pagemenu absolute top-9 right-10 rounded-md overflow-y-auto bg-bg shadow-lg z-20 border border-gray-400 w-96">
|
<div v-show="showInfoMenu" v-click-outside="clickOutside" class="pagemenu absolute top-9 left-20 rounded-md overflow-y-auto bg-bg shadow-lg z-20 border border-gray-400 w-96">
|
||||||
<div v-for="key in comicMetadataKeys" :key="key" class="w-full px-2 py-1">
|
<div v-for="key in comicMetadataKeys" :key="key" class="w-full px-2 py-1">
|
||||||
<p class="text-xs">
|
<p class="text-xs">
|
||||||
<strong>{{ key }}</strong
|
<strong>{{ key }}</strong
|
||||||
@@ -14,17 +14,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="comicMetadata" class="absolute top-0 right-52 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @click.stop.prevent="showInfoMenu = !showInfoMenu">
|
<div v-if="comicMetadata" class="absolute top-0 left-20 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @click.stop.prevent="showInfoMenu = !showInfoMenu">
|
||||||
<span class="material-icons text-xl">more</span>
|
<span class="material-icons text-xl">more</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute top-0 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" style="right: 156px" @mousedown.prevent @click.stop.prevent="showPageMenu = !showPageMenu">
|
<div class="absolute top-0 left-8 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @click.stop.prevent="showPageMenu = !showPageMenu">
|
||||||
<span class="material-icons text-xl">menu</span>
|
<span class="material-icons text-xl">menu</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute top-0 right-20 bg-bg text-gray-100 border-b border-l border-r border-gray-400 rounded-b-md px-2 h-9 flex items-center text-center z-20">
|
<div class="absolute top-0 right-16 bg-bg text-gray-100 border-b border-l border-r border-gray-400 rounded-b-md px-2 h-9 flex items-center text-center z-20">
|
||||||
<p class="font-mono">{{ page + 1 }} / {{ numPages }}</p>
|
<p class="font-mono">{{ page + 1 }} / {{ numPages }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="overflow-hidden m-auto comicwrapper relative">
|
<div class="overflow-hidden w-full h-full relative">
|
||||||
<div v-show="canGoPrev" class="absolute top-0 left-0 h-full w-1/2 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="prev" @mousedown.prevent>
|
<div v-show="canGoPrev" class="absolute top-0 left-0 h-full w-1/2 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="prev" @mousedown.prevent>
|
||||||
<div class="flex items-center justify-center h-full w-1/2">
|
<div class="flex items-center justify-center h-full w-1/2">
|
||||||
<span v-show="loadedFirstPage" class="material-icons text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_back_ios</span>
|
<span v-show="loadedFirstPage" class="material-icons text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_back_ios</span>
|
||||||
@@ -36,17 +36,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-full flex justify-center">
|
<div class="h-full flex justify-center">
|
||||||
<img v-if="mainImg" :src="mainImg" class="object-contain comicimg" />
|
<img v-if="mainImg" :src="mainImg" class="object-contain h-full m-auto" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-show="loading" class="w-full h-full absolute top-0 left-0 flex items-center justify-center z-10">
|
<div v-show="loading" class="w-full h-full absolute top-0 left-0 flex items-center justify-center z-10">
|
||||||
<ui-loading-indicator />
|
<ui-loading-indicator />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <div v-show="loading" class="w-screen h-screen absolute top-0 left-0 bg-black bg-opacity-20 flex items-center justify-center">
|
|
||||||
<ui-loading-indicator />
|
|
||||||
</div> -->
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -61,7 +57,12 @@ Archive.init({
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
url: String
|
url: String,
|
||||||
|
libraryItem: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
|
playerOpen: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -249,15 +250,6 @@ export default {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.pagemenu {
|
.pagemenu {
|
||||||
max-height: calc(100vh - 60px);
|
max-height: calc(100% - 48px);
|
||||||
}
|
|
||||||
.comicimg {
|
|
||||||
height: calc(100vh - 40px);
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
.comicwrapper {
|
|
||||||
width: 100vw;
|
|
||||||
height: calc(100vh - 40px);
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full w-full">
|
<div id="epub-reader" class="h-full w-full">
|
||||||
<div class="h-full flex items-center justify-center">
|
<div class="h-full flex items-center justify-center">
|
||||||
<div style="width: 100px; max-width: 100px" class="h-full hidden sm:flex items-center overflow-x-hidden justify-center">
|
<div style="width: 100px; max-width: 100px" class="h-full hidden sm:flex items-center overflow-x-hidden justify-center">
|
||||||
<span v-if="hasPrev" class="material-icons text-white text-opacity-50 hover:text-opacity-80 cursor-pointer text-6xl" @mousedown.prevent @click="prev">chevron_left</span>
|
<span v-if="hasPrev" class="material-icons text-white text-opacity-50 hover:text-opacity-80 cursor-pointer text-6xl" @mousedown.prevent @click="prev">chevron_left</span>
|
||||||
@@ -28,17 +28,24 @@ export default {
|
|||||||
libraryItem: {
|
libraryItem: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
}
|
},
|
||||||
|
playerOpen: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
windowWidth: 0,
|
windowWidth: 0,
|
||||||
|
windowHeight: 0,
|
||||||
/** @type {ePub.Book} */
|
/** @type {ePub.Book} */
|
||||||
book: null,
|
book: null,
|
||||||
/** @type {ePub.Rendition} */
|
/** @type {ePub.Rendition} */
|
||||||
rendition: null
|
rendition: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
playerOpen() {
|
||||||
|
this.resize()
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
/** @returns {string} */
|
/** @returns {string} */
|
||||||
libraryItemId() {
|
libraryItemId() {
|
||||||
@@ -64,6 +71,10 @@ export default {
|
|||||||
readerWidth() {
|
readerWidth() {
|
||||||
if (this.windowWidth < 640) return this.windowWidth
|
if (this.windowWidth < 640) return this.windowWidth
|
||||||
return this.windowWidth - 200
|
return this.windowWidth - 200
|
||||||
|
},
|
||||||
|
readerHeight() {
|
||||||
|
if (this.windowHeight < 400 || !this.playerOpen) return this.windowHeight
|
||||||
|
return this.windowHeight - 164
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -203,13 +214,13 @@ export default {
|
|||||||
/** @type {ePub.Book} */
|
/** @type {ePub.Book} */
|
||||||
reader.book = new ePub(reader.url, {
|
reader.book = new ePub(reader.url, {
|
||||||
width: this.readerWidth,
|
width: this.readerWidth,
|
||||||
height: window.innerHeight - 50
|
height: this.readerHeight - 50
|
||||||
})
|
})
|
||||||
|
|
||||||
/** @type {ePub.Rendition} */
|
/** @type {ePub.Rendition} */
|
||||||
reader.rendition = reader.book.renderTo('viewer', {
|
reader.rendition = reader.book.renderTo('viewer', {
|
||||||
width: this.readerWidth,
|
width: this.readerWidth,
|
||||||
height: window.innerHeight * 0.8
|
height: this.readerHeight * 0.8
|
||||||
})
|
})
|
||||||
|
|
||||||
// load saved progress
|
// load saved progress
|
||||||
@@ -253,17 +264,19 @@ export default {
|
|||||||
},
|
},
|
||||||
resize() {
|
resize() {
|
||||||
this.windowWidth = window.innerWidth
|
this.windowWidth = window.innerWidth
|
||||||
this.rendition?.resize(this.readerWidth, window.innerHeight * 0.8)
|
this.windowHeight = window.innerHeight
|
||||||
|
this.rendition?.resize(this.readerWidth, this.readerHeight * 0.8)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
this.windowWidth = window.innerWidth
|
||||||
|
this.windowHeight = window.innerHeight
|
||||||
|
window.addEventListener('resize', this.resize)
|
||||||
|
this.initEpub()
|
||||||
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
window.removeEventListener('resize', this.resize)
|
window.removeEventListener('resize', this.resize)
|
||||||
this.book?.destroy()
|
this.book?.destroy()
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.windowWidth = window.innerWidth
|
|
||||||
window.addEventListener('resize', this.resize)
|
|
||||||
this.initEpub()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full">
|
<div class="w-full h-full">
|
||||||
<div class="h-full max-h-full w-full">
|
<div class="h-full max-h-full w-full">
|
||||||
<div class="ebook-viewer absolute overflow-y-scroll left-0 right-0 top-12 w-full max-w-4xl m-auto z-10 border border-black border-opacity-20 shadow-md bg-white">
|
<div class="ebook-viewer absolute overflow-y-scroll left-0 right-0 top-16 w-full max-w-4xl m-auto z-10 border border-black border-opacity-20 shadow-md bg-white">
|
||||||
<iframe title="html-viewer" width="100%"> Loading </iframe>
|
<iframe title="html-viewer" width="100%"> Loading </iframe>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -15,7 +15,12 @@ import defaultCss from '@/assets/ebooks/basic.js'
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
url: String
|
url: String,
|
||||||
|
libraryItem: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
|
playerOpen: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
|
|||||||
@@ -11,15 +11,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="absolute top-0 right-20 bg-bg text-gray-100 border-b border-l border-r border-gray-400 rounded-b-md px-2 h-9 flex items-center text-center">
|
<div class="absolute top-0 right-20 bg-bg text-gray-100 border-b border-l border-r border-gray-400 z-20 rounded-b-md px-2 h-9 flex items-center text-center">
|
||||||
<p class="font-mono">{{ page }} / {{ numPages }}</p>
|
<p class="font-mono">{{ page }} / {{ numPages }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="absolute top-0 right-40 bg-bg text-gray-100 border-b border-l border-r border-gray-400 z-20 rounded-b-md px-2 h-9 flex items-center text-center">
|
||||||
|
<ui-icon-btn icon="zoom_out" :size="8" :disabled="!canScaleDown" borderless class="mr-px" @click="zoomOut" />
|
||||||
|
<ui-icon-btn icon="zoom_in" :size="8" :disabled="!canScaleUp" borderless class="ml-px" @click="zoomIn" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div :style="{ height: pdfHeight + 'px' }" class="overflow-hidden m-auto">
|
<div :style="{ height: pdfHeight + 'px' }" class="overflow-hidden m-auto">
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<div :style="{ width: pdfWidth + 'px', height: pdfHeight + 'px' }" class="w-full h-full overflow-auto">
|
<div :style="{ width: pdfWidth + 'px', height: pdfHeight + 'px' }" class="overflow-auto">
|
||||||
<div v-if="loadedRatio > 0 && loadedRatio < 1" style="background-color: green; color: white; text-align: center" :style="{ width: loadedRatio * 100 + '%' }">{{ Math.floor(loadedRatio * 100) }}%</div>
|
<div v-if="loadedRatio > 0 && loadedRatio < 1" style="background-color: green; color: white; text-align: center" :style="{ width: loadedRatio * 100 + '%' }">{{ Math.floor(loadedRatio * 100) }}%</div>
|
||||||
<pdf ref="pdf" class="m-auto z-10 border border-black border-opacity-20 shadow-md" :src="url" :page="page" :rotate="rotate" @progress="loadedRatio = $event" @error="error" @num-pages="numPagesLoaded" @link-clicked="page = $event"></pdf>
|
<pdf ref="pdf" class="m-auto z-10 border border-black border-opacity-20 shadow-md" :src="url" :page="page" :rotate="rotate" @progress="progressEvt" @error="error" @num-pages="numPagesLoaded" @link-clicked="page = $event" @loaded="loadedEvt"></pdf>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -30,17 +34,25 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import pdf from 'vue-pdf'
|
import pdf from '@teckel/vue-pdf'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
pdf
|
pdf
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
url: String
|
url: String,
|
||||||
|
libraryItem: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
|
playerOpen: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
windowWidth: 0,
|
||||||
|
windowHeight: 0,
|
||||||
|
scale: 1,
|
||||||
rotate: 0,
|
rotate: 0,
|
||||||
loadedRatio: 0,
|
loadedRatio: 0,
|
||||||
page: 1,
|
page: 1,
|
||||||
@@ -48,35 +60,99 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
libraryItemId() {
|
||||||
|
return this.libraryItem?.id
|
||||||
|
},
|
||||||
|
fitToPageWidth() {
|
||||||
|
return this.pdfHeight * 0.6
|
||||||
|
},
|
||||||
pdfWidth() {
|
pdfWidth() {
|
||||||
return this.pdfHeight * 0.6667
|
return this.fitToPageWidth * this.scale
|
||||||
},
|
},
|
||||||
pdfHeight() {
|
pdfHeight() {
|
||||||
return window.innerHeight - 120
|
if (this.windowHeight < 400 || !this.playerOpen) return this.windowHeight - 120
|
||||||
|
return this.windowHeight - 284
|
||||||
|
},
|
||||||
|
maxScale() {
|
||||||
|
return Math.floor((this.windowWidth * 10) / this.fitToPageWidth) / 10
|
||||||
},
|
},
|
||||||
canGoNext() {
|
canGoNext() {
|
||||||
return this.page < this.numPages
|
return this.page < this.numPages
|
||||||
},
|
},
|
||||||
canGoPrev() {
|
canGoPrev() {
|
||||||
return this.page > 1
|
return this.page > 1
|
||||||
|
},
|
||||||
|
canScaleUp() {
|
||||||
|
return this.scale < this.maxScale
|
||||||
|
},
|
||||||
|
canScaleDown() {
|
||||||
|
return this.scale > 1
|
||||||
|
},
|
||||||
|
userMediaProgress() {
|
||||||
|
if (!this.libraryItemId) return
|
||||||
|
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
||||||
|
},
|
||||||
|
savedPage() {
|
||||||
|
return Number(this.userMediaProgress?.ebookLocation || 0)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
zoomIn() {
|
||||||
|
this.scale += 0.1
|
||||||
|
},
|
||||||
|
zoomOut() {
|
||||||
|
this.scale -= 0.1
|
||||||
|
},
|
||||||
|
updateProgress() {
|
||||||
|
if (!this.numPages) {
|
||||||
|
console.error('Num pages not loaded')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
ebookLocation: this.page,
|
||||||
|
ebookProgress: Math.max(0, Math.min(1, (Number(this.page) - 1) / Number(this.numPages)))
|
||||||
|
}
|
||||||
|
this.$axios.$patch(`/api/me/progress/${this.libraryItemId}`, payload).catch((error) => {
|
||||||
|
console.error('EpubReader.updateProgress failed:', error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
loadedEvt() {
|
||||||
|
if (this.savedPage && this.savedPage > 0 && this.savedPage <= this.numPages) {
|
||||||
|
this.page = this.savedPage
|
||||||
|
}
|
||||||
|
},
|
||||||
|
progressEvt(progress) {
|
||||||
|
this.loadedRatio = progress
|
||||||
|
},
|
||||||
numPagesLoaded(e) {
|
numPagesLoaded(e) {
|
||||||
this.numPages = e
|
this.numPages = e
|
||||||
},
|
},
|
||||||
prev() {
|
prev() {
|
||||||
if (this.page <= 1) return
|
if (this.page <= 1) return
|
||||||
this.page--
|
this.page--
|
||||||
|
this.updateProgress()
|
||||||
},
|
},
|
||||||
next() {
|
next() {
|
||||||
if (this.page >= this.numPages) return
|
if (this.page >= this.numPages) return
|
||||||
this.page++
|
this.page++
|
||||||
|
this.updateProgress()
|
||||||
},
|
},
|
||||||
error(err) {
|
error(err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
|
},
|
||||||
|
resize() {
|
||||||
|
this.windowWidth = window.innerWidth
|
||||||
|
this.windowHeight = window.innerHeight
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {
|
||||||
|
this.windowWidth = window.innerWidth
|
||||||
|
this.windowHeight = window.innerHeight
|
||||||
|
window.addEventListener('resize', this.resize)
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
window.removeEventListener('resize', this.resize)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="show" class="w-screen h-screen fixed top-0 left-0 z-60 bg-primary text-white">
|
<div v-if="show" id="reader" class="absolute top-0 left-0 w-full z-60 bg-primary text-white" :class="{ 'reader-player-open': !!streamLibraryItem }">
|
||||||
<div class="absolute top-4 left-4 z-20">
|
<div class="absolute top-4 left-4 z-20">
|
||||||
<span v-if="hasToC && !tocOpen" ref="tocButton" class="material-icons cursor-pointer text-2xl" @click="toggleToC">menu</span>
|
<span v-if="hasToC && !tocOpen" ref="tocButton" class="material-icons cursor-pointer text-2xl" @click="toggleToC">menu</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -17,17 +17,22 @@
|
|||||||
<span class="material-icons cursor-pointer text-2xl" @click="close">close</span>
|
<span class="material-icons cursor-pointer text-2xl" @click="close">close</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<component v-if="componentName" ref="readerComponent" :is="componentName" :url="ebookUrl" :library-item="selectedLibraryItem" />
|
<component v-if="componentName" ref="readerComponent" :is="componentName" :url="ebookUrl" :library-item="selectedLibraryItem" :player-open="!!streamLibraryItem" />
|
||||||
|
|
||||||
<!-- TOC side nav -->
|
<!-- TOC side nav -->
|
||||||
<div v-if="tocOpen" class="w-full h-full fixed inset-0 bg-black/20 z-20" @click.stop.prevent="toggleToC"></div>
|
<div v-if="tocOpen" class="w-full h-full fixed inset-0 bg-black/20 z-20" @click.stop.prevent="toggleToC"></div>
|
||||||
<div v-if="hasToC" class="w-72 h-full max-h-full absolute top-0 left-0 bg-bg shadow-xl transition-transform z-30" :class="tocOpen ? 'translate-x-0' : '-translate-x-72'" @click.stop.prevent="toggleToC">
|
<div v-if="hasToC" class="w-96 h-full max-h-full absolute top-0 left-0 bg-bg shadow-xl transition-transform z-30" :class="tocOpen ? 'translate-x-0' : '-translate-x-96'" @click.stop.prevent="toggleToC">
|
||||||
<div class="p-4 h-full overflow-hidden">
|
<div class="p-4 h-full">
|
||||||
<p class="text-lg font-semibold mb-2">Table of Contents</p>
|
<p class="text-lg font-semibold mb-2">Table of Contents</p>
|
||||||
<div class="tocContent">
|
<div class="tocContent">
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="chapter in chapters" :key="chapter.id" class="py-1">
|
<li v-for="chapter in chapters" :key="chapter.id" class="py-1">
|
||||||
<a :href="chapter.href" @click.prevent="$refs.readerComponent.goToChapter(chapter.href)">{{ chapter.label }}</a>
|
<a :href="chapter.href" class="text-white/70 hover:text-white" @click.prevent="$refs.readerComponent.goToChapter(chapter.href)">{{ chapter.label }}</a>
|
||||||
|
<ul v-if="chapter.subitems.length">
|
||||||
|
<li v-for="subchapter in chapter.subitems" :key="subchapter.id" class="py-1 pl-4">
|
||||||
|
<a :href="subchapter.href" class="text-white/70 hover:text-white" @click.prevent="$refs.readerComponent.goToChapter(subchapter.href)">{{ subchapter.label }}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -67,6 +72,9 @@ export default {
|
|||||||
else if (this.ebookType === 'comic') return 'readers-comic-reader'
|
else if (this.ebookType === 'comic') return 'readers-comic-reader'
|
||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
|
streamLibraryItem() {
|
||||||
|
return this.$store.state.streamLibraryItem
|
||||||
|
},
|
||||||
hasToC() {
|
hasToC() {
|
||||||
return this.isEpub
|
return this.isEpub
|
||||||
},
|
},
|
||||||
@@ -146,7 +154,6 @@ export default {
|
|||||||
},
|
},
|
||||||
openSettings() {},
|
openSettings() {},
|
||||||
hotkey(action) {
|
hotkey(action) {
|
||||||
console.log('Reader hotkey', action)
|
|
||||||
if (!this.$refs.readerComponent) return
|
if (!this.$refs.readerComponent) return
|
||||||
|
|
||||||
if (action === this.$hotkeys.EReader.NEXT_PAGE) {
|
if (action === this.$hotkeys.EReader.NEXT_PAGE) {
|
||||||
@@ -187,12 +194,19 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* @import url(@/assets/calibre/basic.css); */
|
|
||||||
.ebook-viewer {
|
|
||||||
height: calc(100% - 96px);
|
|
||||||
}
|
|
||||||
.tocContent {
|
.tocContent {
|
||||||
height: calc(100% - 36px);
|
height: calc(100% - 36px);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
#reader {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
#reader.reader-player-open {
|
||||||
|
height: calc(100% - 164px);
|
||||||
|
}
|
||||||
|
@media (max-height: 400px) {
|
||||||
|
#reader.reader-player-open {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,16 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full px-1 md:px-2 py-2 overflow-hidden relative" @mouseover="mouseover" @mouseleave="mouseleave" :class="isHovering ? 'bg-white bg-opacity-5' : ''">
|
<div class="w-full px-1 md:px-2 py-2 overflow-hidden relative" @mouseover="mouseover" @mouseleave="mouseleave" :class="isHovering ? 'bg-white bg-opacity-5' : ''">
|
||||||
<div v-if="book" class="flex h-16 md:h-20">
|
<div v-if="book" class="flex h-18 md:h-[5.5rem]">
|
||||||
<div class="w-10 min-w-10 md:w-16 md:max-w-16 h-full">
|
<div class="w-10 min-w-10 md:w-16 md:max-w-16 h-full">
|
||||||
<div class="flex h-full items-center justify-center">
|
<div class="flex h-full items-center justify-center">
|
||||||
<span class="material-icons drag-handle text-lg md:text-xl">menu</span>
|
<span class="material-icons drag-handle text-lg md:text-xl">menu</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-full relative flex items-center" :style="{ width: coverWidth + 'px', minWidth: coverWidth + 'px', maxWidth: coverWidth + 'px' }">
|
<div class="h-full flex items-center" :style="{ width: coverWidth + 'px', minWidth: coverWidth + 'px', maxWidth: coverWidth + 'px' }">
|
||||||
<covers-book-cover :library-item="book" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<div class="relative" :style="{ height: coverHeight + 'px', minHeight: coverHeight + 'px', maxHeight: coverHeight + 'px' }">
|
||||||
<div class="absolute top-0 left-0 bg-black bg-opacity-50 flex items-center justify-center h-full w-full z-10" v-show="isHovering && showPlayBtn">
|
<covers-book-cover :library-item="book" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
<div class="w-8 h-8 bg-white bg-opacity-20 rounded-full flex items-center justify-center hover:bg-opacity-40 cursor-pointer" @click="playClick">
|
<div class="absolute top-0 left-0 flex items-center justify-center bg-black bg-opacity-50 h-full w-full z-10" v-show="isHovering && showPlayBtn">
|
||||||
<span class="material-icons text-2xl">play_arrow</span>
|
<div class="w-8 h-8 bg-white bg-opacity-20 rounded-full flex items-center justify-center hover:bg-opacity-40 cursor-pointer" @click="playClick">
|
||||||
|
<span class="material-icons text-2xl">play_arrow</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -19,9 +21,12 @@
|
|||||||
<div class="truncate max-w-48 md:max-w-md">
|
<div class="truncate max-w-48 md:max-w-md">
|
||||||
<nuxt-link :to="`/item/${book.id}`" class="truncate hover:underline text-sm md:text-base">{{ bookTitle }}</nuxt-link>
|
<nuxt-link :to="`/item/${book.id}`" class="truncate hover:underline text-sm md:text-base">{{ bookTitle }}</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="truncate max-w-48 md:max-w-md text-xs md:text-sm text-gray-300">
|
||||||
|
<nuxt-link v-for="_series in seriesList" :key="_series.id" :to="`/library/${book.libraryId}/series/${_series.id}`" class="hover:underline font-sans text-gray-300"> {{ _series.text }}</nuxt-link>
|
||||||
|
</div>
|
||||||
<div class="truncate max-w-48 md:max-w-md text-xs md:text-sm text-gray-300">
|
<div class="truncate max-w-48 md:max-w-md text-xs md:text-sm text-gray-300">
|
||||||
<template v-for="(author, index) in bookAuthors">
|
<template v-for="(author, index) in bookAuthors">
|
||||||
<nuxt-link :key="author.id" :to="`/author/${author.id}`" class="truncate hover:underline">{{ author.name }}</nuxt-link
|
<nuxt-link :key="author.id" :to="`/author/${author.id}?library=${book.libraryId}`" class="truncate hover:underline">{{ author.name }}</nuxt-link
|
||||||
><span :key="author.id + '-comma'" v-if="index < bookAuthors.length - 1">, </span>
|
><span :key="author.id + '-comma'" v-if="index < bookAuthors.length - 1">, </span>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -96,6 +101,19 @@ export default {
|
|||||||
bookDuration() {
|
bookDuration() {
|
||||||
return this.$elapsedPretty(this.media.duration)
|
return this.$elapsedPretty(this.media.duration)
|
||||||
},
|
},
|
||||||
|
series() {
|
||||||
|
return this.mediaMetadata.series || []
|
||||||
|
},
|
||||||
|
seriesList() {
|
||||||
|
return this.series.map((se) => {
|
||||||
|
let text = se.name
|
||||||
|
if (se.sequence) text += ` #${se.sequence}`
|
||||||
|
return {
|
||||||
|
...se,
|
||||||
|
text
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
isMissing() {
|
isMissing() {
|
||||||
return this.book.isMissing
|
return this.book.isMissing
|
||||||
},
|
},
|
||||||
@@ -117,6 +135,9 @@ export default {
|
|||||||
coverSize() {
|
coverSize() {
|
||||||
return this.$store.state.globals.isMobile ? 30 : 50
|
return this.$store.state.globals.isMobile ? 30 : 50
|
||||||
},
|
},
|
||||||
|
coverHeight() {
|
||||||
|
return this.coverSize * 1.6
|
||||||
|
},
|
||||||
coverWidth() {
|
coverWidth() {
|
||||||
if (this.bookCoverAspectRatio === 1) return this.coverSize * 1.6
|
if (this.bookCoverAspectRatio === 1) return this.coverSize * 1.6
|
||||||
return this.coverSize
|
return this.coverSize
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="truncate max-w-48 md:max-w-md text-xs md:text-sm text-gray-300">
|
<div class="truncate max-w-48 md:max-w-md text-xs md:text-sm text-gray-300">
|
||||||
<template v-for="(author, index) in bookAuthors">
|
<template v-for="(author, index) in bookAuthors">
|
||||||
<nuxt-link :key="author.id" :to="`/author/${author.id}`" class="truncate hover:underline">{{ author.name }}</nuxt-link
|
<nuxt-link :key="author.id" :to="`/author/${author.id}?library=${libraryItem.libraryId}`" class="truncate hover:underline">{{ author.name }}</nuxt-link
|
||||||
><span :key="author.id + '-comma'" v-if="index < bookAuthors.length - 1">, </span>
|
><span :key="author.id + '-comma'" v-if="index < bookAuthors.length - 1">, </span>
|
||||||
</template>
|
</template>
|
||||||
<nuxt-link v-if="episode" :to="`/item/${libraryItem.id}`" class="truncate hover:underline">{{ mediaMetadata.title }}</nuxt-link>
|
<nuxt-link v-if="episode" :to="`/item/${libraryItem.id}`" class="truncate hover:underline">{{ mediaMetadata.title }}</nuxt-link>
|
||||||
|
|||||||
@@ -7,8 +7,7 @@
|
|||||||
<widgets-podcast-type-indicator :type="episode.episodeType" />
|
<widgets-podcast-type-indicator :type="episode.episodeType" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-sm text-gray-200 episode-subtitle mt-1.5 mb-0.5">{{ subtitle }}</p>
|
<p class="text-sm text-gray-200 episode-subtitle mt-1.5 mb-0.5" v-html="subtitle"></p>
|
||||||
|
|
||||||
<div class="flex justify-between pt-2 max-w-xl">
|
<div class="flex justify-between pt-2 max-w-xl">
|
||||||
<p v-if="episode.season" class="text-sm text-gray-300">Season #{{ episode.season }}</p>
|
<p v-if="episode.season" class="text-sm text-gray-300">Season #{{ episode.season }}</p>
|
||||||
<p v-if="episode.episode" class="text-sm text-gray-300">Episode #{{ episode.episode }}</p>
|
<p v-if="episode.episode" class="text-sm text-gray-300">Episode #{{ episode.episode }}</p>
|
||||||
@@ -22,10 +21,6 @@
|
|||||||
<p class="pl-2 pr-1 text-sm font-semibold">{{ timeRemaining }}</p>
|
<p class="pl-2 pr-1 text-sm font-semibold">{{ timeRemaining }}</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- <button v-if="libraryItemIdStreaming && !isStreamingFromDifferentLibrary" class="h-8 w-8 flex justify-center items-center mx-2" :class="isQueued ? 'text-success' : ''" @click.stop="queueBtnClick">
|
|
||||||
<span class="material-icons-outlined">{{ isQueued ? 'playlist_add_check' : 'queue' }}</span>
|
|
||||||
</button> -->
|
|
||||||
|
|
||||||
<ui-tooltip v-if="libraryItemIdStreaming && !isStreamingFromDifferentLibrary" :text="isQueued ? $strings.MessageRemoveFromPlayerQueue : $strings.MessageAddToPlayerQueue" :class="isQueued ? 'text-success' : ''" direction="top">
|
<ui-tooltip v-if="libraryItemIdStreaming && !isStreamingFromDifferentLibrary" :text="isQueued ? $strings.MessageRemoveFromPlayerQueue : $strings.MessageAddToPlayerQueue" :class="isQueued ? 'text-success' : ''" direction="top">
|
||||||
<ui-icon-btn :icon="isQueued ? 'playlist_add_check' : 'playlist_play'" borderless @click="queueBtnClick" />
|
<ui-icon-btn :icon="isQueued ? 'playlist_add_check' : 'playlist_play'" borderless @click="queueBtnClick" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
@@ -89,7 +84,7 @@ export default {
|
|||||||
return this.episode.title || ''
|
return this.episode.title || ''
|
||||||
},
|
},
|
||||||
subtitle() {
|
subtitle() {
|
||||||
return this.episode.subtitle || ''
|
return this.episode.subtitle || this.description
|
||||||
},
|
},
|
||||||
description() {
|
description() {
|
||||||
return this.episode.description || ''
|
return this.episode.description || ''
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="tasksRunning" class="w-4 h-4 mx-3 relative" v-click-outside="clickOutsideObj">
|
<div v-if="tasksToShow.length" class="w-4 h-4 mx-3 relative" v-click-outside="clickOutsideObj">
|
||||||
<button type="button" :disabled="disabled" class="w-10 sm:w-full relative h-full cursor-pointer" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
|
<button type="button" :disabled="disabled" class="w-10 sm:w-full relative h-full cursor-pointer" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
|
||||||
<div class="flex h-full items-center justify-center">
|
<div class="flex h-full items-center justify-center">
|
||||||
<ui-tooltip text="Tasks running" direction="bottom" class="flex items-center">
|
<ui-tooltip v-if="tasksRunning" :text="$strings.LabelTasks" direction="bottom" class="flex items-center">
|
||||||
<widgets-loading-spinner />
|
<widgets-loading-spinner />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
<ui-tooltip v-else text="Activities" direction="bottom" class="flex items-center">
|
||||||
|
<span class="material-icons text-1.5xl" aria-label="Activities" role="button">notifications</span>
|
||||||
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<transition name="menu">
|
<transition name="menu">
|
||||||
<div class="sm:w-80 w-full relative">
|
<div class="sm:w-80 w-full relative">
|
||||||
<div v-show="showMenu" class="absolute z-40 -mt-px w-40 sm:w-full bg-bg border border-black-200 shadow-lg rounded-md py-1 px-2 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm globalTaskRunningMenu">
|
<div v-show="showMenu" class="absolute z-40 -mt-px w-40 sm:w-full bg-bg border border-black-200 shadow-lg rounded-md text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm globalTaskRunningMenu">
|
||||||
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||||
<template v-if="tasksRunningOrFailed.length">
|
<template v-if="tasksToShow.length">
|
||||||
<p class="uppercase text-xs text-gray-400 my-1 px-1 font-semibold">{{ $strings.LabelTasks }}</p>
|
<template v-for="task in tasksToShow">
|
||||||
<template v-for="task in tasksRunningOrFailed">
|
|
||||||
<nuxt-link :key="task.id" v-if="actionLink(task)" :to="actionLink(task)">
|
<nuxt-link :key="task.id" v-if="actionLink(task)" :to="actionLink(task)">
|
||||||
<li class="text-gray-50 select-none relative hover:bg-black-400 py-1 cursor-pointer">
|
<li class="text-gray-50 select-none relative hover:bg-black-400 py-1 cursor-pointer">
|
||||||
<cards-item-task-running-card :task="task" />
|
<cards-item-task-running-card :task="task" />
|
||||||
@@ -54,9 +56,10 @@ export default {
|
|||||||
tasksRunning() {
|
tasksRunning() {
|
||||||
return this.tasks.some((t) => !t.isFinished)
|
return this.tasks.some((t) => !t.isFinished)
|
||||||
},
|
},
|
||||||
tasksRunningOrFailed() {
|
tasksToShow() {
|
||||||
// return just the tasks that are running or failed in the last 1 minute
|
// return just the tasks that are running or failed (or show success) in the last 1 minute
|
||||||
return this.tasks.filter((t) => !t.isFinished || (t.isFailed && t.finishedAt > new Date().getTime() - 1000 * 60)) || []
|
const tasks = this.tasks.filter((t) => !t.isFinished || ((t.isFailed || t.showSuccess) && t.finishedAt > new Date().getTime() - 1000 * 60)) || []
|
||||||
|
return tasks.sort((a, b) => b.startedAt - a.startedAt)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -75,6 +78,8 @@ export default {
|
|||||||
return `/audiobook/${task.data.libraryItemId}/manage?tool=m4b`
|
return `/audiobook/${task.data.libraryItemId}/manage?tool=m4b`
|
||||||
case 'embed-metadata':
|
case 'embed-metadata':
|
||||||
return `/audiobook/${task.data.libraryItemId}/manage?tool=embed`
|
return `/audiobook/${task.data.libraryItemId}/manage?tool=embed`
|
||||||
|
case 'scan-item':
|
||||||
|
return `/item/${task.data.libraryItemId}`
|
||||||
default:
|
default:
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ export default {
|
|||||||
isSocketConnected: false,
|
isSocketConnected: false,
|
||||||
isFirstSocketConnection: true,
|
isFirstSocketConnection: true,
|
||||||
socketConnectionToastId: null,
|
socketConnectionToastId: null,
|
||||||
currentLang: null
|
currentLang: null,
|
||||||
|
multiSessionOtherSessionId: null, // Used for multiple sessions open warning toast
|
||||||
|
multiSessionCurrentSessionId: null // Used for multiple sessions open warning toast
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -300,14 +302,27 @@ export default {
|
|||||||
this.$store.commit('users/updateUserOnline', user)
|
this.$store.commit('users/updateUserOnline', user)
|
||||||
},
|
},
|
||||||
userSessionClosed(sessionId) {
|
userSessionClosed(sessionId) {
|
||||||
|
// If this session or other session is closed then dismiss multiple sessions warning toast
|
||||||
|
if (sessionId === this.multiSessionOtherSessionId || this.multiSessionCurrentSessionId === sessionId) {
|
||||||
|
this.multiSessionOtherSessionId = null
|
||||||
|
this.multiSessionCurrentSessionId = null
|
||||||
|
this.$toast.dismiss('multiple-sessions')
|
||||||
|
}
|
||||||
if (this.$refs.streamContainer) this.$refs.streamContainer.sessionClosedEvent(sessionId)
|
if (this.$refs.streamContainer) this.$refs.streamContainer.sessionClosedEvent(sessionId)
|
||||||
},
|
},
|
||||||
userMediaProgressUpdate(payload) {
|
userMediaProgressUpdate(payload) {
|
||||||
this.$store.commit('user/updateMediaProgress', payload)
|
this.$store.commit('user/updateMediaProgress', payload)
|
||||||
|
|
||||||
if (payload.data) {
|
if (payload.data) {
|
||||||
if (this.$store.getters['getIsMediaStreaming'](payload.data.libraryItemId, payload.data.episodeId)) {
|
if (this.$store.getters['getIsMediaStreaming'](payload.data.libraryItemId, payload.data.episodeId) && this.$store.state.playbackSessionId !== payload.sessionId) {
|
||||||
// TODO: Update currently open session if being played from another device
|
this.multiSessionOtherSessionId = payload.sessionId
|
||||||
|
this.multiSessionCurrentSessionId = this.$store.state.playbackSessionId
|
||||||
|
console.log(`Media progress was updated from another session (${this.multiSessionOtherSessionId}) for currently open media. Device description=${payload.deviceDescription}. Current session id=${this.multiSessionCurrentSessionId}`)
|
||||||
|
if (this.$store.state.streamIsPlaying) {
|
||||||
|
this.$toast.update('multiple-sessions', { content: `Another session is open for this item on device ${payload.deviceDescription}`, options: { timeout: 20000, type: 'warning', pauseOnFocusLoss: false } }, true)
|
||||||
|
} else {
|
||||||
|
this.$eventBus.$emit('playback-time-update', payload.data.currentTime)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+73
-73
@@ -1,16 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.20",
|
"version": "2.2.21",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.20",
|
"version": "2.2.21",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/axios": "^5.13.6",
|
"@nuxtjs/axios": "^5.13.6",
|
||||||
"@nuxtjs/proxy": "^2.1.0",
|
"@nuxtjs/proxy": "^2.1.0",
|
||||||
|
"@teckel/vue-pdf": "^4.3.5",
|
||||||
"core-js": "^3.16.0",
|
"core-js": "^3.16.0",
|
||||||
"cron-parser": "^4.7.1",
|
"cron-parser": "^4.7.1",
|
||||||
"date-fns": "^2.25.0",
|
"date-fns": "^2.25.0",
|
||||||
@@ -21,7 +22,6 @@
|
|||||||
"nuxt-socket-io": "^1.1.18",
|
"nuxt-socket-io": "^1.1.18",
|
||||||
"trix": "^1.3.1",
|
"trix": "^1.3.1",
|
||||||
"v-click-outside": "^3.1.2",
|
"v-click-outside": "^3.1.2",
|
||||||
"vue-pdf": "^4.2.0",
|
|
||||||
"vue-toastification": "^1.7.11",
|
"vue-toastification": "^1.7.11",
|
||||||
"vuedraggable": "^2.24.3"
|
"vuedraggable": "^2.24.3"
|
||||||
},
|
},
|
||||||
@@ -2983,6 +2983,43 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
|
||||||
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
|
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@teckel/vue-pdf": {
|
||||||
|
"version": "4.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@teckel/vue-pdf/-/vue-pdf-4.3.5.tgz",
|
||||||
|
"integrity": "sha512-g2DAbZMPbPc7NPFImOsU/e7rt7wfdmBkmFa2kPsB4x+k+Bs8yC5Icmq/VnTSEq/Y8bNvEY7i6+JoicGnlfQL7Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||||
|
"loader-utils": "^1.4.0",
|
||||||
|
"pdfjs-dist": "^2.5.207 <2.8.0",
|
||||||
|
"raw-loader": "^4.0.1",
|
||||||
|
"vue-resize-sensor": "^2.0.0",
|
||||||
|
"worker-loader": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@teckel/vue-pdf/node_modules/json5": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
||||||
|
"dependencies": {
|
||||||
|
"minimist": "^1.2.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"json5": "lib/cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@teckel/vue-pdf/node_modules/loader-utils": {
|
||||||
|
"version": "1.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz",
|
||||||
|
"integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==",
|
||||||
|
"dependencies": {
|
||||||
|
"big.js": "^5.2.2",
|
||||||
|
"emojis-list": "^3.0.0",
|
||||||
|
"json5": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/anymatch": {
|
"node_modules/@types/anymatch": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-3.0.0.tgz",
|
||||||
@@ -15921,43 +15958,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vue-no-ssr/-/vue-no-ssr-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/vue-no-ssr/-/vue-no-ssr-1.1.1.tgz",
|
||||||
"integrity": "sha512-ZMjqRpWabMPqPc7gIrG0Nw6vRf1+itwf0Itft7LbMXs2g3Zs/NFmevjZGN1x7K3Q95GmIjWbQZTVerxiBxI+0g=="
|
"integrity": "sha512-ZMjqRpWabMPqPc7gIrG0Nw6vRf1+itwf0Itft7LbMXs2g3Zs/NFmevjZGN1x7K3Q95GmIjWbQZTVerxiBxI+0g=="
|
||||||
},
|
},
|
||||||
"node_modules/vue-pdf": {
|
|
||||||
"version": "4.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/vue-pdf/-/vue-pdf-4.3.0.tgz",
|
|
||||||
"integrity": "sha512-zd3lJj6CbtrawgaaDDciTDjkJMUKiLWtbEmBg5CvFn9Noe9oAO/GNy/fc5c59qGuFCJ14ibIV1baw4S07e5bSQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
|
||||||
"loader-utils": "^1.4.0",
|
|
||||||
"pdfjs-dist": "2.6.347",
|
|
||||||
"raw-loader": "^4.0.2",
|
|
||||||
"vue-resize-sensor": "^2.0.0",
|
|
||||||
"worker-loader": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vue-pdf/node_modules/json5": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
|
|
||||||
"dependencies": {
|
|
||||||
"minimist": "^1.2.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"json5": "lib/cli.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vue-pdf/node_modules/loader-utils": {
|
|
||||||
"version": "1.4.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz",
|
|
||||||
"integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==",
|
|
||||||
"dependencies": {
|
|
||||||
"big.js": "^5.2.2",
|
|
||||||
"emojis-list": "^3.0.0",
|
|
||||||
"json5": "^1.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vue-resize-sensor": {
|
"node_modules/vue-resize-sensor": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/vue-resize-sensor/-/vue-resize-sensor-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/vue-resize-sensor/-/vue-resize-sensor-2.0.0.tgz",
|
||||||
@@ -19591,6 +19591,39 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
|
||||||
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
|
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
|
||||||
},
|
},
|
||||||
|
"@teckel/vue-pdf": {
|
||||||
|
"version": "4.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@teckel/vue-pdf/-/vue-pdf-4.3.5.tgz",
|
||||||
|
"integrity": "sha512-g2DAbZMPbPc7NPFImOsU/e7rt7wfdmBkmFa2kPsB4x+k+Bs8yC5Icmq/VnTSEq/Y8bNvEY7i6+JoicGnlfQL7Q==",
|
||||||
|
"requires": {
|
||||||
|
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||||
|
"loader-utils": "^1.4.0",
|
||||||
|
"pdfjs-dist": "^2.5.207 <2.8.0",
|
||||||
|
"raw-loader": "^4.0.1",
|
||||||
|
"vue-resize-sensor": "^2.0.0",
|
||||||
|
"worker-loader": "^2.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"json5": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
||||||
|
"requires": {
|
||||||
|
"minimist": "^1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"loader-utils": {
|
||||||
|
"version": "1.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz",
|
||||||
|
"integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==",
|
||||||
|
"requires": {
|
||||||
|
"big.js": "^5.2.2",
|
||||||
|
"emojis-list": "^3.0.0",
|
||||||
|
"json5": "^1.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/anymatch": {
|
"@types/anymatch": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-3.0.0.tgz",
|
||||||
@@ -29618,39 +29651,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vue-no-ssr/-/vue-no-ssr-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/vue-no-ssr/-/vue-no-ssr-1.1.1.tgz",
|
||||||
"integrity": "sha512-ZMjqRpWabMPqPc7gIrG0Nw6vRf1+itwf0Itft7LbMXs2g3Zs/NFmevjZGN1x7K3Q95GmIjWbQZTVerxiBxI+0g=="
|
"integrity": "sha512-ZMjqRpWabMPqPc7gIrG0Nw6vRf1+itwf0Itft7LbMXs2g3Zs/NFmevjZGN1x7K3Q95GmIjWbQZTVerxiBxI+0g=="
|
||||||
},
|
},
|
||||||
"vue-pdf": {
|
|
||||||
"version": "4.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/vue-pdf/-/vue-pdf-4.3.0.tgz",
|
|
||||||
"integrity": "sha512-zd3lJj6CbtrawgaaDDciTDjkJMUKiLWtbEmBg5CvFn9Noe9oAO/GNy/fc5c59qGuFCJ14ibIV1baw4S07e5bSQ==",
|
|
||||||
"requires": {
|
|
||||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
|
||||||
"loader-utils": "^1.4.0",
|
|
||||||
"pdfjs-dist": "2.6.347",
|
|
||||||
"raw-loader": "^4.0.2",
|
|
||||||
"vue-resize-sensor": "^2.0.0",
|
|
||||||
"worker-loader": "^2.0.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"json5": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
|
|
||||||
"requires": {
|
|
||||||
"minimist": "^1.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"loader-utils": {
|
|
||||||
"version": "1.4.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz",
|
|
||||||
"integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==",
|
|
||||||
"requires": {
|
|
||||||
"big.js": "^5.2.2",
|
|
||||||
"emojis-list": "^3.0.0",
|
|
||||||
"json5": "^1.0.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"vue-resize-sensor": {
|
"vue-resize-sensor": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/vue-resize-sensor/-/vue-resize-sensor-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/vue-resize-sensor/-/vue-resize-sensor-2.0.0.tgz",
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.20",
|
"version": "2.2.21",
|
||||||
"description": "Self-hosted audiobook and podcast client",
|
"description": "Self-hosted audiobook and podcast client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/axios": "^5.13.6",
|
"@nuxtjs/axios": "^5.13.6",
|
||||||
"@nuxtjs/proxy": "^2.1.0",
|
"@nuxtjs/proxy": "^2.1.0",
|
||||||
|
"@teckel/vue-pdf": "^4.3.5",
|
||||||
"core-js": "^3.16.0",
|
"core-js": "^3.16.0",
|
||||||
"cron-parser": "^4.7.1",
|
"cron-parser": "^4.7.1",
|
||||||
"date-fns": "^2.25.0",
|
"date-fns": "^2.25.0",
|
||||||
@@ -25,7 +26,6 @@
|
|||||||
"nuxt-socket-io": "^1.1.18",
|
"nuxt-socket-io": "^1.1.18",
|
||||||
"trix": "^1.3.1",
|
"trix": "^1.3.1",
|
||||||
"v-click-outside": "^3.1.2",
|
"v-click-outside": "^3.1.2",
|
||||||
"vue-pdf": "^4.2.0",
|
|
||||||
"vue-toastification": "^1.7.11",
|
"vue-toastification": "^1.7.11",
|
||||||
"vuedraggable": "^2.24.3"
|
"vuedraggable": "^2.24.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -43,8 +43,8 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
async asyncData({ store, app, params, redirect }) {
|
async asyncData({ store, app, params, redirect, query }) {
|
||||||
const author = await app.$axios.$get(`/api/authors/${params.id}?library=${store.state.libraries.currentLibraryId}&include=items,series`).catch((error) => {
|
const author = await app.$axios.$get(`/api/authors/${params.id}?library=${query.library || store.state.libraries.currentLibraryId}&include=items,series`).catch((error) => {
|
||||||
console.error('Failed to get author', error)
|
console.error('Failed to get author', error)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
@@ -53,6 +53,10 @@ export default {
|
|||||||
return redirect(`/library/${store.state.libraries.currentLibraryId}/authors`)
|
return redirect(`/library/${store.state.libraries.currentLibraryId}/authors`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (query.library) {
|
||||||
|
store.commit('libraries/setCurrentLibrary', query.library)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
author
|
author
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,11 +39,15 @@
|
|||||||
<ui-multi-select v-model="newServerSettings.sortingPrefixes" small :items="newServerSettings.sortingPrefixes" :label="$strings.LabelPrefixesToIgnore" @input="updateSortingPrefixes" :disabled="updatingServerSettings" />
|
<ui-multi-select v-model="newServerSettings.sortingPrefixes" small :items="newServerSettings.sortingPrefixes" :label="$strings.LabelPrefixesToIgnore" @input="updateSortingPrefixes" :disabled="updatingServerSettings" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2 mb-2">
|
||||||
<ui-toggle-switch labeledBy="settings-chromecast-support" v-model="newServerSettings.chromecastEnabled" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('chromecastEnabled', val)" />
|
<ui-toggle-switch labeledBy="settings-chromecast-support" v-model="newServerSettings.chromecastEnabled" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('chromecastEnabled', val)" />
|
||||||
<p class="pl-4" id="settings-chromecast-support">{{ $strings.LabelSettingsChromecastSupport }}</p>
|
<p class="pl-4" id="settings-chromecast-support">{{ $strings.LabelSettingsChromecastSupport }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="w-44 mb-2">
|
||||||
|
<ui-dropdown v-model="newServerSettings.metadataFileFormat" small :items="metadataFileFormats" label="Metadata File Format" @input="updateMetadataFileFormat" :disabled="updatingServerSettings" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="pt-4">
|
<div class="pt-4">
|
||||||
<h2 class="font-semibold">{{ $strings.HeaderSettingsDisplay }}</h2>
|
<h2 class="font-semibold">{{ $strings.HeaderSettingsDisplay }}</h2>
|
||||||
</div>
|
</div>
|
||||||
@@ -272,7 +276,17 @@ export default {
|
|||||||
useBookshelfView: false,
|
useBookshelfView: false,
|
||||||
isPurgingCache: false,
|
isPurgingCache: false,
|
||||||
newServerSettings: {},
|
newServerSettings: {},
|
||||||
showConfirmPurgeCache: false
|
showConfirmPurgeCache: false,
|
||||||
|
metadataFileFormats: [
|
||||||
|
{
|
||||||
|
text: '.json',
|
||||||
|
value: 'json'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '.abs',
|
||||||
|
value: 'abs'
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -341,6 +355,10 @@ export default {
|
|||||||
updateServerLanguage(val) {
|
updateServerLanguage(val) {
|
||||||
this.updateSettingsKey('language', val)
|
this.updateSettingsKey('language', val)
|
||||||
},
|
},
|
||||||
|
updateMetadataFileFormat(val) {
|
||||||
|
if (this.serverSettings.metadataFileFormat === val) return
|
||||||
|
this.updateSettingsKey('metadataFileFormat', val)
|
||||||
|
},
|
||||||
updateSettingsKey(key, val) {
|
updateSettingsKey(key, val) {
|
||||||
this.updateServerSettings({
|
this.updateServerSettings({
|
||||||
[key]: val
|
[key]: val
|
||||||
@@ -350,8 +368,7 @@ export default {
|
|||||||
this.updatingServerSettings = true
|
this.updatingServerSettings = true
|
||||||
this.$store
|
this.$store
|
||||||
.dispatch('updateServerSettings', payload)
|
.dispatch('updateServerSettings', payload)
|
||||||
.then((success) => {
|
.then(() => {
|
||||||
console.log('Updated Server Settings', success)
|
|
||||||
this.updatingServerSettings = false
|
this.updatingServerSettings = false
|
||||||
this.$toast.success('Server settings updated')
|
this.$toast.success('Server settings updated')
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
<nuxt-link v-for="(artist, index) in musicArtists" :key="index" :to="`/artist/${$encode(artist)}`" class="hover:underline">{{ artist }}<span v-if="index < musicArtists.length - 1">, </span></nuxt-link>
|
<nuxt-link v-for="(artist, index) in musicArtists" :key="index" :to="`/artist/${$encode(artist)}`" class="hover:underline">{{ artist }}<span v-if="index < musicArtists.length - 1">, </span></nuxt-link>
|
||||||
</p>
|
</p>
|
||||||
<p v-else-if="authors.length" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl max-w-[calc(100vw-2rem)] overflow-hidden overflow-ellipsis">
|
<p v-else-if="authors.length" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl max-w-[calc(100vw-2rem)] overflow-hidden overflow-ellipsis">
|
||||||
by <nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">, </span></nuxt-link>
|
by <nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}?library=${libraryItem.libraryId}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">, </span></nuxt-link>
|
||||||
</p>
|
</p>
|
||||||
<p v-else class="mb-2 mt-0.5 text-gray-200 text-xl">by Unknown</p>
|
<p v-else class="mb-2 mt-0.5 text-gray-200 text-xl">by Unknown</p>
|
||||||
</template>
|
</template>
|
||||||
@@ -602,10 +602,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
clearProgressClick() {
|
clearProgressClick() {
|
||||||
|
if (!this.userMediaProgress) return
|
||||||
if (confirm(`Are you sure you want to reset your progress?`)) {
|
if (confirm(`Are you sure you want to reset your progress?`)) {
|
||||||
this.resettingProgress = true
|
this.resettingProgress = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$delete(`/api/me/progress/${this.libraryItemId}`)
|
.$delete(`/api/me/progress/${this.userMediaProgress.id}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log('Progress reset complete')
|
console.log('Progress reset complete')
|
||||||
this.$toast.success(`Your progress was reset`)
|
this.$toast.success(`Your progress was reset`)
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
<widgets-podcast-type-indicator :type="episode.episodeType" />
|
<widgets-podcast-type-indicator :type="episode.episodeType" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-sm text-gray-200 mb-4">{{ episode.subtitle }}</p>
|
<p class="text-sm text-gray-200 mb-4 episode-subtitle-long" v-html="episode.subtitle || episode.description" />
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<button class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer focus:outline-none" :class="episode.progress && episode.progress.isFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick(episode)">
|
<button class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer focus:outline-none" :class="episode.progress && episode.progress.isFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick(episode)">
|
||||||
|
|||||||
@@ -78,6 +78,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import Path from 'path'
|
||||||
import uploadHelpers from '@/mixins/uploadHelpers'
|
import uploadHelpers from '@/mixins/uploadHelpers'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -243,7 +244,7 @@ export default {
|
|||||||
ref.setUploadStatus(status)
|
ref.setUploadStatus(status)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
uploadItem(item) {
|
async uploadItem(item) {
|
||||||
var form = new FormData()
|
var form = new FormData()
|
||||||
form.set('title', item.title)
|
form.set('title', item.title)
|
||||||
if (!this.selectedLibraryIsPodcast) {
|
if (!this.selectedLibraryIsPodcast) {
|
||||||
@@ -294,18 +295,41 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var items = this.validateItems()
|
const items = this.validateItems()
|
||||||
if (!items) {
|
if (!items) {
|
||||||
this.$toast.error('Some invalid items')
|
this.$toast.error('Some invalid items')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.processing = true
|
this.processing = true
|
||||||
var itemsUploaded = 0
|
|
||||||
var itemsFailed = 0
|
const itemsToUpload = []
|
||||||
for (let i = 0; i < items.length; i++) {
|
|
||||||
var item = items[i]
|
// Check if path already exists before starting upload
|
||||||
|
// uploading fails if path already exists
|
||||||
|
for (const item of items) {
|
||||||
|
const filepath = Path.join(this.selectedFolder.fullPath, item.directory)
|
||||||
|
const exists = await this.$axios
|
||||||
|
.$post(`/api/filesystem/pathexists`, { filepath })
|
||||||
|
.then((data) => {
|
||||||
|
if (data.exists) {
|
||||||
|
this.$toast.error(`Filepath "${filepath}" already exists on server`)
|
||||||
|
}
|
||||||
|
return data.exists
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to check if filepath exists', error)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
if (!exists) {
|
||||||
|
itemsToUpload.push(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let itemsUploaded = 0
|
||||||
|
let itemsFailed = 0
|
||||||
|
for (const item of itemsToUpload) {
|
||||||
this.updateItemCardStatus(item.index, 'uploading')
|
this.updateItemCardStatus(item.index, 'uploading')
|
||||||
var result = await this.uploadItem(item)
|
const result = await this.uploadItem(item)
|
||||||
if (result) itemsUploaded++
|
if (result) itemsUploaded++
|
||||||
else itemsFailed++
|
else itemsFailed++
|
||||||
this.updateItemCardStatus(item.index, result ? 'success' : 'failed')
|
this.updateItemCardStatus(item.index, result ? 'success' : 'failed')
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ export default class PlayerHandler {
|
|||||||
|
|
||||||
this.failedProgressSyncs = 0
|
this.failedProgressSyncs = 0
|
||||||
this.lastSyncTime = 0
|
this.lastSyncTime = 0
|
||||||
this.lastSyncedAt = 0
|
|
||||||
this.listeningTimeSinceSync = 0
|
this.listeningTimeSinceSync = 0
|
||||||
|
|
||||||
this.playInterval = null
|
this.playInterval = null
|
||||||
@@ -53,6 +52,11 @@ export default class PlayerHandler {
|
|||||||
return this.libraryItem.media.episodes.find(ep => ep.id === this.episodeId)
|
return this.libraryItem.media.episodes.find(ep => ep.id === this.episodeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSessionId(sessionId) {
|
||||||
|
this.currentSessionId = sessionId
|
||||||
|
this.ctx.$store.commit('setPlaybackSessionId', sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
load(libraryItem, episodeId, playWhenReady, playbackRate, startTimeOverride = undefined) {
|
load(libraryItem, episodeId, playWhenReady, playbackRate, startTimeOverride = undefined) {
|
||||||
this.libraryItem = libraryItem
|
this.libraryItem = libraryItem
|
||||||
this.isVideo = libraryItem.mediaType === 'video'
|
this.isVideo = libraryItem.mediaType === 'video'
|
||||||
@@ -183,7 +187,7 @@ export default class PlayerHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async prepare(forceTranscode = false) {
|
async prepare(forceTranscode = false) {
|
||||||
this.currentSessionId = null // Reset session
|
this.setSessionId(null) // Reset session
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
deviceInfo: {
|
deviceInfo: {
|
||||||
@@ -210,6 +214,8 @@ export default class PlayerHandler {
|
|||||||
this.playWhenReady = false
|
this.playWhenReady = false
|
||||||
this.initialPlaybackRate = playbackRate
|
this.initialPlaybackRate = playbackRate
|
||||||
this.startTimeOverride = undefined
|
this.startTimeOverride = undefined
|
||||||
|
this.lastSyncTime = 0
|
||||||
|
this.listeningTimeSinceSync = 0
|
||||||
|
|
||||||
this.prepareSession(session)
|
this.prepareSession(session)
|
||||||
}
|
}
|
||||||
@@ -217,7 +223,7 @@ export default class PlayerHandler {
|
|||||||
prepareSession(session) {
|
prepareSession(session) {
|
||||||
this.failedProgressSyncs = 0
|
this.failedProgressSyncs = 0
|
||||||
this.startTime = this.startTimeOverride !== undefined ? this.startTimeOverride : session.currentTime
|
this.startTime = this.startTimeOverride !== undefined ? this.startTimeOverride : session.currentTime
|
||||||
this.currentSessionId = session.id
|
this.setSessionId(session.id)
|
||||||
this.displayTitle = session.displayTitle
|
this.displayTitle = session.displayTitle
|
||||||
this.displayAuthor = session.displayAuthor
|
this.displayAuthor = session.displayAuthor
|
||||||
|
|
||||||
@@ -262,7 +268,7 @@ export default class PlayerHandler {
|
|||||||
this.player = null
|
this.player = null
|
||||||
this.playerState = 'IDLE'
|
this.playerState = 'IDLE'
|
||||||
this.libraryItem = null
|
this.libraryItem = null
|
||||||
this.currentSessionId = null
|
this.setSessionId(null)
|
||||||
this.startTime = 0
|
this.startTime = 0
|
||||||
this.stopPlayInterval()
|
this.stopPlayInterval()
|
||||||
}
|
}
|
||||||
@@ -287,7 +293,8 @@ export default class PlayerHandler {
|
|||||||
const exactTimeElapsed = ((Date.now() - lastTick) / 1000)
|
const exactTimeElapsed = ((Date.now() - lastTick) / 1000)
|
||||||
lastTick = Date.now()
|
lastTick = Date.now()
|
||||||
this.listeningTimeSinceSync += exactTimeElapsed
|
this.listeningTimeSinceSync += exactTimeElapsed
|
||||||
if (this.listeningTimeSinceSync >= 5) {
|
const TimeToWaitBeforeSync = this.lastSyncTime > 0 ? 5 : 20
|
||||||
|
if (this.listeningTimeSinceSync >= TimeToWaitBeforeSync) {
|
||||||
this.sendProgressSync(currentTime)
|
this.sendProgressSync(currentTime)
|
||||||
}
|
}
|
||||||
}, 1000)
|
}, 1000)
|
||||||
@@ -297,13 +304,17 @@ export default class PlayerHandler {
|
|||||||
let syncData = null
|
let syncData = null
|
||||||
if (this.player) {
|
if (this.player) {
|
||||||
const listeningTimeToAdd = Math.max(0, Math.floor(this.listeningTimeSinceSync))
|
const listeningTimeToAdd = Math.max(0, Math.floor(this.listeningTimeSinceSync))
|
||||||
syncData = {
|
// When opening player and quickly closing dont save progress
|
||||||
timeListened: listeningTimeToAdd,
|
if (listeningTimeToAdd > 20) {
|
||||||
duration: this.getDuration(),
|
syncData = {
|
||||||
currentTime: this.getCurrentTime()
|
timeListened: listeningTimeToAdd,
|
||||||
|
duration: this.getDuration(),
|
||||||
|
currentTime: this.getCurrentTime()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.listeningTimeSinceSync = 0
|
this.listeningTimeSinceSync = 0
|
||||||
|
this.lastSyncTime = 0
|
||||||
return this.ctx.$axios.$post(`/api/session/${this.currentSessionId}/close`, syncData, { timeout: 1000 }).catch((error) => {
|
return this.ctx.$axios.$post(`/api/session/${this.currentSessionId}/close`, syncData, { timeout: 1000 }).catch((error) => {
|
||||||
console.error('Failed to close session', error)
|
console.error('Failed to close session', error)
|
||||||
})
|
})
|
||||||
@@ -322,6 +333,7 @@ export default class PlayerHandler {
|
|||||||
duration: this.getDuration(),
|
duration: this.getDuration(),
|
||||||
currentTime
|
currentTime
|
||||||
}
|
}
|
||||||
|
|
||||||
this.listeningTimeSinceSync = 0
|
this.listeningTimeSinceSync = 0
|
||||||
this.ctx.$axios.$post(`/api/session/${this.currentSessionId}/sync`, syncData, { timeout: 3000 }).then(() => {
|
this.ctx.$axios.$post(`/api/session/${this.currentSessionId}/sync`, syncData, { timeout: 3000 }).then(() => {
|
||||||
this.failedProgressSyncs = 0
|
this.failedProgressSyncs = 0
|
||||||
@@ -383,13 +395,13 @@ export default class PlayerHandler {
|
|||||||
this.player.setPlaybackRate(playbackRate)
|
this.player.setPlaybackRate(playbackRate)
|
||||||
}
|
}
|
||||||
|
|
||||||
seek(time) {
|
seek(time, shouldSync = true) {
|
||||||
if (!this.player) return
|
if (!this.player) return
|
||||||
this.player.seek(time, this.playerPlaying)
|
this.player.seek(time, this.playerPlaying)
|
||||||
this.ctx.setCurrentTime(time)
|
this.ctx.setCurrentTime(time)
|
||||||
|
|
||||||
// Update progress if paused
|
// Update progress if paused
|
||||||
if (!this.playerPlaying) {
|
if (!this.playerPlaying && shouldSync) {
|
||||||
this.sendProgressSync(time)
|
this.sendProgressSync(time)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const SupportedFileTypes = {
|
|||||||
ebook: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
|
ebook: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
|
||||||
info: ['nfo'],
|
info: ['nfo'],
|
||||||
text: ['txt'],
|
text: ['txt'],
|
||||||
metadata: ['opf', 'abs']
|
metadata: ['opf', 'abs', 'xml', 'json']
|
||||||
}
|
}
|
||||||
|
|
||||||
const DownloadStatus = {
|
const DownloadStatus = {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export const state = () => ({
|
|||||||
Source: null,
|
Source: null,
|
||||||
versionData: null,
|
versionData: null,
|
||||||
serverSettings: null,
|
serverSettings: null,
|
||||||
|
playbackSessionId: null,
|
||||||
streamLibraryItem: null,
|
streamLibraryItem: null,
|
||||||
streamEpisodeId: null,
|
streamEpisodeId: null,
|
||||||
streamIsPlaying: false,
|
streamIsPlaying: false,
|
||||||
@@ -35,7 +36,7 @@ export const getters = {
|
|||||||
return state.serverSettings[key]
|
return state.serverSettings[key]
|
||||||
},
|
},
|
||||||
getLibraryItemIdStreaming: state => {
|
getLibraryItemIdStreaming: state => {
|
||||||
return state.streamLibraryItem ? state.streamLibraryItem.id : null
|
return state.streamLibraryItem?.id || null
|
||||||
},
|
},
|
||||||
getIsStreamingFromDifferentLibrary: (state, getters, rootState) => {
|
getIsStreamingFromDifferentLibrary: (state, getters, rootState) => {
|
||||||
if (!state.streamLibraryItem) return false
|
if (!state.streamLibraryItem) return false
|
||||||
@@ -150,6 +151,9 @@ export const mutations = {
|
|||||||
if (!settings) return
|
if (!settings) return
|
||||||
state.serverSettings = settings
|
state.serverSettings = settings
|
||||||
},
|
},
|
||||||
|
setPlaybackSessionId(state, playbackSessionId) {
|
||||||
|
state.playbackSessionId = playbackSessionId
|
||||||
|
},
|
||||||
setMediaPlaying(state, payload) {
|
setMediaPlaying(state, payload) {
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
state.streamLibraryItem = null
|
state.streamLibraryItem = null
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
"ButtonRemoveAll": "Alles löschen",
|
"ButtonRemoveAll": "Alles löschen",
|
||||||
"ButtonRemoveAllLibraryItems": "Lösche alle Bibliothekseinträge",
|
"ButtonRemoveAllLibraryItems": "Lösche alle Bibliothekseinträge",
|
||||||
"ButtonRemoveFromContinueListening": "Lösche den Eintrag aus der Fortsetzungsliste",
|
"ButtonRemoveFromContinueListening": "Lösche den Eintrag aus der Fortsetzungsliste",
|
||||||
|
"ButtonRemoveFromContinueReading": "Remove from Continue Reading",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "Lösche die Serie aus der Serienfortsetzungsliste",
|
"ButtonRemoveSeriesFromContinueSeries": "Lösche die Serie aus der Serienfortsetzungsliste",
|
||||||
"ButtonReScan": "Neu scannen",
|
"ButtonReScan": "Neu scannen",
|
||||||
"ButtonReset": "Zurücksetzen",
|
"ButtonReset": "Zurücksetzen",
|
||||||
@@ -197,6 +198,7 @@
|
|||||||
"LabelComplete": "Vollständig",
|
"LabelComplete": "Vollständig",
|
||||||
"LabelConfirmPassword": "Passwort bestätigen",
|
"LabelConfirmPassword": "Passwort bestätigen",
|
||||||
"LabelContinueListening": "Weiterhören",
|
"LabelContinueListening": "Weiterhören",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Serien fortsetzen",
|
"LabelContinueSeries": "Serien fortsetzen",
|
||||||
"LabelCover": "Titelbild",
|
"LabelCover": "Titelbild",
|
||||||
"LabelCoverImageURL": "URL des Titelbildes",
|
"LabelCoverImageURL": "URL des Titelbildes",
|
||||||
@@ -331,6 +333,7 @@
|
|||||||
"LabelPubDate": "Veröffentlichungsdatum",
|
"LabelPubDate": "Veröffentlichungsdatum",
|
||||||
"LabelPublisher": "Herausgeber",
|
"LabelPublisher": "Herausgeber",
|
||||||
"LabelPublishYear": "Jahr",
|
"LabelPublishYear": "Jahr",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Kürzlich hinzugefügt",
|
"LabelRecentlyAdded": "Kürzlich hinzugefügt",
|
||||||
"LabelRecentSeries": "Aktuelle Serien",
|
"LabelRecentSeries": "Aktuelle Serien",
|
||||||
"LabelRecommended": "Empfohlen",
|
"LabelRecommended": "Empfohlen",
|
||||||
@@ -654,4 +657,4 @@
|
|||||||
"ToastSocketFailedToConnect": "Verbindung zum WebSocket fehlgeschlagen",
|
"ToastSocketFailedToConnect": "Verbindung zum WebSocket fehlgeschlagen",
|
||||||
"ToastUserDeleteFailed": "Benutzer konnte nicht gelöscht werden",
|
"ToastUserDeleteFailed": "Benutzer konnte nicht gelöscht werden",
|
||||||
"ToastUserDeleteSuccess": "Benutzer gelöscht"
|
"ToastUserDeleteSuccess": "Benutzer gelöscht"
|
||||||
}
|
}
|
||||||
@@ -55,6 +55,7 @@
|
|||||||
"ButtonRemoveAll": "Remove All",
|
"ButtonRemoveAll": "Remove All",
|
||||||
"ButtonRemoveAllLibraryItems": "Remove All Library Items",
|
"ButtonRemoveAllLibraryItems": "Remove All Library Items",
|
||||||
"ButtonRemoveFromContinueListening": "Remove from Continue Listening",
|
"ButtonRemoveFromContinueListening": "Remove from Continue Listening",
|
||||||
|
"ButtonRemoveFromContinueReading": "Remove from Continue Reading",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "Remove Series from Continue Series",
|
"ButtonRemoveSeriesFromContinueSeries": "Remove Series from Continue Series",
|
||||||
"ButtonReScan": "Re-Scan",
|
"ButtonReScan": "Re-Scan",
|
||||||
"ButtonReset": "Reset",
|
"ButtonReset": "Reset",
|
||||||
@@ -197,6 +198,7 @@
|
|||||||
"LabelComplete": "Complete",
|
"LabelComplete": "Complete",
|
||||||
"LabelConfirmPassword": "Confirm Password",
|
"LabelConfirmPassword": "Confirm Password",
|
||||||
"LabelContinueListening": "Continue Listening",
|
"LabelContinueListening": "Continue Listening",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Continue Series",
|
"LabelContinueSeries": "Continue Series",
|
||||||
"LabelCover": "Cover",
|
"LabelCover": "Cover",
|
||||||
"LabelCoverImageURL": "Cover Image URL",
|
"LabelCoverImageURL": "Cover Image URL",
|
||||||
@@ -331,6 +333,7 @@
|
|||||||
"LabelPubDate": "Pub Date",
|
"LabelPubDate": "Pub Date",
|
||||||
"LabelPublisher": "Publisher",
|
"LabelPublisher": "Publisher",
|
||||||
"LabelPublishYear": "Publish Year",
|
"LabelPublishYear": "Publish Year",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
"LabelRecommended": "Recommended",
|
"LabelRecommended": "Recommended",
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
"ButtonRemoveAll": "Remover Todos",
|
"ButtonRemoveAll": "Remover Todos",
|
||||||
"ButtonRemoveAllLibraryItems": "Remover Todos los Elementos de la Biblioteca",
|
"ButtonRemoveAllLibraryItems": "Remover Todos los Elementos de la Biblioteca",
|
||||||
"ButtonRemoveFromContinueListening": "Remover de Continuar Escuchando",
|
"ButtonRemoveFromContinueListening": "Remover de Continuar Escuchando",
|
||||||
|
"ButtonRemoveFromContinueReading": "Remove from Continue Reading",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "Remover Serie de Continuar Series",
|
"ButtonRemoveSeriesFromContinueSeries": "Remover Serie de Continuar Series",
|
||||||
"ButtonReScan": "Re-Escanear",
|
"ButtonReScan": "Re-Escanear",
|
||||||
"ButtonReset": "Reiniciar",
|
"ButtonReset": "Reiniciar",
|
||||||
@@ -197,6 +198,7 @@
|
|||||||
"LabelComplete": "Completo",
|
"LabelComplete": "Completo",
|
||||||
"LabelConfirmPassword": "Confirmar Contraseña",
|
"LabelConfirmPassword": "Confirmar Contraseña",
|
||||||
"LabelContinueListening": "Continuar Escuchando",
|
"LabelContinueListening": "Continuar Escuchando",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Continuar Series",
|
"LabelContinueSeries": "Continuar Series",
|
||||||
"LabelCover": "Portada",
|
"LabelCover": "Portada",
|
||||||
"LabelCoverImageURL": "URL de Imagen de Portada",
|
"LabelCoverImageURL": "URL de Imagen de Portada",
|
||||||
@@ -331,6 +333,7 @@
|
|||||||
"LabelPubDate": "Fecha de Publicación",
|
"LabelPubDate": "Fecha de Publicación",
|
||||||
"LabelPublisher": "Editor",
|
"LabelPublisher": "Editor",
|
||||||
"LabelPublishYear": "Año de Publicación",
|
"LabelPublishYear": "Año de Publicación",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Agregado Reciente",
|
"LabelRecentlyAdded": "Agregado Reciente",
|
||||||
"LabelRecentSeries": "Series Recientes",
|
"LabelRecentSeries": "Series Recientes",
|
||||||
"LabelRecommended": "Recomendados",
|
"LabelRecommended": "Recomendados",
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
"ButtonRemoveAll": "Supprimer tout",
|
"ButtonRemoveAll": "Supprimer tout",
|
||||||
"ButtonRemoveAllLibraryItems": "Supprimer tous les articles de la bibliothèque",
|
"ButtonRemoveAllLibraryItems": "Supprimer tous les articles de la bibliothèque",
|
||||||
"ButtonRemoveFromContinueListening": "Ne plus continuer à écouter",
|
"ButtonRemoveFromContinueListening": "Ne plus continuer à écouter",
|
||||||
|
"ButtonRemoveFromContinueReading": "Remove from Continue Reading",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "Ne plus continuer à écouter la série",
|
"ButtonRemoveSeriesFromContinueSeries": "Ne plus continuer à écouter la série",
|
||||||
"ButtonReScan": "Nouvelle analyse",
|
"ButtonReScan": "Nouvelle analyse",
|
||||||
"ButtonReset": "Réinitialiser",
|
"ButtonReset": "Réinitialiser",
|
||||||
@@ -197,6 +198,7 @@
|
|||||||
"LabelComplete": "Complet",
|
"LabelComplete": "Complet",
|
||||||
"LabelConfirmPassword": "Confirmer le mot de passe",
|
"LabelConfirmPassword": "Confirmer le mot de passe",
|
||||||
"LabelContinueListening": "Continuer la lecture",
|
"LabelContinueListening": "Continuer la lecture",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Continuer la série",
|
"LabelContinueSeries": "Continuer la série",
|
||||||
"LabelCover": "Couverture",
|
"LabelCover": "Couverture",
|
||||||
"LabelCoverImageURL": "URL vers l’image de couverture",
|
"LabelCoverImageURL": "URL vers l’image de couverture",
|
||||||
@@ -331,6 +333,7 @@
|
|||||||
"LabelPubDate": "Date de publication",
|
"LabelPubDate": "Date de publication",
|
||||||
"LabelPublisher": "Éditeur",
|
"LabelPublisher": "Éditeur",
|
||||||
"LabelPublishYear": "Année d’édition",
|
"LabelPublishYear": "Année d’édition",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Derniers ajouts",
|
"LabelRecentlyAdded": "Derniers ajouts",
|
||||||
"LabelRecentSeries": "Séries récentes",
|
"LabelRecentSeries": "Séries récentes",
|
||||||
"LabelRecommended": "Recommandé",
|
"LabelRecommended": "Recommandé",
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
"ButtonRemoveAll": "બધું કાઢી નાખો",
|
"ButtonRemoveAll": "બધું કાઢી નાખો",
|
||||||
"ButtonRemoveAllLibraryItems": "બધું પુસ્તકાલય વસ્તુઓ કાઢી નાખો",
|
"ButtonRemoveAllLibraryItems": "બધું પુસ્તકાલય વસ્તુઓ કાઢી નાખો",
|
||||||
"ButtonRemoveFromContinueListening": "સાંભળતી પુસ્તકો માંથી કાઢી નાખો",
|
"ButtonRemoveFromContinueListening": "સાંભળતી પુસ્તકો માંથી કાઢી નાખો",
|
||||||
|
"ButtonRemoveFromContinueReading": "Remove from Continue Reading",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "સાંભળતી સિરીઝ માંથી કાઢી નાખો",
|
"ButtonRemoveSeriesFromContinueSeries": "સાંભળતી સિરીઝ માંથી કાઢી નાખો",
|
||||||
"ButtonReScan": "ફરીથી સ્કેન કરો",
|
"ButtonReScan": "ફરીથી સ્કેન કરો",
|
||||||
"ButtonReset": "રીસેટ કરો",
|
"ButtonReset": "રીસેટ કરો",
|
||||||
@@ -197,6 +198,7 @@
|
|||||||
"LabelComplete": "Complete",
|
"LabelComplete": "Complete",
|
||||||
"LabelConfirmPassword": "Confirm Password",
|
"LabelConfirmPassword": "Confirm Password",
|
||||||
"LabelContinueListening": "Continue Listening",
|
"LabelContinueListening": "Continue Listening",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Continue Series",
|
"LabelContinueSeries": "Continue Series",
|
||||||
"LabelCover": "Cover",
|
"LabelCover": "Cover",
|
||||||
"LabelCoverImageURL": "Cover Image URL",
|
"LabelCoverImageURL": "Cover Image URL",
|
||||||
@@ -331,6 +333,7 @@
|
|||||||
"LabelPubDate": "Pub Date",
|
"LabelPubDate": "Pub Date",
|
||||||
"LabelPublisher": "Publisher",
|
"LabelPublisher": "Publisher",
|
||||||
"LabelPublishYear": "Publish Year",
|
"LabelPublishYear": "Publish Year",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
"LabelRecommended": "Recommended",
|
"LabelRecommended": "Recommended",
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
"ButtonRemoveAll": "सभी हटाएं",
|
"ButtonRemoveAll": "सभी हटाएं",
|
||||||
"ButtonRemoveAllLibraryItems": "पुस्तकालय की सभी आइटम हटाएं",
|
"ButtonRemoveAllLibraryItems": "पुस्तकालय की सभी आइटम हटाएं",
|
||||||
"ButtonRemoveFromContinueListening": "सुनना जारी रखें से हटाएं",
|
"ButtonRemoveFromContinueListening": "सुनना जारी रखें से हटाएं",
|
||||||
|
"ButtonRemoveFromContinueReading": "Remove from Continue Reading",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "इस सीरीज को कंटिन्यू सीरीज से हटा दें",
|
"ButtonRemoveSeriesFromContinueSeries": "इस सीरीज को कंटिन्यू सीरीज से हटा दें",
|
||||||
"ButtonReScan": "पुन: स्कैन करें",
|
"ButtonReScan": "पुन: स्कैन करें",
|
||||||
"ButtonReset": "रीसेट करें",
|
"ButtonReset": "रीसेट करें",
|
||||||
@@ -197,6 +198,7 @@
|
|||||||
"LabelComplete": "Complete",
|
"LabelComplete": "Complete",
|
||||||
"LabelConfirmPassword": "Confirm Password",
|
"LabelConfirmPassword": "Confirm Password",
|
||||||
"LabelContinueListening": "Continue Listening",
|
"LabelContinueListening": "Continue Listening",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Continue Series",
|
"LabelContinueSeries": "Continue Series",
|
||||||
"LabelCover": "Cover",
|
"LabelCover": "Cover",
|
||||||
"LabelCoverImageURL": "Cover Image URL",
|
"LabelCoverImageURL": "Cover Image URL",
|
||||||
@@ -331,6 +333,7 @@
|
|||||||
"LabelPubDate": "Pub Date",
|
"LabelPubDate": "Pub Date",
|
||||||
"LabelPublisher": "Publisher",
|
"LabelPublisher": "Publisher",
|
||||||
"LabelPublishYear": "Publish Year",
|
"LabelPublishYear": "Publish Year",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
"LabelRecommended": "Recommended",
|
"LabelRecommended": "Recommended",
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
"ButtonRemoveAll": "Ukloni sve",
|
"ButtonRemoveAll": "Ukloni sve",
|
||||||
"ButtonRemoveAllLibraryItems": "Ukloni sve stvari iz biblioteke",
|
"ButtonRemoveAllLibraryItems": "Ukloni sve stvari iz biblioteke",
|
||||||
"ButtonRemoveFromContinueListening": "Ukloni iz Nastavi slušati",
|
"ButtonRemoveFromContinueListening": "Ukloni iz Nastavi slušati",
|
||||||
|
"ButtonRemoveFromContinueReading": "Remove from Continue Reading",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "Ukloni seriju iz Nastavi seriju",
|
"ButtonRemoveSeriesFromContinueSeries": "Ukloni seriju iz Nastavi seriju",
|
||||||
"ButtonReScan": "Skeniraj ponovno",
|
"ButtonReScan": "Skeniraj ponovno",
|
||||||
"ButtonReset": "Poništi",
|
"ButtonReset": "Poništi",
|
||||||
@@ -197,6 +198,7 @@
|
|||||||
"LabelComplete": "Complete",
|
"LabelComplete": "Complete",
|
||||||
"LabelConfirmPassword": "Potvrdi lozinku",
|
"LabelConfirmPassword": "Potvrdi lozinku",
|
||||||
"LabelContinueListening": "Nastavi slušanje",
|
"LabelContinueListening": "Nastavi slušanje",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Nastavi seriju",
|
"LabelContinueSeries": "Nastavi seriju",
|
||||||
"LabelCover": "Cover",
|
"LabelCover": "Cover",
|
||||||
"LabelCoverImageURL": "URL od covera",
|
"LabelCoverImageURL": "URL od covera",
|
||||||
@@ -331,6 +333,7 @@
|
|||||||
"LabelPubDate": "Datam izdavanja",
|
"LabelPubDate": "Datam izdavanja",
|
||||||
"LabelPublisher": "Izdavač",
|
"LabelPublisher": "Izdavač",
|
||||||
"LabelPublishYear": "Godina izdavanja",
|
"LabelPublishYear": "Godina izdavanja",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Nedavno dodano",
|
"LabelRecentlyAdded": "Nedavno dodano",
|
||||||
"LabelRecentSeries": "Nedavne serije",
|
"LabelRecentSeries": "Nedavne serije",
|
||||||
"LabelRecommended": "Recommended",
|
"LabelRecommended": "Recommended",
|
||||||
|
|||||||
@@ -55,6 +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",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "Rimuovi la Serie per Continuarla",
|
"ButtonRemoveSeriesFromContinueSeries": "Rimuovi la Serie per Continuarla",
|
||||||
"ButtonReScan": "Ri-scansiona",
|
"ButtonReScan": "Ri-scansiona",
|
||||||
"ButtonReset": "Reset",
|
"ButtonReset": "Reset",
|
||||||
@@ -197,6 +198,7 @@
|
|||||||
"LabelComplete": "Completo",
|
"LabelComplete": "Completo",
|
||||||
"LabelConfirmPassword": "Conferma Password",
|
"LabelConfirmPassword": "Conferma Password",
|
||||||
"LabelContinueListening": "Continua ad Ascoltare",
|
"LabelContinueListening": "Continua ad Ascoltare",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Continua Serie",
|
"LabelContinueSeries": "Continua Serie",
|
||||||
"LabelCover": "Cover",
|
"LabelCover": "Cover",
|
||||||
"LabelCoverImageURL": "Cover Image URL",
|
"LabelCoverImageURL": "Cover Image URL",
|
||||||
@@ -331,6 +333,7 @@
|
|||||||
"LabelPubDate": "Data Pubblicazione",
|
"LabelPubDate": "Data Pubblicazione",
|
||||||
"LabelPublisher": "Editore",
|
"LabelPublisher": "Editore",
|
||||||
"LabelPublishYear": "Anno Pubblicazione",
|
"LabelPublishYear": "Anno Pubblicazione",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Aggiunti Recentemente",
|
"LabelRecentlyAdded": "Aggiunti Recentemente",
|
||||||
"LabelRecentSeries": "Serie Recenti",
|
"LabelRecentSeries": "Serie Recenti",
|
||||||
"LabelRecommended": "Raccomandati",
|
"LabelRecommended": "Raccomandati",
|
||||||
|
|||||||
+63
-60
@@ -55,6 +55,7 @@
|
|||||||
"ButtonRemoveAll": "Alles verwijderen",
|
"ButtonRemoveAll": "Alles verwijderen",
|
||||||
"ButtonRemoveAllLibraryItems": "Verwijder volledige bibliotheekinhoud",
|
"ButtonRemoveAllLibraryItems": "Verwijder volledige bibliotheekinhoud",
|
||||||
"ButtonRemoveFromContinueListening": "Vewijder uit Verder luisteren",
|
"ButtonRemoveFromContinueListening": "Vewijder uit Verder luisteren",
|
||||||
|
"ButtonRemoveFromContinueReading": "Remove from Continue Reading",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "Verwijder serie uit Serie vervolgen",
|
"ButtonRemoveSeriesFromContinueSeries": "Verwijder serie uit Serie vervolgen",
|
||||||
"ButtonReScan": "Nieuwe scan",
|
"ButtonReScan": "Nieuwe scan",
|
||||||
"ButtonReset": "Reset",
|
"ButtonReset": "Reset",
|
||||||
@@ -85,7 +86,7 @@
|
|||||||
"HeaderAdvanced": "Geavanceerd",
|
"HeaderAdvanced": "Geavanceerd",
|
||||||
"HeaderAppriseNotificationSettings": "Apprise-notificatie instellingen",
|
"HeaderAppriseNotificationSettings": "Apprise-notificatie instellingen",
|
||||||
"HeaderAudiobookTools": "Audioboekbestandbeheer tools",
|
"HeaderAudiobookTools": "Audioboekbestandbeheer tools",
|
||||||
"HeaderAudioTracks": "Audio tracks",
|
"HeaderAudioTracks": "Audiotracks",
|
||||||
"HeaderBackups": "Back-ups",
|
"HeaderBackups": "Back-ups",
|
||||||
"HeaderChangePassword": "Wachtwoord wijzigen",
|
"HeaderChangePassword": "Wachtwoord wijzigen",
|
||||||
"HeaderChapters": "Hoofdstukken",
|
"HeaderChapters": "Hoofdstukken",
|
||||||
@@ -149,10 +150,10 @@
|
|||||||
"HeaderStatsTop10Authors": "Top 10 auteurs",
|
"HeaderStatsTop10Authors": "Top 10 auteurs",
|
||||||
"HeaderStatsTop5Genres": "Top 5 genres",
|
"HeaderStatsTop5Genres": "Top 5 genres",
|
||||||
"HeaderTools": "Tools",
|
"HeaderTools": "Tools",
|
||||||
"HeaderUpdateAccount": "Update account",
|
"HeaderUpdateAccount": "Account bijwerken",
|
||||||
"HeaderUpdateAuthor": "Update auteur",
|
"HeaderUpdateAuthor": "Auteur bijwerken",
|
||||||
"HeaderUpdateDetails": "Update details",
|
"HeaderUpdateDetails": "Details bijwerken",
|
||||||
"HeaderUpdateLibrary": "Update bibliotheek",
|
"HeaderUpdateLibrary": "Bibliotheek bijwerken",
|
||||||
"HeaderUsers": "Gebruikers",
|
"HeaderUsers": "Gebruikers",
|
||||||
"HeaderYourStats": "Je statistieken",
|
"HeaderYourStats": "Je statistieken",
|
||||||
"LabelAbridged": "Verkort",
|
"LabelAbridged": "Verkort",
|
||||||
@@ -192,11 +193,12 @@
|
|||||||
"LabelChapterTitle": "Hoofdstuktitel",
|
"LabelChapterTitle": "Hoofdstuktitel",
|
||||||
"LabelClosePlayer": "Sluit speler",
|
"LabelClosePlayer": "Sluit speler",
|
||||||
"LabelCodec": "Codec",
|
"LabelCodec": "Codec",
|
||||||
"LabelCollapseSeries": "Serie inklappen",
|
"LabelCollapseSeries": "Series inklappen",
|
||||||
"LabelCollections": "Collecties",
|
"LabelCollections": "Collecties",
|
||||||
"LabelComplete": "Compleet",
|
"LabelComplete": "Compleet",
|
||||||
"LabelConfirmPassword": "Bevestig wachtwoord",
|
"LabelConfirmPassword": "Bevestig wachtwoord",
|
||||||
"LabelContinueListening": "Verder luisteren",
|
"LabelContinueListening": "Verder luisteren",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Ga verder met serie",
|
"LabelContinueSeries": "Ga verder met serie",
|
||||||
"LabelCover": "Cover",
|
"LabelCover": "Cover",
|
||||||
"LabelCoverImageURL": "Coverafbeelding URL",
|
"LabelCoverImageURL": "Coverafbeelding URL",
|
||||||
@@ -204,7 +206,7 @@
|
|||||||
"LabelCronExpression": "Cron-uitdrukking",
|
"LabelCronExpression": "Cron-uitdrukking",
|
||||||
"LabelCurrent": "Huidig",
|
"LabelCurrent": "Huidig",
|
||||||
"LabelCurrently": "Op dit moment:",
|
"LabelCurrently": "Op dit moment:",
|
||||||
"LabelCustomCronExpression": "Custom Cron-uitdrukking:",
|
"LabelCustomCronExpression": "Aangepaste Cron-uitdrukking:",
|
||||||
"LabelDatetime": "Datum-tijd",
|
"LabelDatetime": "Datum-tijd",
|
||||||
"LabelDescription": "Beschrijving",
|
"LabelDescription": "Beschrijving",
|
||||||
"LabelDeselectAll": "Deselecteer alle",
|
"LabelDeselectAll": "Deselecteer alle",
|
||||||
@@ -259,7 +261,7 @@
|
|||||||
"LabelLanguage": "Taal",
|
"LabelLanguage": "Taal",
|
||||||
"LabelLanguageDefaultServer": "Standaard servertaal",
|
"LabelLanguageDefaultServer": "Standaard servertaal",
|
||||||
"LabelLastBookAdded": "Laatst toegevoegde boek",
|
"LabelLastBookAdded": "Laatst toegevoegde boek",
|
||||||
"LabelLastBookUpdated": "Laatst geupdatete boek",
|
"LabelLastBookUpdated": "Laatst bijgewerkte boek",
|
||||||
"LabelLastSeen": "Laatst gezien",
|
"LabelLastSeen": "Laatst gezien",
|
||||||
"LabelLastTime": "Laatste keer",
|
"LabelLastTime": "Laatste keer",
|
||||||
"LabelLastUpdate": "Laatste update",
|
"LabelLastUpdate": "Laatste update",
|
||||||
@@ -275,7 +277,7 @@
|
|||||||
"LabelLogLevelWarn": "Waarschuwing",
|
"LabelLogLevelWarn": "Waarschuwing",
|
||||||
"LabelLookForNewEpisodesAfterDate": "Zoek naar nieuwe afleveringen na deze datum",
|
"LabelLookForNewEpisodesAfterDate": "Zoek naar nieuwe afleveringen na deze datum",
|
||||||
"LabelMediaPlayer": "Mediaspeler",
|
"LabelMediaPlayer": "Mediaspeler",
|
||||||
"LabelMediaType": "Mediaytype",
|
"LabelMediaType": "Mediatype",
|
||||||
"LabelMetadataProvider": "Metadatabron",
|
"LabelMetadataProvider": "Metadatabron",
|
||||||
"LabelMetaTag": "Meta-tag",
|
"LabelMetaTag": "Meta-tag",
|
||||||
"LabelMetaTags": "Meta-tags",
|
"LabelMetaTags": "Meta-tags",
|
||||||
@@ -316,7 +318,7 @@
|
|||||||
"LabelPermissionsAccessExplicitContent": "Heeft toegang tot expliciete inhoud",
|
"LabelPermissionsAccessExplicitContent": "Heeft toegang tot expliciete inhoud",
|
||||||
"LabelPermissionsDelete": "Kan verwijderen",
|
"LabelPermissionsDelete": "Kan verwijderen",
|
||||||
"LabelPermissionsDownload": "Kan downloaden",
|
"LabelPermissionsDownload": "Kan downloaden",
|
||||||
"LabelPermissionsUpdate": "Kan updaten",
|
"LabelPermissionsUpdate": "Kan bijwerken",
|
||||||
"LabelPermissionsUpload": "Kan uploaden",
|
"LabelPermissionsUpload": "Kan uploaden",
|
||||||
"LabelPhotoPathURL": "Foto pad/URL",
|
"LabelPhotoPathURL": "Foto pad/URL",
|
||||||
"LabelPlaylists": "Afspeellijsten",
|
"LabelPlaylists": "Afspeellijsten",
|
||||||
@@ -331,6 +333,7 @@
|
|||||||
"LabelPubDate": "Publicatiedatum",
|
"LabelPubDate": "Publicatiedatum",
|
||||||
"LabelPublisher": "Uitgever",
|
"LabelPublisher": "Uitgever",
|
||||||
"LabelPublishYear": "Jaar van uitgave",
|
"LabelPublishYear": "Jaar van uitgave",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Recent toegevoegd",
|
"LabelRecentlyAdded": "Recent toegevoegd",
|
||||||
"LabelRecentSeries": "Recente series",
|
"LabelRecentSeries": "Recente series",
|
||||||
"LabelRecommended": "Aangeraden",
|
"LabelRecommended": "Aangeraden",
|
||||||
@@ -356,15 +359,15 @@
|
|||||||
"LabelSettingsDateFormat": "Datum format",
|
"LabelSettingsDateFormat": "Datum format",
|
||||||
"LabelSettingsDisableWatcher": "Watcher uitschakelen",
|
"LabelSettingsDisableWatcher": "Watcher uitschakelen",
|
||||||
"LabelSettingsDisableWatcherForLibrary": "Map-watcher voor bibliotheek uitschakelen",
|
"LabelSettingsDisableWatcherForLibrary": "Map-watcher voor bibliotheek uitschakelen",
|
||||||
"LabelSettingsDisableWatcherHelp": "Schakelt het automatisch toevoegen/updaten van onderdelen wanneer bestandswijzigingen gedetecteerd zijn uit. *Vereist herstart server",
|
"LabelSettingsDisableWatcherHelp": "Schakelt het automatisch toevoegen/bijwerken van onderdelen wanneer bestandswijzigingen gedetecteerd zijn uit. *Vereist herstart server",
|
||||||
"LabelSettingsEnableEReader": "E-reader inschakelen voor alle gebruikers",
|
"LabelSettingsEnableEReader": "E-reader inschakelen voor alle gebruikers",
|
||||||
"LabelSettingsEnableEReaderHelp": "E-reader is nog in ontwikkeling, maar gebruik deze instelling om het beschikbaar te maken voor al je gebruikers (of gebruik de \"Experimentele functies\"-schakelaar voor eigen gebruik)",
|
"LabelSettingsEnableEReaderHelp": "E-reader is nog in ontwikkeling, maar gebruik deze instelling om het beschikbaar te maken voor al je gebruikers (of gebruik de \"Experimentele functies\"-schakelaar voor eigen gebruik)",
|
||||||
"LabelSettingsExperimentalFeatures": "Experimentele functies",
|
"LabelSettingsExperimentalFeatures": "Experimentele functies",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "Functies in ontwikkeling die je feedback en testing kunnen gebruiken. Klik om de Github-discussie te openen.",
|
"LabelSettingsExperimentalFeaturesHelp": "Functies in ontwikkeling die je feedback en testing kunnen gebruiken. Klik om de Github-discussie te openen.",
|
||||||
"LabelSettingsFindCovers": "Zoek covers",
|
"LabelSettingsFindCovers": "Zoek covers",
|
||||||
"LabelSettingsFindCoversHelp": "Als je audioboek geen ingesloten cover of cover in de map heeft, zal de scanner proberen een cover te vinden.<br>Opmerking: Dit zal de scan-duur verlengen",
|
"LabelSettingsFindCoversHelp": "Als je audioboek geen ingesloten cover of cover in de map heeft, zal de scanner proberen een cover te vinden.<br>Opmerking: Dit zal de scan-duur verlengen",
|
||||||
"LabelSettingsHomePageBookshelfView": "Homepagina gebruikt boekenplank-view",
|
"LabelSettingsHomePageBookshelfView": "Boekenplank-view voor homepagina",
|
||||||
"LabelSettingsLibraryBookshelfView": "Bibliotheek gebruikt boekenplank-view",
|
"LabelSettingsLibraryBookshelfView": "Boekenplank-view voor bibliotheek",
|
||||||
"LabelSettingsOverdriveMediaMarkers": "Gebruik Overdrive media markers voor hoofdstukken",
|
"LabelSettingsOverdriveMediaMarkers": "Gebruik Overdrive media markers voor hoofdstukken",
|
||||||
"LabelSettingsOverdriveMediaMarkersHelp": "MP3-bestanden van Overdrive hebben hoofdstuktiming ingesloten als custom ingesloten metadata. Door dit in te schakelen worden deze tags voor hoofdstuktiming automatisch gebruikt.",
|
"LabelSettingsOverdriveMediaMarkersHelp": "MP3-bestanden van Overdrive hebben hoofdstuktiming ingesloten als custom ingesloten metadata. Door dit in te schakelen worden deze tags voor hoofdstuktiming automatisch gebruikt.",
|
||||||
"LabelSettingsParseSubtitles": "Parseer subtitel",
|
"LabelSettingsParseSubtitles": "Parseer subtitel",
|
||||||
@@ -425,8 +428,8 @@
|
|||||||
"LabelToolsEmbedMetadataDescription": "Metadata insluiten in audiobestanden, inclusief coverafbeelding en hoofdstukken.",
|
"LabelToolsEmbedMetadataDescription": "Metadata insluiten in audiobestanden, inclusief coverafbeelding en hoofdstukken.",
|
||||||
"LabelToolsMakeM4b": "Maak M4B-audioboekbestand",
|
"LabelToolsMakeM4b": "Maak M4B-audioboekbestand",
|
||||||
"LabelToolsMakeM4bDescription": "Genereer een .M4B-audioboekbestand met ingesloten metadata, coverafbeelding en hoofdstukken.",
|
"LabelToolsMakeM4bDescription": "Genereer een .M4B-audioboekbestand met ingesloten metadata, coverafbeelding en hoofdstukken.",
|
||||||
"LabelToolsSplitM4b": "Splits M4B in MP3's",
|
"LabelToolsSplitM4b": "Splitst M4B in MP3's",
|
||||||
"LabelToolsSplitM4bDescription": "Maak MP3's van een M4B, gesplits per hoofdstuk met ingesloten metadata, coverafbeelding en hoofdstukken.",
|
"LabelToolsSplitM4bDescription": "Maak MP3's van een M4B, gesplitst per hoofdstuk met ingesloten metadata, coverafbeelding en hoofdstukken.",
|
||||||
"LabelTotalDuration": "Totale duur",
|
"LabelTotalDuration": "Totale duur",
|
||||||
"LabelTotalTimeListened": "Totale tijd geluisterd",
|
"LabelTotalTimeListened": "Totale tijd geluisterd",
|
||||||
"LabelTrackFromFilename": "Track vanuit bestandsnaam",
|
"LabelTrackFromFilename": "Track vanuit bestandsnaam",
|
||||||
@@ -437,10 +440,10 @@
|
|||||||
"LabelType": "Type",
|
"LabelType": "Type",
|
||||||
"LabelUnabridged": "Onverkort",
|
"LabelUnabridged": "Onverkort",
|
||||||
"LabelUnknown": "Onbekend",
|
"LabelUnknown": "Onbekend",
|
||||||
"LabelUpdateCover": "Update cover",
|
"LabelUpdateCover": "Cover bijwerken",
|
||||||
"LabelUpdateCoverHelp": "Sta overschrijven van bestaande covers toe voor de geselecteerde boeken wanneer een match is gevonden",
|
"LabelUpdateCoverHelp": "Sta overschrijven van bestaande covers toe voor de geselecteerde boeken wanneer een match is gevonden",
|
||||||
"LabelUpdatedAt": "Geüpdatet op",
|
"LabelUpdatedAt": "Bijgewerkt op",
|
||||||
"LabelUpdateDetails": "Update details",
|
"LabelUpdateDetails": "Details bijwerken",
|
||||||
"LabelUpdateDetailsHelp": "Sta overschrijven van bestaande details toe voor de geselecteerde boeken wanneer een match is gevonden",
|
"LabelUpdateDetailsHelp": "Sta overschrijven van bestaande details toe voor de geselecteerde boeken wanneer een match is gevonden",
|
||||||
"LabelUploaderDragAndDrop": "Slepen & neerzeten van bestanden of mappen",
|
"LabelUploaderDragAndDrop": "Slepen & neerzeten van bestanden of mappen",
|
||||||
"LabelUploaderDropFiles": "Bestanden neerzetten",
|
"LabelUploaderDropFiles": "Bestanden neerzetten",
|
||||||
@@ -455,16 +458,16 @@
|
|||||||
"LabelViewQueue": "Bekijk afspeelwachtrij",
|
"LabelViewQueue": "Bekijk afspeelwachtrij",
|
||||||
"LabelVolume": "Volume",
|
"LabelVolume": "Volume",
|
||||||
"LabelWeekdaysToRun": "Weekdagen om te draaien",
|
"LabelWeekdaysToRun": "Weekdagen om te draaien",
|
||||||
"LabelYourAudiobookDuration": "Jouw audioboekduur",
|
"LabelYourAudiobookDuration": "Je audioboekduur",
|
||||||
"LabelYourBookmarks": "Jouw boekwijzers",
|
"LabelYourBookmarks": "Je boekwijzers",
|
||||||
"LabelYourPlaylists": "Jouw afspeellijsten",
|
"LabelYourPlaylists": "Je afspeellijsten",
|
||||||
"LabelYourProgress": "Jouw voortgang",
|
"LabelYourProgress": "Je voortgang",
|
||||||
"MessageAddToPlayerQueue": "Toevoegen aan wachtrij",
|
"MessageAddToPlayerQueue": "Toevoegen aan wachtrij",
|
||||||
"MessageAppriseDescription": "Om deze functie te gebruiken heb je een draaiende instantie van <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> nodig of een api die dezelfde requests afhandelt. <br />De Apprise API Url moet het volledige URL-pad zijn om de notificatie te verzenden, b.v., als je API-instantie draait op <code>http://192.168.1.1:8337</code> dan zou je <code>http://192.168.1.1:8337/notify</code> gebruiken.",
|
"MessageAppriseDescription": "Om deze functie te gebruiken heb je een draaiende instantie van <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> nodig of een api die dezelfde requests afhandelt. <br />De Apprise API Url moet het volledige URL-pad zijn om de notificatie te verzenden, b.v., als je API-instantie draait op <code>http://192.168.1.1:8337</code> dan zou je <code>http://192.168.1.1:8337/notify</code> gebruiken.",
|
||||||
"MessageBackupsDescription": "Back-ups omvatten gebruikers, gebruikers' voortgang, bibliotheekonderdeeldetails, serverinstellingen en afbeeldingen bewaard in <code>/metadata/items</code> & <code>/metadata/authors</code>. Back-ups <strong>bevatten niet</strong> de bestanden bewaard in je bibliotheekmappen.",
|
"MessageBackupsDescription": "Back-ups omvatten gebruikers, gebruikers' voortgang, bibliotheekonderdeeldetails, serverinstellingen en afbeeldingen bewaard in <code>/metadata/items</code> & <code>/metadata/authors</code>. Back-ups <strong>bevatten niet</strong> de bestanden bewaard in je bibliotheekmappen.",
|
||||||
"MessageBatchQuickMatchDescription": "Quick Match zal proberen ontbrekende covers en metadata voor de geselecteerde onderdelen te matchten. Schakel de opties hieronder in om Quick Match toe te staan bestaande covers en/of metadata te overschrijven.",
|
"MessageBatchQuickMatchDescription": "Quick Match zal proberen ontbrekende covers en metadata voor de geselecteerde onderdelen te matchten. Schakel de opties hieronder in om Quick Match toe te staan bestaande covers en/of metadata te overschrijven.",
|
||||||
"MessageBookshelfNoCollections": "Je hebt nog geen collecties gemaakt",
|
"MessageBookshelfNoCollections": "Je hebt nog geen collecties gemaakt",
|
||||||
"MessageBookshelfNoResultsForFilter": "Geen resultaten voo filter \"{0}: {1}\"",
|
"MessageBookshelfNoResultsForFilter": "Geen resultaten voor filter \"{0}: {1}\"",
|
||||||
"MessageBookshelfNoRSSFeeds": "Geen RSS-feeds geopend",
|
"MessageBookshelfNoRSSFeeds": "Geen RSS-feeds geopend",
|
||||||
"MessageBookshelfNoSeries": "Je hebt geen series",
|
"MessageBookshelfNoSeries": "Je hebt geen series",
|
||||||
"MessageChapterEndIsAfter": "Hoofdstukeinde is na het einde van je audioboek",
|
"MessageChapterEndIsAfter": "Hoofdstukeinde is na het einde van je audioboek",
|
||||||
@@ -487,32 +490,32 @@
|
|||||||
"MessageConfirmRemovePlaylist": "Weet je zeker dat je je afspeellijst \"{0}\" wil verwijderen?",
|
"MessageConfirmRemovePlaylist": "Weet je zeker dat je je afspeellijst \"{0}\" wil verwijderen?",
|
||||||
"MessageConfirmRenameGenre": "Weet je zeker dat je genre \"{0}\" wil hernoemen naar \"{1}\" voor alle onderdelen?",
|
"MessageConfirmRenameGenre": "Weet je zeker dat je genre \"{0}\" wil hernoemen naar \"{1}\" voor alle onderdelen?",
|
||||||
"MessageConfirmRenameGenreMergeNote": "Opmerking: Dit genre bestaat al, dus zullen ze worden samengevoegd.",
|
"MessageConfirmRenameGenreMergeNote": "Opmerking: Dit genre bestaat al, dus zullen ze worden samengevoegd.",
|
||||||
"MessageConfirmRenameGenreWarning": "Waarschuwing! Een gelijknamig genre met ander hooflettergebruik bestaat al: \"{0}\".",
|
"MessageConfirmRenameGenreWarning": "Waarschuwing! Een gelijknamig genre met ander hoofdlettergebruik bestaat al: \"{0}\".",
|
||||||
"MessageConfirmRenameTag": "Weet je zeker dat je tag \"{0}\" wil hernoemen naar\"{1}\" voor alle onderdelen?",
|
"MessageConfirmRenameTag": "Weet je zeker dat je tag \"{0}\" wil hernoemen naar\"{1}\" voor alle onderdelen?",
|
||||||
"MessageConfirmRenameTagMergeNote": "Opmerking: Deze tag bestaat al, dus zullen ze worden samengevoegd.",
|
"MessageConfirmRenameTagMergeNote": "Opmerking: Deze tag bestaat al, dus zullen ze worden samengevoegd.",
|
||||||
"MessageConfirmRenameTagWarning": "Waarschuwing! Een gelijknamige tag met ander hooflettergebruik bestaat al: \"{0}\".",
|
"MessageConfirmRenameTagWarning": "Waarschuwing! Een gelijknamige tag met ander hoofdlettergebruik bestaat al: \"{0}\".",
|
||||||
"MessageDownloadingEpisode": "Aflevering aan het dowloaden",
|
"MessageDownloadingEpisode": "Aflevering aan het dowloaden",
|
||||||
"MessageDragFilesIntoTrackOrder": "Sleep bestanden in de juiste trackvolgorde",
|
"MessageDragFilesIntoTrackOrder": "Sleep bestanden in de juiste trackvolgorde",
|
||||||
"MessageEmbedFinished": "Insluiting voltooid!",
|
"MessageEmbedFinished": "Insluiting voltooid!",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} aflevering(en) in de rij om te downloaden",
|
"MessageEpisodesQueuedForDownload": "{0} aflevering(en) in de rij om te downloaden",
|
||||||
"MessageFeedURLWillBe": "Feed URL zal {0} zijn",
|
"MessageFeedURLWillBe": "Feed URL zal {0} zijn",
|
||||||
"MessageFetching": "Aan het ophalen...",
|
"MessageFetching": "Aan het ophalen...",
|
||||||
"MessageForceReScanDescription": "zall alle bestanden opnieuw scannen als een verse scan. Audiobestanden ID3-tags, OPF-bestanden en textbestanden zullen als nieuw worden gescand.",
|
"MessageForceReScanDescription": "zal alle bestanden opnieuw scannen als een verse scan. Audiobestanden ID3-tags, OPF-bestanden en textbestanden zullen als nieuw worden gescand.",
|
||||||
"MessageImportantNotice": "Belangrijke opmerking!",
|
"MessageImportantNotice": "Belangrijke opmerking!",
|
||||||
"MessageInsertChapterBelow": "Hoofdstuk hieronder invoegen",
|
"MessageInsertChapterBelow": "Hoofdstuk hieronder invoegen",
|
||||||
"MessageItemsSelected": "{0} onderdelen geselecteerd",
|
"MessageItemsSelected": "{0} onderdelen geselecteerd",
|
||||||
"MessageItemsUpdated": "{0} onderdelen geüpdatet",
|
"MessageItemsUpdated": "{0} onderdelen bijgewerkt",
|
||||||
"MessageJoinUsOn": "Doe mee op",
|
"MessageJoinUsOn": "Doe mee op",
|
||||||
"MessageListeningSessionsInTheLastYear": "{0} luistersessies in het laatste jaar",
|
"MessageListeningSessionsInTheLastYear": "{0} luistersessies in het laatste jaar",
|
||||||
"MessageLoading": "Aan het laden...",
|
"MessageLoading": "Aan het laden...",
|
||||||
"MessageLoadingFolders": "Mappen aan het laden...",
|
"MessageLoadingFolders": "Mappen aan het laden...",
|
||||||
"MessageM4BFailed": "M4B mislukt!",
|
"MessageM4BFailed": "M4B mislukt!",
|
||||||
"MessageM4BFinished": "M4B voltooid!",
|
"MessageM4BFinished": "M4B voltooid!",
|
||||||
"MessageMapChapterTitles": "Map hoofdstuktitels naar je bsetaande audioboekhoofdstukken zonder aanpassing van tijden",
|
"MessageMapChapterTitles": "Map hoofdstuktitels naar je bestaande audioboekhoofdstukken zonder aanpassing van tijden",
|
||||||
"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.",
|
||||||
"MessageNoAudioTracks": "Geen audio tracks",
|
"MessageNoAudioTracks": "Geen audiotracks",
|
||||||
"MessageNoAuthors": "Geen auteurs",
|
"MessageNoAuthors": "Geen auteurs",
|
||||||
"MessageNoBackups": "Geen back-ups",
|
"MessageNoBackups": "Geen back-ups",
|
||||||
"MessageNoBookmarks": "Geen boekwijzers",
|
"MessageNoBookmarks": "Geen boekwijzers",
|
||||||
@@ -535,13 +538,13 @@
|
|||||||
"MessageNoNotifications": "Geen notificaties",
|
"MessageNoNotifications": "Geen notificaties",
|
||||||
"MessageNoPodcastsFound": "Geen podcasts gevonden",
|
"MessageNoPodcastsFound": "Geen podcasts gevonden",
|
||||||
"MessageNoResults": "Geen resultaten",
|
"MessageNoResults": "Geen resultaten",
|
||||||
"MessageNoSearchResultsFor": "Geen zoekresultatn voor \"{0}\"",
|
"MessageNoSearchResultsFor": "Geen zoekresultaten voor \"{0}\"",
|
||||||
"MessageNoSeries": "Geen series",
|
"MessageNoSeries": "Geen series",
|
||||||
"MessageNoTags": "Geen tags",
|
"MessageNoTags": "Geen tags",
|
||||||
"MessageNoTasksRunning": "Geen lopende taken",
|
"MessageNoTasksRunning": "Geen lopende taken",
|
||||||
"MessageNotYetImplemented": "Nog niet geimplementeerd",
|
"MessageNotYetImplemented": "Nog niet geimplementeerd",
|
||||||
"MessageNoUpdateNecessary": "Geen update noodzakelijk",
|
"MessageNoUpdateNecessary": "Geen bijwerking noodzakelijk",
|
||||||
"MessageNoUpdatesWereNecessary": "Geen updates waren noodzakelijk",
|
"MessageNoUpdatesWereNecessary": "Geen bijwerkingen waren noodzakelijk",
|
||||||
"MessageNoUserPlaylists": "Je hebt geen afspeellijsten",
|
"MessageNoUserPlaylists": "Je hebt geen afspeellijsten",
|
||||||
"MessageOr": "of",
|
"MessageOr": "of",
|
||||||
"MessagePauseChapter": "Pauzeer afspelen hoofdstuk",
|
"MessagePauseChapter": "Pauzeer afspelen hoofdstuk",
|
||||||
@@ -549,7 +552,7 @@
|
|||||||
"MessagePlaylistCreateFromCollection": "Afspeellijst aanmaken vanuit collectie",
|
"MessagePlaylistCreateFromCollection": "Afspeellijst aanmaken vanuit collectie",
|
||||||
"MessagePodcastHasNoRSSFeedForMatching": "Podcast heeft geen RSS-feed URL om te gebruiken voor matching",
|
"MessagePodcastHasNoRSSFeedForMatching": "Podcast heeft geen RSS-feed URL om te gebruiken voor matching",
|
||||||
"MessageQuickMatchDescription": "Vul lege onderdeeldetails & cover met eerste matchresultaat van '{0}'. Overschrijft geen details tenzij 'Prefereer gematchte metadata' serverinstelling is ingeschakeld.",
|
"MessageQuickMatchDescription": "Vul lege onderdeeldetails & cover met eerste matchresultaat van '{0}'. Overschrijft geen details tenzij 'Prefereer gematchte metadata' serverinstelling is ingeschakeld.",
|
||||||
"MessageRemoveAllItemsWarning": "WAARSCHUWING! Deze actie zal alle onderdelen in de bibliotheek verwijderen uit de database, inclusief enige updates of matches die je hebt gemaakt. Dit doet niets met je onderliggende bestanden. Weet je het zeker?",
|
"MessageRemoveAllItemsWarning": "WAARSCHUWING! Deze actie zal alle onderdelen in de bibliotheek verwijderen uit de database, inclusief enige bijwerkingen of matches die je hebt gemaakt. Dit doet niets met je onderliggende bestanden. Weet je het zeker?",
|
||||||
"MessageRemoveChapter": "Verwijder hoofdstuk",
|
"MessageRemoveChapter": "Verwijder hoofdstuk",
|
||||||
"MessageRemoveEpisodes": "Verwijder {0} aflevering(en)",
|
"MessageRemoveEpisodes": "Verwijder {0} aflevering(en)",
|
||||||
"MessageRemoveFromPlayerQueue": "Verwijder uit afspeelwachtrij",
|
"MessageRemoveFromPlayerQueue": "Verwijder uit afspeelwachtrij",
|
||||||
@@ -560,8 +563,8 @@
|
|||||||
"MessageRestoreBackupWarning": "Herstellen met een back-up zal de volledige database in /config en de covers in /metadata/items & /metadata/authors overschrijven.<br /><br />Back-ups wijzigen geen bestanden in je bibliotheekmappen. Als je de serverinstelling gebruikt om covers en metadata in je bibliotheekmappen te bewaren dan worden deze niet geback-upt of overschreven.<br /><br />Alle clients die van je server gebruik maken zullen automatisch worden ververst.",
|
"MessageRestoreBackupWarning": "Herstellen met een back-up zal de volledige database in /config en de covers in /metadata/items & /metadata/authors overschrijven.<br /><br />Back-ups wijzigen geen bestanden in je bibliotheekmappen. Als je de serverinstelling gebruikt om covers en metadata in je bibliotheekmappen te bewaren dan worden deze niet geback-upt of overschreven.<br /><br />Alle clients die van je server gebruik maken zullen automatisch worden ververst.",
|
||||||
"MessageSearchResultsFor": "Zoekresultaten voor",
|
"MessageSearchResultsFor": "Zoekresultaten voor",
|
||||||
"MessageServerCouldNotBeReached": "Server niet bereikbaar",
|
"MessageServerCouldNotBeReached": "Server niet bereikbaar",
|
||||||
"MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name",
|
"MessageSetChaptersFromTracksDescription": "Stel hoofdstukken in met ieder audiobestand als een hoofdstuk en de audiobestandsnaam als hoofdstuktitel",
|
||||||
"MessageStartPlaybackAtTime": "Start playback for \"{0}\" at {1}?",
|
"MessageStartPlaybackAtTime": "Afspelen van \"{0}\" beginnen op {1}?",
|
||||||
"MessageThinking": "Aan het denken...",
|
"MessageThinking": "Aan het denken...",
|
||||||
"MessageUploaderItemFailed": "Uploaden mislukt",
|
"MessageUploaderItemFailed": "Uploaden mislukt",
|
||||||
"MessageUploaderItemSuccess": "Uploaden gelukt!",
|
"MessageUploaderItemSuccess": "Uploaden gelukt!",
|
||||||
@@ -569,8 +572,8 @@
|
|||||||
"MessageValidCronExpression": "Geldige cron-uitdrukking",
|
"MessageValidCronExpression": "Geldige cron-uitdrukking",
|
||||||
"MessageWatcherIsDisabledGlobally": "Watcher is globaal uitgeschakeld in serverinstellingen",
|
"MessageWatcherIsDisabledGlobally": "Watcher is globaal uitgeschakeld in serverinstellingen",
|
||||||
"MessageXLibraryIsEmpty": "{0} bibliotheek is leeg!",
|
"MessageXLibraryIsEmpty": "{0} bibliotheek is leeg!",
|
||||||
"MessageYourAudiobookDurationIsLonger": "Duur van jouw audioboek is langer dan de gevonden duur",
|
"MessageYourAudiobookDurationIsLonger": "Duur van je audioboek is langer dan de gevonden duur",
|
||||||
"MessageYourAudiobookDurationIsShorter": "Duur van jouw audioboek is korter dan de gevonden duur",
|
"MessageYourAudiobookDurationIsShorter": "Duur van je audioboek is korter dan de gevonden duur",
|
||||||
"NoteChangeRootPassword": "Root-gebruiker is de enige gebruiker die een leeg wachtwoord kan hebben",
|
"NoteChangeRootPassword": "Root-gebruiker is de enige gebruiker die een leeg wachtwoord kan hebben",
|
||||||
"NoteChapterEditorTimes": "Opmerking: Starttijd van het eerste hoofdstuk moet op 0:00 blijven en de starttijd van het laatste hoofdstuk mag niet de duur van het audioboek overschrijden.",
|
"NoteChapterEditorTimes": "Opmerking: Starttijd van het eerste hoofdstuk moet op 0:00 blijven en de starttijd van het laatste hoofdstuk mag niet de duur van het audioboek overschrijden.",
|
||||||
"NoteFolderPicker": "Opmerking: Reeds gemapte mappen worden niet getoond",
|
"NoteFolderPicker": "Opmerking: Reeds gemapte mappen worden niet getoond",
|
||||||
@@ -585,14 +588,14 @@
|
|||||||
"PlaceholderNewPlaylist": "Nieuwe naam afspeellijst",
|
"PlaceholderNewPlaylist": "Nieuwe naam afspeellijst",
|
||||||
"PlaceholderSearch": "Zoeken..",
|
"PlaceholderSearch": "Zoeken..",
|
||||||
"PlaceholderSearchEpisode": "Aflevering zoeken..",
|
"PlaceholderSearchEpisode": "Aflevering zoeken..",
|
||||||
"ToastAccountUpdateFailed": "Updaten account mislukt",
|
"ToastAccountUpdateFailed": "Bijwerken account mislukt",
|
||||||
"ToastAccountUpdateSuccess": "Account geüpdatet",
|
"ToastAccountUpdateSuccess": "Account bijgewerkt",
|
||||||
"ToastAuthorImageRemoveFailed": "Afbeelding verwijderen mislukt",
|
"ToastAuthorImageRemoveFailed": "Afbeelding verwijderen mislukt",
|
||||||
"ToastAuthorImageRemoveSuccess": "Afbeelding auteur verwijderd",
|
"ToastAuthorImageRemoveSuccess": "Afbeelding auteur verwijderd",
|
||||||
"ToastAuthorUpdateFailed": "Updaten auteur mislukt",
|
"ToastAuthorUpdateFailed": "Bijwerken auteur mislukt",
|
||||||
"ToastAuthorUpdateMerged": "Auteur samengevoegd",
|
"ToastAuthorUpdateMerged": "Auteur samengevoegd",
|
||||||
"ToastAuthorUpdateSuccess": "Auteur geüpdatet",
|
"ToastAuthorUpdateSuccess": "Auteur bijgewerkt",
|
||||||
"ToastAuthorUpdateSuccessNoImageFound": "Auteur geüpdatet (geen afbeelding gevonden)",
|
"ToastAuthorUpdateSuccessNoImageFound": "Auteur bijgewerkt (geen afbeelding gevonden)",
|
||||||
"ToastBackupCreateFailed": "Back-up maken mislukt",
|
"ToastBackupCreateFailed": "Back-up maken mislukt",
|
||||||
"ToastBackupCreateSuccess": "Back-up gemaakt",
|
"ToastBackupCreateSuccess": "Back-up gemaakt",
|
||||||
"ToastBackupDeleteFailed": "Verwijderen back-up mislukt",
|
"ToastBackupDeleteFailed": "Verwijderen back-up mislukt",
|
||||||
@@ -600,27 +603,27 @@
|
|||||||
"ToastBackupRestoreFailed": "Herstellen back-up mislukt",
|
"ToastBackupRestoreFailed": "Herstellen back-up mislukt",
|
||||||
"ToastBackupUploadFailed": "Uploaden back-up mislukt",
|
"ToastBackupUploadFailed": "Uploaden back-up mislukt",
|
||||||
"ToastBackupUploadSuccess": "Back-up geüpload",
|
"ToastBackupUploadSuccess": "Back-up geüpload",
|
||||||
"ToastBatchUpdateFailed": "Bulk-update mislukt",
|
"ToastBatchUpdateFailed": "Bulk-bijwerking mislukt",
|
||||||
"ToastBatchUpdateSuccess": "Bulk-update gelukt",
|
"ToastBatchUpdateSuccess": "Bulk-bijwerking gelukt",
|
||||||
"ToastBookmarkCreateFailed": "Aanmaken boekwijzer mislukt",
|
"ToastBookmarkCreateFailed": "Aanmaken boekwijzer mislukt",
|
||||||
"ToastBookmarkCreateSuccess": "boekwijzer toegevoegd",
|
"ToastBookmarkCreateSuccess": "boekwijzer toegevoegd",
|
||||||
"ToastBookmarkRemoveFailed": "Verwijderen boekwijzer mislukt",
|
"ToastBookmarkRemoveFailed": "Verwijderen boekwijzer mislukt",
|
||||||
"ToastBookmarkRemoveSuccess": "Boekwijzer verwijderd",
|
"ToastBookmarkRemoveSuccess": "Boekwijzer verwijderd",
|
||||||
"ToastBookmarkUpdateFailed": "Updaten boekwijzer mislukt",
|
"ToastBookmarkUpdateFailed": "Bijwerken boekwijzer mislukt",
|
||||||
"ToastBookmarkUpdateSuccess": "Boekwijzer geüpdatet",
|
"ToastBookmarkUpdateSuccess": "Boekwijzer bijgewerkt",
|
||||||
"ToastChaptersHaveErrors": "Hoofdstukken bevatten fouten",
|
"ToastChaptersHaveErrors": "Hoofdstukken bevatten fouten",
|
||||||
"ToastChaptersMustHaveTitles": "Hoofdstukken moeten titels hebben",
|
"ToastChaptersMustHaveTitles": "Hoofdstukken moeten titels hebben",
|
||||||
"ToastCollectionItemsRemoveFailed": "Verwijderen onderdeel (of onderdelen) uit collectie mislukt",
|
"ToastCollectionItemsRemoveFailed": "Verwijderen onderdeel (of onderdelen) uit collectie mislukt",
|
||||||
"ToastCollectionItemsRemoveSuccess": "Onderdeel (of onderdelen) verwijderd uit collectie",
|
"ToastCollectionItemsRemoveSuccess": "Onderdeel (of onderdelen) verwijderd uit collectie",
|
||||||
"ToastCollectionRemoveFailed": "Verwijderen collectie mislukt",
|
"ToastCollectionRemoveFailed": "Verwijderen collectie mislukt",
|
||||||
"ToastCollectionRemoveSuccess": "Collectie verwijderd",
|
"ToastCollectionRemoveSuccess": "Collectie verwijderd",
|
||||||
"ToastCollectionUpdateFailed": "Updaten collectie mislukt",
|
"ToastCollectionUpdateFailed": "Bijwerken collectie mislukt",
|
||||||
"ToastCollectionUpdateSuccess": "Collectie geüpdatet",
|
"ToastCollectionUpdateSuccess": "Collectie bijgewerkt",
|
||||||
"ToastItemCoverUpdateFailed": "Updaten cover onderdeel mislukt",
|
"ToastItemCoverUpdateFailed": "Bijwerken cover onderdeel mislukt",
|
||||||
"ToastItemCoverUpdateSuccess": "Cover onderdeel geüpdatet",
|
"ToastItemCoverUpdateSuccess": "Cover onderdeel bijgewerkt",
|
||||||
"ToastItemDetailsUpdateFailed": "Updaten details onderdeel mislukt",
|
"ToastItemDetailsUpdateFailed": "Bijwerken details onderdeel mislukt",
|
||||||
"ToastItemDetailsUpdateSuccess": "Details onderdeel geüpdatet",
|
"ToastItemDetailsUpdateSuccess": "Details onderdeel bijgewerkt",
|
||||||
"ToastItemDetailsUpdateUnneeded": "Geen updates nodig voor details onderdeel",
|
"ToastItemDetailsUpdateUnneeded": "Geen bijwerking nodig voor details onderdeel",
|
||||||
"ToastItemMarkedAsFinishedFailed": "Markeren als Voltooid mislukt",
|
"ToastItemMarkedAsFinishedFailed": "Markeren als Voltooid mislukt",
|
||||||
"ToastItemMarkedAsFinishedSuccess": "Onderdeel gemarkeerd als Voltooid",
|
"ToastItemMarkedAsFinishedSuccess": "Onderdeel gemarkeerd als Voltooid",
|
||||||
"ToastItemMarkedAsNotFinishedFailed": "Markeren als Niet Voltooid mislukt",
|
"ToastItemMarkedAsNotFinishedFailed": "Markeren als Niet Voltooid mislukt",
|
||||||
@@ -631,22 +634,22 @@
|
|||||||
"ToastLibraryDeleteSuccess": "Bibliotheek verwijderd",
|
"ToastLibraryDeleteSuccess": "Bibliotheek verwijderd",
|
||||||
"ToastLibraryScanFailedToStart": "Starten scan mislukt",
|
"ToastLibraryScanFailedToStart": "Starten scan mislukt",
|
||||||
"ToastLibraryScanStarted": "Scannen bibliotheek gestart",
|
"ToastLibraryScanStarted": "Scannen bibliotheek gestart",
|
||||||
"ToastLibraryUpdateFailed": "Updaten bibliotheek mislukt",
|
"ToastLibraryUpdateFailed": "Bijwerken bibliotheek mislukt",
|
||||||
"ToastLibraryUpdateSuccess": "Bibliotheek \"{0}\" geüpdatet",
|
"ToastLibraryUpdateSuccess": "Bibliotheek \"{0}\" bijgewerkt",
|
||||||
"ToastPlaylistCreateFailed": "Aanmaken afspeellijst mislukt",
|
"ToastPlaylistCreateFailed": "Aanmaken afspeellijst mislukt",
|
||||||
"ToastPlaylistCreateSuccess": "Afspeellijst aangemaakt",
|
"ToastPlaylistCreateSuccess": "Afspeellijst aangemaakt",
|
||||||
"ToastPlaylistRemoveFailed": "Verwijderen afspeellijst mislukt",
|
"ToastPlaylistRemoveFailed": "Verwijderen afspeellijst mislukt",
|
||||||
"ToastPlaylistRemoveSuccess": "Afspeellijst verwijderd",
|
"ToastPlaylistRemoveSuccess": "Afspeellijst verwijderd",
|
||||||
"ToastPlaylistUpdateFailed": "Afspeellijst updaten mislukt",
|
"ToastPlaylistUpdateFailed": "Afspeellijst bijwerken mislukt",
|
||||||
"ToastPlaylistUpdateSuccess": "Afspeellijst geüpdatet",
|
"ToastPlaylistUpdateSuccess": "Afspeellijst bijgewerkt",
|
||||||
"ToastPodcastCreateFailed": "Podcast aanmaken mislukt",
|
"ToastPodcastCreateFailed": "Podcast aanmaken mislukt",
|
||||||
"ToastPodcastCreateSuccess": "Podcast aangemaakt",
|
"ToastPodcastCreateSuccess": "Podcast aangemaakt",
|
||||||
"ToastRemoveItemFromCollectionFailed": "Onderdeel verwijderen uit collectie mislukt",
|
"ToastRemoveItemFromCollectionFailed": "Onderdeel verwijderen uit collectie mislukt",
|
||||||
"ToastRemoveItemFromCollectionSuccess": "Onderdeel verwijderd uit collectie",
|
"ToastRemoveItemFromCollectionSuccess": "Onderdeel verwijderd uit collectie",
|
||||||
"ToastRSSFeedCloseFailed": "Sluiten RSS-feed mislukt",
|
"ToastRSSFeedCloseFailed": "Sluiten RSS-feed mislukt",
|
||||||
"ToastRSSFeedCloseSuccess": "RSS-feed gesloten",
|
"ToastRSSFeedCloseSuccess": "RSS-feed gesloten",
|
||||||
"ToastSeriesUpdateFailed": "Serie update mislukt",
|
"ToastSeriesUpdateFailed": "Bijwerken serie mislukt",
|
||||||
"ToastSeriesUpdateSuccess": "Serie update gelukt",
|
"ToastSeriesUpdateSuccess": "Bijwerken serie gelukt",
|
||||||
"ToastSessionDeleteFailed": "Verwijderen sessie mislukt",
|
"ToastSessionDeleteFailed": "Verwijderen sessie mislukt",
|
||||||
"ToastSessionDeleteSuccess": "Sessie verwijderd",
|
"ToastSessionDeleteSuccess": "Sessie verwijderd",
|
||||||
"ToastSocketConnected": "Socket verbonden",
|
"ToastSocketConnected": "Socket verbonden",
|
||||||
@@ -654,4 +657,4 @@
|
|||||||
"ToastSocketFailedToConnect": "Verbinding Socket mislukt",
|
"ToastSocketFailedToConnect": "Verbinding Socket mislukt",
|
||||||
"ToastUserDeleteFailed": "Verwijderen gebruiker mislukt",
|
"ToastUserDeleteFailed": "Verwijderen gebruiker mislukt",
|
||||||
"ToastUserDeleteSuccess": "Gebruiker verwijderd"
|
"ToastUserDeleteSuccess": "Gebruiker verwijderd"
|
||||||
}
|
}
|
||||||
@@ -55,6 +55,7 @@
|
|||||||
"ButtonRemoveAll": "Usuń wszystko",
|
"ButtonRemoveAll": "Usuń wszystko",
|
||||||
"ButtonRemoveAllLibraryItems": "Usuń wszystkie elementy z biblioteki",
|
"ButtonRemoveAllLibraryItems": "Usuń wszystkie elementy z biblioteki",
|
||||||
"ButtonRemoveFromContinueListening": "Usuń z listy odtwarzania",
|
"ButtonRemoveFromContinueListening": "Usuń z listy odtwarzania",
|
||||||
|
"ButtonRemoveFromContinueReading": "Remove from Continue Reading",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "Usuń serię z listy odtwarzania",
|
"ButtonRemoveSeriesFromContinueSeries": "Usuń serię z listy odtwarzania",
|
||||||
"ButtonReScan": "Ponowne skanowanie",
|
"ButtonReScan": "Ponowne skanowanie",
|
||||||
"ButtonReset": "Resetowanie",
|
"ButtonReset": "Resetowanie",
|
||||||
@@ -197,6 +198,7 @@
|
|||||||
"LabelComplete": "Ukończone",
|
"LabelComplete": "Ukończone",
|
||||||
"LabelConfirmPassword": "Potwierdź hasło",
|
"LabelConfirmPassword": "Potwierdź hasło",
|
||||||
"LabelContinueListening": "Kontynuuj odtwarzanie",
|
"LabelContinueListening": "Kontynuuj odtwarzanie",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Kontynuuj serię",
|
"LabelContinueSeries": "Kontynuuj serię",
|
||||||
"LabelCover": "Okładka",
|
"LabelCover": "Okładka",
|
||||||
"LabelCoverImageURL": "URL okładki",
|
"LabelCoverImageURL": "URL okładki",
|
||||||
@@ -331,6 +333,7 @@
|
|||||||
"LabelPubDate": "Data publikacji",
|
"LabelPubDate": "Data publikacji",
|
||||||
"LabelPublisher": "Wydawca",
|
"LabelPublisher": "Wydawca",
|
||||||
"LabelPublishYear": "Rok publikacji",
|
"LabelPublishYear": "Rok publikacji",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Niedawno dodany",
|
"LabelRecentlyAdded": "Niedawno dodany",
|
||||||
"LabelRecentSeries": "Ostatnie serie",
|
"LabelRecentSeries": "Ostatnie serie",
|
||||||
"LabelRecommended": "Recommended",
|
"LabelRecommended": "Recommended",
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
"ButtonRemoveAll": "Удалить всё",
|
"ButtonRemoveAll": "Удалить всё",
|
||||||
"ButtonRemoveAllLibraryItems": "Удалить все элементы библиотеки",
|
"ButtonRemoveAllLibraryItems": "Удалить все элементы библиотеки",
|
||||||
"ButtonRemoveFromContinueListening": "Удалить из Продолжить слушать",
|
"ButtonRemoveFromContinueListening": "Удалить из Продолжить слушать",
|
||||||
|
"ButtonRemoveFromContinueReading": "Remove from Continue Reading",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "Удалить серию из Продолжить серию",
|
"ButtonRemoveSeriesFromContinueSeries": "Удалить серию из Продолжить серию",
|
||||||
"ButtonReScan": "Пересканировать",
|
"ButtonReScan": "Пересканировать",
|
||||||
"ButtonReset": "Сбросить",
|
"ButtonReset": "Сбросить",
|
||||||
@@ -197,6 +198,7 @@
|
|||||||
"LabelComplete": "Завершить",
|
"LabelComplete": "Завершить",
|
||||||
"LabelConfirmPassword": "Подтвердить пароль",
|
"LabelConfirmPassword": "Подтвердить пароль",
|
||||||
"LabelContinueListening": "Продолжить слушать",
|
"LabelContinueListening": "Продолжить слушать",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Продолжить серию",
|
"LabelContinueSeries": "Продолжить серию",
|
||||||
"LabelCover": "Обложка",
|
"LabelCover": "Обложка",
|
||||||
"LabelCoverImageURL": "URL изображения обложки",
|
"LabelCoverImageURL": "URL изображения обложки",
|
||||||
@@ -331,6 +333,7 @@
|
|||||||
"LabelPubDate": "Дата публикации",
|
"LabelPubDate": "Дата публикации",
|
||||||
"LabelPublisher": "Издатель",
|
"LabelPublisher": "Издатель",
|
||||||
"LabelPublishYear": "Год публикации",
|
"LabelPublishYear": "Год публикации",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Недавно добавленные",
|
"LabelRecentlyAdded": "Недавно добавленные",
|
||||||
"LabelRecentSeries": "Последние серии",
|
"LabelRecentSeries": "Последние серии",
|
||||||
"LabelRecommended": "Рекомендованное",
|
"LabelRecommended": "Рекомендованное",
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
"ButtonRemoveAll": "移除所有",
|
"ButtonRemoveAll": "移除所有",
|
||||||
"ButtonRemoveAllLibraryItems": "移除所有媒体库项目",
|
"ButtonRemoveAllLibraryItems": "移除所有媒体库项目",
|
||||||
"ButtonRemoveFromContinueListening": "从继续收听中删除",
|
"ButtonRemoveFromContinueListening": "从继续收听中删除",
|
||||||
|
"ButtonRemoveFromContinueReading": "Remove from Continue Reading",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "从继续收听系列中删除",
|
"ButtonRemoveSeriesFromContinueSeries": "从继续收听系列中删除",
|
||||||
"ButtonReScan": "重新扫描",
|
"ButtonReScan": "重新扫描",
|
||||||
"ButtonReset": "重置",
|
"ButtonReset": "重置",
|
||||||
@@ -197,6 +198,7 @@
|
|||||||
"LabelComplete": "已完成",
|
"LabelComplete": "已完成",
|
||||||
"LabelConfirmPassword": "确认密码",
|
"LabelConfirmPassword": "确认密码",
|
||||||
"LabelContinueListening": "继续收听",
|
"LabelContinueListening": "继续收听",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "继续收听系列",
|
"LabelContinueSeries": "继续收听系列",
|
||||||
"LabelCover": "封面",
|
"LabelCover": "封面",
|
||||||
"LabelCoverImageURL": "封面图像 URL",
|
"LabelCoverImageURL": "封面图像 URL",
|
||||||
@@ -331,6 +333,7 @@
|
|||||||
"LabelPubDate": "出版日期",
|
"LabelPubDate": "出版日期",
|
||||||
"LabelPublisher": "出版商",
|
"LabelPublisher": "出版商",
|
||||||
"LabelPublishYear": "发布年份",
|
"LabelPublishYear": "发布年份",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "最近添加",
|
"LabelRecentlyAdded": "最近添加",
|
||||||
"LabelRecentSeries": "最近添加系列",
|
"LabelRecentSeries": "最近添加系列",
|
||||||
"LabelRecommended": "推荐内容",
|
"LabelRecommended": "推荐内容",
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.2.20",
|
"version": "2.2.21",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.2.20",
|
"version": "2.2.21",
|
||||||
"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.20",
|
"version": "2.2.21",
|
||||||
"description": "Self-hosted audiobook and podcast server",
|
"description": "Self-hosted audiobook and podcast server",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+1
-1
@@ -75,7 +75,7 @@ class Server {
|
|||||||
this.audioMetadataManager = new AudioMetadataMangaer(this.db, this.taskManager)
|
this.audioMetadataManager = new AudioMetadataMangaer(this.db, this.taskManager)
|
||||||
this.rssFeedManager = new RssFeedManager(this.db)
|
this.rssFeedManager = new RssFeedManager(this.db)
|
||||||
|
|
||||||
this.scanner = new Scanner(this.db, this.coverManager)
|
this.scanner = new Scanner(this.db, this.coverManager, this.taskManager)
|
||||||
this.cronManager = new CronManager(this.db, this.scanner, this.podcastManager)
|
this.cronManager = new CronManager(this.db, this.scanner, this.podcastManager)
|
||||||
|
|
||||||
// Routers
|
// Routers
|
||||||
|
|||||||
@@ -1,27 +1,50 @@
|
|||||||
const Logger = require('../Logger')
|
|
||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
|
const Logger = require('../Logger')
|
||||||
|
const fs = require('../libs/fsExtra')
|
||||||
|
|
||||||
class FileSystemController {
|
class FileSystemController {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
async getPaths(req, res) {
|
async getPaths(req, res) {
|
||||||
var excludedDirs = ['node_modules', 'client', 'server', '.git', 'static', 'build', 'dist', 'metadata', 'config', 'sys', 'proc'].map(dirname => {
|
if (!req.user.isAdminOrUp) {
|
||||||
|
Logger.error(`[FileSystemController] Non-admin user attempting to get filesystem paths`, req.user)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
|
const excludedDirs = ['node_modules', 'client', 'server', '.git', 'static', 'build', 'dist', 'metadata', 'config', 'sys', 'proc'].map(dirname => {
|
||||||
return Path.sep + dirname
|
return Path.sep + dirname
|
||||||
})
|
})
|
||||||
|
|
||||||
// Do not include existing mapped library paths in response
|
// Do not include existing mapped library paths in response
|
||||||
this.db.libraries.forEach(lib => {
|
this.db.libraries.forEach(lib => {
|
||||||
lib.folders.forEach((folder) => {
|
lib.folders.forEach((folder) => {
|
||||||
var dir = folder.fullPath
|
let dir = folder.fullPath
|
||||||
if (dir.includes(global.appRoot)) dir = dir.replace(global.appRoot, '')
|
if (dir.includes(global.appRoot)) dir = dir.replace(global.appRoot, '')
|
||||||
excludedDirs.push(dir)
|
excludedDirs.push(dir)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
Logger.debug(`[Server] get file system paths, excluded: ${excludedDirs.join(', ')}`)
|
|
||||||
res.json({
|
res.json({
|
||||||
directories: await this.getDirectories(global.appRoot, '/', excludedDirs)
|
directories: await this.getDirectories(global.appRoot, '/', excludedDirs)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// POST: api/filesystem/pathexists
|
||||||
|
async checkPathExists(req, res) {
|
||||||
|
if (!req.user.canUpload) {
|
||||||
|
Logger.error(`[FileSystemController] Non-admin user attempting to check path exists`, req.user)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
|
const filepath = req.body.filepath
|
||||||
|
if (!filepath?.length) {
|
||||||
|
return res.sendStatus(400)
|
||||||
|
}
|
||||||
|
|
||||||
|
const exists = await fs.pathExists(filepath)
|
||||||
|
res.json({
|
||||||
|
exists
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = new FileSystemController()
|
module.exports = new FileSystemController()
|
||||||
@@ -709,9 +709,11 @@ class LibraryController {
|
|||||||
async getNarrators(req, res) {
|
async getNarrators(req, res) {
|
||||||
const narrators = {}
|
const narrators = {}
|
||||||
req.libraryItems.forEach((li) => {
|
req.libraryItems.forEach((li) => {
|
||||||
if (li.media.metadata.narrators && li.media.metadata.narrators.length) {
|
if (li.media.metadata.narrators?.length) {
|
||||||
li.media.metadata.narrators.forEach((n) => {
|
li.media.metadata.narrators.forEach((n) => {
|
||||||
if (!narrators[n]) {
|
if (typeof n !== 'string') {
|
||||||
|
Logger.error(`[LibraryController] getNarrators: Invalid narrator "${n}" on book "${li.media.metadata.title}"`)
|
||||||
|
} else if (!narrators[n]) {
|
||||||
narrators[n] = {
|
narrators[n] = {
|
||||||
id: encodeURIComponent(Buffer.from(n).toString('base64')),
|
id: encodeURIComponent(Buffer.from(n).toString('base64')),
|
||||||
name: n,
|
name: n,
|
||||||
|
|||||||
@@ -379,20 +379,23 @@ class LibraryItemController {
|
|||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
|
|
||||||
var itemsUpdated = 0
|
let itemsUpdated = 0
|
||||||
var itemsUnmatched = 0
|
let itemsUnmatched = 0
|
||||||
|
|
||||||
var matchData = req.body
|
const options = req.body.options || {}
|
||||||
var options = matchData.options || {}
|
if (!req.body.libraryItemIds?.length) {
|
||||||
var items = matchData.libraryItemIds
|
return res.sendStatus(400)
|
||||||
if (!items || !items.length) {
|
|
||||||
return res.sendStatus(500)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const libraryItems = req.body.libraryItemIds.map(lid => this.db.getLibraryItem(lid)).filter(li => li)
|
||||||
|
if (!libraryItems?.length) {
|
||||||
|
return res.sendStatus(400)
|
||||||
|
}
|
||||||
|
|
||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
for (const libraryItem of libraryItems) {
|
||||||
var libraryItem = this.db.libraryItems.find(_li => _li.id === items[i])
|
const matchResult = await this.scanner.quickMatchLibraryItem(libraryItem, options)
|
||||||
var matchResult = await this.scanner.quickMatchLibraryItem(libraryItem, options)
|
|
||||||
if (matchResult.updated) {
|
if (matchResult.updated) {
|
||||||
itemsUpdated++
|
itemsUpdated++
|
||||||
} else if (matchResult.warning) {
|
} else if (matchResult.warning) {
|
||||||
@@ -400,7 +403,7 @@ class LibraryItemController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = {
|
const result = {
|
||||||
success: itemsUpdated > 0,
|
success: itemsUpdated > 0,
|
||||||
updates: itemsUpdated,
|
updates: itemsUpdated,
|
||||||
unmatched: itemsUnmatched
|
unmatched: itemsUnmatched
|
||||||
@@ -408,6 +411,33 @@ class LibraryItemController {
|
|||||||
SocketAuthority.clientEmitter(req.user.id, 'batch_quickmatch_complete', result)
|
SocketAuthority.clientEmitter(req.user.id, 'batch_quickmatch_complete', result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// POST: api/items/batch/scan
|
||||||
|
async batchScan(req, res) {
|
||||||
|
if (!req.user.isAdminOrUp) {
|
||||||
|
Logger.warn('User other than admin attempted to batch scan library items', req.user)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.body.libraryItemIds?.length) {
|
||||||
|
return res.sendStatus(400)
|
||||||
|
}
|
||||||
|
|
||||||
|
const libraryItems = req.body.libraryItemIds.map(lid => this.db.getLibraryItem(lid)).filter(li => li)
|
||||||
|
if (!libraryItems?.length) {
|
||||||
|
return res.sendStatus(400)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.sendStatus(200)
|
||||||
|
|
||||||
|
for (const libraryItem of libraryItems) {
|
||||||
|
if (libraryItem.isFile) {
|
||||||
|
Logger.warn(`[LibraryItemController] Re-scanning file library items not yet supported`)
|
||||||
|
} else {
|
||||||
|
await this.scanner.scanLibraryItemByRequest(libraryItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// DELETE: api/items/all
|
// DELETE: api/items/all
|
||||||
async deleteAll(req, res) {
|
async deleteAll(req, res) {
|
||||||
if (!req.user.isAdminOrUp) {
|
if (!req.user.isAdminOrUp) {
|
||||||
@@ -432,7 +462,7 @@ class LibraryItemController {
|
|||||||
return res.sendStatus(500)
|
return res.sendStatus(500)
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await this.scanner.scanLibraryItemById(req.libraryItem.id)
|
const result = await this.scanner.scanLibraryItemByRequest(req.libraryItem)
|
||||||
res.json({
|
res.json({
|
||||||
result: Object.keys(ScanResult).find(key => ScanResult[key] == result)
|
result: Object.keys(ScanResult).find(key => ScanResult[key] == result)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -48,8 +48,7 @@ class MeController {
|
|||||||
|
|
||||||
// DELETE: api/me/progress/:id
|
// DELETE: api/me/progress/:id
|
||||||
async removeMediaProgress(req, res) {
|
async removeMediaProgress(req, res) {
|
||||||
var wasRemoved = req.user.removeMediaProgress(req.params.id)
|
if (!req.user.removeMediaProgress(req.params.id)) {
|
||||||
if (!wasRemoved) {
|
|
||||||
return res.sendStatus(200)
|
return res.sendStatus(200)
|
||||||
}
|
}
|
||||||
await this.db.updateEntity('user', req.user)
|
await this.db.updateEntity('user', req.user)
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ class BookFinder {
|
|||||||
this.fantLab = new FantLab()
|
this.fantLab = new FantLab()
|
||||||
this.audiobookCovers = new AudiobookCovers()
|
this.audiobookCovers = new AudiobookCovers()
|
||||||
|
|
||||||
|
this.providers = ['google', 'itunes', 'openlibrary', 'fantlab', 'audiobookcovers', 'audible', 'audible.ca', 'audible.uk', 'audible.au', 'audible.fr', 'audible.de', 'audible.jp', 'audible.it', 'audible.in', 'audible.es']
|
||||||
|
|
||||||
this.verbose = false
|
this.verbose = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,7 +185,7 @@ class BookFinder {
|
|||||||
var books = []
|
var books = []
|
||||||
var maxTitleDistance = !isNaN(options.titleDistance) ? Number(options.titleDistance) : 4
|
var maxTitleDistance = !isNaN(options.titleDistance) ? Number(options.titleDistance) : 4
|
||||||
var maxAuthorDistance = !isNaN(options.authorDistance) ? Number(options.authorDistance) : 4
|
var maxAuthorDistance = !isNaN(options.authorDistance) ? Number(options.authorDistance) : 4
|
||||||
Logger.debug(`Book Search: title: "${title}", author: "${author}", provider: ${provider}`)
|
Logger.debug(`Book Search: title: "${title}", author: "${author || ''}", provider: ${provider}`)
|
||||||
|
|
||||||
if (provider === 'google') {
|
if (provider === 'google') {
|
||||||
books = await this.getGoogleBooksResults(title, author)
|
books = await this.getGoogleBooksResults(title, author)
|
||||||
@@ -222,19 +224,29 @@ class BookFinder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async findCovers(provider, title, author, options = {}) {
|
async findCovers(provider, title, author, options = {}) {
|
||||||
var searchResults = await this.search(provider, title, author, options)
|
let searchResults = []
|
||||||
|
|
||||||
|
if (provider === 'all') {
|
||||||
|
for (const providerString of this.providers) {
|
||||||
|
const providerResults = await this.search(providerString, title, author, options)
|
||||||
|
Logger.debug(`[BookFinder] Found ${providerResults.length} covers from ${providerString}`)
|
||||||
|
searchResults.push(...providerResults)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
searchResults = await this.search(provider, title, author, options)
|
||||||
|
}
|
||||||
Logger.debug(`[BookFinder] FindCovers search results: ${searchResults.length}`)
|
Logger.debug(`[BookFinder] FindCovers search results: ${searchResults.length}`)
|
||||||
|
|
||||||
var covers = []
|
const covers = []
|
||||||
searchResults.forEach((result) => {
|
searchResults.forEach((result) => {
|
||||||
if (result.covers && result.covers.length) {
|
if (result.covers && result.covers.length) {
|
||||||
covers = covers.concat(result.covers)
|
covers.push(...result.covers)
|
||||||
}
|
}
|
||||||
if (result.cover) {
|
if (result.cover) {
|
||||||
covers.push(result.cover)
|
covers.push(result.cover)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return covers
|
return [...(new Set(covers))]
|
||||||
}
|
}
|
||||||
|
|
||||||
findChapters(asin, region) {
|
findChapters(asin, region) {
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class AbMergeManager {
|
|||||||
toneJsonObject: null
|
toneJsonObject: null
|
||||||
}
|
}
|
||||||
const taskDescription = `Encoding audiobook "${libraryItem.media.metadata.title}" into a single m4b file.`
|
const taskDescription = `Encoding audiobook "${libraryItem.media.metadata.title}" into a single m4b file.`
|
||||||
task.setData('encode-m4b', 'Encoding M4b', taskDescription, taskData)
|
task.setData('encode-m4b', 'Encoding M4b', taskDescription, false, taskData)
|
||||||
this.taskManager.addTask(task)
|
this.taskManager.addTask(task)
|
||||||
Logger.info(`Start m4b encode for ${libraryItem.id} - TaskId: ${task.id}`)
|
Logger.info(`Start m4b encode for ${libraryItem.id} - TaskId: ${task.id}`)
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ class AbMergeManager {
|
|||||||
let toneJsonPath = null
|
let toneJsonPath = null
|
||||||
try {
|
try {
|
||||||
toneJsonPath = Path.join(task.data.itemCachePath, 'metadata.json')
|
toneJsonPath = Path.join(task.data.itemCachePath, 'metadata.json')
|
||||||
await toneHelpers.writeToneMetadataJsonFile(libraryItem, libraryItem.media.chapters, toneJsonPath, 1)
|
await toneHelpers.writeToneMetadataJsonFile(libraryItem, libraryItem.media.chapters, toneJsonPath, 1, 'audio/mp4')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(`[AbMergeManager] Write metadata.json failed`, error)
|
Logger.error(`[AbMergeManager] Write metadata.json failed`, error)
|
||||||
toneJsonPath = null
|
toneJsonPath = null
|
||||||
|
|||||||
@@ -34,7 +34,10 @@ class AudioMetadataMangaer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getToneMetadataObjectForApi(libraryItem) {
|
getToneMetadataObjectForApi(libraryItem) {
|
||||||
return toneHelpers.getToneMetadataObject(libraryItem, libraryItem.media.chapters, libraryItem.media.tracks.length)
|
const audioFiles = libraryItem.media.includedAudioFiles
|
||||||
|
let mimeType = audioFiles[0].mimeType
|
||||||
|
if (audioFiles.some(a => a.mimeType !== mimeType)) mimeType = null
|
||||||
|
return toneHelpers.getToneMetadataObject(libraryItem, libraryItem.media.chapters, libraryItem.media.tracks.length, mimeType)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleBatchEmbed(user, libraryItems, options = {}) {
|
handleBatchEmbed(user, libraryItems, options = {}) {
|
||||||
@@ -56,6 +59,9 @@ class AudioMetadataMangaer {
|
|||||||
// Only writing chapters for single file audiobooks
|
// Only writing chapters for single file audiobooks
|
||||||
const chapters = (audioFiles.length == 1 || forceEmbedChapters) ? libraryItem.media.chapters.map(c => ({ ...c })) : null
|
const chapters = (audioFiles.length == 1 || forceEmbedChapters) ? libraryItem.media.chapters.map(c => ({ ...c })) : null
|
||||||
|
|
||||||
|
let mimeType = audioFiles[0].mimeType
|
||||||
|
if (audioFiles.some(a => a.mimeType !== mimeType)) mimeType = null
|
||||||
|
|
||||||
// Create task
|
// Create task
|
||||||
const taskData = {
|
const taskData = {
|
||||||
libraryItemId: libraryItem.id,
|
libraryItemId: libraryItem.id,
|
||||||
@@ -71,7 +77,7 @@ class AudioMetadataMangaer {
|
|||||||
}
|
}
|
||||||
)),
|
)),
|
||||||
coverPath: libraryItem.media.coverPath,
|
coverPath: libraryItem.media.coverPath,
|
||||||
metadataObject: toneHelpers.getToneMetadataObject(libraryItem, chapters, audioFiles.length),
|
metadataObject: toneHelpers.getToneMetadataObject(libraryItem, chapters, audioFiles.length, mimeType),
|
||||||
itemCachePath,
|
itemCachePath,
|
||||||
chapters,
|
chapters,
|
||||||
options: {
|
options: {
|
||||||
@@ -80,7 +86,7 @@ class AudioMetadataMangaer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const taskDescription = `Embedding metadata in audiobook "${libraryItem.media.metadata.title}".`
|
const taskDescription = `Embedding metadata in audiobook "${libraryItem.media.metadata.title}".`
|
||||||
task.setData('embed-metadata', 'Embedding Metadata', taskDescription, taskData)
|
task.setData('embed-metadata', 'Embedding Metadata', taskDescription, false, taskData)
|
||||||
|
|
||||||
if (this.tasksRunning.length >= this.MAX_CONCURRENT_TASKS) {
|
if (this.tasksRunning.length >= this.MAX_CONCURRENT_TASKS) {
|
||||||
Logger.info(`[AudioMetadataManager] Queueing embed metadata for audiobook "${libraryItem.media.metadata.title}"`)
|
Logger.info(`[AudioMetadataManager] Queueing embed metadata for audiobook "${libraryItem.media.metadata.title}"`)
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ class CronManager {
|
|||||||
for (const libraryItem of libraryItems) {
|
for (const libraryItem of libraryItems) {
|
||||||
const keepAutoDownloading = await this.podcastManager.runEpisodeCheck(libraryItem)
|
const keepAutoDownloading = await this.podcastManager.runEpisodeCheck(libraryItem)
|
||||||
if (!keepAutoDownloading) { // auto download was disabled
|
if (!keepAutoDownloading) { // auto download was disabled
|
||||||
podcastCron.libraryItemIds = podcastCron.libraryItemIds.filter(lid => lid !== libraryItemId) // Filter it out
|
podcastCron.libraryItemIds = podcastCron.libraryItemIds.filter(lid => lid !== libraryItem.id) // Filter it out
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -130,6 +130,8 @@ class PlaybackSessionManager {
|
|||||||
const itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId)
|
const itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId)
|
||||||
SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', {
|
SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', {
|
||||||
id: itemProgress.id,
|
id: itemProgress.id,
|
||||||
|
sessionId: session.id,
|
||||||
|
deviceDescription: session.deviceDescription,
|
||||||
data: itemProgress.toJSON()
|
data: itemProgress.toJSON()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -239,6 +241,8 @@ class PlaybackSessionManager {
|
|||||||
const itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId)
|
const itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId)
|
||||||
SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', {
|
SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', {
|
||||||
id: itemProgress.id,
|
id: itemProgress.id,
|
||||||
|
sessionId: session.id,
|
||||||
|
deviceDescription: session.deviceDescription,
|
||||||
data: itemProgress.toJSON()
|
data: itemProgress.toJSON()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -306,7 +310,7 @@ class PlaybackSessionManager {
|
|||||||
// See https://github.com/advplyr/audiobookshelf/issues/868
|
// See https://github.com/advplyr/audiobookshelf/issues/868
|
||||||
// Remove playback sessions with listening time too high
|
// Remove playback sessions with listening time too high
|
||||||
async removeInvalidSessions() {
|
async removeInvalidSessions() {
|
||||||
const selectFunc = (session) => isNaN(session.timeListening) || Number(session.timeListening) > 3600000000
|
const selectFunc = (session) => isNaN(session.timeListening) || Number(session.timeListening) > 36000000
|
||||||
const numSessionsRemoved = await this.db.removeEntities('session', selectFunc, true)
|
const numSessionsRemoved = await this.db.removeEntities('session', selectFunc, true)
|
||||||
if (numSessionsRemoved) {
|
if (numSessionsRemoved) {
|
||||||
Logger.info(`[PlaybackSessionManager] Removed ${numSessionsRemoved} invalid playback sessions`)
|
Logger.info(`[PlaybackSessionManager] Removed ${numSessionsRemoved} invalid playback sessions`)
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ class PodcastManager {
|
|||||||
libraryId: podcastEpisodeDownload.libraryId,
|
libraryId: podcastEpisodeDownload.libraryId,
|
||||||
libraryItemId: podcastEpisodeDownload.libraryItemId,
|
libraryItemId: podcastEpisodeDownload.libraryItemId,
|
||||||
}
|
}
|
||||||
task.setData('download-podcast-episode', 'Downloading Episode', taskDescription, taskData)
|
task.setData('download-podcast-episode', 'Downloading Episode', taskDescription, false, taskData)
|
||||||
this.taskManager.addTask(task)
|
this.taskManager.addTask(task)
|
||||||
|
|
||||||
SocketAuthority.emitter('episode_download_started', podcastEpisodeDownload.toJSONForClient())
|
SocketAuthority.emitter('episode_download_started', podcastEpisodeDownload.toJSONForClient())
|
||||||
@@ -140,8 +140,6 @@ class PodcastManager {
|
|||||||
async scanAddPodcastEpisodeAudioFile() {
|
async scanAddPodcastEpisodeAudioFile() {
|
||||||
const libraryFile = await this.getLibraryFile(this.currentDownload.targetPath, this.currentDownload.targetRelPath)
|
const libraryFile = await this.getLibraryFile(this.currentDownload.targetPath, this.currentDownload.targetRelPath)
|
||||||
|
|
||||||
// TODO: Set meta tags on new audio file
|
|
||||||
|
|
||||||
const audioFile = await this.probeAudioFile(libraryFile)
|
const audioFile = await this.probeAudioFile(libraryFile)
|
||||||
if (!audioFile) {
|
if (!audioFile) {
|
||||||
return false
|
return false
|
||||||
@@ -178,6 +176,9 @@ class PodcastManager {
|
|||||||
libraryItem.updatedAt = Date.now()
|
libraryItem.updatedAt = Date.now()
|
||||||
await this.db.updateLibraryItem(libraryItem)
|
await this.db.updateLibraryItem(libraryItem)
|
||||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||||
|
const podcastEpisodeExpanded = podcastEpisode.toJSONExpanded()
|
||||||
|
podcastEpisodeExpanded.libraryItem = libraryItem.toJSONExpanded()
|
||||||
|
SocketAuthority.emitter('episode_added', podcastEpisodeExpanded)
|
||||||
|
|
||||||
if (this.currentDownload.isAutoDownload) { // Notifications only for auto downloaded episodes
|
if (this.currentDownload.isAutoDownload) { // Notifications only for auto downloaded episodes
|
||||||
this.notificationManager.onPodcastEpisodeDownloaded(libraryItem, podcastEpisode)
|
this.notificationManager.onPodcastEpisodeDownloaded(libraryItem, podcastEpisode)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const Podcast = require('./mediaTypes/Podcast')
|
|||||||
const Video = require('./mediaTypes/Video')
|
const Video = require('./mediaTypes/Video')
|
||||||
const Music = require('./mediaTypes/Music')
|
const Music = require('./mediaTypes/Music')
|
||||||
const { areEquivalent, copyValue, getId, cleanStringForSearch } = require('../utils/index')
|
const { areEquivalent, copyValue, getId, cleanStringForSearch } = require('../utils/index')
|
||||||
|
const { filePathToPOSIX } = require('../utils/fileUtils')
|
||||||
|
|
||||||
class LibraryItem {
|
class LibraryItem {
|
||||||
constructor(libraryItem = null) {
|
constructor(libraryItem = null) {
|
||||||
@@ -368,7 +369,7 @@ class LibraryItem {
|
|||||||
const fileFoundCheck = this.checkFileFound(lf, true)
|
const fileFoundCheck = this.checkFileFound(lf, true)
|
||||||
if (fileFoundCheck === null) {
|
if (fileFoundCheck === null) {
|
||||||
newLibraryFiles.push(lf)
|
newLibraryFiles.push(lf)
|
||||||
} else if (fileFoundCheck && lf.metadata.format !== 'abs') { // Ignore abs file updates
|
} else if (fileFoundCheck && lf.metadata.format !== 'abs' && lf.metadata.filename !== 'metadata.json') { // Ignore abs file updates
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
existingLibraryFiles.push(lf)
|
existingLibraryFiles.push(lf)
|
||||||
} else {
|
} else {
|
||||||
@@ -499,14 +500,56 @@ class LibraryItem {
|
|||||||
// Make sure metadata book dir exists
|
// Make sure metadata book dir exists
|
||||||
await fs.ensureDir(metadataPath)
|
await fs.ensureDir(metadataPath)
|
||||||
}
|
}
|
||||||
metadataPath = Path.join(metadataPath, 'metadata.abs')
|
|
||||||
|
|
||||||
return abmetadataGenerator.generate(this, metadataPath).then((success) => {
|
const metadataFileFormat = global.ServerSettings.metadataFileFormat
|
||||||
this.isSavingMetadata = false
|
const metadataFilePath = Path.join(metadataPath, `metadata.${metadataFileFormat}`)
|
||||||
if (!success) Logger.error(`[LibraryItem] Failed saving abmetadata to "${metadataPath}"`)
|
if (metadataFileFormat === 'json') {
|
||||||
else Logger.debug(`[LibraryItem] Success saving abmetadata to "${metadataPath}"`)
|
// Remove metadata.abs if it exists
|
||||||
return success
|
if (await fs.pathExists(Path.join(metadataPath, `metadata.abs`))) {
|
||||||
})
|
Logger.debug(`[LibraryItem] Removing metadata.abs for item "${this.media.metadata.title}"`)
|
||||||
|
await fs.remove(Path.join(metadataPath, `metadata.abs`))
|
||||||
|
this.libraryFiles = this.libraryFiles.filter(lf => lf.metadata.path !== filePathToPOSIX(Path.join(metadataPath, `metadata.abs`)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs.writeFile(metadataFilePath, JSON.stringify(this.media.toJSONForMetadataFile(), null, 2)).then(async () => {
|
||||||
|
this.isSavingMetadata = false
|
||||||
|
// Add metadata.json to libraryFiles array if it is new
|
||||||
|
if (global.ServerSettings.storeMetadataWithItem && !this.libraryFiles.some(lf => lf.metadata.path === filePathToPOSIX(metadataFilePath))) {
|
||||||
|
const newLibraryFile = new LibraryFile()
|
||||||
|
await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`)
|
||||||
|
this.libraryFiles.push(newLibraryFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}).catch((error) => {
|
||||||
|
this.isSavingMetadata = false
|
||||||
|
Logger.error(`[LibraryItem] Failed to save json file at "${metadataFilePath}"`, error)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Remove metadata.json if it exists
|
||||||
|
if (await fs.pathExists(Path.join(metadataPath, `metadata.json`))) {
|
||||||
|
Logger.debug(`[LibraryItem] Removing metadata.json for item "${this.media.metadata.title}"`)
|
||||||
|
await fs.remove(Path.join(metadataPath, `metadata.json`))
|
||||||
|
this.libraryFiles = this.libraryFiles.filter(lf => lf.metadata.path !== filePathToPOSIX(Path.join(metadataPath, `metadata.json`)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return abmetadataGenerator.generate(this, metadataFilePath).then(async (success) => {
|
||||||
|
this.isSavingMetadata = false
|
||||||
|
if (!success) Logger.error(`[LibraryItem] Failed saving abmetadata to "${metadataFilePath}"`)
|
||||||
|
else {
|
||||||
|
// Add metadata.abs to libraryFiles array if it is new
|
||||||
|
if (global.ServerSettings.storeMetadataWithItem && !this.libraryFiles.some(lf => lf.metadata.path === filePathToPOSIX(metadataFilePath))) {
|
||||||
|
const newLibraryFile = new LibraryFile()
|
||||||
|
await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.abs`)
|
||||||
|
this.libraryFiles.push(newLibraryFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.debug(`[LibraryItem] Success saving abmetadata to "${metadataFilePath}"`)
|
||||||
|
}
|
||||||
|
return success
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
removeLibraryFile(ino) {
|
removeLibraryFile(ino) {
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ class PlaybackSession {
|
|||||||
this.episodeId = episodeId
|
this.episodeId = episodeId
|
||||||
this.mediaType = libraryItem.mediaType
|
this.mediaType = libraryItem.mediaType
|
||||||
this.mediaMetadata = libraryItem.media.metadata.clone()
|
this.mediaMetadata = libraryItem.media.metadata.clone()
|
||||||
this.chapters = (libraryItem.media.chapters || []).map(c => ({ ...c })) // Only book mediaType has chapters
|
this.chapters = libraryItem.media.getChapters(episodeId)
|
||||||
this.displayTitle = libraryItem.media.getPlaybackTitle(episodeId)
|
this.displayTitle = libraryItem.media.getPlaybackTitle(episodeId)
|
||||||
this.displayAuthor = libraryItem.media.getPlaybackAuthor()
|
this.displayAuthor = libraryItem.media.getPlaybackAuthor()
|
||||||
this.coverPath = libraryItem.media.coverPath
|
this.coverPath = libraryItem.media.coverPath
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class Task {
|
|||||||
this.title = null
|
this.title = null
|
||||||
this.description = null
|
this.description = null
|
||||||
this.error = null
|
this.error = null
|
||||||
|
this.showSuccess = false // If true client side should keep the task visible after success
|
||||||
|
|
||||||
this.isFailed = false
|
this.isFailed = false
|
||||||
this.isFinished = false
|
this.isFinished = false
|
||||||
@@ -25,6 +26,7 @@ class Task {
|
|||||||
title: this.title,
|
title: this.title,
|
||||||
description: this.description,
|
description: this.description,
|
||||||
error: this.error,
|
error: this.error,
|
||||||
|
showSuccess: this.showSuccess,
|
||||||
isFailed: this.isFailed,
|
isFailed: this.isFailed,
|
||||||
isFinished: this.isFinished,
|
isFinished: this.isFinished,
|
||||||
startedAt: this.startedAt,
|
startedAt: this.startedAt,
|
||||||
@@ -32,12 +34,13 @@ class Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setData(action, title, description, data = {}) {
|
setData(action, title, description, showSuccess, data = {}) {
|
||||||
this.id = getId(action)
|
this.id = getId(action)
|
||||||
this.action = action
|
this.action = action
|
||||||
this.data = { ...data }
|
this.data = { ...data }
|
||||||
this.title = title
|
this.title = title
|
||||||
this.description = description
|
this.description = description
|
||||||
|
this.showSuccess = showSuccess
|
||||||
this.startedAt = Date.now()
|
this.startedAt = Date.now()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,7 +51,10 @@ class Task {
|
|||||||
this.setFinished()
|
this.setFinished()
|
||||||
}
|
}
|
||||||
|
|
||||||
setFinished() {
|
setFinished(newDescription = null) {
|
||||||
|
if (newDescription) {
|
||||||
|
this.description = newDescription
|
||||||
|
}
|
||||||
this.isFinished = true
|
this.isFinished = true
|
||||||
this.finishedAt = Date.now()
|
this.finishedAt = Date.now()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const { getFileTimestampsWithIno } = require('../../utils/fileUtils')
|
const { getFileTimestampsWithIno, filePathToPOSIX } = require('../../utils/fileUtils')
|
||||||
const globals = require('../../utils/globals')
|
const globals = require('../../utils/globals')
|
||||||
const FileMetadata = require('../metadata/FileMetadata')
|
const FileMetadata = require('../metadata/FileMetadata')
|
||||||
|
|
||||||
@@ -59,8 +59,8 @@ class LibraryFile {
|
|||||||
var fileMetadata = new FileMetadata()
|
var fileMetadata = new FileMetadata()
|
||||||
fileMetadata.setData(fileTsData)
|
fileMetadata.setData(fileTsData)
|
||||||
fileMetadata.filename = Path.basename(relPath)
|
fileMetadata.filename = Path.basename(relPath)
|
||||||
fileMetadata.path = path
|
fileMetadata.path = filePathToPOSIX(path)
|
||||||
fileMetadata.relPath = relPath
|
fileMetadata.relPath = filePathToPOSIX(relPath)
|
||||||
fileMetadata.ext = Path.extname(relPath)
|
fileMetadata.ext = Path.extname(relPath)
|
||||||
this.ino = fileTsData.ino
|
this.ino = fileTsData.ino
|
||||||
this.metadata = fileMetadata
|
this.metadata = fileMetadata
|
||||||
|
|||||||
@@ -89,6 +89,14 @@ class Book {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toJSONForMetadataFile() {
|
||||||
|
return {
|
||||||
|
tags: [...this.tags],
|
||||||
|
chapters: this.chapters.map(c => ({ ...c })),
|
||||||
|
metadata: this.metadata.toJSONForMetadataFile()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get size() {
|
get size() {
|
||||||
var total = 0
|
var total = 0
|
||||||
this.audioFiles.forEach((af) => total += af.metadata.size)
|
this.audioFiles.forEach((af) => total += af.metadata.size)
|
||||||
@@ -134,6 +142,9 @@ class Book {
|
|||||||
get numTracks() {
|
get numTracks() {
|
||||||
return this.tracks.length
|
return this.tracks.length
|
||||||
}
|
}
|
||||||
|
get isEBookOnly() {
|
||||||
|
return this.ebookFile && !this.numTracks
|
||||||
|
}
|
||||||
|
|
||||||
update(payload) {
|
update(payload) {
|
||||||
const json = this.toJSON()
|
const json = this.toJSON()
|
||||||
@@ -229,7 +240,7 @@ class Book {
|
|||||||
// Look for desc.txt, reader.txt, metadata.abs and opf file then update details if found
|
// Look for desc.txt, reader.txt, metadata.abs and opf file then update details if found
|
||||||
async syncMetadataFiles(textMetadataFiles, opfMetadataOverrideDetails) {
|
async syncMetadataFiles(textMetadataFiles, opfMetadataOverrideDetails) {
|
||||||
let metadataUpdatePayload = {}
|
let metadataUpdatePayload = {}
|
||||||
let tagsUpdated = false
|
let hasUpdated = false
|
||||||
|
|
||||||
const descTxt = textMetadataFiles.find(lf => lf.metadata.filename === 'desc.txt')
|
const descTxt = textMetadataFiles.find(lf => lf.metadata.filename === 'desc.txt')
|
||||||
if (descTxt) {
|
if (descTxt) {
|
||||||
@@ -248,17 +259,25 @@ class Book {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const metadataIsJSON = global.ServerSettings.metadataFileFormat === 'json'
|
||||||
const metadataAbs = textMetadataFiles.find(lf => lf.metadata.filename === 'metadata.abs')
|
const metadataAbs = textMetadataFiles.find(lf => lf.metadata.filename === 'metadata.abs')
|
||||||
if (metadataAbs) {
|
const metadataJson = textMetadataFiles.find(lf => lf.metadata.filename === 'metadata.json')
|
||||||
Logger.debug(`[Book] Found metadata.abs file for "${this.metadata.title}"`)
|
|
||||||
const metadataText = await readTextFile(metadataAbs.metadata.path)
|
const metadataFile = metadataIsJSON ? metadataJson : metadataAbs
|
||||||
const abmetadataUpdates = abmetadataGenerator.parseAndCheckForUpdates(metadataText, this, 'book')
|
if (metadataFile) {
|
||||||
|
Logger.debug(`[Book] Found ${metadataFile.metadata.filename} file for "${this.metadata.title}"`)
|
||||||
|
const metadataText = await readTextFile(metadataFile.metadata.path)
|
||||||
|
const abmetadataUpdates = abmetadataGenerator.parseAndCheckForUpdates(metadataText, this, 'book', metadataIsJSON)
|
||||||
if (abmetadataUpdates && Object.keys(abmetadataUpdates).length) {
|
if (abmetadataUpdates && Object.keys(abmetadataUpdates).length) {
|
||||||
Logger.debug(`[Book] "${this.metadata.title}" changes found in metadata.abs file`, abmetadataUpdates)
|
Logger.debug(`[Book] "${this.metadata.title}" changes found in metadata.abs file`, abmetadataUpdates)
|
||||||
|
|
||||||
if (abmetadataUpdates.tags) { // Set media tags if updated
|
if (abmetadataUpdates.tags) { // Set media tags if updated
|
||||||
this.tags = abmetadataUpdates.tags
|
this.tags = abmetadataUpdates.tags
|
||||||
tagsUpdated = true
|
hasUpdated = true
|
||||||
|
}
|
||||||
|
if (abmetadataUpdates.chapters) { // Set chapters if updated
|
||||||
|
this.chapters = abmetadataUpdates.chapters
|
||||||
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
if (abmetadataUpdates.metadata) {
|
if (abmetadataUpdates.metadata) {
|
||||||
metadataUpdatePayload = {
|
metadataUpdatePayload = {
|
||||||
@@ -267,6 +286,9 @@ class Book {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (metadataAbs || metadataJson) { // Has different metadata file format so mark as updated
|
||||||
|
Logger.debug(`[Book] Found different format metadata file ${(metadataAbs || metadataJson).metadata.filename}, expecting .${global.ServerSettings.metadataFileFormat} for "${this.metadata.title}"`)
|
||||||
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const metadataOpf = textMetadataFiles.find(lf => lf.isOPFFile || lf.metadata.filename === 'metadata.xml')
|
const metadataOpf = textMetadataFiles.find(lf => lf.isOPFFile || lf.metadata.filename === 'metadata.xml')
|
||||||
@@ -280,7 +302,7 @@ class Book {
|
|||||||
if (key === 'tags') { // Add tags only if tags are empty
|
if (key === 'tags') { // Add tags only if tags are empty
|
||||||
if (opfMetadata.tags.length && (!this.tags.length || opfMetadataOverrideDetails)) {
|
if (opfMetadata.tags.length && (!this.tags.length || opfMetadataOverrideDetails)) {
|
||||||
this.tags = opfMetadata.tags
|
this.tags = opfMetadata.tags
|
||||||
tagsUpdated = true
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
} else if (key === 'genres') { // Add genres only if genres are empty
|
} else if (key === 'genres') { // Add genres only if genres are empty
|
||||||
if (opfMetadata.genres.length && (!this.metadata.genres.length || opfMetadataOverrideDetails)) {
|
if (opfMetadata.genres.length && (!this.metadata.genres.length || opfMetadataOverrideDetails)) {
|
||||||
@@ -312,9 +334,9 @@ class Book {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(metadataUpdatePayload).length) {
|
if (Object.keys(metadataUpdatePayload).length) {
|
||||||
return this.metadata.update(metadataUpdatePayload) || tagsUpdated
|
return this.metadata.update(metadataUpdatePayload) || hasUpdated
|
||||||
}
|
}
|
||||||
return tagsUpdated
|
return hasUpdated
|
||||||
}
|
}
|
||||||
|
|
||||||
searchQuery(query) {
|
searchQuery(query) {
|
||||||
@@ -509,5 +531,9 @@ class Book {
|
|||||||
getPlaybackAuthor() {
|
getPlaybackAuthor() {
|
||||||
return this.metadata.authorName
|
return this.metadata.authorName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getChapters() {
|
||||||
|
return this.chapters?.map(ch => ({ ...ch })) || []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = Book
|
module.exports = Book
|
||||||
|
|||||||
@@ -94,6 +94,13 @@ class Podcast {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toJSONForMetadataFile() {
|
||||||
|
return {
|
||||||
|
tags: [...this.tags],
|
||||||
|
metadata: this.metadata.toJSON()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get size() {
|
get size() {
|
||||||
var total = 0
|
var total = 0
|
||||||
this.episodes.forEach((ep) => total += ep.size)
|
this.episodes.forEach((ep) => total += ep.size)
|
||||||
@@ -199,10 +206,11 @@ class Podcast {
|
|||||||
let metadataUpdatePayload = {}
|
let metadataUpdatePayload = {}
|
||||||
let tagsUpdated = false
|
let tagsUpdated = false
|
||||||
|
|
||||||
const metadataAbs = textMetadataFiles.find(lf => lf.metadata.filename === 'metadata.abs')
|
const metadataAbs = textMetadataFiles.find(lf => lf.metadata.filename === 'metadata.abs' || lf.metadata.filename === 'metadata.json')
|
||||||
if (metadataAbs) {
|
if (metadataAbs) {
|
||||||
|
const isJSON = metadataAbs.metadata.filename === 'metadata.json'
|
||||||
const metadataText = await readTextFile(metadataAbs.metadata.path)
|
const metadataText = await readTextFile(metadataAbs.metadata.path)
|
||||||
const abmetadataUpdates = abmetadataGenerator.parseAndCheckForUpdates(metadataText, this, 'podcast')
|
const abmetadataUpdates = abmetadataGenerator.parseAndCheckForUpdates(metadataText, this, 'podcast', isJSON)
|
||||||
if (abmetadataUpdates && Object.keys(abmetadataUpdates).length) {
|
if (abmetadataUpdates && Object.keys(abmetadataUpdates).length) {
|
||||||
Logger.debug(`[Podcast] "${this.metadata.title}" changes found in metadata.abs file`, abmetadataUpdates)
|
Logger.debug(`[Podcast] "${this.metadata.title}" changes found in metadata.abs file`, abmetadataUpdates)
|
||||||
|
|
||||||
@@ -331,5 +339,9 @@ class Podcast {
|
|||||||
if (!audioFile?.metaTags) return false
|
if (!audioFile?.metaTags) return false
|
||||||
return this.metadata.setDataFromAudioMetaTags(audioFile.metaTags, overrideExistingDetails)
|
return this.metadata.setDataFromAudioMetaTags(audioFile.metaTags, overrideExistingDetails)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getChapters(episodeId) {
|
||||||
|
return this.getEpisode(episodeId)?.chapters?.map(ch => ({ ...ch })) || []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = Podcast
|
module.exports = Podcast
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ class BookMetadata {
|
|||||||
construct(metadata) {
|
construct(metadata) {
|
||||||
this.title = metadata.title
|
this.title = metadata.title
|
||||||
this.subtitle = metadata.subtitle
|
this.subtitle = metadata.subtitle
|
||||||
this.authors = (metadata.authors && metadata.authors.map) ? metadata.authors.map(a => ({ ...a })) : []
|
this.authors = (metadata.authors?.map) ? metadata.authors.map(a => ({ ...a })) : []
|
||||||
this.narrators = metadata.narrators ? [...metadata.narrators] : []
|
this.narrators = metadata.narrators ? [...metadata.narrators].filter(n => n) : []
|
||||||
this.series = (metadata.series && metadata.series.map) ? metadata.series.map(s => ({ ...s })) : []
|
this.series = (metadata.series?.map) ? metadata.series.map(s => ({ ...s })) : []
|
||||||
this.genres = metadata.genres ? [...metadata.genres] : []
|
this.genres = metadata.genres ? [...metadata.genres] : []
|
||||||
this.publishedYear = metadata.publishedYear || null
|
this.publishedYear = metadata.publishedYear || null
|
||||||
this.publishedDate = metadata.publishedDate || null
|
this.publishedDate = metadata.publishedDate || null
|
||||||
@@ -109,6 +109,16 @@ class BookMetadata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toJSONForMetadataFile() {
|
||||||
|
const json = this.toJSON()
|
||||||
|
json.authors = json.authors.map(au => au.name)
|
||||||
|
json.series = json.series.map(se => {
|
||||||
|
if (!se.sequence) return se.name
|
||||||
|
return `${se.name} #${se.sequence}`
|
||||||
|
})
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
|
||||||
clone() {
|
clone() {
|
||||||
return new BookMetadata(this.toJSON())
|
return new BookMetadata(this.toJSON())
|
||||||
}
|
}
|
||||||
@@ -191,8 +201,9 @@ class BookMetadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
update(payload) {
|
update(payload) {
|
||||||
var json = this.toJSON()
|
const json = this.toJSON()
|
||||||
var hasUpdates = false
|
let hasUpdates = false
|
||||||
|
|
||||||
for (const key in json) {
|
for (const key in json) {
|
||||||
if (payload[key] !== undefined) {
|
if (payload[key] !== undefined) {
|
||||||
if (!areEquivalent(payload[key], json[key])) {
|
if (!areEquivalent(payload[key], json[key])) {
|
||||||
@@ -230,7 +241,7 @@ class BookMetadata {
|
|||||||
updateNarrator(oldNarratorName, newNarratorName) {
|
updateNarrator(oldNarratorName, newNarratorName) {
|
||||||
if (!this.hasNarrator(oldNarratorName)) return false
|
if (!this.hasNarrator(oldNarratorName)) return false
|
||||||
this.narrators = this.narrators.filter(n => n !== oldNarratorName)
|
this.narrators = this.narrators.filter(n => n !== oldNarratorName)
|
||||||
if (!this.hasNarrator(newNarratorName)) {
|
if (newNarratorName && !this.hasNarrator(newNarratorName)) {
|
||||||
this.narrators.push(newNarratorName)
|
this.narrators.push(newNarratorName)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -373,8 +384,10 @@ class BookMetadata {
|
|||||||
const parsed = parseNameString.parse(authorsTag)
|
const parsed = parseNameString.parse(authorsTag)
|
||||||
if (!parsed) return []
|
if (!parsed) return []
|
||||||
return (parsed.names || []).map((au) => {
|
return (parsed.names || []).map((au) => {
|
||||||
|
const findAuthor = this.authors.find(_au => _au.name == au)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: `new-${Math.floor(Math.random() * 1000000)}`,
|
id: findAuthor?.id || `new-${Math.floor(Math.random() * 1000000)}`,
|
||||||
name: au
|
name: au
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -411,7 +424,7 @@ class BookMetadata {
|
|||||||
return this.narrators.filter(n => cleanStringForSearch(n).includes(query))
|
return this.narrators.filter(n => cleanStringForSearch(n).includes(query))
|
||||||
}
|
}
|
||||||
searchQuery(query) { // Returns key if match is found
|
searchQuery(query) { // Returns key if match is found
|
||||||
const keysToCheck = ['title', 'asin', 'isbn']
|
const keysToCheck = ['title', 'asin', 'isbn', 'subtitle']
|
||||||
for (const key of keysToCheck) {
|
for (const key of keysToCheck) {
|
||||||
if (this[key] && cleanStringForSearch(String(this[key])).includes(query)) {
|
if (this[key] && cleanStringForSearch(String(this[key])).includes(query)) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
const { BookshelfView } = require('../../utils/constants')
|
const { BookshelfView } = require('../../utils/constants')
|
||||||
const { isNullOrNaN } = require('../../utils')
|
|
||||||
const Logger = require('../../Logger')
|
const Logger = require('../../Logger')
|
||||||
|
|
||||||
class ServerSettings {
|
class ServerSettings {
|
||||||
@@ -21,6 +20,7 @@ class ServerSettings {
|
|||||||
// Metadata - choose to store inside users library item folder
|
// Metadata - choose to store inside users library item folder
|
||||||
this.storeCoverWithItem = false
|
this.storeCoverWithItem = false
|
||||||
this.storeMetadataWithItem = false
|
this.storeMetadataWithItem = false
|
||||||
|
this.metadataFileFormat = 'json'
|
||||||
|
|
||||||
// Security/Rate limits
|
// Security/Rate limits
|
||||||
this.rateLimitLoginRequests = 10
|
this.rateLimitLoginRequests = 10
|
||||||
@@ -77,6 +77,7 @@ class ServerSettings {
|
|||||||
|
|
||||||
this.storeCoverWithItem = !!settings.storeCoverWithItem
|
this.storeCoverWithItem = !!settings.storeCoverWithItem
|
||||||
this.storeMetadataWithItem = !!settings.storeMetadataWithItem
|
this.storeMetadataWithItem = !!settings.storeMetadataWithItem
|
||||||
|
this.metadataFileFormat = settings.metadataFileFormat || 'json'
|
||||||
|
|
||||||
this.rateLimitLoginRequests = !isNaN(settings.rateLimitLoginRequests) ? Number(settings.rateLimitLoginRequests) : 10
|
this.rateLimitLoginRequests = !isNaN(settings.rateLimitLoginRequests) ? Number(settings.rateLimitLoginRequests) : 10
|
||||||
this.rateLimitLoginWindow = !isNaN(settings.rateLimitLoginWindow) ? Number(settings.rateLimitLoginWindow) : 10 * 60 * 1000 // 10 Minutes
|
this.rateLimitLoginWindow = !isNaN(settings.rateLimitLoginWindow) ? Number(settings.rateLimitLoginWindow) : 10 * 60 * 1000 // 10 Minutes
|
||||||
@@ -112,6 +113,16 @@ class ServerSettings {
|
|||||||
if (settings.homeBookshelfView == undefined) { // homeBookshelfView was added in 2.1.3
|
if (settings.homeBookshelfView == undefined) { // homeBookshelfView was added in 2.1.3
|
||||||
this.homeBookshelfView = settings.bookshelfView
|
this.homeBookshelfView = settings.bookshelfView
|
||||||
}
|
}
|
||||||
|
if (settings.metadataFileFormat == undefined) { // metadataFileFormat was added in 2.2.21
|
||||||
|
// All users using old settings will stay abs until changed
|
||||||
|
this.metadataFileFormat = 'abs'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
if (!['abs', 'json'].includes(this.metadataFileFormat)) {
|
||||||
|
Logger.error(`[ServerSettings] construct: Invalid metadataFileFormat ${this.metadataFileFormat}`)
|
||||||
|
this.metadataFileFormat = 'json'
|
||||||
|
}
|
||||||
|
|
||||||
if (this.logLevel !== Logger.logLevel) {
|
if (this.logLevel !== Logger.logLevel) {
|
||||||
Logger.setLogLevel(this.logLevel)
|
Logger.setLogLevel(this.logLevel)
|
||||||
@@ -133,6 +144,7 @@ class ServerSettings {
|
|||||||
scannerUseTone: this.scannerUseTone,
|
scannerUseTone: this.scannerUseTone,
|
||||||
storeCoverWithItem: this.storeCoverWithItem,
|
storeCoverWithItem: this.storeCoverWithItem,
|
||||||
storeMetadataWithItem: this.storeMetadataWithItem,
|
storeMetadataWithItem: this.storeMetadataWithItem,
|
||||||
|
metadataFileFormat: this.metadataFileFormat,
|
||||||
rateLimitLoginRequests: this.rateLimitLoginRequests,
|
rateLimitLoginRequests: this.rateLimitLoginRequests,
|
||||||
rateLimitLoginWindow: this.rateLimitLoginWindow,
|
rateLimitLoginWindow: this.rateLimitLoginWindow,
|
||||||
backupSchedule: this.backupSchedule,
|
backupSchedule: this.backupSchedule,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class MediaProgress {
|
|||||||
this.isFinished = false
|
this.isFinished = false
|
||||||
this.hideFromContinueListening = false
|
this.hideFromContinueListening = false
|
||||||
|
|
||||||
this.ebookLocation = null // current cfi tag
|
this.ebookLocation = null // cfi tag for epub, page number for pdf
|
||||||
this.ebookProgress = null // 0 to 1
|
this.ebookProgress = null // 0 to 1
|
||||||
|
|
||||||
this.lastUpdate = null
|
this.lastUpdate = null
|
||||||
@@ -46,18 +46,18 @@ class MediaProgress {
|
|||||||
this.episodeId = progress.episodeId
|
this.episodeId = progress.episodeId
|
||||||
this.duration = progress.duration || 0
|
this.duration = progress.duration || 0
|
||||||
this.progress = progress.progress
|
this.progress = progress.progress
|
||||||
this.currentTime = progress.currentTime
|
this.currentTime = progress.currentTime || 0
|
||||||
this.isFinished = !!progress.isFinished
|
this.isFinished = !!progress.isFinished
|
||||||
this.hideFromContinueListening = !!progress.hideFromContinueListening
|
this.hideFromContinueListening = !!progress.hideFromContinueListening
|
||||||
this.ebookLocation = progress.ebookLocation || null
|
this.ebookLocation = progress.ebookLocation || null
|
||||||
this.ebookProgress = progress.ebookProgress
|
this.ebookProgress = progress.ebookProgress || null
|
||||||
this.lastUpdate = progress.lastUpdate
|
this.lastUpdate = progress.lastUpdate
|
||||||
this.startedAt = progress.startedAt
|
this.startedAt = progress.startedAt
|
||||||
this.finishedAt = progress.finishedAt || null
|
this.finishedAt = progress.finishedAt || null
|
||||||
}
|
}
|
||||||
|
|
||||||
get inProgress() {
|
get inProgress() {
|
||||||
return !this.isFinished && (this.progress > 0 || this.ebookLocation != null)
|
return !this.isFinished && (this.progress > 0 || (this.ebookLocation != null && this.ebookProgress > 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
setData(libraryItemId, progress, episodeId = null) {
|
setData(libraryItemId, progress, episodeId = null) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class Audible {
|
|||||||
'de': '.de',
|
'de': '.de',
|
||||||
'jp': '.co.jp',
|
'jp': '.co.jp',
|
||||||
'it': '.it',
|
'it': '.it',
|
||||||
'in': '.co.in',
|
'in': '.in',
|
||||||
'es': '.es'
|
'es': '.es'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,6 +96,11 @@ class ApiRouter {
|
|||||||
//
|
//
|
||||||
// Item Routes
|
// Item Routes
|
||||||
//
|
//
|
||||||
|
this.router.post('/items/batch/delete', LibraryItemController.batchDelete.bind(this))
|
||||||
|
this.router.post('/items/batch/update', LibraryItemController.batchUpdate.bind(this))
|
||||||
|
this.router.post('/items/batch/get', LibraryItemController.batchGet.bind(this))
|
||||||
|
this.router.post('/items/batch/quickmatch', LibraryItemController.batchQuickMatch.bind(this))
|
||||||
|
this.router.post('/items/batch/scan', LibraryItemController.batchScan.bind(this))
|
||||||
this.router.delete('/items/all', LibraryItemController.deleteAll.bind(this))
|
this.router.delete('/items/all', LibraryItemController.deleteAll.bind(this))
|
||||||
|
|
||||||
this.router.get('/items/:id', LibraryItemController.middleware.bind(this), LibraryItemController.findOne.bind(this))
|
this.router.get('/items/:id', LibraryItemController.middleware.bind(this), LibraryItemController.findOne.bind(this))
|
||||||
@@ -117,11 +122,6 @@ class ApiRouter {
|
|||||||
this.router.post('/items/:id/tone-scan/:index?', LibraryItemController.middleware.bind(this), LibraryItemController.toneScan.bind(this))
|
this.router.post('/items/:id/tone-scan/:index?', LibraryItemController.middleware.bind(this), LibraryItemController.toneScan.bind(this))
|
||||||
this.router.delete('/items/:id/file/:ino', LibraryItemController.middleware.bind(this), LibraryItemController.deleteLibraryFile.bind(this))
|
this.router.delete('/items/:id/file/:ino', LibraryItemController.middleware.bind(this), LibraryItemController.deleteLibraryFile.bind(this))
|
||||||
|
|
||||||
this.router.post('/items/batch/delete', LibraryItemController.batchDelete.bind(this))
|
|
||||||
this.router.post('/items/batch/update', LibraryItemController.batchUpdate.bind(this))
|
|
||||||
this.router.post('/items/batch/get', LibraryItemController.batchGet.bind(this))
|
|
||||||
this.router.post('/items/batch/quickmatch', LibraryItemController.batchQuickMatch.bind(this))
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// User Routes
|
// User Routes
|
||||||
//
|
//
|
||||||
@@ -197,6 +197,7 @@ class ApiRouter {
|
|||||||
// File System Routes
|
// File System Routes
|
||||||
//
|
//
|
||||||
this.router.get('/filesystem', FileSystemController.getPaths.bind(this))
|
this.router.get('/filesystem', FileSystemController.getPaths.bind(this))
|
||||||
|
this.router.post('/filesystem/pathexists', FileSystemController.checkPathExists.bind(this))
|
||||||
|
|
||||||
//
|
//
|
||||||
// Author Routes
|
// Author Routes
|
||||||
|
|||||||
@@ -19,11 +19,13 @@ const ScanOptions = require('./ScanOptions')
|
|||||||
|
|
||||||
const Author = require('../objects/entities/Author')
|
const Author = require('../objects/entities/Author')
|
||||||
const Series = require('../objects/entities/Series')
|
const Series = require('../objects/entities/Series')
|
||||||
|
const Task = require('../objects/Task')
|
||||||
|
|
||||||
class Scanner {
|
class Scanner {
|
||||||
constructor(db, coverManager) {
|
constructor(db, coverManager, taskManager) {
|
||||||
this.db = db
|
this.db = db
|
||||||
this.coverManager = coverManager
|
this.coverManager = coverManager
|
||||||
|
this.taskManager = taskManager
|
||||||
|
|
||||||
this.cancelLibraryScan = {}
|
this.cancelLibraryScan = {}
|
||||||
this.librariesScanning = []
|
this.librariesScanning = []
|
||||||
@@ -46,12 +48,24 @@ class Scanner {
|
|||||||
this.cancelLibraryScan[libraryId] = true
|
this.cancelLibraryScan[libraryId] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
async scanLibraryItemById(libraryItemId) {
|
getScanResultDescription(result) {
|
||||||
const libraryItem = this.db.libraryItems.find(li => li.id === libraryItemId)
|
switch (result) {
|
||||||
if (!libraryItem) {
|
case ScanResult.ADDED:
|
||||||
Logger.error(`[Scanner] Scan libraryItem by id not found ${libraryItemId}`)
|
return 'Added to library'
|
||||||
return ScanResult.NOTHING
|
case ScanResult.NOTHING:
|
||||||
|
return 'No updates necessary'
|
||||||
|
case ScanResult.REMOVED:
|
||||||
|
return 'Removed from library'
|
||||||
|
case ScanResult.UPDATED:
|
||||||
|
return 'Item was updated'
|
||||||
|
case ScanResult.UPTODATE:
|
||||||
|
return 'No updates necessary'
|
||||||
|
default:
|
||||||
|
return ''
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async scanLibraryItemByRequest(libraryItem) {
|
||||||
const library = this.db.libraries.find(lib => lib.id === libraryItem.libraryId)
|
const library = this.db.libraries.find(lib => lib.id === libraryItem.libraryId)
|
||||||
if (!library) {
|
if (!library) {
|
||||||
Logger.error(`[Scanner] Scan libraryItem by id library not found "${libraryItem.libraryId}"`)
|
Logger.error(`[Scanner] Scan libraryItem by id library not found "${libraryItem.libraryId}"`)
|
||||||
@@ -63,7 +77,21 @@ class Scanner {
|
|||||||
return ScanResult.NOTHING
|
return ScanResult.NOTHING
|
||||||
}
|
}
|
||||||
Logger.info(`[Scanner] Scanning Library Item "${libraryItem.media.metadata.title}"`)
|
Logger.info(`[Scanner] Scanning Library Item "${libraryItem.media.metadata.title}"`)
|
||||||
return this.scanLibraryItem(library.mediaType, folder, libraryItem)
|
|
||||||
|
const task = new Task()
|
||||||
|
task.setData('scan-item', `Scan ${libraryItem.media.metadata.title}`, '', true, {
|
||||||
|
libraryItemId: libraryItem.id,
|
||||||
|
libraryId: library.id,
|
||||||
|
mediaType: library.mediaType
|
||||||
|
})
|
||||||
|
this.taskManager.addTask(task)
|
||||||
|
|
||||||
|
const result = await this.scanLibraryItem(library.mediaType, folder, libraryItem)
|
||||||
|
|
||||||
|
task.setFinished(this.getScanResultDescription(result))
|
||||||
|
this.taskManager.taskFinished(task)
|
||||||
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
async scanLibraryItem(libraryMediaType, folder, libraryItem) {
|
async scanLibraryItem(libraryMediaType, folder, libraryItem) {
|
||||||
@@ -111,8 +139,8 @@ class Scanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasUpdated) {
|
if (hasUpdated) {
|
||||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
|
||||||
await this.db.updateLibraryItem(libraryItem)
|
await this.db.updateLibraryItem(libraryItem)
|
||||||
|
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||||
return ScanResult.UPDATED
|
return ScanResult.UPDATED
|
||||||
}
|
}
|
||||||
return ScanResult.UPTODATE
|
return ScanResult.UPTODATE
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ const fs = require('../libs/fsExtra')
|
|||||||
const filePerms = require('./filePerms')
|
const filePerms = require('./filePerms')
|
||||||
const package = require('../../package.json')
|
const package = require('../../package.json')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const { getId, copyValue } = require('./index')
|
const { getId } = require('./index')
|
||||||
|
const areEquivalent = require('../utils/areEquivalent')
|
||||||
|
|
||||||
|
|
||||||
const CurrentAbMetadataVersion = 2
|
const CurrentAbMetadataVersion = 2
|
||||||
@@ -328,11 +329,11 @@ function parseAbMetadataText(text, mediaType) {
|
|||||||
module.exports.parse = parseAbMetadataText
|
module.exports.parse = parseAbMetadataText
|
||||||
|
|
||||||
function checkUpdatedBookAuthors(abmetadataAuthors, authors) {
|
function checkUpdatedBookAuthors(abmetadataAuthors, authors) {
|
||||||
var finalAuthors = []
|
const finalAuthors = []
|
||||||
var hasUpdates = false
|
let hasUpdates = false
|
||||||
|
|
||||||
abmetadataAuthors.forEach((authorName) => {
|
abmetadataAuthors.forEach((authorName) => {
|
||||||
var findAuthor = authors.find(au => au.name.toLowerCase() == authorName.toLowerCase())
|
const findAuthor = authors.find(au => au.name.toLowerCase() == authorName.toLowerCase())
|
||||||
if (!findAuthor) {
|
if (!findAuthor) {
|
||||||
hasUpdates = true
|
hasUpdates = true
|
||||||
finalAuthors.push({
|
finalAuthors.push({
|
||||||
@@ -397,18 +398,81 @@ function checkArraysChanged(abmetadataArray, mediaArray) {
|
|||||||
return abmetadataArray.join(',') != mediaArray.join(',')
|
return abmetadataArray.join(',') != mediaArray.join(',')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseJsonMetadataText(text) {
|
||||||
|
try {
|
||||||
|
const abmetadataData = JSON.parse(text)
|
||||||
|
if (abmetadataData.metadata?.series?.length) {
|
||||||
|
abmetadataData.metadata.series = abmetadataData.metadata.series.map(series => {
|
||||||
|
let sequence = null
|
||||||
|
let name = series
|
||||||
|
// Series sequence match any characters after " #" other than whitespace and another #
|
||||||
|
// e.g. "Name #1a" is valid. "Name #1#a" or "Name #1 a" is not valid.
|
||||||
|
const matchResults = series.match(/ #([^#\s]+)$/) // Pull out sequence #
|
||||||
|
if (matchResults && matchResults.length && matchResults.length > 1) {
|
||||||
|
sequence = matchResults[1] // Group 1
|
||||||
|
name = series.replace(matchResults[0], '')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
sequence
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return abmetadataData
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(`[abmetadataGenerator] Invalid metadata.json JSON`, error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanChaptersArray(chaptersArray, mediaTitle) {
|
||||||
|
const chapters = []
|
||||||
|
let index = 0
|
||||||
|
for (const chap of chaptersArray) {
|
||||||
|
if (chap.start === null || isNaN(chap.start)) {
|
||||||
|
Logger.error(`[abmetadataGenerator] Invalid chapter start time ${chap.start} for "${mediaTitle}" metadata file`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (chap.end === null || isNaN(chap.end)) {
|
||||||
|
Logger.error(`[abmetadataGenerator] Invalid chapter end time ${chap.end} for "${mediaTitle}" metadata file`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (!chap.title || typeof chap.title !== 'string') {
|
||||||
|
Logger.error(`[abmetadataGenerator] Invalid chapter title ${chap.title} for "${mediaTitle}" metadata file`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
chapters.push({
|
||||||
|
id: index++,
|
||||||
|
start: chap.start,
|
||||||
|
end: chap.end,
|
||||||
|
title: chap.title
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return chapters
|
||||||
|
}
|
||||||
|
|
||||||
// Input text from abmetadata file and return object of media changes
|
// Input text from abmetadata file and return object of media changes
|
||||||
// only returns object of changes. empty object means no changes
|
// only returns object of changes. empty object means no changes
|
||||||
function parseAndCheckForUpdates(text, media, mediaType) {
|
function parseAndCheckForUpdates(text, media, mediaType, isJSON) {
|
||||||
if (!text || !media || !media.metadata || !mediaType) {
|
if (!text || !media || !media.metadata || !mediaType) {
|
||||||
Logger.error(`Invalid inputs to parseAndCheckForUpdates`)
|
Logger.error(`Invalid inputs to parseAndCheckForUpdates`)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const mediaMetadata = media.metadata
|
const mediaMetadata = media.metadata
|
||||||
const metadataUpdatePayload = {} // Only updated key/values
|
const metadataUpdatePayload = {} // Only updated key/values
|
||||||
|
|
||||||
const abmetadataData = parseAbMetadataText(text, mediaType)
|
let abmetadataData = null
|
||||||
|
|
||||||
|
if (isJSON) {
|
||||||
|
abmetadataData = parseJsonMetadataText(text)
|
||||||
|
} else {
|
||||||
|
abmetadataData = parseAbMetadataText(text, mediaType)
|
||||||
|
}
|
||||||
|
|
||||||
if (!abmetadataData || !abmetadataData.metadata) {
|
if (!abmetadataData || !abmetadataData.metadata) {
|
||||||
|
Logger.error(`[abmetadataGenerator] Invalid metadata file`)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,6 +505,15 @@ function parseAndCheckForUpdates(text, media, mediaType) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (abmetadataData.chapters && mediaType === 'book') {
|
||||||
|
const abmetadataChaptersCleaned = cleanChaptersArray(abmetadataData.chapters)
|
||||||
|
if (abmetadataChaptersCleaned) {
|
||||||
|
if (!areEquivalent(abmetadataChaptersCleaned, media.chapters)) {
|
||||||
|
updatePayload.chapters = abmetadataChaptersCleaned
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (Object.keys(metadataUpdatePayload).length) {
|
if (Object.keys(metadataUpdatePayload).length) {
|
||||||
updatePayload.metadata = metadataUpdatePayload
|
updatePayload.metadata = metadataUpdatePayload
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,7 +95,12 @@ module.exports.downloadPodcastEpisode = (podcastEpisodeDownload) => {
|
|||||||
method: 'GET',
|
method: 'GET',
|
||||||
responseType: 'stream',
|
responseType: 'stream',
|
||||||
timeout: 30000
|
timeout: 30000
|
||||||
|
}).catch((error) => {
|
||||||
|
Logger.error(`[ffmpegHelpers] Failed to download podcast episode with url "${podcastEpisodeDownload.url}"`, error)
|
||||||
|
return null
|
||||||
})
|
})
|
||||||
|
if (!response) return resolve(false)
|
||||||
|
|
||||||
|
|
||||||
const ffmpeg = Ffmpeg(response.data)
|
const ffmpeg = Ffmpeg(response.data)
|
||||||
ffmpeg.outputOptions(
|
ffmpeg.outputOptions(
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const globals = {
|
|||||||
SupportedEbookTypes: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
|
SupportedEbookTypes: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
|
||||||
SupportedVideoTypes: ['mp4'],
|
SupportedVideoTypes: ['mp4'],
|
||||||
TextFileTypes: ['txt', 'nfo'],
|
TextFileTypes: ['txt', 'nfo'],
|
||||||
MetadataFileTypes: ['opf', 'abs', 'xml']
|
MetadataFileTypes: ['opf', 'abs', 'xml', 'json']
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = globals
|
module.exports = globals
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ const { parseString } = require("xml2js")
|
|||||||
const areEquivalent = require('./areEquivalent')
|
const areEquivalent = require('./areEquivalent')
|
||||||
|
|
||||||
const levenshteinDistance = (str1, str2, caseSensitive = false) => {
|
const levenshteinDistance = (str1, str2, caseSensitive = false) => {
|
||||||
|
str1 = String(str1)
|
||||||
|
str2 = String(str2)
|
||||||
if (!caseSensitive) {
|
if (!caseSensitive) {
|
||||||
str1 = str1.toLowerCase()
|
str1 = str1.toLowerCase()
|
||||||
str2 = str2.toLowerCase()
|
str2 = str2.toLowerCase()
|
||||||
|
|||||||
+100
-90
@@ -346,71 +346,77 @@ module.exports = {
|
|||||||
label: 'Continue Listening',
|
label: 'Continue Listening',
|
||||||
labelStringKey: 'LabelContinueListening',
|
labelStringKey: 'LabelContinueListening',
|
||||||
type: isPodcastLibrary ? 'episode' : mediaType,
|
type: isPodcastLibrary ? 'episode' : mediaType,
|
||||||
entities: [],
|
entities: []
|
||||||
category: 'recentlyListened'
|
},
|
||||||
|
{
|
||||||
|
id: 'continue-reading',
|
||||||
|
label: 'Continue Reading',
|
||||||
|
labelStringKey: 'LabelContinueReading',
|
||||||
|
type: 'book',
|
||||||
|
entities: []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'continue-series',
|
id: 'continue-series',
|
||||||
label: 'Continue Series',
|
label: 'Continue Series',
|
||||||
labelStringKey: 'LabelContinueSeries',
|
labelStringKey: 'LabelContinueSeries',
|
||||||
type: mediaType,
|
type: mediaType,
|
||||||
entities: [],
|
entities: []
|
||||||
category: 'continueSeries'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'episodes-recently-added',
|
id: 'episodes-recently-added',
|
||||||
label: 'Newest Episodes',
|
label: 'Newest Episodes',
|
||||||
labelStringKey: 'LabelNewestEpisodes',
|
labelStringKey: 'LabelNewestEpisodes',
|
||||||
type: 'episode',
|
type: 'episode',
|
||||||
entities: [],
|
entities: []
|
||||||
category: 'newestEpisodes'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'recently-added',
|
id: 'recently-added',
|
||||||
label: 'Recently Added',
|
label: 'Recently Added',
|
||||||
labelStringKey: 'LabelRecentlyAdded',
|
labelStringKey: 'LabelRecentlyAdded',
|
||||||
type: mediaType,
|
type: mediaType,
|
||||||
entities: [],
|
entities: []
|
||||||
category: 'newestItems'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'recent-series',
|
id: 'recent-series',
|
||||||
label: 'Recent Series',
|
label: 'Recent Series',
|
||||||
labelStringKey: 'LabelRecentSeries',
|
labelStringKey: 'LabelRecentSeries',
|
||||||
type: 'series',
|
type: 'series',
|
||||||
entities: [],
|
entities: []
|
||||||
category: 'newestSeries'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'recommended',
|
id: 'recommended',
|
||||||
label: 'Recommended',
|
label: 'Recommended',
|
||||||
labelStringKey: 'LabelRecommended',
|
labelStringKey: 'LabelRecommended',
|
||||||
type: mediaType,
|
type: mediaType,
|
||||||
entities: [],
|
entities: []
|
||||||
category: 'recommended'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'listen-again',
|
id: 'listen-again',
|
||||||
label: 'Listen Again',
|
label: 'Listen Again',
|
||||||
labelStringKey: 'LabelListenAgain',
|
labelStringKey: 'LabelListenAgain',
|
||||||
type: isPodcastLibrary ? 'episode' : mediaType,
|
type: isPodcastLibrary ? 'episode' : mediaType,
|
||||||
entities: [],
|
entities: []
|
||||||
category: 'recentlyFinished'
|
},
|
||||||
|
{
|
||||||
|
id: 'read-again',
|
||||||
|
label: 'Read Again',
|
||||||
|
labelStringKey: 'LabelReadAgain',
|
||||||
|
type: 'book',
|
||||||
|
entities: []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'newest-authors',
|
id: 'newest-authors',
|
||||||
label: 'Newest Authors',
|
label: 'Newest Authors',
|
||||||
labelStringKey: 'LabelNewestAuthors',
|
labelStringKey: 'LabelNewestAuthors',
|
||||||
type: 'authors',
|
type: 'authors',
|
||||||
entities: [],
|
entities: []
|
||||||
category: 'newestAuthors'
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
const categoryMap = {}
|
const categoryMap = {}
|
||||||
shelves.forEach((shelf) => {
|
shelves.forEach((shelf) => {
|
||||||
categoryMap[shelf.category] = {
|
categoryMap[shelf.id] = {
|
||||||
category: shelf.category,
|
id: shelf.id,
|
||||||
biggest: 0,
|
biggest: 0,
|
||||||
smallest: 0,
|
smallest: 0,
|
||||||
items: []
|
items: []
|
||||||
@@ -427,21 +433,21 @@ module.exports = {
|
|||||||
const notStartedBooks = []
|
const notStartedBooks = []
|
||||||
|
|
||||||
for (const libraryItem of libraryItems) {
|
for (const libraryItem of libraryItems) {
|
||||||
if (libraryItem.addedAt > categoryMap.newestItems.smallest) {
|
if (libraryItem.addedAt > categoryMap['recently-added'].smallest) {
|
||||||
|
|
||||||
const indexToPut = categoryMap.newestItems.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.newestItems.items.splice(indexToPut, 0, libraryItem.toJSONMinified())
|
categoryMap['recently-added'].items.splice(indexToPut, 0, libraryItem.toJSONMinified())
|
||||||
} else {
|
} else {
|
||||||
categoryMap.newestItems.items.push(libraryItem.toJSONMinified())
|
categoryMap['recently-added'].items.push(libraryItem.toJSONMinified())
|
||||||
}
|
}
|
||||||
|
|
||||||
if (categoryMap.newestItems.items.length > maxEntitiesPerShelf) {
|
if (categoryMap['recently-added'].items.length > maxEntitiesPerShelf) {
|
||||||
// Remove last item
|
// Remove last item
|
||||||
categoryMap.newestItems.items.pop()
|
categoryMap['recently-added'].items.pop()
|
||||||
categoryMap.newestItems.smallest = categoryMap.newestItems.items[categoryMap.newestItems.items.length - 1].addedAt
|
categoryMap['recently-added'].smallest = categoryMap['recently-added'].items[categoryMap['recently-added'].items.length - 1].addedAt
|
||||||
}
|
}
|
||||||
categoryMap.newestItems.biggest = categoryMap.newestItems.items[0].addedAt
|
categoryMap['recently-added'].biggest = categoryMap['recently-added'].items[0].addedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
const allItemProgress = user.getAllMediaProgressForLibraryItem(libraryItem.id)
|
const allItemProgress = user.getAllMediaProgressForLibraryItem(libraryItem.id)
|
||||||
@@ -450,74 +456,74 @@ module.exports = {
|
|||||||
const podcastEpisodes = libraryItem.media.episodes || []
|
const podcastEpisodes = libraryItem.media.episodes || []
|
||||||
for (const episode of podcastEpisodes) {
|
for (const episode of podcastEpisodes) {
|
||||||
// Newest episodes
|
// Newest episodes
|
||||||
if (episode.addedAt > categoryMap.newestEpisodes.smallest) {
|
if (episode.addedAt > categoryMap['episodes-recently-added'].smallest) {
|
||||||
const libraryItemWithEpisode = {
|
const libraryItemWithEpisode = {
|
||||||
...libraryItem.toJSONMinified(),
|
...libraryItem.toJSONMinified(),
|
||||||
recentEpisode: episode.toJSON()
|
recentEpisode: episode.toJSON()
|
||||||
}
|
}
|
||||||
|
|
||||||
const indexToPut = categoryMap.newestEpisodes.items.findIndex(i => episode.addedAt > i.recentEpisode.addedAt)
|
const indexToPut = categoryMap['episodes-recently-added'].items.findIndex(i => episode.addedAt > i.recentEpisode.addedAt)
|
||||||
if (indexToPut >= 0) {
|
if (indexToPut >= 0) {
|
||||||
categoryMap.newestEpisodes.items.splice(indexToPut, 0, libraryItemWithEpisode)
|
categoryMap['episodes-recently-added'].items.splice(indexToPut, 0, libraryItemWithEpisode)
|
||||||
} else {
|
} else {
|
||||||
categoryMap.newestEpisodes.items.push(libraryItemWithEpisode)
|
categoryMap['episodes-recently-added'].items.push(libraryItemWithEpisode)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (categoryMap.newestEpisodes.items.length > maxEntitiesPerShelf) {
|
if (categoryMap['episodes-recently-added'].items.length > maxEntitiesPerShelf) {
|
||||||
// Remove last item
|
// Remove last item
|
||||||
categoryMap.newestEpisodes.items.pop()
|
categoryMap['episodes-recently-added'].items.pop()
|
||||||
categoryMap.newestEpisodes.smallest = categoryMap.newestEpisodes.items[categoryMap.newestEpisodes.items.length - 1].recentEpisode.addedAt
|
categoryMap['episodes-recently-added'].smallest = categoryMap['episodes-recently-added'].items[categoryMap['episodes-recently-added'].items.length - 1].recentEpisode.addedAt
|
||||||
}
|
}
|
||||||
categoryMap.newestEpisodes.biggest = categoryMap.newestEpisodes.items[0].recentEpisode.addedAt
|
categoryMap['episodes-recently-added'].biggest = categoryMap['episodes-recently-added'].items[0].recentEpisode.addedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
// Episode recently listened and finished
|
// Episode recently listened and finished
|
||||||
const mediaProgress = allItemProgress.find(mp => mp.episodeId === episode.id)
|
const mediaProgress = allItemProgress.find(mp => mp.episodeId === episode.id)
|
||||||
if (mediaProgress) {
|
if (mediaProgress) {
|
||||||
if (mediaProgress.isFinished) {
|
if (mediaProgress.isFinished) {
|
||||||
if (mediaProgress.finishedAt > categoryMap.recentlyFinished.smallest) { // Item belongs on shelf
|
if (mediaProgress.finishedAt > categoryMap['listen-again'].smallest) { // Item belongs on shelf
|
||||||
const libraryItemWithEpisode = {
|
const libraryItemWithEpisode = {
|
||||||
...libraryItem.toJSONMinified(),
|
...libraryItem.toJSONMinified(),
|
||||||
recentEpisode: episode.toJSON(),
|
recentEpisode: episode.toJSON(),
|
||||||
finishedAt: mediaProgress.finishedAt
|
finishedAt: mediaProgress.finishedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
const indexToPut = categoryMap.recentlyFinished.items.findIndex(i => mediaProgress.finishedAt > i.finishedAt)
|
const indexToPut = categoryMap['listen-again'].items.findIndex(i => mediaProgress.finishedAt > i.finishedAt)
|
||||||
if (indexToPut >= 0) {
|
if (indexToPut >= 0) {
|
||||||
categoryMap.recentlyFinished.items.splice(indexToPut, 0, libraryItemWithEpisode)
|
categoryMap['listen-again'].items.splice(indexToPut, 0, libraryItemWithEpisode)
|
||||||
} else {
|
} else {
|
||||||
categoryMap.recentlyFinished.items.push(libraryItemWithEpisode)
|
categoryMap['listen-again'].items.push(libraryItemWithEpisode)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (categoryMap.recentlyFinished.items.length > maxEntitiesPerShelf) {
|
if (categoryMap['listen-again'].items.length > maxEntitiesPerShelf) {
|
||||||
// Remove last item
|
// Remove last item
|
||||||
categoryMap.recentlyFinished.items.pop()
|
categoryMap['listen-again'].items.pop()
|
||||||
categoryMap.recentlyFinished.smallest = categoryMap.recentlyFinished.items[categoryMap.recentlyFinished.items.length - 1].finishedAt
|
categoryMap['listen-again'].smallest = categoryMap['listen-again'].items[categoryMap['listen-again'].items.length - 1].finishedAt
|
||||||
}
|
}
|
||||||
categoryMap.recentlyFinished.biggest = categoryMap.recentlyFinished.items[0].finishedAt
|
categoryMap['listen-again'].biggest = categoryMap['listen-again'].items[0].finishedAt
|
||||||
}
|
}
|
||||||
} else if (mediaProgress.inProgress && !mediaProgress.hideFromContinueListening) { // Handle most recently listened
|
} else if (mediaProgress.inProgress && !mediaProgress.hideFromContinueListening) { // Handle most recently listened
|
||||||
if (mediaProgress.lastUpdate > categoryMap.recentlyListened.smallest) { // Item belongs on shelf
|
if (mediaProgress.lastUpdate > categoryMap['continue-listening'].smallest) { // Item belongs on shelf
|
||||||
const libraryItemWithEpisode = {
|
const libraryItemWithEpisode = {
|
||||||
...libraryItem.toJSONMinified(),
|
...libraryItem.toJSONMinified(),
|
||||||
recentEpisode: episode.toJSON(),
|
recentEpisode: episode.toJSON(),
|
||||||
progressLastUpdate: mediaProgress.lastUpdate
|
progressLastUpdate: mediaProgress.lastUpdate
|
||||||
}
|
}
|
||||||
|
|
||||||
const indexToPut = categoryMap.recentlyListened.items.findIndex(i => mediaProgress.lastUpdate > i.progressLastUpdate)
|
const indexToPut = categoryMap['continue-listening'].items.findIndex(i => mediaProgress.lastUpdate > i.progressLastUpdate)
|
||||||
if (indexToPut >= 0) {
|
if (indexToPut >= 0) {
|
||||||
categoryMap.recentlyListened.items.splice(indexToPut, 0, libraryItemWithEpisode)
|
categoryMap['continue-listening'].items.splice(indexToPut, 0, libraryItemWithEpisode)
|
||||||
} else {
|
} else {
|
||||||
categoryMap.recentlyListened.items.push(libraryItemWithEpisode)
|
categoryMap['continue-listening'].items.push(libraryItemWithEpisode)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (categoryMap.recentlyListened.items.length > maxEntitiesPerShelf) {
|
if (categoryMap['continue-listening'].items.length > maxEntitiesPerShelf) {
|
||||||
// Remove last item
|
// Remove last item
|
||||||
categoryMap.recentlyListened.items.pop()
|
categoryMap['continue-listening'].items.pop()
|
||||||
categoryMap.recentlyListened.smallest = categoryMap.recentlyListened.items[categoryMap.recentlyListened.items.length - 1].progressLastUpdate
|
categoryMap['continue-listening'].smallest = categoryMap['continue-listening'].items[categoryMap['continue-listening'].items.length - 1].progressLastUpdate
|
||||||
}
|
}
|
||||||
|
|
||||||
categoryMap.recentlyListened.biggest = categoryMap.recentlyListened.items[0].progressLastUpdate
|
categoryMap['continue-listening'].biggest = categoryMap['continue-listening'].items[0].progressLastUpdate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -568,21 +574,21 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
seriesMap[librarySeries.id] = series
|
seriesMap[librarySeries.id] = series
|
||||||
|
|
||||||
if (series.addedAt > categoryMap.newestSeries.smallest) {
|
if (series.addedAt > categoryMap['recent-series'].smallest) {
|
||||||
const indexToPut = categoryMap.newestSeries.items.findIndex(i => series.addedAt > i.addedAt)
|
const indexToPut = categoryMap['recent-series'].items.findIndex(i => series.addedAt > i.addedAt)
|
||||||
if (indexToPut >= 0) {
|
if (indexToPut >= 0) {
|
||||||
categoryMap.newestSeries.items.splice(indexToPut, 0, series)
|
categoryMap['recent-series'].items.splice(indexToPut, 0, series)
|
||||||
} else {
|
} else {
|
||||||
categoryMap.newestSeries.items.push(series)
|
categoryMap['recent-series'].items.push(series)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Max series is 5
|
// Max series is 5
|
||||||
if (categoryMap.newestSeries.items.length > 5) {
|
if (categoryMap['recent-series'].items.length > 5) {
|
||||||
categoryMap.newestSeries.items.pop()
|
categoryMap['recent-series'].items.pop()
|
||||||
categoryMap.newestSeries.smallest = categoryMap.newestSeries.items[categoryMap.newestSeries.items.length - 1].addedAt
|
categoryMap['recent-series'].smallest = categoryMap['recent-series'].items[categoryMap['recent-series'].items.length - 1].addedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
categoryMap.newestSeries.biggest = categoryMap.newestSeries.items[0].addedAt
|
categoryMap['recent-series'].biggest = categoryMap['recent-series'].items[0].addedAt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -624,22 +630,22 @@ module.exports = {
|
|||||||
numBooks: 1
|
numBooks: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if (author.addedAt > categoryMap.newestAuthors.smallest) {
|
if (author.addedAt > categoryMap['newest-authors'].smallest) {
|
||||||
|
|
||||||
const indexToPut = categoryMap.newestAuthors.items.findIndex(i => author.addedAt > i.addedAt)
|
const indexToPut = categoryMap['newest-authors'].items.findIndex(i => author.addedAt > i.addedAt)
|
||||||
if (indexToPut >= 0) {
|
if (indexToPut >= 0) {
|
||||||
categoryMap.newestAuthors.items.splice(indexToPut, 0, author)
|
categoryMap['newest-authors'].items.splice(indexToPut, 0, author)
|
||||||
} else {
|
} else {
|
||||||
categoryMap.newestAuthors.items.push(author)
|
categoryMap['newest-authors'].items.push(author)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Max authors is 10
|
// Max authors is 10
|
||||||
if (categoryMap.newestAuthors.items.length > 10) {
|
if (categoryMap['newest-authors'].items.length > 10) {
|
||||||
categoryMap.newestAuthors.items.pop()
|
categoryMap['newest-authors'].items.pop()
|
||||||
categoryMap.newestAuthors.smallest = categoryMap.newestAuthors.items[categoryMap.newestAuthors.items.length - 1].addedAt
|
categoryMap['newest-authors'].smallest = categoryMap['newest-authors'].items[categoryMap['newest-authors'].items.length - 1].addedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
categoryMap.newestAuthors.biggest = categoryMap.newestAuthors.items[0].addedAt
|
categoryMap['newest-authors'].biggest = categoryMap['newest-authors'].items[0].addedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
authorMap[libraryAuthor.id] = author
|
authorMap[libraryAuthor.id] = author
|
||||||
@@ -652,46 +658,50 @@ module.exports = {
|
|||||||
|
|
||||||
// Book listening and finished
|
// Book listening and finished
|
||||||
if (mediaProgress) {
|
if (mediaProgress) {
|
||||||
|
const categoryId = libraryItem.media.isEBookOnly ? 'read-again' : 'listen-again'
|
||||||
|
|
||||||
// Handle most recently finished
|
// Handle most recently finished
|
||||||
if (mediaProgress.isFinished) {
|
if (mediaProgress.isFinished) {
|
||||||
if (mediaProgress.finishedAt > categoryMap.recentlyFinished.smallest) { // Item belongs on shelf
|
if (mediaProgress.finishedAt > categoryMap[categoryId].smallest) { // Item belongs on shelf
|
||||||
const libraryItemObj = {
|
const libraryItemObj = {
|
||||||
...libraryItem.toJSONMinified(),
|
...libraryItem.toJSONMinified(),
|
||||||
finishedAt: mediaProgress.finishedAt
|
finishedAt: mediaProgress.finishedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
const indexToPut = categoryMap.recentlyFinished.items.findIndex(i => mediaProgress.finishedAt > i.finishedAt)
|
const indexToPut = categoryMap[categoryId].items.findIndex(i => mediaProgress.finishedAt > i.finishedAt)
|
||||||
if (indexToPut >= 0) {
|
if (indexToPut >= 0) {
|
||||||
categoryMap.recentlyFinished.items.splice(indexToPut, 0, libraryItemObj)
|
categoryMap[categoryId].items.splice(indexToPut, 0, libraryItemObj)
|
||||||
} else {
|
} else {
|
||||||
categoryMap.recentlyFinished.items.push(libraryItemObj)
|
categoryMap[categoryId].items.push(libraryItemObj)
|
||||||
}
|
}
|
||||||
if (categoryMap.recentlyFinished.items.length > maxEntitiesPerShelf) {
|
if (categoryMap[categoryId].items.length > maxEntitiesPerShelf) {
|
||||||
// Remove last item
|
// Remove last item
|
||||||
categoryMap.recentlyFinished.items.pop()
|
categoryMap[categoryId].items.pop()
|
||||||
categoryMap.recentlyFinished.smallest = categoryMap.recentlyFinished.items[categoryMap.recentlyFinished.items.length - 1].finishedAt
|
categoryMap[categoryId].smallest = categoryMap[categoryId].items[categoryMap[categoryId].items.length - 1].finishedAt
|
||||||
}
|
}
|
||||||
categoryMap.recentlyFinished.biggest = categoryMap.recentlyFinished.items[0].finishedAt
|
categoryMap[categoryId].biggest = categoryMap[categoryId].items[0].finishedAt
|
||||||
}
|
}
|
||||||
} else if (mediaProgress.inProgress && !mediaProgress.hideFromContinueListening) { // Handle most recently listened
|
} else if (mediaProgress.inProgress && !mediaProgress.hideFromContinueListening) { // Handle most recently listened
|
||||||
if (mediaProgress.lastUpdate > categoryMap.recentlyListened.smallest) { // Item belongs on shelf
|
const categoryId = libraryItem.media.isEBookOnly ? 'continue-reading' : 'continue-listening'
|
||||||
|
|
||||||
|
if (mediaProgress.lastUpdate > categoryMap[categoryId].smallest) { // Item belongs on shelf
|
||||||
const libraryItemObj = {
|
const libraryItemObj = {
|
||||||
...libraryItem.toJSONMinified(),
|
...libraryItem.toJSONMinified(),
|
||||||
progressLastUpdate: mediaProgress.lastUpdate
|
progressLastUpdate: mediaProgress.lastUpdate
|
||||||
}
|
}
|
||||||
|
|
||||||
const indexToPut = categoryMap.recentlyListened.items.findIndex(i => mediaProgress.lastUpdate > i.progressLastUpdate)
|
const indexToPut = categoryMap[categoryId].items.findIndex(i => mediaProgress.lastUpdate > i.progressLastUpdate)
|
||||||
if (indexToPut >= 0) {
|
if (indexToPut >= 0) {
|
||||||
categoryMap.recentlyListened.items.splice(indexToPut, 0, libraryItemObj)
|
categoryMap[categoryId].items.splice(indexToPut, 0, libraryItemObj)
|
||||||
} else { // Should only happen when array is < max
|
} else { // Should only happen when array is < max
|
||||||
categoryMap.recentlyListened.items.push(libraryItemObj)
|
categoryMap[categoryId].items.push(libraryItemObj)
|
||||||
}
|
}
|
||||||
if (categoryMap.recentlyListened.items.length > maxEntitiesPerShelf) {
|
if (categoryMap[categoryId].items.length > maxEntitiesPerShelf) {
|
||||||
// Remove last item
|
// Remove last item
|
||||||
categoryMap.recentlyListened.items.pop()
|
categoryMap[categoryId].items.pop()
|
||||||
categoryMap.recentlyListened.smallest = categoryMap.recentlyListened.items[categoryMap.recentlyListened.items.length - 1].progressLastUpdate
|
categoryMap[categoryId].smallest = categoryMap[categoryId].items[categoryMap[categoryId].items.length - 1].progressLastUpdate
|
||||||
}
|
}
|
||||||
categoryMap.recentlyListened.biggest = categoryMap.recentlyListened.items[0].progressLastUpdate
|
categoryMap[categoryId].biggest = categoryMap[categoryId].items[0].progressLastUpdate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -719,12 +729,12 @@ module.exports = {
|
|||||||
sequence: nextBookInSeries.seriesSequence
|
sequence: nextBookInSeries.seriesSequence
|
||||||
}
|
}
|
||||||
|
|
||||||
const indexToPut = categoryMap.continueSeries.items.findIndex(i => i.prevBookInProgressLastUpdate < bookForContinueSeries.prevBookInProgressLastUpdate)
|
const indexToPut = categoryMap['continue-series'].items.findIndex(i => i.prevBookInProgressLastUpdate < bookForContinueSeries.prevBookInProgressLastUpdate)
|
||||||
if (!categoryMap.continueSeries.items.find(book => book.id === bookForContinueSeries.id)) {
|
if (!categoryMap['continue-series'].items.find(book => book.id === bookForContinueSeries.id)) {
|
||||||
if (indexToPut >= 0) {
|
if (indexToPut >= 0) {
|
||||||
categoryMap.continueSeries.items.splice(indexToPut, 0, bookForContinueSeries)
|
categoryMap['continue-series'].items.splice(indexToPut, 0, bookForContinueSeries)
|
||||||
} else if (categoryMap.continueSeries.items.length < 10) { // Max 10 books
|
} else if (categoryMap['continue-series'].items.length < 10) { // Max 10 books
|
||||||
categoryMap.continueSeries.items.push(bookForContinueSeries)
|
categoryMap['continue-series'].items.push(bookForContinueSeries)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -802,8 +812,8 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sort series books by sequence
|
// Sort series books by sequence
|
||||||
if (categoryMap.newestSeries.items.length) {
|
if (categoryMap['recent-series'].items.length) {
|
||||||
for (const seriesItem of categoryMap.newestSeries.items) {
|
for (const seriesItem of categoryMap['recent-series'].items) {
|
||||||
seriesItem.books = naturalSort(seriesItem.books).asc(li => li.seriesSequence)
|
seriesItem.books = naturalSort(seriesItem.books).asc(li => li.seriesSequence)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -811,7 +821,7 @@ 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 => {
|
return categoriesWithItems.map(cat => {
|
||||||
const shelf = shelves.find(s => s.category === cat.category)
|
const shelf = shelves.find(s => s.id === cat.id)
|
||||||
shelf.entities = cat.items
|
shelf.entities = cat.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
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ const tone = require('node-tone')
|
|||||||
const fs = require('../libs/fsExtra')
|
const fs = require('../libs/fsExtra')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
function getToneMetadataObject(libraryItem, chapters, trackTotal) {
|
function getToneMetadataObject(libraryItem, chapters, trackTotal, mimeType = null) {
|
||||||
const bookMetadata = libraryItem.media.metadata
|
const bookMetadata = libraryItem.media.metadata
|
||||||
const coverPath = libraryItem.media.coverPath
|
const coverPath = libraryItem.media.coverPath
|
||||||
|
|
||||||
|
const isMp4 = mimeType === 'audio/mp4'
|
||||||
|
const isMp3 = mimeType === 'audio/mpeg'
|
||||||
|
|
||||||
const metadataObject = {
|
const metadataObject = {
|
||||||
'album': bookMetadata.title || '',
|
'album': bookMetadata.title || '',
|
||||||
'title': bookMetadata.title || '',
|
'title': bookMetadata.title || '',
|
||||||
@@ -28,10 +31,24 @@ function getToneMetadataObject(libraryItem, chapters, trackTotal) {
|
|||||||
metadataObject['composer'] = bookMetadata.narratorName
|
metadataObject['composer'] = bookMetadata.narratorName
|
||||||
}
|
}
|
||||||
if (bookMetadata.firstSeriesName) {
|
if (bookMetadata.firstSeriesName) {
|
||||||
|
if (!isMp3) {
|
||||||
|
metadataObject.additionalFields['----:com.pilabor.tone:SERIES'] = bookMetadata.firstSeriesName
|
||||||
|
}
|
||||||
metadataObject['movementName'] = bookMetadata.firstSeriesName
|
metadataObject['movementName'] = bookMetadata.firstSeriesName
|
||||||
}
|
}
|
||||||
if (bookMetadata.firstSeriesSequence) {
|
if (bookMetadata.firstSeriesSequence) {
|
||||||
metadataObject['movement'] = bookMetadata.firstSeriesSequence
|
// Non-mp3
|
||||||
|
if (!isMp3) {
|
||||||
|
metadataObject.additionalFields['----:com.pilabor.tone:PART'] = bookMetadata.firstSeriesSequence
|
||||||
|
}
|
||||||
|
// MP3 Files with non-integer sequence
|
||||||
|
const isNonIntegerSequence = String(bookMetadata.firstSeriesSequence).includes('.') || isNaN(bookMetadata.firstSeriesSequence)
|
||||||
|
if (isMp3 && isNonIntegerSequence) {
|
||||||
|
metadataObject.additionalFields['PART'] = bookMetadata.firstSeriesSequence
|
||||||
|
}
|
||||||
|
if (!isNonIntegerSequence) {
|
||||||
|
metadataObject['movement'] = bookMetadata.firstSeriesSequence
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (bookMetadata.genres.length) {
|
if (bookMetadata.genres.length) {
|
||||||
metadataObject['genre'] = bookMetadata.genres.join('/')
|
metadataObject['genre'] = bookMetadata.genres.join('/')
|
||||||
@@ -40,7 +57,12 @@ function getToneMetadataObject(libraryItem, chapters, trackTotal) {
|
|||||||
metadataObject['publisher'] = bookMetadata.publisher
|
metadataObject['publisher'] = bookMetadata.publisher
|
||||||
}
|
}
|
||||||
if (bookMetadata.asin) {
|
if (bookMetadata.asin) {
|
||||||
metadataObject.additionalFields['asin'] = bookMetadata.asin
|
if (!isMp3) {
|
||||||
|
metadataObject.additionalFields['----:com.pilabor.tone:AUDIBLE_ASIN'] = bookMetadata.asin
|
||||||
|
}
|
||||||
|
if (!isMp4) {
|
||||||
|
metadataObject.additionalFields['asin'] = bookMetadata.asin
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (bookMetadata.isbn) {
|
if (bookMetadata.isbn) {
|
||||||
metadataObject.additionalFields['isbn'] = bookMetadata.isbn
|
metadataObject.additionalFields['isbn'] = bookMetadata.isbn
|
||||||
@@ -67,8 +89,8 @@ function getToneMetadataObject(libraryItem, chapters, trackTotal) {
|
|||||||
}
|
}
|
||||||
module.exports.getToneMetadataObject = getToneMetadataObject
|
module.exports.getToneMetadataObject = getToneMetadataObject
|
||||||
|
|
||||||
module.exports.writeToneMetadataJsonFile = (libraryItem, chapters, filePath, trackTotal) => {
|
module.exports.writeToneMetadataJsonFile = (libraryItem, chapters, filePath, trackTotal, mimeType) => {
|
||||||
const metadataObject = getToneMetadataObject(libraryItem, chapters, trackTotal)
|
const metadataObject = getToneMetadataObject(libraryItem, chapters, trackTotal, mimeType)
|
||||||
return fs.writeFile(filePath, JSON.stringify({ meta: metadataObject }, null, 2))
|
return fs.writeFile(filePath, JSON.stringify({ meta: metadataObject }, null, 2))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user