mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-06-23 15:10:31 -04:00
New data model play media entity, PlaybackSessionManager
This commit is contained in:
parent
1cf9e85272
commit
099ae7c776
@ -175,8 +175,8 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
batchDeleteClick() {
|
batchDeleteClick() {
|
||||||
var audiobookText = this.numLibraryItemsSelected > 1 ? `these ${this.numLibraryItemsSelected} audiobooks` : 'this audiobook'
|
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 audiobooks from AudioBookshelf`
|
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)) {
|
if (confirm(confirmMsg)) {
|
||||||
this.processingBatchDelete = true
|
this.processingBatchDelete = true
|
||||||
this.$store.commit('setProcessingBatch', true)
|
this.$store.commit('setProcessingBatch', true)
|
||||||
|
@ -300,11 +300,11 @@ export default {
|
|||||||
var firstBookPage = Math.floor(firstBookIndex / this.booksPerFetch)
|
var firstBookPage = Math.floor(firstBookIndex / this.booksPerFetch)
|
||||||
var lastBookPage = Math.floor(lastBookIndex / this.booksPerFetch)
|
var lastBookPage = Math.floor(lastBookIndex / this.booksPerFetch)
|
||||||
if (!this.pagesLoaded[firstBookPage]) {
|
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)
|
this.loadPage(firstBookPage)
|
||||||
}
|
}
|
||||||
if (!this.pagesLoaded[lastBookPage]) {
|
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)
|
this.loadPage(lastBookPage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,16 +95,17 @@ export default {
|
|||||||
user() {
|
user() {
|
||||||
return this.$store.state.user.user
|
return this.$store.state.user.user
|
||||||
},
|
},
|
||||||
userAudiobook() {
|
userLibraryItemProgress() {
|
||||||
if (!this.libraryItemId) return
|
if (!this.libraryItemId) return
|
||||||
return this.$store.getters['user/getUserLibraryItemProgress'](this.libraryItemId)
|
return this.$store.getters['user/getUserLibraryItemProgress'](this.libraryItemId)
|
||||||
},
|
},
|
||||||
userAudiobookCurrentTime() {
|
userItemCurrentTime() {
|
||||||
return this.userAudiobook ? this.userAudiobook.currentTime || 0 : 0
|
return this.userLibraryItemProgress ? this.userLibraryItemProgress.currentTime || 0 : 0
|
||||||
},
|
},
|
||||||
bookmarks() {
|
bookmarks() {
|
||||||
if (!this.userAudiobook) return []
|
return []
|
||||||
return (this.userAudiobook.bookmarks || []).map((bm) => ({ ...bm })).sort((a, b) => a.time - b.time)
|
// if (!this.userAudiobook) return []
|
||||||
|
// return (this.userAudiobook.bookmarks || []).map((bm) => ({ ...bm })).sort((a, b) => a.time - b.time)
|
||||||
},
|
},
|
||||||
streamLibraryItem() {
|
streamLibraryItem() {
|
||||||
return this.$store.state.streamLibraryItem
|
return this.$store.state.streamLibraryItem
|
||||||
@ -236,9 +237,9 @@ export default {
|
|||||||
console.error('No Audio Ref')
|
console.error('No Audio Ref')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
streamOpen(stream) {
|
sessionOpen(session) {
|
||||||
this.$store.commit('setLibraryItemStream', stream.libraryItem)
|
this.$store.commit('setLibraryItemStream', session.libraryItem)
|
||||||
this.playerHandler.prepareStream(stream)
|
this.playerHandler.prepareOpenSession(session)
|
||||||
},
|
},
|
||||||
streamClosed(streamId) {
|
streamClosed(streamId) {
|
||||||
// Stream was closed from the server
|
// Stream was closed from the server
|
||||||
@ -282,7 +283,7 @@ export default {
|
|||||||
if (!libraryItem) return
|
if (!libraryItem) return
|
||||||
this.$store.commit('setLibraryItemStream', libraryItem)
|
this.$store.commit('setLibraryItemStream', libraryItem)
|
||||||
|
|
||||||
this.playerHandler.load(libraryItem, true, this.userAudiobookCurrentTime)
|
this.playerHandler.load(libraryItem, true)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
@ -161,10 +161,12 @@ export default {
|
|||||||
return this._libraryItem.libraryId
|
return this._libraryItem.libraryId
|
||||||
},
|
},
|
||||||
hasEbook() {
|
hasEbook() {
|
||||||
return this.media.numEbooks
|
if (!this.media.ebooks) return 0
|
||||||
|
return this.media.ebooks.length
|
||||||
},
|
},
|
||||||
hasTracks() {
|
hasAudiobook() {
|
||||||
return this.media.numTracks
|
if (!this.media.audiobooks) return 0
|
||||||
|
return this.media.audiobooks.length
|
||||||
},
|
},
|
||||||
processingBatch() {
|
processingBatch() {
|
||||||
return this.store.state.processingBatch
|
return this.store.state.processingBatch
|
||||||
@ -244,7 +246,7 @@ export default {
|
|||||||
return !this.isSelectionMode && this.showExperimentalFeatures && !this.showPlayButton && this.hasEbook
|
return !this.isSelectionMode && this.showExperimentalFeatures && !this.showPlayButton && this.hasEbook
|
||||||
},
|
},
|
||||||
showPlayButton() {
|
showPlayButton() {
|
||||||
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && this.hasTracks && !this.isStreaming
|
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && this.hasAudiobook && !this.isStreaming
|
||||||
},
|
},
|
||||||
showSmallEBookIcon() {
|
showSmallEBookIcon() {
|
||||||
return !this.isSelectionMode && this.showExperimentalFeatures && this.hasEbook
|
return !this.isSelectionMode && this.showExperimentalFeatures && this.hasEbook
|
||||||
@ -310,7 +312,7 @@ export default {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
if (this.userCanUpdate) {
|
if (this.userCanUpdate) {
|
||||||
if (this.hasTracks) {
|
if (this.hasAudiobook) {
|
||||||
items.push({
|
items.push({
|
||||||
func: 'showEditModalTracks',
|
func: 'showEditModalTracks',
|
||||||
text: 'Tracks'
|
text: 'Tracks'
|
||||||
|
@ -175,7 +175,7 @@ export default {
|
|||||||
return this.filterData.languages || []
|
return this.filterData.languages || []
|
||||||
},
|
},
|
||||||
progress() {
|
progress() {
|
||||||
return ['Read', 'Unread', 'In Progress']
|
return ['Finished', 'In Progress', 'Not Started']
|
||||||
},
|
},
|
||||||
sublistItems() {
|
sublistItems() {
|
||||||
return (this[this.sublist] || []).map((item) => {
|
return (this[this.sublist] || []).map((item) => {
|
||||||
|
@ -168,7 +168,6 @@ export default {
|
|||||||
},
|
},
|
||||||
async updateDetails(updatedDetails) {
|
async updateDetails(updatedDetails) {
|
||||||
this.isProcessing = true
|
this.isProcessing = true
|
||||||
console.log('Sending update', updatedDetails.updatePayload)
|
|
||||||
var updateResult = await this.$axios.$patch(`/api/items/${this.libraryItemId}/media`, updatedDetails.updatePayload).catch((error) => {
|
var updateResult = await this.$axios.$patch(`/api/items/${this.libraryItemId}/media`, updatedDetails.updatePayload).catch((error) => {
|
||||||
console.error('Failed to update', error)
|
console.error('Failed to update', error)
|
||||||
return false
|
return false
|
||||||
|
@ -64,7 +64,6 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
_directories() {
|
_directories() {
|
||||||
return this.directories.map((d) => {
|
return this.directories.map((d) => {
|
||||||
console.log('Directories', d)
|
|
||||||
var isUsed = !!this.paths.find((path) => path.endsWith(d.path))
|
var isUsed = !!this.paths.find((path) => path.endsWith(d.path))
|
||||||
var isSelected = d.path === this.selectedPath
|
var isSelected = d.path === this.selectedPath
|
||||||
var classes = []
|
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>
|
||||||
<td class="text-sm">{{ user.type }}</td>
|
<td class="text-sm">{{ user.type }}</td>
|
||||||
<td class="hidden lg:table-cell">
|
<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">
|
<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">Reading: {{ usersOnline[user.id].stream.libraryItem.media.metadata.title || '' }}</p>
|
<p class="truncate text-xs">Listening: {{ usersOnline[user.id].session.libraryItem.media.metadata.title || '' }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="user.audiobooks && getLastRead(user.audiobooks)">
|
<div v-else-if="user.mostRecent">
|
||||||
<p class="truncate text-xs">Last: {{ getLastRead(user.audiobooks) }}</p>
|
<p class="truncate text-xs">Last: {{ user.mostRecent.metadata.title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-xs font-mono hidden sm:table-cell">
|
<td class="text-xs font-mono hidden sm:table-cell">
|
||||||
@ -78,23 +78,11 @@ export default {
|
|||||||
},
|
},
|
||||||
usersOnline() {
|
usersOnline() {
|
||||||
var usermap = {}
|
var usermap = {}
|
||||||
this.$store.state.users.users.forEach((u) => (usermap[u.id] = { online: true, stream: u.stream }))
|
this.$store.state.users.users.forEach((u) => (usermap[u.id] = { online: true, session: u.session }))
|
||||||
return usermap
|
return usermap
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
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) {
|
deleteUserClick(user) {
|
||||||
if (this.isDeletingUser) return
|
if (this.isDeletingUser) return
|
||||||
if (confirm(`Are you sure you want to permanently delete user "${user.username}"?`)) {
|
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)
|
var _files = Array.from(e.target.files)
|
||||||
if (_files && _files.length) {
|
if (_files && _files.length) {
|
||||||
var file = _files[0]
|
var file = _files[0]
|
||||||
console.log('File', file)
|
|
||||||
this.$emit('change', file)
|
this.$emit('change', file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -78,7 +78,7 @@ export default {
|
|||||||
console.error('Failed to get search results', error)
|
console.error('Failed to get search results', error)
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
console.log('Search results', results)
|
// console.log('Search results', results)
|
||||||
this.items = results || []
|
this.items = results || []
|
||||||
this.searching = false
|
this.searching = false
|
||||||
},
|
},
|
||||||
|
@ -276,7 +276,7 @@ export default {
|
|||||||
if (!matchingItem) return false
|
if (!matchingItem) return false
|
||||||
for (var key in item) {
|
for (var key in item) {
|
||||||
if (item[key] !== matchingItem[key]) {
|
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
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -97,9 +97,9 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
console.log('Init Payload', payload)
|
console.log('Init Payload', payload)
|
||||||
if (payload.stream) {
|
if (payload.session) {
|
||||||
if (this.$refs.streamContainer) {
|
if (this.$refs.streamContainer) {
|
||||||
this.$refs.streamContainer.streamOpen(payload.stream)
|
this.$refs.streamContainer.sessionOpen(payload.session)
|
||||||
} else {
|
} else {
|
||||||
console.warn('Stream Container not mounted')
|
console.warn('Stream Container not mounted')
|
||||||
}
|
}
|
||||||
|
@ -74,7 +74,6 @@ module.exports = {
|
|||||||
|
|
||||||
proxy: {
|
proxy: {
|
||||||
'/dev/': { target: 'http://localhost:3333', pathRewrite: { '^/dev/': '' } },
|
'/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' : '/' },
|
'/ebook/': { target: process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '/' },
|
||||||
'/s/': { 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' : '/' }
|
'/metadata/': { target: process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '/' }
|
||||||
|
@ -95,7 +95,7 @@ export default {
|
|||||||
if (!store.getters['user/getUserCanUpdate']) {
|
if (!store.getters['user/getUserCanUpdate']) {
|
||||||
return redirect('/?error=unauthorized')
|
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)
|
console.error('Failed', error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
@ -103,7 +103,7 @@ export default {
|
|||||||
console.error('Not found...', params.id)
|
console.error('Not found...', params.id)
|
||||||
return redirect('/')
|
return redirect('/')
|
||||||
}
|
}
|
||||||
const audiobook = payload.audiobook
|
const audiobook = payload.mediaEntity
|
||||||
return {
|
return {
|
||||||
audiobook,
|
audiobook,
|
||||||
libraryItem: payload.libraryItem,
|
libraryItem: payload.libraryItem,
|
||||||
@ -218,7 +218,7 @@ export default {
|
|||||||
|
|
||||||
this.saving = true
|
this.saving = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$patch(`/api/audiobooks/${this.audiobook.id}/tracks`, { orderedFileData })
|
.$patch(`/api/entities/${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
|
||||||
|
@ -9,8 +9,8 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<div class="px-3">
|
<div class="px-3">
|
||||||
<p class="text-4xl md:text-5xl font-bold">{{ userAudiobooksRead.length }}</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">Books Read</p>
|
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Items Finished</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -35,17 +35,17 @@
|
|||||||
<div class="w-80 my-6 mx-auto">
|
<div class="w-80 my-6 mx-auto">
|
||||||
<h1 class="text-2xl mb-4 font-book">Recent Listening Sessions</h1>
|
<h1 class="text-2xl mb-4 font-book">Recent Listening Sessions</h1>
|
||||||
<p v-if="!mostRecentListeningSessions.length">No Listening Sessions</p>
|
<p v-if="!mostRecentListeningSessions.length">No Listening Sessions</p>
|
||||||
<template v-for="(book, index) in mostRecentListeningSessions">
|
<template v-for="(item, index) in mostRecentListeningSessions">
|
||||||
<div :key="book.id" class="w-full py-0.5">
|
<div :key="item.id" class="w-full py-0.5">
|
||||||
<div class="flex items-center mb-1">
|
<div class="flex items-center mb-1">
|
||||||
<p class="text-sm font-book text-white text-opacity-70 w-6 truncate">{{ index + 1 }}. </p>
|
<p class="text-sm font-book text-white text-opacity-70 w-6 truncate">{{ index + 1 }}. </p>
|
||||||
<div class="w-56">
|
<div class="w-56">
|
||||||
<p class="text-sm font-book text-white text-opacity-80 truncate">{{ book.audiobookTitle }}</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(book.lastUpdate) }}</p>
|
<p class="text-xs text-white text-opacity-50">{{ $dateDistanceFromNow(item.lastUpdate) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<div class="w-18 text-right">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -76,16 +76,11 @@ export default {
|
|||||||
currentLibraryId() {
|
currentLibraryId() {
|
||||||
return this.$store.state.libraries.currentLibraryId
|
return this.$store.state.libraries.currentLibraryId
|
||||||
},
|
},
|
||||||
userAudiobooks() {
|
userItemProgress() {
|
||||||
return Object.values(this.user.audiobooks || {})
|
return this.user.libraryItemProgress || []
|
||||||
},
|
},
|
||||||
userAudiobooksRead() {
|
userItemsFinished() {
|
||||||
return this.userAudiobooks.filter((ab) => !!ab.isRead)
|
return this.userItemProgress.filter((lip) => !!lip.isFinished)
|
||||||
},
|
|
||||||
mostRecentBooksListened() {
|
|
||||||
if (!this.listeningStats) return []
|
|
||||||
var sorted = Object.values(this.listeningStats.books || {}).sort((a, b) => b.lastUpdate - a.lastUpdate)
|
|
||||||
return sorted.slice(0, 10)
|
|
||||||
},
|
},
|
||||||
mostRecentListeningSessions() {
|
mostRecentListeningSessions() {
|
||||||
if (!this.listeningStats) return []
|
if (!this.listeningStats) return []
|
||||||
|
@ -14,7 +14,10 @@
|
|||||||
<h1 class="text-xl pl-2">{{ username }}</h1>
|
<h1 class="text-xl pl-2">{{ username }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="cursor-pointer text-gray-400 hover:text-white" @click="copyToClipboard(userToken)">
|
<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>
|
||||||
<div v-if="showExperimentalFeatures" class="w-full h-px bg-white bg-opacity-10 my-2" />
|
<div v-if="showExperimentalFeatures" class="w-full h-px bg-white bg-opacity-10 my-2" />
|
||||||
<div v-if="showExperimentalFeatures" class="py-2">
|
<div v-if="showExperimentalFeatures" class="py-2">
|
||||||
@ -35,32 +38,32 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="w-full h-px bg-white bg-opacity-10 my-2" />
|
<div class="w-full h-px bg-white bg-opacity-10 my-2" />
|
||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Reading Progress</h1>
|
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Item Progress</h1>
|
||||||
<table v-if="userAudiobooks.length" class="userAudiobooksTable">
|
<table v-if="libraryItemProgress.length" class="userAudiobooksTable">
|
||||||
<tr class="bg-primary bg-opacity-40">
|
<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="text-left"></th>
|
||||||
<th class="w-32">Progress</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">Started At</th>
|
||||||
<th class="w-40 hidden sm:table-cell">Last Update</th>
|
<th class="w-40 hidden sm:table-cell">Last Update</th>
|
||||||
</tr>
|
</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>
|
<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>
|
||||||
<td class="font-book">
|
<td class="font-book">
|
||||||
<p>{{ ab.media && ab.media.metadata ? ab.media.metadata.title : ab.audiobookTitle || 'Unknown' }}</p>
|
<p>{{ item.media && item.media.metadata ? item.media.metadata.title : '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 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>
|
||||||
<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">
|
<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')">
|
<ui-tooltip v-if="item.startedAt" direction="top" :text="$formatDate(item.startedAt, 'MMMM do, yyyy HH:mm')">
|
||||||
<p class="text-sm">{{ $dateDistanceFromNow(ab.startedAt) }}</p>
|
<p class="text-sm">{{ $dateDistanceFromNow(item.startedAt) }}</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center hidden sm:table-cell">
|
<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')">
|
<ui-tooltip v-if="item.lastUpdate" direction="top" :text="$formatDate(item.lastUpdate, 'MMMM do, yyyy HH:mm')">
|
||||||
<p class="text-sm">{{ $dateDistanceFromNow(ab.lastUpdate) }}</p>
|
<p class="text-sm">{{ $dateDistanceFromNow(item.lastUpdate) }}</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -108,15 +111,8 @@ export default {
|
|||||||
userOnline() {
|
userOnline() {
|
||||||
return this.$store.getters['users/getIsUserOnline'](this.user.id)
|
return this.$store.getters['users/getIsUserOnline'](this.user.id)
|
||||||
},
|
},
|
||||||
userAudiobooks() {
|
libraryItemProgress() {
|
||||||
return Object.values(this.user.audiobooks || {})
|
return this.user.libraryItemProgress.sort((a, b) => b.lastUpdate - a.lastUpdate)
|
||||||
.map((uab) => {
|
|
||||||
return {
|
|
||||||
id: uab.audiobookId,
|
|
||||||
...uab
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.sort((a, b) => b.lastUpdate - a.lastUpdate)
|
|
||||||
},
|
},
|
||||||
totalListeningTime() {
|
totalListeningTime() {
|
||||||
return this.listeningStats.totalTime || 0
|
return this.listeningStats.totalTime || 0
|
||||||
@ -169,7 +165,7 @@ export default {
|
|||||||
.userAudiobooksTable tr:hover:not(:first-child) {
|
.userAudiobooksTable tr:hover:not(:first-child) {
|
||||||
background-color: #474747;
|
background-color: #474747;
|
||||||
}
|
}
|
||||||
.userAudiobooksTable tr.isRead {
|
.userAudiobooksTable tr.isFinished {
|
||||||
background-color: rgba(76, 175, 80, 0.1);
|
background-color: rgba(76, 175, 80, 0.1);
|
||||||
}
|
}
|
||||||
.userAudiobooksTable td {
|
.userAudiobooksTable td {
|
||||||
|
@ -1,29 +1,31 @@
|
|||||||
export default class AudioTrack {
|
export default class AudioTrack {
|
||||||
constructor(track) {
|
constructor(track, userToken) {
|
||||||
this.index = track.index || 0
|
this.index = track.index || 0
|
||||||
this.startOffset = track.startOffset || 0 // Total time of all previous tracks
|
this.startOffset = track.startOffset || 0 // Total time of all previous tracks
|
||||||
this.duration = track.duration || 0
|
this.duration = track.duration || 0
|
||||||
this.title = track.metadata ? track.metadata.filename || '' : ''
|
this.title = track.title || ''
|
||||||
this.contentUrl = track.contentUrl || null
|
this.contentUrl = track.contentUrl || null
|
||||||
this.mimeType = track.mimeType
|
this.mimeType = track.mimeType
|
||||||
|
|
||||||
|
this.userToken = userToken
|
||||||
}
|
}
|
||||||
|
|
||||||
get fullContentUrl() {
|
get fullContentUrl() {
|
||||||
if (!this.contentUrl || this.contentUrl.startsWith('http')) return this.contentUrl
|
if (!this.contentUrl || this.contentUrl.startsWith('http')) return this.contentUrl
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
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() {
|
get relativeContentUrl() {
|
||||||
if (!this.contentUrl || this.contentUrl.startsWith('http')) return this.contentUrl
|
if (!this.contentUrl || this.contentUrl.startsWith('http')) return this.contentUrl
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
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.audiobook = null
|
||||||
this.audioTracks = []
|
this.audioTracks = []
|
||||||
this.currentTrackIndex = 0
|
this.currentTrackIndex = 0
|
||||||
this.hlsStreamId = null
|
this.isHlsTranscode = null
|
||||||
this.currentTime = 0
|
this.currentTime = 0
|
||||||
this.playWhenReady = false
|
this.playWhenReady = false
|
||||||
this.defaultPlaybackRate = 1
|
this.defaultPlaybackRate = 1
|
||||||
|
|
||||||
this.playableMimetypes = {}
|
// TODO: Use canDisplayType on receiver to check mime types
|
||||||
|
this.playableMimeTypes = {}
|
||||||
|
|
||||||
this.coverUrl = ''
|
this.coverUrl = ''
|
||||||
this.castPlayerState = 'IDLE'
|
this.castPlayerState = 'IDLE'
|
||||||
|
|
||||||
// Supported audio codecs for chromecast
|
// Supported audio codecs for chromecast
|
||||||
|
|
||||||
this.supportedAudioCodecs = ['opus', 'mp3', 'aac', 'flac', 'webma', 'wav']
|
this.supportedAudioCodecs = ['opus', 'mp3', 'aac', 'flac', 'webma', 'wav']
|
||||||
|
|
||||||
this.initialize()
|
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.audiobook = audiobook
|
||||||
this.audioTracks = tracks
|
this.audioTracks = tracks
|
||||||
this.hlsStreamId = hlsStreamId
|
this.isHlsTranscode = isHlsTranscode
|
||||||
this.playWhenReady = playWhenReady
|
this.playWhenReady = playWhenReady
|
||||||
|
|
||||||
this.currentTime = startTime
|
this.currentTime = startTime
|
||||||
|
@ -11,7 +11,7 @@ export default class LocalPlayer extends EventEmitter {
|
|||||||
this.libraryItem = null
|
this.libraryItem = null
|
||||||
this.audioTracks = []
|
this.audioTracks = []
|
||||||
this.currentTrackIndex = 0
|
this.currentTrackIndex = 0
|
||||||
this.hlsStreamId = null
|
this.isHlsTranscode = null
|
||||||
this.hlsInstance = null
|
this.hlsInstance = null
|
||||||
this.usingNativeplayer = false
|
this.usingNativeplayer = false
|
||||||
this.startTime = 0
|
this.startTime = 0
|
||||||
@ -19,7 +19,7 @@ export default class LocalPlayer extends EventEmitter {
|
|||||||
this.playWhenReady = false
|
this.playWhenReady = false
|
||||||
this.defaultPlaybackRate = 1
|
this.defaultPlaybackRate = 1
|
||||||
|
|
||||||
this.playableMimetypes = {}
|
this.playableMimeTypes = {}
|
||||||
|
|
||||||
this.initialize()
|
this.initialize()
|
||||||
}
|
}
|
||||||
@ -48,9 +48,9 @@ export default class LocalPlayer extends EventEmitter {
|
|||||||
|
|
||||||
var mimeTypes = ['audio/flac', 'audio/mpeg', 'audio/mp4', 'audio/ogg', 'audio/aac']
|
var mimeTypes = ['audio/flac', 'audio/mpeg', 'audio/mp4', 'audio/ogg', 'audio/aac']
|
||||||
mimeTypes.forEach((mt) => {
|
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() {
|
evtPlay() {
|
||||||
@ -80,7 +80,7 @@ export default class LocalPlayer extends EventEmitter {
|
|||||||
this.emit('error', error)
|
this.emit('error', error)
|
||||||
}
|
}
|
||||||
evtLoadedMetadata(data) {
|
evtLoadedMetadata(data) {
|
||||||
if (!this.hlsStreamId) {
|
if (!this.isHlsTranscode) {
|
||||||
this.player.currentTime = this.trackStartTime
|
this.player.currentTime = this.trackStartTime
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,23 +97,16 @@ export default class LocalPlayer extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
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()
|
this.destroyHlsInstance()
|
||||||
if (this.player) {
|
if (this.player) {
|
||||||
this.player.remove()
|
this.player.remove()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
set(libraryItem, tracks, hlsStreamId, startTime, playWhenReady = false) {
|
set(libraryItem, tracks, isHlsTranscode, startTime, playWhenReady = false) {
|
||||||
this.libraryItem = libraryItem
|
this.libraryItem = libraryItem
|
||||||
this.audioTracks = tracks
|
this.audioTracks = tracks
|
||||||
this.hlsStreamId = hlsStreamId
|
this.isHlsTranscode = isHlsTranscode
|
||||||
this.playWhenReady = playWhenReady
|
this.playWhenReady = playWhenReady
|
||||||
this.startTime = startTime
|
this.startTime = startTime
|
||||||
|
|
||||||
@ -121,7 +114,7 @@ export default class LocalPlayer extends EventEmitter {
|
|||||||
this.destroyHlsInstance()
|
this.destroyHlsInstance()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.hlsStreamId) {
|
if (this.isHlsTranscode) {
|
||||||
this.setHlsStream()
|
this.setHlsStream()
|
||||||
} else {
|
} else {
|
||||||
this.setDirectPlay()
|
this.setDirectPlay()
|
||||||
@ -198,7 +191,7 @@ export default class LocalPlayer extends EventEmitter {
|
|||||||
async resetStream(startTime) {
|
async resetStream(startTime) {
|
||||||
this.destroyHlsInstance()
|
this.destroyHlsInstance()
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
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() {
|
playPause() {
|
||||||
@ -234,7 +227,7 @@ export default class LocalPlayer extends EventEmitter {
|
|||||||
|
|
||||||
seek(time) {
|
seek(time) {
|
||||||
if (!this.player) return
|
if (!this.player) return
|
||||||
if (this.hlsStreamId) {
|
if (this.isHlsTranscode) {
|
||||||
// Seeking HLS stream
|
// Seeking HLS stream
|
||||||
var offsetTime = time - (this.currentTrack.startOffset || 0)
|
var offsetTime = time - (this.currentTrack.startOffset || 0)
|
||||||
this.player.currentTime = Math.max(0, offsetTime)
|
this.player.currentTime = Math.max(0, offsetTime)
|
||||||
|
@ -9,7 +9,8 @@ export default class PlayerHandler {
|
|||||||
this.playWhenReady = false
|
this.playWhenReady = false
|
||||||
this.player = null
|
this.player = null
|
||||||
this.playerState = 'IDLE'
|
this.playerState = 'IDLE'
|
||||||
this.currentStreamId = null
|
this.isHlsTranscode = false
|
||||||
|
this.currentSessionId = null
|
||||||
this.startTime = 0
|
this.startTime = 0
|
||||||
|
|
||||||
this.lastSyncTime = 0
|
this.lastSyncTime = 0
|
||||||
@ -35,11 +36,10 @@ export default class PlayerHandler {
|
|||||||
return this.playerState === 'PLAYING'
|
return this.playerState === 'PLAYING'
|
||||||
}
|
}
|
||||||
|
|
||||||
load(libraryItem, playWhenReady, startTime = 0) {
|
load(libraryItem, playWhenReady) {
|
||||||
if (!this.player) this.switchPlayer()
|
if (!this.player) this.switchPlayer()
|
||||||
|
|
||||||
this.libraryItem = libraryItem
|
this.libraryItem = libraryItem
|
||||||
this.startTime = startTime
|
|
||||||
this.playWhenReady = playWhenReady
|
this.playWhenReady = playWhenReady
|
||||||
this.prepare()
|
this.prepare()
|
||||||
}
|
}
|
||||||
@ -125,118 +125,61 @@ export default class PlayerHandler {
|
|||||||
this.ctx.setBufferTime(buffertime)
|
this.ctx.setBufferTime(buffertime)
|
||||||
}
|
}
|
||||||
|
|
||||||
async prepare(forceHls = false) {
|
async prepare(forceTranscode = false) {
|
||||||
var useHls = false
|
var payload = {
|
||||||
|
supportedMimeTypes: Object.keys(this.player.playableMimeTypes),
|
||||||
var runningTotal = 0
|
mediaPlayer: this.isCasting ? 'chromecast' : 'html5',
|
||||||
|
forceTranscode,
|
||||||
var audioTracks = (this.libraryItem.media.tracks || []).map((track) => {
|
forceDirectPlay: this.isCasting // TODO: add transcode support for chromecast
|
||||||
if (!track.metadata) {
|
}
|
||||||
console.error('INVALID TRACK', track)
|
var session = await this.ctx.$axios.$post(`/api/items/${this.libraryItem.id}/play`, payload).catch((error) => {
|
||||||
return null
|
console.error('Failed to start stream', error)
|
||||||
}
|
|
||||||
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
|
|
||||||
})
|
})
|
||||||
|
this.prepareSession(session)
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getMimeTypeForTrack(track) {
|
prepareOpenSession(session) { // Session opened on init socket
|
||||||
var ext = track.metadata.ext
|
if (!this.player) this.switchPlayer()
|
||||||
if (ext === '.mp3' || ext === '.m4b' || ext === '.m4a') {
|
|
||||||
return 'audio/mpeg'
|
this.libraryItem = session.libraryItem
|
||||||
} else if (ext === '.mp4') {
|
this.playWhenReady = false
|
||||||
return 'audio/mp4'
|
this.prepareSession(session)
|
||||||
} else if (ext === '.ogg') {
|
}
|
||||||
return 'audio/ogg'
|
|
||||||
} else if (ext === '.aac' || ext === '.m4p') {
|
prepareSession(session) {
|
||||||
return 'audio/aac'
|
this.startTime = session.currentTime
|
||||||
} else if (ext === '.flac') {
|
this.currentSessionId = session.id
|
||||||
return 'audio/flac'
|
|
||||||
|
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() {
|
closePlayer() {
|
||||||
console.log('[PlayerHandler] Close Player')
|
console.log('[PlayerHandler] Close Player')
|
||||||
|
this.sendCloseSession()
|
||||||
if (this.player) {
|
if (this.player) {
|
||||||
this.player.destroy()
|
this.player.destroy()
|
||||||
}
|
}
|
||||||
this.player = null
|
this.player = null
|
||||||
this.playerState = 'IDLE'
|
this.playerState = 'IDLE'
|
||||||
this.libraryItem = null
|
this.libraryItem = null
|
||||||
this.currentStreamId = null
|
|
||||||
this.startTime = 0
|
this.startTime = 0
|
||||||
this.stopPlayInterval()
|
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) {
|
resetStream(startTime, streamId) {
|
||||||
if (this.currentStreamId === streamId) {
|
if (this.isHlsTranscode && this.currentSessionId === streamId) {
|
||||||
this.player.resetStream(startTime)
|
this.player.resetStream(startTime)
|
||||||
} else {
|
} 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
|
this.listeningTimeSinceSync += exactTimeElapsed
|
||||||
if (this.listeningTimeSinceSync >= 5) {
|
if (this.listeningTimeSinceSync >= 5) {
|
||||||
this.sendProgressSync(currentTime)
|
this.sendProgressSync(currentTime)
|
||||||
this.listeningTimeSinceSync = 0
|
|
||||||
}
|
}
|
||||||
}, 1000)
|
}, 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) {
|
sendProgressSync(currentTime) {
|
||||||
var diffSinceLastSync = Math.abs(this.lastSyncTime - currentTime)
|
var diffSinceLastSync = Math.abs(this.lastSyncTime - currentTime)
|
||||||
if (diffSinceLastSync < 1) return
|
if (diffSinceLastSync < 1) return
|
||||||
|
|
||||||
this.lastSyncTime = currentTime
|
this.lastSyncTime = currentTime
|
||||||
if (this.currentStreamId) { // Updating stream progress (HLS stream)
|
var listeningTimeToAdd = Math.max(0, Math.floor(this.listeningTimeSinceSync))
|
||||||
var listeningTimeToAdd = Math.max(0, Math.floor(this.listeningTimeSinceSync))
|
var syncData = {
|
||||||
var syncData = {
|
timeListened: listeningTimeToAdd,
|
||||||
timeListened: listeningTimeToAdd,
|
currentTime
|
||||||
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)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
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() {
|
stopPlayInterval() {
|
||||||
|
@ -24,11 +24,18 @@ const BookshelfView = {
|
|||||||
TITLES: 1
|
TITLES: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PlayMethod = {
|
||||||
|
DIRECTPLAY: 0,
|
||||||
|
DIRECTSTREAM: 1,
|
||||||
|
TRANSCODE: 2
|
||||||
|
}
|
||||||
|
|
||||||
const Constants = {
|
const Constants = {
|
||||||
SupportedFileTypes,
|
SupportedFileTypes,
|
||||||
DownloadStatus,
|
DownloadStatus,
|
||||||
BookCoverAspectRatio,
|
BookCoverAspectRatio,
|
||||||
BookshelfView
|
BookshelfView,
|
||||||
|
PlayMethod
|
||||||
}
|
}
|
||||||
|
|
||||||
const KeyNames = {
|
const KeyNames = {
|
||||||
|
31
server/Db.js
31
server/Db.js
@ -35,7 +35,6 @@ class Db {
|
|||||||
|
|
||||||
this.libraryItems = []
|
this.libraryItems = []
|
||||||
this.users = []
|
this.users = []
|
||||||
this.sessions = []
|
|
||||||
this.libraries = []
|
this.libraries = []
|
||||||
this.settings = []
|
this.settings = []
|
||||||
this.collections = []
|
this.collections = []
|
||||||
@ -263,7 +262,7 @@ class Db {
|
|||||||
Logger.debug(`[DB] Inserted ${results.inserted} ${entityName}`)
|
Logger.debug(`[DB] Inserted ${results.inserted} ${entityName}`)
|
||||||
|
|
||||||
var arrayKey = this.getEntityArrayKey(entityName)
|
var arrayKey = this.getEntityArrayKey(entityName)
|
||||||
this[arrayKey] = this[arrayKey].concat(entities)
|
if (this[arrayKey]) this[arrayKey] = this[arrayKey].concat(entities)
|
||||||
return true
|
return true
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
Logger.error(`[DB] Failed to insert ${entityName}`, error)
|
Logger.error(`[DB] Failed to insert ${entityName}`, error)
|
||||||
@ -277,7 +276,7 @@ class Db {
|
|||||||
Logger.debug(`[DB] Inserted ${results.inserted} ${entityName}`)
|
Logger.debug(`[DB] Inserted ${results.inserted} ${entityName}`)
|
||||||
|
|
||||||
var arrayKey = this.getEntityArrayKey(entityName)
|
var arrayKey = this.getEntityArrayKey(entityName)
|
||||||
this[arrayKey].push(entity)
|
if (this[arrayKey]) this[arrayKey].push(entity)
|
||||||
return true
|
return true
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
Logger.error(`[DB] Failed to insert ${entityName}`, error)
|
Logger.error(`[DB] Failed to insert ${entityName}`, error)
|
||||||
@ -294,10 +293,12 @@ class Db {
|
|||||||
}).then((results) => {
|
}).then((results) => {
|
||||||
Logger.debug(`[DB] Updated ${entityName}: ${results.updated}`)
|
Logger.debug(`[DB] Updated ${entityName}: ${results.updated}`)
|
||||||
var arrayKey = this.getEntityArrayKey(entityName)
|
var arrayKey = this.getEntityArrayKey(entityName)
|
||||||
this[arrayKey] = this[arrayKey].map(e => {
|
if (this[arrayKey]) {
|
||||||
if (entityIds.includes(e.id)) return entities.find(_e => _e.id === e.id)
|
this[arrayKey] = this[arrayKey].map(e => {
|
||||||
return e
|
if (entityIds.includes(e.id)) return entities.find(_e => _e.id === e.id)
|
||||||
})
|
return e
|
||||||
|
})
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
Logger.error(`[DB] Update ${entityName} Failed: ${error}`)
|
Logger.error(`[DB] Update ${entityName} Failed: ${error}`)
|
||||||
@ -321,9 +322,11 @@ class Db {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var arrayKey = this.getEntityArrayKey(entityName)
|
var arrayKey = this.getEntityArrayKey(entityName)
|
||||||
this[arrayKey] = this[arrayKey].map(e => {
|
if (this[arrayKey]) {
|
||||||
return e.id === entity.id ? entity : e
|
this[arrayKey] = this[arrayKey].map(e => {
|
||||||
})
|
return e.id === entity.id ? entity : e
|
||||||
|
})
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
Logger.error(`[DB] Update entity ${entityName} Failed: ${error}`)
|
Logger.error(`[DB] Update entity ${entityName} Failed: ${error}`)
|
||||||
@ -336,9 +339,11 @@ class Db {
|
|||||||
return entityDb.delete((record) => record.id === entityId).then((results) => {
|
return entityDb.delete((record) => record.id === entityId).then((results) => {
|
||||||
Logger.debug(`[DB] Deleted entity ${entityName}: ${results.deleted}`)
|
Logger.debug(`[DB] Deleted entity ${entityName}: ${results.deleted}`)
|
||||||
var arrayKey = this.getEntityArrayKey(entityName)
|
var arrayKey = this.getEntityArrayKey(entityName)
|
||||||
this[arrayKey] = this[arrayKey].filter(e => {
|
if (this[arrayKey]) {
|
||||||
return e.id !== entityId
|
this[arrayKey] = this[arrayKey].filter(e => {
|
||||||
})
|
return e.id !== entityId
|
||||||
|
})
|
||||||
|
}
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
Logger.error(`[DB] Remove entity ${entityName} Failed: ${error}`)
|
Logger.error(`[DB] Remove entity ${entityName} Failed: ${error}`)
|
||||||
})
|
})
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
|
const { PlayMethod } = require('./utils/constants')
|
||||||
const PlaybackSession = require('./objects/PlaybackSession')
|
const PlaybackSession = require('./objects/PlaybackSession')
|
||||||
|
const Stream = require('./objects/Stream')
|
||||||
|
const Logger = require('./Logger')
|
||||||
|
|
||||||
class PlaybackSessionManager {
|
class PlaybackSessionManager {
|
||||||
constructor(db, emitter, clientEmitter) {
|
constructor(db, emitter, clientEmitter) {
|
||||||
@ -11,25 +14,120 @@ class PlaybackSessionManager {
|
|||||||
this.sessions = []
|
this.sessions = []
|
||||||
}
|
}
|
||||||
|
|
||||||
async startSessionRequest(req, res) {
|
getSession(sessionId) {
|
||||||
var user = req.user
|
return this.sessions.find(s => s.id === sessionId)
|
||||||
var libraryItem = req.libraryItem
|
}
|
||||||
var options = req.query || {}
|
getUserSession(userId) {
|
||||||
const session = await this.startSession(user, libraryItem, options)
|
return this.sessions.find(s => s.userId === userId)
|
||||||
res.json(session)
|
}
|
||||||
|
getStream(sessionId) {
|
||||||
|
var session = this.getSession(sessionId)
|
||||||
|
return session ? session.stream : null
|
||||||
}
|
}
|
||||||
|
|
||||||
async startSession(user, libraryItem, options) {
|
async startSessionRequest(user, libraryItem, mediaEntity, options, res) {
|
||||||
// TODO: Determine what play method to use and setup playback session
|
const session = await this.startSession(user, libraryItem, mediaEntity, options)
|
||||||
// temporary client can pass direct=1 in query string for direct play
|
res.json(session.toJSONForClient())
|
||||||
if (options.direct) {
|
}
|
||||||
var tracks = libraryItem.media.getDirectPlayTracklist(options)
|
|
||||||
|
async syncSessionRequest(user, session, payload, res) {
|
||||||
|
await this.syncSession(user, session, payload)
|
||||||
|
res.json(session.toJSONForClient())
|
||||||
|
}
|
||||||
|
|
||||||
|
async closeSessionRequest(user, session, syncData, res) {
|
||||||
|
await this.closeSession(user, session, syncData)
|
||||||
|
res.sendStatus(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
async startSession(user, libraryItem, mediaEntity, options) {
|
||||||
|
var shouldDirectPlay = options.forceDirectPlay || (!options.forceTranscode && mediaEntity.checkCanDirectPlay(options))
|
||||||
|
|
||||||
|
const userProgress = user.getLibraryItemProgress(libraryItem.id)
|
||||||
|
var userStartTime = 0
|
||||||
|
if (userProgress) userStartTime = userProgress.currentTime || 0
|
||||||
|
const newPlaybackSession = new PlaybackSession()
|
||||||
|
newPlaybackSession.setData(libraryItem, mediaEntity, user)
|
||||||
|
|
||||||
|
var audioTracks = []
|
||||||
|
if (shouldDirectPlay) {
|
||||||
|
Logger.debug(`[PlaybackSessionManager] "${user.username}" starting direct play session for media entity "${mediaEntity.id}"`)
|
||||||
|
audioTracks = mediaEntity.getDirectPlayTracklist(libraryItem.id)
|
||||||
|
newPlaybackSession.playMethod = PlayMethod.DIRECTPLAY
|
||||||
|
} else {
|
||||||
|
Logger.debug(`[PlaybackSessionManager] "${user.username}" starting stream session for media entity "${mediaEntity.id}"`)
|
||||||
|
var stream = new Stream(newPlaybackSession.id, this.StreamsPath, user, libraryItem, mediaEntity, userStartTime, this.clientEmitter.bind(this))
|
||||||
|
await stream.generatePlaylist()
|
||||||
|
audioTracks = [stream.getAudioTrack()]
|
||||||
|
newPlaybackSession.stream = stream
|
||||||
|
newPlaybackSession.playMethod = PlayMethod.TRANSCODE
|
||||||
|
stream.on('closed', () => {
|
||||||
|
Logger.debug(`[PlaybackSessionManager] Stream closed for session "${newPlaybackSession.id}"`)
|
||||||
|
newPlaybackSession.stream = null
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const newPlaybackSession = new PlaybackSession()
|
newPlaybackSession.currentTime = userStartTime
|
||||||
newPlaybackSession.setData(libraryItem, user)
|
newPlaybackSession.audioTracks = audioTracks
|
||||||
|
|
||||||
|
// Will save on the first sync
|
||||||
|
user.currentSessionId = newPlaybackSession.id
|
||||||
|
|
||||||
this.sessions.push(newPlaybackSession)
|
this.sessions.push(newPlaybackSession)
|
||||||
|
this.emitter('user_stream_update', user.toJSONForPublic(this.sessions, this.db.libraryItems))
|
||||||
|
|
||||||
return newPlaybackSession
|
return newPlaybackSession
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async syncSession(user, session, syncData) {
|
||||||
|
session.currentTime = syncData.currentTime
|
||||||
|
session.addListeningTime(syncData.timeListened)
|
||||||
|
Logger.debug(`[PlaybackSessionManager] syncSession "${session.id}" | Total Time Listened: ${session.timeListening}`)
|
||||||
|
|
||||||
|
const itemProgressUpdate = {
|
||||||
|
currentTime: syncData.currentTime,
|
||||||
|
progress: session.progress
|
||||||
|
}
|
||||||
|
var wasUpdated = user.createUpdateLibraryItemProgress(session.libraryItemId, itemProgressUpdate)
|
||||||
|
if (wasUpdated) {
|
||||||
|
await this.db.updateEntity('user', user)
|
||||||
|
var itemProgress = user.getLibraryItemProgress(session.libraryItemId)
|
||||||
|
this.clientEmitter(user.id, 'user_item_progress_updated', {
|
||||||
|
id: itemProgress.id,
|
||||||
|
data: itemProgress.toJSON()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.saveSession(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
async closeSession(user, session, syncData = null) {
|
||||||
|
if (syncData) {
|
||||||
|
await this.syncSession(user, session, syncData)
|
||||||
|
} else {
|
||||||
|
await this.saveSession(session)
|
||||||
|
}
|
||||||
|
Logger.debug(`[PlaybackSessionManager] closeSession "${session.id}"`)
|
||||||
|
this.emitter('user_stream_update', user.toJSONForPublic(this.sessions, this.db.libraryItems))
|
||||||
|
return this.removeSession(session.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
saveSession(session) {
|
||||||
|
if (session.lastSave) {
|
||||||
|
return this.db.updateEntity('session', session)
|
||||||
|
} else {
|
||||||
|
session.lastSave = Date.now()
|
||||||
|
return this.db.insertEntity('session', session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeSession(sessionId) {
|
||||||
|
var session = this.sessions.find(s => s.id === sessionId)
|
||||||
|
if (!session) return
|
||||||
|
if (session.stream) {
|
||||||
|
await session.stream.close()
|
||||||
|
}
|
||||||
|
this.sessions = this.sessions.filter(s => s.id !== sessionId)
|
||||||
|
Logger.debug(`[PlaybackSessionManager] Removed session "${sessionId}"`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = PlaybackSessionManager
|
module.exports = PlaybackSessionManager
|
@ -11,7 +11,6 @@ const { version } = require('../package.json')
|
|||||||
// Utils
|
// Utils
|
||||||
const { ScanResult } = require('./utils/constants')
|
const { ScanResult } = require('./utils/constants')
|
||||||
const filePerms = require('./utils/filePerms')
|
const filePerms = require('./utils/filePerms')
|
||||||
const { secondsToTimestamp } = require('./utils/index')
|
|
||||||
const dbMigration = require('./utils/dbMigration')
|
const dbMigration = require('./utils/dbMigration')
|
||||||
const Logger = require('./Logger')
|
const Logger = require('./Logger')
|
||||||
|
|
||||||
@ -22,9 +21,9 @@ const Scanner = require('./scanner/Scanner')
|
|||||||
const Db = require('./Db')
|
const Db = require('./Db')
|
||||||
const BackupManager = require('./BackupManager')
|
const BackupManager = require('./BackupManager')
|
||||||
const LogManager = require('./LogManager')
|
const LogManager = require('./LogManager')
|
||||||
const ApiController = require('./ApiController')
|
const ApiRouter = require('./routers/ApiRouter')
|
||||||
const HlsController = require('./HlsController')
|
const HlsRouter = require('./routers/HlsRouter')
|
||||||
// const StreamManager = require('./objects/legacy/StreamManager')
|
const StaticRouter = require('./routers/StaticRouter')
|
||||||
const PlaybackSessionManager = require('./PlaybackSessionManager')
|
const PlaybackSessionManager = require('./PlaybackSessionManager')
|
||||||
const DownloadManager = require('./DownloadManager')
|
const DownloadManager = require('./DownloadManager')
|
||||||
const CoverController = require('./CoverController')
|
const CoverController = require('./CoverController')
|
||||||
@ -58,12 +57,13 @@ class Server {
|
|||||||
this.watcher = new Watcher()
|
this.watcher = new Watcher()
|
||||||
this.coverController = new CoverController(this.db, this.cacheManager)
|
this.coverController = new CoverController(this.db, this.cacheManager)
|
||||||
this.scanner = new Scanner(this.db, this.coverController, this.emitter.bind(this))
|
this.scanner = new Scanner(this.db, this.coverController, this.emitter.bind(this))
|
||||||
|
|
||||||
this.playbackSessionManager = new PlaybackSessionManager(this.db, this.emitter.bind(this), this.clientEmitter.bind(this))
|
this.playbackSessionManager = new PlaybackSessionManager(this.db, this.emitter.bind(this), this.clientEmitter.bind(this))
|
||||||
// this.streamManager = new StreamManager(this.db, this.emitter.bind(this), this.clientEmitter.bind(this))
|
|
||||||
this.downloadManager = new DownloadManager(this.db)
|
this.downloadManager = new DownloadManager(this.db)
|
||||||
this.apiController = new ApiController(this.db, this.auth, this.scanner, this.playbackSessionManager, this.downloadManager, this.coverController, this.backupManager, this.watcher, this.cacheManager, this.emitter.bind(this), this.clientEmitter.bind(this))
|
|
||||||
this.hlsController = new HlsController(this.db, this.auth, this.playbackSessionManager, this.emitter.bind(this))
|
// Routers
|
||||||
|
this.apiRouter = new ApiRouter(this.db, this.auth, this.scanner, this.playbackSessionManager, this.downloadManager, this.coverController, this.backupManager, this.watcher, this.cacheManager, this.emitter.bind(this), this.clientEmitter.bind(this))
|
||||||
|
this.hlsRouter = new HlsRouter(this.db, this.auth, this.playbackSessionManager, this.emitter.bind(this))
|
||||||
|
this.staticRouter = new StaticRouter(this.db)
|
||||||
|
|
||||||
Logger.logManager = this.logManager
|
Logger.logManager = this.logManager
|
||||||
|
|
||||||
@ -76,7 +76,7 @@ class Server {
|
|||||||
get usersOnline() {
|
get usersOnline() {
|
||||||
// TODO: Map open user sessions
|
// TODO: Map open user sessions
|
||||||
return Object.values(this.clients).filter(c => c.user).map(client => {
|
return Object.values(this.clients).filter(c => c.user).map(client => {
|
||||||
return client.user.toJSONForPublic([])
|
return client.user.toJSONForPublic(this.playbackSessionManager.sessions, this.db.libraryItems)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,41 +169,9 @@ class Server {
|
|||||||
// Static folder
|
// Static folder
|
||||||
app.use(express.static(Path.join(global.appRoot, 'static')))
|
app.use(express.static(Path.join(global.appRoot, 'static')))
|
||||||
|
|
||||||
app.use('/api', this.authMiddleware.bind(this), this.apiController.router)
|
app.use('/api', this.authMiddleware.bind(this), this.apiRouter.router)
|
||||||
app.use('/hls', this.authMiddleware.bind(this), this.hlsController.router)
|
app.use('/hls', this.authMiddleware.bind(this), this.hlsRouter.router)
|
||||||
|
app.use('/s', this.authMiddleware.bind(this), this.staticRouter.router)
|
||||||
// Static file routes
|
|
||||||
app.get('/lib/:library/:folder/*', this.authMiddleware.bind(this), (req, res) => {
|
|
||||||
var library = this.db.libraries.find(lib => lib.id === req.params.library)
|
|
||||||
if (!library) return res.sendStatus(404)
|
|
||||||
var folder = library.folders.find(fol => fol.id === req.params.folder)
|
|
||||||
if (!folder) return res.status(404).send('Folder not found')
|
|
||||||
|
|
||||||
var remainingPath = req.params['0']
|
|
||||||
var fullPath = Path.join(folder.fullPath, remainingPath)
|
|
||||||
res.sendFile(fullPath)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Book static file routes
|
|
||||||
// LEGACY
|
|
||||||
app.get('/s/book/:id/*', this.authMiddleware.bind(this), (req, res) => {
|
|
||||||
var audiobook = this.db.audiobooks.find(ab => ab.id === req.params.id)
|
|
||||||
if (!audiobook) return res.status(404).send('Book not found with id ' + req.params.id)
|
|
||||||
|
|
||||||
var remainingPath = req.params['0']
|
|
||||||
var fullPath = Path.join(audiobook.fullPath, remainingPath)
|
|
||||||
res.sendFile(fullPath)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Library Item static file routes
|
|
||||||
app.get('/s/item/:id/*', this.authMiddleware.bind(this), (req, res) => {
|
|
||||||
var item = this.db.libraryItems.find(ab => ab.id === req.params.id)
|
|
||||||
if (!item) return res.status(404).send('Item not found with id ' + req.params.id)
|
|
||||||
|
|
||||||
var remainingPath = req.params['0']
|
|
||||||
var fullPath = Path.join(item.path, remainingPath)
|
|
||||||
res.sendFile(fullPath)
|
|
||||||
})
|
|
||||||
|
|
||||||
// EBook static file routes
|
// EBook static file routes
|
||||||
app.get('/ebook/:library/:folder/*', (req, res) => {
|
app.get('/ebook/:library/:folder/*', (req, res) => {
|
||||||
@ -267,14 +235,6 @@ class Server {
|
|||||||
socket.on('scan_item', (libraryItemId) => this.scanLibraryItem(socket, libraryItemId))
|
socket.on('scan_item', (libraryItemId) => this.scanLibraryItem(socket, libraryItemId))
|
||||||
socket.on('save_metadata', (libraryItemId) => this.saveMetadata(socket, libraryItemId))
|
socket.on('save_metadata', (libraryItemId) => this.saveMetadata(socket, libraryItemId))
|
||||||
|
|
||||||
// Streaming (only still used in the mobile app)
|
|
||||||
// socket.on('open_stream', (audiobookId) => this.streamManager.openStreamSocketRequest(socket, audiobookId))
|
|
||||||
// socket.on('close_stream', () => this.streamManager.closeStreamRequest(socket))
|
|
||||||
// socket.on('stream_sync', (syncData) => this.streamManager.streamSync(socket, syncData))
|
|
||||||
|
|
||||||
// Used to sync when playing local book on mobile, will be moved to API route
|
|
||||||
// socket.on('progress_update', (payload) => this.audiobookProgressUpdate(socket, payload))
|
|
||||||
|
|
||||||
// Downloading
|
// Downloading
|
||||||
socket.on('download', (payload) => this.downloadManager.downloadSocketRequest(socket, payload))
|
socket.on('download', (payload) => this.downloadManager.downloadSocketRequest(socket, payload))
|
||||||
socket.on('remove_download', (downloadId) => this.downloadManager.removeSocketRequest(socket, downloadId))
|
socket.on('remove_download', (downloadId) => this.downloadManager.removeSocketRequest(socket, downloadId))
|
||||||
@ -303,7 +263,7 @@ class Server {
|
|||||||
delete this.clients[socket.id]
|
delete this.clients[socket.id]
|
||||||
} else {
|
} else {
|
||||||
Logger.debug('[Server] User Offline ' + _client.user.username)
|
Logger.debug('[Server] User Offline ' + _client.user.username)
|
||||||
this.io.emit('user_offline', _client.user.toJSONForPublic([]))
|
this.io.emit('user_offline', _client.user.toJSONForPublic(this.playbackSessionManager.sessions, this.db.libraryItems))
|
||||||
|
|
||||||
const disconnectTime = Date.now() - _client.connected_at
|
const disconnectTime = Date.now() - _client.connected_at
|
||||||
Logger.info(`[Server] Socket ${socket.id} disconnected from client "${_client.user.username}" after ${disconnectTime}ms`)
|
Logger.info(`[Server] Socket ${socket.id} disconnected from client "${_client.user.username}" after ${disconnectTime}ms`)
|
||||||
@ -487,11 +447,10 @@ class Server {
|
|||||||
|
|
||||||
if (client.user) {
|
if (client.user) {
|
||||||
Logger.debug('[Server] User Offline ' + client.user.username)
|
Logger.debug('[Server] User Offline ' + client.user.username)
|
||||||
this.io.emit('user_offline', client.user.toJSONForPublic(null))
|
this.io.emit('user_offline', client.user.toJSONForPublic(null, this.db.libraryItems))
|
||||||
}
|
}
|
||||||
|
|
||||||
delete this.clients[socketId].user
|
delete this.clients[socketId].user
|
||||||
delete this.clients[socketId].stream
|
|
||||||
if (clientSocket && clientSocket.sheepClient) delete this.clients[socketId].socket.sheepClient
|
if (clientSocket && clientSocket.sheepClient) delete this.clients[socketId].socket.sheepClient
|
||||||
} else if (socketId) {
|
} else if (socketId) {
|
||||||
Logger.warn(`[Server] No client for socket ${socketId}`)
|
Logger.warn(`[Server] No client for socket ${socketId}`)
|
||||||
@ -604,19 +563,23 @@ class Server {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user has stream open
|
// Check if user has session open
|
||||||
if (client.user.stream) {
|
var session = this.playbackSessionManager.getUserSession(user.id)
|
||||||
Logger.info('User has stream open already', client.user.stream)
|
if (session) {
|
||||||
// client.stream = this.streamManager.getStream(client.user.stream)
|
Logger.debug(`[Server] User Online "${client.user.username}" with session open "${session.id}"`)
|
||||||
// if (!client.stream) {
|
session = session.toJSONForClient()
|
||||||
// Logger.error('Invalid user stream id', client.user.stream)
|
var sessionLibraryItem = this.db.libraryItems.find(li => li.id === session.libraryItemId)
|
||||||
// this.streamManager.removeOrphanStreamFiles(client.user.stream)
|
if (!sessionLibraryItem) {
|
||||||
// await this.db.updateUserStream(client.user.id, null)
|
Logger.error(`[Server] Library Item for session "${session.id}" does not exist "${session.libraryItemId}"`)
|
||||||
// }
|
this.playbackSessionManager.removeSession(session.id)
|
||||||
|
session = null
|
||||||
|
} else {
|
||||||
|
session.libraryItem = sessionLibraryItem.toJSONExpanded()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Logger.debug(`[Server] User Online ${client.user.username}`)
|
||||||
}
|
}
|
||||||
|
this.io.emit('user_online', client.user.toJSONForPublic(this.playbackSessionManager.sessions, this.db.libraryItems))
|
||||||
Logger.debug(`[Server] User Online ${client.user.username}`)
|
|
||||||
this.io.emit('user_online', client.user.toJSONForPublic([]))
|
|
||||||
|
|
||||||
user.lastSeen = Date.now()
|
user.lastSeen = Date.now()
|
||||||
await this.db.updateEntity('user', user)
|
await this.db.updateEntity('user', user)
|
||||||
@ -627,7 +590,7 @@ class Server {
|
|||||||
metadataPath: global.MetadataPath,
|
metadataPath: global.MetadataPath,
|
||||||
configPath: global.ConfigPath,
|
configPath: global.ConfigPath,
|
||||||
user: client.user.toJSONForBrowser(),
|
user: client.user.toJSONForBrowser(),
|
||||||
stream: client.stream || null,
|
session,
|
||||||
librariesScanning: this.scanner.librariesScanning,
|
librariesScanning: this.scanner.librariesScanning,
|
||||||
backups: (this.backupManager.backups || []).map(b => b.toJSON())
|
backups: (this.backupManager.backups || []).map(b => b.toJSON())
|
||||||
}
|
}
|
||||||
|
@ -1,57 +0,0 @@
|
|||||||
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()
|
|
@ -5,7 +5,7 @@ class BackupController {
|
|||||||
|
|
||||||
async delete(req, res) {
|
async delete(req, res) {
|
||||||
if (!req.user.isRoot) {
|
if (!req.user.isRoot) {
|
||||||
Logger.error(`[ApiController] Non-Root user attempting to delete backup`, req.user)
|
Logger.error(`[BackupController] Non-Root user attempting to delete backup`, req.user)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
var backup = this.backupManager.backups.find(b => b.id === req.params.id)
|
var backup = this.backupManager.backups.find(b => b.id === req.params.id)
|
||||||
@ -18,11 +18,11 @@ class BackupController {
|
|||||||
|
|
||||||
async upload(req, res) {
|
async upload(req, res) {
|
||||||
if (!req.user.isRoot) {
|
if (!req.user.isRoot) {
|
||||||
Logger.error(`[ApiController] Non-Root user attempting to upload backup`, req.user)
|
Logger.error(`[BackupController] Non-Root user attempting to upload backup`, req.user)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
if (!req.files.file) {
|
if (!req.files.file) {
|
||||||
Logger.error('[ApiController] Upload backup invalid')
|
Logger.error('[BackupController] Upload backup invalid')
|
||||||
return res.sendStatus(500)
|
return res.sendStatus(500)
|
||||||
}
|
}
|
||||||
this.backupManager.uploadBackup(req, res)
|
this.backupManager.uploadBackup(req, res)
|
||||||
|
@ -344,7 +344,7 @@ class LibraryController {
|
|||||||
// PATCH: Change the order of libraries
|
// PATCH: Change the order of libraries
|
||||||
async reorder(req, res) {
|
async reorder(req, res) {
|
||||||
if (!req.user.isRoot) {
|
if (!req.user.isRoot) {
|
||||||
Logger.error('[ApiController] ReorderLibraries invalid user', req.user)
|
Logger.error('[LibraryController] ReorderLibraries invalid user', req.user)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -353,7 +353,7 @@ class LibraryController {
|
|||||||
for (let i = 0; i < orderdata.length; i++) {
|
for (let i = 0; i < orderdata.length; i++) {
|
||||||
var library = this.db.libraries.find(lib => lib.id === orderdata[i].id)
|
var library = this.db.libraries.find(lib => lib.id === orderdata[i].id)
|
||||||
if (!library) {
|
if (!library) {
|
||||||
Logger.error(`[ApiController] Invalid library not found in reorder ${orderdata[i].id}`)
|
Logger.error(`[LibraryController] Invalid library not found in reorder ${orderdata[i].id}`)
|
||||||
return res.sendStatus(500)
|
return res.sendStatus(500)
|
||||||
}
|
}
|
||||||
if (library.update({ displayOrder: orderdata[i].newOrder })) {
|
if (library.update({ displayOrder: orderdata[i].newOrder })) {
|
||||||
@ -363,9 +363,9 @@ class LibraryController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasUpdates) {
|
if (hasUpdates) {
|
||||||
Logger.info(`[ApiController] Updated library display orders`)
|
Logger.info(`[LibraryController] Updated library display orders`)
|
||||||
} else {
|
} else {
|
||||||
Logger.info(`[ApiController] Library orders were up to date`)
|
Logger.info(`[LibraryController] Library orders were up to date`)
|
||||||
}
|
}
|
||||||
|
|
||||||
var libraries = this.db.libraries.map(lib => lib.toJSON())
|
var libraries = this.db.libraries.map(lib => lib.toJSON())
|
||||||
|
@ -142,9 +142,16 @@ class LibraryItemController {
|
|||||||
res.sendStatus(500)
|
res.sendStatus(500)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET: api/items/:id/play
|
|
||||||
|
// POST: api/items/:id/play
|
||||||
startPlaybackSession(req, res) {
|
startPlaybackSession(req, res) {
|
||||||
res.sendStatus(200)
|
var playbackMediaEntity = req.libraryItem.getPlaybackMediaEntity()
|
||||||
|
if (!playbackMediaEntity) {
|
||||||
|
Logger.error(`[LibraryItemController] startPlaybackSession no playback media entity ${req.libraryItem.id}`)
|
||||||
|
return res.sendStatus(404)
|
||||||
|
}
|
||||||
|
const options = req.body || {}
|
||||||
|
this.playbackSessionManager.startSessionRequest(req.user, req.libraryItem, playbackMediaEntity, options, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST api/items/:id/match
|
// POST api/items/:id/match
|
||||||
|
71
server/controllers/MediaEntityController.js
Normal file
71
server/controllers/MediaEntityController.js
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
|
class MediaEntityController {
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
async findOne(req, res) {
|
||||||
|
if (req.query.expanded == 1) return res.json(req.mediaEntity.toJSONExpanded())
|
||||||
|
return res.json(req.mediaEntity)
|
||||||
|
}
|
||||||
|
|
||||||
|
async findWithItem(req, res) {
|
||||||
|
if (req.query.expanded == 1) {
|
||||||
|
return res.json({
|
||||||
|
libraryItem: req.libraryItem.toJSONExpanded(),
|
||||||
|
mediaEntity: req.mediaEntity.toJSONExpanded()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
res.json({
|
||||||
|
libraryItem: req.libraryItem.toJSON(),
|
||||||
|
mediaEntity: req.mediaEntity.toJSON()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// PATCH: api/entities/:id/tracks
|
||||||
|
async updateTracks(req, res) {
|
||||||
|
var libraryItem = req.libraryItem
|
||||||
|
var mediaEntity = req.mediaEntity
|
||||||
|
var orderedFileData = req.body.orderedFileData
|
||||||
|
if (!mediaEntity.updateAudioTracks) {
|
||||||
|
Logger.error(`[MediaEntityController] updateTracks invalid media entity ${mediaEntity.id}`)
|
||||||
|
return res.sendStatus(500)
|
||||||
|
}
|
||||||
|
mediaEntity.updateAudioTracks(orderedFileData)
|
||||||
|
await this.db.updateLibraryItem(libraryItem)
|
||||||
|
this.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||||
|
res.json(libraryItem.toJSON())
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: api/entities/:id/play
|
||||||
|
startPlaybackSession(req, res) {
|
||||||
|
if (!req.mediaEntity.isPlaybackMediaEntity) {
|
||||||
|
Logger.error(`[MediaEntityController] startPlaybackSession invalid media entity ${req.mediaEntity.id}`)
|
||||||
|
return res.sendStatus(500)
|
||||||
|
}
|
||||||
|
const options = req.body || {}
|
||||||
|
this.playbackSessionManager.startSessionRequest(req.user, req.libraryItem, req.mediaEntity, options, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
middleware(req, res, next) {
|
||||||
|
var mediaEntity = null
|
||||||
|
var libraryItem = this.db.libraryItems.find(li => {
|
||||||
|
if (li.mediaType != 'book') return false
|
||||||
|
mediaEntity = li.media.getMediaEntityById(req.params.id)
|
||||||
|
return !!mediaEntity
|
||||||
|
})
|
||||||
|
if (!mediaEntity) return res.sendStatus(404)
|
||||||
|
|
||||||
|
if (req.method == 'DELETE' && !req.user.canDelete) {
|
||||||
|
Logger.warn(`[MediaEntityController] 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('[MediaEntityController] User attempted to update without permission', req.user)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.mediaEntity = mediaEntity
|
||||||
|
req.libraryItem = libraryItem
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = new MediaEntityController()
|
33
server/controllers/SessionController.js
Normal file
33
server/controllers/SessionController.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
|
class SessionController {
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
async findOne(req, res) {
|
||||||
|
return res.json(req.session)
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: api/session/:id/sync
|
||||||
|
sync(req, res) {
|
||||||
|
this.playbackSessionManager.syncSessionRequest(req.user, req.session, req.body, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: api/session/:id/close
|
||||||
|
close(req, res) {
|
||||||
|
this.playbackSessionManager.closeSessionRequest(req.user, req.session, req.body, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
middleware(req, res, next) {
|
||||||
|
var playbackSession = this.playbackSessionManager.getSession(req.params.id)
|
||||||
|
if (!playbackSession) return res.sendStatus(404)
|
||||||
|
|
||||||
|
if (playbackSession.userId !== req.user.id) {
|
||||||
|
Logger.error(`[SessionController] User "${req.user.username}" attempting to access session belonging to another user "${req.params.id}"`)
|
||||||
|
return res.sendStatus(404)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.session = playbackSession
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = new SessionController()
|
@ -6,6 +6,26 @@ const { getId } = require('../utils/index')
|
|||||||
class UserController {
|
class UserController {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
|
findAll(req, res) {
|
||||||
|
if (!req.user.isRoot) return res.sendStatus(403)
|
||||||
|
var users = this.db.users.map(u => this.userJsonWithItemProgressDetails(u))
|
||||||
|
res.json(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
findOne(req, res) {
|
||||||
|
if (!req.user.isRoot) {
|
||||||
|
Logger.error('User other than root attempting to get user', req.user)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = this.db.users.find(u => u.id === req.params.id)
|
||||||
|
if (!user) {
|
||||||
|
return res.sendStatus(404)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(this.userJsonWithItemProgressDetails(user))
|
||||||
|
}
|
||||||
|
|
||||||
async create(req, res) {
|
async create(req, res) {
|
||||||
if (!req.user.isRoot) {
|
if (!req.user.isRoot) {
|
||||||
Logger.warn('Non-root user attempted to create user', req.user)
|
Logger.warn('Non-root user attempted to create user', req.user)
|
||||||
@ -36,26 +56,6 @@ class UserController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
findAll(req, res) {
|
|
||||||
if (!req.user.isRoot) return res.sendStatus(403)
|
|
||||||
var users = this.db.users.map(u => this.userJsonWithBookProgressDetails(u))
|
|
||||||
res.json(users)
|
|
||||||
}
|
|
||||||
|
|
||||||
findOne(req, res) {
|
|
||||||
if (!req.user.isRoot) {
|
|
||||||
Logger.error('User other than root attempting to get user', req.user)
|
|
||||||
return res.sendStatus(403)
|
|
||||||
}
|
|
||||||
|
|
||||||
var user = this.db.users.find(u => u.id === req.params.id)
|
|
||||||
if (!user) {
|
|
||||||
return res.sendStatus(404)
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json(this.userJsonWithBookProgressDetails(user))
|
|
||||||
}
|
|
||||||
|
|
||||||
async update(req, res) {
|
async update(req, res) {
|
||||||
if (!req.user.isRoot) {
|
if (!req.user.isRoot) {
|
||||||
Logger.error('User other than root attempting to update user', req.user)
|
Logger.error('User other than root attempting to update user', req.user)
|
||||||
|
@ -434,8 +434,8 @@ class LibraryItem {
|
|||||||
return this.media.searchQuery(query)
|
return this.media.searchQuery(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
getDirectPlayTracklist(options) {
|
getPlaybackMediaEntity() {
|
||||||
return this.media.getDirectPlayTracklist(options)
|
return this.media.getPlaybackMediaEntity()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = LibraryItem
|
module.exports = LibraryItem
|
@ -9,8 +9,11 @@ class PlaybackSession {
|
|||||||
this.id = null
|
this.id = null
|
||||||
this.userId = null
|
this.userId = null
|
||||||
this.libraryItemId = null
|
this.libraryItemId = null
|
||||||
|
this.mediaEntityId = null
|
||||||
|
|
||||||
this.mediaType = null
|
this.mediaType = null
|
||||||
this.mediaMetadata = null
|
this.mediaMetadata = null
|
||||||
|
this.duration = null
|
||||||
|
|
||||||
this.playMethod = null
|
this.playMethod = null
|
||||||
|
|
||||||
@ -21,6 +24,12 @@ class PlaybackSession {
|
|||||||
this.startedAt = null
|
this.startedAt = null
|
||||||
this.updatedAt = null
|
this.updatedAt = null
|
||||||
|
|
||||||
|
// Not saved in DB
|
||||||
|
this.lastSave = 0
|
||||||
|
this.audioTracks = []
|
||||||
|
this.currentTime = 0
|
||||||
|
this.stream = null
|
||||||
|
|
||||||
if (session) {
|
if (session) {
|
||||||
this.construct(session)
|
this.construct(session)
|
||||||
}
|
}
|
||||||
@ -32,8 +41,10 @@ class PlaybackSession {
|
|||||||
sessionType: this.sessionType,
|
sessionType: this.sessionType,
|
||||||
userId: this.userId,
|
userId: this.userId,
|
||||||
libraryItemId: this.libraryItemId,
|
libraryItemId: this.libraryItemId,
|
||||||
|
mediaEntityId: this.mediaEntityId,
|
||||||
mediaType: this.mediaType,
|
mediaType: this.mediaType,
|
||||||
mediaMetadata: this.mediaMetadata ? this.mediaMetadata.toJSON() : null,
|
mediaMetadata: this.mediaMetadata ? this.mediaMetadata.toJSON() : null,
|
||||||
|
duration: this.duration,
|
||||||
playMethod: this.playMethod,
|
playMethod: this.playMethod,
|
||||||
date: this.date,
|
date: this.date,
|
||||||
dayOfWeek: this.dayOfWeek,
|
dayOfWeek: this.dayOfWeek,
|
||||||
@ -43,12 +54,35 @@ class PlaybackSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toJSONForClient() {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
sessionType: this.sessionType,
|
||||||
|
userId: this.userId,
|
||||||
|
libraryItemId: this.libraryItemId,
|
||||||
|
mediaEntityId: this.mediaEntityId,
|
||||||
|
mediaType: this.mediaType,
|
||||||
|
mediaMetadata: this.mediaMetadata ? this.mediaMetadata.toJSON() : null,
|
||||||
|
duration: this.duration,
|
||||||
|
playMethod: this.playMethod,
|
||||||
|
date: this.date,
|
||||||
|
dayOfWeek: this.dayOfWeek,
|
||||||
|
timeListening: this.timeListening,
|
||||||
|
lastUpdate: this.lastUpdate,
|
||||||
|
updatedAt: this.updatedAt,
|
||||||
|
audioTracks: this.audioTracks.map(at => at.toJSON()),
|
||||||
|
currentTime: this.currentTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
construct(session) {
|
construct(session) {
|
||||||
this.id = session.id
|
this.id = session.id
|
||||||
this.sessionType = session.sessionType
|
this.sessionType = session.sessionType
|
||||||
this.userId = session.userId
|
this.userId = session.userId
|
||||||
this.libraryItemId = session.libraryItemId
|
this.libraryItemId = session.libraryItemId
|
||||||
|
this.mediaEntityId = session.mediaEntityId
|
||||||
this.mediaType = session.mediaType
|
this.mediaType = session.mediaType
|
||||||
|
this.duration = session.duration
|
||||||
this.playMethod = session.playMethod
|
this.playMethod = session.playMethod
|
||||||
|
|
||||||
this.mediaMetadata = null
|
this.mediaMetadata = null
|
||||||
@ -68,30 +102,38 @@ class PlaybackSession {
|
|||||||
this.updatedAt = session.updatedAt || null
|
this.updatedAt = session.updatedAt || null
|
||||||
}
|
}
|
||||||
|
|
||||||
setData(libraryItem, user) {
|
get progress() { // Value between 0 and 1
|
||||||
this.id = getId('ls')
|
if (!this.duration) return 0
|
||||||
|
return Math.max(0, Math.min(this.currentTime / this.duration, 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
setData(libraryItem, mediaEntity, user) {
|
||||||
|
this.id = getId('play')
|
||||||
this.userId = user.id
|
this.userId = user.id
|
||||||
this.libraryItemId = libraryItem.id
|
this.libraryItemId = libraryItem.id
|
||||||
|
this.mediaEntityId = mediaEntity.id
|
||||||
this.mediaType = libraryItem.mediaType
|
this.mediaType = libraryItem.mediaType
|
||||||
this.mediaMetadata = libraryItem.media.metadata.clone()
|
this.mediaMetadata = libraryItem.media.metadata.clone()
|
||||||
this.playMethod = PlayMethod.TRANSCODE
|
this.duration = mediaEntity.duration
|
||||||
|
|
||||||
this.timeListening = 0
|
this.timeListening = 0
|
||||||
|
this.date = date.format(new Date(), 'YYYY-MM-DD')
|
||||||
|
this.dayOfWeek = date.format(new Date(), 'dddd')
|
||||||
this.startedAt = Date.now()
|
this.startedAt = Date.now()
|
||||||
this.updatedAt = Date.now()
|
this.updatedAt = Date.now()
|
||||||
}
|
}
|
||||||
|
|
||||||
addListeningTime(timeListened) {
|
addListeningTime(timeListened) {
|
||||||
if (timeListened && !isNaN(timeListened)) {
|
if (!timeListened || isNaN(timeListened)) return
|
||||||
if (!this.date) {
|
|
||||||
// Set date info on first listening update
|
|
||||||
this.date = date.format(new Date(), 'YYYY-MM-DD')
|
|
||||||
this.dayOfWeek = date.format(new Date(), 'dddd')
|
|
||||||
}
|
|
||||||
|
|
||||||
this.timeListening += timeListened
|
if (!this.date) {
|
||||||
this.updatedAt = Date.now()
|
// Set date info on first listening update
|
||||||
|
this.date = date.format(new Date(), 'YYYY-MM-DD')
|
||||||
|
this.dayOfWeek = date.format(new Date(), 'dddd')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.timeListening += timeListened
|
||||||
|
this.updatedAt = Date.now()
|
||||||
}
|
}
|
||||||
|
|
||||||
// New date since start of listening session
|
// New date since start of listening session
|
||||||
|
@ -6,16 +6,17 @@ const Logger = require('../Logger')
|
|||||||
const { getId, secondsToTimestamp } = require('../utils/index')
|
const { getId, secondsToTimestamp } = require('../utils/index')
|
||||||
const { writeConcatFile } = require('../utils/ffmpegHelpers')
|
const { writeConcatFile } = require('../utils/ffmpegHelpers')
|
||||||
const hlsPlaylistGenerator = require('../utils/hlsPlaylistGenerator')
|
const hlsPlaylistGenerator = require('../utils/hlsPlaylistGenerator')
|
||||||
|
const AudioTrack = require('./files/AudioTrack')
|
||||||
const UserListeningSession = require('./legacy/UserListeningSession')
|
|
||||||
|
|
||||||
class Stream extends EventEmitter {
|
class Stream extends EventEmitter {
|
||||||
constructor(streamPath, client, libraryItem, transcodeOptions = {}) {
|
constructor(sessionId, streamPath, user, libraryItem, mediaEntity, startTime, clientEmitter, transcodeOptions = {}) {
|
||||||
super()
|
super()
|
||||||
|
|
||||||
this.id = getId('str')
|
this.id = sessionId
|
||||||
this.client = client
|
this.user = user
|
||||||
this.libraryItem = libraryItem
|
this.libraryItem = libraryItem
|
||||||
|
this.mediaEntity = mediaEntity
|
||||||
|
this.clientEmitter = clientEmitter
|
||||||
|
|
||||||
this.transcodeOptions = transcodeOptions
|
this.transcodeOptions = transcodeOptions
|
||||||
|
|
||||||
@ -25,7 +26,7 @@ class Stream extends EventEmitter {
|
|||||||
this.concatFilesPath = Path.join(this.streamPath, 'files.txt')
|
this.concatFilesPath = Path.join(this.streamPath, 'files.txt')
|
||||||
this.playlistPath = Path.join(this.streamPath, 'output.m3u8')
|
this.playlistPath = Path.join(this.streamPath, 'output.m3u8')
|
||||||
this.finalPlaylistPath = Path.join(this.streamPath, 'final-output.m3u8')
|
this.finalPlaylistPath = Path.join(this.streamPath, 'final-output.m3u8')
|
||||||
this.startTime = 0
|
this.startTime = startTime
|
||||||
|
|
||||||
this.ffmpeg = null
|
this.ffmpeg = null
|
||||||
this.loop = null
|
this.loop = null
|
||||||
@ -34,53 +35,49 @@ class Stream extends EventEmitter {
|
|||||||
this.isTranscodeComplete = false
|
this.isTranscodeComplete = false
|
||||||
this.segmentsCreated = new Set()
|
this.segmentsCreated = new Set()
|
||||||
this.furthestSegmentCreated = 0
|
this.furthestSegmentCreated = 0
|
||||||
this.clientCurrentTime = 0
|
// this.clientCurrentTime = 0
|
||||||
|
|
||||||
this.listeningSession = new UserListeningSession()
|
|
||||||
this.listeningSession.setData(libraryItem, client.user)
|
|
||||||
|
|
||||||
this.init()
|
this.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
get socket() {
|
|
||||||
return this.client ? this.client.socket || null : null
|
|
||||||
}
|
|
||||||
|
|
||||||
get libraryItemId() {
|
get libraryItemId() {
|
||||||
return this.libraryItem.id
|
return this.libraryItem.id
|
||||||
}
|
}
|
||||||
|
get mediaTitle() {
|
||||||
|
return this.libraryItem.media.metadata.title || ''
|
||||||
|
}
|
||||||
|
get mediaEntityName() {
|
||||||
|
return this.mediaEntity.name
|
||||||
|
}
|
||||||
get itemTitle() {
|
get itemTitle() {
|
||||||
return this.libraryItem ? this.libraryItem.media.metadata.title : null
|
return `${this.mediaTitle} (${this.mediaEntityName})`
|
||||||
}
|
}
|
||||||
|
|
||||||
get totalDuration() {
|
get totalDuration() {
|
||||||
return this.libraryItem.media.duration
|
return this.mediaEntity.duration
|
||||||
|
}
|
||||||
|
get tracks() {
|
||||||
|
return this.mediaEntity.tracks
|
||||||
}
|
}
|
||||||
|
|
||||||
get tracksAudioFileType() {
|
get tracksAudioFileType() {
|
||||||
if (!this.tracks.length) return null
|
if (!this.tracks.length) return null
|
||||||
return this.tracks[0].metadata.ext.toLowerCase().slice(1)
|
return this.tracks[0].metadata.format
|
||||||
|
}
|
||||||
|
get userToken() {
|
||||||
|
return this.user.token
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fmp4 does not work on iOS devices: https://github.com/advplyr/audiobookshelf-app/issues/85
|
// Fmp4 does not work on iOS devices: https://github.com/advplyr/audiobookshelf-app/issues/85
|
||||||
// Workaround: Force AAC transcode for FLAC
|
// Workaround: Force AAC transcode for FLAC
|
||||||
get hlsSegmentType() {
|
get hlsSegmentType() {
|
||||||
return 'mpegts'
|
return 'mpegts'
|
||||||
// var hasFlac = this.tracks.find(t => t.ext.toLowerCase() === '.flac')
|
|
||||||
// return hasFlac ? 'fmp4' : 'mpegts'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get segmentBasename() {
|
get segmentBasename() {
|
||||||
if (this.hlsSegmentType === 'fmp4') return 'output-%d.m4s'
|
if (this.hlsSegmentType === 'fmp4') return 'output-%d.m4s'
|
||||||
return 'output-%d.ts'
|
return 'output-%d.ts'
|
||||||
}
|
}
|
||||||
|
|
||||||
get segmentStartNumber() {
|
get segmentStartNumber() {
|
||||||
if (!this.startTime) return 0
|
if (!this.startTime) return 0
|
||||||
return Math.floor(Math.max(this.startTime - this.maxSeekBackTime, 0) / this.segmentLength)
|
return Math.floor(Math.max(this.startTime - this.maxSeekBackTime, 0) / this.segmentLength)
|
||||||
}
|
}
|
||||||
|
|
||||||
get numSegments() {
|
get numSegments() {
|
||||||
var numSegs = Math.floor(this.totalDuration / this.segmentLength)
|
var numSegs = Math.floor(this.totalDuration / this.segmentLength)
|
||||||
if (this.totalDuration - (numSegs * this.segmentLength) > 0) {
|
if (this.totalDuration - (numSegs * this.segmentLength) > 0) {
|
||||||
@ -88,41 +85,17 @@ class Stream extends EventEmitter {
|
|||||||
}
|
}
|
||||||
return numSegs
|
return numSegs
|
||||||
}
|
}
|
||||||
|
|
||||||
get tracks() {
|
|
||||||
return this.libraryItem.media.tracks
|
|
||||||
}
|
|
||||||
|
|
||||||
get clientUser() {
|
|
||||||
return this.client ? this.client.user || {} : null
|
|
||||||
}
|
|
||||||
|
|
||||||
get userToken() {
|
|
||||||
return this.clientUser ? this.clientUser.token : null
|
|
||||||
}
|
|
||||||
|
|
||||||
get clientUserAudiobooks() {
|
|
||||||
return this.client ? this.clientUser.audiobooks || {} : null
|
|
||||||
}
|
|
||||||
|
|
||||||
get clientUserAudiobookData() {
|
|
||||||
return this.client ? this.clientUserAudiobooks[this.libraryItemId] : null
|
|
||||||
}
|
|
||||||
|
|
||||||
get clientPlaylistUri() {
|
get clientPlaylistUri() {
|
||||||
return `/hls/${this.id}/output.m3u8`
|
return `/hls/${this.id}/output.m3u8`
|
||||||
}
|
}
|
||||||
|
// get clientProgress() {
|
||||||
get clientProgress() {
|
// if (!this.clientCurrentTime) return 0
|
||||||
if (!this.clientCurrentTime) return 0
|
// var prog = Math.min(1, this.clientCurrentTime / this.totalDuration)
|
||||||
var prog = Math.min(1, this.clientCurrentTime / this.totalDuration)
|
// return Number(prog.toFixed(3))
|
||||||
return Number(prog.toFixed(3))
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
get isAACEncodable() {
|
get isAACEncodable() {
|
||||||
return ['mp4', 'm4a', 'm4b'].includes(this.tracksAudioFileType)
|
return ['mp4', 'm4a', 'm4b'].includes(this.tracksAudioFileType)
|
||||||
}
|
}
|
||||||
|
|
||||||
get transcodeForceAAC() {
|
get transcodeForceAAC() {
|
||||||
return !!this.transcodeOptions.forceAAC
|
return !!this.transcodeOptions.forceAAC
|
||||||
}
|
}
|
||||||
@ -130,29 +103,28 @@ class Stream extends EventEmitter {
|
|||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
clientId: this.client.id,
|
userId: this.user.id,
|
||||||
userId: this.client.user.id,
|
|
||||||
libraryItem: this.libraryItem.toJSONExpanded(),
|
libraryItem: this.libraryItem.toJSONExpanded(),
|
||||||
segmentLength: this.segmentLength,
|
segmentLength: this.segmentLength,
|
||||||
playlistPath: this.playlistPath,
|
playlistPath: this.playlistPath,
|
||||||
clientPlaylistUri: this.clientPlaylistUri,
|
clientPlaylistUri: this.clientPlaylistUri,
|
||||||
clientCurrentTime: this.clientCurrentTime,
|
// clientCurrentTime: this.clientCurrentTime,
|
||||||
startTime: this.startTime,
|
startTime: this.startTime,
|
||||||
segmentStartNumber: this.segmentStartNumber,
|
segmentStartNumber: this.segmentStartNumber,
|
||||||
isTranscodeComplete: this.isTranscodeComplete,
|
isTranscodeComplete: this.isTranscodeComplete,
|
||||||
lastUpdate: this.clientUserAudiobookData ? this.clientUserAudiobookData.lastUpdate : 0
|
// lastUpdate: this.clientUserAudiobookData ? this.clientUserAudiobookData.lastUpdate : 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
if (this.clientUserAudiobookData) {
|
// if (this.clientUserAudiobookData) {
|
||||||
var timeRemaining = this.totalDuration - this.clientUserAudiobookData.currentTime
|
// var timeRemaining = this.totalDuration - this.clientUserAudiobookData.currentTime
|
||||||
Logger.info('[STREAM] User has progress for item', this.clientUserAudiobookData.progress, `Time Remaining: ${timeRemaining}s`)
|
// Logger.info('[STREAM] User has progress for item', this.clientUserAudiobookData.progress, `Time Remaining: ${timeRemaining}s`)
|
||||||
if (timeRemaining > 15) {
|
// if (timeRemaining > 15) {
|
||||||
this.startTime = this.clientUserAudiobookData.currentTime
|
// this.startTime = this.clientUserAudiobookData.currentTime
|
||||||
this.clientCurrentTime = this.startTime
|
// this.clientCurrentTime = this.startTime
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkSegmentNumberRequest(segNum) {
|
async checkSegmentNumberRequest(segNum) {
|
||||||
@ -175,39 +147,6 @@ class Stream extends EventEmitter {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
syncStream({ timeListened, currentTime }) {
|
|
||||||
var syncLog = ''
|
|
||||||
// Set user current time
|
|
||||||
if (currentTime !== null && !isNaN(currentTime)) {
|
|
||||||
syncLog = `Update client current time ${secondsToTimestamp(currentTime)}`
|
|
||||||
this.clientCurrentTime = currentTime
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update user listening session
|
|
||||||
var saveListeningSession = false
|
|
||||||
if (timeListened && !isNaN(timeListened)) {
|
|
||||||
|
|
||||||
// Check if listening session should roll to next day
|
|
||||||
if (this.listeningSession.checkDateRollover()) {
|
|
||||||
if (!this.clientUser) {
|
|
||||||
Logger.error(`[Stream] Sync stream invalid client user`)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
this.listeningSession = new UserListeningSession()
|
|
||||||
this.listeningSession.setData(this.libraryItem, this.clientUser)
|
|
||||||
Logger.debug(`[Stream] Listening session rolled to next day`)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.listeningSession.addListeningTime(timeListened)
|
|
||||||
if (syncLog) syncLog += ' | '
|
|
||||||
syncLog += `Add listening time ${timeListened}s, Total time listened ${this.listeningSession.timeListening}s`
|
|
||||||
saveListeningSession = true
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.debug('[Stream]', syncLog)
|
|
||||||
return saveListeningSession ? this.listeningSession : null
|
|
||||||
}
|
|
||||||
|
|
||||||
async generatePlaylist() {
|
async generatePlaylist() {
|
||||||
fs.ensureDirSync(this.streamPath)
|
fs.ensureDirSync(this.streamPath)
|
||||||
await hlsPlaylistGenerator(this.playlistPath, 'output', this.totalDuration, this.segmentLength, this.hlsSegmentType, this.userToken)
|
await hlsPlaylistGenerator(this.playlistPath, 'output', this.totalDuration, this.segmentLength, this.hlsSegmentType, this.userToken)
|
||||||
@ -234,10 +173,8 @@ class Stream extends EventEmitter {
|
|||||||
|
|
||||||
if (this.segmentsCreated.size > 6 && !this.isClientInitialized) {
|
if (this.segmentsCreated.size > 6 && !this.isClientInitialized) {
|
||||||
this.isClientInitialized = true
|
this.isClientInitialized = true
|
||||||
if (this.socket) {
|
Logger.info(`[STREAM] ${this.id} notifying client that stream is ready`)
|
||||||
Logger.info(`[STREAM] ${this.id} notifying client that stream is ready`)
|
this.clientEmit('stream_open', this.toJSON())
|
||||||
this.socket.emit('stream_open', this.toJSON())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var chunks = []
|
var chunks = []
|
||||||
@ -270,33 +207,27 @@ class Stream extends EventEmitter {
|
|||||||
Logger.info('[STREAM-CHECK] Check Files', this.segmentsCreated.size, 'of', this.numSegments, perc, `Furthest Segment: ${this.furthestSegmentCreated}`)
|
Logger.info('[STREAM-CHECK] Check Files', this.segmentsCreated.size, 'of', this.numSegments, perc, `Furthest Segment: ${this.furthestSegmentCreated}`)
|
||||||
// Logger.debug('[STREAM-CHECK] Chunks', chunks.join(', '))
|
// Logger.debug('[STREAM-CHECK] Chunks', chunks.join(', '))
|
||||||
|
|
||||||
if (this.socket) {
|
this.clientEmit('stream_progress', {
|
||||||
this.socket.emit('stream_progress', {
|
stream: this.id,
|
||||||
stream: this.id,
|
percent: perc,
|
||||||
percent: perc,
|
chunks,
|
||||||
chunks,
|
numSegments: this.numSegments
|
||||||
numSegments: this.numSegments
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error('Failed checking files', error)
|
Logger.error('Failed checking files', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
startLoop() {
|
startLoop() {
|
||||||
if (this.socket) {
|
this.clientEmit('stream_progress', { stream: this.id, chunks: [], numSegments: 0, percent: '0%' })
|
||||||
this.socket.emit('stream_progress', { stream: this.id, chunks: [], numSegments: 0, percent: '0%' })
|
|
||||||
}
|
|
||||||
|
|
||||||
clearInterval(this.loop)
|
clearInterval(this.loop)
|
||||||
var intervalId = setInterval(() => {
|
var intervalId = setInterval(() => {
|
||||||
if (!this.isTranscodeComplete) {
|
if (!this.isTranscodeComplete) {
|
||||||
this.checkFiles()
|
this.checkFiles()
|
||||||
} else {
|
} else {
|
||||||
if (this.socket) {
|
Logger.info(`[Stream] ${this.itemTitle} sending stream_ready`)
|
||||||
Logger.info(`[Stream] ${this.itemTitle} sending stream_ready`)
|
this.clientEmit('stream_ready')
|
||||||
this.socket.emit('stream_ready')
|
|
||||||
}
|
|
||||||
clearInterval(intervalId)
|
clearInterval(intervalId)
|
||||||
}
|
}
|
||||||
}, 2000)
|
}, 2000)
|
||||||
@ -409,10 +340,10 @@ class Stream extends EventEmitter {
|
|||||||
// For very small fast load
|
// For very small fast load
|
||||||
if (!this.isClientInitialized) {
|
if (!this.isClientInitialized) {
|
||||||
this.isClientInitialized = true
|
this.isClientInitialized = true
|
||||||
if (this.socket) {
|
|
||||||
Logger.info(`[STREAM] ${this.id} notifying client that stream is ready`)
|
Logger.info(`[STREAM] ${this.id} notifying client that stream is ready`)
|
||||||
this.socket.emit('stream_open', this.toJSON())
|
this.clientEmit('stream_open', this.toJSON())
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.isTranscodeComplete = true
|
this.isTranscodeComplete = true
|
||||||
this.ffmpeg = null
|
this.ffmpeg = null
|
||||||
@ -436,10 +367,8 @@ class Stream extends EventEmitter {
|
|||||||
Logger.error('Failed to delete session data', err)
|
Logger.error('Failed to delete session data', err)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (this.socket) {
|
if (errorMessage) this.clientEmit('stream_error', { id: this.id, error: (errorMessage || '').trim() })
|
||||||
if (errorMessage) this.socket.emit('stream_error', { id: this.id, error: (errorMessage || '').trim() })
|
else this.clientEmit('stream_closed', this.id)
|
||||||
else this.socket.emit('stream_closed', this.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emit('closed')
|
this.emit('closed')
|
||||||
}
|
}
|
||||||
@ -474,9 +403,19 @@ class Stream extends EventEmitter {
|
|||||||
|
|
||||||
this.isTranscodeComplete = false
|
this.isTranscodeComplete = false
|
||||||
this.startTime = time
|
this.startTime = time
|
||||||
this.clientCurrentTime = this.startTime
|
// this.clientCurrentTime = this.startTime
|
||||||
Logger.info(`Stream Reset New Start Time ${secondsToTimestamp(this.startTime)}`)
|
Logger.info(`Stream Reset New Start Time ${secondsToTimestamp(this.startTime)}`)
|
||||||
this.start()
|
this.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clientEmit(evtName, data) {
|
||||||
|
if (this.clientEmitter) this.clientEmitter(this.user.id, evtName, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
getAudioTrack() {
|
||||||
|
var newAudioTrack = new AudioTrack()
|
||||||
|
newAudioTrack.setFromStream(this.itemTitle, this.totalDuration, this.clientPlaylistUri)
|
||||||
|
return newAudioTrack
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = Stream
|
module.exports = Stream
|
@ -1,6 +1,7 @@
|
|||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const AudioFile = require('../files/AudioFile')
|
const AudioFile = require('../files/AudioFile')
|
||||||
const { areEquivalent, copyValue } = require('../../utils/index')
|
const { areEquivalent, copyValue } = require('../../utils/index')
|
||||||
|
const AudioTrack = require('../files/AudioTrack')
|
||||||
|
|
||||||
class Audiobook {
|
class Audiobook {
|
||||||
constructor(audiobook) {
|
constructor(audiobook) {
|
||||||
@ -74,6 +75,7 @@ class Audiobook {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isPlaybackMediaEntity() { return true }
|
||||||
get tracks() {
|
get tracks() {
|
||||||
return this.audioFiles.filter(af => !af.exclude && !af.invalid)
|
return this.audioFiles.filter(af => !af.exclude && !af.invalid)
|
||||||
}
|
}
|
||||||
@ -214,5 +216,25 @@ class Audiobook {
|
|||||||
removeFileWithInode(inode) {
|
removeFileWithInode(inode) {
|
||||||
this.audioFiles = this.audioFiles.filter(af => af.ino !== inode)
|
this.audioFiles = this.audioFiles.filter(af => af.ino !== inode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only checks container format
|
||||||
|
checkCanDirectPlay(payload) {
|
||||||
|
var supportedMimeTypes = payload.supportedMimeTypes || []
|
||||||
|
return !this.tracks.some((t) => !supportedMimeTypes.includes(t.mimeType))
|
||||||
|
}
|
||||||
|
|
||||||
|
getDirectPlayTracklist(libraryItemId) {
|
||||||
|
var tracklist = []
|
||||||
|
|
||||||
|
var startOffset = 0
|
||||||
|
this.tracks.forEach((audioFile) => {
|
||||||
|
var audioTrack = new AudioTrack()
|
||||||
|
audioTrack.setData(libraryItemId, audioFile, startOffset)
|
||||||
|
startOffset += audioTrack.duration
|
||||||
|
tracklist.push(audioTrack)
|
||||||
|
})
|
||||||
|
|
||||||
|
return tracklist
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = Audiobook
|
module.exports = Audiobook
|
@ -47,7 +47,7 @@ class EBook {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSONMinified() {
|
toJSONExpanded() {
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
index: this.index,
|
index: this.index,
|
||||||
@ -59,6 +59,7 @@ class EBook {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isPlaybackMediaEntity() { return false }
|
||||||
get size() {
|
get size() {
|
||||||
return this.ebookFile.metadata.size
|
return this.ebookFile.metadata.size
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
const AudioFile = require('../files/AudioFile')
|
const AudioFile = require('../files/AudioFile')
|
||||||
|
const AudioTrack = require('../files/AudioTrack')
|
||||||
|
|
||||||
class PodcastEpisode {
|
class PodcastEpisode {
|
||||||
constructor(episode) {
|
constructor(episode) {
|
||||||
@ -37,5 +38,22 @@ class PodcastEpisode {
|
|||||||
updatedAt: this.updatedAt
|
updatedAt: this.updatedAt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isPlaybackMediaEntity() { return true }
|
||||||
|
get tracks() {
|
||||||
|
return [this.audioFile]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only checks container format
|
||||||
|
checkCanDirectPlay(payload) {
|
||||||
|
var supportedMimeTypes = payload.supportedMimeTypes || []
|
||||||
|
return supportedMimeTypes.includes(this.audioFile.mimeType)
|
||||||
|
}
|
||||||
|
|
||||||
|
getDirectPlayTracklist(libraryItemId) {
|
||||||
|
var audioTrack = new AudioTrack()
|
||||||
|
audioTrack.setData(libraryItemId, this.audioFile, 0)
|
||||||
|
return [audioTrack]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = PodcastEpisode
|
module.exports = PodcastEpisode
|
@ -64,7 +64,8 @@ class AudioFile {
|
|||||||
channelLayout: this.channelLayout,
|
channelLayout: this.channelLayout,
|
||||||
chapters: this.chapters,
|
chapters: this.chapters,
|
||||||
embeddedCoverArt: this.embeddedCoverArt,
|
embeddedCoverArt: this.embeddedCoverArt,
|
||||||
metaTags: this.metaTags ? this.metaTags.toJSON() : {}
|
metaTags: this.metaTags ? this.metaTags.toJSON() : {},
|
||||||
|
mimeType: this.mimeType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,9 +73,6 @@ class AudioFile {
|
|||||||
this.index = data.index
|
this.index = data.index
|
||||||
this.ino = data.ino
|
this.ino = data.ino
|
||||||
this.metadata = new FileMetadata(data.metadata || {})
|
this.metadata = new FileMetadata(data.metadata || {})
|
||||||
if (!this.metadata.toJSON) {
|
|
||||||
console.error('No metadata tojosnm\n\n\n\n\n\n', this)
|
|
||||||
}
|
|
||||||
this.addedAt = data.addedAt
|
this.addedAt = data.addedAt
|
||||||
this.updatedAt = data.updatedAt
|
this.updatedAt = data.updatedAt
|
||||||
this.manuallyVerified = !!data.manuallyVerified
|
this.manuallyVerified = !!data.manuallyVerified
|
||||||
@ -103,6 +101,22 @@ class AudioFile {
|
|||||||
this.metaTags = new AudioMetaTags(data.metaTags || {})
|
this.metaTags = new AudioMetaTags(data.metaTags || {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get mimeType() {
|
||||||
|
var ext = this.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'
|
||||||
|
}
|
||||||
|
return 'audio/mpeg'
|
||||||
|
}
|
||||||
|
|
||||||
// New scanner creates AudioFile from AudioFileScanner
|
// New scanner creates AudioFile from AudioFileScanner
|
||||||
setDataFromProbe(libraryFile, probeData) {
|
setDataFromProbe(libraryFile, probeData) {
|
||||||
this.ino = libraryFile.ino || null
|
this.ino = libraryFile.ino || null
|
||||||
|
42
server/objects/files/AudioTrack.js
Normal file
42
server/objects/files/AudioTrack.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
const Path = require('path')
|
||||||
|
|
||||||
|
class AudioTrack {
|
||||||
|
constructor() {
|
||||||
|
this.index = null
|
||||||
|
this.startOffset = null
|
||||||
|
this.duration = null
|
||||||
|
this.title = null
|
||||||
|
this.contentUrl = null
|
||||||
|
this.mimeType = null
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
index: this.index,
|
||||||
|
startOffset: this.startOffset,
|
||||||
|
duration: this.duration,
|
||||||
|
title: this.title,
|
||||||
|
contentUrl: this.contentUrl,
|
||||||
|
mimeType: this.mimeType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setData(itemId, audioFile, startOffset) {
|
||||||
|
this.index = audioFile.index
|
||||||
|
this.startOffset = startOffset
|
||||||
|
this.duration = audioFile.duration
|
||||||
|
this.title = audioFile.metadata.filename || ''
|
||||||
|
this.contentUrl = Path.join(`/s/item/${itemId}`, audioFile.metadata.relPath)
|
||||||
|
this.mimeType = audioFile.mimeType
|
||||||
|
}
|
||||||
|
|
||||||
|
setFromStream(title, duration, contentUrl) {
|
||||||
|
this.index = 1
|
||||||
|
this.startOffset = 0
|
||||||
|
this.duration = duration
|
||||||
|
this.title = title
|
||||||
|
this.contentUrl = contentUrl
|
||||||
|
this.mimeType = 'application/vnd.apple.mpegurl'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = AudioTrack
|
@ -108,8 +108,6 @@ class StreamManager {
|
|||||||
|
|
||||||
var stream = await this.openStream(client, libraryItem)
|
var stream = await this.openStream(client, libraryItem)
|
||||||
this.db.updateUserStream(client.user.id, stream.id)
|
this.db.updateUserStream(client.user.id, stream.id)
|
||||||
|
|
||||||
this.emitter('user_stream_update', client.user.toJSONForPublic(this.streams))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async closeStreamRequest(socket) {
|
async closeStreamRequest(socket) {
|
||||||
@ -125,8 +123,6 @@ class StreamManager {
|
|||||||
client.user.stream = null
|
client.user.stream = null
|
||||||
client.stream = null
|
client.stream = null
|
||||||
this.db.updateUserStream(client.user.id, null)
|
this.db.updateUserStream(client.user.id, null)
|
||||||
|
|
||||||
this.emitter('user_stream_update', client.user.toJSONForPublic(this.streams))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async closeStreamApiRequest(userId, streamId) {
|
async closeStreamApiRequest(userId, streamId) {
|
||||||
|
@ -119,6 +119,15 @@ class Book {
|
|||||||
getAudiobookById(audiobookId) {
|
getAudiobookById(audiobookId) {
|
||||||
return this.audiobooks.find(ab => ab.id === audiobookId)
|
return this.audiobooks.find(ab => ab.id === audiobookId)
|
||||||
}
|
}
|
||||||
|
getMediaEntityById(entityId) {
|
||||||
|
var ent = this.audiobooks.find(ab => ab.id === entityId)
|
||||||
|
if (ent) return ent
|
||||||
|
return this.ebooks.find(eb => eb.id === entityId)
|
||||||
|
}
|
||||||
|
getPlaybackMediaEntity() { // Get first playback media entity
|
||||||
|
if (!this.audiobooks.length) return null
|
||||||
|
return this.audiobooks[0]
|
||||||
|
}
|
||||||
|
|
||||||
removeFileWithInode(inode) {
|
removeFileWithInode(inode) {
|
||||||
var audiobookWithIno = this.audiobooks.find(ab => ab.findFileWithInode(inode))
|
var audiobookWithIno = this.audiobooks.find(ab => ab.findFileWithInode(inode))
|
||||||
@ -262,9 +271,5 @@ class Book {
|
|||||||
// newEbook.setData(libraryFile)
|
// newEbook.setData(libraryFile)
|
||||||
// this.ebookFiles.push(newEbook)
|
// this.ebookFiles.push(newEbook)
|
||||||
}
|
}
|
||||||
|
|
||||||
getDirectPlayTracklist(options) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
module.exports = Book
|
module.exports = Book
|
@ -117,6 +117,14 @@ class Podcast {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getMediaEntityById(entityId) {
|
||||||
|
return this.episodes.find(ep => ep.id === entityId)
|
||||||
|
}
|
||||||
|
getPlaybackMediaEntity() { // Get first playback media entity
|
||||||
|
if (!this.episodes.length) return null
|
||||||
|
return this.episodes[0]
|
||||||
|
}
|
||||||
|
|
||||||
setData(scanMediaMetadata) {
|
setData(scanMediaMetadata) {
|
||||||
this.metadata = new PodcastMetadata()
|
this.metadata = new PodcastMetadata()
|
||||||
this.metadata.setData(scanMediaMetadata)
|
this.metadata.setData(scanMediaMetadata)
|
||||||
@ -130,9 +138,5 @@ class Podcast {
|
|||||||
var payload = this.metadata.searchQuery(query)
|
var payload = this.metadata.searchQuery(query)
|
||||||
return payload || {}
|
return payload || {}
|
||||||
}
|
}
|
||||||
|
|
||||||
getDirectPlayTracklist(options) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
module.exports = Podcast
|
module.exports = Podcast
|
@ -37,7 +37,7 @@ class BookMetadata {
|
|||||||
this.isbn = metadata.isbn
|
this.isbn = metadata.isbn
|
||||||
this.asin = metadata.asin
|
this.asin = metadata.asin
|
||||||
this.language = metadata.language
|
this.language = metadata.language
|
||||||
this.explicit = metadata.explicit
|
this.explicit = !!metadata.explicit
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
@ -150,6 +150,7 @@ class BookMetadata {
|
|||||||
this.asin = scanMediaData.asin || null
|
this.asin = scanMediaData.asin || null
|
||||||
this.language = scanMediaData.language || null
|
this.language = scanMediaData.language || null
|
||||||
this.genres = []
|
this.genres = []
|
||||||
|
this.explicit = !!scanMediaData.explicit
|
||||||
|
|
||||||
if (scanMediaData.author) {
|
if (scanMediaData.author) {
|
||||||
this.authors = this.parseAuthorsTag(scanMediaData.author)
|
this.authors = this.parseAuthorsTag(scanMediaData.author)
|
||||||
|
@ -47,7 +47,7 @@ class FileMetadata {
|
|||||||
|
|
||||||
get format() {
|
get format() {
|
||||||
if (!this.ext) return ''
|
if (!this.ext) return ''
|
||||||
return this.ext.slice(1)
|
return this.ext.slice(1).toLowerCase()
|
||||||
}
|
}
|
||||||
get filenameNoExt() {
|
get filenameNoExt() {
|
||||||
return this.filename.replace(this.ext, '')
|
return this.filename.replace(this.ext, '')
|
||||||
|
@ -42,27 +42,8 @@ class LibraryItemProgress {
|
|||||||
this.finishedAt = progress.finishedAt || null
|
this.finishedAt = progress.finishedAt || null
|
||||||
}
|
}
|
||||||
|
|
||||||
updateProgressFromStream(stream) {
|
get inProgress() {
|
||||||
// this.audiobookId = stream.libraryItemId
|
return !this.isFinished && this.progress > 0
|
||||||
// this.totalDuration = stream.totalDuration
|
|
||||||
// this.progress = stream.clientProgress
|
|
||||||
// this.currentTime = stream.clientCurrentTime
|
|
||||||
// this.lastUpdate = Date.now()
|
|
||||||
|
|
||||||
// if (!this.startedAt) {
|
|
||||||
// this.startedAt = Date.now()
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // If has < 10 seconds remaining mark as read
|
|
||||||
// var timeRemaining = this.totalDuration - this.currentTime
|
|
||||||
// if (timeRemaining < 10) {
|
|
||||||
// this.isFinished = true
|
|
||||||
// this.progress = 1
|
|
||||||
// this.finishedAt = Date.now()
|
|
||||||
// } else {
|
|
||||||
// this.isFinished = false
|
|
||||||
// this.finishedAt = null
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setData(libraryItemId, progress) {
|
setData(libraryItemId, progress) {
|
||||||
|
@ -8,7 +8,6 @@ class User {
|
|||||||
this.username = null
|
this.username = null
|
||||||
this.pash = null
|
this.pash = null
|
||||||
this.type = null
|
this.type = null
|
||||||
this.stream = null
|
|
||||||
this.token = null
|
this.token = null
|
||||||
this.isActive = true
|
this.isActive = true
|
||||||
this.isLocked = false
|
this.isLocked = false
|
||||||
@ -79,7 +78,6 @@ class User {
|
|||||||
username: this.username,
|
username: this.username,
|
||||||
pash: this.pash,
|
pash: this.pash,
|
||||||
type: this.type,
|
type: this.type,
|
||||||
stream: this.stream,
|
|
||||||
token: this.token,
|
token: this.token,
|
||||||
libraryItemProgress: this.libraryItemProgress ? this.libraryItemProgress.map(li => li.toJSON()) : [],
|
libraryItemProgress: this.libraryItemProgress ? this.libraryItemProgress.map(li => li.toJSON()) : [],
|
||||||
bookmarks: this.bookmarks ? this.bookmarks.map(b => b.toJSON()) : [],
|
bookmarks: this.bookmarks ? this.bookmarks.map(b => b.toJSON()) : [],
|
||||||
@ -98,7 +96,6 @@ class User {
|
|||||||
id: this.id,
|
id: this.id,
|
||||||
username: this.username,
|
username: this.username,
|
||||||
type: this.type,
|
type: this.type,
|
||||||
stream: this.stream,
|
|
||||||
token: this.token,
|
token: this.token,
|
||||||
libraryItemProgress: this.libraryItemProgress ? this.libraryItemProgress.map(li => li.toJSON()) : [],
|
libraryItemProgress: this.libraryItemProgress ? this.libraryItemProgress.map(li => li.toJSON()) : [],
|
||||||
isActive: this.isActive,
|
isActive: this.isActive,
|
||||||
@ -112,13 +109,14 @@ class User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Data broadcasted
|
// Data broadcasted
|
||||||
toJSONForPublic(streams) {
|
toJSONForPublic(sessions, libraryItems) {
|
||||||
var stream = this.stream && streams ? streams.find(s => s.id === this.stream) : null
|
var session = sessions ? sessions.find(s => s.userId === this.id) : null
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
username: this.username,
|
username: this.username,
|
||||||
type: this.type,
|
type: this.type,
|
||||||
stream: stream ? stream.toJSON() : null,
|
session: session ? session.toJSONForClient() : null,
|
||||||
|
mostRecent: this.getMostRecentItemProgress(libraryItems),
|
||||||
lastSeen: this.lastSeen,
|
lastSeen: this.lastSeen,
|
||||||
createdAt: this.createdAt
|
createdAt: this.createdAt
|
||||||
}
|
}
|
||||||
@ -129,12 +127,11 @@ class User {
|
|||||||
this.username = user.username
|
this.username = user.username
|
||||||
this.pash = user.pash
|
this.pash = user.pash
|
||||||
this.type = user.type
|
this.type = user.type
|
||||||
this.stream = user.stream || null
|
|
||||||
this.token = user.token
|
this.token = user.token
|
||||||
|
|
||||||
this.libraryItemProgress = []
|
this.libraryItemProgress = []
|
||||||
if (user.libraryItemProgress) {
|
if (user.libraryItemProgress) {
|
||||||
this.libraryItemProgress = user.libraryItemProgress.map(li => new LibraryItemProgress(li))
|
this.libraryItemProgress = user.libraryItemProgress.map(li => new LibraryItemProgress(li)).filter(lip => lip.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.bookmarks = []
|
this.bookmarks = []
|
||||||
@ -195,13 +192,22 @@ class User {
|
|||||||
return hasUpdates
|
return hasUpdates
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAudiobookProgressFromStream(stream) {
|
getMostRecentItemProgress(libraryItems) {
|
||||||
// if (!this.audiobooks) this.audiobooks = {}
|
if (!this.libraryItemProgress.length) return null
|
||||||
// if (!this.audiobooks[stream.audiobookId]) {
|
var lip = this.libraryItemProgress.map(lip => lip.toJSON())
|
||||||
// this.audiobooks[stream.audiobookId] = new UserAudiobookData()
|
lip.sort((a, b) => b.lastUpdate - a.lastUpdate)
|
||||||
// }
|
var mostRecentWithLip = lip.find(li => libraryItems.find(_li => _li.id === li.id))
|
||||||
// this.audiobooks[stream.audiobookId].updateProgressFromStream(stream)
|
if (!mostRecentWithLip) return null
|
||||||
// return this.audiobooks[stream.audiobookId]
|
var libraryItem = libraryItems.find(li => li.id === mostRecentWithLip.id)
|
||||||
|
return {
|
||||||
|
...mostRecentWithLip,
|
||||||
|
media: libraryItem.media.toJSONExpanded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getLibraryItemProgress(libraryItemId) {
|
||||||
|
if (!this.libraryItemProgress) return null
|
||||||
|
return this.libraryItemProgress.find(lip => lip.id === libraryItemId)
|
||||||
}
|
}
|
||||||
|
|
||||||
createUpdateLibraryItemProgress(libraryItemId, updatePayload) {
|
createUpdateLibraryItemProgress(libraryItemId, updatePayload) {
|
||||||
@ -254,12 +260,6 @@ class User {
|
|||||||
return this.librariesAccessible.includes(libraryId)
|
return this.librariesAccessible.includes(libraryId)
|
||||||
}
|
}
|
||||||
|
|
||||||
getLibraryItemProgress(libraryItemId) {
|
|
||||||
if (!this.libraryItemProgress) return null
|
|
||||||
var progress = this.libraryItemProgress.find(lip => lip.id === libraryItemId)
|
|
||||||
return progress ? progress.toJSON() : null
|
|
||||||
}
|
|
||||||
|
|
||||||
createBookmark({ libraryItemId, time, title }) {
|
createBookmark({ libraryItemId, time, title }) {
|
||||||
// if (!this.audiobooks) this.audiobooks = {}
|
// if (!this.audiobooks) this.audiobooks = {}
|
||||||
// if (!this.audiobooks[audiobookId]) {
|
// if (!this.audiobooks[audiobookId]) {
|
||||||
|
@ -4,29 +4,30 @@ const fs = require('fs-extra')
|
|||||||
const date = require('date-and-time')
|
const date = require('date-and-time')
|
||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
|
|
||||||
const Logger = require('./Logger')
|
const Logger = require('../Logger')
|
||||||
const { isObject } = require('./utils/index')
|
const { isObject } = require('../utils/index')
|
||||||
const { parsePodcastRssFeedXml } = require('./utils/podcastUtils')
|
const { parsePodcastRssFeedXml } = require('../utils/podcastUtils')
|
||||||
|
|
||||||
const LibraryController = require('./controllers/LibraryController')
|
const LibraryController = require('../controllers/LibraryController')
|
||||||
const UserController = require('./controllers/UserController')
|
const UserController = require('../controllers/UserController')
|
||||||
const CollectionController = require('./controllers/CollectionController')
|
const CollectionController = require('../controllers/CollectionController')
|
||||||
const MeController = require('./controllers/MeController')
|
const MeController = require('../controllers/MeController')
|
||||||
const BackupController = require('./controllers/BackupController')
|
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 MediaEntityController = require('../controllers/MediaEntityController')
|
||||||
|
const SessionController = require('../controllers/SessionController')
|
||||||
|
|
||||||
const BookFinder = require('./finders/BookFinder')
|
const BookFinder = require('../finders/BookFinder')
|
||||||
const AuthorFinder = require('./finders/AuthorFinder')
|
const AuthorFinder = require('../finders/AuthorFinder')
|
||||||
const PodcastFinder = require('./finders/PodcastFinder')
|
const PodcastFinder = require('../finders/PodcastFinder')
|
||||||
|
|
||||||
const Author = require('./objects/entities/Author')
|
const Author = require('../objects/entities/Author')
|
||||||
const Series = require('./objects/entities/Series')
|
const Series = require('../objects/entities/Series')
|
||||||
const FileSystemController = require('./controllers/FileSystemController')
|
const FileSystemController = require('../controllers/FileSystemController')
|
||||||
|
|
||||||
class ApiController {
|
class ApiRouter {
|
||||||
constructor(db, auth, scanner, playbackSessionManager, downloadManager, coverController, backupManager, watcher, cacheManager, emitter, clientEmitter) {
|
constructor(db, auth, scanner, playbackSessionManager, downloadManager, coverController, backupManager, watcher, cacheManager, emitter, clientEmitter) {
|
||||||
this.db = db
|
this.db = db
|
||||||
this.auth = auth
|
this.auth = auth
|
||||||
@ -72,11 +73,12 @@ class ApiController {
|
|||||||
this.router.post('/libraries/order', LibraryController.reorder.bind(this))
|
this.router.post('/libraries/order', LibraryController.reorder.bind(this))
|
||||||
|
|
||||||
//
|
//
|
||||||
// Audiobook Routes
|
// Media Entity Routes
|
||||||
//
|
//
|
||||||
this.router.get('/audiobooks/:id', AudiobookController.middleware.bind(this), AudiobookController.findOne.bind(this))
|
this.router.get('/entities/:id', MediaEntityController.middleware.bind(this), MediaEntityController.findOne.bind(this))
|
||||||
this.router.get('/audiobooks/:id/item', AudiobookController.middleware.bind(this), AudiobookController.findWithItem.bind(this))
|
this.router.get('/entities/:id/item', MediaEntityController.middleware.bind(this), MediaEntityController.findWithItem.bind(this))
|
||||||
this.router.patch('/audiobooks/:id/tracks', AudiobookController.middleware.bind(this), AudiobookController.updateTracks.bind(this))
|
this.router.patch('/entities/:id/tracks', MediaEntityController.middleware.bind(this), MediaEntityController.updateTracks.bind(this))
|
||||||
|
this.router.post('/entities/:id/play', MediaEntityController.middleware.bind(this), MediaEntityController.startPlaybackSession.bind(this))
|
||||||
|
|
||||||
//
|
//
|
||||||
// Item Routes
|
// Item Routes
|
||||||
@ -92,13 +94,11 @@ 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.get('/items/:id/play', LibraryItemController.middleware.bind(this), LibraryItemController.startPlaybackSession.bind(this))
|
this.router.post('/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))
|
||||||
this.router.post('/items/batch/update', LibraryItemController.batchUpdate.bind(this))
|
this.router.post('/items/batch/update', LibraryItemController.batchUpdate.bind(this))
|
||||||
this.router.post('/items/batch/get', LibraryItemController.batchGet.bind(this))
|
this.router.post('/items/batch/get', LibraryItemController.batchGet.bind(this))
|
||||||
// Legacy
|
|
||||||
this.router.get('/items/:id/stream', LibraryItemController.middleware.bind(this), LibraryItemController.openStream.bind(this))
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// User Routes
|
// User Routes
|
||||||
@ -171,6 +171,12 @@ class ApiController {
|
|||||||
this.router.get('/series/search', SeriesController.search.bind(this))
|
this.router.get('/series/search', SeriesController.search.bind(this))
|
||||||
this.router.get('/series/:id', SeriesController.middleware.bind(this), SeriesController.findOne.bind(this))
|
this.router.get('/series/:id', SeriesController.middleware.bind(this), SeriesController.findOne.bind(this))
|
||||||
|
|
||||||
|
//
|
||||||
|
// Playback Session Routes
|
||||||
|
//
|
||||||
|
this.router.post('/session/:id/sync', SessionController.middleware.bind(this), SessionController.sync.bind(this))
|
||||||
|
this.router.post('/session/:id/close', SessionController.middleware.bind(this), SessionController.close.bind(this))
|
||||||
|
|
||||||
//
|
//
|
||||||
// Misc Routes
|
// Misc Routes
|
||||||
//
|
//
|
||||||
@ -180,14 +186,10 @@ class ApiController {
|
|||||||
|
|
||||||
this.router.get('/download/:id', this.download.bind(this))
|
this.router.get('/download/:id', this.download.bind(this))
|
||||||
|
|
||||||
this.router.post('/syncUserAudiobookData', this.syncUserAudiobookData.bind(this))
|
|
||||||
|
|
||||||
this.router.post('/purgecache', this.purgeCache.bind(this))
|
this.router.post('/purgecache', this.purgeCache.bind(this))
|
||||||
|
|
||||||
this.router.post('/syncStream', this.syncStream.bind(this))
|
// OLD
|
||||||
this.router.post('/syncLocal', this.syncLocal.bind(this))
|
// this.router.post('/syncUserAudiobookData', this.syncUserAudiobookData.bind(this))
|
||||||
|
|
||||||
this.router.post('/streams/:id/close', this.closeStream.bind(this))
|
|
||||||
|
|
||||||
this.router.post('/getPodcastFeed', this.getPodcastFeed.bind(this))
|
this.router.post('/getPodcastFeed', this.getPodcastFeed.bind(this))
|
||||||
}
|
}
|
||||||
@ -337,45 +339,21 @@ class ApiController {
|
|||||||
// res.json(allUserAudiobookData)
|
// res.json(allUserAudiobookData)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync audiobook stream progress
|
|
||||||
async syncStream(req, res) {
|
|
||||||
Logger.debug(`[ApiController] syncStream for ${req.user.username} - ${req.body.streamId}`)
|
|
||||||
// this.streamManager.streamSyncFromApi(req, res)
|
|
||||||
res.sendStatus(500)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sync local downloaded audiobook progress
|
|
||||||
async syncLocal(req, res) {
|
|
||||||
// Logger.debug(`[ApiController] syncLocal for ${req.user.username}`)
|
|
||||||
// var progressPayload = req.body
|
|
||||||
// var itemProgress = req.user.updateLibraryItemProgress(progressPayload.libraryItemId, progressPayload)
|
|
||||||
// if (itemProgress) {
|
|
||||||
// await this.db.updateEntity('user', req.user)
|
|
||||||
// this.clientEmitter(req.user.id, 'current_user_audiobook_update', {
|
|
||||||
// id: progressPayload.libraryItemId,
|
|
||||||
// data: itemProgress || null
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
res.sendStatus(200)
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Helper Methods
|
// Helper Methods
|
||||||
//
|
//
|
||||||
userJsonWithBookProgressDetails(user) {
|
userJsonWithItemProgressDetails(user) {
|
||||||
var json = user.toJSONForBrowser()
|
var json = user.toJSONForBrowser()
|
||||||
|
|
||||||
// User audiobook progress attach book details
|
json.libraryItemProgress = json.libraryItemProgress.map(lip => {
|
||||||
if (json.audiobooks && Object.keys(json.audiobooks).length) {
|
var libraryItem = this.db.libraryItems.find(li => li.id === lip.id)
|
||||||
for (const audiobookId in json.audiobooks) {
|
if (!libraryItem) {
|
||||||
var libraryItem = this.db.libraryItems.find(li => li.id === audiobookId)
|
Logger.warn('[ApiRouter] Library item not found for users progress ' + lip.id)
|
||||||
if (!libraryItem) {
|
return null
|
||||||
Logger.error('[ApiController] Library item not found for users progress ' + audiobookId)
|
|
||||||
} else {
|
|
||||||
json.audiobooks[audiobookId].media = libraryItem.media.toJSONExpanded()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
lip.media = libraryItem.media.toJSONExpanded()
|
||||||
|
return lip
|
||||||
|
}).filter(lip => !!lip)
|
||||||
|
|
||||||
return json
|
return json
|
||||||
}
|
}
|
||||||
@ -425,8 +403,7 @@ class ApiController {
|
|||||||
|
|
||||||
async getUserListeningSessionsHelper(userId) {
|
async getUserListeningSessionsHelper(userId) {
|
||||||
var userSessions = await this.db.selectUserSessions(userId)
|
var userSessions = await this.db.selectUserSessions(userId)
|
||||||
var listeningSessions = userSessions.filter(us => us.sessionType === 'listeningSession')
|
return userSessions.sort((a, b) => b.updatedAt - a.updatedAt)
|
||||||
return listeningSessions.sort((a, b) => b.lastUpdate - a.lastUpdate)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserListeningStatsHelpers(userId) {
|
async getUserListeningStatsHelpers(userId) {
|
||||||
@ -435,7 +412,7 @@ class ApiController {
|
|||||||
var listeningSessions = await this.getUserListeningSessionsHelper(userId)
|
var listeningSessions = await this.getUserListeningSessionsHelper(userId)
|
||||||
var listeningStats = {
|
var listeningStats = {
|
||||||
totalTime: 0,
|
totalTime: 0,
|
||||||
books: {},
|
items: {},
|
||||||
days: {},
|
days: {},
|
||||||
dayOfWeek: {},
|
dayOfWeek: {},
|
||||||
today: 0,
|
today: 0,
|
||||||
@ -454,16 +431,15 @@ class ApiController {
|
|||||||
listeningStats.today += s.timeListening
|
listeningStats.today += s.timeListening
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!listeningStats.books[s.audiobookId]) {
|
if (!listeningStats.items[s.libraryItemId]) {
|
||||||
listeningStats.books[s.audiobookId] = {
|
listeningStats.items[s.libraryItemId] = {
|
||||||
id: s.audiobookId,
|
id: s.libraryItemId,
|
||||||
timeListening: s.timeListening,
|
timeListening: s.timeListening,
|
||||||
title: s.audiobookTitle,
|
mediaMetadata: s.mediaMetadata,
|
||||||
author: s.audiobookAuthor,
|
|
||||||
lastUpdate: s.lastUpdate
|
lastUpdate: s.lastUpdate
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
listeningStats.books[s.audiobookId].timeListening += s.timeListening
|
listeningStats.items[s.libraryItemId].timeListening += s.timeListening
|
||||||
}
|
}
|
||||||
|
|
||||||
listeningStats.totalTime += s.timeListening
|
listeningStats.totalTime += s.timeListening
|
||||||
@ -475,18 +451,11 @@ class ApiController {
|
|||||||
if (!req.user.isRoot) {
|
if (!req.user.isRoot) {
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
Logger.info(`[ApiController] Purging all cache`)
|
Logger.info(`[ApiRouter] Purging all cache`)
|
||||||
await this.cacheManager.purgeAll()
|
await this.cacheManager.purgeAll()
|
||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
async closeStream(req, res) {
|
|
||||||
const streamId = req.params.id
|
|
||||||
const userId = req.user.id
|
|
||||||
// this.streamManager.closeStreamApiRequest(userId, streamId)
|
|
||||||
res.sendStatus(200)
|
|
||||||
}
|
|
||||||
|
|
||||||
async createAuthorsAndSeriesForItemUpdate(mediaPayload) {
|
async createAuthorsAndSeriesForItemUpdate(mediaPayload) {
|
||||||
if (mediaPayload.metadata) {
|
if (mediaPayload.metadata) {
|
||||||
var mediaMetadata = mediaPayload.metadata
|
var mediaMetadata = mediaPayload.metadata
|
||||||
@ -501,7 +470,7 @@ class ApiController {
|
|||||||
if (!author) {
|
if (!author) {
|
||||||
author = new Author()
|
author = new Author()
|
||||||
author.setData(mediaMetadata.authors[i])
|
author.setData(mediaMetadata.authors[i])
|
||||||
Logger.debug(`[ApiController] Created new author "${author.name}"`)
|
Logger.debug(`[ApiRouter] Created new author "${author.name}"`)
|
||||||
newAuthors.push(author)
|
newAuthors.push(author)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -525,7 +494,7 @@ class ApiController {
|
|||||||
if (!seriesItem) {
|
if (!seriesItem) {
|
||||||
seriesItem = new Series()
|
seriesItem = new Series()
|
||||||
seriesItem.setData(mediaMetadata.series[i])
|
seriesItem.setData(mediaMetadata.series[i])
|
||||||
Logger.debug(`[ApiController] Created new series "${seriesItem.name}"`)
|
Logger.debug(`[ApiRouter] Created new series "${seriesItem.name}"`)
|
||||||
newSeries.push(seriesItem)
|
newSeries.push(seriesItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -563,4 +532,4 @@ class ApiController {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = ApiController
|
module.exports = ApiRouter
|
@ -1,13 +1,13 @@
|
|||||||
const express = require('express')
|
const express = require('express')
|
||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const fs = require('fs-extra')
|
const fs = require('fs-extra')
|
||||||
const Logger = require('./Logger')
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
class HlsController {
|
class HlsRouter {
|
||||||
constructor(db, auth, playbackSessionManager, emitter) {
|
constructor(db, auth, playbackSessionManager, emitter) {
|
||||||
this.db = db
|
this.db = db
|
||||||
this.auth = auth
|
this.auth = auth
|
||||||
this.streamManager = playbackSessionManager
|
this.playbackSessionManager = playbackSessionManager
|
||||||
this.emitter = emitter
|
this.emitter = emitter
|
||||||
|
|
||||||
this.router = express()
|
this.router = express()
|
||||||
@ -26,13 +26,7 @@ class HlsController {
|
|||||||
|
|
||||||
async streamFileRequest(req, res) {
|
async streamFileRequest(req, res) {
|
||||||
var streamId = req.params.stream
|
var streamId = req.params.stream
|
||||||
var fullFilePath = Path.join(this.streamManager.StreamsPath, streamId, req.params.file)
|
var fullFilePath = Path.join(this.playbackSessionManager.StreamsPath, streamId, req.params.file)
|
||||||
|
|
||||||
// development test stream - ignore
|
|
||||||
if (streamId === 'test') {
|
|
||||||
Logger.debug('Test Stream Request', streamId, req.headers, fullFilePath)
|
|
||||||
return res.sendFile(fullFilePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
var exists = await fs.pathExists(fullFilePath)
|
var exists = await fs.pathExists(fullFilePath)
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
@ -41,20 +35,20 @@ class HlsController {
|
|||||||
var fileExt = Path.extname(req.params.file)
|
var fileExt = Path.extname(req.params.file)
|
||||||
if (fileExt === '.ts' || fileExt === '.m4s') {
|
if (fileExt === '.ts' || fileExt === '.m4s') {
|
||||||
var segNum = this.parseSegmentFilename(req.params.file)
|
var segNum = this.parseSegmentFilename(req.params.file)
|
||||||
var stream = this.streamManager.getStream(streamId)
|
var stream = this.playbackSessionManager.getStream(streamId)
|
||||||
if (!stream) {
|
if (!stream) {
|
||||||
Logger.error(`[HLS-CONTROLLER] Stream ${streamId} does not exist`)
|
Logger.error(`[HlsRouter] Stream ${streamId} does not exist`)
|
||||||
return res.sendStatus(500)
|
return res.sendStatus(500)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stream.isResetting) {
|
if (stream.isResetting) {
|
||||||
Logger.info(`[HLS-CONTROLLER] Stream ${streamId} is currently resetting`)
|
Logger.info(`[HlsRouter] Stream ${streamId} is currently resetting`)
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
} else {
|
} else {
|
||||||
var startTimeForReset = await stream.checkSegmentNumberRequest(segNum)
|
var startTimeForReset = await stream.checkSegmentNumberRequest(segNum)
|
||||||
if (startTimeForReset) {
|
if (startTimeForReset) {
|
||||||
// HLS.js will restart the stream at the new time
|
// HLS.js will restart the stream at the new time
|
||||||
Logger.info(`[HLS-CONTROLLER] Resetting Stream - notify client @${startTimeForReset}s`)
|
Logger.info(`[HlsRouter] Resetting Stream - notify client @${startTimeForReset}s`)
|
||||||
this.emitter('stream_reset', {
|
this.emitter('stream_reset', {
|
||||||
startTime: startTimeForReset,
|
startTime: startTimeForReset,
|
||||||
streamId: stream.id
|
streamId: stream.id
|
||||||
@ -69,4 +63,4 @@ class HlsController {
|
|||||||
res.sendFile(fullFilePath)
|
res.sendFile(fullFilePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = HlsController
|
module.exports = HlsRouter
|
25
server/routers/StaticRouter.js
Normal file
25
server/routers/StaticRouter.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
const express = require('express')
|
||||||
|
const Path = require('path')
|
||||||
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
|
class StaticRouter {
|
||||||
|
constructor(db) {
|
||||||
|
this.db = db
|
||||||
|
|
||||||
|
this.router = express()
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// Library Item static file routes
|
||||||
|
this.router.get('/item/:id/*', (req, res) => {
|
||||||
|
var item = this.db.libraryItems.find(ab => ab.id === req.params.id)
|
||||||
|
if (!item) return res.status(404).send('Item not found with id ' + req.params.id)
|
||||||
|
|
||||||
|
var remainingPath = req.params['0']
|
||||||
|
var fullPath = Path.join(item.path, remainingPath)
|
||||||
|
res.sendFile(fullPath)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = StaticRouter
|
@ -33,20 +33,6 @@ class Scanner {
|
|||||||
this.bookFinder = new BookFinder()
|
this.bookFinder = new BookFinder()
|
||||||
}
|
}
|
||||||
|
|
||||||
getCoverDirectory(audiobook) {
|
|
||||||
if (this.db.serverSettings.storeCoverWithBook) {
|
|
||||||
return {
|
|
||||||
fullPath: audiobook.fullPath,
|
|
||||||
relPath: '/s/book/' + audiobook.id
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
fullPath: Path.posix.join(this.BookMetadataPath, audiobook.id),
|
|
||||||
relPath: Path.posix.join('/metadata', 'books', audiobook.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isLibraryScanning(libraryId) {
|
isLibraryScanning(libraryId) {
|
||||||
return this.librariesScanning.find(ls => ls.id === libraryId)
|
return this.librariesScanning.find(ls => ls.id === libraryId)
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ const fs = require('fs-extra')
|
|||||||
const njodb = require("njodb")
|
const njodb = require("njodb")
|
||||||
|
|
||||||
const { SupportedEbookTypes } = require('./globals')
|
const { SupportedEbookTypes } = require('./globals')
|
||||||
|
const { PlayMethod } = require('./constants')
|
||||||
const { getId } = require('./index')
|
const { getId } = require('./index')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
@ -335,6 +336,8 @@ function cleanSessionObj(db, userListeningSession) {
|
|||||||
newPlaybackSession.mediaType = 'book'
|
newPlaybackSession.mediaType = 'book'
|
||||||
newPlaybackSession.updatedAt = userListeningSession.lastUpdate
|
newPlaybackSession.updatedAt = userListeningSession.lastUpdate
|
||||||
newPlaybackSession.libraryItemId = userListeningSession.audiobookId
|
newPlaybackSession.libraryItemId = userListeningSession.audiobookId
|
||||||
|
newPlaybackSession.mediaEntityId = userListeningSession.audiobookId
|
||||||
|
newPlaybackSession.playMethod = PlayMethod.TRANSCODE
|
||||||
|
|
||||||
// We only have title to transfer over nicely
|
// We only have title to transfer over nicely
|
||||||
var bookMetadata = new BookMetadata()
|
var bookMetadata = new BookMetadata()
|
||||||
|
@ -132,9 +132,6 @@ async function recurseFiles(path, relPathToReplace = null) {
|
|||||||
// Sort from least deep to most
|
// Sort from least deep to most
|
||||||
list.sort((a, b) => a.deep - b.deep)
|
list.sort((a, b) => a.deep - b.deep)
|
||||||
|
|
||||||
// list.forEach((l) => {
|
|
||||||
// console.log(`${l.deep}: ${l.path}`)
|
|
||||||
// })
|
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
module.exports.recurseFiles = recurseFiles
|
module.exports.recurseFiles = recurseFiles
|
||||||
|
@ -28,11 +28,10 @@ module.exports = {
|
|||||||
else if (group === 'narrators') filtered = filtered.filter(li => li.media.metadata && li.media.metadata.hasNarrator(filter))
|
else if (group === 'narrators') filtered = filtered.filter(li => li.media.metadata && li.media.metadata.hasNarrator(filter))
|
||||||
else if (group === 'progress') {
|
else if (group === 'progress') {
|
||||||
filtered = filtered.filter(li => {
|
filtered = filtered.filter(li => {
|
||||||
var userAudiobook = user.getLibraryItemProgress(li.id)
|
var itemProgress = user.getLibraryItemProgress(li.id)
|
||||||
var isRead = userAudiobook && userAudiobook.isRead
|
if (filter === 'Finished' && (itemProgress && itemProgress.isFinished)) return true
|
||||||
if (filter === 'Read' && isRead) return true
|
if (filter === 'Not Started' && !itemProgress) return true
|
||||||
if (filter === 'Unread' && !isRead) return true
|
if (filter === 'In Progress' && (itemProgress && itemProgress.inProgress)) return true
|
||||||
if (filter === 'In Progress' && (userAudiobook && !userAudiobook.isRead && userAudiobook.progress > 0)) return true
|
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
} else if (group === 'languages') {
|
} else if (group === 'languages') {
|
||||||
@ -49,43 +48,6 @@ module.exports = {
|
|||||||
return filtered
|
return filtered
|
||||||
},
|
},
|
||||||
|
|
||||||
getFiltered(audiobooks, filterBy, user) {
|
|
||||||
var filtered = audiobooks
|
|
||||||
|
|
||||||
var searchGroups = ['genres', 'tags', 'series', 'authors', 'progress', 'narrators', 'languages']
|
|
||||||
var group = searchGroups.find(_group => filterBy.startsWith(_group + '.'))
|
|
||||||
if (group) {
|
|
||||||
var filterVal = filterBy.replace(`${group}.`, '')
|
|
||||||
var filter = this.decode(filterVal)
|
|
||||||
if (group === 'genres') filtered = filtered.filter(ab => ab.book && ab.book.genres.includes(filter))
|
|
||||||
else if (group === 'tags') filtered = filtered.filter(ab => ab.tags.includes(filter))
|
|
||||||
else if (group === 'series') {
|
|
||||||
if (filter === 'No Series') filtered = filtered.filter(ab => ab.book && !ab.book.series)
|
|
||||||
else filtered = filtered.filter(ab => ab.book && ab.book.series === filter)
|
|
||||||
}
|
|
||||||
else if (group === 'authors') filtered = filtered.filter(ab => ab.book && ab.book.authorFL && ab.book.authorFL.split(', ').includes(filter))
|
|
||||||
else if (group === 'narrators') filtered = filtered.filter(ab => ab.book && ab.book.narratorFL && ab.book.narratorFL.split(', ').includes(filter))
|
|
||||||
else if (group === 'progress') {
|
|
||||||
filtered = filtered.filter(ab => {
|
|
||||||
var userAudiobook = user.getLibraryItemProgress(ab.id)
|
|
||||||
var isRead = userAudiobook && userAudiobook.isRead
|
|
||||||
if (filter === 'Read' && isRead) return true
|
|
||||||
if (filter === 'Unread' && !isRead) return true
|
|
||||||
if (filter === 'In Progress' && (userAudiobook && !userAudiobook.isRead && userAudiobook.progress > 0)) return true
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
} else if (group === 'languages') {
|
|
||||||
filtered = filtered.filter(ab => ab.book && ab.book.language === filter)
|
|
||||||
}
|
|
||||||
} else if (filterBy === 'issues') {
|
|
||||||
filtered = filtered.filter(ab => {
|
|
||||||
return ab.numMissingParts || ab.numInvalidParts || ab.isMissing || ab.isInvalid
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return filtered
|
|
||||||
},
|
|
||||||
|
|
||||||
getDistinctFilterDataNew(libraryItems) {
|
getDistinctFilterDataNew(libraryItems) {
|
||||||
var data = {
|
var data = {
|
||||||
authors: [],
|
authors: [],
|
||||||
@ -160,26 +122,27 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getSeriesWithProgressFromBooks(user, books) {
|
getSeriesWithProgressFromBooks(user, books) {
|
||||||
var _series = {}
|
return []
|
||||||
books.forEach((audiobook) => {
|
// var _series = {}
|
||||||
if (audiobook.book.series) {
|
// books.forEach((audiobook) => {
|
||||||
var bookWithUserAb = { userAudiobook: user.getLibraryItemProgress(audiobook.id), book: audiobook }
|
// if (audiobook.book.series) {
|
||||||
if (!_series[audiobook.book.series]) {
|
// var bookWithUserAb = { userAudiobook: user.getLibraryItemProgress(audiobook.id), book: audiobook }
|
||||||
_series[audiobook.book.series] = {
|
// if (!_series[audiobook.book.series]) {
|
||||||
id: audiobook.book.series,
|
// _series[audiobook.book.series] = {
|
||||||
name: audiobook.book.series,
|
// id: audiobook.book.series,
|
||||||
type: 'series',
|
// name: audiobook.book.series,
|
||||||
books: [bookWithUserAb]
|
// type: 'series',
|
||||||
}
|
// books: [bookWithUserAb]
|
||||||
} else {
|
// }
|
||||||
_series[audiobook.book.series].books.push(bookWithUserAb)
|
// } else {
|
||||||
}
|
// _series[audiobook.book.series].books.push(bookWithUserAb)
|
||||||
}
|
// }
|
||||||
})
|
// }
|
||||||
return Object.values(_series).map((series) => {
|
// })
|
||||||
series.books = naturalSort(series.books).asc(ab => ab.book.book.volumeNumber)
|
// return Object.values(_series).map((series) => {
|
||||||
return series
|
// series.books = naturalSort(series.books).asc(ab => ab.book.book.volumeNumber)
|
||||||
}).filter((series) => series.books.some((book) => book.userAudiobook && book.userAudiobook.isRead))
|
// return series
|
||||||
|
// }).filter((series) => series.books.some((book) => book.userAudiobook && book.userAudiobook.isRead))
|
||||||
},
|
},
|
||||||
|
|
||||||
sortSeriesBooks(books, seriesId, minified = false) {
|
sortSeriesBooks(books, seriesId, minified = false) {
|
||||||
@ -196,8 +159,9 @@ module.exports = {
|
|||||||
|
|
||||||
getItemsWithUserProgress(user, libraryItems) {
|
getItemsWithUserProgress(user, libraryItems) {
|
||||||
return libraryItems.map(li => {
|
return libraryItems.map(li => {
|
||||||
|
var itemProgress = user.getLibraryItemProgress(li.id)
|
||||||
return {
|
return {
|
||||||
userProgress: user.getLibraryItemProgress(li.id),
|
userProgress: itemProgress ? itemProgress.toJSON() : null,
|
||||||
libraryItem: li
|
libraryItem: li
|
||||||
}
|
}
|
||||||
}).filter(b => !!b.userProgress)
|
}).filter(b => !!b.userProgress)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user