New data model Book media type contains Audiobooks updates

This commit is contained in:
advplyr 2022-03-17 12:25:12 -05:00
parent 1dde02b170
commit c4eeb1cfb7
30 changed files with 347 additions and 247 deletions

View file

@ -104,7 +104,7 @@ export default {
shelves.push({
id: 'books',
label: 'Books',
type: 'books',
type: 'book',
entities: this.results.books.map((res) => res.libraryItem)
})
}

View file

@ -2,7 +2,7 @@
<div class="relative">
<div ref="shelf" class="w-full max-w-full categorizedBookshelfRow relative overflow-x-scroll overflow-y-hidden z-10" :style="{ paddingLeft: paddingLeft * sizeMultiplier + 'rem', height: shelfHeight + 'px' }" @scroll="scrolled">
<div class="w-full h-full pt-6">
<div v-if="shelf.type === 'books'" class="flex items-center">
<div v-if="shelf.type === 'book'" class="flex items-center">
<template v-for="(entity, index) in shelf.entities">
<cards-lazy-book-card :key="entity.id" :ref="`shelf-book-${entity.id}`" :index="index" :width="bookCoverWidth" :height="bookCoverHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" :book-mount="entity" class="relative mx-2" @hook:updated="updatedBookCard" @select="selectItem" @edit="editBook" />
</template>
@ -108,7 +108,7 @@ export default {
},
updateSelectionMode(val) {
var selectedLibraryItems = this.$store.state.selectedLibraryItems
if (this.shelf.type === 'books') {
if (this.shelf.type === 'book') {
this.shelf.entities.forEach((ent) => {
var component = this.$refs[`shelf-book-${ent.id}`]
if (!component || !component.length) return

View file

@ -97,7 +97,7 @@ export default {
},
userAudiobook() {
if (!this.libraryItemId) return
return this.$store.getters['user/getUserAudiobook'](this.libraryItemId)
return this.$store.getters['user/getUserLibraryItemProgress'](this.libraryItemId)
},
userAudiobookCurrentTime() {
return this.userAudiobook ? this.userAudiobook.currentTime || 0 : 0

View file

@ -35,7 +35,7 @@
</div>
<!-- No progress shown for collapsed series in library -->
<div v-if="!booksInSeries" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b" :class="userIsRead ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
<div v-if="!booksInSeries" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
<!-- Overlay is not shown if collapsing series in library -->
<div v-show="!booksInSeries && audiobook && (isHovering || isSelectionMode || isMoreMenuOpen)" class="w-full h-full absolute top-0 left-0 z-10 bg-black rounded hidden md:block" :class="overlayWrapperClasslist">
@ -214,7 +214,7 @@ export default {
return this.title
},
displayAuthor() {
if (this.orderBy === 'media.metadata.authorLF') return this.authorLF
if (this.orderBy === 'media.metadata.authorNameLF') return this.authorLF
return this.author
},
displaySortLine() {
@ -226,13 +226,13 @@ export default {
return null
},
userProgress() {
return this.store.getters['user/getUserAudiobook'](this.libraryItemId)
return this.store.getters['user/getUserLibraryItemProgress'](this.libraryItemId)
},
userProgressPercent() {
return this.userProgress ? this.userProgress.progress || 0 : 0
},
userIsRead() {
return this.userProgress ? !!this.userProgress.isRead : false
itemIsFinished() {
return this.userProgress ? !!this.userProgress.isFinished : false
},
showError() {
return this.hasMissingParts || this.hasInvalidParts || this.isMissing || this.isInvalid
@ -302,7 +302,7 @@ export default {
var items = [
{
func: 'toggleRead',
text: `Mark as ${this.userIsRead ? 'Not Read' : 'Read'}`
text: `Mark as ${this.itemIsFinished ? 'Not Read' : 'Read'}`
},
{
func: 'openCollections',
@ -401,7 +401,7 @@ export default {
toggleRead() {
// More menu func
var updatePayload = {
isRead: !this.userIsRead
isFinished: !this.itemIsFinished
}
this.isProcessingReadUpdate = true
var toast = this.$toast || this.$nuxt.$toast
@ -410,12 +410,12 @@ export default {
.$patch(`/api/me/audiobook/${this.libraryItemId}`, updatePayload)
.then(() => {
this.isProcessingReadUpdate = false
toast.success(`"${this.title}" Marked as ${updatePayload.isRead ? 'Read' : 'Not Read'}`)
toast.success(`"${this.title}" Marked as ${updatePayload.isFinished ? 'Read' : 'Not Read'}`)
})
.catch((error) => {
console.error('Failed', error)
this.isProcessingReadUpdate = false
toast.error(`Failed to mark as ${updatePayload.isRead ? 'Read' : 'Not Read'}`)
toast.error(`Failed to mark as ${updatePayload.isFinished ? 'Read' : 'Not Read'}`)
})
},
itemScanComplete(result) {

View file

@ -139,7 +139,18 @@ export default {
if (!this.selected) return ''
var parts = this.selected.split('.')
if (parts.length > 1) {
return this.$decode(parts[1])
var decoded = this.$decode(parts[1])
if (decoded.startsWith('aut_')) {
var author = this.authors.find((au) => au.id == decoded)
if (author) return author.name
return ''
}
if (decoded.startsWith('ser_')) {
var series = this.series.find((se) => se.id == decoded)
if (series) return series.name
return ''
}
return decoded
}
var _sel = this.items.find((i) => i.value === this.selected)
if (!_sel) return ''
@ -176,7 +187,7 @@ export default {
} else {
return {
text: item.name,
value: item.id
value: this.$encode(item.id)
}
}
})

View file

@ -38,20 +38,20 @@ export default {
},
{
text: 'Author (First Last)',
value: 'media.metadata.author'
value: 'media.metadata.authorName'
},
{
text: 'Author (Last, First)',
value: 'media.metadata.authorLF'
value: 'media.metadata.authorNameLF'
},
{
text: 'Added At',
value: 'addedAt'
},
{
text: 'Duration',
value: 'media.duration'
},
// {
// text: 'Duration',
// value: 'media.duration'
// },
{
text: 'Size',
value: 'size'

View file

@ -52,11 +52,11 @@ export default {
title: 'Files',
component: 'modals-item-tabs-files'
},
{
id: 'download',
title: 'Download',
component: 'modals-item-tabs-download'
},
// {
// id: 'download',
// title: 'Download',
// component: 'modals-item-tabs-download'
// },
{
id: 'match',
title: 'Match',

View file

@ -1,30 +1,36 @@
<template>
<div class="w-full h-full overflow-y-auto overflow-x-hidden px-4 py-6">
<div v-if="!chapters.length" class="flex my-4 text-center justify-center text-xl">No Chapters</div>
<table v-else class="text-sm tracksTable">
<tr class="font-book">
<th class="text-left w-16"><span class="px-4">Id</span></th>
<th class="text-left">Title</th>
<th class="text-center">Start</th>
<th class="text-center">End</th>
</tr>
<template v-for="chapter in chapters">
<tr :key="chapter.id">
<td class="text-left">
<p class="px-4">{{ chapter.id }}</p>
</td>
<td class="font-book">
{{ chapter.title }}
</td>
<td class="font-mono text-center">
{{ $secondsToTimestamp(chapter.start) }}
</td>
<td class="font-mono text-center">
{{ $secondsToTimestamp(chapter.end) }}
</td>
</tr>
</template>
</table>
<div v-if="!audiobooks.length" class="text-center py-8 text-lg">No Audiobooks</div>
<template v-for="audiobook in audiobooks">
<div :key="audiobook.id" class="w-full mb-4">
<div class="w-full p-4 bg-primary">
<p>Audiobook Chapters ({{ audiobook.name }})</p>
</div>
<div v-if="!audiobook.chapters.length" class="flex my-4 text-center justify-center text-xl">No Chapters</div>
<table v-else class="text-sm tracksTable">
<tr class="font-book">
<th class="text-left w-16"><span class="px-4">Id</span></th>
<th class="text-left">Title</th>
<th class="text-center">Start</th>
<th class="text-center">End</th>
</tr>
<tr v-for="chapter in audiobook.chapters" :key="chapter.id">
<td class="text-left">
<p class="px-4">{{ chapter.id }}</p>
</td>
<td class="font-book">
{{ chapter.title }}
</td>
<td class="font-mono text-center">
{{ $secondsToTimestamp(chapter.start) }}
</td>
<td class="font-mono text-center">
{{ $secondsToTimestamp(chapter.end) }}
</td>
</tr>
</table>
</div>
</template>
</div>
</template>
@ -37,23 +43,16 @@ export default {
}
},
data() {
return {
chapters: []
return {}
},
computed: {
media() {
return this.libraryItem ? this.libraryItem.media || {} : {}
},
audiobooks() {
return this.media.audiobooks || []
}
},
watch: {
libraryItem: {
immediate: true,
handler(newVal) {
if (newVal) this.init()
}
}
},
computed: {},
methods: {
init() {
this.chapters = this.libraryItem.chapters || []
}
}
methods: {}
}
</script>

View file

@ -1,47 +1,8 @@
<template>
<div class="w-full h-full overflow-y-auto overflow-x-hidden px-4 py-6">
<div class="mb-4">
<template v-if="hasTracks">
<div class="w-full bg-primary px-4 py-2 flex items-center">
<p class="pr-4">Audio Tracks</p>
<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="`/item/${libraryItem.id}/edit`">
<ui-btn small color="primary">Manage Tracks</ui-btn>
</nuxt-link>
</div>
<table class="text-sm tracksTable break-all">
<tr class="font-book">
<th class="w-16">#</th>
<th class="text-left">Filename</th>
<th class="text-left w-24 min-w-24">Size</th>
<th class="text-left w-24 min-w-24">Duration</th>
<th v-if="showDownload" class="text-center w-24 min-w-24">Download</th>
</tr>
<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.metadata.path : track.metadata.filename }}</td>
<td class="font-mono">
{{ $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/item/${libraryItem.id}/${track.metadata.relPath}?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 v-for="audiobook in audiobooks">
<tables-tracks-table :key="audiobook.id" :title="`Audiobook Tracks (${audiobook.name})`" :audiobook-id="audiobook.id" :tracks="audiobook.tracks" class="mb-4" />
</template>
<tables-library-files-table :files="libraryFiles" :library-item-id="libraryItem.id" :is-missing="isMissing" />
</div>
@ -91,8 +52,11 @@ export default {
showDownload() {
return this.userCanDownload && !this.isMissing
},
hasTracks() {
return this.tracks.length
audiobooks() {
return this.media.audiobooks || []
},
ebooks() {
return this.media.ebooks || []
}
},
methods: {

View file

@ -4,7 +4,7 @@
<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>
<div class="flex-grow" />
<nuxt-link v-if="userCanUpdate" :to="`/item/${audiobookId}/edit`" class="mr-4">
<nuxt-link v-if="userCanUpdate" :to="`/audiobook/${audiobookId}/edit`" class="mr-4">
<ui-btn small color="primary">Manage Tracks</ui-btn>
</nuxt-link>
<div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showTracks ? 'transform rotate-180' : ''">

View file

@ -1,14 +1,14 @@
<template>
<div class="w-full my-2">
<div class="w-full bg-primary px-4 md:px-6 py-2 flex items-center cursor-pointer" @click.stop="clickBar">
<p class="pr-2 md:pr-4">Audio Tracks</p>
<p class="pr-2 md:pr-4">{{ title }}</p>
<div class="h-5 md:h-7 w-5 md:w-7 rounded-full bg-white bg-opacity-10 flex items-center justify-center">
<span class="text-sm font-mono">{{ tracks.length }}</span>
</div>
<!-- <span class="bg-black-400 rounded-xl py-1 px-2 text-sm font-mono">{{ tracks.length }}</span> -->
<div class="flex-grow" />
<ui-btn small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="showFullPath = !showFullPath">Full Path</ui-btn>
<nuxt-link v-if="userCanUpdate" :to="`/item/${libraryItemId}/edit`" class="mr-2 md:mr-4">
<nuxt-link v-if="userCanUpdate" :to="`/audiobook/${audiobookId}/edit`" class="mr-2 md:mr-4" @mousedown.prevent>
<ui-btn small color="primary">Manage Tracks</ui-btn>
</nuxt-link>
<div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showTracks ? 'transform rotate-180' : ''">
@ -38,7 +38,7 @@
{{ $secondsToTimestamp(track.duration) }}
</td>
<td v-if="userCanDownload" class="text-center">
<a :href="`/s/item/${libraryItemId}${$encodeUriPath(track.metadata.relPath)}?token=${userToken}`" download><span class="material-icons icon-text">download</span></a>
<a :href="`/s/item/${audiobookId}${$encodeUriPath(track.metadata.relPath)}?token=${userToken}`" download><span class="material-icons icon-text">download</span></a>
</td>
</tr>
</template>
@ -51,11 +51,15 @@
<script>
export default {
props: {
title: {
type: String,
default: 'Audio Tracks'
},
tracks: {
type: Array,
default: () => []
},
libraryItemId: String
audiobookId: String
},
data() {
return {

View file

@ -0,0 +1,76 @@
<template>
<div class="w-full">
<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 :key="audiobook.id" :title="`Audiobook Tracks (${audiobook.name})`" :tracks="audiobook.tracks" :audiobook-id="audiobook.id" class="mt-6" />
</div>
</template>
<script>
export default {
props: {
audiobook: {
type: Object,
default: () => {}
}
},
data() {
return {}
},
computed: {
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 || []
}
},
methods: {},
mounted() {}
}
</script>