mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-04-11 01:17:50 +02:00
Merge branch 'master' into binary-manager
This commit is contained in:
commit
aa63aa6cf3
@ -22,6 +22,10 @@
|
|||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
|
||||||
</svg>
|
</svg>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
<nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="flex-grow h-full flex justify-center items-center" :class="isPlaylistsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||||
|
<p v-if="isPlaylistsPage || isPodcastLibrary" class="text-sm">{{ $strings.ButtonPlaylists }}</p>
|
||||||
|
<span v-else class="material-icons-outlined text-lg">queue_music</span>
|
||||||
|
</nuxt-link>
|
||||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="flex-grow h-full flex justify-center items-center" :class="isCollectionsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="flex-grow h-full flex justify-center items-center" :class="isCollectionsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||||
<p v-if="isCollectionsPage" class="text-sm">{{ $strings.ButtonCollections }}</p>
|
<p v-if="isCollectionsPage" class="text-sm">{{ $strings.ButtonCollections }}</p>
|
||||||
<span v-else class="material-icons-outlined text-lg">collections_bookmark</span>
|
<span v-else class="material-icons-outlined text-lg">collections_bookmark</span>
|
||||||
@ -293,6 +297,9 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return items
|
return items
|
||||||
|
},
|
||||||
|
showPlaylists() {
|
||||||
|
return this.$store.state.libraries.numUserPlaylists > 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<transition name="slide">
|
<transition name="slide">
|
||||||
<div class="w-full" v-show="showFiles">
|
<div class="w-full" v-if="showFiles">
|
||||||
<table class="text-sm tracksTable">
|
<table class="text-sm tracksTable">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="text-left px-4">{{ $strings.LabelPath }}</th>
|
<th class="text-left px-4">{{ $strings.LabelPath }}</th>
|
||||||
@ -70,7 +70,7 @@ export default {
|
|||||||
},
|
},
|
||||||
audioFiles() {
|
audioFiles() {
|
||||||
if (this.libraryItem.mediaType === 'podcast') {
|
if (this.libraryItem.mediaType === 'podcast') {
|
||||||
return this.libraryItem.media?.episodes.map((ep) => ep.audioFile) || []
|
return this.libraryItem.media?.episodes.map((ep) => ep.audioFile).filter((af) => af) || []
|
||||||
}
|
}
|
||||||
return this.libraryItem.media?.audioFiles || []
|
return this.libraryItem.media?.audioFiles || []
|
||||||
},
|
},
|
||||||
|
@ -1,18 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full px-2 py-3 overflow-hidden relative border-b border-white border-opacity-10" @mouseover="mouseover" @mouseleave="mouseleave">
|
<div :id="`lazy-episode-${index}`" class="w-full h-full cursor-pointer" @mouseover="mouseover" @mouseleave="mouseleave">
|
||||||
<div v-if="episode" class="flex items-center cursor-pointer" :class="{ 'opacity-70': isSelected || selectionMode }" @click="clickedEpisode">
|
<div class="flex" @click="clickedEpisode">
|
||||||
<div class="flex-grow px-2">
|
<div class="flex-grow">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="text-sm font-semibold">{{ title }}</span>
|
<span class="text-sm font-semibold">{{ episodeTitle }}</span>
|
||||||
<widgets-podcast-type-indicator :type="episode.episodeType" />
|
<widgets-podcast-type-indicator :type="episodeType" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-sm text-gray-200 episode-subtitle mt-1.5 mb-0.5" v-html="subtitle"></p>
|
<div class="h-10 flex items-center mt-1.5 mb-0.5">
|
||||||
<div class="flex justify-between pt-2 max-w-xl">
|
<p class="text-sm text-gray-200 episode-subtitle" v-html="episodeSubtitle"></p>
|
||||||
<p v-if="episode.season" class="text-sm text-gray-300">Season #{{ episode.season }}</p>
|
</div>
|
||||||
<p v-if="episode.episode" class="text-sm text-gray-300">Episode #{{ episode.episode }}</p>
|
<div class="h-8 flex items-center">
|
||||||
<p v-if="episode.chapters?.length" class="text-sm text-gray-300">{{ episode.chapters.length }} Chapters</p>
|
<div class="w-full inline-flex justify-between max-w-xl">
|
||||||
<p v-if="publishedAt" class="text-sm text-gray-300">Published {{ $formatDate(publishedAt, dateFormat) }}</p>
|
<p v-if="episode?.season" class="text-sm text-gray-300">Season #{{ episode.season }}</p>
|
||||||
|
<p v-if="episode?.episode" class="text-sm text-gray-300">Episode #{{ episode.episode }}</p>
|
||||||
|
<p v-if="episode?.chapters?.length" class="text-sm text-gray-300">{{ episode.chapters.length }} Chapters</p>
|
||||||
|
<p v-if="publishedAt" class="text-sm text-gray-300">Published {{ $formatDate(publishedAt, dateFormat) }}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center pt-2">
|
<div class="flex items-center pt-2">
|
||||||
@ -37,10 +41,11 @@
|
|||||||
<ui-icon-btn v-if="userCanDelete" icon="close" borderless @click="removeClick" />
|
<ui-icon-btn v-if="userCanDelete" icon="close" borderless @click="removeClick" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isHovering || isSelected || selectionMode" class="hidden md:block w-12 min-w-12" />
|
<div v-if="isHovering || isSelected || isSelectionMode" class="hidden md:block w-12 min-w-12" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isSelected || selectionMode" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-10 z-10 cursor-pointer" @click.stop="clickedSelectionBg" />
|
|
||||||
<div class="hidden md:block md:w-12 md:min-w-12 md:-right-0 md:absolute md:top-0 h-full transform transition-transform z-20" :class="!isHovering && !isSelected && !selectionMode ? 'translate-x-24' : 'translate-x-0'">
|
<div v-if="isSelected || isSelectionMode" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-10 z-10 cursor-pointer" @click.stop="clickedSelectionBg" />
|
||||||
|
<div class="hidden md:block md:w-12 md:min-w-12 md:-right-0 md:absolute md:top-0 h-full transform transition-transform z-20" :class="!isHovering && !isSelected && !isSelectionMode ? 'translate-x-24' : 'translate-x-0'">
|
||||||
<div class="flex h-full items-center">
|
<div class="flex h-full items-center">
|
||||||
<div class="mx-1">
|
<div class="mx-1">
|
||||||
<ui-checkbox v-model="isSelected" @input="selectedUpdated" checkbox-bg="bg" />
|
<ui-checkbox v-model="isSelected" @input="selectedUpdated" checkbox-bg="bg" />
|
||||||
@ -55,84 +60,91 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
index: Number,
|
||||||
libraryItemId: String,
|
libraryItemId: String,
|
||||||
episode: {
|
episode: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => null
|
||||||
},
|
}
|
||||||
selectionMode: Boolean
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isProcessingReadUpdate: false,
|
isProcessingReadUpdate: false,
|
||||||
processingRemove: false,
|
processingRemove: false,
|
||||||
isHovering: false,
|
isHovering: false,
|
||||||
isSelected: false
|
isSelected: false,
|
||||||
|
isSelectionMode: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
store() {
|
||||||
|
return this.$store || this.$nuxt.$store
|
||||||
|
},
|
||||||
|
axios() {
|
||||||
|
return this.$axios || this.$nuxt.$axios
|
||||||
|
},
|
||||||
userCanUpdate() {
|
userCanUpdate() {
|
||||||
return this.$store.getters['user/getUserCanUpdate']
|
return this.store.getters['user/getUserCanUpdate']
|
||||||
},
|
},
|
||||||
userCanDelete() {
|
userCanDelete() {
|
||||||
return this.$store.getters['user/getUserCanDelete']
|
return this.store.getters['user/getUserCanDelete']
|
||||||
},
|
},
|
||||||
audioFile() {
|
episodeId() {
|
||||||
return this.episode.audioFile
|
return this.episode?.id || ''
|
||||||
},
|
},
|
||||||
title() {
|
episodeTitle() {
|
||||||
return this.episode.title || ''
|
return this.episode?.title || ''
|
||||||
},
|
},
|
||||||
subtitle() {
|
episodeSubtitle() {
|
||||||
return this.episode.subtitle || this.description
|
return this.episode?.subtitle || ''
|
||||||
},
|
},
|
||||||
description() {
|
episodeType() {
|
||||||
return this.episode.description || ''
|
return this.episode?.episodeType || ''
|
||||||
},
|
},
|
||||||
duration() {
|
publishedAt() {
|
||||||
return this.$secondsToTimestamp(this.episode.duration)
|
return this.episode?.publishedAt
|
||||||
},
|
},
|
||||||
libraryItemIdStreaming() {
|
dateFormat() {
|
||||||
return this.$store.getters['getLibraryItemIdStreaming']
|
return this.store.state.serverSettings.dateFormat
|
||||||
},
|
|
||||||
isStreamingFromDifferentLibrary() {
|
|
||||||
return this.$store.getters['getIsStreamingFromDifferentLibrary']
|
|
||||||
},
|
|
||||||
isStreaming() {
|
|
||||||
return this.$store.getters['getIsMediaStreaming'](this.libraryItemId, this.episode.id)
|
|
||||||
},
|
|
||||||
isQueued() {
|
|
||||||
return this.$store.getters['getIsMediaQueued'](this.libraryItemId, this.episode.id)
|
|
||||||
},
|
|
||||||
streamIsPlaying() {
|
|
||||||
return this.$store.state.streamIsPlaying && this.isStreaming
|
|
||||||
},
|
},
|
||||||
itemProgress() {
|
itemProgress() {
|
||||||
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId, this.episode.id)
|
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId, this.episodeId)
|
||||||
},
|
},
|
||||||
itemProgressPercent() {
|
itemProgressPercent() {
|
||||||
return this.itemProgress ? this.itemProgress.progress : 0
|
return this.itemProgress?.progress || 0
|
||||||
},
|
},
|
||||||
userIsFinished() {
|
userIsFinished() {
|
||||||
return this.itemProgress ? !!this.itemProgress.isFinished : false
|
return !!this.itemProgress?.isFinished
|
||||||
|
},
|
||||||
|
libraryItemIdStreaming() {
|
||||||
|
return this.store.getters['getLibraryItemIdStreaming']
|
||||||
|
},
|
||||||
|
isStreamingFromDifferentLibrary() {
|
||||||
|
return this.store.getters['getIsStreamingFromDifferentLibrary']
|
||||||
|
},
|
||||||
|
isStreaming() {
|
||||||
|
return this.store.getters['getIsMediaStreaming'](this.libraryItemId, this.episodeId)
|
||||||
|
},
|
||||||
|
isQueued() {
|
||||||
|
return this.store.getters['getIsMediaQueued'](this.libraryItemId, this.episodeId)
|
||||||
|
},
|
||||||
|
streamIsPlaying() {
|
||||||
|
return this.store.state.streamIsPlaying && this.isStreaming
|
||||||
},
|
},
|
||||||
timeRemaining() {
|
timeRemaining() {
|
||||||
if (this.streamIsPlaying) return 'Playing'
|
if (this.streamIsPlaying) return 'Playing'
|
||||||
if (!this.itemProgress) return this.$elapsedPretty(this.episode.duration)
|
if (!this.itemProgress) return this.$elapsedPretty(this.episode?.duration || 0)
|
||||||
if (this.userIsFinished) return 'Finished'
|
if (this.userIsFinished) return 'Finished'
|
||||||
var remaining = Math.floor(this.itemProgress.duration - this.itemProgress.currentTime)
|
|
||||||
|
const duration = this.itemProgress.duration || this.episode?.duration || 0
|
||||||
|
const remaining = Math.floor(duration - this.itemProgress.currentTime)
|
||||||
return `${this.$elapsedPretty(remaining)} left`
|
return `${this.$elapsedPretty(remaining)} left`
|
||||||
},
|
|
||||||
publishedAt() {
|
|
||||||
return this.episode.publishedAt
|
|
||||||
},
|
|
||||||
dateFormat() {
|
|
||||||
return this.$store.state.serverSettings.dateFormat
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
clickAddToPlaylist() {
|
setSelectionMode(isSelectionMode) {
|
||||||
this.$emit('addToPlaylist', this.episode)
|
this.isSelectionMode = isSelectionMode
|
||||||
|
if (!this.isSelectionMode) this.isSelected = false
|
||||||
},
|
},
|
||||||
clickedEpisode() {
|
clickedEpisode() {
|
||||||
this.$emit('view', this.episode)
|
this.$emit('view', this.episode)
|
||||||
@ -150,16 +162,23 @@ export default {
|
|||||||
mouseleave() {
|
mouseleave() {
|
||||||
this.isHovering = false
|
this.isHovering = false
|
||||||
},
|
},
|
||||||
clickEdit() {
|
|
||||||
this.$emit('edit', this.episode)
|
|
||||||
},
|
|
||||||
playClick() {
|
playClick() {
|
||||||
if (this.streamIsPlaying) {
|
if (this.streamIsPlaying) {
|
||||||
this.$eventBus.$emit('pause-item')
|
const eventBus = this.$eventBus || this.$nuxt.$eventBus
|
||||||
|
eventBus.$emit('pause-item')
|
||||||
} else {
|
} else {
|
||||||
this.$emit('play', this.episode)
|
this.$emit('play', this.episode)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
queueBtnClick() {
|
||||||
|
if (this.isQueued) {
|
||||||
|
// Remove from queue
|
||||||
|
this.store.commit('removeItemFromQueue', { libraryItemId: this.libraryItemId, episodeId: this.episodeId })
|
||||||
|
} else {
|
||||||
|
// Add to queue
|
||||||
|
this.$emit('addToQueue', this.episode)
|
||||||
|
}
|
||||||
|
},
|
||||||
toggleFinished(confirmed = false) {
|
toggleFinished(confirmed = false) {
|
||||||
if (!this.userIsFinished && this.itemProgressPercent > 0 && !confirmed) {
|
if (!this.userIsFinished && this.itemProgressPercent > 0 && !confirmed) {
|
||||||
const payload = {
|
const payload = {
|
||||||
@ -171,37 +190,47 @@ export default {
|
|||||||
},
|
},
|
||||||
type: 'yesNo'
|
type: 'yesNo'
|
||||||
}
|
}
|
||||||
this.$store.commit('globals/setConfirmPrompt', payload)
|
this.store.commit('globals/setConfirmPrompt', payload)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var updatePayload = {
|
const updatePayload = {
|
||||||
isFinished: !this.userIsFinished
|
isFinished: !this.userIsFinished
|
||||||
}
|
}
|
||||||
this.isProcessingReadUpdate = true
|
this.isProcessingReadUpdate = true
|
||||||
this.$axios
|
this.axios
|
||||||
.$patch(`/api/me/progress/${this.libraryItemId}/${this.episode.id}`, updatePayload)
|
.$patch(`/api/me/progress/${this.libraryItemId}/${this.episodeId}`, updatePayload)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.isProcessingReadUpdate = false
|
this.isProcessingReadUpdate = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
this.isProcessingReadUpdate = false
|
this.isProcessingReadUpdate = false
|
||||||
this.$toast.error(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedFailed : this.$strings.ToastItemMarkedAsNotFinishedFailed)
|
const toast = this.$toast || this.$nuxt.$toast
|
||||||
|
toast.error(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedFailed : this.$strings.ToastItemMarkedAsNotFinishedFailed)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
clickAddToPlaylist() {
|
||||||
|
this.$emit('addToPlaylist', this.episode)
|
||||||
|
},
|
||||||
|
clickEdit() {
|
||||||
|
this.$emit('edit', this.episode)
|
||||||
|
},
|
||||||
removeClick() {
|
removeClick() {
|
||||||
this.$emit('remove', this.episode)
|
this.$emit('remove', this.episode)
|
||||||
},
|
},
|
||||||
queueBtnClick() {
|
destroy() {
|
||||||
if (this.isQueued) {
|
// destroy the vue listeners, etc
|
||||||
// Remove from queue
|
this.$destroy()
|
||||||
this.$store.commit('removeItemFromQueue', { libraryItemId: this.libraryItemId, episodeId: this.episode.id })
|
|
||||||
} else {
|
// remove the element from the DOM
|
||||||
// Add to queue
|
if (this.$el && this.$el.parentNode) {
|
||||||
this.$emit('addToQueue', this.episode)
|
this.$el.parentNode.removeChild(this.$el)
|
||||||
|
} else if (this.$el && this.$el.remove) {
|
||||||
|
this.$el.remove()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full py-6">
|
<div id="lazy-episodes-table" class="w-full py-6">
|
||||||
<div class="flex flex-wrap flex-col md:flex-row md:items-center mb-4">
|
<div class="flex flex-wrap flex-col md:flex-row md:items-center mb-4">
|
||||||
<div class="flex items-center flex-nowrap whitespace-nowrap mb-2 md:mb-0">
|
<div class="flex items-center flex-nowrap whitespace-nowrap mb-2 md:mb-0">
|
||||||
<p class="text-lg mb-0 font-semibold">{{ $strings.HeaderEpisodes }}</p>
|
<p class="text-lg mb-0 font-semibold">{{ $strings.HeaderEpisodes }}</p>
|
||||||
@ -18,28 +18,41 @@
|
|||||||
<ui-btn :disabled="processing" small class="ml-2 h-9" @click="clearSelected">{{ $strings.ButtonCancel }}</ui-btn>
|
<ui-btn :disabled="processing" small class="ml-2 h-9" @click="clearSelected">{{ $strings.ButtonCancel }}</ui-btn>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<controls-filter-select v-model="filterKey" :items="filterItems" class="w-36 h-9 md:ml-4" />
|
<controls-filter-select v-model="filterKey" :items="filterItems" class="w-36 h-9 md:ml-4" @change="filterSortChanged" />
|
||||||
<controls-sort-select v-model="sortKey" :descending.sync="sortDesc" :items="sortItems" class="w-44 md:w-48 h-9 ml-1 sm:ml-4" />
|
<controls-sort-select v-model="sortKey" :descending.sync="sortDesc" :items="sortItems" class="w-44 md:w-48 h-9 ml-1 sm:ml-4" @change="filterSortChanged" />
|
||||||
<div class="flex-grow md:hidden" />
|
<div class="flex-grow md:hidden" />
|
||||||
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" class="ml-1" @action="contextMenuAction" />
|
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" class="ml-1" @action="contextMenuAction" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="!episodes.length" class="py-4 text-center text-lg">{{ $strings.MessageNoEpisodes }}</p>
|
<!-- <p v-if="!episodes.length" class="py-4 text-center text-lg">{{ $strings.MessageNoEpisodes }}</p> -->
|
||||||
<div v-if="episodes.length" class="w-full py-3 mx-auto flex">
|
<div v-if="episodes.length" class="w-full py-3 mx-auto flex">
|
||||||
<form @submit.prevent="submit" class="flex flex-grow">
|
<form @submit.prevent="submit" class="flex flex-grow">
|
||||||
<ui-text-input v-model="search" @input="inputUpdate" type="search" :placeholder="$strings.PlaceholderSearchEpisode" class="flex-grow mr-2 text-sm md:text-base" />
|
<ui-text-input v-model="search" @input="inputUpdate" type="search" :placeholder="$strings.PlaceholderSearchEpisode" class="flex-grow mr-2 text-sm md:text-base" />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<template v-for="episode in episodesList">
|
<div class="relative min-h-[176px]">
|
||||||
<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" @addToQueue="addEpisodeToQueue" @addToPlaylist="addToPlaylist" />
|
<template v-for="episode in totalEpisodes">
|
||||||
</template>
|
<div :key="episode" :id="`episode-${episode - 1}`" class="w-full h-44 px-2 py-3 overflow-hidden relative border-b border-white/10">
|
||||||
|
<!-- episode is mounted here -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-if="isSearching" class="w-full h-full absolute inset-0 flex justify-center py-12" :class="{ 'bg-black/50': totalEpisodes }">
|
||||||
|
<ui-loading-indicator />
|
||||||
|
</div>
|
||||||
|
<div v-else-if="!totalEpisodes" class="h-44 flex items-center justify-center">
|
||||||
|
<p class="text-lg">{{ $strings.MessageNoEpisodes }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<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" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import Vue from 'vue'
|
||||||
|
import LazyEpisodeRow from './LazyEpisodeRow.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
libraryItem: {
|
libraryItem: {
|
||||||
@ -60,7 +73,15 @@ export default {
|
|||||||
processing: false,
|
processing: false,
|
||||||
search: null,
|
search: null,
|
||||||
searchTimeout: null,
|
searchTimeout: null,
|
||||||
searchText: null
|
searchText: null,
|
||||||
|
isSearching: false,
|
||||||
|
totalEpisodes: 0,
|
||||||
|
episodesPerPage: null,
|
||||||
|
episodeIndexesMounted: [],
|
||||||
|
episodeComponentRefs: {},
|
||||||
|
windowHeight: 0,
|
||||||
|
episodesTableOffsetTop: 0,
|
||||||
|
episodeRowHeight: 176
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@ -194,13 +215,19 @@ export default {
|
|||||||
submit() {},
|
submit() {},
|
||||||
inputUpdate() {
|
inputUpdate() {
|
||||||
clearTimeout(this.searchTimeout)
|
clearTimeout(this.searchTimeout)
|
||||||
|
this.isSearching = true
|
||||||
|
let searchStart = this.searchText
|
||||||
this.searchTimeout = setTimeout(() => {
|
this.searchTimeout = setTimeout(() => {
|
||||||
if (!this.search || !this.search.trim()) {
|
this.isSearching = false
|
||||||
|
if (!this.search?.trim()) {
|
||||||
this.searchText = ''
|
this.searchText = ''
|
||||||
return
|
} else {
|
||||||
|
this.searchText = this.search.toLowerCase().trim()
|
||||||
}
|
}
|
||||||
this.searchText = this.search.toLowerCase().trim()
|
if (searchStart !== this.searchText) {
|
||||||
}, 500)
|
this.init()
|
||||||
|
}
|
||||||
|
}, 750)
|
||||||
},
|
},
|
||||||
contextMenuAction({ action }) {
|
contextMenuAction({ action }) {
|
||||||
if (action === 'quick-match-episodes') {
|
if (action === 'quick-match-episodes') {
|
||||||
@ -304,24 +331,30 @@ export default {
|
|||||||
if (!val) this.episodesToRemove = []
|
if (!val) this.episodesToRemove = []
|
||||||
},
|
},
|
||||||
clearSelected() {
|
clearSelected() {
|
||||||
const episodeRows = this.$refs.episodeRow
|
|
||||||
if (episodeRows && episodeRows.length) {
|
|
||||||
for (const epRow of episodeRows) {
|
|
||||||
if (epRow) epRow.isSelected = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.selectedEpisodes = []
|
this.selectedEpisodes = []
|
||||||
|
this.setSelectionModeForEpisodes()
|
||||||
},
|
},
|
||||||
removeSelectedEpisodes() {
|
removeSelectedEpisodes() {
|
||||||
this.episodesToRemove = this.selectedEpisodes
|
this.episodesToRemove = this.selectedEpisodes
|
||||||
this.showPodcastRemoveModal = true
|
this.showPodcastRemoveModal = true
|
||||||
},
|
},
|
||||||
episodeSelected({ isSelected, episode }) {
|
episodeSelected({ isSelected, episode }) {
|
||||||
|
let isSelectionModeBefore = this.isSelectionMode
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
this.selectedEpisodes.push(episode)
|
this.selectedEpisodes.push(episode)
|
||||||
} else {
|
} else {
|
||||||
this.selectedEpisodes = this.selectedEpisodes.filter((ep) => ep.id !== episode.id)
|
this.selectedEpisodes = this.selectedEpisodes.filter((ep) => ep.id !== episode.id)
|
||||||
}
|
}
|
||||||
|
if (this.isSelectionMode !== isSelectionModeBefore) {
|
||||||
|
this.setSelectionModeForEpisodes()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setSelectionModeForEpisodes() {
|
||||||
|
for (const key in this.episodeComponentRefs) {
|
||||||
|
if (this.episodeComponentRefs[key]?.setSelectionMode) {
|
||||||
|
this.episodeComponentRefs[key].setSelectionMode(this.isSelectionMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
playEpisode(episode) {
|
playEpisode(episode) {
|
||||||
const queueItems = []
|
const queueItems = []
|
||||||
@ -367,12 +400,143 @@ export default {
|
|||||||
this.$store.commit('globals/setSelectedEpisode', episode)
|
this.$store.commit('globals/setSelectedEpisode', episode)
|
||||||
this.$store.commit('globals/setShowViewPodcastEpisodeModal', true)
|
this.$store.commit('globals/setShowViewPodcastEpisodeModal', true)
|
||||||
},
|
},
|
||||||
|
destroyEpisodeComponents() {
|
||||||
|
for (const key in this.episodeComponentRefs) {
|
||||||
|
if (this.episodeComponentRefs[key]?.destroy) {
|
||||||
|
this.episodeComponentRefs[key].destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.episodeComponentRefs = {}
|
||||||
|
this.episodeIndexesMounted = []
|
||||||
|
},
|
||||||
|
mountEpisode(index) {
|
||||||
|
const episodeEl = document.getElementById(`episode-${index}`)
|
||||||
|
if (!episodeEl) {
|
||||||
|
console.warn('Episode row el not found at ' + index)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.episodeIndexesMounted.push(index)
|
||||||
|
|
||||||
|
if (this.episodeComponentRefs[index]) {
|
||||||
|
const episodeComponent = this.episodeComponentRefs[index]
|
||||||
|
episodeEl.appendChild(episodeComponent.$el)
|
||||||
|
if (this.isSelectionMode) {
|
||||||
|
episodeComponent.setSelectionMode(true)
|
||||||
|
if (this.selectedEpisodes.some((i) => i.id === episodeComponent.episodeId)) {
|
||||||
|
episodeComponent.isSelected = true
|
||||||
|
} else {
|
||||||
|
episodeComponent.isSelected = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
episodeComponent.setSelectionMode(false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const _this = this
|
||||||
|
const ComponentClass = Vue.extend(LazyEpisodeRow)
|
||||||
|
const instance = new ComponentClass({
|
||||||
|
propsData: {
|
||||||
|
index,
|
||||||
|
libraryItemId: this.libraryItem.id,
|
||||||
|
episode: this.episodesList[index]
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.$on('selected', (payload) => {
|
||||||
|
_this.episodeSelected(payload)
|
||||||
|
})
|
||||||
|
this.$on('view', (payload) => {
|
||||||
|
_this.viewEpisode(payload)
|
||||||
|
})
|
||||||
|
this.$on('play', (payload) => {
|
||||||
|
_this.playEpisode(payload)
|
||||||
|
})
|
||||||
|
this.$on('addToQueue', (payload) => {
|
||||||
|
_this.addEpisodeToQueue(payload)
|
||||||
|
})
|
||||||
|
this.$on('remove', (payload) => {
|
||||||
|
_this.removeEpisode(payload)
|
||||||
|
})
|
||||||
|
this.$on('edit', (payload) => {
|
||||||
|
_this.editEpisode(payload)
|
||||||
|
})
|
||||||
|
this.$on('addToPlaylist', (payload) => {
|
||||||
|
_this.addToPlaylist(payload)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.episodeComponentRefs[index] = instance
|
||||||
|
instance.$mount()
|
||||||
|
episodeEl.appendChild(instance.$el)
|
||||||
|
|
||||||
|
if (this.isSelectionMode) {
|
||||||
|
instance.setSelectionMode(true)
|
||||||
|
if (this.selectedEpisodes.some((i) => i.id === this.episodesList[index].id)) {
|
||||||
|
instance.isSelected = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mountEpisodes(startIndex, endIndex) {
|
||||||
|
for (let i = startIndex; i < endIndex; i++) {
|
||||||
|
if (!this.episodeIndexesMounted.includes(i)) {
|
||||||
|
this.mountEpisode(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scroll(evt) {
|
||||||
|
if (!evt?.target?.scrollTop) return
|
||||||
|
const scrollTop = Math.max(evt.target.scrollTop - this.episodesTableOffsetTop, 0)
|
||||||
|
let firstEpisodeIndex = Math.floor(scrollTop / this.episodeRowHeight)
|
||||||
|
let lastEpisodeIndex = Math.ceil((scrollTop + this.windowHeight) / this.episodeRowHeight)
|
||||||
|
lastEpisodeIndex = Math.min(this.totalEpisodes - 1, lastEpisodeIndex)
|
||||||
|
|
||||||
|
this.episodeIndexesMounted = this.episodeIndexesMounted.filter((_index) => {
|
||||||
|
if (_index < firstEpisodeIndex || _index >= lastEpisodeIndex) {
|
||||||
|
const el = document.getElementById(`lazy-episode-${_index}`)
|
||||||
|
if (el) el.remove()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
this.mountEpisodes(firstEpisodeIndex, lastEpisodeIndex + 1)
|
||||||
|
},
|
||||||
|
initListeners() {
|
||||||
|
const itemPageWrapper = document.getElementById('item-page-wrapper')
|
||||||
|
if (itemPageWrapper) {
|
||||||
|
itemPageWrapper.addEventListener('scroll', this.scroll)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeListeners() {
|
||||||
|
const itemPageWrapper = document.getElementById('item-page-wrapper')
|
||||||
|
if (itemPageWrapper) {
|
||||||
|
itemPageWrapper.removeEventListener('scroll', this.scroll)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
filterSortChanged() {
|
||||||
|
this.init()
|
||||||
|
},
|
||||||
init() {
|
init() {
|
||||||
this.episodesCopy = this.episodes.map((ep) => ({ ...ep }))
|
this.destroyEpisodeComponents()
|
||||||
|
this.totalEpisodes = this.episodesList.length
|
||||||
|
|
||||||
|
const lazyEpisodesTableEl = document.getElementById('lazy-episodes-table')
|
||||||
|
this.episodesTableOffsetTop = (lazyEpisodesTableEl?.offsetTop || 0) + 64
|
||||||
|
|
||||||
|
this.windowHeight = window.innerHeight
|
||||||
|
this.episodesPerPage = Math.ceil(this.windowHeight / this.episodeRowHeight)
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.mountEpisodes(0, Math.min(this.episodesPerPage, this.totalEpisodes))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.episodesCopy = this.episodes.map((ep) => ({ ...ep }))
|
||||||
|
this.initListeners()
|
||||||
this.init()
|
this.init()
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.removeListeners()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
4
client/package-lock.json
generated
4
client/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.7.0",
|
"version": "2.7.1",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.7.0",
|
"version": "2.7.1",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/axios": "^5.13.6",
|
"@nuxtjs/axios": "^5.13.6",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.7.0",
|
"version": "2.7.1",
|
||||||
"buildNumber": 1,
|
"buildNumber": 1,
|
||||||
"description": "Self-hosted audiobook and podcast client",
|
"description": "Self-hosted audiobook and podcast client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="page-wrapper" class="bg-bg page overflow-hidden" :class="streamLibraryItem ? 'streaming' : ''">
|
<div id="page-wrapper" class="bg-bg page overflow-hidden" :class="streamLibraryItem ? 'streaming' : ''">
|
||||||
<div class="w-full h-full overflow-y-auto px-2 py-6 lg:p-8">
|
<div id="item-page-wrapper" class="w-full h-full overflow-y-auto px-2 py-6 lg:p-8">
|
||||||
<div class="flex flex-col lg:flex-row max-w-6xl mx-auto">
|
<div class="flex flex-col lg:flex-row max-w-6xl mx-auto">
|
||||||
<div class="w-full flex justify-center lg:block lg:w-52" style="min-width: 208px">
|
<div class="w-full flex justify-center lg:block lg:w-52" style="min-width: 208px">
|
||||||
<div class="relative group" style="height: fit-content">
|
<div class="relative group" style="height: fit-content">
|
||||||
@ -136,7 +136,7 @@
|
|||||||
|
|
||||||
<widgets-audiobook-data v-if="tracks.length" :library-item-id="libraryItemId" :is-file="isFile" :media="media" />
|
<widgets-audiobook-data v-if="tracks.length" :library-item-id="libraryItemId" :is-file="isFile" :media="media" />
|
||||||
|
|
||||||
<tables-podcast-episodes-table v-if="isPodcast" :library-item="libraryItem" />
|
<tables-podcast-lazy-episodes-table v-if="isPodcast" :library-item="libraryItem" />
|
||||||
|
|
||||||
<tables-chapters-table v-if="chapters.length" :library-item="libraryItem" class="mt-6" />
|
<tables-chapters-table v-if="chapters.length" :library-item="libraryItem" class="mt-6" />
|
||||||
|
|
||||||
|
@ -155,7 +155,9 @@ export default {
|
|||||||
if (this.episodeIdStreaming === episode.id) return this.streamIsPlaying ? 'Streaming' : 'Play'
|
if (this.episodeIdStreaming === episode.id) return this.streamIsPlaying ? 'Streaming' : 'Play'
|
||||||
if (!episode.progress) return this.$elapsedPretty(episode.duration)
|
if (!episode.progress) return this.$elapsedPretty(episode.duration)
|
||||||
if (episode.progress.isFinished) return 'Finished'
|
if (episode.progress.isFinished) return 'Finished'
|
||||||
var remaining = Math.floor(episode.progress.duration - episode.progress.currentTime)
|
|
||||||
|
const duration = episode.progress.duration || episode.duration
|
||||||
|
const remaining = Math.floor(duration - episode.progress.currentTime)
|
||||||
return `${this.$elapsedPretty(remaining)} left`
|
return `${this.$elapsedPretty(remaining)} left`
|
||||||
},
|
},
|
||||||
playClick(episodeToPlay) {
|
playClick(episodeToPlay) {
|
||||||
|
@ -80,13 +80,11 @@ export const actions = {
|
|||||||
return state.folders
|
return state.folders
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('Loading folders')
|
|
||||||
commit('setFoldersLastUpdate')
|
commit('setFoldersLastUpdate')
|
||||||
|
|
||||||
return this.$axios
|
return this.$axios
|
||||||
.$get('/api/filesystem')
|
.$get('/api/filesystem')
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
console.log('Settings folders', res)
|
|
||||||
commit('setFolders', res.directories)
|
commit('setFolders', res.directories)
|
||||||
return res.directories
|
return res.directories
|
||||||
})
|
})
|
||||||
@ -119,15 +117,16 @@ export const actions = {
|
|||||||
|
|
||||||
dispatch('user/checkUpdateLibrarySortFilter', library.mediaType, { root: true })
|
dispatch('user/checkUpdateLibrarySortFilter', library.mediaType, { root: true })
|
||||||
|
|
||||||
|
if (libraryChanging) {
|
||||||
|
commit('setCollections', [])
|
||||||
|
commit('setUserPlaylists', [])
|
||||||
|
}
|
||||||
|
|
||||||
commit('addUpdate', library)
|
commit('addUpdate', library)
|
||||||
commit('setLibraryIssues', issues)
|
commit('setLibraryIssues', issues)
|
||||||
commit('setLibraryFilterData', filterData)
|
commit('setLibraryFilterData', filterData)
|
||||||
commit('setNumUserPlaylists', numUserPlaylists)
|
commit('setNumUserPlaylists', numUserPlaylists)
|
||||||
commit('setCurrentLibrary', libraryId)
|
commit('setCurrentLibrary', libraryId)
|
||||||
if (libraryChanging) {
|
|
||||||
commit('setCollections', [])
|
|
||||||
commit('setUserPlaylists', [])
|
|
||||||
}
|
|
||||||
return data
|
return data
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.7.0",
|
"version": "2.7.1",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.7.0",
|
"version": "2.7.1",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.7.0",
|
"version": "2.7.1",
|
||||||
"buildNumber": 1,
|
"buildNumber": 1,
|
||||||
"description": "Self-hosted audiobook and podcast server",
|
"description": "Self-hosted audiobook and podcast server",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
@ -139,15 +139,16 @@ class Server {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @temporary
|
* @temporary
|
||||||
* This is necessary for the ebook API endpoint in the mobile apps
|
* This is necessary for the ebook & cover API endpoint in the mobile apps
|
||||||
* The mobile app ereader is using fetch api in Capacitor that is currently difficult to switch to native requests
|
* The mobile app ereader is using fetch api in Capacitor that is currently difficult to switch to native requests
|
||||||
* so we have to allow cors for specific origins to the /api/items/:id/ebook endpoint
|
* so we have to allow cors for specific origins to the /api/items/:id/ebook endpoint
|
||||||
|
* The cover image is fetched with XMLHttpRequest in the mobile apps to load into a canvas and extract colors
|
||||||
* @see https://ionicframework.com/docs/troubleshooting/cors
|
* @see https://ionicframework.com/docs/troubleshooting/cors
|
||||||
*
|
*
|
||||||
* Running in development allows cors to allow testing the mobile apps in the browser
|
* Running in development allows cors to allow testing the mobile apps in the browser
|
||||||
*/
|
*/
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
if (Logger.isDev || req.path.match(/\/api\/items\/([a-z0-9-]{36})\/ebook(\/[0-9]+)?/)) {
|
if (Logger.isDev || req.path.match(/\/api\/items\/([a-z0-9-]{36})\/(ebook|cover)(\/[0-9]+)?/)) {
|
||||||
const allowedOrigins = ['capacitor://localhost', 'http://localhost']
|
const allowedOrigins = ['capacitor://localhost', 'http://localhost']
|
||||||
if (Logger.isDev || allowedOrigins.some(o => o === req.get('origin'))) {
|
if (Logger.isDev || allowedOrigins.some(o => o === req.get('origin'))) {
|
||||||
res.header('Access-Control-Allow-Origin', req.get('origin'))
|
res.header('Access-Control-Allow-Origin', req.get('origin'))
|
||||||
@ -287,7 +288,7 @@ class Server {
|
|||||||
await this.stop()
|
await this.stop()
|
||||||
Logger.info('Server stopped. Exiting.')
|
Logger.info('Server stopped. Exiting.')
|
||||||
} else {
|
} else {
|
||||||
Logger.info('SIGINT (Ctrl+C) received again. Exiting immediately.')
|
Logger.info('SIGINT (Ctrl+C) received again. Exiting immediately.')
|
||||||
}
|
}
|
||||||
process.exit(0)
|
process.exit(0)
|
||||||
})
|
})
|
||||||
@ -398,13 +399,17 @@ class Server {
|
|||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gracefully stop server
|
||||||
|
* Stops watcher and socket server
|
||||||
|
*/
|
||||||
async stop() {
|
async stop() {
|
||||||
Logger.info('=== Stopping Server ===')
|
Logger.info('=== Stopping Server ===')
|
||||||
await this.watcher.close()
|
await this.watcher.close()
|
||||||
Logger.info('Watcher Closed')
|
Logger.info('Watcher Closed')
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
this.server.close((err) => {
|
SocketAuthority.close((err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
Logger.error('Failed to close server', err)
|
Logger.error('Failed to close server', err)
|
||||||
} else {
|
} else {
|
||||||
|
@ -73,6 +73,20 @@ class SocketAuthority {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the Socket.IO server and disconnect all clients
|
||||||
|
*
|
||||||
|
* @param {Function} callback
|
||||||
|
*/
|
||||||
|
close(callback) {
|
||||||
|
Logger.info('[SocketAuthority] Shutting down')
|
||||||
|
// This will close all open socket connections, and also close the underlying http server
|
||||||
|
if (this.io)
|
||||||
|
this.io.close(callback)
|
||||||
|
else
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
|
||||||
initialize(Server) {
|
initialize(Server) {
|
||||||
this.Server = Server
|
this.Server = Server
|
||||||
|
|
||||||
|
@ -419,40 +419,45 @@ class LibraryItem extends Model {
|
|||||||
*/
|
*/
|
||||||
static async getOldById(libraryItemId) {
|
static async getOldById(libraryItemId) {
|
||||||
if (!libraryItemId) return null
|
if (!libraryItemId) return null
|
||||||
const libraryItem = await this.findByPk(libraryItemId, {
|
|
||||||
include: [
|
const libraryItem = await this.findByPk(libraryItemId)
|
||||||
{
|
if (!libraryItem) {
|
||||||
model: this.sequelize.models.book,
|
Logger.error(`[LibraryItem] Library item not found with id "${libraryItemId}"`)
|
||||||
include: [
|
return null
|
||||||
{
|
}
|
||||||
model: this.sequelize.models.author,
|
|
||||||
through: {
|
if (libraryItem.mediaType === 'podcast') {
|
||||||
attributes: []
|
libraryItem.media = await libraryItem.getMedia({
|
||||||
}
|
include: [
|
||||||
},
|
{
|
||||||
{
|
model: this.sequelize.models.podcastEpisode
|
||||||
model: this.sequelize.models.series,
|
}
|
||||||
through: {
|
]
|
||||||
attributes: ['sequence']
|
})
|
||||||
}
|
} else {
|
||||||
|
libraryItem.media = await libraryItem.getMedia({
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: this.sequelize.models.author,
|
||||||
|
through: {
|
||||||
|
attributes: []
|
||||||
}
|
}
|
||||||
]
|
},
|
||||||
},
|
{
|
||||||
{
|
model: this.sequelize.models.series,
|
||||||
model: this.sequelize.models.podcast,
|
through: {
|
||||||
include: [
|
attributes: ['sequence']
|
||||||
{
|
|
||||||
model: this.sequelize.models.podcastEpisode
|
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
],
|
||||||
],
|
order: [
|
||||||
order: [
|
[this.sequelize.models.author, this.sequelize.models.bookAuthor, 'createdAt', 'ASC'],
|
||||||
[this.sequelize.models.book, this.sequelize.models.author, this.sequelize.models.bookAuthor, 'createdAt', 'ASC'],
|
[this.sequelize.models.series, 'bookSeries', 'createdAt', 'ASC']
|
||||||
[this.sequelize.models.book, this.sequelize.models.series, 'bookSeries', 'createdAt', 'ASC']
|
]
|
||||||
]
|
})
|
||||||
})
|
}
|
||||||
if (!libraryItem) return null
|
|
||||||
|
if (!libraryItem.media) return null
|
||||||
return this.getOldLibraryItem(libraryItem)
|
return this.getOldLibraryItem(libraryItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,7 +152,12 @@ class PodcastEpisode extends Model {
|
|||||||
extraData: DataTypes.JSON
|
extraData: DataTypes.JSON
|
||||||
}, {
|
}, {
|
||||||
sequelize,
|
sequelize,
|
||||||
modelName: 'podcastEpisode'
|
modelName: 'podcastEpisode',
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
fields: ['createdAt']
|
||||||
|
}
|
||||||
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
const { podcast } = sequelize.models
|
const { podcast } = sequelize.models
|
||||||
|
@ -48,12 +48,14 @@ class PodcastEpisode {
|
|||||||
this.guid = episode.guid || null
|
this.guid = episode.guid || null
|
||||||
this.pubDate = episode.pubDate
|
this.pubDate = episode.pubDate
|
||||||
this.chapters = episode.chapters?.map(ch => ({ ...ch })) || []
|
this.chapters = episode.chapters?.map(ch => ({ ...ch })) || []
|
||||||
this.audioFile = new AudioFile(episode.audioFile)
|
this.audioFile = episode.audioFile ? new AudioFile(episode.audioFile) : null
|
||||||
this.publishedAt = episode.publishedAt
|
this.publishedAt = episode.publishedAt
|
||||||
this.addedAt = episode.addedAt
|
this.addedAt = episode.addedAt
|
||||||
this.updatedAt = episode.updatedAt
|
this.updatedAt = episode.updatedAt
|
||||||
|
|
||||||
this.audioFile.index = 1 // Only 1 audio file per episode
|
if (this.audioFile) {
|
||||||
|
this.audioFile.index = 1 // Only 1 audio file per episode
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
@ -73,7 +75,7 @@ class PodcastEpisode {
|
|||||||
guid: this.guid,
|
guid: this.guid,
|
||||||
pubDate: this.pubDate,
|
pubDate: this.pubDate,
|
||||||
chapters: this.chapters.map(ch => ({ ...ch })),
|
chapters: this.chapters.map(ch => ({ ...ch })),
|
||||||
audioFile: this.audioFile.toJSON(),
|
audioFile: this.audioFile?.toJSON() || null,
|
||||||
publishedAt: this.publishedAt,
|
publishedAt: this.publishedAt,
|
||||||
addedAt: this.addedAt,
|
addedAt: this.addedAt,
|
||||||
updatedAt: this.updatedAt
|
updatedAt: this.updatedAt
|
||||||
@ -97,8 +99,8 @@ class PodcastEpisode {
|
|||||||
guid: this.guid,
|
guid: this.guid,
|
||||||
pubDate: this.pubDate,
|
pubDate: this.pubDate,
|
||||||
chapters: this.chapters.map(ch => ({ ...ch })),
|
chapters: this.chapters.map(ch => ({ ...ch })),
|
||||||
audioFile: this.audioFile.toJSON(),
|
audioFile: this.audioFile?.toJSON() || null,
|
||||||
audioTrack: this.audioTrack.toJSON(),
|
audioTrack: this.audioTrack?.toJSON() || null,
|
||||||
publishedAt: this.publishedAt,
|
publishedAt: this.publishedAt,
|
||||||
addedAt: this.addedAt,
|
addedAt: this.addedAt,
|
||||||
updatedAt: this.updatedAt,
|
updatedAt: this.updatedAt,
|
||||||
@ -108,6 +110,7 @@ class PodcastEpisode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get audioTrack() {
|
get audioTrack() {
|
||||||
|
if (!this.audioFile) return null
|
||||||
const audioTrack = new AudioTrack()
|
const audioTrack = new AudioTrack()
|
||||||
audioTrack.setData(this.libraryItemId, this.audioFile, 0)
|
audioTrack.setData(this.libraryItemId, this.audioFile, 0)
|
||||||
return audioTrack
|
return audioTrack
|
||||||
@ -116,9 +119,9 @@ class PodcastEpisode {
|
|||||||
return [this.audioTrack]
|
return [this.audioTrack]
|
||||||
}
|
}
|
||||||
get duration() {
|
get duration() {
|
||||||
return this.audioFile.duration
|
return this.audioFile?.duration || 0
|
||||||
}
|
}
|
||||||
get size() { return this.audioFile.metadata.size }
|
get size() { return this.audioFile?.metadata.size || 0 }
|
||||||
get enclosureUrl() {
|
get enclosureUrl() {
|
||||||
return this.enclosure?.url || null
|
return this.enclosure?.url || null
|
||||||
}
|
}
|
||||||
|
@ -468,7 +468,7 @@ class AudioFileScanner {
|
|||||||
audioFiles.length === 1 ||
|
audioFiles.length === 1 ||
|
||||||
audioFiles.length > 1 &&
|
audioFiles.length > 1 &&
|
||||||
audioFiles[0].chapters.length === audioFiles[1].chapters?.length &&
|
audioFiles[0].chapters.length === audioFiles[1].chapters?.length &&
|
||||||
audioFiles[0].chapters.every((c, i) => c.title === audioFiles[1].chapters[i].title)
|
audioFiles[0].chapters.every((c, i) => c.title === audioFiles[1].chapters[i].title && c.start === audioFiles[1].chapters[i].start)
|
||||||
) {
|
) {
|
||||||
libraryScan.addLog(LogLevel.DEBUG, `setChapters: Using embedded chapters in first audio file ${audioFiles[0].metadata?.path}`)
|
libraryScan.addLog(LogLevel.DEBUG, `setChapters: Using embedded chapters in first audio file ${audioFiles[0].metadata?.path}`)
|
||||||
chapters = audioFiles[0].chapters.map((c) => ({ ...c }))
|
chapters = audioFiles[0].chapters.map((c) => ({ ...c }))
|
||||||
|
@ -81,7 +81,12 @@ module.exports.getFileSize = async (path) => {
|
|||||||
* @returns {Promise<number>} epoch timestamp
|
* @returns {Promise<number>} epoch timestamp
|
||||||
*/
|
*/
|
||||||
module.exports.getFileMTimeMs = async (path) => {
|
module.exports.getFileMTimeMs = async (path) => {
|
||||||
return (await getFileStat(path))?.mtimeMs || 0
|
try {
|
||||||
|
return (await getFileStat(path))?.mtimeMs || 0
|
||||||
|
} catch (err) {
|
||||||
|
Logger.error(`[fileUtils] Failed to getFileMtimeMs`, err)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user