mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
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:
parent
6004687deb
commit
7be4fb5fdf
7 changed files with 156 additions and 131 deletions
|
|
@ -52,56 +52,65 @@ 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,
|
*
|
||||||
isFavorite = favorite,
|
* @param req the [GetItemsRequest]
|
||||||
genreIds = genres,
|
* @param overwriteIncludeTypes whether the includeItemTypes field should be overwritten (used from this) or used as-is from the [GetItemsRequest]
|
||||||
minCommunityRating = minCommunityRating,
|
*
|
||||||
personIds = persons,
|
*/
|
||||||
isPlayed = played,
|
fun applyTo(
|
||||||
studioIds = studios,
|
req: GetItemsRequest,
|
||||||
tags = tags,
|
overwriteIncludeTypes: Boolean = true,
|
||||||
officialRatings = officialRatings,
|
) = req.copy(
|
||||||
years =
|
includeItemTypes = if (overwriteIncludeTypes) includeItemTypes else req.includeItemTypes,
|
||||||
buildSet {
|
isFavorite = favorite,
|
||||||
years?.letNotEmpty(::addAll)
|
genreIds = genres,
|
||||||
decades?.forEach { addAll(it..<(it + 10)) }
|
minCommunityRating = minCommunityRating,
|
||||||
},
|
personIds = persons,
|
||||||
is4k =
|
isPlayed = played,
|
||||||
videoTypes?.letNotEmpty {
|
studioIds = studios,
|
||||||
videoTypes.contains(FilterVideoType.FOUR_K).takeIf { it }
|
tags = tags,
|
||||||
},
|
officialRatings = officialRatings,
|
||||||
isHd =
|
years =
|
||||||
videoTypes?.letNotEmpty {
|
buildSet {
|
||||||
if (videoTypes.contains(FilterVideoType.HD)) {
|
years?.letNotEmpty(::addAll)
|
||||||
true
|
decades?.forEach { addAll(it..<(it + 10)) }
|
||||||
} else if (videoTypes.contains(FilterVideoType.SD)) {
|
},
|
||||||
false
|
is4k =
|
||||||
} else {
|
videoTypes?.letNotEmpty {
|
||||||
null
|
videoTypes.contains(FilterVideoType.FOUR_K).takeIf { it }
|
||||||
}
|
},
|
||||||
},
|
isHd =
|
||||||
is3d =
|
videoTypes?.letNotEmpty {
|
||||||
videoTypes?.letNotEmpty {
|
if (videoTypes.contains(FilterVideoType.HD)) {
|
||||||
videoTypes.contains(FilterVideoType.THREE_D).takeIf { it }
|
true
|
||||||
},
|
} else if (videoTypes.contains(FilterVideoType.SD)) {
|
||||||
videoTypes =
|
false
|
||||||
videoTypes?.letNotEmpty {
|
} else {
|
||||||
it.mapNotNull {
|
null
|
||||||
when (it) {
|
}
|
||||||
FilterVideoType.FOUR_K,
|
},
|
||||||
FilterVideoType.HD,
|
is3d =
|
||||||
FilterVideoType.SD,
|
videoTypes?.letNotEmpty {
|
||||||
FilterVideoType.THREE_D,
|
videoTypes.contains(FilterVideoType.THREE_D).takeIf { it }
|
||||||
-> null
|
},
|
||||||
|
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.BLU_RAY -> VideoType.BLU_RAY
|
||||||
FilterVideoType.DVD -> VideoType.DVD
|
FilterVideoType.DVD -> VideoType.DVD
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
},
|
||||||
|
)
|
||||||
|
|
||||||
fun applyTo(req: GetPersonsRequest) =
|
fun applyTo(req: GetPersonsRequest) =
|
||||||
req.copy(
|
req.copy(
|
||||||
|
|
|
||||||
|
|
@ -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,26 +94,30 @@ 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(
|
||||||
GetItemsRequest(
|
overwriteIncludeTypes = false,
|
||||||
parentId = item.id,
|
req =
|
||||||
enableImageTypes = listOf(ImageType.PRIMARY, ImageType.THUMB),
|
GetItemsRequest(
|
||||||
includeItemTypes = includeItemTypes,
|
parentId = item.id,
|
||||||
recursive = true,
|
enableImageTypes = listOf(ImageType.PRIMARY, ImageType.THUMB),
|
||||||
excludeItemIds = listOf(item.id),
|
includeItemTypes = includeItemTypes,
|
||||||
sortBy = sortAndDirection?.let { listOf(sortAndDirection.sort) },
|
recursive = true,
|
||||||
sortOrder = sortAndDirection?.let { listOf(sortAndDirection.direction) },
|
excludeItemIds = listOf(item.id),
|
||||||
fields = DefaultItemFields,
|
sortBy = sortAndDirection?.let { listOf(sortAndDirection.sort) },
|
||||||
startIndex = startIndex,
|
sortOrder = sortAndDirection?.let { listOf(sortAndDirection.direction) },
|
||||||
limit = Playlist.MAX_SIZE,
|
fields = DefaultItemFields,
|
||||||
),
|
startIndex = startIndex,
|
||||||
|
limit = Playlist.MAX_SIZE,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
val items =
|
val items =
|
||||||
GetItemsRequestHandler.execute(api, request).content.items.map {
|
GetItemsRequestHandler.execute(api, request).content.items.map {
|
||||||
|
|
@ -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,
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -95,50 +95,57 @@ fun TvGuideGrid(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
TvGuideGridContent(
|
if (channels.isEmpty()) {
|
||||||
loading = state is LoadingState.Loading,
|
ErrorMessage("Live TV is enabled, but no channels were found.", null)
|
||||||
channels = channels,
|
} else {
|
||||||
programs = programs,
|
TvGuideGridContent(
|
||||||
channelProgramCount = viewModel.channelProgramCount,
|
loading = state is LoadingState.Loading,
|
||||||
start = viewModel.guideStart,
|
channels = channels,
|
||||||
onClickChannel = { index, channel ->
|
programs = programs,
|
||||||
viewModel.navigationManager.navigateTo(
|
channelProgramCount = viewModel.channelProgramCount,
|
||||||
Destination.Playback(
|
start = viewModel.guideStart,
|
||||||
itemId = channel.id,
|
onClickChannel = { index, channel ->
|
||||||
positionMs = 0L,
|
viewModel.navigationManager.navigateTo(
|
||||||
),
|
Destination.Playback(
|
||||||
)
|
itemId = channel.id,
|
||||||
},
|
positionMs = 0L,
|
||||||
onFocus = {
|
),
|
||||||
onRowPosition.invoke(it.row)
|
)
|
||||||
viewModel.onFocusChannel(it)
|
},
|
||||||
},
|
onFocus = {
|
||||||
onClickProgram = { index, program ->
|
onRowPosition.invoke(it.row)
|
||||||
if (program.isFake) {
|
viewModel.onFocusChannel(it)
|
||||||
val now = LocalDateTime.now()
|
},
|
||||||
if (now.isAfter(program.start) && now.isBefore(program.end)) {
|
onClickProgram = { index, program ->
|
||||||
viewModel.navigationManager.navigateTo(
|
if (program.isFake) {
|
||||||
Destination.Playback(
|
val now = LocalDateTime.now()
|
||||||
itemId = program.channelId,
|
if (now.isAfter(program.start) && now.isBefore(program.end)) {
|
||||||
positionMs = 0L,
|
viewModel.navigationManager.navigateTo(
|
||||||
),
|
Destination.Playback(
|
||||||
)
|
itemId = program.channelId,
|
||||||
|
positionMs = 0L,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Toast
|
||||||
|
.makeText(
|
||||||
|
context,
|
||||||
|
"No guide data found!",
|
||||||
|
Toast.LENGTH_SHORT,
|
||||||
|
).show()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Toast
|
viewModel.getItem(program.id)
|
||||||
.makeText(context, "No guide data found!", Toast.LENGTH_SHORT)
|
showItemDialog = index
|
||||||
.show()
|
|
||||||
}
|
}
|
||||||
} else {
|
},
|
||||||
viewModel.getItem(program.id)
|
modifier =
|
||||||
showItemDialog = index
|
Modifier
|
||||||
}
|
.fillMaxSize()
|
||||||
},
|
.background(MaterialTheme.colorScheme.surface)
|
||||||
modifier =
|
.focusRequester(focusRequester),
|
||||||
Modifier
|
)
|
||||||
.fillMaxSize()
|
}
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
|
||||||
.focusRequester(focusRequester),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if (showItemDialog != null) {
|
if (showItemDialog != null) {
|
||||||
val onDismissRequest = { showItemDialog = null }
|
val onDismissRequest = { showItemDialog = null }
|
||||||
|
|
|
||||||
|
|
@ -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 ->
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue