channels: Parallelize stats and persist them

The channels screen enumerated every channel and then fetched each
channel's stats strictly one after another, with a row rebuild and a
row re-save after every single one — with several dozen local channels,
each stat call walking up to four video pages, that took minutes.
Stats now fetch in parallel batches of six and are persisted alongside
the rows snapshot, so an existing install paints the channels screen
with full stats instantly.

Closes #9
This commit is contained in:
Jeena 2026-10-13 08:29:43 +00:00
parent 01c08e1dbb
commit 3d5fd44a59
2 changed files with 54 additions and 10 deletions

View file

@ -67,6 +67,30 @@ object Snapshots {
write(context, "chan-" + handle, json.encodeToString(VideoChannelDto.serializer(), channel))
}
fun saveChannelStats(context: Context, baseUrlKey: String, stats: Map<String, net.jeena.jtubetv.data.api.ChannelStats>) = runCatching {
write(
context,
"chanstats-" + baseUrlKey,
json.encodeToString(
kotlinx.serialization.serializer<Map<String, net.jeena.jtubetv.data.api.ChannelStats>>(),
stats,
),
)
}
fun loadChannelStats(
context: Context,
baseUrlKey: String,
): Map<String, net.jeena.jtubetv.data.api.ChannelStats> =
read(context, "chanstats-" + baseUrlKey)?.let { raw ->
runCatching {
json.decodeFromString(
kotlinx.serialization.serializer<Map<String, net.jeena.jtubetv.data.api.ChannelStats>>(),
raw,
)
}.getOrNull()
} ?: emptyMap()
fun saveContinue(context: Context, key: String, items: List<net.jeena.jtubetv.ui.screens.ContinueItem>) = runCatching {
write(
context,

View file

@ -50,6 +50,8 @@ import androidx.tv.material3.Card
import androidx.tv.material3.CardDefaults
import coil.compose.AsyncImage
import android.util.Log
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import net.jeena.jtubetv.AppGraph
@ -125,11 +127,16 @@ class ChannelsViewModel(private val store: TokenStore) : ViewModel() {
init {
// Snapshot restore (issue #9): paint the last session's channel rows
// instantly; the streaming reload below then updates stats in place.
// instantly, including the per-channel stats (the "minutes of loading"
// complaint) — the streaming reload below then updates them in place.
if (snapshotRows != null) {
userRows = snapshotRows
loading = false
}
if (snapshotContext != null) {
statsMap.putAll(Snapshots.loadChannelStats(snapshotContext, snapshotKey))
if (statsMap.isNotEmpty()) stats = statsMap.toMap()
}
}
@ -188,19 +195,32 @@ class ChannelsViewModel(private val store: TokenStore) : ViewModel() {
}
}
/** Fetch stats for [channels], re-sorting rows as data lands. */
/**
* Fetch stats for [channels] in parallel batches of 6 (each stat call walks
* up to 4 video pages — strictly sequential with ~40 channels took minutes),
* re-sorting the rows whenever a batch lands. Persisted per batch so the
* next start starts warm.
*/
private fun channelStatsLoop(channels: List<VideoChannelDto>, host: String?) {
val missing = channels.filter { !statsMap.containsKey(it.name + "@" + it.host) }
viewModelScope.launch {
for (channel in channels) {
missing.chunked(6).forEach { batch ->
batch.map { channel ->
async {
val key = channel.name + "@" + channel.host
if (statsMap.containsKey(key)) continue
val cs = repo.channelStats(key).getOrNull()
if (cs == null) { android.util.Log.w("JTube", "stats FAIL " + key); continue }
android.util.Log.w("JTube", "stats " + key + " videos=" + cs.videos + " plays=" + cs.plays)
if (cs != null) Pair(key, cs) else null
}
}.awaitAll().filterNotNull().forEach { (key, cs) ->
statsMap[key] = cs
}
stats = statsMap.toMap()
rebuildRows(host)
delay(120)
snapshotContext?.let { ctx ->
kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) {
Snapshots.saveChannelStats(ctx, snapshotKey, statsMap.toMap())
}
}
}
}
}