channels: drop the all-channels listing walk
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.
This commit is contained in:
parent
29dee24849
commit
710a0f23cc
2 changed files with 35 additions and 79 deletions
|
|
@ -158,16 +158,6 @@ class PeerTubeRepository(
|
||||||
suspend fun localChannels(start: Long, count: Int, search: String? = null): Result<VideoChannelListResponse> =
|
suspend fun localChannels(start: Long, count: Int, search: String? = null): Result<VideoChannelListResponse> =
|
||||||
runCatching { network.api.channels(start = start, count = count, search = search) }
|
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<Pair<List<VideoChannelDto>, Long>> =
|
|
||||||
runCatching {
|
|
||||||
val response = network.api.channels(search = null, start = start, count = count)
|
|
||||||
response.data to response.total
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun videoChannel(handle: String): Result<VideoChannelDto> =
|
suspend fun videoChannel(handle: String): Result<VideoChannelDto> =
|
||||||
runCatching { network.api.videoChannel(handle) }
|
runCatching { network.api.videoChannel(handle) }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,8 @@ import androidx.tv.material3.Card
|
||||||
import androidx.tv.material3.CardDefaults
|
import androidx.tv.material3.CardDefaults
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import net.jeena.jtubetv.AppGraph
|
import net.jeena.jtubetv.AppGraph
|
||||||
|
|
@ -117,10 +119,6 @@ class ChannelsViewModel(private val store: TokenStore) : ViewModel() {
|
||||||
|
|
||||||
private var allChannels: List<VideoChannelDto> = emptyList()
|
private var allChannels: List<VideoChannelDto> = emptyList()
|
||||||
|
|
||||||
/** Channels from followed instances, grouped per platform. */
|
|
||||||
var followedRows by mutableStateOf<List<UserGroup>>(emptyList())
|
|
||||||
private set
|
|
||||||
|
|
||||||
private val repo: PeerTubeRepository = AppGraph.repository
|
private val repo: PeerTubeRepository = AppGraph.repository
|
||||||
private val statsMap = HashMap<String, ChannelStats>()
|
private val statsMap = HashMap<String, ChannelStats>()
|
||||||
|
|
||||||
|
|
@ -167,10 +165,10 @@ class ChannelsViewModel(private val store: TokenStore) : ViewModel() {
|
||||||
loadLocalsFromVideos(host)
|
loadLocalsFromVideos(host)
|
||||||
loading = false
|
loading = false
|
||||||
|
|
||||||
// Background: the channel listing only refines — banners, exact
|
// Background: banners + exact subscriber counts per channel — the
|
||||||
// follower counts, owner accounts for the row headers, the
|
// only card data the video listing cannot carry. Six parallel
|
||||||
// zero-video locals, and the followed-instance rows.
|
// per-channel calls, no listing walk.
|
||||||
refineFromListing(host)
|
refineChannelMeta(host)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
error = e.message ?: "Could not load channels"
|
error = e.message ?: "Could not load channels"
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -234,53 +232,38 @@ class ChannelsViewModel(private val store: TokenStore) : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Streams the full channel listing (thirteen 100-row pages on an instance
|
* Background touch-up: per-channel metadata the video listing cannot carry
|
||||||
* following others — the locals are spread thin across them). Replaces the
|
* (banners + exact subscriber counts, shown on the cards). Batches of six
|
||||||
* local stubs with full objects (banners + exact subscribers + owner), adds
|
* parallel per-channel calls; the rows only repaint when a card's data
|
||||||
* the zero-video locals that the video listing cannot see, and builds the
|
* actually changed — sort keys are stat-stable, so no visible churn.
|
||||||
* followed-instance rows. Rebuilds only when a group actually changed.
|
|
||||||
*/
|
*/
|
||||||
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 {
|
viewModelScope.launch {
|
||||||
var start = 0L
|
handles.chunked(6).forEach { batch ->
|
||||||
var total: Long? = null
|
batch.map { handle ->
|
||||||
var retries = 0
|
async { handle to repo.videoChannel(handle).getOrNull() }
|
||||||
var localsFingerprint = ""
|
}.awaitAll().filterNotNull().forEach { (handle, full) ->
|
||||||
var foreignCount = -1
|
if (full == null) return@forEach
|
||||||
while (total == null || start < total) {
|
val merged = allChannels.toMutableList()
|
||||||
val page = repo.channelList(start, 100).getOrNull()
|
val index = merged.indexOfFirst {
|
||||||
if (page == null) {
|
it.name + "@" + it.host == handle && it.host == host
|
||||||
retries++
|
}
|
||||||
if (retries > 2) break
|
if (index >= 0 &&
|
||||||
delay(500)
|
(merged[index].banners != full.banners ||
|
||||||
continue
|
merged[index].followersCount != full.followersCount)
|
||||||
}
|
) {
|
||||||
retries = 0
|
merged[index] = merged[index].copy(
|
||||||
if (total == null) total = page.second
|
banners = full.banners,
|
||||||
val merged = allChannels.toMutableList()
|
followersCount = full.followersCount,
|
||||||
for (c in page.first) {
|
)
|
||||||
val existing = merged.indexOfFirst { it.name == c.name && it.host == c.host }
|
allChannels = merged
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
rebuildRows(host)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -297,23 +280,6 @@ class ChannelsViewModel(private val store: TokenStore) : ViewModel() {
|
||||||
|
|
||||||
private fun rebuildRowsInner(host: String?) {
|
private fun rebuildRowsInner(host: String?) {
|
||||||
val locals = allChannels.filter { it.host == host }
|
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<VideoChannelDto> { statOf(it) }
|
|
||||||
.thenByDescending { it.followersCount },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.sortedByDescending { g -> g.channels.sumOf { it.followersCount } }
|
|
||||||
val ownNames = userChannels.map { c -> c.name }.toSet()
|
val ownNames = userChannels.map { c -> c.name }.toSet()
|
||||||
val byOwner = locals.groupBy { ownerKey(it) }
|
val byOwner = locals.groupBy { ownerKey(it) }
|
||||||
val groups = byOwner.values.map { row ->
|
val groups = byOwner.values.map { row ->
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue