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()) + } + } } } }