mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-01-13 09:28:20 +01: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() {
|
||||
var categories = await this.$axios
|
||||
.$get(`/api/libraries/${this.currentLibraryId}/categories?minified=1`)
|
||||
.$get(`/api/libraries/${this.currentLibraryId}/personalized?minified=1`)
|
||||
.then((data) => {
|
||||
console.log('Personalized data', data)
|
||||
return data
|
||||
})
|
||||
.catch((error) => {
|
||||
|
@ -239,7 +239,7 @@ export default {
|
||||
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 fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1`
|
||||
|
||||
|
@ -8,7 +8,7 @@
|
||||
<!-- Alternative bookshelf title/author/sort -->
|
||||
<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' }">
|
||||
<span v-if="volumeNumber">#{{ volumeNumber }} </span>{{ displayTitle }}
|
||||
{{ displayTitle }}
|
||||
</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>
|
||||
@ -21,7 +21,7 @@
|
||||
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }" class="font-book text-gray-300 text-center">{{ title }}</p>
|
||||
</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 -->
|
||||
<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() {
|
||||
return this.store.state.showExperimentalFeatures
|
||||
},
|
||||
_audiobook() {
|
||||
_libraryItem() {
|
||||
return this.audiobook || {}
|
||||
},
|
||||
book() {
|
||||
return this._audiobook.book || {}
|
||||
media() {
|
||||
return this._libraryItem.media || {}
|
||||
},
|
||||
mediaMetadata() {
|
||||
return this.media.metadata || {}
|
||||
},
|
||||
placeholderUrl() {
|
||||
return '/book_placeholder.jpg'
|
||||
},
|
||||
bookCoverSrc() {
|
||||
return this.store.getters['audiobooks/getBookCoverSrc'](this._audiobook, this.placeholderUrl)
|
||||
return this.store.getters['audiobooks/getLibraryItemCoverSrc'](this._libraryItem, this.placeholderUrl)
|
||||
},
|
||||
audiobookId() {
|
||||
return this._audiobook.id
|
||||
libraryItemId() {
|
||||
return this._libraryItem.id
|
||||
},
|
||||
series() {
|
||||
return this.book.series
|
||||
return this.mediaMetadata.series
|
||||
},
|
||||
libraryId() {
|
||||
return this._audiobook.libraryId
|
||||
return this._libraryItem.libraryId
|
||||
},
|
||||
hasEbook() {
|
||||
return this._audiobook.numEbooks
|
||||
return this.media.numEbooks
|
||||
},
|
||||
hasTracks() {
|
||||
return this._audiobook.numTracks
|
||||
return this.media.numTracks
|
||||
},
|
||||
processingBatch() {
|
||||
return this.store.state.processingBatch
|
||||
},
|
||||
booksInSeries() {
|
||||
// Only added to audiobook object when collapseSeries is enabled
|
||||
return this._audiobook.booksInSeries
|
||||
return this._libraryItem.booksInSeries
|
||||
},
|
||||
hasCover() {
|
||||
return !!this.book.cover
|
||||
return !!this.media.coverPath
|
||||
},
|
||||
squareAspectRatio() {
|
||||
return this.bookCoverAspectRatio === 1
|
||||
@ -181,43 +184,49 @@ export default {
|
||||
return this.width / baseSize
|
||||
},
|
||||
title() {
|
||||
return this.book.title || ''
|
||||
return this.mediaMetadata.title || ''
|
||||
},
|
||||
playIconFontSize() {
|
||||
return Math.max(2, 3 * this.sizeMultiplier)
|
||||
},
|
||||
author() {
|
||||
return this.book.author
|
||||
authors() {
|
||||
return this.mediaMetadata.authors || []
|
||||
},
|
||||
authorFL() {
|
||||
return this.book.authorFL || this.author
|
||||
author() {
|
||||
return this.authors.map((au) => au.name).join(', ')
|
||||
},
|
||||
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() {
|
||||
return this.book.volumeNumber || null
|
||||
return this.mediaMetadata.volumeNumber || null
|
||||
},
|
||||
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
|
||||
},
|
||||
displayAuthor() {
|
||||
if (this.orderBy === 'book.authorLF') return this.authorLF
|
||||
return this.authorFL
|
||||
if (this.orderBy === 'media.metadata.authorLF') return this.authorLF
|
||||
return this.author
|
||||
},
|
||||
displaySortLine() {
|
||||
if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._audiobook.mtimeMs)
|
||||
if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._audiobook.birthtimeMs)
|
||||
if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._audiobook.addedAt)
|
||||
if (this.orderBy === 'duration') return 'Duration: ' + this.$elapsedPrettyExtended(this._audiobook.duration, false)
|
||||
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._audiobook.size)
|
||||
if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs)
|
||||
if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs)
|
||||
if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._libraryItem.addedAt)
|
||||
if (this.orderBy === 'duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false)
|
||||
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
|
||||
return null
|
||||
},
|
||||
userProgress() {
|
||||
return this.store.getters['user/getUserAudiobook'](this.audiobookId)
|
||||
return this.store.getters['user/getUserAudiobook'](this.libraryItemId)
|
||||
},
|
||||
userProgressPercent() {
|
||||
return this.userProgress ? this.userProgress.progress || 0 : 0
|
||||
@ -229,7 +238,7 @@ export default {
|
||||
return this.hasMissingParts || this.hasInvalidParts || this.isMissing || this.isInvalid
|
||||
},
|
||||
isStreaming() {
|
||||
return this.store.getters['getAudiobookIdStreaming'] === this.audiobookId
|
||||
return this.store.getters['getlibraryItemIdStreaming'] === this.libraryItemId
|
||||
},
|
||||
showReadButton() {
|
||||
return !this.isSelectionMode && this.showExperimentalFeatures && !this.showPlayButton && this.hasEbook
|
||||
@ -241,16 +250,16 @@ export default {
|
||||
return !this.isSelectionMode && this.showExperimentalFeatures && this.hasEbook
|
||||
},
|
||||
isMissing() {
|
||||
return this._audiobook.isMissing
|
||||
return this._libraryItem.isMissing
|
||||
},
|
||||
isInvalid() {
|
||||
return this._audiobook.isInvalid
|
||||
return this._libraryItem.isInvalid
|
||||
},
|
||||
hasMissingParts() {
|
||||
return this._audiobook.hasMissingParts
|
||||
return this._libraryItem.hasMissingParts
|
||||
},
|
||||
hasInvalidParts() {
|
||||
return this._audiobook.hasInvalidParts
|
||||
return this._libraryItem.hasInvalidParts
|
||||
},
|
||||
errorText() {
|
||||
if (this.isMissing) return 'Audiobook directory is missing!'
|
||||
@ -349,11 +358,11 @@ export default {
|
||||
return this.title
|
||||
},
|
||||
authorCleaned() {
|
||||
if (!this.authorFL) return ''
|
||||
if (this.authorFL.length > 30) {
|
||||
return this.authorFL.slice(0, 27) + '...'
|
||||
if (!this.author) return ''
|
||||
if (this.author.length > 30) {
|
||||
return this.author.slice(0, 27) + '...'
|
||||
}
|
||||
return this.authorFL
|
||||
return this.author
|
||||
},
|
||||
isAlternativeBookshelfView() {
|
||||
var constants = this.$constants || this.$nuxt.$constants
|
||||
@ -382,7 +391,7 @@ export default {
|
||||
var router = this.$router || this.$nuxt.$router
|
||||
if (router) {
|
||||
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 axios = this.$axios || this.$nuxt.$axios
|
||||
axios
|
||||
.$patch(`/api/me/audiobook/${this.audiobookId}`, updatePayload)
|
||||
.$patch(`/api/me/audiobook/${this.libraryItemId}`, updatePayload)
|
||||
.then(() => {
|
||||
this.isProcessingReadUpdate = false
|
||||
toast.success(`"${this.title}" Marked as ${updatePayload.isRead ? 'Read' : 'Not Read'}`)
|
||||
@ -425,7 +434,7 @@ export default {
|
||||
rescan() {
|
||||
this.rescanning = true
|
||||
this._socket.once('audiobook_scan_complete', this.audiobookScanComplete)
|
||||
this._socket.emit('scan_audiobook', this.audiobookId)
|
||||
this._socket.emit('scan_libraryItem', this.libraryItemId)
|
||||
},
|
||||
showEditModalTracks() {
|
||||
// More menu func
|
||||
@ -503,7 +512,7 @@ export default {
|
||||
},
|
||||
play() {
|
||||
var eventBus = this.$eventBus || this.$nuxt.$eventBus
|
||||
eventBus.$emit('play-audiobook', this.audiobookId)
|
||||
eventBus.$emit('play-audiobook', this.libraryItemId)
|
||||
},
|
||||
mouseover() {
|
||||
this.isHovering = true
|
||||
|
@ -34,27 +34,23 @@ export default {
|
||||
items: [
|
||||
{
|
||||
text: 'Title',
|
||||
value: 'book.title'
|
||||
value: 'media.metadata.title'
|
||||
},
|
||||
{
|
||||
text: 'Author (First Last)',
|
||||
value: 'book.authorFL'
|
||||
value: 'media.metadata.author'
|
||||
},
|
||||
{
|
||||
text: 'Author (Last, First)',
|
||||
value: 'book.authorLF'
|
||||
value: 'media.metadata.authorLF'
|
||||
},
|
||||
{
|
||||
text: 'Added At',
|
||||
value: 'addedAt'
|
||||
},
|
||||
{
|
||||
text: 'Volume #',
|
||||
value: 'book.volumeNumber'
|
||||
},
|
||||
{
|
||||
text: 'Duration',
|
||||
value: 'duration'
|
||||
value: 'media.duration'
|
||||
},
|
||||
{
|
||||
text: 'Size',
|
||||
@ -89,7 +85,8 @@ export default {
|
||||
}
|
||||
},
|
||||
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)
|
||||
if (!_sel) return ''
|
||||
return _sel.text
|
||||
|
@ -5,8 +5,8 @@
|
||||
<div class="absolute cover-bg" ref="coverBg" />
|
||||
</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'" />
|
||||
<div v-show="loading && audiobook" class="absolute top-0 left-0 h-full w-full flex items-center justify-center">
|
||||
<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 && 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>
|
||||
<div class="absolute top-2 right-2">
|
||||
<div class="la-ball-spin-clockwise la-sm">
|
||||
@ -44,11 +44,10 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
audiobook: {
|
||||
libraryItem: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
authorOverride: String,
|
||||
width: {
|
||||
type: Number,
|
||||
default: 120
|
||||
@ -75,12 +74,15 @@ export default {
|
||||
height() {
|
||||
return this.width * this.bookCoverAspectRatio
|
||||
},
|
||||
book() {
|
||||
if (!this.audiobook) return {}
|
||||
return this.audiobook.book || {}
|
||||
media() {
|
||||
if (!this.libraryItem) return {}
|
||||
return this.libraryItem.media || {}
|
||||
},
|
||||
mediaMetadata() {
|
||||
return this.media.metadata || {}
|
||||
},
|
||||
title() {
|
||||
return this.book.title || 'No Title'
|
||||
return this.mediaMetadata.title || 'No Title'
|
||||
},
|
||||
titleCleaned() {
|
||||
if (this.title.length > 60) {
|
||||
@ -88,9 +90,11 @@ export default {
|
||||
}
|
||||
return this.title
|
||||
},
|
||||
authors() {
|
||||
return this.mediaMetadata.authors || []
|
||||
},
|
||||
author() {
|
||||
if (this.authorOverride) return this.authorOverride
|
||||
return this.book.author || 'Unknown'
|
||||
return this.authors.map((au) => au.name).join(', ')
|
||||
},
|
||||
authorCleaned() {
|
||||
if (this.author.length > 30) {
|
||||
@ -102,15 +106,15 @@ export default {
|
||||
return '/book_placeholder.jpg'
|
||||
},
|
||||
fullCoverUrl() {
|
||||
if (!this.audiobook) return null
|
||||
if (!this.libraryItem) return null
|
||||
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() {
|
||||
return this.book.cover || this.placeholderUrl
|
||||
return this.media.coverPath || this.placeholderUrl
|
||||
},
|
||||
hasCover() {
|
||||
return !!this.book.cover
|
||||
return !!this.media.coverPath
|
||||
},
|
||||
sizeMultiplier() {
|
||||
var baseSize = this.squareAspectRatio ? 192 : 120
|
||||
@ -138,7 +142,6 @@ export default {
|
||||
this.$refs.coverBg.style.backgroundImage = `url("${this.fullCoverUrl}")`
|
||||
}
|
||||
},
|
||||
hideCoverBg() {},
|
||||
imageLoaded() {
|
||||
this.loading = false
|
||||
this.$nextTick(() => {
|
||||
|
@ -63,7 +63,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
getCoverUrl(book) {
|
||||
return this.store.getters['audiobooks/getBookCoverSrc'](book, '')
|
||||
return this.store.getters['audiobooks/getLibraryItemCoverSrc'](book, '')
|
||||
},
|
||||
async buildCoverImg(coverData, bgCoverWidth, offsetLeft, zIndex, forceCoverBg = false) {
|
||||
var src = coverData.coverUrl
|
||||
|
@ -22,7 +22,7 @@ export default {
|
||||
return '/book_placeholder.jpg'
|
||||
},
|
||||
fullCoverUrl() {
|
||||
return this.$store.getters['audiobooks/getBookCoverSrc'](this.audiobook, this.placeholderUrl)
|
||||
return this.$store.getters['audiobooks/getLibraryItemCoverSrc'](this.audiobook, this.placeholderUrl)
|
||||
},
|
||||
hasCover() {
|
||||
return !!this.audiobook.book.cover
|
||||
|
@ -19,7 +19,7 @@
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</modals-modal>
|
||||
</template>
|
||||
@ -29,7 +29,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
processing: false,
|
||||
audiobook: null,
|
||||
libraryItem: null,
|
||||
fetchOnShow: false,
|
||||
tabs: [
|
||||
{
|
||||
@ -42,11 +42,6 @@ export default {
|
||||
title: 'Cover',
|
||||
component: 'modals-edit-tabs-cover'
|
||||
},
|
||||
// {
|
||||
// id: 'tracks',
|
||||
// title: 'Tracks',
|
||||
// component: 'modals-edit-tabs-tracks'
|
||||
// },
|
||||
{
|
||||
id: 'chapters',
|
||||
title: 'Chapters',
|
||||
@ -89,12 +84,12 @@ export default {
|
||||
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()
|
||||
return
|
||||
}
|
||||
this.fetchOnShow = false
|
||||
this.audiobook = null
|
||||
this.libraryItem = null
|
||||
this.init()
|
||||
this.registerListeners()
|
||||
} else {
|
||||
@ -148,26 +143,29 @@ export default {
|
||||
return _tab ? _tab.component : ''
|
||||
},
|
||||
isMissing() {
|
||||
return this.selectedAudiobook.isMissing
|
||||
return this.selectedLibraryItem.isMissing
|
||||
},
|
||||
selectedAudiobook() {
|
||||
return this.$store.state.selectedAudiobook || {}
|
||||
selectedLibraryItem() {
|
||||
return this.$store.state.selectedLibraryItem || {}
|
||||
},
|
||||
selectedAudiobookId() {
|
||||
return this.selectedAudiobook.id
|
||||
selectedLibraryItemId() {
|
||||
return this.selectedLibraryItem.id
|
||||
},
|
||||
book() {
|
||||
return this.audiobook ? this.audiobook.book || {} : {}
|
||||
media() {
|
||||
return this.libraryItem ? this.libraryItem.media || {} : {}
|
||||
},
|
||||
mediaMetadata() {
|
||||
return this.media.metadata || {}
|
||||
},
|
||||
title() {
|
||||
return this.book.title || 'No Title'
|
||||
return this.mediaMetadata.title || 'No Title'
|
||||
},
|
||||
bookshelfBookIds() {
|
||||
return this.$store.state.bookshelfBookIds || []
|
||||
},
|
||||
currentBookshelfIndex() {
|
||||
if (!this.bookshelfBookIds.length) return 0
|
||||
return this.bookshelfBookIds.findIndex((bid) => bid === this.selectedAudiobookId)
|
||||
return this.bookshelfBookIds.findIndex((bid) => bid === this.selectedLibraryItemId)
|
||||
},
|
||||
canGoPrev() {
|
||||
return this.bookshelfBookIds.length && this.currentBookshelfIndex > 0
|
||||
@ -188,7 +186,7 @@ export default {
|
||||
})
|
||||
this.processing = false
|
||||
if (prevBook) {
|
||||
this.$store.commit('showEditModalOnTab', { audiobook: prevBook, tab: this.selectedTab })
|
||||
this.$store.commit('showEditModalOnTab', { libraryItem: prevBook, tab: this.selectedTab })
|
||||
this.$nextTick(this.init)
|
||||
} else {
|
||||
console.error('Book not found', prevBookId)
|
||||
@ -205,7 +203,7 @@ export default {
|
||||
})
|
||||
this.processing = false
|
||||
if (nextBook) {
|
||||
this.$store.commit('showEditModalOnTab', { audiobook: nextBook, tab: this.selectedTab })
|
||||
this.$store.commit('showEditModalOnTab', { libraryItem: nextBook, tab: this.selectedTab })
|
||||
this.$nextTick(this.init)
|
||||
} else {
|
||||
console.error('Book not found', nextBookId)
|
||||
@ -223,16 +221,16 @@ export default {
|
||||
}
|
||||
},
|
||||
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()
|
||||
},
|
||||
async fetchFull() {
|
||||
try {
|
||||
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
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch audiobook', this.selectedAudiobookId, error)
|
||||
console.error('Failed to fetch audiobook', this.selectedLibraryItemId, error)
|
||||
this.processing = false
|
||||
this.show = false
|
||||
}
|
||||
|
@ -31,7 +31,7 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
audiobook: {
|
||||
libraryItem: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
@ -42,7 +42,7 @@ export default {
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
audiobook: {
|
||||
libraryItem: {
|
||||
immediate: true,
|
||||
handler(newVal) {
|
||||
if (newVal) this.init()
|
||||
@ -52,7 +52,7 @@ export default {
|
||||
computed: {},
|
||||
methods: {
|
||||
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="flex">
|
||||
<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 -->
|
||||
<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="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>
|
||||
@ -82,7 +82,7 @@
|
||||
export default {
|
||||
props: {
|
||||
processing: Boolean,
|
||||
audiobook: {
|
||||
libraryItem: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
@ -102,7 +102,7 @@ export default {
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
audiobook: {
|
||||
libraryItem: {
|
||||
immediate: true,
|
||||
handler(newVal) {
|
||||
if (newVal) {
|
||||
@ -134,17 +134,17 @@ export default {
|
||||
bookCoverAspectRatio() {
|
||||
return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE ? 1 : 1.6
|
||||
},
|
||||
audiobookId() {
|
||||
return this.audiobook ? this.audiobook.id : null
|
||||
libraryItemId() {
|
||||
return this.libraryItem ? this.libraryItem.id : null
|
||||
},
|
||||
book() {
|
||||
return this.audiobook ? this.audiobook.book || {} : {}
|
||||
media() {
|
||||
return this.libraryItem ? this.libraryItem.media || {} : {}
|
||||
},
|
||||
audiobookPath() {
|
||||
return this.audiobook ? this.audiobook.path : null
|
||||
mediaMetadata() {
|
||||
return this.media.metadata || {}
|
||||
},
|
||||
otherFiles() {
|
||||
return this.audiobook ? this.audiobook.otherFiles || [] : []
|
||||
libraryFiles() {
|
||||
return this.libraryItem ? this.libraryItem.libraryFiles || [] : []
|
||||
},
|
||||
userCanUpload() {
|
||||
return this.$store.getters['user/getUserCanUpload']
|
||||
@ -153,12 +153,11 @@ export default {
|
||||
return this.$store.getters['user/getToken']
|
||||
},
|
||||
localCovers() {
|
||||
return this.otherFiles
|
||||
.filter((f) => f.filetype === 'image')
|
||||
return this.libraryFiles
|
||||
.filter((f) => f.fileType === 'image')
|
||||
.map((file) => {
|
||||
var _file = { ...file }
|
||||
var imgRelPath = _file.path.replace(this.audiobookPath, '')
|
||||
_file.localPath = `/s/book/${this.audiobookId}/${imgRelPath}`
|
||||
_file.localPath = `/s/item/${this.libraryItemId}/${this.$encodeUriPath(file.metadata.relPath)}`
|
||||
return _file
|
||||
})
|
||||
}
|
||||
@ -170,7 +169,7 @@ export default {
|
||||
form.set('cover', this.selectedFile)
|
||||
|
||||
this.$axios
|
||||
.$post(`/api/books/${this.audiobook.id}/cover`, form)
|
||||
.$post(`/api/books/${this.libraryItemId}/cover`, form)
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
this.$toast.error(data.error)
|
||||
@ -203,17 +202,17 @@ export default {
|
||||
},
|
||||
init() {
|
||||
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.hasSearched = false
|
||||
}
|
||||
this.imageUrl = this.book.cover || ''
|
||||
this.searchTitle = this.book.title || ''
|
||||
this.searchAuthor = this.book.authorFL || ''
|
||||
this.imageUrl = this.media.coverPath || ''
|
||||
this.searchTitle = this.mediaMetadata.title || ''
|
||||
this.searchAuthor = this.mediaMetadata.authorName || ''
|
||||
this.provider = localStorage.getItem('book-provider') || 'openlibrary'
|
||||
},
|
||||
removeCover() {
|
||||
if (!this.book.cover) {
|
||||
if (!this.media.coverPath) {
|
||||
this.imageUrl = ''
|
||||
return
|
||||
}
|
||||
@ -223,7 +222,7 @@ export default {
|
||||
this.updateCover(this.imageUrl)
|
||||
},
|
||||
async updateCover(cover) {
|
||||
if (cover === this.book.cover) {
|
||||
if (cover === this.media.coverPath) {
|
||||
console.warn('Cover has not changed..', cover)
|
||||
return
|
||||
}
|
||||
@ -233,7 +232,7 @@ export default {
|
||||
|
||||
// Download cover from url and use
|
||||
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)
|
||||
if (error.response && error.response.data) {
|
||||
this.$toast.error(error.response.data)
|
||||
@ -247,7 +246,7 @@ export default {
|
||||
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)
|
||||
if (error.response && error.response.data) {
|
||||
this.$toast.error(error.response.data)
|
||||
@ -259,7 +258,7 @@ export default {
|
||||
this.$toast.success('Update Successful')
|
||||
this.$emit('close')
|
||||
} else {
|
||||
this.imageUrl = this.book.cover || ''
|
||||
this.imageUrl = this.media.coverPath || ''
|
||||
}
|
||||
this.isProcessing = false
|
||||
},
|
||||
@ -292,7 +291,7 @@ export default {
|
||||
setCover(coverFile) {
|
||||
this.isProcessing = true
|
||||
this.$axios
|
||||
.$patch(`/api/books/${this.audiobook.id}/coverfile`, coverFile)
|
||||
.$patch(`/api/books/${this.libraryItemId}/coverfile`, coverFile)
|
||||
.then((data) => {
|
||||
console.log('response data', data)
|
||||
if (data && typeof data === 'string') {
|
||||
|
@ -13,7 +13,8 @@
|
||||
|
||||
<div class="flex mt-2 -mx-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 class="flex-grow px-1">
|
||||
<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="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 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>
|
||||
|
||||
@ -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="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" />
|
||||
|
||||
@ -91,7 +93,7 @@
|
||||
export default {
|
||||
props: {
|
||||
processing: Boolean,
|
||||
audiobook: {
|
||||
libraryItem: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
@ -105,7 +107,6 @@ export default {
|
||||
author: null,
|
||||
narrator: null,
|
||||
series: null,
|
||||
volumeNumber: null,
|
||||
publishYear: null,
|
||||
publisher: null,
|
||||
language: null,
|
||||
@ -122,7 +123,7 @@ export default {
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
audiobook: {
|
||||
libraryItem: {
|
||||
immediate: true,
|
||||
handler(newVal) {
|
||||
if (newVal) this.init()
|
||||
@ -142,13 +143,16 @@ export default {
|
||||
return this.$store.getters['user/getIsRoot']
|
||||
},
|
||||
isMissing() {
|
||||
return !!this.audiobook && !!this.audiobook.isMissing
|
||||
return !!this.libraryItem && !!this.libraryItem.isMissing
|
||||
},
|
||||
audiobookId() {
|
||||
return this.audiobook ? this.audiobook.id : null
|
||||
libraryItemId() {
|
||||
return this.libraryItem ? this.libraryItem.id : null
|
||||
},
|
||||
book() {
|
||||
return this.audiobook ? this.audiobook.book || {} : {}
|
||||
media() {
|
||||
return this.libraryItem ? this.libraryItem.media || {} : {}
|
||||
},
|
||||
mediaMetadata() {
|
||||
return this.media.metadata || {}
|
||||
},
|
||||
userCanDelete() {
|
||||
return this.$store.getters['user/getUserCanDelete']
|
||||
@ -166,7 +170,7 @@ export default {
|
||||
return this.$store.state.libraries.filterData || {}
|
||||
},
|
||||
libraryId() {
|
||||
return this.audiobook ? this.audiobook.libraryId : null
|
||||
return this.libraryItem ? this.libraryItem.libraryId : null
|
||||
},
|
||||
libraryProvider() {
|
||||
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
|
||||
}
|
||||
this.$axios
|
||||
.$post(`/api/books/${this.audiobookId}/match`, matchOptions)
|
||||
.$post(`/api/books/${this.libraryItemId}/match`, matchOptions)
|
||||
.then((res) => {
|
||||
this.quickMatching = false
|
||||
if (res.warning) {
|
||||
this.$toast.warning(res.warning)
|
||||
} else if (res.updated) {
|
||||
this.$toast.success('Audiobook details updated')
|
||||
this.$toast.success('Item details updated')
|
||||
} else {
|
||||
this.$toast.info('No updates were made')
|
||||
}
|
||||
@ -236,7 +240,7 @@ export default {
|
||||
saveMetadata() {
|
||||
this.savingMetadata = true
|
||||
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() {
|
||||
if (this.isProcessing) {
|
||||
@ -260,7 +264,7 @@ export default {
|
||||
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)
|
||||
return false
|
||||
})
|
||||
@ -271,35 +275,34 @@ export default {
|
||||
}
|
||||
},
|
||||
init() {
|
||||
this.details.title = this.book.title
|
||||
this.details.subtitle = this.book.subtitle
|
||||
this.details.description = this.book.description
|
||||
this.details.author = this.book.author
|
||||
this.details.narrator = this.book.narrator
|
||||
this.details.genres = this.book.genres || []
|
||||
this.details.series = this.book.series
|
||||
this.details.volumeNumber = this.book.volumeNumber
|
||||
this.details.publishYear = this.book.publishYear
|
||||
this.details.publisher = this.book.publisher || null
|
||||
this.details.language = this.book.language || null
|
||||
this.details.isbn = this.book.isbn || null
|
||||
this.details.asin = this.book.asin || null
|
||||
this.details.title = this.mediaMetadata.title
|
||||
this.details.subtitle = this.mediaMetadata.subtitle
|
||||
this.details.description = this.mediaMetadata.description
|
||||
this.details.authors = this.mediaMetadata.authors
|
||||
this.details.narrator = this.mediaMetadata.narrator
|
||||
this.details.genres = this.mediaMetadata.genres || []
|
||||
this.details.series = this.mediaMetadata.series
|
||||
this.details.publishYear = this.mediaMetadata.publishYear
|
||||
this.details.publisher = this.mediaMetadata.publisher || null
|
||||
this.details.language = this.mediaMetadata.language || null
|
||||
this.details.isbn = this.mediaMetadata.isbn || null
|
||||
this.details.asin = this.mediaMetadata.asin || null
|
||||
|
||||
this.newTags = this.audiobook.tags || []
|
||||
this.newTags = this.media.tags || []
|
||||
},
|
||||
deleteAudiobook() {
|
||||
if (confirm(`Are you sure you want to remove this audiobook?\n\n*Does not delete your files, only removes the audiobook from AudioBookshelf`)) {
|
||||
removeItem() {
|
||||
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.$axios
|
||||
.$delete(`/api/books/${this.audiobookId}`)
|
||||
.$delete(`/api/books/${this.libraryItemId}`)
|
||||
.then(() => {
|
||||
console.log('Audiobook removed')
|
||||
this.$toast.success('Audiobook Removed')
|
||||
console.log('Item removed')
|
||||
this.$toast.success('Item Removed')
|
||||
this.$emit('close')
|
||||
this.isProcessing = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Remove Audiobook failed', error)
|
||||
console.error('Remove item failed', error)
|
||||
this.isProcessing = false
|
||||
})
|
||||
}
|
||||
|
@ -65,7 +65,7 @@
|
||||
export default {
|
||||
props: {
|
||||
processing: Boolean,
|
||||
audiobook: {
|
||||
libraryItem: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
@ -86,14 +86,14 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
audiobookId() {
|
||||
return this.audiobook ? this.audiobook.id : null
|
||||
libraryItemId() {
|
||||
return this.libraryItem ? this.libraryItem.id : null
|
||||
},
|
||||
_audiobook() {
|
||||
return this.audiobook || {}
|
||||
media() {
|
||||
return this.libraryItem ? this.libraryItem.media || {} : {}
|
||||
},
|
||||
downloads() {
|
||||
return this.$store.getters['downloads/getDownloads'](this.audiobookId)
|
||||
return this.$store.getters['downloads/getDownloads'](this.libraryItemId)
|
||||
},
|
||||
singleAudioDownload() {
|
||||
return this.downloads.find((d) => d.type === 'singleAudio')
|
||||
@ -108,24 +108,21 @@ export default {
|
||||
return this.zipDownload ? this.zipDownload.status : false
|
||||
},
|
||||
isSingleTrack() {
|
||||
if (!this.audiobook.tracks) return false
|
||||
return this.audiobook.tracks.length === 1
|
||||
if (!this.libraryItem.tracks) return false
|
||||
return this.libraryItem.tracks.length === 1
|
||||
},
|
||||
singleTrackPath() {
|
||||
if (!this.isSingleTrack) return null
|
||||
return this.audiobook.tracks[0].path
|
||||
},
|
||||
audioFiles() {
|
||||
return this.audiobook ? this.audiobook.audioFiles || [] : []
|
||||
},
|
||||
otherFiles() {
|
||||
return this.audiobook ? this.audiobook.otherFiles || [] : []
|
||||
libraryFiles() {
|
||||
return this.libraryItem.libraryFiles
|
||||
},
|
||||
totalFiles() {
|
||||
return this.audioFiles.length + this.otherFiles.length
|
||||
return this.libraryFiles.length
|
||||
},
|
||||
showM4bDownload() {
|
||||
return !this._audiobook.isMissing && !this._audiobook.isInvalid && this._audiobook.tracks.length
|
||||
return !this.libraryItem.isMissing && this.media.tracks.length
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -9,7 +9,7 @@
|
||||
</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`">
|
||||
<nuxt-link v-if="userCanUpdate" :to="`/item/${libraryItem.id}/edit`">
|
||||
<ui-btn small color="primary">Manage Tracks</ui-btn>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
@ -21,20 +21,20 @@
|
||||
<th class="text-left">Duration</th>
|
||||
<th v-if="showDownload" class="text-center">Download</th>
|
||||
</tr>
|
||||
<template v-for="track in tracksCleaned">
|
||||
<template v-for="track in tracks">
|
||||
<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-sans">{{ showFullPath ? track.path : track.filename }}</td>
|
||||
<td class="font-mono">
|
||||
{{ $bytesPretty(track.size) }}
|
||||
{{ $bytesPretty(track.metadata.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>
|
||||
<a :href="`/s/item/${libraryItem.id}/${track.metadata.relPath}?token=${userToken}`" download><span class="material-icons icon-text">download</span></a>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
@ -43,26 +43,26 @@
|
||||
<div v-else class="flex my-4 text-center justify-center text-xl">No Audio Tracks</div>
|
||||
</div>
|
||||
|
||||
<tables-all-files-table :audiobook="audiobook" />
|
||||
<tables-library-files-table :files="libraryFiles" :library-item-id="libraryItem.id" :is-missing="isMissing" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
audiobook: {
|
||||
libraryItem: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tracks: null,
|
||||
tracks: [],
|
||||
showFullPath: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
audiobook: {
|
||||
libraryItem: {
|
||||
immediate: true,
|
||||
handler(newVal) {
|
||||
if (newVal) this.init()
|
||||
@ -70,22 +70,11 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
audiobookPath() {
|
||||
return this.audiobook.path
|
||||
media() {
|
||||
return this.libraryItem.media || {}
|
||||
},
|
||||
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')
|
||||
}
|
||||
})
|
||||
libraryFiles() {
|
||||
return this.libraryItem.libraryFiles || []
|
||||
},
|
||||
userToken() {
|
||||
return this.$store.getters['user/getToken']
|
||||
@ -97,18 +86,18 @@ export default {
|
||||
return this.$store.getters['user/getUserCanDownload']
|
||||
},
|
||||
isMissing() {
|
||||
return this.audiobook.isMissing
|
||||
return this.libraryItem.isMissing
|
||||
},
|
||||
showDownload() {
|
||||
return this.userCanDownload && !this.isMissing
|
||||
},
|
||||
hasTracks() {
|
||||
return this.audiobook.tracks.length
|
||||
return this.tracks.length
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
this.tracks = this.audiobook.tracks
|
||||
this.tracks = this.media.tracks || []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -94,14 +94,14 @@
|
||||
export default {
|
||||
props: {
|
||||
processing: Boolean,
|
||||
audiobook: {
|
||||
libraryItem: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
audiobookId: null,
|
||||
libraryItemId: null,
|
||||
searchTitle: null,
|
||||
searchAuthor: null,
|
||||
lastSearch: null,
|
||||
@ -126,7 +126,7 @@ export default {
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
audiobook: {
|
||||
libraryItem: {
|
||||
immediate: true,
|
||||
handler(newVal) {
|
||||
if (newVal) this.init()
|
||||
@ -209,19 +209,19 @@ export default {
|
||||
isbn: true
|
||||
}
|
||||
|
||||
if (this.audiobook.id !== this.audiobookId) {
|
||||
if (this.libraryItem.id !== this.libraryItemId) {
|
||||
this.searchResults = []
|
||||
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.searchAuthor = null
|
||||
return
|
||||
}
|
||||
this.searchTitle = this.audiobook.book.title
|
||||
this.searchAuthor = this.audiobook.book.authorFL || ''
|
||||
this.searchTitle = this.libraryItem.media.metadata.title
|
||||
this.searchAuthor = this.libraryItem.media.metadata.authorName || ''
|
||||
this.provider = localStorage.getItem('book-provider') || 'google'
|
||||
},
|
||||
selectMatch(match) {
|
||||
@ -247,7 +247,7 @@ export default {
|
||||
var coverPayload = {
|
||||
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)
|
||||
return false
|
||||
})
|
||||
@ -264,7 +264,7 @@ export default {
|
||||
var bookUpdatePayload = {
|
||||
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)
|
||||
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>
|
||||
<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">
|
||||
<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">
|
||||
<span class="text-sm font-mono">{{ files.length }}</span>
|
||||
</div>
|
||||
<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>
|
||||
<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>
|
||||
@ -22,19 +19,19 @@
|
||||
<th class="text-left px-4 w-24">Filetype</th>
|
||||
<th v-if="userCanDownload && !isMissing" class="text-center w-20">Download</th>
|
||||
</tr>
|
||||
<template v-for="file in otherFilesCleaned">
|
||||
<template v-for="file in files">
|
||||
<tr :key="file.path">
|
||||
<td class="font-book pl-2">
|
||||
{{ showFullPath ? file.fullPath : file.path }}
|
||||
{{ showFullPath ? file.metadata.path : file.metadata.relPath }}
|
||||
</td>
|
||||
<td class="text-xs">
|
||||
<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>
|
||||
<p>{{ file.filetype }}</p>
|
||||
<p>{{ file.metadata.ext }}</p>
|
||||
</div>
|
||||
</td>
|
||||
<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>
|
||||
</tr>
|
||||
</template>
|
||||
@ -51,10 +48,8 @@ export default {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
audiobook: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
}
|
||||
libraryItemId: String,
|
||||
isMissing: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -63,40 +58,19 @@ export default {
|
||||
}
|
||||
},
|
||||
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() {
|
||||
return this.$store.getters['user/getToken']
|
||||
},
|
||||
isMissing() {
|
||||
return this.audiobook.isMissing
|
||||
},
|
||||
userCanDownload() {
|
||||
return this.$store.getters['user/getUserCanDownload']
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
readEbookClick(file) {
|
||||
this.$store.commit('showEReaderForFile', { audiobook: this.audiobook, file })
|
||||
// this.$store.commit('showEReaderForFile', { audiobook: this.audiobook, file })
|
||||
},
|
||||
clickBar() {
|
||||
this.showFiles = !this.showFiles
|
||||
},
|
||||
getRelativePath(path) {
|
||||
var relativePath = path.replace(/\\/g, '/').replace(this.audiobookPath.replace(/\\/g, '/') + '/', '')
|
||||
return this.$encodeUriPath(relativePath)
|
||||
}
|
||||
},
|
||||
mounted() {}
|
@ -8,7 +8,7 @@
|
||||
<!-- <span class="bg-black-400 rounded-xl py-1 px-2 text-sm font-mono">{{ tracks.length }}</span> -->
|
||||
<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/${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>
|
||||
</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' : ''">
|
||||
@ -25,20 +25,20 @@
|
||||
<th class="text-left w-20">Duration</th>
|
||||
<th v-if="userCanDownload" class="text-center w-20">Download</th>
|
||||
</tr>
|
||||
<template v-for="track in tracksCleaned">
|
||||
<template v-for="track in tracks">
|
||||
<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-sans">{{ showFullPath ? track.metadata.path : track.metadata.filename }}</td>
|
||||
<td class="font-mono">
|
||||
{{ $bytesPretty(track.size) }}
|
||||
{{ $bytesPretty(track.metadata.size) }}
|
||||
</td>
|
||||
<td class="font-mono">
|
||||
{{ $secondsToTimestamp(track.duration) }}
|
||||
</td>
|
||||
<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>
|
||||
</tr>
|
||||
</template>
|
||||
@ -55,10 +55,7 @@ export default {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
audiobook: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
}
|
||||
libraryItemId: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -67,20 +64,6 @@ export default {
|
||||
}
|
||||
},
|
||||
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() {
|
||||
return this.$store.getters['user/getToken']
|
||||
},
|
||||
@ -94,10 +77,6 @@ export default {
|
||||
methods: {
|
||||
clickBar() {
|
||||
this.showTracks = !this.showTracks
|
||||
},
|
||||
getRelativePath(path) {
|
||||
var relativePath = path.replace(/\\/g, '/').replace(this.audiobookPath.replace(/\\/g, '/') + '/', '')
|
||||
return this.$encodeUriPath(relativePath)
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
|
@ -74,7 +74,6 @@ module.exports = {
|
||||
|
||||
proxy: {
|
||||
'/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' : '/' },
|
||||
'/ebook/': { 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-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>
|
||||
|
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
|
||||
|
||||
var coverImg = this.ctx.$store.getters['audiobooks/getBookCoverSrc'](audiobook)
|
||||
var coverImg = this.ctx.$store.getters['audiobooks/getLibraryItemCoverSrc'](audiobook)
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
this.coverUrl = coverImg
|
||||
} else {
|
||||
|
@ -3,7 +3,7 @@ export default (ctx) => {
|
||||
// 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) => {
|
||||
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
|
||||
return `${window.location.origin}${coverUrl}`
|
||||
})
|
||||
|
@ -12,25 +12,21 @@ export const state = () => ({
|
||||
})
|
||||
|
||||
export const getters = {
|
||||
getBookCoverSrc: (state, getters, rootState, rootGetters) => (bookItem, placeholder = '/book_placeholder.jpg') => {
|
||||
if (!bookItem) return placeholder
|
||||
var book = bookItem.book
|
||||
if (!book || !book.cover || book.cover === placeholder) return placeholder
|
||||
getLibraryItemCoverSrc: (state, getters, rootState, rootGetters) => (libraryItem, placeholder = '/book_placeholder.jpg') => {
|
||||
if (!libraryItem) return placeholder
|
||||
var media = libraryItem.media
|
||||
if (!media || !media.coverPath || media.coverPath === placeholder) return placeholder
|
||||
|
||||
// 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 bookLastUpdate = book.lastUpdate || Date.now()
|
||||
|
||||
if (!bookItem.id) {
|
||||
console.error('No book item id', bookItem)
|
||||
}
|
||||
var lastUpdate = libraryItem.updatedAt || Date.now()
|
||||
|
||||
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,
|
||||
showEReader: false,
|
||||
selectedAudiobook: null,
|
||||
selectedLibraryItem: null,
|
||||
selectedAudiobookFile: null,
|
||||
developerMode: false,
|
||||
selectedAudiobooks: [],
|
||||
@ -108,14 +109,14 @@ export const mutations = {
|
||||
setStreamAudiobook(state, audiobook) {
|
||||
state.streamAudiobook = audiobook
|
||||
},
|
||||
showEditModal(state, audiobook) {
|
||||
showEditModal(state, libraryItem) {
|
||||
state.editModalTab = 'details'
|
||||
state.selectedAudiobook = audiobook
|
||||
state.selectedLibraryItem = libraryItem
|
||||
state.showEditModal = true
|
||||
},
|
||||
showEditModalOnTab(state, { audiobook, tab }) {
|
||||
showEditModalOnTab(state, { libraryItem, tab }) {
|
||||
state.editModalTab = tab
|
||||
state.selectedAudiobook = audiobook
|
||||
state.selectedLibraryItem = libraryItem
|
||||
state.showEditModal = true
|
||||
},
|
||||
setEditModalTab(state, tab) {
|
||||
@ -124,15 +125,15 @@ export const mutations = {
|
||||
setShowEditModal(state, val) {
|
||||
state.showEditModal = val
|
||||
},
|
||||
showEReader(state, audiobook) {
|
||||
showEReader(state, libraryItem) {
|
||||
state.selectedAudiobookFile = null
|
||||
state.selectedAudiobook = audiobook
|
||||
state.selectedLibraryItem = libraryItem
|
||||
|
||||
state.showEReader = true
|
||||
},
|
||||
showEReaderForFile(state, { audiobook, file }) {
|
||||
showEReaderForFile(state, { libraryItem, file }) {
|
||||
state.selectedAudiobookFile = file
|
||||
state.selectedAudiobook = audiobook
|
||||
state.selectedLibraryItem = libraryItem
|
||||
|
||||
state.showEReader = true
|
||||
},
|
||||
@ -142,8 +143,8 @@ export const mutations = {
|
||||
setDeveloperMode(state, val) {
|
||||
state.developerMode = val
|
||||
},
|
||||
setSelectedAudiobook(state, val) {
|
||||
Vue.set(state, 'selectedAudiobook', val)
|
||||
setSelectedLibraryItem(state, val) {
|
||||
Vue.set(state, 'selectedLibraryItem', val)
|
||||
},
|
||||
setSelectedAudiobooks(state, audiobooks) {
|
||||
Vue.set(state, 'selectedAudiobooks', audiobooks)
|
||||
|
@ -124,7 +124,7 @@ export const actions = {
|
||||
}
|
||||
|
||||
this.$axios
|
||||
.$get(`/api/libraries/${state.currentLibraryId}/filters`)
|
||||
.$get(`/api/libraries/${state.currentLibraryId}/filterdata`)
|
||||
.then((data) => {
|
||||
commit('setLibraryFilterData', data)
|
||||
})
|
||||
|
@ -19,8 +19,9 @@ new LibraryItem({
|
||||
lastScan: 1646784672127,
|
||||
scanVersion: 1.72,
|
||||
isMissing: false,
|
||||
entityType: 'book',
|
||||
entity: { // Book.js
|
||||
mediaType: 'book',
|
||||
media: { // Book.js
|
||||
coverPath: '/metadata/books/li_abai123wir/cover.webp',
|
||||
metadata: { // BookMetadata.js
|
||||
title: 'Wizards First Rule',
|
||||
subtitle: null,
|
||||
|
@ -14,6 +14,7 @@ const UserController = require('./controllers/UserController')
|
||||
const CollectionController = require('./controllers/CollectionController')
|
||||
const MeController = require('./controllers/MeController')
|
||||
const BackupController = require('./controllers/BackupController')
|
||||
const LibraryItemController = require('./controllers/LibraryItemController')
|
||||
|
||||
const BookFinder = require('./finders/BookFinder')
|
||||
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.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/books', LibraryController.middleware.bind(this), LibraryController.getBooksForLibrary.bind(this))
|
||||
this.router.get('/libraries/:id/items', LibraryController.middleware.bind(this), LibraryController.getLibraryItems.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/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/filters', LibraryController.middleware.bind(this), LibraryController.getLibraryFilters.bind(this))
|
||||
this.router.get('/libraries/:id/personalized', LibraryController.middleware.bind(this), LibraryController.getLibraryUserPersonalized.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/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.post('/libraries/:id/matchbooks', LibraryController.middleware.bind(this), LibraryController.matchBooks.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
|
||||
//
|
||||
|
@ -10,14 +10,14 @@ class CacheManager {
|
||||
this.CoverCachePath = Path.join(this.CachePath, 'covers')
|
||||
}
|
||||
|
||||
async handleCoverCache(res, audiobook, options = {}) {
|
||||
async handleCoverCache(res, libraryItem, options = {}) {
|
||||
const format = options.format || 'webp'
|
||||
const width = options.width || 400
|
||||
const height = options.height || null
|
||||
|
||||
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
|
||||
if (await fs.pathExists(path)) {
|
||||
@ -35,7 +35,7 @@ class CacheManager {
|
||||
// Write cache
|
||||
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)
|
||||
|
||||
var readStream = fs.createReadStream(writtenFile)
|
||||
|
@ -69,7 +69,6 @@ class Server {
|
||||
|
||||
Logger.logManager = this.logManager
|
||||
|
||||
this.expressApp = null
|
||||
this.server = null
|
||||
this.io = null
|
||||
|
||||
@ -154,8 +153,6 @@ class Server {
|
||||
await this.init()
|
||||
|
||||
const app = express()
|
||||
this.expressApp = app
|
||||
|
||||
this.server = http.createServer(app)
|
||||
|
||||
app.use(this.auth.cors)
|
||||
@ -167,9 +164,6 @@ class Server {
|
||||
const distPath = Path.join(global.appRoot, '/client/dist')
|
||||
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
|
||||
app.use('/metadata', this.authMiddleware.bind(this), express.static(global.MetadataPath))
|
||||
|
||||
@ -179,6 +173,9 @@ class Server {
|
||||
// Static folder
|
||||
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
|
||||
app.get('/lib/:library/:folder/*', this.authMiddleware.bind(this), (req, res) => {
|
||||
var library = this.db.libraries.find(lib => lib.id === req.params.library)
|
||||
@ -192,6 +189,7 @@ class Server {
|
||||
})
|
||||
|
||||
// Book static file routes
|
||||
// LEGACY
|
||||
app.get('/s/book/:id/*', this.authMiddleware.bind(this), (req, res) => {
|
||||
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)
|
||||
@ -201,6 +199,16 @@ class Server {
|
||||
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
|
||||
app.get('/ebook/:library/:folder/*', (req, res) => {
|
||||
var library = this.db.libraries.find(lib => lib.id === req.params.library)
|
||||
@ -214,8 +222,10 @@ class Server {
|
||||
})
|
||||
|
||||
// Client dynamic routes
|
||||
app.get('/audiobook/:id', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
||||
app.get('/audiobook/:id/edit', (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'))) // 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/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')))
|
||||
@ -224,31 +234,16 @@ class Server {
|
||||
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.use('/api', this.authMiddleware.bind(this), this.apiController.router)
|
||||
app.use('/hls', this.authMiddleware.bind(this), this.hlsController.router)
|
||||
|
||||
// Incomplete work in progress
|
||||
// app.use('/feeds', this.rssFeeds.router)
|
||||
|
||||
app.post('/login', this.getLoginRateLimiter(), (req, res) => this.auth.login(req, res))
|
||||
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.get('/ping', (req, res) => {
|
||||
Logger.info('Recieved ping')
|
||||
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, () => {
|
||||
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, {
|
||||
@ -265,21 +260,25 @@ class Server {
|
||||
}
|
||||
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))
|
||||
|
||||
// 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
|
||||
socket.on('scan', this.scan.bind(this))
|
||||
socket.on('cancel_scan', this.cancelScan.bind(this))
|
||||
socket.on('scan_audiobook', (audiobookId) => this.scanAudiobook(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('close_stream', () => this.streamManager.closeStreamRequest(socket))
|
||||
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))
|
||||
|
||||
// Downloading
|
||||
@ -299,10 +298,6 @@ class Server {
|
||||
socket.on('update_bookmark', (payload) => this.updateBookmark(socket, payload))
|
||||
socket.on('delete_bookmark', (payload) => this.deleteBookmark(socket, payload))
|
||||
|
||||
socket.on('test', () => {
|
||||
socket.emit('test_received', socket.id)
|
||||
})
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
Logger.removeSocketListener(socket.id)
|
||||
|
||||
|
@ -39,10 +39,9 @@ class LibraryController {
|
||||
|
||||
async findOne(req, res) {
|
||||
if (req.query.include && req.query.include === 'filterdata') {
|
||||
var books = this.db.audiobooks.filter(ab => ab.libraryId === req.library.id)
|
||||
return res.json({
|
||||
filterdata: libraryHelpers.getDistinctFilterData(books),
|
||||
issues: libraryHelpers.getNumIssues(books),
|
||||
filterdata: libraryHelpers.getDistinctFilterDataNew(req.libraryItems),
|
||||
issues: libraryHelpers.getNumIssues(req.libraryItems),
|
||||
library: req.library
|
||||
})
|
||||
}
|
||||
@ -91,38 +90,70 @@ class LibraryController {
|
||||
return res.json(libraryJson)
|
||||
}
|
||||
|
||||
// api/libraries/:id/books
|
||||
getBooksForLibrary(req, res) {
|
||||
// api/libraries/:id/items
|
||||
// TODO: Optimize this method, audiobooks are iterated through several times but can be combined
|
||||
getLibraryItems(req, res) {
|
||||
var libraryId = req.library.id
|
||||
var audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === libraryId)
|
||||
|
||||
if (req.query.filter) {
|
||||
audiobooks = libraryHelpers.getFiltered(audiobooks, req.query.filter, req.user)
|
||||
var media = req.query.media || 'all'
|
||||
var libraryItems = this.db.libraryItems.filter(li => {
|
||||
if (li.libraryId !== libraryId) return false
|
||||
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) {
|
||||
var orderByNumber = req.query.sort === 'book.volumeNumber'
|
||||
var direction = req.query.desc === '1' ? 'desc' : 'asc'
|
||||
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 (orderByNumber && !isNaN(value)) return Number(value)
|
||||
return value
|
||||
if (payload.filterBy) {
|
||||
libraryItems = libraryHelpers.getFilteredLibraryItems(libraryItems, payload.filterBy, req.user)
|
||||
payload.total = libraryItems.length
|
||||
}
|
||||
|
||||
if (payload.sortBy) {
|
||||
var sortKey = payload.sortBy
|
||||
|
||||
// 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)) {
|
||||
var page = req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0
|
||||
var limit = Number(req.query.limit)
|
||||
var startIndex = page * limit
|
||||
audiobooks = audiobooks.slice(startIndex, startIndex + limit)
|
||||
// TODO: Potentially implement collapse series again
|
||||
libraryItems = libraryItems.map(ab => payload.minified ? ab.toJSONMinified() : ab.toJSON())
|
||||
|
||||
if (payload.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
|
||||
// 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 audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === libraryId)
|
||||
@ -274,6 +305,7 @@ class LibraryController {
|
||||
res.json(payload)
|
||||
}
|
||||
|
||||
// LEGACY
|
||||
// api/libraries/:id/books/filters
|
||||
async getLibraryFilters(req, res) {
|
||||
var library = req.library
|
||||
@ -281,6 +313,45 @@ class LibraryController {
|
||||
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
|
||||
async getLibraryCategories(req, res) {
|
||||
var library = req.library
|
||||
@ -491,6 +562,7 @@ class LibraryController {
|
||||
return res.status(404).send('Library not found')
|
||||
}
|
||||
req.library = library
|
||||
req.libraryItems = this.db.libraryItems.filter(li => li.libraryId === library.id)
|
||||
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 Logger = require('../Logger')
|
||||
const Path = require('path')
|
||||
const Author = require('../objects/Author')
|
||||
const Author = require('../objects/legacy/Author')
|
||||
const Audnexus = require('../providers/Audnexus')
|
||||
|
||||
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.birthtimeMs = null
|
||||
this.addedAt = null
|
||||
this.lastUpdate = null
|
||||
this.updatedAt = null
|
||||
this.lastScan = null
|
||||
this.scanVersion = null
|
||||
|
||||
// Entity was scanned and not found
|
||||
this.isMissing = false
|
||||
|
||||
this.entityType = null
|
||||
this.entity = null
|
||||
this.mediaType = null
|
||||
this.media = null
|
||||
|
||||
this.libraryFiles = []
|
||||
|
||||
@ -45,17 +45,17 @@ class LibraryItem {
|
||||
this.ctimeMs = libraryItem.ctimeMs || 0
|
||||
this.birthtimeMs = libraryItem.birthtimeMs || 0
|
||||
this.addedAt = libraryItem.addedAt
|
||||
this.lastUpdate = libraryItem.lastUpdate || this.addedAt
|
||||
this.updatedAt = libraryItem.updatedAt || this.addedAt
|
||||
this.lastScan = libraryItem.lastScan || null
|
||||
this.scanVersion = libraryItem.scanVersion || null
|
||||
|
||||
this.isMissing = !!libraryItem.isMissing
|
||||
|
||||
this.entityType = libraryItem.entityType
|
||||
if (this.entityType === 'book') {
|
||||
this.entity = new Book(libraryItem.entity)
|
||||
} else if (this.entityType === 'podcast') {
|
||||
this.entity = new Podcast(libraryItem.entity)
|
||||
this.mediaType = libraryItem.mediaType
|
||||
if (this.mediaType === 'book') {
|
||||
this.media = new Book(libraryItem.media)
|
||||
} else if (this.mediaType === 'podcast') {
|
||||
this.media = new Podcast(libraryItem.media)
|
||||
}
|
||||
|
||||
this.libraryFiles = libraryItem.libraryFiles.map(f => new LibraryFile(f))
|
||||
@ -73,14 +73,64 @@ class LibraryItem {
|
||||
ctimeMs: this.ctimeMs,
|
||||
birthtimeMs: this.birthtimeMs,
|
||||
addedAt: this.addedAt,
|
||||
lastUpdate: this.lastUpdate,
|
||||
updatedAt: this.updatedAt,
|
||||
lastScan: this.lastScan,
|
||||
scanVersion: this.scanVersion,
|
||||
isMissing: !!this.isMissing,
|
||||
entityType: this.entityType,
|
||||
entity: this.entity.toJSON(),
|
||||
mediaType: this.mediaType,
|
||||
media: this.media.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
|
@ -7,7 +7,6 @@ class Book {
|
||||
this.metadata = null
|
||||
|
||||
this.coverPath = null
|
||||
this.relCoverPath = null
|
||||
this.tags = []
|
||||
this.audioFiles = []
|
||||
this.ebookFiles = []
|
||||
@ -21,7 +20,6 @@ class Book {
|
||||
construct(book) {
|
||||
this.metadata = new BookMetadata(book.metadata)
|
||||
this.coverPath = book.coverPath
|
||||
this.relCoverPath = book.relCoverPath
|
||||
this.tags = [...book.tags]
|
||||
this.audioFiles = book.audioFiles.map(f => new AudioFile(f))
|
||||
this.ebookFiles = book.ebookFiles.map(f => new EBookFile(f))
|
||||
@ -32,12 +30,53 @@ class Book {
|
||||
return {
|
||||
metadata: this.metadata.toJSON(),
|
||||
coverPath: this.coverPath,
|
||||
relCoverPath: this.relCoverPath,
|
||||
tags: [...this.tags],
|
||||
audioFiles: this.audioFiles.map(f => f.toJSON()),
|
||||
ebookFiles: this.ebookFiles.map(f => f.toJSON()),
|
||||
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
|
@ -6,8 +6,8 @@ class Podcast {
|
||||
this.id = null
|
||||
|
||||
this.metadata = null
|
||||
this.cover = null
|
||||
this.coverFullPath = null
|
||||
this.coverPath = null
|
||||
this.tags = []
|
||||
this.episodes = []
|
||||
|
||||
this.createdAt = null
|
||||
@ -21,8 +21,8 @@ class Podcast {
|
||||
construct(podcast) {
|
||||
this.id = podcast.id
|
||||
this.metadata = new PodcastMetadata(podcast.metadata)
|
||||
this.cover = podcast.cover
|
||||
this.coverFullPath = podcast.coverFullPath
|
||||
this.coverPath = podcast.coverPath
|
||||
this.tags = [...podcast.tags]
|
||||
this.episodes = podcast.episodes.map((e) => new PodcastEpisode(e))
|
||||
this.createdAt = podcast.createdAt
|
||||
this.lastUpdate = podcast.lastUpdate
|
||||
@ -32,8 +32,32 @@ class Podcast {
|
||||
return {
|
||||
id: this.id,
|
||||
metadata: this.metadata.toJSON(),
|
||||
cover: this.cover,
|
||||
coverFullPath: this.coverFullPath,
|
||||
coverPath: this.coverPath,
|
||||
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()),
|
||||
createdAt: this.createdAt,
|
||||
lastUpdate: this.lastUpdate
|
||||
|
@ -1,3 +1,4 @@
|
||||
const globals = require('../../utils/globals')
|
||||
const FileMetadata = require('../metadata/FileMetadata')
|
||||
|
||||
class LibraryFile {
|
||||
@ -24,8 +25,18 @@ class LibraryFile {
|
||||
ino: this.ino,
|
||||
metadata: this.metadata.toJSON(),
|
||||
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
|
@ -1,5 +1,5 @@
|
||||
const { isNullOrNaN } = require('../utils/index')
|
||||
const AudioFileMetadata = require('./metadata/AudioMetaTags')
|
||||
const { isNullOrNaN } = require('../../utils/index')
|
||||
const AudioFileMetadata = require('../metadata/AudioMetaTags')
|
||||
|
||||
class AudioFile {
|
||||
constructor(data) {
|
@ -1,4 +1,4 @@
|
||||
var { bytesPretty } = require('../utils/fileUtils')
|
||||
var { bytesPretty } = require('../../utils/fileUtils')
|
||||
|
||||
class AudioTrack {
|
||||
constructor(audioTrack = null) {
|
@ -1,12 +1,12 @@
|
||||
const Path = require('path')
|
||||
const fs = require('fs-extra')
|
||||
const { bytesPretty, readTextFile, getIno } = require('../utils/fileUtils')
|
||||
const { comparePaths, getId, elapsedPretty } = require('../utils/index')
|
||||
const { parseOpfMetadataXML } = require('../utils/parseOpfMetadata')
|
||||
const { extractCoverArt } = require('../utils/ffmpegHelpers')
|
||||
const nfoGenerator = require('../utils/nfoGenerator')
|
||||
const abmetadataGenerator = require('../utils/abmetadataGenerator')
|
||||
const Logger = require('../Logger')
|
||||
const { bytesPretty, readTextFile, getIno } = require('../../utils/fileUtils')
|
||||
const { comparePaths, getId, elapsedPretty } = require('../../utils/index')
|
||||
const { parseOpfMetadataXML } = require('../../utils/parseOpfMetadata')
|
||||
const { extractCoverArt } = require('../../utils/ffmpegHelpers')
|
||||
const nfoGenerator = require('../../utils/nfoGenerator')
|
||||
const abmetadataGenerator = require('../../utils/abmetadataGenerator')
|
||||
const Logger = require('../../Logger')
|
||||
const Book = require('./Book')
|
||||
const AudioTrack = require('./AudioTrack')
|
||||
const AudioFile = require('./AudioFile')
|
@ -1,5 +1,5 @@
|
||||
const { getId } = require('../utils/index')
|
||||
const Logger = require('../Logger')
|
||||
const { getId } = require('../../utils/index')
|
||||
const Logger = require('../../Logger')
|
||||
|
||||
class Author {
|
||||
constructor(author = null) {
|
@ -1,6 +1,6 @@
|
||||
const Path = require('path')
|
||||
const Logger = require('../Logger')
|
||||
const parseAuthors = require('../utils/parseAuthors')
|
||||
const Logger = require('../../Logger')
|
||||
const parseAuthors = require('../../utils/parseAuthors')
|
||||
|
||||
class Book {
|
||||
constructor(book = null) {
|
@ -13,6 +13,7 @@ class BookMetadata {
|
||||
this.isbn = null
|
||||
this.asin = null
|
||||
this.language = null
|
||||
this.explicit = false
|
||||
|
||||
if (metadata) {
|
||||
this.construct(metadata)
|
||||
@ -33,6 +34,7 @@ class BookMetadata {
|
||||
this.isbn = metadata.isbn
|
||||
this.asin = metadata.asin
|
||||
this.language = metadata.language
|
||||
this.explicit = metadata.explicit
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
@ -49,8 +51,54 @@ class BookMetadata {
|
||||
description: this.description,
|
||||
isbn: this.isbn,
|
||||
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
|
@ -41,5 +41,10 @@ class FileMetadata {
|
||||
clone() {
|
||||
return new FileMetadata(this.toJSON())
|
||||
}
|
||||
|
||||
get format() {
|
||||
if (!this.ext) return ''
|
||||
return this.ext.slice(1)
|
||||
}
|
||||
}
|
||||
module.exports = FileMetadata
|
@ -9,6 +9,7 @@ class PodcastMetadata {
|
||||
this.itunesPageUrl = null
|
||||
this.itunesId = null
|
||||
this.itunesArtistId = null
|
||||
this.explicit = false
|
||||
|
||||
if (metadata) {
|
||||
this.construct(metadata)
|
||||
@ -25,6 +26,7 @@ class PodcastMetadata {
|
||||
this.itunesPageUrl = metadata.itunesPageUrl
|
||||
this.itunesId = metadata.itunesId
|
||||
this.itunesArtistId = metadata.itunesArtistId
|
||||
this.explicit = metadata.explicit
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
@ -38,7 +40,12 @@ class PodcastMetadata {
|
||||
itunesPageUrl: this.itunesPageUrl,
|
||||
itunesId: this.itunesId,
|
||||
itunesArtistId: this.itunesArtistId,
|
||||
explicit: this.explicit
|
||||
}
|
||||
}
|
||||
|
||||
toJSONExpanded() {
|
||||
return this.toJSON()
|
||||
}
|
||||
}
|
||||
module.exports = PodcastMetadata
|
@ -1,4 +1,4 @@
|
||||
const AudioFileMetadata = require('../objects/AudioFileMetadata')
|
||||
const AudioFileMetadata = require('../objects/metadata/AudioMetaTags')
|
||||
|
||||
class AudioProbeData {
|
||||
constructor() {
|
||||
|
@ -10,7 +10,7 @@ const { ScanResult, LogLevel } = require('../utils/constants')
|
||||
|
||||
const AudioFileScanner = require('./AudioFileScanner')
|
||||
const BookFinder = require('../finders/BookFinder')
|
||||
const Audiobook = require('../objects/Audiobook')
|
||||
const Audiobook = require('../objects/legacy/Audiobook')
|
||||
const LibraryScan = require('./LibraryScan')
|
||||
const ScanOptions = require('./ScanOptions')
|
||||
|
||||
|
@ -3,7 +3,7 @@ const fs = require('fs-extra')
|
||||
const njodb = require("njodb")
|
||||
|
||||
const { SupportedEbookTypes } = require('./globals')
|
||||
const Audiobook = require('../objects/Audiobook')
|
||||
const Audiobook = require('../objects/legacy/Audiobook')
|
||||
const LibraryItem = require('../objects/LibraryItem')
|
||||
|
||||
const Logger = require('../Logger')
|
||||
@ -142,7 +142,7 @@ function makeLibraryItemFromOldAb(audiobook) {
|
||||
libraryItem.lastScan = audiobook.lastScan
|
||||
libraryItem.scanVersion = audiobook.scanVersion
|
||||
libraryItem.isMissing = audiobook.isMissing
|
||||
libraryItem.entityType = 'book'
|
||||
libraryItem.mediaType = 'book'
|
||||
|
||||
var bookEntity = new Book()
|
||||
var bookMetadata = new BookMetadata(audiobook.book)
|
||||
@ -159,8 +159,6 @@ function makeLibraryItemFromOldAb(audiobook) {
|
||||
|
||||
bookEntity.metadata = bookMetadata
|
||||
bookEntity.coverPath = audiobook.book.coverFullPath
|
||||
// Path relative to library item
|
||||
bookEntity.relCoverPath = getRelativePath(audiobook.book.coverFullPath, audiobook.fullPath)
|
||||
bookEntity.tags = [...audiobook.tags]
|
||||
|
||||
var payload = makeFilesFromOldAb(audiobook)
|
||||
@ -171,7 +169,7 @@ function makeLibraryItemFromOldAb(audiobook) {
|
||||
bookEntity.chapters = audiobook.chapters.map(c => ({ ...c }))
|
||||
}
|
||||
|
||||
libraryItem.entity = bookEntity
|
||||
libraryItem.media = bookEntity
|
||||
libraryItem.libraryFiles = payload.libraryFiles
|
||||
return libraryItem
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
const globals = {
|
||||
SupportedImageTypes: ['png', 'jpg', 'jpeg', 'webp'],
|
||||
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
|
||||
|
@ -8,6 +8,45 @@ module.exports = {
|
||||
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) {
|
||||
var filtered = audiobooks
|
||||
|
||||
@ -45,6 +84,55 @@ module.exports = {
|
||||
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) {
|
||||
var data = {
|
||||
authors: [],
|
||||
@ -246,9 +334,11 @@ module.exports = {
|
||||
return totalSize
|
||||
},
|
||||
|
||||
getNumIssues(books) {
|
||||
return books.filter(ab => {
|
||||
return ab.numMissingParts || ab.numInvalidParts || ab.isMissing || ab.isInvalid
|
||||
}).length
|
||||
getNumIssues(libraryItems) {
|
||||
// TODO: Implement issues
|
||||
return libraryItems.filter(li => li.isMissing).length
|
||||
// return books.filter(ab => {
|
||||
// return ab.numMissingParts || ab.numInvalidParts || ab.isMissing || ab.isInvalid
|
||||
// }).length
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user