From 3d5fd44a5960b07c3f42875711f9bae8a6b887d7 Mon Sep 17 00:00:00 2001 From: Jeena Date: Tue, 13 Oct 2026 08:29:43 +0000 Subject: [PATCH] channels: Parallelize stats and persist them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../net/jeena/jtubetv/ui/list/Snapshots.kt | 24 +++++++++++ .../jtubetv/ui/screens/ChannelsScreen.kt | 40 ++++++++++++++----- 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/net/jeena/jtubetv/ui/list/Snapshots.kt b/app/src/main/java/net/jeena/jtubetv/ui/list/Snapshots.kt index f2fc26f..be56817 100644 --- a/app/src/main/java/net/jeena/jtubetv/ui/list/Snapshots.kt +++ b/app/src/main/java/net/jeena/jtubetv/ui/list/Snapshots.kt @@ -67,6 +67,30 @@ object Snapshots { write(context, "chan-" + handle, json.encodeToString(VideoChannelDto.serializer(), channel)) } + fun saveChannelStats(context: Context, baseUrlKey: String, stats: Map) = runCatching { + write( + context, + "chanstats-" + baseUrlKey, + json.encodeToString( + kotlinx.serialization.serializer>(), + stats, + ), + ) + } + + fun loadChannelStats( + context: Context, + baseUrlKey: String, + ): Map = + read(context, "chanstats-" + baseUrlKey)?.let { raw -> + runCatching { + json.decodeFromString( + kotlinx.serialization.serializer>(), + raw, + ) + }.getOrNull() + } ?: emptyMap() + fun saveContinue(context: Context, key: String, items: List) = runCatching { write( context, diff --git a/app/src/main/java/net/jeena/jtubetv/ui/screens/ChannelsScreen.kt b/app/src/main/java/net/jeena/jtubetv/ui/screens/ChannelsScreen.kt index d6f0d2d..6d19403 100644 --- a/app/src/main/java/net/jeena/jtubetv/ui/screens/ChannelsScreen.kt +++ b/app/src/main/java/net/jeena/jtubetv/ui/screens/ChannelsScreen.kt @@ -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, host: String?) { + val missing = channels.filter { !statsMap.containsKey(it.name + "@" + it.host) } viewModelScope.launch { - for (channel in channels) { - 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) - statsMap[key] = cs + missing.chunked(6).forEach { batch -> + batch.map { channel -> + async { + val key = channel.name + "@" + channel.host + val cs = repo.channelStats(key).getOrNull() + 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()) + } + } } } }