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
This commit is contained in:
Damontecres 2026-06-03 12:40:53 -04:00 • committed by GitHub
parent 6c64470c28
commit 5ae452155e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 88 additions and 25 deletions

View file

@ -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()) }
}

View file

@ -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 -> {

View file

@ -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

View file

@ -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,
)
}

View file

@ -300,6 +300,7 @@ fun SeerrDiscoverPage(
overviewTwoLines = true,
quickDetails = details,
timeRemaining = null,
endsAt = null,
showLogo = preferences.appPreferences.interfacePreferences.showLogos,
logoImageUrl = null, // TODO
modifier =

View file

@ -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)

View file

@ -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),