temp: room db refactor

This commit is contained in:
2026-02-21 13:37:53 +01:00
parent b21454c764
commit c0bd0b531d
27 changed files with 177 additions and 376 deletions

View File

@@ -1,95 +0,0 @@
package hu.bbara.purefin.core.data
import hu.bbara.purefin.core.data.local.room.OfflineRepository
import hu.bbara.purefin.core.data.local.room.OnlineRepository
import hu.bbara.purefin.core.data.session.UserSessionRepository
import hu.bbara.purefin.core.model.Episode
import hu.bbara.purefin.core.model.Library
import hu.bbara.purefin.core.model.Media
import hu.bbara.purefin.core.model.MediaRepositoryState
import hu.bbara.purefin.core.model.Movie
import hu.bbara.purefin.core.model.Series
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton
/**
* Active media repository that delegates to either online or offline repository
* based on user preference.
*/
@OptIn(ExperimentalCoroutinesApi::class)
@Singleton
class ActiveMediaRepository @Inject constructor(
@OnlineRepository private val onlineRepository: MediaRepository,
@OfflineRepository private val offlineRepository: MediaRepository,
private val userSessionRepository: UserSessionRepository
) : MediaRepository {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
// Switch between repositories based on offline mode preference
private val activeRepository: StateFlow<MediaRepository> =
userSessionRepository.isOfflineMode
.map { isOffline ->
if (isOffline) offlineRepository else onlineRepository
}
.stateIn(scope, SharingStarted.Eagerly, onlineRepository)
// Delegate all MediaRepository interface methods to the active repository
override val libraries: StateFlow<List<Library>> =
activeRepository.flatMapLatest { it.libraries }
.stateIn(scope, SharingStarted.Eagerly, emptyList())
override val movies: StateFlow<Map<UUID, Movie>> =
activeRepository.flatMapLatest { it.movies }
.stateIn(scope, SharingStarted.Eagerly, emptyMap())
override val series: StateFlow<Map<UUID, Series>> =
activeRepository.flatMapLatest { it.series }
.stateIn(scope, SharingStarted.Eagerly, emptyMap())
override val episodes: StateFlow<Map<UUID, Episode>> =
activeRepository.flatMapLatest { it.episodes }
.stateIn(scope, SharingStarted.Eagerly, emptyMap())
override val state: StateFlow<MediaRepositoryState> =
activeRepository.flatMapLatest { it.state }
.stateIn(scope, SharingStarted.Eagerly, MediaRepositoryState.Loading)
override val continueWatching: StateFlow<List<Media>> =
activeRepository.flatMapLatest { it.continueWatching }
.stateIn(scope, SharingStarted.Eagerly, emptyList())
override val nextUp: StateFlow<List<Media>> =
activeRepository.flatMapLatest { it.nextUp }
.stateIn(scope, SharingStarted.Eagerly, emptyList())
override val latestLibraryContent: StateFlow<Map<UUID, List<Media>>> =
activeRepository.flatMapLatest { it.latestLibraryContent }
.stateIn(scope, SharingStarted.Eagerly, emptyMap())
override fun observeSeriesWithContent(seriesId: UUID): Flow<Series?> =
activeRepository.flatMapLatest { it.observeSeriesWithContent(seriesId) }
override suspend fun ensureReady() {
activeRepository.value.ensureReady()
}
override suspend fun updateWatchProgress(mediaId: UUID, positionMs: Long, durationMs: Long) {
activeRepository.value.updateWatchProgress(mediaId, positionMs, durationMs)
}
override suspend fun refreshHomeData() {
activeRepository.value.refreshHomeData()
}
}

View File

