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> =
|
||||
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> =
|
||||
runCatching { network.api.videoChannel(handle) }
|
||||
|
||||
|
|
|
|||
|
|
@ -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<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 statsMap = HashMap<String, ChannelStats>()
|
||||
|
||||
|
|
@ -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<VideoChannelDto> { 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 ->
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue