mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-05-31 12:15:49 -04:00
New api routes, updating web client pages, audiobooks to libraryItem migration
This commit is contained in:
parent
b97ed953f7
commit
2a30cc428f
@ -85,8 +85,9 @@ export default {
|
|||||||
},
|
},
|
||||||
async fetchCategories() {
|
async fetchCategories() {
|
||||||
var categories = await this.$axios
|
var categories = await this.$axios
|
||||||
.$get(`/api/libraries/${this.currentLibraryId}/categories?minified=1`)
|
.$get(`/api/libraries/${this.currentLibraryId}/personalized?minified=1`)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
|
console.log('Personalized data', data)
|
||||||
return data
|
return data
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
@ -239,7 +239,7 @@ export default {
|
|||||||
this.currentSFQueryString = this.buildSearchParams()
|
this.currentSFQueryString = this.buildSearchParams()
|
||||||
}
|
}
|
||||||
|
|
||||||
var entityPath = this.entityName === 'books' || this.entityName === 'series-books' ? `books/all` : this.entityName
|
var entityPath = this.entityName === 'books' || this.entityName === 'series-books' ? `items` : this.entityName
|
||||||
var sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : ''
|
var sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : ''
|
||||||
var fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1`
|
var fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1`
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
<!-- Alternative bookshelf title/author/sort -->
|
<!-- Alternative bookshelf title/author/sort -->
|
||||||
<div v-if="isAlternativeBookshelfView" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }">
|
<div v-if="isAlternativeBookshelfView" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }">
|
||||||
<p class="truncate" :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }">
|
<p class="truncate" :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }">
|
||||||
<span v-if="volumeNumber">#{{ volumeNumber }} </span>{{ displayTitle }}
|
{{ displayTitle }}
|
||||||
</p>
|
</p>
|
||||||
<p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayAuthor }}</p>
|
<p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayAuthor }}</p>
|
||||||
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
|
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
|
||||||
@ -21,7 +21,7 @@
|
|||||||
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }" class="font-book text-gray-300 text-center">{{ title }}</p>
|
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }" class="font-book text-gray-300 text-center">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<img v-show="audiobook" ref="cover" :src="bookCoverSrc" class="w-full h-full object-contain transition-opacity duration-300" :class="showCoverBg ? 'object-contain' : 'object-fill'" @load="imageLoaded" :style="{ opacity: imageReady ? 1 : 0 }" />
|
<img v-show="audiobook" ref="cover" :src="bookCoverSrc" class="w-full h-full transition-opacity duration-300" :class="showCoverBg ? 'object-contain' : 'object-fill'" @load="imageLoaded" :style="{ opacity: imageReady ? 1 : 0 }" />
|
||||||
|
|
||||||
<!-- Placeholder Cover Title & Author -->
|
<!-- Placeholder Cover Title & Author -->
|
||||||
<div v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
<div v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
||||||
@ -136,42 +136,45 @@ export default {
|
|||||||
showExperimentalFeatures() {
|
showExperimentalFeatures() {
|
||||||
return this.store.state.showExperimentalFeatures
|
return this.store.state.showExperimentalFeatures
|
||||||
},
|
},
|
||||||
_audiobook() {
|
_libraryItem() {
|
||||||
return this.audiobook || {}
|
return this.audiobook || {}
|
||||||
},
|
},
|
||||||
book() {
|
media() {
|
||||||
return this._audiobook.book || {}
|
return this._libraryItem.media || {}
|
||||||
|
},
|
||||||
|
mediaMetadata() {
|
||||||
|
return this.media.metadata || {}
|
||||||
},
|
},
|
||||||
placeholderUrl() {
|
placeholderUrl() {
|
||||||
return '/book_placeholder.jpg'
|
return '/book_placeholder.jpg'
|
||||||
},
|
},
|
||||||
bookCoverSrc() {
|
bookCoverSrc() {
|
||||||
return this.store.getters['audiobooks/getBookCoverSrc'](this._audiobook, this.placeholderUrl)
|
return this.store.getters['audiobooks/getLibraryItemCoverSrc'](this._libraryItem, this.placeholderUrl)
|
||||||
},
|
},
|
||||||
audiobookId() {
|
libraryItemId() {
|
||||||
return this._audiobook.id
|
return this._libraryItem.id
|
||||||
},
|
},
|
||||||
series() {
|
series() {
|
||||||
return this.book.series
|
return this.mediaMetadata.series
|
||||||
},
|
},
|
||||||
libraryId() {
|
libraryId() {
|
||||||
return this._audiobook.libraryId
|
return this._libraryItem.libraryId
|
||||||
},
|
},
|
||||||
hasEbook() {
|
hasEbook() {
|
||||||
return this._audiobook.numEbooks
|
return this.media.numEbooks
|
||||||
},
|
},
|
||||||
hasTracks() {
|
hasTracks() {
|
||||||
return this._audiobook.numTracks
|
return this.media.numTracks
|
||||||
},
|
},
|
||||||
processingBatch() {
|
processingBatch() {
|
||||||
return this.store.state.processingBatch
|
return this.store.state.processingBatch
|
||||||
},
|
},
|
||||||
booksInSeries() {
|
booksInSeries() {
|
||||||
// Only added to audiobook object when collapseSeries is enabled
|
// Only added to audiobook object when collapseSeries is enabled
|
||||||
return this._audiobook.booksInSeries
|
return this._libraryItem.booksInSeries
|
||||||
},
|
},
|
||||||
hasCover() {
|
hasCover() {
|
||||||
return !!this.book.cover
|
return !!this.media.coverPath
|
||||||
},
|
},
|
||||||
squareAspectRatio() {
|
squareAspectRatio() {
|
||||||
return this.bookCoverAspectRatio === 1
|
return this.bookCoverAspectRatio === 1
|
||||||
@ -181,43 +184,49 @@ export default {
|
|||||||
return this.width / baseSize
|
return this.width / baseSize
|
||||||
},
|
},
|
||||||
title() {
|
title() {
|
||||||
return this.book.title || ''
|
return this.mediaMetadata.title || ''
|
||||||
},
|
},
|
||||||
playIconFontSize() {
|
playIconFontSize() {
|
||||||
return Math.max(2, 3 * this.sizeMultiplier)
|
return Math.max(2, 3 * this.sizeMultiplier)
|
||||||
},
|
},
|
||||||
author() {
|
authors() {
|
||||||
return this.book.author
|
return this.mediaMetadata.authors || []
|
||||||
},
|
},
|
||||||
authorFL() {
|
author() {
|
||||||
return this.book.authorFL || this.author
|
return this.authors.map((au) => au.name).join(', ')
|
||||||
},
|
},
|
||||||
authorLF() {
|
authorLF() {
|
||||||
return this.book.authorLF || this.author
|
return this.authors
|
||||||
|
.map((au) => {
|
||||||
|
var parts = au.name.split(' ')
|
||||||
|
if (parts.length === 1) return parts[0]
|
||||||
|
return `${parts[1]}, ${parts[0]}`
|
||||||
|
})
|
||||||
|
.join(', ')
|
||||||
},
|
},
|
||||||
volumeNumber() {
|
volumeNumber() {
|
||||||
return this.book.volumeNumber || null
|
return this.mediaMetadata.volumeNumber || null
|
||||||
},
|
},
|
||||||
displayTitle() {
|
displayTitle() {
|
||||||
if (this.orderBy === 'book.title' && this.sortingIgnorePrefix && this.title.toLowerCase().startsWith('the ')) {
|
if (this.orderBy === 'media.metadata.title' && this.sortingIgnorePrefix && this.title.toLowerCase().startsWith('the ')) {
|
||||||
return this.title.substr(4) + ', The'
|
return this.title.substr(4) + ', The'
|
||||||
}
|
}
|
||||||
return this.title
|
return this.title
|
||||||
},
|
},
|
||||||
displayAuthor() {
|
displayAuthor() {
|
||||||
if (this.orderBy === 'book.authorLF') return this.authorLF
|
if (this.orderBy === 'media.metadata.authorLF') return this.authorLF
|
||||||
return this.authorFL
|
return this.author
|
||||||
},
|
},
|
||||||
displaySortLine() {
|
displaySortLine() {
|
||||||
if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._audiobook.mtimeMs)
|
if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs)
|
||||||
if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._audiobook.birthtimeMs)
|
if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs)
|
||||||
if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._audiobook.addedAt)
|
if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._libraryItem.addedAt)
|
||||||
if (this.orderBy === 'duration') return 'Duration: ' + this.$elapsedPrettyExtended(this._audiobook.duration, false)
|
if (this.orderBy === 'duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false)
|
||||||
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._audiobook.size)
|
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
|
||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
userProgress() {
|
userProgress() {
|
||||||
return this.store.getters['user/getUserAudiobook'](this.audiobookId)
|
return this.store.getters['user/getUserAudiobook'](this.libraryItemId)
|
||||||
},
|
},
|
||||||
userProgressPercent() {
|
userProgressPercent() {
|
||||||
return this.userProgress ? this.userProgress.progress || 0 : 0
|
return this.userProgress ? this.userProgress.progress || 0 : 0
|
||||||
@ -229,7 +238,7 @@ export default {
|
|||||||
return this.hasMissingParts || this.hasInvalidParts || this.isMissing || this.isInvalid
|
return this.hasMissingParts || this.hasInvalidParts || this.isMissing || this.isInvalid
|
||||||
},
|
},
|
||||||
isStreaming() {
|
isStreaming() {
|
||||||
return this.store.getters['getAudiobookIdStreaming'] === this.audiobookId
|
return this.store.getters['getlibraryItemIdStreaming'] === this.libraryItemId
|
||||||
},
|
},
|
||||||
showReadButton() {
|
showReadButton() {
|
||||||
return !this.isSelectionMode && this.showExperimentalFeatures && !this.showPlayButton && this.hasEbook
|
return !this.isSelectionMode && this.showExperimentalFeatures && !this.showPlayButton && this.hasEbook
|
||||||
@ -241,16 +250,16 @@ export default {
|
|||||||
return !this.isSelectionMode && this.showExperimentalFeatures && this.hasEbook
|
return !this.isSelectionMode && this.showExperimentalFeatures && this.hasEbook
|
||||||
},
|
},
|
||||||
isMissing() {
|
isMissing() {
|
||||||
return this._audiobook.isMissing
|
return this._libraryItem.isMissing
|
||||||
},
|
},
|
||||||
isInvalid() {
|
isInvalid() {
|
||||||
return this._audiobook.isInvalid
|
return this._libraryItem.isInvalid
|
||||||
},
|
},
|
||||||
hasMissingParts() {
|
hasMissingParts() {
|
||||||
return this._audiobook.hasMissingParts
|
return this._libraryItem.hasMissingParts
|
||||||
},
|
},
|
||||||
hasInvalidParts() {
|
hasInvalidParts() {
|
||||||
return this._audiobook.hasInvalidParts
|
return this._libraryItem.hasInvalidParts
|
||||||
},
|
},
|
||||||
errorText() {
|
errorText() {
|
||||||
if (this.isMissing) return 'Audiobook directory is missing!'
|
if (this.isMissing) return 'Audiobook directory is missing!'
|
||||||
@ -349,11 +358,11 @@ export default {
|
|||||||
return this.title
|
return this.title
|
||||||
},
|
},
|
||||||
authorCleaned() {
|
authorCleaned() {
|
||||||
if (!this.authorFL) return ''
|
if (!this.author) return ''
|
||||||
if (this.authorFL.length > 30) {
|
if (this.author.length > 30) {
|
||||||
return this.authorFL.slice(0, 27) + '...'
|
return this.author.slice(0, 27) + '...'
|
||||||
}
|
}
|
||||||
return this.authorFL
|
return this.author
|
||||||
},
|
},
|
||||||
isAlternativeBookshelfView() {
|
isAlternativeBookshelfView() {
|
||||||
var constants = this.$constants || this.$nuxt.$constants
|
var constants = this.$constants || this.$nuxt.$constants
|
||||||
@ -382,7 +391,7 @@ export default {
|
|||||||
var router = this.$router || this.$nuxt.$router
|
var router = this.$router || this.$nuxt.$router
|
||||||
if (router) {
|
if (router) {
|
||||||
if (this.booksInSeries) router.push(`/library/${this.libraryId}/series/${this.$encode(this.series)}`)
|
if (this.booksInSeries) router.push(`/library/${this.libraryId}/series/${this.$encode(this.series)}`)
|
||||||
else router.push(`/audiobook/${this.audiobookId}`)
|
else router.push(`/item/${this.libraryItemId}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -398,7 +407,7 @@ export default {
|
|||||||
var toast = this.$toast || this.$nuxt.$toast
|
var toast = this.$toast || this.$nuxt.$toast
|
||||||
var axios = this.$axios || this.$nuxt.$axios
|
var axios = this.$axios || this.$nuxt.$axios
|
||||||
axios
|
axios
|
||||||
.$patch(`/api/me/audiobook/${this.audiobookId}`, updatePayload)
|
.$patch(`/api/me/audiobook/${this.libraryItemId}`, updatePayload)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.isProcessingReadUpdate = false
|
this.isProcessingReadUpdate = false
|
||||||
toast.success(`"${this.title}" Marked as ${updatePayload.isRead ? 'Read' : 'Not Read'}`)
|
toast.success(`"${this.title}" Marked as ${updatePayload.isRead ? 'Read' : 'Not Read'}`)
|
||||||
@ -425,7 +434,7 @@ export default {
|
|||||||
rescan() {
|
rescan() {
|
||||||
this.rescanning = true
|
this.rescanning = true
|
||||||
this._socket.once('audiobook_scan_complete', this.audiobookScanComplete)
|
this._socket.once('audiobook_scan_complete', this.audiobookScanComplete)
|
||||||
this._socket.emit('scan_audiobook', this.audiobookId)
|
this._socket.emit('scan_libraryItem', this.libraryItemId)
|
||||||
},
|
},
|
||||||
showEditModalTracks() {
|
showEditModalTracks() {
|
||||||
// More menu func
|
// More menu func
|
||||||
@ -503,7 +512,7 @@ export default {
|
|||||||
},
|
},
|
||||||
play() {
|
play() {
|
||||||
var eventBus = this.$eventBus || this.$nuxt.$eventBus
|
var eventBus = this.$eventBus || this.$nuxt.$eventBus
|
||||||
eventBus.$emit('play-audiobook', this.audiobookId)
|
eventBus.$emit('play-audiobook', this.libraryItemId)
|
||||||
},
|
},
|
||||||
mouseover() {
|
mouseover() {
|
||||||
this.isHovering = true
|
this.isHovering = true
|
||||||
|
@ -34,27 +34,23 @@ export default {
|
|||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
text: 'Title',
|
text: 'Title',
|
||||||
value: 'book.title'
|
value: 'media.metadata.title'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Author (First Last)',
|
text: 'Author (First Last)',
|
||||||
value: 'book.authorFL'
|
value: 'media.metadata.author'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Author (Last, First)',
|
text: 'Author (Last, First)',
|
||||||
value: 'book.authorLF'
|
value: 'media.metadata.authorLF'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Added At',
|
text: 'Added At',
|
||||||
value: 'addedAt'
|
value: 'addedAt'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
text: 'Volume #',
|
|
||||||
value: 'book.volumeNumber'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
text: 'Duration',
|
text: 'Duration',
|
||||||
value: 'duration'
|
value: 'media.duration'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Size',
|
text: 'Size',
|
||||||
@ -89,7 +85,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
selectedText() {
|
selectedText() {
|
||||||
var _selected = this.selected === 'book.author' ? 'book.authorFL' : this.selected
|
var _selected = this.selected
|
||||||
|
if (this.selected.startsWith('book.')) _selected = _selected.replace('book.', 'media.metadata.')
|
||||||
var _sel = this.items.find((i) => i.value === _selected)
|
var _sel = this.items.find((i) => i.value === _selected)
|
||||||
if (!_sel) return ''
|
if (!_sel) return ''
|
||||||
return _sel.text
|
return _sel.text
|
||||||
|
@ -5,8 +5,8 @@
|
|||||||
<div class="absolute cover-bg" ref="coverBg" />
|
<div class="absolute cover-bg" ref="coverBg" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<img v-if="audiobook" ref="cover" :src="fullCoverUrl" loading="lazy" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0 z-10 duration-300 transition-opacity" :style="{ opacity: imageReady ? '1' : '0' }" :class="showCoverBg ? 'object-contain' : 'object-fill'" />
|
<img v-if="libraryItem" ref="cover" :src="fullCoverUrl" loading="lazy" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0 z-10 duration-300 transition-opacity" :style="{ opacity: imageReady ? '1' : '0' }" :class="showCoverBg ? 'object-contain' : 'object-fill'" />
|
||||||
<div v-show="loading && audiobook" class="absolute top-0 left-0 h-full w-full flex items-center justify-center">
|
<div v-show="loading && libraryItem" class="absolute top-0 left-0 h-full w-full flex items-center justify-center">
|
||||||
<p class="font-book text-center" :style="{ fontSize: 0.75 * sizeMultiplier + 'rem' }">{{ title }}</p>
|
<p class="font-book text-center" :style="{ fontSize: 0.75 * sizeMultiplier + 'rem' }">{{ title }}</p>
|
||||||
<div class="absolute top-2 right-2">
|
<div class="absolute top-2 right-2">
|
||||||
<div class="la-ball-spin-clockwise la-sm">
|
<div class="la-ball-spin-clockwise la-sm">
|
||||||
@ -44,11 +44,10 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
audiobook: {
|
libraryItem: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
},
|
},
|
||||||
authorOverride: String,
|
|
||||||
width: {
|
width: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 120
|
default: 120
|
||||||
@ -75,12 +74,15 @@ export default {
|
|||||||
height() {
|
height() {
|
||||||
return this.width * this.bookCoverAspectRatio
|
return this.width * this.bookCoverAspectRatio
|
||||||
},
|
},
|
||||||
book() {
|
media() {
|
||||||
if (!this.audiobook) return {}
|
if (!this.libraryItem) return {}
|
||||||
return this.audiobook.book || {}
|
return this.libraryItem.media || {}
|
||||||
|
},
|
||||||
|
mediaMetadata() {
|
||||||
|
return this.media.metadata || {}
|
||||||
},
|
},
|
||||||
title() {
|
title() {
|
||||||
return this.book.title || 'No Title'
|
return this.mediaMetadata.title || 'No Title'
|
||||||
},
|
},
|
||||||
titleCleaned() {
|
titleCleaned() {
|
||||||
if (this.title.length > 60) {
|
if (this.title.length > 60) {
|
||||||
@ -88,9 +90,11 @@ export default {
|
|||||||
}
|
}
|
||||||
return this.title
|
return this.title
|
||||||
},
|
},
|
||||||
|
authors() {
|
||||||
|
return this.mediaMetadata.authors || []
|
||||||
|
},
|
||||||
author() {
|
author() {
|
||||||
if (this.authorOverride) return this.authorOverride
|
return this.authors.map((au) => au.name).join(', ')
|
||||||
return this.book.author || 'Unknown'
|
|
||||||
},
|
},
|
||||||
authorCleaned() {
|
authorCleaned() {
|
||||||
if (this.author.length > 30) {
|
if (this.author.length > 30) {
|
||||||
@ -102,15 +106,15 @@ export default {
|
|||||||
return '/book_placeholder.jpg'
|
return '/book_placeholder.jpg'
|
||||||
},
|
},
|
||||||
fullCoverUrl() {
|
fullCoverUrl() {
|
||||||
if (!this.audiobook) return null
|
if (!this.libraryItem) return null
|
||||||
var store = this.$store || this.$nuxt.$store
|
var store = this.$store || this.$nuxt.$store
|
||||||
return store.getters['audiobooks/getBookCoverSrc'](this.audiobook, this.placeholderUrl)
|
return store.getters['audiobooks/getLibraryItemCoverSrc'](this.libraryItem, this.placeholderUrl)
|
||||||
},
|
},
|
||||||
cover() {
|
cover() {
|
||||||
return this.book.cover || this.placeholderUrl
|
return this.media.coverPath || this.placeholderUrl
|
||||||
},
|
},
|
||||||
hasCover() {
|
hasCover() {
|
||||||
return !!this.book.cover
|
return !!this.media.coverPath
|
||||||
},
|
},
|
||||||
sizeMultiplier() {
|
sizeMultiplier() {
|
||||||
var baseSize = this.squareAspectRatio ? 192 : 120
|
var baseSize = this.squareAspectRatio ? 192 : 120
|
||||||
@ -138,7 +142,6 @@ export default {
|
|||||||
this.$refs.coverBg.style.backgroundImage = `url("${this.fullCoverUrl}")`
|
this.$refs.coverBg.style.backgroundImage = `url("${this.fullCoverUrl}")`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
hideCoverBg() {},
|
|
||||||
imageLoaded() {
|
imageLoaded() {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
|
@ -63,7 +63,7 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getCoverUrl(book) {
|
getCoverUrl(book) {
|
||||||
return this.store.getters['audiobooks/getBookCoverSrc'](book, '')
|
return this.store.getters['audiobooks/getLibraryItemCoverSrc'](book, '')
|
||||||
},
|
},
|
||||||
async buildCoverImg(coverData, bgCoverWidth, offsetLeft, zIndex, forceCoverBg = false) {
|
async buildCoverImg(coverData, bgCoverWidth, offsetLeft, zIndex, forceCoverBg = false) {
|
||||||
var src = coverData.coverUrl
|
var src = coverData.coverUrl
|
||||||
|
@ -22,7 +22,7 @@ export default {
|
|||||||
return '/book_placeholder.jpg'
|
return '/book_placeholder.jpg'
|
||||||
},
|
},
|
||||||
fullCoverUrl() {
|
fullCoverUrl() {
|
||||||
return this.$store.getters['audiobooks/getBookCoverSrc'](this.audiobook, this.placeholderUrl)
|
return this.$store.getters['audiobooks/getLibraryItemCoverSrc'](this.audiobook, this.placeholderUrl)
|
||||||
},
|
},
|
||||||
hasCover() {
|
hasCover() {
|
||||||
return !!this.audiobook.book.cover
|
return !!this.audiobook.book.cover
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full h-full text-sm rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300">
|
<div class="w-full h-full text-sm rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300">
|
||||||
<component v-if="audiobook && show" :is="tabName" :audiobook="audiobook" :processing.sync="processing" @close="show = false" @selectTab="selectTab" />
|
<component v-if="libraryItem && show" :is="tabName" :library-item="libraryItem" :processing.sync="processing" @close="show = false" @selectTab="selectTab" />
|
||||||
</div>
|
</div>
|
||||||
</modals-modal>
|
</modals-modal>
|
||||||
</template>
|
</template>
|
||||||
@ -29,7 +29,7 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
processing: false,
|
processing: false,
|
||||||
audiobook: null,
|
libraryItem: null,
|
||||||
fetchOnShow: false,
|
fetchOnShow: false,
|
||||||
tabs: [
|
tabs: [
|
||||||
{
|
{
|
||||||
@ -42,11 +42,6 @@ export default {
|
|||||||
title: 'Cover',
|
title: 'Cover',
|
||||||
component: 'modals-edit-tabs-cover'
|
component: 'modals-edit-tabs-cover'
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// id: 'tracks',
|
|
||||||
// title: 'Tracks',
|
|
||||||
// component: 'modals-edit-tabs-tracks'
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
id: 'chapters',
|
id: 'chapters',
|
||||||
title: 'Chapters',
|
title: 'Chapters',
|
||||||
@ -89,12 +84,12 @@ export default {
|
|||||||
this.selectedTab = availableTabIds[0]
|
this.selectedTab = availableTabIds[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.audiobook && this.audiobook.id === this.selectedAudiobookId) {
|
if (this.libraryItem && this.libraryItem.id === this.selectedLibraryItemId) {
|
||||||
if (this.fetchOnShow) this.fetchFull()
|
if (this.fetchOnShow) this.fetchFull()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.fetchOnShow = false
|
this.fetchOnShow = false
|
||||||
this.audiobook = null
|
this.libraryItem = null
|
||||||
this.init()
|
this.init()
|
||||||
this.registerListeners()
|
this.registerListeners()
|
||||||
} else {
|
} else {
|
||||||
@ -148,26 +143,29 @@ export default {
|
|||||||
return _tab ? _tab.component : ''
|
return _tab ? _tab.component : ''
|
||||||
},
|
},
|
||||||
isMissing() {
|
isMissing() {
|
||||||
return this.selectedAudiobook.isMissing
|
return this.selectedLibraryItem.isMissing
|
||||||
},
|
},
|
||||||
selectedAudiobook() {
|
selectedLibraryItem() {
|
||||||
return this.$store.state.selectedAudiobook || {}
|
return this.$store.state.selectedLibraryItem || {}
|
||||||
},
|
},
|
||||||
selectedAudiobookId() {
|
selectedLibraryItemId() {
|
||||||
return this.selectedAudiobook.id
|
return this.selectedLibraryItem.id
|
||||||
},
|
},
|
||||||
book() {
|
media() {
|
||||||
return this.audiobook ? this.audiobook.book || {} : {}
|
return this.libraryItem ? this.libraryItem.media || {} : {}
|
||||||
|
},
|
||||||
|
mediaMetadata() {
|
||||||
|
return this.media.metadata || {}
|
||||||
},
|
},
|
||||||
title() {
|
title() {
|
||||||
return this.book.title || 'No Title'
|
return this.mediaMetadata.title || 'No Title'
|
||||||
},
|
},
|
||||||
bookshelfBookIds() {
|
bookshelfBookIds() {
|
||||||
return this.$store.state.bookshelfBookIds || []
|
return this.$store.state.bookshelfBookIds || []
|
||||||
},
|
},
|
||||||
currentBookshelfIndex() {
|
currentBookshelfIndex() {
|
||||||
if (!this.bookshelfBookIds.length) return 0
|
if (!this.bookshelfBookIds.length) return 0
|
||||||
return this.bookshelfBookIds.findIndex((bid) => bid === this.selectedAudiobookId)
|
return this.bookshelfBookIds.findIndex((bid) => bid === this.selectedLibraryItemId)
|
||||||
},
|
},
|
||||||
canGoPrev() {
|
canGoPrev() {
|
||||||
return this.bookshelfBookIds.length && this.currentBookshelfIndex > 0
|
return this.bookshelfBookIds.length && this.currentBookshelfIndex > 0
|
||||||
@ -188,7 +186,7 @@ export default {
|
|||||||
})
|
})
|
||||||
this.processing = false
|
this.processing = false
|
||||||
if (prevBook) {
|
if (prevBook) {
|
||||||
this.$store.commit('showEditModalOnTab', { audiobook: prevBook, tab: this.selectedTab })
|
this.$store.commit('showEditModalOnTab', { libraryItem: prevBook, tab: this.selectedTab })
|
||||||
this.$nextTick(this.init)
|
this.$nextTick(this.init)
|
||||||
} else {
|
} else {
|
||||||
console.error('Book not found', prevBookId)
|
console.error('Book not found', prevBookId)
|
||||||
@ -205,7 +203,7 @@ export default {
|
|||||||
})
|
})
|
||||||
this.processing = false
|
this.processing = false
|
||||||
if (nextBook) {
|
if (nextBook) {
|
||||||
this.$store.commit('showEditModalOnTab', { audiobook: nextBook, tab: this.selectedTab })
|
this.$store.commit('showEditModalOnTab', { libraryItem: nextBook, tab: this.selectedTab })
|
||||||
this.$nextTick(this.init)
|
this.$nextTick(this.init)
|
||||||
} else {
|
} else {
|
||||||
console.error('Book not found', nextBookId)
|
console.error('Book not found', nextBookId)
|
||||||
@ -223,16 +221,16 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
init() {
|
init() {
|
||||||
this.$store.commit('audiobooks/addListener', { meth: this.audiobookUpdated, id: 'edit-modal', audiobookId: this.selectedAudiobookId })
|
this.$store.commit('audiobooks/addListener', { meth: this.audiobookUpdated, id: 'edit-modal', audiobookId: this.selectedLibraryItemId })
|
||||||
this.fetchFull()
|
this.fetchFull()
|
||||||
},
|
},
|
||||||
async fetchFull() {
|
async fetchFull() {
|
||||||
try {
|
try {
|
||||||
this.processing = true
|
this.processing = true
|
||||||
this.audiobook = await this.$axios.$get(`/api/books/${this.selectedAudiobookId}`)
|
this.libraryItem = await this.$axios.$get(`/api/items/${this.selectedLibraryItemId}?expanded=1`)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch audiobook', this.selectedAudiobookId, error)
|
console.error('Failed to fetch audiobook', this.selectedLibraryItemId, error)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
this.show = false
|
this.show = false
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
audiobook: {
|
libraryItem: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
}
|
}
|
||||||
@ -42,7 +42,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
audiobook: {
|
libraryItem: {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
handler(newVal) {
|
handler(newVal) {
|
||||||
if (newVal) this.init()
|
if (newVal) this.init()
|
||||||
@ -52,7 +52,7 @@ export default {
|
|||||||
computed: {},
|
computed: {},
|
||||||
methods: {
|
methods: {
|
||||||
init() {
|
init() {
|
||||||
this.chapters = this.audiobook.chapters || []
|
this.chapters = this.libraryItem.chapters || []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,9 @@
|
|||||||
<div class="w-full h-full overflow-hidden overflow-y-auto px-4 py-6 relative">
|
<div class="w-full h-full overflow-hidden overflow-y-auto px-4 py-6 relative">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<covers-book-cover :audiobook="audiobook" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-book-cover :library-item="libraryItem" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
<!-- book cover overlay -->
|
<!-- book cover overlay -->
|
||||||
<div v-if="book.cover" class="absolute top-0 left-0 w-full h-full z-10 opacity-0 hover:opacity-100 transition-opacity duration-100">
|
<div v-if="media.coverPath" class="absolute top-0 left-0 w-full h-full z-10 opacity-0 hover:opacity-100 transition-opacity duration-100">
|
||||||
<div class="absolute top-0 left-0 w-full h-16 bg-gradient-to-b from-black-600 to-transparent" />
|
<div class="absolute top-0 left-0 w-full h-16 bg-gradient-to-b from-black-600 to-transparent" />
|
||||||
<div class="p-1 absolute top-1 right-1 text-red-500 rounded-full w-8 h-8 cursor-pointer hover:text-red-400 shadow-sm" @click="removeCover">
|
<div class="p-1 absolute top-1 right-1 text-red-500 rounded-full w-8 h-8 cursor-pointer hover:text-red-400 shadow-sm" @click="removeCover">
|
||||||
<span class="material-icons">delete</span>
|
<span class="material-icons">delete</span>
|
||||||
@ -82,7 +82,7 @@
|
|||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
processing: Boolean,
|
processing: Boolean,
|
||||||
audiobook: {
|
libraryItem: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
}
|
}
|
||||||
@ -102,7 +102,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
audiobook: {
|
libraryItem: {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
handler(newVal) {
|
handler(newVal) {
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
@ -134,17 +134,17 @@ export default {
|
|||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE ? 1 : 1.6
|
return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE ? 1 : 1.6
|
||||||
},
|
},
|
||||||
audiobookId() {
|
libraryItemId() {
|
||||||
return this.audiobook ? this.audiobook.id : null
|
return this.libraryItem ? this.libraryItem.id : null
|
||||||
},
|
},
|
||||||
book() {
|
media() {
|
||||||
return this.audiobook ? this.audiobook.book || {} : {}
|
return this.libraryItem ? this.libraryItem.media || {} : {}
|
||||||
},
|
},
|
||||||
audiobookPath() {
|
mediaMetadata() {
|
||||||
return this.audiobook ? this.audiobook.path : null
|
return this.media.metadata || {}
|
||||||
},
|
},
|
||||||
otherFiles() {
|
libraryFiles() {
|
||||||
return this.audiobook ? this.audiobook.otherFiles || [] : []
|
return this.libraryItem ? this.libraryItem.libraryFiles || [] : []
|
||||||
},
|
},
|
||||||
userCanUpload() {
|
userCanUpload() {
|
||||||
return this.$store.getters['user/getUserCanUpload']
|
return this.$store.getters['user/getUserCanUpload']
|
||||||
@ -153,12 +153,11 @@ export default {
|
|||||||
return this.$store.getters['user/getToken']
|
return this.$store.getters['user/getToken']
|
||||||
},
|
},
|
||||||
localCovers() {
|
localCovers() {
|
||||||
return this.otherFiles
|
return this.libraryFiles
|
||||||
.filter((f) => f.filetype === 'image')
|
.filter((f) => f.fileType === 'image')
|
||||||
.map((file) => {
|
.map((file) => {
|
||||||
var _file = { ...file }
|
var _file = { ...file }
|
||||||
var imgRelPath = _file.path.replace(this.audiobookPath, '')
|
_file.localPath = `/s/item/${this.libraryItemId}/${this.$encodeUriPath(file.metadata.relPath)}`
|
||||||
_file.localPath = `/s/book/${this.audiobookId}/${imgRelPath}`
|
|
||||||
return _file
|
return _file
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -170,7 +169,7 @@ export default {
|
|||||||
form.set('cover', this.selectedFile)
|
form.set('cover', this.selectedFile)
|
||||||
|
|
||||||
this.$axios
|
this.$axios
|
||||||
.$post(`/api/books/${this.audiobook.id}/cover`, form)
|
.$post(`/api/books/${this.libraryItemId}/cover`, form)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
this.$toast.error(data.error)
|
this.$toast.error(data.error)
|
||||||
@ -203,17 +202,17 @@ export default {
|
|||||||
},
|
},
|
||||||
init() {
|
init() {
|
||||||
this.showLocalCovers = false
|
this.showLocalCovers = false
|
||||||
if (this.coversFound.length && (this.searchTitle !== this.book.title || this.searchAuthor !== this.book.authorFL)) {
|
if (this.coversFound.length && (this.searchTitle !== this.mediaMetadata.title || this.searchAuthor !== this.mediaMetadata.authorName)) {
|
||||||
this.coversFound = []
|
this.coversFound = []
|
||||||
this.hasSearched = false
|
this.hasSearched = false
|
||||||
}
|
}
|
||||||
this.imageUrl = this.book.cover || ''
|
this.imageUrl = this.media.coverPath || ''
|
||||||
this.searchTitle = this.book.title || ''
|
this.searchTitle = this.mediaMetadata.title || ''
|
||||||
this.searchAuthor = this.book.authorFL || ''
|
this.searchAuthor = this.mediaMetadata.authorName || ''
|
||||||
this.provider = localStorage.getItem('book-provider') || 'openlibrary'
|
this.provider = localStorage.getItem('book-provider') || 'openlibrary'
|
||||||
},
|
},
|
||||||
removeCover() {
|
removeCover() {
|
||||||
if (!this.book.cover) {
|
if (!this.media.coverPath) {
|
||||||
this.imageUrl = ''
|
this.imageUrl = ''
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -223,7 +222,7 @@ export default {
|
|||||||
this.updateCover(this.imageUrl)
|
this.updateCover(this.imageUrl)
|
||||||
},
|
},
|
||||||
async updateCover(cover) {
|
async updateCover(cover) {
|
||||||
if (cover === this.book.cover) {
|
if (cover === this.media.coverPath) {
|
||||||
console.warn('Cover has not changed..', cover)
|
console.warn('Cover has not changed..', cover)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -233,7 +232,7 @@ export default {
|
|||||||
|
|
||||||
// Download cover from url and use
|
// Download cover from url and use
|
||||||
if (cover.startsWith('http:') || cover.startsWith('https:')) {
|
if (cover.startsWith('http:') || cover.startsWith('https:')) {
|
||||||
success = await this.$axios.$post(`/api/books/${this.audiobook.id}/cover`, { url: cover }).catch((error) => {
|
success = await this.$axios.$post(`/api/items/${this.libraryItemId}/cover`, { url: cover }).catch((error) => {
|
||||||
console.error('Failed to download cover from url', error)
|
console.error('Failed to download cover from url', error)
|
||||||
if (error.response && error.response.data) {
|
if (error.response && error.response.data) {
|
||||||
this.$toast.error(error.response.data)
|
this.$toast.error(error.response.data)
|
||||||
@ -247,7 +246,7 @@ export default {
|
|||||||
cover: cover
|
cover: cover
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
success = await this.$axios.$patch(`/api/books/${this.audiobook.id}`, updatePayload).catch((error) => {
|
success = await this.$axios.$patch(`/api/books/${this.libraryItemId}`, updatePayload).catch((error) => {
|
||||||
console.error('Failed to update', error)
|
console.error('Failed to update', error)
|
||||||
if (error.response && error.response.data) {
|
if (error.response && error.response.data) {
|
||||||
this.$toast.error(error.response.data)
|
this.$toast.error(error.response.data)
|
||||||
@ -259,7 +258,7 @@ export default {
|
|||||||
this.$toast.success('Update Successful')
|
this.$toast.success('Update Successful')
|
||||||
this.$emit('close')
|
this.$emit('close')
|
||||||
} else {
|
} else {
|
||||||
this.imageUrl = this.book.cover || ''
|
this.imageUrl = this.media.coverPath || ''
|
||||||
}
|
}
|
||||||
this.isProcessing = false
|
this.isProcessing = false
|
||||||
},
|
},
|
||||||
@ -292,7 +291,7 @@ export default {
|
|||||||
setCover(coverFile) {
|
setCover(coverFile) {
|
||||||
this.isProcessing = true
|
this.isProcessing = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$patch(`/api/books/${this.audiobook.id}/coverfile`, coverFile)
|
.$patch(`/api/books/${this.libraryItemId}/coverfile`, coverFile)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
console.log('response data', data)
|
console.log('response data', data)
|
||||||
if (data && typeof data === 'string') {
|
if (data && typeof data === 'string') {
|
||||||
|
@ -13,7 +13,8 @@
|
|||||||
|
|
||||||
<div class="flex mt-2 -mx-1">
|
<div class="flex mt-2 -mx-1">
|
||||||
<div class="w-3/4 px-1">
|
<div class="w-3/4 px-1">
|
||||||
<ui-text-input-with-label v-model="details.author" label="Author" />
|
<!-- <ui-text-input-with-label v-model="details.authors" label="Author" /> -->
|
||||||
|
<p>Authors placeholder</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-1">
|
<div class="flex-grow px-1">
|
||||||
<ui-text-input-with-label v-model="details.publishYear" type="number" label="Publish Year" />
|
<ui-text-input-with-label v-model="details.publishYear" type="number" label="Publish Year" />
|
||||||
@ -22,10 +23,11 @@
|
|||||||
|
|
||||||
<div class="flex mt-2 -mx-1">
|
<div class="flex mt-2 -mx-1">
|
||||||
<div class="w-3/4 px-1">
|
<div class="w-3/4 px-1">
|
||||||
<ui-input-dropdown ref="seriesDropdown" v-model="details.series" label="Series" :items="series" />
|
<p>Series placeholder</p>
|
||||||
|
<!-- <ui-input-dropdown ref="seriesDropdown" v-model="details.series" label="Series" :items="series" /> -->
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-1">
|
<div class="flex-grow px-1">
|
||||||
<ui-text-input-with-label v-model="details.volumeNumber" label="Volume #" />
|
<!-- <ui-text-input-with-label v-model="details.volumeNumber" label="Volume #" /> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -64,7 +66,7 @@
|
|||||||
|
|
||||||
<div class="absolute bottom-0 left-0 w-full py-4 bg-bg" :class="isScrollable ? 'box-shadow-md-up' : 'box-shadow-sm-up border-t border-primary border-opacity-50'">
|
<div class="absolute bottom-0 left-0 w-full py-4 bg-bg" :class="isScrollable ? 'box-shadow-md-up' : 'box-shadow-sm-up border-t border-primary border-opacity-50'">
|
||||||
<div class="flex items-center px-4">
|
<div class="flex items-center px-4">
|
||||||
<ui-btn v-if="userCanDelete" color="error" type="button" class="h-8" :padding-x="3" small @click.stop.prevent="deleteAudiobook">Remove</ui-btn>
|
<ui-btn v-if="userCanDelete" color="error" type="button" class="h-8" :padding-x="3" small @click.stop.prevent="removeItem">Remove</ui-btn>
|
||||||
|
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|
||||||
@ -91,7 +93,7 @@
|
|||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
processing: Boolean,
|
processing: Boolean,
|
||||||
audiobook: {
|
libraryItem: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
}
|
}
|
||||||
@ -105,7 +107,6 @@ export default {
|
|||||||
author: null,
|
author: null,
|
||||||
narrator: null,
|
narrator: null,
|
||||||
series: null,
|
series: null,
|
||||||
volumeNumber: null,
|
|
||||||
publishYear: null,
|
publishYear: null,
|
||||||
publisher: null,
|
publisher: null,
|
||||||
language: null,
|
language: null,
|
||||||
@ -122,7 +123,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
audiobook: {
|
libraryItem: {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
handler(newVal) {
|
handler(newVal) {
|
||||||
if (newVal) this.init()
|
if (newVal) this.init()
|
||||||
@ -142,13 +143,16 @@ export default {
|
|||||||
return this.$store.getters['user/getIsRoot']
|
return this.$store.getters['user/getIsRoot']
|
||||||
},
|
},
|
||||||
isMissing() {
|
isMissing() {
|
||||||
return !!this.audiobook && !!this.audiobook.isMissing
|
return !!this.libraryItem && !!this.libraryItem.isMissing
|
||||||
},
|
},
|
||||||
audiobookId() {
|
libraryItemId() {
|
||||||
return this.audiobook ? this.audiobook.id : null
|
return this.libraryItem ? this.libraryItem.id : null
|
||||||
},
|
},
|
||||||
book() {
|
media() {
|
||||||
return this.audiobook ? this.audiobook.book || {} : {}
|
return this.libraryItem ? this.libraryItem.media || {} : {}
|
||||||
|
},
|
||||||
|
mediaMetadata() {
|
||||||
|
return this.media.metadata || {}
|
||||||
},
|
},
|
||||||
userCanDelete() {
|
userCanDelete() {
|
||||||
return this.$store.getters['user/getUserCanDelete']
|
return this.$store.getters['user/getUserCanDelete']
|
||||||
@ -166,7 +170,7 @@ export default {
|
|||||||
return this.$store.state.libraries.filterData || {}
|
return this.$store.state.libraries.filterData || {}
|
||||||
},
|
},
|
||||||
libraryId() {
|
libraryId() {
|
||||||
return this.audiobook ? this.audiobook.libraryId : null
|
return this.libraryItem ? this.libraryItem.libraryId : null
|
||||||
},
|
},
|
||||||
libraryProvider() {
|
libraryProvider() {
|
||||||
return this.$store.getters['libraries/getLibraryProvider'](this.libraryId) || 'google'
|
return this.$store.getters['libraries/getLibraryProvider'](this.libraryId) || 'google'
|
||||||
@ -185,13 +189,13 @@ export default {
|
|||||||
author: this.details.author !== this.book.author ? this.details.author : null
|
author: this.details.author !== this.book.author ? this.details.author : null
|
||||||
}
|
}
|
||||||
this.$axios
|
this.$axios
|
||||||
.$post(`/api/books/${this.audiobookId}/match`, matchOptions)
|
.$post(`/api/books/${this.libraryItemId}/match`, matchOptions)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
this.quickMatching = false
|
this.quickMatching = false
|
||||||
if (res.warning) {
|
if (res.warning) {
|
||||||
this.$toast.warning(res.warning)
|
this.$toast.warning(res.warning)
|
||||||
} else if (res.updated) {
|
} else if (res.updated) {
|
||||||
this.$toast.success('Audiobook details updated')
|
this.$toast.success('Item details updated')
|
||||||
} else {
|
} else {
|
||||||
this.$toast.info('No updates were made')
|
this.$toast.info('No updates were made')
|
||||||
}
|
}
|
||||||
@ -236,7 +240,7 @@ export default {
|
|||||||
saveMetadata() {
|
saveMetadata() {
|
||||||
this.savingMetadata = true
|
this.savingMetadata = true
|
||||||
this.$root.socket.once('save_metadata_complete', this.saveMetadataComplete)
|
this.$root.socket.once('save_metadata_complete', this.saveMetadataComplete)
|
||||||
this.$root.socket.emit('save_metadata', this.audiobookId)
|
this.$root.socket.emit('save_metadata', this.libraryItemId)
|
||||||
},
|
},
|
||||||
submitForm() {
|
submitForm() {
|
||||||
if (this.isProcessing) {
|
if (this.isProcessing) {
|
||||||
@ -260,7 +264,7 @@ export default {
|
|||||||
tags: this.newTags
|
tags: this.newTags
|
||||||
}
|
}
|
||||||
|
|
||||||
var updatedAudiobook = await this.$axios.$patch(`/api/books/${this.audiobook.id}`, updatePayload).catch((error) => {
|
var updatedAudiobook = await this.$axios.$patch(`/api/books/${this.libraryItemId}`, updatePayload).catch((error) => {
|
||||||
console.error('Failed to update', error)
|
console.error('Failed to update', error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
@ -271,35 +275,34 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
init() {
|
init() {
|
||||||
this.details.title = this.book.title
|
this.details.title = this.mediaMetadata.title
|
||||||
this.details.subtitle = this.book.subtitle
|
this.details.subtitle = this.mediaMetadata.subtitle
|
||||||
this.details.description = this.book.description
|
this.details.description = this.mediaMetadata.description
|
||||||
this.details.author = this.book.author
|
this.details.authors = this.mediaMetadata.authors
|
||||||
this.details.narrator = this.book.narrator
|
this.details.narrator = this.mediaMetadata.narrator
|
||||||
this.details.genres = this.book.genres || []
|
this.details.genres = this.mediaMetadata.genres || []
|
||||||
this.details.series = this.book.series
|
this.details.series = this.mediaMetadata.series
|
||||||
this.details.volumeNumber = this.book.volumeNumber
|
this.details.publishYear = this.mediaMetadata.publishYear
|
||||||
this.details.publishYear = this.book.publishYear
|
this.details.publisher = this.mediaMetadata.publisher || null
|
||||||
this.details.publisher = this.book.publisher || null
|
this.details.language = this.mediaMetadata.language || null
|
||||||
this.details.language = this.book.language || null
|
this.details.isbn = this.mediaMetadata.isbn || null
|
||||||
this.details.isbn = this.book.isbn || null
|
this.details.asin = this.mediaMetadata.asin || null
|
||||||
this.details.asin = this.book.asin || null
|
|
||||||
|
|
||||||
this.newTags = this.audiobook.tags || []
|
this.newTags = this.media.tags || []
|
||||||
},
|
},
|
||||||
deleteAudiobook() {
|
removeItem() {
|
||||||
if (confirm(`Are you sure you want to remove this audiobook?\n\n*Does not delete your files, only removes the audiobook from AudioBookshelf`)) {
|
if (confirm(`Are you sure you want to remove this item?\n\n*Does not delete your files, only removes the item from audiobookshelf`)) {
|
||||||
this.isProcessing = true
|
this.isProcessing = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$delete(`/api/books/${this.audiobookId}`)
|
.$delete(`/api/books/${this.libraryItemId}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log('Audiobook removed')
|
console.log('Item removed')
|
||||||
this.$toast.success('Audiobook Removed')
|
this.$toast.success('Item Removed')
|
||||||
this.$emit('close')
|
this.$emit('close')
|
||||||
this.isProcessing = false
|
this.isProcessing = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Remove Audiobook failed', error)
|
console.error('Remove item failed', error)
|
||||||
this.isProcessing = false
|
this.isProcessing = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -65,7 +65,7 @@
|
|||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
processing: Boolean,
|
processing: Boolean,
|
||||||
audiobook: {
|
libraryItem: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
}
|
}
|
||||||
@ -86,14 +86,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
audiobookId() {
|
libraryItemId() {
|
||||||
return this.audiobook ? this.audiobook.id : null
|
return this.libraryItem ? this.libraryItem.id : null
|
||||||
},
|
},
|
||||||
_audiobook() {
|
media() {
|
||||||
return this.audiobook || {}
|
return this.libraryItem ? this.libraryItem.media || {} : {}
|
||||||
},
|
},
|
||||||
downloads() {
|
downloads() {
|
||||||
return this.$store.getters['downloads/getDownloads'](this.audiobookId)
|
return this.$store.getters['downloads/getDownloads'](this.libraryItemId)
|
||||||
},
|
},
|
||||||
singleAudioDownload() {
|
singleAudioDownload() {
|
||||||
return this.downloads.find((d) => d.type === 'singleAudio')
|
return this.downloads.find((d) => d.type === 'singleAudio')
|
||||||
@ -108,24 +108,21 @@ export default {
|
|||||||
return this.zipDownload ? this.zipDownload.status : false
|
return this.zipDownload ? this.zipDownload.status : false
|
||||||
},
|
},
|
||||||
isSingleTrack() {
|
isSingleTrack() {
|
||||||
if (!this.audiobook.tracks) return false
|
if (!this.libraryItem.tracks) return false
|
||||||
return this.audiobook.tracks.length === 1
|
return this.libraryItem.tracks.length === 1
|
||||||
},
|
},
|
||||||
singleTrackPath() {
|
singleTrackPath() {
|
||||||
if (!this.isSingleTrack) return null
|
if (!this.isSingleTrack) return null
|
||||||
return this.audiobook.tracks[0].path
|
return this.audiobook.tracks[0].path
|
||||||
},
|
},
|
||||||
audioFiles() {
|
libraryFiles() {
|
||||||
return this.audiobook ? this.audiobook.audioFiles || [] : []
|
return this.libraryItem.libraryFiles
|
||||||
},
|
|
||||||
otherFiles() {
|
|
||||||
return this.audiobook ? this.audiobook.otherFiles || [] : []
|
|
||||||
},
|
},
|
||||||
totalFiles() {
|
totalFiles() {
|
||||||
return this.audioFiles.length + this.otherFiles.length
|
return this.libraryFiles.length
|
||||||
},
|
},
|
||||||
showM4bDownload() {
|
showM4bDownload() {
|
||||||
return !this._audiobook.isMissing && !this._audiobook.isInvalid && this._audiobook.tracks.length
|
return !this.libraryItem.isMissing && this.media.tracks.length
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="showFullPath = !showFullPath">Full Path</ui-btn>
|
<ui-btn small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="showFullPath = !showFullPath">Full Path</ui-btn>
|
||||||
<nuxt-link v-if="userCanUpdate" :to="`/audiobook/${audiobook.id}/edit`">
|
<nuxt-link v-if="userCanUpdate" :to="`/item/${libraryItem.id}/edit`">
|
||||||
<ui-btn small color="primary">Manage Tracks</ui-btn>
|
<ui-btn small color="primary">Manage Tracks</ui-btn>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
@ -21,20 +21,20 @@
|
|||||||
<th class="text-left">Duration</th>
|
<th class="text-left">Duration</th>
|
||||||
<th v-if="showDownload" class="text-center">Download</th>
|
<th v-if="showDownload" class="text-center">Download</th>
|
||||||
</tr>
|
</tr>
|
||||||
<template v-for="track in tracksCleaned">
|
<template v-for="track in tracks">
|
||||||
<tr :key="track.index">
|
<tr :key="track.index">
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<p>{{ track.index }}</p>
|
<p>{{ track.index }}</p>
|
||||||
</td>
|
</td>
|
||||||
<td class="font-sans">{{ showFullPath ? track.fullPath : track.filename }}</td>
|
<td class="font-sans">{{ showFullPath ? track.path : track.filename }}</td>
|
||||||
<td class="font-mono">
|
<td class="font-mono">
|
||||||
{{ $bytesPretty(track.size) }}
|
{{ $bytesPretty(track.metadata.size) }}
|
||||||
</td>
|
</td>
|
||||||
<td class="font-mono">
|
<td class="font-mono">
|
||||||
{{ $secondsToTimestamp(track.duration) }}
|
{{ $secondsToTimestamp(track.duration) }}
|
||||||
</td>
|
</td>
|
||||||
<td v-if="showDownload" class="font-mono text-center">
|
<td v-if="showDownload" class="font-mono text-center">
|
||||||
<a :href="`/s/book/${audiobook.id}/${track.relativePath}?token=${userToken}`" download><span class="material-icons icon-text">download</span></a>
|
<a :href="`/s/item/${libraryItem.id}/${track.metadata.relPath}?token=${userToken}`" download><span class="material-icons icon-text">download</span></a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
@ -43,26 +43,26 @@
|
|||||||
<div v-else class="flex my-4 text-center justify-center text-xl">No Audio Tracks</div>
|
<div v-else class="flex my-4 text-center justify-center text-xl">No Audio Tracks</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<tables-all-files-table :audiobook="audiobook" />
|
<tables-library-files-table :files="libraryFiles" :library-item-id="libraryItem.id" :is-missing="isMissing" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
audiobook: {
|
libraryItem: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
tracks: null,
|
tracks: [],
|
||||||
showFullPath: false
|
showFullPath: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
audiobook: {
|
libraryItem: {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
handler(newVal) {
|
handler(newVal) {
|
||||||
if (newVal) this.init()
|
if (newVal) this.init()
|
||||||
@ -70,22 +70,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
audiobookPath() {
|
media() {
|
||||||
return this.audiobook.path
|
return this.libraryItem.media || {}
|
||||||
},
|
},
|
||||||
tracksCleaned() {
|
libraryFiles() {
|
||||||
return this.tracks.map((track) => {
|
return this.libraryItem.libraryFiles || []
|
||||||
var trackPath = track.path.replace(/\\/g, '/')
|
|
||||||
var audiobookPath = this.audiobookPath.replace(/\\/g, '/')
|
|
||||||
|
|
||||||
return {
|
|
||||||
...track,
|
|
||||||
relativePath: trackPath
|
|
||||||
.replace(audiobookPath + '/', '')
|
|
||||||
.replace(/%/g, '%25')
|
|
||||||
.replace(/#/g, '%23')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
userToken() {
|
userToken() {
|
||||||
return this.$store.getters['user/getToken']
|
return this.$store.getters['user/getToken']
|
||||||
@ -97,18 +86,18 @@ export default {
|
|||||||
return this.$store.getters['user/getUserCanDownload']
|
return this.$store.getters['user/getUserCanDownload']
|
||||||
},
|
},
|
||||||
isMissing() {
|
isMissing() {
|
||||||
return this.audiobook.isMissing
|
return this.libraryItem.isMissing
|
||||||
},
|
},
|
||||||
showDownload() {
|
showDownload() {
|
||||||
return this.userCanDownload && !this.isMissing
|
return this.userCanDownload && !this.isMissing
|
||||||
},
|
},
|
||||||
hasTracks() {
|
hasTracks() {
|
||||||
return this.audiobook.tracks.length
|
return this.tracks.length
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
init() {
|
init() {
|
||||||
this.tracks = this.audiobook.tracks
|
this.tracks = this.media.tracks || []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -94,14 +94,14 @@
|
|||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
processing: Boolean,
|
processing: Boolean,
|
||||||
audiobook: {
|
libraryItem: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
audiobookId: null,
|
libraryItemId: null,
|
||||||
searchTitle: null,
|
searchTitle: null,
|
||||||
searchAuthor: null,
|
searchAuthor: null,
|
||||||
lastSearch: null,
|
lastSearch: null,
|
||||||
@ -126,7 +126,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
audiobook: {
|
libraryItem: {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
handler(newVal) {
|
handler(newVal) {
|
||||||
if (newVal) this.init()
|
if (newVal) this.init()
|
||||||
@ -209,19 +209,19 @@ export default {
|
|||||||
isbn: true
|
isbn: true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.audiobook.id !== this.audiobookId) {
|
if (this.libraryItem.id !== this.libraryItemId) {
|
||||||
this.searchResults = []
|
this.searchResults = []
|
||||||
this.hasSearched = false
|
this.hasSearched = false
|
||||||
this.audiobookId = this.audiobook.id
|
this.libraryItemId = this.libraryItem.id
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.audiobook.book || !this.audiobook.book.title) {
|
if (!this.libraryItem.media || !this.libraryItem.media.metadata.title) {
|
||||||
this.searchTitle = null
|
this.searchTitle = null
|
||||||
this.searchAuthor = null
|
this.searchAuthor = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.searchTitle = this.audiobook.book.title
|
this.searchTitle = this.libraryItem.media.metadata.title
|
||||||
this.searchAuthor = this.audiobook.book.authorFL || ''
|
this.searchAuthor = this.libraryItem.media.metadata.authorName || ''
|
||||||
this.provider = localStorage.getItem('book-provider') || 'google'
|
this.provider = localStorage.getItem('book-provider') || 'google'
|
||||||
},
|
},
|
||||||
selectMatch(match) {
|
selectMatch(match) {
|
||||||
@ -247,7 +247,7 @@ export default {
|
|||||||
var coverPayload = {
|
var coverPayload = {
|
||||||
url: updatePayload.cover
|
url: updatePayload.cover
|
||||||
}
|
}
|
||||||
var success = await this.$axios.$post(`/api/books/${this.audiobook.id}/cover`, coverPayload).catch((error) => {
|
var success = await this.$axios.$post(`/api/books/${this.libraryItemId}/cover`, coverPayload).catch((error) => {
|
||||||
console.error('Failed to update', error)
|
console.error('Failed to update', error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
@ -264,7 +264,7 @@ export default {
|
|||||||
var bookUpdatePayload = {
|
var bookUpdatePayload = {
|
||||||
book: updatePayload
|
book: updatePayload
|
||||||
}
|
}
|
||||||
var success = await this.$axios.$patch(`/api/books/${this.audiobook.id}`, bookUpdatePayload).catch((error) => {
|
var success = await this.$axios.$patch(`/api/books/${this.libraryItemId}`, bookUpdatePayload).catch((error) => {
|
||||||
console.error('Failed to update', error)
|
console.error('Failed to update', error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
@ -1,110 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="w-full h-full overflow-y-auto overflow-x-hidden px-4 py-6">
|
|
||||||
<template v-if="hasTracks">
|
|
||||||
<div class="w-full bg-primary px-4 py-2 flex items-center">
|
|
||||||
<div class="h-7 w-7 rounded-full bg-white bg-opacity-10 flex items-center justify-center">
|
|
||||||
<span class="text-sm font-mono">{{ tracks.length }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex-grow" />
|
|
||||||
<ui-btn small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="showFullPath = !showFullPath">Full Path</ui-btn>
|
|
||||||
<nuxt-link v-if="userCanUpdate" :to="`/audiobook/${audiobook.id}/edit`" class="mr-4">
|
|
||||||
<ui-btn small color="primary">Manage Tracks</ui-btn>
|
|
||||||
</nuxt-link>
|
|
||||||
</div>
|
|
||||||
<table class="text-sm tracksTable">
|
|
||||||
<tr class="font-book">
|
|
||||||
<th>#</th>
|
|
||||||
<th class="text-left">Filename</th>
|
|
||||||
<th class="text-left">Size</th>
|
|
||||||
<th class="text-left">Duration</th>
|
|
||||||
<th v-if="showDownload" class="text-center">Download</th>
|
|
||||||
</tr>
|
|
||||||
<template v-for="track in tracksCleaned">
|
|
||||||
<tr :key="track.index">
|
|
||||||
<td class="text-center">
|
|
||||||
<p>{{ track.index }}</p>
|
|
||||||
</td>
|
|
||||||
<td class="font-sans">{{ showFullPath ? track.fullPath : track.filename }}</td>
|
|
||||||
<td class="font-mono">
|
|
||||||
{{ $bytesPretty(track.size) }}
|
|
||||||
</td>
|
|
||||||
<td class="font-mono">
|
|
||||||
{{ $secondsToTimestamp(track.duration) }}
|
|
||||||
</td>
|
|
||||||
<td v-if="showDownload" class="font-mono text-center">
|
|
||||||
<a :href="`/s/book/${audiobook.id}/${track.relativePath}?token=${userToken}`" download><span class="material-icons icon-text">download</span></a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
</table>
|
|
||||||
</template>
|
|
||||||
<div v-else class="flex my-4 text-center justify-center text-xl">No Audio Tracks</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
audiobook: {
|
|
||||||
type: Object,
|
|
||||||
default: () => {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
tracks: null,
|
|
||||||
showFullPath: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
audiobook: {
|
|
||||||
immediate: true,
|
|
||||||
handler(newVal) {
|
|
||||||
if (newVal) this.init()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
audiobookPath() {
|
|
||||||
return this.audiobook.path
|
|
||||||
},
|
|
||||||
tracksCleaned() {
|
|
||||||
return this.tracks.map((track) => {
|
|
||||||
var trackPath = track.path.replace(/\\/g, '/')
|
|
||||||
var audiobookPath = this.audiobookPath.replace(/\\/g, '/')
|
|
||||||
|
|
||||||
return {
|
|
||||||
...track,
|
|
||||||
relativePath: trackPath
|
|
||||||
.replace(audiobookPath + '/', '')
|
|
||||||
.replace(/%/g, '%25')
|
|
||||||
.replace(/#/g, '%23')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
userToken() {
|
|
||||||
return this.$store.getters['user/getToken']
|
|
||||||
},
|
|
||||||
userCanUpdate() {
|
|
||||||
return this.$store.getters['user/getUserCanUpdate']
|
|
||||||
},
|
|
||||||
userCanDownload() {
|
|
||||||
return this.$store.getters['user/getUserCanDownload']
|
|
||||||
},
|
|
||||||
isMissing() {
|
|
||||||
return this.audiobook.isMissing
|
|
||||||
},
|
|
||||||
showDownload() {
|
|
||||||
return this.userCanDownload && !this.isMissing
|
|
||||||
},
|
|
||||||
hasTracks() {
|
|
||||||
return this.audiobook.tracks.length
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
init() {
|
|
||||||
this.tracks = this.audiobook.tracks
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
@ -1,14 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full my-2">
|
<div class="w-full my-2">
|
||||||
<div class="w-full bg-primary px-4 md:px-6 py-2 flex items-center cursor-pointer" @click.stop="clickBar">
|
<div class="w-full bg-primary px-4 md:px-6 py-2 flex items-center cursor-pointer" @click.stop="clickBar">
|
||||||
<p class="pr-2 md:pr-4">Other Files</p>
|
<p class="pr-2 md:pr-4">Library Files</p>
|
||||||
<div class="h-5 md:h-7 w-5 md:w-7 rounded-full bg-white bg-opacity-10 flex items-center justify-center">
|
<div class="h-5 md:h-7 w-5 md:w-7 rounded-full bg-white bg-opacity-10 flex items-center justify-center">
|
||||||
<span class="text-sm font-mono">{{ files.length }}</span>
|
<span class="text-sm font-mono">{{ files.length }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<!-- <nuxt-link :to="`/audiobook/${audiobookId}/edit`" class="mr-4">
|
|
||||||
<ui-btn small color="primary">Manage Tracks</ui-btn>
|
|
||||||
</nuxt-link> -->
|
|
||||||
<ui-btn small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="showFullPath = !showFullPath">Full Path</ui-btn>
|
<ui-btn small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="showFullPath = !showFullPath">Full Path</ui-btn>
|
||||||
<div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showFiles ? 'transform rotate-180' : ''">
|
<div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showFiles ? 'transform rotate-180' : ''">
|
||||||
<span class="material-icons text-4xl">expand_more</span>
|
<span class="material-icons text-4xl">expand_more</span>
|
||||||
@ -22,19 +19,19 @@
|
|||||||
<th class="text-left px-4 w-24">Filetype</th>
|
<th class="text-left px-4 w-24">Filetype</th>
|
||||||
<th v-if="userCanDownload && !isMissing" class="text-center w-20">Download</th>
|
<th v-if="userCanDownload && !isMissing" class="text-center w-20">Download</th>
|
||||||
</tr>
|
</tr>
|
||||||
<template v-for="file in otherFilesCleaned">
|
<template v-for="file in files">
|
||||||
<tr :key="file.path">
|
<tr :key="file.path">
|
||||||
<td class="font-book pl-2">
|
<td class="font-book pl-2">
|
||||||
{{ showFullPath ? file.fullPath : file.path }}
|
{{ showFullPath ? file.metadata.path : file.metadata.relPath }}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-xs">
|
<td class="text-xs">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span v-if="file.filetype === 'ebook'" class="material-icons text-base mr-1 cursor-pointer text-white text-opacity-60 hover:text-opacity-100" @click="readEbookClick(file)">auto_stories </span>
|
<span v-if="file.filetype === 'ebook'" class="material-icons text-base mr-1 cursor-pointer text-white text-opacity-60 hover:text-opacity-100" @click="readEbookClick(file)">auto_stories </span>
|
||||||
<p>{{ file.filetype }}</p>
|
<p>{{ file.metadata.ext }}</p>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td v-if="userCanDownload && !isMissing" class="text-center">
|
<td v-if="userCanDownload && !isMissing" class="text-center">
|
||||||
<a :href="`/s/book/${audiobookId}/${file.relativePath}?token=${userToken}`" download><span class="material-icons icon-text">download</span></a>
|
<a :href="`/s/item/${libraryItemId}${$encodeUriPath(file.metadata.relPath)}?token=${userToken}`" download><span class="material-icons icon-text">download</span></a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
@ -51,10 +48,8 @@ export default {
|
|||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
audiobook: {
|
libraryItemId: String,
|
||||||
type: Object,
|
isMissing: Boolean
|
||||||
default: () => null
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -63,40 +58,19 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
audiobookId() {
|
|
||||||
return this.audiobook.id
|
|
||||||
},
|
|
||||||
audiobookPath() {
|
|
||||||
return this.audiobook.path
|
|
||||||
},
|
|
||||||
otherFilesCleaned() {
|
|
||||||
return this.files.map((file) => {
|
|
||||||
return {
|
|
||||||
...file,
|
|
||||||
relativePath: this.getRelativePath(file.path)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
userToken() {
|
userToken() {
|
||||||
return this.$store.getters['user/getToken']
|
return this.$store.getters['user/getToken']
|
||||||
},
|
},
|
||||||
isMissing() {
|
|
||||||
return this.audiobook.isMissing
|
|
||||||
},
|
|
||||||
userCanDownload() {
|
userCanDownload() {
|
||||||
return this.$store.getters['user/getUserCanDownload']
|
return this.$store.getters['user/getUserCanDownload']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
readEbookClick(file) {
|
readEbookClick(file) {
|
||||||
this.$store.commit('showEReaderForFile', { audiobook: this.audiobook, file })
|
// this.$store.commit('showEReaderForFile', { audiobook: this.audiobook, file })
|
||||||
},
|
},
|
||||||
clickBar() {
|
clickBar() {
|
||||||
this.showFiles = !this.showFiles
|
this.showFiles = !this.showFiles
|
||||||
},
|
|
||||||
getRelativePath(path) {
|
|
||||||
var relativePath = path.replace(/\\/g, '/').replace(this.audiobookPath.replace(/\\/g, '/') + '/', '')
|
|
||||||
return this.$encodeUriPath(relativePath)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
@ -8,7 +8,7 @@
|
|||||||
<!-- <span class="bg-black-400 rounded-xl py-1 px-2 text-sm font-mono">{{ tracks.length }}</span> -->
|
<!-- <span class="bg-black-400 rounded-xl py-1 px-2 text-sm font-mono">{{ tracks.length }}</span> -->
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="showFullPath = !showFullPath">Full Path</ui-btn>
|
<ui-btn small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="showFullPath = !showFullPath">Full Path</ui-btn>
|
||||||
<nuxt-link v-if="userCanUpdate" :to="`/audiobook/${audiobookId}/edit`" class="mr-2 md:mr-4">
|
<nuxt-link v-if="userCanUpdate" :to="`/audiobook/${libraryItemId}/edit`" class="mr-2 md:mr-4">
|
||||||
<ui-btn small color="primary">Manage Tracks</ui-btn>
|
<ui-btn small color="primary">Manage Tracks</ui-btn>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showTracks ? 'transform rotate-180' : ''">
|
<div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showTracks ? 'transform rotate-180' : ''">
|
||||||
@ -25,20 +25,20 @@
|
|||||||
<th class="text-left w-20">Duration</th>
|
<th class="text-left w-20">Duration</th>
|
||||||
<th v-if="userCanDownload" class="text-center w-20">Download</th>
|
<th v-if="userCanDownload" class="text-center w-20">Download</th>
|
||||||
</tr>
|
</tr>
|
||||||
<template v-for="track in tracksCleaned">
|
<template v-for="track in tracks">
|
||||||
<tr :key="track.index">
|
<tr :key="track.index">
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<p>{{ track.index }}</p>
|
<p>{{ track.index }}</p>
|
||||||
</td>
|
</td>
|
||||||
<td class="font-sans">{{ showFullPath ? track.fullPath : track.filename }}</td>
|
<td class="font-sans">{{ showFullPath ? track.metadata.path : track.metadata.filename }}</td>
|
||||||
<td class="font-mono">
|
<td class="font-mono">
|
||||||
{{ $bytesPretty(track.size) }}
|
{{ $bytesPretty(track.metadata.size) }}
|
||||||
</td>
|
</td>
|
||||||
<td class="font-mono">
|
<td class="font-mono">
|
||||||
{{ $secondsToTimestamp(track.duration) }}
|
{{ $secondsToTimestamp(track.duration) }}
|
||||||
</td>
|
</td>
|
||||||
<td v-if="userCanDownload" class="text-center">
|
<td v-if="userCanDownload" class="text-center">
|
||||||
<a :href="`/s/book/${audiobook.id}/${track.relativePath}?token=${userToken}`" download><span class="material-icons icon-text">download</span></a>
|
<a :href="`/s/item/${libraryItemId}${$encodeUriPath(track.metadata.relPath)}?token=${userToken}`" download><span class="material-icons icon-text">download</span></a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
@ -55,10 +55,7 @@ export default {
|
|||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
audiobook: {
|
libraryItemId: String
|
||||||
type: Object,
|
|
||||||
default: () => null
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -67,20 +64,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
audiobookId() {
|
|
||||||
return this.audiobook.id
|
|
||||||
},
|
|
||||||
audiobookPath() {
|
|
||||||
return this.audiobook.path
|
|
||||||
},
|
|
||||||
tracksCleaned() {
|
|
||||||
return this.tracks.map((track) => {
|
|
||||||
return {
|
|
||||||
...track,
|
|
||||||
relativePath: this.getRelativePath(track.path)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
userToken() {
|
userToken() {
|
||||||
return this.$store.getters['user/getToken']
|
return this.$store.getters['user/getToken']
|
||||||
},
|
},
|
||||||
@ -94,10 +77,6 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
clickBar() {
|
clickBar() {
|
||||||
this.showTracks = !this.showTracks
|
this.showTracks = !this.showTracks
|
||||||
},
|
|
||||||
getRelativePath(path) {
|
|
||||||
var relativePath = path.replace(/\\/g, '/').replace(this.audiobookPath.replace(/\\/g, '/') + '/', '')
|
|
||||||
return this.$encodeUriPath(relativePath)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
|
@ -74,7 +74,6 @@ module.exports = {
|
|||||||
|
|
||||||
proxy: {
|
proxy: {
|
||||||
'/dev/': { target: 'http://localhost:3333', pathRewrite: { '^/dev/': '' } },
|
'/dev/': { target: 'http://localhost:3333', pathRewrite: { '^/dev/': '' } },
|
||||||
'/local/': { target: process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '/' },
|
|
||||||
'/lib/': { target: process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '/' },
|
'/lib/': { target: process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '/' },
|
||||||
'/ebook/': { target: process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '/' },
|
'/ebook/': { target: process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '/' },
|
||||||
'/s/': { target: process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '/' },
|
'/s/': { target: process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '/' },
|
||||||
|
@ -164,7 +164,7 @@
|
|||||||
|
|
||||||
<tables-audio-files-table v-if="otherAudioFiles.length" :audiobook-id="audiobook.id" :files="otherAudioFiles" class="mt-6" />
|
<tables-audio-files-table v-if="otherAudioFiles.length" :audiobook-id="audiobook.id" :files="otherAudioFiles" class="mt-6" />
|
||||||
|
|
||||||
<tables-other-files-table v-if="otherFiles.length" :audiobook="audiobook" :files="otherFiles" class="mt-6" />
|
<tables-library-files-table v-if="otherFiles.length" :audiobook="audiobook" :files="otherFiles" class="mt-6" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
475
client/pages/item/_id/index.vue
Normal file
475
client/pages/item/_id/index.vue
Normal file
@ -0,0 +1,475 @@
|
|||||||
|
<template>
|
||||||
|
<div id="page-wrapper" class="bg-bg page overflow-hidden" :class="streamAudiobook ? 'streaming' : ''">
|
||||||
|
<div class="w-full h-full overflow-y-auto px-2 py-6 md:p-8">
|
||||||
|
<div class="flex flex-col md:flex-row max-w-6xl mx-auto">
|
||||||
|
<div class="w-full flex justify-center md:block md:w-52" style="min-width: 208px">
|
||||||
|
<div class="relative" style="height: fit-content">
|
||||||
|
<covers-book-cover :library-item="libraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
|
|
||||||
|
<!-- Book Progress Bar -->
|
||||||
|
<div class="absolute bottom-0 left-0 h-1.5 bg-yellow-400 shadow-sm z-10" :class="userIsRead ? 'bg-success' : 'bg-yellow-400'" :style="{ width: 208 * progressPercent + 'px' }"></div>
|
||||||
|
|
||||||
|
<!-- Book Cover Overlay -->
|
||||||
|
<div class="absolute top-0 left-0 w-full h-full z-10 bg-black bg-opacity-30 opacity-0 hover:opacity-100 transition-opacity" @mousedown.prevent @mouseup.prevent>
|
||||||
|
<div v-show="showPlayButton && !streaming" class="h-full flex items-center justify-center pointer-events-none">
|
||||||
|
<div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto cursor-pointer" @click.stop.prevent="startStream">
|
||||||
|
<span class="material-icons text-4xl">play_circle_filled</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="absolute bottom-2.5 right-2.5 z-10 material-icons text-lg cursor-pointer text-white text-opacity-75 hover:text-opacity-100 hover:scale-110 transform duration-200" @click="showEditCover">edit</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow px-2 py-6 md:py-0 md:px-10">
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="flex sm:items-end flex-col sm:flex-row">
|
||||||
|
<h1 class="text-2xl md:text-3xl font-sans">
|
||||||
|
{{ title }}
|
||||||
|
</h1>
|
||||||
|
<p v-if="subtitle" class="sm:ml-4 text-gray-400 text-xl md:text-2xl">{{ subtitle }}</p>
|
||||||
|
</div>
|
||||||
|
<!-- <p v-if="subtitle" class="ml-4 text-gray-400 text-2xl block sm:hidden">{{ subtitle }}</p> -->
|
||||||
|
|
||||||
|
<p v-if="authorsList.length" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl">
|
||||||
|
by <nuxt-link v-for="(author, index) in authorsList" :key="index" :to="`/library/${libraryId}/bookshelf?filter=authors.${$encode(author)}`" class="hover:underline">{{ author }}<span v-if="index < authorsList.length - 1">, </span></nuxt-link>
|
||||||
|
</p>
|
||||||
|
<p v-else class="mb-2 mt-0.5 text-gray-200 text-xl">by Unknown</p>
|
||||||
|
|
||||||
|
<nuxt-link v-for="_series in seriesList" :key="_series.id" :to="`/library/${libraryId}/series/${_series.id}}`" class="hover:underline font-sans text-gray-300 text-lg leading-7"> {{ _series.text }}</nuxt-link>
|
||||||
|
|
||||||
|
<div v-if="narrator" class="flex py-0.5 mt-4">
|
||||||
|
<div class="w-32">
|
||||||
|
<span class="text-white text-opacity-60 uppercase text-sm">Narrated By</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<template v-for="(narrator, index) in narrators">
|
||||||
|
<nuxt-link :key="narrator" :to="`/library/${libraryId}/bookshelf?filter=narrators.${$encode(narrator)}`" class="hover:underline">{{ narrator }}</nuxt-link
|
||||||
|
><span :key="index" v-if="index < narrators.length - 1">, </span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="publishYear" class="flex py-0.5">
|
||||||
|
<div class="w-32">
|
||||||
|
<span class="text-white text-opacity-60 uppercase text-sm">Publish Year</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ publishYear }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex py-0.5" v-if="genres.length">
|
||||||
|
<div class="w-32">
|
||||||
|
<span class="text-white text-opacity-60 uppercase text-sm">Genres</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<template v-for="(genre, index) in genres">
|
||||||
|
<nuxt-link :key="genre" :to="`/library/${libraryId}/bookshelf?filter=genres.${$encode(genre)}`" class="hover:underline">{{ genre }}</nuxt-link
|
||||||
|
><span :key="index" v-if="index < genres.length - 1">, </span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="tracks.length" class="flex py-0.5">
|
||||||
|
<div class="w-32">
|
||||||
|
<span class="text-white text-opacity-60 uppercase text-sm">Duration</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ durationPretty }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="tracks.length" class="flex py-0.5">
|
||||||
|
<div class="w-32">
|
||||||
|
<span class="text-white text-opacity-60 uppercase text-sm">Size</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ sizePretty }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hidden md:block flex-grow" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Alerts -->
|
||||||
|
<div v-show="showExperimentalReadAlert" class="bg-error p-4 rounded-xl flex items-center">
|
||||||
|
<span class="material-icons text-2xl">warning_amber</span>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- Progress -->
|
||||||
|
<div v-if="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-else class="text-xs">Finished {{ $formatDate(userProgressFinishedAt, 'MM/dd/yyyy') }}</p>
|
||||||
|
<p v-if="progressPercent < 1" class="text-gray-200 text-xs">{{ $elapsedPretty(userTimeRemaining) }} remaining</p>
|
||||||
|
<p class="text-gray-400 text-xs pt-1">Started {{ $formatDate(userProgressStartedAt, 'MM/dd/yyyy') }}</p>
|
||||||
|
|
||||||
|
<div v-if="!resettingProgress" class="absolute -top-1.5 -right-1.5 p-1 w-5 h-5 rounded-full bg-bg hover:bg-error border border-primary flex items-center justify-center cursor-pointer" @click.stop="clearProgressClick">
|
||||||
|
<span class="material-icons text-sm">close</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-center md:justify-start pt-4">
|
||||||
|
<ui-btn v-if="showPlayButton" :disabled="streaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="startStream">
|
||||||
|
<span v-show="!streaming" class="material-icons -ml-2 pr-1 text-white">play_arrow</span>
|
||||||
|
{{ streaming ? 'Streaming' : 'Play' }}
|
||||||
|
</ui-btn>
|
||||||
|
<ui-btn v-else-if="isMissing || isInvalid" color="error" :padding-x="4" small class="flex items-center h-9 mr-2">
|
||||||
|
<span v-show="!streaming" class="material-icons -ml-2 pr-1 text-white">error</span>
|
||||||
|
{{ isMissing ? 'Missing' : 'Incomplete' }}
|
||||||
|
</ui-btn>
|
||||||
|
|
||||||
|
<ui-btn v-if="showExperimentalFeatures && numEbooks" color="info" :padding-x="4" small class="flex items-center h-9 mr-2" @click="openEbook">
|
||||||
|
<span class="material-icons -ml-2 pr-2 text-white">auto_stories</span>
|
||||||
|
Read
|
||||||
|
</ui-btn>
|
||||||
|
|
||||||
|
<ui-tooltip v-if="userCanUpdate" text="Edit" direction="top">
|
||||||
|
<ui-icon-btn icon="edit" class="mx-0.5" @click="editClick" />
|
||||||
|
</ui-tooltip>
|
||||||
|
|
||||||
|
<ui-tooltip v-if="userCanDownload" :disabled="isMissing" text="Download" direction="top">
|
||||||
|
<ui-icon-btn icon="download" :disabled="isMissing" class="mx-0.5" @click="downloadClick" />
|
||||||
|
</ui-tooltip>
|
||||||
|
|
||||||
|
<ui-tooltip :text="isRead ? 'Mark as Not Read' : 'Mark as Read'" direction="top">
|
||||||
|
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="isRead" class="mx-0.5" @click="toggleRead" />
|
||||||
|
</ui-tooltip>
|
||||||
|
|
||||||
|
<ui-tooltip text="Collections" direction="top">
|
||||||
|
<ui-icon-btn icon="collections_bookmark" class="mx-0.5" outlined @click="collectionsClick" />
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4 max-w-2xl">
|
||||||
|
<p class="text-base text-gray-100 whitespace-pre-line">{{ description }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="missingParts.length" class="bg-error border-red-800 shadow-md p-4">
|
||||||
|
<p class="text-sm mb-2">
|
||||||
|
Missing Parts <span class="text-sm">({{ missingParts.length }})</span>
|
||||||
|
</p>
|
||||||
|
<p class="text-sm font-mono">{{ missingPartChunks.join(', ') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="invalidParts.length" class="bg-error border-red-800 shadow-md p-4">
|
||||||
|
<p class="text-sm mb-2">
|
||||||
|
Invalid Parts <span class="text-sm">({{ invalidParts.length }})</span>
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<p v-for="part in invalidParts" :key="part.filename" class="text-sm font-mono">{{ part.filename }}: {{ part.error }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<tables-tracks-table v-if="tracks.length" :tracks="tracks" :library-item-id="libraryItemId" class="mt-6" />
|
||||||
|
|
||||||
|
<!-- <tables-audio-files-table v-if="otherAudioFiles.length" :library-item-id="libraryItemId" :files="otherAudioFiles" class="mt-6" /> -->
|
||||||
|
|
||||||
|
<tables-library-files-table v-if="libraryFiles.length" :is-missing="isMissing" :library-item-id="libraryItemId" :files="libraryFiles" class="mt-6" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
async asyncData({ store, params, app, redirect, route }) {
|
||||||
|
if (!store.state.user.user) {
|
||||||
|
return redirect(`/login?redirect=${route.path}`)
|
||||||
|
}
|
||||||
|
var item = await app.$axios.$get(`/api/items/${params.id}?expanded=1`).catch((error) => {
|
||||||
|
console.error('Failed', error)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
if (!item) {
|
||||||
|
console.error('No item...', params.id)
|
||||||
|
return redirect('/')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
libraryItem: item
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isRead: false,
|
||||||
|
resettingProgress: false,
|
||||||
|
isProcessingReadUpdate: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
userIsRead: {
|
||||||
|
immediate: true,
|
||||||
|
handler(newVal) {
|
||||||
|
this.isRead = newVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
coverAspectRatio() {
|
||||||
|
return this.$store.getters['getServerSetting']('coverAspectRatio')
|
||||||
|
},
|
||||||
|
bookCoverAspectRatio() {
|
||||||
|
return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE ? 1 : 1.6
|
||||||
|
},
|
||||||
|
bookCoverWidth() {
|
||||||
|
return 208
|
||||||
|
},
|
||||||
|
isDeveloperMode() {
|
||||||
|
return this.$store.state.developerMode
|
||||||
|
},
|
||||||
|
showExperimentalFeatures() {
|
||||||
|
return this.$store.state.showExperimentalFeatures
|
||||||
|
},
|
||||||
|
missingPartChunks() {
|
||||||
|
if (this.missingParts === 1) return this.missingParts[0]
|
||||||
|
var chunks = []
|
||||||
|
|
||||||
|
var currentIndex = this.missingParts[0]
|
||||||
|
var currentChunk = [this.missingParts[0]]
|
||||||
|
|
||||||
|
for (let i = 1; i < this.missingParts.length; i++) {
|
||||||
|
var partIndex = this.missingParts[i]
|
||||||
|
if (currentIndex === partIndex - 1) {
|
||||||
|
currentChunk.push(partIndex)
|
||||||
|
currentIndex = partIndex
|
||||||
|
} else {
|
||||||
|
// console.log('Chunk ended', currentChunk.join(', '), currentIndex, partIndex)
|
||||||
|
if (currentChunk.length === 0) {
|
||||||
|
console.error('How is current chunk 0?', currentChunk.join(', '))
|
||||||
|
}
|
||||||
|
chunks.push(currentChunk)
|
||||||
|
currentChunk = [partIndex]
|
||||||
|
currentIndex = partIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentChunk.length) {
|
||||||
|
chunks.push(currentChunk)
|
||||||
|
}
|
||||||
|
chunks = chunks.map((chunk) => {
|
||||||
|
if (chunk.length === 1) return chunk[0]
|
||||||
|
else return `${chunk[0]}-${chunk[chunk.length - 1]}`
|
||||||
|
})
|
||||||
|
return chunks
|
||||||
|
},
|
||||||
|
isMissing() {
|
||||||
|
return this.libraryItem.isMissing
|
||||||
|
},
|
||||||
|
isInvalid() {
|
||||||
|
return this.libraryItem.isInvalid
|
||||||
|
},
|
||||||
|
showPlayButton() {
|
||||||
|
return !this.isMissing && !this.isInvalid && this.tracks.length
|
||||||
|
},
|
||||||
|
missingParts() {
|
||||||
|
return this.libraryItem.missingParts || []
|
||||||
|
},
|
||||||
|
invalidParts() {
|
||||||
|
return this.libraryItem.invalidParts || []
|
||||||
|
},
|
||||||
|
libraryId() {
|
||||||
|
return this.libraryItem.libraryId
|
||||||
|
},
|
||||||
|
folderId() {
|
||||||
|
return this.libraryItem.folderId
|
||||||
|
},
|
||||||
|
libraryItemId() {
|
||||||
|
return this.libraryItem.id
|
||||||
|
},
|
||||||
|
media() {
|
||||||
|
return this.libraryItem.media || {}
|
||||||
|
},
|
||||||
|
mediaMetadata() {
|
||||||
|
return this.media.metadata || {}
|
||||||
|
},
|
||||||
|
title() {
|
||||||
|
return this.mediaMetadata.title || 'No Title'
|
||||||
|
},
|
||||||
|
publishYear() {
|
||||||
|
return this.mediaMetadata.publishYear
|
||||||
|
},
|
||||||
|
narrator() {
|
||||||
|
return this.mediaMetadata.narratorName
|
||||||
|
},
|
||||||
|
subtitle() {
|
||||||
|
return this.mediaMetadata.subtitle
|
||||||
|
},
|
||||||
|
genres() {
|
||||||
|
return this.mediaMetadata.genres || []
|
||||||
|
},
|
||||||
|
authors() {
|
||||||
|
return this.mediaMetadata.authors || []
|
||||||
|
},
|
||||||
|
authorsList() {
|
||||||
|
return this.authors.map((au) => au.name)
|
||||||
|
},
|
||||||
|
narrators() {
|
||||||
|
return this.mediaMetadata.narrators || []
|
||||||
|
},
|
||||||
|
series() {
|
||||||
|
return this.media.series || []
|
||||||
|
},
|
||||||
|
seriesList() {
|
||||||
|
return this.series.map((se) => {
|
||||||
|
var text = se.name
|
||||||
|
if (se.sequence) text += ` #${se.sequence}`
|
||||||
|
return {
|
||||||
|
...se,
|
||||||
|
text
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
durationPretty() {
|
||||||
|
return this.$elapsedPretty(this.media.duration)
|
||||||
|
},
|
||||||
|
duration() {
|
||||||
|
return this.media.duration
|
||||||
|
},
|
||||||
|
sizePretty() {
|
||||||
|
return this.$bytesPretty(this.media.size)
|
||||||
|
},
|
||||||
|
libraryFiles() {
|
||||||
|
return this.libraryItem.libraryFiles || []
|
||||||
|
},
|
||||||
|
otherAudioFiles() {
|
||||||
|
return this.audioFiles.filter((af) => {
|
||||||
|
return !this.tracks.find((t) => t.path === af.path)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
tracks() {
|
||||||
|
return this.media.tracks || []
|
||||||
|
},
|
||||||
|
audioFiles() {
|
||||||
|
return this.media.audioFiles || []
|
||||||
|
},
|
||||||
|
ebooks() {
|
||||||
|
return this.media.ebookFiles
|
||||||
|
},
|
||||||
|
showExperimentalReadAlert() {
|
||||||
|
return !this.tracks.length && this.ebooks.length && !this.showExperimentalFeatures
|
||||||
|
},
|
||||||
|
numEbooks() {
|
||||||
|
return this.media.numEbooks
|
||||||
|
},
|
||||||
|
description() {
|
||||||
|
return this.mediaMetadata.description || ''
|
||||||
|
},
|
||||||
|
userAudiobooks() {
|
||||||
|
return this.$store.state.user.user ? this.$store.state.user.user.audiobooks || {} : {}
|
||||||
|
},
|
||||||
|
userAudiobook() {
|
||||||
|
return this.userAudiobooks[this.libraryItemId] || null
|
||||||
|
},
|
||||||
|
userCurrentTime() {
|
||||||
|
return this.userAudiobook ? this.userAudiobook.currentTime : 0
|
||||||
|
},
|
||||||
|
userIsRead() {
|
||||||
|
return this.userAudiobook ? !!this.userAudiobook.isRead : false
|
||||||
|
},
|
||||||
|
userTimeRemaining() {
|
||||||
|
return this.duration - this.userCurrentTime
|
||||||
|
},
|
||||||
|
progressPercent() {
|
||||||
|
return this.userAudiobook ? Math.max(Math.min(1, this.userAudiobook.progress), 0) : 0
|
||||||
|
},
|
||||||
|
userProgressStartedAt() {
|
||||||
|
return this.userAudiobook ? this.userAudiobook.startedAt : 0
|
||||||
|
},
|
||||||
|
userProgressFinishedAt() {
|
||||||
|
return this.userAudiobook ? this.userAudiobook.finishedAt : 0
|
||||||
|
},
|
||||||
|
streamAudiobook() {
|
||||||
|
return this.$store.state.streamAudiobook
|
||||||
|
},
|
||||||
|
streaming() {
|
||||||
|
return this.streamAudiobook && this.streamAudiobook.id === this.libraryItemId
|
||||||
|
},
|
||||||
|
userCanUpdate() {
|
||||||
|
return this.$store.getters['user/getUserCanUpdate']
|
||||||
|
},
|
||||||
|
userCanDelete() {
|
||||||
|
return this.$store.getters['user/getUserCanDelete']
|
||||||
|
},
|
||||||
|
userCanDownload() {
|
||||||
|
return this.$store.getters['user/getUserCanDownload']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
showEditCover() {
|
||||||
|
this.$store.commit('setBookshelfBookIds', [])
|
||||||
|
this.$store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'cover' })
|
||||||
|
},
|
||||||
|
openEbook() {
|
||||||
|
this.$store.commit('showEReader', this.libraryItem)
|
||||||
|
},
|
||||||
|
toggleRead() {
|
||||||
|
var updatePayload = {
|
||||||
|
isRead: !this.isRead
|
||||||
|
}
|
||||||
|
this.isProcessingReadUpdate = true
|
||||||
|
this.$axios
|
||||||
|
.$patch(`/api/me/audiobook/${this.libraryItemId}`, updatePayload)
|
||||||
|
.then(() => {
|
||||||
|
this.isProcessingReadUpdate = false
|
||||||
|
this.$toast.success(`"${this.title}" Marked as ${updatePayload.isRead ? 'Read' : 'Not Read'}`)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed', error)
|
||||||
|
this.isProcessingReadUpdate = false
|
||||||
|
this.$toast.error(`Failed to mark as ${updatePayload.isRead ? 'Read' : 'Not Read'}`)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
startStream() {
|
||||||
|
this.$eventBus.$emit('play-audiobook', this.libraryItem.id)
|
||||||
|
},
|
||||||
|
editClick() {
|
||||||
|
this.$store.commit('setBookshelfBookIds', [])
|
||||||
|
this.$store.commit('showEditModal', this.libraryItem)
|
||||||
|
},
|
||||||
|
audiobookUpdated() {
|
||||||
|
console.log('Audiobook Updated - Fetch full audiobook')
|
||||||
|
this.$axios
|
||||||
|
.$get(`/api/books/${this.libraryItemId}`)
|
||||||
|
.then((audiobook) => {
|
||||||
|
console.log('Updated audiobook', audiobook)
|
||||||
|
this.libraryItem = audiobook
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed', error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
clearProgressClick() {
|
||||||
|
if (confirm(`Are you sure you want to reset your progress?`)) {
|
||||||
|
this.resettingProgress = true
|
||||||
|
this.$axios
|
||||||
|
.$patch(`/api/me/audiobook/${this.libraryItemId}/reset-progress`)
|
||||||
|
.then(() => {
|
||||||
|
console.log('Progress reset complete')
|
||||||
|
this.$toast.success(`Your progress was reset`)
|
||||||
|
this.resettingProgress = false
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Progress reset failed', error)
|
||||||
|
this.resettingProgress = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
downloadClick() {
|
||||||
|
this.$store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'download' })
|
||||||
|
},
|
||||||
|
collectionsClick() {
|
||||||
|
this.$store.commit('setSelectedAudiobook', this.libraryItem)
|
||||||
|
this.$store.commit('globals/setShowUserCollectionsModal', true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$store.commit('audiobooks/addListener', { id: 'audiobook', audiobookId: this.libraryItemId, meth: this.libraryItemUpdated })
|
||||||
|
|
||||||
|
// use this audiobooks library id as the current
|
||||||
|
if (this.libraryId) {
|
||||||
|
this.$store.commit('libraries/setCurrentLibrary', this.libraryId)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.$store.commit('audiobooks/removeListener', 'audiobook')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
@ -76,7 +76,7 @@ export default class CastPlayer extends EventEmitter {
|
|||||||
|
|
||||||
this.currentTime = startTime
|
this.currentTime = startTime
|
||||||
|
|
||||||
var coverImg = this.ctx.$store.getters['audiobooks/getBookCoverSrc'](audiobook)
|
var coverImg = this.ctx.$store.getters['audiobooks/getLibraryItemCoverSrc'](audiobook)
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
this.coverUrl = coverImg
|
this.coverUrl = coverImg
|
||||||
} else {
|
} else {
|
||||||
|
@ -3,7 +3,7 @@ export default (ctx) => {
|
|||||||
// Fetch background covers for chromecast (temp)
|
// Fetch background covers for chromecast (temp)
|
||||||
var covers = await ctx.$axios.$get(`/api/libraries/${ctx.$store.state.libraries.currentLibraryId}/books/all?limit=40&minified=1`).then((data) => {
|
var covers = await ctx.$axios.$get(`/api/libraries/${ctx.$store.state.libraries.currentLibraryId}/books/all?limit=40&minified=1`).then((data) => {
|
||||||
return data.results.filter((b) => b.book.cover).map((ab) => {
|
return data.results.filter((b) => b.book.cover).map((ab) => {
|
||||||
var coverUrl = ctx.$store.getters['audiobooks/getBookCoverSrc'](ab)
|
var coverUrl = ctx.$store.getters['audiobooks/getLibraryItemCoverSrc'](ab)
|
||||||
if (process.env.NODE_ENV === 'development') return coverUrl
|
if (process.env.NODE_ENV === 'development') return coverUrl
|
||||||
return `${window.location.origin}${coverUrl}`
|
return `${window.location.origin}${coverUrl}`
|
||||||
})
|
})
|
||||||
|
@ -12,25 +12,21 @@ export const state = () => ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
getBookCoverSrc: (state, getters, rootState, rootGetters) => (bookItem, placeholder = '/book_placeholder.jpg') => {
|
getLibraryItemCoverSrc: (state, getters, rootState, rootGetters) => (libraryItem, placeholder = '/book_placeholder.jpg') => {
|
||||||
if (!bookItem) return placeholder
|
if (!libraryItem) return placeholder
|
||||||
var book = bookItem.book
|
var media = libraryItem.media
|
||||||
if (!book || !book.cover || book.cover === placeholder) return placeholder
|
if (!media || !media.coverPath || media.coverPath === placeholder) return placeholder
|
||||||
|
|
||||||
// Absolute URL covers (should no longer be used)
|
// Absolute URL covers (should no longer be used)
|
||||||
if (book.cover.startsWith('http:') || book.cover.startsWith('https:')) return book.cover
|
if (media.coverPath.startsWith('http:') || media.coverPath.startsWith('https:')) return media.coverPath
|
||||||
|
|
||||||
var userToken = rootGetters['user/getToken']
|
var userToken = rootGetters['user/getToken']
|
||||||
var bookLastUpdate = book.lastUpdate || Date.now()
|
var lastUpdate = libraryItem.updatedAt || Date.now()
|
||||||
|
|
||||||
if (!bookItem.id) {
|
|
||||||
console.error('No book item id', bookItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') { // Testing
|
if (process.env.NODE_ENV !== 'production') { // Testing
|
||||||
return `http://localhost:3333/api/books/${bookItem.id}/cover?token=${userToken}&ts=${bookLastUpdate}`
|
return `http://localhost:3333/api/items/${libraryItem.id}/cover?token=${userToken}&ts=${lastUpdate}`
|
||||||
}
|
}
|
||||||
return `/api/books/${bookItem.id}/cover?token=${userToken}&ts=${bookLastUpdate}`
|
return `/api/items/${libraryItem.id}/cover?token=${userToken}&ts=${lastUpdate}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ export const state = () => ({
|
|||||||
showEditModal: false,
|
showEditModal: false,
|
||||||
showEReader: false,
|
showEReader: false,
|
||||||
selectedAudiobook: null,
|
selectedAudiobook: null,
|
||||||
|
selectedLibraryItem: null,
|
||||||
selectedAudiobookFile: null,
|
selectedAudiobookFile: null,
|
||||||
developerMode: false,
|
developerMode: false,
|
||||||
selectedAudiobooks: [],
|
selectedAudiobooks: [],
|
||||||
@ -108,14 +109,14 @@ export const mutations = {
|
|||||||
setStreamAudiobook(state, audiobook) {
|
setStreamAudiobook(state, audiobook) {
|
||||||
state.streamAudiobook = audiobook
|
state.streamAudiobook = audiobook
|
||||||
},
|
},
|
||||||
showEditModal(state, audiobook) {
|
showEditModal(state, libraryItem) {
|
||||||
state.editModalTab = 'details'
|
state.editModalTab = 'details'
|
||||||
state.selectedAudiobook = audiobook
|
state.selectedLibraryItem = libraryItem
|
||||||
state.showEditModal = true
|
state.showEditModal = true
|
||||||
},
|
},
|
||||||
showEditModalOnTab(state, { audiobook, tab }) {
|
showEditModalOnTab(state, { libraryItem, tab }) {
|
||||||
state.editModalTab = tab
|
state.editModalTab = tab
|
||||||
state.selectedAudiobook = audiobook
|
state.selectedLibraryItem = libraryItem
|
||||||
state.showEditModal = true
|
state.showEditModal = true
|
||||||
},
|
},
|
||||||
setEditModalTab(state, tab) {
|
setEditModalTab(state, tab) {
|
||||||
@ -124,15 +125,15 @@ export const mutations = {
|
|||||||
setShowEditModal(state, val) {
|
setShowEditModal(state, val) {
|
||||||
state.showEditModal = val
|
state.showEditModal = val
|
||||||
},
|
},
|
||||||
showEReader(state, audiobook) {
|
showEReader(state, libraryItem) {
|
||||||
state.selectedAudiobookFile = null
|
state.selectedAudiobookFile = null
|
||||||
state.selectedAudiobook = audiobook
|
state.selectedLibraryItem = libraryItem
|
||||||
|
|
||||||
state.showEReader = true
|
state.showEReader = true
|
||||||
},
|
},
|
||||||
showEReaderForFile(state, { audiobook, file }) {
|
showEReaderForFile(state, { libraryItem, file }) {
|
||||||
state.selectedAudiobookFile = file
|
state.selectedAudiobookFile = file
|
||||||
state.selectedAudiobook = audiobook
|
state.selectedLibraryItem = libraryItem
|
||||||
|
|
||||||
state.showEReader = true
|
state.showEReader = true
|
||||||
},
|
},
|
||||||
@ -142,8 +143,8 @@ export const mutations = {
|
|||||||
setDeveloperMode(state, val) {
|
setDeveloperMode(state, val) {
|
||||||
state.developerMode = val
|
state.developerMode = val
|
||||||
},
|
},
|
||||||
setSelectedAudiobook(state, val) {
|
setSelectedLibraryItem(state, val) {
|
||||||
Vue.set(state, 'selectedAudiobook', val)
|
Vue.set(state, 'selectedLibraryItem', val)
|
||||||
},
|
},
|
||||||
setSelectedAudiobooks(state, audiobooks) {
|
setSelectedAudiobooks(state, audiobooks) {
|
||||||
Vue.set(state, 'selectedAudiobooks', audiobooks)
|
Vue.set(state, 'selectedAudiobooks', audiobooks)
|
||||||
|
@ -124,7 +124,7 @@ export const actions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.$axios
|
this.$axios
|
||||||
.$get(`/api/libraries/${state.currentLibraryId}/filters`)
|
.$get(`/api/libraries/${state.currentLibraryId}/filterdata`)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
commit('setLibraryFilterData', data)
|
commit('setLibraryFilterData', data)
|
||||||
})
|
})
|
||||||
|
@ -19,8 +19,9 @@ new LibraryItem({
|
|||||||
lastScan: 1646784672127,
|
lastScan: 1646784672127,
|
||||||
scanVersion: 1.72,
|
scanVersion: 1.72,
|
||||||
isMissing: false,
|
isMissing: false,
|
||||||
entityType: 'book',
|
mediaType: 'book',
|
||||||
entity: { // Book.js
|
media: { // Book.js
|
||||||
|
coverPath: '/metadata/books/li_abai123wir/cover.webp',
|
||||||
metadata: { // BookMetadata.js
|
metadata: { // BookMetadata.js
|
||||||
title: 'Wizards First Rule',
|
title: 'Wizards First Rule',
|
||||||
subtitle: null,
|
subtitle: null,
|
||||||
|
@ -14,6 +14,7 @@ const UserController = require('./controllers/UserController')
|
|||||||
const CollectionController = require('./controllers/CollectionController')
|
const CollectionController = require('./controllers/CollectionController')
|
||||||
const MeController = require('./controllers/MeController')
|
const MeController = require('./controllers/MeController')
|
||||||
const BackupController = require('./controllers/BackupController')
|
const BackupController = require('./controllers/BackupController')
|
||||||
|
const LibraryItemController = require('./controllers/LibraryItemController')
|
||||||
|
|
||||||
const BookFinder = require('./finders/BookFinder')
|
const BookFinder = require('./finders/BookFinder')
|
||||||
const AuthorFinder = require('./finders/AuthorFinder')
|
const AuthorFinder = require('./finders/AuthorFinder')
|
||||||
@ -53,19 +54,29 @@ class ApiController {
|
|||||||
this.router.patch('/libraries/:id', LibraryController.middleware.bind(this), LibraryController.update.bind(this))
|
this.router.patch('/libraries/:id', LibraryController.middleware.bind(this), LibraryController.update.bind(this))
|
||||||
this.router.delete('/libraries/:id', LibraryController.middleware.bind(this), LibraryController.delete.bind(this))
|
this.router.delete('/libraries/:id', LibraryController.middleware.bind(this), LibraryController.delete.bind(this))
|
||||||
|
|
||||||
this.router.get('/libraries/:id/books/all', LibraryController.middleware.bind(this), LibraryController.getBooksForLibrary2.bind(this))
|
this.router.get('/libraries/:id/items', LibraryController.middleware.bind(this), LibraryController.getLibraryItems.bind(this))
|
||||||
this.router.get('/libraries/:id/books', LibraryController.middleware.bind(this), LibraryController.getBooksForLibrary.bind(this))
|
|
||||||
this.router.get('/libraries/:id/series', LibraryController.middleware.bind(this), LibraryController.getAllSeriesForLibrary.bind(this))
|
this.router.get('/libraries/:id/series', LibraryController.middleware.bind(this), LibraryController.getAllSeriesForLibrary.bind(this))
|
||||||
this.router.get('/libraries/:id/series/:series', LibraryController.middleware.bind(this), LibraryController.getSeriesForLibrary.bind(this))
|
this.router.get('/libraries/:id/series/:series', LibraryController.middleware.bind(this), LibraryController.getSeriesForLibrary.bind(this))
|
||||||
this.router.get('/libraries/:id/collections', LibraryController.middleware.bind(this), LibraryController.getCollectionsForLibrary.bind(this))
|
this.router.get('/libraries/:id/collections', LibraryController.middleware.bind(this), LibraryController.getCollectionsForLibrary.bind(this))
|
||||||
this.router.get('/libraries/:id/categories', LibraryController.middleware.bind(this), LibraryController.getLibraryCategories.bind(this))
|
this.router.get('/libraries/:id/personalized', LibraryController.middleware.bind(this), LibraryController.getLibraryUserPersonalized.bind(this))
|
||||||
this.router.get('/libraries/:id/filters', LibraryController.middleware.bind(this), LibraryController.getLibraryFilters.bind(this))
|
this.router.get('/libraries/:id/filterdata', LibraryController.middleware.bind(this), LibraryController.getLibraryFilterData.bind(this))
|
||||||
this.router.get('/libraries/:id/search', LibraryController.middleware.bind(this), LibraryController.search.bind(this))
|
this.router.get('/libraries/:id/search', LibraryController.middleware.bind(this), LibraryController.search.bind(this))
|
||||||
this.router.get('/libraries/:id/stats', LibraryController.middleware.bind(this), LibraryController.stats.bind(this))
|
this.router.get('/libraries/:id/stats', LibraryController.middleware.bind(this), LibraryController.stats.bind(this))
|
||||||
this.router.get('/libraries/:id/authors', LibraryController.middleware.bind(this), LibraryController.getAuthors.bind(this))
|
this.router.get('/libraries/:id/authors', LibraryController.middleware.bind(this), LibraryController.getAuthors.bind(this))
|
||||||
this.router.post('/libraries/:id/matchbooks', LibraryController.middleware.bind(this), LibraryController.matchBooks.bind(this))
|
this.router.post('/libraries/:id/matchbooks', LibraryController.middleware.bind(this), LibraryController.matchBooks.bind(this))
|
||||||
this.router.post('/libraries/order', LibraryController.reorder.bind(this))
|
this.router.post('/libraries/order', LibraryController.reorder.bind(this))
|
||||||
|
|
||||||
|
// Legacy
|
||||||
|
this.router.get('/libraries/:id/books/all', LibraryController.middleware.bind(this), LibraryController.getBooksForLibrary.bind(this))
|
||||||
|
this.router.get('/libraries/:id/categories', LibraryController.middleware.bind(this), LibraryController.getLibraryCategories.bind(this))
|
||||||
|
this.router.get('/libraries/:id/filters', LibraryController.middleware.bind(this), LibraryController.getLibraryFilters.bind(this))
|
||||||
|
|
||||||
|
//
|
||||||
|
// Item Routes
|
||||||
|
//
|
||||||
|
this.router.get('/items/:id', LibraryItemController.middleware.bind(this), LibraryItemController.findOne.bind(this))
|
||||||
|
this.router.get('/items/:id/cover', LibraryItemController.middleware.bind(this), LibraryItemController.getCover.bind(this))
|
||||||
|
|
||||||
//
|
//
|
||||||
// Book Routes
|
// Book Routes
|
||||||
//
|
//
|
||||||
|
@ -10,14 +10,14 @@ class CacheManager {
|
|||||||
this.CoverCachePath = Path.join(this.CachePath, 'covers')
|
this.CoverCachePath = Path.join(this.CachePath, 'covers')
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleCoverCache(res, audiobook, options = {}) {
|
async handleCoverCache(res, libraryItem, options = {}) {
|
||||||
const format = options.format || 'webp'
|
const format = options.format || 'webp'
|
||||||
const width = options.width || 400
|
const width = options.width || 400
|
||||||
const height = options.height || null
|
const height = options.height || null
|
||||||
|
|
||||||
res.type(`image/${format}`)
|
res.type(`image/${format}`)
|
||||||
|
|
||||||
var path = Path.join(this.CoverCachePath, `${audiobook.id}_${width}${height ? `x${height}` : ''}`) + '.' + format
|
var path = Path.join(this.CoverCachePath, `${libraryItem.id}_${width}${height ? `x${height}` : ''}`) + '.' + format
|
||||||
|
|
||||||
// Cache exists
|
// Cache exists
|
||||||
if (await fs.pathExists(path)) {
|
if (await fs.pathExists(path)) {
|
||||||
@ -35,7 +35,7 @@ class CacheManager {
|
|||||||
// Write cache
|
// Write cache
|
||||||
await fs.ensureDir(this.CoverCachePath)
|
await fs.ensureDir(this.CoverCachePath)
|
||||||
|
|
||||||
let writtenFile = await resizeImage(audiobook.book.coverFullPath, path, width, height)
|
let writtenFile = await resizeImage(libraryItem.media.coverPath, path, width, height)
|
||||||
if (!writtenFile) return res.sendStatus(400)
|
if (!writtenFile) return res.sendStatus(400)
|
||||||
|
|
||||||
var readStream = fs.createReadStream(writtenFile)
|
var readStream = fs.createReadStream(writtenFile)
|
||||||
|
@ -69,7 +69,6 @@ class Server {
|
|||||||
|
|
||||||
Logger.logManager = this.logManager
|
Logger.logManager = this.logManager
|
||||||
|
|
||||||
this.expressApp = null
|
|
||||||
this.server = null
|
this.server = null
|
||||||
this.io = null
|
this.io = null
|
||||||
|
|
||||||
@ -154,8 +153,6 @@ class Server {
|
|||||||
await this.init()
|
await this.init()
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
this.expressApp = app
|
|
||||||
|
|
||||||
this.server = http.createServer(app)
|
this.server = http.createServer(app)
|
||||||
|
|
||||||
app.use(this.auth.cors)
|
app.use(this.auth.cors)
|
||||||
@ -167,9 +164,6 @@ class Server {
|
|||||||
const distPath = Path.join(global.appRoot, '/client/dist')
|
const distPath = Path.join(global.appRoot, '/client/dist')
|
||||||
app.use(express.static(distPath))
|
app.use(express.static(distPath))
|
||||||
|
|
||||||
// Old static path for covers
|
|
||||||
app.use('/local', this.authMiddleware.bind(this), express.static(global.AudiobookPath))
|
|
||||||
|
|
||||||
// Metadata folder static path
|
// Metadata folder static path
|
||||||
app.use('/metadata', this.authMiddleware.bind(this), express.static(global.MetadataPath))
|
app.use('/metadata', this.authMiddleware.bind(this), express.static(global.MetadataPath))
|
||||||
|
|
||||||
@ -179,6 +173,9 @@ class Server {
|
|||||||
// Static folder
|
// Static folder
|
||||||
app.use(express.static(Path.join(global.appRoot, 'static')))
|
app.use(express.static(Path.join(global.appRoot, 'static')))
|
||||||
|
|
||||||
|
app.use('/api', this.authMiddleware.bind(this), this.apiController.router)
|
||||||
|
app.use('/hls', this.authMiddleware.bind(this), this.hlsController.router)
|
||||||
|
|
||||||
// Static file routes
|
// Static file routes
|
||||||
app.get('/lib/:library/:folder/*', this.authMiddleware.bind(this), (req, res) => {
|
app.get('/lib/:library/:folder/*', this.authMiddleware.bind(this), (req, res) => {
|
||||||
var library = this.db.libraries.find(lib => lib.id === req.params.library)
|
var library = this.db.libraries.find(lib => lib.id === req.params.library)
|
||||||
@ -192,6 +189,7 @@ class Server {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Book static file routes
|
// Book static file routes
|
||||||
|
// LEGACY
|
||||||
app.get('/s/book/:id/*', this.authMiddleware.bind(this), (req, res) => {
|
app.get('/s/book/:id/*', this.authMiddleware.bind(this), (req, res) => {
|
||||||
var audiobook = this.db.audiobooks.find(ab => ab.id === req.params.id)
|
var audiobook = this.db.audiobooks.find(ab => ab.id === req.params.id)
|
||||||
if (!audiobook) return res.status(404).send('Book not found with id ' + req.params.id)
|
if (!audiobook) return res.status(404).send('Book not found with id ' + req.params.id)
|
||||||
@ -201,6 +199,16 @@ class Server {
|
|||||||
res.sendFile(fullPath)
|
res.sendFile(fullPath)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Library Item static file routes
|
||||||
|
app.get('/s/item/:id/*', this.authMiddleware.bind(this), (req, res) => {
|
||||||
|
var item = this.db.libraryItems.find(ab => ab.id === req.params.id)
|
||||||
|
if (!item) return res.status(404).send('Item not found with id ' + req.params.id)
|
||||||
|
|
||||||
|
var remainingPath = req.params['0']
|
||||||
|
var fullPath = Path.join(item.path, remainingPath)
|
||||||
|
res.sendFile(fullPath)
|
||||||
|
})
|
||||||
|
|
||||||
// EBook static file routes
|
// EBook static file routes
|
||||||
app.get('/ebook/:library/:folder/*', (req, res) => {
|
app.get('/ebook/:library/:folder/*', (req, res) => {
|
||||||
var library = this.db.libraries.find(lib => lib.id === req.params.library)
|
var library = this.db.libraries.find(lib => lib.id === req.params.library)
|
||||||
@ -214,8 +222,10 @@ class Server {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Client dynamic routes
|
// Client dynamic routes
|
||||||
app.get('/audiobook/:id', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
app.get('/audiobook/:id', (req, res) => res.sendFile(Path.join(distPath, 'index.html'))) // LEGACY
|
||||||
app.get('/audiobook/:id/edit', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
app.get('/audiobook/:id/edit', (req, res) => res.sendFile(Path.join(distPath, 'index.html'))) // LEGACY
|
||||||
|
app.get('/item/:id', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
||||||
|
app.get('/item/:id/edit', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
||||||
app.get('/library/:library', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
app.get('/library/:library', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
||||||
app.get('/library/:library/search', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
app.get('/library/:library/search', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
||||||
app.get('/library/:library/bookshelf/:id?', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
app.get('/library/:library/bookshelf/:id?', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
||||||
@ -224,31 +234,16 @@ class Server {
|
|||||||
app.get('/config/users/:id', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
app.get('/config/users/:id', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
||||||
app.get('/collection/:id', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
app.get('/collection/:id', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
||||||
|
|
||||||
app.use('/api', this.authMiddleware.bind(this), this.apiController.router)
|
app.post('/login', this.getLoginRateLimiter(), (req, res) => this.auth.login(req, res))
|
||||||
app.use('/hls', this.authMiddleware.bind(this), this.hlsController.router)
|
|
||||||
|
|
||||||
// Incomplete work in progress
|
|
||||||
// app.use('/feeds', this.rssFeeds.router)
|
|
||||||
|
|
||||||
app.post('/upload', this.authMiddleware.bind(this), this.handleUpload.bind(this))
|
app.post('/upload', this.authMiddleware.bind(this), this.handleUpload.bind(this))
|
||||||
|
|
||||||
var loginRateLimiter = this.getLoginRateLimiter()
|
|
||||||
app.post('/login', loginRateLimiter, (req, res) => this.auth.login(req, res))
|
|
||||||
|
|
||||||
app.post('/logout', this.authMiddleware.bind(this), this.logout.bind(this))
|
app.post('/logout', this.authMiddleware.bind(this), this.logout.bind(this))
|
||||||
|
|
||||||
app.get('/ping', (req, res) => {
|
app.get('/ping', (req, res) => {
|
||||||
Logger.info('Recieved ping')
|
Logger.info('Recieved ping')
|
||||||
res.json({ success: true })
|
res.json({ success: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
// Used in development to set-up streams without authentication
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
|
||||||
app.use('/test-hls', this.hlsController.router)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.server.listen(this.Port, this.Host, () => {
|
this.server.listen(this.Port, this.Host, () => {
|
||||||
Logger.info(`Running on http://${this.Host}:${this.Port}`)
|
Logger.info(`Listening on http://${this.Host}:${this.Port}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.io = new SocketIO.Server(this.server, {
|
this.io = new SocketIO.Server(this.server, {
|
||||||
@ -265,21 +260,25 @@ class Server {
|
|||||||
}
|
}
|
||||||
socket.sheepClient = this.clients[socket.id]
|
socket.sheepClient = this.clients[socket.id]
|
||||||
|
|
||||||
Logger.info('[SOCKET] Socket Connected', socket.id)
|
Logger.info('[Server] Socket Connected', socket.id)
|
||||||
|
|
||||||
socket.on('auth', (token) => this.authenticateSocket(socket, token))
|
socket.on('auth', (token) => this.authenticateSocket(socket, token))
|
||||||
|
|
||||||
|
// TODO: Most of these web socket listeners will be moved to API routes instead
|
||||||
|
// with the goal of the web socket connection being a nice-to-have not need-to-have
|
||||||
|
|
||||||
// Scanning
|
// Scanning
|
||||||
socket.on('scan', this.scan.bind(this))
|
socket.on('scan', this.scan.bind(this))
|
||||||
socket.on('cancel_scan', this.cancelScan.bind(this))
|
socket.on('cancel_scan', this.cancelScan.bind(this))
|
||||||
socket.on('scan_audiobook', (audiobookId) => this.scanAudiobook(socket, audiobookId))
|
socket.on('scan_audiobook', (audiobookId) => this.scanAudiobook(socket, audiobookId))
|
||||||
socket.on('save_metadata', (audiobookId) => this.saveMetadata(socket, audiobookId))
|
socket.on('save_metadata', (audiobookId) => this.saveMetadata(socket, audiobookId))
|
||||||
|
|
||||||
// Streaming
|
// Streaming (only still used in the mobile app)
|
||||||
socket.on('open_stream', (audiobookId) => this.streamManager.openStreamSocketRequest(socket, audiobookId))
|
socket.on('open_stream', (audiobookId) => this.streamManager.openStreamSocketRequest(socket, audiobookId))
|
||||||
socket.on('close_stream', () => this.streamManager.closeStreamRequest(socket))
|
socket.on('close_stream', () => this.streamManager.closeStreamRequest(socket))
|
||||||
socket.on('stream_sync', (syncData) => this.streamManager.streamSync(socket, syncData))
|
socket.on('stream_sync', (syncData) => this.streamManager.streamSync(socket, syncData))
|
||||||
|
|
||||||
|
// Used to sync when playing local book on mobile, will be moved to API route
|
||||||
socket.on('progress_update', (payload) => this.audiobookProgressUpdate(socket, payload))
|
socket.on('progress_update', (payload) => this.audiobookProgressUpdate(socket, payload))
|
||||||
|
|
||||||
// Downloading
|
// Downloading
|
||||||
@ -299,10 +298,6 @@ class Server {
|
|||||||
socket.on('update_bookmark', (payload) => this.updateBookmark(socket, payload))
|
socket.on('update_bookmark', (payload) => this.updateBookmark(socket, payload))
|
||||||
socket.on('delete_bookmark', (payload) => this.deleteBookmark(socket, payload))
|
socket.on('delete_bookmark', (payload) => this.deleteBookmark(socket, payload))
|
||||||
|
|
||||||
socket.on('test', () => {
|
|
||||||
socket.emit('test_received', socket.id)
|
|
||||||
})
|
|
||||||
|
|
||||||
socket.on('disconnect', () => {
|
socket.on('disconnect', () => {
|
||||||
Logger.removeSocketListener(socket.id)
|
Logger.removeSocketListener(socket.id)
|
||||||
|
|
||||||
|
@ -39,10 +39,9 @@ class LibraryController {
|
|||||||
|
|
||||||
async findOne(req, res) {
|
async findOne(req, res) {
|
||||||
if (req.query.include && req.query.include === 'filterdata') {
|
if (req.query.include && req.query.include === 'filterdata') {
|
||||||
var books = this.db.audiobooks.filter(ab => ab.libraryId === req.library.id)
|
|
||||||
return res.json({
|
return res.json({
|
||||||
filterdata: libraryHelpers.getDistinctFilterData(books),
|
filterdata: libraryHelpers.getDistinctFilterDataNew(req.libraryItems),
|
||||||
issues: libraryHelpers.getNumIssues(books),
|
issues: libraryHelpers.getNumIssues(req.libraryItems),
|
||||||
library: req.library
|
library: req.library
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -91,38 +90,70 @@ class LibraryController {
|
|||||||
return res.json(libraryJson)
|
return res.json(libraryJson)
|
||||||
}
|
}
|
||||||
|
|
||||||
// api/libraries/:id/books
|
// api/libraries/:id/items
|
||||||
getBooksForLibrary(req, res) {
|
// TODO: Optimize this method, audiobooks are iterated through several times but can be combined
|
||||||
|
getLibraryItems(req, res) {
|
||||||
var libraryId = req.library.id
|
var libraryId = req.library.id
|
||||||
var audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === libraryId)
|
var media = req.query.media || 'all'
|
||||||
|
var libraryItems = this.db.libraryItems.filter(li => {
|
||||||
if (req.query.filter) {
|
if (li.libraryId !== libraryId) return false
|
||||||
audiobooks = libraryHelpers.getFiltered(audiobooks, req.query.filter, req.user)
|
if (media != 'all') return li.mediaType == media
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
var payload = {
|
||||||
|
results: [],
|
||||||
|
total: libraryItems.length,
|
||||||
|
limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0,
|
||||||
|
page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0,
|
||||||
|
sortBy: req.query.sort,
|
||||||
|
sortDesc: req.query.desc === '1',
|
||||||
|
filterBy: req.query.filter,
|
||||||
|
media,
|
||||||
|
minified: req.query.minified === '1',
|
||||||
|
collapseseries: req.query.collapseseries === '1'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.query.sort) {
|
if (payload.filterBy) {
|
||||||
var orderByNumber = req.query.sort === 'book.volumeNumber'
|
libraryItems = libraryHelpers.getFilteredLibraryItems(libraryItems, payload.filterBy, req.user)
|
||||||
var direction = req.query.desc === '1' ? 'desc' : 'asc'
|
payload.total = libraryItems.length
|
||||||
audiobooks = sort(audiobooks)[direction]((ab) => {
|
}
|
||||||
// Supports dot notation strings i.e. "book.title"
|
|
||||||
var value = req.query.sort.split('.').reduce((a, b) => a[b], ab)
|
if (payload.sortBy) {
|
||||||
if (orderByNumber && !isNaN(value)) return Number(value)
|
var sortKey = payload.sortBy
|
||||||
return value
|
|
||||||
|
// old sort key
|
||||||
|
if (sortKey.startsWith('book.')) {
|
||||||
|
sortKey = sortKey.replace('book.', 'media.metadata.')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle server setting sortingIgnorePrefix
|
||||||
|
if (sortKey === 'media.metadata.title' && this.db.serverSettings.sortingIgnorePrefix) {
|
||||||
|
// BookMetadata.js has titleIgnorePrefix getter
|
||||||
|
sortKey += 'IgnorePrefix'
|
||||||
|
}
|
||||||
|
|
||||||
|
var direction = payload.sortDesc ? 'desc' : 'asc'
|
||||||
|
libraryItems = naturalSort(libraryItems)[direction]((li) => {
|
||||||
|
|
||||||
|
// Supports dot notation strings i.e. "media.metadata.title"
|
||||||
|
return sortKey.split('.').reduce((a, b) => a[b], li)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.query.limit && !isNaN(req.query.limit)) {
|
// TODO: Potentially implement collapse series again
|
||||||
var page = req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0
|
libraryItems = libraryItems.map(ab => payload.minified ? ab.toJSONMinified() : ab.toJSON())
|
||||||
var limit = Number(req.query.limit)
|
|
||||||
var startIndex = page * limit
|
if (payload.limit) {
|
||||||
audiobooks = audiobooks.slice(startIndex, startIndex + limit)
|
var startIndex = payload.page * payload.limit
|
||||||
|
libraryItems = libraryItems.slice(startIndex, startIndex + payload.limit)
|
||||||
}
|
}
|
||||||
res.json(audiobooks)
|
payload.results = libraryItems
|
||||||
|
res.json(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
// api/libraries/:id/books/all
|
// api/libraries/:id/books/all
|
||||||
// TODO: Optimize this method, audiobooks are iterated through several times but can be combined
|
// TODO: Optimize this method, audiobooks are iterated through several times but can be combined
|
||||||
getBooksForLibrary2(req, res) {
|
getBooksForLibrary(req, res) {
|
||||||
var libraryId = req.library.id
|
var libraryId = req.library.id
|
||||||
|
|
||||||
var audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === libraryId)
|
var audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === libraryId)
|
||||||
@ -274,6 +305,7 @@ class LibraryController {
|
|||||||
res.json(payload)
|
res.json(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LEGACY
|
||||||
// api/libraries/:id/books/filters
|
// api/libraries/:id/books/filters
|
||||||
async getLibraryFilters(req, res) {
|
async getLibraryFilters(req, res) {
|
||||||
var library = req.library
|
var library = req.library
|
||||||
@ -281,6 +313,45 @@ class LibraryController {
|
|||||||
res.json(libraryHelpers.getDistinctFilterData(books))
|
res.json(libraryHelpers.getDistinctFilterData(books))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getLibraryFilterData(req, res) {
|
||||||
|
res.json(libraryHelpers.getDistinctFilterDataNew(req.libraryItems))
|
||||||
|
}
|
||||||
|
|
||||||
|
// api/libraries/:id/books/personalized
|
||||||
|
async getLibraryUserPersonalized(req, res) {
|
||||||
|
var libraryItems = req.libraryItems
|
||||||
|
var limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12
|
||||||
|
var minified = req.query.minified === '1'
|
||||||
|
|
||||||
|
var booksWithUserAb = libraryHelpers.getBooksWithUserAudiobook(req.user, libraryItems)
|
||||||
|
|
||||||
|
var categories = [
|
||||||
|
{
|
||||||
|
id: 'continue-reading',
|
||||||
|
label: 'Continue Reading',
|
||||||
|
type: 'books',
|
||||||
|
entities: libraryHelpers.getBooksMostRecentlyRead(booksWithUserAb, limitPerShelf, minified)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'recently-added',
|
||||||
|
label: 'Recently Added',
|
||||||
|
type: 'books',
|
||||||
|
entities: libraryHelpers.getBooksMostRecentlyAdded(libraryItems, limitPerShelf, minified)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'read-again',
|
||||||
|
label: 'Read Again',
|
||||||
|
type: 'books',
|
||||||
|
entities: libraryHelpers.getBooksMostRecentlyFinished(booksWithUserAb, limitPerShelf, minified)
|
||||||
|
}
|
||||||
|
].filter(cats => { // Remove categories with no items
|
||||||
|
return cats.entities.length
|
||||||
|
})
|
||||||
|
|
||||||
|
res.json(categories)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LEGACY
|
||||||
// api/libraries/:id/books/categories
|
// api/libraries/:id/books/categories
|
||||||
async getLibraryCategories(req, res) {
|
async getLibraryCategories(req, res) {
|
||||||
var library = req.library
|
var library = req.library
|
||||||
@ -491,6 +562,7 @@ class LibraryController {
|
|||||||
return res.status(404).send('Library not found')
|
return res.status(404).send('Library not found')
|
||||||
}
|
}
|
||||||
req.library = library
|
req.library = library
|
||||||
|
req.libraryItems = this.db.libraryItems.filter(li => li.libraryId === library.id)
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
37
server/controllers/LibraryItemController.js
Normal file
37
server/controllers/LibraryItemController.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
const Logger = require('../Logger')
|
||||||
|
const { reqSupportsWebp } = require('../utils/index')
|
||||||
|
|
||||||
|
class LibraryItemController {
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
findOne(req, res) {
|
||||||
|
if (req.query.expanded == 1) return res.json(req.libraryItem.toJSONExpanded())
|
||||||
|
res.json(req.libraryItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET api/items/:id/cover
|
||||||
|
async getCover(req, res) {
|
||||||
|
let { query: { width, height, format }, libraryItem } = req
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
format: format || (reqSupportsWebp(req) ? 'webp' : 'jpeg'),
|
||||||
|
height: height ? parseInt(height) : null,
|
||||||
|
width: width ? parseInt(width) : null
|
||||||
|
}
|
||||||
|
return this.cacheManager.handleCoverCache(res, libraryItem, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
middleware(req, res, next) {
|
||||||
|
var item = this.db.libraryItems.find(li => li.id === req.params.id)
|
||||||
|
if (!item || !item.media || !item.media.coverPath) return res.sendStatus(404)
|
||||||
|
|
||||||
|
// Check user can access this audiobooks library
|
||||||
|
if (!req.user.checkCanAccessLibrary(item.libraryId)) {
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.libraryItem = item
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = new LibraryItemController()
|
@ -1,7 +1,7 @@
|
|||||||
const fs = require('fs-extra')
|
const fs = require('fs-extra')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const Author = require('../objects/Author')
|
const Author = require('../objects/legacy/Author')
|
||||||
const Audnexus = require('../providers/Audnexus')
|
const Audnexus = require('../providers/Audnexus')
|
||||||
|
|
||||||
const { downloadFile } = require('../utils/fileUtils')
|
const { downloadFile } = require('../utils/fileUtils')
|
||||||
|
@ -1,129 +0,0 @@
|
|||||||
class AudioFileMetadata {
|
|
||||||
constructor(metadata) {
|
|
||||||
this.tagAlbum = null
|
|
||||||
this.tagArtist = null
|
|
||||||
this.tagGenre = null
|
|
||||||
this.tagTitle = null
|
|
||||||
this.tagSeries = null
|
|
||||||
this.tagSeriesPart = null
|
|
||||||
this.tagTrack = null
|
|
||||||
this.tagDisc = null
|
|
||||||
this.tagSubtitle = null
|
|
||||||
this.tagAlbumArtist = null
|
|
||||||
this.tagDate = null
|
|
||||||
this.tagComposer = null
|
|
||||||
this.tagPublisher = null
|
|
||||||
this.tagComment = null
|
|
||||||
this.tagDescription = null
|
|
||||||
this.tagEncoder = null
|
|
||||||
this.tagEncodedBy = null
|
|
||||||
this.tagIsbn = null
|
|
||||||
this.tagLanguage = null
|
|
||||||
this.tagASIN = null
|
|
||||||
|
|
||||||
if (metadata) {
|
|
||||||
this.construct(metadata)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON() {
|
|
||||||
// Only return the tags that are actually set
|
|
||||||
var json = {}
|
|
||||||
for (const key in this) {
|
|
||||||
if (key.startsWith('tag') && this[key]) {
|
|
||||||
json[key] = this[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return json
|
|
||||||
}
|
|
||||||
|
|
||||||
construct(metadata) {
|
|
||||||
this.tagAlbum = metadata.tagAlbum || null
|
|
||||||
this.tagArtist = metadata.tagArtist || null
|
|
||||||
this.tagGenre = metadata.tagGenre || null
|
|
||||||
this.tagTitle = metadata.tagTitle || null
|
|
||||||
this.tagSeries = metadata.tagSeries || null
|
|
||||||
this.tagSeriesPart = metadata.tagSeriesPart || null
|
|
||||||
this.tagTrack = metadata.tagTrack || null
|
|
||||||
this.tagDisc = metadata.tagDisc || null
|
|
||||||
this.tagSubtitle = metadata.tagSubtitle || null
|
|
||||||
this.tagAlbumArtist = metadata.tagAlbumArtist || null
|
|
||||||
this.tagDate = metadata.tagDate || null
|
|
||||||
this.tagComposer = metadata.tagComposer || null
|
|
||||||
this.tagPublisher = metadata.tagPublisher || null
|
|
||||||
this.tagComment = metadata.tagComment || null
|
|
||||||
this.tagDescription = metadata.tagDescription || null
|
|
||||||
this.tagEncoder = metadata.tagEncoder || null
|
|
||||||
this.tagEncodedBy = metadata.tagEncodedBy || null
|
|
||||||
this.tagIsbn = metadata.tagIsbn || null
|
|
||||||
this.tagLanguage = metadata.tagLanguage || null
|
|
||||||
this.tagASIN = metadata.tagASIN || null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Data parsed in prober.js
|
|
||||||
setData(payload) {
|
|
||||||
this.tagAlbum = payload.file_tag_album || null
|
|
||||||
this.tagArtist = payload.file_tag_artist || null
|
|
||||||
this.tagGenre = payload.file_tag_genre || null
|
|
||||||
this.tagTitle = payload.file_tag_title || null
|
|
||||||
this.tagSeries = payload.file_tag_series || null
|
|
||||||
this.tagSeriesPart = payload.file_tag_seriespart || null
|
|
||||||
this.tagTrack = payload.file_tag_track || null
|
|
||||||
this.tagDisc = payload.file_tag_disc || null
|
|
||||||
this.tagSubtitle = payload.file_tag_subtitle || null
|
|
||||||
this.tagAlbumArtist = payload.file_tag_albumartist || null
|
|
||||||
this.tagDate = payload.file_tag_date || null
|
|
||||||
this.tagComposer = payload.file_tag_composer || null
|
|
||||||
this.tagPublisher = payload.file_tag_publisher || null
|
|
||||||
this.tagComment = payload.file_tag_comment || null
|
|
||||||
this.tagDescription = payload.file_tag_description || null
|
|
||||||
this.tagEncoder = payload.file_tag_encoder || null
|
|
||||||
this.tagEncodedBy = payload.file_tag_encodedby || null
|
|
||||||
this.tagIsbn = payload.file_tag_isbn || null
|
|
||||||
this.tagLanguage = payload.file_tag_language || null
|
|
||||||
this.tagASIN = payload.file_tag_asin || null
|
|
||||||
}
|
|
||||||
|
|
||||||
updateData(payload) {
|
|
||||||
const dataMap = {
|
|
||||||
tagAlbum: payload.file_tag_album || null,
|
|
||||||
tagArtist: payload.file_tag_artist || null,
|
|
||||||
tagGenre: payload.file_tag_genre || null,
|
|
||||||
tagTitle: payload.file_tag_title || null,
|
|
||||||
tagSeries: payload.file_tag_series || null,
|
|
||||||
tagSeriesPart: payload.file_tag_seriespart || null,
|
|
||||||
tagTrack: payload.file_tag_track || null,
|
|
||||||
tagDisc: payload.file_tag_disc || null,
|
|
||||||
tagSubtitle: payload.file_tag_subtitle || null,
|
|
||||||
tagAlbumArtist: payload.file_tag_albumartist || null,
|
|
||||||
tagDate: payload.file_tag_date || null,
|
|
||||||
tagComposer: payload.file_tag_composer || null,
|
|
||||||
tagPublisher: payload.file_tag_publisher || null,
|
|
||||||
tagComment: payload.file_tag_comment || null,
|
|
||||||
tagDescription: payload.file_tag_description || null,
|
|
||||||
tagEncoder: payload.file_tag_encoder || null,
|
|
||||||
tagEncodedBy: payload.file_tag_encodedby || null,
|
|
||||||
tagIsbn: payload.file_tag_isbn || null,
|
|
||||||
tagLanguage: payload.file_tag_language || null,
|
|
||||||
tagASIN: payload.file_tag_asin || null
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasUpdates = false
|
|
||||||
for (const key in dataMap) {
|
|
||||||
if (dataMap[key] !== this[key]) {
|
|
||||||
this[key] = dataMap[key]
|
|
||||||
hasUpdates = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return hasUpdates
|
|
||||||
}
|
|
||||||
|
|
||||||
isEqual(audioFileMetadata) {
|
|
||||||
if (!audioFileMetadata || !audioFileMetadata.toJSON) return false
|
|
||||||
for (const key in audioFileMetadata.toJSON()) {
|
|
||||||
if (audioFileMetadata[key] !== this[key]) return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
module.exports = AudioFileMetadata
|
|
@ -17,15 +17,15 @@ class LibraryItem {
|
|||||||
this.ctimeMs = null
|
this.ctimeMs = null
|
||||||
this.birthtimeMs = null
|
this.birthtimeMs = null
|
||||||
this.addedAt = null
|
this.addedAt = null
|
||||||
this.lastUpdate = null
|
this.updatedAt = null
|
||||||
this.lastScan = null
|
this.lastScan = null
|
||||||
this.scanVersion = null
|
this.scanVersion = null
|
||||||
|
|
||||||
// Entity was scanned and not found
|
// Entity was scanned and not found
|
||||||
this.isMissing = false
|
this.isMissing = false
|
||||||
|
|
||||||
this.entityType = null
|
this.mediaType = null
|
||||||
this.entity = null
|
this.media = null
|
||||||
|
|
||||||
this.libraryFiles = []
|
this.libraryFiles = []
|
||||||
|
|
||||||
@ -45,17 +45,17 @@ class LibraryItem {
|
|||||||
this.ctimeMs = libraryItem.ctimeMs || 0
|
this.ctimeMs = libraryItem.ctimeMs || 0
|
||||||
this.birthtimeMs = libraryItem.birthtimeMs || 0
|
this.birthtimeMs = libraryItem.birthtimeMs || 0
|
||||||
this.addedAt = libraryItem.addedAt
|
this.addedAt = libraryItem.addedAt
|
||||||
this.lastUpdate = libraryItem.lastUpdate || this.addedAt
|
this.updatedAt = libraryItem.updatedAt || this.addedAt
|
||||||
this.lastScan = libraryItem.lastScan || null
|
this.lastScan = libraryItem.lastScan || null
|
||||||
this.scanVersion = libraryItem.scanVersion || null
|
this.scanVersion = libraryItem.scanVersion || null
|
||||||
|
|
||||||
this.isMissing = !!libraryItem.isMissing
|
this.isMissing = !!libraryItem.isMissing
|
||||||
|
|
||||||
this.entityType = libraryItem.entityType
|
this.mediaType = libraryItem.mediaType
|
||||||
if (this.entityType === 'book') {
|
if (this.mediaType === 'book') {
|
||||||
this.entity = new Book(libraryItem.entity)
|
this.media = new Book(libraryItem.media)
|
||||||
} else if (this.entityType === 'podcast') {
|
} else if (this.mediaType === 'podcast') {
|
||||||
this.entity = new Podcast(libraryItem.entity)
|
this.media = new Podcast(libraryItem.media)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.libraryFiles = libraryItem.libraryFiles.map(f => new LibraryFile(f))
|
this.libraryFiles = libraryItem.libraryFiles.map(f => new LibraryFile(f))
|
||||||
@ -73,14 +73,64 @@ class LibraryItem {
|
|||||||
ctimeMs: this.ctimeMs,
|
ctimeMs: this.ctimeMs,
|
||||||
birthtimeMs: this.birthtimeMs,
|
birthtimeMs: this.birthtimeMs,
|
||||||
addedAt: this.addedAt,
|
addedAt: this.addedAt,
|
||||||
lastUpdate: this.lastUpdate,
|
updatedAt: this.updatedAt,
|
||||||
lastScan: this.lastScan,
|
lastScan: this.lastScan,
|
||||||
scanVersion: this.scanVersion,
|
scanVersion: this.scanVersion,
|
||||||
isMissing: !!this.isMissing,
|
isMissing: !!this.isMissing,
|
||||||
entityType: this.entityType,
|
mediaType: this.mediaType,
|
||||||
entity: this.entity.toJSON(),
|
media: this.media.toJSON(),
|
||||||
libraryFiles: this.libraryFiles.map(f => f.toJSON())
|
libraryFiles: this.libraryFiles.map(f => f.toJSON())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toJSONMinified() {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
ino: this.ino,
|
||||||
|
libraryId: this.libraryId,
|
||||||
|
folderId: this.folderId,
|
||||||
|
path: this.path,
|
||||||
|
relPath: this.relPath,
|
||||||
|
mtimeMs: this.mtimeMs,
|
||||||
|
ctimeMs: this.ctimeMs,
|
||||||
|
birthtimeMs: this.birthtimeMs,
|
||||||
|
addedAt: this.addedAt,
|
||||||
|
updatedAt: this.updatedAt,
|
||||||
|
isMissing: !!this.isMissing,
|
||||||
|
mediaType: this.mediaType,
|
||||||
|
media: this.media.toJSONMinified(),
|
||||||
|
numFiles: this.libraryFiles.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds additional helpful fields like media duration, tracks, etc.
|
||||||
|
toJSONExpanded() {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
ino: this.ino,
|
||||||
|
libraryId: this.libraryId,
|
||||||
|
folderId: this.folderId,
|
||||||
|
path: this.path,
|
||||||
|
relPath: this.relPath,
|
||||||
|
mtimeMs: this.mtimeMs,
|
||||||
|
ctimeMs: this.ctimeMs,
|
||||||
|
birthtimeMs: this.birthtimeMs,
|
||||||
|
addedAt: this.addedAt,
|
||||||
|
updatedAt: this.updatedAt,
|
||||||
|
lastScan: this.lastScan,
|
||||||
|
scanVersion: this.scanVersion,
|
||||||
|
isMissing: !!this.isMissing,
|
||||||
|
mediaType: this.mediaType,
|
||||||
|
media: this.media.toJSONExpanded(),
|
||||||
|
libraryFiles: this.libraryFiles.map(f => f.toJSON()),
|
||||||
|
size: this.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get size() {
|
||||||
|
var total = 0
|
||||||
|
this.libraryFiles.forEach((lf) => total += lf.metadata.size)
|
||||||
|
return total
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = LibraryItem
|
module.exports = LibraryItem
|
@ -7,7 +7,6 @@ class Book {
|
|||||||
this.metadata = null
|
this.metadata = null
|
||||||
|
|
||||||
this.coverPath = null
|
this.coverPath = null
|
||||||
this.relCoverPath = null
|
|
||||||
this.tags = []
|
this.tags = []
|
||||||
this.audioFiles = []
|
this.audioFiles = []
|
||||||
this.ebookFiles = []
|
this.ebookFiles = []
|
||||||
@ -21,7 +20,6 @@ class Book {
|
|||||||
construct(book) {
|
construct(book) {
|
||||||
this.metadata = new BookMetadata(book.metadata)
|
this.metadata = new BookMetadata(book.metadata)
|
||||||
this.coverPath = book.coverPath
|
this.coverPath = book.coverPath
|
||||||
this.relCoverPath = book.relCoverPath
|
|
||||||
this.tags = [...book.tags]
|
this.tags = [...book.tags]
|
||||||
this.audioFiles = book.audioFiles.map(f => new AudioFile(f))
|
this.audioFiles = book.audioFiles.map(f => new AudioFile(f))
|
||||||
this.ebookFiles = book.ebookFiles.map(f => new EBookFile(f))
|
this.ebookFiles = book.ebookFiles.map(f => new EBookFile(f))
|
||||||
@ -32,12 +30,53 @@ class Book {
|
|||||||
return {
|
return {
|
||||||
metadata: this.metadata.toJSON(),
|
metadata: this.metadata.toJSON(),
|
||||||
coverPath: this.coverPath,
|
coverPath: this.coverPath,
|
||||||
relCoverPath: this.relCoverPath,
|
|
||||||
tags: [...this.tags],
|
tags: [...this.tags],
|
||||||
audioFiles: this.audioFiles.map(f => f.toJSON()),
|
audioFiles: this.audioFiles.map(f => f.toJSON()),
|
||||||
ebookFiles: this.ebookFiles.map(f => f.toJSON()),
|
ebookFiles: this.ebookFiles.map(f => f.toJSON()),
|
||||||
chapters: this.chapters.map(c => ({ ...c }))
|
chapters: this.chapters.map(c => ({ ...c }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toJSONMinified() {
|
||||||
|
return {
|
||||||
|
metadata: this.metadata.toJSON(),
|
||||||
|
coverPath: this.coverPath,
|
||||||
|
tags: [...this.tags],
|
||||||
|
numTracks: this.tracks.length,
|
||||||
|
numAudioFiles: this.audioFiles.length,
|
||||||
|
numEbooks: this.ebookFiles.length,
|
||||||
|
numChapters: this.chapters.length,
|
||||||
|
duration: this.duration,
|
||||||
|
size: this.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSONExpanded() {
|
||||||
|
return {
|
||||||
|
metadata: this.metadata.toJSONExpanded(),
|
||||||
|
coverPath: this.coverPath,
|
||||||
|
tags: [...this.tags],
|
||||||
|
audioFiles: this.audioFiles.map(f => f.toJSON()),
|
||||||
|
ebookFiles: this.ebookFiles.map(f => f.toJSON()),
|
||||||
|
chapters: this.chapters.map(c => ({ ...c })),
|
||||||
|
duration: this.duration,
|
||||||
|
size: this.size,
|
||||||
|
tracks: this.tracks.map(t => t.toJSON())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get tracks() {
|
||||||
|
return this.audioFiles.filter(af => !af.exclude && !af.invalid)
|
||||||
|
}
|
||||||
|
get duration() {
|
||||||
|
var total = 0
|
||||||
|
this.tracks.forEach((track) => total += track.duration)
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
get size() {
|
||||||
|
var total = 0
|
||||||
|
this.audioFiles.forEach((af) => total += af.metadata.size)
|
||||||
|
return total
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = Book
|
module.exports = Book
|
@ -6,8 +6,8 @@ class Podcast {
|
|||||||
this.id = null
|
this.id = null
|
||||||
|
|
||||||
this.metadata = null
|
this.metadata = null
|
||||||
this.cover = null
|
this.coverPath = null
|
||||||
this.coverFullPath = null
|
this.tags = []
|
||||||
this.episodes = []
|
this.episodes = []
|
||||||
|
|
||||||
this.createdAt = null
|
this.createdAt = null
|
||||||
@ -21,8 +21,8 @@ class Podcast {
|
|||||||
construct(podcast) {
|
construct(podcast) {
|
||||||
this.id = podcast.id
|
this.id = podcast.id
|
||||||
this.metadata = new PodcastMetadata(podcast.metadata)
|
this.metadata = new PodcastMetadata(podcast.metadata)
|
||||||
this.cover = podcast.cover
|
this.coverPath = podcast.coverPath
|
||||||
this.coverFullPath = podcast.coverFullPath
|
this.tags = [...podcast.tags]
|
||||||
this.episodes = podcast.episodes.map((e) => new PodcastEpisode(e))
|
this.episodes = podcast.episodes.map((e) => new PodcastEpisode(e))
|
||||||
this.createdAt = podcast.createdAt
|
this.createdAt = podcast.createdAt
|
||||||
this.lastUpdate = podcast.lastUpdate
|
this.lastUpdate = podcast.lastUpdate
|
||||||
@ -32,8 +32,32 @@ class Podcast {
|
|||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
metadata: this.metadata.toJSON(),
|
metadata: this.metadata.toJSON(),
|
||||||
cover: this.cover,
|
coverPath: this.coverPath,
|
||||||
coverFullPath: this.coverFullPath,
|
tags: [...this.tags],
|
||||||
|
episodes: this.episodes.map(e => e.toJSON()),
|
||||||
|
createdAt: this.createdAt,
|
||||||
|
lastUpdate: this.lastUpdate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSONMinified() {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
metadata: this.metadata.toJSON(),
|
||||||
|
coverPath: this.coverPath,
|
||||||
|
tags: [...this.tags],
|
||||||
|
episodes: this.episodes.map(e => e.toJSON()),
|
||||||
|
createdAt: this.createdAt,
|
||||||
|
lastUpdate: this.lastUpdate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSONExpanded() {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
metadata: this.metadata.toJSONExpanded(),
|
||||||
|
coverPath: this.coverPath,
|
||||||
|
tags: [...this.tags],
|
||||||
episodes: this.episodes.map(e => e.toJSON()),
|
episodes: this.episodes.map(e => e.toJSON()),
|
||||||
createdAt: this.createdAt,
|
createdAt: this.createdAt,
|
||||||
lastUpdate: this.lastUpdate
|
lastUpdate: this.lastUpdate
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
const globals = require('../../utils/globals')
|
||||||
const FileMetadata = require('../metadata/FileMetadata')
|
const FileMetadata = require('../metadata/FileMetadata')
|
||||||
|
|
||||||
class LibraryFile {
|
class LibraryFile {
|
||||||
@ -24,8 +25,18 @@ class LibraryFile {
|
|||||||
ino: this.ino,
|
ino: this.ino,
|
||||||
metadata: this.metadata.toJSON(),
|
metadata: this.metadata.toJSON(),
|
||||||
addedAt: this.addedAt,
|
addedAt: this.addedAt,
|
||||||
updatedAt: this.updatedAt
|
updatedAt: this.updatedAt,
|
||||||
|
fileType: this.fileType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get fileType() {
|
||||||
|
if (globals.SupportedImageTypes.includes(this.metadata.format)) return 'image'
|
||||||
|
if (globals.SupportedAudioTypes.includes(this.metadata.format)) return 'audio'
|
||||||
|
if (globals.SupportedEbookTypes.includes(this.metadata.format)) return 'ebook'
|
||||||
|
if (globals.TextFileTypes.includes(this.metadata.format)) return 'text'
|
||||||
|
if (globals.MetadataFileTypes.includes(this.metadata.format)) return 'metadata'
|
||||||
|
return 'unknown'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = LibraryFile
|
module.exports = LibraryFile
|
@ -1,5 +1,5 @@
|
|||||||
const { isNullOrNaN } = require('../utils/index')
|
const { isNullOrNaN } = require('../../utils/index')
|
||||||
const AudioFileMetadata = require('./metadata/AudioMetaTags')
|
const AudioFileMetadata = require('../metadata/AudioMetaTags')
|
||||||
|
|
||||||
class AudioFile {
|
class AudioFile {
|
||||||
constructor(data) {
|
constructor(data) {
|
@ -1,4 +1,4 @@
|
|||||||
var { bytesPretty } = require('../utils/fileUtils')
|
var { bytesPretty } = require('../../utils/fileUtils')
|
||||||
|
|
||||||
class AudioTrack {
|
class AudioTrack {
|
||||||
constructor(audioTrack = null) {
|
constructor(audioTrack = null) {
|
@ -1,12 +1,12 @@
|
|||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const fs = require('fs-extra')
|
const fs = require('fs-extra')
|
||||||
const { bytesPretty, readTextFile, getIno } = require('../utils/fileUtils')
|
const { bytesPretty, readTextFile, getIno } = require('../../utils/fileUtils')
|
||||||
const { comparePaths, getId, elapsedPretty } = require('../utils/index')
|
const { comparePaths, getId, elapsedPretty } = require('../../utils/index')
|
||||||
const { parseOpfMetadataXML } = require('../utils/parseOpfMetadata')
|
const { parseOpfMetadataXML } = require('../../utils/parseOpfMetadata')
|
||||||
const { extractCoverArt } = require('../utils/ffmpegHelpers')
|
const { extractCoverArt } = require('../../utils/ffmpegHelpers')
|
||||||
const nfoGenerator = require('../utils/nfoGenerator')
|
const nfoGenerator = require('../../utils/nfoGenerator')
|
||||||
const abmetadataGenerator = require('../utils/abmetadataGenerator')
|
const abmetadataGenerator = require('../../utils/abmetadataGenerator')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../../Logger')
|
||||||
const Book = require('./Book')
|
const Book = require('./Book')
|
||||||
const AudioTrack = require('./AudioTrack')
|
const AudioTrack = require('./AudioTrack')
|
||||||
const AudioFile = require('./AudioFile')
|
const AudioFile = require('./AudioFile')
|
@ -1,5 +1,5 @@
|
|||||||
const { getId } = require('../utils/index')
|
const { getId } = require('../../utils/index')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../../Logger')
|
||||||
|
|
||||||
class Author {
|
class Author {
|
||||||
constructor(author = null) {
|
constructor(author = null) {
|
@ -1,6 +1,6 @@
|
|||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../../Logger')
|
||||||
const parseAuthors = require('../utils/parseAuthors')
|
const parseAuthors = require('../../utils/parseAuthors')
|
||||||
|
|
||||||
class Book {
|
class Book {
|
||||||
constructor(book = null) {
|
constructor(book = null) {
|
@ -13,6 +13,7 @@ class BookMetadata {
|
|||||||
this.isbn = null
|
this.isbn = null
|
||||||
this.asin = null
|
this.asin = null
|
||||||
this.language = null
|
this.language = null
|
||||||
|
this.explicit = false
|
||||||
|
|
||||||
if (metadata) {
|
if (metadata) {
|
||||||
this.construct(metadata)
|
this.construct(metadata)
|
||||||
@ -33,6 +34,7 @@ class BookMetadata {
|
|||||||
this.isbn = metadata.isbn
|
this.isbn = metadata.isbn
|
||||||
this.asin = metadata.asin
|
this.asin = metadata.asin
|
||||||
this.language = metadata.language
|
this.language = metadata.language
|
||||||
|
this.explicit = metadata.explicit
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
@ -49,8 +51,54 @@ class BookMetadata {
|
|||||||
description: this.description,
|
description: this.description,
|
||||||
isbn: this.isbn,
|
isbn: this.isbn,
|
||||||
asin: this.asin,
|
asin: this.asin,
|
||||||
language: this.language
|
language: this.language,
|
||||||
|
explicit: this.explicit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toJSONExpanded() {
|
||||||
|
return {
|
||||||
|
title: this.title,
|
||||||
|
subtitle: this.subtitle,
|
||||||
|
authors: this.authors.map(a => ({ ...a })), // Author JSONMinimal with name and id
|
||||||
|
narrators: [...this.narrators],
|
||||||
|
series: this.series.map(s => ({ ...s })),
|
||||||
|
genres: [...this.genres],
|
||||||
|
publishedYear: this.publishedYear,
|
||||||
|
publishedDate: this.publishedDate,
|
||||||
|
publisher: this.publisher,
|
||||||
|
description: this.description,
|
||||||
|
isbn: this.isbn,
|
||||||
|
asin: this.asin,
|
||||||
|
language: this.language,
|
||||||
|
explicit: this.explicit,
|
||||||
|
authorName: this.authorName,
|
||||||
|
narratorName: this.narratorName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get titleIgnorePrefix() {
|
||||||
|
if (!this.title) return ''
|
||||||
|
if (this.title.toLowerCase().startsWith('the ')) {
|
||||||
|
return this.title.substr(4) + ', The'
|
||||||
|
}
|
||||||
|
return this.title
|
||||||
|
}
|
||||||
|
get authorName() {
|
||||||
|
return this.authors.map(au => au.name).join(', ')
|
||||||
|
}
|
||||||
|
get narratorName() {
|
||||||
|
return this.narrators.join(', ')
|
||||||
|
}
|
||||||
|
|
||||||
|
hasAuthor(authorName) {
|
||||||
|
return !!this.authors.find(au => au.name == authorName)
|
||||||
|
}
|
||||||
|
hasSeries(seriesName) {
|
||||||
|
return !!this.series.find(se => se.name == seriesName)
|
||||||
|
}
|
||||||
|
hasNarrator(narratorName) {
|
||||||
|
return this.narrators.includes(narratorName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = BookMetadata
|
module.exports = BookMetadata
|
@ -41,5 +41,10 @@ class FileMetadata {
|
|||||||
clone() {
|
clone() {
|
||||||
return new FileMetadata(this.toJSON())
|
return new FileMetadata(this.toJSON())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get format() {
|
||||||
|
if (!this.ext) return ''
|
||||||
|
return this.ext.slice(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = FileMetadata
|
module.exports = FileMetadata
|
@ -9,6 +9,7 @@ class PodcastMetadata {
|
|||||||
this.itunesPageUrl = null
|
this.itunesPageUrl = null
|
||||||
this.itunesId = null
|
this.itunesId = null
|
||||||
this.itunesArtistId = null
|
this.itunesArtistId = null
|
||||||
|
this.explicit = false
|
||||||
|
|
||||||
if (metadata) {
|
if (metadata) {
|
||||||
this.construct(metadata)
|
this.construct(metadata)
|
||||||
@ -25,6 +26,7 @@ class PodcastMetadata {
|
|||||||
this.itunesPageUrl = metadata.itunesPageUrl
|
this.itunesPageUrl = metadata.itunesPageUrl
|
||||||
this.itunesId = metadata.itunesId
|
this.itunesId = metadata.itunesId
|
||||||
this.itunesArtistId = metadata.itunesArtistId
|
this.itunesArtistId = metadata.itunesArtistId
|
||||||
|
this.explicit = metadata.explicit
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
@ -38,7 +40,12 @@ class PodcastMetadata {
|
|||||||
itunesPageUrl: this.itunesPageUrl,
|
itunesPageUrl: this.itunesPageUrl,
|
||||||
itunesId: this.itunesId,
|
itunesId: this.itunesId,
|
||||||
itunesArtistId: this.itunesArtistId,
|
itunesArtistId: this.itunesArtistId,
|
||||||
|
explicit: this.explicit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toJSONExpanded() {
|
||||||
|
return this.toJSON()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = PodcastMetadata
|
module.exports = PodcastMetadata
|
@ -1,4 +1,4 @@
|
|||||||
const AudioFileMetadata = require('../objects/AudioFileMetadata')
|
const AudioFileMetadata = require('../objects/metadata/AudioMetaTags')
|
||||||
|
|
||||||
class AudioProbeData {
|
class AudioProbeData {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -10,7 +10,7 @@ const { ScanResult, LogLevel } = require('../utils/constants')
|
|||||||
|
|
||||||
const AudioFileScanner = require('./AudioFileScanner')
|
const AudioFileScanner = require('./AudioFileScanner')
|
||||||
const BookFinder = require('../finders/BookFinder')
|
const BookFinder = require('../finders/BookFinder')
|
||||||
const Audiobook = require('../objects/Audiobook')
|
const Audiobook = require('../objects/legacy/Audiobook')
|
||||||
const LibraryScan = require('./LibraryScan')
|
const LibraryScan = require('./LibraryScan')
|
||||||
const ScanOptions = require('./ScanOptions')
|
const ScanOptions = require('./ScanOptions')
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ const fs = require('fs-extra')
|
|||||||
const njodb = require("njodb")
|
const njodb = require("njodb")
|
||||||
|
|
||||||
const { SupportedEbookTypes } = require('./globals')
|
const { SupportedEbookTypes } = require('./globals')
|
||||||
const Audiobook = require('../objects/Audiobook')
|
const Audiobook = require('../objects/legacy/Audiobook')
|
||||||
const LibraryItem = require('../objects/LibraryItem')
|
const LibraryItem = require('../objects/LibraryItem')
|
||||||
|
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
@ -142,7 +142,7 @@ function makeLibraryItemFromOldAb(audiobook) {
|
|||||||
libraryItem.lastScan = audiobook.lastScan
|
libraryItem.lastScan = audiobook.lastScan
|
||||||
libraryItem.scanVersion = audiobook.scanVersion
|
libraryItem.scanVersion = audiobook.scanVersion
|
||||||
libraryItem.isMissing = audiobook.isMissing
|
libraryItem.isMissing = audiobook.isMissing
|
||||||
libraryItem.entityType = 'book'
|
libraryItem.mediaType = 'book'
|
||||||
|
|
||||||
var bookEntity = new Book()
|
var bookEntity = new Book()
|
||||||
var bookMetadata = new BookMetadata(audiobook.book)
|
var bookMetadata = new BookMetadata(audiobook.book)
|
||||||
@ -159,8 +159,6 @@ function makeLibraryItemFromOldAb(audiobook) {
|
|||||||
|
|
||||||
bookEntity.metadata = bookMetadata
|
bookEntity.metadata = bookMetadata
|
||||||
bookEntity.coverPath = audiobook.book.coverFullPath
|
bookEntity.coverPath = audiobook.book.coverFullPath
|
||||||
// Path relative to library item
|
|
||||||
bookEntity.relCoverPath = getRelativePath(audiobook.book.coverFullPath, audiobook.fullPath)
|
|
||||||
bookEntity.tags = [...audiobook.tags]
|
bookEntity.tags = [...audiobook.tags]
|
||||||
|
|
||||||
var payload = makeFilesFromOldAb(audiobook)
|
var payload = makeFilesFromOldAb(audiobook)
|
||||||
@ -171,7 +169,7 @@ function makeLibraryItemFromOldAb(audiobook) {
|
|||||||
bookEntity.chapters = audiobook.chapters.map(c => ({ ...c }))
|
bookEntity.chapters = audiobook.chapters.map(c => ({ ...c }))
|
||||||
}
|
}
|
||||||
|
|
||||||
libraryItem.entity = bookEntity
|
libraryItem.media = bookEntity
|
||||||
libraryItem.libraryFiles = payload.libraryFiles
|
libraryItem.libraryFiles = payload.libraryFiles
|
||||||
return libraryItem
|
return libraryItem
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
const globals = {
|
const globals = {
|
||||||
SupportedImageTypes: ['png', 'jpg', 'jpeg', 'webp'],
|
SupportedImageTypes: ['png', 'jpg', 'jpeg', 'webp'],
|
||||||
SupportedAudioTypes: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'mp4', 'aac'],
|
SupportedAudioTypes: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'mp4', 'aac'],
|
||||||
SupportedEbookTypes: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz']
|
SupportedEbookTypes: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
|
||||||
|
TextFileTypes: ['txt', 'nfo'],
|
||||||
|
MetadataFileTypes: ['opf', 'abs']
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = globals
|
module.exports = globals
|
||||||
|
@ -8,6 +8,45 @@ module.exports = {
|
|||||||
return Buffer.from(decodeURIComponent(text), 'base64').toString()
|
return Buffer.from(decodeURIComponent(text), 'base64').toString()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getFilteredLibraryItems(libraryItems, filterBy, user) {
|
||||||
|
var filtered = libraryItems
|
||||||
|
|
||||||
|
var searchGroups = ['genres', 'tags', 'series', 'authors', 'progress', 'narrators', 'languages']
|
||||||
|
var group = searchGroups.find(_group => filterBy.startsWith(_group + '.'))
|
||||||
|
if (group) {
|
||||||
|
var filterVal = filterBy.replace(`${group}.`, '')
|
||||||
|
var filter = this.decode(filterVal)
|
||||||
|
if (group === 'genres') filtered = filtered.filter(li => li.media.metadata && li.media.metadata.genres.includes(filter))
|
||||||
|
else if (group === 'tags') filtered = filtered.filter(li => li.media.tags.includes(filter))
|
||||||
|
else if (group === 'series') {
|
||||||
|
if (filter === 'No Series') filtered = filtered.filter(li => li.media.metadata && !li.media.metadata.series.length)
|
||||||
|
else filtered = filtered.filter(li => li.media.metadata && li.media.metadata.hasSeries(filter))
|
||||||
|
}
|
||||||
|
else if (group === 'authors') filtered = filtered.filter(li => li.media.metadata && li.media.metadata.hasAuthor(filter))
|
||||||
|
else if (group === 'narrators') filtered = filtered.filter(li => li.media.metadata && li.media.metadata.hasNarrator(filter))
|
||||||
|
else if (group === 'progress') {
|
||||||
|
filtered = filtered.filter(li => {
|
||||||
|
var userAudiobook = user.getAudiobookJSON(li.id)
|
||||||
|
var isRead = userAudiobook && userAudiobook.isRead
|
||||||
|
if (filter === 'Read' && isRead) return true
|
||||||
|
if (filter === 'Unread' && !isRead) return true
|
||||||
|
if (filter === 'In Progress' && (userAudiobook && !userAudiobook.isRead && userAudiobook.progress > 0)) return true
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
} else if (group === 'languages') {
|
||||||
|
filtered = filtered.filter(li => li.media.metadata && li.media.metadata.language === filter)
|
||||||
|
}
|
||||||
|
} else if (filterBy === 'issues') {
|
||||||
|
filtered = filtered.filter(ab => {
|
||||||
|
// TODO: Update filter for issues
|
||||||
|
return ab.isMissing
|
||||||
|
// return ab.numMissingParts || ab.numInvalidParts || ab.isMissing || ab.isInvalid
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
},
|
||||||
|
|
||||||
getFiltered(audiobooks, filterBy, user) {
|
getFiltered(audiobooks, filterBy, user) {
|
||||||
var filtered = audiobooks
|
var filtered = audiobooks
|
||||||
|
|
||||||
@ -45,6 +84,55 @@ module.exports = {
|
|||||||
return filtered
|
return filtered
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getDistinctFilterDataNew(libraryItems) {
|
||||||
|
var data = {
|
||||||
|
authors: [],
|
||||||
|
genres: [],
|
||||||
|
tags: [],
|
||||||
|
series: [],
|
||||||
|
narrators: [],
|
||||||
|
languages: []
|
||||||
|
}
|
||||||
|
libraryItems.forEach((li) => {
|
||||||
|
var mediaMetadata = li.media.metadata
|
||||||
|
if (mediaMetadata.authors.length) {
|
||||||
|
mediaMetadata.authors.forEach((author) => {
|
||||||
|
if (author && !data.authors.includes(author.name)) data.authors.push(author.name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (mediaMetadata.series.length) {
|
||||||
|
mediaMetadata.series.forEach((series) => {
|
||||||
|
if (series && !data.series.includes(series.name)) data.series.push(series.name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (mediaMetadata.genres.length) {
|
||||||
|
mediaMetadata.genres.forEach((genre) => {
|
||||||
|
if (genre && !data.genres.includes(genre)) data.genres.push(genre)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (li.media.tags.length) {
|
||||||
|
li.media.tags.forEach((tag) => {
|
||||||
|
if (tag && !data.tags.includes(tag)) data.tags.push(tag)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (mediaMetadata.narrators.length) {
|
||||||
|
mediaMetadata.narrators.forEach((narrator) => {
|
||||||
|
if (narrator && !data.narrators.includes(narrator)) data.narrators.push(narrator)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (mediaMetadata.language && !data.languages.includes(mediaMetadata.language)) data.languages.push(mediaMetadata.language)
|
||||||
|
})
|
||||||
|
data.authors = naturalSort(data.authors).asc()
|
||||||
|
data.genres = naturalSort(data.genres).asc()
|
||||||
|
data.tags = naturalSort(data.tags).asc()
|
||||||
|
data.series = naturalSort(data.series).asc()
|
||||||
|
data.narrators = naturalSort(data.narrators).asc()
|
||||||
|
data.languages = naturalSort(data.languages).asc()
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
// TODO: Remove legacy
|
||||||
getDistinctFilterData(audiobooks) {
|
getDistinctFilterData(audiobooks) {
|
||||||
var data = {
|
var data = {
|
||||||
authors: [],
|
authors: [],
|
||||||
@ -246,9 +334,11 @@ module.exports = {
|
|||||||
return totalSize
|
return totalSize
|
||||||
},
|
},
|
||||||
|
|
||||||
getNumIssues(books) {
|
getNumIssues(libraryItems) {
|
||||||
return books.filter(ab => {
|
// TODO: Implement issues
|
||||||
return ab.numMissingParts || ab.numInvalidParts || ab.isMissing || ab.isInvalid
|
return libraryItems.filter(li => li.isMissing).length
|
||||||
}).length
|
// return books.filter(ab => {
|
||||||
|
// return ab.numMissingParts || ab.numInvalidParts || ab.isMissing || ab.isInvalid
|
||||||
|
// }).length
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user