mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-01-12 17:08:33 +01:00
New data model edit tracks page, match, quick match, clean out old files
This commit is contained in:
parent
be1e1e7ba0
commit
7d66f1eec9
@ -103,9 +103,6 @@ export default {
|
|||||||
userAudiobooks() {
|
userAudiobooks() {
|
||||||
return this.$store.state.user.user.audiobooks || {}
|
return this.$store.state.user.user.audiobooks || {}
|
||||||
},
|
},
|
||||||
selectedSeries() {
|
|
||||||
return this.$store.state.audiobooks.selectedSeries
|
|
||||||
},
|
|
||||||
userCanUpdate() {
|
userCanUpdate() {
|
||||||
return this.$store.getters['user/getUserCanUpdate']
|
return this.$store.getters['user/getUserCanUpdate']
|
||||||
},
|
},
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
<covers-hover-book-cover :audiobook="book" />
|
<covers-hover-book-cover :audiobook="book" />
|
||||||
</td>
|
</td>
|
||||||
<td class="body-cell min-w-64 max-w-64 px-2">
|
<td class="body-cell min-w-64 max-w-64 px-2">
|
||||||
<nuxt-link :to="`/audiobook/${book.id}`" class="hover:underline">
|
<nuxt-link :to="`/item/${book.id}`" class="hover:underline">
|
||||||
<p class="truncate">
|
<p class="truncate">
|
||||||
{{ book.book.title }}<span v-if="book.book.subtitle">: {{ book.book.subtitle }}</span>
|
{{ book.book.title }}<span v-if="book.book.subtitle">: {{ book.book.subtitle }}</span>
|
||||||
</p>
|
</p>
|
||||||
@ -24,7 +24,7 @@
|
|||||||
<p class="truncate">{{ seriesText }}</p>
|
<p class="truncate">{{ seriesText }}</p>
|
||||||
</td>
|
</td>
|
||||||
<td class="body-cell min-w-24 max-w-24 px-2">
|
<td class="body-cell min-w-24 max-w-24 px-2">
|
||||||
<p class="truncate">{{ book.book.publishYear }}</p>
|
<p class="truncate">{{ book.book.publishedYear }}</p>
|
||||||
</td>
|
</td>
|
||||||
<td class="body-cell min-w-80 max-w-80 px-2">
|
<td class="body-cell min-w-80 max-w-80 px-2">
|
||||||
<p class="truncate">{{ book.book.description }}</p>
|
<p class="truncate">{{ book.book.description }}</p>
|
||||||
@ -148,7 +148,7 @@ export default {
|
|||||||
this.$store.commit('showEReader', this.book)
|
this.$store.commit('showEReader', this.book)
|
||||||
},
|
},
|
||||||
downloadClick() {
|
downloadClick() {
|
||||||
this.$store.commit('showEditModalOnTab', { audiobook: this.book, tab: 'download' })
|
this.$store.commit('showEditModalOnTab', { libraryItem: this.book, tab: 'download' })
|
||||||
},
|
},
|
||||||
toggleRead() {
|
toggleRead() {
|
||||||
var updatePayload = {
|
var updatePayload = {
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
<p class="text-center text-2xl font-book mb-4 py-4">Audiobookshelf is empty!</p>
|
<p class="text-center text-2xl font-book mb-4 py-4">Audiobookshelf is empty!</p>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<ui-btn to="/config" color="primary" class="w-52 mr-2">Configure Scanner</ui-btn>
|
<ui-btn to="/config" color="primary" class="w-52 mr-2">Configure Scanner</ui-btn>
|
||||||
<ui-btn color="success" class="w-52" @click="scan">Scan Audiobooks</ui-btn>
|
<ui-btn color="success" class="w-52" @click="scan">Scan Library</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="loaded && !shelves.length && search" class="w-full h-40 flex items-center justify-center">
|
<div v-else-if="loaded && !shelves.length && search" class="w-full h-40 flex items-center justify-center">
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<div v-show="routeName === route.iod" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="routeName === route.iod" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<div class="w-full h-10 px-4 border-t border-black border-opacity-20 absolute left-0 flex flex-col justify-center" :style="{ bottom: streamAudiobook && isMobileLandscape ? '300px' : '65px' }">
|
<div class="w-full h-10 px-4 border-t border-black border-opacity-20 absolute left-0 flex flex-col justify-center" :style="{ bottom: streamLibraryItem && isMobileLandscape ? '300px' : '65px' }">
|
||||||
<p class="font-mono text-sm">v{{ $config.version }}</p>
|
<p class="font-mono text-sm">v{{ $config.version }}</p>
|
||||||
<a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-sm">Update available: {{ latestVersion }}</a>
|
<a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-sm">Update available: {{ latestVersion }}</a>
|
||||||
</div>
|
</div>
|
||||||
@ -109,8 +109,8 @@ export default {
|
|||||||
githubTagUrl() {
|
githubTagUrl() {
|
||||||
return this.versionData.githubTagUrl
|
return this.versionData.githubTagUrl
|
||||||
},
|
},
|
||||||
streamAudiobook() {
|
streamLibraryItem() {
|
||||||
return this.$store.state.streamAudiobook
|
return this.$store.state.streamLibraryItem
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<h1>{{ book.title }}</h1>
|
<h1>{{ book.title }}</h1>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<p>{{ book.publishYear }}</p>
|
<p>{{ book.publishedYear }}</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-gray-400">{{ book.author }}</p>
|
<p class="text-gray-400">{{ book.author }}</p>
|
||||||
<div class="w-full max-h-12 overflow-hidden">
|
<div class="w-full max-h-12 overflow-hidden">
|
||||||
|
@ -149,7 +149,7 @@ export default {
|
|||||||
return '/book_placeholder.jpg'
|
return '/book_placeholder.jpg'
|
||||||
},
|
},
|
||||||
bookCoverSrc() {
|
bookCoverSrc() {
|
||||||
return this.store.getters['audiobooks/getLibraryItemCoverSrc'](this._libraryItem, this.placeholderUrl)
|
return this.store.getters['globals/getLibraryItemCoverSrc'](this._libraryItem, this.placeholderUrl)
|
||||||
},
|
},
|
||||||
libraryItemId() {
|
libraryItemId() {
|
||||||
return this._libraryItem.id
|
return this._libraryItem.id
|
||||||
@ -418,7 +418,7 @@ export default {
|
|||||||
toast.error(`Failed to mark as ${updatePayload.isRead ? 'Read' : 'Not Read'}`)
|
toast.error(`Failed to mark as ${updatePayload.isRead ? 'Read' : 'Not Read'}`)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
audiobookScanComplete(result) {
|
itemScanComplete(result) {
|
||||||
this.rescanning = false
|
this.rescanning = false
|
||||||
var toast = this.$toast || this.$nuxt.$toast
|
var toast = this.$toast || this.$nuxt.$toast
|
||||||
if (!result) {
|
if (!result) {
|
||||||
@ -433,23 +433,23 @@ export default {
|
|||||||
},
|
},
|
||||||
rescan() {
|
rescan() {
|
||||||
this.rescanning = true
|
this.rescanning = true
|
||||||
this._socket.once('audiobook_scan_complete', this.audiobookScanComplete)
|
this._socket.once('item_scan_complete', this.itemScanComplete)
|
||||||
this._socket.emit('scan_libraryItem', this.libraryItemId)
|
this._socket.emit('scan_item', this.libraryItemId)
|
||||||
},
|
},
|
||||||
showEditModalTracks() {
|
showEditModalTracks() {
|
||||||
// More menu func
|
// More menu func
|
||||||
this.store.commit('showEditModalOnTab', { audiobook: this.audiobook, tab: 'tracks' })
|
this.store.commit('showEditModalOnTab', { libraryItem: this.audiobook, tab: 'tracks' })
|
||||||
},
|
},
|
||||||
showEditModalMatch() {
|
showEditModalMatch() {
|
||||||
// More menu func
|
// More menu func
|
||||||
this.store.commit('showEditModalOnTab', { audiobook: this.audiobook, tab: 'match' })
|
this.store.commit('showEditModalOnTab', { libraryItem: this.audiobook, tab: 'match' })
|
||||||
},
|
},
|
||||||
showEditModalDownload() {
|
showEditModalDownload() {
|
||||||
// More menu func
|
// More menu func
|
||||||
this.store.commit('showEditModalOnTab', { audiobook: this.audiobook, tab: 'download' })
|
this.store.commit('showEditModalOnTab', { libraryItem: this.audiobook, tab: 'download' })
|
||||||
},
|
},
|
||||||
openCollections() {
|
openCollections() {
|
||||||
this.store.commit('setSelectedAudiobook', this.audiobook)
|
this.store.commit('setSelectedLibraryItem', this.audiobook)
|
||||||
this.store.commit('globals/setShowUserCollectionsModal', true)
|
this.store.commit('globals/setShowUserCollectionsModal', true)
|
||||||
},
|
},
|
||||||
createMoreMenu() {
|
createMoreMenu() {
|
||||||
|
@ -99,7 +99,7 @@ export default {
|
|||||||
fullCoverUrl() {
|
fullCoverUrl() {
|
||||||
if (!this.libraryItem) return null
|
if (!this.libraryItem) return null
|
||||||
var store = this.$store || this.$nuxt.$store
|
var store = this.$store || this.$nuxt.$store
|
||||||
return store.getters['audiobooks/getLibraryItemCoverSrc'](this.libraryItem, this.placeholderUrl)
|
return store.getters['globals/getLibraryItemCoverSrc'](this.libraryItem, this.placeholderUrl)
|
||||||
},
|
},
|
||||||
cover() {
|
cover() {
|
||||||
return this.media.coverPath || this.placeholderUrl
|
return this.media.coverPath || this.placeholderUrl
|
||||||
|
@ -63,7 +63,7 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getCoverUrl(book) {
|
getCoverUrl(book) {
|
||||||
return this.store.getters['audiobooks/getLibraryItemCoverSrc'](book, '')
|
return this.store.getters['globals/getLibraryItemCoverSrc'](book, '')
|
||||||
},
|
},
|
||||||
async buildCoverImg(coverData, bgCoverWidth, offsetLeft, zIndex, forceCoverBg = false) {
|
async buildCoverImg(coverData, bgCoverWidth, offsetLeft, zIndex, forceCoverBg = false) {
|
||||||
var src = coverData.coverUrl
|
var src = coverData.coverUrl
|
||||||
|
@ -22,7 +22,7 @@ export default {
|
|||||||
return '/book_placeholder.jpg'
|
return '/book_placeholder.jpg'
|
||||||
},
|
},
|
||||||
fullCoverUrl() {
|
fullCoverUrl() {
|
||||||
return this.$store.getters['audiobooks/getLibraryItemCoverSrc'](this.audiobook, this.placeholderUrl)
|
return this.$store.getters['globals/getLibraryItemCoverSrc'](this.audiobook, this.placeholderUrl)
|
||||||
},
|
},
|
||||||
hasCover() {
|
hasCover() {
|
||||||
return !!this.audiobook.book.cover
|
return !!this.audiobook.book.cover
|
||||||
|
@ -169,7 +169,7 @@ export default {
|
|||||||
if (this.currentBookshelfIndex - 1 < 0) return
|
if (this.currentBookshelfIndex - 1 < 0) return
|
||||||
var prevBookId = this.bookshelfBookIds[this.currentBookshelfIndex - 1]
|
var prevBookId = this.bookshelfBookIds[this.currentBookshelfIndex - 1]
|
||||||
this.processing = true
|
this.processing = true
|
||||||
var prevBook = await this.$axios.$get(`/api/books/${prevBookId}`).catch((error) => {
|
var prevBook = await this.$axios.$get(`/api/items/${prevBookId}`).catch((error) => {
|
||||||
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed to fetch book'
|
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed to fetch book'
|
||||||
this.$toast.error(errorMsg)
|
this.$toast.error(errorMsg)
|
||||||
return null
|
return null
|
||||||
@ -186,7 +186,7 @@ export default {
|
|||||||
if (this.currentBookshelfIndex >= this.bookshelfBookIds.length - 1) return
|
if (this.currentBookshelfIndex >= this.bookshelfBookIds.length - 1) return
|
||||||
this.processing = true
|
this.processing = true
|
||||||
var nextBookId = this.bookshelfBookIds[this.currentBookshelfIndex + 1]
|
var nextBookId = this.bookshelfBookIds[this.currentBookshelfIndex + 1]
|
||||||
var nextBook = await this.$axios.$get(`/api/books/${nextBookId}`).catch((error) => {
|
var nextBook = await this.$axios.$get(`/api/items/${nextBookId}`).catch((error) => {
|
||||||
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed to fetch book'
|
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed to fetch book'
|
||||||
this.$toast.error(errorMsg)
|
this.$toast.error(errorMsg)
|
||||||
return null
|
return null
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
<div class="w-full overflow-y-auto overflow-x-hidden max-h-96">
|
<div class="w-full overflow-y-auto overflow-x-hidden max-h-96">
|
||||||
<transition-group name="list-complete" tag="div">
|
<transition-group name="list-complete" tag="div">
|
||||||
<template v-for="collection in sortedCollections">
|
<template v-for="collection in sortedCollections">
|
||||||
<modals-collections-user-collection-item :key="collection.id" :collection="collection" class="list-complete-item" @add="addToCollection" @remove="removeFromCollection" @close="show = false" />
|
<modals-collections-user-collection-item :key="collection.id" :collection="collection" :book-cover-aspect-ratio="bookCoverAspectRatio" class="list-complete-item" @add="addToCollection" @remove="removeFromCollection" @close="show = false" />
|
||||||
</template>
|
</template>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
</div>
|
</div>
|
||||||
@ -50,7 +50,7 @@ export default {
|
|||||||
this.loadCollections()
|
this.loadCollections()
|
||||||
this.newCollectionName = ''
|
this.newCollectionName = ''
|
||||||
} else {
|
} else {
|
||||||
this.$store.commit('setSelectedAudiobook', null)
|
this.$store.commit('setSelectedLibraryItem', null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -65,15 +65,18 @@ export default {
|
|||||||
},
|
},
|
||||||
title() {
|
title() {
|
||||||
if (this.showBatchUserCollectionModal) {
|
if (this.showBatchUserCollectionModal) {
|
||||||
return `${this.selectedBookIds.length} Books Selected`
|
return `${this.selectedBookIds.length} Items Selected`
|
||||||
}
|
}
|
||||||
return this.selectedAudiobook ? this.selectedAudiobook.book.title : ''
|
return this.selectedLibraryItem ? this.selectedLibraryItem.media.metadata.title : ''
|
||||||
},
|
},
|
||||||
selectedAudiobook() {
|
bookCoverAspectRatio() {
|
||||||
return this.$store.state.selectedAudiobook
|
return this.$store.getters['getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
selectedAudiobookId() {
|
selectedLibraryItem() {
|
||||||
return this.selectedAudiobook ? this.selectedAudiobook.id : null
|
return this.$store.state.selectedLibraryItem
|
||||||
|
},
|
||||||
|
selectedLibraryItemId() {
|
||||||
|
return this.selectedLibraryItem ? this.selectedLibraryItem.id : null
|
||||||
},
|
},
|
||||||
collections() {
|
collections() {
|
||||||
return this.$store.state.user.collections || []
|
return this.$store.state.user.collections || []
|
||||||
@ -87,7 +90,7 @@ export default {
|
|||||||
var collectionBookIds = c.books.map((b) => b.id)
|
var collectionBookIds = c.books.map((b) => b.id)
|
||||||
includesBook = !this.selectedBookIds.find((id) => !collectionBookIds.includes(id))
|
includesBook = !this.selectedBookIds.find((id) => !collectionBookIds.includes(id))
|
||||||
} else {
|
} else {
|
||||||
includesBook = !!c.books.find((b) => b.id === this.selectedAudiobookId)
|
includesBook = !!c.books.find((b) => b.id === this.selectedLibraryItemId)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -112,7 +115,7 @@ export default {
|
|||||||
this.$store.dispatch('user/loadUserCollections')
|
this.$store.dispatch('user/loadUserCollections')
|
||||||
},
|
},
|
||||||
removeFromCollection(collection) {
|
removeFromCollection(collection) {
|
||||||
if (!this.selectedAudiobookId && !this.selectedBookIds.length) return
|
if (!this.selectedLibraryItemId && !this.selectedBookIds.length) return
|
||||||
this.processing = true
|
this.processing = true
|
||||||
|
|
||||||
if (this.showBatchUserCollectionModal) {
|
if (this.showBatchUserCollectionModal) {
|
||||||
@ -132,7 +135,7 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
// Remove single book
|
// Remove single book
|
||||||
this.$axios
|
this.$axios
|
||||||
.$delete(`/api/collections/${collection.id}/book/${this.selectedAudiobookId}`)
|
.$delete(`/api/collections/${collection.id}/book/${this.selectedLibraryItemId}`)
|
||||||
.then((updatedCollection) => {
|
.then((updatedCollection) => {
|
||||||
console.log(`Book removed from collection`, updatedCollection)
|
console.log(`Book removed from collection`, updatedCollection)
|
||||||
this.$toast.success('Book removed from collection')
|
this.$toast.success('Book removed from collection')
|
||||||
@ -146,7 +149,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
addToCollection(collection) {
|
addToCollection(collection) {
|
||||||
if (!this.selectedAudiobookId && !this.selectedBookIds.length) return
|
if (!this.selectedLibraryItemId && !this.selectedBookIds.length) return
|
||||||
this.processing = true
|
this.processing = true
|
||||||
|
|
||||||
if (this.showBatchUserCollectionModal) {
|
if (this.showBatchUserCollectionModal) {
|
||||||
@ -164,10 +167,10 @@ export default {
|
|||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
if (!this.selectedAudiobookId) return
|
if (!this.selectedLibraryItemId) return
|
||||||
|
|
||||||
this.$axios
|
this.$axios
|
||||||
.$post(`/api/collections/${collection.id}/book`, { id: this.selectedAudiobookId })
|
.$post(`/api/collections/${collection.id}/book`, { id: this.selectedLibraryItemId })
|
||||||
.then((updatedCollection) => {
|
.then((updatedCollection) => {
|
||||||
console.log(`Book added to collection`, updatedCollection)
|
console.log(`Book added to collection`, updatedCollection)
|
||||||
this.$toast.success('Book added to collection')
|
this.$toast.success('Book added to collection')
|
||||||
@ -181,12 +184,12 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
submitCreateCollection() {
|
submitCreateCollection() {
|
||||||
if (!this.newCollectionName || (!this.selectedAudiobookId && !this.selectedBookIds.length)) {
|
if (!this.newCollectionName || (!this.selectedLibraryItemId && !this.selectedBookIds.length)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.processing = true
|
this.processing = true
|
||||||
|
|
||||||
var books = this.showBatchUserCollectionModal ? this.selectedBookIds : [this.selectedAudiobookId]
|
var books = this.showBatchUserCollectionModal ? this.selectedBookIds : [this.selectedLibraryItemId]
|
||||||
var newCollection = {
|
var newCollection = {
|
||||||
books: books,
|
books: books,
|
||||||
libraryId: this.currentLibraryId,
|
libraryId: this.currentLibraryId,
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<!-- <span class="material-icons" :class="highlight ? 'text-success' : 'text-white text-opacity-80'">{{ highlight ? 'bookmark' : 'bookmark_border' }}</span> -->
|
<!-- <span class="material-icons" :class="highlight ? 'text-success' : 'text-white text-opacity-80'">{{ highlight ? 'bookmark' : 'bookmark_border' }}</span> -->
|
||||||
<div class="w-20 max-w-20 text-center">
|
<div class="w-20 max-w-20 text-center">
|
||||||
<!-- <img src="/Logo.png" /> -->
|
<!-- <img src="/Logo.png" /> -->
|
||||||
<covers-collection-cover :book-items="books" :width="80" :height="40 * 1.6" />
|
<covers-collection-cover :book-items="books" :width="80" :height="40 * bookCoverAspectRatio" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow overflow-hidden px-2">
|
<div class="flex-grow overflow-hidden px-2">
|
||||||
<!-- <template v-if="isEditing">
|
<!-- <template v-if="isEditing">
|
||||||
@ -38,7 +38,8 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
},
|
},
|
||||||
highlight: Boolean
|
highlight: Boolean,
|
||||||
|
bookCoverAspectRatio: Number
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -154,7 +154,7 @@ export default {
|
|||||||
var coverPayload = {
|
var coverPayload = {
|
||||||
url: updatePayload.cover
|
url: updatePayload.cover
|
||||||
}
|
}
|
||||||
var success = await this.$axios.$post(`/api/books/${this.audiobook.id}/cover`, coverPayload).catch((error) => {
|
var success = await this.$axios.$post(`/api/items/${this.audiobook.id}/cover`, coverPayload).catch((error) => {
|
||||||
console.error('Failed to update', error)
|
console.error('Failed to update', error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
@ -171,7 +171,7 @@ export default {
|
|||||||
var bookUpdatePayload = {
|
var bookUpdatePayload = {
|
||||||
book: updatePayload
|
book: updatePayload
|
||||||
}
|
}
|
||||||
var success = await this.$axios.$patch(`/api/books/${this.audiobook.id}`, bookUpdatePayload).catch((error) => {
|
var success = await this.$axios.$patch(`/api/items/${this.audiobook.id}`, bookUpdatePayload).catch((error) => {
|
||||||
console.error('Failed to update', error)
|
console.error('Failed to update', error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
<div v-if="showLocalCovers" class="flex items-center justify-center">
|
<div v-if="showLocalCovers" class="flex items-center justify-center">
|
||||||
<template v-for="cover in localCovers">
|
<template v-for="cover in localCovers">
|
||||||
<div :key="cover.path" class="m-0.5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover.localPath === imageUrl ? 'border-yellow-300' : ''" @click="setCover(cover)">
|
<div :key="cover.path" class="m-0.5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover.metadata.path === coverPath ? 'border-yellow-300' : ''" @click="setCover(cover)">
|
||||||
<div class="h-24 bg-primary" :style="{ width: 96 / bookCoverAspectRatio + 'px' }">
|
<div class="h-24 bg-primary" :style="{ width: 96 / bookCoverAspectRatio + 'px' }">
|
||||||
<img :src="`${cover.localPath}?token=${userToken}`" class="h-full w-full object-contain" />
|
<img :src="`${cover.localPath}?token=${userToken}`" class="h-full w-full object-contain" />
|
||||||
</div>
|
</div>
|
||||||
@ -140,6 +140,9 @@ export default {
|
|||||||
media() {
|
media() {
|
||||||
return this.libraryItem ? this.libraryItem.media || {} : {}
|
return this.libraryItem ? this.libraryItem.media || {} : {}
|
||||||
},
|
},
|
||||||
|
coverPath() {
|
||||||
|
return this.media.coverPath
|
||||||
|
},
|
||||||
mediaMetadata() {
|
mediaMetadata() {
|
||||||
return this.media.metadata || {}
|
return this.media.metadata || {}
|
||||||
},
|
},
|
||||||
@ -222,7 +225,7 @@ export default {
|
|||||||
this.updateCover(this.imageUrl)
|
this.updateCover(this.imageUrl)
|
||||||
},
|
},
|
||||||
async updateCover(cover) {
|
async updateCover(cover) {
|
||||||
if (cover === this.media.coverPath) {
|
if (cover === this.coverPath) {
|
||||||
console.warn('Cover has not changed..', cover)
|
console.warn('Cover has not changed..', cover)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -299,23 +302,7 @@ export default {
|
|||||||
this.hasSearched = true
|
this.hasSearched = true
|
||||||
},
|
},
|
||||||
setCover(coverFile) {
|
setCover(coverFile) {
|
||||||
this.isProcessing = true
|
this.updateCover(coverFile.metadata.path)
|
||||||
this.$axios
|
|
||||||
.$patch(`/api/books/${this.libraryItemId}/coverfile`, coverFile)
|
|
||||||
.then((data) => {
|
|
||||||
console.log('response data', data)
|
|
||||||
if (data && typeof data === 'string') {
|
|
||||||
this.$toast.success(data)
|
|
||||||
}
|
|
||||||
this.isProcessing = false
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Failed to update', error)
|
|
||||||
if (error.response && error.response.data) {
|
|
||||||
this.$toast.error(error.response.data)
|
|
||||||
}
|
|
||||||
this.isProcessing = false
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -84,14 +84,22 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
quickMatch() {
|
quickMatch() {
|
||||||
|
if (this.quickMatching) return
|
||||||
|
if (!this.$refs.itemDetailsEdit) return
|
||||||
|
|
||||||
|
var { title, author } = this.$refs.itemDetailsEdit.getTitleAndAuthorName()
|
||||||
|
if (!title) {
|
||||||
|
this.$toast.error('Must have a title for quick match')
|
||||||
|
return
|
||||||
|
}
|
||||||
this.quickMatching = true
|
this.quickMatching = true
|
||||||
var matchOptions = {
|
var matchOptions = {
|
||||||
provider: this.libraryProvider,
|
provider: this.libraryProvider,
|
||||||
title: this.details.title,
|
title: title || null,
|
||||||
author: this.details.author !== this.book.author ? this.details.author : null
|
author: author || null
|
||||||
}
|
}
|
||||||
this.$axios
|
this.$axios
|
||||||
.$post(`/api/books/${this.libraryItemId}/match`, matchOptions)
|
.$post(`/api/items/${this.libraryItemId}/match`, matchOptions)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
this.quickMatching = false
|
this.quickMatching = false
|
||||||
if (res.warning) {
|
if (res.warning) {
|
||||||
|
@ -61,9 +61,9 @@
|
|||||||
<ui-checkbox v-model="selectedMatchUsage.publisher" />
|
<ui-checkbox v-model="selectedMatchUsage.publisher" />
|
||||||
<ui-text-input-with-label v-model="selectedMatch.publisher" :disabled="!selectedMatchUsage.publisher" label="Publisher" class="flex-grow ml-4" />
|
<ui-text-input-with-label v-model="selectedMatch.publisher" :disabled="!selectedMatchUsage.publisher" label="Publisher" class="flex-grow ml-4" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedMatch.publishYear" class="flex items-center py-2">
|
<div v-if="selectedMatch.publishedYear" class="flex items-center py-2">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.publishYear" />
|
<ui-checkbox v-model="selectedMatchUsage.publishedYear" />
|
||||||
<ui-text-input-with-label v-model="selectedMatch.publishYear" :disabled="!selectedMatchUsage.publishYear" label="Publish Year" class="flex-grow ml-4" />
|
<ui-text-input-with-label v-model="selectedMatch.publishedYear" :disabled="!selectedMatchUsage.publishedYear" label="Published Year" class="flex-grow ml-4" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="selectedMatch.series" class="flex items-center py-2">
|
<div v-if="selectedMatch.series" class="flex items-center py-2">
|
||||||
@ -117,7 +117,7 @@ export default {
|
|||||||
narrator: true,
|
narrator: true,
|
||||||
description: true,
|
description: true,
|
||||||
publisher: true,
|
publisher: true,
|
||||||
publishYear: true,
|
publishedYear: true,
|
||||||
series: true,
|
series: true,
|
||||||
volumeNumber: true,
|
volumeNumber: true,
|
||||||
asin: true,
|
asin: true,
|
||||||
@ -202,7 +202,7 @@ export default {
|
|||||||
narrator: true,
|
narrator: true,
|
||||||
description: true,
|
description: true,
|
||||||
publisher: true,
|
publisher: true,
|
||||||
publishYear: true,
|
publishedYear: true,
|
||||||
series: true,
|
series: true,
|
||||||
volumeNumber: true,
|
volumeNumber: true,
|
||||||
asin: true,
|
asin: true,
|
||||||
@ -247,7 +247,7 @@ export default {
|
|||||||
var coverPayload = {
|
var coverPayload = {
|
||||||
url: updatePayload.cover
|
url: updatePayload.cover
|
||||||
}
|
}
|
||||||
var success = await this.$axios.$post(`/api/books/${this.libraryItemId}/cover`, coverPayload).catch((error) => {
|
var success = await this.$axios.$post(`/api/items/${this.libraryItemId}/cover`, coverPayload).catch((error) => {
|
||||||
console.error('Failed to update', error)
|
console.error('Failed to update', error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
@ -264,7 +264,7 @@ export default {
|
|||||||
var bookUpdatePayload = {
|
var bookUpdatePayload = {
|
||||||
book: updatePayload
|
book: updatePayload
|
||||||
}
|
}
|
||||||
var success = await this.$axios.$patch(`/api/books/${this.libraryItemId}`, bookUpdatePayload).catch((error) => {
|
var success = await this.$axios.$patch(`/api/items/${this.libraryItemId}`, bookUpdatePayload).catch((error) => {
|
||||||
console.error('Failed to update', error)
|
console.error('Failed to update', error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
@ -47,22 +47,22 @@ export default {
|
|||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
abTitle() {
|
abTitle() {
|
||||||
return this.selectedAudiobook.book.title
|
return this.selectedLibraryItem.media.metadata.title
|
||||||
},
|
},
|
||||||
abAuthor() {
|
abAuthor() {
|
||||||
return this.selectedAudiobook.book.author
|
return this.selectedLibraryItem.media.metadata.authorName
|
||||||
},
|
},
|
||||||
selectedAudiobook() {
|
selectedLibraryItem() {
|
||||||
return this.$store.state.selectedAudiobook
|
return this.$store.state.selectedLibraryItem
|
||||||
},
|
},
|
||||||
libraryId() {
|
libraryId() {
|
||||||
return this.selectedAudiobook.libraryId
|
return this.selectedLibraryItem.libraryId
|
||||||
},
|
},
|
||||||
folderId() {
|
folderId() {
|
||||||
return this.selectedAudiobook.folderId
|
return this.selectedLibraryItem.folderId
|
||||||
},
|
},
|
||||||
ebooks() {
|
ebooks() {
|
||||||
return this.selectedAudiobook.ebooks || []
|
return this.selectedLibraryItem.media.ebooks || []
|
||||||
},
|
},
|
||||||
epubEbook() {
|
epubEbook() {
|
||||||
return this.ebooks.find((eb) => eb.ext === '.epub')
|
return this.ebooks.find((eb) => eb.ext === '.epub')
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<p class="pr-4">Other Audio Files</p>
|
<p class="pr-4">Other Audio Files</p>
|
||||||
<span class="bg-black-400 rounded-xl py-1 px-2 text-sm font-mono">{{ files.length }}</span>
|
<span class="bg-black-400 rounded-xl py-1 px-2 text-sm font-mono">{{ files.length }}</span>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<nuxt-link v-if="userCanUpdate" :to="`/audiobook/${audiobookId}/edit`" class="mr-4">
|
<nuxt-link v-if="userCanUpdate" :to="`/item/${audiobookId}/edit`" class="mr-4">
|
||||||
<ui-btn small color="primary">Manage Tracks</ui-btn>
|
<ui-btn small color="primary">Manage Tracks</ui-btn>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showTracks ? 'transform rotate-180' : ''">
|
<div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showTracks ? 'transform rotate-180' : ''">
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
<!-- <span class="bg-black-400 rounded-xl py-1 px-2 text-sm font-mono">{{ tracks.length }}</span> -->
|
<!-- <span class="bg-black-400 rounded-xl py-1 px-2 text-sm font-mono">{{ tracks.length }}</span> -->
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="showFullPath = !showFullPath">Full Path</ui-btn>
|
<ui-btn small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="showFullPath = !showFullPath">Full Path</ui-btn>
|
||||||
<nuxt-link v-if="userCanUpdate" :to="`/audiobook/${libraryItemId}/edit`" class="mr-2 md:mr-4">
|
<nuxt-link v-if="userCanUpdate" :to="`/item/${libraryItemId}/edit`" class="mr-2 md:mr-4">
|
||||||
<ui-btn small color="primary">Manage Tracks</ui-btn>
|
<ui-btn small color="primary">Manage Tracks</ui-btn>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showTracks ? 'transform rotate-180' : ''">
|
<div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showTracks ? 'transform rotate-180' : ''">
|
||||||
|
@ -76,9 +76,6 @@ export default {
|
|||||||
currentUserId() {
|
currentUserId() {
|
||||||
return this.$store.state.user.user.id
|
return this.$store.state.user.user.id
|
||||||
},
|
},
|
||||||
userStream() {
|
|
||||||
return this.$store.state.streamAudiobook
|
|
||||||
},
|
|
||||||
usersOnline() {
|
usersOnline() {
|
||||||
var usermap = {}
|
var usermap = {}
|
||||||
this.$store.state.users.users.forEach((u) => (usermap[u.id] = { online: true, stream: u.stream }))
|
this.$store.state.users.users.forEach((u) => (usermap[u.id] = { online: true, stream: u.stream }))
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
<ui-multi-select-query-input ref="authorsSelect" v-model="details.authors" label="Authors" endpoint="authors/search" />
|
<ui-multi-select-query-input ref="authorsSelect" v-model="details.authors" label="Authors" endpoint="authors/search" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-1">
|
<div class="flex-grow px-1">
|
||||||
<ui-text-input-with-label v-model="details.publishYear" type="number" label="Publish Year" />
|
<ui-text-input-with-label v-model="details.publishedYear" type="number" label="Publish Year" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -108,7 +108,7 @@ export default {
|
|||||||
authors: [],
|
authors: [],
|
||||||
narrators: [],
|
narrators: [],
|
||||||
series: [],
|
series: [],
|
||||||
publishYear: null,
|
publishedYear: null,
|
||||||
publisher: null,
|
publisher: null,
|
||||||
language: null,
|
language: null,
|
||||||
isbn: null,
|
isbn: null,
|
||||||
@ -173,6 +173,13 @@ export default {
|
|||||||
this.forceBlur()
|
this.forceBlur()
|
||||||
return this.checkForChanges()
|
return this.checkForChanges()
|
||||||
},
|
},
|
||||||
|
getTitleAndAuthorName() {
|
||||||
|
this.forceBlur()
|
||||||
|
return {
|
||||||
|
title: this.details.title,
|
||||||
|
author: (this.details.authors || []).map((au) => au.name).join(', ')
|
||||||
|
}
|
||||||
|
},
|
||||||
mapBatchDetails(batchDetails) {
|
mapBatchDetails(batchDetails) {
|
||||||
for (const key in batchDetails) {
|
for (const key in batchDetails) {
|
||||||
if (key === 'tags') {
|
if (key === 'tags') {
|
||||||
@ -317,7 +324,7 @@ export default {
|
|||||||
this.details.narrators = [...(this.mediaMetadata.narrators || [])]
|
this.details.narrators = [...(this.mediaMetadata.narrators || [])]
|
||||||
this.details.genres = [...(this.mediaMetadata.genres || [])]
|
this.details.genres = [...(this.mediaMetadata.genres || [])]
|
||||||
this.details.series = (this.mediaMetadata.series || []).map((se) => ({ ...se }))
|
this.details.series = (this.mediaMetadata.series || []).map((se) => ({ ...se }))
|
||||||
this.details.publishYear = this.mediaMetadata.publishYear
|
this.details.publishedYear = this.mediaMetadata.publishedYear
|
||||||
this.details.publisher = this.mediaMetadata.publisher || null
|
this.details.publisher = this.mediaMetadata.publisher || null
|
||||||
this.details.language = this.mediaMetadata.language || null
|
this.details.language = this.mediaMetadata.language || null
|
||||||
this.details.isbn = this.mediaMetadata.isbn || null
|
this.details.isbn = this.mediaMetadata.isbn || null
|
||||||
|
@ -35,9 +35,6 @@ export default {
|
|||||||
if (this.$store.state.selectedLibraryItems) {
|
if (this.$store.state.selectedLibraryItems) {
|
||||||
this.$store.commit('setSelectedLibraryItems', [])
|
this.$store.commit('setSelectedLibraryItems', [])
|
||||||
}
|
}
|
||||||
if (this.$store.state.audiobooks.keywordFilter) {
|
|
||||||
this.$store.commit('audiobooks/setKeywordFilter', '')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -282,12 +279,6 @@ export default {
|
|||||||
if (!download || !download.audiobookId) {
|
if (!download || !download.audiobookId) {
|
||||||
return console.error('Invalid download object', download)
|
return console.error('Invalid download object', download)
|
||||||
}
|
}
|
||||||
|
|
||||||
// var audiobook = this.$store.getters['audiobooks/getAudiobook'](download.audiobookId)
|
|
||||||
// if (!audiobook) {
|
|
||||||
// return console.error('Audiobook not found for download', download)
|
|
||||||
// }
|
|
||||||
// this.$store.commit('showEditModalOnTab', { audiobook, tab: 'download' })
|
|
||||||
},
|
},
|
||||||
downloadStarted(download) {
|
downloadStarted(download) {
|
||||||
download.status = this.$constants.DownloadStatus.PENDING
|
download.status = this.$constants.DownloadStatus.PENDING
|
||||||
@ -495,7 +486,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Playing audiobook
|
// Playing audiobook
|
||||||
if (this.$store.state.streamAudiobook && Object.values(this.$hotkeys.AudioPlayer).includes(name)) {
|
if (this.$store.state.streamLibraryItem && Object.values(this.$hotkeys.AudioPlayer).includes(name)) {
|
||||||
this.$eventBus.$emit('player-hotkey', name)
|
this.$eventBus.$emit('player-hotkey', name)
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="page-wrapper" class="page p-6 overflow-y-auto relative" :class="streamAudiobook ? 'streaming' : ''">
|
<div id="page-wrapper" class="page p-6 overflow-y-auto relative" :class="streamLibraryItem ? 'streaming' : ''">
|
||||||
<div class="w-full max-w-xl mx-auto">
|
<div class="w-full max-w-xl mx-auto">
|
||||||
<h1 class="text-2xl">Account</h1>
|
<h1 class="text-2xl">Account</h1>
|
||||||
|
|
||||||
@ -46,8 +46,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
streamAudiobook() {
|
streamLibraryItem() {
|
||||||
return this.$store.state.streamAudiobook
|
return this.$store.state.streamLibraryItem
|
||||||
},
|
},
|
||||||
user() {
|
user() {
|
||||||
return this.$store.state.user.user || null
|
return this.$store.state.user.user || null
|
||||||
|
@ -1,280 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div id="page-wrapper" class="bg-bg page overflow-hidden relative" :class="streamAudiobook ? 'streaming' : ''">
|
|
||||||
<div v-show="saving" class="absolute z-20 w-full h-full flex items-center justify-center">
|
|
||||||
<ui-loading-indicator />
|
|
||||||
</div>
|
|
||||||
<div class="w-full h-full overflow-y-auto p-8">
|
|
||||||
<div class="w-full flex justify-between items-center pb-6 pt-2">
|
|
||||||
<p class="text-lg">Drag files into correct track order</p>
|
|
||||||
<ui-btn color="success" @click="saveTracklist">Save Tracklist</ui-btn>
|
|
||||||
</div>
|
|
||||||
<div class="w-full flex items-center text-sm py-4 bg-primary border-l border-r border-t border-gray-600">
|
|
||||||
<div class="font-book text-center px-4 w-12">New</div>
|
|
||||||
<div class="font-book text-center px-4 w-24 flex items-center cursor-pointer text-white text-opacity-40 hover:text-opacity-100" @click="sortByCurrent" @mousedown.prevent>
|
|
||||||
<span class="text-white">Current</span>
|
|
||||||
<span class="material-icons ml-1" :class="currentSort === 'current' ? 'text-white text-opacity-100 text-lg' : 'text-sm'">{{ currentSort === 'current' ? 'expand_more' : 'unfold_more' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="font-book text-center px-4 w-32 flex items-center cursor-pointer text-white text-opacity-40 hover:text-opacity-100" @click="sortByFilenameTrack" @mousedown.prevent>
|
|
||||||
<span class="text-white">Track From Filename</span>
|
|
||||||
<span class="material-icons ml-1" :class="currentSort === 'track-filename' ? 'text-white text-opacity-100 text-lg' : 'text-sm'">{{ currentSort === 'track-filename' ? 'expand_more' : 'unfold_more' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="font-book text-center px-4 w-32 flex items-center cursor-pointer text-white text-opacity-40 hover:text-opacity-100" @click="sortByMetadataTrack" @mousedown.prevent>
|
|
||||||
<span class="text-white">Track From Metadata</span>
|
|
||||||
<span class="material-icons ml-1" :class="currentSort === 'metadata' ? 'text-white text-opacity-100 text-lg' : 'text-sm'">{{ currentSort === 'metadata' ? 'expand_more' : 'unfold_more' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="font-mono w-20 text-center">Disc From Filename</div>
|
|
||||||
<div class="font-mono w-20 text-center">Disc From Metadata</div>
|
|
||||||
<div class="font-book text-center px-4 flex-grow flex items-center cursor-pointer text-white text-opacity-40 hover:text-opacity-100" @click="sortByFilename" @mousedown.prevent>
|
|
||||||
<span class="text-white">Filename</span>
|
|
||||||
<span class="material-icons ml-1" :class="currentSort === 'filename' ? 'text-white text-opacity-100 text-lg' : 'text-sm'">{{ currentSort === 'filename' ? 'expand_more' : 'unfold_more' }}</span>
|
|
||||||
</div>
|
|
||||||
<!-- <div class="font-book truncate px-4 flex-grow">Filename</div> -->
|
|
||||||
|
|
||||||
<div class="font-mono w-20 text-center">Size</div>
|
|
||||||
<div class="font-mono w-20 text-center">Duration</div>
|
|
||||||
<div class="font-mono text-center w-20">Status</div>
|
|
||||||
<div class="font-mono w-56">Notes</div>
|
|
||||||
<div class="font-book w-40">Include in Tracklist</div>
|
|
||||||
</div>
|
|
||||||
<draggable v-model="files" v-bind="dragOptions" class="list-group border border-gray-600" draggable=".item" tag="ul" @start="drag = true" @end="drag = false" @update="draggableUpdate">
|
|
||||||
<transition-group type="transition" :name="!drag ? 'flip-list' : null">
|
|
||||||
<li v-for="(audio, index) in files" :key="audio.path" :class="audio.include ? 'item' : 'exclude'" class="w-full list-group-item flex items-center">
|
|
||||||
<div class="font-book text-center px-4 py-1 w-12">
|
|
||||||
{{ audio.include ? index - numExcluded + 1 : -1 }}
|
|
||||||
</div>
|
|
||||||
<div class="font-book text-center px-4 w-24">{{ audio.index }}</div>
|
|
||||||
<div class="font-book text-center px-2 w-32">
|
|
||||||
{{ audio.trackNumFromFilename }}
|
|
||||||
</div>
|
|
||||||
<div class="font-book text-center w-32">
|
|
||||||
{{ audio.trackNumFromMeta }}
|
|
||||||
</div>
|
|
||||||
<div class="font-book truncate px-4 w-20">
|
|
||||||
{{ audio.discNumFromFilename }}
|
|
||||||
</div>
|
|
||||||
<div class="font-book truncate px-4 w-20">
|
|
||||||
{{ audio.discNumFromMeta }}
|
|
||||||
</div>
|
|
||||||
<div class="font-book truncate px-4 flex-grow">
|
|
||||||
{{ audio.filename }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="font-mono w-20 text-center">
|
|
||||||
{{ $bytesPretty(audio.size) }}
|
|
||||||
</div>
|
|
||||||
<div class="font-mono w-20">
|
|
||||||
{{ $secondsToTimestamp(audio.duration) }}
|
|
||||||
</div>
|
|
||||||
<div class="font-mono text-center w-20">
|
|
||||||
<span class="material-icons text-sm" :class="audio.invalid ? 'text-error' : 'text-success'">{{ getStatusIcon(audio) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="font-sans text-xs font-normal w-56">
|
|
||||||
{{ audio.error }}
|
|
||||||
</div>
|
|
||||||
<div class="font-sans text-xs font-normal w-40 flex justify-center">
|
|
||||||
<ui-toggle-switch v-model="audio.include" :off-color="'error'" @input="includeToggled(audio)" />
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</transition-group>
|
|
||||||
</draggable>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import draggable from 'vuedraggable'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
draggable
|
|
||||||
},
|
|
||||||
async asyncData({ store, params, app, redirect, route }) {
|
|
||||||
if (!store.state.user.user) {
|
|
||||||
return redirect(`/login?redirect=${route.path}`)
|
|
||||||
}
|
|
||||||
if (!store.getters['user/getUserCanUpdate']) {
|
|
||||||
return redirect('/?error=unauthorized')
|
|
||||||
}
|
|
||||||
var audiobook = await app.$axios.$get(`/api/books/${params.id}`).catch((error) => {
|
|
||||||
console.error('Failed', error)
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
if (!audiobook) {
|
|
||||||
console.error('No audiobook...', params.id)
|
|
||||||
return redirect('/')
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
audiobook,
|
|
||||||
files: audiobook.audioFiles ? audiobook.audioFiles.map((af) => ({ ...af, include: !af.exclude })) : []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
drag: false,
|
|
||||||
dragOptions: {
|
|
||||||
animation: 200,
|
|
||||||
group: 'description',
|
|
||||||
ghostClass: 'ghost'
|
|
||||||
},
|
|
||||||
saving: false,
|
|
||||||
currentSort: 'current'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
audioFiles() {
|
|
||||||
return this.audiobook.audioFiles || []
|
|
||||||
},
|
|
||||||
numExcluded() {
|
|
||||||
var count = 0
|
|
||||||
this.files.forEach((file) => {
|
|
||||||
if (!file.include) count++
|
|
||||||
})
|
|
||||||
return count
|
|
||||||
},
|
|
||||||
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
|
|
||||||
},
|
|
||||||
missingParts() {
|
|
||||||
return this.audiobook.missingParts || []
|
|
||||||
},
|
|
||||||
invalidParts() {
|
|
||||||
return this.audiobook.invalidParts || []
|
|
||||||
},
|
|
||||||
audiobookId() {
|
|
||||||
return this.audiobook.id
|
|
||||||
},
|
|
||||||
title() {
|
|
||||||
return this.book.title || 'No Title'
|
|
||||||
},
|
|
||||||
author() {
|
|
||||||
return this.book.author || 'Unknown'
|
|
||||||
},
|
|
||||||
tracks() {
|
|
||||||
return this.audiobook.tracks
|
|
||||||
},
|
|
||||||
durationPretty() {
|
|
||||||
return this.audiobook.durationPretty
|
|
||||||
},
|
|
||||||
sizePretty() {
|
|
||||||
return this.audiobook.sizePretty
|
|
||||||
},
|
|
||||||
book() {
|
|
||||||
return this.audiobook.book || {}
|
|
||||||
},
|
|
||||||
tracks() {
|
|
||||||
return this.audiobook.tracks || []
|
|
||||||
},
|
|
||||||
streamAudiobook() {
|
|
||||||
return this.$store.state.streamAudiobook
|
|
||||||
},
|
|
||||||
showExperimentalFeatures() {
|
|
||||||
return this.$store.state.showExperimentalFeatures
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
draggableUpdate(e) {
|
|
||||||
this.currentSort = ''
|
|
||||||
},
|
|
||||||
sortByCurrent() {
|
|
||||||
this.files.sort((a, b) => {
|
|
||||||
if (a.index === null) return 1
|
|
||||||
return a.index - b.index
|
|
||||||
})
|
|
||||||
this.currentSort = 'current'
|
|
||||||
},
|
|
||||||
sortByMetadataTrack() {
|
|
||||||
this.files.sort((a, b) => {
|
|
||||||
if (a.trackNumFromMeta === null) return 1
|
|
||||||
return a.trackNumFromMeta - b.trackNumFromMeta
|
|
||||||
})
|
|
||||||
this.currentSort = 'metadata'
|
|
||||||
},
|
|
||||||
sortByFilenameTrack() {
|
|
||||||
this.files.sort((a, b) => {
|
|
||||||
if (a.trackNumFromFilename === null) return 1
|
|
||||||
return a.trackNumFromFilename - b.trackNumFromFilename
|
|
||||||
})
|
|
||||||
this.currentSort = 'track-filename'
|
|
||||||
},
|
|
||||||
sortByFilename() {
|
|
||||||
this.files.sort((a, b) => {
|
|
||||||
return (a.filename || '').toLowerCase().localeCompare((b.filename || '').toLowerCase())
|
|
||||||
})
|
|
||||||
this.currentSort = 'filename'
|
|
||||||
},
|
|
||||||
includeToggled(audio) {
|
|
||||||
var new_index = 0
|
|
||||||
if (audio.include) {
|
|
||||||
new_index = this.numExcluded
|
|
||||||
}
|
|
||||||
var old_index = this.files.findIndex((f) => f.ino === audio.ino)
|
|
||||||
if (new_index >= this.files.length) {
|
|
||||||
var k = new_index - this.files.length + 1
|
|
||||||
while (k--) {
|
|
||||||
this.files.push(undefined)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.files.splice(new_index, 0, this.files.splice(old_index, 1)[0])
|
|
||||||
},
|
|
||||||
saveTracklist() {
|
|
||||||
var orderedFileData = this.files.map((file) => {
|
|
||||||
return {
|
|
||||||
index: file.index,
|
|
||||||
filename: file.filename,
|
|
||||||
ino: file.ino,
|
|
||||||
exclude: !file.include
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
this.saving = true
|
|
||||||
this.$axios
|
|
||||||
.$patch(`/api/books/${this.audiobook.id}/tracks`, { orderedFileData })
|
|
||||||
.then((data) => {
|
|
||||||
console.log('Finished patching files', data)
|
|
||||||
this.saving = false
|
|
||||||
this.$toast.success('Tracks Updated')
|
|
||||||
this.$router.push(`/audiobook/${this.audiobookId}`)
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Failed', error)
|
|
||||||
this.saving = false
|
|
||||||
})
|
|
||||||
},
|
|
||||||
getStatusIcon(audio) {
|
|
||||||
if (audio.invalid) {
|
|
||||||
return 'error_outline'
|
|
||||||
} else {
|
|
||||||
return 'check_circle'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
@ -1,494 +0,0 @@
|
|||||||
<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 :audiobook="audiobook" :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="authorFL" 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-if="series" :to="`/library/${libraryId}/series/${$encode(series)}`" class="hover:underline font-sans text-gray-300 text-lg leading-7"> {{ seriesText }}</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>
|
|
||||||
|
|
||||||
<ui-btn v-if="isDeveloperMode" class="mx-2" @click="openRssFeed">Open RSS Feed</ui-btn>
|
|
||||||
</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" :audiobook="audiobook" class="mt-6" />
|
|
||||||
|
|
||||||
<tables-audio-files-table v-if="otherAudioFiles.length" :audiobook-id="audiobook.id" :files="otherAudioFiles" class="mt-6" />
|
|
||||||
|
|
||||||
<tables-library-files-table v-if="otherFiles.length" :audiobook="audiobook" :files="otherFiles" 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 audiobook = await app.$axios.$get(`/api/books/${params.id}`).catch((error) => {
|
|
||||||
console.error('Failed', error)
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
if (!audiobook) {
|
|
||||||
console.error('No audiobook...', params.id)
|
|
||||||
return redirect('/')
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
audiobook
|
|
||||||
}
|
|
||||||
},
|
|
||||||
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.audiobook.isMissing
|
|
||||||
},
|
|
||||||
isInvalid() {
|
|
||||||
return this.audiobook.isInvalid
|
|
||||||
},
|
|
||||||
showPlayButton() {
|
|
||||||
return !this.isMissing && !this.isInvalid && this.tracks.length
|
|
||||||
},
|
|
||||||
missingParts() {
|
|
||||||
return this.audiobook.missingParts || []
|
|
||||||
},
|
|
||||||
invalidParts() {
|
|
||||||
return this.audiobook.invalidParts || []
|
|
||||||
},
|
|
||||||
libraryId() {
|
|
||||||
return this.audiobook.libraryId
|
|
||||||
},
|
|
||||||
folderId() {
|
|
||||||
return this.audiobook.folderId
|
|
||||||
},
|
|
||||||
audiobookId() {
|
|
||||||
return this.audiobook.id
|
|
||||||
},
|
|
||||||
title() {
|
|
||||||
return this.book.title || 'No Title'
|
|
||||||
},
|
|
||||||
publishYear() {
|
|
||||||
return this.book.publishYear
|
|
||||||
},
|
|
||||||
narrator() {
|
|
||||||
return this.book.narrator
|
|
||||||
},
|
|
||||||
subtitle() {
|
|
||||||
return this.book.subtitle
|
|
||||||
},
|
|
||||||
genres() {
|
|
||||||
return this.book.genres || []
|
|
||||||
},
|
|
||||||
author() {
|
|
||||||
return this.book.author || 'Unknown'
|
|
||||||
},
|
|
||||||
authorFL() {
|
|
||||||
return this.book.authorFL
|
|
||||||
},
|
|
||||||
authorsList() {
|
|
||||||
return this.authorFL ? this.authorFL.split(', ') : []
|
|
||||||
},
|
|
||||||
authorLF() {
|
|
||||||
return this.book.authorLF
|
|
||||||
},
|
|
||||||
narrators() {
|
|
||||||
if (!this.book.narratorFL) return []
|
|
||||||
return this.book.narratorFL.split(', ') || []
|
|
||||||
},
|
|
||||||
authorTooltipText() {
|
|
||||||
var txt = ['FL: ' + this.authorFL || 'Not Set', 'LF: ' + this.authorLF || 'Not Set']
|
|
||||||
return txt.join('<br>')
|
|
||||||
},
|
|
||||||
series() {
|
|
||||||
return this.book.series || null
|
|
||||||
},
|
|
||||||
volumeNumber() {
|
|
||||||
return this.book.volumeNumber || null
|
|
||||||
},
|
|
||||||
seriesText() {
|
|
||||||
if (!this.series) return ''
|
|
||||||
if (!this.volumeNumber) return this.series
|
|
||||||
return `${this.series} #${this.volumeNumber}`
|
|
||||||
},
|
|
||||||
durationPretty() {
|
|
||||||
return this.audiobook.durationPretty
|
|
||||||
},
|
|
||||||
duration() {
|
|
||||||
return this.audiobook.duration
|
|
||||||
},
|
|
||||||
sizePretty() {
|
|
||||||
return this.audiobook.sizePretty
|
|
||||||
},
|
|
||||||
book() {
|
|
||||||
return this.audiobook.book || {}
|
|
||||||
},
|
|
||||||
otherFiles() {
|
|
||||||
return this.audiobook.otherFiles || []
|
|
||||||
},
|
|
||||||
otherAudioFiles() {
|
|
||||||
return this.audioFiles.filter((af) => {
|
|
||||||
return !this.tracks.find((t) => t.path === af.path)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
tracks() {
|
|
||||||
return this.audiobook.tracks || []
|
|
||||||
},
|
|
||||||
audioFiles() {
|
|
||||||
return this.audiobook.audioFiles || []
|
|
||||||
},
|
|
||||||
ebooks() {
|
|
||||||
return this.audiobook.ebooks
|
|
||||||
},
|
|
||||||
showExperimentalReadAlert() {
|
|
||||||
return !this.tracks.length && this.ebooks.length && !this.showExperimentalFeatures
|
|
||||||
},
|
|
||||||
numEbooks() {
|
|
||||||
return this.audiobook.numEbooks
|
|
||||||
},
|
|
||||||
description() {
|
|
||||||
return this.book.description || ''
|
|
||||||
},
|
|
||||||
userAudiobooks() {
|
|
||||||
return this.$store.state.user.user ? this.$store.state.user.user.audiobooks || {} : {}
|
|
||||||
},
|
|
||||||
userAudiobook() {
|
|
||||||
return this.userAudiobooks[this.audiobookId] || 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.audiobookId
|
|
||||||
},
|
|
||||||
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', { audiobook: this.audiobook, tab: 'cover' })
|
|
||||||
},
|
|
||||||
openEbook() {
|
|
||||||
this.$store.commit('showEReader', this.audiobook)
|
|
||||||
},
|
|
||||||
toggleRead() {
|
|
||||||
var updatePayload = {
|
|
||||||
isRead: !this.isRead
|
|
||||||
}
|
|
||||||
this.isProcessingReadUpdate = true
|
|
||||||
this.$axios
|
|
||||||
.$patch(`/api/me/audiobook/${this.audiobookId}`, 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'}`)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
openRssFeed() {
|
|
||||||
this.$axios
|
|
||||||
.$post('/api/feed', { audiobookId: this.audiobook.id })
|
|
||||||
.then((res) => {
|
|
||||||
console.log('Feed open', res)
|
|
||||||
this.$toast.success('RSS Feed Open')
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Failed', error)
|
|
||||||
this.$toast.error('Failed to open feed')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
startStream() {
|
|
||||||
this.$eventBus.$emit('play-item', this.audiobook.id)
|
|
||||||
},
|
|
||||||
editClick() {
|
|
||||||
this.$store.commit('setBookshelfBookIds', [])
|
|
||||||
this.$store.commit('showEditModal', this.audiobook)
|
|
||||||
},
|
|
||||||
audiobookUpdated() {
|
|
||||||
console.log('Audiobook Updated - Fetch full audiobook')
|
|
||||||
this.$axios
|
|
||||||
.$get(`/api/books/${this.audiobookId}`)
|
|
||||||
.then((audiobook) => {
|
|
||||||
console.log('Updated audiobook', audiobook)
|
|
||||||
this.audiobook = 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.audiobookId}/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', { audiobook: this.audiobook, tab: 'download' })
|
|
||||||
},
|
|
||||||
collectionsClick() {
|
|
||||||
this.$store.commit('setSelectedAudiobook', this.audiobook)
|
|
||||||
this.$store.commit('globals/setShowUserCollectionsModal', true)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.$store.commit('audiobooks/addListener', { id: 'audiobook', audiobookId: this.audiobookId, meth: this.audiobookUpdated })
|
|
||||||
|
|
||||||
// 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>
|
|
@ -19,8 +19,8 @@
|
|||||||
<ui-multi-select-query-input ref="authorsSelect" v-model="batchDetails.authors" :disabled="!selectedBatchUsage.authors" label="Authors" endpoint="authors/search" class="mb-4 ml-4" />
|
<ui-multi-select-query-input ref="authorsSelect" v-model="batchDetails.authors" :disabled="!selectedBatchUsage.authors" label="Authors" endpoint="authors/search" class="mb-4 ml-4" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center px-4 w-1/2">
|
<div class="flex items-center px-4 w-1/2">
|
||||||
<ui-checkbox v-model="selectedBatchUsage.publishYear" />
|
<ui-checkbox v-model="selectedBatchUsage.publishedYear" />
|
||||||
<ui-text-input-with-label ref="publishYearInput" v-model="batchDetails.publishYear" :disabled="!selectedBatchUsage.publishYear" label="Publish Year" class="mb-4 ml-4" />
|
<ui-text-input-with-label ref="publishedYearInput" v-model="batchDetails.publishedYear" :disabled="!selectedBatchUsage.publishedYear" label="Publish Year" class="mb-4 ml-4" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center px-4 w-1/2">
|
<div class="flex items-center px-4 w-1/2">
|
||||||
<ui-checkbox v-model="selectedBatchUsage.series" />
|
<ui-checkbox v-model="selectedBatchUsage.series" />
|
||||||
@ -114,7 +114,7 @@ export default {
|
|||||||
batchDetails: {
|
batchDetails: {
|
||||||
subtitle: null,
|
subtitle: null,
|
||||||
authors: null,
|
authors: null,
|
||||||
publishYear: null,
|
publishedYear: null,
|
||||||
series: [],
|
series: [],
|
||||||
genres: [],
|
genres: [],
|
||||||
tags: [],
|
tags: [],
|
||||||
@ -126,7 +126,7 @@ export default {
|
|||||||
selectedBatchUsage: {
|
selectedBatchUsage: {
|
||||||
subtitle: false,
|
subtitle: false,
|
||||||
authors: false,
|
authors: false,
|
||||||
publishYear: false,
|
publishedYear: false,
|
||||||
series: false,
|
series: false,
|
||||||
genres: false,
|
genres: false,
|
||||||
tags: false,
|
tags: false,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="page-wrapper" class="bg-bg page overflow-hidden" :class="streamAudiobook ? 'streaming' : ''">
|
<div id="page-wrapper" class="bg-bg page overflow-hidden" :class="streamLibraryItem ? 'streaming' : ''">
|
||||||
<div class="w-full h-full overflow-y-auto px-2 py-6 md:p-8">
|
<div class="w-full h-full overflow-y-auto px-2 py-6 md:p-8">
|
||||||
<div class="flex flex-col sm:flex-row max-w-6xl mx-auto">
|
<div class="flex flex-col sm:flex-row max-w-6xl mx-auto">
|
||||||
<div class="w-full flex justify-center md:block sm:w-32 md:w-52" style="min-width: 240px">
|
<div class="w-full flex justify-center md:block sm:w-32 md:w-52" style="min-width: 240px">
|
||||||
@ -66,8 +66,8 @@ export default {
|
|||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.$store.getters['getBookCoverAspectRatio']
|
return this.$store.getters['getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
streamAudiobook() {
|
streamLibraryItem() {
|
||||||
return this.$store.state.streamAudiobook
|
return this.$store.state.streamLibraryItem
|
||||||
},
|
},
|
||||||
bookItems() {
|
bookItems() {
|
||||||
return this.collection.books || []
|
return this.collection.books || []
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="page-wrapper" class="page p-6 overflow-y-auto relative" :class="streamAudiobook ? 'streaming' : ''">
|
<div id="page-wrapper" class="page p-6 overflow-y-auto relative" :class="streamLibraryItem ? 'streaming' : ''">
|
||||||
<app-config-side-nav :is-open.sync="sideDrawerOpen" />
|
<app-config-side-nav :is-open.sync="sideDrawerOpen" />
|
||||||
<div class="configContent" :class="`page-${currentPage}`">
|
<div class="configContent" :class="`page-${currentPage}`">
|
||||||
<div v-show="isMobile" class="w-full pb-4 px-2 flex border-b border-white border-opacity-10 mb-2">
|
<div v-show="isMobile" class="w-full pb-4 px-2 flex border-b border-white border-opacity-10 mb-2">
|
||||||
@ -38,8 +38,8 @@ export default {
|
|||||||
isMobile() {
|
isMobile() {
|
||||||
return this.$store.state.globals.isMobile
|
return this.$store.state.globals.isMobile
|
||||||
},
|
},
|
||||||
streamAudiobook() {
|
streamLibraryItem() {
|
||||||
return this.$store.state.streamAudiobook
|
return this.$store.state.streamLibraryItem
|
||||||
},
|
},
|
||||||
currentPage() {
|
currentPage() {
|
||||||
if (!this.$route.name) return 'Settings'
|
if (!this.$route.name) return 'Settings'
|
||||||
|
@ -120,7 +120,7 @@
|
|||||||
|
|
||||||
<div class="flex items-center py-4">
|
<div class="flex items-center py-4">
|
||||||
<ui-btn color="bg" small :padding-x="4" class="hidden lg:block mr-2" :loading="isPurgingCache" @click="purgeCache">Purge Cache</ui-btn>
|
<ui-btn color="bg" small :padding-x="4" class="hidden lg:block mr-2" :loading="isPurgingCache" @click="purgeCache">Purge Cache</ui-btn>
|
||||||
<ui-btn color="bg" small :padding-x="4" class="hidden lg:block" :loading="isResettingAudiobooks" @click="resetAudiobooks">Remove All Audiobooks</ui-btn>
|
<ui-btn color="bg" small :padding-x="4" class="hidden lg:block" :loading="isResettingLibraryItems" @click="resetLibraryItems">Remove All Library Items</ui-btn>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<p class="pr-2 text-sm font-book text-yellow-400">
|
<p class="pr-2 text-sm font-book text-yellow-400">
|
||||||
Report bugs, request features, and contribute on
|
Report bugs, request features, and contribute on
|
||||||
@ -169,9 +169,6 @@
|
|||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="hidden md:block">
|
|
||||||
<a href="https://github.com/advplyr/audiobookshelf/discussions/75#discussion-3604812" target="_blank" class="text-blue-500 hover:text-blue-300 underline">Join the discussion</a>
|
|
||||||
</div>-->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -181,14 +178,14 @@
|
|||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isResettingAudiobooks: false,
|
isResettingLibraryItems: false,
|
||||||
updatingServerSettings: false,
|
updatingServerSettings: false,
|
||||||
useSquareBookCovers: false,
|
useSquareBookCovers: false,
|
||||||
useAlternativeBookshelfView: false,
|
useAlternativeBookshelfView: false,
|
||||||
isPurgingCache: false,
|
isPurgingCache: false,
|
||||||
newServerSettings: {},
|
newServerSettings: {},
|
||||||
tooltips: {
|
tooltips: {
|
||||||
scannerDisableWatcher: 'Disables the automatic adding/updating of audiobooks when file changes are detected. *Requires server restart',
|
scannerDisableWatcher: 'Disables the automatic adding/updating of items when file changes are detected. *Requires server restart',
|
||||||
scannerPreferOpfMetadata: 'OPF file metadata will be used for book details over folder names',
|
scannerPreferOpfMetadata: 'OPF file metadata will be used for book details over folder names',
|
||||||
scannerPreferAudioMetadata: 'Audio file ID3 meta tags will be used for book details over folder names',
|
scannerPreferAudioMetadata: 'Audio file ID3 meta tags will be used for book details over folder names',
|
||||||
scannerParseSubtitle: 'Extract subtitles from audiobook folder names.<br>Subtitle must be seperated by " - "<br>i.e. "Book Title - A Subtitle Here" has the subtitle "A Subtitle Here"',
|
scannerParseSubtitle: 'Extract subtitles from audiobook folder names.<br>Subtitle must be seperated by " - "<br>i.e. "Book Title - A Subtitle Here" has the subtitle "A Subtitle Here"',
|
||||||
@ -271,20 +268,20 @@ export default {
|
|||||||
|
|
||||||
this.useAlternativeBookshelfView = this.newServerSettings.bookshelfView === this.$constants.BookshelfView.TITLES
|
this.useAlternativeBookshelfView = this.newServerSettings.bookshelfView === this.$constants.BookshelfView.TITLES
|
||||||
},
|
},
|
||||||
resetAudiobooks() {
|
resetLibraryItems() {
|
||||||
if (confirm('WARNING! This action will remove all audiobooks from the database including any updates or matches you have made. This does not do anything to your actual files. Shall we continue?')) {
|
if (confirm('WARNING! This action will remove all library items from the database including any updates or matches you have made. This does not do anything to your actual files. Shall we continue?')) {
|
||||||
this.isResettingAudiobooks = true
|
this.isResettingLibraryItems = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$delete('/api/books/all')
|
.$delete('/api/items/all')
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.isResettingAudiobooks = false
|
this.isResettingLibraryItems = false
|
||||||
this.$toast.success('Successfully reset audiobooks')
|
this.$toast.success('Successfully reset items')
|
||||||
location.reload()
|
location.reload()
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('failed to reset audiobooks', error)
|
console.error('failed to reset items', error)
|
||||||
this.isResettingAudiobooks = false
|
this.isResettingLibraryItems = false
|
||||||
this.$toast.error('Failed to reset audiobooks - manually remove the /config/audiobooks folder')
|
this.$toast.error('Failed to reset items - manually remove the /config/libraryItems folder')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="page-wrapper" class="page overflow-y-auto" :class="streamAudiobook ? 'streaming' : ''">
|
<div id="page-wrapper" class="page overflow-y-auto" :class="streamLibraryItem ? 'streaming' : ''">
|
||||||
<div class="w-full max-w-4xl mx-auto">
|
<div class="w-full max-w-4xl mx-auto">
|
||||||
<div class="mb-4 flex flex-col sm:flex-row items-start sm:items-end">
|
<div class="mb-4 flex flex-col sm:flex-row items-start sm:items-end">
|
||||||
<p class="text-2xl mr-4 mb-2 sm:mb-0">Logger</p>
|
<p class="text-2xl mr-4 mb-2 sm:mb-0">Logger</p>
|
||||||
@ -93,8 +93,8 @@ export default {
|
|||||||
serverSettings() {
|
serverSettings() {
|
||||||
return this.$store.state.serverSettings
|
return this.$store.state.serverSettings
|
||||||
},
|
},
|
||||||
streamAudiobook() {
|
streamLibraryItem() {
|
||||||
return this.$store.state.streamAudiobook
|
return this.$store.state.streamLibraryItem
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page" :class="streamAudiobook ? 'streaming' : ''"></div>
|
<div class="page" :class="streamLibraryItem ? 'streaming' : ''"></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -11,8 +11,8 @@ export default {
|
|||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
streamAudiobook() {
|
streamLibraryItem() {
|
||||||
return this.$store.state.streamAudiobook
|
return this.$store.state.streamLibraryItem
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {},
|
methods: {},
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="page-wrapper" class="bg-bg page overflow-hidden relative" :class="streamAudiobook ? 'streaming' : ''">
|
<div id="page-wrapper" class="bg-bg page overflow-hidden relative" :class="streamLibraryItem ? 'streaming' : ''">
|
||||||
<div v-show="saving" class="absolute z-20 w-full h-full flex items-center justify-center">
|
<div v-show="saving" class="absolute z-20 w-full h-full flex items-center justify-center">
|
||||||
<ui-loading-indicator />
|
<ui-loading-indicator />
|
||||||
</div>
|
</div>
|
||||||
@ -38,7 +38,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<draggable v-model="files" v-bind="dragOptions" class="list-group border border-gray-600" draggable=".item" tag="ul" @start="drag = true" @end="drag = false" @update="draggableUpdate">
|
<draggable v-model="files" v-bind="dragOptions" class="list-group border border-gray-600" draggable=".item" tag="ul" @start="drag = true" @end="drag = false" @update="draggableUpdate">
|
||||||
<transition-group type="transition" :name="!drag ? 'flip-list' : null">
|
<transition-group type="transition" :name="!drag ? 'flip-list' : null">
|
||||||
<li v-for="(audio, index) in files" :key="audio.path" :class="audio.include ? 'item' : 'exclude'" class="w-full list-group-item flex items-center">
|
<li v-for="(audio, index) in files" :key="audio.ino" :class="audio.include ? 'item' : 'exclude'" class="w-full list-group-item flex items-center">
|
||||||
<div class="font-book text-center px-4 py-1 w-12">
|
<div class="font-book text-center px-4 py-1 w-12">
|
||||||
{{ audio.include ? index - numExcluded + 1 : -1 }}
|
{{ audio.include ? index - numExcluded + 1 : -1 }}
|
||||||
</div>
|
</div>
|
||||||
@ -56,11 +56,11 @@
|
|||||||
{{ audio.discNumFromMeta }}
|
{{ audio.discNumFromMeta }}
|
||||||
</div>
|
</div>
|
||||||
<div class="font-book truncate px-4 flex-grow">
|
<div class="font-book truncate px-4 flex-grow">
|
||||||
{{ audio.filename }}
|
{{ audio.metadata.filename }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="font-mono w-20 text-center">
|
<div class="font-mono w-20 text-center">
|
||||||
{{ $bytesPretty(audio.size) }}
|
{{ $bytesPretty(audio.metadata.size) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="font-mono w-20">
|
<div class="font-mono w-20">
|
||||||
{{ $secondsToTimestamp(audio.duration) }}
|
{{ $secondsToTimestamp(audio.duration) }}
|
||||||
@ -95,17 +95,17 @@ export default {
|
|||||||
if (!store.getters['user/getUserCanUpdate']) {
|
if (!store.getters['user/getUserCanUpdate']) {
|
||||||
return redirect('/?error=unauthorized')
|
return redirect('/?error=unauthorized')
|
||||||
}
|
}
|
||||||
var audiobook = await app.$axios.$get(`/api/books/${params.id}`).catch((error) => {
|
var libraryItem = await app.$axios.$get(`/api/items/${params.id}?extended=1`).catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
if (!audiobook) {
|
if (!libraryItem) {
|
||||||
console.error('No audiobook...', params.id)
|
console.error('No item...', params.id)
|
||||||
return redirect('/')
|
return redirect('/')
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
audiobook,
|
libraryItem,
|
||||||
files: audiobook.audioFiles ? audiobook.audioFiles.map((af) => ({ ...af, include: !af.exclude })) : []
|
files: libraryItem.media.audioFiles ? libraryItem.media.audioFiles.map((af) => ({ ...af, include: !af.exclude })) : []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@ -121,8 +121,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
media() {
|
||||||
|
return this.libraryItem.media || {}
|
||||||
|
},
|
||||||
|
mediaMetadata() {
|
||||||
|
return this.media.metadata || []
|
||||||
|
},
|
||||||
audioFiles() {
|
audioFiles() {
|
||||||
return this.audiobook.audioFiles || []
|
return this.media.audioFiles || []
|
||||||
},
|
},
|
||||||
numExcluded() {
|
numExcluded() {
|
||||||
var count = 0
|
var count = 0
|
||||||
@ -131,69 +137,23 @@ export default {
|
|||||||
})
|
})
|
||||||
return count
|
return count
|
||||||
},
|
},
|
||||||
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
|
|
||||||
},
|
|
||||||
missingParts() {
|
missingParts() {
|
||||||
return this.audiobook.missingParts || []
|
return this.media.missingParts || []
|
||||||
},
|
},
|
||||||
invalidParts() {
|
libraryItemId() {
|
||||||
return this.audiobook.invalidParts || []
|
return this.libraryItem.id
|
||||||
},
|
|
||||||
audiobookId() {
|
|
||||||
return this.audiobook.id
|
|
||||||
},
|
},
|
||||||
title() {
|
title() {
|
||||||
return this.book.title || 'No Title'
|
return this.mediaMetadata.title || 'No Title'
|
||||||
},
|
},
|
||||||
author() {
|
author() {
|
||||||
return this.book.author || 'Unknown'
|
return this.mediaMetadata.authorName || 'Unknown'
|
||||||
},
|
},
|
||||||
tracks() {
|
tracks() {
|
||||||
return this.audiobook.tracks
|
return this.media.tracks
|
||||||
},
|
},
|
||||||
durationPretty() {
|
streamLibraryItem() {
|
||||||
return this.audiobook.durationPretty
|
return this.$store.state.streamLibraryItem
|
||||||
},
|
|
||||||
sizePretty() {
|
|
||||||
return this.audiobook.sizePretty
|
|
||||||
},
|
|
||||||
book() {
|
|
||||||
return this.audiobook.book || {}
|
|
||||||
},
|
|
||||||
tracks() {
|
|
||||||
return this.audiobook.tracks || []
|
|
||||||
},
|
|
||||||
streamAudiobook() {
|
|
||||||
return this.$store.state.streamAudiobook
|
|
||||||
},
|
},
|
||||||
showExperimentalFeatures() {
|
showExperimentalFeatures() {
|
||||||
return this.$store.state.showExperimentalFeatures
|
return this.$store.state.showExperimentalFeatures
|
||||||
@ -226,7 +186,7 @@ export default {
|
|||||||
},
|
},
|
||||||
sortByFilename() {
|
sortByFilename() {
|
||||||
this.files.sort((a, b) => {
|
this.files.sort((a, b) => {
|
||||||
return (a.filename || '').toLowerCase().localeCompare((b.filename || '').toLowerCase())
|
return (a.metadata.filename || '').toLowerCase().localeCompare((b.metadata.filename || '').toLowerCase())
|
||||||
})
|
})
|
||||||
this.currentSort = 'filename'
|
this.currentSort = 'filename'
|
||||||
},
|
},
|
||||||
@ -248,7 +208,7 @@ export default {
|
|||||||
var orderedFileData = this.files.map((file) => {
|
var orderedFileData = this.files.map((file) => {
|
||||||
return {
|
return {
|
||||||
index: file.index,
|
index: file.index,
|
||||||
filename: file.filename,
|
filename: file.metadata.filename,
|
||||||
ino: file.ino,
|
ino: file.ino,
|
||||||
exclude: !file.include
|
exclude: !file.include
|
||||||
}
|
}
|
||||||
@ -256,12 +216,12 @@ export default {
|
|||||||
|
|
||||||
this.saving = true
|
this.saving = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$patch(`/api/books/${this.audiobook.id}/tracks`, { orderedFileData })
|
.$patch(`/api/items/${this.libraryItemId}/tracks`, { orderedFileData })
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
console.log('Finished patching files', data)
|
console.log('Finished patching files', data)
|
||||||
this.saving = false
|
this.saving = false
|
||||||
this.$toast.success('Tracks Updated')
|
this.$toast.success('Tracks Updated')
|
||||||
this.$router.push(`/audiobook/${this.audiobookId}`)
|
this.$router.push(`/item/${this.libraryItemId}`)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="page-wrapper" class="bg-bg page overflow-hidden" :class="streamAudiobook ? 'streaming' : ''">
|
<div id="page-wrapper" class="bg-bg page overflow-hidden" :class="streamLibraryItem ? 'streaming' : ''">
|
||||||
<div class="w-full h-full overflow-y-auto px-2 py-6 md:p-8">
|
<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="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="w-full flex justify-center md:block md:w-52" style="min-width: 208px">
|
||||||
@ -50,12 +50,12 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="publishYear" class="flex py-0.5">
|
<div v-if="publishedYear" class="flex py-0.5">
|
||||||
<div class="w-32">
|
<div class="w-32">
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">Publish Year</span>
|
<span class="text-white text-opacity-60 uppercase text-sm">Publish Year</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{{ publishYear }}
|
{{ publishedYear }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex py-0.5" v-if="genres.length">
|
<div class="flex py-0.5" v-if="genres.length">
|
||||||
@ -283,8 +283,8 @@ export default {
|
|||||||
title() {
|
title() {
|
||||||
return this.mediaMetadata.title || 'No Title'
|
return this.mediaMetadata.title || 'No Title'
|
||||||
},
|
},
|
||||||
publishYear() {
|
publishedYear() {
|
||||||
return this.mediaMetadata.publishYear
|
return this.mediaMetadata.publishedYear
|
||||||
},
|
},
|
||||||
narrator() {
|
narrator() {
|
||||||
return this.mediaMetadata.narratorName
|
return this.mediaMetadata.narratorName
|
||||||
@ -376,11 +376,11 @@ export default {
|
|||||||
userProgressFinishedAt() {
|
userProgressFinishedAt() {
|
||||||
return this.userAudiobook ? this.userAudiobook.finishedAt : 0
|
return this.userAudiobook ? this.userAudiobook.finishedAt : 0
|
||||||
},
|
},
|
||||||
streamAudiobook() {
|
streamLibraryItem() {
|
||||||
return this.$store.state.streamAudiobook
|
return this.$store.state.streamLibraryItem
|
||||||
},
|
},
|
||||||
streaming() {
|
streaming() {
|
||||||
return this.streamAudiobook && this.streamAudiobook.id === this.libraryItemId
|
return this.streamLibraryItem && this.streamLibraryItem.id === this.libraryItemId
|
||||||
},
|
},
|
||||||
userCanUpdate() {
|
userCanUpdate() {
|
||||||
return this.$store.getters['user/getUserCanUpdate']
|
return this.$store.getters['user/getUserCanUpdate']
|
||||||
@ -425,16 +425,16 @@ export default {
|
|||||||
this.$store.commit('showEditModal', this.libraryItem)
|
this.$store.commit('showEditModal', this.libraryItem)
|
||||||
},
|
},
|
||||||
audiobookUpdated() {
|
audiobookUpdated() {
|
||||||
console.log('Audiobook Updated - Fetch full audiobook')
|
// console.log('Audiobook Updated - Fetch full audiobook')
|
||||||
this.$axios
|
// this.$axios
|
||||||
.$get(`/api/books/${this.libraryItemId}`)
|
// .$get(`/api/books/${this.libraryItemId}`)
|
||||||
.then((audiobook) => {
|
// .then((audiobook) => {
|
||||||
console.log('Updated audiobook', audiobook)
|
// console.log('Updated audiobook', audiobook)
|
||||||
this.libraryItem = audiobook
|
// this.libraryItem = audiobook
|
||||||
})
|
// })
|
||||||
.catch((error) => {
|
// .catch((error) => {
|
||||||
console.error('Failed', error)
|
// console.error('Failed', error)
|
||||||
})
|
// })
|
||||||
},
|
},
|
||||||
clearProgressClick() {
|
clearProgressClick() {
|
||||||
if (confirm(`Are you sure you want to reset your progress?`)) {
|
if (confirm(`Are you sure you want to reset your progress?`)) {
|
||||||
@ -456,20 +456,16 @@ export default {
|
|||||||
this.$store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'download' })
|
this.$store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'download' })
|
||||||
},
|
},
|
||||||
collectionsClick() {
|
collectionsClick() {
|
||||||
this.$store.commit('setSelectedAudiobook', this.libraryItem)
|
this.$store.commit('setSelectedLibraryItem', this.libraryItem)
|
||||||
this.$store.commit('globals/setShowUserCollectionsModal', true)
|
this.$store.commit('globals/setShowUserCollectionsModal', true)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$store.commit('audiobooks/addListener', { id: 'audiobook', audiobookId: this.libraryItemId, meth: this.libraryItemUpdated })
|
|
||||||
|
|
||||||
// use this audiobooks library id as the current
|
// use this audiobooks library id as the current
|
||||||
if (this.libraryId) {
|
if (this.libraryId) {
|
||||||
this.$store.commit('libraries/setCurrentLibrary', this.libraryId)
|
this.$store.commit('libraries/setCurrentLibrary', this.libraryId)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {}
|
||||||
this.$store.commit('audiobooks/removeListener', 'audiobook')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page" :class="streamAudiobook ? 'streaming' : ''">
|
<div class="page" :class="streamLibraryItem ? 'streaming' : ''">
|
||||||
<div class="flex h-full">
|
<div class="flex h-full">
|
||||||
<app-side-rail class="hidden md:block" />
|
<app-side-rail class="hidden md:block" />
|
||||||
<div class="flex-grow">
|
<div class="flex-grow">
|
||||||
@ -38,8 +38,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
streamAudiobook() {
|
streamLibraryItem() {
|
||||||
return this.$store.state.streamAudiobook
|
return this.$store.state.streamLibraryItem
|
||||||
},
|
},
|
||||||
currentLibraryId() {
|
currentLibraryId() {
|
||||||
return this.$store.state.libraries.currentLibraryId
|
return this.$store.state.libraries.currentLibraryId
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page" :class="streamAudiobook ? 'streaming' : ''">
|
<div class="page" :class="streamLibraryItem ? 'streaming' : ''">
|
||||||
<div class="flex h-full">
|
<div class="flex h-full">
|
||||||
<app-side-rail class="hidden md:block" />
|
<app-side-rail class="hidden md:block" />
|
||||||
<div class="flex-grow">
|
<div class="flex-grow">
|
||||||
@ -34,8 +34,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
streamAudiobook() {
|
streamLibraryItem() {
|
||||||
return this.$store.state.streamAudiobook
|
return this.$store.state.streamLibraryItem
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {}
|
methods: {}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page" :class="streamAudiobook ? 'streaming' : ''">
|
<div class="page" :class="streamLibraryItem ? 'streaming' : ''">
|
||||||
<div class="flex h-full">
|
<div class="flex h-full">
|
||||||
<app-side-rail class="hidden md:block" />
|
<app-side-rail class="hidden md:block" />
|
||||||
<div class="flex-grow">
|
<div class="flex-grow">
|
||||||
@ -26,8 +26,8 @@ export default {
|
|||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
streamAudiobook() {
|
streamLibraryItem() {
|
||||||
return this.$store.state.streamAudiobook
|
return this.$store.state.streamLibraryItem
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {},
|
methods: {},
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page" :class="streamAudiobook ? 'streaming' : ''">
|
<div class="page" :class="streamLibraryItem ? 'streaming' : ''">
|
||||||
<div class="flex h-full">
|
<div class="flex h-full">
|
||||||
<app-side-rail class="hidden md:block" />
|
<app-side-rail class="hidden md:block" />
|
||||||
<div class="flex-grow">
|
<div class="flex-grow">
|
||||||
@ -49,8 +49,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
streamAudiobook() {
|
streamLibraryItem() {
|
||||||
return this.$store.state.streamAudiobook
|
return this.$store.state.streamLibraryItem
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page" :class="streamAudiobook ? 'streaming' : ''">
|
<div class="page" :class="streamLibraryItem ? 'streaming' : ''">
|
||||||
<div class="flex h-full">
|
<div class="flex h-full">
|
||||||
<app-side-rail class="hidden md:block" />
|
<app-side-rail class="hidden md:block" />
|
||||||
<div class="flex-grow">
|
<div class="flex-grow">
|
||||||
@ -53,8 +53,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
streamAudiobook() {
|
streamLibraryItem() {
|
||||||
return this.$store.state.streamAudiobook
|
return this.$store.state.streamLibraryItem
|
||||||
},
|
},
|
||||||
hasResults() {
|
hasResults() {
|
||||||
return Object.values(this.results).find((r) => !!r)
|
return Object.values(this.results).find((r) => !!r)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page" :class="streamAudiobook ? 'streaming' : ''">
|
<div class="page" :class="streamLibraryItem ? 'streaming' : ''">
|
||||||
<div class="flex h-full">
|
<div class="flex h-full">
|
||||||
<app-side-rail class="hidden md:block" />
|
<app-side-rail class="hidden md:block" />
|
||||||
<div class="flex-grow">
|
<div class="flex-grow">
|
||||||
@ -35,8 +35,8 @@ export default {
|
|||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
streamAudiobook() {
|
streamLibraryItem() {
|
||||||
return this.$store.state.streamAudiobook
|
return this.$store.state.streamLibraryItem
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {},
|
mounted() {},
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="page-wrapper" class="page p-0 sm:p-6 overflow-y-auto" :class="streamAudiobook ? 'streaming' : ''">
|
<div id="page-wrapper" class="page p-0 sm:p-6 overflow-y-auto" :class="streamLibraryItem ? 'streaming' : ''">
|
||||||
<div class="w-full max-w-6xl mx-auto">
|
<div class="w-full max-w-6xl mx-auto">
|
||||||
<!-- Library & folder picker -->
|
<!-- Library & folder picker -->
|
||||||
<div class="flex my-6 -mx-2">
|
<div class="flex my-6 -mx-2">
|
||||||
@ -91,8 +91,8 @@ export default {
|
|||||||
})
|
})
|
||||||
return extensions
|
return extensions
|
||||||
},
|
},
|
||||||
streamAudiobook() {
|
streamLibraryItem() {
|
||||||
return this.$store.state.streamAudiobook
|
return this.$store.state.streamLibraryItem
|
||||||
},
|
},
|
||||||
libraries() {
|
libraries() {
|
||||||
return this.$store.state.libraries.libraries
|
return this.$store.state.libraries.libraries
|
||||||
|
@ -76,7 +76,7 @@ export default class CastPlayer extends EventEmitter {
|
|||||||
|
|
||||||
this.currentTime = startTime
|
this.currentTime = startTime
|
||||||
|
|
||||||
var coverImg = this.ctx.$store.getters['audiobooks/getLibraryItemCoverSrc'](audiobook)
|
var coverImg = this.ctx.$store.getters['globals/getLibraryItemCoverSrc'](audiobook)
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
this.coverUrl = coverImg
|
this.coverUrl = coverImg
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
export default (ctx) => {
|
export default (ctx) => {
|
||||||
var sendInit = async (castContext) => {
|
var sendInit = async (castContext) => {
|
||||||
// Fetch background covers for chromecast (temp)
|
// Fetch background covers for chromecast (temp)
|
||||||
var covers = await ctx.$axios.$get(`/api/libraries/${ctx.$store.state.libraries.currentLibraryId}/books/all?limit=40&minified=1`).then((data) => {
|
var covers = await ctx.$axios.$get(`/api/libraries/${ctx.$store.state.libraries.currentLibraryId}/items?limit=40&minified=1`).then((data) => {
|
||||||
return data.results.filter((b) => b.book.cover).map((ab) => {
|
return data.results.filter((b) => b.book.cover).map((ab) => {
|
||||||
var coverUrl = ctx.$store.getters['audiobooks/getLibraryItemCoverSrc'](ab)
|
var coverUrl = ctx.$store.getters['globals/getLibraryItemCoverSrc'](ab)
|
||||||
if (process.env.NODE_ENV === 'development') return coverUrl
|
if (process.env.NODE_ENV === 'development') return coverUrl
|
||||||
return `${window.location.origin}${coverUrl}`
|
return `${window.location.origin}${coverUrl}`
|
||||||
})
|
})
|
||||||
|
@ -1,135 +0,0 @@
|
|||||||
const STANDARD_GENRES = ['Adventure', 'Autobiography', 'Biography', 'Childrens', 'Comedy', 'Crime', 'Dystopian', 'Fantasy', 'Fiction', 'Health', 'History', 'Horror', 'Mystery', 'New Adult', 'Nonfiction', 'Philosophy', 'Politics', 'Religion', 'Romance', 'Sci-Fi', 'Self-Help', 'Short Story', 'Technology', 'Thriller', 'True Crime', 'Western', 'Young Adult']
|
|
||||||
|
|
||||||
export const state = () => ({
|
|
||||||
audiobooks: [],
|
|
||||||
loadedLibraryId: '',
|
|
||||||
listeners: [],
|
|
||||||
genres: [...STANDARD_GENRES],
|
|
||||||
tags: [],
|
|
||||||
series: [],
|
|
||||||
keywordFilter: null,
|
|
||||||
selectedSeries: null
|
|
||||||
})
|
|
||||||
|
|
||||||
export const getters = {
|
|
||||||
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 (media.coverPath.startsWith('http:') || media.coverPath.startsWith('https:')) return media.coverPath
|
|
||||||
|
|
||||||
var userToken = rootGetters['user/getToken']
|
|
||||||
var lastUpdate = libraryItem.updatedAt || Date.now()
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') { // Testing
|
|
||||||
return `http://localhost:3333/api/items/${libraryItem.id}/cover?token=${userToken}&ts=${lastUpdate}`
|
|
||||||
}
|
|
||||||
return `/api/items/${libraryItem.id}/cover?token=${userToken}&ts=${lastUpdate}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const actions = {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export const mutations = {
|
|
||||||
setKeywordFilter(state, val) {
|
|
||||||
state.keywordFilter = val
|
|
||||||
},
|
|
||||||
setSelectedSeries(state, val) {
|
|
||||||
state.selectedSeries = val
|
|
||||||
},
|
|
||||||
set(state, audiobooks) {
|
|
||||||
// GENRES
|
|
||||||
var genres = [...state.genres]
|
|
||||||
audiobooks.forEach((ab) => {
|
|
||||||
if (!ab.book) return
|
|
||||||
genres = genres.concat(ab.book.genres)
|
|
||||||
})
|
|
||||||
state.genres = [...new Set(genres)] // Remove Duplicates
|
|
||||||
state.genres.sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : 1)
|
|
||||||
|
|
||||||
// TAGS
|
|
||||||
var tags = []
|
|
||||||
audiobooks.forEach((ab) => {
|
|
||||||
tags = tags.concat(ab.tags)
|
|
||||||
})
|
|
||||||
state.tags = [...new Set(tags)] // Remove Duplicates
|
|
||||||
state.tags.sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : 1)
|
|
||||||
|
|
||||||
// SERIES
|
|
||||||
var series = []
|
|
||||||
audiobooks.forEach((ab) => {
|
|
||||||
if (!ab.book || !ab.book.series || series.includes(ab.book.series)) return
|
|
||||||
series.push(ab.book.series)
|
|
||||||
})
|
|
||||||
state.series = series
|
|
||||||
state.series.sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : 1)
|
|
||||||
|
|
||||||
state.audiobooks = audiobooks
|
|
||||||
state.listeners.forEach((listener) => {
|
|
||||||
listener.meth()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
remove(state, audiobook) {
|
|
||||||
state.audiobooks = state.audiobooks.filter(a => a.id !== audiobook.id)
|
|
||||||
|
|
||||||
if (audiobook.book) {
|
|
||||||
// GENRES
|
|
||||||
audiobook.book.genres.forEach((genre) => {
|
|
||||||
if (!STANDARD_GENRES.includes(genre)) {
|
|
||||||
var isInOtherAB = state.audiobooks.find(ab => {
|
|
||||||
return ab.book && ab.book.genres.includes(genre)
|
|
||||||
})
|
|
||||||
if (!isInOtherAB) {
|
|
||||||
// Genre is not used by any other audiobook - remove it
|
|
||||||
state.genres = state.genres.filter(g => g !== genre)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// SERIES
|
|
||||||
if (audiobook.book.series) {
|
|
||||||
var isInOtherAB = state.audiobooks.find(ab => ab.book && ab.book.series === audiobook.book.series)
|
|
||||||
if (!isInOtherAB) {
|
|
||||||
// Series not used in any other audiobook - remove it
|
|
||||||
state.series = state.series.filter(s => s !== audiobook.book.series)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TAGS
|
|
||||||
audiobook.tags.forEach((tag) => {
|
|
||||||
var isInOtherAB = state.audiobooks.find(ab => {
|
|
||||||
return ab.tags.includes(tag)
|
|
||||||
})
|
|
||||||
if (!isInOtherAB) {
|
|
||||||
// Tag is not used by any other audiobook - remove it
|
|
||||||
state.tags = state.tags.filter(t => t !== tag)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
state.listeners.forEach((listener) => {
|
|
||||||
if (!listener.audiobookId || listener.audiobookId === audiobook.id) {
|
|
||||||
listener.meth()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
addListener(state, listener) {
|
|
||||||
var index = state.listeners.findIndex(l => l.id === listener.id)
|
|
||||||
if (index >= 0) state.listeners.splice(index, 1, listener)
|
|
||||||
else state.listeners.push(listener)
|
|
||||||
},
|
|
||||||
removeListener(state, listenerId) {
|
|
||||||
state.listeners = state.listeners.filter(l => l.id !== listenerId)
|
|
||||||
},
|
|
||||||
audiobookUpdated(state, audiobook) {
|
|
||||||
state.listeners.forEach((listener) => {
|
|
||||||
if (!listener.audiobookId || listener.audiobookId === audiobook.id) {
|
|
||||||
listener.meth()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -11,7 +11,24 @@ export const state = () => ({
|
|||||||
isChromecastInitialized: false // Script loaded
|
isChromecastInitialized: false // Script loaded
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getters = {}
|
export const getters = {
|
||||||
|
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 (media.coverPath.startsWith('http:') || media.coverPath.startsWith('https:')) return media.coverPath
|
||||||
|
|
||||||
|
var userToken = rootGetters['user/getToken']
|
||||||
|
var lastUpdate = libraryItem.updatedAt || Date.now()
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') { // Testing
|
||||||
|
return `http://localhost:3333/api/items/${libraryItem.id}/cover?token=${userToken}&ts=${lastUpdate}`
|
||||||
|
}
|
||||||
|
return `/api/items/${libraryItem.id}/cover?token=${userToken}&ts=${lastUpdate}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
updateWindowSize(state, { width, height }) {
|
updateWindowSize(state, { width, height }) {
|
||||||
|
@ -8,7 +8,6 @@ export const state = () => ({
|
|||||||
editModalTab: 'details',
|
editModalTab: 'details',
|
||||||
showEditModal: false,
|
showEditModal: false,
|
||||||
showEReader: false,
|
showEReader: false,
|
||||||
selectedAudiobook: null,
|
|
||||||
selectedLibraryItem: null,
|
selectedLibraryItem: null,
|
||||||
selectedAudiobookFile: null,
|
selectedAudiobookFile: null,
|
||||||
developerMode: false,
|
developerMode: false,
|
||||||
|
@ -68,11 +68,6 @@ export const actions = {
|
|||||||
console.warn('Access not allowed to library')
|
console.warn('Access not allowed to library')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// var library = state.libraries.find(lib => lib.id === libraryId)
|
|
||||||
// if (library) {
|
|
||||||
// commit('setCurrentLibrary', libraryId)
|
|
||||||
// return library
|
|
||||||
// }
|
|
||||||
|
|
||||||
return this.$axios
|
return this.$axios
|
||||||
.$get(`/api/libraries/${libraryId}?include=filterdata`)
|
.$get(`/api/libraries/${libraryId}?include=filterdata`)
|
||||||
|
@ -8,7 +8,6 @@ const Logger = require('./Logger')
|
|||||||
const { isObject } = require('./utils/index')
|
const { isObject } = require('./utils/index')
|
||||||
const { parsePodcastRssFeedXml } = require('./utils/podcastUtils')
|
const { parsePodcastRssFeedXml } = require('./utils/podcastUtils')
|
||||||
|
|
||||||
const BookController = require('./controllers/BookController')
|
|
||||||
const LibraryController = require('./controllers/LibraryController')
|
const LibraryController = require('./controllers/LibraryController')
|
||||||
const UserController = require('./controllers/UserController')
|
const UserController = require('./controllers/UserController')
|
||||||
const CollectionController = require('./controllers/CollectionController')
|
const CollectionController = require('./controllers/CollectionController')
|
||||||
@ -74,6 +73,8 @@ class ApiController {
|
|||||||
//
|
//
|
||||||
// Item Routes
|
// Item Routes
|
||||||
//
|
//
|
||||||
|
this.router.delete('/items/all', LibraryItemController.deleteAll.bind(this))
|
||||||
|
|
||||||
this.router.get('/items/:id', LibraryItemController.middleware.bind(this), LibraryItemController.findOne.bind(this))
|
this.router.get('/items/:id', LibraryItemController.middleware.bind(this), LibraryItemController.findOne.bind(this))
|
||||||
this.router.patch('/items/:id', LibraryItemController.middleware.bind(this), LibraryItemController.update.bind(this))
|
this.router.patch('/items/:id', LibraryItemController.middleware.bind(this), LibraryItemController.update.bind(this))
|
||||||
this.router.delete('/items/:id', LibraryItemController.middleware.bind(this), LibraryItemController.delete.bind(this))
|
this.router.delete('/items/:id', LibraryItemController.middleware.bind(this), LibraryItemController.delete.bind(this))
|
||||||
@ -83,27 +84,13 @@ class ApiController {
|
|||||||
this.router.patch('/items/:id/cover', LibraryItemController.middleware.bind(this), LibraryItemController.updateCover.bind(this))
|
this.router.patch('/items/:id/cover', LibraryItemController.middleware.bind(this), LibraryItemController.updateCover.bind(this))
|
||||||
this.router.delete('/items/:id/cover', LibraryItemController.middleware.bind(this), LibraryItemController.removeCover.bind(this))
|
this.router.delete('/items/:id/cover', LibraryItemController.middleware.bind(this), LibraryItemController.removeCover.bind(this))
|
||||||
this.router.get('/items/:id/stream', LibraryItemController.middleware.bind(this), LibraryItemController.openStream.bind(this))
|
this.router.get('/items/:id/stream', LibraryItemController.middleware.bind(this), LibraryItemController.openStream.bind(this))
|
||||||
|
this.router.post('/items/:id/match', LibraryItemController.middleware.bind(this), LibraryItemController.match.bind(this))
|
||||||
|
this.router.patch('/items/:id/tracks', LibraryItemController.middleware.bind(this), LibraryItemController.updateTracks.bind(this))
|
||||||
|
|
||||||
this.router.post('/items/batch/delete', LibraryItemController.batchDelete.bind(this))
|
this.router.post('/items/batch/delete', LibraryItemController.batchDelete.bind(this))
|
||||||
this.router.post('/items/batch/update', LibraryItemController.batchUpdate.bind(this))
|
this.router.post('/items/batch/update', LibraryItemController.batchUpdate.bind(this))
|
||||||
this.router.post('/items/batch/get', LibraryItemController.batchGet.bind(this))
|
this.router.post('/items/batch/get', LibraryItemController.batchGet.bind(this))
|
||||||
|
|
||||||
//
|
|
||||||
// Book Routes
|
|
||||||
//
|
|
||||||
this.router.get('/books', BookController.findAll.bind(this))
|
|
||||||
this.router.get('/books/:id', BookController.findOne.bind(this))
|
|
||||||
this.router.patch('/books/:id', BookController.update.bind(this))
|
|
||||||
this.router.delete('/books/:id', BookController.delete.bind(this))
|
|
||||||
|
|
||||||
this.router.delete('/books/all', BookController.deleteAll.bind(this))
|
|
||||||
this.router.patch('/books/:id/tracks', BookController.updateTracks.bind(this))
|
|
||||||
this.router.get('/books/:id/stream', BookController.openStream.bind(this))
|
|
||||||
this.router.post('/books/:id/cover', BookController.uploadCover.bind(this))
|
|
||||||
this.router.get('/books/:id/cover', BookController.getCover.bind(this))
|
|
||||||
this.router.patch('/books/:id/coverfile', BookController.updateCoverFromFile.bind(this))
|
|
||||||
this.router.post('/books/:id/match', BookController.match.bind(this))
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// User Routes
|
// User Routes
|
||||||
//
|
//
|
||||||
|
@ -218,8 +218,6 @@ class Server {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Client dynamic routes
|
// Client dynamic routes
|
||||||
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', (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('/item/:id/edit', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
||||||
app.get('/library/:library', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
app.get('/library/:library', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
||||||
|
@ -1,279 +0,0 @@
|
|||||||
const Logger = require('../Logger')
|
|
||||||
const { reqSupportsWebp } = require('../utils/index')
|
|
||||||
|
|
||||||
class BookController {
|
|
||||||
constructor() { }
|
|
||||||
|
|
||||||
findAll(req, res) {
|
|
||||||
var audiobooks = []
|
|
||||||
|
|
||||||
if (req.query.q) {
|
|
||||||
audiobooks = this.db.audiobooks.filter(ab => {
|
|
||||||
return ab.isSearchMatch(req.query.q)
|
|
||||||
}).map(ab => ab.toJSONMinified())
|
|
||||||
} else {
|
|
||||||
audiobooks = this.db.audiobooks.map(ab => ab.toJSONMinified())
|
|
||||||
}
|
|
||||||
res.json(audiobooks)
|
|
||||||
}
|
|
||||||
|
|
||||||
findOne(req, res) {
|
|
||||||
var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
|
|
||||||
if (!audiobook) return res.sendStatus(404)
|
|
||||||
|
|
||||||
// Check user can access this audiobooks library
|
|
||||||
if (!req.user.checkCanAccessLibrary(audiobook.libraryId)) {
|
|
||||||
return res.sendStatus(403)
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json(audiobook.toJSONExpanded())
|
|
||||||
}
|
|
||||||
|
|
||||||
async update(req, res) {
|
|
||||||
if (!req.user.canUpdate) {
|
|
||||||
Logger.warn('User attempted to update without permission', req.user)
|
|
||||||
return res.sendStatus(403)
|
|
||||||
}
|
|
||||||
var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
|
|
||||||
if (!audiobook) return res.sendStatus(404)
|
|
||||||
|
|
||||||
// Book has cover and update is removing cover then purge cache
|
|
||||||
if (audiobook.cover && req.body.book && (req.body.book.cover === '' || req.body.book.cover === null)) {
|
|
||||||
await this.cacheManager.purgeCoverCache(audiobook.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasUpdates = audiobook.update(req.body)
|
|
||||||
if (hasUpdates) {
|
|
||||||
await this.db.updateAudiobook(audiobook)
|
|
||||||
this.emitter('audiobook_updated', audiobook.toJSONExpanded())
|
|
||||||
}
|
|
||||||
res.json(audiobook.toJSON())
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(req, res) {
|
|
||||||
if (!req.user.canDelete) {
|
|
||||||
Logger.warn('User attempted to delete without permission', req.user)
|
|
||||||
return res.sendStatus(403)
|
|
||||||
}
|
|
||||||
var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
|
|
||||||
if (!audiobook) return res.sendStatus(404)
|
|
||||||
|
|
||||||
await this.handleDeleteAudiobook(audiobook)
|
|
||||||
res.sendStatus(200)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DELETE: api/books/all
|
|
||||||
async deleteAll(req, res) {
|
|
||||||
if (!req.user.isRoot) {
|
|
||||||
Logger.warn('User other than root attempted to delete all library items', req.user)
|
|
||||||
return res.sendStatus(403)
|
|
||||||
}
|
|
||||||
Logger.info('Removing all Library Items')
|
|
||||||
var success = await this.db.recreateLibraryItemsDb()
|
|
||||||
if (success) res.sendStatus(200)
|
|
||||||
else res.sendStatus(500)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// POST: api/books/batch/delete
|
|
||||||
async batchDelete(req, res) {
|
|
||||||
if (!req.user.canDelete) {
|
|
||||||
Logger.warn('User attempted to delete without permission', req.user)
|
|
||||||
return res.sendStatus(403)
|
|
||||||
}
|
|
||||||
var { audiobookIds } = req.body
|
|
||||||
if (!audiobookIds || !audiobookIds.length) {
|
|
||||||
return res.sendStatus(500)
|
|
||||||
}
|
|
||||||
|
|
||||||
var audiobooksToDelete = this.db.audiobooks.filter(ab => audiobookIds.includes(ab.id))
|
|
||||||
if (!audiobooksToDelete.length) {
|
|
||||||
return res.sendStatus(404)
|
|
||||||
}
|
|
||||||
for (let i = 0; i < audiobooksToDelete.length; i++) {
|
|
||||||
Logger.info(`[ApiController] Deleting Audiobook "${audiobooksToDelete[i].title}"`)
|
|
||||||
await this.handleDeleteAudiobook(audiobooksToDelete[i])
|
|
||||||
}
|
|
||||||
res.sendStatus(200)
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST: api/books/batch/update
|
|
||||||
async batchUpdate(req, res) {
|
|
||||||
if (!req.user.canUpdate) {
|
|
||||||
Logger.warn('User attempted to batch update without permission', req.user)
|
|
||||||
return res.sendStatus(403)
|
|
||||||
}
|
|
||||||
var updatePayloads = req.body
|
|
||||||
if (!updatePayloads || !updatePayloads.length) {
|
|
||||||
return res.sendStatus(500)
|
|
||||||
}
|
|
||||||
|
|
||||||
var audiobooksUpdated = 0
|
|
||||||
var audiobooks = updatePayloads.map((up) => {
|
|
||||||
var audiobookUpdates = up.updates
|
|
||||||
var ab = this.db.audiobooks.find(_ab => _ab.id === up.id)
|
|
||||||
if (!ab) return null
|
|
||||||
var hasUpdated = ab.update(audiobookUpdates)
|
|
||||||
if (!hasUpdated) return null
|
|
||||||
audiobooksUpdated++
|
|
||||||
return ab
|
|
||||||
}).filter(ab => ab)
|
|
||||||
|
|
||||||
if (audiobooksUpdated) {
|
|
||||||
Logger.info(`[ApiController] ${audiobooksUpdated} Audiobooks have updates`)
|
|
||||||
for (let i = 0; i < audiobooks.length; i++) {
|
|
||||||
await this.db.updateAudiobook(audiobooks[i])
|
|
||||||
this.emitter('audiobook_updated', audiobooks[i].toJSONExpanded())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
updates: audiobooksUpdated
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST: api/books/batch/get
|
|
||||||
async batchGet(req, res) {
|
|
||||||
var bookIds = req.body.books || []
|
|
||||||
if (!bookIds.length) {
|
|
||||||
return res.status(403).send('Invalid payload')
|
|
||||||
}
|
|
||||||
var audiobooks = this.db.audiobooks.filter(ab => bookIds.includes(ab.id)).map((ab) => ab.toJSONExpanded())
|
|
||||||
res.json(audiobooks)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PATCH: api/books/:id/tracks
|
|
||||||
async updateTracks(req, res) {
|
|
||||||
if (!req.user.canUpdate) {
|
|
||||||
Logger.warn('User attempted to update audiotracks without permission', req.user)
|
|
||||||
return res.sendStatus(403)
|
|
||||||
}
|
|
||||||
var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
|
|
||||||
if (!audiobook) return res.sendStatus(404)
|
|
||||||
var orderedFileData = req.body.orderedFileData
|
|
||||||
Logger.info(`Updating audiobook tracks called ${audiobook.id}`)
|
|
||||||
audiobook.updateAudioTracks(orderedFileData)
|
|
||||||
await this.db.updateAudiobook(audiobook)
|
|
||||||
this.emitter('audiobook_updated', audiobook.toJSONExpanded())
|
|
||||||
res.json(audiobook.toJSON())
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET: api/books/:id/stream
|
|
||||||
openStream(req, res) {
|
|
||||||
var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
|
|
||||||
if (!audiobook) return res.sendStatus(404)
|
|
||||||
|
|
||||||
this.streamManager.openStreamApiRequest(res, req.user, audiobook)
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST: api/books/:id/cover
|
|
||||||
async uploadCover(req, res) {
|
|
||||||
if (!req.user.canUpload || !req.user.canUpdate) {
|
|
||||||
Logger.warn('User attempted to upload a cover without permission', req.user)
|
|
||||||
return res.sendStatus(403)
|
|
||||||
}
|
|
||||||
|
|
||||||
var audiobookId = req.params.id
|
|
||||||
var audiobook = this.db.audiobooks.find(ab => ab.id === audiobookId)
|
|
||||||
if (!audiobook) {
|
|
||||||
return res.status(404).send('Audiobook not found')
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = null
|
|
||||||
if (req.body && req.body.url) {
|
|
||||||
Logger.debug(`[ApiController] Requesting download cover from url "${req.body.url}"`)
|
|
||||||
result = await this.coverController.downloadCoverFromUrl(audiobook, req.body.url)
|
|
||||||
} else if (req.files && req.files.cover) {
|
|
||||||
Logger.debug(`[ApiController] Handling uploaded cover`)
|
|
||||||
var coverFile = req.files.cover
|
|
||||||
result = await this.coverController.uploadCover(audiobook, coverFile)
|
|
||||||
} else {
|
|
||||||
return res.status(400).send('Invalid request no file or url')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result && result.error) {
|
|
||||||
return res.status(400).send(result.error)
|
|
||||||
} else if (!result || !result.cover) {
|
|
||||||
return res.status(500).send('Unknown error occurred')
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.db.updateAudiobook(audiobook)
|
|
||||||
this.emitter('audiobook_updated', audiobook.toJSONExpanded())
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
cover: result.cover
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// PATCH api/books/:id/coverfile
|
|
||||||
async updateCoverFromFile(req, res) {
|
|
||||||
if (!req.user.canUpdate) {
|
|
||||||
Logger.warn('User attempted to update without permission', req.user)
|
|
||||||
return res.sendStatus(403)
|
|
||||||
}
|
|
||||||
var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
|
|
||||||
if (!audiobook) return res.sendStatus(404)
|
|
||||||
|
|
||||||
var coverFile = req.body
|
|
||||||
var updated = await audiobook.setCoverFromFile(coverFile)
|
|
||||||
|
|
||||||
if (updated) {
|
|
||||||
await this.db.updateAudiobook(audiobook)
|
|
||||||
await this.cacheManager.purgeCoverCache(audiobook.id)
|
|
||||||
this.emitter('audiobook_updated', audiobook.toJSONExpanded())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updated) res.status(200).send('Cover updated successfully')
|
|
||||||
else res.status(200).send('No update was made to cover')
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET api/books/:id/cover
|
|
||||||
async getCover(req, res) {
|
|
||||||
let { query: { width, height, format }, params: { id } } = req
|
|
||||||
var audiobook = this.db.audiobooks.find(a => a.id === id)
|
|
||||||
if (!audiobook || !audiobook.book.cover) return res.sendStatus(404)
|
|
||||||
|
|
||||||
// Check user can access this audiobooks library
|
|
||||||
if (!req.user.checkCanAccessLibrary(audiobook.libraryId)) {
|
|
||||||
return res.sendStatus(403)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Temp fix for books without a full cover path
|
|
||||||
if (audiobook.book.cover && !audiobook.book.coverFullPath) {
|
|
||||||
var isFixed = audiobook.fixFullCoverPath()
|
|
||||||
if (!isFixed) {
|
|
||||||
Logger.warn(`[BookController] Failed to fix full cover path "${audiobook.book.cover}" for "${audiobook.book.title}"`)
|
|
||||||
return res.sendStatus(404)
|
|
||||||
}
|
|
||||||
await this.db.updateEntity('audiobook', audiobook)
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
format: format || (reqSupportsWebp(req) ? 'webp' : 'jpeg'),
|
|
||||||
height: height ? parseInt(height) : null,
|
|
||||||
width: width ? parseInt(width) : null
|
|
||||||
}
|
|
||||||
return this.cacheManager.handleCoverCache(res, audiobook, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST api/books/:id/match
|
|
||||||
async match(req, res) {
|
|
||||||
if (!req.user.canUpdate) {
|
|
||||||
Logger.warn('User attempted to match without permission', req.user)
|
|
||||||
return res.sendStatus(403)
|
|
||||||
}
|
|
||||||
var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
|
|
||||||
if (!audiobook) return res.sendStatus(404)
|
|
||||||
|
|
||||||
// Check user can access this audiobooks library
|
|
||||||
if (!req.user.checkCanAccessLibrary(audiobook.libraryId)) {
|
|
||||||
return res.sendStatus(403)
|
|
||||||
}
|
|
||||||
|
|
||||||
var options = req.body || {}
|
|
||||||
var matchResult = await this.scanner.quickMatchBook(audiobook, options)
|
|
||||||
res.json(matchResult)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
module.exports = new BookController()
|
|
@ -230,7 +230,7 @@ class LibraryController {
|
|||||||
res.json(libraryHelpers.getDistinctFilterDataNew(req.libraryItems))
|
res.json(libraryHelpers.getDistinctFilterDataNew(req.libraryItems))
|
||||||
}
|
}
|
||||||
|
|
||||||
// api/libraries/:id/books/personalized
|
// api/libraries/:id/personalized
|
||||||
async getLibraryUserPersonalized(req, res) {
|
async getLibraryUserPersonalized(req, res) {
|
||||||
var libraryItems = req.libraryItems
|
var libraryItems = req.libraryItems
|
||||||
var limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12
|
var limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12
|
||||||
|
@ -138,6 +138,26 @@ class LibraryItemController {
|
|||||||
this.streamManager.openStreamApiRequest(res, req.user, req.libraryItem)
|
this.streamManager.openStreamApiRequest(res, req.user, req.libraryItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// POST api/items/:id/match
|
||||||
|
async match(req, res) {
|
||||||
|
var libraryItem = req.libraryItem
|
||||||
|
|
||||||
|
var options = req.body || {}
|
||||||
|
var matchResult = await this.scanner.quickMatchBook(libraryItem, options)
|
||||||
|
res.json(matchResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PATCH: api/items/:id/tracks
|
||||||
|
async updateTracks(req, res) {
|
||||||
|
var libraryItem = req.libraryItem
|
||||||
|
var orderedFileData = req.body.orderedFileData
|
||||||
|
Logger.info(`Updating item tracks called ${libraryItem.id}`)
|
||||||
|
libraryItem.media.updateAudioTracks(orderedFileData)
|
||||||
|
await this.db.updateLibraryItem(libraryItem)
|
||||||
|
this.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||||
|
res.json(libraryItem.toJSON())
|
||||||
|
}
|
||||||
|
|
||||||
// POST: api/items/batch/delete
|
// POST: api/items/batch/delete
|
||||||
async batchDelete(req, res) {
|
async batchDelete(req, res) {
|
||||||
if (!req.user.canDelete) {
|
if (!req.user.canDelete) {
|
||||||
@ -202,6 +222,18 @@ class LibraryItemController {
|
|||||||
res.json(libraryItems)
|
res.json(libraryItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DELETE: api/items/all
|
||||||
|
async deleteAll(req, res) {
|
||||||
|
if (!req.user.isRoot) {
|
||||||
|
Logger.warn('User other than root attempted to delete all library items', req.user)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
Logger.info('Removing all Library Items')
|
||||||
|
var success = await this.db.recreateLibraryItemsDb()
|
||||||
|
if (success) res.sendStatus(200)
|
||||||
|
else res.sendStatus(500)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
middleware(req, res, next) {
|
middleware(req, res, next) {
|
||||||
var item = this.db.libraryItems.find(li => li.id === req.params.id)
|
var item = this.db.libraryItems.find(li => li.id === req.params.id)
|
||||||
|
@ -18,16 +18,16 @@ class MeController {
|
|||||||
|
|
||||||
// PATCH: api/me/audiobook/:id/reset-progress
|
// PATCH: api/me/audiobook/:id/reset-progress
|
||||||
async resetAudiobookProgress(req, res) {
|
async resetAudiobookProgress(req, res) {
|
||||||
var audiobook = this.db.audiobooks.find(ab => ab.id === req.params.id)
|
var libraryItem = this.db.libraryItems.find(li => li.id === req.params.id)
|
||||||
if (!audiobook) {
|
if (!libraryItem) {
|
||||||
return res.status(404).send('Audiobook not found')
|
return res.status(404).send('Item not found')
|
||||||
}
|
}
|
||||||
req.user.resetAudiobookProgress(audiobook)
|
req.user.resetAudiobookProgress(libraryItem)
|
||||||
await this.db.updateEntity('user', req.user)
|
await this.db.updateEntity('user', req.user)
|
||||||
|
|
||||||
var userAudiobookData = req.user.audiobooks[audiobook.id]
|
var userAudiobookData = req.user.audiobooks[libraryItem.id]
|
||||||
if (userAudiobookData) {
|
if (userAudiobookData) {
|
||||||
this.clientEmitter(req.user.id, 'current_user_audiobook_update', { id: audiobook.id, data: userAudiobookData })
|
this.clientEmitter(req.user.id, 'current_user_audiobook_update', { id: libraryItem.id, data: userAudiobookData })
|
||||||
}
|
}
|
||||||
|
|
||||||
this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||||
@ -36,11 +36,11 @@ class MeController {
|
|||||||
|
|
||||||
// PATCH: api/me/audiobook/:id
|
// PATCH: api/me/audiobook/:id
|
||||||
async updateAudiobookData(req, res) {
|
async updateAudiobookData(req, res) {
|
||||||
var audiobook = this.db.audiobooks.find(ab => ab.id === req.params.id)
|
var libraryItem = this.db.libraryItems.find(ab => ab.id === req.params.id)
|
||||||
if (!audiobook) {
|
if (!libraryItem) {
|
||||||
return res.status(404).send('Audiobook not found')
|
return res.status(404).send('Item not found')
|
||||||
}
|
}
|
||||||
var wasUpdated = req.user.updateAudiobookData(audiobook.id, req.body)
|
var wasUpdated = req.user.updateAudiobookData(libraryItem.id, req.body)
|
||||||
if (wasUpdated) {
|
if (wasUpdated) {
|
||||||
await this.db.updateEntity('user', req.user)
|
await this.db.updateEntity('user', req.user)
|
||||||
this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||||
@ -57,9 +57,9 @@ class MeController {
|
|||||||
|
|
||||||
var shouldUpdate = false
|
var shouldUpdate = false
|
||||||
userAbDataPayloads.forEach((userAbData) => {
|
userAbDataPayloads.forEach((userAbData) => {
|
||||||
var audiobook = this.db.audiobooks.find(ab => ab.id === userAbData.audiobookId)
|
var libraryItem = this.db.libraryItems.find(li => li.id === userAbData.audiobookId)
|
||||||
if (audiobook) {
|
if (libraryItem) {
|
||||||
var wasUpdated = req.user.updateAudiobookData(audiobook.id, userAbData)
|
var wasUpdated = req.user.updateAudiobookData(libraryItem.id, userAbData)
|
||||||
if (wasUpdated) shouldUpdate = true
|
if (wasUpdated) shouldUpdate = true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -250,11 +250,11 @@ class User {
|
|||||||
return madeUpdates
|
return madeUpdates
|
||||||
}
|
}
|
||||||
|
|
||||||
resetAudiobookProgress(audiobook) {
|
resetAudiobookProgress(libraryItem) {
|
||||||
if (!this.audiobooks || !this.audiobooks[audiobook.id]) {
|
if (!this.audiobooks || !this.audiobooks[libraryItem.id]) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return this.updateAudiobookData(audiobook.id, {
|
return this.updateAudiobookData(libraryItem.id, {
|
||||||
progress: 0,
|
progress: 0,
|
||||||
currentTime: 0,
|
currentTime: 0,
|
||||||
isRead: false,
|
isRead: false,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
const Path = require('path')
|
||||||
const Logger = require('../../Logger')
|
const Logger = require('../../Logger')
|
||||||
const BookMetadata = require('../metadata/BookMetadata')
|
const BookMetadata = require('../metadata/BookMetadata')
|
||||||
const AudioFile = require('../files/AudioFile')
|
const AudioFile = require('../files/AudioFile')
|
||||||
@ -124,6 +125,27 @@ class Book {
|
|||||||
return hasUpdates
|
return hasUpdates
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateAudioTracks(orderedFileData) {
|
||||||
|
var index = 1
|
||||||
|
this.audioFiles = orderedFileData.map((fileData) => {
|
||||||
|
var audioFile = this.audioFiles.find(af => af.ino === fileData.ino)
|
||||||
|
audioFile.manuallyVerified = true
|
||||||
|
audioFile.invalid = false
|
||||||
|
audioFile.error = null
|
||||||
|
if (fileData.exclude !== undefined) {
|
||||||
|
audioFile.exclude = !!fileData.exclude
|
||||||
|
}
|
||||||
|
if (audioFile.exclude) {
|
||||||
|
audioFile.index = -1
|
||||||
|
} else {
|
||||||
|
audioFile.index = index++
|
||||||
|
}
|
||||||
|
return audioFile
|
||||||
|
})
|
||||||
|
|
||||||
|
this.rebuildTracks()
|
||||||
|
}
|
||||||
|
|
||||||
updateCover(coverPath) {
|
updateCover(coverPath) {
|
||||||
coverPath = coverPath.replace(/\\/g, '/')
|
coverPath = coverPath.replace(/\\/g, '/')
|
||||||
if (this.coverPath === coverPath) return false
|
if (this.coverPath === coverPath) return false
|
||||||
|
@ -65,6 +65,7 @@ class LibraryFile {
|
|||||||
this.metadata = fileMetadata
|
this.metadata = fileMetadata
|
||||||
this.addedAt = Date.now()
|
this.addedAt = Date.now()
|
||||||
this.updatedAt = Date.now()
|
this.updatedAt = Date.now()
|
||||||
|
console.log('Library file set from path', path, 'rel path', relPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = LibraryFile
|
module.exports = LibraryFile
|
@ -135,7 +135,7 @@ class BookMetadata {
|
|||||||
this.title = scanMediaData.title || null
|
this.title = scanMediaData.title || null
|
||||||
this.subtitle = scanMediaData.subtitle || null
|
this.subtitle = scanMediaData.subtitle || null
|
||||||
this.narrators = []
|
this.narrators = []
|
||||||
this.publishYear = scanMediaData.publishYear || null
|
this.publishedYear = scanMediaData.publishedYear || null
|
||||||
this.description = scanMediaData.description || null
|
this.description = scanMediaData.description || null
|
||||||
this.isbn = scanMediaData.isbn || null
|
this.isbn = scanMediaData.isbn || null
|
||||||
this.asin = scanMediaData.asin || null
|
this.asin = scanMediaData.asin || null
|
||||||
@ -166,7 +166,7 @@ class BookMetadata {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag: 'tagDate',
|
tag: 'tagDate',
|
||||||
key: 'publishYear'
|
key: 'publishedYear'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag: 'tagSubtitle',
|
tag: 'tagSubtitle',
|
||||||
|
@ -16,7 +16,7 @@ class Audible {
|
|||||||
author: authors ? authors.map(({ name }) => name).join(', ') : null,
|
author: authors ? authors.map(({ name }) => name).join(', ') : null,
|
||||||
narrator: narrators ? narrators.map(({ name }) => name).join(', ') : null,
|
narrator: narrators ? narrators.map(({ name }) => name).join(', ') : null,
|
||||||
publisher: publisher_name,
|
publisher: publisher_name,
|
||||||
publishYear: release_date ? release_date.split('-')[0] : null,
|
publishedYear: release_date ? release_date.split('-')[0] : null,
|
||||||
description: stripHtml(publisher_summary).result,
|
description: stripHtml(publisher_summary).result,
|
||||||
cover: this.getBestImageLink(product_images),
|
cover: this.getBestImageLink(product_images),
|
||||||
asin,
|
asin,
|
||||||
|
@ -23,7 +23,7 @@ class GoogleBooks {
|
|||||||
subtitle: subtitle || null,
|
subtitle: subtitle || null,
|
||||||
author: authors ? authors.join(', ') : null,
|
author: authors ? authors.join(', ') : null,
|
||||||
publisher,
|
publisher,
|
||||||
publishYear: publisherDate ? publisherDate.split('-')[0] : null,
|
publishedYear: publisherDate ? publisherDate.split('-')[0] : null,
|
||||||
description,
|
description,
|
||||||
cover: imageLinks && imageLinks.thumbnail ? imageLinks.thumbnail : null,
|
cover: imageLinks && imageLinks.thumbnail ? imageLinks.thumbnail : null,
|
||||||
genres: categories ? categories.join(', ') : null,
|
genres: categories ? categories.join(', ') : null,
|
||||||
|
@ -65,7 +65,7 @@ class OpenLibrary {
|
|||||||
return {
|
return {
|
||||||
title: doc.title,
|
title: doc.title,
|
||||||
author: doc.author_name ? doc.author_name.join(', ') : null,
|
author: doc.author_name ? doc.author_name.join(', ') : null,
|
||||||
publishYear: this.parsePublishYear(doc, worksData),
|
publishedYear: this.parsePublishYear(doc, worksData),
|
||||||
edition: doc.cover_edition_key,
|
edition: doc.cover_edition_key,
|
||||||
cover: doc.cover_edition_key ? `https://covers.openlibrary.org/b/OLID/${doc.cover_edition_key}-L.jpg` : null,
|
cover: doc.cover_edition_key ? `https://covers.openlibrary.org/b/OLID/${doc.cover_edition_key}-L.jpg` : null,
|
||||||
...worksData
|
...worksData
|
||||||
|
@ -65,7 +65,7 @@ class iTunes {
|
|||||||
title: data.collectionName,
|
title: data.collectionName,
|
||||||
author: data.artistName,
|
author: data.artistName,
|
||||||
description: stripHtml(data.description || '').result,
|
description: stripHtml(data.description || '').result,
|
||||||
publishYear: data.releaseDate ? data.releaseDate.split('-')[0] : null,
|
publishedYear: data.releaseDate ? data.releaseDate.split('-')[0] : null,
|
||||||
genres: data.primaryGenreName ? [data.primaryGenreName] : [],
|
genres: data.primaryGenreName ? [data.primaryGenreName] : [],
|
||||||
cover: this.getCoverArtwork(data)
|
cover: this.getCoverArtwork(data)
|
||||||
}
|
}
|
||||||
|
@ -10,15 +10,15 @@ class AudioFileScanner {
|
|||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
getTrackAndDiscNumberFromFilename(mediaMetadataFromScan, audioLibraryFile) {
|
getTrackAndDiscNumberFromFilename(mediaMetadataFromScan, audioLibraryFile) {
|
||||||
const { title, author, series, publishYear } = mediaMetadataFromScan
|
const { title, author, series, publishedYear } = mediaMetadataFromScan
|
||||||
const { filename, path } = audioLibraryFile.metadata
|
const { filename, path } = audioLibraryFile.metadata
|
||||||
var partbasename = Path.basename(filename, Path.extname(filename))
|
var partbasename = Path.basename(filename, Path.extname(filename))
|
||||||
|
|
||||||
// Remove title, author, series, and publishYear from filename if there
|
// Remove title, author, series, and publishedYear from filename if there
|
||||||
if (title) partbasename = partbasename.replace(title, '')
|
if (title) partbasename = partbasename.replace(title, '')
|
||||||
if (author) partbasename = partbasename.replace(author, '')
|
if (author) partbasename = partbasename.replace(author, '')
|
||||||
if (series) partbasename = partbasename.replace(series, '')
|
if (series) partbasename = partbasename.replace(series, '')
|
||||||
if (publishYear) partbasename = partbasename.replace(publishYear)
|
if (publishedYear) partbasename = partbasename.replace(publishedYear)
|
||||||
|
|
||||||
// Look for disc number
|
// Look for disc number
|
||||||
var discNumber = null
|
var discNumber = null
|
||||||
|
@ -108,7 +108,7 @@ class Scanner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('Finished library item scan', libraryItem.hasMediaFiles, hasUpdated)
|
|
||||||
if (!libraryItem.hasMediaFiles) { // Library Item is invalid
|
if (!libraryItem.hasMediaFiles) { // Library Item is invalid
|
||||||
libraryItem.setInvalid()
|
libraryItem.setInvalid()
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
@ -671,10 +671,10 @@ class Scanner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async quickMatchBook(audiobook, options = {}) {
|
async quickMatchBook(libraryItem, options = {}) {
|
||||||
var provider = options.provider || 'google'
|
var provider = options.provider || 'google'
|
||||||
var searchTitle = options.title || audiobook.book._title
|
var searchTitle = options.title || libraryItem.media.metadata.title
|
||||||
var searchAuthor = options.author || audiobook.book._author
|
var searchAuthor = options.author || libraryItem.media.metadata.authorName
|
||||||
|
|
||||||
var results = await this.bookFinder.search(provider, searchTitle, searchAuthor)
|
var results = await this.bookFinder.search(provider, searchTitle, searchAuthor)
|
||||||
if (!results.length) {
|
if (!results.length) {
|
||||||
@ -686,40 +686,70 @@ class Scanner {
|
|||||||
|
|
||||||
// Update cover if not set OR overrideCover flag
|
// Update cover if not set OR overrideCover flag
|
||||||
var hasUpdated = false
|
var hasUpdated = false
|
||||||
if (matchData.cover && (!audiobook.book.cover || options.overrideCover)) {
|
if (matchData.cover && (!libraryItem.media.coverPath || options.overrideCover)) {
|
||||||
Logger.debug(`[BookController] Updating cover "${matchData.cover}"`)
|
Logger.debug(`[Scanner] Updating cover "${matchData.cover}"`)
|
||||||
var coverResult = await this.coverController.downloadCoverFromUrl(audiobook, matchData.cover)
|
var coverResult = await this.coverController.downloadCoverFromUrl(libraryItem, matchData.cover)
|
||||||
if (!coverResult || coverResult.error || !coverResult.cover) {
|
if (!coverResult || coverResult.error || !coverResult.cover) {
|
||||||
Logger.warn(`[BookController] Match cover "${matchData.cover}" failed to use: ${coverResult ? coverResult.error : 'Unknown Error'}`)
|
Logger.warn(`[Scanner] Match cover "${matchData.cover}" failed to use: ${coverResult ? coverResult.error : 'Unknown Error'}`)
|
||||||
} else {
|
} else {
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update book details if not set OR overrideDetails flag
|
// Update media metadata if not set OR overrideDetails flag
|
||||||
const detailKeysToUpdate = ['title', 'subtitle', 'author', 'narrator', 'publisher', 'publishYear', 'series', 'volumeNumber', 'asin', 'isbn']
|
const detailKeysToUpdate = ['title', 'subtitle', 'narrator', 'publisher', 'publishedYear', 'asin', 'isbn']
|
||||||
const updatePayload = {}
|
const updatePayload = {}
|
||||||
for (const key in matchData) {
|
for (const key in matchData) {
|
||||||
if (matchData[key] && detailKeysToUpdate.includes(key) && (!audiobook.book[key] || options.overrideDetails)) {
|
if (matchData[key] && detailKeysToUpdate.includes(key)) {
|
||||||
updatePayload[key] = matchData[key]
|
if (key === 'narrator') {
|
||||||
|
if ((!libraryItem.media.metadata.narratorName || options.overrideDetails)) {
|
||||||
|
updatePayload.narrators = [matchData[key]]
|
||||||
|
}
|
||||||
|
} else if ((!libraryItem.media.metadata[key] || options.overrideDetails)) {
|
||||||
|
updatePayload[key] = matchData[key]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add or set author if not set
|
||||||
|
if (matchData.author && !libraryItem.media.metadata.authorName) {
|
||||||
|
var author = this.db.authors.find(au => au.checkNameEquals(matchData.author))
|
||||||
|
if (!author) {
|
||||||
|
author = new Author()
|
||||||
|
author.setData({ name: matchData.author })
|
||||||
|
await this.db.insertEntity('author', author)
|
||||||
|
this.emitter('author_added', author)
|
||||||
|
}
|
||||||
|
updatePayload.authors = [author.toJSONMinimal()]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add or set series if not set
|
||||||
|
if (matchData.series && !libraryItem.media.metadata.seriesName) {
|
||||||
|
var seriesItem = this.db.series.find(au => au.checkNameEquals(matchData.series))
|
||||||
|
if (!seriesItem) {
|
||||||
|
seriesItem = new Series()
|
||||||
|
seriesItem.setData({ name: matchData.series })
|
||||||
|
await this.db.insertEntity('series', seriesItem)
|
||||||
|
this.emitter('series_added', seriesItem)
|
||||||
|
}
|
||||||
|
updatePayload.series = [seriesItem.toJSONMinimal(matchData.volumeNumber)]
|
||||||
|
}
|
||||||
|
|
||||||
if (Object.keys(updatePayload).length) {
|
if (Object.keys(updatePayload).length) {
|
||||||
Logger.debug('[BookController] Updating details', updatePayload)
|
Logger.debug('[Scanner] Updating details', updatePayload)
|
||||||
if (audiobook.update({ book: updatePayload })) {
|
if (libraryItem.media.update({ metadata: updatePayload })) {
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasUpdated) {
|
if (hasUpdated) {
|
||||||
await this.db.updateAudiobook(audiobook)
|
await this.db.updateLibraryItem(libraryItem)
|
||||||
this.emitter('audiobook_updated', audiobook.toJSONExpanded())
|
this.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
updated: hasUpdated,
|
updated: hasUpdated,
|
||||||
audiobook: audiobook.toJSONExpanded()
|
libraryItem: libraryItem.toJSONExpanded()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ const bookKeyMap = {
|
|||||||
subtitle: 'subtitle',
|
subtitle: 'subtitle',
|
||||||
author: 'authorFL',
|
author: 'authorFL',
|
||||||
narrator: 'narratorFL',
|
narrator: 'narratorFL',
|
||||||
publishYear: 'publishYear',
|
publishedYear: 'publishedYear',
|
||||||
publisher: 'publisher',
|
publisher: 'publisher',
|
||||||
description: 'description',
|
description: 'description',
|
||||||
isbn: 'isbn',
|
isbn: 'isbn',
|
||||||
|
@ -75,7 +75,6 @@ function makeSeriesFromOldAb({ series, volumeNumber }) {
|
|||||||
function getRelativePath(srcPath, basePath) {
|
function getRelativePath(srcPath, basePath) {
|
||||||
srcPath = srcPath.replace(/\\/g, '/')
|
srcPath = srcPath.replace(/\\/g, '/')
|
||||||
basePath = basePath.replace(/\\/g, '/')
|
basePath = basePath.replace(/\\/g, '/')
|
||||||
if (basePath.endsWith('/')) basePath = basePath.slice(0, -1)
|
|
||||||
return srcPath.replace(basePath, '')
|
return srcPath.replace(basePath, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,6 +155,7 @@ function makeLibraryItemFromOldAb(audiobook) {
|
|||||||
|
|
||||||
var bookEntity = new Book()
|
var bookEntity = new Book()
|
||||||
var bookMetadata = new BookMetadata(audiobook.book)
|
var bookMetadata = new BookMetadata(audiobook.book)
|
||||||
|
bookMetadata.publishedYear = audiobook.book.publishYear || null
|
||||||
if (audiobook.book.narrator) {
|
if (audiobook.book.narrator) {
|
||||||
bookMetadata.narrators = audiobook.book._narratorsList
|
bookMetadata.narrators = audiobook.book._narratorsList
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,7 @@ async function writeMetadataFile(audiobook, outputPath) {
|
|||||||
`title=${audiobook.title}`,
|
`title=${audiobook.title}`,
|
||||||
`artist=${audiobook.authorFL}`,
|
`artist=${audiobook.authorFL}`,
|
||||||
`album_artist=${audiobook.authorFL}`,
|
`album_artist=${audiobook.authorFL}`,
|
||||||
`date=${audiobook.book.publishYear || ''}`,
|
`date=${audiobook.book.publishedYear || ''}`,
|
||||||
`description=${audiobook.book.description}`,
|
`description=${audiobook.book.description}`,
|
||||||
`genre=${audiobook.book._genres.join(';')}`,
|
`genre=${audiobook.book._genres.join(';')}`,
|
||||||
`comment=Audiobookshelf v${package.version}`
|
`comment=Audiobookshelf v${package.version}`
|
||||||
|
@ -29,7 +29,7 @@ async function generate(audiobook, nfoFilename = 'metadata.nfo') {
|
|||||||
'Narrator': book.narrator,
|
'Narrator': book.narrator,
|
||||||
'Series': book.series,
|
'Series': book.series,
|
||||||
'Volume Number': book.volumeNumber,
|
'Volume Number': book.volumeNumber,
|
||||||
'Publish Year': book.publishYear,
|
'Publish Year': book.publishedYear,
|
||||||
'Genre': book.genres ? book.genres.join(', ') : '',
|
'Genre': book.genres ? book.genres.join(', ') : '',
|
||||||
'Duration': audiobook.durationPretty,
|
'Duration': audiobook.durationPretty,
|
||||||
'Chapters': jsonObj.chapters.length
|
'Chapters': jsonObj.chapters.length
|
||||||
|
@ -113,7 +113,7 @@ module.exports.parseOpfMetadataXML = async (xml) => {
|
|||||||
title: fetchTitle(metadata),
|
title: fetchTitle(metadata),
|
||||||
author: fetchCreator(creators, 'aut'),
|
author: fetchCreator(creators, 'aut'),
|
||||||
narrator: fetchNarrators(creators, metadata),
|
narrator: fetchNarrators(creators, metadata),
|
||||||
publishYear: fetchDate(metadata),
|
publishedYear: fetchDate(metadata),
|
||||||
publisher: fetchPublisher(metadata),
|
publisher: fetchPublisher(metadata),
|
||||||
isbn: fetchISBN(metadata),
|
isbn: fetchISBN(metadata),
|
||||||
description: fetchDescription(metadata),
|
description: fetchDescription(metadata),
|
||||||
|
@ -119,28 +119,16 @@ function groupFileItemsIntoLibraryItemDirs(fileItems) {
|
|||||||
return libraryItemGroup
|
return libraryItemGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanFileObjects(libraryItemPath, libraryItemRelPath, files) {
|
function cleanFileObjects(libraryItemPath, folderPath, files) {
|
||||||
return Promise.all(files.map(async (file) => {
|
return Promise.all(files.map(async (file) => {
|
||||||
var filePath = Path.posix.join(libraryItemPath, file)
|
var filePath = Path.posix.join(libraryItemPath, file)
|
||||||
var relFilePath = Path.posix.join(libraryItemRelPath, file)
|
var relFilePath = filePath.replace(folderPath, '')
|
||||||
var newLibraryFile = new LibraryFile()
|
var newLibraryFile = new LibraryFile()
|
||||||
await newLibraryFile.setDataFromPath(filePath, relFilePath)
|
await newLibraryFile.setDataFromPath(filePath, relFilePath)
|
||||||
return newLibraryFile
|
return newLibraryFile
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFileType(ext) {
|
|
||||||
var ext_cleaned = ext.toLowerCase()
|
|
||||||
if (ext_cleaned.startsWith('.')) ext_cleaned = ext_cleaned.slice(1)
|
|
||||||
if (globals.SupportedAudioTypes.includes(ext_cleaned)) return 'audio'
|
|
||||||
if (globals.SupportedImageTypes.includes(ext_cleaned)) return 'image'
|
|
||||||
if (globals.SupportedEbookTypes.includes(ext_cleaned)) return 'ebook'
|
|
||||||
if (ext_cleaned === 'nfo') return 'info'
|
|
||||||
if (ext_cleaned === 'txt') return 'text'
|
|
||||||
if (ext_cleaned === 'opf') return 'opf'
|
|
||||||
return 'unknown'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scan folder
|
// Scan folder
|
||||||
async function scanFolder(libraryMediaType, folder, serverSettings = {}) {
|
async function scanFolder(libraryMediaType, folder, serverSettings = {}) {
|
||||||
var folderPath = folder.fullPath.replace(/\\/g, '/')
|
var folderPath = folder.fullPath.replace(/\\/g, '/')
|
||||||
@ -164,7 +152,7 @@ async function scanFolder(libraryMediaType, folder, serverSettings = {}) {
|
|||||||
for (const libraryItemPath in libraryItemGrouping) {
|
for (const libraryItemPath in libraryItemGrouping) {
|
||||||
var libraryItemData = getDataFromMediaDir(libraryMediaType, folderPath, libraryItemPath, serverSettings)
|
var libraryItemData = getDataFromMediaDir(libraryMediaType, folderPath, libraryItemPath, serverSettings)
|
||||||
|
|
||||||
var fileObjs = await cleanFileObjects(libraryItemData.path, libraryItemData.relPath, libraryItemGrouping[libraryItemPath])
|
var fileObjs = await cleanFileObjects(libraryItemData.path, folderPath, libraryItemGrouping[libraryItemPath])
|
||||||
var libraryItemFolderStats = await getFileTimestampsWithIno(libraryItemData.path)
|
var libraryItemFolderStats = await getFileTimestampsWithIno(libraryItemData.path)
|
||||||
items.push({
|
items.push({
|
||||||
folderId: folder.id,
|
folderId: folder.id,
|
||||||
@ -236,7 +224,7 @@ function getBookDataFromDir(folderPath, relPath, parseSubtitle = false) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var publishYear = null
|
var publishedYear = null
|
||||||
// If Title is of format 1999 OR (1999) - Title, then use 1999 as publish year
|
// If Title is of format 1999 OR (1999) - Title, then use 1999 as publish year
|
||||||
var publishYearMatch = title.match(/^(\(?[0-9]{4}\)?) - (.+)/)
|
var publishYearMatch = title.match(/^(\(?[0-9]{4}\)?) - (.+)/)
|
||||||
if (publishYearMatch && publishYearMatch.length > 2 && publishYearMatch[1]) {
|
if (publishYearMatch && publishYearMatch.length > 2 && publishYearMatch[1]) {
|
||||||
@ -245,7 +233,7 @@ function getBookDataFromDir(folderPath, relPath, parseSubtitle = false) {
|
|||||||
publishYearMatch[1] = publishYearMatch[1].slice(1, -1)
|
publishYearMatch[1] = publishYearMatch[1].slice(1, -1)
|
||||||
}
|
}
|
||||||
if (!isNaN(publishYearMatch[1])) {
|
if (!isNaN(publishYearMatch[1])) {
|
||||||
publishYear = publishYearMatch[1]
|
publishedYear = publishYearMatch[1]
|
||||||
title = publishYearMatch[2]
|
title = publishYearMatch[2]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -267,7 +255,7 @@ function getBookDataFromDir(folderPath, relPath, parseSubtitle = false) {
|
|||||||
subtitle,
|
subtitle,
|
||||||
series,
|
series,
|
||||||
sequence: volumeNumber,
|
sequence: volumeNumber,
|
||||||
publishYear,
|
publishedYear,
|
||||||
},
|
},
|
||||||
relPath: relPath, // relative audiobook path i.e. /Author Name/Book Name/..
|
relPath: relPath, // relative audiobook path i.e. /Author Name/Book Name/..
|
||||||
path: Path.posix.join(folderPath, relPath) // i.e. /audiobook/Author Name/Book Name/..
|
path: Path.posix.join(folderPath, relPath) // i.e. /audiobook/Author Name/Book Name/..
|
||||||
@ -281,7 +269,7 @@ function getDataFromMediaDir(libraryMediaType, folderPath, relPath, serverSettin
|
|||||||
|
|
||||||
|
|
||||||
async function getLibraryItemFileData(libraryMediaType, folder, libraryItemPath, serverSettings = {}) {
|
async function getLibraryItemFileData(libraryMediaType, folder, libraryItemPath, serverSettings = {}) {
|
||||||
var fileItems = await recurseFiles(libraryItemPath, folder.fullPath)
|
var fileItems = await recurseFiles(libraryItemPath)
|
||||||
|
|
||||||
libraryItemPath = libraryItemPath.replace(/\\/g, '/')
|
libraryItemPath = libraryItemPath.replace(/\\/g, '/')
|
||||||
var folderFullPath = folder.fullPath.replace(/\\/g, '/')
|
var folderFullPath = folder.fullPath.replace(/\\/g, '/')
|
||||||
|
Loading…
Reference in New Issue
Block a user