Load all channel EPG titles before showing the TV channel list.
Prefetch now-playing titles for every channel when a category opens so program info is visible immediately, not only on row focus. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
9377c9fa0e
commit
522cd00e83
4 changed files with 65 additions and 13 deletions
|
|
@ -12,8 +12,8 @@ android {
|
|||
applicationId = "nl.vonas.mediacluster.tv"
|
||||
minSdk = 24
|
||||
targetSdk = 35
|
||||
versionCode = 39
|
||||
versionName = "0.11.7"
|
||||
versionCode = 40
|
||||
versionName = "0.11.8"
|
||||
buildConfigField("String", "DEFAULT_API_BASE", "\"https://master.vonas.nl\"")
|
||||
ndk {
|
||||
abiFilters += listOf("arm64-v8a", "armeabi-v7a")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
package nl.vonas.mediacluster.tv.data
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
|
|
@ -338,6 +341,31 @@ class ApiClient(
|
|||
}
|
||||
}
|
||||
|
||||
/** Vul ontbrekende now-titels aan (server-batch of parallel per zender). */
|
||||
suspend fun fetchMissingNowTitles(channels: List<IptvChannel>): Map<Int, String> {
|
||||
val result = linkedMapOf<Int, String>()
|
||||
channels.forEach { ch ->
|
||||
ch.nowTitle?.takeIf { it.isNotBlank() }?.let { result[ch.streamId] = it }
|
||||
}
|
||||
val missing = channels.filter { !result.containsKey(it.streamId) }
|
||||
if (missing.isEmpty()) return result
|
||||
|
||||
coroutineScope {
|
||||
missing.chunked(10).forEach { batch ->
|
||||
batch.map { ch ->
|
||||
async(Dispatchers.IO) {
|
||||
runCatching {
|
||||
iptvEpg(ch.streamId).now?.title?.takeIf { it.isNotBlank() }
|
||||
}.getOrNull()?.let { ch.streamId to it }
|
||||
}
|
||||
}.awaitAll().forEach { pair ->
|
||||
pair?.let { (id, title) -> result[id] = title }
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun JSONObject.toEpgProgram(): EpgProgram? {
|
||||
val title = optString("title").ifBlank { return null }
|
||||
return EpgProgram(
|
||||
|
|
|
|||
|
|
@ -183,9 +183,9 @@ fun LiveTvPlayerScreen(
|
|||
}
|
||||
}
|
||||
|
||||
fun applyMenuEpg(channels: List<IptvChannel>) {
|
||||
for (ch in channels) {
|
||||
ch.nowTitle?.let { menuEpgNow[ch.streamId] = it }
|
||||
fun applyMenuEpg(channels: List<IptvChannel>, titles: Map<Int, String>) {
|
||||
for ((id, title) in titles) {
|
||||
menuEpgNow[id] = title
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -203,8 +203,10 @@ fun LiveTvPlayerScreen(
|
|||
api.iptvChannels(includeEpg = true)
|
||||
}
|
||||
if (gen != menuLoadGen) return@LaunchedEffect
|
||||
val titles = api.fetchMissingNowTitles(loaded)
|
||||
if (gen != menuLoadGen) return@LaunchedEffect
|
||||
menuChannels = loaded
|
||||
applyMenuEpg(loaded)
|
||||
applyMenuEpg(loaded, titles)
|
||||
} catch (_: Exception) {
|
||||
if (gen == menuLoadGen) menuChannels = emptyList()
|
||||
} finally {
|
||||
|
|
@ -658,7 +660,7 @@ private fun LiveChannelSwitcher(
|
|||
)
|
||||
}
|
||||
Text(
|
||||
epgNow[ch.streamId].orEmpty(),
|
||||
epgNow[ch.streamId] ?: ch.nowTitle.orEmpty(),
|
||||
color = McColors.TextDim,
|
||||
fontSize = 12.sp,
|
||||
maxLines = 1,
|
||||
|
|
|
|||
|
|
@ -118,10 +118,17 @@ fun TvScreen(
|
|||
loadingChannels = true
|
||||
focusedEpg = null
|
||||
focusedStreamId = null
|
||||
epgNowTitles.clear()
|
||||
try {
|
||||
channels = api.iptvChannels(catId, includeEpg = true)
|
||||
for (ch in channels) {
|
||||
ch.nowTitle?.let { epgNowTitles[ch.streamId] = it }
|
||||
val loaded = api.iptvChannels(catId, includeEpg = true)
|
||||
val titles = api.fetchMissingNowTitles(loaded)
|
||||
channels = loaded
|
||||
titles.forEach { (id, title) -> epgNowTitles[id] = title }
|
||||
val focusId = lastStreamId?.takeIf { id -> loaded.any { it.streamId == id } }
|
||||
?: loaded.firstOrNull()?.streamId
|
||||
focusId?.let { id ->
|
||||
focusedStreamId = id
|
||||
runCatching { focusedEpg = api.iptvEpg(id) }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
error = e.message
|
||||
|
|
@ -131,6 +138,18 @@ fun TvScreen(
|
|||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(selectedCategoryId, channels, loadingChannels, focusInList) {
|
||||
if (!focusInList || loadingChannels || channels.isEmpty()) return@LaunchedEffect
|
||||
delay(100)
|
||||
val targetId = lastStreamId?.takeIf { id -> channels.any { it.streamId == id } }
|
||||
?: channels.first().streamId
|
||||
val idx = channels.indexOfFirst { it.streamId == targetId }.coerceAtLeast(0)
|
||||
channelListState.scrollToItem(idx)
|
||||
delay(80)
|
||||
channelFocus[targetId]?.let { runCatching { it.requestFocus() } }
|
||||
focusedStreamId = targetId
|
||||
}
|
||||
|
||||
LaunchedEffect(focusContentNonce, loading, categories, channels, selectedCategoryId, lastStreamId) {
|
||||
if (loading || categories.isEmpty()) return@LaunchedEffect
|
||||
delay(120)
|
||||
|
|
@ -201,7 +220,10 @@ fun TvScreen(
|
|||
selectedId = selectedCategoryId,
|
||||
listState = categoryRowState,
|
||||
categoryFocus = categoryFocus,
|
||||
onSelect = { onCategoryChange(it) },
|
||||
onSelect = {
|
||||
onCategoryChange(it)
|
||||
focusInList = true
|
||||
},
|
||||
onFocused = {
|
||||
focusInList = false
|
||||
onContentFocused()
|
||||
|
|
@ -231,7 +253,7 @@ fun TvScreen(
|
|||
when {
|
||||
loadingChannels -> {
|
||||
Box(Modifier.weight(1f), contentAlignment = Alignment.Center) {
|
||||
Text("Zenders laden…", color = McColors.TextDim, fontSize = 14.sp)
|
||||
Text("Zenders en gids laden…", color = McColors.TextDim, fontSize = 14.sp)
|
||||
}
|
||||
}
|
||||
channels.isEmpty() -> {
|
||||
|
|
@ -250,7 +272,7 @@ fun TvScreen(
|
|||
val fr = channelFocus.getOrPut(ch.streamId) { FocusRequester() }
|
||||
ChannelRow(
|
||||
channel = ch,
|
||||
nowTitle = ch.nowTitle ?: epgNowTitles[ch.streamId],
|
||||
nowTitle = epgNowTitles[ch.streamId] ?: ch.nowTitle,
|
||||
isFocused = focusedStreamId == ch.streamId,
|
||||
isLastWatched = ch.streamId == lastStreamId,
|
||||
modifier = Modifier.focusRequester(fr),
|
||||
|
|
|
|||
Loading…
Reference in a new issue