mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-01-26 21:09:38 +00:00
New data model play media entity, PlaybackSessionManager
This commit is contained in:
parent
1cf9e85272
commit
099ae7c776
54 changed files with 841 additions and 902 deletions
|
|
@ -175,8 +175,8 @@ export default {
|
|||
})
|
||||
},
|
||||
batchDeleteClick() {
|
||||
var audiobookText = this.numLibraryItemsSelected > 1 ? `these ${this.numLibraryItemsSelected} audiobooks` : 'this audiobook'
|
||||
var confirmMsg = `Are you sure you want to remove ${audiobookText}?\n\n*Does not delete your files, only removes the audiobooks from AudioBookshelf`
|
||||
var audiobookText = this.numLibraryItemsSelected > 1 ? `these ${this.numLibraryItemsSelected} items` : 'this item'
|
||||
var confirmMsg = `Are you sure you want to remove ${audiobookText}?\n\n*Does not delete your files, only removes the items from Audiobookshelf`
|
||||
if (confirm(confirmMsg)) {
|
||||
this.processingBatchDelete = true
|
||||
this.$store.commit('setProcessingBatch', true)
|
||||
|
|
|
|||
|
|
@ -300,11 +300,11 @@ export default {
|
|||
var firstBookPage = Math.floor(firstBookIndex / this.booksPerFetch)
|
||||
var lastBookPage = Math.floor(lastBookIndex / this.booksPerFetch)
|
||||
if (!this.pagesLoaded[firstBookPage]) {
|
||||
console.log('Must load next batch', firstBookPage, 'book index', firstBookIndex)
|
||||
// console.log('Must load next batch', firstBookPage, 'book index', firstBookIndex)
|
||||
this.loadPage(firstBookPage)
|
||||
}
|
||||
if (!this.pagesLoaded[lastBookPage]) {
|
||||
console.log('Must load last next batch', lastBookPage, 'book index', lastBookIndex)
|
||||
// console.log('Must load last next batch', lastBookPage, 'book index', lastBookIndex)
|
||||
this.loadPage(lastBookPage)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -95,16 +95,17 @@ export default {
|
|||
user() {
|
||||
return this.$store.state.user.user
|
||||
},
|
||||
userAudiobook() {
|
||||
userLibraryItemProgress() {
|
||||
if (!this.libraryItemId) return
|
||||
return this.$store.getters['user/getUserLibraryItemProgress'](this.libraryItemId)
|
||||
},
|
||||
userAudiobookCurrentTime() {
|
||||
return this.userAudiobook ? this.userAudiobook.currentTime || 0 : 0
|
||||
userItemCurrentTime() {
|
||||
return this.userLibraryItemProgress ? this.userLibraryItemProgress.currentTime || 0 : 0
|
||||
},
|
||||
bookmarks() {
|
||||
if (!this.userAudiobook) return []
|
||||
return (this.userAudiobook.bookmarks || []).map((bm) => ({ ...bm })).sort((a, b) => a.time - b.time)
|
||||
return []
|
||||
// if (!this.userAudiobook) return []
|
||||
// return (this.userAudiobook.bookmarks || []).map((bm) => ({ ...bm })).sort((a, b) => a.time - b.time)
|
||||
},
|
||||
streamLibraryItem() {
|
||||
return this.$store.state.streamLibraryItem
|
||||
|
|
@ -236,9 +237,9 @@ export default {
|
|||
console.error('No Audio Ref')
|
||||
}
|
||||
},
|
||||
streamOpen(stream) {
|
||||
this.$store.commit('setLibraryItemStream', stream.libraryItem)
|
||||
this.playerHandler.prepareStream(stream)
|
||||
sessionOpen(session) {
|
||||
this.$store.commit('setLibraryItemStream', session.libraryItem)
|
||||
this.playerHandler.prepareOpenSession(session)
|
||||
},
|
||||
streamClosed(streamId) {
|
||||
// Stream was closed from the server
|
||||
|
|
@ -282,7 +283,7 @@ export default {
|
|||
if (!libraryItem) return
|
||||
this.$store.commit('setLibraryItemStream', libraryItem)
|
||||
|
||||
this.playerHandler.load(libraryItem, true, this.userAudiobookCurrentTime)
|
||||
this.playerHandler.load(libraryItem, true)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
|
|
|||
|
|
@ -161,10 +161,12 @@ export default {
|
|||
return this._libraryItem.libraryId
|
||||
},
|
||||
hasEbook() {
|
||||
return this.media.numEbooks
|
||||
if (!this.media.ebooks) return 0
|
||||
return this.media.ebooks.length
|
||||
},
|
||||
hasTracks() {
|
||||
return this.media.numTracks
|
||||
hasAudiobook() {
|
||||
if (!this.media.audiobooks) return 0
|
||||
return this.media.audiobooks.length
|
||||
},
|
||||
processingBatch() {
|
||||
return this.store.state.processingBatch
|
||||
|
|
@ -244,7 +246,7 @@ export default {
|
|||
return !this.isSelectionMode && this.showExperimentalFeatures && !this.showPlayButton && this.hasEbook
|
||||
},
|
||||
showPlayButton() {
|
||||
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && this.hasTracks && !this.isStreaming
|
||||
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && this.hasAudiobook && !this.isStreaming
|
||||
},
|
||||
showSmallEBookIcon() {
|
||||
return !this.isSelectionMode && this.showExperimentalFeatures && this.hasEbook
|
||||
|
|
@ -310,7 +312,7 @@ export default {
|
|||
}
|
||||
]
|
||||
if (this.userCanUpdate) {
|
||||
if (this.hasTracks) {
|
||||
if (this.hasAudiobook) {
|
||||
items.push({
|
||||
func: 'showEditModalTracks',
|
||||
text: 'Tracks'
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ export default {
|
|||
return this.filterData.languages || []
|
||||
},
|
||||
progress() {
|
||||
return ['Read', 'Unread', 'In Progress']
|
||||
return ['Finished', 'In Progress', 'Not Started']
|
||||
},
|
||||
sublistItems() {
|
||||
return (this[this.sublist] || []).map((item) => {
|
||||
|
|
|
|||
|
|
@ -168,7 +168,6 @@ export default {
|
|||
},
|
||||
async updateDetails(updatedDetails) {
|
||||
this.isProcessing = true
|
||||
console.log('Sending update', updatedDetails.updatePayload)
|
||||
var updateResult = await this.$axios.$patch(`/api/items/${this.libraryItemId}/media`, updatedDetails.updatePayload).catch((error) => {
|
||||
console.error('Failed to update', error)
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -64,7 +64,6 @@ export default {
|
|||
computed: {
|
||||
_directories() {
|
||||
return this.directories.map((d) => {
|
||||
console.log('Directories', d)
|
||||
var isUsed = !!this.paths.find((path) => path.endsWith(d.path))
|
||||
var isSelected = d.path === this.selectedPath
|
||||
var classes = []
|
||||
|
|
|
|||
|
|
@ -1,105 +0,0 @@
|
|||
<template>
|
||||
<div class="w-full my-2">
|
||||
<div class="w-full bg-primary px-4 py-2 flex items-center cursor-pointer">
|
||||
<p class="pr-4">All Files</p>
|
||||
<span class="bg-black-400 rounded-xl py-1 px-2 text-sm font-mono">{{ allFiles.length }}</span>
|
||||
<div class="flex-grow" />
|
||||
|
||||
<ui-btn small :color="showFullPath ? 'gray-600' : 'primary'" @click.stop="showFullPath = !showFullPath">Full Path</ui-btn>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<table class="text-sm tracksTable">
|
||||
<tr class="font-book">
|
||||
<th class="text-left px-4">Path</th>
|
||||
<th class="text-left px-4 w-24">Filetype</th>
|
||||
<th v-if="userCanDownload" class="text-center w-20">Download</th>
|
||||
</tr>
|
||||
<template v-for="file in allFiles">
|
||||
<tr :key="file.path">
|
||||
<td class="font-book pl-2">
|
||||
{{ showFullPath ? file.fullPath : file.path }}
|
||||
</td>
|
||||
<td class="text-xs">
|
||||
<p>{{ file.filetype }}</p>
|
||||
</td>
|
||||
<td v-if="userCanDownload" class="text-center">
|
||||
<a :href="`/s/book/${audiobookId}/${file.relativePath}?token=${userToken}`" download><span class="material-icons icon-text">download</span></a>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
audiobook: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showFullPath: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
audiobookId() {
|
||||
return this.audiobook.id
|
||||
},
|
||||
audiobookPath() {
|
||||
return this.audiobook.path
|
||||
},
|
||||
userCanDownload() {
|
||||
return this.$store.getters['user/getUserCanDownload']
|
||||
},
|
||||
userToken() {
|
||||
return this.$store.getters['user/getToken']
|
||||
},
|
||||
isMissing() {
|
||||
return this.audiobook.isMissing
|
||||
},
|
||||
showDownload() {
|
||||
return this.userCanDownload && !this.isMissing
|
||||
},
|
||||
otherFiles() {
|
||||
return this.audiobook.otherFiles || []
|
||||
},
|
||||
audioFiles() {
|
||||
return this.audiobook.audioFiles || []
|
||||
},
|
||||
audioFilesCleaned() {
|
||||
return this.audioFiles.map((af) => {
|
||||
return {
|
||||
path: af.path,
|
||||
fullPath: af.fullPath,
|
||||
relativePath: this.getRelativePath(af.path),
|
||||
filetype: 'audio'
|
||||
}
|
||||
})
|
||||
},
|
||||
otherFilesCleaned() {
|
||||
return this.otherFiles.map((af) => {
|
||||
return {
|
||||
path: af.path,
|
||||
fullPath: af.fullPath,
|
||||
relativePath: this.getRelativePath(af.path),
|
||||
filetype: af.filetype
|
||||
}
|
||||
})
|
||||
},
|
||||
allFiles() {
|
||||
return this.audioFilesCleaned.concat(this.otherFilesCleaned)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getRelativePath(path) {
|
||||
var relativePath = path.replace(/\\/g, '/').replace(this.audiobookPath.replace(/\\/g, '/') + '/', '')
|
||||
return this.$encodeUriPath(relativePath)
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -26,11 +26,11 @@
|
|||
</td>
|
||||
<td class="text-sm">{{ user.type }}</td>
|
||||
<td class="hidden lg:table-cell">
|
||||
<div v-if="usersOnline[user.id] && usersOnline[user.id].stream && usersOnline[user.id].stream.libraryItem && usersOnline[user.id].stream.libraryItem.media">
|
||||
<p class="truncate text-xs">Reading: {{ usersOnline[user.id].stream.libraryItem.media.metadata.title || '' }}</p>
|
||||
<div v-if="usersOnline[user.id] && usersOnline[user.id].session && usersOnline[user.id].session.libraryItem && usersOnline[user.id].session.libraryItem.media">
|
||||
<p class="truncate text-xs">Listening: {{ usersOnline[user.id].session.libraryItem.media.metadata.title || '' }}</p>
|
||||
</div>
|
||||
<div v-else-if="user.audiobooks && getLastRead(user.audiobooks)">
|
||||
<p class="truncate text-xs">Last: {{ getLastRead(user.audiobooks) }}</p>
|
||||
<div v-else-if="user.mostRecent">
|
||||
<p class="truncate text-xs">Last: {{ user.mostRecent.metadata.title }}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-xs font-mono hidden sm:table-cell">
|
||||
|
|
@ -78,23 +78,11 @@ export default {
|
|||
},
|
||||
usersOnline() {
|
||||
var usermap = {}
|
||||
this.$store.state.users.users.forEach((u) => (usermap[u.id] = { online: true, stream: u.stream }))
|
||||
this.$store.state.users.users.forEach((u) => (usermap[u.id] = { online: true, session: u.session }))
|
||||
return usermap
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getLastRead(audiobooks) {
|
||||
var abs = Object.values(audiobooks).filter((ab) => {
|
||||
return ab.progress > 0
|
||||
})
|
||||
if (abs.length) {
|
||||
abs = abs.sort((a, b) => b.lastUpdate - a.lastUpdate)
|
||||
// Book object is attached on request
|
||||
if (abs[0].book) return abs[0].book.title
|
||||
return abs[0].audiobookTitle ? abs[0].audiobookTitle : null
|
||||
}
|
||||
return null
|
||||
},
|
||||
deleteUserClick(user) {
|
||||
if (this.isDeletingUser) return
|
||||
if (confirm(`Are you sure you want to permanently delete user "${user.username}"?`)) {
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ export default {
|
|||
var _files = Array.from(e.target.files)
|
||||
if (_files && _files.length) {
|
||||
var file = _files[0]
|
||||
console.log('File', file)
|
||||
this.$emit('change', file)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ export default {
|
|||
console.error('Failed to get search results', error)
|
||||
return []
|
||||
})
|
||||
console.log('Search results', results)
|
||||
// console.log('Search results', results)
|
||||
this.items = results || []
|
||||
this.searching = false
|
||||
},
|
||||
|
|
|
|||
|
|
@ -276,7 +276,7 @@ export default {
|
|||
if (!matchingItem) return false
|
||||
for (var key in item) {
|
||||
if (item[key] !== matchingItem[key]) {
|
||||
console.log('Object array item keys changed', key, item[key], matchingItem[key])
|
||||
// console.log('Object array item keys changed', key, item[key], matchingItem[key])
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,9 +97,9 @@ export default {
|
|||
return
|
||||
}
|
||||
console.log('Init Payload', payload)
|
||||
if (payload.stream) {
|
||||
if (payload.session) {
|
||||
if (this.$refs.streamContainer) {
|
||||
this.$refs.streamContainer.streamOpen(payload.stream)
|
||||
this.$refs.streamContainer.sessionOpen(payload.session)
|
||||
} else {
|
||||
console.warn('Stream Container not mounted')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,7 +74,6 @@ module.exports = {
|
|||
|
||||
proxy: {
|
||||
'/dev/': { target: 'http://localhost:3333', pathRewrite: { '^/dev/': '' } },
|
||||
'/lib/': { target: process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '/' },
|
||||
'/ebook/': { target: process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '/' },
|
||||
'/s/': { target: process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '/' },
|
||||
'/metadata/': { target: process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '/' }
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ export default {
|
|||
if (!store.getters['user/getUserCanUpdate']) {
|
||||
return redirect('/?error=unauthorized')
|
||||
}
|
||||
var payload = await app.$axios.$get(`/api/audiobooks/${params.id}/item?expanded=1`).catch((error) => {
|
||||
var payload = await app.$axios.$get(`/api/entities/${params.id}/item?expanded=1`).catch((error) => {
|
||||
console.error('Failed', error)
|
||||
return false
|
||||
})
|
||||
|
|
@ -103,7 +103,7 @@ export default {
|
|||
console.error('Not found...', params.id)
|
||||
return redirect('/')
|
||||
}
|
||||
const audiobook = payload.audiobook
|
||||
const audiobook = payload.mediaEntity
|
||||
return {
|
||||
audiobook,
|
||||
libraryItem: payload.libraryItem,
|
||||
|
|
@ -218,7 +218,7 @@ export default {
|
|||
|
||||
this.saving = true
|
||||
this.$axios
|
||||
.$patch(`/api/audiobooks/${this.audiobook.id}/tracks`, { orderedFileData })
|
||||
.$patch(`/api/entities/${this.audiobook.id}/tracks`, { orderedFileData })
|
||||
.then((data) => {
|
||||
console.log('Finished patching files', data)
|
||||
this.saving = false
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@
|
|||
/>
|
||||
</svg>
|
||||
<div class="px-3">
|
||||
<p class="text-4xl md:text-5xl font-bold">{{ userAudiobooksRead.length }}</p>
|
||||
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Books Read</p>
|
||||
<p class="text-4xl md:text-5xl font-bold">{{ userItemsFinished.length }}</p>
|
||||
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Items Finished</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -35,17 +35,17 @@
|
|||
<div class="w-80 my-6 mx-auto">
|
||||
<h1 class="text-2xl mb-4 font-book">Recent Listening Sessions</h1>
|
||||
<p v-if="!mostRecentListeningSessions.length">No Listening Sessions</p>
|
||||
<template v-for="(book, index) in mostRecentListeningSessions">
|
||||
<div :key="book.id" class="w-full py-0.5">
|
||||
<template v-for="(item, index) in mostRecentListeningSessions">
|
||||
<div :key="item.id" class="w-full py-0.5">
|
||||
<div class="flex items-center mb-1">
|
||||
<p class="text-sm font-book text-white text-opacity-70 w-6 truncate">{{ index + 1 }}. </p>
|
||||
<div class="w-56">
|
||||
<p class="text-sm font-book text-white text-opacity-80 truncate">{{ book.audiobookTitle }}</p>
|
||||
<p class="text-xs text-white text-opacity-50">{{ $dateDistanceFromNow(book.lastUpdate) }}</p>
|
||||
<p class="text-sm font-book text-white text-opacity-80 truncate">{{ item.mediaMetadata.title }}</p>
|
||||
<p class="text-xs text-white text-opacity-50">{{ $dateDistanceFromNow(item.lastUpdate) }}</p>
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
<div class="w-18 text-right">
|
||||
<p class="text-sm font-bold">{{ $elapsedPretty(book.timeListening) }}</p>
|
||||
<p class="text-sm font-bold">{{ $elapsedPretty(item.timeListening) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -76,16 +76,11 @@ export default {
|
|||
currentLibraryId() {
|
||||
return this.$store.state.libraries.currentLibraryId
|
||||
},
|
||||
userAudiobooks() {
|
||||
return Object.values(this.user.audiobooks || {})
|
||||
userItemProgress() {
|
||||
return this.user.libraryItemProgress || []
|
||||
},
|
||||
userAudiobooksRead() {
|
||||
return this.userAudiobooks.filter((ab) => !!ab.isRead)
|
||||
},
|
||||
mostRecentBooksListened() {
|
||||
if (!this.listeningStats) return []
|
||||
var sorted = Object.values(this.listeningStats.books || {}).sort((a, b) => b.lastUpdate - a.lastUpdate)
|
||||
return sorted.slice(0, 10)
|
||||
userItemsFinished() {
|
||||
return this.userItemProgress.filter((lip) => !!lip.isFinished)
|
||||
},
|
||||
mostRecentListeningSessions() {
|
||||
if (!this.listeningStats) return []
|
||||
|
|
|
|||
|
|
@ -14,7 +14,10 @@
|
|||
<h1 class="text-xl pl-2">{{ username }}</h1>
|
||||
</div>
|
||||
<div class="cursor-pointer text-gray-400 hover:text-white" @click="copyToClipboard(userToken)">
|
||||
<p class="py-2 text-xs"><strong class="text-white">API Token: </strong><br /><span class="text-white">{{ userToken }}</span><span class="material-icons pl-2 text-base">content_copy</span></p>
|
||||
<p class="py-2 text-xs">
|
||||
<strong class="text-white">API Token: </strong><br /><span class="text-white">{{ userToken }}</span
|
||||
><span class="material-icons pl-2 text-base">content_copy</span>
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="showExperimentalFeatures" class="w-full h-px bg-white bg-opacity-10 my-2" />
|
||||
<div v-if="showExperimentalFeatures" class="py-2">
|
||||
|
|
@ -35,32 +38,32 @@
|
|||
</div>
|
||||
<div class="w-full h-px bg-white bg-opacity-10 my-2" />
|
||||
<div class="py-2">
|
||||
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Reading Progress</h1>
|
||||
<table v-if="userAudiobooks.length" class="userAudiobooksTable">
|
||||
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Item Progress</h1>
|
||||
<table v-if="libraryItemProgress.length" class="userAudiobooksTable">
|
||||
<tr class="bg-primary bg-opacity-40">
|
||||
<th class="w-16 text-left">Book</th>
|
||||
<th class="w-16 text-left">Item</th>
|
||||
<th class="text-left"></th>
|
||||
<th class="w-32">Progress</th>
|
||||
<th class="w-40 hidden sm:table-cell">Started At</th>
|
||||
<th class="w-40 hidden sm:table-cell">Last Update</th>
|
||||
</tr>
|
||||
<tr v-for="ab in userAudiobooks" :key="ab.audiobookId" :class="!ab.isRead ? '' : 'isRead'">
|
||||
<tr v-for="item in libraryItemProgress" :key="item.id" :class="!item.isFinished ? '' : 'isFinished'">
|
||||
<td>
|
||||
<covers-book-cover :width="50" :library-item="ab" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
<covers-book-cover :width="50" :library-item="item" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
</td>
|
||||
<td class="font-book">
|
||||
<p>{{ ab.media && ab.media.metadata ? ab.media.metadata.title : ab.audiobookTitle || 'Unknown' }}</p>
|
||||
<p v-if="ab.media && ab.media.metadata && ab.media.metadata.authorName" class="text-white text-opacity-50 text-sm font-sans">by {{ ab.media.metadata.authorName }}</p>
|
||||
<p>{{ item.media && item.media.metadata ? item.media.metadata.title : 'Unknown' }}</p>
|
||||
<p v-if="item.media && item.media.metadata && item.media.metadata.authorName" class="text-white text-opacity-50 text-sm font-sans">by {{ item.media.metadata.authorName }}</p>
|
||||
</td>
|
||||
<td class="text-center">{{ Math.floor(ab.progress * 100) }}%</td>
|
||||
<td class="text-center">{{ Math.floor(item.progress * 100) }}%</td>
|
||||
<td class="text-center hidden sm:table-cell">
|
||||
<ui-tooltip v-if="ab.startedAt" direction="top" :text="$formatDate(ab.startedAt, 'MMMM do, yyyy HH:mm')">
|
||||
<p class="text-sm">{{ $dateDistanceFromNow(ab.startedAt) }}</p>
|
||||
<ui-tooltip v-if="item.startedAt" direction="top" :text="$formatDate(item.startedAt, 'MMMM do, yyyy HH:mm')">
|
||||
<p class="text-sm">{{ $dateDistanceFromNow(item.startedAt) }}</p>
|
||||
</ui-tooltip>
|
||||
</td>
|
||||
<td class="text-center hidden sm:table-cell">
|
||||
<ui-tooltip v-if="ab.lastUpdate" direction="top" :text="$formatDate(ab.lastUpdate, 'MMMM do, yyyy HH:mm')">
|
||||
<p class="text-sm">{{ $dateDistanceFromNow(ab.lastUpdate) }}</p>
|
||||
<ui-tooltip v-if="item.lastUpdate" direction="top" :text="$formatDate(item.lastUpdate, 'MMMM do, yyyy HH:mm')">
|
||||
<p class="text-sm">{{ $dateDistanceFromNow(item.lastUpdate) }}</p>
|
||||
</ui-tooltip>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -108,15 +111,8 @@ export default {
|
|||
userOnline() {
|
||||
return this.$store.getters['users/getIsUserOnline'](this.user.id)
|
||||
},
|
||||
userAudiobooks() {
|
||||
return Object.values(this.user.audiobooks || {})
|
||||
.map((uab) => {
|
||||
return {
|
||||
id: uab.audiobookId,
|
||||
...uab
|
||||
}
|
||||
})
|
||||
.sort((a, b) => b.lastUpdate - a.lastUpdate)
|
||||
libraryItemProgress() {
|
||||
return this.user.libraryItemProgress.sort((a, b) => b.lastUpdate - a.lastUpdate)
|
||||
},
|
||||
totalListeningTime() {
|
||||
return this.listeningStats.totalTime || 0
|
||||
|
|
@ -169,7 +165,7 @@ export default {
|
|||
.userAudiobooksTable tr:hover:not(:first-child) {
|
||||
background-color: #474747;
|
||||
}
|
||||
.userAudiobooksTable tr.isRead {
|
||||
.userAudiobooksTable tr.isFinished {
|
||||
background-color: rgba(76, 175, 80, 0.1);
|
||||
}
|
||||
.userAudiobooksTable td {
|
||||
|
|
|
|||
|
|
@ -1,29 +1,31 @@
|
|||
export default class AudioTrack {
|
||||
constructor(track) {
|
||||
constructor(track, userToken) {
|
||||
this.index = track.index || 0
|
||||
this.startOffset = track.startOffset || 0 // Total time of all previous tracks
|
||||
this.duration = track.duration || 0
|
||||
this.title = track.metadata ? track.metadata.filename || '' : ''
|
||||
this.title = track.title || ''
|
||||
this.contentUrl = track.contentUrl || null
|
||||
this.mimeType = track.mimeType
|
||||
|
||||
this.userToken = userToken
|
||||
}
|
||||
|
||||
get fullContentUrl() {
|
||||
if (!this.contentUrl || this.contentUrl.startsWith('http')) return this.contentUrl
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return `${process.env.serverUrl}${this.contentUrl}`
|
||||
return `${process.env.serverUrl}${this.contentUrl}?token=${this.userToken}`
|
||||
}
|
||||
return `${window.location.origin}${this.contentUrl}`
|
||||
return `${window.location.origin}${this.contentUrl}?token=${this.userToken}`
|
||||
}
|
||||
|
||||
get relativeContentUrl() {
|
||||
if (!this.contentUrl || this.contentUrl.startsWith('http')) return this.contentUrl
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return `${process.env.serverUrl}${this.contentUrl}`
|
||||
return `${process.env.serverUrl}${this.contentUrl}?token=${this.userToken}`
|
||||
}
|
||||
|
||||
return this.contentUrl
|
||||
return this.contentUrl + '?token=${this.userToken}'
|
||||
}
|
||||
}
|
||||
|
|
@ -12,17 +12,19 @@ export default class CastPlayer extends EventEmitter {
|
|||
this.audiobook = null
|
||||
this.audioTracks = []
|
||||
this.currentTrackIndex = 0
|
||||
this.hlsStreamId = null
|
||||
this.isHlsTranscode = null
|
||||
this.currentTime = 0
|
||||
this.playWhenReady = false
|
||||
this.defaultPlaybackRate = 1
|
||||
|
||||
this.playableMimetypes = {}
|
||||
// TODO: Use canDisplayType on receiver to check mime types
|
||||
this.playableMimeTypes = {}
|
||||
|
||||
this.coverUrl = ''
|
||||
this.castPlayerState = 'IDLE'
|
||||
|
||||
// Supported audio codecs for chromecast
|
||||
|
||||
this.supportedAudioCodecs = ['opus', 'mp3', 'aac', 'flac', 'webma', 'wav']
|
||||
|
||||
this.initialize()
|
||||
|
|
@ -68,10 +70,10 @@ export default class CastPlayer extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
async set(audiobook, tracks, hlsStreamId, startTime, playWhenReady = false) {
|
||||
async set(audiobook, tracks, isHlsTranscode, startTime, playWhenReady = false) {
|
||||
this.audiobook = audiobook
|
||||
this.audioTracks = tracks
|
||||
this.hlsStreamId = hlsStreamId
|
||||
this.isHlsTranscode = isHlsTranscode
|
||||
this.playWhenReady = playWhenReady
|
||||
|
||||
this.currentTime = startTime
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export default class LocalPlayer extends EventEmitter {
|
|||
this.libraryItem = null
|
||||
this.audioTracks = []
|
||||
this.currentTrackIndex = 0
|
||||
this.hlsStreamId = null
|
||||
this.isHlsTranscode = null
|
||||
this.hlsInstance = null
|
||||
this.usingNativeplayer = false
|
||||
this.startTime = 0
|
||||
|
|
@ -19,7 +19,7 @@ export default class LocalPlayer extends EventEmitter {
|
|||
this.playWhenReady = false
|
||||
this.defaultPlaybackRate = 1
|
||||
|
||||
this.playableMimetypes = {}
|
||||
this.playableMimeTypes = {}
|
||||
|
||||
this.initialize()
|
||||
}
|
||||
|
|
@ -48,9 +48,9 @@ export default class LocalPlayer extends EventEmitter {
|
|||
|
||||
var mimeTypes = ['audio/flac', 'audio/mpeg', 'audio/mp4', 'audio/ogg', 'audio/aac']
|
||||
mimeTypes.forEach((mt) => {
|
||||
this.playableMimetypes[mt] = this.player.canPlayType(mt)
|
||||
this.playableMimeTypes[mt] = this.player.canPlayType(mt)
|
||||
})
|
||||
console.log(`[LocalPlayer] Supported mime types`, this.playableMimetypes)
|
||||
console.log(`[LocalPlayer] Supported mime types`, this.playableMimeTypes)
|
||||
}
|
||||
|
||||
evtPlay() {
|
||||
|
|
@ -80,7 +80,7 @@ export default class LocalPlayer extends EventEmitter {
|
|||
this.emit('error', error)
|
||||
}
|
||||
evtLoadedMetadata(data) {
|
||||
if (!this.hlsStreamId) {
|
||||
if (!this.isHlsTranscode) {
|
||||
this.player.currentTime = this.trackStartTime
|
||||
}
|
||||
|
||||
|
|
@ -97,23 +97,16 @@ export default class LocalPlayer extends EventEmitter {
|
|||
}
|
||||
|
||||
destroy() {
|
||||
if (this.hlsStreamId) {
|
||||
// Close HLS Stream
|
||||
console.log('Closing HLS Streams', this.hlsStreamId)
|
||||
this.ctx.$axios.$post(`/api/streams/${this.hlsStreamId}/close`).catch((error) => {
|
||||
console.error('Failed to request close hls stream', this.hlsStreamId, error)
|
||||
})
|
||||
}
|
||||
this.destroyHlsInstance()
|
||||
if (this.player) {
|
||||
this.player.remove()
|
||||
}
|
||||
}
|
||||
|
||||
set(libraryItem, tracks, hlsStreamId, startTime, playWhenReady = false) {
|
||||
set(libraryItem, tracks, isHlsTranscode, startTime, playWhenReady = false) {
|
||||
this.libraryItem = libraryItem
|
||||
this.audioTracks = tracks
|
||||
this.hlsStreamId = hlsStreamId
|
||||
this.isHlsTranscode = isHlsTranscode
|
||||
this.playWhenReady = playWhenReady
|
||||
this.startTime = startTime
|
||||
|
||||
|
|
@ -121,7 +114,7 @@ export default class LocalPlayer extends EventEmitter {
|
|||
this.destroyHlsInstance()
|
||||
}
|
||||
|
||||
if (this.hlsStreamId) {
|
||||
if (this.isHlsTranscode) {
|
||||
this.setHlsStream()
|
||||
} else {
|
||||
this.setDirectPlay()
|
||||
|
|
@ -198,7 +191,7 @@ export default class LocalPlayer extends EventEmitter {
|
|||
async resetStream(startTime) {
|
||||
this.destroyHlsInstance()
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
this.set(this.libraryItem, this.audioTracks, this.hlsStreamId, startTime, true)
|
||||
this.set(this.libraryItem, this.audioTracks, this.isHlsTranscode, startTime, true)
|
||||
}
|
||||
|
||||
playPause() {
|
||||
|
|
@ -234,7 +227,7 @@ export default class LocalPlayer extends EventEmitter {
|
|||
|
||||
seek(time) {
|
||||
if (!this.player) return
|
||||
if (this.hlsStreamId) {
|
||||
if (this.isHlsTranscode) {
|
||||
// Seeking HLS stream
|
||||
var offsetTime = time - (this.currentTrack.startOffset || 0)
|
||||
this.player.currentTime = Math.max(0, offsetTime)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ export default class PlayerHandler {
|
|||
this.playWhenReady = false
|
||||
this.player = null
|
||||
this.playerState = 'IDLE'
|
||||
this.currentStreamId = null
|
||||
this.isHlsTranscode = false
|
||||
this.currentSessionId = null
|
||||
this.startTime = 0
|
||||
|
||||
this.lastSyncTime = 0
|
||||
|
|
@ -35,11 +36,10 @@ export default class PlayerHandler {
|
|||
return this.playerState === 'PLAYING'
|
||||
}
|
||||
|
||||
load(libraryItem, playWhenReady, startTime = 0) {
|
||||
load(libraryItem, playWhenReady) {
|
||||
if (!this.player) this.switchPlayer()
|
||||
|
||||
this.libraryItem = libraryItem
|
||||
this.startTime = startTime
|
||||
this.playWhenReady = playWhenReady
|
||||
this.prepare()
|
||||
}
|
||||
|
|
@ -125,118 +125,61 @@ export default class PlayerHandler {
|
|||
this.ctx.setBufferTime(buffertime)
|
||||
}
|
||||
|
||||
async prepare(forceHls = false) {
|
||||
var useHls = false
|
||||
|
||||
var runningTotal = 0
|
||||
|
||||
var audioTracks = (this.libraryItem.media.tracks || []).map((track) => {
|
||||
if (!track.metadata) {
|
||||
console.error('INVALID TRACK', track)
|
||||
return null
|
||||
}
|
||||
var audioTrack = new AudioTrack(track)
|
||||
audioTrack.startOffset = runningTotal
|
||||
audioTrack.contentUrl = `/s/item/${this.libraryItem.id}/${this.ctx.$encodeUriPath(track.metadata.relPath.replace(/^\//, ''))}?token=${this.userToken}`
|
||||
audioTrack.mimeType = this.getMimeTypeForTrack(track)
|
||||
audioTrack.canDirectPlay = !!this.player.playableMimetypes[audioTrack.mimeType]
|
||||
|
||||
runningTotal += audioTrack.duration
|
||||
return audioTrack
|
||||
async prepare(forceTranscode = false) {
|
||||
var payload = {
|
||||
supportedMimeTypes: Object.keys(this.player.playableMimeTypes),
|
||||
mediaPlayer: this.isCasting ? 'chromecast' : 'html5',
|
||||
forceTranscode,
|
||||
forceDirectPlay: this.isCasting // TODO: add transcode support for chromecast
|
||||
}
|
||||
var session = await this.ctx.$axios.$post(`/api/items/${this.libraryItem.id}/play`, payload).catch((error) => {
|
||||
console.error('Failed to start stream', error)
|
||||
})
|
||||
|
||||
// All html5 audio player plays use HLS unless experimental features is on
|
||||
if (!this.isCasting) {
|
||||
if (forceHls || !this.ctx.showExperimentalFeatures) {
|
||||
useHls = true
|
||||
} else {
|
||||
// Use HLS if any audio track cannot be direct played
|
||||
useHls = !!audioTracks.find(at => !at.canDirectPlay)
|
||||
|
||||
if (useHls) {
|
||||
console.warn(`[PlayerHandler] An audio track cannot be direct played`, audioTracks.find(at => !at.canDirectPlay))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (useHls) {
|
||||
var stream = await this.ctx.$axios.$get(`/api/items/${this.libraryItem.id}/stream`).catch((error) => {
|
||||
console.error('Failed to start stream', error)
|
||||
})
|
||||
if (stream) {
|
||||
console.log(`[PlayerHandler] prepare hls stream`, stream)
|
||||
this.setHlsStream(stream)
|
||||
} else {
|
||||
console.error(`[PlayerHandler] Failed to start HLS stream`)
|
||||
}
|
||||
} else {
|
||||
this.setDirectPlay(audioTracks)
|
||||
}
|
||||
this.prepareSession(session)
|
||||
}
|
||||
|
||||
getMimeTypeForTrack(track) {
|
||||
var ext = track.metadata.ext
|
||||
if (ext === '.mp3' || ext === '.m4b' || ext === '.m4a') {
|
||||
return 'audio/mpeg'
|
||||
} else if (ext === '.mp4') {
|
||||
return 'audio/mp4'
|
||||
} else if (ext === '.ogg') {
|
||||
return 'audio/ogg'
|
||||
} else if (ext === '.aac' || ext === '.m4p') {
|
||||
return 'audio/aac'
|
||||
} else if (ext === '.flac') {
|
||||
return 'audio/flac'
|
||||
prepareOpenSession(session) { // Session opened on init socket
|
||||
if (!this.player) this.switchPlayer()
|
||||
|
||||
this.libraryItem = session.libraryItem
|
||||
this.playWhenReady = false
|
||||
this.prepareSession(session)
|
||||
}
|
||||
|
||||
prepareSession(session) {
|
||||
this.startTime = session.currentTime
|
||||
this.currentSessionId = session.id
|
||||
|
||||
console.log('[PlayerHandler] Preparing Session', session)
|
||||
var audioTracks = session.audioTracks.map(at => new AudioTrack(at, this.userToken))
|
||||
|
||||
this.ctx.playerLoading = true
|
||||
this.isHlsTranscode = true
|
||||
if (session.playMethod === this.ctx.$constants.PlayMethod.DIRECTPLAY) {
|
||||
this.isHlsTranscode = false
|
||||
}
|
||||
return 'audio/mpeg'
|
||||
|
||||
this.player.set(this.libraryItem, audioTracks, this.isHlsTranscode, this.startTime, this.playWhenReady)
|
||||
}
|
||||
|
||||
closePlayer() {
|
||||
console.log('[PlayerHandler] Close Player')
|
||||
this.sendCloseSession()
|
||||
if (this.player) {
|
||||
this.player.destroy()
|
||||
}
|
||||
this.player = null
|
||||
this.playerState = 'IDLE'
|
||||
this.libraryItem = null
|
||||
this.currentStreamId = null
|
||||
this.startTime = 0
|
||||
this.stopPlayInterval()
|
||||
}
|
||||
|
||||
prepareStream(stream) {
|
||||
if (!this.player) this.switchPlayer()
|
||||
this.libraryItem = stream.libraryItem
|
||||
this.setHlsStream({
|
||||
streamId: stream.id,
|
||||
streamUrl: stream.clientPlaylistUri,
|
||||
startTime: stream.clientCurrentTime
|
||||
})
|
||||
}
|
||||
|
||||
setHlsStream(stream) {
|
||||
this.currentStreamId = stream.streamId
|
||||
var audioTrack = new AudioTrack({
|
||||
duration: this.libraryItem.media.duration,
|
||||
contentUrl: stream.streamUrl + '?token=' + this.userToken,
|
||||
mimeType: 'application/vnd.apple.mpegurl'
|
||||
})
|
||||
this.startTime = stream.startTime
|
||||
this.ctx.playerLoading = true
|
||||
this.player.set(this.libraryItem, [audioTrack], this.currentStreamId, stream.startTime, this.playWhenReady)
|
||||
}
|
||||
|
||||
setDirectPlay(audioTracks) {
|
||||
this.currentStreamId = null
|
||||
this.ctx.playerLoading = true
|
||||
this.player.set(this.libraryItem, audioTracks, null, this.startTime, this.playWhenReady)
|
||||
}
|
||||
|
||||
resetStream(startTime, streamId) {
|
||||
if (this.currentStreamId === streamId) {
|
||||
if (this.isHlsTranscode && this.currentSessionId === streamId) {
|
||||
this.player.resetStream(startTime)
|
||||
} else {
|
||||
console.warn('resetStream mismatch streamId', this.currentStreamId, streamId)
|
||||
console.warn('resetStream mismatch streamId', this.currentSessionId, streamId)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -254,43 +197,39 @@ export default class PlayerHandler {
|
|||
this.listeningTimeSinceSync += exactTimeElapsed
|
||||
if (this.listeningTimeSinceSync >= 5) {
|
||||
this.sendProgressSync(currentTime)
|
||||
this.listeningTimeSinceSync = 0
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
sendCloseSession() {
|
||||
var syncData = null
|
||||
if (this.player) {
|
||||
var listeningTimeToAdd = Math.max(0, Math.floor(this.listeningTimeSinceSync))
|
||||
syncData = {
|
||||
timeListened: listeningTimeToAdd,
|
||||
currentTime: this.player.getCurrentTime()
|
||||
}
|
||||
}
|
||||
this.listeningTimeSinceSync = 0
|
||||
return this.ctx.$axios.$post(`/api/session/${this.currentSessionId}/close`, syncData, { timeout: 1000 }).catch((error) => {
|
||||
console.error('Failed to close session', error)
|
||||
})
|
||||
}
|
||||
|
||||
sendProgressSync(currentTime) {
|
||||
var diffSinceLastSync = Math.abs(this.lastSyncTime - currentTime)
|
||||
if (diffSinceLastSync < 1) return
|
||||
|
||||
this.lastSyncTime = currentTime
|
||||
if (this.currentStreamId) { // Updating stream progress (HLS stream)
|
||||
var listeningTimeToAdd = Math.max(0, Math.floor(this.listeningTimeSinceSync))
|
||||
var syncData = {
|
||||
timeListened: listeningTimeToAdd,
|
||||
currentTime,
|
||||
streamId: this.currentStreamId,
|
||||
audiobookId: this.libraryItem.id
|
||||
}
|
||||
this.ctx.$axios.$post('/api/syncStream', syncData, { timeout: 1000 }).catch((error) => {
|
||||
console.error('Failed to update stream progress', error)
|
||||
})
|
||||
} else {
|
||||
// Direct play via chromecast does not yet have backend stream session model
|
||||
// so the progress update for the libraryItem is updated this way (instead of through the stream)
|
||||
var duration = this.getDuration()
|
||||
var syncData = {
|
||||
totalDuration: duration,
|
||||
currentTime,
|
||||
progress: duration > 0 ? currentTime / duration : 0,
|
||||
isRead: false,
|
||||
audiobookId: this.libraryItem.id,
|
||||
lastUpdate: Date.now()
|
||||
}
|
||||
this.ctx.$axios.$post('/api/syncLocal', syncData, { timeout: 1000 }).catch((error) => {
|
||||
console.error('Failed to update local progress', error)
|
||||
})
|
||||
var listeningTimeToAdd = Math.max(0, Math.floor(this.listeningTimeSinceSync))
|
||||
var syncData = {
|
||||
timeListened: listeningTimeToAdd,
|
||||
currentTime
|
||||
}
|
||||
this.listeningTimeSinceSync = 0
|
||||
this.ctx.$axios.$post(`/api/session/${this.currentSessionId}/sync`, syncData, { timeout: 1000 }).catch((error) => {
|
||||
console.error('Failed to update session progress', error)
|
||||
})
|
||||
}
|
||||
|
||||
stopPlayInterval() {
|
||||
|
|
|
|||
|
|
@ -24,11 +24,18 @@ const BookshelfView = {
|
|||
TITLES: 1
|
||||
}
|
||||
|
||||
const PlayMethod = {
|
||||
DIRECTPLAY: 0,
|
||||
DIRECTSTREAM: 1,
|
||||
TRANSCODE: 2
|
||||
}
|
||||
|
||||
const Constants = {
|
||||
SupportedFileTypes,
|
||||
DownloadStatus,
|
||||
BookCoverAspectRatio,
|
||||
BookshelfView
|
||||
BookshelfView,
|
||||
PlayMethod
|
||||
}
|
||||
|
||||
const KeyNames = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue