mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-06-25 12:12:26 +02:00
New data model removing media entity for books
This commit is contained in:
parent
920ca683b9
commit
3150822117
@ -100,8 +100,8 @@ export default {
|
|||||||
selectedLibraryItems() {
|
selectedLibraryItems() {
|
||||||
return this.$store.state.selectedLibraryItems
|
return this.$store.state.selectedLibraryItems
|
||||||
},
|
},
|
||||||
userItemProgress() {
|
userMediaProgress() {
|
||||||
return this.$store.state.user.user.libraryItemProgress || []
|
return this.$store.state.user.user.mediaProgress || []
|
||||||
},
|
},
|
||||||
userCanUpdate() {
|
userCanUpdate() {
|
||||||
return this.$store.getters['user/getUserCanUpdate']
|
return this.$store.getters['user/getUserCanUpdate']
|
||||||
@ -115,7 +115,7 @@ export default {
|
|||||||
selectedIsFinished() {
|
selectedIsFinished() {
|
||||||
// Find an item that is not finished, if none then all items finished
|
// Find an item that is not finished, if none then all items finished
|
||||||
return !this.selectedLibraryItems.find((libraryItemId) => {
|
return !this.selectedLibraryItems.find((libraryItemId) => {
|
||||||
var itemProgress = this.userItemProgress.find((lip) => lip.id === libraryItemId)
|
var itemProgress = this.userMediaProgress.find((lip) => lip.id === libraryItemId)
|
||||||
return !itemProgress || !itemProgress.isFinished
|
return !itemProgress || !itemProgress.isFinished
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -82,9 +82,6 @@ export default {
|
|||||||
shelfHeight() {
|
shelfHeight() {
|
||||||
return this.bookCoverHeight + 48
|
return this.bookCoverHeight + 48
|
||||||
},
|
},
|
||||||
userAudiobooks() {
|
|
||||||
return this.$store.state.user.user ? this.$store.state.user.user.audiobooks || {} : {}
|
|
||||||
},
|
|
||||||
paddingLeft() {
|
paddingLeft() {
|
||||||
if (window.innerWidth < 768) return 1
|
if (window.innerWidth < 768) return 1
|
||||||
return 2.5
|
return 2.5
|
||||||
|
@ -93,12 +93,12 @@ export default {
|
|||||||
user() {
|
user() {
|
||||||
return this.$store.state.user.user
|
return this.$store.state.user.user
|
||||||
},
|
},
|
||||||
userLibraryItemProgress() {
|
userMediaProgress() {
|
||||||
if (!this.libraryItemId) return
|
if (!this.libraryItemId) return
|
||||||
return this.$store.getters['user/getUserLibraryItemProgress'](this.libraryItemId)
|
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
||||||
},
|
},
|
||||||
userItemCurrentTime() {
|
userItemCurrentTime() {
|
||||||
return this.userLibraryItemProgress ? this.userLibraryItemProgress.currentTime || 0 : 0
|
return this.userMediaProgress ? this.userMediaProgress.currentTime || 0 : 0
|
||||||
},
|
},
|
||||||
bookmarks() {
|
bookmarks() {
|
||||||
if (!this.libraryItemId) return []
|
if (!this.libraryItemId) return []
|
||||||
|
@ -17,11 +17,12 @@
|
|||||||
<div v-if="booksInSeries" class="absolute z-20 top-1.5 right-1.5 rounded-md leading-3 text-sm p-1 font-semibold text-white flex items-center justify-center" style="background-color: #cd9d49dd">{{ booksInSeries }}</div>
|
<div v-if="booksInSeries" class="absolute z-20 top-1.5 right-1.5 rounded-md leading-3 text-sm p-1 font-semibold text-white flex items-center justify-center" style="background-color: #cd9d49dd">{{ booksInSeries }}</div>
|
||||||
|
|
||||||
<div class="w-full h-full absolute top-0 left-0 rounded overflow-hidden z-10">
|
<div class="w-full h-full absolute top-0 left-0 rounded overflow-hidden z-10">
|
||||||
<div v-show="audiobook && !imageReady" class="absolute top-0 left-0 w-full h-full flex items-center justify-center" :style="{ padding: sizeMultiplier * 0.5 + 'rem' }">
|
<div v-show="libraryItem && !imageReady" class="absolute top-0 left-0 w-full h-full flex items-center justify-center" :style="{ padding: sizeMultiplier * 0.5 + 'rem' }">
|
||||||
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }" class="font-book text-gray-300 text-center">{{ title }}</p>
|
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }" class="font-book text-gray-300 text-center">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<img v-show="audiobook" ref="cover" :src="bookCoverSrc" class="w-full h-full transition-opacity duration-300" :class="showCoverBg ? 'object-contain' : 'object-fill'" @load="imageLoaded" :style="{ opacity: imageReady ? 1 : 0 }" />
|
<!-- Cover Image -->
|
||||||
|
<img v-show="libraryItem" ref="cover" :src="bookCoverSrc" class="w-full h-full transition-opacity duration-300" :class="showCoverBg ? 'object-contain' : 'object-fill'" @load="imageLoaded" :style="{ opacity: imageReady ? 1 : 0 }" />
|
||||||
|
|
||||||
<!-- Placeholder Cover Title & Author -->
|
<!-- Placeholder Cover Title & Author -->
|
||||||
<div v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
<div v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
||||||
@ -38,7 +39,7 @@
|
|||||||
<div v-if="!booksInSeries" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
|
<div v-if="!booksInSeries" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
|
||||||
|
|
||||||
<!-- Overlay is not shown if collapsing series in library -->
|
<!-- Overlay is not shown if collapsing series in library -->
|
||||||
<div v-show="!booksInSeries && audiobook && (isHovering || isSelectionMode || isMoreMenuOpen)" class="w-full h-full absolute top-0 left-0 z-10 bg-black rounded hidden md:block" :class="overlayWrapperClasslist">
|
<div v-show="!booksInSeries && libraryItem && (isHovering || isSelectionMode || isMoreMenuOpen)" class="w-full h-full absolute top-0 left-0 z-10 bg-black rounded hidden md:block" :class="overlayWrapperClasslist">
|
||||||
<div v-show="showPlayButton" class="h-full flex items-center justify-center pointer-events-none">
|
<div v-show="showPlayButton" class="h-full flex items-center justify-center pointer-events-none">
|
||||||
<div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto" @click.stop.prevent="play">
|
<div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto" @click.stop.prevent="play">
|
||||||
<span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">play_circle_filled</span>
|
<span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">play_circle_filled</span>
|
||||||
@ -65,7 +66,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Series name overlay -->
|
<!-- Series name overlay -->
|
||||||
<div v-if="booksInSeries && audiobook && isHovering" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-60 rounded flex items-center justify-center" :style="{ padding: sizeMultiplier + 'rem' }">
|
<div v-if="booksInSeries && libraryItem && isHovering" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-60 rounded flex items-center justify-center" :style="{ padding: sizeMultiplier + 'rem' }">
|
||||||
<p class="text-gray-200 text-center" :style="{ fontSize: 1.1 * sizeMultiplier + 'rem' }">{{ series }}</p>
|
<p class="text-gray-200 text-center" :style="{ fontSize: 1.1 * sizeMultiplier + 'rem' }">{{ series }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -115,7 +116,7 @@ export default {
|
|||||||
isHovering: false,
|
isHovering: false,
|
||||||
isMoreMenuOpen: false,
|
isMoreMenuOpen: false,
|
||||||
isProcessingReadUpdate: false,
|
isProcessingReadUpdate: false,
|
||||||
audiobook: null,
|
libraryItem: null,
|
||||||
imageReady: false,
|
imageReady: false,
|
||||||
rescanning: false,
|
rescanning: false,
|
||||||
selected: false,
|
selected: false,
|
||||||
@ -127,7 +128,7 @@ export default {
|
|||||||
bookMount: {
|
bookMount: {
|
||||||
handler(newVal) {
|
handler(newVal) {
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
this.audiobook = newVal
|
this.libraryItem = newVal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -137,7 +138,7 @@ export default {
|
|||||||
return this.store.state.showExperimentalFeatures
|
return this.store.state.showExperimentalFeatures
|
||||||
},
|
},
|
||||||
_libraryItem() {
|
_libraryItem() {
|
||||||
return this.audiobook || {}
|
return this.libraryItem || {}
|
||||||
},
|
},
|
||||||
media() {
|
media() {
|
||||||
return this._libraryItem.media || {}
|
return this._libraryItem.media || {}
|
||||||
@ -161,18 +162,17 @@ export default {
|
|||||||
return this._libraryItem.libraryId
|
return this._libraryItem.libraryId
|
||||||
},
|
},
|
||||||
hasEbook() {
|
hasEbook() {
|
||||||
if (!this.media.ebooks) return 0
|
return this.media.ebookFile
|
||||||
return this.media.ebooks.length
|
|
||||||
},
|
},
|
||||||
hasAudiobook() {
|
numTracks() {
|
||||||
if (!this.media.audiobooks) return 0
|
if (this.media.tracks) return this.media.tracks.length
|
||||||
return this.media.audiobooks.length
|
return this.media.numTracks || 0 // toJSONMinified
|
||||||
},
|
},
|
||||||
processingBatch() {
|
processingBatch() {
|
||||||
return this.store.state.processingBatch
|
return this.store.state.processingBatch
|
||||||
},
|
},
|
||||||
booksInSeries() {
|
booksInSeries() {
|
||||||
// Only added to audiobook object when collapseSeries is enabled
|
// Only added to item object when collapseSeries is enabled
|
||||||
return this._libraryItem.booksInSeries
|
return this._libraryItem.booksInSeries
|
||||||
},
|
},
|
||||||
hasCover() {
|
hasCover() {
|
||||||
@ -228,7 +228,7 @@ export default {
|
|||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
userProgress() {
|
userProgress() {
|
||||||
return this.store.getters['user/getUserLibraryItemProgress'](this.libraryItemId)
|
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
||||||
},
|
},
|
||||||
userProgressPercent() {
|
userProgressPercent() {
|
||||||
return this.userProgress ? this.userProgress.progress || 0 : 0
|
return this.userProgress ? this.userProgress.progress || 0 : 0
|
||||||
@ -246,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.hasAudiobook && !this.isStreaming
|
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && this.numTracks && !this.isStreaming
|
||||||
},
|
},
|
||||||
showSmallEBookIcon() {
|
showSmallEBookIcon() {
|
||||||
return !this.isSelectionMode && this.showExperimentalFeatures && this.hasEbook
|
return !this.isSelectionMode && this.showExperimentalFeatures && this.hasEbook
|
||||||
@ -264,8 +264,8 @@ export default {
|
|||||||
return this._libraryItem.hasInvalidParts
|
return this._libraryItem.hasInvalidParts
|
||||||
},
|
},
|
||||||
errorText() {
|
errorText() {
|
||||||
if (this.isMissing) return 'Audiobook directory is missing!'
|
if (this.isMissing) return 'Item directory is missing!'
|
||||||
else if (this.isInvalid) return 'Audiobook has no audio tracks & ebook'
|
else if (this.isInvalid) return 'Item has no audio tracks & ebook'
|
||||||
var txt = ''
|
var txt = ''
|
||||||
if (this.hasMissingParts) {
|
if (this.hasMissingParts) {
|
||||||
txt = `${this.hasMissingParts} missing parts.`
|
txt = `${this.hasMissingParts} missing parts.`
|
||||||
@ -312,7 +312,7 @@ export default {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
if (this.userCanUpdate) {
|
if (this.userCanUpdate) {
|
||||||
if (this.hasAudiobook) {
|
if (this.numTracks) {
|
||||||
items.push({
|
items.push({
|
||||||
func: 'showEditModalTracks',
|
func: 'showEditModalTracks',
|
||||||
text: 'Tracks'
|
text: 'Tracks'
|
||||||
@ -382,7 +382,7 @@ export default {
|
|||||||
if (!val) this.selected = false
|
if (!val) this.selected = false
|
||||||
},
|
},
|
||||||
setEntity(libraryItem) {
|
setEntity(libraryItem) {
|
||||||
this.audiobook = libraryItem
|
this.libraryItem = libraryItem
|
||||||
},
|
},
|
||||||
clickCard(e) {
|
clickCard(e) {
|
||||||
if (this.isSelectionMode) {
|
if (this.isSelectionMode) {
|
||||||
@ -398,7 +398,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
editClick() {
|
editClick() {
|
||||||
this.$emit('edit', this.audiobook)
|
this.$emit('edit', this.libraryItem)
|
||||||
},
|
},
|
||||||
toggleFinished() {
|
toggleFinished() {
|
||||||
var updatePayload = {
|
var updatePayload = {
|
||||||
@ -444,18 +444,18 @@ export default {
|
|||||||
},
|
},
|
||||||
showEditModalTracks() {
|
showEditModalTracks() {
|
||||||
// More menu func
|
// More menu func
|
||||||
this.store.commit('showEditModalOnTab', { libraryItem: this.audiobook, tab: 'tracks' })
|
this.store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'tracks' })
|
||||||
},
|
},
|
||||||
showEditModalMatch() {
|
showEditModalMatch() {
|
||||||
// More menu func
|
// More menu func
|
||||||
this.store.commit('showEditModalOnTab', { libraryItem: this.audiobook, tab: 'match' })
|
this.store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'match' })
|
||||||
},
|
},
|
||||||
showEditModalDownload() {
|
showEditModalDownload() {
|
||||||
// More menu func
|
// More menu func
|
||||||
this.store.commit('showEditModalOnTab', { libraryItem: this.audiobook, tab: 'download' })
|
this.store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'download' })
|
||||||
},
|
},
|
||||||
openCollections() {
|
openCollections() {
|
||||||
this.store.commit('setSelectedLibraryItem', this.audiobook)
|
this.store.commit('setSelectedLibraryItem', this.libraryItem)
|
||||||
this.store.commit('globals/setShowUserCollectionsModal', true)
|
this.store.commit('globals/setShowUserCollectionsModal', true)
|
||||||
},
|
},
|
||||||
createMoreMenu() {
|
createMoreMenu() {
|
||||||
@ -509,12 +509,12 @@ export default {
|
|||||||
this.createMoreMenu()
|
this.createMoreMenu()
|
||||||
},
|
},
|
||||||
clickReadEBook() {
|
clickReadEBook() {
|
||||||
this.store.commit('showEReader', this.audiobook)
|
this.store.commit('showEReader', this.media.ebookFile)
|
||||||
},
|
},
|
||||||
selectBtnClick() {
|
selectBtnClick() {
|
||||||
if (this.processingBatch) return
|
if (this.processingBatch) return
|
||||||
this.selected = !this.selected
|
this.selected = !this.selected
|
||||||
this.$emit('select', this.audiobook)
|
this.$emit('select', this.libraryItem)
|
||||||
},
|
},
|
||||||
play() {
|
play() {
|
||||||
var eventBus = this.$eventBus || this.$nuxt.$eventBus
|
var eventBus = this.$eventBus || this.$nuxt.$eventBus
|
||||||
|
@ -138,6 +138,7 @@ export default {
|
|||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.imageReady = true
|
this.imageReady = true
|
||||||
})
|
})
|
||||||
|
|
||||||
if (this.$refs.cover && this.cover !== this.placeholderUrl) {
|
if (this.$refs.cover && this.cover !== this.placeholderUrl) {
|
||||||
var { naturalWidth, naturalHeight } = this.$refs.cover
|
var { naturalWidth, naturalHeight } = this.$refs.cover
|
||||||
var aspectRatio = naturalHeight / naturalWidth
|
var aspectRatio = naturalHeight / naturalWidth
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full overflow-y-auto overflow-x-hidden px-4 py-6">
|
<div class="w-full h-full overflow-y-auto overflow-x-hidden px-4 py-6">
|
||||||
<div v-if="!audiobooks.length" class="text-center py-8 text-lg">No Audiobooks</div>
|
<div class="w-full mb-4">
|
||||||
<template v-for="audiobook in audiobooks">
|
|
||||||
<div :key="audiobook.id" class="w-full mb-4">
|
|
||||||
<div class="w-full p-4 bg-primary">
|
<div class="w-full p-4 bg-primary">
|
||||||
<p>Audiobook Chapters ({{ audiobook.name }})</p>
|
<p>Audiobook Chapters</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!audiobook.chapters.length" class="flex my-4 text-center justify-center text-xl">No Chapters</div>
|
<div v-if="!chapters.length" class="flex my-4 text-center justify-center text-xl">No Chapters</div>
|
||||||
<table v-else class="text-sm tracksTable">
|
<table v-else class="text-sm tracksTable">
|
||||||
<tr class="font-book">
|
<tr class="font-book">
|
||||||
<th class="text-left w-16"><span class="px-4">Id</span></th>
|
<th class="text-left w-16"><span class="px-4">Id</span></th>
|
||||||
@ -14,7 +12,7 @@
|
|||||||
<th class="text-center">Start</th>
|
<th class="text-center">Start</th>
|
||||||
<th class="text-center">End</th>
|
<th class="text-center">End</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-for="chapter in audiobook.chapters" :key="chapter.id">
|
<tr v-for="chapter in chapters" :key="chapter.id">
|
||||||
<td class="text-left">
|
<td class="text-left">
|
||||||
<p class="px-4">{{ chapter.id }}</p>
|
<p class="px-4">{{ chapter.id }}</p>
|
||||||
</td>
|
</td>
|
||||||
@ -30,7 +28,6 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -49,8 +46,8 @@ export default {
|
|||||||
media() {
|
media() {
|
||||||
return this.libraryItem ? this.libraryItem.media || {} : {}
|
return this.libraryItem ? this.libraryItem.media || {} : {}
|
||||||
},
|
},
|
||||||
audiobooks() {
|
chapters() {
|
||||||
return this.media.audiobooks || []
|
return this.media.chapters || []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {}
|
methods: {}
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
<!-- <span class="bg-black-400 rounded-xl py-1 px-2 text-sm font-mono">{{ tracks.length }}</span> -->
|
<!-- <span class="bg-black-400 rounded-xl py-1 px-2 text-sm font-mono">{{ tracks.length }}</span> -->
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="showFullPath = !showFullPath">Full Path</ui-btn>
|
<ui-btn small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="showFullPath = !showFullPath">Full Path</ui-btn>
|
||||||
<nuxt-link v-if="userCanUpdate" :to="`/audiobook/${audiobookId}/edit`" class="mr-2 md:mr-4" @mousedown.prevent>
|
<nuxt-link v-if="userCanUpdate" :to="`/audiobook/${libraryItemId}/edit`" class="mr-2 md:mr-4" @mousedown.prevent>
|
||||||
<ui-btn small color="primary">Manage Tracks</ui-btn>
|
<ui-btn small color="primary">Manage Tracks</ui-btn>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showTracks ? 'transform rotate-180' : ''">
|
<div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showTracks ? 'transform rotate-180' : ''">
|
||||||
@ -38,7 +38,7 @@
|
|||||||
{{ $secondsToTimestamp(track.duration) }}
|
{{ $secondsToTimestamp(track.duration) }}
|
||||||
</td>
|
</td>
|
||||||
<td v-if="userCanDownload" class="text-center">
|
<td v-if="userCanDownload" class="text-center">
|
||||||
<a :href="`/s/item/${audiobookId}${$encodeUriPath(track.metadata.relPath)}?token=${userToken}`" download><span class="material-icons icon-text">download</span></a>
|
<a :href="`/s/item/${libraryItemId}${$encodeUriPath(track.metadata.relPath)}?token=${userToken}`" download><span class="material-icons icon-text">download</span></a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
@ -59,7 +59,7 @@ export default {
|
|||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
audiobookId: String
|
libraryItemId: String
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -82,18 +82,17 @@ export default {
|
|||||||
mediaMetadata() {
|
mediaMetadata() {
|
||||||
return this.media.metadata || {}
|
return this.media.metadata || {}
|
||||||
},
|
},
|
||||||
|
tracks() {
|
||||||
|
return this.media.tracks || []
|
||||||
|
},
|
||||||
bookTitle() {
|
bookTitle() {
|
||||||
return this.mediaMetadata.title || ''
|
return this.mediaMetadata.title || ''
|
||||||
},
|
},
|
||||||
bookAuthor() {
|
bookAuthor() {
|
||||||
return (this.mediaMetadata.authors || []).map((au) => au.name).join(', ')
|
return (this.mediaMetadata.authors || []).map((au) => au.name).join(', ')
|
||||||
},
|
},
|
||||||
defaultAudiobook() {
|
|
||||||
if (!this.media.audiobooks.length) return null
|
|
||||||
return this.media.audiobooks[0]
|
|
||||||
},
|
|
||||||
bookDuration() {
|
bookDuration() {
|
||||||
return this.$secondsToTimestamp(this.defaultAudiobook.duration)
|
return this.$secondsToTimestamp(this.media.duration)
|
||||||
},
|
},
|
||||||
isMissing() {
|
isMissing() {
|
||||||
return this.book.isMissing
|
return this.book.isMissing
|
||||||
@ -105,10 +104,10 @@ export default {
|
|||||||
return this.$store.getters['getLibraryItemIdStreaming'] === this.book.id
|
return this.$store.getters['getLibraryItemIdStreaming'] === this.book.id
|
||||||
},
|
},
|
||||||
showPlayBtn() {
|
showPlayBtn() {
|
||||||
return !this.isMissing && !this.isInvalid && !this.isStreaming && this.defaultAudiobook
|
return !this.isMissing && !this.isInvalid && !this.isStreaming && this.tracks.length
|
||||||
},
|
},
|
||||||
itemProgress() {
|
itemProgress() {
|
||||||
return this.$store.getters['user/getUserLibraryItemProgress'](this.book.id)
|
return this.$store.getters['user/getUserMediaProgress'](this.book.id)
|
||||||
},
|
},
|
||||||
userIsFinished() {
|
userIsFinished() {
|
||||||
return this.itemProgress ? !!this.itemProgress.isFinished : false
|
return this.itemProgress ? !!this.itemProgress.isFinished : false
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<textarea v-model="inputValue" :rows="rows" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" class="py-2 px-3 rounded bg-primary text-gray-200 focus:border-gray-500 focus:outline-none" :class="transparent ? '' : 'border border-gray-600'" @change="change" />
|
<textarea ref="input" v-model="inputValue" :rows="rows" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" class="py-2 px-3 rounded bg-primary text-gray-200 focus:border-gray-500 focus:outline-none" :class="transparent ? '' : 'border border-gray-600'" @change="change" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -31,6 +31,11 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
change(e) {
|
change(e) {
|
||||||
this.$emit('change', e.target.value)
|
this.$emit('change', e.target.value)
|
||||||
|
},
|
||||||
|
blur() {
|
||||||
|
if (this.$refs.input && this.$refs.input.blur) {
|
||||||
|
this.$refs.input.blur()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<p class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</p>
|
<p class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</p>
|
||||||
<ui-textarea-input v-model="inputValue" :disabled="disabled" :rows="rows" class="w-full" />
|
<ui-textarea-input ref="input" v-model="inputValue" :disabled="disabled" :rows="rows" class="w-full" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -29,7 +29,13 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {},
|
methods: {
|
||||||
|
blur() {
|
||||||
|
if (this.$refs.input && this.$refs.input.blur) {
|
||||||
|
this.$refs.input.blur()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
@ -16,14 +16,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<tables-tracks-table :key="audiobook.id" :title="`Audiobook Tracks (${audiobook.name})`" :tracks="audiobook.tracks" :audiobook-id="audiobook.id" class="mt-6" />
|
<tables-tracks-table :title="`Audiobook Tracks`" :tracks="media.tracks" :library-item-id="libraryItemId" class="mt-6" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
audiobook: {
|
libraryItemId: String,
|
||||||
|
media: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
}
|
}
|
||||||
@ -64,10 +65,10 @@ export default {
|
|||||||
return chunks
|
return chunks
|
||||||
},
|
},
|
||||||
missingParts() {
|
missingParts() {
|
||||||
return this.audiobook.missingParts || []
|
return this.media.missingParts || []
|
||||||
},
|
},
|
||||||
invalidParts() {
|
invalidParts() {
|
||||||
return this.audiobook.invalidParts || []
|
return this.media.invalidParts || []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {},
|
methods: {},
|
||||||
|
@ -4,10 +4,10 @@
|
|||||||
<div id="formWrapper" class="px-4 py-6 details-form-wrapper w-full overflow-hidden overflow-y-auto">
|
<div id="formWrapper" class="px-4 py-6 details-form-wrapper w-full overflow-hidden overflow-y-auto">
|
||||||
<div class="flex -mx-1">
|
<div class="flex -mx-1">
|
||||||
<div class="w-1/2 px-1">
|
<div class="w-1/2 px-1">
|
||||||
<ui-text-input-with-label v-model="details.title" label="Title" />
|
<ui-text-input-with-label ref="titleInput" v-model="details.title" label="Title" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-1">
|
<div class="flex-grow px-1">
|
||||||
<ui-text-input-with-label v-model="details.subtitle" label="Subtitle" />
|
<ui-text-input-with-label ref="subtitleInput" v-model="details.subtitle" label="Subtitle" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -17,7 +17,7 @@
|
|||||||
<ui-multi-select-query-input ref="authorsSelect" v-model="details.authors" label="Authors" endpoint="authors/search" />
|
<ui-multi-select-query-input ref="authorsSelect" v-model="details.authors" label="Authors" endpoint="authors/search" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-1">
|
<div class="flex-grow px-1">
|
||||||
<ui-text-input-with-label v-model="details.publishedYear" type="number" label="Publish Year" />
|
<ui-text-input-with-label ref="publishYearInput" v-model="details.publishedYear" type="number" label="Publish Year" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -27,7 +27,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ui-textarea-with-label v-model="details.description" :rows="3" label="Description" class="mt-2" />
|
<ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" label="Description" class="mt-2" />
|
||||||
|
|
||||||
<div class="flex mt-2 -mx-1">
|
<div class="flex mt-2 -mx-1">
|
||||||
<div class="w-1/2 px-1">
|
<div class="w-1/2 px-1">
|
||||||
@ -43,19 +43,19 @@
|
|||||||
<ui-multi-select ref="narratorsSelect" v-model="details.narrators" label="Narrators" :items="narrators" />
|
<ui-multi-select ref="narratorsSelect" v-model="details.narrators" label="Narrators" :items="narrators" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/4 px-1">
|
<div class="w-1/4 px-1">
|
||||||
<ui-text-input-with-label v-model="details.isbn" label="ISBN" />
|
<ui-text-input-with-label ref="isbnInput" v-model="details.isbn" label="ISBN" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/4 px-1">
|
<div class="w-1/4 px-1">
|
||||||
<ui-text-input-with-label v-model="details.asin" label="ASIN" />
|
<ui-text-input-with-label ref="asinInput" v-model="details.asin" label="ASIN" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex mt-2 -mx-1">
|
<div class="flex mt-2 -mx-1">
|
||||||
<div class="w-1/2 px-1">
|
<div class="w-1/2 px-1">
|
||||||
<ui-text-input-with-label v-model="details.publisher" label="Publisher" />
|
<ui-text-input-with-label ref="publisherInput" v-model="details.publisher" label="Publisher" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/4 px-1">
|
<div class="w-1/4 px-1">
|
||||||
<ui-text-input-with-label v-model="details.language" label="Language" />
|
<ui-text-input-with-label ref="languageInput" v-model="details.language" label="Language" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-1 pt-6">
|
<div class="flex-grow px-1 pt-6">
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
@ -194,6 +194,15 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
forceBlur() {
|
forceBlur() {
|
||||||
|
if (this.$refs.titleInput) this.$refs.titleInput.blur()
|
||||||
|
if (this.$refs.subtitleInput) this.$refs.subtitleInput.blur()
|
||||||
|
if (this.$refs.publishYearInput) this.$refs.publishYearInput.blur()
|
||||||
|
if (this.$refs.descriptionInput) this.$refs.descriptionInput.blur()
|
||||||
|
if (this.$refs.isbnInput) this.$refs.isbnInput.blur()
|
||||||
|
if (this.$refs.asinInput) this.$refs.asinInput.blur()
|
||||||
|
if (this.$refs.publisherInput) this.$refs.publisherInput.blur()
|
||||||
|
if (this.$refs.languageInput) this.$refs.languageInput.blur()
|
||||||
|
|
||||||
if (this.$refs.authorsSelect && this.$refs.authorsSelect.isFocused) {
|
if (this.$refs.authorsSelect && this.$refs.authorsSelect.isFocused) {
|
||||||
this.$refs.authorsSelect.forceBlur()
|
this.$refs.authorsSelect.forceBlur()
|
||||||
}
|
}
|
||||||
|
@ -258,8 +258,8 @@ export default {
|
|||||||
userStreamUpdate(user) {
|
userStreamUpdate(user) {
|
||||||
this.$store.commit('users/updateUser', user)
|
this.$store.commit('users/updateUser', user)
|
||||||
},
|
},
|
||||||
userItemProgressUpdate(payload) {
|
userMediaProgressUpdate(payload) {
|
||||||
this.$store.commit('user/updateItemProgress', payload)
|
this.$store.commit('user/updateMediaProgress', payload)
|
||||||
},
|
},
|
||||||
collectionAdded(collection) {
|
collectionAdded(collection) {
|
||||||
this.$store.commit('user/addUpdateCollection', collection)
|
this.$store.commit('user/addUpdateCollection', collection)
|
||||||
@ -384,7 +384,7 @@ export default {
|
|||||||
this.socket.on('user_online', this.userOnline)
|
this.socket.on('user_online', this.userOnline)
|
||||||
this.socket.on('user_offline', this.userOffline)
|
this.socket.on('user_offline', this.userOffline)
|
||||||
this.socket.on('user_stream_update', this.userStreamUpdate)
|
this.socket.on('user_stream_update', this.userStreamUpdate)
|
||||||
this.socket.on('user_item_progress_updated', this.userItemProgressUpdate)
|
this.socket.on('user_item_progress_updated', this.userMediaProgressUpdate)
|
||||||
|
|
||||||
// User Collection Listeners
|
// User Collection Listeners
|
||||||
this.socket.on('collection_added', this.collectionAdded)
|
this.socket.on('collection_added', this.collectionAdded)
|
||||||
|
@ -95,19 +95,21 @@ 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/entities/${params.id}/item?expanded=1`).catch((error) => {
|
var libraryItem = await app.$axios.$get(`/api/items/${params.id}?expanded=1`).catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
if (!payload) {
|
if (!libraryItem) {
|
||||||
console.error('Not found...', params.id)
|
console.error('Not found...', params.id)
|
||||||
return redirect('/')
|
return redirect('/')
|
||||||
}
|
}
|
||||||
const audiobook = payload.mediaEntity
|
if (libraryItem.mediaType != 'book') {
|
||||||
|
console.error('Invalid media type')
|
||||||
|
return redirect('/')
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
audiobook,
|
libraryItem,
|
||||||
libraryItem: payload.libraryItem,
|
files: libraryItem.media.audioFiles ? libraryItem.media.audioFiles.map((af) => ({ ...af, include: !af.exclude })) : []
|
||||||
files: audiobook.audioFiles ? audiobook.audioFiles.map((af) => ({ ...af, include: !af.exclude })) : []
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@ -130,7 +132,7 @@ export default {
|
|||||||
return this.media.metadata || []
|
return this.media.metadata || []
|
||||||
},
|
},
|
||||||
audioFiles() {
|
audioFiles() {
|
||||||
return this.audiobook.audioFiles || []
|
return this.media.audioFiles || []
|
||||||
},
|
},
|
||||||
numExcluded() {
|
numExcluded() {
|
||||||
var count = 0
|
var count = 0
|
||||||
@ -140,7 +142,7 @@ export default {
|
|||||||
return count
|
return count
|
||||||
},
|
},
|
||||||
missingParts() {
|
missingParts() {
|
||||||
return this.audiobook.missingParts || []
|
return this.media.missingParts || []
|
||||||
},
|
},
|
||||||
libraryItemId() {
|
libraryItemId() {
|
||||||
return this.libraryItem.id
|
return this.libraryItem.id
|
||||||
@ -152,7 +154,7 @@ export default {
|
|||||||
return this.mediaMetadata.authorName || 'Unknown'
|
return this.mediaMetadata.authorName || 'Unknown'
|
||||||
},
|
},
|
||||||
tracks() {
|
tracks() {
|
||||||
return this.audiobook.tracks
|
return this.media.tracks
|
||||||
},
|
},
|
||||||
streamLibraryItem() {
|
streamLibraryItem() {
|
||||||
return this.$store.state.streamLibraryItem
|
return this.$store.state.streamLibraryItem
|
||||||
@ -218,7 +220,7 @@ export default {
|
|||||||
|
|
||||||
this.saving = true
|
this.saving = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$patch(`/api/entities/${this.audiobook.id}/tracks`, { orderedFileData })
|
.$patch(`/api/items/${this.libraryItem.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
|
||||||
|
@ -83,7 +83,7 @@ export default {
|
|||||||
},
|
},
|
||||||
playableBooks() {
|
playableBooks() {
|
||||||
return this.bookItems.filter((book) => {
|
return this.bookItems.filter((book) => {
|
||||||
return !book.isMissing && !book.isInvalid && book.media.audiobooks.length
|
return !book.isMissing && !book.isInvalid && book.media.tracks.length
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
streaming() {
|
streaming() {
|
||||||
@ -116,7 +116,7 @@ export default {
|
|||||||
},
|
},
|
||||||
clickPlay() {
|
clickPlay() {
|
||||||
var nextBookNotRead = this.playableBooks.find((pb) => {
|
var nextBookNotRead = this.playableBooks.find((pb) => {
|
||||||
var prog = this.$store.getters['user/getUserLibraryItemProgress'](pb.id)
|
var prog = this.$store.getters['user/getUserMediaProgress'](pb.id)
|
||||||
return !prog || !prog.isFinished
|
return !prog || !prog.isFinished
|
||||||
})
|
})
|
||||||
if (nextBookNotRead) {
|
if (nextBookNotRead) {
|
||||||
|
@ -76,11 +76,11 @@ export default {
|
|||||||
currentLibraryId() {
|
currentLibraryId() {
|
||||||
return this.$store.state.libraries.currentLibraryId
|
return this.$store.state.libraries.currentLibraryId
|
||||||
},
|
},
|
||||||
userItemProgress() {
|
userMediaProgress() {
|
||||||
return this.user.libraryItemProgress || []
|
return this.user.mediaProgress || []
|
||||||
},
|
},
|
||||||
userItemsFinished() {
|
userItemsFinished() {
|
||||||
return this.userItemProgress.filter((lip) => !!lip.isFinished)
|
return this.userMediaProgress.filter((lip) => !!lip.isFinished)
|
||||||
},
|
},
|
||||||
mostRecentListeningSessions() {
|
mostRecentListeningSessions() {
|
||||||
if (!this.listeningStats) return []
|
if (!this.listeningStats) return []
|
||||||
|
@ -39,7 +39,7 @@
|
|||||||
<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">Item Progress</h1>
|
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Item Progress</h1>
|
||||||
<table v-if="libraryItemProgress.length" class="userAudiobooksTable">
|
<table v-if="mediaProgress.length" class="userAudiobooksTable">
|
||||||
<tr class="bg-primary bg-opacity-40">
|
<tr class="bg-primary bg-opacity-40">
|
||||||
<th class="w-16 text-left">Item</th>
|
<th class="w-16 text-left">Item</th>
|
||||||
<th class="text-left"></th>
|
<th class="text-left"></th>
|
||||||
@ -47,7 +47,7 @@
|
|||||||
<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="item in libraryItemProgress" :key="item.id" :class="!item.isFinished ? '' : 'isFinished'">
|
<tr v-for="item in mediaProgress" :key="item.id" :class="!item.isFinished ? '' : 'isFinished'">
|
||||||
<td>
|
<td>
|
||||||
<covers-book-cover :width="50" :library-item="item" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-book-cover :width="50" :library-item="item" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
</td>
|
</td>
|
||||||
@ -111,8 +111,8 @@ export default {
|
|||||||
userOnline() {
|
userOnline() {
|
||||||
return this.$store.getters['users/getIsUserOnline'](this.user.id)
|
return this.$store.getters['users/getIsUserOnline'](this.user.id)
|
||||||
},
|
},
|
||||||
libraryItemProgress() {
|
mediaProgress() {
|
||||||
return this.user.libraryItemProgress.sort((a, b) => b.lastUpdate - a.lastUpdate)
|
return this.user.mediaProgress.sort((a, b) => b.lastUpdate - a.lastUpdate)
|
||||||
},
|
},
|
||||||
totalListeningTime() {
|
totalListeningTime() {
|
||||||
return this.listeningStats.totalTime || 0
|
return this.listeningStats.totalTime || 0
|
||||||
|
@ -69,7 +69,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="audiobooks.length" class="flex py-0.5">
|
<div v-if="tracks.length" class="flex py-0.5">
|
||||||
<div class="w-32">
|
<div class="w-32">
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">Duration</span>
|
<span class="text-white text-opacity-60 uppercase text-sm">Duration</span>
|
||||||
</div>
|
</div>
|
||||||
@ -77,7 +77,7 @@
|
|||||||
{{ durationPretty }}
|
{{ durationPretty }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="audiobooks.length" class="flex py-0.5">
|
<div class="flex py-0.5">
|
||||||
<div class="w-32">
|
<div class="w-32">
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">Size</span>
|
<span class="text-white text-opacity-60 uppercase text-sm">Size</span>
|
||||||
</div>
|
</div>
|
||||||
@ -143,9 +143,8 @@
|
|||||||
<p class="text-base text-gray-100 whitespace-pre-line">{{ description }}</p>
|
<p class="text-base text-gray-100 whitespace-pre-line">{{ description }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-for="audiobook in audiobooks">
|
<widgets-audiobook-data v-if="tracks.length" :library-item-id="libraryItemId" :media="media" />
|
||||||
<widgets-audiobook-data :key="audiobook.id" :audiobook="audiobook" />
|
|
||||||
</template>
|
|
||||||
<tables-library-files-table v-if="libraryFiles.length" :is-missing="isMissing" :library-item-id="libraryItemId" :files="libraryFiles" class="mt-6" />
|
<tables-library-files-table v-if="libraryFiles.length" :is-missing="isMissing" :library-item-id="libraryItemId" :files="libraryFiles" class="mt-6" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -205,7 +204,7 @@ export default {
|
|||||||
showPlayButton() {
|
showPlayButton() {
|
||||||
if (this.isMissing || this.isInvalid) return false
|
if (this.isMissing || this.isInvalid) return false
|
||||||
if (this.isPodcast) return this.podcastEpisodes.length
|
if (this.isPodcast) return this.podcastEpisodes.length
|
||||||
return this.audiobooks.length
|
return this.tracks.length
|
||||||
},
|
},
|
||||||
libraryId() {
|
libraryId() {
|
||||||
return this.libraryItem.libraryId
|
return this.libraryItem.libraryId
|
||||||
@ -222,13 +221,12 @@ export default {
|
|||||||
mediaMetadata() {
|
mediaMetadata() {
|
||||||
return this.media.metadata || {}
|
return this.media.metadata || {}
|
||||||
},
|
},
|
||||||
|
tracks() {
|
||||||
|
return this.media.tracks || []
|
||||||
|
},
|
||||||
podcastEpisodes() {
|
podcastEpisodes() {
|
||||||
return this.media.episodes || []
|
return this.media.episodes || []
|
||||||
},
|
},
|
||||||
defaultAudiobook() {
|
|
||||||
if (!this.audiobooks.length) return null
|
|
||||||
return this.audiobooks[0]
|
|
||||||
},
|
|
||||||
title() {
|
title() {
|
||||||
return this.mediaMetadata.title || 'No Title'
|
return this.mediaMetadata.title || 'No Title'
|
||||||
},
|
},
|
||||||
@ -271,51 +269,47 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
durationPretty() {
|
durationPretty() {
|
||||||
if (!this.defaultAudiobook) return 'N/A'
|
if (!this.tracks.length) return 'N/A'
|
||||||
return this.$elapsedPretty(this.defaultAudiobook.duration)
|
return this.$elapsedPretty(this.media.duration)
|
||||||
},
|
},
|
||||||
duration() {
|
duration() {
|
||||||
if (!this.defaultAudiobook) return 0
|
if (!this.tracks.length) return 0
|
||||||
return this.defaultAudiobook.duration
|
return this.media.duration
|
||||||
},
|
},
|
||||||
sizePretty() {
|
sizePretty() {
|
||||||
if (!this.defaultAudiobook) return 'N/A'
|
return this.$bytesPretty(this.media.size)
|
||||||
return this.$bytesPretty(this.defaultAudiobook.size)
|
|
||||||
},
|
},
|
||||||
libraryFiles() {
|
libraryFiles() {
|
||||||
return this.libraryItem.libraryFiles || []
|
return this.libraryItem.libraryFiles || []
|
||||||
},
|
},
|
||||||
audiobooks() {
|
|
||||||
return this.media.audiobooks || []
|
|
||||||
},
|
|
||||||
ebooks() {
|
ebooks() {
|
||||||
return this.media.ebooks || []
|
return this.media.ebooks || []
|
||||||
},
|
},
|
||||||
showExperimentalReadAlert() {
|
showExperimentalReadAlert() {
|
||||||
return !this.audiobooks.length && this.ebooks.length && !this.showExperimentalFeatures
|
return !this.tracks.length && this.ebooks.length && !this.showExperimentalFeatures
|
||||||
},
|
},
|
||||||
description() {
|
description() {
|
||||||
return this.mediaMetadata.description || ''
|
return this.mediaMetadata.description || ''
|
||||||
},
|
},
|
||||||
userItemProgress() {
|
userMediaProgress() {
|
||||||
return this.$store.getters['user/getUserLibraryItemProgress'](this.libraryItemId)
|
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
||||||
},
|
},
|
||||||
userIsFinished() {
|
userIsFinished() {
|
||||||
return this.userItemProgress ? !!this.userItemProgress.isFinished : false
|
return this.userMediaProgress ? !!this.userMediaProgress.isFinished : false
|
||||||
},
|
},
|
||||||
userTimeRemaining() {
|
userTimeRemaining() {
|
||||||
if (!this.userItemProgress) return 0
|
if (!this.userMediaProgress) return 0
|
||||||
var duration = this.userItemProgress.duration || this.duration
|
var duration = this.userMediaProgress.duration || this.duration
|
||||||
return duration - this.userItemProgress.currentTime
|
return duration - this.userMediaProgress.currentTime
|
||||||
},
|
},
|
||||||
progressPercent() {
|
progressPercent() {
|
||||||
return this.userItemProgress ? Math.max(Math.min(1, this.userItemProgress.progress), 0) : 0
|
return this.userMediaProgress ? Math.max(Math.min(1, this.userMediaProgress.progress), 0) : 0
|
||||||
},
|
},
|
||||||
userProgressStartedAt() {
|
userProgressStartedAt() {
|
||||||
return this.userItemProgress ? this.userItemProgress.startedAt : 0
|
return this.userMediaProgress ? this.userMediaProgress.startedAt : 0
|
||||||
},
|
},
|
||||||
userProgressFinishedAt() {
|
userProgressFinishedAt() {
|
||||||
return this.userItemProgress ? this.userItemProgress.finishedAt : 0
|
return this.userMediaProgress ? this.userMediaProgress.finishedAt : 0
|
||||||
},
|
},
|
||||||
streamLibraryItem() {
|
streamLibraryItem() {
|
||||||
return this.$store.state.streamLibraryItem
|
return this.$store.state.streamLibraryItem
|
||||||
@ -365,17 +359,11 @@ export default {
|
|||||||
this.$store.commit('setBookshelfBookIds', [])
|
this.$store.commit('setBookshelfBookIds', [])
|
||||||
this.$store.commit('showEditModal', this.libraryItem)
|
this.$store.commit('showEditModal', this.libraryItem)
|
||||||
},
|
},
|
||||||
audiobookUpdated() {
|
libraryItemUpdated(libraryItem) {
|
||||||
// console.log('Audiobook Updated - Fetch full audiobook')
|
if (libraryItem.id === this.libraryItemId) {
|
||||||
// this.$axios
|
console.log('Item was updated', libraryItem)
|
||||||
// .$get(`/api/books/${this.libraryItemId}`)
|
this.libraryItem = libraryItem
|
||||||
// .then((audiobook) => {
|
}
|
||||||
// console.log('Updated audiobook', audiobook)
|
|
||||||
// this.libraryItem = audiobook
|
|
||||||
// })
|
|
||||||
// .catch((error) => {
|
|
||||||
// console.error('Failed', error)
|
|
||||||
// })
|
|
||||||
},
|
},
|
||||||
clearProgressClick() {
|
clearProgressClick() {
|
||||||
if (confirm(`Are you sure you want to reset your progress?`)) {
|
if (confirm(`Are you sure you want to reset your progress?`)) {
|
||||||
@ -402,11 +390,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
// use this audiobooks library id as the current
|
// use this items library id as the current
|
||||||
if (this.libraryId) {
|
if (this.libraryId) {
|
||||||
this.$store.commit('libraries/setCurrentLibrary', this.libraryId)
|
this.$store.commit('libraries/setCurrentLibrary', this.libraryId)
|
||||||
}
|
}
|
||||||
|
this.$root.socket.on('item_updated', this.libraryItemUpdated)
|
||||||
},
|
},
|
||||||
beforeDestroy() {}
|
beforeDestroy() {
|
||||||
|
this.$root.socket.off('item_updated', this.libraryItemUpdated)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -11,7 +11,6 @@ export default class PlayerHandler {
|
|||||||
this.playerState = 'IDLE'
|
this.playerState = 'IDLE'
|
||||||
this.isHlsTranscode = false
|
this.isHlsTranscode = false
|
||||||
this.currentSessionId = null
|
this.currentSessionId = null
|
||||||
this.mediaEntityId = null
|
|
||||||
this.startTime = 0
|
this.startTime = 0
|
||||||
|
|
||||||
this.lastSyncTime = 0
|
this.lastSyncTime = 0
|
||||||
@ -150,7 +149,6 @@ export default class PlayerHandler {
|
|||||||
prepareSession(session) {
|
prepareSession(session) {
|
||||||
this.startTime = session.currentTime
|
this.startTime = session.currentTime
|
||||||
this.currentSessionId = session.id
|
this.currentSessionId = session.id
|
||||||
this.mediaEntityId = session.mediaEntityId
|
|
||||||
|
|
||||||
console.log('[PlayerHandler] Preparing Session', session)
|
console.log('[PlayerHandler] Preparing Session', session)
|
||||||
var audioTracks = session.audioTracks.map(at => new AudioTrack(at, this.userToken))
|
var audioTracks = session.audioTracks.map(at => new AudioTrack(at, this.userToken))
|
||||||
@ -210,7 +208,6 @@ export default class PlayerHandler {
|
|||||||
syncData = {
|
syncData = {
|
||||||
timeListened: listeningTimeToAdd,
|
timeListened: listeningTimeToAdd,
|
||||||
duration: this.getDuration(),
|
duration: this.getDuration(),
|
||||||
mediaEntityId: this.mediaEntityId,
|
|
||||||
currentTime: this.getCurrentTime()
|
currentTime: this.getCurrentTime()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -229,7 +226,6 @@ export default class PlayerHandler {
|
|||||||
var syncData = {
|
var syncData = {
|
||||||
timeListened: listeningTimeToAdd,
|
timeListened: listeningTimeToAdd,
|
||||||
duration: this.getDuration(),
|
duration: this.getDuration(),
|
||||||
mediaEntityId: this.mediaEntityId,
|
|
||||||
currentTime
|
currentTime
|
||||||
}
|
}
|
||||||
this.listeningTimeSinceSync = 0
|
this.listeningTimeSinceSync = 0
|
||||||
|
@ -22,9 +22,9 @@ export const getters = {
|
|||||||
getToken: (state) => {
|
getToken: (state) => {
|
||||||
return state.user ? state.user.token : null
|
return state.user ? state.user.token : null
|
||||||
},
|
},
|
||||||
getUserLibraryItemProgress: (state) => (libraryItemId) => {
|
getUserMediaProgress: (state) => (libraryItemId) => {
|
||||||
if (!state.user.libraryItemProgress) return null
|
if (!state.user.mediaProgress) return null
|
||||||
return state.user.libraryItemProgress.find(li => li.id == libraryItemId)
|
return state.user.mediaProgress.find(li => li.id == libraryItemId)
|
||||||
},
|
},
|
||||||
getUserBookmarksForItem: (state) => (libraryItemId) => {
|
getUserBookmarksForItem: (state) => (libraryItemId) => {
|
||||||
if (!state.user.bookmarks) return []
|
if (!state.user.bookmarks) return []
|
||||||
@ -107,16 +107,16 @@ export const mutations = {
|
|||||||
localStorage.removeItem('token')
|
localStorage.removeItem('token')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateItemProgress(state, { id, data }) {
|
updateMediaProgress(state, { id, data }) {
|
||||||
if (!state.user) return
|
if (!state.user) return
|
||||||
if (!data) {
|
if (!data) {
|
||||||
state.user.libraryItemProgress = state.user.libraryItemProgress.filter(lip => lip.id != id)
|
state.user.mediaProgress = state.user.mediaProgress.filter(lip => lip.id != id)
|
||||||
} else {
|
} else {
|
||||||
var indexOf = state.user.libraryItemProgress.findIndex(lip => lip.id == id)
|
var indexOf = state.user.mediaProgress.findIndex(lip => lip.id == id)
|
||||||
if (indexOf >= 0) {
|
if (indexOf >= 0) {
|
||||||
state.user.libraryItemProgress.splice(indexOf, 1, data)
|
state.user.mediaProgress.splice(indexOf, 1, data)
|
||||||
} else {
|
} else {
|
||||||
state.user.libraryItemProgress.push(data)
|
state.user.mediaProgress.push(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -53,11 +53,6 @@ new LibraryItem({
|
|||||||
language: 'english',
|
language: 'english',
|
||||||
explicit: false
|
explicit: false
|
||||||
},
|
},
|
||||||
audiobooks: [
|
|
||||||
{ // Audiobook.js
|
|
||||||
id: 'au_289374asf0a98',
|
|
||||||
index: 1,
|
|
||||||
name: 'default',
|
|
||||||
audioFiles: [
|
audioFiles: [
|
||||||
{ // AudioFile.js
|
{ // AudioFile.js
|
||||||
ino: "55450570412017066",
|
ino: "55450570412017066",
|
||||||
@ -124,15 +119,6 @@ new LibraryItem({
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
missingParts: [4, 10], // Array of missing parts in tracklist
|
missingParts: [4, 10], // Array of missing parts in tracklist
|
||||||
addedAt: 1646784672127,
|
|
||||||
updatedAt: 1646784672127
|
|
||||||
}
|
|
||||||
],
|
|
||||||
ebooks: [
|
|
||||||
{ // EBook.js
|
|
||||||
id: 'eb_289374asf0a98',
|
|
||||||
index: 1,
|
|
||||||
name: 'default',
|
|
||||||
ebookFile: { // EBookFile.js
|
ebookFile: { // EBookFile.js
|
||||||
ino: "55450570412017066",
|
ino: "55450570412017066",
|
||||||
metadata: { // FileMetadata.js
|
metadata: { // FileMetadata.js
|
||||||
@ -148,11 +134,7 @@ new LibraryItem({
|
|||||||
ebookFormat: 'mobi',
|
ebookFormat: 'mobi',
|
||||||
addedAt: 1646784672127,
|
addedAt: 1646784672127,
|
||||||
updatedAt: 1646784672127
|
updatedAt: 1646784672127
|
||||||
},
|
|
||||||
addedAt: 1646784672127,
|
|
||||||
updatedAt: 1646784672127
|
|
||||||
}
|
}
|
||||||
]
|
|
||||||
},
|
},
|
||||||
libraryFiles: [
|
libraryFiles: [
|
||||||
{ // LibraryFile.js
|
{ // LibraryFile.js
|
||||||
|
@ -125,7 +125,7 @@ class Server {
|
|||||||
|
|
||||||
this.auth.init()
|
this.auth.init()
|
||||||
|
|
||||||
await this.checkUserLibraryItemProgress() // Remove invalid user item progress
|
await this.checkUserMediaProgress() // Remove invalid user item progress
|
||||||
await this.purgeMetadata() // Remove metadata folders without library item
|
await this.purgeMetadata() // Remove metadata folders without library item
|
||||||
|
|
||||||
await this.backupManager.init()
|
await this.backupManager.init()
|
||||||
@ -299,16 +299,17 @@ class Server {
|
|||||||
return purged
|
return purged
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove user library item progress entries that dont have a library item
|
// Remove user media progress entries that dont have a library item
|
||||||
async checkUserLibraryItemProgress() {
|
// TODO: Check podcast episode exists still
|
||||||
|
async checkUserMediaProgress() {
|
||||||
for (let i = 0; i < this.db.users.length; i++) {
|
for (let i = 0; i < this.db.users.length; i++) {
|
||||||
var _user = this.db.users[i]
|
var _user = this.db.users[i]
|
||||||
if (_user.libraryItemProgress) {
|
if (_user.mediaProgress) {
|
||||||
var itemProgressIdsToRemove = _user.libraryItemProgress.map(lip => lip.id).filter(lipId => !this.db.libraryItems.find(_li => _li.id == lipId))
|
var itemProgressIdsToRemove = _user.mediaProgress.map(lip => lip.id).filter(lipId => !this.db.libraryItems.find(_li => _li.id == lipId))
|
||||||
if (itemProgressIdsToRemove.length) {
|
if (itemProgressIdsToRemove.length) {
|
||||||
Logger.debug(`[Server] Found ${itemProgressIdsToRemove.length} library item progress data to remove from user ${_user.username}`)
|
Logger.debug(`[Server] Found ${itemProgressIdsToRemove.length} media progress data to remove from user ${_user.username}`)
|
||||||
for (const lipId of itemProgressIdsToRemove) {
|
for (const lipId of itemProgressIdsToRemove) {
|
||||||
_user.removeLibraryItemProgress(lipId)
|
_user.removeMediaProgress(lipId)
|
||||||
}
|
}
|
||||||
await this.db.updateEntity('user', _user)
|
await this.db.updateEntity('user', _user)
|
||||||
}
|
}
|
||||||
@ -378,14 +379,14 @@ class Server {
|
|||||||
var session = this.playbackSessionManager.getUserSession(user.id)
|
var session = this.playbackSessionManager.getUserSession(user.id)
|
||||||
if (session) {
|
if (session) {
|
||||||
Logger.debug(`[Server] User Online "${client.user.username}" with session open "${session.id}"`)
|
Logger.debug(`[Server] User Online "${client.user.username}" with session open "${session.id}"`)
|
||||||
session = session.toJSONForClient()
|
|
||||||
var sessionLibraryItem = this.db.libraryItems.find(li => li.id === session.libraryItemId)
|
var sessionLibraryItem = this.db.libraryItems.find(li => li.id === session.libraryItemId)
|
||||||
if (!sessionLibraryItem) {
|
if (!sessionLibraryItem) {
|
||||||
Logger.error(`[Server] Library Item for session "${session.id}" does not exist "${session.libraryItemId}"`)
|
Logger.error(`[Server] Library Item for session "${session.id}" does not exist "${session.libraryItemId}"`)
|
||||||
this.playbackSessionManager.removeSession(session.id)
|
this.playbackSessionManager.removeSession(session.id)
|
||||||
session = null
|
session = null
|
||||||
} else {
|
}
|
||||||
session.libraryItem = sessionLibraryItem.toJSONExpanded()
|
if (session) {
|
||||||
|
session = session.toJSONForClient(sessionLibraryItem)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Logger.debug(`[Server] User Online ${client.user.username}`)
|
Logger.debug(`[Server] User Online ${client.user.username}`)
|
||||||
|
@ -138,11 +138,7 @@ class LibraryController {
|
|||||||
// api/libraries/:id/items
|
// api/libraries/:id/items
|
||||||
// TODO: Optimize this method, items are iterated through several times but can be combined
|
// TODO: Optimize this method, items are iterated through several times but can be combined
|
||||||
getLibraryItems(req, res) {
|
getLibraryItems(req, res) {
|
||||||
var media = req.query.media || 'all'
|
var libraryItems = req.libraryItems
|
||||||
var libraryItems = req.libraryItems.filter(li => {
|
|
||||||
if (media != 'all') return li.mediaType == media
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
var payload = {
|
var payload = {
|
||||||
results: [],
|
results: [],
|
||||||
total: libraryItems.length,
|
total: libraryItems.length,
|
||||||
@ -151,7 +147,7 @@ class LibraryController {
|
|||||||
sortBy: req.query.sort,
|
sortBy: req.query.sort,
|
||||||
sortDesc: req.query.desc === '1',
|
sortDesc: req.query.desc === '1',
|
||||||
filterBy: req.query.filter,
|
filterBy: req.query.filter,
|
||||||
media,
|
mediaType: req.library.mediaType,
|
||||||
minified: req.query.minified === '1',
|
minified: req.query.minified === '1',
|
||||||
collapseseries: req.query.collapseseries === '1'
|
collapseseries: req.query.collapseseries === '1'
|
||||||
}
|
}
|
||||||
|
@ -164,13 +164,26 @@ class LibraryItemController {
|
|||||||
|
|
||||||
// POST: api/items/:id/play
|
// POST: api/items/:id/play
|
||||||
startPlaybackSession(req, res) {
|
startPlaybackSession(req, res) {
|
||||||
var playbackMediaEntity = req.libraryItem.getPlaybackMediaEntity()
|
if (!req.libraryItem.media.numTracks) {
|
||||||
if (!playbackMediaEntity) {
|
Logger.error(`[LibraryItemController] startPlaybackSession cannot playback ${req.libraryItem.id}`)
|
||||||
Logger.error(`[LibraryItemController] startPlaybackSession no playback media entity ${req.libraryItem.id}`)
|
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
}
|
}
|
||||||
const options = req.body || {}
|
const options = req.body || {}
|
||||||
this.playbackSessionManager.startSessionRequest(req.user, req.libraryItem, playbackMediaEntity, options, res)
|
this.playbackSessionManager.startSessionRequest(req.user, req.libraryItem, options, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PATCH: api/items/:id/tracks
|
||||||
|
async updateTracks(req, res) {
|
||||||
|
var libraryItem = req.libraryItem
|
||||||
|
var orderedFileData = req.body.orderedFileData
|
||||||
|
if (!libraryItem.media.updateAudioTracks) {
|
||||||
|
Logger.error(`[LibraryItemController] updateTracks invalid media type ${libraryItem.id}`)
|
||||||
|
return res.sendStatus(500)
|
||||||
|
}
|
||||||
|
libraryItem.media.updateAudioTracks(orderedFileData)
|
||||||
|
await this.db.updateLibraryItem(libraryItem)
|
||||||
|
this.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||||
|
res.json(libraryItem.toJSON())
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST api/items/:id/match
|
// POST api/items/:id/match
|
||||||
|
@ -17,8 +17,8 @@ class MeController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DELETE: api/me/progress/:id
|
// DELETE: api/me/progress/:id
|
||||||
async removeLibraryItemProgress(req, res) {
|
async removeMediaProgress(req, res) {
|
||||||
var wasRemoved = req.user.removeLibraryItemProgress(req.params.id)
|
var wasRemoved = req.user.removeMediaProgress(req.params.id)
|
||||||
if (!wasRemoved) {
|
if (!wasRemoved) {
|
||||||
return res.sendStatus(200)
|
return res.sendStatus(200)
|
||||||
}
|
}
|
||||||
@ -30,12 +30,12 @@ class MeController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PATCH: api/me/progress/:id
|
// PATCH: api/me/progress/:id
|
||||||
async createUpdateLibraryItemProgress(req, res) {
|
async createUpdateMediaProgress(req, res) {
|
||||||
var libraryItem = this.db.libraryItems.find(ab => ab.id === req.params.id)
|
var libraryItem = this.db.libraryItems.find(ab => ab.id === req.params.id)
|
||||||
if (!libraryItem) {
|
if (!libraryItem) {
|
||||||
return res.status(404).send('Item not found')
|
return res.status(404).send('Item not found')
|
||||||
}
|
}
|
||||||
var wasUpdated = req.user.createUpdateLibraryItemProgress(libraryItem, req.body)
|
var wasUpdated = req.user.createUpdateMediaProgress(libraryItem, req.body)
|
||||||
if (wasUpdated) {
|
if (wasUpdated) {
|
||||||
await this.db.updateEntity('user', req.user)
|
await this.db.updateEntity('user', req.user)
|
||||||
this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||||
@ -44,7 +44,7 @@ class MeController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PATCH: api/me/progress/batch/update
|
// PATCH: api/me/progress/batch/update
|
||||||
async batchUpdateLibraryItemProgress(req, res) {
|
async batchUpdateMediaProgress(req, res) {
|
||||||
var itemProgressPayloads = req.body
|
var itemProgressPayloads = req.body
|
||||||
if (!itemProgressPayloads || !itemProgressPayloads.length) {
|
if (!itemProgressPayloads || !itemProgressPayloads.length) {
|
||||||
return res.sendStatus(500)
|
return res.sendStatus(500)
|
||||||
@ -54,10 +54,10 @@ class MeController {
|
|||||||
itemProgressPayloads.forEach((itemProgress) => {
|
itemProgressPayloads.forEach((itemProgress) => {
|
||||||
var libraryItem = this.db.libraryItems.find(li => li.id === itemProgress.id) // Make sure this library item exists
|
var libraryItem = this.db.libraryItems.find(li => li.id === itemProgress.id) // Make sure this library item exists
|
||||||
if (libraryItem) {
|
if (libraryItem) {
|
||||||
var wasUpdated = req.user.createUpdateLibraryItemProgress(libraryItem, itemProgress)
|
var wasUpdated = req.user.createUpdateMediaProgress(libraryItem, itemProgress)
|
||||||
if (wasUpdated) shouldUpdate = true
|
if (wasUpdated) shouldUpdate = true
|
||||||
} else {
|
} else {
|
||||||
Logger.error(`[MeController] batchUpdateLibraryItemProgress: Library Item does not exist ${itemProgress.id}`)
|
Logger.error(`[MeController] batchUpdateMediaProgress: Library Item does not exist ${itemProgress.id}`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,71 +0,0 @@
|
|||||||
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()
|
|
@ -25,14 +25,16 @@ class PlaybackSessionManager {
|
|||||||
return session ? session.stream : null
|
return session ? session.stream : null
|
||||||
}
|
}
|
||||||
|
|
||||||
async startSessionRequest(user, libraryItem, mediaEntity, options, res) {
|
async startSessionRequest(user, libraryItem, options, res) {
|
||||||
const session = await this.startSession(user, libraryItem, mediaEntity, options)
|
const session = await this.startSession(user, libraryItem, options)
|
||||||
res.json(session.toJSONForClient())
|
res.json(session.toJSONForClient(libraryItem))
|
||||||
}
|
}
|
||||||
|
|
||||||
async syncSessionRequest(user, session, payload, res) {
|
async syncSessionRequest(user, session, payload, res) {
|
||||||
await this.syncSession(user, session, payload)
|
var result = await this.syncSession(user, session, payload)
|
||||||
res.json(session.toJSONForClient())
|
if (result) {
|
||||||
|
res.json(session.toJSONForClient(result.libraryItem))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async closeSessionRequest(user, session, syncData, res) {
|
async closeSessionRequest(user, session, syncData, res) {
|
||||||
@ -40,23 +42,23 @@ class PlaybackSessionManager {
|
|||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
async startSession(user, libraryItem, mediaEntity, options) {
|
async startSession(user, libraryItem, options) {
|
||||||
var shouldDirectPlay = options.forceDirectPlay || (!options.forceTranscode && mediaEntity.checkCanDirectPlay(options))
|
var shouldDirectPlay = options.forceDirectPlay || (!options.forceTranscode && libraryItem.media.checkCanDirectPlay(options))
|
||||||
|
|
||||||
const userProgress = user.getLibraryItemProgress(libraryItem.id)
|
const userProgress = user.getMediaProgress(libraryItem.id)
|
||||||
var userStartTime = 0
|
var userStartTime = 0
|
||||||
if (userProgress) userStartTime = userProgress.currentTime || 0
|
if (userProgress) userStartTime = userProgress.currentTime || 0
|
||||||
const newPlaybackSession = new PlaybackSession()
|
const newPlaybackSession = new PlaybackSession()
|
||||||
newPlaybackSession.setData(libraryItem, mediaEntity, user)
|
newPlaybackSession.setData(libraryItem, user)
|
||||||
|
|
||||||
var audioTracks = []
|
var audioTracks = []
|
||||||
if (shouldDirectPlay) {
|
if (shouldDirectPlay) {
|
||||||
Logger.debug(`[PlaybackSessionManager] "${user.username}" starting direct play session for media entity "${mediaEntity.id}"`)
|
Logger.debug(`[PlaybackSessionManager] "${user.username}" starting direct play session for item "${libraryItem.id}"`)
|
||||||
audioTracks = mediaEntity.getDirectPlayTracklist(libraryItem.id)
|
audioTracks = libraryItem.getDirectPlayTracklist(libraryItem.id)
|
||||||
newPlaybackSession.playMethod = PlayMethod.DIRECTPLAY
|
newPlaybackSession.playMethod = PlayMethod.DIRECTPLAY
|
||||||
} else {
|
} else {
|
||||||
Logger.debug(`[PlaybackSessionManager] "${user.username}" starting stream session for media entity "${mediaEntity.id}"`)
|
Logger.debug(`[PlaybackSessionManager] "${user.username}" starting stream session for item "${libraryItem.id}"`)
|
||||||
var stream = new Stream(newPlaybackSession.id, this.StreamsPath, user, libraryItem, mediaEntity, userStartTime, this.clientEmitter.bind(this))
|
var stream = new Stream(newPlaybackSession.id, this.StreamsPath, user, libraryItem, userStartTime, this.clientEmitter.bind(this))
|
||||||
await stream.generatePlaylist()
|
await stream.generatePlaylist()
|
||||||
audioTracks = [stream.getAudioTrack()]
|
audioTracks = [stream.getAudioTrack()]
|
||||||
newPlaybackSession.stream = stream
|
newPlaybackSession.stream = stream
|
||||||
@ -83,7 +85,7 @@ class PlaybackSessionManager {
|
|||||||
var libraryItem = this.db.libraryItems.find(li => li.id === session.libraryItemId)
|
var libraryItem = this.db.libraryItems.find(li => li.id === session.libraryItemId)
|
||||||
if (!libraryItem) {
|
if (!libraryItem) {
|
||||||
Logger.error(`[PlaybackSessionManager] syncSession Library Item not found "${sessino.libraryItemId}"`)
|
Logger.error(`[PlaybackSessionManager] syncSession Library Item not found "${sessino.libraryItemId}"`)
|
||||||
return
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
session.currentTime = syncData.currentTime
|
session.currentTime = syncData.currentTime
|
||||||
@ -91,21 +93,23 @@ class PlaybackSessionManager {
|
|||||||
Logger.debug(`[PlaybackSessionManager] syncSession "${session.id}" | Total Time Listened: ${session.timeListening}`)
|
Logger.debug(`[PlaybackSessionManager] syncSession "${session.id}" | Total Time Listened: ${session.timeListening}`)
|
||||||
|
|
||||||
const itemProgressUpdate = {
|
const itemProgressUpdate = {
|
||||||
mediaEntityId: syncData.mediaEntityId || null,
|
|
||||||
duration: syncData.duration,
|
duration: syncData.duration,
|
||||||
currentTime: syncData.currentTime,
|
currentTime: syncData.currentTime,
|
||||||
progress: session.progress
|
progress: session.progress
|
||||||
}
|
}
|
||||||
var wasUpdated = user.createUpdateLibraryItemProgress(libraryItem, itemProgressUpdate)
|
var wasUpdated = user.createUpdateMediaProgress(libraryItem, itemProgressUpdate)
|
||||||
if (wasUpdated) {
|
if (wasUpdated) {
|
||||||
await this.db.updateEntity('user', user)
|
await this.db.updateEntity('user', user)
|
||||||
var itemProgress = user.getLibraryItemProgress(session.libraryItemId)
|
var itemProgress = user.getMediaProgress(session.libraryItemId)
|
||||||
this.clientEmitter(user.id, 'user_item_progress_updated', {
|
this.clientEmitter(user.id, 'user_item_progress_updated', {
|
||||||
id: itemProgress.id,
|
id: itemProgress.id,
|
||||||
data: itemProgress.toJSON()
|
data: itemProgress.toJSON()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
this.saveSession(session)
|
this.saveSession(session)
|
||||||
|
return {
|
||||||
|
libraryItem
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async closeSession(user, session, syncData = null) {
|
async closeSession(user, session, syncData = null) {
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
const fs = require('fs-extra')
|
const fs = require('fs-extra')
|
||||||
|
const cron = require('node-cron')
|
||||||
|
const axios = require('axios')
|
||||||
|
|
||||||
|
const { parsePodcastRssFeedXml } = require('../utils/podcastUtils')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
const { downloadFile } = require('../utils/fileUtils')
|
const { downloadFile } = require('../utils/fileUtils')
|
||||||
@ -16,6 +20,15 @@ class PodcastManager {
|
|||||||
|
|
||||||
this.downloadQueue = []
|
this.downloadQueue = []
|
||||||
this.currentDownload = null
|
this.currentDownload = null
|
||||||
|
|
||||||
|
this.episodeScheduleTask = null
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
var podcastsWithAutoDownload = this.db.libraryItems.find(li => li.mediaType === 'podcast' && li.media.autoDownloadEpisodes)
|
||||||
|
if (podcastsWithAutoDownload.length) {
|
||||||
|
this.schedulePodcastEpisodeCron()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadPodcastEpisodes(libraryItem, episodesToDownload) {
|
async downloadPodcastEpisodes(libraryItem, episodesToDownload) {
|
||||||
@ -97,5 +110,37 @@ class PodcastManager {
|
|||||||
newAudioFile.setDataFromProbe(libraryFile, audioProbeData)
|
newAudioFile.setDataFromProbe(libraryFile, audioProbeData)
|
||||||
return newAudioFile
|
return newAudioFile
|
||||||
}
|
}
|
||||||
|
|
||||||
|
schedulePodcastEpisodeCron() {
|
||||||
|
try {
|
||||||
|
this.episodeScheduleTask = cron.schedule(this.serverSettings.podcastEpisodeSchedule, this.checkForNewEpisodes.bind(this))
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(`[PodcastManager] Failed to schedule podcast cron ${this.serverSettings.backupSchedule}`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkForNewEpisodes() {
|
||||||
|
var podcastsWithAutoDownload = this.db.libraryItems.find(li => li.mediaType === 'podcast' && li.media.autoDownloadEpisodes)
|
||||||
|
for (const libraryItem of podcastsWithAutoDownload) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getPodcastFeed(podcastMedia) {
|
||||||
|
axios.get(podcastMedia.feedUrl).then(async (data) => {
|
||||||
|
if (!data || !data.data) {
|
||||||
|
Logger.error('Invalid podcast feed request response')
|
||||||
|
return res.status(500).send('Bad response from feed request')
|
||||||
|
}
|
||||||
|
var podcast = await parsePodcastRssFeedXml(data.data)
|
||||||
|
if (!podcast) {
|
||||||
|
return res.status(500).send('Invalid podcast RSS feed')
|
||||||
|
}
|
||||||
|
res.json(podcast)
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('Failed', error)
|
||||||
|
res.status(500).send(error)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = PodcastManager
|
module.exports = PodcastManager
|
@ -358,8 +358,8 @@ class LibraryItem {
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
if (filesRemoved.length) {
|
if (filesRemoved.length) {
|
||||||
if (this.media.audiobooks && this.media.audiobooks.length) {
|
if (this.media.mediaType === 'book') {
|
||||||
this.media.audiobooks.forEach(ab => ab.checkUpdateMissingTracks())
|
this.media.checkUpdateMissingTracks()
|
||||||
}
|
}
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
@ -404,19 +404,16 @@ class LibraryItem {
|
|||||||
var hasUpdated = false
|
var hasUpdated = false
|
||||||
|
|
||||||
if (this.mediaType === 'book') {
|
if (this.mediaType === 'book') {
|
||||||
// Add/update ebook files (ebooks that were removed are removed in checkScanData)
|
// Add/update ebook file (ebooks that were removed are removed in checkScanData)
|
||||||
this.libraryFiles.forEach((lf) => {
|
this.libraryFiles.forEach((lf) => {
|
||||||
if (lf.fileType === 'ebook') {
|
if (lf.fileType === 'ebook') {
|
||||||
var existingFile = this.media.findFileWithInode(lf.ino)
|
if (!this.media.ebookFile) {
|
||||||
if (!existingFile) {
|
this.media.setEbookFile(lf)
|
||||||
this.media.addEbookFile(lf)
|
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
} else if (existingFile.ebookFormat) {
|
} else if (this.media.ebookFile.ino == lf.ino && this.media.ebookFile.updateFromLibraryFile(lf)) { // Update existing ebookFile
|
||||||
if (existingFile.updateFromLibraryFile(lf)) {// EBookFile.js
|
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -447,8 +444,8 @@ class LibraryItem {
|
|||||||
return this.media.searchQuery(query)
|
return this.media.searchQuery(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
getPlaybackMediaEntity() {
|
getDirectPlayTracklist(libraryItemId) {
|
||||||
return this.media.getPlaybackMediaEntity()
|
return this.media.getDirectPlayTracklist(libraryItemId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = LibraryItem
|
module.exports = LibraryItem
|
@ -9,10 +9,10 @@ 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.coverPath = null
|
||||||
this.duration = null
|
this.duration = null
|
||||||
|
|
||||||
this.playMethod = null
|
this.playMethod = null
|
||||||
@ -41,9 +41,9 @@ 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,
|
||||||
|
coverPath: this.coverPath,
|
||||||
duration: this.duration,
|
duration: this.duration,
|
||||||
playMethod: this.playMethod,
|
playMethod: this.playMethod,
|
||||||
date: this.date,
|
date: this.date,
|
||||||
@ -54,15 +54,15 @@ class PlaybackSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSONForClient() {
|
toJSONForClient(libraryItem) {
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
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,
|
||||||
|
coverPath: this.coverPath,
|
||||||
duration: this.duration,
|
duration: this.duration,
|
||||||
playMethod: this.playMethod,
|
playMethod: this.playMethod,
|
||||||
date: this.date,
|
date: this.date,
|
||||||
@ -71,7 +71,8 @@ class PlaybackSession {
|
|||||||
lastUpdate: this.lastUpdate,
|
lastUpdate: this.lastUpdate,
|
||||||
updatedAt: this.updatedAt,
|
updatedAt: this.updatedAt,
|
||||||
audioTracks: this.audioTracks.map(at => at.toJSON()),
|
audioTracks: this.audioTracks.map(at => at.toJSON()),
|
||||||
currentTime: this.currentTime
|
currentTime: this.currentTime,
|
||||||
|
libraryItem: libraryItem.toJSONExpanded()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,7 +81,6 @@ class PlaybackSession {
|
|||||||
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.duration = session.duration
|
||||||
this.playMethod = session.playMethod
|
this.playMethod = session.playMethod
|
||||||
@ -93,7 +93,7 @@ class PlaybackSession {
|
|||||||
this.mediaMetadata = new PodcastMetadata(session.mediaMetadata)
|
this.mediaMetadata = new PodcastMetadata(session.mediaMetadata)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.coverPath = session.coverPath
|
||||||
this.date = session.date
|
this.date = session.date
|
||||||
this.dayOfWeek = session.dayOfWeek
|
this.dayOfWeek = session.dayOfWeek
|
||||||
|
|
||||||
@ -107,14 +107,14 @@ class PlaybackSession {
|
|||||||
return Math.max(0, Math.min(this.currentTime / this.duration, 1))
|
return Math.max(0, Math.min(this.currentTime / this.duration, 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
setData(libraryItem, mediaEntity, user) {
|
setData(libraryItem, user) {
|
||||||
this.id = getId('play')
|
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.duration = mediaEntity.duration
|
this.coverPath = libraryItem.media.coverPath
|
||||||
|
this.duration = libraryItem.media.duration
|
||||||
|
|
||||||
this.timeListening = 0
|
this.timeListening = 0
|
||||||
this.date = date.format(new Date(), 'YYYY-MM-DD')
|
this.date = date.format(new Date(), 'YYYY-MM-DD')
|
||||||
|
@ -39,6 +39,9 @@ class ServerSettings {
|
|||||||
this.coverAspectRatio = BookCoverAspectRatio.SQUARE
|
this.coverAspectRatio = BookCoverAspectRatio.SQUARE
|
||||||
this.bookshelfView = BookshelfView.STANDARD
|
this.bookshelfView = BookshelfView.STANDARD
|
||||||
|
|
||||||
|
// Podcasts
|
||||||
|
this.podcastEpisodeSchedule = '0 * * * *' // Every hour
|
||||||
|
|
||||||
this.sortingIgnorePrefix = false
|
this.sortingIgnorePrefix = false
|
||||||
this.chromecastEnabled = false
|
this.chromecastEnabled = false
|
||||||
this.logLevel = Logger.logLevel
|
this.logLevel = Logger.logLevel
|
||||||
|
@ -9,13 +9,12 @@ const hlsPlaylistGenerator = require('../utils/hlsPlaylistGenerator')
|
|||||||
const AudioTrack = require('./files/AudioTrack')
|
const AudioTrack = require('./files/AudioTrack')
|
||||||
|
|
||||||
class Stream extends EventEmitter {
|
class Stream extends EventEmitter {
|
||||||
constructor(sessionId, streamPath, user, libraryItem, mediaEntity, startTime, clientEmitter, transcodeOptions = {}) {
|
constructor(sessionId, streamPath, user, libraryItem, startTime, clientEmitter, transcodeOptions = {}) {
|
||||||
super()
|
super()
|
||||||
|
|
||||||
this.id = sessionId
|
this.id = sessionId
|
||||||
this.user = user
|
this.user = user
|
||||||
this.libraryItem = libraryItem
|
this.libraryItem = libraryItem
|
||||||
this.mediaEntity = mediaEntity
|
|
||||||
this.clientEmitter = clientEmitter
|
this.clientEmitter = clientEmitter
|
||||||
|
|
||||||
this.transcodeOptions = transcodeOptions
|
this.transcodeOptions = transcodeOptions
|
||||||
@ -46,17 +45,12 @@ class Stream extends EventEmitter {
|
|||||||
get mediaTitle() {
|
get mediaTitle() {
|
||||||
return this.libraryItem.media.metadata.title || ''
|
return this.libraryItem.media.metadata.title || ''
|
||||||
}
|
}
|
||||||
get mediaEntityName() {
|
|
||||||
return this.mediaEntity.name
|
|
||||||
}
|
|
||||||
get itemTitle() {
|
|
||||||
return `${this.mediaTitle} (${this.mediaEntityName})`
|
|
||||||
}
|
|
||||||
get totalDuration() {
|
get totalDuration() {
|
||||||
return this.mediaEntity.duration
|
return this.libraryItem.media.duration
|
||||||
}
|
}
|
||||||
get tracks() {
|
get tracks() {
|
||||||
return this.mediaEntity.tracks
|
// TODO: Podcast episode tracks
|
||||||
|
return this.libraryItem.media.tracks
|
||||||
}
|
}
|
||||||
get tracksAudioFileType() {
|
get tracksAudioFileType() {
|
||||||
if (!this.tracks.length) return null
|
if (!this.tracks.length) return null
|
||||||
@ -226,7 +220,7 @@ class Stream extends EventEmitter {
|
|||||||
if (!this.isTranscodeComplete) {
|
if (!this.isTranscodeComplete) {
|
||||||
this.checkFiles()
|
this.checkFiles()
|
||||||
} else {
|
} else {
|
||||||
Logger.info(`[Stream] ${this.itemTitle} sending stream_ready`)
|
Logger.info(`[Stream] ${this.mediaTitle} sending stream_ready`)
|
||||||
this.clientEmit('stream_ready')
|
this.clientEmit('stream_ready')
|
||||||
clearInterval(intervalId)
|
clearInterval(intervalId)
|
||||||
}
|
}
|
||||||
@ -414,7 +408,7 @@ class Stream extends EventEmitter {
|
|||||||
|
|
||||||
getAudioTrack() {
|
getAudioTrack() {
|
||||||
var newAudioTrack = new AudioTrack()
|
var newAudioTrack = new AudioTrack()
|
||||||
newAudioTrack.setFromStream(this.itemTitle, this.totalDuration, this.clientPlaylistUri)
|
newAudioTrack.setFromStream(this.mediaTitle, this.totalDuration, this.clientPlaylistUri)
|
||||||
return newAudioTrack
|
return newAudioTrack
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,6 @@ class PodcastEpisode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get isPlaybackMediaEntity() { return true }
|
|
||||||
get tracks() {
|
get tracks() {
|
||||||
return [this.audioFile]
|
return [this.audioFile]
|
||||||
}
|
}
|
||||||
|
@ -5,10 +5,9 @@ const abmetadataGenerator = require('../../utils/abmetadataGenerator')
|
|||||||
const { areEquivalent, copyValue } = require('../../utils/index')
|
const { areEquivalent, copyValue } = require('../../utils/index')
|
||||||
const { parseOpfMetadataXML } = require('../../utils/parseOpfMetadata')
|
const { parseOpfMetadataXML } = require('../../utils/parseOpfMetadata')
|
||||||
const { readTextFile } = require('../../utils/fileUtils')
|
const { readTextFile } = require('../../utils/fileUtils')
|
||||||
|
const AudioFile = require('../files/AudioFile')
|
||||||
|
const AudioTrack = require('../files/AudioTrack')
|
||||||
const EBookFile = require('../files/EBookFile')
|
const EBookFile = require('../files/EBookFile')
|
||||||
const Audiobook = require('../entities/Audiobook')
|
|
||||||
const EBook = require('../entities/EBook')
|
|
||||||
|
|
||||||
class Book {
|
class Book {
|
||||||
constructor(book) {
|
constructor(book) {
|
||||||
@ -17,8 +16,10 @@ class Book {
|
|||||||
this.coverPath = null
|
this.coverPath = null
|
||||||
this.tags = []
|
this.tags = []
|
||||||
|
|
||||||
this.audiobooks = []
|
this.audioFiles = []
|
||||||
this.ebooks = []
|
this.chapters = []
|
||||||
|
this.missingParts = []
|
||||||
|
this.ebookFile = null
|
||||||
|
|
||||||
this.lastCoverSearch = null
|
this.lastCoverSearch = null
|
||||||
this.lastCoverSearchQuery = null
|
this.lastCoverSearchQuery = null
|
||||||
@ -32,8 +33,10 @@ class Book {
|
|||||||
this.metadata = new BookMetadata(book.metadata)
|
this.metadata = new BookMetadata(book.metadata)
|
||||||
this.coverPath = book.coverPath
|
this.coverPath = book.coverPath
|
||||||
this.tags = [...book.tags]
|
this.tags = [...book.tags]
|
||||||
this.audiobooks = book.audiobooks.map(ab => new Audiobook(ab))
|
this.audioFiles = book.audioFiles.map(f => new AudioFile(f))
|
||||||
this.ebooks = book.ebooks.map(eb => new EBook(eb))
|
this.chapters = book.chapters.map(c => ({ ...c }))
|
||||||
|
this.missingParts = book.missingParts ? [...book.missingParts] : []
|
||||||
|
this.ebookFile = book.ebookFile ? new EBookFile(book.ebookFile) : null
|
||||||
this.lastCoverSearch = book.lastCoverSearch || null
|
this.lastCoverSearch = book.lastCoverSearch || null
|
||||||
this.lastCoverSearchQuery = book.lastCoverSearchQuery || null
|
this.lastCoverSearchQuery = book.lastCoverSearchQuery || null
|
||||||
}
|
}
|
||||||
@ -43,8 +46,10 @@ class Book {
|
|||||||
metadata: this.metadata.toJSON(),
|
metadata: this.metadata.toJSON(),
|
||||||
coverPath: this.coverPath,
|
coverPath: this.coverPath,
|
||||||
tags: [...this.tags],
|
tags: [...this.tags],
|
||||||
audiobooks: this.audiobooks.map(ab => ab.toJSON()),
|
audioFiles: this.audioFiles.map(f => f.toJSON()),
|
||||||
ebooks: this.ebooks.map(eb => eb.toJSON())
|
chapters: this.chapters.map(c => ({ ...c })),
|
||||||
|
missingParts: [...this.missingParts],
|
||||||
|
ebookFile: this.ebookFile ? this.ebookFile.toJSON() : null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,9 +58,13 @@ class Book {
|
|||||||
metadata: this.metadata.toJSON(),
|
metadata: this.metadata.toJSON(),
|
||||||
coverPath: this.coverPath,
|
coverPath: this.coverPath,
|
||||||
tags: [...this.tags],
|
tags: [...this.tags],
|
||||||
audiobooks: this.audiobooks.map(ab => ab.toJSONMinified()),
|
numTracks: this.tracks.length,
|
||||||
ebooks: this.ebooks.map(eb => eb.toJSONMinified()),
|
numAudioFiles: this.audioFiles.length,
|
||||||
size: this.size
|
numChapters: this.chapters.length,
|
||||||
|
numMissingParts: this.missingParts.length,
|
||||||
|
duration: this.duration,
|
||||||
|
size: this.size,
|
||||||
|
ebookFormat: this.ebookFile ? this.ebookFile.ebookFormat : null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,24 +73,26 @@ class Book {
|
|||||||
metadata: this.metadata.toJSONExpanded(),
|
metadata: this.metadata.toJSONExpanded(),
|
||||||
coverPath: this.coverPath,
|
coverPath: this.coverPath,
|
||||||
tags: [...this.tags],
|
tags: [...this.tags],
|
||||||
audiobooks: this.audiobooks.map(ab => ab.toJSONExpanded()),
|
audioFiles: this.audioFiles.map(f => f.toJSON()),
|
||||||
ebooks: this.ebooks.map(eb => eb.toJSONExpanded()),
|
chapters: this.chapters.map(c => ({ ...c })),
|
||||||
|
duration: this.duration,
|
||||||
size: this.size,
|
size: this.size,
|
||||||
|
tracks: this.tracks.map(t => t.toJSON()),
|
||||||
|
missingParts: [...this.missingParts],
|
||||||
|
ebookFile: this.ebookFile ? this.ebookFile.toJSON() : null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get size() {
|
get size() {
|
||||||
var total = 0
|
var total = 0
|
||||||
this.audiobooks.forEach((ab) => {
|
this.audioFiles.forEach((af) => total += af.metadata.size)
|
||||||
total += ab.size
|
if (this.ebookFile) {
|
||||||
})
|
total += this.ebookFile.metadata.size
|
||||||
this.ebooks.forEach((eb) => {
|
}
|
||||||
total += eb.size
|
|
||||||
})
|
|
||||||
return total
|
return total
|
||||||
}
|
}
|
||||||
get hasMediaEntities() {
|
get hasMediaEntities() {
|
||||||
return !!(this.audiobooks.length + this.ebooks.length)
|
return !!this.tracks.length || this.ebookFile
|
||||||
}
|
}
|
||||||
get shouldSearchForCover() {
|
get shouldSearchForCover() {
|
||||||
if (this.coverPath) return false
|
if (this.coverPath) return false
|
||||||
@ -89,10 +100,22 @@ class Book {
|
|||||||
return (Date.now() - this.lastCoverSearch) > 1000 * 60 * 60 * 24 * 7 // 7 day
|
return (Date.now() - this.lastCoverSearch) > 1000 * 60 * 60 * 24 * 7 // 7 day
|
||||||
}
|
}
|
||||||
get hasEmbeddedCoverArt() {
|
get hasEmbeddedCoverArt() {
|
||||||
return this.audiobooks.some(ab => ab.hasEmbeddedCoverArt)
|
return this.audioFiles.some(af => af.embeddedCoverArt)
|
||||||
}
|
}
|
||||||
get hasIssues() {
|
get hasIssues() {
|
||||||
return this.audiobooks.some(ab => ab.missingParts.length)
|
return this.missingParts.length || this.audioFiles.some(af => af.invalid)
|
||||||
|
}
|
||||||
|
get tracks() {
|
||||||
|
|
||||||
|
return this.audioFiles.filter(af => !af.exclude && !af.invalid)
|
||||||
|
}
|
||||||
|
get duration() {
|
||||||
|
var total = 0
|
||||||
|
this.tracks.forEach((track) => total += track.duration)
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
get numTracks() {
|
||||||
|
return this.tracks.length
|
||||||
}
|
}
|
||||||
|
|
||||||
update(payload) {
|
update(payload) {
|
||||||
@ -123,42 +146,22 @@ class Book {
|
|||||||
this.coverPath = coverPath
|
this.coverPath = coverPath
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
getAudiobookById(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))
|
if (this.audioFiles.some(af => af.ino === inode)) {
|
||||||
if (audiobookWithIno) {
|
this.audioFiles = this.audioFiles.filter(af => af.ino !== inode)
|
||||||
audiobookWithIno.removeFileWithInode(inode)
|
|
||||||
if (!audiobookWithIno.audioFiles.length) { // All audio files removed = remove audiobook
|
|
||||||
this.audiobooks = this.audiobooks.filter(ab => ab.id !== audiobookWithIno.id)
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
var ebookWithIno = this.ebooks.find(eb => eb.findFileWithInode(inode))
|
if (this.ebookFile && this.ebookFile.ino === inode) {
|
||||||
if (ebookWithIno) {
|
this.ebookFile = null
|
||||||
this.ebooks = this.ebooks.filter(eb => eb.id !== ebookWithIno.id) // Remove ebook
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
findFileWithInode(inode) {
|
findFileWithInode(inode) {
|
||||||
var audioFile = this.audiobooks.find(ab => ab.findFileWithInode(inode))
|
var audioFile = this.audioFiles.find(af => af.ino === inode)
|
||||||
if (audioFile) return audioFile
|
if (audioFile) return audioFile
|
||||||
var ebookFile = this.ebooks.find(eb => eb.findFileWithInode(inode))
|
if (this.ebookFile && this.ebookFile.ino === inode) return this.ebookFile
|
||||||
if (ebookFile) return ebookFile
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,9 +172,8 @@ class Book {
|
|||||||
|
|
||||||
// Audio file metadata tags map to book details (will not overwrite)
|
// Audio file metadata tags map to book details (will not overwrite)
|
||||||
setMetadataFromAudioFile(overrideExistingDetails = false) {
|
setMetadataFromAudioFile(overrideExistingDetails = false) {
|
||||||
if (!this.audiobooks.length) return false
|
if (!this.audioFiles.length) return false
|
||||||
var audiobook = this.audiobooks[0]
|
var audioFile = this.audioFiles[0]
|
||||||
var audioFile = audiobook.audioFiles[0]
|
|
||||||
if (!audioFile.metaTags) return false
|
if (!audioFile.metaTags) return false
|
||||||
return this.metadata.setDataFromAudioMetaTags(audioFile.metaTags, overrideExistingDetails)
|
return this.metadata.setDataFromAudioMetaTags(audioFile.metaTags, overrideExistingDetails)
|
||||||
}
|
}
|
||||||
@ -276,50 +278,135 @@ class Book {
|
|||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
|
|
||||||
addEbookFile(libraryFile) {
|
setEbookFile(libraryFile) {
|
||||||
var ebookFile = new EBookFile()
|
var ebookFile = new EBookFile()
|
||||||
ebookFile.setData(libraryFile)
|
ebookFile.setData(libraryFile)
|
||||||
|
this.ebookFile = ebookFile
|
||||||
var ebookIndex = this.ebooks.length + 1
|
|
||||||
var newEBook = new EBook()
|
|
||||||
newEBook.setData(ebookFile, ebookIndex)
|
|
||||||
this.ebooks.push(newEBook)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getCreateAudiobookVariant(variant) {
|
addAudioFile(audioFile) {
|
||||||
if (this.audiobooks.length) {
|
this.audioFiles.push(audioFile)
|
||||||
var ab = this.audiobooks.find(ab => ab.name == variantName)
|
|
||||||
if (ab) return ab
|
|
||||||
}
|
|
||||||
var abIndex = this.audiobooks.length + 1
|
|
||||||
var newAb = new Audiobook()
|
|
||||||
newAb.setData(variant, abIndex)
|
|
||||||
this.audiobooks.push(newAb)
|
|
||||||
return newAb
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addAudioFileToAudiobook(audioFile, variant = 'default') { // Create if none
|
updateAudioTracks(orderedFileData) {
|
||||||
var audiobook = this.getCreateAudiobookVariant(variant)
|
var index = 1
|
||||||
audiobook.audioFiles.push(audioFile)
|
this.audioFiles = orderedFileData.map((fileData) => {
|
||||||
|
var audioFile = this.audioFiles.find(af => af.ino === fileData.ino)
|
||||||
|
audioFile.manuallyVerified = true
|
||||||
|
audioFile.invalid = false
|
||||||
|
audioFile.error = null
|
||||||
|
if (fileData.exclude !== undefined) {
|
||||||
|
audioFile.exclude = !!fileData.exclude
|
||||||
}
|
}
|
||||||
|
if (audioFile.exclude) {
|
||||||
getLongestDuration() {
|
audioFile.index = -1
|
||||||
if (!this.audiobooks.length) return 0
|
} else {
|
||||||
var longest = 0
|
audioFile.index = index++
|
||||||
this.audiobooks.forEach((ab) => {
|
}
|
||||||
if (ab.duration > longest) longest = ab.duration
|
return audioFile
|
||||||
})
|
})
|
||||||
return longest
|
|
||||||
|
this.rebuildTracks()
|
||||||
}
|
}
|
||||||
getTotalAudioTracks() {
|
|
||||||
var total = 0
|
rebuildTracks() {
|
||||||
this.audiobooks.forEach((ab) => total += ab.tracks.length)
|
this.audioFiles.sort((a, b) => a.index - b.index)
|
||||||
return total
|
this.missingParts = []
|
||||||
|
this.setChapters()
|
||||||
|
this.checkUpdateMissingTracks()
|
||||||
}
|
}
|
||||||
getTotalDuration() {
|
|
||||||
var total = 0
|
checkUpdateMissingTracks() {
|
||||||
this.audiobooks.forEach((ab) => total += ab.duration)
|
var currMissingParts = (this.missingParts || []).join(',') || ''
|
||||||
return total
|
|
||||||
|
var current_index = 1
|
||||||
|
var missingParts = []
|
||||||
|
|
||||||
|
for (let i = 0; i < this.tracks.length; i++) {
|
||||||
|
var _track = this.tracks[i]
|
||||||
|
if (_track.index > current_index) {
|
||||||
|
var num_parts_missing = _track.index - current_index
|
||||||
|
for (let x = 0; x < num_parts_missing && x < 9999; x++) {
|
||||||
|
missingParts.push(current_index + x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current_index = _track.index + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
this.missingParts = missingParts
|
||||||
|
|
||||||
|
var newMissingParts = (this.missingParts || []).join(',') || ''
|
||||||
|
var wasUpdated = newMissingParts !== currMissingParts
|
||||||
|
if (wasUpdated && this.missingParts.length) {
|
||||||
|
Logger.info(`[Audiobook] "${this.name}" has ${missingParts.length} missing parts`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return wasUpdated
|
||||||
|
}
|
||||||
|
|
||||||
|
setChapters() {
|
||||||
|
// If 1 audio file without chapters, then no chapters will be set
|
||||||
|
var includedAudioFiles = this.audioFiles.filter(af => !af.exclude)
|
||||||
|
if (includedAudioFiles.length === 1) {
|
||||||
|
// 1 audio file with chapters
|
||||||
|
if (includedAudioFiles[0].chapters) {
|
||||||
|
this.chapters = includedAudioFiles[0].chapters.map(c => ({ ...c }))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.chapters = []
|
||||||
|
var currChapterId = 0
|
||||||
|
var currStartTime = 0
|
||||||
|
includedAudioFiles.forEach((file) => {
|
||||||
|
// If audio file has chapters use chapters
|
||||||
|
if (file.chapters && file.chapters.length) {
|
||||||
|
file.chapters.forEach((chapter) => {
|
||||||
|
var chapterDuration = chapter.end - chapter.start
|
||||||
|
if (chapterDuration > 0) {
|
||||||
|
var title = `Chapter ${currChapterId}`
|
||||||
|
if (chapter.title) {
|
||||||
|
title += ` (${chapter.title})`
|
||||||
|
}
|
||||||
|
this.chapters.push({
|
||||||
|
id: currChapterId++,
|
||||||
|
start: currStartTime,
|
||||||
|
end: currStartTime + chapterDuration,
|
||||||
|
title
|
||||||
|
})
|
||||||
|
currStartTime += chapterDuration
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (file.duration) {
|
||||||
|
// Otherwise just use track has chapter
|
||||||
|
this.chapters.push({
|
||||||
|
id: currChapterId++,
|
||||||
|
start: currStartTime,
|
||||||
|
end: currStartTime + file.duration,
|
||||||
|
title: file.metadata.filename ? Path.basename(file.metadata.filename, Path.extname(file.metadata.filename)) : `Chapter ${currChapterId}`
|
||||||
|
})
|
||||||
|
currStartTime += file.duration
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 = Book
|
module.exports = Book
|
@ -74,6 +74,14 @@ class Podcast {
|
|||||||
get hasIssues() {
|
get hasIssues() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
get duration() {
|
||||||
|
var total = 0
|
||||||
|
this.episodes.forEach((ep) => total += ep.duration)
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
get numTracks() {
|
||||||
|
return this.episodes.length
|
||||||
|
}
|
||||||
|
|
||||||
update(payload) {
|
update(payload) {
|
||||||
var json = this.toJSON()
|
var json = this.toJSON()
|
||||||
@ -110,14 +118,6 @@ 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(mediaMetadata) {
|
setData(mediaMetadata) {
|
||||||
this.metadata = new PodcastMetadata()
|
this.metadata = new PodcastMetadata()
|
||||||
if (mediaMetadata.metadata) {
|
if (mediaMetadata.metadata) {
|
||||||
@ -137,26 +137,19 @@ class Podcast {
|
|||||||
return payload || {}
|
return payload || {}
|
||||||
}
|
}
|
||||||
|
|
||||||
getLongestDuration() {
|
|
||||||
if (!this.episodes.length) return 0
|
|
||||||
var longest = 0
|
|
||||||
this.episodes.forEach((ab) => {
|
|
||||||
if (ab.duration > longest) longest = ab.duration
|
|
||||||
})
|
|
||||||
return longest
|
|
||||||
}
|
|
||||||
|
|
||||||
getTotalAudioTracks() {
|
|
||||||
return this.episodes.length
|
|
||||||
}
|
|
||||||
getTotalDuration() {
|
|
||||||
var total = 0
|
|
||||||
this.episodes.forEach((ep) => total += ep.duration)
|
|
||||||
return total
|
|
||||||
}
|
|
||||||
|
|
||||||
addPodcastEpisode(podcastEpisode) {
|
addPodcastEpisode(podcastEpisode) {
|
||||||
this.episodes.push(podcastEpisode)
|
this.episodes.push(podcastEpisode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only checks container format
|
||||||
|
checkCanDirectPlay(payload, epsiodeIndex = 0) {
|
||||||
|
var episode = this.episodes[epsiodeIndex]
|
||||||
|
return episode.checkCanDirectPlay(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
getDirectPlayTracklist(libraryItemId, episodeIndex = 0) {
|
||||||
|
var episode = this.episodes[episodeIndex]
|
||||||
|
return episode.getDirectPlayTracklist(libraryItemId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = Podcast
|
module.exports = Podcast
|
@ -77,7 +77,8 @@ class BookMetadata {
|
|||||||
explicit: this.explicit,
|
explicit: this.explicit,
|
||||||
authorName: this.authorName,
|
authorName: this.authorName,
|
||||||
authorNameLF: this.authorNameLF,
|
authorNameLF: this.authorNameLF,
|
||||||
narratorName: this.narratorName
|
narratorName: this.narratorName,
|
||||||
|
seriesName: this.seriesName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
const Logger = require('../../Logger')
|
const Logger = require('../../Logger')
|
||||||
|
|
||||||
class LibraryItemProgress {
|
class MediaProgress {
|
||||||
constructor(progress) {
|
constructor(progress) {
|
||||||
this.id = null // Same as library item id
|
this.id = null // Same as library item id
|
||||||
this.libraryItemId = null
|
this.libraryItemId = null
|
||||||
this.mediaEntityId = null
|
this.episodeId = null // For podcasts
|
||||||
|
|
||||||
this.duration = null
|
this.duration = null
|
||||||
this.progress = null // 0 to 1
|
this.progress = null // 0 to 1
|
||||||
@ -24,7 +24,7 @@ class LibraryItemProgress {
|
|||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
libraryItemId: this.libraryItemId,
|
libraryItemId: this.libraryItemId,
|
||||||
mediaEntityId: this.mediaEntityId,
|
episodeId: this.episodeId,
|
||||||
duration: this.duration,
|
duration: this.duration,
|
||||||
progress: this.progress,
|
progress: this.progress,
|
||||||
currentTime: this.currentTime,
|
currentTime: this.currentTime,
|
||||||
@ -38,7 +38,7 @@ class LibraryItemProgress {
|
|||||||
construct(progress) {
|
construct(progress) {
|
||||||
this.id = progress.id
|
this.id = progress.id
|
||||||
this.libraryItemId = progress.libraryItemId
|
this.libraryItemId = progress.libraryItemId
|
||||||
this.mediaEntityId = progress.mediaEntityId || null
|
this.episodeId = progress.episodeId
|
||||||
this.duration = progress.duration || 0
|
this.duration = progress.duration || 0
|
||||||
this.progress = progress.progress
|
this.progress = progress.progress
|
||||||
this.currentTime = progress.currentTime
|
this.currentTime = progress.currentTime
|
||||||
@ -52,10 +52,10 @@ class LibraryItemProgress {
|
|||||||
return !this.isFinished && this.progress > 0
|
return !this.isFinished && this.progress > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
setData(libraryItemId, mediaEntityId, progress) {
|
setData(libraryItemId, progress) {
|
||||||
this.id = libraryItemId
|
this.id = libraryItemId
|
||||||
this.libraryItemId = libraryItemId
|
this.libraryItemId = libraryItemId
|
||||||
this.mediaEntityId = mediaEntityId
|
this.episodeId = progress.episodeId || null
|
||||||
this.duration = progress.duration || 0
|
this.duration = progress.duration || 0
|
||||||
this.progress = Math.min(1, (progress.progress || 0))
|
this.progress = Math.min(1, (progress.progress || 0))
|
||||||
this.currentTime = progress.currentTime || 0
|
this.currentTime = progress.currentTime || 0
|
||||||
@ -97,4 +97,4 @@ class LibraryItemProgress {
|
|||||||
return hasUpdates
|
return hasUpdates
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = LibraryItemProgress
|
module.exports = MediaProgress
|
@ -1,6 +1,6 @@
|
|||||||
const Logger = require('../../Logger')
|
const Logger = require('../../Logger')
|
||||||
const AudioBookmark = require('./AudioBookmark')
|
const AudioBookmark = require('./AudioBookmark')
|
||||||
const LibraryItemProgress = require('./LibraryItemProgress')
|
const MediaProgress = require('./MediaProgress')
|
||||||
|
|
||||||
class User {
|
class User {
|
||||||
constructor(user) {
|
constructor(user) {
|
||||||
@ -14,7 +14,7 @@ class User {
|
|||||||
this.lastSeen = null
|
this.lastSeen = null
|
||||||
this.createdAt = null
|
this.createdAt = null
|
||||||
|
|
||||||
this.libraryItemProgress = []
|
this.mediaProgress = []
|
||||||
this.bookmarks = []
|
this.bookmarks = []
|
||||||
|
|
||||||
this.settings = {}
|
this.settings = {}
|
||||||
@ -84,7 +84,7 @@ class User {
|
|||||||
pash: this.pash,
|
pash: this.pash,
|
||||||
type: this.type,
|
type: this.type,
|
||||||
token: this.token,
|
token: this.token,
|
||||||
libraryItemProgress: this.libraryItemProgress ? this.libraryItemProgress.map(li => li.toJSON()) : [],
|
mediaProgress: this.mediaProgress ? this.mediaProgress.map(li => li.toJSON()) : [],
|
||||||
bookmarks: this.bookmarks ? this.bookmarks.map(b => b.toJSON()) : [],
|
bookmarks: this.bookmarks ? this.bookmarks.map(b => b.toJSON()) : [],
|
||||||
isActive: this.isActive,
|
isActive: this.isActive,
|
||||||
isLocked: this.isLocked,
|
isLocked: this.isLocked,
|
||||||
@ -103,7 +103,7 @@ class User {
|
|||||||
username: this.username,
|
username: this.username,
|
||||||
type: this.type,
|
type: this.type,
|
||||||
token: this.token,
|
token: this.token,
|
||||||
libraryItemProgress: this.libraryItemProgress ? this.libraryItemProgress.map(li => li.toJSON()) : [],
|
mediaProgress: this.mediaProgress ? this.mediaProgress.map(li => li.toJSON()) : [],
|
||||||
bookmarks: this.bookmarks ? this.bookmarks.map(b => b.toJSON()) : [],
|
bookmarks: this.bookmarks ? this.bookmarks.map(b => b.toJSON()) : [],
|
||||||
isActive: this.isActive,
|
isActive: this.isActive,
|
||||||
isLocked: this.isLocked,
|
isLocked: this.isLocked,
|
||||||
@ -118,12 +118,19 @@ class User {
|
|||||||
|
|
||||||
// Data broadcasted
|
// Data broadcasted
|
||||||
toJSONForPublic(sessions, libraryItems) {
|
toJSONForPublic(sessions, libraryItems) {
|
||||||
var session = sessions ? sessions.find(s => s.userId === this.id) : null
|
var userSession = sessions ? sessions.find(s => s.userId === this.id) : null
|
||||||
|
var session = null
|
||||||
|
if (session) {
|
||||||
|
var libraryItem = libraryItems.find(li => li.id === session.libraryItemId)
|
||||||
|
if (libraryItem) {
|
||||||
|
session = userSession.toJSONForClient(libraryItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
username: this.username,
|
username: this.username,
|
||||||
type: this.type,
|
type: this.type,
|
||||||
session: session ? session.toJSONForClient() : null,
|
session,
|
||||||
mostRecent: this.getMostRecentItemProgress(libraryItems),
|
mostRecent: this.getMostRecentItemProgress(libraryItems),
|
||||||
lastSeen: this.lastSeen,
|
lastSeen: this.lastSeen,
|
||||||
createdAt: this.createdAt
|
createdAt: this.createdAt
|
||||||
@ -137,9 +144,9 @@ class User {
|
|||||||
this.type = user.type
|
this.type = user.type
|
||||||
this.token = user.token
|
this.token = user.token
|
||||||
|
|
||||||
this.libraryItemProgress = []
|
this.mediaProgress = []
|
||||||
if (user.libraryItemProgress) {
|
if (user.mediaProgress) {
|
||||||
this.libraryItemProgress = user.libraryItemProgress.map(li => new LibraryItemProgress(li)).filter(lip => lip.id)
|
this.mediaProgress = user.mediaProgress.map(li => new MediaProgress(li)).filter(lip => lip.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.bookmarks = []
|
this.bookmarks = []
|
||||||
@ -217,8 +224,8 @@ class User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getMostRecentItemProgress(libraryItems) {
|
getMostRecentItemProgress(libraryItems) {
|
||||||
if (!this.libraryItemProgress.length) return null
|
if (!this.mediaProgress.length) return null
|
||||||
var lip = this.libraryItemProgress.map(lip => lip.toJSON())
|
var lip = this.mediaProgress.map(lip => lip.toJSON())
|
||||||
lip.sort((a, b) => b.lastUpdate - a.lastUpdate)
|
lip.sort((a, b) => b.lastUpdate - a.lastUpdate)
|
||||||
var mostRecentWithLip = lip.find(li => libraryItems.find(_li => _li.id === li.id))
|
var mostRecentWithLip = lip.find(li => libraryItems.find(_li => _li.id === li.id))
|
||||||
if (!mostRecentWithLip) return null
|
if (!mostRecentWithLip) return null
|
||||||
@ -229,35 +236,27 @@ class User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getLibraryItemProgress(libraryItemId) {
|
getMediaProgress(libraryItemId) {
|
||||||
if (!this.libraryItemProgress) return null
|
if (!this.mediaProgress) return null
|
||||||
return this.libraryItemProgress.find(lip => lip.id === libraryItemId)
|
return this.mediaProgress.find(lip => lip.id === libraryItemId)
|
||||||
}
|
}
|
||||||
|
|
||||||
createUpdateLibraryItemProgress(libraryItem, updatePayload) {
|
createUpdateMediaProgress(libraryItem, updatePayload) {
|
||||||
var itemProgress = this.libraryItemProgress.find(li => li.id === libraryItem.id)
|
var itemProgress = this.mediaProgress.find(li => li.id === libraryItem.id)
|
||||||
if (!itemProgress) {
|
if (!itemProgress) {
|
||||||
var newItemProgress = new LibraryItemProgress()
|
var newItemProgress = new MediaProgress()
|
||||||
|
|
||||||
var mediaEntity = null
|
newItemProgress.setData(libraryItem.id, updatePayload)
|
||||||
if (updatePayload.mediaEntityId) mediaEntity = libraryItem.media.getMediaEntityById(updatePayload.mediaEntityId)
|
this.mediaProgress.push(newItemProgress)
|
||||||
if (!mediaEntity) mediaEntity = libraryItem.media.getPlaybackMediaEntity()
|
|
||||||
if (!mediaEntity) {
|
|
||||||
Logger.error(`[User] createUpdateLibraryItemProgress invalid library item has no playback media entity "${libraryItem.id}"`)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
newItemProgress.setData(libraryItem.id, mediaEntity.id, updatePayload)
|
|
||||||
this.libraryItemProgress.push(newItemProgress)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
var wasUpdated = itemProgress.update(updatePayload)
|
var wasUpdated = itemProgress.update(updatePayload)
|
||||||
return wasUpdated
|
return wasUpdated
|
||||||
}
|
}
|
||||||
|
|
||||||
removeLibraryItemProgress(libraryItemId) {
|
removeMediaProgress(libraryItemId) {
|
||||||
if (!this.libraryItemProgress.some(lip => lip.id == libraryItemId)) return false
|
if (!this.mediaProgress.some(lip => lip.id == libraryItemId)) return false
|
||||||
this.libraryItemProgress = this.libraryItemProgress.filter(lip => lip.id != libraryItemId)
|
this.mediaProgress = this.mediaProgress.filter(lip => lip.id != libraryItemId)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -329,30 +328,31 @@ class User {
|
|||||||
this.bookmarks = this.bookmarks.filter(bm => (bm.libraryItemId !== libraryItemId || bm.time !== time))
|
this.bookmarks = this.bookmarks.filter(bm => (bm.libraryItemId !== libraryItemId || bm.time !== time))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: re-do mobile sync
|
||||||
syncLocalUserAudiobookData(localUserAudiobookData, audiobook) {
|
syncLocalUserAudiobookData(localUserAudiobookData, audiobook) {
|
||||||
if (!localUserAudiobookData || !localUserAudiobookData.audiobookId) {
|
// if (!localUserAudiobookData || !localUserAudiobookData.audiobookId) {
|
||||||
Logger.error(`[User] Invalid local user audiobook data`, localUserAudiobookData)
|
// Logger.error(`[User] Invalid local user audiobook data`, localUserAudiobookData)
|
||||||
return false
|
// return false
|
||||||
}
|
// }
|
||||||
if (!this.audiobooks) this.audiobooks = {}
|
// if (!this.audiobooks) this.audiobooks = {}
|
||||||
|
|
||||||
if (!this.audiobooks[localUserAudiobookData.audiobookId]) {
|
// if (!this.audiobooks[localUserAudiobookData.audiobookId]) {
|
||||||
this.audiobooks[localUserAudiobookData.audiobookId] = new UserAudiobookData(localUserAudiobookData)
|
// this.audiobooks[localUserAudiobookData.audiobookId] = new UserAudiobookData(localUserAudiobookData)
|
||||||
return true
|
// return true
|
||||||
}
|
// }
|
||||||
|
|
||||||
var userAbD = this.audiobooks[localUserAudiobookData.audiobookId]
|
// var userAbD = this.audiobooks[localUserAudiobookData.audiobookId]
|
||||||
if (userAbD.lastUpdate >= localUserAudiobookData.lastUpdate) {
|
// if (userAbD.lastUpdate >= localUserAudiobookData.lastUpdate) {
|
||||||
// Server audiobook data is more recent
|
// // Server audiobook data is more recent
|
||||||
return false
|
// return false
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Local Data More recent
|
// // Local Data More recent
|
||||||
var wasUpdated = this.audiobooks[localUserAudiobookData.audiobookId].update(localUserAudiobookData)
|
// var wasUpdated = this.audiobooks[localUserAudiobookData.audiobookId].update(localUserAudiobookData)
|
||||||
if (wasUpdated) {
|
// if (wasUpdated) {
|
||||||
Logger.debug(`[User] syncLocalUserAudiobookData local data was more recent for "${audiobook.title}"`)
|
// Logger.debug(`[User] syncLocalUserAudiobookData local data was more recent for "${audiobook.title}"`)
|
||||||
}
|
// }
|
||||||
return wasUpdated
|
// return wasUpdated
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = User
|
module.exports = User
|
@ -12,7 +12,6 @@ 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 MediaEntityController = require('../controllers/MediaEntityController')
|
|
||||||
const SessionController = require('../controllers/SessionController')
|
const SessionController = require('../controllers/SessionController')
|
||||||
const PodcastController = require('../controllers/PodcastController')
|
const PodcastController = require('../controllers/PodcastController')
|
||||||
const MiscController = require('../controllers/MiscController')
|
const MiscController = require('../controllers/MiscController')
|
||||||
@ -72,14 +71,6 @@ class ApiRouter {
|
|||||||
|
|
||||||
this.router.post('/libraries/order', LibraryController.reorder.bind(this))
|
this.router.post('/libraries/order', LibraryController.reorder.bind(this))
|
||||||
|
|
||||||
//
|
|
||||||
// Media Entity Routes
|
|
||||||
//
|
|
||||||
this.router.get('/entities/:id', MediaEntityController.middleware.bind(this), MediaEntityController.findOne.bind(this))
|
|
||||||
this.router.get('/entities/:id/item', MediaEntityController.middleware.bind(this), MediaEntityController.findWithItem.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
|
||||||
//
|
//
|
||||||
@ -95,6 +86,7 @@ class ApiRouter {
|
|||||||
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.post('/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.patch('/items/:id/tracks', LibraryItemController.middleware.bind(this), LibraryItemController.updateTracks.bind(this))
|
||||||
this.router.get('/items/:id/scan', LibraryItemController.middleware.bind(this), LibraryItemController.scan.bind(this)) // Root only
|
this.router.get('/items/:id/scan', LibraryItemController.middleware.bind(this), LibraryItemController.scan.bind(this)) // Root only
|
||||||
|
|
||||||
this.router.post('/items/batch/delete', LibraryItemController.batchDelete.bind(this))
|
this.router.post('/items/batch/delete', LibraryItemController.batchDelete.bind(this))
|
||||||
@ -132,9 +124,9 @@ class ApiRouter {
|
|||||||
//
|
//
|
||||||
this.router.get('/me/listening-sessions', MeController.getListeningSessions.bind(this))
|
this.router.get('/me/listening-sessions', MeController.getListeningSessions.bind(this))
|
||||||
this.router.get('/me/listening-stats', MeController.getListeningStats.bind(this))
|
this.router.get('/me/listening-stats', MeController.getListeningStats.bind(this))
|
||||||
this.router.patch('/me/progress/:id', MeController.createUpdateLibraryItemProgress.bind(this))
|
this.router.patch('/me/progress/:id', MeController.createUpdateMediaProgress.bind(this))
|
||||||
this.router.delete('/me/progress/:id', MeController.removeLibraryItemProgress.bind(this))
|
this.router.delete('/me/progress/:id', MeController.removeMediaProgress.bind(this))
|
||||||
this.router.patch('/me/progress/batch/update', MeController.batchUpdateLibraryItemProgress.bind(this))
|
this.router.patch('/me/progress/batch/update', MeController.batchUpdateMediaProgress.bind(this))
|
||||||
this.router.post('/me/item/:id/bookmark', MeController.createBookmark.bind(this))
|
this.router.post('/me/item/:id/bookmark', MeController.createBookmark.bind(this))
|
||||||
this.router.patch('/me/item/:id/bookmark', MeController.updateBookmark.bind(this))
|
this.router.patch('/me/item/:id/bookmark', MeController.updateBookmark.bind(this))
|
||||||
this.router.delete('/me/item/:id/bookmark/:time', MeController.removeBookmark.bind(this))
|
this.router.delete('/me/item/:id/bookmark/:time', MeController.removeBookmark.bind(this))
|
||||||
@ -266,7 +258,7 @@ class ApiRouter {
|
|||||||
userJsonWithItemProgressDetails(user) {
|
userJsonWithItemProgressDetails(user) {
|
||||||
var json = user.toJSONForBrowser()
|
var json = user.toJSONForBrowser()
|
||||||
|
|
||||||
json.libraryItemProgress = json.libraryItemProgress.map(lip => {
|
json.mediaProgress = json.mediaProgress.map(lip => {
|
||||||
var libraryItem = this.db.libraryItems.find(li => li.id === lip.id)
|
var libraryItem = this.db.libraryItems.find(li => li.id === lip.id)
|
||||||
if (!libraryItem) {
|
if (!libraryItem) {
|
||||||
Logger.warn('[ApiRouter] Library item not found for users progress ' + lip.id)
|
Logger.warn('[ApiRouter] Library item not found for users progress ' + lip.id)
|
||||||
@ -283,7 +275,7 @@ class ApiRouter {
|
|||||||
// Remove libraryItem from users
|
// Remove libraryItem from users
|
||||||
for (let i = 0; i < this.db.users.length; i++) {
|
for (let i = 0; i < this.db.users.length; i++) {
|
||||||
var user = this.db.users[i]
|
var user = this.db.users[i]
|
||||||
var madeUpdates = user.removeLibraryItemProgress(libraryItem.id)
|
var madeUpdates = user.removeMediaProgress(libraryItem.id)
|
||||||
if (madeUpdates) {
|
if (madeUpdates) {
|
||||||
await this.db.updateEntity('user', user)
|
await this.db.updateEntity('user', user)
|
||||||
}
|
}
|
||||||
|
@ -169,12 +169,11 @@ class AudioFileScanner {
|
|||||||
if (existingAF) {
|
if (existingAF) {
|
||||||
if (existingAF.updateFromScan) existingAF.updateFromScan(audioFiles[i])
|
if (existingAF.updateFromScan) existingAF.updateFromScan(audioFiles[i])
|
||||||
} else {
|
} else {
|
||||||
libraryItem.media.addAudioFileToAudiobook(audioFiles[i])
|
libraryItem.media.addAudioFile(audioFiles[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: support for multiple audiobooks in a book item will need to pass an audiobook variant name here
|
|
||||||
async scanAudioFiles(audioLibraryFiles, scanData, libraryItem, preferAudioMetadata, libraryScan = null) {
|
async scanAudioFiles(audioLibraryFiles, scanData, libraryItem, preferAudioMetadata, libraryScan = null) {
|
||||||
var hasUpdated = false
|
var hasUpdated = false
|
||||||
|
|
||||||
@ -189,14 +188,14 @@ class AudioFileScanner {
|
|||||||
return !libraryItem.media.findFileWithInode(af.ino)
|
return !libraryItem.media.findFileWithInode(af.ino)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Adding audio files to book media
|
// Book: Adding audio files to book media
|
||||||
if (libraryItem.mediaType === 'book') {
|
if (libraryItem.mediaType === 'book') {
|
||||||
if (newAudioFiles.length) {
|
if (newAudioFiles.length) {
|
||||||
// Single Track Audiobooks
|
// Single Track Audiobooks
|
||||||
if (totalAudioFilesToInclude === 1) {
|
if (totalAudioFilesToInclude === 1) {
|
||||||
var af = audioScanResult.audioFiles[0]
|
var af = audioScanResult.audioFiles[0]
|
||||||
af.index = 1
|
af.index = 1
|
||||||
libraryItem.media.addAudioFileToAudiobook(af)
|
libraryItem.media.addAudioFile(af)
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
} else {
|
} else {
|
||||||
this.runSmartTrackOrder(libraryItem, audioScanResult.audioFiles)
|
this.runSmartTrackOrder(libraryItem, audioScanResult.audioFiles)
|
||||||
@ -221,12 +220,7 @@ class AudioFileScanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasUpdated) {
|
if (hasUpdated) {
|
||||||
if (!libraryItem.media.audiobooks.length) {
|
libraryItem.media.rebuildTracks()
|
||||||
Logger.error(`[AudioFileScanner] Updates were made but library item has no audiobooks`, libraryItem)
|
|
||||||
} else {
|
|
||||||
var audiobook = libraryItem.media.audiobooks[0]
|
|
||||||
audiobook.rebuildTracks()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} // End Book media type
|
} // End Book media type
|
||||||
}
|
}
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
const AuthorFinder = require('../finders/AuthorFinder')
|
|
||||||
|
|
||||||
class AuthorScanner {
|
|
||||||
constructor(db) {
|
|
||||||
this.db = db
|
|
||||||
this.authorFinder = new AuthorFinder()
|
|
||||||
}
|
|
||||||
|
|
||||||
getUniqueAuthors() {
|
|
||||||
var authorFls = this.db.audiobooks.map(b => b.book.authorFL)
|
|
||||||
var authors = []
|
|
||||||
authorFls.forEach((auth) => {
|
|
||||||
authors = authors.concat(auth.split(', ').map(a => a.trim()))
|
|
||||||
})
|
|
||||||
return [...new Set(authors)]
|
|
||||||
}
|
|
||||||
|
|
||||||
async scanAuthors() {
|
|
||||||
var authors = this.getUniqueAuthors()
|
|
||||||
for (let i = 0; i < authors.length; i++) {
|
|
||||||
var authorName = authors[i]
|
|
||||||
var author = await this.authorFinder.getAuthorByName(authorName)
|
|
||||||
if (!author) {
|
|
||||||
return res.status(500).send('Failed to create author')
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.db.insertEntity('author', author)
|
|
||||||
this.emitter('author_added', author.toJSON())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
module.exports = AuthorScanner
|
|
@ -613,37 +613,38 @@ class Scanner {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Redo metadata
|
||||||
async saveMetadata(audiobookId) {
|
async saveMetadata(audiobookId) {
|
||||||
if (audiobookId) {
|
// if (audiobookId) {
|
||||||
var audiobook = this.db.audiobooks.find(ab => ab.id === audiobookId)
|
// var audiobook = this.db.audiobooks.find(ab => ab.id === audiobookId)
|
||||||
if (!audiobook) {
|
// if (!audiobook) {
|
||||||
return {
|
// return {
|
||||||
error: 'Audiobook not found'
|
// error: 'Audiobook not found'
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
var savedPath = await audiobook.writeNfoFile()
|
// var savedPath = await audiobook.writeNfoFile()
|
||||||
return {
|
// return {
|
||||||
audiobookId,
|
// audiobookId,
|
||||||
audiobookTitle: audiobook.title,
|
// audiobookTitle: audiobook.title,
|
||||||
savedPath
|
// savedPath
|
||||||
}
|
// }
|
||||||
} else {
|
// } else {
|
||||||
var response = {
|
// var response = {
|
||||||
success: 0,
|
// success: 0,
|
||||||
failed: 0
|
// failed: 0
|
||||||
}
|
// }
|
||||||
for (let i = 0; i < this.db.audiobooks.length; i++) {
|
// for (let i = 0; i < this.db.audiobooks.length; i++) {
|
||||||
var audiobook = this.db.audiobooks[i]
|
// var audiobook = this.db.audiobooks[i]
|
||||||
var savedPath = await audiobook.writeNfoFile()
|
// var savedPath = await audiobook.writeNfoFile()
|
||||||
if (savedPath) {
|
// if (savedPath) {
|
||||||
Logger.info(`[Scanner] Saved metadata nfo ${savedPath}`)
|
// Logger.info(`[Scanner] Saved metadata nfo ${savedPath}`)
|
||||||
response.success++
|
// response.success++
|
||||||
} else {
|
// } else {
|
||||||
response.failed++
|
// response.failed++
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
return response
|
// return response
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
async quickMatchBook(libraryItem, options = {}) {
|
async quickMatchBook(libraryItem, options = {}) {
|
||||||
@ -728,48 +729,49 @@ class Scanner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Redo quick match full library
|
||||||
async matchLibraryBooks(library) {
|
async matchLibraryBooks(library) {
|
||||||
if (this.isLibraryScanning(library.id)) {
|
// if (this.isLibraryScanning(library.id)) {
|
||||||
Logger.error(`[Scanner] Already scanning ${library.id}`)
|
// Logger.error(`[Scanner] Already scanning ${library.id}`)
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
|
|
||||||
const provider = library.provider || 'google'
|
// const provider = library.provider || 'google'
|
||||||
var audiobooksInLibrary = this.db.audiobooks.filter(ab => ab.libraryId === library.id)
|
// var audiobooksInLibrary = this.db.audiobooks.filter(ab => ab.libraryId === library.id)
|
||||||
if (!audiobooksInLibrary.length) {
|
// if (!audiobooksInLibrary.length) {
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
|
|
||||||
var libraryScan = new LibraryScan()
|
// var libraryScan = new LibraryScan()
|
||||||
libraryScan.setData(library, null, 'match')
|
// libraryScan.setData(library, null, 'match')
|
||||||
this.librariesScanning.push(libraryScan.getScanEmitData)
|
// this.librariesScanning.push(libraryScan.getScanEmitData)
|
||||||
this.emitter('scan_start', libraryScan.getScanEmitData)
|
// this.emitter('scan_start', libraryScan.getScanEmitData)
|
||||||
|
|
||||||
Logger.info(`[Scanner] Starting library match books scan ${libraryScan.id} for ${libraryScan.libraryName}`)
|
// Logger.info(`[Scanner] Starting library match books scan ${libraryScan.id} for ${libraryScan.libraryName}`)
|
||||||
|
|
||||||
for (let i = 0; i < audiobooksInLibrary.length; i++) {
|
// for (let i = 0; i < audiobooksInLibrary.length; i++) {
|
||||||
var audiobook = audiobooksInLibrary[i]
|
// var audiobook = audiobooksInLibrary[i]
|
||||||
Logger.debug(`[Scanner] Quick matching "${audiobook.title}" (${i + 1} of ${audiobooksInLibrary.length})`)
|
// Logger.debug(`[Scanner] Quick matching "${audiobook.title}" (${i + 1} of ${audiobooksInLibrary.length})`)
|
||||||
var result = await this.quickMatchBook(audiobook, { provider })
|
// var result = await this.quickMatchBook(audiobook, { provider })
|
||||||
if (result.warning) {
|
// if (result.warning) {
|
||||||
Logger.warn(`[Scanner] Match warning ${result.warning} for audiobook "${audiobook.title}"`)
|
// Logger.warn(`[Scanner] Match warning ${result.warning} for audiobook "${audiobook.title}"`)
|
||||||
} else if (result.updated) {
|
// } else if (result.updated) {
|
||||||
libraryScan.resultsUpdated++
|
// libraryScan.resultsUpdated++
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (this.cancelLibraryScan[libraryScan.libraryId]) {
|
// if (this.cancelLibraryScan[libraryScan.libraryId]) {
|
||||||
Logger.info(`[Scanner] Library match scan canceled for "${libraryScan.libraryName}"`)
|
// Logger.info(`[Scanner] Library match scan canceled for "${libraryScan.libraryName}"`)
|
||||||
delete this.cancelLibraryScan[libraryScan.libraryId]
|
// delete this.cancelLibraryScan[libraryScan.libraryId]
|
||||||
var scanData = libraryScan.getScanEmitData
|
// var scanData = libraryScan.getScanEmitData
|
||||||
scanData.results = false
|
// scanData.results = false
|
||||||
this.emitter('scan_complete', scanData)
|
// this.emitter('scan_complete', scanData)
|
||||||
this.librariesScanning = this.librariesScanning.filter(ls => ls.id !== library.id)
|
// this.librariesScanning = this.librariesScanning.filter(ls => ls.id !== library.id)
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
this.librariesScanning = this.librariesScanning.filter(ls => ls.id !== library.id)
|
// this.librariesScanning = this.librariesScanning.filter(ls => ls.id !== library.id)
|
||||||
this.emitter('scan_complete', libraryScan.getScanEmitData)
|
// this.emitter('scan_complete', libraryScan.getScanEmitData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = Scanner
|
module.exports = Scanner
|
@ -27,7 +27,7 @@ const Series = require('../objects/entities/Series')
|
|||||||
const Audiobook = require('../objects/entities/Audiobook')
|
const Audiobook = require('../objects/entities/Audiobook')
|
||||||
const EBook = require('../objects/entities/EBook')
|
const EBook = require('../objects/entities/EBook')
|
||||||
|
|
||||||
const LibraryItemProgress = require('../objects/user/LibraryItemProgress')
|
const MediaProgress = require('../objects/user/MediaProgress')
|
||||||
const PlaybackSession = require('../objects/PlaybackSession')
|
const PlaybackSession = require('../objects/PlaybackSession')
|
||||||
|
|
||||||
const { isObject } = require('.')
|
const { isObject } = require('.')
|
||||||
@ -164,7 +164,7 @@ function cleanOldCoverPath(coverPath) {
|
|||||||
|
|
||||||
function makeLibraryItemFromOldAb(audiobook) {
|
function makeLibraryItemFromOldAb(audiobook) {
|
||||||
var libraryItem = new LibraryItem()
|
var libraryItem = new LibraryItem()
|
||||||
libraryItem.id = getId('li')
|
libraryItem.id = audiobook.id
|
||||||
libraryItem.ino = audiobook.ino
|
libraryItem.ino = audiobook.ino
|
||||||
libraryItem.libraryId = audiobook.libraryId
|
libraryItem.libraryId = audiobook.libraryId
|
||||||
libraryItem.folderId = audiobook.folderId
|
libraryItem.folderId = audiobook.folderId
|
||||||
@ -199,35 +199,17 @@ function makeLibraryItemFromOldAb(audiobook) {
|
|||||||
bookEntity.tags = [...audiobook.tags]
|
bookEntity.tags = [...audiobook.tags]
|
||||||
|
|
||||||
var payload = makeFilesFromOldAb(audiobook)
|
var payload = makeFilesFromOldAb(audiobook)
|
||||||
if (payload.audioFiles.length) {
|
bookEntity.audioFiles = payload.audioFiles
|
||||||
var newAudiobook = new Audiobook()
|
bookEntity.chapters = []
|
||||||
newAudiobook.id = audiobook.id
|
|
||||||
newAudiobook.index = 1
|
|
||||||
newAudiobook.name = 'default'
|
|
||||||
newAudiobook.audioFiles = payload.audioFiles
|
|
||||||
if (audiobook.chapters && audiobook.chapters.length) {
|
if (audiobook.chapters && audiobook.chapters.length) {
|
||||||
newAudiobook.chapters = audiobook.chapters.map(c => ({ ...c }))
|
bookEntity.chapters = audiobook.chapters.map(c => ({ ...c }))
|
||||||
}
|
}
|
||||||
newAudiobook.missingParts = audiobook.missingParts || []
|
bookEntity.missingParts = audiobook.missingParts || []
|
||||||
newAudiobook.addedAt = audiobook.addedAt
|
|
||||||
newAudiobook.updatedAt = audiobook.lastUpdate
|
|
||||||
|
|
||||||
bookEntity.audiobooks.push(newAudiobook)
|
if (payload.ebookFiles.length) {
|
||||||
|
bookEntity.ebookFile = payload.ebookFiles[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
var ebookIndex = 1
|
|
||||||
payload.ebookFiles.forEach(ebookFile => {
|
|
||||||
var newEBook = new EBook()
|
|
||||||
newEBook.id = getId('eb')
|
|
||||||
newEBook.index = ebookIndex++
|
|
||||||
newEBook.name = ebookFile.metadata.filenameNoExt
|
|
||||||
newEBook.ebookFile = ebookFile
|
|
||||||
newEBook.addedAt = audiobook.addedAt
|
|
||||||
newEBook.updatedAt = audiobook.lastUpdate
|
|
||||||
|
|
||||||
bookEntity.ebooks.push(newEBook)
|
|
||||||
})
|
|
||||||
|
|
||||||
libraryItem.media = bookEntity
|
libraryItem.media = bookEntity
|
||||||
libraryItem.libraryFiles = payload.libraryFiles
|
libraryItem.libraryFiles = payload.libraryFiles
|
||||||
return libraryItem
|
return libraryItem
|
||||||
@ -258,55 +240,6 @@ async function migrateLibraryItems(db) {
|
|||||||
|
|
||||||
var libraryItems = audiobooks.map((ab) => makeLibraryItemFromOldAb(ab))
|
var libraryItems = audiobooks.map((ab) => makeLibraryItemFromOldAb(ab))
|
||||||
|
|
||||||
// User library item progress was using the auidobook ID when migrated
|
|
||||||
// now that library items are created the LibraryItemProgress objects
|
|
||||||
// need the library item id to be set
|
|
||||||
for (const user of db.users) {
|
|
||||||
if (user.libraryItemProgress.length) {
|
|
||||||
user.libraryItemProgress = user.libraryItemProgress.map(lip => {
|
|
||||||
var audiobookId = lip.id
|
|
||||||
var libraryItemWithAudiobook = libraryItems.find(li => li.media.getAudiobookById && !!li.media.getAudiobookById(audiobookId))
|
|
||||||
if (!libraryItemWithAudiobook) {
|
|
||||||
Logger.error('[dbMigration] Failed to find library item with audiobook id', audiobookId)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
lip.id = libraryItemWithAudiobook.id
|
|
||||||
lip.libraryItemId = libraryItemWithAudiobook.id
|
|
||||||
return lip
|
|
||||||
}).filter(lip => !!lip)
|
|
||||||
}
|
|
||||||
if (user.bookmarks.length) {
|
|
||||||
user.bookmarks = user.bookmarks.map((bookmark) => {
|
|
||||||
var audiobookId = bookmark.libraryItemId
|
|
||||||
var libraryItemWithAudiobook = libraryItems.find(li => li.media.getAudiobookById && !!li.media.getAudiobookById(audiobookId))
|
|
||||||
if (!libraryItemWithAudiobook) {
|
|
||||||
Logger.error('[dbMigration] Failed to find library item with audiobook id', audiobookId)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
bookmark.libraryItemId = libraryItemWithAudiobook.id
|
|
||||||
return bookmark
|
|
||||||
}).filter(bm => !!bm)
|
|
||||||
}
|
|
||||||
if (user.libraryItemProgress.length || user.bookmarks.length) {
|
|
||||||
await db.updateEntity('user', user)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update session LibraryItemId's
|
|
||||||
var sessions = await db.sessionsDb.select(() => true).then((results) => results.data)
|
|
||||||
if (sessions.length) {
|
|
||||||
sessions = sessions.map(se => {
|
|
||||||
var libraryItemWithAudiobook = libraryItems.find(li => li.media.getAudiobookById && !!li.media.getAudiobookById(se.mediaEntityId))
|
|
||||||
if (!libraryItemWithAudiobook) {
|
|
||||||
Logger.error('[dbMigration] Failed to find library item with audiobook id', se.mediaEntityId)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
se.libraryItemId = libraryItemWithAudiobook.id
|
|
||||||
return se
|
|
||||||
}).filter(se => !!se)
|
|
||||||
await db.updateEntities('session', sessions)
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.info(`>>> ${libraryItems.length} Library Items made`)
|
Logger.info(`>>> ${libraryItems.length} Library Items made`)
|
||||||
await db.insertEntities('libraryItem', libraryItems)
|
await db.insertEntities('libraryItem', libraryItems)
|
||||||
if (authorsToAdd.length) {
|
if (authorsToAdd.length) {
|
||||||
@ -327,28 +260,27 @@ async function migrateLibraryItems(db) {
|
|||||||
function cleanUserObject(db, userObj) {
|
function cleanUserObject(db, userObj) {
|
||||||
var cleanedUserPayload = {
|
var cleanedUserPayload = {
|
||||||
...userObj,
|
...userObj,
|
||||||
libraryItemProgress: [],
|
mediaProgress: [],
|
||||||
bookmarks: []
|
bookmarks: []
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserAudiobookData is now LibraryItemProgress and AudioBookmarks separated
|
// UserAudiobookData is now MediaProgress and AudioBookmarks separated
|
||||||
if (userObj.audiobooks) {
|
if (userObj.audiobooks) {
|
||||||
for (const audiobookId in userObj.audiobooks) {
|
for (const audiobookId in userObj.audiobooks) {
|
||||||
if (isObject(userObj.audiobooks[audiobookId])) {
|
if (isObject(userObj.audiobooks[audiobookId])) {
|
||||||
// Bookmarks now live on User.js object instead of inside UserAudiobookData
|
// Bookmarks now live on User.js object instead of inside UserAudiobookData
|
||||||
if (userObj.audiobooks[audiobookId].bookmarks) {
|
if (userObj.audiobooks[audiobookId].bookmarks) {
|
||||||
const cleanedBookmarks = userObj.audiobooks[audiobookId].bookmarks.map((bm) => {
|
const cleanedBookmarks = userObj.audiobooks[audiobookId].bookmarks.map((bm) => {
|
||||||
bm.libraryItemId = audiobookId // Temp placeholder replace with libraryItemId when created
|
bm.libraryItemId = audiobookId
|
||||||
return bm
|
return bm
|
||||||
})
|
})
|
||||||
cleanedUserPayload.bookmarks = cleanedUserPayload.bookmarks.concat(cleanedBookmarks)
|
cleanedUserPayload.bookmarks = cleanedUserPayload.bookmarks.concat(cleanedBookmarks)
|
||||||
}
|
}
|
||||||
|
|
||||||
var userAudiobookData = new UserAudiobookData(userObj.audiobooks[audiobookId]) // Legacy object
|
var userAudiobookData = new UserAudiobookData(userObj.audiobooks[audiobookId]) // Legacy object
|
||||||
var liProgress = new LibraryItemProgress() // New Progress Object
|
var liProgress = new MediaProgress() // New Progress Object
|
||||||
liProgress.id = userAudiobookData.audiobookId // This ID will be updated when library item is created
|
liProgress.id = userAudiobookData.audiobookId
|
||||||
liProgress.libraryItemId = userAudiobookData.audiobookId
|
liProgress.libraryItemId = userAudiobookData.audiobookId
|
||||||
liProgress.mediaEntityId = userAudiobookData.audiobookId
|
|
||||||
liProgress.duration = userAudiobookData.totalDuration
|
liProgress.duration = userAudiobookData.totalDuration
|
||||||
liProgress.isFinished = !!userAudiobookData.isRead
|
liProgress.isFinished = !!userAudiobookData.isRead
|
||||||
Object.keys(liProgress.toJSON()).forEach((key) => {
|
Object.keys(liProgress.toJSON()).forEach((key) => {
|
||||||
@ -356,7 +288,7 @@ function cleanUserObject(db, userObj) {
|
|||||||
liProgress[key] = userAudiobookData[key]
|
liProgress[key] = userAudiobookData[key]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
cleanedUserPayload.libraryItemProgress.push(liProgress.toJSON())
|
cleanedUserPayload.mediaProgress.push(liProgress.toJSON())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -376,8 +308,7 @@ function cleanSessionObj(db, userListeningSession) {
|
|||||||
newPlaybackSession.id = getId('play')
|
newPlaybackSession.id = getId('play')
|
||||||
newPlaybackSession.mediaType = 'book'
|
newPlaybackSession.mediaType = 'book'
|
||||||
newPlaybackSession.updatedAt = userListeningSession.lastUpdate
|
newPlaybackSession.updatedAt = userListeningSession.lastUpdate
|
||||||
newPlaybackSession.libraryItemId = userListeningSession.audiobookId // Temp
|
newPlaybackSession.libraryItemId = userListeningSession.audiobookId
|
||||||
newPlaybackSession.mediaEntityId = userListeningSession.audiobookId
|
|
||||||
newPlaybackSession.playMethod = PlayMethod.TRANSCODE
|
newPlaybackSession.playMethod = PlayMethod.TRANSCODE
|
||||||
|
|
||||||
// We only have title to transfer over nicely
|
// We only have title to transfer over nicely
|
||||||
|
@ -28,7 +28,7 @@ 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 itemProgress = user.getLibraryItemProgress(li.id)
|
var itemProgress = user.getMediaProgress(li.id)
|
||||||
if (filter === 'Finished' && (itemProgress && itemProgress.isFinished)) return true
|
if (filter === 'Finished' && (itemProgress && itemProgress.isFinished)) return true
|
||||||
if (filter === 'Not Started' && !itemProgress) return true
|
if (filter === 'Not Started' && !itemProgress) return true
|
||||||
if (filter === 'In Progress' && (itemProgress && itemProgress.inProgress)) return true
|
if (filter === 'In Progress' && (itemProgress && itemProgress.inProgress)) return true
|
||||||
@ -126,7 +126,7 @@ module.exports = {
|
|||||||
// var _series = {}
|
// var _series = {}
|
||||||
// books.forEach((audiobook) => {
|
// books.forEach((audiobook) => {
|
||||||
// if (audiobook.book.series) {
|
// if (audiobook.book.series) {
|
||||||
// var bookWithUserAb = { userAudiobook: user.getLibraryItemProgress(audiobook.id), book: audiobook }
|
// var bookWithUserAb = { userAudiobook: user.getMediaProgress(audiobook.id), book: audiobook }
|
||||||
// if (!_series[audiobook.book.series]) {
|
// if (!_series[audiobook.book.series]) {
|
||||||
// _series[audiobook.book.series] = {
|
// _series[audiobook.book.series] = {
|
||||||
// id: audiobook.book.series,
|
// id: audiobook.book.series,
|
||||||
@ -159,7 +159,7 @@ module.exports = {
|
|||||||
|
|
||||||
getItemsWithUserProgress(user, libraryItems) {
|
getItemsWithUserProgress(user, libraryItems) {
|
||||||
return libraryItems.map(li => {
|
return libraryItems.map(li => {
|
||||||
var itemProgress = user.getLibraryItemProgress(li.id)
|
var itemProgress = user.getMediaProgress(li.id)
|
||||||
return {
|
return {
|
||||||
userProgress: itemProgress ? itemProgress.toJSON() : null,
|
userProgress: itemProgress ? itemProgress.toJSON() : null,
|
||||||
libraryItem: li
|
libraryItem: li
|
||||||
@ -241,13 +241,13 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getItemDurationStats(libraryItems) {
|
getItemDurationStats(libraryItems) {
|
||||||
var sorted = sort(libraryItems).desc(li => li.media.getLongestDuration())
|
var sorted = sort(libraryItems).desc(li => li.media.duration)
|
||||||
var top10 = sorted.slice(0, 10).map(li => ({ title: li.media.metadata.title, duration: li.media.getLongestDuration() })).filter(i => i.duration > 0)
|
var top10 = sorted.slice(0, 10).map(li => ({ title: li.media.metadata.title, duration: li.media.duration })).filter(i => i.duration > 0)
|
||||||
var totalDuration = 0
|
var totalDuration = 0
|
||||||
var numAudioTracks = 0
|
var numAudioTracks = 0
|
||||||
libraryItems.forEach((li) => {
|
libraryItems.forEach((li) => {
|
||||||
totalDuration += li.media.getTotalDuration()
|
totalDuration += li.media.duration
|
||||||
numAudioTracks += li.media.getTotalAudioTracks()
|
numAudioTracks += li.media.numTracks
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
totalDuration,
|
totalDuration,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user