-
Player Queue
+
{{ $strings.HeaderPlayerQueue }}
{{ playerQueueItems.length }} Items
diff --git a/client/components/player/PlayerUi.vue b/client/components/player/PlayerUi.vue
index 2f5775ef..1f723294 100644
--- a/client/components/player/PlayerUi.vue
+++ b/client/components/player/PlayerUi.vue
@@ -22,15 +22,15 @@
format_list_bulleted
+
+
timelapse
-
-
diff --git a/client/components/tables/ChaptersTable.vue b/client/components/tables/ChaptersTable.vue
index e9c7e39d..d7e616ed 100644
--- a/client/components/tables/ChaptersTable.vue
+++ b/client/components/tables/ChaptersTable.vue
@@ -72,11 +72,23 @@ export default {
this.expanded = !this.expanded
},
goToTimestamp(time) {
+ const queueItem = {
+ libraryItemId: this.libraryItemId,
+ libraryId: this.libraryItem.libraryId,
+ episodeId: null,
+ title: this.metadata.title,
+ subtitle: this.metadata.authors.map((au) => au.name).join(', '),
+ caption: '',
+ duration: this.media.duration || null,
+ coverPath: this.media.coverPath || null
+ }
+
if (this.$store.getters['getIsMediaStreaming'](this.libraryItemId)) {
this.$eventBus.$emit('play-item', {
libraryItemId: this.libraryItemId,
episodeId: null,
- startTime: time
+ startTime: time,
+ queueItems: [queueItem]
})
} else {
const payload = {
@@ -86,7 +98,8 @@ export default {
this.$eventBus.$emit('play-item', {
libraryItemId: this.libraryItemId,
episodeId: null,
- startTime: time
+ startTime: time,
+ queueItems: [queueItem]
})
}
},
diff --git a/client/components/tables/collection/BookTableRow.vue b/client/components/tables/collection/BookTableRow.vue
index 0177b331..b56a7e42 100644
--- a/client/components/tables/collection/BookTableRow.vue
+++ b/client/components/tables/collection/BookTableRow.vue
@@ -137,8 +137,22 @@ export default {
this.isHovering = false
},
playClick() {
+ const queueItems = [
+ {
+ libraryItemId: this.book.id,
+ libraryId: this.book.libraryId,
+ episodeId: null,
+ title: this.bookTitle,
+ subtitle: this.bookAuthors.map((au) => au.name).join(', '),
+ caption: '',
+ duration: this.media.duration || null,
+ coverPath: this.media.coverPath || null
+ }
+ ]
+
this.$eventBus.$emit('play-item', {
- libraryItemId: this.book.id
+ libraryItemId: this.book.id,
+ queueItems
})
},
clickEdit() {
diff --git a/client/pages/collection/_id.vue b/client/pages/collection/_id.vue
index 976583d5..e402cf50 100644
--- a/client/pages/collection/_id.vue
+++ b/client/pages/collection/_id.vue
@@ -122,13 +122,42 @@ export default {
}
},
clickPlay() {
- var nextBookNotRead = this.playableBooks.find((pb) => {
- var prog = this.$store.getters['user/getUserMediaProgress'](pb.id)
- return !prog || !prog.isFinished
+ const queueItems = []
+
+ // Collection queue will start at the first unfinished book
+ // if all books are finished then entire collection is queued
+ const itemsWithProgress = this.playableBooks.map((item) => {
+ return {
+ ...item,
+ progress: this.$store.getters['user/getUserMediaProgress'](item.id)
+ }
})
- if (nextBookNotRead) {
+
+ const hasUnfinishedItems = itemsWithProgress.some((i) => !i.progress || !i.progress.isFinished)
+ if (!hasUnfinishedItems) {
+ console.warn('All items in collection are finished - starting at first item')
+ }
+
+ for (let i = 0; i < itemsWithProgress.length; i++) {
+ const libraryItem = itemsWithProgress[i]
+ if (!hasUnfinishedItems || !libraryItem.progress || !libraryItem.progress.isFinished) {
+ queueItems.push({
+ libraryItemId: libraryItem.id,
+ libraryId: libraryItem.libraryId,
+ episodeId: null,
+ title: libraryItem.media.metadata.title,
+ subtitle: libraryItem.media.metadata.authors.map((au) => au.name).join(', '),
+ caption: '',
+ duration: libraryItem.media.duration || null,
+ coverPath: libraryItem.media.coverPath || null
+ })
+ }
+ }
+
+ if (queueItems.length >= 0) {
this.$eventBus.$emit('play-item', {
- libraryItemId: nextBookNotRead.id
+ libraryItemId: queueItems[0].libraryItemId,
+ queueItems
})
}
}
diff --git a/client/pages/config/sessions.vue b/client/pages/config/sessions.vue
index 3799714b..0ca72da7 100644
--- a/client/pages/config/sessions.vue
+++ b/client/pages/config/sessions.vue
@@ -127,12 +127,38 @@ export default {
this.processingGoToTimestamp = false
return
}
- if (session.episodeId && !libraryItem.media.episodes.find((ep) => ep.id === session.episodeId)) {
+ if (session.episodeId && !libraryItem.media.episodes.some((ep) => ep.id === session.episodeId)) {
this.$toast.error('Failed to get podcast episode')
this.processingGoToTimestamp = false
return
}
+ var queueItem = {}
+ if (session.episodeId) {
+ var episode = libraryItem.media.episodes.find((ep) => ep.id === session.episodeId)
+ queueItem = {
+ libraryItemId: libraryItem.id,
+ libraryId: libraryItem.libraryId,
+ episodeId: episode.id,
+ title: episode.title,
+ subtitle: libraryItem.media.metadata.title,
+ caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date',
+ duration: episode.audioFile.duration || null,
+ coverPath: libraryItem.media.coverPath || null
+ }
+ } else {
+ queueItem = {
+ libraryItemId: libraryItem.id,
+ libraryId: libraryItem.libraryId,
+ episodeId: null,
+ title: libraryItem.media.metadata.title,
+ subtitle: libraryItem.media.metadata.authors.map((au) => au.name).join(', '),
+ caption: '',
+ duration: libraryItem.media.duration || null,
+ coverPath: libraryItem.media.coverPath || null
+ }
+ }
+
const payload = {
message: this.$getString('MessageStartPlaybackAtTime', [session.displayTitle, this.$secondsToTimestamp(session.currentTime)]),
callback: (confirmed) => {
@@ -140,7 +166,8 @@ export default {
this.$eventBus.$emit('play-item', {
libraryItemId: libraryItem.id,
episodeId: session.episodeId || null,
- startTime: session.currentTime
+ startTime: session.currentTime,
+ queueItems: [queueItem]
})
}
this.processingGoToTimestamp = false
diff --git a/client/pages/config/users/_id/sessions.vue b/client/pages/config/users/_id/sessions.vue
index 63544d54..2d2dd4e9 100644
--- a/client/pages/config/users/_id/sessions.vue
+++ b/client/pages/config/users/_id/sessions.vue
@@ -114,12 +114,38 @@ export default {
this.processingGoToTimestamp = false
return
}
- if (session.episodeId && !libraryItem.media.episodes.find((ep) => ep.id === session.episodeId)) {
+ if (session.episodeId && !libraryItem.media.episodes.some((ep) => ep.id === session.episodeId)) {
this.$toast.error('Failed to get podcast episode')
this.processingGoToTimestamp = false
return
}
+ var queueItem = {}
+ if (session.episodeId) {
+ var episode = libraryItem.media.episodes.find((ep) => ep.id === session.episodeId)
+ queueItem = {
+ libraryItemId: libraryItem.id,
+ libraryId: libraryItem.libraryId,
+ episodeId: episode.id,
+ title: episode.title,
+ subtitle: libraryItem.media.metadata.title,
+ caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date',
+ duration: episode.audioFile.duration || null,
+ coverPath: libraryItem.media.coverPath || null
+ }
+ } else {
+ queueItem = {
+ libraryItemId: libraryItem.id,
+ libraryId: libraryItem.libraryId,
+ episodeId: null,
+ title: libraryItem.media.metadata.title,
+ subtitle: libraryItem.media.metadata.authors.map((au) => au.name).join(', '),
+ caption: '',
+ duration: libraryItem.media.duration || null,
+ coverPath: libraryItem.media.coverPath || null
+ }
+ }
+
const payload = {
message: this.$getString('MessageStartPlaybackAtTime', [session.displayTitle, this.$secondsToTimestamp(session.currentTime)]),
callback: (confirmed) => {
@@ -127,7 +153,8 @@ export default {
this.$eventBus.$emit('play-item', {
libraryItemId: libraryItem.id,
episodeId: session.episodeId || null,
- startTime: session.currentTime
+ startTime: session.currentTime,
+ queueItems: [queueItem]
})
}
this.processingGoToTimestamp = false
diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue
index dbd0856a..f0070744 100644
--- a/client/pages/item/_id/index.vue
+++ b/client/pages/item/_id/index.vue
@@ -137,12 +137,16 @@
{{ isMissing ? $strings.LabelMissing : $strings.LabelIncomplete }}
+
+
+
+
auto_stories
{{ $strings.ButtonRead }}
-
+
@@ -398,6 +402,9 @@ export default {
isStreaming() {
return this.streamLibraryItem && this.streamLibraryItem.id === this.libraryItemId
},
+ isQueued() {
+ return this.$store.getters['getIsMediaQueued'](this.libraryItemId)
+ },
userCanUpdate() {
return this.$store.getters['user/getUserCanUpdate']
},
@@ -412,6 +419,10 @@ export default {
// If rss feed is open then show feed url to users otherwise just show to admins
return this.userIsAdminOrUp || this.rssFeedUrl
+ },
+ showQueueBtn() {
+ if (this.isPodcast || this.isVideo) return false
+ return !this.$store.getters['getIsStreamingFromDifferentLibrary'] && this.streamLibraryItem
}
},
methods: {
@@ -536,6 +547,7 @@ export default {
if (!podcastProgress || !podcastProgress.isFinished) {
queueItems.push({
libraryItemId: this.libraryItemId,
+ libraryId: this.libraryId,
episodeId: episode.id,
title: episode.title,
subtitle: this.title,
@@ -545,6 +557,18 @@ export default {
})
}
}
+ } else {
+ const queueItem = {
+ libraryItemId: this.libraryItemId,
+ libraryId: this.libraryId,
+ episodeId: null,
+ title: this.title,
+ subtitle: this.authors.map((au) => au.name).join(', '),
+ caption: '',
+ duration: this.duration || null,
+ coverPath: this.media.coverPath || null
+ }
+ queueItems.push(queueItem)
}
this.$eventBus.$emit('play-item', {
@@ -615,6 +639,26 @@ export default {
console.log('RSS Feed Closed', data)
this.rssFeedUrl = null
}
+ },
+ queueBtnClick() {
+ if (this.isQueued) {
+ // Remove from queue
+ this.$store.commit('removeItemFromQueue', { libraryItemId: this.libraryItemId })
+ } else {
+ // Add to queue
+
+ const queueItem = {
+ libraryItemId: this.libraryItemId,
+ libraryId: this.libraryId,
+ episodeId: null,
+ title: this.title,
+ subtitle: this.authors.map((au) => au.name).join(', '),
+ caption: '',
+ duration: this.duration || null,
+ coverPath: this.media.coverPath || null
+ }
+ this.$store.commit('addItemToQueue', queueItem)
+ }
}
},
mounted() {
diff --git a/client/strings/en-us.json b/client/strings/en-us.json
index 9753c340..07ac9160 100644
--- a/client/strings/en-us.json
+++ b/client/strings/en-us.json
@@ -107,6 +107,7 @@
"HeaderOtherFiles": "Other Files",
"HeaderOpenRSSFeed": "Open RSS Feed",
"HeaderPermissions": "Permissions",
+ "HeaderPlayerQueue": "Player Queue",
"HeaderPodcastsToAdd": "Podcasts to Add",
"HeaderPreviewCover": "Preview Cover",
"HeaderRemoveEpisode": "Remove Episode",