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:
parent
6c64470c28
commit
5ae452155e
7 changed files with 88 additions and 25 deletions
|
|
@ -92,7 +92,12 @@ data class BaseItem(
|
||||||
|
|
||||||
val favorite get() = data.userData?.isFavorite ?: false
|
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
|
* 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) {
|
} else if (type == BaseItemKind.BOX_SET) {
|
||||||
data.productionYear?.let { add(it.toString()) }
|
data.productionYear?.let { add(it.toString()) }
|
||||||
data.childCount?.let { add("$it items") }
|
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 {
|
} else {
|
||||||
data.productionYear?.let { add(it.toString()) }
|
data.productionYear?.let { add(it.toString()) }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.DefaultUserConfiguration
|
||||||
import com.github.damontecres.wholphin.preferences.HomePagePreferences
|
import com.github.damontecres.wholphin.preferences.HomePagePreferences
|
||||||
import com.github.damontecres.wholphin.ui.DefaultItemFields
|
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.SlimItemFields
|
||||||
import com.github.damontecres.wholphin.ui.components.getGenreImageMap
|
import com.github.damontecres.wholphin.ui.components.getGenreImageMap
|
||||||
import com.github.damontecres.wholphin.ui.main.settings.Library
|
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.GetItemsRequestHandler
|
||||||
import com.github.damontecres.wholphin.util.GetLiveTvChannelsRequestHandler
|
import com.github.damontecres.wholphin.util.GetLiveTvChannelsRequestHandler
|
||||||
import com.github.damontecres.wholphin.util.GetPersonsHandler
|
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.GetRecordingsRequestHandler
|
||||||
import com.github.damontecres.wholphin.util.GetStudiosRequestHandler
|
import com.github.damontecres.wholphin.util.GetStudiosRequestHandler
|
||||||
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
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.userApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.userViewsApi
|
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.UUID
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import org.jellyfin.sdk.model.api.CollectionType
|
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.ImageType
|
||||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||||
import org.jellyfin.sdk.model.api.SortOrder
|
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.GetLatestMediaRequest
|
||||||
import org.jellyfin.sdk.model.api.request.GetLiveTvChannelsRequest
|
import org.jellyfin.sdk.model.api.request.GetLiveTvChannelsRequest
|
||||||
import org.jellyfin.sdk.model.api.request.GetPersonsRequest
|
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.GetRecordingsRequest
|
||||||
import org.jellyfin.sdk.model.api.request.GetStudiosRequest
|
import org.jellyfin.sdk.model.api.request.GetStudiosRequest
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
@ -281,7 +284,7 @@ class HomeSettingsService
|
||||||
if (it.collectionType == CollectionType.LIVETV) {
|
if (it.collectionType == CollectionType.LIVETV) {
|
||||||
HomeRowConfigDisplay(
|
HomeRowConfigDisplay(
|
||||||
id = index,
|
id = index,
|
||||||
title = ResStringProvider(R.string.live_tv),
|
title = ResStringProvider(R.string.watch_live),
|
||||||
config = HomeRowConfig.TvPrograms(),
|
config = HomeRowConfig.TvPrograms(),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -367,7 +370,7 @@ class HomeSettingsService
|
||||||
if (userDto.tvAccess) {
|
if (userDto.tvAccess) {
|
||||||
HomeRowConfigDisplay(
|
HomeRowConfigDisplay(
|
||||||
id = id++,
|
id = id++,
|
||||||
title = ResStringProvider(R.string.live_tv),
|
title = ResStringProvider(R.string.watch_live),
|
||||||
config = HomeRowConfig.TvPrograms(),
|
config = HomeRowConfig.TvPrograms(),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -520,7 +523,7 @@ class HomeSettingsService
|
||||||
is HomeRowConfig.TvPrograms -> {
|
is HomeRowConfig.TvPrograms -> {
|
||||||
HomeRowConfigDisplay(
|
HomeRowConfigDisplay(
|
||||||
id = id,
|
id = id,
|
||||||
title = ResStringProvider(R.string.live_tv),
|
title = ResStringProvider(R.string.watch_live),
|
||||||
config,
|
config,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1037,29 +1040,39 @@ class HomeSettingsService
|
||||||
|
|
||||||
is HomeRowConfig.TvPrograms -> {
|
is HomeRowConfig.TvPrograms -> {
|
||||||
val request =
|
val request =
|
||||||
GetRecommendedProgramsRequest(
|
GetProgramsDto(
|
||||||
userId = userDto.id,
|
userId = userDto.id,
|
||||||
fields = DefaultItemFields,
|
fields = ProgramItemFields,
|
||||||
limit = limit,
|
limit = limit,
|
||||||
enableUserData = true,
|
enableUserData = true,
|
||||||
enableImages = true,
|
enableImages = true,
|
||||||
enableImageTypes = listOf(ImageType.PRIMARY, ImageType.LOGO),
|
enableImageTypes = listOf(ImageType.PRIMARY, ImageType.LOGO),
|
||||||
imageTypeLimit = 1,
|
imageTypeLimit = 1,
|
||||||
|
isAiring = true,
|
||||||
|
minEndDate = DateTime.now().plusMinutes(1),
|
||||||
)
|
)
|
||||||
// paging not supported
|
if (usePaging) {
|
||||||
api.liveTvApi
|
ApiRequestPager(
|
||||||
.getRecommendedPrograms(request)
|
api,
|
||||||
.content.items
|
request,
|
||||||
.map { BaseItem(it, row.viewOptions.useSeries) }
|
GetProgramsDtoHandler,
|
||||||
.let {
|
scope,
|
||||||
Success(
|
useSeriesForPrimary = row.viewOptions.useSeries,
|
||||||
ResStringProvider(R.string.live_tv),
|
).init()
|
||||||
it,
|
} else {
|
||||||
row.viewOptions,
|
api.liveTvApi
|
||||||
rowType = row,
|
.getPrograms(request)
|
||||||
showViewMore = it.size >= limit,
|
.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 -> {
|
is HomeRowConfig.TvChannels -> {
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,8 @@ val PhotoItemFields =
|
||||||
ItemFields.HEIGHT,
|
ItemFields.HEIGHT,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val ProgramItemFields = DefaultItemFields + listOf(ItemFields.CHANNEL_INFO)
|
||||||
|
|
||||||
object Cards {
|
object Cards {
|
||||||
const val HEIGHT_2X3_DP = 172
|
const val HEIGHT_2X3_DP = 172
|
||||||
val height2x3 = HEIGHT_2X3_DP.dp
|
val height2x3 = HEIGHT_2X3_DP.dp
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.platform.LocalResources
|
import androidx.compose.ui.platform.LocalResources
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
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.getTimeFormatter
|
||||||
import com.github.damontecres.wholphin.ui.util.LocalClock
|
import com.github.damontecres.wholphin.ui.util.LocalClock
|
||||||
import com.github.damontecres.wholphin.ui.util.LocalInterfaceCustomization
|
import com.github.damontecres.wholphin.ui.util.LocalInterfaceCustomization
|
||||||
|
import org.jellyfin.sdk.model.DateTime
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -37,6 +37,7 @@ fun QuickDetails(
|
||||||
timeRemaining: Duration?,
|
timeRemaining: Duration?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
textStyle: TextStyle = MaterialTheme.typography.titleSmall,
|
textStyle: TextStyle = MaterialTheme.typography.titleSmall,
|
||||||
|
endsAt: DateTime? = null,
|
||||||
) {
|
) {
|
||||||
val enabled = LocalInterfaceCustomization.current.enabledDisplayToggles
|
val enabled = LocalInterfaceCustomization.current.enabledDisplayToggles
|
||||||
val inlineContentMap = rememberQuickDetailsContentMap(textStyle)
|
val inlineContentMap = rememberQuickDetailsContentMap(textStyle)
|
||||||
|
|
@ -53,7 +54,11 @@ fun QuickDetails(
|
||||||
QuickDetailsText(details.criticRating, Modifier, textStyle, inlineContentMap)
|
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,
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -300,6 +300,7 @@ fun SeerrDiscoverPage(
|
||||||
overviewTwoLines = true,
|
overviewTwoLines = true,
|
||||||
quickDetails = details,
|
quickDetails = details,
|
||||||
timeRemaining = null,
|
timeRemaining = null,
|
||||||
|
endsAt = null,
|
||||||
showLogo = preferences.appPreferences.interfacePreferences.showLogos,
|
showLogo = preferences.appPreferences.interfacePreferences.showLogos,
|
||||||
logoImageUrl = null, // TODO
|
logoImageUrl = null, // TODO
|
||||||
modifier =
|
modifier =
|
||||||
|
|
|
||||||
|
|
@ -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.HomeRowLoadingState
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import org.jellyfin.sdk.model.DateTime
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import org.jellyfin.sdk.model.api.MediaType
|
import org.jellyfin.sdk.model.api.MediaType
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
@ -550,6 +551,7 @@ fun HomePageHeader(
|
||||||
overviewTwoLines = isEpisode,
|
overviewTwoLines = isEpisode,
|
||||||
quickDetails = item?.ui?.quickDetails,
|
quickDetails = item?.ui?.quickDetails,
|
||||||
timeRemaining = item?.timeRemainingOrRuntime,
|
timeRemaining = item?.timeRemainingOrRuntime,
|
||||||
|
endsAt = item?.data?.endDate,
|
||||||
showLogo = showLogo,
|
showLogo = showLogo,
|
||||||
logoImageUrl = rememberLogoUrl(item),
|
logoImageUrl = rememberLogoUrl(item),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
|
@ -564,6 +566,7 @@ fun HomePageHeader(
|
||||||
overviewTwoLines: Boolean,
|
overviewTwoLines: Boolean,
|
||||||
quickDetails: QuickDetailsData?,
|
quickDetails: QuickDetailsData?,
|
||||||
timeRemaining: Duration?,
|
timeRemaining: Duration?,
|
||||||
|
endsAt: DateTime?,
|
||||||
showLogo: Boolean,
|
showLogo: Boolean,
|
||||||
logoImageUrl: String?,
|
logoImageUrl: String?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
|
@ -587,7 +590,7 @@ fun HomePageHeader(
|
||||||
if (subtitle != null) {
|
if (subtitle != null) {
|
||||||
EpisodeName(subtitle)
|
EpisodeName(subtitle)
|
||||||
}
|
}
|
||||||
QuickDetails(quickDetails, timeRemaining)
|
QuickDetails(quickDetails, timeRemaining, endsAt = endsAt)
|
||||||
val overviewModifier =
|
val overviewModifier =
|
||||||
Modifier
|
Modifier
|
||||||
.padding(0.dp)
|
.padding(0.dp)
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,7 @@ enum class LibraryRowType(
|
||||||
GENRES(R.string.genres),
|
GENRES(R.string.genres),
|
||||||
STUDIOS(R.string.studios),
|
STUDIOS(R.string.studios),
|
||||||
TV_CHANNELS(R.string.channels),
|
TV_CHANNELS(R.string.channels),
|
||||||
TV_PROGRAMS(R.string.live_tv),
|
TV_PROGRAMS(R.string.watch_live),
|
||||||
RECENTLY_RECORDED(R.string.recently_recorded),
|
RECENTLY_RECORDED(R.string.recently_recorded),
|
||||||
COLLECTION(R.string.collection),
|
COLLECTION(R.string.collection),
|
||||||
PLAYLIST(R.string.playlist),
|
PLAYLIST(R.string.playlist),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue