revert(data): remove ETag caching and head-fetch home refresh

Reverts 5491c38 (ETag-based conditional HTTP caching for home refresh) and 1ec9ff9 (head-fetch rows and count-only libraries). Restores the pre-ETag home refresh path that fully loads library content per library and fetches each home row in a single request.

Preserves unrelated later improvements: SingleFlight deduplication (86f44ac), nullable toLibrary skip for unsupported library types (80b1bd8), and upsert-before-publish ordering to prevent empty intermediate states (f0a7828).
This commit is contained in:
2026-06-26 15:44:05 +00:00
parent 13bc9410b4
commit c2cc4e9e65
11 changed files with 155 additions and 707 deletions

View File

@@ -1,6 +1,5 @@
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
@@ -14,15 +13,4 @@ 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>?
}

View File

@@ -68,17 +68,16 @@ 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 = it.size == 0,
isEmpty = when (it.type) {
LibraryKind.MOVIES -> localMediaRepository.movies.value.isEmpty()
LibraryKind.SERIES -> localMediaRepository.series.value.isEmpty()
}
)
}
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())

View File

@@ -6,13 +6,18 @@ 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.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import java.util.UUID
import javax.inject.Inject
@@ -26,35 +31,24 @@ class LibraryViewModel @Inject constructor(
private val selectedLibrary = MutableStateFlow<UUID?>(null)
// 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()
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())
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) {

View File

@@ -1,13 +0,0 @@
package hu.bbara.purefin.core.jellyfin
import javax.inject.Qualifier
/**
* Marks an `OkHttpClient` that is wired into the Jellyfin SDK's `OkHttpFactory`.
* Distinct from the unqualified client used for image loading and media streaming
* because the SDK has its own auth path and the ETag interceptor should not leak
* into non-SDK HTTP calls.
*/
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class JellyfinSdkClient