feat: Add HomeData caching for faster application loading

This commit is contained in:
2026-02-18 19:05:53 +01:00
parent 198e40d1e8
commit 82547a781c
4 changed files with 133 additions and 2 deletions

View File

@@ -1,6 +1,9 @@
package hu.bbara.purefin.data package hu.bbara.purefin.data
import androidx.datastore.core.DataStore
import hu.bbara.purefin.client.JellyfinApiClient import hu.bbara.purefin.client.JellyfinApiClient
import hu.bbara.purefin.data.cache.CachedMediaItem
import hu.bbara.purefin.data.cache.HomeCache
import hu.bbara.purefin.data.local.room.OfflineDatabase import hu.bbara.purefin.data.local.room.OfflineDatabase
import hu.bbara.purefin.data.local.room.OfflineRoomMediaLocalDataSource import hu.bbara.purefin.data.local.room.OfflineRoomMediaLocalDataSource
import hu.bbara.purefin.data.local.room.RoomMediaLocalDataSource import hu.bbara.purefin.data.local.room.RoomMediaLocalDataSource
@@ -25,7 +28,6 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.CollectionType import org.jellyfin.sdk.model.api.CollectionType
@@ -43,7 +45,8 @@ class InMemoryMediaRepository @Inject constructor(
val userSessionRepository: UserSessionRepository, val userSessionRepository: UserSessionRepository,
val jellyfinApiClient: JellyfinApiClient, val jellyfinApiClient: JellyfinApiClient,
private val localDataSource: RoomMediaLocalDataSource, private val localDataSource: RoomMediaLocalDataSource,
@OfflineDatabase private val offlineDataSource: OfflineRoomMediaLocalDataSource @OfflineDatabase private val offlineDataSource: OfflineRoomMediaLocalDataSource,
private val homeCacheDataStore: DataStore<HomeCache>
) : MediaRepository { ) : MediaRepository {
private val ready = CompletableDeferred<Unit>() private val ready = CompletableDeferred<Unit>()
@@ -84,10 +87,57 @@ class InMemoryMediaRepository @Inject constructor(
init { init {
scope.launch { scope.launch {
loadFromCache()
runCatching { ensureReady() } runCatching { ensureReady() }
} }
} }
private suspend fun loadFromCache() {
val cache = homeCacheDataStore.data.first()
if (cache.continueWatching.isNotEmpty()) {
_continueWatching.value = cache.continueWatching.mapNotNull { it.toMedia() }
}
if (cache.nextUp.isNotEmpty()) {
_nextUp.value = cache.nextUp.mapNotNull { it.toMedia() }
}
if (cache.latestLibraryContent.isNotEmpty()) {
_latestLibraryContent.value = cache.latestLibraryContent.mapNotNull { (key, items) ->
val uuid = runCatching { UUID.fromString(key) }.getOrNull() ?: return@mapNotNull null
uuid to items.mapNotNull { it.toMedia() }
}.toMap()
}
}
private suspend fun persistHomeCache() {
val cache = HomeCache(
continueWatching = _continueWatching.value.map { it.toCachedItem() },
nextUp = _nextUp.value.map { it.toCachedItem() },
latestLibraryContent = _latestLibraryContent.value.map { (uuid, items) ->
uuid.toString() to items.map { it.toCachedItem() }
}.toMap()
)
homeCacheDataStore.updateData { cache }
}
private fun Media.toCachedItem(): CachedMediaItem = when (this) {
is Media.MovieMedia -> CachedMediaItem(type = "MOVIE", id = movieId.toString())
is Media.SeriesMedia -> CachedMediaItem(type = "SERIES", id = seriesId.toString())
is Media.SeasonMedia -> CachedMediaItem(type = "SEASON", id = seasonId.toString(), seriesId = seriesId.toString())
is Media.EpisodeMedia -> CachedMediaItem(type = "EPISODE", id = episodeId.toString(), seriesId = seriesId.toString())
}
private fun CachedMediaItem.toMedia(): Media? {
val uuid = runCatching { UUID.fromString(id) }.getOrNull() ?: return null
val seriesUuid = seriesId?.let { runCatching { UUID.fromString(it) }.getOrNull() }
return when (type) {
"MOVIE" -> Media.MovieMedia(movieId = uuid)
"SERIES" -> Media.SeriesMedia(seriesId = uuid)
"SEASON" -> Media.SeasonMedia(seasonId = uuid, seriesId = seriesUuid ?: return null)
"EPISODE" -> Media.EpisodeMedia(episodeId = uuid, seriesId = seriesUuid ?: return null)
else -> null
}
}
override suspend fun ensureReady() { override suspend fun ensureReady() {
if (ready.isCompleted) { if (ready.isCompleted) {
ready.await() // rethrows if completed exceptionally ready.await() // rethrows if completed exceptionally
@@ -105,6 +155,7 @@ class InMemoryMediaRepository @Inject constructor(
loadContinueWatching() loadContinueWatching()
loadNextUp() loadNextUp()
loadLatestLibraryContent() loadLatestLibraryContent()
persistHomeCache()
_state.value = MediaRepositoryState.Ready _state.value = MediaRepositoryState.Ready
initialLoadTimestamp = System.currentTimeMillis() initialLoadTimestamp = System.currentTimeMillis()
ready.complete(Unit) ready.complete(Unit)
@@ -302,6 +353,7 @@ class InMemoryMediaRepository @Inject constructor(
loadContinueWatching() loadContinueWatching()
loadNextUp() loadNextUp()
loadLatestLibraryContent() loadLatestLibraryContent()
persistHomeCache()
initialLoadTimestamp = System.currentTimeMillis() initialLoadTimestamp = System.currentTimeMillis()
} }

View File

@@ -0,0 +1,17 @@
package hu.bbara.purefin.data.cache
import kotlinx.serialization.Serializable
@Serializable
data class CachedMediaItem(
val type: String,
val id: String,
val seriesId: String? = null
)
@Serializable
data class HomeCache(
val continueWatching: List<CachedMediaItem> = emptyList(),
val nextUp: List<CachedMediaItem> = emptyList(),
val latestLibraryContent: Map<String, List<CachedMediaItem>> = emptyMap()
)

View File

@@ -0,0 +1,32 @@
package hu.bbara.purefin.data.cache
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
import androidx.datastore.dataStoreFile
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class HomeCacheModule {
@Provides
@Singleton
fun provideHomeCacheDataStore(
@ApplicationContext context: Context
): DataStore<HomeCache> {
return DataStoreFactory.create(
serializer = HomeCacheSerializer,
produceFile = { context.dataStoreFile("home_cache.json") },
corruptionHandler = ReplaceFileCorruptionHandler(
produceNewData = { HomeCacheSerializer.defaultValue }
)
)
}
}

View File

@@ -0,0 +1,30 @@
package hu.bbara.purefin.data.cache
import androidx.datastore.core.CorruptionException
import androidx.datastore.core.Serializer
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import java.io.InputStream
import java.io.OutputStream
object HomeCacheSerializer : Serializer<HomeCache> {
override val defaultValue: HomeCache
get() = HomeCache()
override suspend fun readFrom(input: InputStream): HomeCache {
try {
return Json.decodeFromString<HomeCache>(
input.readBytes().decodeToString()
)
} catch (serialization: SerializationException) {
throw CorruptionException("proto", serialization)
}
}
override suspend fun writeTo(t: HomeCache, output: OutputStream) {
output.write(
Json.encodeToString(t)
.encodeToByteArray()
)
}
}