From 0610cca7b1c0b4b2cf97c898b2f776f8bf978dea Mon Sep 17 00:00:00 2001 From: Vito0912 <86927734+Vito0912@users.noreply.github.com> Date: Thu, 25 Sep 2025 09:43:38 +0200 Subject: [PATCH] Add autoplay for next book in series after queue ends --- .../components/app/MediaPlayerContainer.vue | 90 ++++++++++++++----- server/controllers/SeriesController.js | 24 +++++ server/routers/ApiRouter.js | 1 + 3 files changed, 93 insertions(+), 22 deletions(-) diff --git a/client/components/app/MediaPlayerContainer.vue b/client/components/app/MediaPlayerContainer.vue index 1a2b1d30a..8de81d8c7 100644 --- a/client/components/app/MediaPlayerContainer.vue +++ b/client/components/app/MediaPlayerContainer.vue @@ -183,29 +183,75 @@ export default { methods: { mediaFinished(libraryItemId, episodeId) { // Play next item in queue - if (!this.playerQueueItems.length || !this.$store.state.playerQueueAutoPlay) { - // TODO: Set media finished flag so play button will play next queue item - return - } - var currentQueueIndex = this.playerQueueItems.findIndex((i) => { - if (episodeId) return i.libraryItemId === libraryItemId && i.episodeId === episodeId - return i.libraryItemId === libraryItemId - }) - if (currentQueueIndex < 0) { - console.error('Media finished not found in queue - using first in queue', this.playerQueueItems) - currentQueueIndex = -1 - } - if (currentQueueIndex === this.playerQueueItems.length - 1) { - console.log('Finished last item in queue') - return - } - const nextItemInQueue = this.playerQueueItems[currentQueueIndex + 1] - if (nextItemInQueue) { - this.playLibraryItem({ - libraryItemId: nextItemInQueue.libraryItemId, - episodeId: nextItemInQueue.episodeId || null, - queueItems: this.playerQueueItems + if (this.playerQueueItems.length && this.$store.state.playerQueueAutoPlay) { + var currentQueueIndex = this.playerQueueItems.findIndex((i) => { + if (episodeId) return i.libraryItemId === libraryItemId && i.episodeId === episodeId + return i.libraryItemId === libraryItemId }) + if (currentQueueIndex < 0) { + console.error('Media finished not found in queue - using first in queue', this.playerQueueItems) + currentQueueIndex = -1 + } + if (currentQueueIndex < this.playerQueueItems.length - 1) { + const nextItemInQueue = this.playerQueueItems[currentQueueIndex + 1] + if (nextItemInQueue) { + this.playLibraryItem({ + libraryItemId: nextItemInQueue.libraryItemId, + episodeId: nextItemInQueue.episodeId || null, + queueItems: this.playerQueueItems + }) + return + } + } + } + + // If no queue or queue is finished, try to play next book in series + if (this.$store.state.playerQueueAutoPlay && !episodeId && this.streamLibraryItem && this.streamLibraryItem.mediaType === 'book') { + this.checkAndPlayNextInSeries(libraryItemId) + } + }, + + async checkAndPlayNextInSeries(libraryItemId) { + const libraryItem = this.streamLibraryItem + if (!libraryItem || !libraryItem.media.metadata.series || !libraryItem.media.metadata.series.length) { + console.log('No series found for library item') + return + } + + // Try to find next book in each series this book belongs to + for (const series of libraryItem.media.metadata.series) { + try { + const nextBook = await this.$axios.$get(`/api/series/${series.id}/next-book/${libraryItemId}`) + if (nextBook) { + console.log(`Playing next book in series "${series.name}":`, nextBook.media.metadata.title) + + const queueItem = { + libraryItemId: nextBook.id, + libraryId: nextBook.libraryId, + episodeId: null, + title: nextBook.media.metadata.title || 'Unknown Title', + subtitle: (nextBook.media.metadata.authors || []).map((au) => au.name || 'Unknown Author').join(', '), + caption: series.name + (series.sequence ? ` #${series.sequence}` : ''), + duration: nextBook.media.duration || null, + coverPath: nextBook.media.coverPath || null + } + + this.playLibraryItem({ + libraryItemId: nextBook.id, + episodeId: null, + queueItems: [queueItem] + }) + return + } + } catch (error) { + if (error.response?.status === 404) { + console.log(`No next book found in series "${series.name}"`) + continue + } else { + console.error('Error getting next book in series:', error) + continue + } + } } }, setPlaying(isPlaying) { diff --git a/server/controllers/SeriesController.js b/server/controllers/SeriesController.js index 21c93f332..ba86571f5 100644 --- a/server/controllers/SeriesController.js +++ b/server/controllers/SeriesController.js @@ -86,6 +86,30 @@ class SeriesController { res.json(req.series.toOldJSON()) } + /** + * GET: /api/series/:id/next-book/:libraryItemId + * Get the next book in a series after the given library item + * + * @param {SeriesControllerRequest} req + * @param {Response} res + */ + async getNextBook(req, res) { + const currentLibraryItemId = req.params.libraryItemId + const libraryItems = req.libraryItemsInSeries + + const currentBookIndex = libraryItems.findIndex((li) => li.id === currentLibraryItemId) + if (currentBookIndex === -1) { + return res.status(404).send('Current book not found in series') + } + + if (currentBookIndex >= libraryItems.length - 1) { + return res.status(404).send('No next book in series') + } + + const nextBook = libraryItems[currentBookIndex + 1] + res.json(nextBook.toOldJSONExpanded()) + } + /** * * @param {RequestWithUser} req diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 6446ecc80..71070484b 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -222,6 +222,7 @@ class ApiRouter { // this.router.get('/series/:id', SeriesController.middleware.bind(this), SeriesController.findOne.bind(this)) this.router.patch('/series/:id', SeriesController.middleware.bind(this), SeriesController.update.bind(this)) + this.router.get('/series/:id/next-book/:libraryItemId', SeriesController.middleware.bind(this), SeriesController.getNextBook.bind(this)) // // Playback Session Routes