From 5ae452155ed8bc261ab7ba880deaa00b0d5b3e95 Mon Sep 17 00:00:00 2001 From: Damontecres Date: Wed, 3 Jun 2026 12:40:53 -0400 Subject: [PATCH] Fix showing live tv programs on home page (#1504) ## Description Fixes the inconsistent live tv programs shown on the home page by using a different API endpoint & filtering. Also some UI fixes: - Use the "Watch live" string consistently for the row instead of it sometimes switching to "Live TV" - Adjust the "Ends at" in the home page header to show the actual end time - Show the channel name in "quick details" - Show season & episode number, if applicable, in "quick details" ### Related issues Fixes #1080 ### Testing Nvidia shield w/ fake EPG data ## Screenshots N/A, just UI fixes, nothing new ## AI or LLM usage None --- .../wholphin/data/model/BaseItem.kt | 18 ++++++- .../wholphin/services/HomeSettingsService.kt | 53 ++++++++++++------- .../damontecres/wholphin/ui/UiConstants.kt | 2 + .../wholphin/ui/components/QuickDetails.kt | 32 ++++++++++- .../wholphin/ui/discover/SeerrDiscoverPage.kt | 1 + .../damontecres/wholphin/ui/main/HomePage.kt | 5 +- .../main/settings/HomeLibraryRowTypeList.kt | 2 +- 7 files changed, 88 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt index 9d3fdfa1..4b3c248f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt @@ -92,7 +92,12 @@ data class BaseItem( val favorite get() = data.userData?.isFavorite ?: false - val timeRemainingOrRuntime: Duration? get() = data.timeRemaining ?: data.runTimeTicks?.ticks + val timeRemainingOrRuntime: Duration? + get() = + when (type) { + BaseItemKind.PROGRAM -> null + else -> data.timeRemaining ?: data.runTimeTicks?.ticks + } /** * Contains pre computed UI elements that would be expensive to create on the main thread @@ -137,6 +142,17 @@ data class BaseItem( } else if (type == BaseItemKind.BOX_SET) { data.productionYear?.let { add(it.toString()) } data.childCount?.let { add("$it items") } + } else if (type == BaseItemKind.PROGRAM) { + data.channelName?.let(::add) + if (data.isSeries == true) { + // TV episode + data.seasonEpisode?.let(::add) + data.premiereDate?.let { + add(getDateFormatter().format(it)) + } + } else { + data.productionYear?.let { add(it.toString()) } + } } else { data.productionYear?.let { add(it.toString()) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt index da0a7ab6..7a0dcc9b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt @@ -13,6 +13,7 @@ import com.github.damontecres.wholphin.data.model.createStudioDestination import com.github.damontecres.wholphin.preferences.DefaultUserConfiguration import com.github.damontecres.wholphin.preferences.HomePagePreferences import com.github.damontecres.wholphin.ui.DefaultItemFields +import com.github.damontecres.wholphin.ui.ProgramItemFields import com.github.damontecres.wholphin.ui.SlimItemFields import com.github.damontecres.wholphin.ui.components.getGenreImageMap import com.github.damontecres.wholphin.ui.main.settings.Library @@ -30,6 +31,7 @@ import com.github.damontecres.wholphin.util.GetGenresRequestHandler import com.github.damontecres.wholphin.util.GetItemsRequestHandler import com.github.damontecres.wholphin.util.GetLiveTvChannelsRequestHandler import com.github.damontecres.wholphin.util.GetPersonsHandler +import com.github.damontecres.wholphin.util.GetProgramsDtoHandler import com.github.damontecres.wholphin.util.GetRecordingsRequestHandler import com.github.damontecres.wholphin.util.GetStudiosRequestHandler import com.github.damontecres.wholphin.util.HomeRowLoadingState @@ -54,9 +56,11 @@ import org.jellyfin.sdk.api.client.extensions.liveTvApi import org.jellyfin.sdk.api.client.extensions.userApi import org.jellyfin.sdk.api.client.extensions.userLibraryApi import org.jellyfin.sdk.api.client.extensions.userViewsApi +import org.jellyfin.sdk.model.DateTime import org.jellyfin.sdk.model.UUID import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.CollectionType +import org.jellyfin.sdk.model.api.GetProgramsDto import org.jellyfin.sdk.model.api.ImageType import org.jellyfin.sdk.model.api.ItemSortBy import org.jellyfin.sdk.model.api.SortOrder @@ -66,7 +70,6 @@ import org.jellyfin.sdk.model.api.request.GetItemsRequest import org.jellyfin.sdk.model.api.request.GetLatestMediaRequest import org.jellyfin.sdk.model.api.request.GetLiveTvChannelsRequest import org.jellyfin.sdk.model.api.request.GetPersonsRequest -import org.jellyfin.sdk.model.api.request.GetRecommendedProgramsRequest import org.jellyfin.sdk.model.api.request.GetRecordingsRequest import org.jellyfin.sdk.model.api.request.GetStudiosRequest import timber.log.Timber @@ -281,7 +284,7 @@ class HomeSettingsService if (it.collectionType == CollectionType.LIVETV) { HomeRowConfigDisplay( id = index, - title = ResStringProvider(R.string.live_tv), + title = ResStringProvider(R.string.watch_live), config = HomeRowConfig.TvPrograms(), ) } else { @@ -367,7 +370,7 @@ class HomeSettingsService if (userDto.tvAccess) { HomeRowConfigDisplay( id = id++, - title = ResStringProvider(R.string.live_tv), + title = ResStringProvider(R.string.watch_live), config = HomeRowConfig.TvPrograms(), ) } else { @@ -520,7 +523,7 @@ class HomeSettingsService is HomeRowConfig.TvPrograms -> { HomeRowConfigDisplay( id = id, - title = ResStringProvider(R.string.live_tv), + title = ResStringProvider(R.string.watch_live), config, ) } @@ -1037,29 +1040,39 @@ class HomeSettingsService is HomeRowConfig.TvPrograms -> { val request = - GetRecommendedProgramsRequest( + GetProgramsDto( userId = userDto.id, - fields = DefaultItemFields, + fields = ProgramItemFields, limit = limit, enableUserData = true, enableImages = true, enableImageTypes = listOf(ImageType.PRIMARY, ImageType.LOGO), imageTypeLimit = 1, + isAiring = true, + minEndDate = DateTime.now().plusMinutes(1), ) - // paging not supported - api.liveTvApi - .getRecommendedPrograms(request) - .content.items - .map { BaseItem(it, row.viewOptions.useSeries) } - .let { - Success( - ResStringProvider(R.string.live_tv), - it, - row.viewOptions, - rowType = row, - showViewMore = it.size >= limit, - ) - } + if (usePaging) { + ApiRequestPager( + api, + request, + GetProgramsDtoHandler, + scope, + useSeriesForPrimary = row.viewOptions.useSeries, + ).init() + } else { + api.liveTvApi + .getPrograms(request) + .content.items + .map { BaseItem(it, row.viewOptions.useSeries) } + }.let { + Success( + ResStringProvider(R.string.watch_live), + it, + row.viewOptions, + rowType = row, + showViewMore = it.size >= limit, + ) + } } is HomeRowConfig.TvChannels -> { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/UiConstants.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/UiConstants.kt index ab0f69f8..8ec10cea 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/UiConstants.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/UiConstants.kt @@ -83,6 +83,8 @@ val PhotoItemFields = ItemFields.HEIGHT, ) +val ProgramItemFields = DefaultItemFields + listOf(ItemFields.CHANNEL_INFO) + object Cards { const val HEIGHT_2X3_DP = 172 val height2x3 = HEIGHT_2X3_DP.dp diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/QuickDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/QuickDetails.kt index 2c30f337..e9f34133 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/QuickDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/QuickDetails.kt @@ -11,7 +11,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.AnnotatedString @@ -29,6 +28,7 @@ import com.github.damontecres.wholphin.ui.dot import com.github.damontecres.wholphin.ui.getTimeFormatter import com.github.damontecres.wholphin.ui.util.LocalClock import com.github.damontecres.wholphin.ui.util.LocalInterfaceCustomization +import org.jellyfin.sdk.model.DateTime import kotlin.time.Duration @Composable @@ -37,6 +37,7 @@ fun QuickDetails( timeRemaining: Duration?, modifier: Modifier = Modifier, textStyle: TextStyle = MaterialTheme.typography.titleSmall, + endsAt: DateTime? = null, ) { val enabled = LocalInterfaceCustomization.current.enabledDisplayToggles val inlineContentMap = rememberQuickDetailsContentMap(textStyle) @@ -53,7 +54,11 @@ fun QuickDetails( QuickDetailsText(details.criticRating, Modifier, textStyle, inlineContentMap) } } - timeRemaining?.let { TimeRemaining(it, textStyle = textStyle) } + if (timeRemaining != null) { + TimeRemaining(timeRemaining, textStyle = textStyle) + } else if (endsAt != null) { + EndsAt(endsAt, textStyle = textStyle) + } } } @@ -150,3 +155,26 @@ fun TimeRemaining( modifier = modifier, ) } + +@Composable +fun EndsAt( + endsAt: DateTime, + modifier: Modifier = Modifier, + textStyle: TextStyle = MaterialTheme.typography.titleSmall, +) { + val resources = LocalResources.current + val remainingStr = + remember(endsAt, resources) { + val endTimeStr = getTimeFormatter().format(endsAt) + buildAnnotatedString { + dot() + append(resources.getString(R.string.ends_at, endTimeStr)) + } + } + Text( + text = remainingStr, + style = textStyle, + maxLines = 1, + modifier = modifier, + ) +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/discover/SeerrDiscoverPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/discover/SeerrDiscoverPage.kt index 6751a154..5825cc13 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/discover/SeerrDiscoverPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/discover/SeerrDiscoverPage.kt @@ -300,6 +300,7 @@ fun SeerrDiscoverPage( overviewTwoLines = true, quickDetails = details, timeRemaining = null, + endsAt = null, showLogo = preferences.appPreferences.interfacePreferences.showLogos, logoImageUrl = null, // TODO modifier = diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt index 1ccfb429..db76fb3e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt @@ -88,6 +88,7 @@ import com.github.damontecres.wholphin.ui.util.ScrollToTopBringIntoViewSpec import com.github.damontecres.wholphin.util.HomeRowLoadingState import com.github.damontecres.wholphin.util.LoadingState import kotlinx.coroutines.delay +import org.jellyfin.sdk.model.DateTime import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.MediaType import timber.log.Timber @@ -550,6 +551,7 @@ fun HomePageHeader( overviewTwoLines = isEpisode, quickDetails = item?.ui?.quickDetails, timeRemaining = item?.timeRemainingOrRuntime, + endsAt = item?.data?.endDate, showLogo = showLogo, logoImageUrl = rememberLogoUrl(item), modifier = modifier, @@ -564,6 +566,7 @@ fun HomePageHeader( overviewTwoLines: Boolean, quickDetails: QuickDetailsData?, timeRemaining: Duration?, + endsAt: DateTime?, showLogo: Boolean, logoImageUrl: String?, modifier: Modifier = Modifier, @@ -587,7 +590,7 @@ fun HomePageHeader( if (subtitle != null) { EpisodeName(subtitle) } - QuickDetails(quickDetails, timeRemaining) + QuickDetails(quickDetails, timeRemaining, endsAt = endsAt) val overviewModifier = Modifier .padding(0.dp) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeLibraryRowTypeList.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeLibraryRowTypeList.kt index 8c603911..1e5fe4ea 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeLibraryRowTypeList.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeLibraryRowTypeList.kt @@ -136,7 +136,7 @@ enum class LibraryRowType( GENRES(R.string.genres), STUDIOS(R.string.studios), TV_CHANNELS(R.string.channels), - TV_PROGRAMS(R.string.live_tv), + TV_PROGRAMS(R.string.watch_live), RECENTLY_RECORDED(R.string.recently_recorded), COLLECTION(R.string.collection), PLAYLIST(R.string.playlist),