mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Add playing podcast episodes, episode progress, podcast page, podcast home page shelves
This commit is contained in:
		
							parent
							
								
									e32d05ea27
								
							
						
					
					
						commit
						0e665e2091
					
				| @ -151,7 +151,9 @@ export default { | ||||
|       this.$store.commit('showEditModalOnTab', { libraryItem: this.book, tab: 'download' }) | ||||
|     }, | ||||
|     startStream() { | ||||
|       this.$eventBus.$emit('play-item', this.book.id) | ||||
|       this.$eventBus.$emit('play-item', { | ||||
|         libraryItemId: this.book.id | ||||
|       }) | ||||
|     }, | ||||
|     editClick() { | ||||
|       this.$emit('edit', this.book) | ||||
|  | ||||
| @ -167,7 +167,7 @@ export default { | ||||
|     libraryItemUpdated(libraryItem) { | ||||
|       console.log('libraryItem updated', libraryItem) | ||||
|       this.shelves.forEach((shelf) => { | ||||
|         if (shelf.type === 'books') { | ||||
|         if (shelf.type == 'book' || shelf.type == 'podcast') { | ||||
|           shelf.entities = shelf.entities.map((ent) => { | ||||
|             if (ent.id === libraryItem.id) { | ||||
|               return libraryItem | ||||
| @ -186,7 +186,7 @@ export default { | ||||
|     }, | ||||
|     removeBookFromShelf(libraryItem) { | ||||
|       this.shelves.forEach((shelf) => { | ||||
|         if (shelf.type === 'books') { | ||||
|         if (shelf.type == 'book' || shelf.type == 'podcast') { | ||||
|           shelf.entities = shelf.entities.filter((ent) => { | ||||
|             return ent.id !== libraryItem.id | ||||
|           }) | ||||
|  | ||||
| @ -2,7 +2,7 @@ | ||||
|   <div class="relative"> | ||||
|     <div ref="shelf" class="w-full max-w-full categorizedBookshelfRow relative overflow-x-scroll overflow-y-hidden z-10" :style="{ paddingLeft: paddingLeft * sizeMultiplier + 'rem', height: shelfHeight + 'px' }" @scroll="scrolled"> | ||||
|       <div class="w-full h-full pt-6"> | ||||
|         <div v-if="shelf.type === 'book'" class="flex items-center"> | ||||
|         <div v-if="shelf.type === 'book' || shelf.type === 'podcast'" class="flex items-center"> | ||||
|           <template v-for="(entity, index) in shelf.entities"> | ||||
|             <cards-lazy-book-card :key="entity.id" :ref="`shelf-book-${entity.id}`" :index="index" :width="bookCoverWidth" :height="bookCoverHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" :book-mount="entity" class="relative mx-2" @hook:updated="updatedBookCard" @select="selectItem" @edit="editBook" /> | ||||
|           </template> | ||||
|  | ||||
| @ -133,6 +133,10 @@ export default { | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     setPlaying(isPlaying) { | ||||
|       this.isPlaying = isPlaying | ||||
|       this.$store.commit('setIsPlaying', isPlaying) | ||||
|     }, | ||||
|     setSleepTimer(seconds) { | ||||
|       this.sleepTimerSet = true | ||||
|       this.sleepTimerTime = seconds | ||||
| @ -221,7 +225,7 @@ export default { | ||||
|     }, | ||||
|     closePlayer() { | ||||
|       this.playerHandler.closePlayer() | ||||
|       this.$store.commit('setLibraryItemStream', null) | ||||
|       this.$store.commit('setMediaPlaying', null) | ||||
|     }, | ||||
|     streamProgress(data) { | ||||
|       if (!data.numSegments) return | ||||
| @ -234,7 +238,10 @@ export default { | ||||
|       } | ||||
|     }, | ||||
|     sessionOpen(session) { | ||||
|       this.$store.commit('setLibraryItemStream', session.libraryItem) | ||||
|       this.$store.commit('setMediaPlaying', { | ||||
|         libraryItem: session.libraryItem, | ||||
|         episodeId: session.episodeId | ||||
|       }) | ||||
|       this.playerHandler.prepareOpenSession(session) | ||||
|     }, | ||||
|     streamClosed(streamId) { | ||||
| @ -271,24 +278,40 @@ export default { | ||||
|         this.playerHandler.switchPlayer() | ||||
|       } | ||||
|     }, | ||||
|     async playLibraryItem(libraryItemId) { | ||||
|     async playLibraryItem(payload) { | ||||
|       var libraryItemId = payload.libraryItemId | ||||
|       var episodeId = payload.episodeId || null | ||||
| 
 | ||||
|       if (this.playerHandler.libraryItemId == libraryItemId && this.playerHandler.episodeId == episodeId) { | ||||
|         this.playerHandler.play() | ||||
|         return | ||||
|       } | ||||
| 
 | ||||
|       var libraryItem = await this.$axios.$get(`/api/items/${libraryItemId}?expanded=1`).catch((error) => { | ||||
|         console.error('Failed to fetch full item', error) | ||||
|         return null | ||||
|       }) | ||||
|       if (!libraryItem) return | ||||
|       this.$store.commit('setLibraryItemStream', libraryItem) | ||||
|       this.$store.commit('setMediaPlaying', { | ||||
|         libraryItem, | ||||
|         episodeId | ||||
|       }) | ||||
| 
 | ||||
|       this.playerHandler.load(libraryItem, true) | ||||
|       this.playerHandler.load(libraryItem, episodeId, true) | ||||
|     }, | ||||
|     pauseItem() { | ||||
|       this.playerHandler.pause() | ||||
|     } | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.$eventBus.$on('cast-session-active', this.castSessionActive) | ||||
|     this.$eventBus.$on('play-item', this.playLibraryItem) | ||||
|     this.$eventBus.$on('pause-item', this.pauseItem) | ||||
|   }, | ||||
|   beforeDestroy() { | ||||
|     this.$eventBus.$off('cast-session-active', this.castSessionActive) | ||||
|     this.$eventBus.$off('play-item', this.playLibraryItem) | ||||
|     this.$eventBus.$off('pause-item', this.pauseItem) | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|  | ||||
| @ -527,7 +527,9 @@ export default { | ||||
|     }, | ||||
|     play() { | ||||
|       var eventBus = this.$eventBus || this.$nuxt.$eventBus | ||||
|       eventBus.$emit('play-item', this.libraryItemId) | ||||
|       eventBus.$emit('play-item', { | ||||
|         libraryItemId: this.libraryItemId | ||||
|       }) | ||||
|     }, | ||||
|     mouseover() { | ||||
|       this.isHovering = true | ||||
|  | ||||
| @ -123,8 +123,9 @@ export default { | ||||
|       if (!this.userCanUpdate && !this.userCanDownload) return [] | ||||
|       return this.tabs.filter((tab) => { | ||||
|         if (tab.id === 'download' && this.isMissing) return false | ||||
|         if (tab.id === 'chapters' && this.mediaType !== 'book') return false | ||||
|         if (tab.id === 'episodes' && this.mediaType !== 'podcast') return false | ||||
|         if (this.mediaType == 'podcast' && (tab.id == 'match' || tab.id == 'chapters')) return false | ||||
|         if (this.mediaType == 'book' && tab.id == 'episodes') return false | ||||
| 
 | ||||
|         if ((tab.id === 'download' || tab.id === 'files') && this.userCanDownload) return true | ||||
|         if (tab.id !== 'download' && tab.id !== 'files' && this.userCanUpdate) return true | ||||
|         if (tab.id === 'match' && this.userCanUpdate && this.showExperimentalFeatures) return true | ||||
|  | ||||
| @ -35,7 +35,7 @@ | ||||
|                 </div> | ||||
|               </td> | ||||
|               <td v-if="userCanDownload && !isMissing" class="text-center"> | ||||
|                 <a :href="`/s/item/${libraryItemId}${$encodeUriPath(file.metadata.relPath)}?token=${userToken}`" download><span class="material-icons icon-text">download</span></a> | ||||
|                 <a :href="`/s/item/${libraryItemId}/${$encodeUriPath(file.metadata.relPath).replace(/^\//, '')}?token=${userToken}`" download><span class="material-icons icon-text">download</span></a> | ||||
|               </td> | ||||
|             </tr> | ||||
|           </template> | ||||
|  | ||||
| @ -38,7 +38,7 @@ | ||||
|                 {{ $secondsToTimestamp(track.duration) }} | ||||
|               </td> | ||||
|               <td v-if="userCanDownload" class="text-center"> | ||||
|                 <a :href="`/s/item/${libraryItemId}${$encodeUriPath(track.metadata.relPath)}?token=${userToken}`" download><span class="material-icons icon-text">download</span></a> | ||||
|                 <a :href="`/s/item/${libraryItemId}/${$encodeUriPath(track.metadata.relPath).replace(/^\//, '')}?token=${userToken}`" download><span class="material-icons icon-text">download</span></a> | ||||
|               </td> | ||||
|             </tr> | ||||
|           </template> | ||||
|  | ||||
| @ -27,11 +27,6 @@ | ||||
|         <span class="material-icons text-lg text-white text-opacity-70 hover:text-opacity-100 cursor-pointer">radio_button_unchecked</span> | ||||
|       </div> --> | ||||
|     </div> | ||||
|     <!-- <div class="absolute top-0 left-0 z-40 bg-red-500 w-full h-full"> | ||||
|       <div class="w-24 h-full absolute top-0 -right-24 transform transition-transform" :class="isHovering ? 'translate-x-0' : '-translate-x-24'"> | ||||
|         <span class="material-icons">edit</span> | ||||
|       </div> | ||||
|     </div> --> | ||||
|     <div class="w-40 absolute top-0 -right-24 h-full transform transition-transform" :class="!isHovering ? 'translate-x-0' : '-translate-x-24'"> | ||||
|       <div class="flex h-full items-center"> | ||||
|         <ui-tooltip :text="userIsFinished ? 'Mark as Not Finished' : 'Mark as Finished'" direction="top"> | ||||
| @ -126,7 +121,9 @@ export default { | ||||
|       this.isHovering = false | ||||
|     }, | ||||
|     playClick() { | ||||
|       this.$eventBus.$emit('play-item', this.book.id) | ||||
|       this.$eventBus.$emit('play-item', { | ||||
|         libraryItemId: this.book.id | ||||
|       }) | ||||
|     }, | ||||
|     clickEdit() { | ||||
|       this.$emit('edit', this.book) | ||||
|  | ||||
							
								
								
									
										161
									
								
								client/components/tables/podcast/EpisodeTableRow.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								client/components/tables/podcast/EpisodeTableRow.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,161 @@ | ||||
| <template> | ||||
|   <div class="w-full px-2 py-3 overflow-hidden relative border-b border-white border-opacity-10" @mouseover="mouseover" @mouseleave="mouseleave"> | ||||
|     <div v-if="episode" class="flex items-center h-24"> | ||||
|       <div class="w-12 min-w-12 max-w-16 h-full"> | ||||
|         <div class="flex h-full items-center justify-center"> | ||||
|           <span class="material-icons drag-handle text-lg text-white text-opacity-50 hover:text-opacity-100">menu</span> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="flex-grow px-2"> | ||||
|         <p class="text-sm font-semibold"> | ||||
|           {{ title }} | ||||
|         </p> | ||||
|         <p class="text-sm"> | ||||
|           {{ description }} | ||||
|         </p> | ||||
|         <div class="flex items-center pt-2"> | ||||
|           <div class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer" :class="userIsFinished ? 'text-white text-opacity-40' : ''" @click="playClick"> | ||||
|             <span class="material-icons">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span> | ||||
|             <p class="pl-2 pr-1 text-sm">{{ timeRemaining }}</p> | ||||
|           </div> | ||||
| 
 | ||||
|           <ui-tooltip :text="userIsFinished ? 'Mark as Not Finished' : 'Mark as Finished'" direction="top"> | ||||
|             <ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" borderless class="mx-1 mt-0.5" @click="toggleFinished" /> | ||||
|           </ui-tooltip> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="w-24 min-w-24" /> | ||||
|     </div> | ||||
|     <div class="w-24 min-w-24 -right-0 absolute top-0 h-full transform transition-transform" :class="!isHovering ? 'translate-x-32' : 'translate-x-0'"> | ||||
|       <div class="flex h-full items-center"> | ||||
|         <div class="mx-1"> | ||||
|           <ui-icon-btn icon="edit" borderless @click="clickEdit" /> | ||||
|         </div> | ||||
|         <div class="mx-1"> | ||||
|           <ui-icon-btn icon="close" borderless @click="removeClick" /> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <div v-if="!userIsFinished" class="absolute bottom-0 left-0 h-1 bg-warning" :style="{ width: itemProgressPercent * 100 + '%' }" /> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| export default { | ||||
|   props: { | ||||
|     libraryItemId: String, | ||||
|     episode: { | ||||
|       type: Object, | ||||
|       default: () => {} | ||||
|     }, | ||||
|     isDragging: Boolean | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       isProcessingReadUpdate: false, | ||||
|       processingRemove: false, | ||||
|       isHovering: false | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
|     isDragging: { | ||||
|       handler(newVal) { | ||||
|         if (newVal) { | ||||
|           this.isHovering = false | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     audioFile() { | ||||
|       return this.episode.audioFile | ||||
|     }, | ||||
|     title() { | ||||
|       return this.episode.title || '' | ||||
|     }, | ||||
|     description() { | ||||
|       return this.episode.description || '' | ||||
|     }, | ||||
|     duration() { | ||||
|       return this.$secondsToTimestamp(this.episode.duration) | ||||
|     }, | ||||
|     isStreaming() { | ||||
|       return this.$store.getters['getIsEpisodeStreaming'](this.libraryItemId, this.episode.id) | ||||
|     }, | ||||
|     streamIsPlaying() { | ||||
|       return this.$store.state.streamIsPlaying && this.isStreaming | ||||
|     }, | ||||
|     itemProgress() { | ||||
|       return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId, this.episode.id) | ||||
|     }, | ||||
|     itemProgressPercent() { | ||||
|       return this.itemProgress ? this.itemProgress.progress : 0 | ||||
|     }, | ||||
|     userIsFinished() { | ||||
|       return this.itemProgress ? !!this.itemProgress.isFinished : false | ||||
|     }, | ||||
|     timeRemaining() { | ||||
|       if (this.streamIsPlaying) return 'Playing' | ||||
|       if (!this.itemProgress) return this.$elapsedPretty(this.episode.duration) | ||||
|       if (this.userIsFinished) return 'Finished' | ||||
|       var remaining = Math.floor(this.itemProgress.duration - this.itemProgress.currentTime) | ||||
|       return `${this.$elapsedPretty(remaining)} left` | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     mouseover() { | ||||
|       if (this.isDragging) return | ||||
|       this.isHovering = true | ||||
|     }, | ||||
|     mouseleave() { | ||||
|       this.isHovering = false | ||||
|     }, | ||||
|     clickEdit() {}, | ||||
|     playClick() { | ||||
|       if (this.streamIsPlaying) { | ||||
|         this.$eventBus.$emit('pause-item') | ||||
|       } else { | ||||
|         this.$eventBus.$emit('play-item', { | ||||
|           libraryItemId: this.libraryItemId, | ||||
|           episodeId: this.episode.id | ||||
|         }) | ||||
|       } | ||||
|     }, | ||||
|     toggleFinished() { | ||||
|       var updatePayload = { | ||||
|         isFinished: !this.userIsFinished | ||||
|       } | ||||
|       this.isProcessingReadUpdate = true | ||||
|       this.$axios | ||||
|         .$patch(`/api/me/progress/${this.libraryItemId}/${this.episode.id}`, updatePayload) | ||||
|         .then(() => { | ||||
|           this.isProcessingReadUpdate = false | ||||
|           this.$toast.success(`Item marked as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`) | ||||
|         }) | ||||
|         .catch((error) => { | ||||
|           console.error('Failed', error) | ||||
|           this.isProcessingReadUpdate = false | ||||
|           this.$toast.error(`Failed to mark as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`) | ||||
|         }) | ||||
|     }, | ||||
|     removeClick() { | ||||
|       this.processingRemove = true | ||||
| 
 | ||||
|       this.$axios | ||||
|         .$delete(`/api/items/${this.libraryItemId}/episode/${this.episode.id}`) | ||||
|         .then((updatedPodcast) => { | ||||
|           console.log(`Episode removed from podcast`, updatedPodcast) | ||||
|           this.$toast.success('Episode removed from podcast') | ||||
|           this.processingRemove = false | ||||
|         }) | ||||
|         .catch((error) => { | ||||
|           console.error('Failed to remove episode from podcast', error) | ||||
|           this.$toast.error('Failed to remove episode from podcast') | ||||
|           this.processingRemove = false | ||||
|         }) | ||||
|     } | ||||
|   }, | ||||
|   mounted() {} | ||||
| } | ||||
| </script> | ||||
							
								
								
									
										99
									
								
								client/components/tables/podcast/EpisodesTable.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								client/components/tables/podcast/EpisodesTable.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,99 @@ | ||||
| <template> | ||||
|   <div class="w-full py-6"> | ||||
|     <p class="text-lg mb-0 font-semibold">Episodes</p> | ||||
|     <draggable v-model="episodesCopy" v-bind="dragOptions" class="list-group" handle=".drag-handle" draggable=".item" tag="div" @start="drag = true" @end="drag = false" @update="draggableUpdate"> | ||||
|       <transition-group type="transition" :name="!drag ? 'episode' : null"> | ||||
|         <template v-for="episode in episodesCopy"> | ||||
|           <tables-podcast-episode-table-row :key="episode.id" :is-dragging="drag" :episode="episode" :library-item-id="libraryItem.id" class="item" :class="drag ? '' : 'episode'" /> | ||||
|         </template> | ||||
|       </transition-group> | ||||
|     </draggable> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import draggable from 'vuedraggable' | ||||
| 
 | ||||
| export default { | ||||
|   components: { | ||||
|     draggable | ||||
|   }, | ||||
|   props: { | ||||
|     libraryItem: { | ||||
|       type: Object, | ||||
|       default: () => {} | ||||
|     } | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       drag: false, | ||||
|       dragOptions: { | ||||
|         animation: 200, | ||||
|         group: 'description', | ||||
|         ghostClass: 'ghost' | ||||
|       }, | ||||
|       episodesCopy: [] | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
|     libraryItem: { | ||||
|       handler(newVal) { | ||||
|         this.init() | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     media() { | ||||
|       return this.libraryItem.media || {} | ||||
|     }, | ||||
|     mediaMetadata() { | ||||
|       return this.media.metadata || {} | ||||
|     }, | ||||
|     episodes() { | ||||
|       return this.media.episodes || [] | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     draggableUpdate() { | ||||
|       var episodesUpdate = { | ||||
|         episodes: this.episodesCopy.map((b) => b.id) | ||||
|       } | ||||
|       this.$axios | ||||
|         .$patch(`/api/items/${this.libraryItem.id}/episodes`, episodesUpdate) | ||||
|         .then((podcast) => { | ||||
|           console.log('Podcast updated', podcast) | ||||
|         }) | ||||
|         .catch((error) => { | ||||
|           console.error('Failed to update podcast', error) | ||||
|           this.$toast.error('Failed to save podcast episode order') | ||||
|         }) | ||||
|     }, | ||||
|     init() { | ||||
|       this.episodesCopy = this.episodes.map((ep) => { | ||||
|         return { | ||||
|           ...ep | ||||
|         } | ||||
|       }) | ||||
|     } | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.init() | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style> | ||||
| .episode-item { | ||||
|   transition: all 0.4s ease; | ||||
| } | ||||
| 
 | ||||
| .episode-enter-from, | ||||
| .episode-leave-to { | ||||
|   opacity: 0; | ||||
|   transform: translateX(30px); | ||||
| } | ||||
| 
 | ||||
| .episode-leave-active { | ||||
|   position: absolute; | ||||
| } | ||||
| </style> | ||||
| @ -120,7 +120,9 @@ export default { | ||||
|         return !prog || !prog.isFinished | ||||
|       }) | ||||
|       if (nextBookNotRead) { | ||||
|         this.$eventBus.$emit('play-item', nextBookNotRead.id) | ||||
|         this.$eventBus.$emit('play-item', { | ||||
|           libraryItemId: nextBookNotRead.id | ||||
|         }) | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|  | ||||
| @ -7,7 +7,7 @@ | ||||
|             <covers-book-cover :library-item="libraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" /> | ||||
| 
 | ||||
|             <!-- Item Progress Bar --> | ||||
|             <div class="absolute bottom-0 left-0 h-1.5 bg-yellow-400 shadow-sm z-10" :class="userIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: 208 * progressPercent + 'px' }"></div> | ||||
|             <div v-if="!isPodcast" class="absolute bottom-0 left-0 h-1.5 bg-yellow-400 shadow-sm z-10" :class="userIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: 208 * progressPercent + 'px' }"></div> | ||||
| 
 | ||||
|             <!-- Item Cover Overlay --> | ||||
|             <div class="absolute top-0 left-0 w-full h-full z-10 bg-black bg-opacity-30 opacity-0 hover:opacity-100 transition-opacity" @mousedown.prevent @mouseup.prevent> | ||||
| @ -96,7 +96,7 @@ | ||||
|           </div> | ||||
| 
 | ||||
|           <!-- Progress --> | ||||
|           <div v-if="progressPercent > 0" class="px-4 py-2 mt-4 bg-primary text-sm font-semibold rounded-md text-gray-100 relative max-w-max mx-auto md:mx-0" :class="resettingProgress ? 'opacity-25' : ''"> | ||||
|           <div v-if="!isPodcast && progressPercent > 0" class="px-4 py-2 mt-4 bg-primary text-sm font-semibold rounded-md text-gray-100 relative max-w-max mx-auto md:mx-0" :class="resettingProgress ? 'opacity-25' : ''"> | ||||
|             <p v-if="progressPercent < 1" class="leading-6">Your Progress: {{ Math.round(progressPercent * 100) }}%</p> | ||||
|             <p v-else class="text-xs">Finished {{ $formatDate(userProgressFinishedAt, 'MM/dd/yyyy') }}</p> | ||||
|             <p v-if="progressPercent < 1" class="text-gray-200 text-xs">{{ $elapsedPretty(userTimeRemaining) }} remaining</p> | ||||
| @ -145,6 +145,8 @@ | ||||
| 
 | ||||
|           <widgets-audiobook-data v-if="tracks.length" :library-item-id="libraryItemId" :media="media" /> | ||||
| 
 | ||||
|           <tables-podcast-episodes-table v-if="isPodcast" :library-item="libraryItem" /> | ||||
| 
 | ||||
|           <tables-library-files-table v-if="libraryFiles.length" :is-missing="isMissing" :library-item-id="libraryItemId" :files="libraryFiles" class="mt-6" /> | ||||
|         </div> | ||||
|       </div> | ||||
| @ -353,7 +355,20 @@ export default { | ||||
|         }) | ||||
|     }, | ||||
|     startStream() { | ||||
|       this.$eventBus.$emit('play-item', this.libraryItem.id) | ||||
|       var episodeId = null | ||||
|       if (this.isPodcast) { | ||||
|         var episode = this.podcastEpisodes.find((ep) => { | ||||
|           var podcastProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItemId, ep.id) | ||||
|           return !podcastProgress || !podcastProgress.isFinished | ||||
|         }) | ||||
|         if (!episode) episode = this.podcastEpisodes[0] | ||||
|         episodeId = episode.id | ||||
|       } | ||||
| 
 | ||||
|       this.$eventBus.$emit('play-item', { | ||||
|         libraryItemId: this.libraryItem.id, | ||||
|         episodeId | ||||
|       }) | ||||
|     }, | ||||
|     editClick() { | ||||
|       this.$store.commit('setBookshelfBookIds', []) | ||||
|  | ||||
| @ -6,6 +6,7 @@ export default class PlayerHandler { | ||||
|   constructor(ctx) { | ||||
|     this.ctx = ctx | ||||
|     this.libraryItem = null | ||||
|     this.episodeId = null | ||||
|     this.playWhenReady = false | ||||
|     this.player = null | ||||
|     this.playerState = 'IDLE' | ||||
| @ -23,6 +24,9 @@ export default class PlayerHandler { | ||||
|   get isCasting() { | ||||
|     return this.ctx.$store.state.globals.isCasting | ||||
|   } | ||||
|   get libraryItemId() { | ||||
|     return this.libraryItem ? this.libraryItem.id : null | ||||
|   } | ||||
|   get isPlayingCastedItem() { | ||||
|     return this.libraryItem && (this.player instanceof CastPlayer) | ||||
|   } | ||||
| @ -36,10 +40,11 @@ export default class PlayerHandler { | ||||
|     return this.playerState === 'PLAYING' | ||||
|   } | ||||
| 
 | ||||
|   load(libraryItem, playWhenReady) { | ||||
|   load(libraryItem, episodeId, playWhenReady) { | ||||
|     if (!this.player) this.switchPlayer() | ||||
| 
 | ||||
|     this.libraryItem = libraryItem | ||||
|     this.episodeId = episodeId | ||||
|     this.playWhenReady = playWhenReady | ||||
|     this.prepare() | ||||
|   } | ||||
| @ -113,7 +118,7 @@ export default class PlayerHandler { | ||||
|       this.ctx.setCurrentTime(this.player.getCurrentTime()) | ||||
|     } | ||||
| 
 | ||||
|     this.ctx.isPlaying = this.playerState === 'PLAYING' | ||||
|     this.ctx.setPlaying(this.playerState === 'PLAYING') | ||||
|     this.ctx.playerLoading = this.playerState === 'LOADING' | ||||
|   } | ||||
| 
 | ||||
| @ -132,7 +137,9 @@ export default class PlayerHandler { | ||||
|       forceTranscode, | ||||
|       forceDirectPlay: this.isCasting // TODO: add transcode support for chromecast
 | ||||
|     } | ||||
|     var session = await this.ctx.$axios.$post(`/api/items/${this.libraryItem.id}/play`, payload).catch((error) => { | ||||
| 
 | ||||
|     var path = this.episodeId ? `/api/items/${this.libraryItem.id}/play/${this.episodeId}` : `/api/items/${this.libraryItem.id}/play` | ||||
|     var session = await this.ctx.$axios.$post(path, payload).catch((error) => { | ||||
|       console.error('Failed to start stream', error) | ||||
|     }) | ||||
|     this.prepareSession(session) | ||||
|  | ||||
| @ -5,6 +5,8 @@ export const state = () => ({ | ||||
|   versionData: null, | ||||
|   serverSettings: null, | ||||
|   streamLibraryItem: null, | ||||
|   streamEpisodeId: null, | ||||
|   streamIsPlaying: false, | ||||
|   editModalTab: 'details', | ||||
|   showEditModal: false, | ||||
|   showEReader: false, | ||||
| @ -38,6 +40,10 @@ export const getters = { | ||||
|   getNumLibraryItemsSelected: state => state.selectedLibraryItems.length, | ||||
|   getLibraryItemIdStreaming: state => { | ||||
|     return state.streamLibraryItem ? state.streamLibraryItem.id : null | ||||
|   }, | ||||
|   getIsEpisodeStreaming: state => (libraryItemId, episodeId) => { | ||||
|     if (!state.streamLibraryItem) return null | ||||
|     return state.streamLibraryItem.id == libraryItemId && state.streamEpisodeId == episodeId | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @ -105,8 +111,18 @@ export const mutations = { | ||||
|     if (!settings) return | ||||
|     state.serverSettings = settings | ||||
|   }, | ||||
|   setLibraryItemStream(state, libraryItem) { | ||||
|     state.streamLibraryItem = libraryItem | ||||
|   setMediaPlaying(state, payload) { | ||||
|     if (!payload) { | ||||
|       state.streamLibraryItem = null | ||||
|       state.streamEpisodeId = null | ||||
|       state.streamIsPlaying = false | ||||
|     } else { | ||||
|       state.streamLibraryItem = payload.libraryItem | ||||
|       state.streamEpisodeId = payload.episodeId || null | ||||
|     } | ||||
|   }, | ||||
|   setIsPlaying(state, isPlaying) { | ||||
|     state.streamIsPlaying = isPlaying | ||||
|   }, | ||||
|   showEditModal(state, libraryItem) { | ||||
|     state.editModalTab = 'details' | ||||
|  | ||||
| @ -22,9 +22,12 @@ export const getters = { | ||||
|   getToken: (state) => { | ||||
|     return state.user ? state.user.token : null | ||||
|   }, | ||||
|   getUserMediaProgress: (state) => (libraryItemId) => { | ||||
|   getUserMediaProgress: (state) => (libraryItemId, episodeId = null) => { | ||||
|     if (!state.user.mediaProgress) return null | ||||
|     return state.user.mediaProgress.find(li => li.id == libraryItemId) | ||||
|     return state.user.mediaProgress.find(li => { | ||||
|       if (episodeId && li.episodeId !== episodeId) return false | ||||
|       return li.id == libraryItemId | ||||
|     }) | ||||
|   }, | ||||
|   getUserBookmarksForItem: (state) => (libraryItemId) => { | ||||
|     if (!state.user.bookmarks) return [] | ||||
|  | ||||
| @ -384,6 +384,10 @@ class Server { | ||||
|         Logger.error(`[Server] Library Item for session "${session.id}" does not exist "${session.libraryItemId}"`) | ||||
|         this.playbackSessionManager.removeSession(session.id) | ||||
|         session = null | ||||
|       } else if (session.mediaType === 'podcast' && !sessionLibraryItem.media.checkHasEpisode(session.episodeId)) { | ||||
|         Logger.error(`[Server] Library Item for session "${session.id}" episode ${session.episodeId} does not exist "${session.libraryItemId}"`) | ||||
|         this.playbackSessionManager.removeSession(session.id) | ||||
|         session = null | ||||
|       } | ||||
|       if (session) { | ||||
|         session = session.toJSONForClient(sessionLibraryItem) | ||||
|  | ||||
| @ -275,6 +275,8 @@ class LibraryController { | ||||
| 
 | ||||
|   // api/libraries/:id/personalized
 | ||||
|   async getLibraryUserPersonalized(req, res) { | ||||
|     var mediaType = req.library.mediaType | ||||
|     var isPodcastLibrary = mediaType == 'podcast' | ||||
|     var libraryItems = req.libraryItems | ||||
|     var limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12 | ||||
|     var minified = req.query.minified === '1' | ||||
| @ -283,8 +285,8 @@ class LibraryController { | ||||
| 
 | ||||
|     var categories = [ | ||||
|       { | ||||
|         id: 'continue-reading', | ||||
|         label: 'Continue Reading', | ||||
|         id: 'continue-listening', | ||||
|         label: 'Continue Listening', | ||||
|         type: req.library.mediaType, | ||||
|         entities: libraryHelpers.getItemsMostRecentlyListened(itemsWithUserProgress, limitPerShelf, minified) | ||||
|       }, | ||||
| @ -295,8 +297,8 @@ class LibraryController { | ||||
|         entities: libraryHelpers.getItemsMostRecentlyAdded(libraryItems, limitPerShelf, minified) | ||||
|       }, | ||||
|       { | ||||
|         id: 'read-again', | ||||
|         label: 'Read Again', | ||||
|         id: 'listen-again', | ||||
|         label: 'Listen Again', | ||||
|         type: req.library.mediaType, | ||||
|         entities: libraryHelpers.getItemsMostRecentlyFinished(itemsWithUserProgress, limitPerShelf, minified) | ||||
|       } | ||||
|  | ||||
| @ -169,7 +169,24 @@ class LibraryItemController { | ||||
|       return res.sendStatus(404) | ||||
|     } | ||||
|     const options = req.body || {} | ||||
|     this.playbackSessionManager.startSessionRequest(req.user, req.libraryItem, options, res) | ||||
|     this.playbackSessionManager.startSessionRequest(req.user, req.libraryItem, null, options, res) | ||||
|   } | ||||
| 
 | ||||
|   // POST: api/items/:id/play/:episodeId
 | ||||
|   startEpisodePlaybackSession(req, res) { | ||||
|     var libraryItem = req.libraryItem | ||||
|     if (!libraryItem.media.numTracks) { | ||||
|       Logger.error(`[LibraryItemController] startPlaybackSession cannot playback ${libraryItem.id}`) | ||||
|       return res.sendStatus(404) | ||||
|     } | ||||
|     var episodeId = req.params.episodeId | ||||
|     if (!libraryItem.media.episodes.find(ep => ep.id === episodeId)) { | ||||
|       Logger.error(`[LibraryItemController] startPlaybackSession episode ${episodeId} not found for item ${libraryItem.id}`) | ||||
|       return res.sendStatus(404) | ||||
|     } | ||||
| 
 | ||||
|     const options = req.body || {} | ||||
|     this.playbackSessionManager.startSessionRequest(req.user, libraryItem, episodeId, options, res) | ||||
|   } | ||||
| 
 | ||||
|   // PATCH: api/items/:id/tracks
 | ||||
| @ -186,6 +203,38 @@ class LibraryItemController { | ||||
|     res.json(libraryItem.toJSON()) | ||||
|   } | ||||
| 
 | ||||
|   // PATCH: api/items/:id/episodes
 | ||||
|   async updateEpisodes(req, res) { | ||||
|     var libraryItem = req.libraryItem | ||||
|     var orderedFileData = req.body.episodes | ||||
|     if (!libraryItem.media.setEpisodeOrder) { | ||||
|       Logger.error(`[LibraryItemController] updateEpisodes invalid media type ${libraryItem.id}`) | ||||
|       return res.sendStatus(500) | ||||
|     } | ||||
|     libraryItem.media.setEpisodeOrder(orderedFileData) | ||||
|     await this.db.updateLibraryItem(libraryItem) | ||||
|     this.emitter('item_updated', libraryItem.toJSONExpanded()) | ||||
|     res.json(libraryItem.toJSON()) | ||||
|   } | ||||
| 
 | ||||
|   // DELETE: api/items/:id/episode/:episodeId
 | ||||
|   async removeEpisode(req, res) { | ||||
|     var episodeId = req.params.episodeId | ||||
|     var libraryItem = req.libraryItem | ||||
|     if (!libraryItem.mediaType !== 'podcast') { | ||||
|       Logger.error(`[LibraryItemController] removeEpisode invalid media type ${libraryItem.id}`) | ||||
|       return res.sendStatus(500) | ||||
|     } | ||||
|     if (!libraryItem.media.episodes.find(ep => ep.id === episodeId)) { | ||||
|       Logger.error(`[LibraryItemController] removeEpisode episode ${episodeId} not found for item ${libraryItem.id}`) | ||||
|       return res.sendStatus(404) | ||||
|     } | ||||
|     libraryItem.media.removeEpisode(episodeId) | ||||
|     await this.db.updateLibraryItem(libraryItem) | ||||
|     this.emitter('item_updated', libraryItem.toJSONExpanded()) | ||||
|     res.json(libraryItem.toJSON()) | ||||
|   } | ||||
| 
 | ||||
|   // POST api/items/:id/match
 | ||||
|   async match(req, res) { | ||||
|     var libraryItem = req.libraryItem | ||||
|  | ||||
| @ -43,6 +43,26 @@ class MeController { | ||||
|     res.sendStatus(200) | ||||
|   } | ||||
| 
 | ||||
|   // PATCH: api/me/progress/:id/:episodeId
 | ||||
|   async createUpdateEpisodeMediaProgress(req, res) { | ||||
|     var episodeId = req.params.episodeId | ||||
|     var libraryItem = this.db.libraryItems.find(ab => ab.id === req.params.id) | ||||
|     if (!libraryItem) { | ||||
|       return res.status(404).send('Item not found') | ||||
|     } | ||||
|     if (!libraryItem.media.episodes.find(ep => ep.id === episodeId)) { | ||||
|       Logger.error(`[MeController] removeEpisode episode ${episodeId} not found for item ${libraryItem.id}`) | ||||
|       return res.status(404).send('Episode not found') | ||||
|     } | ||||
| 
 | ||||
|     var wasUpdated = req.user.createUpdateMediaProgress(libraryItem, req.body, episodeId) | ||||
|     if (wasUpdated) { | ||||
|       await this.db.updateEntity('user', req.user) | ||||
|       this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) | ||||
|     } | ||||
|     res.sendStatus(200) | ||||
|   } | ||||
| 
 | ||||
|   // PATCH: api/me/progress/batch/update
 | ||||
|   async batchUpdateMediaProgress(req, res) { | ||||
|     var itemProgressPayloads = req.body | ||||
|  | ||||
| @ -25,8 +25,8 @@ class PlaybackSessionManager { | ||||
|     return session ? session.stream : null | ||||
|   } | ||||
| 
 | ||||
|   async startSessionRequest(user, libraryItem, options, res) { | ||||
|     const session = await this.startSession(user, libraryItem, options) | ||||
|   async startSessionRequest(user, libraryItem, episodeId, options, res) { | ||||
|     const session = await this.startSession(user, libraryItem, episodeId, options) | ||||
|     res.json(session.toJSONForClient(libraryItem)) | ||||
|   } | ||||
| 
 | ||||
| @ -42,23 +42,23 @@ class PlaybackSessionManager { | ||||
|     res.sendStatus(200) | ||||
|   } | ||||
| 
 | ||||
|   async startSession(user, libraryItem, options) { | ||||
|     var shouldDirectPlay = options.forceDirectPlay || (!options.forceTranscode && libraryItem.media.checkCanDirectPlay(options)) | ||||
|   async startSession(user, libraryItem, episodeId, options) { | ||||
|     var shouldDirectPlay = options.forceDirectPlay || (!options.forceTranscode && libraryItem.media.checkCanDirectPlay(options, episodeId)) | ||||
| 
 | ||||
|     const userProgress = user.getMediaProgress(libraryItem.id) | ||||
|     const userProgress = user.getMediaProgress(libraryItem.id, episodeId) | ||||
|     var userStartTime = 0 | ||||
|     if (userProgress) userStartTime = userProgress.currentTime || 0 | ||||
|     const newPlaybackSession = new PlaybackSession() | ||||
|     newPlaybackSession.setData(libraryItem, user) | ||||
|     newPlaybackSession.setData(libraryItem, user, episodeId) | ||||
| 
 | ||||
|     var audioTracks = [] | ||||
|     if (shouldDirectPlay) { | ||||
|       Logger.debug(`[PlaybackSessionManager] "${user.username}" starting direct play session for item "${libraryItem.id}"`) | ||||
|       audioTracks = libraryItem.getDirectPlayTracklist(libraryItem.id) | ||||
|       audioTracks = libraryItem.getDirectPlayTracklist(libraryItem.id, episodeId) | ||||
|       newPlaybackSession.playMethod = PlayMethod.DIRECTPLAY | ||||
|     } else { | ||||
|       Logger.debug(`[PlaybackSessionManager] "${user.username}" starting stream session for item "${libraryItem.id}"`) | ||||
|       var stream = new Stream(newPlaybackSession.id, this.StreamsPath, user, libraryItem, userStartTime, this.clientEmitter.bind(this)) | ||||
|       var stream = new Stream(newPlaybackSession.id, this.StreamsPath, user, libraryItem, episodeId, userStartTime, this.clientEmitter.bind(this)) | ||||
|       await stream.generatePlaylist() | ||||
|       audioTracks = [stream.getAudioTrack()] | ||||
|       newPlaybackSession.stream = stream | ||||
| @ -84,7 +84,7 @@ class PlaybackSessionManager { | ||||
|   async syncSession(user, session, syncData) { | ||||
|     var libraryItem = this.db.libraryItems.find(li => li.id === session.libraryItemId) | ||||
|     if (!libraryItem) { | ||||
|       Logger.error(`[PlaybackSessionManager] syncSession Library Item not found "${sessino.libraryItemId}"`) | ||||
|       Logger.error(`[PlaybackSessionManager] syncSession Library Item not found "${session.libraryItemId}"`) | ||||
|       return null | ||||
|     } | ||||
| 
 | ||||
| @ -97,10 +97,11 @@ class PlaybackSessionManager { | ||||
|       currentTime: syncData.currentTime, | ||||
|       progress: session.progress | ||||
|     } | ||||
|     var wasUpdated = user.createUpdateMediaProgress(libraryItem, itemProgressUpdate) | ||||
|     var wasUpdated = user.createUpdateMediaProgress(libraryItem, itemProgressUpdate, session.episodeId) | ||||
|     if (wasUpdated) { | ||||
| 
 | ||||
|       await this.db.updateEntity('user', user) | ||||
|       var itemProgress = user.getMediaProgress(session.libraryItemId) | ||||
|       var itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId) | ||||
|       this.clientEmitter(user.id, 'user_item_progress_updated', { | ||||
|         id: itemProgress.id, | ||||
|         data: itemProgress.toJSON() | ||||
|  | ||||
| @ -440,8 +440,8 @@ class LibraryItem { | ||||
|     return this.media.searchQuery(query) | ||||
|   } | ||||
| 
 | ||||
|   getDirectPlayTracklist(libraryItemId) { | ||||
|     return this.media.getDirectPlayTracklist(libraryItemId) | ||||
|   getDirectPlayTracklist(libraryItemId, episodeId) { | ||||
|     return this.media.getDirectPlayTracklist(libraryItemId, episodeId) | ||||
|   } | ||||
| } | ||||
| module.exports = LibraryItem | ||||
| @ -9,6 +9,7 @@ class PlaybackSession { | ||||
|     this.id = null | ||||
|     this.userId = null | ||||
|     this.libraryItemId = null | ||||
|     this.episodeId = null | ||||
| 
 | ||||
|     this.mediaType = null | ||||
|     this.mediaMetadata = null | ||||
| @ -41,6 +42,7 @@ class PlaybackSession { | ||||
|       sessionType: this.sessionType, | ||||
|       userId: this.userId, | ||||
|       libraryItemId: this.libraryItemId, | ||||
|       episodeId: this.episodeId, | ||||
|       mediaType: this.mediaType, | ||||
|       mediaMetadata: this.mediaMetadata ? this.mediaMetadata.toJSON() : null, | ||||
|       coverPath: this.coverPath, | ||||
| @ -60,6 +62,7 @@ class PlaybackSession { | ||||
|       sessionType: this.sessionType, | ||||
|       userId: this.userId, | ||||
|       libraryItemId: this.libraryItemId, | ||||
|       episodeId: this.episodeId, | ||||
|       mediaType: this.mediaType, | ||||
|       mediaMetadata: this.mediaMetadata ? this.mediaMetadata.toJSON() : null, | ||||
|       coverPath: this.coverPath, | ||||
| @ -81,7 +84,8 @@ class PlaybackSession { | ||||
|     this.sessionType = session.sessionType | ||||
|     this.userId = session.userId | ||||
|     this.libraryItemId = session.libraryItemId | ||||
|     this.mediaType = session.mediaType | ||||
|     this.episodeId = session.episodeId, | ||||
|       this.mediaType = session.mediaType | ||||
|     this.duration = session.duration | ||||
|     this.playMethod = session.playMethod | ||||
| 
 | ||||
| @ -107,10 +111,11 @@ class PlaybackSession { | ||||
|     return Math.max(0, Math.min(this.currentTime / this.duration, 1)) | ||||
|   } | ||||
| 
 | ||||
|   setData(libraryItem, user) { | ||||
|   setData(libraryItem, user, episodeId = null) { | ||||
|     this.id = getId('play') | ||||
|     this.userId = user.id | ||||
|     this.libraryItemId = libraryItem.id | ||||
|     this.episodeId = episodeId | ||||
|     this.mediaType = libraryItem.mediaType | ||||
|     this.mediaMetadata = libraryItem.media.metadata.clone() | ||||
|     this.coverPath = libraryItem.media.coverPath | ||||
|  | ||||
| @ -9,12 +9,13 @@ const hlsPlaylistGenerator = require('../utils/hlsPlaylistGenerator') | ||||
| const AudioTrack = require('./files/AudioTrack') | ||||
| 
 | ||||
| class Stream extends EventEmitter { | ||||
|   constructor(sessionId, streamPath, user, libraryItem, startTime, clientEmitter, transcodeOptions = {}) { | ||||
|   constructor(sessionId, streamPath, user, libraryItem, episodeId, startTime, clientEmitter, transcodeOptions = {}) { | ||||
|     super() | ||||
| 
 | ||||
|     this.id = sessionId | ||||
|     this.user = user | ||||
|     this.libraryItem = libraryItem | ||||
|     this.episodeId = episodeId | ||||
|     this.clientEmitter = clientEmitter | ||||
| 
 | ||||
|     this.transcodeOptions = transcodeOptions | ||||
| @ -34,22 +35,28 @@ class Stream extends EventEmitter { | ||||
|     this.isTranscodeComplete = false | ||||
|     this.segmentsCreated = new Set() | ||||
|     this.furthestSegmentCreated = 0 | ||||
|     // this.clientCurrentTime = 0
 | ||||
| 
 | ||||
|     this.init() | ||||
|   } | ||||
| 
 | ||||
|   get isPodcast() { | ||||
|     return this.libraryItem.mediaType === 'podcast' | ||||
|   } | ||||
|   get episode() { | ||||
|     if (!this.isPodcast) return null | ||||
|     return this.libraryItem.media.episodes.find(ep => ep.id === this.episodeId) | ||||
|   } | ||||
|   get libraryItemId() { | ||||
|     return this.libraryItem.id | ||||
|   } | ||||
|   get mediaTitle() { | ||||
|     if (this.episode) return this.episode.title || '' | ||||
|     return this.libraryItem.media.metadata.title || '' | ||||
|   } | ||||
|   get totalDuration() { | ||||
|     if (this.episode) return this.episode.duration | ||||
|     return this.libraryItem.media.duration | ||||
|   } | ||||
|   get tracks() { | ||||
|     // TODO: Podcast episode tracks
 | ||||
|     if (this.episode) return this.episode.tracks | ||||
|     return this.libraryItem.media.tracks | ||||
|   } | ||||
|   get tracksAudioFileType() { | ||||
| @ -99,28 +106,16 @@ class Stream extends EventEmitter { | ||||
|       id: this.id, | ||||
|       userId: this.user.id, | ||||
|       libraryItem: this.libraryItem.toJSONExpanded(), | ||||
|       episode: this.episode ? this.episode.toJSONExpanded() : null, | ||||
|       segmentLength: this.segmentLength, | ||||
|       playlistPath: this.playlistPath, | ||||
|       clientPlaylistUri: this.clientPlaylistUri, | ||||
|       // clientCurrentTime: this.clientCurrentTime,
 | ||||
|       startTime: this.startTime, | ||||
|       segmentStartNumber: this.segmentStartNumber, | ||||
|       isTranscodeComplete: this.isTranscodeComplete, | ||||
|       // lastUpdate: this.clientUserAudiobookData ? this.clientUserAudiobookData.lastUpdate : 0
 | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   init() { | ||||
|     // if (this.clientUserAudiobookData) {
 | ||||
|     //   var timeRemaining = this.totalDuration - this.clientUserAudiobookData.currentTime
 | ||||
|     //   Logger.info('[STREAM] User has progress for item', this.clientUserAudiobookData.progress, `Time Remaining: ${timeRemaining}s`)
 | ||||
|     //   if (timeRemaining > 15) {
 | ||||
|     //     this.startTime = this.clientUserAudiobookData.currentTime
 | ||||
|     //     this.clientCurrentTime = this.startTime
 | ||||
|     //   }
 | ||||
|     // }
 | ||||
|   } | ||||
| 
 | ||||
|   async checkSegmentNumberRequest(segNum) { | ||||
|     var segStartTime = segNum * this.segmentLength | ||||
|     if (this.startTime > segStartTime) { | ||||
|  | ||||
| @ -143,14 +143,20 @@ class Podcast { | ||||
|     return payload || {} | ||||
|   } | ||||
| 
 | ||||
|   checkHasEpisode(episodeId) { | ||||
|     return this.episodes.some(ep => ep.id === episodeId) | ||||
|   } | ||||
| 
 | ||||
|   // Only checks container format
 | ||||
|   checkCanDirectPlay(payload, epsiodeIndex = 0) { | ||||
|     var episode = this.episodes[epsiodeIndex] | ||||
|   checkCanDirectPlay(payload, episodeId) { | ||||
|     var episode = this.episodes.find(ep => ep.id === episodeId) | ||||
|     if (!episode) return false | ||||
|     return episode.checkCanDirectPlay(payload) | ||||
|   } | ||||
| 
 | ||||
|   getDirectPlayTracklist(libraryItemId, episodeIndex = 0) { | ||||
|     var episode = this.episodes[episodeIndex] | ||||
|   getDirectPlayTracklist(libraryItemId, episodeId) { | ||||
|     var episode = this.episodes.find(ep => ep.id === episodeId) | ||||
|     if (!episode) return false | ||||
|     return episode.getDirectPlayTracklist(libraryItemId) | ||||
|   } | ||||
| 
 | ||||
| @ -164,6 +170,15 @@ class Podcast { | ||||
|     this.episodes.push(pe) | ||||
|   } | ||||
| 
 | ||||
|   setEpisodeOrder(episodeIds) { | ||||
|     this.episodes = this.episodes.map(ep => { | ||||
|       var indexOf = episodeIds.findIndex(id => id === ep.id) | ||||
|       ep.index = indexOf + 1 | ||||
|       return ep | ||||
|     }) | ||||
|     this.episodes.sort((a, b) => b.index - a.index) | ||||
|   } | ||||
| 
 | ||||
|   reorderEpisodes() { | ||||
|     var hasUpdates = false | ||||
|     this.episodes = naturalSort(this.episodes).asc((ep) => ep.bestFilename) | ||||
| @ -173,7 +188,12 @@ class Podcast { | ||||
|         hasUpdates = true | ||||
|       } | ||||
|     } | ||||
|     this.episodes.sort((a, b) => b.index - a.index) | ||||
|     return hasUpdates | ||||
|   } | ||||
| 
 | ||||
|   removeEpisode(episodeId) { | ||||
|     this.episodes = this.episodes.filter(ep => ep.id !== episodeId) | ||||
|   } | ||||
| } | ||||
| module.exports = Podcast | ||||
| @ -52,10 +52,10 @@ class MediaProgress { | ||||
|     return !this.isFinished && this.progress > 0 | ||||
|   } | ||||
| 
 | ||||
|   setData(libraryItemId, progress) { | ||||
|   setData(libraryItemId, progress, episodeId = null) { | ||||
|     this.id = libraryItemId | ||||
|     this.libraryItemId = libraryItemId | ||||
|     this.episodeId = progress.episodeId || null | ||||
|     this.episodeId = episodeId | ||||
|     this.duration = progress.duration || 0 | ||||
|     this.progress = Math.min(1, (progress.progress || 0)) | ||||
|     this.currentTime = progress.currentTime || 0 | ||||
| @ -74,11 +74,11 @@ class MediaProgress { | ||||
|     for (const key in payload) { | ||||
|       if (this[key] !== undefined && payload[key] !== this[key]) { | ||||
|         if (key === 'isFinished') { | ||||
|           if (!payload[key]) { // Updating to Not Read - Reset progress and current time
 | ||||
|           if (!payload[key]) { // Updating to Not Finished - Reset progress and current time
 | ||||
|             this.finishedAt = null | ||||
|             this.progress = 0 | ||||
|             this.currentTime = 0 | ||||
|           } else { // Updating to Read
 | ||||
|           } else { // Updating to Finished
 | ||||
|             if (!this.finishedAt) this.finishedAt = Date.now() | ||||
|             this.progress = 1 | ||||
|           } | ||||
| @ -88,6 +88,16 @@ class MediaProgress { | ||||
|         hasUpdates = true | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     if (this.progress >= 1 && !this.isFinished) { | ||||
|       this.isFinished = true | ||||
|       this.finishedAt = Date.now() | ||||
|       this.progress = 1 | ||||
|     } else if (this.progress < 1 && this.isFinished) { | ||||
|       this.isFinished = false | ||||
|       this.finishedAt = null | ||||
|     } | ||||
| 
 | ||||
|     if (!this.startedAt) { | ||||
|       this.startedAt = Date.now() | ||||
|     } | ||||
|  | ||||
| @ -236,17 +236,23 @@ class User { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   getMediaProgress(libraryItemId) { | ||||
|   getMediaProgress(libraryItemId, episodeId = null) { | ||||
|     if (!this.mediaProgress) return null | ||||
|     return this.mediaProgress.find(lip => lip.id === libraryItemId) | ||||
|     return this.mediaProgress.find(lip => { | ||||
|       if (episodeId && lip.episodeId !== episodeId) return false | ||||
|       return lip.id === libraryItemId | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   createUpdateMediaProgress(libraryItem, updatePayload) { | ||||
|     var itemProgress = this.mediaProgress.find(li => li.id === libraryItem.id) | ||||
|   createUpdateMediaProgress(libraryItem, updatePayload, episodeId = null) { | ||||
|     var itemProgress = this.mediaProgress.find(li => { | ||||
|       if (episodeId && li.episodeId !== episodeId) return false | ||||
|       return li.id === libraryItem.id | ||||
|     }) | ||||
|     if (!itemProgress) { | ||||
|       var newItemProgress = new MediaProgress() | ||||
| 
 | ||||
|       newItemProgress.setData(libraryItem.id, updatePayload) | ||||
|       newItemProgress.setData(libraryItem.id, updatePayload, episodeId) | ||||
|       this.mediaProgress.push(newItemProgress) | ||||
|       return true | ||||
|     } | ||||
|  | ||||
| @ -86,7 +86,10 @@ class ApiRouter { | ||||
|     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/play', LibraryItemController.middleware.bind(this), LibraryItemController.startPlaybackSession.bind(this)) | ||||
|     this.router.post('/items/:id/play/:episodeId', LibraryItemController.middleware.bind(this), LibraryItemController.startEpisodePlaybackSession.bind(this)) | ||||
|     this.router.patch('/items/:id/tracks', LibraryItemController.middleware.bind(this), LibraryItemController.updateTracks.bind(this)) | ||||
|     this.router.patch('/items/:id/episodes', LibraryItemController.middleware.bind(this), LibraryItemController.updateEpisodes.bind(this)) | ||||
|     this.router.delete('/items/:id/episode/:episodeId', LibraryItemController.middleware.bind(this), LibraryItemController.removeEpisode.bind(this)) | ||||
|     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)) | ||||
| @ -126,6 +129,7 @@ class ApiRouter { | ||||
|     this.router.get('/me/listening-stats', MeController.getListeningStats.bind(this)) | ||||
|     this.router.patch('/me/progress/:id', MeController.createUpdateMediaProgress.bind(this)) | ||||
|     this.router.delete('/me/progress/:id', MeController.removeMediaProgress.bind(this)) | ||||
|     this.router.patch('/me/progress/:id/:episodeId', MeController.createUpdateEpisodeMediaProgress.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.patch('/me/item/:id/bookmark', MeController.updateBookmark.bind(this)) | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user