mirror of
https://github.com/bbara04/Purefin.git
synced 2026-03-31 17:10:08 +02:00
feat: Add HomeData caching for faster application loading
This commit is contained in:
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
17
app/src/main/java/hu/bbara/purefin/data/cache/HomeCache.kt
vendored
Normal file
17
app/src/main/java/hu/bbara/purefin/data/cache/HomeCache.kt
vendored
Normal 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()
|
||||||
|
)
|
||||||
32
app/src/main/java/hu/bbara/purefin/data/cache/HomeCacheModule.kt
vendored
Normal file
32
app/src/main/java/hu/bbara/purefin/data/cache/HomeCacheModule.kt
vendored
Normal 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 }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
30
app/src/main/java/hu/bbara/purefin/data/cache/HomeCacheSerializer.kt
vendored
Normal file
30
app/src/main/java/hu/bbara/purefin/data/cache/HomeCacheSerializer.kt
vendored
Normal 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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user