diff --git a/app/src/main/java/hu/bbara/purefin/data/InMemoryMediaRepository.kt b/app/src/main/java/hu/bbara/purefin/data/InMemoryMediaRepository.kt index f72e416..2f1fe7a 100644 --- a/app/src/main/java/hu/bbara/purefin/data/InMemoryMediaRepository.kt +++ b/app/src/main/java/hu/bbara/purefin/data/InMemoryMediaRepository.kt @@ -1,6 +1,9 @@ package hu.bbara.purefin.data +import androidx.datastore.core.DataStore 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.OfflineRoomMediaLocalDataSource 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.launch import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.CollectionType @@ -43,7 +45,8 @@ class InMemoryMediaRepository @Inject constructor( val userSessionRepository: UserSessionRepository, val jellyfinApiClient: JellyfinApiClient, private val localDataSource: RoomMediaLocalDataSource, - @OfflineDatabase private val offlineDataSource: OfflineRoomMediaLocalDataSource + @OfflineDatabase private val offlineDataSource: OfflineRoomMediaLocalDataSource, + private val homeCacheDataStore: DataStore ) : MediaRepository { private val ready = CompletableDeferred() @@ -84,10 +87,57 @@ class InMemoryMediaRepository @Inject constructor( init { scope.launch { + loadFromCache() 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() { if (ready.isCompleted) { ready.await() // rethrows if completed exceptionally @@ -105,6 +155,7 @@ class InMemoryMediaRepository @Inject constructor( loadContinueWatching() loadNextUp() loadLatestLibraryContent() + persistHomeCache() _state.value = MediaRepositoryState.Ready initialLoadTimestamp = System.currentTimeMillis() ready.complete(Unit) @@ -302,6 +353,7 @@ class InMemoryMediaRepository @Inject constructor( loadContinueWatching() loadNextUp() loadLatestLibraryContent() + persistHomeCache() initialLoadTimestamp = System.currentTimeMillis() } diff --git a/app/src/main/java/hu/bbara/purefin/data/cache/HomeCache.kt b/app/src/main/java/hu/bbara/purefin/data/cache/HomeCache.kt new file mode 100644 index 0000000..dc08861 --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/data/cache/HomeCache.kt @@ -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 = emptyList(), + val nextUp: List = emptyList(), + val latestLibraryContent: Map> = emptyMap() +) diff --git a/app/src/main/java/hu/bbara/purefin/data/cache/HomeCacheModule.kt b/app/src/main/java/hu/bbara/purefin/data/cache/HomeCacheModule.kt new file mode 100644 index 0000000..740546c --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/data/cache/HomeCacheModule.kt @@ -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 { + return DataStoreFactory.create( + serializer = HomeCacheSerializer, + produceFile = { context.dataStoreFile("home_cache.json") }, + corruptionHandler = ReplaceFileCorruptionHandler( + produceNewData = { HomeCacheSerializer.defaultValue } + ) + ) + } +} diff --git a/app/src/main/java/hu/bbara/purefin/data/cache/HomeCacheSerializer.kt b/app/src/main/java/hu/bbara/purefin/data/cache/HomeCacheSerializer.kt new file mode 100644 index 0000000..ecdbc7d --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/data/cache/HomeCacheSerializer.kt @@ -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 { + override val defaultValue: HomeCache + get() = HomeCache() + + override suspend fun readFrom(input: InputStream): HomeCache { + try { + return Json.decodeFromString( + input.readBytes().decodeToString() + ) + } catch (serialization: SerializationException) { + throw CorruptionException("proto", serialization) + } + } + + override suspend fun writeTo(t: HomeCache, output: OutputStream) { + output.write( + Json.encodeToString(t) + .encodeToByteArray() + ) + } +}