From 7be4fb5fdf03f1793765fdb8501c9791806a0442 Mon Sep 17 00:00:00 2001 From: damontecres <154766448+damontecres@users.noreply.github.com> Date: Mon, 24 Nov 2025 11:40:15 -0500 Subject: [PATCH] Fix possible TV guide crash & update Play All libraries (#315) - Fixes a possible crash if the server has Live TV enabled, but no channels - Disable Play All/Shuffle for Collections "library" --- .../wholphin/data/model/GetItemsFilter.kt | 105 ++++++++++-------- .../wholphin/services/PlaylistCreator.kt | 44 ++++---- .../wholphin/ui/data/AddPlaylistViewModel.kt | 6 +- .../ui/detail/CollectionFolderBoxSet.kt | 3 +- .../wholphin/ui/detail/livetv/TvGuideGrid.kt | 91 ++++++++------- .../wholphin/ui/nav/DestinationContent.kt | 11 +- .../wholphin/ui/playback/PlaybackConstants.kt | 27 +++-- 7 files changed, 156 insertions(+), 131 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/GetItemsFilter.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/GetItemsFilter.kt index b7d01423..99d4d38d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/GetItemsFilter.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/GetItemsFilter.kt @@ -52,56 +52,65 @@ data class GetItemsFilter( return newFilter } - fun applyTo(req: GetItemsRequest) = - req.copy( - includeItemTypes = includeItemTypes, - isFavorite = favorite, - genreIds = genres, - minCommunityRating = minCommunityRating, - personIds = persons, - isPlayed = played, - studioIds = studios, - tags = tags, - officialRatings = officialRatings, - years = - buildSet { - years?.letNotEmpty(::addAll) - decades?.forEach { addAll(it..<(it + 10)) } - }, - is4k = - videoTypes?.letNotEmpty { - videoTypes.contains(FilterVideoType.FOUR_K).takeIf { it } - }, - isHd = - videoTypes?.letNotEmpty { - if (videoTypes.contains(FilterVideoType.HD)) { - true - } else if (videoTypes.contains(FilterVideoType.SD)) { - false - } else { - null - } - }, - is3d = - videoTypes?.letNotEmpty { - videoTypes.contains(FilterVideoType.THREE_D).takeIf { it } - }, - videoTypes = - videoTypes?.letNotEmpty { - it.mapNotNull { - when (it) { - FilterVideoType.FOUR_K, - FilterVideoType.HD, - FilterVideoType.SD, - FilterVideoType.THREE_D, - -> null + /** + * Add the filtering from this into the [GetItemsRequest], overwriting the fields + * + * @param req the [GetItemsRequest] + * @param overwriteIncludeTypes whether the includeItemTypes field should be overwritten (used from this) or used as-is from the [GetItemsRequest] + * + */ + fun applyTo( + req: GetItemsRequest, + overwriteIncludeTypes: Boolean = true, + ) = req.copy( + includeItemTypes = if (overwriteIncludeTypes) includeItemTypes else req.includeItemTypes, + isFavorite = favorite, + genreIds = genres, + minCommunityRating = minCommunityRating, + personIds = persons, + isPlayed = played, + studioIds = studios, + tags = tags, + officialRatings = officialRatings, + years = + buildSet { + years?.letNotEmpty(::addAll) + decades?.forEach { addAll(it..<(it + 10)) } + }, + is4k = + videoTypes?.letNotEmpty { + videoTypes.contains(FilterVideoType.FOUR_K).takeIf { it } + }, + isHd = + videoTypes?.letNotEmpty { + if (videoTypes.contains(FilterVideoType.HD)) { + true + } else if (videoTypes.contains(FilterVideoType.SD)) { + false + } else { + null + } + }, + is3d = + videoTypes?.letNotEmpty { + videoTypes.contains(FilterVideoType.THREE_D).takeIf { it } + }, + videoTypes = + videoTypes?.letNotEmpty { + it.mapNotNull { + when (it) { + FilterVideoType.FOUR_K, + FilterVideoType.HD, + FilterVideoType.SD, + FilterVideoType.THREE_D, + -> null - FilterVideoType.BLU_RAY -> VideoType.BLU_RAY - FilterVideoType.DVD -> VideoType.DVD - } + FilterVideoType.BLU_RAY -> VideoType.BLU_RAY + FilterVideoType.DVD -> VideoType.DVD } - }, - ) + } + }, + ) fun applyTo(req: GetPersonsRequest) = req.copy( 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 eaab4e7c..1b7e6d72 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 @@ -11,6 +11,7 @@ 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.indexOfFirstOrNull +import com.github.damontecres.wholphin.ui.playback.playable import com.github.damontecres.wholphin.ui.toServerString import com.github.damontecres.wholphin.util.ApiRequestPager import com.github.damontecres.wholphin.util.GetEpisodesRequestHandler @@ -93,26 +94,30 @@ class PlaylistCreator item: BaseItemDto, startIndex: Int = 0, sortAndDirection: SortAndDirection?, - recursive: Boolean, filter: GetItemsFilter, ): Playlist { val includeItemTypes = - item.collectionType?.baseItemKinds?.takeIf { it.isNotEmpty() } - ?: listOf(BaseItemKind.MOVIE, BaseItemKind.EPISODE, BaseItemKind.VIDEO) + item.collectionType + ?.baseItemKinds + ?.filter { it.playable } + ?.takeIf { it.isNotEmpty() } + ?: BaseItemKind.entries.filter { it.playable } val request = filter.applyTo( - GetItemsRequest( - parentId = item.id, - enableImageTypes = listOf(ImageType.PRIMARY, ImageType.THUMB), - includeItemTypes = includeItemTypes, - recursive = true, - excludeItemIds = listOf(item.id), - sortBy = sortAndDirection?.let { listOf(sortAndDirection.sort) }, - sortOrder = sortAndDirection?.let { listOf(sortAndDirection.direction) }, - fields = DefaultItemFields, - startIndex = startIndex, - limit = Playlist.MAX_SIZE, - ), + overwriteIncludeTypes = false, + req = + GetItemsRequest( + parentId = item.id, + enableImageTypes = listOf(ImageType.PRIMARY, ImageType.THUMB), + includeItemTypes = includeItemTypes, + recursive = true, + excludeItemIds = listOf(item.id), + sortBy = sortAndDirection?.let { listOf(sortAndDirection.sort) }, + sortOrder = sortAndDirection?.let { listOf(sortAndDirection.direction) }, + fields = DefaultItemFields, + startIndex = startIndex, + limit = Playlist.MAX_SIZE, + ), ) val items = GetItemsRequestHandler.execute(api, request).content.items.map { @@ -144,7 +149,6 @@ class PlaylistCreator } else { sortAndDirection }, - recursive = recursive, filter = filter, ), ) @@ -211,7 +215,7 @@ class PlaylistCreator else -> PlaylistCreationResult.Error(null, "Unsupported type: ${item.type}") } - suspend fun getPlaylists( + suspend fun getServerPlaylists( mediaType: MediaType?, scope: CoroutineScope, ): List { @@ -234,7 +238,7 @@ class PlaylistCreator } } - suspend fun createPlaylist( + suspend fun createServerPlaylist( name: String, initialItems: List, ): UUID? = @@ -251,14 +255,14 @@ class PlaylistCreator .toUUIDOrNull() } - suspend fun addToPlaylist( + suspend fun addToServerPlaylist( playlistId: UUID, itemId: UUID, ) { api.playlistsApi.addItemToPlaylist(playlistId, listOf(itemId)) } - suspend fun removeFromPlaylist( + suspend fun removeFromServerPlaylist( playlistId: UUID, itemId: UUID, ) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/data/AddPlaylistViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/data/AddPlaylistViewModel.kt index 555ac5fa..009c4268 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/data/AddPlaylistViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/data/AddPlaylistViewModel.kt @@ -30,7 +30,7 @@ class AddPlaylistViewModel viewModelScope.launchIO { this@AddPlaylistViewModel.playlistState.setValueOnMain(PlaylistLoadingState.Loading) try { - val playlists = playlistCreator.getPlaylists(mediaType, viewModelScope) + val playlists = playlistCreator.getServerPlaylists(mediaType, viewModelScope) this@AddPlaylistViewModel.playlistState.setValueOnMain(PlaylistLoadingState.Success(playlists)) } catch (ex: Exception) { playlistState.setValueOnMain(PlaylistLoadingState.Error(ex)) @@ -43,7 +43,7 @@ class AddPlaylistViewModel itemId: UUID, ) { viewModelScope.launchIO(ExceptionHandler(autoToast = true)) { - playlistCreator.addToPlaylist(playlistId, itemId) + playlistCreator.addToServerPlaylist(playlistId, itemId) } } @@ -52,7 +52,7 @@ class AddPlaylistViewModel itemId: UUID, ) { viewModelScope.launchIO(ExceptionHandler(autoToast = true)) { - val playlistId = playlistCreator.createPlaylist(playlistName, listOf(itemId)) + val playlistId = playlistCreator.createServerPlaylist(playlistName, listOf(itemId)) if (playlistId == null) { showToast(context, "Error creating playlist", Toast.LENGTH_LONG) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderBoxSet.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderBoxSet.kt index 86578340..dc622414 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderBoxSet.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderBoxSet.kt @@ -27,6 +27,7 @@ fun CollectionFolderBoxSet( modifier: Modifier = Modifier, filter: GetItemsFilter = GetItemsFilter(), preferencesViewModel: PreferencesViewModel = hiltViewModel(), + playEnabled: Boolean = false, ) { var showHeader by remember { mutableStateOf(true) } CollectionFolderGrid( @@ -44,6 +45,6 @@ fun CollectionFolderBoxSet( showHeader = position < columns }, params = CollectionFolderGridParameters.POSTER, - playEnabled = true, + playEnabled = playEnabled, ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/TvGuideGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/TvGuideGrid.kt index 50b26c5e..2dc72b8b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/TvGuideGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/TvGuideGrid.kt @@ -95,50 +95,57 @@ fun TvGuideGrid( } } Column(modifier = modifier) { - TvGuideGridContent( - loading = state is LoadingState.Loading, - channels = channels, - programs = programs, - channelProgramCount = viewModel.channelProgramCount, - start = viewModel.guideStart, - onClickChannel = { index, channel -> - viewModel.navigationManager.navigateTo( - Destination.Playback( - itemId = channel.id, - positionMs = 0L, - ), - ) - }, - onFocus = { - onRowPosition.invoke(it.row) - viewModel.onFocusChannel(it) - }, - onClickProgram = { index, program -> - if (program.isFake) { - val now = LocalDateTime.now() - if (now.isAfter(program.start) && now.isBefore(program.end)) { - viewModel.navigationManager.navigateTo( - Destination.Playback( - itemId = program.channelId, - positionMs = 0L, - ), - ) + if (channels.isEmpty()) { + ErrorMessage("Live TV is enabled, but no channels were found.", null) + } else { + TvGuideGridContent( + loading = state is LoadingState.Loading, + channels = channels, + programs = programs, + channelProgramCount = viewModel.channelProgramCount, + start = viewModel.guideStart, + onClickChannel = { index, channel -> + viewModel.navigationManager.navigateTo( + Destination.Playback( + itemId = channel.id, + positionMs = 0L, + ), + ) + }, + onFocus = { + onRowPosition.invoke(it.row) + viewModel.onFocusChannel(it) + }, + onClickProgram = { index, program -> + if (program.isFake) { + val now = LocalDateTime.now() + if (now.isAfter(program.start) && now.isBefore(program.end)) { + viewModel.navigationManager.navigateTo( + Destination.Playback( + itemId = program.channelId, + positionMs = 0L, + ), + ) + } else { + Toast + .makeText( + context, + "No guide data found!", + Toast.LENGTH_SHORT, + ).show() + } } else { - Toast - .makeText(context, "No guide data found!", Toast.LENGTH_SHORT) - .show() + viewModel.getItem(program.id) + showItemDialog = index } - } else { - viewModel.getItem(program.id) - showItemDialog = index - } - }, - modifier = - Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.surface) - .focusRequester(focusRequester), - ) + }, + modifier = + Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surface) + .focusRequester(focusRequester), + ) + } } if (showItemDialog != null) { val onDismissRequest = { showItemDialog = null } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt index dedb3ad0..b3bc2fce 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt @@ -103,11 +103,12 @@ fun DestinationContent( BaseItemKind.BOX_SET -> CollectionFolderBoxSet( - preferences, - destination.itemId, - destination.item, - false, - modifier, + preferences = preferences, + itemId = destination.itemId, + item = destination.item, + recursive = false, + playEnabled = true, + modifier = modifier, ) BaseItemKind.PLAYLIST -> diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackConstants.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackConstants.kt index e579117e..b801e8ee 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackConstants.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackConstants.kt @@ -37,21 +37,24 @@ val PrefContentScale.scale: ContentScale val BaseItemKind.playable: Boolean get() = when (this) { + BaseItemKind.EPISODE, + BaseItemKind.MOVIE, + BaseItemKind.MUSIC_VIDEO, + BaseItemKind.TRAILER, + BaseItemKind.VIDEO, + -> true + + // TODO add support for these eventually + BaseItemKind.LIVE_TV_CHANNEL, + BaseItemKind.LIVE_TV_PROGRAM, + BaseItemKind.PROGRAM, + BaseItemKind.RECORDING, + BaseItemKind.TV_CHANNEL, + BaseItemKind.TV_PROGRAM, BaseItemKind.AUDIO_BOOK, BaseItemKind.AUDIO, BaseItemKind.CHANNEL, - BaseItemKind.EPISODE, - BaseItemKind.MOVIE, - BaseItemKind.LIVE_TV_CHANNEL, - BaseItemKind.LIVE_TV_PROGRAM, - BaseItemKind.MUSIC_VIDEO, - BaseItemKind.PROGRAM, - BaseItemKind.RECORDING, - BaseItemKind.TRAILER, - BaseItemKind.TV_CHANNEL, - BaseItemKind.TV_PROGRAM, - BaseItemKind.VIDEO, - -> true + -> false BaseItemKind.AGGREGATE_FOLDER, BaseItemKind.BASE_PLUGIN_FOLDER,