New api routes, updating web client pages, audiobooks to libraryItem migration

This commit is contained in:
advplyr 2022-03-10 18:45:02 -06:00
parent b97ed953f7
commit 2a30cc428f
51 changed files with 1225 additions and 654 deletions

View file

@ -19,7 +19,7 @@
</div>
<div class="w-full h-full text-sm rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300">
<component v-if="audiobook && show" :is="tabName" :audiobook="audiobook" :processing.sync="processing" @close="show = false" @selectTab="selectTab" />
<component v-if="libraryItem && show" :is="tabName" :library-item="libraryItem" :processing.sync="processing" @close="show = false" @selectTab="selectTab" />
</div>
</modals-modal>
</template>
@ -29,7 +29,7 @@ export default {
data() {
return {
processing: false,
audiobook: null,
libraryItem: null,
fetchOnShow: false,
tabs: [
{
@ -42,11 +42,6 @@ export default {
title: 'Cover',
component: 'modals-edit-tabs-cover'
},
// {
// id: 'tracks',
// title: 'Tracks',
// component: 'modals-edit-tabs-tracks'
// },
{
id: 'chapters',
title: 'Chapters',
@ -89,12 +84,12 @@ export default {
this.selectedTab = availableTabIds[0]
}
if (this.audiobook && this.audiobook.id === this.selectedAudiobookId) {
if (this.libraryItem && this.libraryItem.id === this.selectedLibraryItemId) {
if (this.fetchOnShow) this.fetchFull()
return
}
this.fetchOnShow = false
this.audiobook = null
this.libraryItem = null
this.init()
this.registerListeners()
} else {
@ -148,26 +143,29 @@ export default {
return _tab ? _tab.component : ''
},
isMissing() {
return this.selectedAudiobook.isMissing
return this.selectedLibraryItem.isMissing
},
selectedAudiobook() {
return this.$store.state.selectedAudiobook || {}
selectedLibraryItem() {
return this.$store.state.selectedLibraryItem || {}
},
selectedAudiobookId() {
return this.selectedAudiobook.id
selectedLibraryItemId() {
return this.selectedLibraryItem.id
},
book() {
return this.audiobook ? this.audiobook.book || {} : {}
media() {
return this.libraryItem ? this.libraryItem.media || {} : {}
},
mediaMetadata() {
return this.media.metadata || {}
},
title() {
return this.book.title || 'No Title'
return this.mediaMetadata.title || 'No Title'
},
bookshelfBookIds() {
return this.$store.state.bookshelfBookIds || []
},
currentBookshelfIndex() {
if (!this.bookshelfBookIds.length) return 0
return this.bookshelfBookIds.findIndex((bid) => bid === this.selectedAudiobookId)
return this.bookshelfBookIds.findIndex((bid) => bid === this.selectedLibraryItemId)
},
canGoPrev() {
return this.bookshelfBookIds.length && this.currentBookshelfIndex > 0
@ -188,7 +186,7 @@ export default {
})
this.processing = false
if (prevBook) {
this.$store.commit('showEditModalOnTab', { audiobook: prevBook, tab: this.selectedTab })
this.$store.commit('showEditModalOnTab', { libraryItem: prevBook, tab: this.selectedTab })
this.$nextTick(this.init)
} else {
console.error('Book not found', prevBookId)
@ -205,7 +203,7 @@ export default {
})
this.processing = false
if (nextBook) {
this.$store.commit('showEditModalOnTab', { audiobook: nextBook, tab: this.selectedTab })
this.$store.commit('showEditModalOnTab', { libraryItem: nextBook, tab: this.selectedTab })
this.$nextTick(this.init)
} else {
console.error('Book not found', nextBookId)
@ -223,16 +221,16 @@ export default {
}
},
init() {
this.$store.commit('audiobooks/addListener', { meth: this.audiobookUpdated, id: 'edit-modal', audiobookId: this.selectedAudiobookId })
this.$store.commit('audiobooks/addListener', { meth: this.audiobookUpdated, id: 'edit-modal', audiobookId: this.selectedLibraryItemId })
this.fetchFull()
},
async fetchFull() {
try {
this.processing = true
this.audiobook = await this.$axios.$get(`/api/books/${this.selectedAudiobookId}`)
this.libraryItem = await this.$axios.$get(`/api/items/${this.selectedLibraryItemId}?expanded=1`)
this.processing = false
} catch (error) {
console.error('Failed to fetch audiobook', this.selectedAudiobookId, error)
console.error('Failed to fetch audiobook', this.selectedLibraryItemId, error)
this.processing = false
this.show = false
}

View file

@ -31,7 +31,7 @@
<script>
export default {
props: {
audiobook: {
libraryItem: {
type: Object,
default: () => {}
}
@ -42,7 +42,7 @@ export default {
}
},
watch: {
audiobook: {
libraryItem: {
immediate: true,
handler(newVal) {
if (newVal) this.init()
@ -52,7 +52,7 @@ export default {
computed: {},
methods: {
init() {
this.chapters = this.audiobook.chapters || []
this.chapters = this.libraryItem.chapters || []
}
}
}

View file

@ -2,9 +2,9 @@
<div class="w-full h-full overflow-hidden overflow-y-auto px-4 py-6 relative">
<div class="flex">
<div class="relative">
<covers-book-cover :audiobook="audiobook" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<covers-book-cover :library-item="libraryItem" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<!-- book cover overlay -->
<div v-if="book.cover" class="absolute top-0 left-0 w-full h-full z-10 opacity-0 hover:opacity-100 transition-opacity duration-100">
<div v-if="media.coverPath" class="absolute top-0 left-0 w-full h-full z-10 opacity-0 hover:opacity-100 transition-opacity duration-100">
<div class="absolute top-0 left-0 w-full h-16 bg-gradient-to-b from-black-600 to-transparent" />
<div class="p-1 absolute top-1 right-1 text-red-500 rounded-full w-8 h-8 cursor-pointer hover:text-red-400 shadow-sm" @click="removeCover">
<span class="material-icons">delete</span>
@ -82,7 +82,7 @@
export default {
props: {
processing: Boolean,
audiobook: {
libraryItem: {
type: Object,
default: () => {}
}
@ -102,7 +102,7 @@ export default {
}
},
watch: {
audiobook: {
libraryItem: {
immediate: true,
handler(newVal) {
if (newVal) {
@ -134,17 +134,17 @@ export default {
bookCoverAspectRatio() {
return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE ? 1 : 1.6
},
audiobookId() {
return this.audiobook ? this.audiobook.id : null
libraryItemId() {
return this.libraryItem ? this.libraryItem.id : null
},
book() {
return this.audiobook ? this.audiobook.book || {} : {}
media() {
return this.libraryItem ? this.libraryItem.media || {} : {}
},
audiobookPath() {
return this.audiobook ? this.audiobook.path : null
mediaMetadata() {
return this.media.metadata || {}
},
otherFiles() {
return this.audiobook ? this.audiobook.otherFiles || [] : []
libraryFiles() {
return this.libraryItem ? this.libraryItem.libraryFiles || [] : []
},
userCanUpload() {
return this.$store.getters['user/getUserCanUpload']
@ -153,12 +153,11 @@ export default {
return this.$store.getters['user/getToken']
},
localCovers() {
return this.otherFiles
.filter((f) => f.filetype === 'image')
return this.libraryFiles
.filter((f) => f.fileType === 'image')
.map((file) => {
var _file = { ...file }
var imgRelPath = _file.path.replace(this.audiobookPath, '')
_file.localPath = `/s/book/${this.audiobookId}/${imgRelPath}`
_file.localPath = `/s/item/${this.libraryItemId}/${this.$encodeUriPath(file.metadata.relPath)}`
return _file
})
}
@ -170,7 +169,7 @@ export default {
form.set('cover', this.selectedFile)
this.$axios
.$post(`/api/books/${this.audiobook.id}/cover`, form)
.$post(`/api/books/${this.libraryItemId}/cover`, form)
.then((data) => {
if (data.error) {
this.$toast.error(data.error)
@ -203,17 +202,17 @@ export default {
},
init() {
this.showLocalCovers = false
if (this.coversFound.length && (this.searchTitle !== this.book.title || this.searchAuthor !== this.book.authorFL)) {
if (this.coversFound.length && (this.searchTitle !== this.mediaMetadata.title || this.searchAuthor !== this.mediaMetadata.authorName)) {
this.coversFound = []
this.hasSearched = false
}
this.imageUrl = this.book.cover || ''
this.searchTitle = this.book.title || ''
this.searchAuthor = this.book.authorFL || ''
this.imageUrl = this.media.coverPath || ''
this.searchTitle = this.mediaMetadata.title || ''
this.searchAuthor = this.mediaMetadata.authorName || ''
this.provider = localStorage.getItem('book-provider') || 'openlibrary'
},
removeCover() {
if (!this.book.cover) {
if (!this.media.coverPath) {
this.imageUrl = ''
return
}
@ -223,7 +222,7 @@ export default {
this.updateCover(this.imageUrl)
},
async updateCover(cover) {
if (cover === this.book.cover) {
if (cover === this.media.coverPath) {
console.warn('Cover has not changed..', cover)
return
}
@ -233,7 +232,7 @@ export default {
// Download cover from url and use
if (cover.startsWith('http:') || cover.startsWith('https:')) {
success = await this.$axios.$post(`/api/books/${this.audiobook.id}/cover`, { url: cover }).catch((error) => {
success = await this.$axios.$post(`/api/items/${this.libraryItemId}/cover`, { url: cover }).catch((error) => {
console.error('Failed to download cover from url', error)
if (error.response && error.response.data) {
this.$toast.error(error.response.data)
@ -247,7 +246,7 @@ export default {
cover: cover
}
}
success = await this.$axios.$patch(`/api/books/${this.audiobook.id}`, updatePayload).catch((error) => {
success = await this.$axios.$patch(`/api/books/${this.libraryItemId}`, updatePayload).catch((error) => {
console.error('Failed to update', error)
if (error.response && error.response.data) {
this.$toast.error(error.response.data)
@ -259,7 +258,7 @@ export default {
this.$toast.success('Update Successful')
this.$emit('close')
} else {
this.imageUrl = this.book.cover || ''
this.imageUrl = this.media.coverPath || ''
}
this.isProcessing = false
},
@ -292,7 +291,7 @@ export default {
setCover(coverFile) {
this.isProcessing = true
this.$axios
.$patch(`/api/books/${this.audiobook.id}/coverfile`, coverFile)
.$patch(`/api/books/${this.libraryItemId}/coverfile`, coverFile)
.then((data) => {
console.log('response data', data)
if (data && typeof data === 'string') {

View file

@ -13,7 +13,8 @@
<div class="flex mt-2 -mx-1">
<div class="w-3/4 px-1">
<ui-text-input-with-label v-model="details.author" label="Author" />
<!-- <ui-text-input-with-label v-model="details.authors" label="Author" /> -->
<p>Authors placeholder</p>
</div>
<div class="flex-grow px-1">
<ui-text-input-with-label v-model="details.publishYear" type="number" label="Publish Year" />
@ -22,10 +23,11 @@
<div class="flex mt-2 -mx-1">
<div class="w-3/4 px-1">
<ui-input-dropdown ref="seriesDropdown" v-model="details.series" label="Series" :items="series" />
<p>Series placeholder</p>
<!-- <ui-input-dropdown ref="seriesDropdown" v-model="details.series" label="Series" :items="series" /> -->
</div>
<div class="flex-grow px-1">
<ui-text-input-with-label v-model="details.volumeNumber" label="Volume #" />
<!-- <ui-text-input-with-label v-model="details.volumeNumber" label="Volume #" /> -->
</div>
</div>
@ -64,7 +66,7 @@
<div class="absolute bottom-0 left-0 w-full py-4 bg-bg" :class="isScrollable ? 'box-shadow-md-up' : 'box-shadow-sm-up border-t border-primary border-opacity-50'">
<div class="flex items-center px-4">
<ui-btn v-if="userCanDelete" color="error" type="button" class="h-8" :padding-x="3" small @click.stop.prevent="deleteAudiobook">Remove</ui-btn>
<ui-btn v-if="userCanDelete" color="error" type="button" class="h-8" :padding-x="3" small @click.stop.prevent="removeItem">Remove</ui-btn>
<div class="flex-grow" />
@ -91,7 +93,7 @@
export default {
props: {
processing: Boolean,
audiobook: {
libraryItem: {
type: Object,
default: () => {}
}
@ -105,7 +107,6 @@ export default {
author: null,
narrator: null,
series: null,
volumeNumber: null,
publishYear: null,
publisher: null,
language: null,
@ -122,7 +123,7 @@ export default {
}
},
watch: {
audiobook: {
libraryItem: {
immediate: true,
handler(newVal) {
if (newVal) this.init()
@ -142,13 +143,16 @@ export default {
return this.$store.getters['user/getIsRoot']
},
isMissing() {
return !!this.audiobook && !!this.audiobook.isMissing
return !!this.libraryItem && !!this.libraryItem.isMissing
},
audiobookId() {
return this.audiobook ? this.audiobook.id : null
libraryItemId() {
return this.libraryItem ? this.libraryItem.id : null
},
book() {
return this.audiobook ? this.audiobook.book || {} : {}
media() {
return this.libraryItem ? this.libraryItem.media || {} : {}
},
mediaMetadata() {
return this.media.metadata || {}
},
userCanDelete() {
return this.$store.getters['user/getUserCanDelete']
@ -166,7 +170,7 @@ export default {
return this.$store.state.libraries.filterData || {}
},
libraryId() {
return this.audiobook ? this.audiobook.libraryId : null
return this.libraryItem ? this.libraryItem.libraryId : null
},
libraryProvider() {
return this.$store.getters['libraries/getLibraryProvider'](this.libraryId) || 'google'
@ -185,13 +189,13 @@ export default {
author: this.details.author !== this.book.author ? this.details.author : null
}
this.$axios
.$post(`/api/books/${this.audiobookId}/match`, matchOptions)
.$post(`/api/books/${this.libraryItemId}/match`, matchOptions)
.then((res) => {
this.quickMatching = false
if (res.warning) {
this.$toast.warning(res.warning)
} else if (res.updated) {
this.$toast.success('Audiobook details updated')
this.$toast.success('Item details updated')
} else {
this.$toast.info('No updates were made')
}
@ -236,7 +240,7 @@ export default {
saveMetadata() {
this.savingMetadata = true
this.$root.socket.once('save_metadata_complete', this.saveMetadataComplete)
this.$root.socket.emit('save_metadata', this.audiobookId)
this.$root.socket.emit('save_metadata', this.libraryItemId)
},
submitForm() {
if (this.isProcessing) {
@ -260,7 +264,7 @@ export default {
tags: this.newTags
}
var updatedAudiobook = await this.$axios.$patch(`/api/books/${this.audiobook.id}`, updatePayload).catch((error) => {
var updatedAudiobook = await this.$axios.$patch(`/api/books/${this.libraryItemId}`, updatePayload).catch((error) => {
console.error('Failed to update', error)
return false
})
@ -271,35 +275,34 @@ export default {
}
},
init() {
this.details.title = this.book.title
this.details.subtitle = this.book.subtitle
this.details.description = this.book.description
this.details.author = this.book.author
this.details.narrator = this.book.narrator
this.details.genres = this.book.genres || []
this.details.series = this.book.series
this.details.volumeNumber = this.book.volumeNumber
this.details.publishYear = this.book.publishYear
this.details.publisher = this.book.publisher || null
this.details.language = this.book.language || null
this.details.isbn = this.book.isbn || null
this.details.asin = this.book.asin || null
this.details.title = this.mediaMetadata.title
this.details.subtitle = this.mediaMetadata.subtitle
this.details.description = this.mediaMetadata.description
this.details.authors = this.mediaMetadata.authors
this.details.narrator = this.mediaMetadata.narrator
this.details.genres = this.mediaMetadata.genres || []
this.details.series = this.mediaMetadata.series
this.details.publishYear = this.mediaMetadata.publishYear
this.details.publisher = this.mediaMetadata.publisher || null
this.details.language = this.mediaMetadata.language || null
this.details.isbn = this.mediaMetadata.isbn || null
this.details.asin = this.mediaMetadata.asin || null
this.newTags = this.audiobook.tags || []
this.newTags = this.media.tags || []
},
deleteAudiobook() {
if (confirm(`Are you sure you want to remove this audiobook?\n\n*Does not delete your files, only removes the audiobook from AudioBookshelf`)) {
removeItem() {
if (confirm(`Are you sure you want to remove this item?\n\n*Does not delete your files, only removes the item from audiobookshelf`)) {
this.isProcessing = true
this.$axios
.$delete(`/api/books/${this.audiobookId}`)
.$delete(`/api/books/${this.libraryItemId}`)
.then(() => {
console.log('Audiobook removed')
this.$toast.success('Audiobook Removed')
console.log('Item removed')
this.$toast.success('Item Removed')
this.$emit('close')
this.isProcessing = false
})
.catch((error) => {
console.error('Remove Audiobook failed', error)
console.error('Remove item failed', error)
this.isProcessing = false
})
}

View file

@ -65,7 +65,7 @@
export default {
props: {
processing: Boolean,
audiobook: {
libraryItem: {
type: Object,
default: () => {}
}
@ -86,14 +86,14 @@ export default {
}
},
computed: {
audiobookId() {
return this.audiobook ? this.audiobook.id : null
libraryItemId() {
return this.libraryItem ? this.libraryItem.id : null
},
_audiobook() {
return this.audiobook || {}
media() {
return this.libraryItem ? this.libraryItem.media || {} : {}
},
downloads() {
return this.$store.getters['downloads/getDownloads'](this.audiobookId)
return this.$store.getters['downloads/getDownloads'](this.libraryItemId)
},
singleAudioDownload() {
return this.downloads.find((d) => d.type === 'singleAudio')
@ -108,24 +108,21 @@ export default {
return this.zipDownload ? this.zipDownload.status : false
},
isSingleTrack() {
if (!this.audiobook.tracks) return false
return this.audiobook.tracks.length === 1
if (!this.libraryItem.tracks) return false
return this.libraryItem.tracks.length === 1
},
singleTrackPath() {
if (!this.isSingleTrack) return null
return this.audiobook.tracks[0].path
},
audioFiles() {
return this.audiobook ? this.audiobook.audioFiles || [] : []
},
otherFiles() {
return this.audiobook ? this.audiobook.otherFiles || [] : []
libraryFiles() {
return this.libraryItem.libraryFiles
},
totalFiles() {
return this.audioFiles.length + this.otherFiles.length
return this.libraryFiles.length
},
showM4bDownload() {
return !this._audiobook.isMissing && !this._audiobook.isInvalid && this._audiobook.tracks.length
return !this.libraryItem.isMissing && this.media.tracks.length
}
},
methods: {

View file

@ -9,7 +9,7 @@
</div>
<div class="flex-grow" />
<ui-btn small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="showFullPath = !showFullPath">Full Path</ui-btn>
<nuxt-link v-if="userCanUpdate" :to="`/audiobook/${audiobook.id}/edit`">
<nuxt-link v-if="userCanUpdate" :to="`/item/${libraryItem.id}/edit`">
<ui-btn small color="primary">Manage Tracks</ui-btn>
</nuxt-link>
</div>
@ -21,20 +21,20 @@
<th class="text-left">Duration</th>
<th v-if="showDownload" class="text-center">Download</th>
</tr>
<template v-for="track in tracksCleaned">
<template v-for="track in tracks">
<tr :key="track.index">
<td class="text-center">
<p>{{ track.index }}</p>
</td>
<td class="font-sans">{{ showFullPath ? track.fullPath : track.filename }}</td>
<td class="font-sans">{{ showFullPath ? track.path : track.filename }}</td>
<td class="font-mono">
{{ $bytesPretty(track.size) }}
{{ $bytesPretty(track.metadata.size) }}
</td>
<td class="font-mono">
{{ $secondsToTimestamp(track.duration) }}
</td>
<td v-if="showDownload" class="font-mono text-center">
<a :href="`/s/book/${audiobook.id}/${track.relativePath}?token=${userToken}`" download><span class="material-icons icon-text">download</span></a>
<a :href="`/s/item/${libraryItem.id}/${track.metadata.relPath}?token=${userToken}`" download><span class="material-icons icon-text">download</span></a>
</td>
</tr>
</template>
@ -43,26 +43,26 @@
<div v-else class="flex my-4 text-center justify-center text-xl">No Audio Tracks</div>
</div>
<tables-all-files-table :audiobook="audiobook" />
<tables-library-files-table :files="libraryFiles" :library-item-id="libraryItem.id" :is-missing="isMissing" />
</div>
</template>
<script>
export default {
props: {
audiobook: {
libraryItem: {
type: Object,
default: () => {}
}
},
data() {
return {
tracks: null,
tracks: [],
showFullPath: false
}
},
watch: {
audiobook: {
libraryItem: {
immediate: true,
handler(newVal) {
if (newVal) this.init()
@ -70,22 +70,11 @@ export default {
}
},
computed: {
audiobookPath() {
return this.audiobook.path
media() {
return this.libraryItem.media || {}
},
tracksCleaned() {
return this.tracks.map((track) => {
var trackPath = track.path.replace(/\\/g, '/')
var audiobookPath = this.audiobookPath.replace(/\\/g, '/')
return {
...track,
relativePath: trackPath
.replace(audiobookPath + '/', '')
.replace(/%/g, '%25')
.replace(/#/g, '%23')
}
})
libraryFiles() {
return this.libraryItem.libraryFiles || []
},
userToken() {
return this.$store.getters['user/getToken']
@ -97,18 +86,18 @@ export default {
return this.$store.getters['user/getUserCanDownload']
},
isMissing() {
return this.audiobook.isMissing
return this.libraryItem.isMissing
},
showDownload() {
return this.userCanDownload && !this.isMissing
},
hasTracks() {
return this.audiobook.tracks.length
return this.tracks.length
}
},
methods: {
init() {
this.tracks = this.audiobook.tracks
this.tracks = this.media.tracks || []
}
}
}

View file

@ -94,14 +94,14 @@
export default {
props: {
processing: Boolean,
audiobook: {
libraryItem: {
type: Object,
default: () => {}
}
},
data() {
return {
audiobookId: null,
libraryItemId: null,
searchTitle: null,
searchAuthor: null,
lastSearch: null,
@ -126,7 +126,7 @@ export default {
}
},
watch: {
audiobook: {
libraryItem: {
immediate: true,
handler(newVal) {
if (newVal) this.init()
@ -209,19 +209,19 @@ export default {
isbn: true
}
if (this.audiobook.id !== this.audiobookId) {
if (this.libraryItem.id !== this.libraryItemId) {
this.searchResults = []
this.hasSearched = false
this.audiobookId = this.audiobook.id
this.libraryItemId = this.libraryItem.id
}
if (!this.audiobook.book || !this.audiobook.book.title) {
if (!this.libraryItem.media || !this.libraryItem.media.metadata.title) {
this.searchTitle = null
this.searchAuthor = null
return
}
this.searchTitle = this.audiobook.book.title
this.searchAuthor = this.audiobook.book.authorFL || ''
this.searchTitle = this.libraryItem.media.metadata.title
this.searchAuthor = this.libraryItem.media.metadata.authorName || ''
this.provider = localStorage.getItem('book-provider') || 'google'
},
selectMatch(match) {
@ -247,7 +247,7 @@ export default {
var coverPayload = {
url: updatePayload.cover
}
var success = await this.$axios.$post(`/api/books/${this.audiobook.id}/cover`, coverPayload).catch((error) => {
var success = await this.$axios.$post(`/api/books/${this.libraryItemId}/cover`, coverPayload).catch((error) => {
console.error('Failed to update', error)
return false
})
@ -264,7 +264,7 @@ export default {
var bookUpdatePayload = {
book: updatePayload
}
var success = await this.$axios.$patch(`/api/books/${this.audiobook.id}`, bookUpdatePayload).catch((error) => {
var success = await this.$axios.$patch(`/api/books/${this.libraryItemId}`, bookUpdatePayload).catch((error) => {
console.error('Failed to update', error)
return false
})

View file

@ -1,110 +0,0 @@
<template>
<div class="w-full h-full overflow-y-auto overflow-x-hidden px-4 py-6">
<template v-if="hasTracks">
<div class="w-full bg-primary px-4 py-2 flex items-center">
<div class="h-7 w-7 rounded-full bg-white bg-opacity-10 flex items-center justify-center">
<span class="text-sm font-mono">{{ tracks.length }}</span>
</div>
<div class="flex-grow" />
<ui-btn small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="showFullPath = !showFullPath">Full Path</ui-btn>
<nuxt-link v-if="userCanUpdate" :to="`/audiobook/${audiobook.id}/edit`" class="mr-4">
<ui-btn small color="primary">Manage Tracks</ui-btn>
</nuxt-link>
</div>
<table class="text-sm tracksTable">
<tr class="font-book">
<th>#</th>
<th class="text-left">Filename</th>
<th class="text-left">Size</th>
<th class="text-left">Duration</th>
<th v-if="showDownload" class="text-center">Download</th>
</tr>
<template v-for="track in tracksCleaned">
<tr :key="track.index">
<td class="text-center">
<p>{{ track.index }}</p>
</td>
<td class="font-sans">{{ showFullPath ? track.fullPath : track.filename }}</td>
<td class="font-mono">
{{ $bytesPretty(track.size) }}
</td>
<td class="font-mono">
{{ $secondsToTimestamp(track.duration) }}
</td>
<td v-if="showDownload" class="font-mono text-center">
<a :href="`/s/book/${audiobook.id}/${track.relativePath}?token=${userToken}`" download><span class="material-icons icon-text">download</span></a>
</td>
</tr>
</template>
</table>
</template>
<div v-else class="flex my-4 text-center justify-center text-xl">No Audio Tracks</div>
</div>
</template>
<script>
export default {
props: {
audiobook: {
type: Object,
default: () => {}
}
},
data() {
return {
tracks: null,
showFullPath: false
}
},
watch: {
audiobook: {
immediate: true,
handler(newVal) {
if (newVal) this.init()
}
}
},
computed: {
audiobookPath() {
return this.audiobook.path
},
tracksCleaned() {
return this.tracks.map((track) => {
var trackPath = track.path.replace(/\\/g, '/')
var audiobookPath = this.audiobookPath.replace(/\\/g, '/')
return {
...track,
relativePath: trackPath
.replace(audiobookPath + '/', '')
.replace(/%/g, '%25')
.replace(/#/g, '%23')
}
})
},
userToken() {
return this.$store.getters['user/getToken']
},
userCanUpdate() {
return this.$store.getters['user/getUserCanUpdate']
},
userCanDownload() {
return this.$store.getters['user/getUserCanDownload']
},
isMissing() {
return this.audiobook.isMissing
},
showDownload() {
return this.userCanDownload && !this.isMissing
},
hasTracks() {
return this.audiobook.tracks.length
}
},
methods: {
init() {
this.tracks = this.audiobook.tracks
}
}
}
</script>