From 15190a657a54dee69a151768b5597de1df88f935 Mon Sep 17 00:00:00 2001 From: Barnabas Balogh Date: Thu, 29 Jan 2026 18:16:11 +0100 Subject: [PATCH] feat: add Room database entities and DAOs for Episodes, Seasons, and Series --- app/build.gradle.kts | 3 + .../purefin/data/local/dao/EpisodeDao.kt | 35 +++ .../bbara/purefin/data/local/dao/SeasonDao.kt | 40 ++++ .../bbara/purefin/data/local/dao/SeriesDao.kt | 41 ++++ .../purefin/data/local/db/PurefinDatabase.kt | 26 ++ .../data/local/db/PurefinTypeConverters.kt | 52 ++++ .../purefin/data/local/di/DatabaseModule.kt | 50 ++++ .../data/local/entity/EpisodeEntity.kt | 48 ++++ .../purefin/data/local/entity/SeasonEntity.kt | 28 +++ .../purefin/data/local/entity/SeriesEntity.kt | 15 ++ .../local/relations/SeasonWithEpisodes.kt | 15 ++ .../relations/SeriesWithSeasonsAndEpisodes.kt | 16 ++ .../local/repository/LocalMediaRepository.kt | 40 ++++ .../repository/RoomLocalMediaRepository.kt | 225 ++++++++++++++++++ .../hu/bbara/purefin/data/model/Episode.kt | 5 +- .../hu/bbara/purefin/data/model/Season.kt | 6 +- gradle/libs.versions.toml | 4 + 17 files changed, 644 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/hu/bbara/purefin/data/local/dao/EpisodeDao.kt create mode 100644 app/src/main/java/hu/bbara/purefin/data/local/dao/SeasonDao.kt create mode 100644 app/src/main/java/hu/bbara/purefin/data/local/dao/SeriesDao.kt create mode 100644 app/src/main/java/hu/bbara/purefin/data/local/db/PurefinDatabase.kt create mode 100644 app/src/main/java/hu/bbara/purefin/data/local/db/PurefinTypeConverters.kt create mode 100644 app/src/main/java/hu/bbara/purefin/data/local/di/DatabaseModule.kt create mode 100644 app/src/main/java/hu/bbara/purefin/data/local/entity/EpisodeEntity.kt create mode 100644 app/src/main/java/hu/bbara/purefin/data/local/entity/SeasonEntity.kt create mode 100644 app/src/main/java/hu/bbara/purefin/data/local/entity/SeriesEntity.kt create mode 100644 app/src/main/java/hu/bbara/purefin/data/local/relations/SeasonWithEpisodes.kt create mode 100644 app/src/main/java/hu/bbara/purefin/data/local/relations/SeriesWithSeasonsAndEpisodes.kt create mode 100644 app/src/main/java/hu/bbara/purefin/data/local/repository/LocalMediaRepository.kt create mode 100644 app/src/main/java/hu/bbara/purefin/data/local/repository/RoomLocalMediaRepository.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 833a7b2..9020a11 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -73,6 +73,8 @@ dependencies { implementation(libs.medi3.ui.compose) implementation(libs.androidx.navigation3.runtime) implementation(libs.androidx.navigation3.ui) + implementation(libs.androidx.room.runtime) + implementation(libs.androidx.room.ktx) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) @@ -81,4 +83,5 @@ dependencies { debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.compose.ui.test.manifest) ksp(libs.hilt.compiler) + ksp(libs.androidx.room.compiler) } diff --git a/app/src/main/java/hu/bbara/purefin/data/local/dao/EpisodeDao.kt b/app/src/main/java/hu/bbara/purefin/data/local/dao/EpisodeDao.kt new file mode 100644 index 0000000..c56ac31 --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/data/local/dao/EpisodeDao.kt @@ -0,0 +1,35 @@ +package hu.bbara.purefin.data.local.dao + +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Upsert +import hu.bbara.purefin.data.local.entity.EpisodeEntity +import java.util.UUID + +@Dao +interface EpisodeDao { + + @Upsert + suspend fun upsertEpisode(episode: EpisodeEntity) + + @Upsert + suspend fun upsertEpisodes(episodes: List) + + @Query("DELETE FROM episodes WHERE id = :episodeId") + suspend fun deleteEpisode(episodeId: UUID) + + @Query("DELETE FROM episodes WHERE seasonId = :seasonId") + suspend fun deleteEpisodesForSeason(seasonId: UUID) + + @Query("DELETE FROM episodes WHERE seriesId = :seriesId") + suspend fun deleteEpisodesForSeries(seriesId: UUID) + + @Query("SELECT * FROM episodes WHERE id = :episodeId") + suspend fun getEpisodeById(episodeId: UUID): EpisodeEntity? + + @Query("SELECT * FROM episodes WHERE seasonId = :seasonId ORDER BY `index` ASC") + suspend fun getEpisodesForSeason(seasonId: UUID): List + + @Query("SELECT * FROM episodes WHERE seriesId = :seriesId ORDER BY seasonId, `index` ASC") + suspend fun getEpisodesForSeries(seriesId: UUID): List +} diff --git a/app/src/main/java/hu/bbara/purefin/data/local/dao/SeasonDao.kt b/app/src/main/java/hu/bbara/purefin/data/local/dao/SeasonDao.kt new file mode 100644 index 0000000..9985175 --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/data/local/dao/SeasonDao.kt @@ -0,0 +1,40 @@ +package hu.bbara.purefin.data.local.dao + +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Upsert +import hu.bbara.purefin.data.local.entity.SeasonEntity +import hu.bbara.purefin.data.local.relations.SeasonWithEpisodes +import java.util.UUID +import kotlinx.coroutines.flow.Flow + +@Dao +interface SeasonDao { + + @Upsert + suspend fun upsertSeason(season: SeasonEntity) + + @Upsert + suspend fun upsertSeasons(seasons: List) + + @Query("DELETE FROM seasons WHERE id = :seasonId") + suspend fun deleteSeason(seasonId: UUID) + + @Query("DELETE FROM seasons WHERE seriesId = :seriesId") + suspend fun deleteSeasonsForSeries(seriesId: UUID) + + @Transaction + @Query("SELECT * FROM seasons WHERE id = :seasonId") + suspend fun getSeasonWithEpisodes(seasonId: UUID): SeasonWithEpisodes? + + @Query("SELECT * FROM seasons WHERE id = :seasonId") + suspend fun getSeasonById(seasonId: UUID): SeasonEntity? + + @Query("SELECT * FROM seasons WHERE seriesId = :seriesId ORDER BY `index` ASC") + suspend fun getSeasonsForSeries(seriesId: UUID): List + + @Transaction + @Query("SELECT * FROM seasons WHERE seriesId = :seriesId ORDER BY `index` ASC") + fun observeSeasonsWithEpisodes(seriesId: UUID): Flow> +} diff --git a/app/src/main/java/hu/bbara/purefin/data/local/dao/SeriesDao.kt b/app/src/main/java/hu/bbara/purefin/data/local/dao/SeriesDao.kt new file mode 100644 index 0000000..97a0f3a --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/data/local/dao/SeriesDao.kt @@ -0,0 +1,41 @@ +package hu.bbara.purefin.data.local.dao + +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Upsert +import hu.bbara.purefin.data.local.entity.SeriesEntity +import hu.bbara.purefin.data.local.relations.SeriesWithSeasonsAndEpisodes +import java.util.UUID +import kotlinx.coroutines.flow.Flow + +@Dao +interface SeriesDao { + + @Upsert + suspend fun upsertSeries(series: SeriesEntity) + + @Upsert + suspend fun upsertSeries(series: List) + + @Query("DELETE FROM series WHERE id = :seriesId") + suspend fun deleteSeries(seriesId: UUID) + + @Transaction + @Query("SELECT * FROM series WHERE id = :seriesId") + suspend fun getSeriesWithContent(seriesId: UUID): SeriesWithSeasonsAndEpisodes? + + @Transaction + @Query("SELECT * FROM series WHERE id = :seriesId") + fun observeSeriesWithContent(seriesId: UUID): Flow + + @Transaction + @Query("SELECT * FROM series") + fun observeAllSeriesWithContent(): Flow> + + @Query("SELECT * FROM series WHERE id = :seriesId") + suspend fun getSeriesById(seriesId: UUID): SeriesEntity? + + @Query("SELECT * FROM series") + fun observeSeriesEntities(): Flow> +} diff --git a/app/src/main/java/hu/bbara/purefin/data/local/db/PurefinDatabase.kt b/app/src/main/java/hu/bbara/purefin/data/local/db/PurefinDatabase.kt new file mode 100644 index 0000000..e0ab014 --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/data/local/db/PurefinDatabase.kt @@ -0,0 +1,26 @@ +package hu.bbara.purefin.data.local.db + +import androidx.room.Database +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import hu.bbara.purefin.data.local.dao.EpisodeDao +import hu.bbara.purefin.data.local.dao.SeasonDao +import hu.bbara.purefin.data.local.dao.SeriesDao +import hu.bbara.purefin.data.local.entity.EpisodeEntity +import hu.bbara.purefin.data.local.entity.SeasonEntity +import hu.bbara.purefin.data.local.entity.SeriesEntity + +@Database( + entities = [SeriesEntity::class, SeasonEntity::class, EpisodeEntity::class], + version = 1, + exportSchema = false +) +@TypeConverters(PurefinTypeConverters::class) +abstract class PurefinDatabase : RoomDatabase() { + + abstract fun seriesDao(): SeriesDao + + abstract fun seasonDao(): SeasonDao + + abstract fun episodeDao(): EpisodeDao +} diff --git a/app/src/main/java/hu/bbara/purefin/data/local/db/PurefinTypeConverters.kt b/app/src/main/java/hu/bbara/purefin/data/local/db/PurefinTypeConverters.kt new file mode 100644 index 0000000..7f1fedd --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/data/local/db/PurefinTypeConverters.kt @@ -0,0 +1,52 @@ +package hu.bbara.purefin.data.local.db + +import androidx.room.TypeConverter +import hu.bbara.purefin.data.local.entity.EpisodeCastMemberEntity +import java.util.UUID +import org.json.JSONArray +import org.json.JSONObject + +class PurefinTypeConverters { + + @TypeConverter + fun fromUuid(value: UUID?): String? = value?.toString() + + @TypeConverter + fun toUuid(value: String?): UUID? = value?.let(UUID::fromString) + + @TypeConverter + fun fromCastMembers(value: List?): String? { + if (value.isNullOrEmpty()) return null + val jsonArray = JSONArray() + value.forEach { member -> + jsonArray.put( + JSONObject().apply { + put("name", member.name) + put("role", member.role) + put("imageUrl", member.imageUrl) + } + ) + } + return jsonArray.toString() + } + + @TypeConverter + fun toCastMembers(value: String?): List { + if (value.isNullOrBlank()) return emptyList() + val jsonArray = JSONArray(value) + val members = mutableListOf() + for (index in 0 until jsonArray.length()) { + val jsonObject = jsonArray.optJSONObject(index) ?: continue + val imageUrl = when { + jsonObject.has("imageUrl") && !jsonObject.isNull("imageUrl") -> jsonObject.optString("imageUrl") + else -> null + } + members += EpisodeCastMemberEntity( + name = jsonObject.optString("name"), + role = jsonObject.optString("role"), + imageUrl = imageUrl?.ifBlank { null } + ) + } + return members + } +} diff --git a/app/src/main/java/hu/bbara/purefin/data/local/di/DatabaseModule.kt b/app/src/main/java/hu/bbara/purefin/data/local/di/DatabaseModule.kt new file mode 100644 index 0000000..0612ecf --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/data/local/di/DatabaseModule.kt @@ -0,0 +1,50 @@ +package hu.bbara.purefin.data.local.di + +import android.content.Context +import androidx.room.Room +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import hu.bbara.purefin.data.local.dao.EpisodeDao +import hu.bbara.purefin.data.local.dao.SeasonDao +import hu.bbara.purefin.data.local.dao.SeriesDao +import hu.bbara.purefin.data.local.db.PurefinDatabase +import hu.bbara.purefin.data.local.repository.LocalMediaRepository +import hu.bbara.purefin.data.local.repository.RoomLocalMediaRepository +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object DatabaseModule { + + @Provides + @Singleton + fun provideDatabase( + @ApplicationContext context: Context + ): PurefinDatabase { + return Room.databaseBuilder( + context, + PurefinDatabase::class.java, + "purefin.db" + ).build() + } + + @Provides + fun provideSeriesDao(database: PurefinDatabase): SeriesDao = database.seriesDao() + + @Provides + fun provideSeasonDao(database: PurefinDatabase): SeasonDao = database.seasonDao() + + @Provides + fun provideEpisodeDao(database: PurefinDatabase): EpisodeDao = database.episodeDao() + + @Provides + @Singleton + fun provideLocalMediaRepository( + seriesDao: SeriesDao, + seasonDao: SeasonDao, + episodeDao: EpisodeDao + ): LocalMediaRepository = RoomLocalMediaRepository(seriesDao, seasonDao, episodeDao) +} diff --git a/app/src/main/java/hu/bbara/purefin/data/local/entity/EpisodeEntity.kt b/app/src/main/java/hu/bbara/purefin/data/local/entity/EpisodeEntity.kt new file mode 100644 index 0000000..861d286 --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/data/local/entity/EpisodeEntity.kt @@ -0,0 +1,48 @@ +package hu.bbara.purefin.data.local.entity + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey +import java.util.UUID + +@Entity( + tableName = "episodes", + foreignKeys = [ + ForeignKey( + entity = SeriesEntity::class, + parentColumns = ["id"], + childColumns = ["seriesId"], + onDelete = ForeignKey.CASCADE, + onUpdate = ForeignKey.CASCADE + ), + ForeignKey( + entity = SeasonEntity::class, + parentColumns = ["id"], + childColumns = ["seasonId"], + onDelete = ForeignKey.CASCADE, + onUpdate = ForeignKey.CASCADE + ) + ], + indices = [Index(value = ["seriesId"]), Index(value = ["seasonId"])] +) +data class EpisodeEntity( + @PrimaryKey val id: UUID, + val seriesId: UUID, + val seasonId: UUID, + val title: String, + val index: Int, + val releaseDate: String, + val rating: String, + val runtime: String, + val format: String, + val synopsis: String, + val heroImageUrl: String, + val cast: List +) + +data class EpisodeCastMemberEntity( + val name: String, + val role: String, + val imageUrl: String? +) diff --git a/app/src/main/java/hu/bbara/purefin/data/local/entity/SeasonEntity.kt b/app/src/main/java/hu/bbara/purefin/data/local/entity/SeasonEntity.kt new file mode 100644 index 0000000..62b9c20 --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/data/local/entity/SeasonEntity.kt @@ -0,0 +1,28 @@ +package hu.bbara.purefin.data.local.entity + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey +import java.util.UUID + +@Entity( + tableName = "seasons", + foreignKeys = [ + ForeignKey( + entity = SeriesEntity::class, + parentColumns = ["id"], + childColumns = ["seriesId"], + onDelete = ForeignKey.CASCADE, + onUpdate = ForeignKey.CASCADE + ) + ], + indices = [Index(value = ["seriesId"])] +) +data class SeasonEntity( + @PrimaryKey val id: UUID, + val seriesId: UUID, + val name: String, + val index: Int, + val episodeCount: Int +) diff --git a/app/src/main/java/hu/bbara/purefin/data/local/entity/SeriesEntity.kt b/app/src/main/java/hu/bbara/purefin/data/local/entity/SeriesEntity.kt new file mode 100644 index 0000000..ad3e5b5 --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/data/local/entity/SeriesEntity.kt @@ -0,0 +1,15 @@ +package hu.bbara.purefin.data.local.entity + +import androidx.room.Entity +import androidx.room.PrimaryKey +import java.util.UUID + +@Entity(tableName = "series") +data class SeriesEntity( + @PrimaryKey val id: UUID, + val name: String, + val synopsis: String, + val year: String, + val heroImageUrl: String, + val seasonCount: Int +) diff --git a/app/src/main/java/hu/bbara/purefin/data/local/relations/SeasonWithEpisodes.kt b/app/src/main/java/hu/bbara/purefin/data/local/relations/SeasonWithEpisodes.kt new file mode 100644 index 0000000..d59f7ab --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/data/local/relations/SeasonWithEpisodes.kt @@ -0,0 +1,15 @@ +package hu.bbara.purefin.data.local.relations + +import androidx.room.Embedded +import androidx.room.Relation +import hu.bbara.purefin.data.local.entity.EpisodeEntity +import hu.bbara.purefin.data.local.entity.SeasonEntity + +data class SeasonWithEpisodes( + @Embedded val season: SeasonEntity, + @Relation( + parentColumn = "id", + entityColumn = "seasonId" + ) + val episodes: List +) diff --git a/app/src/main/java/hu/bbara/purefin/data/local/relations/SeriesWithSeasonsAndEpisodes.kt b/app/src/main/java/hu/bbara/purefin/data/local/relations/SeriesWithSeasonsAndEpisodes.kt new file mode 100644 index 0000000..0190575 --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/data/local/relations/SeriesWithSeasonsAndEpisodes.kt @@ -0,0 +1,16 @@ +package hu.bbara.purefin.data.local.relations + +import androidx.room.Embedded +import androidx.room.Relation +import hu.bbara.purefin.data.local.entity.SeasonEntity +import hu.bbara.purefin.data.local.entity.SeriesEntity + +data class SeriesWithSeasonsAndEpisodes( + @Embedded val series: SeriesEntity, + @Relation( + entity = SeasonEntity::class, + parentColumn = "id", + entityColumn = "seriesId" + ) + val seasons: List +) diff --git a/app/src/main/java/hu/bbara/purefin/data/local/repository/LocalMediaRepository.kt b/app/src/main/java/hu/bbara/purefin/data/local/repository/LocalMediaRepository.kt new file mode 100644 index 0000000..baa5186 --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/data/local/repository/LocalMediaRepository.kt @@ -0,0 +1,40 @@ +package hu.bbara.purefin.data.local.repository + +import hu.bbara.purefin.data.model.Episode +import hu.bbara.purefin.data.model.Season +import hu.bbara.purefin.data.model.Series +import java.util.UUID +import kotlinx.coroutines.flow.Flow + +interface LocalMediaRepository { + + suspend fun upsertSeries(series: Series) + + suspend fun upsertSeries(seriesList: List) + + suspend fun getSeries(seriesId: UUID, includeContent: Boolean = true): Series? + + fun observeSeries(seriesId: UUID): Flow + + fun observeAllSeries(): Flow> + + suspend fun deleteSeries(seriesId: UUID) + + suspend fun upsertSeason(season: Season) + + suspend fun upsertSeasons(seasons: List) + + suspend fun getSeason(seasonId: UUID, includeEpisodes: Boolean = true): Season? + + fun observeSeasons(seriesId: UUID): Flow> + + suspend fun deleteSeason(seasonId: UUID) + + suspend fun upsertEpisode(episode: Episode) + + suspend fun upsertEpisodes(episodes: List) + + suspend fun getEpisode(episodeId: UUID): Episode? + + suspend fun deleteEpisode(episodeId: UUID) +} diff --git a/app/src/main/java/hu/bbara/purefin/data/local/repository/RoomLocalMediaRepository.kt b/app/src/main/java/hu/bbara/purefin/data/local/repository/RoomLocalMediaRepository.kt new file mode 100644 index 0000000..71a3fbf --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/data/local/repository/RoomLocalMediaRepository.kt @@ -0,0 +1,225 @@ +package hu.bbara.purefin.data.local.repository + +import hu.bbara.purefin.app.content.episode.CastMember +import hu.bbara.purefin.data.local.dao.EpisodeDao +import hu.bbara.purefin.data.local.dao.SeasonDao +import hu.bbara.purefin.data.local.dao.SeriesDao +import hu.bbara.purefin.data.local.entity.EpisodeCastMemberEntity +import hu.bbara.purefin.data.local.entity.EpisodeEntity +import hu.bbara.purefin.data.local.entity.SeasonEntity +import hu.bbara.purefin.data.local.entity.SeriesEntity +import hu.bbara.purefin.data.local.relations.SeasonWithEpisodes +import hu.bbara.purefin.data.local.relations.SeriesWithSeasonsAndEpisodes +import hu.bbara.purefin.data.model.Episode +import hu.bbara.purefin.data.model.Season +import hu.bbara.purefin.data.model.Series +import java.util.UUID +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext + +class RoomLocalMediaRepository @Inject constructor( + private val seriesDao: SeriesDao, + private val seasonDao: SeasonDao, + private val episodeDao: EpisodeDao +) : LocalMediaRepository { + + override suspend fun upsertSeries(series: Series) { + withContext(Dispatchers.IO) { + seriesDao.upsertSeries(series.toEntity()) + upsertSeasonsInternal(series.seasons) + } + } + + override suspend fun upsertSeries(seriesList: List) { + if (seriesList.isEmpty()) return + withContext(Dispatchers.IO) { + seriesDao.upsertSeries(seriesList.map { it.toEntity() }) + val seasons = seriesList.flatMap { it.seasons } + upsertSeasonsInternal(seasons) + } + } + + override suspend fun getSeries(seriesId: UUID, includeContent: Boolean): Series? { + return withContext(Dispatchers.IO) { + if (includeContent) { + seriesDao.getSeriesWithContent(seriesId)?.toDomain() + } else { + seriesDao.getSeriesById(seriesId)?.toDomain(emptyList()) + } + } + } + + override fun observeSeries(seriesId: UUID): Flow { + return seriesDao.observeSeriesWithContent(seriesId).map { relation -> + relation?.toDomain() + } + } + + override fun observeAllSeries(): Flow> { + return seriesDao.observeAllSeriesWithContent().map { relations -> + relations.map { it.toDomain() } + } + } + + override suspend fun deleteSeries(seriesId: UUID) { + withContext(Dispatchers.IO) { + seriesDao.deleteSeries(seriesId) + } + } + + override suspend fun upsertSeason(season: Season) { + withContext(Dispatchers.IO) { + upsertSeasonsInternal(listOf(season)) + } + } + + override suspend fun upsertSeasons(seasons: List) { + if (seasons.isEmpty()) return + withContext(Dispatchers.IO) { + upsertSeasonsInternal(seasons) + } + } + + override suspend fun getSeason(seasonId: UUID, includeEpisodes: Boolean): Season? { + return withContext(Dispatchers.IO) { + if (includeEpisodes) { + seasonDao.getSeasonWithEpisodes(seasonId)?.toDomain() + } else { + seasonDao.getSeasonById(seasonId)?.toDomain(emptyList()) + } + } + } + + override fun observeSeasons(seriesId: UUID): Flow> { + return seasonDao.observeSeasonsWithEpisodes(seriesId).map { relations -> + relations.map { it.toDomain() } + } + } + + override suspend fun deleteSeason(seasonId: UUID) { + withContext(Dispatchers.IO) { + seasonDao.deleteSeason(seasonId) + } + } + + override suspend fun upsertEpisode(episode: Episode) { + withContext(Dispatchers.IO) { + episodeDao.upsertEpisode(episode.toEntity()) + } + } + + override suspend fun upsertEpisodes(episodes: List) { + if (episodes.isEmpty()) return + withContext(Dispatchers.IO) { + episodeDao.upsertEpisodes(episodes.map { it.toEntity() }) + } + } + + override suspend fun getEpisode(episodeId: UUID): Episode? { + return withContext(Dispatchers.IO) { + episodeDao.getEpisodeById(episodeId)?.toDomain() + } + } + + override suspend fun deleteEpisode(episodeId: UUID) { + withContext(Dispatchers.IO) { + episodeDao.deleteEpisode(episodeId) + } + } + + private suspend fun upsertSeasonsInternal(seasons: List) { + if (seasons.isEmpty()) return + seasonDao.upsertSeasons(seasons.map { it.toEntity() }) + val episodes = seasons.flatMap { it.episodes } + if (episodes.isNotEmpty()) { + episodeDao.upsertEpisodes(episodes.map { it.toEntity() }) + } + } +} + +private fun SeriesEntity.toDomain(seasons: List): Series = Series( + id = id, + name = name, + synopsis = synopsis, + year = year, + heroImageUrl = heroImageUrl, + seasonCount = seasonCount, + seasons = seasons +) + +private fun SeasonEntity.toDomain(episodes: List): Season = Season( + id = id, + seriesId = seriesId, + name = name, + index = index, + episodeCount = episodeCount, + episodes = episodes +) + +private fun EpisodeEntity.toDomain(): Episode = Episode( + id = id, + seriesId = seriesId, + seasonId = seasonId, + title = title, + index = index, + releaseDate = releaseDate, + rating = rating, + runtime = runtime, + format = format, + synopsis = synopsis, + heroImageUrl = heroImageUrl, + cast = cast.map { it.toDomain() } +) + +private fun SeriesWithSeasonsAndEpisodes.toDomain(): Series = + series.toDomain(seasons.map { it.toDomain() }) + +private fun SeasonWithEpisodes.toDomain(): Season = + season.toDomain(episodes.map { it.toDomain() }) + +private fun Series.toEntity(): SeriesEntity = SeriesEntity( + id = id, + name = name, + synopsis = synopsis, + year = year, + heroImageUrl = heroImageUrl, + seasonCount = seasonCount +) + +private fun Season.toEntity(): SeasonEntity = SeasonEntity( + id = id, + seriesId = seriesId, + name = name, + index = index, + episodeCount = episodeCount +) + +private fun Episode.toEntity(): EpisodeEntity = EpisodeEntity( + id = id, + seriesId = seriesId, + seasonId = seasonId, + title = title, + index = index, + releaseDate = releaseDate, + rating = rating, + runtime = runtime, + format = format, + synopsis = synopsis, + heroImageUrl = heroImageUrl, + cast = cast.map { it.toEntity() } +) + +private fun EpisodeCastMemberEntity.toDomain(): CastMember = CastMember( + name = name, + role = role, + imageUrl = imageUrl +) + +private fun CastMember.toEntity(): EpisodeCastMemberEntity = EpisodeCastMemberEntity( + name = name, + role = role, + imageUrl = imageUrl +) diff --git a/app/src/main/java/hu/bbara/purefin/data/model/Episode.kt b/app/src/main/java/hu/bbara/purefin/data/model/Episode.kt index f6ea2f7..d5e06c8 100644 --- a/app/src/main/java/hu/bbara/purefin/data/model/Episode.kt +++ b/app/src/main/java/hu/bbara/purefin/data/model/Episode.kt @@ -5,6 +5,8 @@ import java.util.UUID data class Episode( val id: UUID, + val seriesId: UUID, + val seasonId: UUID, val title: String, val index: Int, val releaseDate: String, @@ -13,6 +15,5 @@ data class Episode( val format: String, val synopsis: String, val heroImageUrl: String, - val cast: List, - val season: Season, + val cast: List ) diff --git a/app/src/main/java/hu/bbara/purefin/data/model/Season.kt b/app/src/main/java/hu/bbara/purefin/data/model/Season.kt index d95a75f..f505580 100644 --- a/app/src/main/java/hu/bbara/purefin/data/model/Season.kt +++ b/app/src/main/java/hu/bbara/purefin/data/model/Season.kt @@ -4,9 +4,9 @@ import java.util.UUID data class Season( val id: UUID, + val seriesId: UUID, val name: String, val index: Int, val episodeCount: Int, - val episodes: List, - val series: Series -) \ No newline at end of file + val episodes: List +) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1d6e560..fe787bf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,6 +19,7 @@ foundation = "1.10.1" coil = "3.3.0" media3 = "1.9.0" nav3Core = "1.0.0" +room = "2.6.1" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -54,6 +55,9 @@ medi3-ui = { group = "androidx.media3", name = "media3-ui", version.ref = "media medi3-ui-compose = { group = "androidx.media3", name = "media3-ui-compose-material3", version.ref = "media3"} androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "nav3Core" } androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "nav3Core" } +androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } +androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } +androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } [plugins]