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"
|
applicationId = "nl.vonas.mediacluster.tv"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 39
|
versionCode = 40
|
||||||
versionName = "0.11.7"
|
versionName = "0.11.8"
|
||||||
buildConfigField("String", "DEFAULT_API_BASE", "\"https://master.vonas.nl\"")
|
buildConfigField("String", "DEFAULT_API_BASE", "\"https://master.vonas.nl\"")
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters += listOf("arm64-v8a", "armeabi-v7a")
|
abiFilters += listOf("arm64-v8a", "armeabi-v7a")
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
package nl.vonas.mediacluster.tv.data
|
package nl.vonas.mediacluster.tv.data
|
||||||
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.OkHttpClient
|
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? {
|
private fun JSONObject.toEpgProgram(): EpgProgram? {
|
||||||
val title = optString("title").ifBlank { return null }
|
val title = optString("title").ifBlank { return null }
|
||||||
return EpgProgram(
|
return EpgProgram(
|
||||||
|
|
|
||||||
|
|
@ -183,9 +183,9 @@ fun LiveTvPlayerScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun applyMenuEpg(channels: List<IptvChannel>) {
|
fun applyMenuEpg(channels: List<IptvChannel>, titles: Map<Int, String>) {
|
||||||
for (ch in channels) {
|
for ((id, title) in titles) {
|
||||||
ch.nowTitle?.let { menuEpgNow[ch.streamId] = it }
|
menuEpgNow[id] = title
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -203,8 +203,10 @@ fun LiveTvPlayerScreen(
|
||||||
api.iptvChannels(includeEpg = true)
|
api.iptvChannels(includeEpg = true)
|
||||||
}
|
}
|
||||||
if (gen != menuLoadGen) return@LaunchedEffect
|
if (gen != menuLoadGen) return@LaunchedEffect
|
||||||
|
val titles = api.fetchMissingNowTitles(loaded)
|
||||||
|
if (gen != menuLoadGen) return@LaunchedEffect
|
||||||
menuChannels = loaded
|
menuChannels = loaded
|
||||||
applyMenuEpg(loaded)
|
applyMenuEpg(loaded, titles)
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
if (gen == menuLoadGen) menuChannels = emptyList()
|
if (gen == menuLoadGen) menuChannels = emptyList()
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -658,7 +660,7 @@ private fun LiveChannelSwitcher(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
epgNow[ch.streamId].orEmpty(),
|
epgNow[ch.streamId] ?: ch.nowTitle.orEmpty(),
|
||||||
color = McColors.TextDim,
|
color = McColors.TextDim,
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
|
|
|
||||||
|
|
@ -118,10 +118,17 @@ fun TvScreen(
|
||||||
loadingChannels = true
|
loadingChannels = true
|
||||||
focusedEpg = null
|
focusedEpg = null
|
||||||
focusedStreamId = null
|
focusedStreamId = null
|
||||||
|
epgNowTitles.clear()
|
||||||
try {
|
try {
|
||||||
channels = api.iptvChannels(catId, includeEpg = true)
|
val loaded = api.iptvChannels(catId, includeEpg = true)
|
||||||
for (ch in channels) {
|
val titles = api.fetchMissingNowTitles(loaded)
|
||||||
ch.nowTitle?.let { epgNowTitles[ch.streamId] = it }
|
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) {
|
} catch (e: Exception) {
|
||||||
error = e.message
|
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) {
|
LaunchedEffect(focusContentNonce, loading, categories, channels, selectedCategoryId, lastStreamId) {
|
||||||
if (loading || categories.isEmpty()) return@LaunchedEffect
|
if (loading || categories.isEmpty()) return@LaunchedEffect
|
||||||
delay(120)
|
delay(120)
|
||||||
|
|
@ -201,7 +220,10 @@ fun TvScreen(
|
||||||
selectedId = selectedCategoryId,
|
selectedId = selectedCategoryId,
|
||||||
listState = categoryRowState,
|
listState = categoryRowState,
|
||||||
categoryFocus = categoryFocus,
|
categoryFocus = categoryFocus,
|
||||||
onSelect = { onCategoryChange(it) },
|
onSelect = {
|
||||||
|
onCategoryChange(it)
|
||||||
|
focusInList = true
|
||||||
|
},
|
||||||
onFocused = {
|
onFocused = {
|
||||||
focusInList = false
|
focusInList = false
|
||||||
onContentFocused()
|
onContentFocused()
|
||||||
|
|
@ -231,7 +253,7 @@ fun TvScreen(
|
||||||
when {
|
when {
|
||||||
loadingChannels -> {
|
loadingChannels -> {
|
||||||
Box(Modifier.weight(1f), contentAlignment = Alignment.Center) {
|
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() -> {
|
channels.isEmpty() -> {
|
||||||
|
|
@ -250,7 +272,7 @@ fun TvScreen(
|
||||||
val fr = channelFocus.getOrPut(ch.streamId) { FocusRequester() }
|
val fr = channelFocus.getOrPut(ch.streamId) { FocusRequester() }
|
||||||
ChannelRow(
|
ChannelRow(
|
||||||
channel = ch,
|
channel = ch,
|
||||||
nowTitle = ch.nowTitle ?: epgNowTitles[ch.streamId],
|
nowTitle = epgNowTitles[ch.streamId] ?: ch.nowTitle,
|
||||||
isFocused = focusedStreamId == ch.streamId,
|
isFocused = focusedStreamId == ch.streamId,
|
||||||
isLastWatched = ch.streamId == lastStreamId,
|
isLastWatched = ch.streamId == lastStreamId,
|
||||||
modifier = Modifier.focusRequester(fr),
|
modifier = Modifier.focusRequester(fr),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue