mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-03 17:30:39 +02:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fc8fec62a0 | |||
| 034d858f18 | |||
| ebc9e1a888 | |||
| c5a9c2bf5a | |||
| 3dbce8fd71 | |||
| b2d299dba6 | |||
| cb5d9a8287 | |||
| f9530897c0 | |||
| 7c7e8285a4 | |||
| 7b3f9a1e0c | |||
| 399e0ea0bc | |||
| a47b0bce57 | |||
| 4b60b4f73e | |||
| d88b20addd | |||
| 5d12cc3f23 | |||
| 84fb7ce8b3 | |||
| 243cc672f7 | |||
| 663546dd77 | |||
| 1b79b3f42d |
@@ -166,6 +166,7 @@ export default {
|
|||||||
isFinished: newIsFinished
|
isFinished: newIsFinished
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
console.log('Progress payloads', updateProgressPayloads)
|
||||||
this.$axios
|
this.$axios
|
||||||
.patch(`/api/me/progress/batch/update`, updateProgressPayloads)
|
.patch(`/api/me/progress/batch/update`, updateProgressPayloads)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|||||||
@@ -7,6 +7,11 @@
|
|||||||
<cards-lazy-book-card :key="entity.id" :ref="`shelf-book-${entity.id}`" :index="index" :width="bookCoverWidth" :height="bookCoverHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" :book-mount="entity" class="relative mx-2" @hook:updated="updatedBookCard" @select="selectItem" @edit="editBook" />
|
<cards-lazy-book-card :key="entity.id" :ref="`shelf-book-${entity.id}`" :index="index" :width="bookCoverWidth" :height="bookCoverHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" :book-mount="entity" class="relative mx-2" @hook:updated="updatedBookCard" @select="selectItem" @edit="editBook" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="shelf.type === 'episode'" class="flex items-center">
|
||||||
|
<template v-for="(entity, index) in shelf.entities">
|
||||||
|
<cards-lazy-book-card :key="entity.recentEpisode.id" :ref="`shelf-episode-${entity.recentEpisode.id}`" :index="index" :width="bookCoverWidth" :height="bookCoverHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" :book-mount="entity" class="relative mx-2" @hook:updated="updatedBookCard" @select="selectItem" @edit="editEpisode" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
<div v-if="shelf.type === 'series'" class="flex items-center">
|
<div v-if="shelf.type === 'series'" class="flex items-center">
|
||||||
<template v-for="entity in shelf.entities">
|
<template v-for="entity in shelf.entities">
|
||||||
<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" />
|
||||||
@@ -70,11 +75,6 @@ export default {
|
|||||||
selectedAuthor: null
|
selectedAuthor: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
|
||||||
isSelectionMode(newVal) {
|
|
||||||
this.updateSelectionMode(newVal)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
bookCoverHeight() {
|
bookCoverHeight() {
|
||||||
return this.bookCoverWidth * this.bookCoverAspectRatio
|
return this.bookCoverWidth * this.bookCoverAspectRatio
|
||||||
@@ -94,6 +94,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
clearSelectedEntities() {
|
||||||
|
this.updateSelectionMode(false)
|
||||||
|
},
|
||||||
editAuthor(author) {
|
editAuthor(author) {
|
||||||
this.selectedAuthor = author
|
this.selectedAuthor = author
|
||||||
this.showAuthorModal = true
|
this.showAuthorModal = true
|
||||||
@@ -103,9 +106,14 @@ export default {
|
|||||||
this.$store.commit('setBookshelfBookIds', bookIds)
|
this.$store.commit('setBookshelfBookIds', bookIds)
|
||||||
this.$store.commit('showEditModal', audiobook)
|
this.$store.commit('showEditModal', audiobook)
|
||||||
},
|
},
|
||||||
|
editEpisode({ libraryItem, episode }) {
|
||||||
|
this.$store.commit('setSelectedLibraryItem', libraryItem)
|
||||||
|
this.$store.commit('globals/setSelectedEpisode', episode)
|
||||||
|
this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)
|
||||||
|
},
|
||||||
updateSelectionMode(val) {
|
updateSelectionMode(val) {
|
||||||
var selectedLibraryItems = this.$store.state.selectedLibraryItems
|
var selectedLibraryItems = this.$store.state.selectedLibraryItems
|
||||||
if (this.shelf.type === 'book') {
|
if (this.shelf.type === 'book' || this.shelf.type === 'podcast') {
|
||||||
this.shelf.entities.forEach((ent) => {
|
this.shelf.entities.forEach((ent) => {
|
||||||
var component = this.$refs[`shelf-book-${ent.id}`]
|
var component = this.$refs[`shelf-book-${ent.id}`]
|
||||||
if (!component || !component.length) return
|
if (!component || !component.length) return
|
||||||
@@ -113,10 +121,24 @@ export default {
|
|||||||
component.setSelectionMode(val)
|
component.setSelectionMode(val)
|
||||||
component.selected = selectedLibraryItems.includes(ent.id)
|
component.selected = selectedLibraryItems.includes(ent.id)
|
||||||
})
|
})
|
||||||
|
} else if (this.shelf.type === 'episode') {
|
||||||
|
this.shelf.entities.forEach((ent) => {
|
||||||
|
var component = this.$refs[`shelf-episode-${ent.recentEpisode.id}`]
|
||||||
|
if (!component || !component.length) return
|
||||||
|
component = component[0]
|
||||||
|
component.setSelectionMode(val)
|
||||||
|
component.selected = selectedLibraryItems.includes(ent.id)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
selectItem(libraryItem) {
|
selectItem(libraryItem) {
|
||||||
this.$store.commit('toggleLibraryItemSelected', libraryItem.id)
|
this.$store.commit('toggleLibraryItemSelected', libraryItem.id)
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$eventBus.$emit('item-selected', libraryItem)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
itemSelectedEvt() {
|
||||||
|
this.updateSelectionMode(this.isSelectionMode)
|
||||||
},
|
},
|
||||||
scrolled() {
|
scrolled() {
|
||||||
clearTimeout(this.scrollTimer)
|
clearTimeout(this.scrollTimer)
|
||||||
@@ -160,6 +182,14 @@ export default {
|
|||||||
this.canScrollLeft = false
|
this.canScrollLeft = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$eventBus.$on('bookshelf-clear-selection', this.clearSelectedEntities)
|
||||||
|
this.$eventBus.$on('item-selected', this.itemSelectedEvt)
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.$eventBus.$off('bookshelf-clear-selection', this.clearSelectedEntities)
|
||||||
|
this.$eventBus.$off('item-selected', this.itemSelectedEvt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ export default {
|
|||||||
totalShelves: 0,
|
totalShelves: 0,
|
||||||
bookshelfMarginLeft: 0,
|
bookshelfMarginLeft: 0,
|
||||||
isSelectionMode: false,
|
isSelectionMode: false,
|
||||||
isSelectAll: false,
|
|
||||||
currentSFQueryString: null,
|
currentSFQueryString: null,
|
||||||
pendingReset: false,
|
pendingReset: false,
|
||||||
keywordFilter: null,
|
keywordFilter: null,
|
||||||
@@ -90,9 +89,12 @@ export default {
|
|||||||
return this.$store.getters['libraries/getCurrentLibraryMediaType'] == 'podcast'
|
return this.$store.getters['libraries/getCurrentLibraryMediaType'] == 'podcast'
|
||||||
},
|
},
|
||||||
emptyMessage() {
|
emptyMessage() {
|
||||||
if (this.page === 'series') return `You have no series`
|
if (this.page === 'series') return 'You have no series'
|
||||||
if (this.page === 'collections') return "You haven't made any collections yet"
|
if (this.page === 'collections') return "You haven't made any collections yet"
|
||||||
if (this.hasFilter) return `No Results for filter "${this.filterName}: ${this.filterValue}"`
|
if (this.hasFilter) {
|
||||||
|
if (this.filterName === 'Issues') return 'No Issues'
|
||||||
|
return `No Results for filter "${this.filterName}: ${this.filterValue}"`
|
||||||
|
}
|
||||||
return 'No results'
|
return 'No results'
|
||||||
},
|
},
|
||||||
entityName() {
|
entityName() {
|
||||||
@@ -217,7 +219,6 @@ export default {
|
|||||||
clearSelectedEntities() {
|
clearSelectedEntities() {
|
||||||
this.updateBookSelectionMode(false)
|
this.updateBookSelectionMode(false)
|
||||||
this.isSelectionMode = false
|
this.isSelectionMode = false
|
||||||
this.isSelectAll = false
|
|
||||||
},
|
},
|
||||||
selectEntity(entity) {
|
selectEntity(entity) {
|
||||||
if (this.entityName === 'books' || this.entityName === 'series-books') {
|
if (this.entityName === 'books' || this.entityName === 'series-books') {
|
||||||
@@ -339,7 +340,6 @@ export default {
|
|||||||
this.totalEntities = 0
|
this.totalEntities = 0
|
||||||
this.currentPage = 0
|
this.currentPage = 0
|
||||||
this.isSelectionMode = false
|
this.isSelectionMode = false
|
||||||
this.isSelectAll = false
|
|
||||||
this.initialized = false
|
this.initialized = false
|
||||||
|
|
||||||
this.initSizeData()
|
this.initSizeData()
|
||||||
|
|||||||
@@ -69,7 +69,8 @@ export default {
|
|||||||
sleepTimerTime: 0,
|
sleepTimerTime: 0,
|
||||||
sleepTimerRemaining: 0,
|
sleepTimerRemaining: 0,
|
||||||
sleepTimer: null,
|
sleepTimer: null,
|
||||||
displayTitle: null
|
displayTitle: null,
|
||||||
|
initialPlaybackRate: 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -204,6 +205,7 @@ export default {
|
|||||||
this.playerHandler.setVolume(volume)
|
this.playerHandler.setVolume(volume)
|
||||||
},
|
},
|
||||||
setPlaybackRate(playbackRate) {
|
setPlaybackRate(playbackRate) {
|
||||||
|
this.initialPlaybackRate = playbackRate
|
||||||
this.playerHandler.setPlaybackRate(playbackRate)
|
this.playerHandler.setPlaybackRate(playbackRate)
|
||||||
},
|
},
|
||||||
seek(time) {
|
seek(time) {
|
||||||
@@ -253,7 +255,7 @@ export default {
|
|||||||
libraryItem: session.libraryItem,
|
libraryItem: session.libraryItem,
|
||||||
episodeId: session.episodeId
|
episodeId: session.episodeId
|
||||||
})
|
})
|
||||||
this.playerHandler.prepareOpenSession(session)
|
this.playerHandler.prepareOpenSession(session, this.initialPlaybackRate)
|
||||||
},
|
},
|
||||||
streamOpen(session) {
|
streamOpen(session) {
|
||||||
console.log(`[StreamContainer] Stream session open`, session)
|
console.log(`[StreamContainer] Stream session open`, session)
|
||||||
@@ -311,7 +313,7 @@ export default {
|
|||||||
episodeId
|
episodeId
|
||||||
})
|
})
|
||||||
|
|
||||||
this.playerHandler.load(libraryItem, episodeId, true)
|
this.playerHandler.load(libraryItem, episodeId, true, this.initialPlaybackRate)
|
||||||
},
|
},
|
||||||
pauseItem() {
|
pauseItem() {
|
||||||
this.playerHandler.pause()
|
this.playerHandler.pause()
|
||||||
|
|||||||
@@ -35,8 +35,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- No progress shown for collapsed series in library and podcasts -->
|
<!-- No progress shown for collapsed series in library and podcasts (unless showing podcast episode) -->
|
||||||
<div v-if="!booksInSeries && !isPodcast" 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)" class="w-full h-full absolute top-0 left-0 z-10 bg-black rounded hidden md:block" :class="overlayWrapperClasslist">
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
<span class="material-icons" :class="selected ? 'text-yellow-400' : ''" :style="{ fontSize: 1.25 * sizeMultiplier + 'rem' }">{{ selected ? 'radio_button_checked' : 'radio_button_unchecked' }}</span>
|
<span class="material-icons" :class="selected ? 'text-yellow-400' : ''" :style="{ fontSize: 1.25 * sizeMultiplier + 'rem' }">{{ selected ? 'radio_button_checked' : 'radio_button_unchecked' }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ref="moreIcon" v-show="!isSelectionMode" class="hidden md:block absolute cursor-pointer hover:text-yellow-300" :style="{ bottom: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickShowMore">
|
<div ref="moreIcon" v-show="!isSelectionMode && !recentEpisode" class="hidden md:block absolute cursor-pointer hover:text-yellow-300" :style="{ bottom: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickShowMore">
|
||||||
<span class="material-icons" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">more_vert</span>
|
<span class="material-icons" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">more_vert</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -77,13 +77,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<!-- Volume number -->
|
<!-- Series sequence -->
|
||||||
<div v-if="seriesSequence && showSequence && !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="seriesSequence && showSequence && !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` }">
|
||||||
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">#{{ seriesSequence }}</p>
|
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">#{{ seriesSequence }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 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` }">
|
||||||
|
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">Episode #{{ recentEpisodeNumber }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Podcast Num Episodes -->
|
<!-- Podcast Num Episodes -->
|
||||||
<div v-if="numEpisodes && !isHovering && !isSelectionMode" class="absolute rounded-full bg-black bg-opacity-90 box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', width: 1.25 * sizeMultiplier + 'rem', height: 1.25 * sizeMultiplier + 'rem' }">
|
<div v-else-if="numEpisodes && !isHovering && !isSelectionMode" class="absolute rounded-full bg-black bg-opacity-90 box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', width: 1.25 * sizeMultiplier + 'rem', height: 1.25 * sizeMultiplier + 'rem' }">
|
||||||
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ numEpisodes }}</p>
|
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ numEpisodes }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -190,6 +195,17 @@ export default {
|
|||||||
processingBatch() {
|
processingBatch() {
|
||||||
return this.store.state.processingBatch
|
return this.store.state.processingBatch
|
||||||
},
|
},
|
||||||
|
recentEpisode() {
|
||||||
|
// Only added to item when getting currently listening podcasts
|
||||||
|
return this._libraryItem.recentEpisode
|
||||||
|
},
|
||||||
|
recentEpisodeNumber() {
|
||||||
|
if (!this.recentEpisode) return null
|
||||||
|
if (this.recentEpisode.episode) {
|
||||||
|
return this.recentEpisode.episode.replace(/^#/, '')
|
||||||
|
}
|
||||||
|
return this.recentEpisode.index
|
||||||
|
},
|
||||||
collapsedSeries() {
|
collapsedSeries() {
|
||||||
// Only added to item object when collapseSeries is enabled
|
// Only added to item object when collapseSeries is enabled
|
||||||
return this._libraryItem.collapsedSeries
|
return this._libraryItem.collapsedSeries
|
||||||
@@ -240,7 +256,13 @@ export default {
|
|||||||
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
|
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
|
||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
|
episodeProgress() {
|
||||||
|
// Only used on home page currently listening podcast shelf
|
||||||
|
if (!this.recentEpisode) return null
|
||||||
|
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId, this.recentEpisode.id)
|
||||||
|
},
|
||||||
userProgress() {
|
userProgress() {
|
||||||
|
if (this.episodeProgress) return this.episodeProgress
|
||||||
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
||||||
},
|
},
|
||||||
userProgressPercent() {
|
userProgressPercent() {
|
||||||
@@ -250,7 +272,7 @@ export default {
|
|||||||
return this.userProgress ? !!this.userProgress.isFinished : false
|
return this.userProgress ? !!this.userProgress.isFinished : false
|
||||||
},
|
},
|
||||||
showError() {
|
showError() {
|
||||||
return this.hasMissingParts || this.hasInvalidParts || this.isMissing || this.isInvalid
|
return this.numMissingParts || this.isMissing || this.isInvalid
|
||||||
},
|
},
|
||||||
isStreaming() {
|
isStreaming() {
|
||||||
return this.store.getters['getlibraryItemIdStreaming'] === this.libraryItemId
|
return this.store.getters['getlibraryItemIdStreaming'] === this.libraryItemId
|
||||||
@@ -259,7 +281,7 @@ export default {
|
|||||||
return !this.isSelectionMode && this.showExperimentalFeatures && !this.showPlayButton && this.hasEbook
|
return !this.isSelectionMode && this.showExperimentalFeatures && !this.showPlayButton && this.hasEbook
|
||||||
},
|
},
|
||||||
showPlayButton() {
|
showPlayButton() {
|
||||||
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && this.numTracks && !this.isStreaming
|
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && !this.isStreaming && (this.numTracks || this.recentEpisode)
|
||||||
},
|
},
|
||||||
showSmallEBookIcon() {
|
showSmallEBookIcon() {
|
||||||
return !this.isSelectionMode && this.showExperimentalFeatures && this.hasEbook
|
return !this.isSelectionMode && this.showExperimentalFeatures && this.hasEbook
|
||||||
@@ -270,22 +292,19 @@ export default {
|
|||||||
isInvalid() {
|
isInvalid() {
|
||||||
return this._libraryItem.isInvalid
|
return this._libraryItem.isInvalid
|
||||||
},
|
},
|
||||||
hasMissingParts() {
|
numMissingParts() {
|
||||||
return this._libraryItem.hasMissingParts
|
if (this.isPodcast) return 0
|
||||||
},
|
return this.media.numMissingParts
|
||||||
hasInvalidParts() {
|
|
||||||
return this._libraryItem.hasInvalidParts
|
|
||||||
},
|
},
|
||||||
errorText() {
|
errorText() {
|
||||||
if (this.isMissing) return 'Item directory is missing!'
|
if (this.isMissing) return 'Item directory is missing!'
|
||||||
else if (this.isInvalid) return 'Item has no audio tracks & ebook'
|
else if (this.isInvalid) {
|
||||||
var txt = ''
|
if (this.isPodcast) return 'Podcast has no episodes'
|
||||||
if (this.hasMissingParts) {
|
return 'Item has no audio tracks & ebook'
|
||||||
txt = `${this.hasMissingParts} missing parts.`
|
|
||||||
}
|
}
|
||||||
if (this.hasInvalidParts) {
|
var txt = ''
|
||||||
if (this.hasMissingParts) txt += ' '
|
if (this.numMissingParts) {
|
||||||
txt += `${this.hasInvalidParts} invalid parts.`
|
txt = `${this.numMissingParts} missing parts.`
|
||||||
}
|
}
|
||||||
return txt || 'Unknown Error'
|
return txt || 'Unknown Error'
|
||||||
},
|
},
|
||||||
@@ -406,6 +425,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
editClick() {
|
editClick() {
|
||||||
|
if (this.recentEpisode) {
|
||||||
|
return this.$emit('edit', { libraryItem: this.libraryItem, episode: this.recentEpisode })
|
||||||
|
}
|
||||||
this.$emit('edit', this.libraryItem)
|
this.$emit('edit', this.libraryItem)
|
||||||
},
|
},
|
||||||
toggleFinished() {
|
toggleFinished() {
|
||||||
@@ -529,7 +551,8 @@ export default {
|
|||||||
play() {
|
play() {
|
||||||
var eventBus = this.$eventBus || this.$nuxt.$eventBus
|
var eventBus = this.$eventBus || this.$nuxt.$eventBus
|
||||||
eventBus.$emit('play-item', {
|
eventBus.$emit('play-item', {
|
||||||
libraryItemId: this.libraryItemId
|
libraryItemId: this.libraryItemId,
|
||||||
|
episodeId: this.recentEpisode ? this.recentEpisode.id : null
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
mouseover() {
|
mouseover() {
|
||||||
|
|||||||
@@ -132,6 +132,11 @@ export default {
|
|||||||
text: 'Tag',
|
text: 'Tag',
|
||||||
value: 'tags',
|
value: 'tags',
|
||||||
sublist: true
|
sublist: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Issues',
|
||||||
|
value: 'issues',
|
||||||
|
sublist: false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -166,26 +171,26 @@ export default {
|
|||||||
selectedText() {
|
selectedText() {
|
||||||
if (!this.selected) return ''
|
if (!this.selected) return ''
|
||||||
var parts = this.selected.split('.')
|
var parts = this.selected.split('.')
|
||||||
var filterName = this.selectItems.find((i) => i.value === parts[0]);
|
var filterName = this.selectItems.find((i) => i.value === parts[0])
|
||||||
var filterValue = null;
|
var filterValue = null
|
||||||
if (parts.length > 1) {
|
if (parts.length > 1) {
|
||||||
var decoded = this.$decode(parts[1])
|
var decoded = this.$decode(parts[1])
|
||||||
if (decoded.startsWith('aut_')) {
|
if (decoded.startsWith('aut_')) {
|
||||||
var author = this.authors.find((au) => au.id == decoded)
|
var author = this.authors.find((au) => au.id == decoded)
|
||||||
if (author) filterValue = author.name;
|
if (author) filterValue = author.name
|
||||||
} else if (decoded.startsWith('ser_')) {
|
} else if (decoded.startsWith('ser_')) {
|
||||||
var series = this.series.find((se) => se.id == decoded)
|
var series = this.series.find((se) => se.id == decoded)
|
||||||
if (series) filterValue = series.name
|
if (series) filterValue = series.name
|
||||||
} else {
|
} else {
|
||||||
filterValue = decoded;
|
filterValue = decoded
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (filterName && filterValue) {
|
if (filterName && filterValue) {
|
||||||
return `${filterName.text}: ${filterValue}`;
|
return `${filterName.text}: ${filterValue}`
|
||||||
} else if (filterName) {
|
} else if (filterName) {
|
||||||
return filterName.text;
|
return filterName.text
|
||||||
} else if (filterValue) {
|
} else if (filterValue) {
|
||||||
return filterValue;
|
return filterValue
|
||||||
} else {
|
} else {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
@@ -212,7 +217,7 @@ export default {
|
|||||||
return ['Finished', 'In Progress', 'Not Started']
|
return ['Finished', 'In Progress', 'Not Started']
|
||||||
},
|
},
|
||||||
missing() {
|
missing() {
|
||||||
return ['ASIN', 'ISBN', 'Subtitle', 'Author', 'Publish Year', 'Series', 'Volume Number', 'Description', 'Genres', 'Tags', 'Narrator', 'Publisher', 'Language', ]
|
return ['ASIN', 'ISBN', 'Subtitle', 'Author', 'Publish Year', 'Series', 'Volume Number', 'Description', 'Genres', 'Tags', 'Narrator', 'Publisher', 'Language']
|
||||||
},
|
},
|
||||||
sublistItems() {
|
sublistItems() {
|
||||||
return (this[this.sublist] || []).map((item) => {
|
return (this[this.sublist] || []).map((item) => {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<p v-if="bookResults.length" class="uppercase text-xs text-gray-400 my-1 px-1 font-semibold">Books</p>
|
<p v-if="bookResults.length" class="uppercase text-xs text-gray-400 my-1 px-1 font-semibold">Books</p>
|
||||||
<template v-for="item in bookResults">
|
<template v-for="item in bookResults">
|
||||||
<li :key="item.libraryItem.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option">
|
<li :key="item.libraryItem.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option">
|
||||||
<nuxt-link :to="`/item/${item.id}`">
|
<nuxt-link :to="`/item/${item.libraryItem.id}`">
|
||||||
<cards-item-search-card :library-item="item.libraryItem" :match-key="item.matchKey" :match-text="item.matchText" :search="lastSearch" />
|
<cards-item-search-card :library-item="item.libraryItem" :match-key="item.matchKey" :match-text="item.matchText" :search="lastSearch" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</li>
|
</li>
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
<p v-if="podcastResults.length" class="uppercase text-xs text-gray-400 my-1 px-1 font-semibold">Podcasts</p>
|
<p v-if="podcastResults.length" class="uppercase text-xs text-gray-400 my-1 px-1 font-semibold">Podcasts</p>
|
||||||
<template v-for="item in podcastResults">
|
<template v-for="item in podcastResults">
|
||||||
<li :key="item.libraryItem.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option">
|
<li :key="item.libraryItem.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option">
|
||||||
<nuxt-link :to="`/item/${item.id}`">
|
<nuxt-link :to="`/item/${item.libraryItem.id}`">
|
||||||
<cards-item-search-card :library-item="item.libraryItem" :match-key="item.matchKey" :match-text="item.matchText" :search="lastSearch" />
|
<cards-item-search-card :library-item="item.libraryItem" :match-key="item.matchKey" :match-text="item.matchText" :search="lastSearch" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -181,15 +181,18 @@ export default {
|
|||||||
if (this.currentBookshelfIndex - 1 < 0) return
|
if (this.currentBookshelfIndex - 1 < 0) return
|
||||||
var prevBookId = this.bookshelfBookIds[this.currentBookshelfIndex - 1]
|
var prevBookId = this.bookshelfBookIds[this.currentBookshelfIndex - 1]
|
||||||
this.processing = true
|
this.processing = true
|
||||||
var prevBook = await this.$axios.$get(`/api/items/${prevBookId}`).catch((error) => {
|
var prevBook = await this.$axios.$get(`/api/items/${prevBookId}?expanded=1`).catch((error) => {
|
||||||
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed to fetch book'
|
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed to fetch book'
|
||||||
this.$toast.error(errorMsg)
|
this.$toast.error(errorMsg)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
this.processing = false
|
this.processing = false
|
||||||
if (prevBook) {
|
if (prevBook) {
|
||||||
this.$store.commit('showEditModalOnTab', { libraryItem: prevBook, tab: this.selectedTab })
|
this.unregisterListeners()
|
||||||
this.$nextTick(this.init)
|
this.libraryItem = prevBook
|
||||||
|
this.selectedTab = 'details'
|
||||||
|
this.$store.commit('setSelectedLibraryItem', prevBook)
|
||||||
|
this.$nextTick(this.registerListeners)
|
||||||
} else {
|
} else {
|
||||||
console.error('Book not found', prevBookId)
|
console.error('Book not found', prevBookId)
|
||||||
}
|
}
|
||||||
@@ -198,15 +201,18 @@ export default {
|
|||||||
if (this.currentBookshelfIndex >= this.bookshelfBookIds.length - 1) return
|
if (this.currentBookshelfIndex >= this.bookshelfBookIds.length - 1) return
|
||||||
this.processing = true
|
this.processing = true
|
||||||
var nextBookId = this.bookshelfBookIds[this.currentBookshelfIndex + 1]
|
var nextBookId = this.bookshelfBookIds[this.currentBookshelfIndex + 1]
|
||||||
var nextBook = await this.$axios.$get(`/api/items/${nextBookId}`).catch((error) => {
|
var nextBook = await this.$axios.$get(`/api/items/${nextBookId}?expanded=1`).catch((error) => {
|
||||||
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed to fetch book'
|
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed to fetch book'
|
||||||
this.$toast.error(errorMsg)
|
this.$toast.error(errorMsg)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
this.processing = false
|
this.processing = false
|
||||||
if (nextBook) {
|
if (nextBook) {
|
||||||
this.$store.commit('showEditModalOnTab', { libraryItem: nextBook, tab: this.selectedTab })
|
this.unregisterListeners()
|
||||||
this.$nextTick(this.init)
|
this.libraryItem = nextBook
|
||||||
|
this.selectedTab = 'details'
|
||||||
|
this.$store.commit('setSelectedLibraryItem', nextBook)
|
||||||
|
this.$nextTick(this.registerListeners)
|
||||||
} else {
|
} else {
|
||||||
console.error('Book not found', nextBookId)
|
console.error('Book not found', nextBookId)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,17 +35,6 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
|
||||||
value: Boolean,
|
|
||||||
libraryItem: {
|
|
||||||
type: Object,
|
|
||||||
default: () => {}
|
|
||||||
},
|
|
||||||
episode: {
|
|
||||||
type: Object,
|
|
||||||
default: () => {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
processing: false,
|
processing: false,
|
||||||
@@ -72,12 +61,18 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
show: {
|
show: {
|
||||||
get() {
|
get() {
|
||||||
return this.value
|
return this.$store.state.globals.showEditPodcastEpisode
|
||||||
},
|
},
|
||||||
set(val) {
|
set(val) {
|
||||||
this.$emit('input', val)
|
this.$store.commit('globals/setShowEditPodcastEpisodeModal', val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
libraryItem() {
|
||||||
|
return this.$store.state.selectedLibraryItem
|
||||||
|
},
|
||||||
|
episode() {
|
||||||
|
return this.$store.state.globals.selectedEpisode
|
||||||
|
},
|
||||||
episodeId() {
|
episodeId() {
|
||||||
return this.episode ? this.episode.id : null
|
return this.episode ? this.episode.id : null
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -124,7 +124,13 @@ export default {
|
|||||||
episodesToDownload = this.episodesSelected.map((episodeIndex) => this.episodes[Number(episodeIndex)])
|
episodesToDownload = this.episodesSelected.map((episodeIndex) => this.episodes[Number(episodeIndex)])
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Podcast payload', episodesToDownload)
|
var payloadSize = JSON.stringify(episodesToDownload).length
|
||||||
|
var sizeInMb = payloadSize / 1024 / 1024
|
||||||
|
var sizeInMbPretty = sizeInMb.toFixed(2) + 'MB'
|
||||||
|
console.log('Request size', sizeInMb)
|
||||||
|
if (sizeInMb > 4.99) {
|
||||||
|
return this.$toast.error(`Request is too large (${sizeInMbPretty}) should be < 5Mb`)
|
||||||
|
}
|
||||||
|
|
||||||
this.processing = true
|
this.processing = true
|
||||||
this.$axios
|
this.$axios
|
||||||
@@ -144,7 +150,8 @@ export default {
|
|||||||
init() {
|
init() {
|
||||||
for (let i = 0; i < this.episodes.length; i++) {
|
for (let i = 0; i < this.episodes.length; i++) {
|
||||||
var episode = this.episodes[i]
|
var episode = this.episodes[i]
|
||||||
if (episode.enclosure && !this.itemEpisodeMap[episode.enclosure.url]) { // Do not include episodes already downloaded
|
if (episode.enclosure && !this.itemEpisodeMap[episode.enclosure.url]) {
|
||||||
|
// Do not include episodes already downloaded
|
||||||
this.$set(this.selectedEpisodes, String(i), false)
|
this.$set(this.selectedEpisodes, String(i), false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,63 +1,42 @@
|
|||||||
<template>
|
<template>
|
||||||
<modals-modal v-model="show" name="new-podcast-modal" :width="1200" :height="'unset'" :processing="processing">
|
<modals-modal v-model="show" name="new-podcast-modal" :width="1000" :height="'unset'" :processing="processing">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div ref="wrapper" id="podcast-wrapper" class="p-4 w-full text-sm py-2 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
|
<div ref="wrapper" id="podcast-wrapper" class="p-4 w-full text-sm py-2 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
|
||||||
<div class="flex flex-wrap">
|
<div class="w-full p-4">
|
||||||
<div class="w-full md:w-1/2 p-4">
|
<p class="text-lg font-semibold mb-2">Details</p>
|
||||||
<p class="text-lg font-semibold mb-2">Details</p>
|
|
||||||
<div class="flex flex-wrap">
|
<div v-if="podcast.imageUrl" class="p-1 w-full">
|
||||||
<div v-if="podcast.imageUrl" class="p-1 w-full">
|
<img :src="podcast.imageUrl" class="h-16 w-16 object-contain" />
|
||||||
<img :src="podcast.imageUrl" class="h-16 w-16 object-contain" />
|
</div>
|
||||||
</div>
|
<div class="flex">
|
||||||
<div class="p-1 w-full">
|
<div class="w-full md:w-1/2 p-2">
|
||||||
<ui-text-input-with-label v-model="podcast.title" label="Title" @input="titleUpdated" />
|
<ui-text-input-with-label v-model="podcast.title" label="Title" @input="titleUpdated" />
|
||||||
</div>
|
</div>
|
||||||
<div class="p-1 w-full">
|
<div class="w-full md:w-1/2 p-2">
|
||||||
<ui-text-input-with-label v-model="podcast.author" label="Author" />
|
<ui-text-input-with-label v-model="podcast.author" label="Author" />
|
||||||
</div>
|
|
||||||
<div class="p-1 w-full">
|
|
||||||
<ui-text-input-with-label v-model="podcast.feedUrl" label="Feed URL" readonly />
|
|
||||||
</div>
|
|
||||||
<div class="p-1 w-full">
|
|
||||||
<ui-multi-select v-model="podcast.genres" :items="podcast.genres" label="Genres" />
|
|
||||||
</div>
|
|
||||||
<div class="p-1 w-full">
|
|
||||||
<ui-textarea-with-label v-model="podcast.description" label="Description" />
|
|
||||||
</div>
|
|
||||||
<div class="p-1 w-full">
|
|
||||||
<ui-dropdown v-model="selectedFolderId" :items="folderItems" :disabled="processing" label="Folder" @input="folderUpdated" />
|
|
||||||
</div>
|
|
||||||
<div class="p-1 w-full">
|
|
||||||
<ui-text-input-with-label v-model="fullPath" label="Podcast Path" readonly />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full md:w-1/2 p-4">
|
<div class="flex">
|
||||||
<p class="text-lg font-semibold mb-2">Episodes</p>
|
<div class="w-full md:w-1/2 p-2">
|
||||||
<div ref="episodeContainer" id="episodes-scroll" class="w-full overflow-x-hidden overflow-y-auto">
|
<ui-text-input-with-label v-model="podcast.feedUrl" label="Feed URL" readonly />
|
||||||
<div class="relative">
|
</div>
|
||||||
<div class="absolute top-0 left-0 h-full flex items-center p-2">
|
<div class="w-full md:w-1/2 p-2">
|
||||||
<ui-checkbox v-model="selectAll" small checkbox-bg="primary" border-color="gray-600" />
|
<ui-multi-select v-model="podcast.genres" :items="podcast.genres" label="Genres" />
|
||||||
</div>
|
</div>
|
||||||
<div class="px-8 py-2">
|
</div>
|
||||||
<p class="font-semibold text-gray-200">Select all episodes</p>
|
<div class="p-2 w-full">
|
||||||
</div>
|
<ui-textarea-with-label v-model="podcast.description" label="Description" :rows="3" />
|
||||||
</div>
|
</div>
|
||||||
<div v-for="(episode, index) in episodes" :key="index" class="relative cursor-pointer" :class="selectedEpisodes[String(index)] ? 'bg-success bg-opacity-10' : index % 2 == 0 ? 'bg-primary bg-opacity-25 hover:bg-opacity-40' : 'bg-primary bg-opacity-5 hover:bg-opacity-25'" @click="toggleSelectEpisode(index)">
|
<div class="flex">
|
||||||
<div class="absolute top-0 left-0 h-full flex items-center p-2">
|
<div class="w-full md:w-1/2 p-2">
|
||||||
<ui-checkbox v-model="selectedEpisodes[String(index)]" small checkbox-bg="primary" border-color="gray-600" />
|
<ui-dropdown v-model="selectedFolderId" :items="folderItems" :disabled="processing" label="Folder" @input="folderUpdated" />
|
||||||
</div>
|
</div>
|
||||||
<div class="px-8 py-2">
|
<div class="w-full md:w-1/2 p-2">
|
||||||
<p v-if="episode.episode" class="font-semibold text-gray-200">#{{ episode.episode }}</p>
|
<ui-text-input-with-label v-model="fullPath" label="Podcast Path" readonly />
|
||||||
<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 class="text-xs text-gray-300">Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -66,7 +45,7 @@
|
|||||||
<div class="px-4">
|
<div class="px-4">
|
||||||
<ui-checkbox v-model="podcast.autoDownloadEpisodes" label="Auto Download Episodes" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
|
<ui-checkbox v-model="podcast.autoDownloadEpisodes" label="Auto Download Episodes" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
|
||||||
</div>
|
</div>
|
||||||
<ui-btn color="success" :disabled="disableSubmit" @click="submit">{{ buttonText }}</ui-btn>
|
<ui-btn color="success" @click="submit">Add Podcast</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</modals-modal>
|
</modals-modal>
|
||||||
@@ -104,8 +83,7 @@ export default {
|
|||||||
itunesId: '',
|
itunesId: '',
|
||||||
itunesArtistId: '',
|
itunesArtistId: '',
|
||||||
autoDownloadEpisodes: false
|
autoDownloadEpisodes: false
|
||||||
},
|
}
|
||||||
selectedEpisodes: {}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -127,16 +105,6 @@ export default {
|
|||||||
this.$emit('input', val)
|
this.$emit('input', val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
selectAll: {
|
|
||||||
get() {
|
|
||||||
return this.episodesSelected.length == this.episodes.length
|
|
||||||
},
|
|
||||||
set(val) {
|
|
||||||
for (const key in this.selectedEpisodes) {
|
|
||||||
this.selectedEpisodes[key] = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
title() {
|
title() {
|
||||||
return this._podcastData.title
|
return this._podcastData.title
|
||||||
},
|
},
|
||||||
@@ -166,17 +134,6 @@ export default {
|
|||||||
if (!this.podcastFeedData) return []
|
if (!this.podcastFeedData) return []
|
||||||
return this.podcastFeedData.episodes || []
|
return this.podcastFeedData.episodes || []
|
||||||
},
|
},
|
||||||
episodesSelected() {
|
|
||||||
return Object.keys(this.selectedEpisodes).filter((key) => !!this.selectedEpisodes[key])
|
|
||||||
},
|
|
||||||
disableSubmit() {
|
|
||||||
return !this.episodesSelected.length && !this.podcast.autoDownloadEpisodes
|
|
||||||
},
|
|
||||||
buttonText() {
|
|
||||||
if (!this.episodesSelected.length) return 'Add Podcast'
|
|
||||||
if (this.episodesSelected.length == 1) return 'Add Podcast & Download 1 Episode'
|
|
||||||
return `Add Podcast & Download ${this.episodesSelected.length} Episodes`
|
|
||||||
},
|
|
||||||
selectedFolder() {
|
selectedFolder() {
|
||||||
return this.folders.find((f) => f.id === this.selectedFolderId)
|
return this.folders.find((f) => f.id === this.selectedFolderId)
|
||||||
},
|
},
|
||||||
@@ -196,15 +153,7 @@ export default {
|
|||||||
}
|
}
|
||||||
this.fullPath = Path.join(this.selectedFolderPath, this.podcast.title)
|
this.fullPath = Path.join(this.selectedFolderPath, this.podcast.title)
|
||||||
},
|
},
|
||||||
toggleSelectEpisode(index) {
|
|
||||||
this.selectedEpisodes[String(index)] = !this.selectedEpisodes[String(index)]
|
|
||||||
},
|
|
||||||
submit() {
|
submit() {
|
||||||
var episodesToDownload = []
|
|
||||||
if (this.episodesSelected.length) {
|
|
||||||
episodesToDownload = this.episodesSelected.map((episodeIndex) => this.episodes[Number(episodeIndex)])
|
|
||||||
}
|
|
||||||
|
|
||||||
const podcastPayload = {
|
const podcastPayload = {
|
||||||
path: this.fullPath,
|
path: this.fullPath,
|
||||||
folderId: this.selectedFolderId,
|
folderId: this.selectedFolderId,
|
||||||
@@ -224,8 +173,7 @@ export default {
|
|||||||
language: this.podcast.language
|
language: this.podcast.language
|
||||||
},
|
},
|
||||||
autoDownloadEpisodes: this.podcast.autoDownloadEpisodes
|
autoDownloadEpisodes: this.podcast.autoDownloadEpisodes
|
||||||
},
|
}
|
||||||
episodesToDownload
|
|
||||||
}
|
}
|
||||||
console.log('Podcast payload', podcastPayload)
|
console.log('Podcast payload', podcastPayload)
|
||||||
|
|
||||||
@@ -260,10 +208,6 @@ export default {
|
|||||||
this.podcast.language = this._podcastData.language || ''
|
this.podcast.language = this._podcastData.language || ''
|
||||||
this.podcast.autoDownloadEpisodes = false
|
this.podcast.autoDownloadEpisodes = false
|
||||||
|
|
||||||
for (let i = 0; i < this.episodes.length; i++) {
|
|
||||||
this.$set(this.selectedEpisodes, String(i), false)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.folderItems[0]) {
|
if (this.folderItems[0]) {
|
||||||
this.selectedFolderId = this.folderItems[0].value
|
this.selectedFolderId = this.folderItems[0].value
|
||||||
this.folderUpdated()
|
this.folderUpdated()
|
||||||
|
|||||||
@@ -16,8 +16,6 @@
|
|||||||
</template>
|
</template>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
</draggable>
|
</draggable>
|
||||||
|
|
||||||
<modals-podcast-edit-episode v-model="showEditEpisodeModal" :library-item="libraryItem" :episode="selectedEpisode" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -40,8 +38,6 @@ export default {
|
|||||||
sortDesc: true,
|
sortDesc: true,
|
||||||
drag: false,
|
drag: false,
|
||||||
episodesCopy: [],
|
episodesCopy: [],
|
||||||
selectedEpisode: null,
|
|
||||||
showEditEpisodeModal: false,
|
|
||||||
orderChanged: false,
|
orderChanged: false,
|
||||||
savingOrder: false
|
savingOrder: false
|
||||||
}
|
}
|
||||||
@@ -97,8 +93,9 @@ export default {
|
|||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
editEpisode(episode) {
|
editEpisode(episode) {
|
||||||
this.selectedEpisode = episode
|
this.$store.commit('setSelectedLibraryItem', this.libraryItem)
|
||||||
this.showEditEpisodeModal = true
|
this.$store.commit('globals/setSelectedEpisode', episode)
|
||||||
|
this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)
|
||||||
},
|
},
|
||||||
draggableUpdate() {
|
draggableUpdate() {
|
||||||
this.orderChanged = this.checkHasOrderChanged()
|
this.orderChanged = this.checkHasOrderChanged()
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
<modals-user-collections-modal />
|
<modals-user-collections-modal />
|
||||||
<modals-edit-collection-modal />
|
<modals-edit-collection-modal />
|
||||||
<modals-bookshelf-texture-modal />
|
<modals-bookshelf-texture-modal />
|
||||||
|
<modals-podcast-edit-episode />
|
||||||
<readers-reader />
|
<readers-reader />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.0.1",
|
"version": "2.0.2",
|
||||||
"description": "Audiobook manager and player",
|
"description": "Audiobook manager and player",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -20,6 +20,14 @@
|
|||||||
<p class="pl-4 text-lg">Number of backups to keep</p>
|
<p class="pl-4 text-lg">Number of backups to keep</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center py-2">
|
||||||
|
<ui-text-input type="number" v-model="maxBackupSize" no-spinner :disabled="updatingServerSettings" :padding-x="1" text-center class="w-10" @change="updateBackupsSettings" />
|
||||||
|
|
||||||
|
<ui-tooltip :text="maxBackupSizeTooltip">
|
||||||
|
<p class="pl-4 text-lg">Maximum backup size (in GB) <span class="material-icons icon-text">info_outlined</span></p>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
<tables-backups-table />
|
<tables-backups-table />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -32,6 +40,7 @@ export default {
|
|||||||
updatingServerSettings: false,
|
updatingServerSettings: false,
|
||||||
dailyBackups: true,
|
dailyBackups: true,
|
||||||
backupsToKeep: 2,
|
backupsToKeep: 2,
|
||||||
|
maxBackupSize: 1,
|
||||||
newServerSettings: {}
|
newServerSettings: {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -47,19 +56,27 @@ export default {
|
|||||||
dailyBackupsTooltip() {
|
dailyBackupsTooltip() {
|
||||||
return 'Runs at 1am every day (your server time). Saved in /metadata/backups.'
|
return 'Runs at 1am every day (your server time). Saved in /metadata/backups.'
|
||||||
},
|
},
|
||||||
|
maxBackupSizeTooltip() {
|
||||||
|
return 'As a safeguard against misconfiguration, backups will fail if they exceed the configured size.'
|
||||||
|
},
|
||||||
serverSettings() {
|
serverSettings() {
|
||||||
return this.$store.state.serverSettings
|
return this.$store.state.serverSettings
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateBackupsSettings() {
|
updateBackupsSettings() {
|
||||||
|
if (isNaN(this.maxBackupSize) || this.maxBackupSize <= 0) {
|
||||||
|
this.$toast.error('Invalid maximum backup size')
|
||||||
|
return
|
||||||
|
}
|
||||||
if (isNaN(this.backupsToKeep) || this.backupsToKeep <= 0 || this.backupsToKeep > 99) {
|
if (isNaN(this.backupsToKeep) || this.backupsToKeep <= 0 || this.backupsToKeep > 99) {
|
||||||
this.$toast.error('Invalid number of backups to keep')
|
this.$toast.error('Invalid number of backups to keep')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var updatePayload = {
|
var updatePayload = {
|
||||||
backupSchedule: this.dailyBackups ? '0 1 * * *' : false,
|
backupSchedule: this.dailyBackups ? '0 1 * * *' : false,
|
||||||
backupsToKeep: Number(this.backupsToKeep)
|
backupsToKeep: Number(this.backupsToKeep),
|
||||||
|
maxBackupSize: Number(this.maxBackupSize)
|
||||||
}
|
}
|
||||||
this.updateServerSettings(updatePayload)
|
this.updateServerSettings(updatePayload)
|
||||||
},
|
},
|
||||||
@@ -81,6 +98,7 @@ export default {
|
|||||||
|
|
||||||
this.backupsToKeep = this.newServerSettings.backupsToKeep || 2
|
this.backupsToKeep = this.newServerSettings.backupsToKeep || 2
|
||||||
this.dailyBackups = !!this.newServerSettings.backupSchedule
|
this.dailyBackups = !!this.newServerSettings.backupSchedule
|
||||||
|
this.maxBackupSize = this.newServerSettings.maxBackupSize || 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|||||||
@@ -95,6 +95,21 @@
|
|||||||
<p class="ml-4">Book has no audio tracks but has valid ebook files. The e-reader is experimental and can be turned on in config.</p>
|
<p class="ml-4">Book has no audio tracks but has valid ebook files. The e-reader is experimental and can be turned on in config.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="episodeDownloadsQueued.length" class="px-4 py-2 mt-4 bg-info bg-opacity-40 text-sm font-semibold rounded-md text-gray-100 relative max-w-max mx-auto md:mx-0">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<p class="text-sm py-1">{{ episodeDownloadsQueued.length }} Episode{{ episodeDownloadsQueued.length === 1 ? '' : 's' }} queued for download</p>
|
||||||
|
|
||||||
|
<span class="material-icons hover:text-error text-xl ml-3 cursor-pointer" @click="clearDownloadQueue">close</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="episodesDownloading.length" class="px-4 py-2 mt-4 bg-success bg-opacity-20 text-sm font-semibold rounded-md text-gray-100 relative max-w-max mx-auto md:mx-0">
|
||||||
|
<div v-for="episode in episodesDownloading" :key="episode.id" class="flex items-center">
|
||||||
|
<widgets-loading-spinner />
|
||||||
|
<p class="text-sm py-1 pl-4">Downloading episode "{{ episode.episodeDisplayTitle }}"</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Progress -->
|
<!-- Progress -->
|
||||||
<div v-if="!isPodcast && progressPercent > 0" class="px-4 py-2 mt-4 bg-primary text-sm font-semibold rounded-md text-gray-100 relative max-w-max mx-auto md:mx-0" :class="resettingProgress ? 'opacity-25' : ''">
|
<div v-if="!isPodcast && progressPercent > 0" class="px-4 py-2 mt-4 bg-primary text-sm font-semibold rounded-md text-gray-100 relative max-w-max mx-auto md:mx-0" :class="resettingProgress ? 'opacity-25' : ''">
|
||||||
<p v-if="progressPercent < 1" class="leading-6">Your Progress: {{ Math.round(progressPercent * 100) }}%</p>
|
<p v-if="progressPercent < 1" class="leading-6">Your Progress: {{ Math.round(progressPercent * 100) }}%</p>
|
||||||
@@ -163,7 +178,9 @@ export default {
|
|||||||
if (!store.state.user.user) {
|
if (!store.state.user.user) {
|
||||||
return redirect(`/login?redirect=${route.path}`)
|
return redirect(`/login?redirect=${route.path}`)
|
||||||
}
|
}
|
||||||
var item = await app.$axios.$get(`/api/items/${params.id}?expanded=1&include=authors`).catch((error) => {
|
|
||||||
|
// Include episode downloads for podcasts
|
||||||
|
var item = await app.$axios.$get(`/api/items/${params.id}?expanded=1&include=authors,downloads`).catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
@@ -181,7 +198,9 @@ export default {
|
|||||||
isProcessingReadUpdate: false,
|
isProcessingReadUpdate: false,
|
||||||
fetchingRSSFeed: false,
|
fetchingRSSFeed: false,
|
||||||
showPodcastEpisodeFeed: false,
|
showPodcastEpisodeFeed: false,
|
||||||
podcastFeedEpisodes: []
|
podcastFeedEpisodes: [],
|
||||||
|
episodesDownloading: [],
|
||||||
|
episodeDownloadsQueued: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -333,6 +352,20 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
clearDownloadQueue() {
|
||||||
|
if (confirm('Are you sure you want to clear episode download queue?')) {
|
||||||
|
this.$axios
|
||||||
|
.$get(`/api/podcasts/${this.libraryItemId}/clear-queue`)
|
||||||
|
.then(() => {
|
||||||
|
this.$toast.success('Episode download queue cleared')
|
||||||
|
this.episodeDownloadQueued = []
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to clear queue', error)
|
||||||
|
this.$toast.error('Failed to clear queue')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
async findEpisodesClick() {
|
async findEpisodesClick() {
|
||||||
if (!this.mediaMetadata.feedUrl) {
|
if (!this.mediaMetadata.feedUrl) {
|
||||||
return this.$toast.error('Podcast does not have an RSS Feed')
|
return this.$toast.error('Podcast does not have an RSS Feed')
|
||||||
@@ -425,17 +458,44 @@ export default {
|
|||||||
collectionsClick() {
|
collectionsClick() {
|
||||||
this.$store.commit('setSelectedLibraryItem', this.libraryItem)
|
this.$store.commit('setSelectedLibraryItem', this.libraryItem)
|
||||||
this.$store.commit('globals/setShowUserCollectionsModal', true)
|
this.$store.commit('globals/setShowUserCollectionsModal', true)
|
||||||
|
},
|
||||||
|
episodeDownloadQueued(episodeDownload) {
|
||||||
|
if (episodeDownload.libraryItemId === this.libraryItemId) {
|
||||||
|
this.episodeDownloadsQueued.push(episodeDownload)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
episodeDownloadStarted(episodeDownload) {
|
||||||
|
if (episodeDownload.libraryItemId === this.libraryItemId) {
|
||||||
|
this.episodeDownloadsQueued = this.episodeDownloadsQueued.filter((d) => d.id !== episodeDownload.id)
|
||||||
|
this.episodesDownloading.push(episodeDownload)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
episodeDownloadFinished(episodeDownload) {
|
||||||
|
if (episodeDownload.libraryItemId === this.libraryItemId) {
|
||||||
|
this.episodeDownloadsQueued = this.episodeDownloadsQueued.filter((d) => d.id !== episodeDownload.id)
|
||||||
|
this.episodesDownloading = this.episodesDownloading.filter((d) => d.id !== episodeDownload.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
if (this.libraryItem.episodesDownloading) {
|
||||||
|
this.episodeDownloadsQueued = this.libraryItem.episodesDownloading || []
|
||||||
|
}
|
||||||
|
|
||||||
// use this items library id as the current
|
// use this items library id as the current
|
||||||
if (this.libraryId) {
|
if (this.libraryId) {
|
||||||
this.$store.commit('libraries/setCurrentLibrary', this.libraryId)
|
this.$store.commit('libraries/setCurrentLibrary', this.libraryId)
|
||||||
}
|
}
|
||||||
this.$root.socket.on('item_updated', this.libraryItemUpdated)
|
this.$root.socket.on('item_updated', this.libraryItemUpdated)
|
||||||
|
this.$root.socket.on('episode_download_queued', this.episodeDownloadQueued)
|
||||||
|
this.$root.socket.on('episode_download_started', this.episodeDownloadStarted)
|
||||||
|
this.$root.socket.on('episode_download_finished', this.episodeDownloadFinished)
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.$root.socket.off('item_updated', this.libraryItemUpdated)
|
this.$root.socket.off('item_updated', this.libraryItemUpdated)
|
||||||
|
this.$root.socket.off('episode_download_queued', this.episodeDownloadQueued)
|
||||||
|
this.$root.socket.off('episode_download_started', this.episodeDownloadStarted)
|
||||||
|
this.$root.socket.off('episode_download_finished', this.episodeDownloadFinished)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export default class PlayerHandler {
|
|||||||
this.displayTitle = null
|
this.displayTitle = null
|
||||||
this.displayAuthor = null
|
this.displayAuthor = null
|
||||||
this.playWhenReady = false
|
this.playWhenReady = false
|
||||||
|
this.initialPlaybackRate = 1
|
||||||
this.player = null
|
this.player = null
|
||||||
this.playerState = 'IDLE'
|
this.playerState = 'IDLE'
|
||||||
this.isHlsTranscode = false
|
this.isHlsTranscode = false
|
||||||
@@ -46,12 +47,13 @@ export default class PlayerHandler {
|
|||||||
return this.libraryItem.media.episodes.find(ep => ep.id === this.episodeId)
|
return this.libraryItem.media.episodes.find(ep => ep.id === this.episodeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
load(libraryItem, episodeId, playWhenReady) {
|
load(libraryItem, episodeId, playWhenReady, playbackRate) {
|
||||||
if (!this.player) this.switchPlayer()
|
if (!this.player) this.switchPlayer()
|
||||||
|
|
||||||
this.libraryItem = libraryItem
|
this.libraryItem = libraryItem
|
||||||
this.episodeId = episodeId
|
this.episodeId = episodeId
|
||||||
this.playWhenReady = playWhenReady
|
this.playWhenReady = playWhenReady
|
||||||
|
this.initialPlaybackRate = playbackRate
|
||||||
this.prepare()
|
this.prepare()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +115,7 @@ export default class PlayerHandler {
|
|||||||
console.log('[PlayerHandler] Player state change', state)
|
console.log('[PlayerHandler] Player state change', state)
|
||||||
this.playerState = state
|
this.playerState = state
|
||||||
if (this.playerState === 'PLAYING') {
|
if (this.playerState === 'PLAYING') {
|
||||||
|
this.setPlaybackRate(this.initialPlaybackRate)
|
||||||
this.startPlayInterval()
|
this.startPlayInterval()
|
||||||
} else {
|
} else {
|
||||||
this.stopPlayInterval()
|
this.stopPlayInterval()
|
||||||
@@ -151,11 +154,12 @@ export default class PlayerHandler {
|
|||||||
this.prepareSession(session)
|
this.prepareSession(session)
|
||||||
}
|
}
|
||||||
|
|
||||||
prepareOpenSession(session) { // Session opened on init socket
|
prepareOpenSession(session, playbackRate) { // Session opened on init socket
|
||||||
if (!this.player) this.switchPlayer()
|
if (!this.player) this.switchPlayer()
|
||||||
|
|
||||||
this.libraryItem = session.libraryItem
|
this.libraryItem = session.libraryItem
|
||||||
this.playWhenReady = false
|
this.playWhenReady = false
|
||||||
|
this.initialPlaybackRate = playbackRate
|
||||||
this.prepareSession(session)
|
this.prepareSession(session)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,6 +296,7 @@ export default class PlayerHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setPlaybackRate(playbackRate) {
|
setPlaybackRate(playbackRate) {
|
||||||
|
this.initialPlaybackRate = playbackRate // Might be loaded from settings before player is started
|
||||||
if (!this.player) return
|
if (!this.player) return
|
||||||
this.player.setPlaybackRate(playbackRate)
|
this.player.setPlaybackRate(playbackRate)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ export const state = () => ({
|
|||||||
showBatchUserCollectionModal: false,
|
showBatchUserCollectionModal: false,
|
||||||
showUserCollectionsModal: false,
|
showUserCollectionsModal: false,
|
||||||
showEditCollectionModal: false,
|
showEditCollectionModal: false,
|
||||||
|
showEditPodcastEpisode: false,
|
||||||
|
selectedEpisode: null,
|
||||||
selectedCollection: null,
|
selectedCollection: null,
|
||||||
showBookshelfTextureModal: false,
|
showBookshelfTextureModal: false,
|
||||||
isCasting: false, // Actively casting
|
isCasting: false, // Actively casting
|
||||||
@@ -46,10 +48,16 @@ export const mutations = {
|
|||||||
setShowEditCollectionModal(state, val) {
|
setShowEditCollectionModal(state, val) {
|
||||||
state.showEditCollectionModal = val
|
state.showEditCollectionModal = val
|
||||||
},
|
},
|
||||||
|
setShowEditPodcastEpisodeModal(state, val) {
|
||||||
|
state.showEditPodcastEpisode = val
|
||||||
|
},
|
||||||
setEditCollection(state, collection) {
|
setEditCollection(state, collection) {
|
||||||
state.selectedCollection = collection
|
state.selectedCollection = collection
|
||||||
state.showEditCollectionModal = true
|
state.showEditCollectionModal = true
|
||||||
},
|
},
|
||||||
|
setSelectedEpisode(state, episode) {
|
||||||
|
state.selectedEpisode = episode
|
||||||
|
},
|
||||||
setShowBookshelfTextureModal(state, val) {
|
setShowBookshelfTextureModal(state, val) {
|
||||||
state.showBookshelfTextureModal = val
|
state.showBookshelfTextureModal = val
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.0.1",
|
"version": "2.0.2",
|
||||||
"description": "Self-hosted audiobook server for managing and playing audiobooks",
|
"description": "Self-hosted audiobook server for managing and playing audiobooks",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -316,6 +316,39 @@ class Db {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async bulkInsertEntities(entityName, entities, batchSize = 500) {
|
||||||
|
// Group entities in batches of size batchSize
|
||||||
|
var entityBatches = []
|
||||||
|
var batch = []
|
||||||
|
var index = 0
|
||||||
|
entities.forEach((ent) => {
|
||||||
|
batch.push(ent)
|
||||||
|
index++
|
||||||
|
if (index >= batchSize) {
|
||||||
|
entityBatches.push(batch)
|
||||||
|
index = 0
|
||||||
|
batch = []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (batch.length) entityBatches.push(batch)
|
||||||
|
|
||||||
|
Logger.info(`[Db] bulkInsertEntities: ${entities.length} ${entityName} to ${entityBatches.length} batches of max size ${batchSize}`)
|
||||||
|
|
||||||
|
// Start inserting batches
|
||||||
|
var batchIndex = 1
|
||||||
|
for (const entityBatch of entityBatches) {
|
||||||
|
Logger.info(`[Db] bulkInsertEntities: Start inserting batch ${batchIndex} of ${entityBatch.length} for ${entityName}`)
|
||||||
|
var success = await this.insertEntities(entityName, entityBatch)
|
||||||
|
if (success) {
|
||||||
|
Logger.info(`[Db] bulkInsertEntities: Success inserting batch ${batchIndex} for ${entityName}`)
|
||||||
|
} else {
|
||||||
|
Logger.info(`[Db] bulkInsertEntities: Failed inserting batch ${batchIndex} for ${entityName}`)
|
||||||
|
}
|
||||||
|
batchIndex++
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
updateEntities(entityName, entities) {
|
updateEntities(entityName, entities) {
|
||||||
var entityDb = this.getEntityDb(entityName)
|
var entityDb = this.getEntityDb(entityName)
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -153,8 +153,8 @@ class Server {
|
|||||||
|
|
||||||
app.use(this.auth.cors)
|
app.use(this.auth.cors)
|
||||||
app.use(fileUpload())
|
app.use(fileUpload())
|
||||||
app.use(express.urlencoded({ extended: true, limit: "3mb" }));
|
app.use(express.urlencoded({ extended: true, limit: "5mb" }));
|
||||||
app.use(express.json({ limit: "3mb" }))
|
app.use(express.json({ limit: "5mb" }))
|
||||||
|
|
||||||
// Static path to generated nuxt
|
// Static path to generated nuxt
|
||||||
const distPath = Path.join(global.appRoot, '/client/dist')
|
const distPath = Path.join(global.appRoot, '/client/dist')
|
||||||
|
|||||||
@@ -176,12 +176,29 @@ class LibraryController {
|
|||||||
sortKey += 'IgnorePrefix'
|
sortKey += 'IgnorePrefix'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start sort
|
||||||
var direction = payload.sortDesc ? 'desc' : 'asc'
|
var direction = payload.sortDesc ? 'desc' : 'asc'
|
||||||
libraryItems = naturalSort(libraryItems)[direction]((li) => {
|
var sortArray = [
|
||||||
|
{
|
||||||
|
[direction]: (li) => {
|
||||||
|
// Supports dot notation strings i.e. "media.metadata.title"
|
||||||
|
return sortKey.split('.').reduce((a, b) => a[b], li)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
// Supports dot notation strings i.e. "media.metadata.title"
|
// Secondary sort when sorting by book author use series sort title
|
||||||
return sortKey.split('.').reduce((a, b) => a[b], li)
|
if (payload.mediaType === 'book' && payload.sortBy.includes('author')) {
|
||||||
})
|
sortArray.push({
|
||||||
|
asc: (li) => {
|
||||||
|
if (li.media.metadata.series && li.media.metadata.series.length) {
|
||||||
|
return li.media.metadata.getSeriesSortTitle(li.media.metadata.series[0])
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
libraryItems = naturalSort(libraryItems).by(sortArray)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Potentially implement collapse series again
|
// TODO: Potentially implement collapse series again
|
||||||
@@ -283,12 +300,12 @@ class LibraryController {
|
|||||||
var limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12
|
var limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12
|
||||||
var minified = req.query.minified === '1'
|
var minified = req.query.minified === '1'
|
||||||
|
|
||||||
var itemsWithUserProgress = libraryHelpers.getItemsWithUserProgress(req.user, libraryItems)
|
var itemsWithUserProgress = libraryHelpers.getMediaProgressWithItems(req.user, libraryItems)
|
||||||
var categories = [
|
var categories = [
|
||||||
{
|
{
|
||||||
id: 'continue-listening',
|
id: 'continue-listening',
|
||||||
label: 'Continue Listening',
|
label: 'Continue Listening',
|
||||||
type: req.library.mediaType,
|
type: isPodcastLibrary ? 'episode' : req.library.mediaType,
|
||||||
entities: libraryHelpers.getItemsMostRecentlyListened(itemsWithUserProgress, limitPerShelf, minified)
|
entities: libraryHelpers.getItemsMostRecentlyListened(itemsWithUserProgress, limitPerShelf, minified)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -300,7 +317,7 @@ class LibraryController {
|
|||||||
{
|
{
|
||||||
id: 'listen-again',
|
id: 'listen-again',
|
||||||
label: 'Listen Again',
|
label: 'Listen Again',
|
||||||
type: req.library.mediaType,
|
type: isPodcastLibrary ? 'episode' : req.library.mediaType,
|
||||||
entities: libraryHelpers.getItemsMostRecentlyFinished(itemsWithUserProgress, limitPerShelf, minified)
|
entities: libraryHelpers.getItemsMostRecentlyFinished(itemsWithUserProgress, limitPerShelf, minified)
|
||||||
}
|
}
|
||||||
].filter(cats => { // Remove categories with no items
|
].filter(cats => { // Remove categories with no items
|
||||||
@@ -355,57 +372,17 @@ class LibraryController {
|
|||||||
entities: authors
|
entities: authors
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
var episodesRecentlyAdded = libraryHelpers.getEpisodesRecentlyAdded(libraryItems, limitPerShelf, minified)
|
||||||
res.json(categories)
|
if (episodesRecentlyAdded.length) {
|
||||||
}
|
categories.splice(1, 0, {
|
||||||
|
id: 'episodes-recently-added',
|
||||||
// LEGACY
|
label: 'Newest Episodes',
|
||||||
// api/libraries/:id/books/categories
|
type: 'episode',
|
||||||
async getLibraryCategories(req, res) {
|
entities: episodesRecentlyAdded
|
||||||
var library = req.library
|
})
|
||||||
var books = this.db.audiobooks.filter(ab => ab.libraryId === library.id)
|
|
||||||
var limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12
|
|
||||||
var minified = req.query.minified === '1'
|
|
||||||
|
|
||||||
var booksWithUserAb = libraryHelpers.getItemsWithUserProgress(req.user, books)
|
|
||||||
var series = libraryHelpers.getSeriesFromBooks(books, minified)
|
|
||||||
var seriesWithUserAb = libraryHelpers.getSeriesWithProgressFromBooks(req.user, books)
|
|
||||||
|
|
||||||
var categories = [
|
|
||||||
{
|
|
||||||
id: 'continue-reading',
|
|
||||||
label: 'Continue Reading',
|
|
||||||
type: 'books',
|
|
||||||
entities: libraryHelpers.getBooksMostRecentlyRead(booksWithUserAb, limitPerShelf, minified)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'continue-series',
|
|
||||||
label: 'Continue Series',
|
|
||||||
type: 'books',
|
|
||||||
entities: libraryHelpers.getBooksNextInSeries(seriesWithUserAb, limitPerShelf, minified)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'recently-added',
|
|
||||||
label: 'Recently Added',
|
|
||||||
type: 'books',
|
|
||||||
entities: libraryHelpers.getBooksMostRecentlyAdded(books, limitPerShelf, minified)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'read-again',
|
|
||||||
label: 'Read Again',
|
|
||||||
type: 'books',
|
|
||||||
entities: libraryHelpers.getBooksMostRecentlyFinished(booksWithUserAb, limitPerShelf, minified)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'recent-series',
|
|
||||||
label: 'Recent Series',
|
|
||||||
type: 'series',
|
|
||||||
entities: libraryHelpers.getSeriesMostRecentlyAdded(series, limitPerShelf)
|
|
||||||
}
|
}
|
||||||
].filter(cats => { // Remove categories with no items
|
}
|
||||||
return cats.entities.length
|
|
||||||
})
|
|
||||||
|
|
||||||
res.json(categories)
|
res.json(categories)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ class LibraryItemController {
|
|||||||
}
|
}
|
||||||
}).filter(au => au)
|
}).filter(au => au)
|
||||||
}
|
}
|
||||||
|
} else if (includeEntities.includes('downloads')) {
|
||||||
|
var downloadsInQueue = this.podcastManager.getEpisodeDownloadsInQueue(req.libraryItem.id)
|
||||||
|
item.episodesDownloading = downloadsInQueue.map(d => d.toJSONForClient())
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.json(item)
|
return res.json(item)
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ class PodcastController {
|
|||||||
|
|
||||||
var podcastPath = payload.path.replace(/\\/g, '/')
|
var podcastPath = payload.path.replace(/\\/g, '/')
|
||||||
if (await fs.pathExists(podcastPath)) {
|
if (await fs.pathExists(podcastPath)) {
|
||||||
Logger.error(`[PodcastController] Attempt to create podcast when folder path already exists "${podcastPath}"`)
|
Logger.error(`[PodcastController] Podcast folder already exists "${podcastPath}"`)
|
||||||
return res.status(400).send('Path already exists')
|
return res.status(400).send('Podcast already exists')
|
||||||
}
|
}
|
||||||
|
|
||||||
var success = await fs.ensureDir(podcastPath).then(() => true).catch((error) => {
|
var success = await fs.ensureDir(podcastPath).then(() => true).catch((error) => {
|
||||||
@@ -63,7 +63,8 @@ class PodcastController {
|
|||||||
// Download and save cover image
|
// Download and save cover image
|
||||||
if (payload.media.metadata.imageUrl) {
|
if (payload.media.metadata.imageUrl) {
|
||||||
// TODO: Scan cover image to library files
|
// TODO: Scan cover image to library files
|
||||||
var coverResponse = await this.coverManager.downloadCoverFromUrl(libraryItem, payload.media.metadata.imageUrl)
|
// Podcast cover will always go into library item folder
|
||||||
|
var coverResponse = await this.coverManager.downloadCoverFromUrl(libraryItem, payload.media.metadata.imageUrl, true)
|
||||||
if (coverResponse) {
|
if (coverResponse) {
|
||||||
if (coverResponse.error) {
|
if (coverResponse.error) {
|
||||||
Logger.error(`[PodcastController] Download cover error from "${payload.media.metadata.imageUrl}": ${coverResponse.error}`)
|
Logger.error(`[PodcastController] Download cover error from "${payload.media.metadata.imageUrl}": ${coverResponse.error}`)
|
||||||
@@ -101,6 +102,7 @@ class PodcastController {
|
|||||||
Logger.error('Invalid podcast feed request response')
|
Logger.error('Invalid podcast feed request response')
|
||||||
return res.status(500).send('Bad response from feed request')
|
return res.status(500).send('Bad response from feed request')
|
||||||
}
|
}
|
||||||
|
Logger.debug(`[PdocastController] Podcast feed size ${(data.data.length / 1024 / 1024).toFixed(2)}MB`)
|
||||||
var payload = await parsePodcastRssFeedXml(data.data, includeRaw)
|
var payload = await parsePodcastRssFeedXml(data.data, includeRaw)
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
return res.status(500).send('Invalid podcast RSS feed')
|
return res.status(500).send('Invalid podcast RSS feed')
|
||||||
@@ -128,6 +130,26 @@ class PodcastController {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearEpisodeDownloadQueue(req, res) {
|
||||||
|
if (!req.user.canUpdate) {
|
||||||
|
Logger.error(`[PodcastController] User attempting to clear download queue without permission "${req.user.username}"`)
|
||||||
|
return res.sendStatus(500)
|
||||||
|
}
|
||||||
|
this.podcastManager.clearDownloadQueue(req.params.id)
|
||||||
|
res.sendStatus(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
getEpisodeDownloads(req, res) {
|
||||||
|
var libraryItem = this.db.getLibraryItem(req.params.id)
|
||||||
|
if (!libraryItem || libraryItem.mediaType !== 'podcast') {
|
||||||
|
return res.sendStatus(404)
|
||||||
|
}
|
||||||
|
var downloadsInQueue = this.podcastManager.getEpisodeDownloadsInQueue(libraryItem.id)
|
||||||
|
res.json({
|
||||||
|
downloads: downloadsInQueue.map(d => d.toJSONForClient())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async downloadEpisodes(req, res) {
|
async downloadEpisodes(req, res) {
|
||||||
var libraryItem = this.db.getLibraryItem(req.params.id)
|
var libraryItem = this.db.getLibraryItem(req.params.id)
|
||||||
if (!libraryItem || libraryItem.mediaType !== 'podcast') {
|
if (!libraryItem || libraryItem.mediaType !== 'podcast') {
|
||||||
|
|||||||
@@ -23,9 +23,6 @@ class BackupManager {
|
|||||||
this.scheduleTask = null
|
this.scheduleTask = null
|
||||||
|
|
||||||
this.backups = []
|
this.backups = []
|
||||||
|
|
||||||
// If backup exceeds this value it will be aborted
|
|
||||||
this.MaxBytesBeforeAbort = 1000000000 // ~ 1GB
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get serverSettings() {
|
get serverSettings() {
|
||||||
@@ -263,7 +260,8 @@ class BackupManager {
|
|||||||
reject(err)
|
reject(err)
|
||||||
})
|
})
|
||||||
archive.on('progress', ({ fs: fsobj }) => {
|
archive.on('progress', ({ fs: fsobj }) => {
|
||||||
if (fsobj.processedBytes > this.MaxBytesBeforeAbort) {
|
const maxBackupSizeInBytes = this.serverSettings.maxBackupSize * 1000 * 1000 * 1000
|
||||||
|
if (fsobj.processedBytes > maxBackupSizeInBytes) {
|
||||||
Logger.error(`[BackupManager] Archiver is too large - aborting to prevent endless loop, Bytes Processed: ${fsobj.processedBytes}`)
|
Logger.error(`[BackupManager] Archiver is too large - aborting to prevent endless loop, Bytes Processed: ${fsobj.processedBytes}`)
|
||||||
archive.abort()
|
archive.abort()
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@@ -119,9 +119,10 @@ class CoverManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadCoverFromUrl(libraryItem, url) {
|
async downloadCoverFromUrl(libraryItem, url, forceLibraryItemFolder = false) {
|
||||||
try {
|
try {
|
||||||
var coverDirPath = this.getCoverDirectory(libraryItem)
|
// Force save cover with library item is used for adding new podcasts
|
||||||
|
var coverDirPath = forceLibraryItemFolder ? libraryItem.path : this.getCoverDirectory(libraryItem)
|
||||||
await fs.ensureDir(coverDirPath)
|
await fs.ensureDir(coverDirPath)
|
||||||
|
|
||||||
var temppath = Path.posix.join(coverDirPath, 'cover')
|
var temppath = Path.posix.join(coverDirPath, 'cover')
|
||||||
|
|||||||
@@ -78,6 +78,13 @@ class PlaybackSessionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async startSession(user, libraryItem, episodeId, options) {
|
async startSession(user, libraryItem, episodeId, options) {
|
||||||
|
// Close any sessions already open for user
|
||||||
|
var userSessions = this.sessions.filter(playbackSession => playbackSession.userId === user.id)
|
||||||
|
for (const session of userSessions) {
|
||||||
|
Logger.info(`[PlaybackSessionManager] startSession: Closing open session "${session.displayTitle}" for user "${user.username}"`)
|
||||||
|
await this.closeSession(user, session, null)
|
||||||
|
}
|
||||||
|
|
||||||
var shouldDirectPlay = options.forceDirectPlay || (!options.forceTranscode && libraryItem.media.checkCanDirectPlay(options, episodeId))
|
var shouldDirectPlay = options.forceDirectPlay || (!options.forceTranscode && libraryItem.media.checkCanDirectPlay(options, episodeId))
|
||||||
var mediaPlayer = options.mediaPlayer || 'unknown'
|
var mediaPlayer = options.mediaPlayer || 'unknown'
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,23 @@ class PodcastManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getEpisodeDownloadsInQueue(libraryItemId) {
|
||||||
|
return this.downloadQueue.filter(d => d.libraryItemId === libraryItemId)
|
||||||
|
}
|
||||||
|
|
||||||
|
clearDownloadQueue(libraryItemId = null) {
|
||||||
|
if (!this.downloadQueue.length) return
|
||||||
|
|
||||||
|
if (!libraryItemId) {
|
||||||
|
Logger.info(`[PodcastManager] Clearing all downloads in queue (${this.downloadQueue.length})`)
|
||||||
|
this.downloadQueue = []
|
||||||
|
} else {
|
||||||
|
var itemDownloads = this.getEpisodeDownloadsInQueue(libraryItemId)
|
||||||
|
Logger.info(`[PodcastManager] Clearing downloads in queue for item "${libraryItemId}" (${itemDownloads.length})`)
|
||||||
|
this.downloadQueue = this.downloadQueue.filter(d => d.libraryItemId !== libraryItemId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async downloadPodcastEpisodes(libraryItem, episodesToDownload) {
|
async downloadPodcastEpisodes(libraryItem, episodesToDownload) {
|
||||||
var index = libraryItem.media.episodes.length + 1
|
var index = libraryItem.media.episodes.length + 1
|
||||||
episodesToDownload.forEach((ep) => {
|
episodesToDownload.forEach((ep) => {
|
||||||
@@ -50,8 +67,11 @@ class PodcastManager {
|
|||||||
async startPodcastEpisodeDownload(podcastEpisodeDownload) {
|
async startPodcastEpisodeDownload(podcastEpisodeDownload) {
|
||||||
if (this.currentDownload) {
|
if (this.currentDownload) {
|
||||||
this.downloadQueue.push(podcastEpisodeDownload)
|
this.downloadQueue.push(podcastEpisodeDownload)
|
||||||
|
this.emitter('episode_download_queued', podcastEpisodeDownload.toJSONForClient())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.emitter('episode_download_started', podcastEpisodeDownload.toJSONForClient())
|
||||||
this.currentDownload = podcastEpisodeDownload
|
this.currentDownload = podcastEpisodeDownload
|
||||||
|
|
||||||
// Ignores all added files to this dir
|
// Ignores all added files to this dir
|
||||||
@@ -65,11 +85,17 @@ class PodcastManager {
|
|||||||
success = await this.scanAddPodcastEpisodeAudioFile()
|
success = await this.scanAddPodcastEpisodeAudioFile()
|
||||||
if (!success) {
|
if (!success) {
|
||||||
await fs.remove(this.currentDownload.targetPath)
|
await fs.remove(this.currentDownload.targetPath)
|
||||||
|
this.currentDownload.setFinished(false)
|
||||||
} else {
|
} else {
|
||||||
Logger.info(`[PodcastManager] Successfully downloaded podcast episode "${this.currentDownload.podcastEpisode.title}"`)
|
Logger.info(`[PodcastManager] Successfully downloaded podcast episode "${this.currentDownload.podcastEpisode.title}"`)
|
||||||
|
this.currentDownload.setFinished(true)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
this.currentDownload.setFinished(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.emitter('episode_download_finished', this.currentDownload.toJSONForClient())
|
||||||
|
|
||||||
this.watcher.removeIgnoreDir(this.currentDownload.libraryItem.path)
|
this.watcher.removeIgnoreDir(this.currentDownload.libraryItem.path)
|
||||||
this.currentDownload = null
|
this.currentDownload = null
|
||||||
if (this.downloadQueue.length) {
|
if (this.downloadQueue.length) {
|
||||||
|
|||||||
@@ -10,22 +10,42 @@ class PodcastEpisodeDownload {
|
|||||||
this.libraryItem = null
|
this.libraryItem = null
|
||||||
|
|
||||||
this.isDownloading = false
|
this.isDownloading = false
|
||||||
|
this.isFinished = false
|
||||||
|
this.failed = false
|
||||||
|
|
||||||
this.startedAt = null
|
this.startedAt = null
|
||||||
this.createdAt = null
|
this.createdAt = null
|
||||||
this.finishedAt = null
|
this.finishedAt = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toJSONForClient() {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
// podcastEpisode: this.podcastEpisode ? this.podcastEpisode.toJSON() : null,
|
||||||
|
episodeDisplayTitle: this.podcastEpisode ? this.podcastEpisode.bestFilename : null,
|
||||||
|
url: this.url,
|
||||||
|
libraryItemId: this.libraryItem ? this.libraryItem.id : null,
|
||||||
|
isDownloading: this.isDownloading,
|
||||||
|
isFinished: this.isFinished,
|
||||||
|
failed: this.failed,
|
||||||
|
startedAt: this.startedAt,
|
||||||
|
createdAt: this.createdAt,
|
||||||
|
finishedAt: this.finishedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get targetFilename() {
|
get targetFilename() {
|
||||||
return sanitizeFilename(`${this.podcastEpisode.bestFilename}.mp3`)
|
return sanitizeFilename(`${this.podcastEpisode.bestFilename}.mp3`)
|
||||||
}
|
}
|
||||||
|
|
||||||
get targetPath() {
|
get targetPath() {
|
||||||
return Path.join(this.libraryItem.path, this.targetFilename)
|
return Path.join(this.libraryItem.path, this.targetFilename)
|
||||||
}
|
}
|
||||||
|
|
||||||
get targetRelPath() {
|
get targetRelPath() {
|
||||||
return this.targetFilename
|
return this.targetFilename
|
||||||
}
|
}
|
||||||
|
get libraryItemId() {
|
||||||
|
return this.libraryItem ? this.libraryItem.id : null
|
||||||
|
}
|
||||||
|
|
||||||
setData(podcastEpisode, libraryItem) {
|
setData(podcastEpisode, libraryItem) {
|
||||||
this.id = getId('epdl')
|
this.id = getId('epdl')
|
||||||
@@ -34,5 +54,11 @@ class PodcastEpisodeDownload {
|
|||||||
this.libraryItem = libraryItem
|
this.libraryItem = libraryItem
|
||||||
this.createdAt = Date.now()
|
this.createdAt = Date.now()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setFinished(success) {
|
||||||
|
this.finishedAt = Date.now()
|
||||||
|
this.isFinished = true
|
||||||
|
this.failed = !success
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = PodcastEpisodeDownload
|
module.exports = PodcastEpisodeDownload
|
||||||
@@ -257,5 +257,9 @@ class Podcast {
|
|||||||
if (!episode) return 0
|
if (!episode) return 0
|
||||||
return episode.duration
|
return episode.duration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getEpisode(episodeId) {
|
||||||
|
return this.episodes.find(ep => ep.id == episodeId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = Podcast
|
module.exports = Podcast
|
||||||
@@ -162,6 +162,11 @@ class BookMetadata {
|
|||||||
if (!series) return null
|
if (!series) return null
|
||||||
return series.sequence || ''
|
return series.sequence || ''
|
||||||
}
|
}
|
||||||
|
getSeriesSortTitle(series) {
|
||||||
|
if (!series) return ''
|
||||||
|
if (!series.sequence) return series.name
|
||||||
|
return `${series.name} #${series.sequence}`
|
||||||
|
}
|
||||||
|
|
||||||
update(payload) {
|
update(payload) {
|
||||||
var json = this.toJSON()
|
var json = this.toJSON()
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class ServerSettings {
|
|||||||
// this.backupSchedule = '0 1 * * *' // If false then auto-backups are disabled (default every day at 1am)
|
// this.backupSchedule = '0 1 * * *' // If false then auto-backups are disabled (default every day at 1am)
|
||||||
this.backupSchedule = false
|
this.backupSchedule = false
|
||||||
this.backupsToKeep = 2
|
this.backupsToKeep = 2
|
||||||
|
this.maxBackupSize = 1
|
||||||
this.backupMetadataCovers = true
|
this.backupMetadataCovers = true
|
||||||
|
|
||||||
// Logger
|
// Logger
|
||||||
@@ -78,6 +79,7 @@ class ServerSettings {
|
|||||||
|
|
||||||
this.backupSchedule = settings.backupSchedule || false
|
this.backupSchedule = settings.backupSchedule || false
|
||||||
this.backupsToKeep = settings.backupsToKeep || 2
|
this.backupsToKeep = settings.backupsToKeep || 2
|
||||||
|
this.maxBackupSize = settings.maxBackupSize || 1
|
||||||
this.backupMetadataCovers = settings.backupMetadataCovers !== false
|
this.backupMetadataCovers = settings.backupMetadataCovers !== false
|
||||||
|
|
||||||
this.loggerDailyLogsToKeep = settings.loggerDailyLogsToKeep || 7
|
this.loggerDailyLogsToKeep = settings.loggerDailyLogsToKeep || 7
|
||||||
@@ -114,6 +116,7 @@ class ServerSettings {
|
|||||||
rateLimitLoginWindow: this.rateLimitLoginWindow,
|
rateLimitLoginWindow: this.rateLimitLoginWindow,
|
||||||
backupSchedule: this.backupSchedule,
|
backupSchedule: this.backupSchedule,
|
||||||
backupsToKeep: this.backupsToKeep,
|
backupsToKeep: this.backupsToKeep,
|
||||||
|
maxBackupSize: this.maxBackupSize,
|
||||||
backupMetadataCovers: this.backupMetadataCovers,
|
backupMetadataCovers: this.backupMetadataCovers,
|
||||||
loggerDailyLogsToKeep: this.loggerDailyLogsToKeep,
|
loggerDailyLogsToKeep: this.loggerDailyLogsToKeep,
|
||||||
loggerScannerLogsToKeep: this.loggerScannerLogsToKeep,
|
loggerScannerLogsToKeep: this.loggerScannerLogsToKeep,
|
||||||
|
|||||||
@@ -251,6 +251,11 @@ class User {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAllMediaProgressForLibraryItem(libraryItemId) {
|
||||||
|
if (!this.mediaProgress) return []
|
||||||
|
return this.mediaProgress.filter(li => li.libraryItemId === libraryItemId)
|
||||||
|
}
|
||||||
|
|
||||||
createUpdateMediaProgress(libraryItem, updatePayload, episodeId = null) {
|
createUpdateMediaProgress(libraryItem, updatePayload, episodeId = null) {
|
||||||
var itemProgress = this.mediaProgress.find(li => {
|
var itemProgress = this.mediaProgress.find(li => {
|
||||||
if (episodeId && li.episodeId !== episodeId) return false
|
if (episodeId && li.episodeId !== episodeId) return false
|
||||||
|
|||||||
@@ -126,10 +126,10 @@ class ApiRouter {
|
|||||||
//
|
//
|
||||||
this.router.get('/me/listening-sessions', MeController.getListeningSessions.bind(this))
|
this.router.get('/me/listening-sessions', MeController.getListeningSessions.bind(this))
|
||||||
this.router.get('/me/listening-stats', MeController.getListeningStats.bind(this))
|
this.router.get('/me/listening-stats', MeController.getListeningStats.bind(this))
|
||||||
|
this.router.patch('/me/progress/batch/update', MeController.batchUpdateMediaProgress.bind(this))
|
||||||
this.router.patch('/me/progress/:id', MeController.createUpdateMediaProgress.bind(this))
|
this.router.patch('/me/progress/:id', MeController.createUpdateMediaProgress.bind(this))
|
||||||
this.router.delete('/me/progress/:id', MeController.removeMediaProgress.bind(this))
|
this.router.delete('/me/progress/:id', MeController.removeMediaProgress.bind(this))
|
||||||
this.router.patch('/me/progress/:id/:episodeId', MeController.createUpdateEpisodeMediaProgress.bind(this))
|
this.router.patch('/me/progress/:id/:episodeId', MeController.createUpdateEpisodeMediaProgress.bind(this))
|
||||||
this.router.patch('/me/progress/batch/update', MeController.batchUpdateMediaProgress.bind(this))
|
|
||||||
this.router.post('/me/item/:id/bookmark', MeController.createBookmark.bind(this))
|
this.router.post('/me/item/:id/bookmark', MeController.createBookmark.bind(this))
|
||||||
this.router.patch('/me/item/:id/bookmark', MeController.updateBookmark.bind(this))
|
this.router.patch('/me/item/:id/bookmark', MeController.updateBookmark.bind(this))
|
||||||
this.router.delete('/me/item/:id/bookmark/:time', MeController.removeBookmark.bind(this))
|
this.router.delete('/me/item/:id/bookmark/:time', MeController.removeBookmark.bind(this))
|
||||||
@@ -178,6 +178,8 @@ class ApiRouter {
|
|||||||
this.router.post('/podcasts', PodcastController.create.bind(this))
|
this.router.post('/podcasts', PodcastController.create.bind(this))
|
||||||
this.router.post('/podcasts/feed', PodcastController.getPodcastFeed.bind(this))
|
this.router.post('/podcasts/feed', PodcastController.getPodcastFeed.bind(this))
|
||||||
this.router.get('/podcasts/:id/checknew', PodcastController.checkNewEpisodes.bind(this))
|
this.router.get('/podcasts/:id/checknew', PodcastController.checkNewEpisodes.bind(this))
|
||||||
|
this.router.get('/podcasts/:id/downloads', PodcastController.getEpisodeDownloads.bind(this))
|
||||||
|
this.router.get('/podcasts/:id/clear-queue', PodcastController.clearEpisodeDownloadQueue.bind(this))
|
||||||
this.router.post('/podcasts/:id/download-episodes', PodcastController.downloadEpisodes.bind(this))
|
this.router.post('/podcasts/:id/download-episodes', PodcastController.downloadEpisodes.bind(this))
|
||||||
this.router.patch('/podcasts/:id/episode/:episodeId', PodcastController.updateEpisode.bind(this))
|
this.router.patch('/podcasts/:id/episode/:episodeId', PodcastController.updateEpisode.bind(this))
|
||||||
|
|
||||||
|
|||||||
@@ -518,7 +518,7 @@ class Scanner {
|
|||||||
var altDir = `${itemDir}/${firstNest}`
|
var altDir = `${itemDir}/${firstNest}`
|
||||||
|
|
||||||
var fullPath = Path.posix.join(folder.fullPath.replace(/\\/g, '/'), itemDir)
|
var fullPath = Path.posix.join(folder.fullPath.replace(/\\/g, '/'), itemDir)
|
||||||
var childLibraryItem = this.db.libraryItems.find(li => li.path !== fullPath && li.fullPath.startsWith(fullPath))
|
var childLibraryItem = this.db.libraryItems.find(li => li.path !== fullPath && li.path.startsWith(fullPath))
|
||||||
if (!childLibraryItem) {
|
if (!childLibraryItem) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -642,7 +642,7 @@ class Scanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update media metadata if not set OR overrideDetails flag
|
// Update media metadata if not set OR overrideDetails flag
|
||||||
const detailKeysToUpdate = ['title', 'subtitle', 'narrator', 'publisher', 'publishedYear', 'asin', 'isbn']
|
const detailKeysToUpdate = ['title', 'subtitle', 'description', 'narrator', 'publisher', 'publishedYear', 'asin', 'isbn']
|
||||||
const updatePayload = {}
|
const updatePayload = {}
|
||||||
for (const key in matchData) {
|
for (const key in matchData) {
|
||||||
if (matchData[key] && detailKeysToUpdate.includes(key)) {
|
if (matchData[key] && detailKeysToUpdate.includes(key)) {
|
||||||
|
|||||||
@@ -242,10 +242,10 @@ async function migrateLibraryItems(db) {
|
|||||||
var libraryItems = audiobooks.map((ab) => makeLibraryItemFromOldAb(ab))
|
var libraryItems = audiobooks.map((ab) => makeLibraryItemFromOldAb(ab))
|
||||||
|
|
||||||
Logger.info(`>>> ${libraryItems.length} Library Items made`)
|
Logger.info(`>>> ${libraryItems.length} Library Items made`)
|
||||||
await db.insertEntities('libraryItem', libraryItems)
|
await db.bulkInsertEntities('libraryItem', libraryItems)
|
||||||
if (authorsToAdd.length) {
|
if (authorsToAdd.length) {
|
||||||
Logger.info(`>>> ${authorsToAdd.length} Authors made`)
|
Logger.info(`>>> ${authorsToAdd.length} Authors made`)
|
||||||
await db.insertEntities('author', authorsToAdd)
|
await db.bulkInsertEntities('author', authorsToAdd)
|
||||||
}
|
}
|
||||||
if (seriesToAdd.length) {
|
if (seriesToAdd.length) {
|
||||||
Logger.info(`>>> ${seriesToAdd.length} Series made`)
|
Logger.info(`>>> ${seriesToAdd.length} Series made`)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const { sort, createNewSortInstance } = require('fast-sort')
|
const { sort, createNewSortInstance } = require('fast-sort')
|
||||||
|
const Logger = require('../Logger')
|
||||||
const naturalSort = createNewSortInstance({
|
const naturalSort = createNewSortInstance({
|
||||||
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
|
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
|
||||||
})
|
})
|
||||||
@@ -56,7 +57,7 @@ module.exports = {
|
|||||||
} else if (filterBy === 'issues') {
|
} else if (filterBy === 'issues') {
|
||||||
filtered = filtered.filter(ab => {
|
filtered = filtered.filter(ab => {
|
||||||
// TODO: Update filter for issues
|
// TODO: Update filter for issues
|
||||||
return ab.isMissing
|
return ab.isMissing || ab.isInvalid
|
||||||
// return ab.numMissingParts || ab.numInvalidParts || ab.isMissing || ab.isInvalid
|
// return ab.numMissingParts || ab.numInvalidParts || ab.isMissing || ab.isInvalid
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -172,14 +173,28 @@ module.exports = {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
getItemsWithUserProgress(user, libraryItems) {
|
getMediaProgressWithItems(user, libraryItems) {
|
||||||
return libraryItems.map(li => {
|
var mediaProgress = []
|
||||||
var itemProgress = user.getMediaProgress(li.id)
|
libraryItems.forEach(li => {
|
||||||
return {
|
var itemProgress = user.getAllMediaProgressForLibraryItem(li.id).map(mp => {
|
||||||
userProgress: itemProgress ? itemProgress.toJSON() : null,
|
var episode = null
|
||||||
libraryItem: li
|
if (mp.episodeId) {
|
||||||
}
|
episode = li.media.getEpisode(mp.episodeId)
|
||||||
}).filter(b => !!b.userProgress)
|
if (!episode) {
|
||||||
|
// Episode not found for library item
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
userProgress: mp.toJSON(),
|
||||||
|
libraryItem: li,
|
||||||
|
episode
|
||||||
|
}
|
||||||
|
}).filter(mp => !!mp)
|
||||||
|
|
||||||
|
mediaProgress = mediaProgress.concat(itemProgress)
|
||||||
|
})
|
||||||
|
return mediaProgress
|
||||||
},
|
},
|
||||||
|
|
||||||
getItemsMostRecentlyListened(itemsWithUserProgress, limit, minified = false) {
|
getItemsMostRecentlyListened(itemsWithUserProgress, limit, minified = false) {
|
||||||
@@ -187,7 +202,13 @@ module.exports = {
|
|||||||
itemsInProgress.sort((a, b) => {
|
itemsInProgress.sort((a, b) => {
|
||||||
return b.userProgress.lastUpdate - a.userProgress.lastUpdate
|
return b.userProgress.lastUpdate - a.userProgress.lastUpdate
|
||||||
})
|
})
|
||||||
return itemsInProgress.map(b => minified ? b.libraryItem.toJSONMinified() : b.libraryItem.toJSONExpanded()).slice(0, limit)
|
return itemsInProgress.map(b => {
|
||||||
|
var libjson = minified ? b.libraryItem.toJSONMinified() : b.libraryItem.toJSONExpanded()
|
||||||
|
if (b.episode) {
|
||||||
|
libjson.recentEpisode = b.episode
|
||||||
|
}
|
||||||
|
return libjson
|
||||||
|
}).slice(0, limit)
|
||||||
},
|
},
|
||||||
|
|
||||||
getBooksNextInSeries(seriesWithUserAb, limit, minified = false) {
|
getBooksNextInSeries(seriesWithUserAb, limit, minified = false) {
|
||||||
@@ -202,17 +223,39 @@ module.exports = {
|
|||||||
return booksNextInSeries.sort((a, b) => { return b.DateLastReadSeries - a.DateLastReadSeries }).map(b => minified ? b.book.toJSONMinified() : b.book.toJSONExpanded()).slice(0, limit)
|
return booksNextInSeries.sort((a, b) => { return b.DateLastReadSeries - a.DateLastReadSeries }).map(b => minified ? b.book.toJSONMinified() : b.book.toJSONExpanded()).slice(0, limit)
|
||||||
},
|
},
|
||||||
|
|
||||||
getItemsMostRecentlyAdded(libraryItems, limit, minified = false) {
|
|
||||||
var itemsSortedByAddedAt = sort(libraryItems).desc(li => li.addedAt)
|
|
||||||
return itemsSortedByAddedAt.map(b => minified ? b.toJSONMinified() : b.toJSONExpanded()).slice(0, limit)
|
|
||||||
},
|
|
||||||
|
|
||||||
getItemsMostRecentlyFinished(itemsWithUserProgress, limit, minified = false) {
|
getItemsMostRecentlyFinished(itemsWithUserProgress, limit, minified = false) {
|
||||||
var itemsFinished = itemsWithUserProgress.filter((data) => data.userProgress && data.userProgress.isFinished)
|
var itemsFinished = itemsWithUserProgress.filter((data) => data.userProgress && data.userProgress.isFinished)
|
||||||
itemsFinished.sort((a, b) => {
|
itemsFinished.sort((a, b) => {
|
||||||
return b.userProgress.finishedAt - a.userProgress.finishedAt
|
return b.userProgress.finishedAt - a.userProgress.finishedAt
|
||||||
})
|
})
|
||||||
return itemsFinished.map(i => minified ? i.libraryItem.toJSONMinified() : i.libraryItem.toJSONExpanded()).slice(0, limit)
|
return itemsFinished.map(i => {
|
||||||
|
var libjson = minified ? i.libraryItem.toJSONMinified() : i.libraryItem.toJSONExpanded()
|
||||||
|
if (i.episode) {
|
||||||
|
libjson.recentEpisode = i.episode
|
||||||
|
}
|
||||||
|
return libjson
|
||||||
|
}).slice(0, limit)
|
||||||
|
},
|
||||||
|
|
||||||
|
getItemsMostRecentlyAdded(libraryItems, limit, minified = false) {
|
||||||
|
var itemsSortedByAddedAt = sort(libraryItems).desc(li => li.addedAt)
|
||||||
|
return itemsSortedByAddedAt.map(b => minified ? b.toJSONMinified() : b.toJSONExpanded()).slice(0, limit)
|
||||||
|
},
|
||||||
|
|
||||||
|
getEpisodesRecentlyAdded(libraryItems, limit, minified = false) {
|
||||||
|
var libraryItemsWithEpisode = []
|
||||||
|
libraryItems.forEach((li) => {
|
||||||
|
if (li.mediaType !== 'podcast' || !li.media.hasMediaEntities) return
|
||||||
|
var libjson = minified ? li.toJSONMinified() : li.toJSONExpanded()
|
||||||
|
var episodes = sort(li.media.episodes || []).desc(ep => ep.addedAt)
|
||||||
|
episodes.forEach((ep) => {
|
||||||
|
var lie = { ...libjson }
|
||||||
|
lie.recentEpisode = ep
|
||||||
|
libraryItemsWithEpisode.push(lie)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
libraryItemsWithEpisode = sort(libraryItemsWithEpisode).desc(lie => lie.recentEpisode.addedAt)
|
||||||
|
return libraryItemsWithEpisode.slice(0, limit)
|
||||||
},
|
},
|
||||||
|
|
||||||
getSeriesMostRecentlyAdded(series, limit) {
|
getSeriesMostRecentlyAdded(series, limit) {
|
||||||
|
|||||||
Reference in New Issue
Block a user