From 710a0f23cc190f1ad1ff561ac984785848259b95 Mon Sep 17 00:00:00 2001 From: Jeena Date: Tue, 13 Oct 2026 23:03:58 +0000 Subject: [PATCH] channels: drop the all-channels listing walk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 13-page listing only produced rows that were never rendered (followed-instance groups had no UI consumer) and 12 of its pages were spent finding locals already known. What the cards still show and the video listing cannot carry — banners, exact subscriber counts — comes from one per-channel call per local channel, in six parallel batches. Zero-video local channels are no longer listed: they had nothing on their cards anyway; the signed-in user's own channels stay complete via the account listing. --- .../jeena/jtubetv/data/PeerTubeRepository.kt | 10 -- .../jtubetv/ui/screens/ChannelsScreen.kt | 104 ++++++------------ 2 files changed, 35 insertions(+), 79 deletions(-) diff --git a/app/src/main/java/net/jeena/jtubetv/data/PeerTubeRepository.kt b/app/src/main/java/net/jeena/jtubetv/data/PeerTubeRepository.kt index 2f81d77..b143d8e 100644 --- a/app/src/main/java/net/jeena/jtubetv/data/PeerTubeRepository.kt +++ b/app/src/main/java/net/jeena/jtubetv/data/PeerTubeRepository.kt @@ -158,16 +158,6 @@ class PeerTubeRepository( suspend fun localChannels(start: Long, count: Int, search: String? = null): Result = runCatching { network.api.channels(start = start, count = count, search = search) } - /** - * Plain channel listing (the API has no local-only filter; returns local + - * followed-instance channels). Caller splits by host. - */ - suspend fun channelList(start: Long, count: Int): Result, Long>> = - runCatching { - val response = network.api.channels(search = null, start = start, count = count) - response.data to response.total - } - suspend fun videoChannel(handle: String): Result = runCatching { network.api.videoChannel(handle) } 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 02447c4..302cfbb 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 @@ -117,10 +119,6 @@ class ChannelsViewModel(private val store: TokenStore) : ViewModel() { private var allChannels: List = emptyList() - /** Channels from followed instances, grouped per platform. */ - var followedRows by mutableStateOf>(emptyList()) - private set - private val repo: PeerTubeRepository = AppGraph.repository private val statsMap = HashMap() @@ -167,10 +165,10 @@ class ChannelsViewModel(private val store: TokenStore) : ViewModel() { loadLocalsFromVideos(host) loading = false - // Background: the channel listing only refines — banners, exact - // follower counts, owner accounts for the row headers, the - // zero-video locals, and the followed-instance rows. - refineFromListing(host) + // Background: banners + exact subscriber counts per channel — the + // only card data the video listing cannot carry. Six parallel + // per-channel calls, no listing walk. + refineChannelMeta(host) } catch (e: Exception) { error = e.message ?: "Could not load channels" } finally { @@ -234,53 +232,38 @@ class ChannelsViewModel(private val store: TokenStore) : ViewModel() { } /** - * Streams the full channel listing (thirteen 100-row pages on an instance - * following others — the locals are spread thin across them). Replaces the - * local stubs with full objects (banners + exact subscribers + owner), adds - * the zero-video locals that the video listing cannot see, and builds the - * followed-instance rows. Rebuilds only when a group actually changed. + * Background touch-up: per-channel metadata the video listing cannot carry + * (banners + exact subscriber counts, shown on the cards). Batches of six + * parallel per-channel calls; the rows only repaint when a card's data + * actually changed — sort keys are stat-stable, so no visible churn. */ - private fun refineFromListing(host: String?) { + private fun refineChannelMeta(host: String?) { + val handles = allChannels + .filter { it.host == host && it.host != null } + .filter { it.banners.isEmpty() || it.followersCount == 0L } + .map { it.name + "@" + it.host!! } viewModelScope.launch { - var start = 0L - var total: Long? = null - var retries = 0 - var localsFingerprint = "" - var foreignCount = -1 - while (total == null || start < total) { - val page = repo.channelList(start, 100).getOrNull() - if (page == null) { - retries++ - if (retries > 2) break - delay(500) - continue - } - retries = 0 - if (total == null) total = page.second - val merged = allChannels.toMutableList() - for (c in page.first) { - val existing = merged.indexOfFirst { it.name == c.name && it.host == c.host } - if (existing >= 0) merged[existing] = c - else merged += c - } - allChannels = merged - start += 100 - android.util.Log.w( - "JTube", - "listing page " + (start / 100) + ": total=" + total + " all=" + allChannels.size, - ) - // Full locals: banners/subscribers may have changed → repaint. - val fp = allChannels.filter { it.host == host } - .joinToString("|") { it.name + ":" + it.followersCount } - if (fp != localsFingerprint) { - localsFingerprint = fp - rebuildRows(host) - } - val fc = allChannels.count { it.host != host } - if (fc != foreignCount) { - foreignCount = fc - rebuildRows(host) // rebuilds both; userRows stay key-stable + handles.chunked(6).forEach { batch -> + batch.map { handle -> + async { handle to repo.videoChannel(handle).getOrNull() } + }.awaitAll().filterNotNull().forEach { (handle, full) -> + if (full == null) return@forEach + val merged = allChannels.toMutableList() + val index = merged.indexOfFirst { + it.name + "@" + it.host == handle && it.host == host + } + if (index >= 0 && + (merged[index].banners != full.banners || + merged[index].followersCount != full.followersCount) + ) { + merged[index] = merged[index].copy( + banners = full.banners, + followersCount = full.followersCount, + ) + allChannels = merged + } } + rebuildRows(host) } } } @@ -297,23 +280,6 @@ class ChannelsViewModel(private val store: TokenStore) : ViewModel() { private fun rebuildRowsInner(host: String?) { val locals = allChannels.filter { it.host == host } - val foreign = allChannels.filter { it.host != host } - followedRows = foreign - .groupBy { it.host ?: "?" } - .values - .map { row -> - UserGroup( - ownerName = row.firstOrNull()?.host ?: "?", - label = (platformNames[row.firstOrNull()?.host] - ?: row.firstOrNull()?.host ?: "?") + " (followed)", - avatar = null, - channels = row.sortedWith( - compareByDescending { statOf(it) } - .thenByDescending { it.followersCount }, - ), - ) - } - .sortedByDescending { g -> g.channels.sumOf { it.followersCount } } val ownNames = userChannels.map { c -> c.name }.toSet() val byOwner = locals.groupBy { ownerKey(it) } val groups = byOwner.values.map { row ->