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"
This commit is contained in:
damontecres 2025-11-24 11:40:15 -05:00 committed by GitHub
parent 6004687deb
commit 7be4fb5fdf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 156 additions and 131 deletions

View file

@ -52,9 +52,18 @@ data class GetItemsFilter(
return newFilter return newFilter
} }
fun applyTo(req: GetItemsRequest) = /**
req.copy( * Add the filtering from this into the [GetItemsRequest], overwriting the fields
includeItemTypes = includeItemTypes, *
* @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, isFavorite = favorite,
genreIds = genres, genreIds = genres,
minCommunityRating = minCommunityRating, minCommunityRating = minCommunityRating,

View file

@ -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.components.baseItemKinds
import com.github.damontecres.wholphin.ui.data.SortAndDirection import com.github.damontecres.wholphin.ui.data.SortAndDirection
import com.github.damontecres.wholphin.ui.indexOfFirstOrNull 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.ui.toServerString
import com.github.damontecres.wholphin.util.ApiRequestPager import com.github.damontecres.wholphin.util.ApiRequestPager
import com.github.damontecres.wholphin.util.GetEpisodesRequestHandler import com.github.damontecres.wholphin.util.GetEpisodesRequestHandler
@ -93,14 +94,18 @@ class PlaylistCreator
item: BaseItemDto, item: BaseItemDto,
startIndex: Int = 0, startIndex: Int = 0,
sortAndDirection: SortAndDirection?, sortAndDirection: SortAndDirection?,
recursive: Boolean,
filter: GetItemsFilter, filter: GetItemsFilter,
): Playlist { ): Playlist {
val includeItemTypes = val includeItemTypes =
item.collectionType?.baseItemKinds?.takeIf { it.isNotEmpty() } item.collectionType
?: listOf(BaseItemKind.MOVIE, BaseItemKind.EPISODE, BaseItemKind.VIDEO) ?.baseItemKinds
?.filter { it.playable }
?.takeIf { it.isNotEmpty() }
?: BaseItemKind.entries.filter { it.playable }
val request = val request =
filter.applyTo( filter.applyTo(
overwriteIncludeTypes = false,
req =
GetItemsRequest( GetItemsRequest(
parentId = item.id, parentId = item.id,
enableImageTypes = listOf(ImageType.PRIMARY, ImageType.THUMB), enableImageTypes = listOf(ImageType.PRIMARY, ImageType.THUMB),
@ -144,7 +149,6 @@ class PlaylistCreator
} else { } else {
sortAndDirection sortAndDirection
}, },
recursive = recursive,
filter = filter, filter = filter,
), ),
) )
@ -211,7 +215,7 @@ class PlaylistCreator
else -> PlaylistCreationResult.Error(null, "Unsupported type: ${item.type}") else -> PlaylistCreationResult.Error(null, "Unsupported type: ${item.type}")
} }
suspend fun getPlaylists( suspend fun getServerPlaylists(
mediaType: MediaType?, mediaType: MediaType?,
scope: CoroutineScope, scope: CoroutineScope,
): List<PlaylistInfo?> { ): List<PlaylistInfo?> {
@ -234,7 +238,7 @@ class PlaylistCreator
} }
} }
suspend fun createPlaylist( suspend fun createServerPlaylist(
name: String, name: String,
initialItems: List<UUID>, initialItems: List<UUID>,
): UUID? = ): UUID? =
@ -251,14 +255,14 @@ class PlaylistCreator
.toUUIDOrNull() .toUUIDOrNull()
} }
suspend fun addToPlaylist( suspend fun addToServerPlaylist(
playlistId: UUID, playlistId: UUID,
itemId: UUID, itemId: UUID,
) { ) {
api.playlistsApi.addItemToPlaylist(playlistId, listOf(itemId)) api.playlistsApi.addItemToPlaylist(playlistId, listOf(itemId))
} }
suspend fun removeFromPlaylist( suspend fun removeFromServerPlaylist(
playlistId: UUID, playlistId: UUID,
itemId: UUID, itemId: UUID,
) { ) {

View file

@ -30,7 +30,7 @@ class AddPlaylistViewModel
viewModelScope.launchIO { viewModelScope.launchIO {
this@AddPlaylistViewModel.playlistState.setValueOnMain(PlaylistLoadingState.Loading) this@AddPlaylistViewModel.playlistState.setValueOnMain(PlaylistLoadingState.Loading)
try { try {
val playlists = playlistCreator.getPlaylists(mediaType, viewModelScope) val playlists = playlistCreator.getServerPlaylists(mediaType, viewModelScope)
this@AddPlaylistViewModel.playlistState.setValueOnMain(PlaylistLoadingState.Success(playlists)) this@AddPlaylistViewModel.playlistState.setValueOnMain(PlaylistLoadingState.Success(playlists))
} catch (ex: Exception) { } catch (ex: Exception) {
playlistState.setValueOnMain(PlaylistLoadingState.Error(ex)) playlistState.setValueOnMain(PlaylistLoadingState.Error(ex))
@ -43,7 +43,7 @@ class AddPlaylistViewModel
itemId: UUID, itemId: UUID,
) { ) {
viewModelScope.launchIO(ExceptionHandler(autoToast = true)) { viewModelScope.launchIO(ExceptionHandler(autoToast = true)) {
playlistCreator.addToPlaylist(playlistId, itemId) playlistCreator.addToServerPlaylist(playlistId, itemId)
} }
} }
@ -52,7 +52,7 @@ class AddPlaylistViewModel
itemId: UUID, itemId: UUID,
) { ) {
viewModelScope.launchIO(ExceptionHandler(autoToast = true)) { viewModelScope.launchIO(ExceptionHandler(autoToast = true)) {
val playlistId = playlistCreator.createPlaylist(playlistName, listOf(itemId)) val playlistId = playlistCreator.createServerPlaylist(playlistName, listOf(itemId))
if (playlistId == null) { if (playlistId == null) {
showToast(context, "Error creating playlist", Toast.LENGTH_LONG) showToast(context, "Error creating playlist", Toast.LENGTH_LONG)
} }

View file

@ -27,6 +27,7 @@ fun CollectionFolderBoxSet(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
filter: GetItemsFilter = GetItemsFilter(), filter: GetItemsFilter = GetItemsFilter(),
preferencesViewModel: PreferencesViewModel = hiltViewModel(), preferencesViewModel: PreferencesViewModel = hiltViewModel(),
playEnabled: Boolean = false,
) { ) {
var showHeader by remember { mutableStateOf(true) } var showHeader by remember { mutableStateOf(true) }
CollectionFolderGrid( CollectionFolderGrid(
@ -44,6 +45,6 @@ fun CollectionFolderBoxSet(
showHeader = position < columns showHeader = position < columns
}, },
params = CollectionFolderGridParameters.POSTER, params = CollectionFolderGridParameters.POSTER,
playEnabled = true, playEnabled = playEnabled,
) )
} }

View file

@ -95,6 +95,9 @@ fun TvGuideGrid(
} }
} }
Column(modifier = modifier) { Column(modifier = modifier) {
if (channels.isEmpty()) {
ErrorMessage("Live TV is enabled, but no channels were found.", null)
} else {
TvGuideGridContent( TvGuideGridContent(
loading = state is LoadingState.Loading, loading = state is LoadingState.Loading,
channels = channels, channels = channels,
@ -125,8 +128,11 @@ fun TvGuideGrid(
) )
} else { } else {
Toast Toast
.makeText(context, "No guide data found!", Toast.LENGTH_SHORT) .makeText(
.show() context,
"No guide data found!",
Toast.LENGTH_SHORT,
).show()
} }
} else { } else {
viewModel.getItem(program.id) viewModel.getItem(program.id)
@ -140,6 +146,7 @@ fun TvGuideGrid(
.focusRequester(focusRequester), .focusRequester(focusRequester),
) )
} }
}
if (showItemDialog != null) { if (showItemDialog != null) {
val onDismissRequest = { showItemDialog = null } val onDismissRequest = { showItemDialog = null }
ProgramDialog( ProgramDialog(

View file

@ -103,11 +103,12 @@ fun DestinationContent(
BaseItemKind.BOX_SET -> BaseItemKind.BOX_SET ->
CollectionFolderBoxSet( CollectionFolderBoxSet(
preferences, preferences = preferences,
destination.itemId, itemId = destination.itemId,
destination.item, item = destination.item,
false, recursive = false,
modifier, playEnabled = true,
modifier = modifier,
) )
BaseItemKind.PLAYLIST -> BaseItemKind.PLAYLIST ->

View file

@ -37,21 +37,24 @@ val PrefContentScale.scale: ContentScale
val BaseItemKind.playable: Boolean val BaseItemKind.playable: Boolean
get() = get() =
when (this) { 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_BOOK,
BaseItemKind.AUDIO, BaseItemKind.AUDIO,
BaseItemKind.CHANNEL, BaseItemKind.CHANNEL,
BaseItemKind.EPISODE, -> false
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
BaseItemKind.AGGREGATE_FOLDER, BaseItemKind.AGGREGATE_FOLDER,
BaseItemKind.BASE_PLUGIN_FOLDER, BaseItemKind.BASE_PLUGIN_FOLDER,