mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-04 18:00:45 +02:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 174decf8da | |||
| 0700f12896 | |||
| 3dc848a106 | |||
| c17612a233 | |||
| 7313d151f8 | |||
| 97dc9fbccf | |||
| 9a87e4af73 | |||
| 4ccb4243f7 | |||
| eb25ca7af5 | |||
| 872d5178e6 | |||
| d11501b2c6 | |||
| 7e05804bcf | |||
| a73b72a07b | |||
| 8ec4bd4279 | |||
| e362456895 | |||
| 8cd7de25ad | |||
| 99ea7866c5 | |||
| 3194b4cd87 | |||
| 149f52b33c | |||
| 575ec9d00b | |||
| 40e999fcae | |||
| ac57b2b867 | |||
| 3cafa87eda | |||
| dee4ca3559 | |||
| 772c7b3217 | |||
| c0dd58a94e | |||
| 91e116969a | |||
| 1f37e32f91 | |||
| 221061ea30 | |||
| 1e8e45431d | |||
| 381a81e4bb | |||
| be28b9899e | |||
| 37ca139195 | |||
| 6b02779e0f | |||
| ff6d95dc4d | |||
| e611d7a8fd | |||
| 67f6cd3c56 |
@@ -5,7 +5,7 @@ set -o pipefail
|
|||||||
FFMPEG_INSTALL_DIR="/usr/lib/audiobookshelf-ffmpeg"
|
FFMPEG_INSTALL_DIR="/usr/lib/audiobookshelf-ffmpeg"
|
||||||
DEFAULT_DATA_DIR="/usr/share/audiobookshelf"
|
DEFAULT_DATA_DIR="/usr/share/audiobookshelf"
|
||||||
CONFIG_PATH="/etc/default/audiobookshelf"
|
CONFIG_PATH="/etc/default/audiobookshelf"
|
||||||
DEFAULT_PORT=7331
|
DEFAULT_PORT=13378
|
||||||
DEFAULT_HOST="0.0.0.0"
|
DEFAULT_HOST="0.0.0.0"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,10 @@
|
|||||||
#bookshelf {
|
#bookshelf {
|
||||||
height: calc(100% - 40px);
|
height: calc(100% - 40px);
|
||||||
background-image: linear-gradient(to right bottom, #2e2e2e, #303030, #313131, #333333, #353535, #343434, #323232, #313131, #2c2c2c, #282828, #232323, #1f1f1f);
|
background-image: linear-gradient(to right bottom, #2e2e2e, #303030, #313131, #333333, #353535, #343434, #323232, #313131, #2c2c2c, #282828, #232323, #1f1f1f);
|
||||||
|
|
||||||
|
/* For Firefox */
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #855620 rgba(0, 0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bookshelf-row {
|
.bookshelf-row {
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ export default {
|
|||||||
var newIsFinished = !this.selectedIsFinished
|
var newIsFinished = !this.selectedIsFinished
|
||||||
var updateProgressPayloads = this.selectedLibraryItems.map((lid) => {
|
var updateProgressPayloads = this.selectedLibraryItems.map((lid) => {
|
||||||
return {
|
return {
|
||||||
id: lid,
|
libraryItemId: lid,
|
||||||
isFinished: newIsFinished
|
isFinished: newIsFinished
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -17,6 +17,11 @@
|
|||||||
<cards-lazy-series-card :key="entity.name" :series-mount="entity" :height="bookCoverHeight" :width="bookCoverWidth * 2" :book-cover-aspect-ratio="bookCoverAspectRatio" class="relative mx-2" @hook:updated="updatedBookCard" />
|
<cards-lazy-series-card :key="entity.name" :series-mount="entity" :height="bookCoverHeight" :width="bookCoverWidth * 2" :book-cover-aspect-ratio="bookCoverAspectRatio" class="relative mx-2" @hook:updated="updatedBookCard" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="shelf.type === 'tags'" class="flex items-center">
|
||||||
|
<template v-for="entity in shelf.entities">
|
||||||
|
<cards-group-card :key="entity.name" :group="entity" :height="bookCoverHeight" :width="bookCoverWidth * 2" :book-cover-aspect-ratio="bookCoverAspectRatio" class="relative mx-2" @hook:updated="updatedBookCard" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
<div v-if="shelf.type === 'authors'" class="flex items-center">
|
<div v-if="shelf.type === 'authors'" class="flex items-center">
|
||||||
<template v-for="entity in shelf.entities">
|
<template v-for="entity in shelf.entities">
|
||||||
<cards-author-card :key="entity.id" :width="bookCoverWidth / 1.25" :height="bookCoverWidth" :author="entity" :size-multiplier="sizeMultiplier" @hook:updated="updatedBookCard" class="pb-6 mx-2" @edit="editAuthor" />
|
<cards-author-card :key="entity.id" :width="bookCoverWidth / 1.25" :height="bookCoverWidth" :author="entity" :size-multiplier="sizeMultiplier" @hook:updated="updatedBookCard" class="pb-6 mx-2" @edit="editAuthor" />
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export default {
|
|||||||
authors: {
|
authors: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -215,6 +215,7 @@ export default {
|
|||||||
this.$toast.success('Removed library items with issues')
|
this.$toast.success('Removed library items with issues')
|
||||||
this.$router.push(`/library/${this.currentLibraryId}/bookshelf`)
|
this.$router.push(`/library/${this.currentLibraryId}/bookshelf`)
|
||||||
this.processingIssues = false
|
this.processingIssues = false
|
||||||
|
this.$store.dispatch('libraries/fetch', this.currentLibraryId)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to remove library items with issues', error)
|
console.error('Failed to remove library items with issues', error)
|
||||||
@@ -228,7 +229,7 @@ export default {
|
|||||||
this.processingSeries = true
|
this.processingSeries = true
|
||||||
var updateProgressPayloads = this.seriesLibraryItemIds.map((lid) => {
|
var updateProgressPayloads = this.seriesLibraryItemIds.map((lid) => {
|
||||||
return {
|
return {
|
||||||
id: lid,
|
libraryItemId: lid,
|
||||||
isFinished: newIsFinished
|
isFinished: newIsFinished
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -302,4 +303,4 @@ export default {
|
|||||||
#toolbar {
|
#toolbar {
|
||||||
box-shadow: 0px 8px 6px #111111aa;
|
box-shadow: 0px 8px 6px #111111aa;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -44,11 +44,14 @@
|
|||||||
@close="closePlayer"
|
@close="closePlayer"
|
||||||
@showBookmarks="showBookmarks"
|
@showBookmarks="showBookmarks"
|
||||||
@showSleepTimer="showSleepTimerModal = true"
|
@showSleepTimer="showSleepTimerModal = true"
|
||||||
|
@showPlayerQueueItems="showPlayerQueueItemsModal = true"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :current-time="bookmarkCurrentTime" :library-item-id="libraryItemId" @select="selectBookmark" />
|
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :current-time="bookmarkCurrentTime" :library-item-id="libraryItemId" @select="selectBookmark" />
|
||||||
|
|
||||||
<modals-sleep-timer-modal v-model="showSleepTimerModal" :timer-set="sleepTimerSet" :timer-time="sleepTimerTime" :remaining="sleepTimerRemaining" @set="setSleepTimer" @cancel="cancelSleepTimer" @increment="incrementSleepTimer" @decrement="decrementSleepTimer" />
|
<modals-sleep-timer-modal v-model="showSleepTimerModal" :timer-set="sleepTimerSet" :timer-time="sleepTimerTime" :remaining="sleepTimerRemaining" @set="setSleepTimer" @cancel="cancelSleepTimer" @increment="incrementSleepTimer" @decrement="decrementSleepTimer" />
|
||||||
|
|
||||||
|
<modals-player-queue-items-modal v-model="showPlayerQueueItemsModal" :library-item-id="libraryItemId" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -66,6 +69,7 @@ export default {
|
|||||||
isPlaying: false,
|
isPlaying: false,
|
||||||
currentTime: 0,
|
currentTime: 0,
|
||||||
showSleepTimerModal: false,
|
showSleepTimerModal: false,
|
||||||
|
showPlayerQueueItemsModal: false,
|
||||||
sleepTimerSet: false,
|
sleepTimerSet: false,
|
||||||
sleepTimerTime: 0,
|
sleepTimerTime: 0,
|
||||||
sleepTimerRemaining: 0,
|
sleepTimerRemaining: 0,
|
||||||
@@ -138,9 +142,39 @@ export default {
|
|||||||
podcastAuthor() {
|
podcastAuthor() {
|
||||||
if (!this.isPodcast) return null
|
if (!this.isPodcast) return null
|
||||||
return this.mediaMetadata.author || 'Unknown'
|
return this.mediaMetadata.author || 'Unknown'
|
||||||
|
},
|
||||||
|
playerQueueItems() {
|
||||||
|
return this.$store.state.playerQueueItems || []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
mediaFinished(libraryItemId, episodeId) {
|
||||||
|
// Play next item in queue
|
||||||
|
if (!this.playerQueueItems.length || !this.$store.state.playerQueueAutoPlay) {
|
||||||
|
// TODO: Set media finished flag so play button will play next queue item
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var currentQueueIndex = this.playerQueueItems.findIndex((i) => {
|
||||||
|
if (episodeId) return i.libraryItemId === libraryItemId && i.episodeId === episodeId
|
||||||
|
return i.libraryItemId === libraryItemId
|
||||||
|
})
|
||||||
|
if (currentQueueIndex < 0) {
|
||||||
|
console.error('Media finished not found in queue', this.playerQueueItems)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (currentQueueIndex === this.playerQueueItems.length - 1) {
|
||||||
|
console.log('Finished last item in queue')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const nextItemInQueue = this.playerQueueItems[currentQueueIndex + 1]
|
||||||
|
if (nextItemInQueue) {
|
||||||
|
this.playLibraryItem({
|
||||||
|
libraryItemId: nextItemInQueue.libraryItemId,
|
||||||
|
episodeId: nextItemInQueue.episodeId || null,
|
||||||
|
queueItems: this.playerQueueItems
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
setPlaying(isPlaying) {
|
setPlaying(isPlaying) {
|
||||||
this.isPlaying = isPlaying
|
this.isPlaying = isPlaying
|
||||||
this.$store.commit('setIsPlaying', isPlaying)
|
this.$store.commit('setIsPlaying', isPlaying)
|
||||||
@@ -313,6 +347,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
sessionOpen(session) {
|
sessionOpen(session) {
|
||||||
|
// For opening session on init (temporarily unused)
|
||||||
this.$store.commit('setMediaPlaying', {
|
this.$store.commit('setMediaPlaying', {
|
||||||
libraryItem: session.libraryItem,
|
libraryItem: session.libraryItem,
|
||||||
episodeId: session.episodeId
|
episodeId: session.episodeId
|
||||||
@@ -376,7 +411,8 @@ export default {
|
|||||||
if (!libraryItem) return
|
if (!libraryItem) return
|
||||||
this.$store.commit('setMediaPlaying', {
|
this.$store.commit('setMediaPlaying', {
|
||||||
libraryItem,
|
libraryItem,
|
||||||
episodeId
|
episodeId,
|
||||||
|
queueItems: payload.queueItems || []
|
||||||
})
|
})
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (this.$refs.audioPlayer) this.$refs.audioPlayer.checkUpdateChapterTrack()
|
if (this.$refs.audioPlayer) this.$refs.audioPlayer.checkUpdateChapterTrack()
|
||||||
|
|||||||
@@ -1,26 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="rounded-sm h-full relative" :style="{ padding: `0px ${paddingX}px` }" @mouseover="mouseoverCard" @mouseleave="mouseleaveCard" @click="clickCard">
|
<div class="rounded-sm h-full relative" :style="{ width: width + 'px', height: height + 'px' }" @mouseover="mouseoverCard" @mouseleave="mouseleaveCard" @click="clickCard">
|
||||||
<nuxt-link :to="groupTo" class="cursor-pointer">
|
<nuxt-link :to="groupTo" class="cursor-pointer">
|
||||||
<div class="w-full h-full relative" :class="isHovering ? 'bg-black-400' : 'bg-primary'" :style="{ height: coverHeight + 'px', width: coverWidth + 'px' }">
|
<div class="w-full h-full relative" :class="isHovering ? 'bg-black-400' : 'bg-primary'">
|
||||||
<covers-group-cover ref="groupcover" :id="seriesId" :name="groupName" :group-to="groupTo" :type="groupType" :book-items="bookItems" :width="coverWidth" :height="coverHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-group-cover ref="groupcover" :id="groupEncode" :name="groupName" :type="groupType" :book-items="bookItems" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
|
|
||||||
<div v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity z-30" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: `${sizeMultiplier}rem` }">
|
<div v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity z-30" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: `${sizeMultiplier}rem` }">
|
||||||
<p class="font-book" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ groupName }}</p>
|
<p class="font-book" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ groupName }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="absolute top-2 right-2 w-7 h-7 rounded-lg bg-black bg-opacity-90 text-gray-300 box-shadow-book flex items-center justify-center border border-white border-opacity-25 pointer-events-none z-40">
|
<div class="absolute z-10 top-1.5 right-1.5 rounded-md leading-3 text-sm p-1 font-semibold text-white flex items-center justify-center" style="background-color: #cd9d49dd">{{ bookItems.length }}</div>
|
||||||
<p class="font-book text-xl">{{ bookItems.length }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!isCategorized" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto bottom-0 h-6 rounded-md font-book text-center" :style="{ width: Math.min(160, coverWidth) + 'px' }">
|
|
||||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${1 * sizeMultiplier}rem` }">
|
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ groupName }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -31,11 +23,8 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
default: () => null
|
default: () => null
|
||||||
},
|
},
|
||||||
width: {
|
width: Number,
|
||||||
type: Number,
|
height: Number,
|
||||||
default: 120
|
|
||||||
},
|
|
||||||
isCategorized: Boolean,
|
|
||||||
bookCoverAspectRatio: Number
|
bookCoverAspectRatio: Number
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -43,23 +32,7 @@ export default {
|
|||||||
isHovering: false
|
isHovering: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
|
||||||
width(newVal) {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
if (this.$refs.groupcover) {
|
|
||||||
this.$refs.groupcover.init()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
seriesId() {
|
|
||||||
return this.groupEncode
|
|
||||||
},
|
|
||||||
labelFontSize() {
|
|
||||||
if (this.coverWidth < 160) return 0.75
|
|
||||||
return 0.875
|
|
||||||
},
|
|
||||||
currentLibraryId() {
|
currentLibraryId() {
|
||||||
return this.$store.state.libraries.currentLibraryId
|
return this.$store.state.libraries.currentLibraryId
|
||||||
},
|
},
|
||||||
@@ -70,29 +43,11 @@ export default {
|
|||||||
return this._group.type
|
return this._group.type
|
||||||
},
|
},
|
||||||
groupTo() {
|
groupTo() {
|
||||||
if (this.groupType === 'series') {
|
return `/library/${this.currentLibraryId}/bookshelf?filter=${this.filter}`
|
||||||
return `/library/${this.currentLibraryId}/series/${this._group.id}`
|
|
||||||
} else if (this.groupType === 'collection') {
|
|
||||||
return `/collection/${this._group.id}`
|
|
||||||
} else {
|
|
||||||
return `/library/${this.currentLibraryId}/bookshelf?filter=tags.${this.groupEncode}`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
squareAspectRatio() {
|
|
||||||
return this.bookCoverAspectRatio === 1
|
|
||||||
},
|
|
||||||
coverWidth() {
|
|
||||||
return this.width * 2
|
|
||||||
},
|
|
||||||
coverHeight() {
|
|
||||||
return this.width * this.bookCoverAspectRatio
|
|
||||||
},
|
},
|
||||||
sizeMultiplier() {
|
sizeMultiplier() {
|
||||||
var baseSize = this.squareAspectRatio ? 192 : 120
|
if (this.bookCoverAspectRatio === 1) return this.width / (120 * 1.6 * 2)
|
||||||
return this.width / baseSize
|
return this.width / 240
|
||||||
},
|
|
||||||
paddingX() {
|
|
||||||
return 16 * this.sizeMultiplier
|
|
||||||
},
|
},
|
||||||
bookItems() {
|
bookItems() {
|
||||||
return this._group.books || []
|
return this._group.books || []
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
<div v-if="!booksInSeries && (!isPodcast || episodeProgress)" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
|
<div v-if="!booksInSeries && (!isPodcast || episodeProgress)" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
|
||||||
|
|
||||||
<!-- Overlay is not shown if collapsing series in library -->
|
<!-- Overlay is not shown if collapsing series in library -->
|
||||||
<div v-show="!booksInSeries && libraryItem && (isHovering || isSelectionMode || isMoreMenuOpen)" class="w-full h-full absolute top-0 left-0 z-10 bg-black rounded hidden md:block" :class="overlayWrapperClasslist">
|
<div v-show="!booksInSeries && libraryItem && (isHovering || isSelectionMode || isMoreMenuOpen) && !processing" class="w-full h-full absolute top-0 left-0 z-10 bg-black rounded hidden md:block" :class="overlayWrapperClasslist">
|
||||||
<div v-show="showPlayButton" class="h-full flex items-center justify-center pointer-events-none">
|
<div v-show="showPlayButton" class="h-full flex items-center justify-center pointer-events-none">
|
||||||
<div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto" @click.stop.prevent="play">
|
<div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto" @click.stop.prevent="play">
|
||||||
<span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">play_circle_filled</span>
|
<span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">play_circle_filled</span>
|
||||||
@@ -66,6 +66,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Processing/loading spinner overlay -->
|
||||||
|
<div v-if="processing" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-40 rounded flex items-center justify-center">
|
||||||
|
<widgets-loading-spinner size="la-lg" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Series name overlay -->
|
<!-- Series name overlay -->
|
||||||
<div v-if="booksInSeries && libraryItem && isHovering" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-60 rounded flex items-center justify-center" :style="{ padding: sizeMultiplier + 'rem' }">
|
<div v-if="booksInSeries && libraryItem && isHovering" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-60 rounded flex items-center justify-center" :style="{ padding: sizeMultiplier + 'rem' }">
|
||||||
<p class="text-gray-200 text-center" :style="{ fontSize: 1.1 * sizeMultiplier + 'rem' }">{{ series }}</p>
|
<p class="text-gray-200 text-center" :style="{ fontSize: 1.1 * sizeMultiplier + 'rem' }">{{ series }}</p>
|
||||||
@@ -88,7 +93,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Podcast Episode # -->
|
<!-- Podcast Episode # -->
|
||||||
<div v-if="recentEpisodeNumber && !isHovering && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
|
<div v-if="recentEpisodeNumber && !isHovering && !isSelectionMode && !processing" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
|
||||||
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">Episode #{{ recentEpisodeNumber }}</p>
|
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">Episode #{{ recentEpisodeNumber }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -129,10 +134,9 @@ export default {
|
|||||||
return {
|
return {
|
||||||
isHovering: false,
|
isHovering: false,
|
||||||
isMoreMenuOpen: false,
|
isMoreMenuOpen: false,
|
||||||
isProcessingReadUpdate: false,
|
processing: false,
|
||||||
libraryItem: null,
|
libraryItem: null,
|
||||||
imageReady: false,
|
imageReady: false,
|
||||||
rescanning: false,
|
|
||||||
selected: false,
|
selected: false,
|
||||||
isSelectionMode: false,
|
isSelectionMode: false,
|
||||||
showCoverBg: false
|
showCoverBg: false
|
||||||
@@ -382,12 +386,14 @@ export default {
|
|||||||
{
|
{
|
||||||
func: 'toggleFinished',
|
func: 'toggleFinished',
|
||||||
text: `Mark as ${this.itemIsFinished ? 'Not Finished' : 'Finished'}`
|
text: `Mark as ${this.itemIsFinished ? 'Not Finished' : 'Finished'}`
|
||||||
},
|
|
||||||
{
|
|
||||||
func: 'openCollections',
|
|
||||||
text: 'Add to Collection'
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
if (this.userCanUpdate) {
|
||||||
|
items.push({
|
||||||
|
func: 'openCollections',
|
||||||
|
text: 'Add to Collection'
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (this.userCanUpdate) {
|
if (this.userCanUpdate) {
|
||||||
items.push({
|
items.push({
|
||||||
@@ -490,6 +496,7 @@ export default {
|
|||||||
this.libraryItem = libraryItem
|
this.libraryItem = libraryItem
|
||||||
},
|
},
|
||||||
clickCard(e) {
|
clickCard(e) {
|
||||||
|
if (this.processing) return
|
||||||
if (this.isSelectionMode) {
|
if (this.isSelectionMode) {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -526,7 +533,7 @@ export default {
|
|||||||
var updatePayload = {
|
var updatePayload = {
|
||||||
isFinished: !this.itemIsFinished
|
isFinished: !this.itemIsFinished
|
||||||
}
|
}
|
||||||
this.isProcessingReadUpdate = true
|
this.processing = true
|
||||||
|
|
||||||
var apiEndpoint = `/api/me/progress/${this.libraryItemId}`
|
var apiEndpoint = `/api/me/progress/${this.libraryItemId}`
|
||||||
if (this.recentEpisode) apiEndpoint += `/${this.recentEpisode.id}`
|
if (this.recentEpisode) apiEndpoint += `/${this.recentEpisode.id}`
|
||||||
@@ -536,12 +543,12 @@ export default {
|
|||||||
axios
|
axios
|
||||||
.$patch(apiEndpoint, updatePayload)
|
.$patch(apiEndpoint, updatePayload)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.isProcessingReadUpdate = false
|
this.processing = false
|
||||||
toast.success(`Item marked as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`)
|
toast.success(`Item marked as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
this.isProcessingReadUpdate = false
|
this.processing = false
|
||||||
toast.error(`Failed to mark as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`)
|
toast.error(`Failed to mark as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -549,11 +556,12 @@ export default {
|
|||||||
this.$emit('editPodcast', this.libraryItem)
|
this.$emit('editPodcast', this.libraryItem)
|
||||||
},
|
},
|
||||||
rescan() {
|
rescan() {
|
||||||
this.rescanning = true
|
if (this.processing) return
|
||||||
this.$axios
|
const axios = this.$axios || this.$nuxt.$axios
|
||||||
|
this.processing = true
|
||||||
|
axios
|
||||||
.$get(`/api/items/${this.libraryItemId}/scan`)
|
.$get(`/api/items/${this.libraryItemId}/scan`)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
this.rescanning = false
|
|
||||||
var result = data.result
|
var result = data.result
|
||||||
if (!result) {
|
if (!result) {
|
||||||
this.$toast.error(`Re-Scan Failed for "${this.title}"`)
|
this.$toast.error(`Re-Scan Failed for "${this.title}"`)
|
||||||
@@ -564,11 +572,12 @@ export default {
|
|||||||
} else if (result === 'REMOVED') {
|
} else if (result === 'REMOVED') {
|
||||||
this.$toast.error(`Re-Scan complete item was removed`)
|
this.$toast.error(`Re-Scan complete item was removed`)
|
||||||
}
|
}
|
||||||
|
this.processing = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to scan library item', error)
|
console.error('Failed to scan library item', error)
|
||||||
this.$toast.error('Failed to scan library item')
|
this.$toast.error('Failed to scan library item')
|
||||||
this.rescanning = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
showEditModalFiles() {
|
showEditModalFiles() {
|
||||||
@@ -647,11 +656,50 @@ export default {
|
|||||||
this.selected = !this.selected
|
this.selected = !this.selected
|
||||||
this.$emit('select', this.libraryItem)
|
this.$emit('select', this.libraryItem)
|
||||||
},
|
},
|
||||||
play() {
|
async play() {
|
||||||
var eventBus = this.$eventBus || this.$nuxt.$eventBus
|
var eventBus = this.$eventBus || this.$nuxt.$eventBus
|
||||||
|
|
||||||
|
const queueItems = []
|
||||||
|
// Podcast episode load queue items
|
||||||
|
if (this.recentEpisode) {
|
||||||
|
const axios = this.$axios || this.$nuxt.$axios
|
||||||
|
this.processing = true
|
||||||
|
const fullLibraryItem = await axios.$get(`/api/items/${this.libraryItemId}`).catch((err) => {
|
||||||
|
console.error('Failed to fetch library item', err)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
this.processing = false
|
||||||
|
|
||||||
|
if (fullLibraryItem && fullLibraryItem.media.episodes) {
|
||||||
|
const episodes = fullLibraryItem.media.episodes || []
|
||||||
|
// Sort from least recent to most recent
|
||||||
|
episodes.sort((a, b) => String(a.publishedAt).localeCompare(String(b.publishedAt), undefined, { numeric: true, sensitivity: 'base' }))
|
||||||
|
|
||||||
|
const episodeIndex = episodes.findIndex((ep) => ep.id === this.recentEpisode.id)
|
||||||
|
if (episodeIndex >= 0) {
|
||||||
|
for (let i = episodeIndex; i < episodes.length; i++) {
|
||||||
|
const episode = episodes[i]
|
||||||
|
const podcastProgress = this.store.getters['user/getUserMediaProgress'](this.libraryItemId, episode.id)
|
||||||
|
if (!podcastProgress || !podcastProgress.isFinished) {
|
||||||
|
queueItems.push({
|
||||||
|
libraryItemId: this.libraryItemId,
|
||||||
|
episodeId: episode.id,
|
||||||
|
title: episode.title,
|
||||||
|
subtitle: this.mediaMetadata.title,
|
||||||
|
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date',
|
||||||
|
duration: episode.audioFile.duration || null,
|
||||||
|
coverPath: this.media.coverPath || null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
eventBus.$emit('play-item', {
|
eventBus.$emit('play-item', {
|
||||||
libraryItemId: this.libraryItemId,
|
libraryItemId: this.libraryItemId,
|
||||||
episodeId: this.recentEpisode ? this.recentEpisode.id : null
|
episodeId: this.recentEpisode ? this.recentEpisode.id : null,
|
||||||
|
queueItems
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
mouseover() {
|
mouseover() {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
|
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
|
||||||
<covers-collection-cover ref="cover" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-collection-cover ref="cover" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
</div>
|
</div>
|
||||||
<div v-show="isHovering" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-40 pointer-events-none">
|
<div v-show="isHovering && userCanUpdate" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-40 pointer-events-none">
|
||||||
<div class="absolute pointer-events-auto" :style="{ top: 0.5 * sizeMultiplier + 'rem', right: 0.5 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickEdit">
|
<div class="absolute pointer-events-auto" :style="{ top: 0.5 * sizeMultiplier + 'rem', right: 0.5 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickEdit">
|
||||||
<span class="material-icons text-xl text-white text-opacity-75 hover:text-opacity-100">edit</span>
|
<span class="material-icons text-xl text-white text-opacity-75 hover:text-opacity-100">edit</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -30,7 +30,12 @@ export default {
|
|||||||
bookshelfView: {
|
bookshelfView: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0
|
default: 0
|
||||||
}
|
},
|
||||||
|
collectionMount: {
|
||||||
|
type: Object,
|
||||||
|
default: () => null
|
||||||
|
},
|
||||||
|
isTag: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -64,6 +69,9 @@ export default {
|
|||||||
isAlternativeBookshelfView() {
|
isAlternativeBookshelfView() {
|
||||||
const constants = this.$constants || this.$nuxt.$constants
|
const constants = this.$constants || this.$nuxt.$constants
|
||||||
return this.bookshelfView == constants.BookshelfView.TITLES
|
return this.bookshelfView == constants.BookshelfView.TITLES
|
||||||
|
},
|
||||||
|
userCanUpdate() {
|
||||||
|
return this.store.getters['user/getUserCanUpdate']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -99,6 +107,10 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {
|
||||||
|
if (this.collectionMount) {
|
||||||
|
this.setEntity(this.collectionMount)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<div ref="card" :id="`series-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
<div ref="card" :id="`series-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||||
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
||||||
<div class="w-full h-full bg-primary relative rounded overflow-hidden z-0">
|
<div class="w-full h-full bg-primary relative rounded overflow-hidden z-0">
|
||||||
<covers-group-cover v-if="series" ref="cover" :id="seriesId" :name="displayTitle" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" :group-to="seriesBooksRoute" />
|
<covers-group-cover v-if="series" ref="cover" :id="seriesId" :name="displayTitle" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="absolute z-10 top-1.5 right-1.5 rounded-md leading-3 text-sm p-1 font-semibold text-white flex items-center justify-center" style="background-color: #cd9d49dd">{{ books.length }}</div>
|
<div class="absolute z-10 top-1.5 right-1.5 rounded-md leading-3 text-sm p-1 font-semibold text-white flex items-center justify-center" style="background-color: #cd9d49dd">{{ books.length }}</div>
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ export default {
|
|||||||
},
|
},
|
||||||
width: Number,
|
width: Number,
|
||||||
height: Number,
|
height: Number,
|
||||||
groupTo: String,
|
|
||||||
bookCoverAspectRatio: Number
|
bookCoverAspectRatio: Number
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p v-if="!imageFailed" class="absolute -bottom-5 left-0 right-0 mx-auto text-xs text-gray-300 text-center">{{ resolution }}</p>
|
<p v-if="!imageFailed && showResolution" class="absolute -bottom-5 left-0 right-0 mx-auto text-xs text-gray-300 text-center">{{ resolution }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -31,7 +31,11 @@ export default {
|
|||||||
default: 120
|
default: 120
|
||||||
},
|
},
|
||||||
showOpenNewTab: Boolean,
|
showOpenNewTab: Boolean,
|
||||||
bookCoverAspectRatio: Number
|
bookCoverAspectRatio: Number,
|
||||||
|
showResolution: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute bottom-0 left-0 right-0 w-full py-2 px-4 flex">
|
<div class="absolute bottom-0 left-0 right-0 w-full py-2 px-4 flex">
|
||||||
<ui-btn small color="error" type="button" @click.stop="removeClick">Remove</ui-btn>
|
<ui-btn v-if="userCanDelete" small color="error" type="button" @click.stop="removeClick">Remove</ui-btn>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn color="success" type="submit">Save</ui-btn>
|
<ui-btn color="success" type="submit">Save</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
@@ -85,6 +85,9 @@ export default {
|
|||||||
},
|
},
|
||||||
books() {
|
books() {
|
||||||
return this.collection.books || []
|
return this.collection.books || []
|
||||||
|
},
|
||||||
|
userCanDelete() {
|
||||||
|
return this.$store.getters['user/getUserCanDelete']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -12,11 +12,11 @@
|
|||||||
|
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|
||||||
<ui-tooltip v-if="mediaType == 'book'" :disabled="!!quickMatching" :text="`(Root User Only) Populate empty book details & cover with first book result from '${libraryProvider}'. Does not overwrite details.`" direction="bottom" class="mr-2 md:mr-4">
|
<ui-tooltip :disabled="!!quickMatching" :text="`Populate empty ${mediaType} details & cover with first ${mediaType} result from '${libraryProvider}'. Does not overwrite details unless 'Prefer matched metadata' server setting is enabled.`" direction="bottom" class="mr-2 md:mr-4">
|
||||||
<ui-btn v-if="userIsAdminOrUp" :loading="quickMatching" color="bg" type="button" class="h-full" small @click.stop.prevent="quickMatch">Quick Match</ui-btn>
|
<ui-btn v-if="userIsAdminOrUp" :loading="quickMatching" color="bg" type="button" class="h-full" small @click.stop.prevent="quickMatch">Quick Match</ui-btn>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-tooltip :disabled="!!libraryScan" text="(Root User Only) Rescan audiobook including metadata" direction="bottom" class="mr-2 md:mr-4">
|
<ui-tooltip :disabled="!!libraryScan" text="Rescan library item including metadata" direction="bottom" class="mr-2 md:mr-4">
|
||||||
<ui-btn v-if="userIsAdminOrUp && !isFile" :loading="rescanning" :disabled="!!libraryScan" color="bg" type="button" class="h-full" small @click.stop.prevent="rescan">Re-Scan</ui-btn>
|
<ui-btn v-if="userIsAdminOrUp && !isFile" :loading="rescanning" :disabled="!!libraryScan" color="bg" type="button" class="h-full" small @click.stop.prevent="rescan">Re-Scan</ui-btn>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,15 @@
|
|||||||
<div class="w-full h-full overflow-y-auto overflow-x-hidden px-4 py-6">
|
<div class="w-full h-full overflow-y-auto overflow-x-hidden px-4 py-6">
|
||||||
<div class="w-full mb-4">
|
<div class="w-full mb-4">
|
||||||
<div v-if="userIsAdminOrUp" class="flex items-end justify-end mb-4">
|
<div v-if="userIsAdminOrUp" class="flex items-end justify-end mb-4">
|
||||||
<!-- <p v-if="autoDownloadEpisodes">Last new episode check {{ $formatDate(lastEpisodeCheck) }}</p> -->
|
|
||||||
<ui-text-input-with-label ref="lastCheckInput" v-model="lastEpisodeCheckInput" :disabled="checkingNewEpisodes" type="datetime-local" label="Look for new episodes after this date" class="max-w-xs mr-2" />
|
<ui-text-input-with-label ref="lastCheckInput" v-model="lastEpisodeCheckInput" :disabled="checkingNewEpisodes" type="datetime-local" label="Look for new episodes after this date" class="max-w-xs mr-2" />
|
||||||
|
<ui-text-input-with-label ref="maxEpisodesInput" v-model="maxEpisodesToDownload" :disabled="checkingNewEpisodes" type="number" label="Max episodes" class="w-16 mr-2" input-class="h-10">
|
||||||
|
<div class="flex -mb-0.5">
|
||||||
|
<p class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': checkingNewEpisodes }">Limit</p>
|
||||||
|
<ui-tooltip direction="top" text="Max # of episodes to download. Use 0 for unlimited.">
|
||||||
|
<span class="material-icons text-base">info_outlined</span>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
|
</ui-text-input-with-label>
|
||||||
<ui-btn :loading="checkingNewEpisodes" @click="checkForNewEpisodes">Check & Download New Episodes</ui-btn>
|
<ui-btn :loading="checkingNewEpisodes" @click="checkForNewEpisodes">Check & Download New Episodes</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -52,7 +59,8 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
checkingNewEpisodes: false,
|
checkingNewEpisodes: false,
|
||||||
lastEpisodeCheckInput: null
|
lastEpisodeCheckInput: null,
|
||||||
|
maxEpisodesToDownload: 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -89,6 +97,16 @@ export default {
|
|||||||
if (this.$refs.lastCheckInput) {
|
if (this.$refs.lastCheckInput) {
|
||||||
this.$refs.lastCheckInput.blur()
|
this.$refs.lastCheckInput.blur()
|
||||||
}
|
}
|
||||||
|
if (this.$refs.maxEpisodesInput) {
|
||||||
|
this.$refs.maxEpisodesInput.blur()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.maxEpisodesToDownload < 0) {
|
||||||
|
this.maxEpisodesToDownload = 3
|
||||||
|
this.$toast.error('Invalid max episodes to download')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.checkingNewEpisodes = true
|
this.checkingNewEpisodes = true
|
||||||
const lastEpisodeCheck = new Date(this.lastEpisodeCheckInput).valueOf()
|
const lastEpisodeCheck = new Date(this.lastEpisodeCheckInput).valueOf()
|
||||||
|
|
||||||
@@ -102,7 +120,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.$axios
|
this.$axios
|
||||||
.$get(`/api/podcasts/${this.libraryItemId}/checknew`)
|
.$get(`/api/podcasts/${this.libraryItemId}/checknew?limit=${this.maxEpisodesToDownload}`)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.episodes && response.episodes.length) {
|
if (response.episodes && response.episodes.length) {
|
||||||
console.log('New episodes', response.episodes.length)
|
console.log('New episodes', response.episodes.length)
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="item" class="w-full flex items-center px-4 py-2" :class="wrapperClass" @mouseover="mouseover" @mouseleave="mouseleave">
|
||||||
|
<covers-preview-cover :src="coverUrl" :width="48" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" />
|
||||||
|
<div class="flex-grow px-2 py-1 queue-item-row-content truncate">
|
||||||
|
<p class="text-gray-200 text-sm truncate">{{ title }}</p>
|
||||||
|
<p class="text-gray-300 text-sm">{{ subtitle }}</p>
|
||||||
|
<p v-if="caption" class="text-gray-400 text-xs">{{ caption }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-28">
|
||||||
|
<p v-if="isOpenInPlayer" class="text-sm text-right text-gray-400">Streaming</p>
|
||||||
|
<div v-else-if="isHovering" class="flex items-center justify-end -mx-1">
|
||||||
|
<button class="outline-none mx-1 flex items-center" @click.stop="playClick">
|
||||||
|
<span class="material-icons text-success">play_arrow</span>
|
||||||
|
</button>
|
||||||
|
<button class="outline-none mx-1 flex items-center" @click.stop="removeClick">
|
||||||
|
<span class="material-icons text-error">close</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p v-else class="text-gray-400 text-sm text-right">{{ durationPretty }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
item: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
|
index: Number
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isHovering: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
title() {
|
||||||
|
return this.item.title || ''
|
||||||
|
},
|
||||||
|
subtitle() {
|
||||||
|
return this.item.subtitle || ''
|
||||||
|
},
|
||||||
|
caption() {
|
||||||
|
return this.item.caption
|
||||||
|
},
|
||||||
|
libraryItemId() {
|
||||||
|
return this.item.libraryItemId
|
||||||
|
},
|
||||||
|
episodeId() {
|
||||||
|
return this.item.episodeId
|
||||||
|
},
|
||||||
|
coverPath() {
|
||||||
|
return this.item.coverPath
|
||||||
|
},
|
||||||
|
coverUrl() {
|
||||||
|
if (!this.coverPath) return '/book_placeholder.jpg'
|
||||||
|
return this.$store.getters['globals/getLibraryItemCoverSrcById'](this.libraryItemId)
|
||||||
|
},
|
||||||
|
bookCoverAspectRatio() {
|
||||||
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
|
},
|
||||||
|
duration() {
|
||||||
|
return this.item.duration
|
||||||
|
},
|
||||||
|
durationPretty() {
|
||||||
|
if (!this.duration) return 'N/A'
|
||||||
|
return this.$elapsedPretty(this.duration)
|
||||||
|
},
|
||||||
|
isOpenInPlayer() {
|
||||||
|
return this.$store.getters['getIsMediaStreaming'](this.libraryItemId, this.episodeId)
|
||||||
|
},
|
||||||
|
wrapperClass() {
|
||||||
|
if (this.isOpenInPlayer) return 'bg-yellow-400 bg-opacity-10'
|
||||||
|
if (this.index % 2 === 0) return 'bg-gray-300 bg-opacity-5 hover:bg-opacity-10'
|
||||||
|
return 'bg-bg hover:bg-gray-300 hover:bg-opacity-10'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
mouseover() {
|
||||||
|
this.isHovering = true
|
||||||
|
},
|
||||||
|
mouseleave() {
|
||||||
|
this.isHovering = false
|
||||||
|
},
|
||||||
|
playClick() {
|
||||||
|
this.$emit('play', this.item)
|
||||||
|
},
|
||||||
|
removeClick() {
|
||||||
|
this.$emit('remove', this.item)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.queue-item-row-content {
|
||||||
|
max-width: calc(100% - 48px - 128px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<template>
|
||||||
|
<modals-modal v-model="show" name="queue-items" :width="800" :height="'unset'">
|
||||||
|
<template #outer>
|
||||||
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
|
<p class="font-book text-3xl text-white truncate">Player Queue</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden py-4" style="max-height: 80vh">
|
||||||
|
<div v-if="show" class="w-full h-full">
|
||||||
|
<div class="pb-4 px-4 flex items-center">
|
||||||
|
<p class="text-base text-gray-200">Player Queue</p>
|
||||||
|
<p class="text-base text-gray-400 px-4">{{ playerQueueItems.length }} Items</p>
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<ui-checkbox v-model="playerQueueAutoPlay" label="Auto Play" medium checkbox-bg="primary" border-color="gray-600" label-class="pl-2 mb-px" />
|
||||||
|
</div>
|
||||||
|
<modals-player-queue-item-row v-for="(item, index) in playerQueueItems" :key="index" :item="item" :index="index" @play="playItem" @remove="removeItem" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</modals-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: Boolean,
|
||||||
|
libraryItemId: String
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
show: {
|
||||||
|
get() {
|
||||||
|
return this.value
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('input', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
playerQueueAutoPlay: {
|
||||||
|
get() {
|
||||||
|
return this.$store.state.playerQueueAutoPlay
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$store.commit('setPlayerQueueAutoPlay', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
playerQueueItems() {
|
||||||
|
return this.$store.state.playerQueueItems || []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
playItem(item) {
|
||||||
|
this.$eventBus.$emit('play-item', {
|
||||||
|
libraryItemId: item.libraryItemId,
|
||||||
|
episodeId: item.episodeId || null,
|
||||||
|
queueItems: this.playerQueueItems
|
||||||
|
})
|
||||||
|
this.show = false
|
||||||
|
},
|
||||||
|
removeItem(item) {
|
||||||
|
const updatedQueue = this.playerQueueItems.filter((i) => {
|
||||||
|
if (!i.episodeId) return i.libraryItemId !== item.libraryItemId
|
||||||
|
return i.libraryItemId !== item.libraryItemId || i.episodeId !== item.episodeId
|
||||||
|
})
|
||||||
|
this.$store.commit('setPlayerQueueItems', updatedQueue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -11,11 +11,11 @@
|
|||||||
v-for="(episode, index) in episodes"
|
v-for="(episode, index) in episodes"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="relative"
|
class="relative"
|
||||||
:class="episode.enclosure && itemEpisodeMap[episode.enclosure.url] ? 'bg-primary bg-opacity-40' : selectedEpisodes[String(index)] ? 'cursor-pointer bg-success bg-opacity-10' : index % 2 == 0 ? 'cursor-pointer bg-primary bg-opacity-25 hover:bg-opacity-40' : 'cursor-pointer bg-primary bg-opacity-5 hover:bg-opacity-25'"
|
:class="itemEpisodeMap[episode.enclosure.url] ? 'bg-primary bg-opacity-40' : selectedEpisodes[String(index)] ? 'cursor-pointer bg-success bg-opacity-10' : index % 2 == 0 ? 'cursor-pointer bg-primary bg-opacity-25 hover:bg-opacity-40' : 'cursor-pointer bg-primary bg-opacity-5 hover:bg-opacity-25'"
|
||||||
@click="toggleSelectEpisode(index)"
|
@click="toggleSelectEpisode(index, episode)"
|
||||||
>
|
>
|
||||||
<div class="absolute top-0 left-0 h-full flex items-center p-2">
|
<div class="absolute top-0 left-0 h-full flex items-center p-2">
|
||||||
<span v-if="episode.enclosure && itemEpisodeMap[episode.enclosure.url]" class="material-icons text-success text-xl">download_done</span>
|
<span v-if="itemEpisodeMap[episode.enclosure.url]" class="material-icons text-success text-xl">download_done</span>
|
||||||
<ui-checkbox v-else v-model="selectedEpisodes[String(index)]" small checkbox-bg="primary" border-color="gray-600" />
|
<ui-checkbox v-else v-model="selectedEpisodes[String(index)]" small checkbox-bg="primary" border-color="gray-600" />
|
||||||
</div>
|
</div>
|
||||||
<div class="px-8 py-2">
|
<div class="px-8 py-2">
|
||||||
@@ -23,20 +23,13 @@
|
|||||||
<p class="break-words mb-1">{{ episode.title }}</p>
|
<p class="break-words mb-1">{{ episode.title }}</p>
|
||||||
<p v-if="episode.subtitle" class="break-words mb-1 text-sm text-gray-300 episode-subtitle">{{ episode.subtitle }}</p>
|
<p v-if="episode.subtitle" class="break-words mb-1 text-sm text-gray-300 episode-subtitle">{{ episode.subtitle }}</p>
|
||||||
<p class="text-xs text-gray-300">Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}</p>
|
<p class="text-xs text-gray-300">Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}</p>
|
||||||
<!-- <span class="material-icons cursor-pointer text-lg hover:text-success" @click="saveEpisode(episode)">save</span> -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end pt-4">
|
<div class="flex justify-end pt-4">
|
||||||
<div class="relative">
|
<ui-checkbox v-if="!allDownloaded" v-model="selectAll" @input="toggleSelectAll" label="Select all episodes" small checkbox-bg="primary" border-color="gray-600" class="mx-8" />
|
||||||
<div class="absolute top-0 left-0 h-full flex items-center p-2">
|
<ui-btn v-if="!allDownloaded" :disabled="!episodesSelected.length" @click="submit">{{ buttonText }}</ui-btn>
|
||||||
<ui-checkbox v-model="selectAll" small checkbox-bg="primary" border-color="gray-600" :disabled="allDownloaded" />
|
<p v-else class="text-success text-base px-2 py-4">All episodes are downloaded</p>
|
||||||
</div>
|
|
||||||
<div class="px-8 py-2">
|
|
||||||
<p :class="!allDownloaded ? 'font-semibold text-gray-200' : 'text-gray-400'">Select all episodes</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ui-btn :disabled="!episodesSelected.length" @click="submit">{{ buttonText }}</ui-btn>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</modals-modal>
|
</modals-modal>
|
||||||
@@ -58,7 +51,8 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
processing: false,
|
processing: false,
|
||||||
selectedEpisodes: {}
|
selectedEpisodes: {},
|
||||||
|
selectAll: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -78,22 +72,12 @@ export default {
|
|||||||
this.$emit('input', val)
|
this.$emit('input', val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
selectAll: {
|
|
||||||
get() {
|
|
||||||
return this.episodesSelected.length == this.episodes.filter((_, index) => !(this.episodes[index].enclosure && this.itemEpisodeMap[this.episodes[index].enclosure.url])).length
|
|
||||||
},
|
|
||||||
set(val) {
|
|
||||||
for (const key in this.selectedEpisodes) {
|
|
||||||
this.selectedEpisodes[key] = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
title() {
|
title() {
|
||||||
if (!this.libraryItem) return ''
|
if (!this.libraryItem) return ''
|
||||||
return this.libraryItem.media.metadata.title || 'Unknown'
|
return this.libraryItem.media.metadata.title || 'Unknown'
|
||||||
},
|
},
|
||||||
allDownloaded() {
|
allDownloaded() {
|
||||||
return Object.values(this.episodes).filter((episode) => !(episode.enclosure && this.itemEpisodeMap[episode.enclosure.url])).length === 0
|
return !this.episodes.some((episode) => !this.itemEpisodeMap[episode.enclosure.url])
|
||||||
},
|
},
|
||||||
episodesSelected() {
|
episodesSelected() {
|
||||||
return Object.keys(this.selectedEpisodes).filter((key) => !!this.selectedEpisodes[key])
|
return Object.keys(this.selectedEpisodes).filter((key) => !!this.selectedEpisodes[key])
|
||||||
@@ -115,8 +99,27 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleSelectEpisode(index) {
|
toggleSelectAll(val) {
|
||||||
|
for (let i = 0; i < this.episodes.length; i++) {
|
||||||
|
const episode = this.episodes[i]
|
||||||
|
if (this.itemEpisodeMap[episode.enclosure.url]) this.selectedEpisodes[String(i)] = false
|
||||||
|
else this.$set(this.selectedEpisodes, String(i), val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
checkSetIsSelectedAll() {
|
||||||
|
for (let i = 0; i < this.episodes.length; i++) {
|
||||||
|
const episode = this.episodes[i]
|
||||||
|
if (!this.itemEpisodeMap[episode.enclosure.url] && !this.selectedEpisodes[String(i)]) {
|
||||||
|
this.selectAll = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.selectAll = true
|
||||||
|
},
|
||||||
|
toggleSelectEpisode(index, episode) {
|
||||||
|
if (this.itemEpisodeMap[episode.enclosure.url]) return
|
||||||
this.$set(this.selectedEpisodes, String(index), !this.selectedEpisodes[String(index)])
|
this.$set(this.selectedEpisodes, String(index), !this.selectedEpisodes[String(index)])
|
||||||
|
this.checkSetIsSelectedAll()
|
||||||
},
|
},
|
||||||
submit() {
|
submit() {
|
||||||
var episodesToDownload = []
|
var episodesToDownload = []
|
||||||
@@ -145,17 +148,15 @@ export default {
|
|||||||
console.error('Failed to download episodes', error)
|
console.error('Failed to download episodes', error)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
this.$toast.error(errorMsg)
|
this.$toast.error(errorMsg)
|
||||||
|
|
||||||
|
this.selectedEpisodes = {}
|
||||||
|
this.selectAll = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
init() {
|
init() {
|
||||||
this.episodes.sort((a, b) => (a.publishedAt < b.publishedAt) ? 1 : -1)
|
this.episodes.sort((a, b) => (a.publishedAt < b.publishedAt ? 1 : -1))
|
||||||
for (let i = 0; i < this.episodes.length; i++) {
|
this.selectAll = false
|
||||||
var episode = this.episodes[i]
|
this.selectedEpisodes = {}
|
||||||
if (episode.enclosure && !this.itemEpisodeMap[episode.enclosure.url]) {
|
|
||||||
// Do not include episodes already downloaded
|
|
||||||
this.$set(this.selectedEpisodes, String(i), false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
@@ -170,4 +171,4 @@ export default {
|
|||||||
#episodes-scroll {
|
#episodes-scroll {
|
||||||
max-height: calc(80vh - 200px);
|
max-height: calc(80vh - 200px);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -27,6 +27,10 @@
|
|||||||
<span class="material-icons text-2xl sm:text-3xl transform transition-transform" :class="useChapterTrack ? 'rotate-180' : ''">timelapse</span>
|
<span class="material-icons text-2xl sm:text-3xl transform transition-transform" :class="useChapterTrack ? 'rotate-180' : ''">timelapse</span>
|
||||||
</div>
|
</div>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
|
<button v-if="playerQueueItems.length" class="outline-none text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showPlayerQueueItems')">
|
||||||
|
<span class="material-icons text-2xl sm:text-3xl">playlist_play</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<player-playback-controls :loading="loading" :seek-loading="seekLoading" :playback-rate.sync="playbackRate" :paused="paused" :has-next-chapter="hasNextChapter" @prevChapter="prevChapter" @nextChapter="nextChapter" @jumpForward="jumpForward" @jumpBackward="jumpBackward" @setPlaybackRate="setPlaybackRate" @playPause="playPause" />
|
<player-playback-controls :loading="loading" :seek-loading="seekLoading" :playback-rate.sync="playbackRate" :paused="paused" :has-next-chapter="hasNextChapter" @prevChapter="prevChapter" @nextChapter="nextChapter" @jumpForward="jumpForward" @jumpBackward="jumpBackward" @setPlaybackRate="setPlaybackRate" @playPause="playPause" />
|
||||||
@@ -138,6 +142,9 @@ export default {
|
|||||||
hasNextChapter() {
|
hasNextChapter() {
|
||||||
if (!this.chapters.length) return false
|
if (!this.chapters.length) return false
|
||||||
return this.currentChapterIndex < this.chapters.length - 1
|
return this.currentChapterIndex < this.chapters.length - 1
|
||||||
|
},
|
||||||
|
playerQueueItems() {
|
||||||
|
return this.$store.state.playerQueueItems || []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -27,15 +27,15 @@
|
|||||||
<span class="material-icons text-lg text-white text-opacity-70 hover:text-opacity-100 cursor-pointer">radio_button_unchecked</span>
|
<span class="material-icons text-lg text-white text-opacity-70 hover:text-opacity-100 cursor-pointer">radio_button_unchecked</span>
|
||||||
</div> -->
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
<div class="w-40 absolute top-0 -right-24 h-full transform transition-transform" :class="!isHovering ? 'translate-x-0' : '-translate-x-24'">
|
<div class="w-40 absolute top-0 -right-24 h-full transform transition-transform" :class="!isHovering ? 'translate-x-0' : translateDistance">
|
||||||
<div class="flex h-full items-center">
|
<div class="flex h-full items-center">
|
||||||
<ui-tooltip :text="userIsFinished ? 'Mark as Not Finished' : 'Mark as Finished'" direction="top">
|
<ui-tooltip :text="userIsFinished ? 'Mark as Not Finished' : 'Mark as Finished'" direction="top">
|
||||||
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" borderless class="mx-1 mt-0.5" @click="toggleFinished" />
|
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" borderless class="mx-1 mt-0.5" @click="toggleFinished" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
<div class="mx-1" :class="isHovering ? '' : 'ml-6'">
|
<div v-if="userCanUpdate" class="mx-1" :class="isHovering ? '' : 'ml-6'">
|
||||||
<ui-icon-btn icon="edit" borderless @click="clickEdit" />
|
<ui-icon-btn icon="edit" borderless @click="clickEdit" />
|
||||||
</div>
|
</div>
|
||||||
<div class="mx-1">
|
<div v-if="userCanDelete" class="mx-1">
|
||||||
<ui-icon-btn icon="close" borderless @click="removeClick" />
|
<ui-icon-btn icon="close" borderless @click="removeClick" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -71,6 +71,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
translateDistance() {
|
||||||
|
if (!this.userCanUpdate && !this.userCanDelete) return 'translate-x-0'
|
||||||
|
else if (!this.userCanUpdate || !this.userCanDelete) return '-translate-x-12'
|
||||||
|
return '-translate-x-24'
|
||||||
|
},
|
||||||
media() {
|
media() {
|
||||||
return this.book.media || {}
|
return this.book.media || {}
|
||||||
},
|
},
|
||||||
@@ -113,6 +118,12 @@ export default {
|
|||||||
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
|
||||||
|
},
|
||||||
|
userCanUpdate() {
|
||||||
|
return this.$store.getters['user/getUserCanUpdate']
|
||||||
|
},
|
||||||
|
userCanDelete() {
|
||||||
|
return this.$store.getters['user/getUserCanDelete']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -133,10 +133,7 @@ export default {
|
|||||||
if (this.streamIsPlaying) {
|
if (this.streamIsPlaying) {
|
||||||
this.$eventBus.$emit('pause-item')
|
this.$eventBus.$emit('pause-item')
|
||||||
} else {
|
} else {
|
||||||
this.$eventBus.$emit('play-item', {
|
this.$emit('play', this.episode)
|
||||||
libraryItemId: this.libraryItemId,
|
|
||||||
episodeId: this.episode.id
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
toggleFinished(confirmed = false) {
|
toggleFinished(confirmed = false) {
|
||||||
|
|||||||
@@ -4,14 +4,17 @@
|
|||||||
<p class="text-lg mb-0 font-semibold">Episodes</p>
|
<p class="text-lg mb-0 font-semibold">Episodes</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<template v-if="isSelectionMode">
|
<template v-if="isSelectionMode">
|
||||||
<ui-btn color="error" small @click="removeSelectedEpisodes">Remove {{ selectedEpisodes.length }} episode{{ selectedEpisodes.length > 1 ? 's' : '' }}</ui-btn>
|
<ui-tooltip :text="`Mark as ${selectedIsFinished ? 'Not Finished' : 'Finished'}`" direction="bottom">
|
||||||
<ui-btn small class="ml-2" @click="clearSelected">Cancel</ui-btn>
|
<ui-read-icon-btn :disabled="processing" :is-read="selectedIsFinished" @click="toggleBatchFinished" class="mx-1.5" />
|
||||||
|
</ui-tooltip>
|
||||||
|
<ui-btn color="error" :disabled="processing" small class="h-9" @click="removeSelectedEpisodes">Remove {{ selectedEpisodes.length }} episode{{ selectedEpisodes.length > 1 ? 's' : '' }}</ui-btn>
|
||||||
|
<ui-btn :disabled="processing" small class="ml-2 h-9" @click="clearSelected">Cancel</ui-btn>
|
||||||
</template>
|
</template>
|
||||||
<controls-episode-sort-select v-else v-model="sortKey" :descending.sync="sortDesc" class="w-36 sm:w-44 md:w-48 h-9 ml-1 sm:ml-4" />
|
<controls-episode-sort-select v-else v-model="sortKey" :descending.sync="sortDesc" class="w-36 sm:w-44 md:w-48 h-9 ml-1 sm:ml-4" />
|
||||||
</div>
|
</div>
|
||||||
<p v-if="!episodes.length" class="py-4 text-center text-lg">No Episodes</p>
|
<p v-if="!episodes.length" class="py-4 text-center text-lg">No Episodes</p>
|
||||||
<template v-for="episode in episodesSorted">
|
<template v-for="episode in episodesSorted">
|
||||||
<tables-podcast-episode-table-row ref="episodeRow" :key="episode.id" :episode="episode" :library-item-id="libraryItem.id" :selection-mode="isSelectionMode" class="item" @remove="removeEpisode" @edit="editEpisode" @view="viewEpisode" @selected="episodeSelected" />
|
<tables-podcast-episode-table-row ref="episodeRow" :key="episode.id" :episode="episode" :library-item-id="libraryItem.id" :selection-mode="isSelectionMode" class="item" @play="playEpisode" @remove="removeEpisode" @edit="editEpisode" @view="viewEpisode" @selected="episodeSelected" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<modals-podcast-remove-episode v-model="showPodcastRemoveModal" @input="removeEpisodeModalToggled" :library-item="libraryItem" :episodes="episodesToRemove" @clearSelected="clearSelected" />
|
<modals-podcast-remove-episode v-model="showPodcastRemoveModal" @input="removeEpisodeModalToggled" :library-item="libraryItem" :episodes="episodesToRemove" @clearSelected="clearSelected" />
|
||||||
@@ -34,7 +37,8 @@ export default {
|
|||||||
selectedEpisode: null,
|
selectedEpisode: null,
|
||||||
showPodcastRemoveModal: false,
|
showPodcastRemoveModal: false,
|
||||||
selectedEpisodes: [],
|
selectedEpisodes: [],
|
||||||
episodesToRemove: []
|
episodesToRemove: [],
|
||||||
|
processing: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -65,9 +69,40 @@ export default {
|
|||||||
}
|
}
|
||||||
return String(a[this.sortKey]).localeCompare(String(b[this.sortKey]), undefined, { numeric: true, sensitivity: 'base' })
|
return String(a[this.sortKey]).localeCompare(String(b[this.sortKey]), undefined, { numeric: true, sensitivity: 'base' })
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
selectedIsFinished() {
|
||||||
|
// Find an item that is not finished, if none then all items finished
|
||||||
|
return !this.selectedEpisodes.find((episode) => {
|
||||||
|
var itemProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItem.id, episode.id)
|
||||||
|
return !itemProgress || !itemProgress.isFinished
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
toggleBatchFinished() {
|
||||||
|
this.processing = true
|
||||||
|
var newIsFinished = !this.selectedIsFinished
|
||||||
|
var updateProgressPayloads = this.selectedEpisodes.map((episode) => {
|
||||||
|
return {
|
||||||
|
libraryItemId: this.libraryItem.id,
|
||||||
|
episodeId: episode.id,
|
||||||
|
isFinished: newIsFinished
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.$axios
|
||||||
|
.patch(`/api/me/progress/batch/update`, updateProgressPayloads)
|
||||||
|
.then(() => {
|
||||||
|
this.$toast.success('Batch update success!')
|
||||||
|
this.processing = false
|
||||||
|
this.clearSelected()
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.$toast.error('Batch update failed')
|
||||||
|
console.error('Failed to batch update read/not read', error)
|
||||||
|
this.processing = false
|
||||||
|
})
|
||||||
|
},
|
||||||
removeEpisodeModalToggled(val) {
|
removeEpisodeModalToggled(val) {
|
||||||
if (!val) this.episodesToRemove = []
|
if (!val) this.episodesToRemove = []
|
||||||
},
|
},
|
||||||
@@ -91,6 +126,33 @@ export default {
|
|||||||
this.selectedEpisodes = this.selectedEpisodes.filter((ep) => ep.id !== episode.id)
|
this.selectedEpisodes = this.selectedEpisodes.filter((ep) => ep.id !== episode.id)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
playEpisode(episode) {
|
||||||
|
const queueItems = []
|
||||||
|
|
||||||
|
const episodesInListeningOrder = this.episodesCopy.map((ep) => ({ ...ep })).sort((a, b) => String(a.publishedAt).localeCompare(String(b.publishedAt), undefined, { numeric: true, sensitivity: 'base' }))
|
||||||
|
const episodeIndex = episodesInListeningOrder.findIndex((e) => e.id === episode.id)
|
||||||
|
for (let i = episodeIndex; i < episodesInListeningOrder.length; i++) {
|
||||||
|
const episode = episodesInListeningOrder[i]
|
||||||
|
const podcastProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItem.id, episode.id)
|
||||||
|
if (!podcastProgress || !podcastProgress.isFinished) {
|
||||||
|
queueItems.push({
|
||||||
|
libraryItemId: this.libraryItem.id,
|
||||||
|
episodeId: episode.id,
|
||||||
|
title: episode.title,
|
||||||
|
subtitle: this.mediaMetadata.title,
|
||||||
|
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date',
|
||||||
|
duration: episode.audioFile.duration || null,
|
||||||
|
coverPath: this.media.coverPath || null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$eventBus.$emit('play-item', {
|
||||||
|
libraryItemId: this.libraryItem.id,
|
||||||
|
episodeId: episode.id,
|
||||||
|
queueItems
|
||||||
|
})
|
||||||
|
},
|
||||||
removeEpisode(episode) {
|
removeEpisode(episode) {
|
||||||
this.episodesToRemove = [episode]
|
this.episodesToRemove = [episode]
|
||||||
this.showPodcastRemoveModal = true
|
this.showPodcastRemoveModal = true
|
||||||
|
|||||||
@@ -55,9 +55,10 @@ export default {
|
|||||||
},
|
},
|
||||||
labelClassname() {
|
labelClassname() {
|
||||||
if (this.labelClass) return this.labelClass
|
if (this.labelClass) return this.labelClass
|
||||||
var classes = ['pl-1']
|
var classes = []
|
||||||
if (this.small) classes.push('text-xs md:text-sm')
|
if (this.small) classes.push('text-xs md:text-sm pl-1')
|
||||||
else if (this.medium) classes.push('text-base md:text-lg')
|
else if (this.medium) classes.push('text-base md:text-lg pl-2')
|
||||||
|
else classes.push('pl-2')
|
||||||
return classes.join(' ')
|
return classes.join(' ')
|
||||||
},
|
},
|
||||||
svgClass() {
|
svgClass() {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
{{ label }}<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em>
|
{{ label }}<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em>
|
||||||
</p>
|
</p>
|
||||||
</slot>
|
</slot>
|
||||||
<ui-text-input ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" class="w-full" @blur="inputBlurred" />
|
<ui-text-input ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" class="w-full" :class="inputClass" @blur="inputBlurred" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -20,7 +20,8 @@ export default {
|
|||||||
default: 'text'
|
default: 'text'
|
||||||
},
|
},
|
||||||
readonly: Boolean,
|
readonly: Boolean,
|
||||||
disabled: Boolean
|
disabled: Boolean,
|
||||||
|
inputClass: String
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
|
|||||||
@@ -147,6 +147,16 @@ export default {
|
|||||||
margin-top: -2px;
|
margin-top: -2px;
|
||||||
margin-left: -2px;
|
margin-left: -2px;
|
||||||
}
|
}
|
||||||
|
.la-ball-spin-clockwise.la-lg {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise.la-lg > div {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
margin-top: -4px;
|
||||||
|
margin-left: -4px;
|
||||||
|
}
|
||||||
.la-ball-spin-clockwise.la-2x {
|
.la-ball-spin-clockwise.la-2x {
|
||||||
width: 64px;
|
width: 64px;
|
||||||
height: 64px;
|
height: 64px;
|
||||||
|
|||||||
+13
-19
@@ -111,21 +111,8 @@ export default {
|
|||||||
reconnectFailed() {
|
reconnectFailed() {
|
||||||
console.error('[SOCKET] reconnect failed')
|
console.error('[SOCKET] reconnect failed')
|
||||||
},
|
},
|
||||||
init(payload, count = 0) {
|
init(payload) {
|
||||||
if (!this.$refs.streamContainer) {
|
|
||||||
if (count > 20) {
|
|
||||||
console.error('Stream container never mounted')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setTimeout(() => {
|
|
||||||
this.init(payload, ++count)
|
|
||||||
}, 100)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
console.log('Init Payload', payload)
|
console.log('Init Payload', payload)
|
||||||
if (payload.session) {
|
|
||||||
this.$refs.streamContainer.sessionOpen(payload.session)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start scans currently running
|
// Start scans currently running
|
||||||
if (payload.librariesScanning) {
|
if (payload.librariesScanning) {
|
||||||
@@ -535,6 +522,17 @@ export default {
|
|||||||
if (res && res.hasUpdate) this.showUpdateToast(res)
|
if (res && res.hasUpdate) this.showUpdateToast(res)
|
||||||
})
|
})
|
||||||
.catch((err) => console.error(err))
|
.catch((err) => console.error(err))
|
||||||
|
},
|
||||||
|
initLocalStorage() {
|
||||||
|
// If experimental features set in local storage
|
||||||
|
var experimentalFeaturesSaved = localStorage.getItem('experimental')
|
||||||
|
if (experimentalFeaturesSaved === '1') {
|
||||||
|
this.$store.commit('setExperimentalFeatures', true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queue auto play
|
||||||
|
var playerQueueAutoPlay = localStorage.getItem('playerQueueAutoPlay')
|
||||||
|
this.$store.commit('setPlayerQueueAutoPlay', playerQueueAutoPlay !== '0')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeMount() {
|
beforeMount() {
|
||||||
@@ -548,11 +546,7 @@ export default {
|
|||||||
|
|
||||||
this.$store.dispatch('libraries/load')
|
this.$store.dispatch('libraries/load')
|
||||||
|
|
||||||
// If experimental features set in local storage
|
this.initLocalStorage()
|
||||||
var experimentalFeaturesSaved = localStorage.getItem('experimental')
|
|
||||||
if (experimentalFeaturesSaved === '1') {
|
|
||||||
this.$store.commit('setExperimentalFeatures', true)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.checkVersionUpdate()
|
this.checkVersionUpdate()
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.1.4",
|
"version": "2.1.5",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.1.2",
|
"version": "2.1.5",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/axios": "^5.13.6",
|
"@nuxtjs/axios": "^5.13.6",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.1.4",
|
"version": "2.1.5",
|
||||||
"description": "Self-hosted audiobook and podcast client",
|
"description": "Self-hosted audiobook and podcast client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -256,7 +256,6 @@ export default {
|
|||||||
console.log('Chapter already playing', this.isLoadingChapter, this.isPlayingChapter)
|
console.log('Chapter already playing', this.isLoadingChapter, this.isPlayingChapter)
|
||||||
if (this.isLoadingChapter) return
|
if (this.isLoadingChapter) return
|
||||||
if (this.isPlayingChapter) {
|
if (this.isPlayingChapter) {
|
||||||
console.log('Destroying chapter')
|
|
||||||
this.destroyAudioEl()
|
this.destroyAudioEl()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -427,6 +426,9 @@ export default {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.destroyAudioEl()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -19,9 +19,9 @@
|
|||||||
{{ streaming ? 'Streaming' : 'Play' }}
|
{{ streaming ? 'Streaming' : 'Play' }}
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
|
|
||||||
<ui-icon-btn icon="edit" class="mx-0.5" @click="editClick" />
|
<ui-icon-btn v-if="userCanUpdate" icon="edit" class="mx-0.5" @click="editClick" />
|
||||||
|
|
||||||
<ui-icon-btn icon="delete" class="mx-0.5" @click="removeClick" />
|
<ui-icon-btn v-if="userCanDelete" icon="delete" class="mx-0.5" @click="removeClick" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-8 max-w-2xl">
|
<div class="my-8 max-w-2xl">
|
||||||
@@ -92,6 +92,12 @@ export default {
|
|||||||
},
|
},
|
||||||
showPlayButton() {
|
showPlayButton() {
|
||||||
return this.playableBooks.length
|
return this.playableBooks.length
|
||||||
|
},
|
||||||
|
userCanUpdate() {
|
||||||
|
return this.$store.getters['user/getUserCanUpdate']
|
||||||
|
},
|
||||||
|
userCanDelete() {
|
||||||
|
return this.$store.getters['user/getUserCanDelete']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<!-- Item Cover Overlay -->
|
<!-- Item Cover Overlay -->
|
||||||
<div class="absolute top-0 left-0 w-full h-full z-10 bg-black bg-opacity-30 opacity-0 hover:opacity-100 transition-opacity" @mousedown.prevent @mouseup.prevent>
|
<div class="absolute top-0 left-0 w-full h-full z-10 bg-black bg-opacity-30 opacity-0 hover:opacity-100 transition-opacity" @mousedown.prevent @mouseup.prevent>
|
||||||
<div v-show="showPlayButton && !isStreaming" class="h-full flex items-center justify-center pointer-events-none">
|
<div v-show="showPlayButton && !isStreaming" class="h-full flex items-center justify-center pointer-events-none">
|
||||||
<div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto cursor-pointer" @click.stop.prevent="startStream">
|
<div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto cursor-pointer" @click.stop.prevent="playItem">
|
||||||
<span class="material-icons text-4xl">play_circle_filled</span>
|
<span class="material-icons text-4xl">play_circle_filled</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -128,7 +128,7 @@
|
|||||||
|
|
||||||
<!-- Icon buttons -->
|
<!-- Icon buttons -->
|
||||||
<div class="flex items-center justify-center md:justify-start pt-4">
|
<div class="flex items-center justify-center md:justify-start pt-4">
|
||||||
<ui-btn v-if="showPlayButton" :disabled="isStreaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="startStream">
|
<ui-btn v-if="showPlayButton" :disabled="isStreaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="playItem">
|
||||||
<span v-show="!isStreaming" class="material-icons -ml-2 pr-1 text-white">play_arrow</span>
|
<span v-show="!isStreaming" class="material-icons -ml-2 pr-1 text-white">play_arrow</span>
|
||||||
{{ isStreaming ? 'Playing' : 'Play' }}
|
{{ isStreaming ? 'Playing' : 'Play' }}
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
@@ -150,7 +150,7 @@
|
|||||||
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" class="mx-0.5" @click="toggleFinished" />
|
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" class="mx-0.5" @click="toggleFinished" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-tooltip v-if="!isPodcast" text="Collections" direction="top">
|
<ui-tooltip v-if="!isPodcast && userCanUpdate" text="Collections" direction="top">
|
||||||
<ui-icon-btn icon="collections_bookmark" class="mx-0.5" outlined @click="collectionsClick" />
|
<ui-icon-btn icon="collections_bookmark" class="mx-0.5" outlined @click="collectionsClick" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
@@ -429,14 +429,14 @@ export default {
|
|||||||
message: `Start playback for "${this.title}" at ${this.$secondsToTimestamp(bookmark.time)}?`,
|
message: `Start playback for "${this.title}" at ${this.$secondsToTimestamp(bookmark.time)}?`,
|
||||||
callback: (confirmed) => {
|
callback: (confirmed) => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
this.startStream(bookmark.time)
|
this.playItem(bookmark.time)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
type: 'yesNo'
|
type: 'yesNo'
|
||||||
}
|
}
|
||||||
this.$store.commit('globals/setConfirmPrompt', payload)
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
} else {
|
} else {
|
||||||
this.startStream(bookmark.time)
|
this.playItem(bookmark.time)
|
||||||
}
|
}
|
||||||
this.showBookmarksModal = false
|
this.showBookmarksModal = false
|
||||||
},
|
},
|
||||||
@@ -515,21 +515,43 @@ export default {
|
|||||||
this.$toast.error(`Failed to mark as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`)
|
this.$toast.error(`Failed to mark as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
startStream(startTime = null) {
|
playItem(startTime = null) {
|
||||||
var episodeId = null
|
var episodeId = null
|
||||||
|
const queueItems = []
|
||||||
if (this.isPodcast) {
|
if (this.isPodcast) {
|
||||||
var episode = this.podcastEpisodes.find((ep) => {
|
const episodesInListeningOrder = this.podcastEpisodes.map((ep) => ({ ...ep })).sort((a, b) => String(a.publishedAt).localeCompare(String(b.publishedAt), undefined, { numeric: true, sensitivity: 'base' }))
|
||||||
|
|
||||||
|
// Find most recent episode unplayed
|
||||||
|
var episodeIndex = episodesInListeningOrder.findLastIndex((ep) => {
|
||||||
var podcastProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItemId, ep.id)
|
var podcastProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItemId, ep.id)
|
||||||
return !podcastProgress || !podcastProgress.isFinished
|
return !podcastProgress || !podcastProgress.isFinished
|
||||||
})
|
})
|
||||||
if (!episode) episode = this.podcastEpisodes[0]
|
if (episodeIndex < 0) episodeIndex = 0
|
||||||
episodeId = episode.id
|
|
||||||
|
episodeId = episodesInListeningOrder[episodeIndex].id
|
||||||
|
|
||||||
|
for (let i = episodeIndex; i < episodesInListeningOrder.length; i++) {
|
||||||
|
const episode = episodesInListeningOrder[i]
|
||||||
|
const podcastProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItemId, episode.id)
|
||||||
|
if (!podcastProgress || !podcastProgress.isFinished) {
|
||||||
|
queueItems.push({
|
||||||
|
libraryItemId: this.libraryItemId,
|
||||||
|
episodeId: episode.id,
|
||||||
|
title: episode.title,
|
||||||
|
subtitle: this.title,
|
||||||
|
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date',
|
||||||
|
duration: episode.audioFile.duration || null,
|
||||||
|
coverPath: this.libraryItem.media.coverPath || null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$eventBus.$emit('play-item', {
|
this.$eventBus.$emit('play-item', {
|
||||||
libraryItemId: this.libraryItem.id,
|
libraryItemId: this.libraryItem.id,
|
||||||
episodeId,
|
episodeId,
|
||||||
startTime
|
startTime,
|
||||||
|
queueItems
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
editClick() {
|
editClick() {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page" :class="streamLibraryItem ? 'streaming' : ''">
|
<div class="page" :class="streamLibraryItem ? 'streaming' : ''">
|
||||||
<app-book-shelf-toolbar :page="id || ''" :view-mode.sync="viewMode" />
|
<app-book-shelf-toolbar :page="id || ''" :view-mode.sync="viewMode"/>
|
||||||
<app-lazy-bookshelf :page="id || ''" :view-mode="viewMode" />
|
<app-lazy-bookshelf :page="id || ''" :view-mode="viewMode" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -42,4 +42,4 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {}
|
methods: {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -133,6 +133,8 @@ export default class PlayerHandler {
|
|||||||
|
|
||||||
// TODO: Add listening time between last sync and now?
|
// TODO: Add listening time between last sync and now?
|
||||||
this.sendProgressSync(currentTime)
|
this.sendProgressSync(currentTime)
|
||||||
|
|
||||||
|
this.ctx.mediaFinished(this.libraryItemId, this.episodeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
playerStateChange(state) {
|
playerStateChange(state) {
|
||||||
|
|||||||
@@ -46,6 +46,14 @@ export const getters = {
|
|||||||
return `http://localhost:3333/api/items/${libraryItem.id}/cover?token=${userToken}&ts=${lastUpdate}`
|
return `http://localhost:3333/api/items/${libraryItem.id}/cover?token=${userToken}&ts=${lastUpdate}`
|
||||||
}
|
}
|
||||||
return `/api/items/${libraryItem.id}/cover?token=${userToken}&ts=${lastUpdate}`
|
return `/api/items/${libraryItem.id}/cover?token=${userToken}&ts=${lastUpdate}`
|
||||||
|
},
|
||||||
|
getLibraryItemCoverSrcById: (state, getters, rootState, rootGetters) => (libraryItemId, placeholder = '/book_placeholder.jpg') => {
|
||||||
|
if (!libraryItemId) return placeholder
|
||||||
|
var userToken = rootGetters['user/getToken']
|
||||||
|
if (process.env.NODE_ENV !== 'production') { // Testing
|
||||||
|
return `http://localhost:3333/api/items/${libraryItemId}/cover?token=${userToken}`
|
||||||
|
}
|
||||||
|
return `/api/items/${libraryItemId}/cover?token=${userToken}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ export const state = () => ({
|
|||||||
streamLibraryItem: null,
|
streamLibraryItem: null,
|
||||||
streamEpisodeId: null,
|
streamEpisodeId: null,
|
||||||
streamIsPlaying: false,
|
streamIsPlaying: false,
|
||||||
|
playerQueueItems: [],
|
||||||
|
playerQueueAutoPlay: true,
|
||||||
playerIsFullscreen: false,
|
playerIsFullscreen: false,
|
||||||
editModalTab: 'details',
|
editModalTab: 'details',
|
||||||
showEditModal: false,
|
showEditModal: false,
|
||||||
@@ -144,14 +146,23 @@ export const mutations = {
|
|||||||
state.streamLibraryItem = null
|
state.streamLibraryItem = null
|
||||||
state.streamEpisodeId = null
|
state.streamEpisodeId = null
|
||||||
state.streamIsPlaying = false
|
state.streamIsPlaying = false
|
||||||
|
state.playerQueueItems = []
|
||||||
} else {
|
} else {
|
||||||
state.streamLibraryItem = payload.libraryItem
|
state.streamLibraryItem = payload.libraryItem
|
||||||
state.streamEpisodeId = payload.episodeId || null
|
state.streamEpisodeId = payload.episodeId || null
|
||||||
|
state.playerQueueItems = payload.queueItems || []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setIsPlaying(state, isPlaying) {
|
setIsPlaying(state, isPlaying) {
|
||||||
state.streamIsPlaying = isPlaying
|
state.streamIsPlaying = isPlaying
|
||||||
},
|
},
|
||||||
|
setPlayerQueueItems(state, items) {
|
||||||
|
state.playerQueueItems = items || []
|
||||||
|
},
|
||||||
|
setPlayerQueueAutoPlay(state, autoPlay) {
|
||||||
|
state.playerQueueAutoPlay = !!autoPlay
|
||||||
|
localStorage.setItem('playerQueueAutoPlay', !!autoPlay ? '1' : '0')
|
||||||
|
},
|
||||||
showEditModal(state, libraryItem) {
|
showEditModal(state, libraryItem) {
|
||||||
state.editModalTab = 'details'
|
state.editModalTab = 'details'
|
||||||
state.selectedLibraryItem = libraryItem
|
state.selectedLibraryItem = libraryItem
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ module.exports = {
|
|||||||
safelist: [
|
safelist: [
|
||||||
'bg-success',
|
'bg-success',
|
||||||
'bg-red-600',
|
'bg-red-600',
|
||||||
|
'bg-yellow-400',
|
||||||
'text-green-500',
|
'text-green-500',
|
||||||
'py-1.5',
|
'py-1.5',
|
||||||
'bg-info',
|
'bg-info',
|
||||||
@@ -18,7 +19,8 @@ module.exports = {
|
|||||||
'min-w-5',
|
'min-w-5',
|
||||||
'w-3.5',
|
'w-3.5',
|
||||||
'h-3.5',
|
'h-3.5',
|
||||||
'border-warning'
|
'border-warning',
|
||||||
|
'mb-px'
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
theme: {
|
theme: {
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.1.4",
|
"version": "2.1.5",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.1.2",
|
"version": "2.1.5",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.26.1",
|
"axios": "^0.26.1",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.1.4",
|
"version": "2.1.5",
|
||||||
"description": "Self-hosted audiobook and podcast server",
|
"description": "Self-hosted audiobook and podcast server",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ const commandLineArgs = require('./server/libs/commandLineArgs')
|
|||||||
const options = commandLineArgs(optionDefinitions)
|
const options = commandLineArgs(optionDefinitions)
|
||||||
|
|
||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
if (process.env.TOKEN_SECRET == null) process.env.TOKEN_SECRET = '09f26e402586e2faa8da4c98a35f1b20d6b033c6097befa8be3486a829587fe2f90a832bd3ff9d42710a4da095a2ce285b009f0c3730cd9b8e1af3eb84df6611'
|
|
||||||
process.env.NODE_ENV = 'production'
|
process.env.NODE_ENV = 'production'
|
||||||
|
|
||||||
const server = require('./server/Server')
|
const server = require('./server/Server')
|
||||||
|
|||||||
+1
-21
@@ -440,26 +440,7 @@ class Server {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user has session open
|
Logger.debug(`[Server] User Online ${client.user.username}`)
|
||||||
var session = this.playbackSessionManager.getUserSession(user.id)
|
|
||||||
if (session) {
|
|
||||||
Logger.debug(`[Server] User Online "${client.user.username}" with session open "${session.id}"`)
|
|
||||||
var sessionLibraryItem = this.db.libraryItems.find(li => li.id === session.libraryItemId)
|
|
||||||
if (!sessionLibraryItem) {
|
|
||||||
Logger.error(`[Server] Library Item for session "${session.id}" does not exist "${session.libraryItemId}"`)
|
|
||||||
this.playbackSessionManager.removeSession(session.id)
|
|
||||||
session = null
|
|
||||||
} else if (session.mediaType === 'podcast' && !sessionLibraryItem.media.checkHasEpisode(session.episodeId)) {
|
|
||||||
Logger.error(`[Server] Library Item for session "${session.id}" episode ${session.episodeId} does not exist "${session.libraryItemId}"`)
|
|
||||||
this.playbackSessionManager.removeSession(session.id)
|
|
||||||
session = null
|
|
||||||
}
|
|
||||||
if (session) {
|
|
||||||
session = session.toJSONForClient(sessionLibraryItem)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Logger.debug(`[Server] User Online ${client.user.username}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.io.emit('user_online', client.user.toJSONForPublic(this.playbackSessionManager.sessions, this.db.libraryItems))
|
this.io.emit('user_online', client.user.toJSONForPublic(this.playbackSessionManager.sessions, this.db.libraryItems))
|
||||||
|
|
||||||
@@ -470,7 +451,6 @@ class Server {
|
|||||||
metadataPath: global.MetadataPath,
|
metadataPath: global.MetadataPath,
|
||||||
configPath: global.ConfigPath,
|
configPath: global.ConfigPath,
|
||||||
user: client.user.toJSONForBrowser(),
|
user: client.user.toJSONForBrowser(),
|
||||||
session,
|
|
||||||
librariesScanning: this.scanner.librariesScanning,
|
librariesScanning: this.scanner.librariesScanning,
|
||||||
backups: (this.backupManager.backups || []).map(b => b.toJSON())
|
backups: (this.backupManager.backups || []).map(b => b.toJSON())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,18 +24,11 @@ class CollectionController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
findOne(req, res) {
|
findOne(req, res) {
|
||||||
var collection = this.db.collections.find(c => c.id === req.params.id)
|
res.json(req.collection.toJSONExpanded(this.db.libraryItems))
|
||||||
if (!collection) {
|
|
||||||
return res.status(404).send('Collection not found')
|
|
||||||
}
|
|
||||||
res.json(collection.toJSONExpanded(this.db.libraryItems))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(req, res) {
|
async update(req, res) {
|
||||||
var collection = this.db.collections.find(c => c.id === req.params.id)
|
const collection = req.collection
|
||||||
if (!collection) {
|
|
||||||
return res.status(404).send('Collection not found')
|
|
||||||
}
|
|
||||||
var wasUpdated = collection.update(req.body)
|
var wasUpdated = collection.update(req.body)
|
||||||
var jsonExpanded = collection.toJSONExpanded(this.db.libraryItems)
|
var jsonExpanded = collection.toJSONExpanded(this.db.libraryItems)
|
||||||
if (wasUpdated) {
|
if (wasUpdated) {
|
||||||
@@ -46,10 +39,7 @@ class CollectionController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async delete(req, res) {
|
async delete(req, res) {
|
||||||
var collection = this.db.collections.find(c => c.id === req.params.id)
|
const collection = req.collection
|
||||||
if (!collection) {
|
|
||||||
return res.status(404).send('Collection not found')
|
|
||||||
}
|
|
||||||
var jsonExpanded = collection.toJSONExpanded(this.db.libraryItems)
|
var jsonExpanded = collection.toJSONExpanded(this.db.libraryItems)
|
||||||
await this.db.removeEntity('collection', collection.id)
|
await this.db.removeEntity('collection', collection.id)
|
||||||
this.emitter('collection_removed', jsonExpanded)
|
this.emitter('collection_removed', jsonExpanded)
|
||||||
@@ -57,10 +47,7 @@ class CollectionController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async addBook(req, res) {
|
async addBook(req, res) {
|
||||||
var collection = this.db.collections.find(c => c.id === req.params.id)
|
const collection = req.collection
|
||||||
if (!collection) {
|
|
||||||
return res.status(404).send('Collection not found')
|
|
||||||
}
|
|
||||||
var libraryItem = this.db.libraryItems.find(li => li.id === req.body.id)
|
var libraryItem = this.db.libraryItems.find(li => li.id === req.body.id)
|
||||||
if (!libraryItem) {
|
if (!libraryItem) {
|
||||||
return res.status(500).send('Book not found')
|
return res.status(500).send('Book not found')
|
||||||
@@ -80,11 +67,7 @@ class CollectionController {
|
|||||||
|
|
||||||
// DELETE: api/collections/:id/book/:bookId
|
// DELETE: api/collections/:id/book/:bookId
|
||||||
async removeBook(req, res) {
|
async removeBook(req, res) {
|
||||||
var collection = this.db.collections.find(c => c.id === req.params.id)
|
const collection = req.collection
|
||||||
if (!collection) {
|
|
||||||
return res.status(404).send('Collection not found')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (collection.books.includes(req.params.bookId)) {
|
if (collection.books.includes(req.params.bookId)) {
|
||||||
collection.removeBook(req.params.bookId)
|
collection.removeBook(req.params.bookId)
|
||||||
var jsonExpanded = collection.toJSONExpanded(this.db.libraryItems)
|
var jsonExpanded = collection.toJSONExpanded(this.db.libraryItems)
|
||||||
@@ -96,10 +79,7 @@ class CollectionController {
|
|||||||
|
|
||||||
// POST: api/collections/:id/batch/add
|
// POST: api/collections/:id/batch/add
|
||||||
async addBatch(req, res) {
|
async addBatch(req, res) {
|
||||||
var collection = this.db.collections.find(c => c.id === req.params.id)
|
const collection = req.collection
|
||||||
if (!collection) {
|
|
||||||
return res.status(404).send('Collection not found')
|
|
||||||
}
|
|
||||||
if (!req.body.books || !req.body.books.length) {
|
if (!req.body.books || !req.body.books.length) {
|
||||||
return res.status(500).send('Invalid request body')
|
return res.status(500).send('Invalid request body')
|
||||||
}
|
}
|
||||||
@@ -120,10 +100,7 @@ class CollectionController {
|
|||||||
|
|
||||||
// POST: api/collections/:id/batch/remove
|
// POST: api/collections/:id/batch/remove
|
||||||
async removeBatch(req, res) {
|
async removeBatch(req, res) {
|
||||||
var collection = this.db.collections.find(c => c.id === req.params.id)
|
const collection = req.collection
|
||||||
if (!collection) {
|
|
||||||
return res.status(404).send('Collection not found')
|
|
||||||
}
|
|
||||||
if (!req.body.books || !req.body.books.length) {
|
if (!req.body.books || !req.body.books.length) {
|
||||||
return res.status(500).send('Invalid request body')
|
return res.status(500).send('Invalid request body')
|
||||||
}
|
}
|
||||||
@@ -141,5 +118,25 @@ class CollectionController {
|
|||||||
}
|
}
|
||||||
res.json(collection.toJSONExpanded(this.db.libraryItems))
|
res.json(collection.toJSONExpanded(this.db.libraryItems))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
middleware(req, res, next) {
|
||||||
|
if (req.params.id) {
|
||||||
|
var collection = this.db.collections.find(c => c.id === req.params.id)
|
||||||
|
if (!collection) {
|
||||||
|
return res.status(404).send('Collection not found')
|
||||||
|
}
|
||||||
|
req.collection = collection
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method == 'DELETE' && !req.user.canDelete) {
|
||||||
|
Logger.warn(`[CollectionController] User attempted to delete without permission`, req.user.username)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
} else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) {
|
||||||
|
Logger.warn('[CollectionController] User attempted to update without permission', req.user.username)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = new CollectionController()
|
module.exports = new CollectionController()
|
||||||
@@ -165,6 +165,7 @@ class LibraryController {
|
|||||||
if (payload.filterBy) {
|
if (payload.filterBy) {
|
||||||
// If filtering by series, will include seriesName and seriesSequence on media metadata
|
// If filtering by series, will include seriesName and seriesSequence on media metadata
|
||||||
filterSeries = (payload.mediaType == 'book' && payload.filterBy.startsWith('series.')) ? libraryHelpers.decode(payload.filterBy.replace('series.', '')) : null
|
filterSeries = (payload.mediaType == 'book' && payload.filterBy.startsWith('series.')) ? libraryHelpers.decode(payload.filterBy.replace('series.', '')) : null
|
||||||
|
if (filterSeries === 'No Series') filterSeries = null
|
||||||
|
|
||||||
libraryItems = libraryHelpers.getFilteredLibraryItems(libraryItems, payload.filterBy, req.user, this.rssFeedManager.feedsArray)
|
libraryItems = libraryHelpers.getFilteredLibraryItems(libraryItems, payload.filterBy, req.user, this.rssFeedManager.feedsArray)
|
||||||
payload.total = libraryItems.length
|
payload.total = libraryItems.length
|
||||||
|
|||||||
@@ -96,9 +96,9 @@ class MeController {
|
|||||||
|
|
||||||
var shouldUpdate = false
|
var shouldUpdate = false
|
||||||
itemProgressPayloads.forEach((itemProgress) => {
|
itemProgressPayloads.forEach((itemProgress) => {
|
||||||
var libraryItem = this.db.libraryItems.find(li => li.id === itemProgress.id) // Make sure this library item exists
|
var libraryItem = this.db.libraryItems.find(li => li.id === itemProgress.libraryItemId) // Make sure this library item exists
|
||||||
if (libraryItem) {
|
if (libraryItem) {
|
||||||
var wasUpdated = req.user.createUpdateMediaProgress(libraryItem, itemProgress)
|
var wasUpdated = req.user.createUpdateMediaProgress(libraryItem, itemProgress, itemProgress.episodeId)
|
||||||
if (wasUpdated) shouldUpdate = true
|
if (wasUpdated) shouldUpdate = true
|
||||||
} else {
|
} else {
|
||||||
Logger.error(`[MeController] batchUpdateMediaProgress: Library Item does not exist ${itemProgress.id}`)
|
Logger.error(`[MeController] batchUpdateMediaProgress: Library Item does not exist ${itemProgress.id}`)
|
||||||
|
|||||||
@@ -140,7 +140,9 @@ class PodcastController {
|
|||||||
return res.status(500).send('Podcast has no rss feed url')
|
return res.status(500).send('Podcast has no rss feed url')
|
||||||
}
|
}
|
||||||
|
|
||||||
var newEpisodes = await this.podcastManager.checkAndDownloadNewEpisodes(libraryItem)
|
const maxEpisodesToDownload = !isNaN(req.query.limit) ? Number(req.query.limit) : 3
|
||||||
|
|
||||||
|
var newEpisodes = await this.podcastManager.checkAndDownloadNewEpisodes(libraryItem, maxEpisodesToDownload)
|
||||||
res.json({
|
res.json({
|
||||||
episodes: newEpisodes || []
|
episodes: newEpisodes || []
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class PlaybackSessionManager {
|
|||||||
this.clientEmitter = clientEmitter
|
this.clientEmitter = clientEmitter
|
||||||
|
|
||||||
this.sessions = []
|
this.sessions = []
|
||||||
|
this.localSessionLock = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
getSession(sessionId) {
|
getSession(sessionId) {
|
||||||
@@ -58,18 +59,26 @@ class PlaybackSessionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async syncLocalSessionRequest(user, sessionJson, res) {
|
async syncLocalSessionRequest(user, sessionJson, res) {
|
||||||
|
if (this.localSessionLock[sessionJson.id]) {
|
||||||
|
Logger.debug(`[PlaybackSessionManager] syncLocalSessionRequest: Local session is locked and already syncing`)
|
||||||
|
return res.sendStatus(200)
|
||||||
|
}
|
||||||
|
|
||||||
var libraryItem = this.db.getLibraryItem(sessionJson.libraryItemId)
|
var libraryItem = this.db.getLibraryItem(sessionJson.libraryItemId)
|
||||||
if (!libraryItem) {
|
if (!libraryItem) {
|
||||||
Logger.error(`[PlaybackSessionManager] syncLocalSessionRequest: Library item not found for session "${sessionJson.libraryItemId}"`)
|
Logger.error(`[PlaybackSessionManager] syncLocalSessionRequest: Library item not found for session "${sessionJson.libraryItemId}"`)
|
||||||
return res.sendStatus(200)
|
return res.sendStatus(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.localSessionLock[sessionJson.id] = true // Lock local session
|
||||||
|
|
||||||
var session = await this.db.getPlaybackSession(sessionJson.id)
|
var session = await this.db.getPlaybackSession(sessionJson.id)
|
||||||
if (!session) {
|
if (!session) {
|
||||||
// New session from local
|
// New session from local
|
||||||
session = new PlaybackSession(sessionJson)
|
session = new PlaybackSession(sessionJson)
|
||||||
await this.db.insertEntity('session', session)
|
await this.db.insertEntity('session', session)
|
||||||
} else {
|
} else {
|
||||||
|
session.currentTime = sessionJson.currentTime
|
||||||
session.timeListening = sessionJson.timeListening
|
session.timeListening = sessionJson.timeListening
|
||||||
session.updatedAt = sessionJson.updatedAt
|
session.updatedAt = sessionJson.updatedAt
|
||||||
session.date = date.format(new Date(), 'YYYY-MM-DD')
|
session.date = date.format(new Date(), 'YYYY-MM-DD')
|
||||||
@@ -94,6 +103,9 @@ class PlaybackSessionManager {
|
|||||||
data: itemProgress.toJSON()
|
data: itemProgress.toJSON()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
delete this.localSessionLock[sessionJson.id] // Unlock local session
|
||||||
|
|
||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,4 +268,4 @@ class PlaybackSessionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = PlaybackSessionManager
|
module.exports = PlaybackSessionManager
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ class PodcastManager {
|
|||||||
return libraryItem.media.autoDownloadEpisodes
|
return libraryItem.media.autoDownloadEpisodes
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkPodcastForNewEpisodes(podcastLibraryItem, dateToCheckForEpisodesAfter) {
|
async checkPodcastForNewEpisodes(podcastLibraryItem, dateToCheckForEpisodesAfter, maxNewEpisodes = 3) {
|
||||||
if (!podcastLibraryItem.media.metadata.feedUrl) {
|
if (!podcastLibraryItem.media.metadata.feedUrl) {
|
||||||
Logger.error(`[PodcastManager] checkPodcastForNewEpisodes no feed url for ${podcastLibraryItem.media.metadata.title} (ID: ${podcastLibraryItem.id})`)
|
Logger.error(`[PodcastManager] checkPodcastForNewEpisodes no feed url for ${podcastLibraryItem.media.metadata.title} (ID: ${podcastLibraryItem.id})`)
|
||||||
return false
|
return false
|
||||||
@@ -234,15 +234,18 @@ class PodcastManager {
|
|||||||
|
|
||||||
// Filter new and not already has
|
// Filter new and not already has
|
||||||
var newEpisodes = feed.episodes.filter(ep => ep.publishedAt > dateToCheckForEpisodesAfter && !podcastLibraryItem.media.checkHasEpisodeByFeedUrl(ep.enclosure.url))
|
var newEpisodes = feed.episodes.filter(ep => ep.publishedAt > dateToCheckForEpisodesAfter && !podcastLibraryItem.media.checkHasEpisodeByFeedUrl(ep.enclosure.url))
|
||||||
// Max new episodes for safety = 3
|
|
||||||
newEpisodes = newEpisodes.slice(0, 3)
|
if (maxNewEpisodes > 0) {
|
||||||
|
newEpisodes = newEpisodes.slice(0, maxNewEpisodes)
|
||||||
|
}
|
||||||
|
|
||||||
return newEpisodes
|
return newEpisodes
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkAndDownloadNewEpisodes(libraryItem) {
|
async checkAndDownloadNewEpisodes(libraryItem, maxEpisodesToDownload) {
|
||||||
const lastEpisodeCheckDate = new Date(libraryItem.media.lastEpisodeCheck || 0)
|
const lastEpisodeCheckDate = new Date(libraryItem.media.lastEpisodeCheck || 0)
|
||||||
Logger.info(`[PodcastManager] checkAndDownloadNewEpisodes for "${libraryItem.media.metadata.title}" - Last episode check: ${lastEpisodeCheckDate}`)
|
Logger.info(`[PodcastManager] checkAndDownloadNewEpisodes for "${libraryItem.media.metadata.title}" - Last episode check: ${lastEpisodeCheckDate}`)
|
||||||
var newEpisodes = await this.checkPodcastForNewEpisodes(libraryItem, libraryItem.media.lastEpisodeCheck)
|
var newEpisodes = await this.checkPodcastForNewEpisodes(libraryItem, libraryItem.media.lastEpisodeCheck, maxEpisodesToDownload)
|
||||||
if (newEpisodes.length) {
|
if (newEpisodes.length) {
|
||||||
Logger.info(`[PodcastManager] Found ${newEpisodes.length} new episodes for podcast "${libraryItem.media.metadata.title}" - starting download`)
|
Logger.info(`[PodcastManager] Found ${newEpisodes.length} new episodes for podcast "${libraryItem.media.metadata.title}" - starting download`)
|
||||||
this.downloadPodcastEpisodes(libraryItem, newEpisodes, false)
|
this.downloadPodcastEpisodes(libraryItem, newEpisodes, false)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class RssFeedManager {
|
|||||||
return Object.values(this.feeds).find(feed => feed.entityId === libraryItemId)
|
return Object.values(this.feeds).find(feed => feed.entityId === libraryItemId)
|
||||||
}
|
}
|
||||||
|
|
||||||
getFeed(req, res) {
|
async getFeed(req, res) {
|
||||||
var feed = this.feeds[req.params.id]
|
var feed = this.feeds[req.params.id]
|
||||||
if (!feed) {
|
if (!feed) {
|
||||||
Logger.error(`[RssFeedManager] Feed not found ${req.params.id}`)
|
Logger.error(`[RssFeedManager] Feed not found ${req.params.id}`)
|
||||||
@@ -38,6 +38,15 @@ class RssFeedManager {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (feed.entityType === 'item') {
|
||||||
|
const libraryItem = this.db.getLibraryItem(feed.entityId)
|
||||||
|
if (libraryItem && (!feed.entityUpdatedAt || libraryItem.updatedAt > feed.entityUpdatedAt)) {
|
||||||
|
Logger.debug(`[RssFeedManager] Updating RSS feed for item ${libraryItem.id} "${libraryItem.media.metadata.title}"`)
|
||||||
|
feed.updateFromItem(libraryItem)
|
||||||
|
await this.db.updateEntity('feed', feed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var xml = feed.buildXml()
|
var xml = feed.buildXml()
|
||||||
res.set('Content-Type', 'text/xml')
|
res.set('Content-Type', 'text/xml')
|
||||||
res.send(xml)
|
res.send(xml)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class Feed {
|
|||||||
this.userId = null
|
this.userId = null
|
||||||
this.entityType = null
|
this.entityType = null
|
||||||
this.entityId = null
|
this.entityId = null
|
||||||
|
this.entityUpdatedAt = null
|
||||||
|
|
||||||
this.coverPath = null
|
this.coverPath = null
|
||||||
this.serverAddress = null
|
this.serverAddress = null
|
||||||
@@ -79,6 +80,7 @@ class Feed {
|
|||||||
this.userId = userId
|
this.userId = userId
|
||||||
this.entityType = 'item'
|
this.entityType = 'item'
|
||||||
this.entityId = libraryItem.id
|
this.entityId = libraryItem.id
|
||||||
|
this.entityUpdatedAt = libraryItem.updatedAt
|
||||||
this.coverPath = media.coverPath || null
|
this.coverPath = media.coverPath || null
|
||||||
this.serverAddress = serverAddress
|
this.serverAddress = serverAddress
|
||||||
this.feedUrl = feedUrl
|
this.feedUrl = feedUrl
|
||||||
@@ -111,6 +113,39 @@ class Feed {
|
|||||||
this.updatedAt = Date.now()
|
this.updatedAt = Date.now()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateFromItem(libraryItem) {
|
||||||
|
const media = libraryItem.media
|
||||||
|
const mediaMetadata = media.metadata
|
||||||
|
const isPodcast = libraryItem.mediaType === 'podcast'
|
||||||
|
const author = isPodcast ? mediaMetadata.author : mediaMetadata.authorName
|
||||||
|
|
||||||
|
this.entityUpdatedAt = libraryItem.updatedAt
|
||||||
|
|
||||||
|
this.meta.title = mediaMetadata.title
|
||||||
|
this.meta.description = mediaMetadata.description
|
||||||
|
this.meta.author = author
|
||||||
|
this.meta.imageUrl = media.coverPath ? `${this.serverAddress}/feed/${this.slug}/cover` : `${this.serverAddress}/Logo.png`
|
||||||
|
this.meta.explicit = !!mediaMetadata.explicit
|
||||||
|
|
||||||
|
this.episodes = []
|
||||||
|
if (isPodcast) { // PODCAST EPISODES
|
||||||
|
media.episodes.forEach((episode) => {
|
||||||
|
var feedEpisode = new FeedEpisode()
|
||||||
|
feedEpisode.setFromPodcastEpisode(libraryItem, this.serverAddress, this.slug, episode, this.meta)
|
||||||
|
this.episodes.push(feedEpisode)
|
||||||
|
})
|
||||||
|
} else { // AUDIOBOOK EPISODES
|
||||||
|
media.tracks.forEach((audioTrack) => {
|
||||||
|
var feedEpisode = new FeedEpisode()
|
||||||
|
feedEpisode.setFromAudiobookTrack(libraryItem, this.serverAddress, this.slug, audioTrack, this.meta)
|
||||||
|
this.episodes.push(feedEpisode)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updatedAt = Date.now()
|
||||||
|
this.xml = null
|
||||||
|
}
|
||||||
|
|
||||||
buildXml() {
|
buildXml() {
|
||||||
if (this.xml) return this.xml
|
if (this.xml) return this.xml
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ class FeedEpisode {
|
|||||||
setFromAudiobookTrack(libraryItem, serverAddress, slug, audioTrack, meta) {
|
setFromAudiobookTrack(libraryItem, serverAddress, slug, audioTrack, meta) {
|
||||||
// Example: <pubDate>Fri, 04 Feb 2015 00:00:00 GMT</pubDate>
|
// Example: <pubDate>Fri, 04 Feb 2015 00:00:00 GMT</pubDate>
|
||||||
const timeOffset = isNaN(audioTrack.index) ? 0 : (Number(audioTrack.index) * 1000) // Offset pubdate to ensure correct order
|
const timeOffset = isNaN(audioTrack.index) ? 0 : (Number(audioTrack.index) * 1000) // Offset pubdate to ensure correct order
|
||||||
const audiobookPubDate = date.format(new Date(libraryItem.addedAt + timeOffset), 'ddd, DD MMM YYYY HH:mm:ss [GMT]')
|
const audiobookPubDate = date.format(new Date(libraryItem.addedAt - timeOffset), 'ddd, DD MMM YYYY HH:mm:ss [GMT]')
|
||||||
|
|
||||||
const contentUrl = `/feed/${slug}/item/${audioTrack.index}/${audioTrack.metadata.filename}`
|
const contentUrl = `/feed/${slug}/item/${audioTrack.index}/${audioTrack.metadata.filename}`
|
||||||
const media = libraryItem.media
|
const media = libraryItem.media
|
||||||
|
|||||||
@@ -228,6 +228,7 @@ class Podcast {
|
|||||||
|
|
||||||
addPodcastEpisode(podcastEpisode) {
|
addPodcastEpisode(podcastEpisode) {
|
||||||
this.episodes.push(podcastEpisode)
|
this.episodes.push(podcastEpisode)
|
||||||
|
this.reorderEpisodes()
|
||||||
}
|
}
|
||||||
|
|
||||||
addNewEpisodeFromAudioFile(audioFile, index) {
|
addNewEpisodeFromAudioFile(audioFile, index) {
|
||||||
@@ -241,15 +242,13 @@ class Podcast {
|
|||||||
reorderEpisodes() {
|
reorderEpisodes() {
|
||||||
var hasUpdates = false
|
var hasUpdates = false
|
||||||
|
|
||||||
// TODO: Sort by published date
|
this.episodes = naturalSort(this.episodes).desc((ep) => ep.publishedAt)
|
||||||
this.episodes = naturalSort(this.episodes).asc((ep) => ep.bestFilename)
|
|
||||||
for (let i = 0; i < this.episodes.length; i++) {
|
for (let i = 0; i < this.episodes.length; i++) {
|
||||||
if (this.episodes[i].index !== (i + 1)) {
|
if (this.episodes[i].index !== (i + 1)) {
|
||||||
this.episodes[i].index = i + 1
|
this.episodes[i].index = i + 1
|
||||||
hasUpdates = true
|
hasUpdates = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.episodes.sort((a, b) => b.index - a.index)
|
|
||||||
return hasUpdates
|
return hasUpdates
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -116,16 +116,16 @@ class ApiRouter {
|
|||||||
//
|
//
|
||||||
// Collection Routes
|
// Collection Routes
|
||||||
//
|
//
|
||||||
this.router.post('/collections', CollectionController.create.bind(this))
|
this.router.post('/collections', CollectionController.middleware.bind(this), CollectionController.create.bind(this))
|
||||||
this.router.get('/collections', CollectionController.findAll.bind(this))
|
this.router.get('/collections', CollectionController.findAll.bind(this))
|
||||||
this.router.get('/collections/:id', CollectionController.findOne.bind(this))
|
this.router.get('/collections/:id', CollectionController.middleware.bind(this), CollectionController.findOne.bind(this))
|
||||||
this.router.patch('/collections/:id', CollectionController.update.bind(this))
|
this.router.patch('/collections/:id', CollectionController.middleware.bind(this), CollectionController.update.bind(this))
|
||||||
this.router.delete('/collections/:id', CollectionController.delete.bind(this))
|
this.router.delete('/collections/:id', CollectionController.middleware.bind(this), CollectionController.delete.bind(this))
|
||||||
|
|
||||||
this.router.post('/collections/:id/book', CollectionController.addBook.bind(this))
|
this.router.post('/collections/:id/book', CollectionController.middleware.bind(this), CollectionController.addBook.bind(this))
|
||||||
this.router.delete('/collections/:id/book/:bookId', CollectionController.removeBook.bind(this))
|
this.router.delete('/collections/:id/book/:bookId', CollectionController.middleware.bind(this), CollectionController.removeBook.bind(this))
|
||||||
this.router.post('/collections/:id/batch/add', CollectionController.addBatch.bind(this))
|
this.router.post('/collections/:id/batch/add', CollectionController.middleware.bind(this), CollectionController.addBatch.bind(this))
|
||||||
this.router.post('/collections/:id/batch/remove', CollectionController.removeBatch.bind(this))
|
this.router.post('/collections/:id/batch/remove', CollectionController.middleware.bind(this), CollectionController.removeBatch.bind(this))
|
||||||
|
|
||||||
//
|
//
|
||||||
// Current User Routes (Me)
|
// Current User Routes (Me)
|
||||||
|
|||||||
+125
-32
@@ -10,6 +10,7 @@ const { ScanResult, LogLevel } = require('../utils/constants')
|
|||||||
|
|
||||||
const MediaFileScanner = require('./MediaFileScanner')
|
const MediaFileScanner = require('./MediaFileScanner')
|
||||||
const BookFinder = require('../finders/BookFinder')
|
const BookFinder = require('../finders/BookFinder')
|
||||||
|
const PodcastFinder = require('../finders/PodcastFinder')
|
||||||
const LibraryItem = require('../objects/LibraryItem')
|
const LibraryItem = require('../objects/LibraryItem')
|
||||||
const LibraryScan = require('./LibraryScan')
|
const LibraryScan = require('./LibraryScan')
|
||||||
const ScanOptions = require('./ScanOptions')
|
const ScanOptions = require('./ScanOptions')
|
||||||
@@ -28,7 +29,12 @@ class Scanner {
|
|||||||
this.cancelLibraryScan = {}
|
this.cancelLibraryScan = {}
|
||||||
this.librariesScanning = []
|
this.librariesScanning = []
|
||||||
|
|
||||||
|
// Watcher file update scan vars
|
||||||
|
this.pendingFileUpdatesToScan = []
|
||||||
|
this.scanningFilesChanged = false
|
||||||
|
|
||||||
this.bookFinder = new BookFinder()
|
this.bookFinder = new BookFinder()
|
||||||
|
this.podcastFinder = new PodcastFinder()
|
||||||
}
|
}
|
||||||
|
|
||||||
isLibraryScanning(libraryId) {
|
isLibraryScanning(libraryId) {
|
||||||
@@ -494,7 +500,16 @@ class Scanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async scanFilesChanged(fileUpdates) {
|
async scanFilesChanged(fileUpdates) {
|
||||||
if (!fileUpdates.length) return
|
if (!fileUpdates || !fileUpdates.length) return
|
||||||
|
|
||||||
|
// If already scanning files from watcher then add these updates to queue
|
||||||
|
if (this.scanningFilesChanged) {
|
||||||
|
this.pendingFileUpdatesToScan.push(fileUpdates)
|
||||||
|
Logger.debug(`[Scanner] Already scanning files from watcher - file updates pushed to queue (size ${this.pendingFileUpdatesToScan.length})`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.scanningFilesChanged = true
|
||||||
|
|
||||||
// files grouped by folder
|
// files grouped by folder
|
||||||
var folderGroups = this.getFileUpdatesGrouped(fileUpdates)
|
var folderGroups = this.getFileUpdatesGrouped(fileUpdates)
|
||||||
|
|
||||||
@@ -520,6 +535,13 @@ class Scanner {
|
|||||||
var folderScanResults = await this.scanFolderUpdates(library, folder, fileUpdateGroup)
|
var folderScanResults = await this.scanFolderUpdates(library, folder, fileUpdateGroup)
|
||||||
Logger.debug(`[Scanner] Folder scan results`, folderScanResults)
|
Logger.debug(`[Scanner] Folder scan results`, folderScanResults)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.scanningFilesChanged = false
|
||||||
|
|
||||||
|
if (this.pendingFileUpdatesToScan.length) {
|
||||||
|
Logger.debug(`[Scanner] File updates finished scanning with more updates in queue (${this.pendingFileUpdatesToScan.length})`)
|
||||||
|
this.scanFilesChanged(this.pendingFileUpdatesToScan.shift())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async scanFolderUpdates(library, folder, fileUpdateGroup) {
|
async scanFolderUpdates(library, folder, fileUpdateGroup) {
|
||||||
@@ -652,16 +674,6 @@ class Scanner {
|
|||||||
var provider = options.provider || 'google'
|
var provider = options.provider || 'google'
|
||||||
var searchTitle = options.title || libraryItem.media.metadata.title
|
var searchTitle = options.title || libraryItem.media.metadata.title
|
||||||
var searchAuthor = options.author || libraryItem.media.metadata.authorName
|
var searchAuthor = options.author || libraryItem.media.metadata.authorName
|
||||||
var searchISBN = options.isbn || libraryItem.media.metadata.isbn
|
|
||||||
var searchASIN = options.asin || libraryItem.media.metadata.asin
|
|
||||||
|
|
||||||
var results = await this.bookFinder.search(provider, searchTitle, searchAuthor, searchISBN, searchASIN)
|
|
||||||
if (!results.length) {
|
|
||||||
return {
|
|
||||||
warning: `No ${provider} match found`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var matchData = results[0]
|
|
||||||
|
|
||||||
// Set to override existing metadata if scannerPreferMatchedMetadata setting is true
|
// Set to override existing metadata if scannerPreferMatchedMetadata setting is true
|
||||||
if (this.db.serverSettings.scannerPreferMatchedMetadata) {
|
if (this.db.serverSettings.scannerPreferMatchedMetadata) {
|
||||||
@@ -669,18 +681,110 @@ class Scanner {
|
|||||||
options.overrideDetails = true
|
options.overrideDetails = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update cover if not set OR overrideCover flag
|
var updatePayload = {}
|
||||||
var hasUpdated = false
|
var hasUpdated = false
|
||||||
if (matchData.cover && (!libraryItem.media.coverPath || options.overrideCover)) {
|
|
||||||
Logger.debug(`[Scanner] Updating cover "${matchData.cover}"`)
|
if (libraryItem.mediaType === 'book') {
|
||||||
var coverResult = await this.coverManager.downloadCoverFromUrl(libraryItem, matchData.cover)
|
var searchISBN = options.isbn || libraryItem.media.metadata.isbn
|
||||||
if (!coverResult || coverResult.error || !coverResult.cover) {
|
var searchASIN = options.asin || libraryItem.media.metadata.asin
|
||||||
Logger.warn(`[Scanner] Match cover "${matchData.cover}" failed to use: ${coverResult ? coverResult.error : 'Unknown Error'}`)
|
|
||||||
} else {
|
var results = await this.bookFinder.search(provider, searchTitle, searchAuthor, searchISBN, searchASIN)
|
||||||
|
if (!results.length) {
|
||||||
|
return {
|
||||||
|
warning: `No ${provider} match found`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var matchData = results[0]
|
||||||
|
|
||||||
|
// Update cover if not set OR overrideCover flag
|
||||||
|
if (matchData.cover && (!libraryItem.media.coverPath || options.overrideCover)) {
|
||||||
|
Logger.debug(`[Scanner] Updating cover "${matchData.cover}"`)
|
||||||
|
var coverResult = await this.coverManager.downloadCoverFromUrl(libraryItem, matchData.cover)
|
||||||
|
if (!coverResult || coverResult.error || !coverResult.cover) {
|
||||||
|
Logger.warn(`[Scanner] Match cover "${matchData.cover}" failed to use: ${coverResult ? coverResult.error : 'Unknown Error'}`)
|
||||||
|
} else {
|
||||||
|
hasUpdated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePayload = await this.quickMatchBookBuildUpdatePayload(libraryItem, matchData, options)
|
||||||
|
} else { // Podcast quick match
|
||||||
|
var results = await this.podcastFinder.search(searchTitle)
|
||||||
|
if (!results.length) {
|
||||||
|
return {
|
||||||
|
warning: `No ${provider} match found`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var matchData = results[0]
|
||||||
|
|
||||||
|
// Update cover if not set OR overrideCover flag
|
||||||
|
if (matchData.cover && (!libraryItem.media.coverPath || options.overrideCover)) {
|
||||||
|
Logger.debug(`[Scanner] Updating cover "${matchData.cover}"`)
|
||||||
|
var coverResult = await this.coverManager.downloadCoverFromUrl(libraryItem, matchData.cover)
|
||||||
|
if (!coverResult || coverResult.error || !coverResult.cover) {
|
||||||
|
Logger.warn(`[Scanner] Match cover "${matchData.cover}" failed to use: ${coverResult ? coverResult.error : 'Unknown Error'}`)
|
||||||
|
} else {
|
||||||
|
hasUpdated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePayload = this.quickMatchPodcastBuildUpdatePayload(libraryItem, matchData, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(updatePayload).length) {
|
||||||
|
Logger.debug('[Scanner] Updating details', updatePayload)
|
||||||
|
if (libraryItem.media.update(updatePayload)) {
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasUpdated) {
|
||||||
|
await this.db.updateLibraryItem(libraryItem)
|
||||||
|
this.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
updated: hasUpdated,
|
||||||
|
libraryItem: libraryItem.toJSONExpanded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
quickMatchPodcastBuildUpdatePayload(libraryItem, matchData, options) {
|
||||||
|
const updatePayload = {}
|
||||||
|
updatePayload.metadata = {}
|
||||||
|
|
||||||
|
const matchDataTransformed = {
|
||||||
|
title: matchData.title || null,
|
||||||
|
author: matchData.artistName || null,
|
||||||
|
genres: matchData.genres || [],
|
||||||
|
itunesId: matchData.id || null,
|
||||||
|
itunesPageUrl: matchData.pageUrl || null,
|
||||||
|
itunesArtistId: matchData.artistId || null,
|
||||||
|
releaseDate: matchData.releaseDate || null,
|
||||||
|
imageUrl: matchData.cover || null,
|
||||||
|
description: matchData.descriptionPlain || null
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in matchDataTransformed) {
|
||||||
|
if (matchDataTransformed[key]) {
|
||||||
|
if (key === 'genres') {
|
||||||
|
if ((!libraryItem.media.metadata.genres || options.overrideDetails)) {
|
||||||
|
updatePayload.metadata[key] = matchDataTransformed[key].split(',').map(v => v.trim()).filter(v => !!v)
|
||||||
|
}
|
||||||
|
} else if (!libraryItem.media.metadata[key] || options.overrideDetails) {
|
||||||
|
updatePayload.metadata[key] = matchDataTransformed[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Object.keys(updatePayload.metadata).length) {
|
||||||
|
delete updatePayload.metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatePayload
|
||||||
|
}
|
||||||
|
|
||||||
|
async quickMatchBookBuildUpdatePayload(libraryItem, matchData, options) {
|
||||||
// Update media metadata if not set OR overrideDetails flag
|
// Update media metadata if not set OR overrideDetails flag
|
||||||
const detailKeysToUpdate = ['title', 'subtitle', 'description', 'narrator', 'publisher', 'publishedYear', 'genres', 'tags', 'language', 'explicit', 'asin', 'isbn']
|
const detailKeysToUpdate = ['title', 'subtitle', 'description', 'narrator', 'publisher', 'publishedYear', 'genres', 'tags', 'language', 'explicit', 'asin', 'isbn']
|
||||||
const updatePayload = {}
|
const updatePayload = {}
|
||||||
@@ -743,22 +847,11 @@ class Scanner {
|
|||||||
updatePayload.metadata.series = seriesPayload
|
updatePayload.metadata.series = seriesPayload
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(updatePayload).length) {
|
if (!Object.keys(updatePayload.metadata).length) {
|
||||||
Logger.debug('[Scanner] Updating details', updatePayload)
|
delete updatePayload.metadata
|
||||||
if (libraryItem.media.update(updatePayload)) {
|
|
||||||
hasUpdated = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasUpdated) {
|
return updatePayload
|
||||||
await this.db.updateLibraryItem(libraryItem)
|
|
||||||
this.emitter('item_updated', libraryItem.toJSONExpanded())
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
updated: hasUpdated,
|
|
||||||
libraryItem: libraryItem.toJSONExpanded()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async matchLibraryItems(library) {
|
async matchLibraryItems(library) {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
|||||||
const sanitizeHtml = require('../libs/sanitizeHtml')
|
const sanitizeHtml = require('../libs/sanitizeHtml')
|
||||||
|
const {entities} = require("./htmlEntities");
|
||||||
|
|
||||||
function sanitize(html) {
|
function sanitize(html) {
|
||||||
const sanitizerOptions = {
|
const sanitizerOptions = {
|
||||||
@@ -17,12 +18,22 @@ function sanitize(html) {
|
|||||||
}
|
}
|
||||||
module.exports.sanitize = sanitize
|
module.exports.sanitize = sanitize
|
||||||
|
|
||||||
function stripAllTags(html) {
|
function stripAllTags(html, shouldDecodeEntities = true) {
|
||||||
const sanitizerOptions = {
|
const sanitizerOptions = {
|
||||||
allowedTags: [],
|
allowedTags: [],
|
||||||
disallowedTagsMode: 'discard'
|
disallowedTagsMode: 'discard'
|
||||||
}
|
}
|
||||||
|
|
||||||
return sanitizeHtml(html, sanitizerOptions)
|
let sanitized = sanitizeHtml(html, sanitizerOptions)
|
||||||
|
return shouldDecodeEntities ? decodeHTMLEntities(sanitized) : sanitized
|
||||||
|
}
|
||||||
|
module.exports.stripAllTags = stripAllTags
|
||||||
|
|
||||||
|
function decodeHTMLEntities(strToDecode) {
|
||||||
|
return strToDecode.replace(/\&([^;]+);?/g, function (entity) {
|
||||||
|
if (entity in entities) {
|
||||||
|
return entities[entity]
|
||||||
|
}
|
||||||
|
return entity;
|
||||||
|
})
|
||||||
}
|
}
|
||||||
module.exports.stripAllTags = stripAllTags
|
|
||||||
Reference in New Issue
Block a user