@@ -5,9 +5,9 @@ import hu.bbara.purefin.core.data.cache.CachedMediaItem
import hu.bbara.purefin.core.data.cache.HomeCache
import hu.bbara.purefin.core.data.client.JellyfinApiClient
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
import hu.bbara.purefin.core.data.local.room.OfflineDatabase
import hu.bbara.purefin.core.data.local.room.OfflineRoomMediaLocalDataSource
import hu.bbara.purefin.core.data.local.room.RoomMediaLocalDataSource
import hu.bbara.purefin.core.data.room.OfflineDatabase
import hu.bbara.purefin.core.data.room.local.RoomMediaLocalDataSource
import hu.bbara.purefin.core.data.room.offline.OfflineRoomMediaLocalDataSource
import hu.bbara.purefin.core.data.session.UserSessionRepository
import hu.bbara.purefin.core.model.Episode
import hu.bbara.purefin.core.model.Library
@@ -185,7 +185,6 @@ class InMemoryMediaRepository @Inject constructor(
it.toLibrary()
}
localDataSource.saveLibraries(emptyLibraries)
offlineDataSource.saveLibraries(emptyLibraries)
val filledLibraries = emptyLibraries.map { library ->
return@map loadLibrary(library)

View File

@@ -4,22 +4,12 @@ import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import hu.bbara.purefin.core.data.local.room.OfflineRepository
import hu.bbara.purefin.core.data.local.room.OnlineRepository
@Module
@InstallIn(SingletonComponent::class)
abstract class MediaRepositoryModule {
@Binds
@OnlineRepository
abstract fun bindOnlineMediaRepository(impl: InMemoryMediaRepository): MediaRepository
abstract fun bindInMemoryMediaRepository(impl: InMemoryMediaRepository): MediaRepository
@Binds
@OfflineRepository
abstract fun bindOfflineMediaRepository(impl: OfflineMediaRepository): MediaRepository
// Default binding delegates to online/offline based on user preference
@Binds
abstract fun bindDefaultMediaRepository(impl: ActiveMediaRepository): MediaRepository
}

View File

@@ -1,21 +1,16 @@
package hu.bbara.purefin.core.data
import hu.bbara.purefin.core.data.local.room.OfflineDatabase
import hu.bbara.purefin.core.data.local.room.OfflineRoomMediaLocalDataSource
import hu.bbara.purefin.core.data.room.OfflineDatabase
import hu.bbara.purefin.core.data.room.offline.OfflineRoomMediaLocalDataSource
import hu.bbara.purefin.core.model.Episode
import hu.bbara.purefin.core.model.Library
import hu.bbara.purefin.core.model.Media
import hu.bbara.purefin.core.model.MediaRepositoryState
import hu.bbara.purefin.core.model.Movie
import hu.bbara.purefin.core.model.Series
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.stateIn
import java.util.UUID
import javax.inject.Inject
@@ -28,40 +23,23 @@ import javax.inject.Singleton
@Singleton
class OfflineMediaRepository @Inject constructor(
@OfflineDatabase private val localDataSource: OfflineRoomMediaLocalDataSource
) : MediaRepository {
) {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
// Offline repository is always ready (no network loading required)
private val _state: MutableStateFlow<MediaRepositoryState> = MutableStateFlow(MediaRepositoryState.Ready)
override val state: StateFlow<MediaRepositoryState> = _state.asStateFlow()
override val libraries: StateFlow<List<Library>> = localDataSource.librariesFlow
.stateIn(scope, SharingStarted.Eagerly, emptyList())
override val movies: StateFlow<Map<UUID, Movie>> = localDataSource.moviesFlow
val movies: StateFlow<Map<UUID, Movie>> = localDataSource.moviesFlow
.stateIn(scope, SharingStarted.Eagerly, emptyMap())
override val series: StateFlow<Map<UUID, Series>> = localDataSource.seriesFlow
val series: StateFlow<Map<UUID, Series>> = localDataSource.seriesFlow
.stateIn(scope, SharingStarted.Eagerly, emptyMap())
override val episodes: StateFlow<Map<UUID, Episode>> = localDataSource.episodesFlow
val episodes: StateFlow<Map<UUID, Episode>> = localDataSource.episodesFlow
.stateIn(scope, SharingStarted.Eagerly, emptyMap())
// Offline mode doesn't support these server-side features
override val continueWatching: StateFlow<List<Media>> = MutableStateFlow(emptyList())
override val nextUp: StateFlow<List<Media>> = MutableStateFlow(emptyList())
override val latestLibraryContent: StateFlow<Map<UUID, List<Media>>> = MutableStateFlow(emptyMap())
override fun observeSeriesWithContent(seriesId: UUID): Flow<Series?> {
fun observeSeriesWithContent(seriesId: UUID): Flow<Series?> {
return localDataSource.observeSeriesWithContent(seriesId)
}
override suspend fun ensureReady() {
// Offline repository is always ready - no initialization needed
}
override suspend fun updateWatchProgress(mediaId: UUID, positionMs: Long, durationMs: Long) {
suspend fun updateWatchProgress(mediaId: UUID, positionMs: Long, durationMs: Long) {
if (durationMs <= 0) return
val progressPercent = (positionMs.toDouble() / durationMs.toDouble()) * 100.0
val watched = progressPercent >= 90.0
@@ -69,7 +47,4 @@ class OfflineMediaRepository @Inject constructor(
localDataSource.updateWatchProgress(mediaId, progressPercent, watched)
}
override suspend fun refreshHomeData() {
// No-op for offline repository - no network refresh available
}
}

View File

@@ -1,33 +0,0 @@
package hu.bbara.purefin.core.data.local.room
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import hu.bbara.purefin.core.data.local.room.dao.CastMemberDao
import hu.bbara.purefin.core.data.local.room.dao.EpisodeDao
import hu.bbara.purefin.core.data.local.room.dao.LibraryDao
import hu.bbara.purefin.core.data.local.room.dao.MovieDao
import hu.bbara.purefin.core.data.local.room.dao.SeasonDao
import hu.bbara.purefin.core.data.local.room.dao.SeriesDao
@Database(
entities = [
MovieEntity::class,
SeriesEntity::class,
SeasonEntity::class,
EpisodeEntity::class,
LibraryEntity::class,
CastMemberEntity::class
],
version = 3,
exportSchema = false
)
@TypeConverters(UuidConverters::class)
abstract class MediaDatabase : RoomDatabase() {
abstract fun movieDao(): MovieDao
abstract fun seriesDao(): SeriesDao
abstract fun seasonDao(): SeasonDao
abstract fun episodeDao(): EpisodeDao
abstract fun libraryDao(): LibraryDao
abstract fun castMemberDao(): CastMemberDao
}

View File

@@ -1,33 +0,0 @@
package hu.bbara.purefin.core.data.local.room
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import hu.bbara.purefin.core.data.local.room.dao.CastMemberDao
import hu.bbara.purefin.core.data.local.room.dao.EpisodeDao
import hu.bbara.purefin.core.data.local.room.dao.LibraryDao
import hu.bbara.purefin.core.data.local.room.dao.MovieDao
import hu.bbara.purefin.core.data.local.room.dao.SeasonDao
import hu.bbara.purefin.core.data.local.room.dao.SeriesDao
@Database(
entities = [
MovieEntity::class,
SeriesEntity::class,
SeasonEntity::class,
EpisodeEntity::class,
LibraryEntity::class,
CastMemberEntity::class
],
version = 3,
exportSchema = false
)
@TypeConverters(UuidConverters::class)
abstract class OfflineMediaDatabase : RoomDatabase() {
abstract fun movieDao(): MovieDao
abstract fun seriesDao(): SeriesDao
abstract fun seasonDao(): SeasonDao
abstract fun episodeDao(): EpisodeDao
abstract fun libraryDao(): LibraryDao
abstract fun castMemberDao(): CastMemberDao
}

View File

@@ -1,4 +1,4 @@
package hu.bbara.purefin.core.data.local.room
package hu.bbara.purefin.core.data.room
import javax.inject.Qualifier

View File

@@ -1,4 +1,4 @@
package hu.bbara.purefin.core.data.local.room
package hu.bbara.purefin.core.data.room
import android.content.Context
import androidx.room.Room
@@ -7,12 +7,16 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import hu.bbara.purefin.core.data.local.room.dao.CastMemberDao
import hu.bbara.purefin.core.data.local.room.dao.EpisodeDao
import hu.bbara.purefin.core.data.local.room.dao.LibraryDao
import hu.bbara.purefin.core.data.local.room.dao.MovieDao
import hu.bbara.purefin.core.data.local.room.dao.SeasonDao
import hu.bbara.purefin.core.data.local.room.dao.SeriesDao
import hu.bbara.purefin.core.data.room.dao.CastMemberDao
import hu.bbara.purefin.core.data.room.dao.EpisodeDao
import hu.bbara.purefin.core.data.room.dao.LibraryDao
import hu.bbara.purefin.core.data.room.dao.MovieDao
import hu.bbara.purefin.core.data.room.dao.SeasonDao
import hu.bbara.purefin.core.data.room.dao.SeriesDao
import hu.bbara.purefin.core.data.room.local.MediaDatabase
import hu.bbara.purefin.core.data.room.local.RoomMediaLocalDataSource
import hu.bbara.purefin.core.data.room.offline.OfflineMediaDatabase
import hu.bbara.purefin.core.data.room.offline.OfflineRoomMediaLocalDataSource
import javax.inject.Singleton
@Module
@@ -77,13 +81,6 @@ object MediaDatabaseModule {
@OfflineDatabase
fun provideOfflineEpisodeDao(@OfflineDatabase db: OfflineMediaDatabase) = db.episodeDao()
@Provides
@OfflineDatabase
fun provideOfflineCastMemberDao(@OfflineDatabase db: OfflineMediaDatabase) = db.castMemberDao()
@Provides
@OfflineDatabase
fun provideOfflineLibraryDao(@OfflineDatabase db: OfflineMediaDatabase) = db.libraryDao()
// Data Sources
@Provides
@@ -109,11 +106,9 @@ object MediaDatabaseModule {
@OfflineDatabase movieDao: MovieDao,
@OfflineDatabase seriesDao: SeriesDao,
@OfflineDatabase seasonDao: SeasonDao,
@OfflineDatabase episodeDao: EpisodeDao,
@OfflineDatabase castMemberDao: CastMemberDao,
@OfflineDatabase libraryDao: LibraryDao
@OfflineDatabase episodeDao: EpisodeDao
): OfflineRoomMediaLocalDataSource = OfflineRoomMediaLocalDataSource(
database, movieDao, seriesDao, seasonDao, episodeDao, castMemberDao, libraryDao
database, movieDao, seriesDao, seasonDao, episodeDao
)
// Default (unqualified) data source for backward compatibility

View File

@@ -1,7 +1,12 @@
package hu.bbara.purefin.core.data.local.room
package hu.bbara.purefin.core.data.room
import androidx.room.Embedded
import androidx.room.Relation
import hu.bbara.purefin.core.data.room.entity.EpisodeEntity
import hu.bbara.purefin.core.data.room.entity.LibraryEntity
import hu.bbara.purefin.core.data.room.entity.MovieEntity
import hu.bbara.purefin.core.data.room.entity.SeasonEntity
import hu.bbara.purefin.core.data.room.entity.SeriesEntity
data class SeasonWithEpisodes(
@Embedded val season: SeasonEntity,

View File

@@ -1,4 +1,4 @@
package hu.bbara.purefin.core.data.local.room
package hu.bbara.purefin.core.data.room
import androidx.room.TypeConverter
import java.util.UUID

View File

@@ -1,9 +1,9 @@
package hu.bbara.purefin.core.data.local.room.dao
package hu.bbara.purefin.core.data.room.dao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Upsert
import hu.bbara.purefin.core.data.local.room.CastMemberEntity
import hu.bbara.purefin.core.data.room.entity.CastMemberEntity
import java.util.UUID
@Dao

View File

@@ -1,9 +1,9 @@
package hu.bbara.purefin.core.data.local.room.dao
package hu.bbara.purefin.core.data.room.dao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Upsert
import hu.bbara.purefin.core.data.local.room.EpisodeEntity
import hu.bbara.purefin.core.data.room.entity.EpisodeEntity
import kotlinx.coroutines.flow.Flow
import java.util.UUID

View File

@@ -1,10 +1,10 @@
package hu.bbara.purefin.core.data.local.room.dao
package hu.bbara.purefin.core.data.room.dao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Upsert
import hu.bbara.purefin.core.data.local.room.LibraryEntity
import hu.bbara.purefin.core.data.local.room.LibraryWithContent
import hu.bbara.purefin.core.data.room.LibraryWithContent
import hu.bbara.purefin.core.data.room.entity.LibraryEntity
import kotlinx.coroutines.flow.Flow
@Dao

View File

@@ -1,9 +1,9 @@
package hu.bbara.purefin.core.data.local.room.dao
package hu.bbara.purefin.core.data.room.dao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Upsert
import hu.bbara.purefin.core.data.local.room.MovieEntity
import hu.bbara.purefin.core.data.room.entity.MovieEntity
import kotlinx.coroutines.flow.Flow
import java.util.UUID

View File

@@ -1,9 +1,9 @@
package hu.bbara.purefin.core.data.local.room.dao
package hu.bbara.purefin.core.data.room.dao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Upsert
import hu.bbara.purefin.core.data.local.room.SeasonEntity
import hu.bbara.purefin.core.data.room.entity.SeasonEntity
import java.util.UUID
@Dao

View File

@@ -1,11 +1,11 @@
package hu.bbara.purefin.core.data.local.room.dao
package hu.bbara.purefin.core.data.room.dao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Upsert
import hu.bbara.purefin.core.data.local.room.SeriesEntity
import hu.bbara.purefin.core.data.local.room.SeriesWithSeasonsAndEpisodes
import hu.bbara.purefin.core.data.room.SeriesWithSeasonsAndEpisodes
import hu.bbara.purefin.core.data.room.entity.SeriesEntity
import kotlinx.coroutines.flow.Flow
import java.util.UUID

View File

@@ -1,4 +1,4 @@
package hu.bbara.purefin.core.data.local.room
package hu.bbara.purefin.core.data.room.entity
import androidx.room.Entity
import androidx.room.Index

View File

@@ -1,4 +1,4 @@
package hu.bbara.purefin.core.data.local.room
package hu.bbara.purefin.core.data.room.entity
import androidx.room.Entity
import androidx.room.ForeignKey

View File

@@ -1,4 +1,4 @@
package hu.bbara.purefin.core.data.local.room
package hu.bbara.purefin.core.data.room.entity
import androidx.room.Entity
import androidx.room.PrimaryKey

View File

@@ -1,4 +1,4 @@
package hu.bbara.purefin.core.data.local.room
package hu.bbara.purefin.core.data.room.entity
import androidx.room.Entity
import androidx.room.ForeignKey

View File

@@ -1,4 +1,4 @@
package hu.bbara.purefin.core.data.local.room
package hu.bbara.purefin.core.data.room.entity
import androidx.room.Entity
import androidx.room.ForeignKey
@@ -20,7 +20,7 @@ import java.util.UUID
)
data class SeasonEntity(
@PrimaryKey val id: UUID,
val seriesId: UUID,
val seriesId: UUID?,
val name: String,
val index: Int,
val unwatchedEpisodeCount: Int,

View File

@@ -1,4 +1,4 @@
package hu.bbara.purefin.core.data.local.room
package hu.bbara.purefin.core.data.room.entity
import androidx.room.Entity
import androidx.room.ForeignKey
@@ -12,14 +12,15 @@ import java.util.UUID
ForeignKey(
entity = LibraryEntity::class,
parentColumns = ["id"],
childColumns = ["libraryId"]
childColumns = ["libraryId"],
deferred = true
),
],
indices = [Index("libraryId")]
)
data class SeriesEntity(
@PrimaryKey val id: UUID,
val libraryId: UUID,
val libraryId: UUID?,
val name: String,
val synopsis: String,
val year: String,

View File

@@ -0,0 +1,40 @@
package hu.bbara.purefin.core.data.room.local
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import hu.bbara.purefin.core.data.room.UuidConverters
import hu.bbara.purefin.core.data.room.dao.CastMemberDao
import hu.bbara.purefin.core.data.room.dao.EpisodeDao
import hu.bbara.purefin.core.data.room.dao.LibraryDao
import hu.bbara.purefin.core.data.room.dao.MovieDao
import hu.bbara.purefin.core.data.room.dao.SeasonDao
import hu.bbara.purefin.core.data.room.dao.SeriesDao
import hu.bbara.purefin.core.data.room.entity.CastMemberEntity
import hu.bbara.purefin.core.data.room.entity.EpisodeEntity
import hu.bbara.purefin.core.data.room.entity.LibraryEntity
import hu.bbara.purefin.core.data.room.entity.MovieEntity
import hu.bbara.purefin.core.data.room.entity.SeasonEntity
import hu.bbara.purefin.core.data.room.entity.SeriesEntity
@Database(
entities = [
MovieEntity::class,
SeriesEntity::class,
SeasonEntity::class,
EpisodeEntity::class,
LibraryEntity::class,
CastMemberEntity::class
],
version = 3,
exportSchema = false
)
@TypeConverters(UuidConverters::class)
abstract class MediaDatabase : RoomDatabase() {
abstract fun movieDao(): MovieDao
abstract fun seriesDao(): SeriesDao
abstract fun seasonDao(): SeasonDao
abstract fun episodeDao(): EpisodeDao
abstract fun libraryDao(): LibraryDao
abstract fun castMemberDao(): CastMemberDao
}

View File

@@ -1,12 +1,18 @@
package hu.bbara.purefin.core.data.local.room
package hu.bbara.purefin.core.data.room.local
import androidx.room.withTransaction
import hu.bbara.purefin.core.data.local.room.dao.CastMemberDao
import hu.bbara.purefin.core.data.local.room.dao.EpisodeDao
import hu.bbara.purefin.core.data.local.room.dao.LibraryDao
import hu.bbara.purefin.core.data.local.room.dao.MovieDao
import hu.bbara.purefin.core.data.local.room.dao.SeasonDao
import hu.bbara.purefin.core.data.local.room.dao.SeriesDao
import hu.bbara.purefin.core.data.room.dao.CastMemberDao
import hu.bbara.purefin.core.data.room.dao.EpisodeDao
import hu.bbara.purefin.core.data.room.dao.LibraryDao
import hu.bbara.purefin.core.data.room.dao.MovieDao
import hu.bbara.purefin.core.data.room.dao.SeasonDao
import hu.bbara.purefin.core.data.room.dao.SeriesDao
import hu.bbara.purefin.core.data.room.entity.CastMemberEntity
import hu.bbara.purefin.core.data.room.entity.EpisodeEntity
import hu.bbara.purefin.core.data.room.entity.LibraryEntity
import hu.bbara.purefin.core.data.room.entity.MovieEntity
import hu.bbara.purefin.core.data.room.entity.SeasonEntity
import hu.bbara.purefin.core.data.room.entity.SeriesEntity
import hu.bbara.purefin.core.model.CastMember
import hu.bbara.purefin.core.model.Episode
import hu.bbara.purefin.core.model.Library

View File

@@ -0,0 +1,32 @@
package hu.bbara.purefin.core.data.room.offline
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import hu.bbara.purefin.core.data.room.UuidConverters
import hu.bbara.purefin.core.data.room.dao.EpisodeDao
import hu.bbara.purefin.core.data.room.dao.MovieDao
import hu.bbara.purefin.core.data.room.dao.SeasonDao
import hu.bbara.purefin.core.data.room.dao.SeriesDao
import hu.bbara.purefin.core.data.room.entity.EpisodeEntity
import hu.bbara.purefin.core.data.room.entity.MovieEntity
import hu.bbara.purefin.core.data.room.entity.SeasonEntity
import hu.bbara.purefin.core.data.room.entity.SeriesEntity
@Database(
entities = [
MovieEntity::class,
SeriesEntity::class,
SeasonEntity::class,
EpisodeEntity::class,
],
version = 3,
exportSchema = false
)
@TypeConverters(UuidConverters::class)
abstract class OfflineMediaDatabase : RoomDatabase() {
abstract fun movieDao(): MovieDao
abstract fun seriesDao(): SeriesDao
abstract fun seasonDao(): SeasonDao
abstract fun episodeDao(): EpisodeDao
}

View File

@@ -1,21 +1,20 @@
package hu.bbara.purefin.core.data.local.room
package hu.bbara.purefin.core.data.room.offline
import androidx.room.withTransaction
import hu.bbara.purefin.core.data.local.room.dao.CastMemberDao
import hu.bbara.purefin.core.data.local.room.dao.EpisodeDao
import hu.bbara.purefin.core.data.local.room.dao.LibraryDao
import hu.bbara.purefin.core.data.local.room.dao.MovieDao
import hu.bbara.purefin.core.data.local.room.dao.SeasonDao
import hu.bbara.purefin.core.data.local.room.dao.SeriesDao
import hu.bbara.purefin.core.model.CastMember
import hu.bbara.purefin.core.data.room.dao.EpisodeDao
import hu.bbara.purefin.core.data.room.dao.MovieDao
import hu.bbara.purefin.core.data.room.dao.SeasonDao
import hu.bbara.purefin.core.data.room.dao.SeriesDao
import hu.bbara.purefin.core.data.room.entity.EpisodeEntity
import hu.bbara.purefin.core.data.room.entity.MovieEntity
import hu.bbara.purefin.core.data.room.entity.SeasonEntity
import hu.bbara.purefin.core.data.room.entity.SeriesEntity
import hu.bbara.purefin.core.model.Episode
import hu.bbara.purefin.core.model.Library
import hu.bbara.purefin.core.model.Movie
import hu.bbara.purefin.core.model.Season
import hu.bbara.purefin.core.model.Series
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import org.jellyfin.sdk.model.api.CollectionType
import java.util.UUID
import javax.inject.Singleton
@@ -26,31 +25,18 @@ class OfflineRoomMediaLocalDataSource(
private val seriesDao: SeriesDao,
private val seasonDao: SeasonDao,
private val episodeDao: EpisodeDao,
private val castMemberDao: CastMemberDao,
private val libraryDao: LibraryDao
) {
// Lightweight Flows for list screens (home, library)
val librariesFlow: Flow<List<Library>> = libraryDao.observeAllWithContent()
.map { relation ->
relation.map {
it.library.toDomain(
movies = it.movies.map { e -> e.toDomain(cast = emptyList()) },
series = it.series.map { e -> e.toDomain(seasons = emptyList(), cast = emptyList()) }
)
}
}
val moviesFlow: Flow<Map<UUID, Movie>> = movieDao.observeAll()
.map { entities -> entities.associate { it.id to it.toDomain(cast = emptyList()) } }
.map { entities -> entities.associate { it.id to it.toDomain() } }
val seriesFlow: Flow<Map<UUID, Series>> = seriesDao.observeAll()
.map { entities ->
entities.associate { it.id to it.toDomain(seasons = emptyList(), cast = emptyList()) }
entities.associate { it.id to it.toDomain(seasons = emptyList()) }
}
val episodesFlow: Flow<Map<UUID, Episode>> = episodeDao.observeAll()
.map { entities -> entities.associate { it.id to it.toDomain(cast = emptyList()) } }
.map { entities -> entities.associate { it.id to it.toDomain() } }
// Full content Flow for series detail screen (scoped to one series)
fun observeSeriesWithContent(seriesId: UUID): Flow<Series?> =
@@ -59,21 +45,12 @@ class OfflineRoomMediaLocalDataSource(
it.series.toDomain(
seasons = it.seasons.map { swe ->
swe.season.toDomain(
episodes = swe.episodes.map { ep -> ep.toDomain(cast = emptyList()) }
episodes = swe.episodes.map { ep -> ep.toDomain() }
)
},
cast = emptyList()
)
} )
}
}
suspend fun saveLibraries(libraries: List<Library>) {
database.withTransaction {
libraryDao.deleteAll()
libraryDao.upsertAll(libraries.map { it.toEntity() })
}
}
suspend fun saveMovies(movies: List<Movie>) {
database.withTransaction {
movieDao.upsertAll(movies.map { it.toEntity() })
@@ -115,15 +92,13 @@ class OfflineRoomMediaLocalDataSource(
suspend fun getMovies(): List<Movie> {
val movies = movieDao.getAll()
return movies.map { entity ->
val cast = castMemberDao.getByMovieId(entity.id).map { it.toDomain() }
entity.toDomain(cast)
entity.toDomain()
}
}
suspend fun getMovie(id: UUID): Movie? {
val entity = movieDao.getById(id) ?: return null
val cast = castMemberDao.getByMovieId(id).map { it.toDomain() }
return entity.toDomain(cast)
return entity.toDomain()
}
suspend fun getSeries(): List<Series> {
@@ -136,24 +111,21 @@ class OfflineRoomMediaLocalDataSource(
private suspend fun getSeriesInternal(id: UUID, includeContent: Boolean): Series? {
val entity = seriesDao.getById(id) ?: return null
val cast = castMemberDao.getBySeriesId(id).map { it.toDomain() }
val seasons = if (includeContent) {
seasonDao.getBySeriesId(id).map { seasonEntity ->
val episodes = episodeDao.getBySeasonId(seasonEntity.id).map { episodeEntity ->
val episodeCast = castMemberDao.getByEpisodeId(episodeEntity.id).map { it.toDomain() }
episodeEntity.toDomain(episodeCast)
episodeEntity.toDomain()
}
seasonEntity.toDomain(episodes)
}
} else emptyList()
return entity.toDomain(seasons, cast)
return entity.toDomain(seasons)
}
suspend fun getSeason(seriesId: UUID, seasonId: UUID): Season? {
val seasonEntity = seasonDao.getById(seasonId) ?: return null
val episodes = episodeDao.getBySeasonId(seasonId).map { episodeEntity ->
val episodeCast = castMemberDao.getByEpisodeId(episodeEntity.id).map { it.toDomain() }
episodeEntity.toDomain(episodeCast)
episodeEntity.toDomain()
}
return seasonEntity.toDomain(episodes)
}
@@ -161,8 +133,7 @@ class OfflineRoomMediaLocalDataSource(
suspend fun getSeasons(seriesId: UUID): List<Season> {
return seasonDao.getBySeriesId(seriesId).map { seasonEntity ->
val episodes = episodeDao.getBySeasonId(seasonEntity.id).map { episodeEntity ->
val episodeCast = castMemberDao.getByEpisodeId(episodeEntity.id).map { it.toDomain() }
episodeEntity.toDomain(episodeCast)
episodeEntity.toDomain()
}
seasonEntity.toDomain(episodes)
}
@@ -170,14 +141,12 @@ class OfflineRoomMediaLocalDataSource(
suspend fun getEpisode(seriesId: UUID, seasonId: UUID, episodeId: UUID): Episode? {
val episodeEntity = episodeDao.getById(episodeId) ?: return null
val cast = castMemberDao.getByEpisodeId(episodeId).map { it.toDomain() }
return episodeEntity.toDomain(cast)
return episodeEntity.toDomain()
}
suspend fun getEpisodeById(episodeId: UUID): Episode? {
val episodeEntity = episodeDao.getById(episodeId) ?: return null
val cast = castMemberDao.getByEpisodeId(episodeId).map { it.toDomain() }
return episodeEntity.toDomain(cast)
return episodeEntity.toDomain()
}
suspend fun updateWatchProgress(mediaId: UUID, progress: Double?, watched: Boolean) {
@@ -199,33 +168,10 @@ class OfflineRoomMediaLocalDataSource(
suspend fun getEpisodesBySeries(seriesId: UUID): List<Episode> {
return episodeDao.getBySeriesId(seriesId).map { episodeEntity ->
val cast = castMemberDao.getByEpisodeId(episodeEntity.id).map { it.toDomain() }
episodeEntity.toDomain(cast)
episodeEntity.toDomain()
}
}
private fun Library.toEntity() = LibraryEntity(
id = id,
name = name,
type = when (type) {
CollectionType.MOVIES -> "MOVIES"
CollectionType.TVSHOWS -> "TVSHOWS"
else -> throw UnsupportedOperationException("Unsupported library type: $type")
}
)
private fun LibraryEntity.toDomain(series: List<Series>, movies: List<Movie>) = Library(
id = id,
name = name,
type = when (type) {
"MOVIES" -> CollectionType.MOVIES
"TVSHOWS" -> CollectionType.TVSHOWS
else -> throw UnsupportedOperationException("Unsupported library type: $type")
},
movies = if (type == "MOVIES") movies else null,
series = if (type == "TVSHOWS") series else null,
)
private fun Movie.toEntity() = MovieEntity(
id = id,
libraryId = libraryId,
@@ -278,7 +224,7 @@ class OfflineRoomMediaLocalDataSource(
heroImageUrl = heroImageUrl
)
private fun MovieEntity.toDomain(cast: List<CastMember>) = Movie(
private fun MovieEntity.toDomain() = Movie(
id = id,
libraryId = libraryId,
title = title,
@@ -292,10 +238,10 @@ class OfflineRoomMediaLocalDataSource(
heroImageUrl = heroImageUrl,
audioTrack = audioTrack,
subtitles = subtitles,
cast = cast
cast = emptyList()
)
private fun SeriesEntity.toDomain(seasons: List<Season>, cast: List<CastMember>) = Series(
private fun SeriesEntity.toDomain(seasons: List<Season>) = Series(
id = id,
libraryId = libraryId,
name = name,
@@ -305,7 +251,7 @@ class OfflineRoomMediaLocalDataSource(
unwatchedEpisodeCount = unwatchedEpisodeCount,
seasonCount = seasonCount,
seasons = seasons,
cast = cast
cast = emptyList()
)
private fun SeasonEntity.toDomain(episodes: List<Episode>) = Season(
@@ -318,7 +264,7 @@ class OfflineRoomMediaLocalDataSource(
episodes = episodes
)
private fun EpisodeEntity.toDomain(cast: List<CastMember>) = Episode(
private fun EpisodeEntity.toDomain() = Episode(
id = id,
seriesId = seriesId,
seasonId = seasonId,
@@ -332,33 +278,6 @@ class OfflineRoomMediaLocalDataSource(
watched = watched,
format = format,
heroImageUrl = heroImageUrl,
cast = cast
)
private fun CastMember.toMovieEntity(movieId: UUID) = CastMemberEntity(
name = name,
role = role,
imageUrl = imageUrl,
movieId = movieId
)
private fun CastMember.toSeriesEntity(seriesId: UUID) = CastMemberEntity(
name = name,
role = role,
imageUrl = imageUrl,
seriesId = seriesId
)
private fun CastMember.toEpisodeEntity(episodeId: UUID) = CastMemberEntity(
name = name,
role = role,
imageUrl = imageUrl,
episodeId = episodeId
)
private fun CastMemberEntity.toDomain() = CastMember(
name = name,
role = role,
imageUrl = imageUrl
cast = emptyList()
)
}

View File

@@ -11,9 +11,9 @@ import androidx.media3.exoplayer.offline.DownloadRequest
import dagger.hilt.android.qualifiers.ApplicationContext
import hu.bbara.purefin.core.data.client.JellyfinApiClient
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
import hu.bbara.purefin.core.data.local.room.OfflineDatabase
import hu.bbara.purefin.core.data.local.room.OfflineRoomMediaLocalDataSource
import hu.bbara.purefin.core.data.local.room.dao.MovieDao
import hu.bbara.purefin.core.data.room.OfflineDatabase
import hu.bbara.purefin.core.data.room.dao.MovieDao
import hu.bbara.purefin.core.data.room.offline.OfflineRoomMediaLocalDataSource
import hu.bbara.purefin.core.data.session.UserSessionRepository
import hu.bbara.purefin.core.model.Movie
import kotlinx.coroutines.Dispatchers