mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-01-16 10:58:16 +01:00
New data model Book media type contains Audiobooks updates
This commit is contained in:
parent
1dde02b170
commit
c4eeb1cfb7
@ -104,7 +104,7 @@ export default {
|
|||||||
shelves.push({
|
shelves.push({
|
||||||
id: 'books',
|
id: 'books',
|
||||||
label: 'Books',
|
label: 'Books',
|
||||||
type: 'books',
|
type: 'book',
|
||||||
entities: this.results.books.map((res) => res.libraryItem)
|
entities: this.results.books.map((res) => res.libraryItem)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<div class="relative">
|
<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 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 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">
|
<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" />
|
<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>
|
</template>
|
||||||
@ -108,7 +108,7 @@ export default {
|
|||||||
},
|
},
|
||||||
updateSelectionMode(val) {
|
updateSelectionMode(val) {
|
||||||
var selectedLibraryItems = this.$store.state.selectedLibraryItems
|
var selectedLibraryItems = this.$store.state.selectedLibraryItems
|
||||||
if (this.shelf.type === 'books') {
|
if (this.shelf.type === 'book') {
|
||||||
this.shelf.entities.forEach((ent) => {
|
this.shelf.entities.forEach((ent) => {
|
||||||
var component = this.$refs[`shelf-book-${ent.id}`]
|
var component = this.$refs[`shelf-book-${ent.id}`]
|
||||||
if (!component || !component.length) return
|
if (!component || !component.length) return
|
||||||
|
@ -97,7 +97,7 @@ export default {
|
|||||||
},
|
},
|
||||||
userAudiobook() {
|
userAudiobook() {
|
||||||
if (!this.libraryItemId) return
|
if (!this.libraryItemId) return
|
||||||
return this.$store.getters['user/getUserAudiobook'](this.libraryItemId)
|
return this.$store.getters['user/getUserLibraryItemProgress'](this.libraryItemId)
|
||||||
},
|
},
|
||||||
userAudiobookCurrentTime() {
|
userAudiobookCurrentTime() {
|
||||||
return this.userAudiobook ? this.userAudiobook.currentTime || 0 : 0
|
return this.userAudiobook ? this.userAudiobook.currentTime || 0 : 0
|
||||||
|
@ -35,7 +35,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- No progress shown for collapsed series in library -->
|
<!-- 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 -->
|
<!-- 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">
|
<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
|
return this.title
|
||||||
},
|
},
|
||||||
displayAuthor() {
|
displayAuthor() {
|
||||||
if (this.orderBy === 'media.metadata.authorLF') return this.authorLF
|
if (this.orderBy === 'media.metadata.authorNameLF') return this.authorLF
|
||||||
return this.author
|
return this.author
|
||||||
},
|
},
|
||||||
displaySortLine() {
|
displaySortLine() {
|
||||||
@ -226,13 +226,13 @@ export default {
|
|||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
userProgress() {
|
userProgress() {
|
||||||
return this.store.getters['user/getUserAudiobook'](this.libraryItemId)
|
return this.store.getters['user/getUserLibraryItemProgress'](this.libraryItemId)
|
||||||
},
|
},
|
||||||
userProgressPercent() {
|
userProgressPercent() {
|
||||||
return this.userProgress ? this.userProgress.progress || 0 : 0
|
return this.userProgress ? this.userProgress.progress || 0 : 0
|
||||||
},
|
},
|
||||||
userIsRead() {
|
itemIsFinished() {
|
||||||
return this.userProgress ? !!this.userProgress.isRead : false
|
return this.userProgress ? !!this.userProgress.isFinished : false
|
||||||
},
|
},
|
||||||
showError() {
|
showError() {
|
||||||
return this.hasMissingParts || this.hasInvalidParts || this.isMissing || this.isInvalid
|
return this.hasMissingParts || this.hasInvalidParts || this.isMissing || this.isInvalid
|
||||||
@ -302,7 +302,7 @@ export default {
|
|||||||
var items = [
|
var items = [
|
||||||
{
|
{
|
||||||
func: 'toggleRead',
|
func: 'toggleRead',
|
||||||
text: `Mark as ${this.userIsRead ? 'Not Read' : 'Read'}`
|
text: `Mark as ${this.itemIsFinished ? 'Not Read' : 'Read'}`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
func: 'openCollections',
|
func: 'openCollections',
|
||||||
@ -401,7 +401,7 @@ export default {
|
|||||||
toggleRead() {
|
toggleRead() {
|
||||||
// More menu func
|
// More menu func
|
||||||
var updatePayload = {
|
var updatePayload = {
|
||||||
isRead: !this.userIsRead
|
isFinished: !this.itemIsFinished
|
||||||
}
|
}
|
||||||
this.isProcessingReadUpdate = true
|
this.isProcessingReadUpdate = true
|
||||||
var toast = this.$toast || this.$nuxt.$toast
|
var toast = this.$toast || this.$nuxt.$toast
|
||||||
@ -410,12 +410,12 @@ export default {
|
|||||||
.$patch(`/api/me/audiobook/${this.libraryItemId}`, updatePayload)
|
.$patch(`/api/me/audiobook/${this.libraryItemId}`, updatePayload)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.isProcessingReadUpdate = false
|
this.isProcessingReadUpdate = false
|
||||||
toast.success(`"${this.title}" Marked as ${updatePayload.isRead ? 'Read' : 'Not Read'}`)
|
toast.success(`"${this.title}" Marked as ${updatePayload.isFinished ? 'Read' : 'Not Read'}`)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
this.isProcessingReadUpdate = false
|
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) {
|
itemScanComplete(result) {
|
||||||
|
@ -139,7 +139,18 @@ export default {
|
|||||||
if (!this.selected) return ''
|
if (!this.selected) return ''
|
||||||
var parts = this.selected.split('.')
|
var parts = this.selected.split('.')
|
||||||
if (parts.length > 1) {
|
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)
|
var _sel = this.items.find((i) => i.value === this.selected)
|
||||||
if (!_sel) return ''
|
if (!_sel) return ''
|
||||||
@ -176,7 +187,7 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
text: item.name,
|
text: item.name,
|
||||||
value: item.id
|
value: this.$encode(item.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -38,20 +38,20 @@ export default {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Author (First Last)',
|
text: 'Author (First Last)',
|
||||||
value: 'media.metadata.author'
|
value: 'media.metadata.authorName'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Author (Last, First)',
|
text: 'Author (Last, First)',
|
||||||
value: 'media.metadata.authorLF'
|
value: 'media.metadata.authorNameLF'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Added At',
|
text: 'Added At',
|
||||||
value: 'addedAt'
|
value: 'addedAt'
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
text: 'Duration',
|
// text: 'Duration',
|
||||||
value: 'media.duration'
|
// value: 'media.duration'
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
text: 'Size',
|
text: 'Size',
|
||||||
value: 'size'
|
value: 'size'
|
||||||
|
@ -52,11 +52,11 @@ export default {
|
|||||||
title: 'Files',
|
title: 'Files',
|
||||||
component: 'modals-item-tabs-files'
|
component: 'modals-item-tabs-files'
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
id: 'download',
|
// id: 'download',
|
||||||
title: 'Download',
|
// title: 'Download',
|
||||||
component: 'modals-item-tabs-download'
|
// component: 'modals-item-tabs-download'
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
id: 'match',
|
id: 'match',
|
||||||
title: 'Match',
|
title: 'Match',
|
||||||
|
@ -1,30 +1,36 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full overflow-y-auto overflow-x-hidden px-4 py-6">
|
<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>
|
<div v-if="!audiobooks.length" class="text-center py-8 text-lg">No Audiobooks</div>
|
||||||
<table v-else class="text-sm tracksTable">
|
<template v-for="audiobook in audiobooks">
|
||||||
<tr class="font-book">
|
<div :key="audiobook.id" class="w-full mb-4">
|
||||||
<th class="text-left w-16"><span class="px-4">Id</span></th>
|
<div class="w-full p-4 bg-primary">
|
||||||
<th class="text-left">Title</th>
|
<p>Audiobook Chapters ({{ audiobook.name }})</p>
|
||||||
<th class="text-center">Start</th>
|
</div>
|
||||||
<th class="text-center">End</th>
|
<div v-if="!audiobook.chapters.length" class="flex my-4 text-center justify-center text-xl">No Chapters</div>
|
||||||
</tr>
|
<table v-else class="text-sm tracksTable">
|
||||||
<template v-for="chapter in chapters">
|
<tr class="font-book">
|
||||||
<tr :key="chapter.id">
|
<th class="text-left w-16"><span class="px-4">Id</span></th>
|
||||||
<td class="text-left">
|
<th class="text-left">Title</th>
|
||||||
<p class="px-4">{{ chapter.id }}</p>
|
<th class="text-center">Start</th>
|
||||||
</td>
|
<th class="text-center">End</th>
|
||||||
<td class="font-book">
|
</tr>
|
||||||
{{ chapter.title }}
|
<tr v-for="chapter in audiobook.chapters" :key="chapter.id">
|
||||||
</td>
|
<td class="text-left">
|
||||||
<td class="font-mono text-center">
|
<p class="px-4">{{ chapter.id }}</p>
|
||||||
{{ $secondsToTimestamp(chapter.start) }}
|
</td>
|
||||||
</td>
|
<td class="font-book">
|
||||||
<td class="font-mono text-center">
|
{{ chapter.title }}
|
||||||
{{ $secondsToTimestamp(chapter.end) }}
|
</td>
|
||||||
</td>
|
<td class="font-mono text-center">
|
||||||
</tr>
|
{{ $secondsToTimestamp(chapter.start) }}
|
||||||
</template>
|
</td>
|
||||||
</table>
|
<td class="font-mono text-center">
|
||||||
|
{{ $secondsToTimestamp(chapter.end) }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -37,23 +43,16 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {}
|
||||||
chapters: []
|
},
|
||||||
|
computed: {
|
||||||
|
media() {
|
||||||
|
return this.libraryItem ? this.libraryItem.media || {} : {}
|
||||||
|
},
|
||||||
|
audiobooks() {
|
||||||
|
return this.media.audiobooks || []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
methods: {}
|
||||||
libraryItem: {
|
|
||||||
immediate: true,
|
|
||||||
handler(newVal) {
|
|
||||||
if (newVal) this.init()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {},
|
|
||||||
methods: {
|
|
||||||
init() {
|
|
||||||
this.chapters = this.libraryItem.chapters || []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
@ -1,47 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full overflow-y-auto overflow-x-hidden px-4 py-6">
|
<div class="w-full h-full overflow-y-auto overflow-x-hidden px-4 py-6">
|
||||||
<div class="mb-4">
|
<template v-for="audiobook in audiobooks">
|
||||||
<template v-if="hasTracks">
|
<tables-tracks-table :key="audiobook.id" :title="`Audiobook Tracks (${audiobook.name})`" :audiobook-id="audiobook.id" :tracks="audiobook.tracks" class="mb-4" />
|
||||||
<div class="w-full bg-primary px-4 py-2 flex items-center">
|
</template>
|
||||||
<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>
|
|
||||||
|
|
||||||
<tables-library-files-table :files="libraryFiles" :library-item-id="libraryItem.id" :is-missing="isMissing" />
|
<tables-library-files-table :files="libraryFiles" :library-item-id="libraryItem.id" :is-missing="isMissing" />
|
||||||
</div>
|
</div>
|
||||||
@ -91,8 +52,11 @@ export default {
|
|||||||
showDownload() {
|
showDownload() {
|
||||||
return this.userCanDownload && !this.isMissing
|
return this.userCanDownload && !this.isMissing
|
||||||
},
|
},
|
||||||
hasTracks() {
|
audiobooks() {
|
||||||
return this.tracks.length
|
return this.media.audiobooks || []
|
||||||
|
},
|
||||||
|
ebooks() {
|
||||||
|
return this.media.ebooks || []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -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="`/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>
|
<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' : ''">
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full my-2">
|
<div class="w-full my-2">
|
||||||
<div class="w-full bg-primary px-4 md:px-6 py-2 flex items-center cursor-pointer" @click.stop="clickBar">
|
<div class="w-full bg-primary px-4 md:px-6 py-2 flex items-center cursor-pointer" @click.stop="clickBar">
|
||||||
<p class="pr-2 md:pr-4">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">
|
<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>
|
<span class="text-sm font-mono">{{ tracks.length }}</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- <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="`/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>
|
<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' : ''">
|
||||||
@ -38,7 +38,7 @@
|
|||||||
{{ $secondsToTimestamp(track.duration) }}
|
{{ $secondsToTimestamp(track.duration) }}
|
||||||
</td>
|
</td>
|
||||||
<td v-if="userCanDownload" class="text-center">
|
<td v-if="userCanDownload" class="text-center">
|
||||||
<a :href="`/s/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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
@ -51,11 +51,15 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: 'Audio Tracks'
|
||||||
|
},
|
||||||
tracks: {
|
tracks: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
libraryItemId: String
|
audiobookId: String
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
76
client/components/widgets/AudiobookData.vue
Normal file
76
client/components/widgets/AudiobookData.vue
Normal 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>
|
@ -95,17 +95,19 @@ export default {
|
|||||||
if (!store.getters['user/getUserCanUpdate']) {
|
if (!store.getters['user/getUserCanUpdate']) {
|
||||||
return redirect('/?error=unauthorized')
|
return redirect('/?error=unauthorized')
|
||||||
}
|
}
|
||||||
var libraryItem = await app.$axios.$get(`/api/items/${params.id}?extended=1`).catch((error) => {
|
var payload = await app.$axios.$get(`/api/audiobooks/${params.id}/item?expanded=1`).catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
if (!libraryItem) {
|
if (!payload) {
|
||||||
console.error('No item...', params.id)
|
console.error('Not found...', params.id)
|
||||||
return redirect('/')
|
return redirect('/')
|
||||||
}
|
}
|
||||||
|
const audiobook = payload.audiobook
|
||||||
return {
|
return {
|
||||||
libraryItem,
|
audiobook,
|
||||||
files: libraryItem.media.audioFiles ? libraryItem.media.audioFiles.map((af) => ({ ...af, include: !af.exclude })) : []
|
libraryItem: payload.libraryItem,
|
||||||
|
files: audiobook.audioFiles ? audiobook.audioFiles.map((af) => ({ ...af, include: !af.exclude })) : []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@ -128,7 +130,7 @@ export default {
|
|||||||
return this.media.metadata || []
|
return this.media.metadata || []
|
||||||
},
|
},
|
||||||
audioFiles() {
|
audioFiles() {
|
||||||
return this.media.audioFiles || []
|
return this.audiobook.audioFiles || []
|
||||||
},
|
},
|
||||||
numExcluded() {
|
numExcluded() {
|
||||||
var count = 0
|
var count = 0
|
||||||
@ -138,7 +140,7 @@ export default {
|
|||||||
return count
|
return count
|
||||||
},
|
},
|
||||||
missingParts() {
|
missingParts() {
|
||||||
return this.media.missingParts || []
|
return this.audiobook.missingParts || []
|
||||||
},
|
},
|
||||||
libraryItemId() {
|
libraryItemId() {
|
||||||
return this.libraryItem.id
|
return this.libraryItem.id
|
||||||
@ -150,7 +152,7 @@ export default {
|
|||||||
return this.mediaMetadata.authorName || 'Unknown'
|
return this.mediaMetadata.authorName || 'Unknown'
|
||||||
},
|
},
|
||||||
tracks() {
|
tracks() {
|
||||||
return this.media.tracks
|
return this.audiobook.tracks
|
||||||
},
|
},
|
||||||
streamLibraryItem() {
|
streamLibraryItem() {
|
||||||
return this.$store.state.streamLibraryItem
|
return this.$store.state.streamLibraryItem
|
||||||
@ -216,7 +218,7 @@ export default {
|
|||||||
|
|
||||||
this.saving = true
|
this.saving = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$patch(`/api/items/${this.libraryItemId}/tracks`, { orderedFileData })
|
.$patch(`/api/audiobooks/${this.audiobook.id}/tracks`, { orderedFileData })
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
console.log('Finished patching files', data)
|
console.log('Finished patching files', data)
|
||||||
this.saving = false
|
this.saving = false
|
@ -117,7 +117,7 @@
|
|||||||
{{ isMissing ? 'Missing' : 'Incomplete' }}
|
{{ isMissing ? 'Missing' : 'Incomplete' }}
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
|
|
||||||
<ui-btn v-if="showExperimentalFeatures && numEbooks" color="info" :padding-x="4" small class="flex items-center h-9 mr-2" @click="openEbook">
|
<ui-btn v-if="showExperimentalFeatures && ebooks.length" 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>
|
<span class="material-icons -ml-2 pr-2 text-white">auto_stories</span>
|
||||||
Read
|
Read
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
@ -143,26 +143,9 @@
|
|||||||
<p class="text-base text-gray-100 whitespace-pre-line">{{ description }}</p>
|
<p class="text-base text-gray-100 whitespace-pre-line">{{ description }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="missingParts.length" class="bg-error border-red-800 shadow-md p-4">
|
<template v-for="audiobook in audiobooks">
|
||||||
<p class="text-sm mb-2">
|
<widgets-audiobook-data :key="audiobook.id" :audiobook="audiobook" />
|
||||||
Missing Parts <span class="text-sm">({{ missingParts.length }})</span>
|
</template>
|
||||||
</p>
|
|
||||||
<p class="text-sm font-mono">{{ missingPartChunks.join(', ') }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="invalidParts.length" class="bg-error border-red-800 shadow-md p-4">
|
|
||||||
<p class="text-sm mb-2">
|
|
||||||
Invalid Parts <span class="text-sm">({{ invalidParts.length }})</span>
|
|
||||||
</p>
|
|
||||||
<div>
|
|
||||||
<p v-for="part in invalidParts" :key="part.filename" class="text-sm font-mono">{{ part.filename }}: {{ part.error }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<tables-tracks-table v-if="tracks.length" :tracks="tracks" :library-item-id="libraryItemId" class="mt-6" />
|
|
||||||
|
|
||||||
<!-- <tables-audio-files-table v-if="otherAudioFiles.length" :library-item-id="libraryItemId" :files="otherAudioFiles" class="mt-6" /> -->
|
|
||||||
|
|
||||||
<tables-library-files-table v-if="libraryFiles.length" :is-missing="isMissing" :library-item-id="libraryItemId" :files="libraryFiles" class="mt-6" />
|
<tables-library-files-table v-if="libraryFiles.length" :is-missing="isMissing" :library-item-id="libraryItemId" :files="libraryFiles" class="mt-6" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -219,37 +202,6 @@ export default {
|
|||||||
showExperimentalFeatures() {
|
showExperimentalFeatures() {
|
||||||
return this.$store.state.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() {
|
isMissing() {
|
||||||
return this.libraryItem.isMissing
|
return this.libraryItem.isMissing
|
||||||
},
|
},
|
||||||
@ -257,13 +209,7 @@ export default {
|
|||||||
return this.libraryItem.isInvalid
|
return this.libraryItem.isInvalid
|
||||||
},
|
},
|
||||||
showPlayButton() {
|
showPlayButton() {
|
||||||
return !this.isMissing && !this.isInvalid && this.tracks.length
|
return !this.isMissing && !this.isInvalid && this.audiobooks.length
|
||||||
},
|
|
||||||
missingParts() {
|
|
||||||
return this.libraryItem.missingParts || []
|
|
||||||
},
|
|
||||||
invalidParts() {
|
|
||||||
return this.libraryItem.invalidParts || []
|
|
||||||
},
|
},
|
||||||
libraryId() {
|
libraryId() {
|
||||||
return this.libraryItem.libraryId
|
return this.libraryItem.libraryId
|
||||||
@ -280,6 +226,9 @@ export default {
|
|||||||
mediaMetadata() {
|
mediaMetadata() {
|
||||||
return this.media.metadata || {}
|
return this.media.metadata || {}
|
||||||
},
|
},
|
||||||
|
audiobooks() {
|
||||||
|
return this.media.audiobooks || []
|
||||||
|
},
|
||||||
title() {
|
title() {
|
||||||
return this.mediaMetadata.title || 'No Title'
|
return this.mediaMetadata.title || 'No Title'
|
||||||
},
|
},
|
||||||
@ -341,14 +290,11 @@ export default {
|
|||||||
return this.media.audioFiles || []
|
return this.media.audioFiles || []
|
||||||
},
|
},
|
||||||
ebooks() {
|
ebooks() {
|
||||||
return this.media.ebookFiles
|
return this.media.ebooks || []
|
||||||
},
|
},
|
||||||
showExperimentalReadAlert() {
|
showExperimentalReadAlert() {
|
||||||
return !this.tracks.length && this.ebooks.length && !this.showExperimentalFeatures
|
return !this.tracks.length && this.ebooks.length && !this.showExperimentalFeatures
|
||||||
},
|
},
|
||||||
numEbooks() {
|
|
||||||
return this.media.numEbooks
|
|
||||||
},
|
|
||||||
description() {
|
description() {
|
||||||
return this.mediaMetadata.description || ''
|
return this.mediaMetadata.description || ''
|
||||||
},
|
},
|
||||||
|
@ -22,8 +22,9 @@ export const getters = {
|
|||||||
getToken: (state) => {
|
getToken: (state) => {
|
||||||
return state.user ? state.user.token : null
|
return state.user ? state.user.token : null
|
||||||
},
|
},
|
||||||
getUserAudiobook: (state) => (audiobookId) => {
|
getUserLibraryItemProgress: (state) => (libraryItemId) => {
|
||||||
return state.user && state.user.audiobooks ? state.user.audiobooks[audiobookId] || null : null
|
if (!state.user.libraryItemProgress) return null
|
||||||
|
return state.user.libraryItemProgress.find(li => li.id == libraryItemId)
|
||||||
},
|
},
|
||||||
getUserSetting: (state) => (key) => {
|
getUserSetting: (state) => (key) => {
|
||||||
return state.settings ? state.settings[key] : null
|
return state.settings ? state.settings[key] : null
|
||||||
|
@ -16,6 +16,7 @@ const BackupController = require('./controllers/BackupController')
|
|||||||
const LibraryItemController = require('./controllers/LibraryItemController')
|
const LibraryItemController = require('./controllers/LibraryItemController')
|
||||||
const SeriesController = require('./controllers/SeriesController')
|
const SeriesController = require('./controllers/SeriesController')
|
||||||
const AuthorController = require('./controllers/AuthorController')
|
const AuthorController = require('./controllers/AuthorController')
|
||||||
|
const AudiobookController = require('./controllers/AudiobookController')
|
||||||
|
|
||||||
const BookFinder = require('./finders/BookFinder')
|
const BookFinder = require('./finders/BookFinder')
|
||||||
const AuthorFinder = require('./finders/AuthorFinder')
|
const AuthorFinder = require('./finders/AuthorFinder')
|
||||||
@ -70,6 +71,13 @@ class ApiController {
|
|||||||
|
|
||||||
this.router.post('/libraries/order', LibraryController.reorder.bind(this))
|
this.router.post('/libraries/order', LibraryController.reorder.bind(this))
|
||||||
|
|
||||||
|
//
|
||||||
|
// Audiobook Routes
|
||||||
|
//
|
||||||
|
this.router.get('/audiobooks/:id', AudiobookController.middleware.bind(this), AudiobookController.findOne.bind(this))
|
||||||
|
this.router.get('/audiobooks/:id/item', AudiobookController.middleware.bind(this), AudiobookController.findWithItem.bind(this))
|
||||||
|
this.router.patch('/audiobooks/:id/tracks', AudiobookController.middleware.bind(this), AudiobookController.updateTracks.bind(this))
|
||||||
|
|
||||||
//
|
//
|
||||||
// Item Routes
|
// Item Routes
|
||||||
//
|
//
|
||||||
@ -84,7 +92,6 @@ 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.post('/items/:id/match', LibraryItemController.middleware.bind(this), LibraryItemController.match.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.get('/items/:id/play', LibraryItemController.middleware.bind(this), LibraryItemController.startPlaybackSession.bind(this))
|
this.router.get('/items/:id/play', LibraryItemController.middleware.bind(this), LibraryItemController.startPlaybackSession.bind(this))
|
||||||
|
|
||||||
this.router.post('/items/batch/delete', LibraryItemController.batchDelete.bind(this))
|
this.router.post('/items/batch/delete', LibraryItemController.batchDelete.bind(this))
|
||||||
|
@ -36,6 +36,10 @@ class CacheManager {
|
|||||||
// Write cache
|
// Write cache
|
||||||
await fs.ensureDir(this.CoverCachePath)
|
await fs.ensureDir(this.CoverCachePath)
|
||||||
|
|
||||||
|
if (!libraryItem.media.coverPath || !await fs.pathExists(libraryItem.media.coverPath)) {
|
||||||
|
return res.sendStatus(404)
|
||||||
|
}
|
||||||
|
|
||||||
let writtenFile = await resizeImage(libraryItem.media.coverPath, path, width, height)
|
let writtenFile = await resizeImage(libraryItem.media.coverPath, path, width, height)
|
||||||
if (!writtenFile) return res.sendStatus(400)
|
if (!writtenFile) return res.sendStatus(400)
|
||||||
|
|
||||||
|
@ -219,7 +219,7 @@ class Server {
|
|||||||
|
|
||||||
// Client dynamic routes
|
// Client dynamic routes
|
||||||
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('/audiobook/:id/edit', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
||||||
app.get('/library/:library', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
app.get('/library/:library', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
||||||
app.get('/library/:library/search', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
app.get('/library/:library/search', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
||||||
app.get('/library/:library/bookshelf/:id?', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
app.get('/library/:library/bookshelf/:id?', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
||||||
|
57
server/controllers/AudiobookController.js
Normal file
57
server/controllers/AudiobookController.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
|
class AudiobookController {
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
async findOne(req, res) {
|
||||||
|
if (req.query.expanded == 1) return res.json(req.audiobook.toJSONExpanded())
|
||||||
|
return res.json(req.audiobook)
|
||||||
|
}
|
||||||
|
|
||||||
|
async findWithItem(req, res) {
|
||||||
|
if (req.query.expanded == 1) {
|
||||||
|
return res.json({
|
||||||
|
libraryItem: req.libraryItem.toJSONExpanded(),
|
||||||
|
audiobook: req.audiobook.toJSONExpanded()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
res.json({
|
||||||
|
libraryItem: req.libraryItem.toJSON(),
|
||||||
|
audiobook: req.audiobook.toJSON()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// PATCH: api/audiobooks/:id/tracks
|
||||||
|
async updateTracks(req, res) {
|
||||||
|
var libraryItem = req.libraryItem
|
||||||
|
var audiobook = req.audiobook
|
||||||
|
var orderedFileData = req.body.orderedFileData
|
||||||
|
audiobook.updateAudioTracks(orderedFileData)
|
||||||
|
await this.db.updateLibraryItem(libraryItem)
|
||||||
|
this.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||||
|
res.json(libraryItem.toJSON())
|
||||||
|
}
|
||||||
|
|
||||||
|
middleware(req, res, next) {
|
||||||
|
var audiobook = null
|
||||||
|
var libraryItem = this.db.libraryItems.find(li => {
|
||||||
|
if (li.mediaType != 'book') return false
|
||||||
|
audiobook = li.media.getAudiobookById(req.params.id)
|
||||||
|
return !!audiobook
|
||||||
|
})
|
||||||
|
if (!audiobook) return res.sendStatus(404)
|
||||||
|
|
||||||
|
if (req.method == 'DELETE' && !req.user.canDelete) {
|
||||||
|
Logger.warn(`[AudiobookController] User attempted to delete without permission`, req.user)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
} else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) {
|
||||||
|
Logger.warn('[AudiobookController] User attempted to update without permission', req.user)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.libraryItem = libraryItem
|
||||||
|
req.audiobook = audiobook
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = new AudiobookController()
|
@ -146,7 +146,7 @@ class LibraryController {
|
|||||||
if (payload.sortBy) {
|
if (payload.sortBy) {
|
||||||
var sortKey = payload.sortBy
|
var sortKey = payload.sortBy
|
||||||
|
|
||||||
// old sort key
|
// old sort key TODO: should be mutated in dbMigration
|
||||||
if (sortKey.startsWith('book.')) {
|
if (sortKey.startsWith('book.')) {
|
||||||
sortKey = sortKey.replace('book.', 'media.metadata.')
|
sortKey = sortKey.replace('book.', 'media.metadata.')
|
||||||
}
|
}
|
||||||
@ -263,26 +263,26 @@ class LibraryController {
|
|||||||
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
|
||||||
var minified = req.query.minified === '1'
|
var minified = req.query.minified === '1'
|
||||||
|
|
||||||
var booksWithUserAb = libraryHelpers.getBooksWithUserAudiobook(req.user, libraryItems)
|
var itemsWithUserProgress = libraryHelpers.getItemsWithUserProgress(req.user, libraryItems)
|
||||||
|
|
||||||
var categories = [
|
var categories = [
|
||||||
{
|
{
|
||||||
id: 'continue-reading',
|
id: 'continue-reading',
|
||||||
label: 'Continue Reading',
|
label: 'Continue Reading',
|
||||||
type: 'books',
|
type: req.library.mediaType,
|
||||||
entities: libraryHelpers.getBooksMostRecentlyRead(booksWithUserAb, limitPerShelf, minified)
|
entities: libraryHelpers.getItemsMostRecentlyListened(itemsWithUserProgress, limitPerShelf, minified)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'recently-added',
|
id: 'recently-added',
|
||||||
label: 'Recently Added',
|
label: 'Recently Added',
|
||||||
type: 'books',
|
type: req.library.mediaType,
|
||||||
entities: libraryHelpers.getBooksMostRecentlyAdded(libraryItems, limitPerShelf, minified)
|
entities: libraryHelpers.getItemsMostRecentlyAdded(libraryItems, limitPerShelf, minified)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'read-again',
|
id: 'read-again',
|
||||||
label: 'Read Again',
|
label: 'Read Again',
|
||||||
type: 'books',
|
type: req.library.mediaType,
|
||||||
entities: libraryHelpers.getBooksMostRecentlyFinished(booksWithUserAb, limitPerShelf, minified)
|
entities: libraryHelpers.getItemsMostRecentlyFinished(itemsWithUserProgress, limitPerShelf, minified)
|
||||||
}
|
}
|
||||||
].filter(cats => { // Remove categories with no items
|
].filter(cats => { // Remove categories with no items
|
||||||
return cats.entities.length
|
return cats.entities.length
|
||||||
@ -299,7 +299,7 @@ class LibraryController {
|
|||||||
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
|
||||||
var minified = req.query.minified === '1'
|
var minified = req.query.minified === '1'
|
||||||
|
|
||||||
var booksWithUserAb = libraryHelpers.getBooksWithUserAudiobook(req.user, books)
|
var booksWithUserAb = libraryHelpers.getItemsWithUserProgress(req.user, books)
|
||||||
var series = libraryHelpers.getSeriesFromBooks(books, minified)
|
var series = libraryHelpers.getSeriesFromBooks(books, minified)
|
||||||
var seriesWithUserAb = libraryHelpers.getSeriesWithProgressFromBooks(req.user, books)
|
var seriesWithUserAb = libraryHelpers.getSeriesWithProgressFromBooks(req.user, books)
|
||||||
|
|
||||||
|
@ -156,17 +156,6 @@ class LibraryItemController {
|
|||||||
res.json(matchResult)
|
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) {
|
||||||
|
@ -106,7 +106,8 @@ class LibraryItem {
|
|||||||
isInvalid: !!this.isInvalid,
|
isInvalid: !!this.isInvalid,
|
||||||
mediaType: this.mediaType,
|
mediaType: this.mediaType,
|
||||||
media: this.media.toJSONMinified(),
|
media: this.media.toJSONMinified(),
|
||||||
numFiles: this.libraryFiles.length
|
numFiles: this.libraryFiles.length,
|
||||||
|
size: this.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,7 +137,7 @@ class Book {
|
|||||||
return hasUpdated
|
return hasUpdated
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
var { authorLF, authorFL } = parseAuthors(author)
|
var { authorLF, authorFL } = parseAuthors.parse(author)
|
||||||
var hasUpdated = authorLF !== this.authorLF || authorFL !== this.authorFL
|
var hasUpdated = authorLF !== this.authorLF || authorFL !== this.authorFL
|
||||||
this.authorFL = authorFL || null
|
this.authorFL = authorFL || null
|
||||||
this.authorLF = authorLF || null
|
this.authorLF = authorLF || null
|
||||||
@ -155,7 +155,7 @@ class Book {
|
|||||||
return hasUpdated
|
return hasUpdated
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
var { authorFL } = parseAuthors(narrator)
|
var { authorFL } = parseAuthors.parse(narrator)
|
||||||
var hasUpdated = authorFL !== this.narratorFL
|
var hasUpdated = authorFL !== this.narratorFL
|
||||||
this.narratorFL = authorFL || null
|
this.narratorFL = authorFL || null
|
||||||
return hasUpdated
|
return hasUpdated
|
||||||
|
@ -116,6 +116,10 @@ class Book {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAudiobookById(audiobookId) {
|
||||||
|
return this.audiobooks.find(ab => ab.id === audiobookId)
|
||||||
|
}
|
||||||
|
|
||||||
removeFileWithInode(inode) {
|
removeFileWithInode(inode) {
|
||||||
var audiobookWithIno = this.audiobooks.find(ab => ab.findFileWithInode(inode))
|
var audiobookWithIno = this.audiobooks.find(ab => ab.findFileWithInode(inode))
|
||||||
if (audiobookWithIno) {
|
if (audiobookWithIno) {
|
||||||
|
@ -76,6 +76,7 @@ class BookMetadata {
|
|||||||
language: this.language,
|
language: this.language,
|
||||||
explicit: this.explicit,
|
explicit: this.explicit,
|
||||||
authorName: this.authorName,
|
authorName: this.authorName,
|
||||||
|
authorNameLF: this.authorNameLF,
|
||||||
narratorName: this.narratorName
|
narratorName: this.narratorName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -95,6 +96,10 @@ class BookMetadata {
|
|||||||
if (!this.authors.length) return ''
|
if (!this.authors.length) return ''
|
||||||
return this.authors.map(au => au.name).join(', ')
|
return this.authors.map(au => au.name).join(', ')
|
||||||
}
|
}
|
||||||
|
get authorNameLF() { // Last, First
|
||||||
|
if (!this.authors.length) return ''
|
||||||
|
return this.authors.map(au => parseNameString.nameToLastFirst(au.name)).join(', ')
|
||||||
|
}
|
||||||
get seriesName() {
|
get seriesName() {
|
||||||
if (!this.series.length) return ''
|
if (!this.series.length) return ''
|
||||||
return this.series.map(se => {
|
return this.series.map(se => {
|
||||||
@ -243,13 +248,13 @@ class BookMetadata {
|
|||||||
|
|
||||||
// Returns array of names in First Last format
|
// Returns array of names in First Last format
|
||||||
parseNarratorsTag(narratorsTag) {
|
parseNarratorsTag(narratorsTag) {
|
||||||
var parsed = parseNameString(narratorsTag)
|
var parsed = parseNameString.parse(narratorsTag)
|
||||||
return parsed ? parsed.names : []
|
return parsed ? parsed.names : []
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return array of authors minified with placeholder id
|
// Return array of authors minified with placeholder id
|
||||||
parseAuthorsTag(authorsTag) {
|
parseAuthorsTag(authorsTag) {
|
||||||
var parsed = parseNameString(authorsTag)
|
var parsed = parseNameString.parse(authorsTag)
|
||||||
if (!parsed) return []
|
if (!parsed) return []
|
||||||
return (parsed.names || []).map((au) => {
|
return (parsed.names || []).map((au) => {
|
||||||
return {
|
return {
|
||||||
|
@ -3,12 +3,12 @@ const Logger = require('../../Logger')
|
|||||||
class LibraryItemProgress {
|
class LibraryItemProgress {
|
||||||
constructor(progress) {
|
constructor(progress) {
|
||||||
this.id = null // Same as library item id
|
this.id = null // Same as library item id
|
||||||
this.libararyItemId = null
|
this.libraryItemId = null
|
||||||
|
|
||||||
this.totalDuration = null // seconds
|
this.totalDuration = null // seconds
|
||||||
this.progress = null // 0 to 1
|
this.progress = null // 0 to 1
|
||||||
this.currentTime = null // seconds
|
this.currentTime = null // seconds
|
||||||
this.isRead = false
|
this.isFinished = false
|
||||||
|
|
||||||
this.lastUpdate = null
|
this.lastUpdate = null
|
||||||
this.startedAt = null
|
this.startedAt = null
|
||||||
@ -22,11 +22,11 @@ class LibraryItemProgress {
|
|||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
libararyItemId: this.libararyItemId,
|
libraryItemId: this.libraryItemId,
|
||||||
totalDuration: this.totalDuration,
|
totalDuration: this.totalDuration,
|
||||||
progress: this.progress,
|
progress: this.progress,
|
||||||
currentTime: this.currentTime,
|
currentTime: this.currentTime,
|
||||||
isRead: this.isRead,
|
isFinished: this.isFinished,
|
||||||
lastUpdate: this.lastUpdate,
|
lastUpdate: this.lastUpdate,
|
||||||
startedAt: this.startedAt,
|
startedAt: this.startedAt,
|
||||||
finishedAt: this.finishedAt
|
finishedAt: this.finishedAt
|
||||||
@ -35,11 +35,11 @@ class LibraryItemProgress {
|
|||||||
|
|
||||||
construct(progress) {
|
construct(progress) {
|
||||||
this.id = progress.id
|
this.id = progress.id
|
||||||
this.libararyItemId = progress.libararyItemId
|
this.libraryItemId = progress.libraryItemId
|
||||||
this.totalDuration = progress.totalDuration
|
this.totalDuration = progress.totalDuration
|
||||||
this.progress = progress.progress
|
this.progress = progress.progress
|
||||||
this.currentTime = progress.currentTime
|
this.currentTime = progress.currentTime
|
||||||
this.isRead = !!progress.isRead
|
this.isFinished = !!progress.isFinished
|
||||||
this.lastUpdate = progress.lastUpdate
|
this.lastUpdate = progress.lastUpdate
|
||||||
this.startedAt = progress.startedAt
|
this.startedAt = progress.startedAt
|
||||||
this.finishedAt = progress.finishedAt || null
|
this.finishedAt = progress.finishedAt || null
|
||||||
@ -59,11 +59,11 @@ class LibraryItemProgress {
|
|||||||
// If has < 10 seconds remaining mark as read
|
// If has < 10 seconds remaining mark as read
|
||||||
var timeRemaining = this.totalDuration - this.currentTime
|
var timeRemaining = this.totalDuration - this.currentTime
|
||||||
if (timeRemaining < 10) {
|
if (timeRemaining < 10) {
|
||||||
this.isRead = true
|
this.isFinished = true
|
||||||
this.progress = 1
|
this.progress = 1
|
||||||
this.finishedAt = Date.now()
|
this.finishedAt = Date.now()
|
||||||
} else {
|
} else {
|
||||||
this.isRead = false
|
this.isFinished = false
|
||||||
this.finishedAt = null
|
this.finishedAt = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -72,7 +72,7 @@ class LibraryItemProgress {
|
|||||||
var hasUpdates = false
|
var hasUpdates = false
|
||||||
for (const key in payload) {
|
for (const key in payload) {
|
||||||
if (this[key] !== undefined && payload[key] !== this[key]) {
|
if (this[key] !== undefined && payload[key] !== this[key]) {
|
||||||
if (key === 'isRead') {
|
if (key === 'isFinished') {
|
||||||
if (!payload[key]) { // Updating to Not Read - Reset progress and current time
|
if (!payload[key]) { // Updating to Not Read - Reset progress and current time
|
||||||
this.finishedAt = null
|
this.finishedAt = null
|
||||||
this.progress = 0
|
this.progress = 0
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
const Logger = require('../../Logger')
|
const Logger = require('../../Logger')
|
||||||
const { isObject } = require('../../utils')
|
|
||||||
const AudioBookmark = require('./AudioBookmark')
|
const AudioBookmark = require('./AudioBookmark')
|
||||||
const LibraryItemProgress = require('./LibraryItemProgress')
|
const LibraryItemProgress = require('./LibraryItemProgress')
|
||||||
|
|
||||||
|
@ -245,6 +245,27 @@ async function migrateLibraryItems(db) {
|
|||||||
|
|
||||||
var libraryItems = audiobooks.map((ab) => makeLibraryItemFromOldAb(ab))
|
var libraryItems = audiobooks.map((ab) => makeLibraryItemFromOldAb(ab))
|
||||||
|
|
||||||
|
// User library item progress was using the auidobook ID when migrated
|
||||||
|
// now that library items are created the LibraryItemProgress objects
|
||||||
|
// need the library item id to be set
|
||||||
|
for (const user of db.users) {
|
||||||
|
if (user.libraryItemProgress.length) {
|
||||||
|
user.libraryItemProgress = user.libraryItemProgress.map(lip => {
|
||||||
|
var audiobookId = lip.id
|
||||||
|
var libraryItemWithAudiobook = libraryItems.find(li => li.media.getAudiobookById && !!li.media.getAudiobookById(audiobookId))
|
||||||
|
if (!libraryItemWithAudiobook) {
|
||||||
|
Logger.error('[dbMigration] Failed to find library item with audiobook id', audiobookId)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
lip.id = libraryItemWithAudiobook.id
|
||||||
|
lip.libraryItemId = libraryItemWithAudiobook.id
|
||||||
|
return lip
|
||||||
|
}).filter(lip => !!lip)
|
||||||
|
await db.updateEntity('user', user)
|
||||||
|
Logger.debug(`>>> User ${user.username} with ${user.libraryItemProgress.length} progress entries were updated`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Logger.info(`>>> ${libraryItems.length} Library Items made`)
|
Logger.info(`>>> ${libraryItems.length} Library Items made`)
|
||||||
await db.insertEntities('libraryItem', libraryItems)
|
await db.insertEntities('libraryItem', libraryItems)
|
||||||
if (authorsToAdd.length) {
|
if (authorsToAdd.length) {
|
||||||
@ -286,8 +307,9 @@ function cleanUserObject(db, userObj) {
|
|||||||
|
|
||||||
var userAudiobookData = new UserAudiobookData(userObj.audiobooks[audiobookId]) // Legacy object
|
var userAudiobookData = new UserAudiobookData(userObj.audiobooks[audiobookId]) // Legacy object
|
||||||
var liProgress = new LibraryItemProgress() // New Progress Object
|
var liProgress = new LibraryItemProgress() // New Progress Object
|
||||||
liProgress.id = userAudiobookData.audiobookId
|
liProgress.id = userAudiobookData.audiobookId // This ID is INCORRECT, will be updated when library item is created
|
||||||
liProgress.libraryItemId = userAudiobookData.audiobookId
|
liProgress.libraryItemId = userAudiobookData.audiobookId
|
||||||
|
liProgress.isFinished = !!userAudiobookData.isRead
|
||||||
Object.keys(liProgress.toJSON()).forEach((key) => {
|
Object.keys(liProgress.toJSON()).forEach((key) => {
|
||||||
if (userAudiobookData[key] !== undefined) {
|
if (userAudiobookData[key] !== undefined) {
|
||||||
liProgress[key] = userAudiobookData[key]
|
liProgress[key] = userAudiobookData[key]
|
||||||
|
@ -194,21 +194,21 @@ module.exports = {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
getBooksWithUserAudiobook(user, books) {
|
getItemsWithUserProgress(user, libraryItems) {
|
||||||
return books.map(book => {
|
return libraryItems.map(li => {
|
||||||
return {
|
return {
|
||||||
userAudiobook: user.getLibraryItemProgress(book.id),
|
userProgress: user.getLibraryItemProgress(li.id),
|
||||||
book
|
libraryItem: li
|
||||||
}
|
}
|
||||||
}).filter(b => !!b.userAudiobook)
|
}).filter(b => !!b.userProgress)
|
||||||
},
|
},
|
||||||
|
|
||||||
getBooksMostRecentlyRead(booksWithUserAb, limit, minified = false) {
|
getItemsMostRecentlyListened(itemsWithUserProgress, limit, minified = false) {
|
||||||
var booksWithProgress = booksWithUserAb.filter((data) => data.userAudiobook && data.userAudiobook.progress > 0 && !data.userAudiobook.isRead)
|
var itemsInProgress = itemsWithUserProgress.filter((data) => data.userProgress && data.userProgress.progress > 0 && !data.userProgress.isFinished)
|
||||||
booksWithProgress.sort((a, b) => {
|
itemsInProgress.sort((a, b) => {
|
||||||
return b.userAudiobook.lastUpdate - a.userAudiobook.lastUpdate
|
return b.userProgress.lastUpdate - a.userProgress.lastUpdate
|
||||||
})
|
})
|
||||||
return booksWithProgress.map(b => minified ? b.book.toJSONMinified() : b.book.toJSONExpanded()).slice(0, limit)
|
return itemsInProgress.map(b => minified ? b.libraryItem.toJSONMinified() : b.libraryItem.toJSONExpanded()).slice(0, limit)
|
||||||
},
|
},
|
||||||
|
|
||||||
getBooksNextInSeries(seriesWithUserAb, limit, minified = false) {
|
getBooksNextInSeries(seriesWithUserAb, limit, minified = false) {
|
||||||
@ -223,17 +223,17 @@ module.exports = {
|
|||||||
return booksNextInSeries.sort((a, b) => { return b.DateLastReadSeries - a.DateLastReadSeries }).map(b => minified ? b.book.toJSONMinified() : b.book.toJSONExpanded()).slice(0, limit)
|
return booksNextInSeries.sort((a, b) => { return b.DateLastReadSeries - a.DateLastReadSeries }).map(b => minified ? b.book.toJSONMinified() : b.book.toJSONExpanded()).slice(0, limit)
|
||||||
},
|
},
|
||||||
|
|
||||||
getBooksMostRecentlyAdded(books, limit, minified = false) {
|
getItemsMostRecentlyAdded(libraryItems, limit, minified = false) {
|
||||||
var booksSortedByAddedAt = sort(books).desc(book => book.addedAt)
|
var itemsSortedByAddedAt = sort(libraryItems).desc(li => li.addedAt)
|
||||||
return booksSortedByAddedAt.map(b => minified ? b.toJSONMinified() : b.toJSONExpanded()).slice(0, limit)
|
return itemsSortedByAddedAt.map(b => minified ? b.toJSONMinified() : b.toJSONExpanded()).slice(0, limit)
|
||||||
},
|
},
|
||||||
|
|
||||||
getBooksMostRecentlyFinished(booksWithUserAb, limit, minified = false) {
|
getItemsMostRecentlyFinished(itemsWithUserProgress, limit, minified = false) {
|
||||||
var booksRead = booksWithUserAb.filter((data) => data.userAudiobook && data.userAudiobook.isRead)
|
var itemsFinished = itemsWithUserProgress.filter((data) => data.userProgress && data.userProgress.isFinished)
|
||||||
booksRead.sort((a, b) => {
|
itemsFinished.sort((a, b) => {
|
||||||
return b.userAudiobook.finishedAt - a.userAudiobook.finishedAt
|
return b.userProgress.finishedAt - a.userProgress.finishedAt
|
||||||
})
|
})
|
||||||
return booksRead.map(b => minified ? b.book.toJSONMinified() : b.book.toJSONExpanded()).slice(0, limit)
|
return itemsFinished.map(i => minified ? i.libraryItem.toJSONMinified() : i.libraryItem.toJSONExpanded()).slice(0, limit)
|
||||||
},
|
},
|
||||||
|
|
||||||
getSeriesMostRecentlyAdded(series, limit) {
|
getSeriesMostRecentlyAdded(series, limit) {
|
||||||
|
@ -27,7 +27,16 @@ function checkIsALastName(name) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = (nameString) => {
|
// Handle name already in First Last format and return Last, First
|
||||||
|
module.exports.nameToLastFirst = (firstLast) => {
|
||||||
|
var nameObj = parseName(firstLast)
|
||||||
|
if (!nameObj.last_name) return nameObj.first_name
|
||||||
|
else if (!nameObj.first_name) return nameObj.last_name
|
||||||
|
return `${nameObj.last_name}, ${nameObj.first_name}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle any name string
|
||||||
|
module.exports.parse = (nameString) => {
|
||||||
if (!nameString) return null
|
if (!nameString) return null
|
||||||
|
|
||||||
var splitNames = []
|
var splitNames = []
|
||||||
|
Loading…
Reference in New Issue
Block a user