mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-23 19:26:50 +00:00
perf(home): head-fetch rows and count-only libraries
Add a head-only fetch path for the home rows (Suggestions, Continue Watching, Next Up, Latest per library). The first HEAD_LIMIT items are fetched first; if the head matches the cached head, the full request is skipped. Most home refreshes now only pay for the small head request, not the full payload. Replace per-library getLibraryContent calls in the home page with getLibraryItemCount (limit=0, enableTotalRecordCount=true). The home page only uses Library.size; the full movies/series lists are fetched on demand by LibraryViewModel via the new loadLibraryContent method. The per-row loaders now upsert full movie/series details into onlineMediaRepository so the home viewmodel can still resolve them.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package hu.bbara.purefin.core.data
|
||||
|
||||
import hu.bbara.purefin.core.model.MediaUiModel
|
||||
import hu.bbara.purefin.model.Library
|
||||
import hu.bbara.purefin.model.Media
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -13,4 +14,15 @@ interface HomeRepository {
|
||||
val latestLibraryContent: StateFlow<Map<UUID, List<Media>>>
|
||||
fun ensureReady()
|
||||
suspend fun refreshHomeData()
|
||||
|
||||
/**
|
||||
* Fetches the full content (movies or series) of a single library on
|
||||
* demand. Used by the library detail screen; the home page no longer
|
||||
* pays for full library content on every refresh.
|
||||
*
|
||||
* Returns `null` when the server returned 304 Not Modified for the
|
||||
* per-library ETag — the caller should keep its previously fetched
|
||||
* copy. An empty list is a legitimate "the library is empty" result.
|
||||
*/
|
||||
suspend fun loadLibraryContent(libraryId: UUID): List<MediaUiModel>?
|
||||
}
|
||||
|
||||
@@ -68,16 +68,17 @@ class AppViewModel @Inject constructor(
|
||||
|
||||
val libraries = homeRepository.libraries.map { libraries ->
|
||||
libraries.map {
|
||||
// The home page no longer fetches the full library content
|
||||
// (see InMemoryAppContentRepository.loadLibraries). isEmpty
|
||||
// therefore reflects the actual server-reported size, not
|
||||
// the contents of the local media repository.
|
||||
LibraryUiModel(
|
||||
id = it.id,
|
||||
name = it.name,
|
||||
type = it.type,
|
||||
posterUrl = it.posterUrl,
|
||||
size = it.size,
|
||||
isEmpty = when (it.type) {
|
||||
LibraryKind.MOVIES -> localMediaRepository.movies.value.isEmpty()
|
||||
LibraryKind.SERIES -> localMediaRepository.series.value.isEmpty()
|
||||
}
|
||||
isEmpty = it.size == 0,
|
||||
)
|
||||
}
|
||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
|
||||
|
||||
@@ -6,18 +6,13 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import hu.bbara.purefin.core.data.HomeRepository
|
||||
import hu.bbara.purefin.core.data.MediaMetadataUpdater
|
||||
import hu.bbara.purefin.core.model.MediaUiModel
|
||||
import hu.bbara.purefin.core.model.MovieUiModel
|
||||
import hu.bbara.purefin.core.model.SeriesUiModel
|
||||
import hu.bbara.purefin.core.navigation.MovieDto
|
||||
import hu.bbara.purefin.core.navigation.NavigationManager
|
||||
import hu.bbara.purefin.core.navigation.Route
|
||||
import hu.bbara.purefin.core.navigation.SeriesDto
|
||||
import hu.bbara.purefin.model.LibraryKind
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
@@ -31,24 +26,35 @@ class LibraryViewModel @Inject constructor(
|
||||
|
||||
private val selectedLibrary = MutableStateFlow<UUID?>(null)
|
||||
|
||||
val contents: StateFlow<List<MediaUiModel>> = combine(selectedLibrary, homeRepository.libraries) {
|
||||
libraryId, libraries ->
|
||||
if (libraryId == null) {
|
||||
return@combine emptyList()
|
||||
}
|
||||
val library = libraries.find { it.id == libraryId } ?: return@combine emptyList()
|
||||
when (library.type) {
|
||||
LibraryKind.SERIES -> library.series!!.map { series ->
|
||||
SeriesUiModel(series)
|
||||
}
|
||||
LibraryKind.MOVIES -> library.movies!!.map { movie ->
|
||||
MovieUiModel(movie)
|
||||
}
|
||||
}
|
||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
|
||||
// Local cache of the last-fetched content per library. Used to preserve
|
||||
// content when re-selecting a library whose ETag matches (so the
|
||||
// repository's 304 response can be treated as "use the cached copy").
|
||||
private val cachedContents = mutableMapOf<UUID, List<MediaUiModel>>()
|
||||
|
||||
private val _contents = MutableStateFlow<List<MediaUiModel>>(emptyList())
|
||||
val contents: StateFlow<List<MediaUiModel>> = _contents.asStateFlow()
|
||||
|
||||
init {
|
||||
viewModelScope.launch { homeRepository.ensureReady() }
|
||||
viewModelScope.launch {
|
||||
selectedLibrary.collect { libraryId ->
|
||||
if (libraryId == null) {
|
||||
_contents.value = emptyList()
|
||||
} else {
|
||||
val fresh = homeRepository.loadLibraryContent(libraryId)
|
||||
if (fresh != null) {
|
||||
cachedContents[libraryId] = fresh
|
||||
_contents.value = fresh
|
||||
} else {
|
||||
// ETag 304 — keep the cached copy for this library if
|
||||
// we have one. On a first-visit 304 (rare, would
|
||||
// require the library's ETag to be set without any
|
||||
// prior fetch) the screen briefly shows empty.
|
||||
_contents.value = cachedContents[libraryId] ?: emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onMovieSelected(movieId: UUID) {
|
||||
|
||||
Reference in New Issue
Block a user