refactor: Include media data in HomeCache so the first startup is faster.

This commit is contained in:
2026-04-21 22:04:30 +02:00
parent a2931b78f9
commit 7f83bb181b
2 changed files with 315 additions and 18 deletions

View File

@@ -15,8 +15,17 @@ import hu.bbara.purefin.core.model.Movie
import hu.bbara.purefin.core.model.Season
import hu.bbara.purefin.core.model.Series
import hu.bbara.purefin.data.jellyfin.client.JellyfinApiClient
import hu.bbara.purefin.data.offline.cache.CachedMediaItem
import hu.bbara.purefin.data.offline.cache.HomeCache
import hu.bbara.purefin.data.offline.cache.toCachedEpisode
import hu.bbara.purefin.data.offline.cache.toCachedItem
import hu.bbara.purefin.data.offline.cache.toCachedLibrary
import hu.bbara.purefin.data.offline.cache.toCachedMovie
import hu.bbara.purefin.data.offline.cache.toCachedSeries
import hu.bbara.purefin.data.offline.cache.toEpisode
import hu.bbara.purefin.data.offline.cache.toLibrary
import hu.bbara.purefin.data.offline.cache.toMedia
import hu.bbara.purefin.data.offline.cache.toMovie
import hu.bbara.purefin.data.offline.cache.toSeries
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -112,6 +121,16 @@ class InMemoryAppContentRepository @Inject constructor(
private suspend fun loadHomeCache() {
Log.d(TAG, "Loading home cache")
val cache = homeCacheDataStore.data.first()
if (cache.libraries.isNotEmpty()) {
val libraries = cache.libraries.mapNotNull { it.toLibrary() }
librariesState.value = libraries
onlineMediaRepository.upsertMovies(
libraries.filter { it.type == LibraryKind.MOVIES }.flatMap { it.movies.orEmpty() }
)
onlineMediaRepository.upsertSeries(
libraries.filter { it.type == LibraryKind.SERIES }.flatMap { it.series.orEmpty() }
)
}
if (cache.suggestions.isNotEmpty()) {
suggestionsState.value = cache.suggestions.mapNotNull { it.toMedia() }
}
@@ -127,10 +146,23 @@ class InMemoryAppContentRepository @Inject constructor(
uuid to items.mapNotNull { it.toMedia() }
}.toMap()
}
if (cache.movies.isNotEmpty()) {
onlineMediaRepository.upsertMovies(cache.movies.mapNotNull { it.toMovie() })
}
if (cache.series.isNotEmpty()) {
onlineMediaRepository.upsertSeries(cache.series.mapNotNull { it.toSeries() })
}
if (cache.episodes.isNotEmpty()) {
onlineMediaRepository.upsertEpisodes(cache.episodes.mapNotNull { it.toEpisode() })
}
Log.d(TAG, "Home cache loaded")
}
private suspend fun persistHomeCache() {
val referencedMediaIds = collectReferencedMediaIds()
val movies = onlineMediaRepository.movies.value
val series = onlineMediaRepository.series.value
val episodes = onlineMediaRepository.episodes.value
val cache = HomeCache(
suggestions = suggestionsState.value.map { it.toCachedItem() },
continueWatching = continueWatchingState.value.map { it.toCachedItem() },
@@ -138,27 +170,39 @@ class InMemoryAppContentRepository @Inject constructor(
latestLibraryContent = latestLibraryContentState.value.map { (uuid, items) ->
uuid.toString() to items.map { it.toCachedItem() }
}.toMap(),
libraries = librariesState.value.map { it.toCachedLibrary() },
movies = referencedMediaIds.movieIds.mapNotNull { movies[it] }.map { it.toCachedMovie() },
series = referencedMediaIds.seriesIds.mapNotNull { series[it] }.map { it.toCachedSeries() },
episodes = referencedMediaIds.episodeIds.mapNotNull { episodes[it] }.map { it.toCachedEpisode() },
)
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
private fun collectReferencedMediaIds(): ReferencedHomeMediaIds {
val movieIds = mutableSetOf<UUID>()
val seriesIds = mutableSetOf<UUID>()
val episodeIds = mutableSetOf<UUID>()
val referencedMedia = buildList {
addAll(suggestionsState.value)
addAll(continueWatchingState.value)
addAll(nextUpState.value)
latestLibraryContentState.value.values.forEach { addAll(it) }
}
referencedMedia.forEach { media ->
when (media) {
is Media.MovieMedia -> movieIds += media.movieId
is Media.SeriesMedia -> seriesIds += media.seriesId
is Media.SeasonMedia -> seriesIds += media.seriesId
is Media.EpisodeMedia -> episodeIds += media.episodeId
}
}
return ReferencedHomeMediaIds(
movieIds = movieIds,
seriesIds = seriesIds,
episodeIds = episodeIds,
)
}
suspend fun loadLibraries() {
@@ -412,3 +456,9 @@ class InMemoryAppContentRepository @Inject constructor(
private const val TAG = "InMemoryAppContentRepo"
}
}
private data class ReferencedHomeMediaIds(
val movieIds: Set<UUID>,
val seriesIds: Set<UUID>,
val episodeIds: Set<UUID>,
)

View File

@@ -1,6 +1,70 @@
package hu.bbara.purefin.data.offline.cache
import hu.bbara.purefin.core.model.CastMember
import hu.bbara.purefin.core.model.Episode
import hu.bbara.purefin.core.model.Library
import hu.bbara.purefin.core.model.LibraryKind
import hu.bbara.purefin.core.model.Media
import hu.bbara.purefin.core.model.Movie
import hu.bbara.purefin.core.model.Series
import kotlinx.serialization.Serializable
import java.util.UUID
@Serializable
data class CachedCastMember(
val name: String,
val role: String,
val imageUrl: String? = null
)
@Serializable
data class CachedMovie(
val id: String,
val libraryId: String,
val title: String,
val progress: Double? = null,
val watched: Boolean,
val year: String,
val rating: String,
val runtime: String,
val format: String,
val synopsis: String,
val imageUrlPrefix: String,
val audioTrack: String,
val subtitles: String,
val cast: List<CachedCastMember> = emptyList()
)
@Serializable
data class CachedSeries(
val id: String,
val libraryId: String,
val name: String,
val synopsis: String,
val year: String,
val imageUrlPrefix: String,
val unwatchedEpisodeCount: Int,
val seasonCount: Int,
val cast: List<CachedCastMember> = emptyList()
)
@Serializable
data class CachedEpisode(
val id: String,
val seriesId: String,
val seasonId: String,
val index: Int,
val title: String,
val synopsis: String,
val releaseDate: String,
val rating: String,
val runtime: String,
val progress: Double? = null,
val watched: Boolean,
val format: String,
val imageUrlPrefix: String,
val cast: List<CachedCastMember> = emptyList()
)
@Serializable
data class CachedMediaItem(
@@ -9,10 +73,193 @@ data class CachedMediaItem(
val seriesId: String? = null
)
@Serializable
data class CachedLibrary(
val id: String,
val name: String,
val type: String,
val posterUrl: String,
val series: List<CachedSeries>? = null,
val movies: List<CachedMovie>? = null,
)
@Serializable
data class HomeCache(
val suggestions: List<CachedMediaItem> = emptyList(),
val continueWatching: List<CachedMediaItem> = emptyList(),
val nextUp: List<CachedMediaItem> = emptyList(),
val latestLibraryContent: Map<String, List<CachedMediaItem>> = emptyMap()
val latestLibraryContent: Map<String, List<CachedMediaItem>> = emptyMap(),
val libraries: List<CachedLibrary> = emptyList(),
val movies: List<CachedMovie> = emptyList(),
val series: List<CachedSeries> = emptyList(),
val episodes: List<CachedEpisode> = emptyList()
)
fun Library.toCachedLibrary() = CachedLibrary(
id = id.toString(),
name = name,
type = type.name,
posterUrl = posterUrl,
series = series?.map { it.toCachedSeries() },
movies = movies?.map { it.toCachedMovie() },
)
fun CachedLibrary.toLibrary(): Library? {
val libraryId = id.toUuidOrNull() ?: return null
return when (type) {
"MOVIES" -> Library(
id = libraryId,
name = name,
type = LibraryKind.MOVIES,
posterUrl = posterUrl,
movies = movies?.mapNotNull { it.toMovie() } ?: emptyList(),
)
"SERIES" -> Library(
id = libraryId,
name = name,
type = LibraryKind.SERIES,
posterUrl = posterUrl,
series = series?.mapNotNull { it.toSeries() } ?: emptyList(),
)
else -> null
}
}
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())
}
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
}
}
fun Movie.toCachedMovie() = CachedMovie(
id = id.toString(),
libraryId = libraryId.toString(),
title = title,
progress = progress,
watched = watched,
year = year,
rating = rating,
runtime = runtime,
format = format,
synopsis = synopsis,
imageUrlPrefix = imageUrlPrefix,
audioTrack = audioTrack,
subtitles = subtitles,
cast = cast.map { it.toCachedCastMember() },
)
fun CachedMovie.toMovie(): Movie? {
val movieId = id.toUuidOrNull() ?: return null
val libraryUuid = libraryId.toUuidOrNull() ?: return null
return Movie(
id = movieId,
libraryId = libraryUuid,
title = title,
progress = progress,
watched = watched,
year = year,
rating = rating,
runtime = runtime,
format = format,
synopsis = synopsis,
imageUrlPrefix = imageUrlPrefix,
audioTrack = audioTrack,
subtitles = subtitles,
cast = cast.map { it.toCastMember() },
)
}
fun Series.toCachedSeries() = CachedSeries(
id = id.toString(),
libraryId = libraryId.toString(),
name = name,
synopsis = synopsis,
year = year,
imageUrlPrefix = imageUrlPrefix,
unwatchedEpisodeCount = unwatchedEpisodeCount,
seasonCount = seasonCount,
cast = cast.map { it.toCachedCastMember() },
)
fun CachedSeries.toSeries(): Series? {
val seriesId = id.toUuidOrNull() ?: return null
val libraryUuid = libraryId.toUuidOrNull() ?: return null
return Series(
id = seriesId,
libraryId = libraryUuid,
name = name,
synopsis = synopsis,
year = year,
imageUrlPrefix = imageUrlPrefix,
unwatchedEpisodeCount = unwatchedEpisodeCount,
seasonCount = seasonCount,
seasons = emptyList(),
cast = cast.map { it.toCastMember() },
)
}
fun Episode.toCachedEpisode() = CachedEpisode(
id = id.toString(),
seriesId = seriesId.toString(),
seasonId = seasonId.toString(),
index = index,
title = title,
synopsis = synopsis,
releaseDate = releaseDate,
rating = rating,
runtime = runtime,
progress = progress,
watched = watched,
format = format,
imageUrlPrefix = imageUrlPrefix,
cast = cast.map { it.toCachedCastMember() },
)
fun CachedEpisode.toEpisode(): Episode? {
val episodeId = id.toUuidOrNull() ?: return null
val seriesUuid = seriesId.toUuidOrNull() ?: return null
val seasonUuid = seasonId.toUuidOrNull() ?: return null
return Episode(
id = episodeId,
seriesId = seriesUuid,
seasonId = seasonUuid,
index = index,
title = title,
synopsis = synopsis,
releaseDate = releaseDate,
rating = rating,
runtime = runtime,
progress = progress,
watched = watched,
format = format,
imageUrlPrefix = imageUrlPrefix,
cast = cast.map { it.toCastMember() },
)
}
private fun CastMember.toCachedCastMember() = CachedCastMember(
name = name,
role = role,
imageUrl = imageUrl,
)
private fun CachedCastMember.toCastMember() = CastMember(
name = name,
role = role,
imageUrl = imageUrl,
)
private fun String.toUuidOrNull(): UUID? = runCatching { UUID.fromString(this) }.getOrNull()