From 925a4b9d82145834e632f0888d6d4c5b1793d717 Mon Sep 17 00:00:00 2001 From: Ray <154766448+damontecres@users.noreply.github.com> Date: Sun, 21 Dec 2025 21:30:00 -0500 Subject: [PATCH] Add multi-part videos to playback queue (#520) ## Description When playing media with multiple parts, the additional parts will be added to the playback queue. This means a multi-part movie or episode will be play all of the parts in order before the next item in the queue. This only affects the playback queue. There's no UI changes and no indication that a movie or episode has multiple parts. I'll work on that in a separate PR. ### Related issues Related to #455 --- .../wholphin/services/PlaylistCreator.kt | 70 +++++++++++++++---- .../damontecres/wholphin/ui/Extensions.kt | 7 ++ .../wholphin/ui/playback/PlaybackViewModel.kt | 11 +-- 3 files changed, 68 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/PlaylistCreator.kt b/app/src/main/java/com/github/damontecres/wholphin/services/PlaylistCreator.kt index 4d15d414..824342d5 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/PlaylistCreator.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/PlaylistCreator.kt @@ -10,6 +10,7 @@ import com.github.damontecres.wholphin.data.model.PlaylistInfo import com.github.damontecres.wholphin.ui.DefaultItemFields import com.github.damontecres.wholphin.ui.components.baseItemKinds import com.github.damontecres.wholphin.ui.data.SortAndDirection +import com.github.damontecres.wholphin.ui.gt import com.github.damontecres.wholphin.ui.indexOfFirstOrNull import com.github.damontecres.wholphin.ui.playback.playable import com.github.damontecres.wholphin.ui.toServerString @@ -22,6 +23,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineScope import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.playlistsApi +import org.jellyfin.sdk.api.client.extensions.videosApi import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.CreatePlaylistDto @@ -64,10 +66,14 @@ class PlaylistCreator sortBy = if (shuffled) ItemSortBy.RANDOM else null, limit = Playlist.MAX_SIZE, ) - val episodes = GetEpisodesRequestHandler.execute(api, request).content.items + val episodes = + GetEpisodesRequestHandler + .execute(api, request) + .content.items + .convertAndAddParts(false) val startIndex = episodeId?.let { episodes.indexOfFirstOrNull { it.id == episodeId } } ?: 0 - return Playlist(episodes.map { BaseItem.from(it, api) }, startIndex) + return Playlist(episodes, startIndex) } suspend fun createFromPlaylistId( @@ -82,12 +88,11 @@ class PlaylistCreator startIndex = startIndex, limit = Playlist.MAX_SIZE, ) - val items = GetPlaylistItemsRequestHandler.execute(api, request).content.items - var baseItems = items.map { BaseItem.from(it, api) } + var items = GetPlaylistItemsRequestHandler.execute(api, request).content.items if (shuffled) { - baseItems = baseItems.shuffled() + items = items.shuffled() } - return Playlist(baseItems, 0) + return Playlist(items.convertAndAddParts(), 0) } private suspend fun createFromCollection( @@ -120,19 +125,20 @@ class PlaylistCreator ), ) val items = - GetItemsRequestHandler.execute(api, request).content.items.map { - BaseItem.from(it, api) - } + GetItemsRequestHandler + .execute(api, request) + .content.items + .convertAndAddParts() return Playlist(items, 0) } suspend fun createFrom( item: BaseItemDto, startIndex: Int = 0, - sortAndDirection: SortAndDirection?, - shuffled: Boolean, - recursive: Boolean, - filter: GetItemsFilter, + sortAndDirection: SortAndDirection? = SortAndDirection.DEFAULT, + shuffled: Boolean = false, + recursive: Boolean = false, + filter: GetItemsFilter = GetItemsFilter(), ): PlaylistCreationResult = when (item.type) { BaseItemKind.BOX_SET, @@ -166,7 +172,7 @@ class PlaylistCreator ), ) } else { - PlaylistCreationResult.Error(null, "Episode has not seriesId") + PlaylistCreationResult.Error(null, "Episode has no seriesId") } } @@ -182,7 +188,7 @@ class PlaylistCreator ), ) } else { - PlaylistCreationResult.Error(null, "Episode has not seriesId") + PlaylistCreationResult.Error(null, "Episode has no seriesId") } } @@ -207,6 +213,26 @@ class PlaylistCreator ) } + BaseItemKind.MOVIE, + BaseItemKind.VIDEO, + BaseItemKind.MUSIC_VIDEO, + -> { + val list = + buildList { + add(BaseItem(item, false)) + + if (item.partCount.gt(1)) { + api.videosApi + .getAdditionalPart(item.id) + .content.items + .map { + BaseItem(it, false) + }.let(::addAll) + } + } + PlaylistCreationResult.Success(Playlist(list, 0)) + } + // Not support yet // BaseItemKind.AGGREGATE_FOLDER -> TODO() // BaseItemKind.FOLDER -> TODO() @@ -220,6 +246,20 @@ class PlaylistCreator } } + private suspend fun List.convertAndAddParts(useSeriesForPrimary: Boolean = false): List = + buildList { + this@convertAndAddParts.forEach { ep -> + add(BaseItem(ep, useSeriesForPrimary)) + if (ep.partCount.gt(1)) { + val parts = + api.videosApi.getAdditionalPart(ep.id).content.items.map { part -> + BaseItem(part, useSeriesForPrimary) + } + addAll(parts) + } + } + } + suspend fun getServerPlaylists( mediaType: MediaType?, scope: CoroutineScope, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/Extensions.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/Extensions.kt index 93ad9946..bc39c48e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/Extensions.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/Extensions.kt @@ -418,3 +418,10 @@ fun rememberBackDropImage(item: BaseItem): String? { val imageUrlService = LocalImageUrlService.current return remember(item) { imageUrlService.getItemImageUrl(item, ImageType.BACKDROP) } } + +/** + * Check if this, coalescing nulls to zero, is greater than that + */ +fun Int?.gt(that: Int) = (this ?: 0) > that + +fun Int?.lt(that: Int) = (this ?: 0) < that diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt index e462a312..1d380e08 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt @@ -286,11 +286,12 @@ class PlaybackViewModel playNextUp() } - if (!isPlaylist && queriedItem.type == BaseItemKind.EPISODE) { - val playlist = - playlistCreator.createFromEpisode(queriedItem.seriesId!!, queriedItem.id) - withContext(Dispatchers.Main) { - this@PlaybackViewModel.playlist.value = playlist + if (!isPlaylist) { + val result = playlistCreator.createFrom(queriedItem) + if (result is PlaylistCreationResult.Success && result.playlist.items.isNotEmpty()) { + withContext(Dispatchers.Main) { + this@PlaybackViewModel.playlist.value = result.playlist + } } } }