diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/ActiveMediaRepository.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/ActiveMediaRepository.kt deleted file mode 100644 index c46ad13..0000000 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/ActiveMediaRepository.kt +++ /dev/null @@ -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 = - 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> = - activeRepository.flatMapLatest { it.libraries } - .stateIn(scope, SharingStarted.Eagerly, emptyList()) - - override val movies: StateFlow> = - activeRepository.flatMapLatest { it.movies } - .stateIn(scope, SharingStarted.Eagerly, emptyMap()) - - override val series: StateFlow> = - activeRepository.flatMapLatest { it.series } - .stateIn(scope, SharingStarted.Eagerly, emptyMap()) - - override val episodes: StateFlow> = - activeRepository.flatMapLatest { it.episodes } - .stateIn(scope, SharingStarted.Eagerly, emptyMap()) - - override val state: StateFlow = - activeRepository.flatMapLatest { it.state } - .stateIn(scope, SharingStarted.Eagerly, MediaRepositoryState.Loading) - - override val continueWatching: StateFlow> = - activeRepository.flatMapLatest { it.continueWatching } - .stateIn(scope, SharingStarted.Eagerly, emptyList()) - - override val nextUp: StateFlow> = - activeRepository.flatMapLatest { it.nextUp } - .stateIn(scope, SharingStarted.Eagerly, emptyList()) - - override val latestLibraryContent: StateFlow>> = - activeRepository.flatMapLatest { it.latestLibraryContent } - .stateIn(scope, SharingStarted.Eagerly, emptyMap()) - - override fun observeSeriesWithContent(seriesId: UUID): Flow = - 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() - } -} diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/InMemoryMediaRepository.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/InMemoryMediaRepository.kt index c49bf38..de291ba 100644 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/InMemoryMediaRepository.kt +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/InMemoryMediaRepository.kt @@ -5,9 +5,6 @@ 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.session.UserSessionRepository import hu.bbara.purefin.core.model.Episode import hu.bbara.purefin.core.model.Library @@ -22,11 +19,11 @@ 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.first -import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import org.jellyfin.sdk.model.api.BaseItemDto @@ -45,8 +42,6 @@ import javax.inject.Singleton class InMemoryMediaRepository @Inject constructor( val userSessionRepository: UserSessionRepository, val jellyfinApiClient: JellyfinApiClient, - private val localDataSource: RoomMediaLocalDataSource, - @OfflineDatabase private val offlineDataSource: OfflineRoomMediaLocalDataSource, private val homeCacheDataStore: DataStore ) : MediaRepository { @@ -58,23 +53,24 @@ class InMemoryMediaRepository @Inject constructor( private val _state: MutableStateFlow = MutableStateFlow(MediaRepositoryState.Loading) override val state: StateFlow = _state.asStateFlow() - override val libraries: StateFlow> = localDataSource.librariesFlow - .stateIn(scope, SharingStarted.Eagerly, emptyList()) - override val movies: StateFlow> = localDataSource.moviesFlow - .stateIn(scope, SharingStarted.Eagerly, emptyMap()) + private val _libraries: MutableStateFlow> = MutableStateFlow(emptyList()) + override val libraries: StateFlow> = _libraries.asStateFlow() - override val series: StateFlow> = localDataSource.seriesFlow - .stateIn(scope, SharingStarted.Eagerly, emptyMap()) + private val _movies: MutableStateFlow> = MutableStateFlow(emptyMap()) + override val movies: StateFlow> = _movies.asStateFlow() - override val episodes: StateFlow> = localDataSource.episodesFlow - .stateIn(scope, SharingStarted.Eagerly, emptyMap()) + private val _series: MutableStateFlow> = MutableStateFlow(emptyMap()) + override val series: StateFlow> = _series.asStateFlow() + + private val _episodes: MutableStateFlow> = MutableStateFlow(emptyMap()) + override val episodes: StateFlow> = _episodes.asStateFlow() override fun observeSeriesWithContent(seriesId: UUID): Flow { scope.launch { awaitReady() ensureSeriesContentLoaded(seriesId) } - return localDataSource.observeSeriesWithContent(seriesId) + return _series.map { it[seriesId] } } private val _continueWatching: MutableStateFlow> = MutableStateFlow(emptyList()) @@ -181,20 +177,19 @@ class InMemoryMediaRepository @Inject constructor( //TODO add support for playlists val filteredLibraries = librariesItem.filter { it.collectionType == CollectionType.MOVIES || it.collectionType == CollectionType.TVSHOWS } - val emptyLibraries = filteredLibraries.map { - it.toLibrary() - } - localDataSource.saveLibraries(emptyLibraries) - offlineDataSource.saveLibraries(emptyLibraries) + val emptyLibraries = filteredLibraries.map { it.toLibrary() } + _libraries.value = emptyLibraries val filledLibraries = emptyLibraries.map { library -> return@map loadLibrary(library) } + _libraries.value = filledLibraries + val movies = filledLibraries.filter { it.type == CollectionType.MOVIES }.flatMap { it.movies.orEmpty() } - localDataSource.saveMovies(movies) + _movies.update { current -> current + movies.associateBy { it.id } } val series = filledLibraries.filter { it.type == CollectionType.TVSHOWS }.flatMap { it.series.orEmpty() } - localDataSource.saveSeries(series) + _series.update { current -> current + series.associateBy { it.id } } } suspend fun loadLibrary(library: Library): Library { @@ -216,7 +211,7 @@ class InMemoryMediaRepository @Inject constructor( val movieItem = jellyfinApiClient.getItemInfo(movie.id) ?: throw RuntimeException("Movie not found") val updatedMovie = movieItem.toMovie(serverUrl(), movie.libraryId) - localDataSource.saveMovies(listOf(updatedMovie)) + _movies.update { it + (updatedMovie.id to updatedMovie) } return updatedMovie } @@ -224,7 +219,7 @@ class InMemoryMediaRepository @Inject constructor( val seriesItem = jellyfinApiClient.getItemInfo(series.id) ?: throw RuntimeException("Series not found") val updatedSeries = seriesItem.toSeries(serverUrl(), series.libraryId) - localDataSource.saveSeries(listOf(updatedSeries)) + _series.update { it + (updatedSeries.id to updatedSeries) } return updatedSeries } @@ -247,7 +242,7 @@ class InMemoryMediaRepository @Inject constructor( when (item.type) { BaseItemKind.EPISODE -> { val episode = item.toEpisode(serverUrl()) - localDataSource.saveEpisode(episode) + _episodes.update { it + (episode.id to episode) } } else -> { /* Do nothing */ } } @@ -267,7 +262,7 @@ class InMemoryMediaRepository @Inject constructor( // Load episodes nextUpItems.forEach { item -> val episode = item.toEpisode(serverUrl()) - localDataSource.saveEpisode(episode) + _episodes.update { it + (episode.id to episode) } } } @@ -313,10 +308,8 @@ class InMemoryMediaRepository @Inject constructor( private suspend fun ensureSeriesContentLoaded(seriesId: UUID) { awaitReady() - // Skip if content is already cached in Room - localDataSource.getSeriesWithContent(seriesId)?.takeIf { it.seasons.isNotEmpty() }?.let { - return - } + // Skip if content is already loaded in-memory + _series.value[seriesId]?.takeIf { it.seasons.isNotEmpty() }?.let { return } val series = this.series.value[seriesId] ?: throw RuntimeException("Series not found") @@ -328,16 +321,30 @@ class InMemoryMediaRepository @Inject constructor( season.copy(episodes = episodes) } val updatedSeries = series.copy(seasons = filledSeasons) - localDataSource.saveSeries(listOf(updatedSeries)) - localDataSource.saveSeriesContent(updatedSeries) + _series.update { it + (updatedSeries.id to updatedSeries) } + + val allEpisodes = filledSeasons.flatMap { it.episodes } + _episodes.update { current -> current + allEpisodes.associateBy { it.id } } } override 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 - // Write to Room — the reactive Flows propagate changes to UI automatically - localDataSource.updateWatchProgress(mediaId, progressPercent, watched) + + if (_movies.value.containsKey(mediaId)) { + _movies.update { current -> + val movie = current[mediaId] ?: return@update current + current + (mediaId to movie.copy(progress = progressPercent, watched = watched)) + } + return + } + if (_episodes.value.containsKey(mediaId)) { + _episodes.update { current -> + val episode = current[mediaId] ?: return@update current + current + (mediaId to episode.copy(progress = progressPercent, watched = watched)) + } + } } companion object { diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/MediaRepositoryModule.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/MediaRepositoryModule.kt index e80cb06..e219da8 100644 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/MediaRepositoryModule.kt +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/MediaRepositoryModule.kt @@ -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 } diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/OfflineMediaRepository.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/OfflineMediaRepository.kt index 1875f38..eb46d2a 100644 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/OfflineMediaRepository.kt +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/OfflineMediaRepository.kt @@ -1,21 +1,15 @@ 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.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 @@ -27,41 +21,24 @@ import javax.inject.Singleton */ @Singleton class OfflineMediaRepository @Inject constructor( - @OfflineDatabase private val localDataSource: OfflineRoomMediaLocalDataSource -) : MediaRepository { - + private val localDataSource: OfflineRoomMediaLocalDataSource +) { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - // Offline repository is always ready (no network loading required) - private val _state: MutableStateFlow = MutableStateFlow(MediaRepositoryState.Ready) - override val state: StateFlow = _state.asStateFlow() - - override val libraries: StateFlow> = localDataSource.librariesFlow - .stateIn(scope, SharingStarted.Eagerly, emptyList()) - - override val movies: StateFlow> = localDataSource.moviesFlow + val movies: StateFlow> = localDataSource.moviesFlow .stateIn(scope, SharingStarted.Eagerly, emptyMap()) - override val series: StateFlow> = localDataSource.seriesFlow + val series: StateFlow> = localDataSource.seriesFlow .stateIn(scope, SharingStarted.Eagerly, emptyMap()) - override val episodes: StateFlow> = localDataSource.episodesFlow + val episodes: StateFlow> = localDataSource.episodesFlow .stateIn(scope, SharingStarted.Eagerly, emptyMap()) - // Offline mode doesn't support these server-side features - override val continueWatching: StateFlow> = MutableStateFlow(emptyList()) - override val nextUp: StateFlow> = MutableStateFlow(emptyList()) - override val latestLibraryContent: StateFlow>> = MutableStateFlow(emptyMap()) - - override fun observeSeriesWithContent(seriesId: UUID): Flow { + fun observeSeriesWithContent(seriesId: UUID): Flow { 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 +46,4 @@ class OfflineMediaRepository @Inject constructor( localDataSource.updateWatchProgress(mediaId, progressPercent, watched) } - override suspend fun refreshHomeData() { - // No-op for offline repository - no network refresh available - } } diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/DatabaseQualifiers.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/DatabaseQualifiers.kt deleted file mode 100644 index 99e7508..0000000 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/DatabaseQualifiers.kt +++ /dev/null @@ -1,35 +0,0 @@ -package hu.bbara.purefin.core.data.local.room - -import javax.inject.Qualifier - -/** - * Qualifier for online database and its components. - * Used for the primary MediaDatabase that syncs with the Jellyfin server. - */ -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class OnlineDatabase - -/** - * Qualifier for offline database and its components. - * Used for the OfflineMediaDatabase that stores downloaded content for offline viewing. - */ -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class OfflineDatabase - -/** - * Qualifier for the online media repository. - * Provides access to media synced from the Jellyfin server. - */ -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class OnlineRepository - -/** - * Qualifier for the offline media repository. - * Provides access to media downloaded for offline viewing. - */ -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class OfflineRepository diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/MediaDatabase.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/MediaDatabase.kt deleted file mode 100644 index 4fa7b2b..0000000 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/MediaDatabase.kt +++ /dev/null @@ -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 -} diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/MediaDatabaseModule.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/MediaDatabaseModule.kt deleted file mode 100644 index f0103d6..0000000 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/MediaDatabaseModule.kt +++ /dev/null @@ -1,125 +0,0 @@ -package hu.bbara.purefin.core.data.local.room - -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.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 javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -object MediaDatabaseModule { - - // Online Database and DAOs - @Provides - @Singleton - @OnlineDatabase - fun provideOnlineDatabase(@ApplicationContext context: Context): MediaDatabase = - Room.inMemoryDatabaseBuilder(context, MediaDatabase::class.java) - .fallbackToDestructiveMigration() - .build() - - @Provides - @OnlineDatabase - fun provideOnlineMovieDao(@OnlineDatabase db: MediaDatabase) = db.movieDao() - - @Provides - @OnlineDatabase - fun provideOnlineSeriesDao(@OnlineDatabase db: MediaDatabase) = db.seriesDao() - - @Provides - @OnlineDatabase - fun provideOnlineSeasonDao(@OnlineDatabase db: MediaDatabase) = db.seasonDao() - - @Provides - @OnlineDatabase - fun provideOnlineEpisodeDao(@OnlineDatabase db: MediaDatabase) = db.episodeDao() - - @Provides - @OnlineDatabase - fun provideOnlineCastMemberDao(@OnlineDatabase db: MediaDatabase) = db.castMemberDao() - - @Provides - @OnlineDatabase - fun provideOnlineLibraryDao(@OnlineDatabase db: MediaDatabase) = db.libraryDao() - - // Offline Database and DAOs - @Provides - @Singleton - @OfflineDatabase - fun provideOfflineDatabase(@ApplicationContext context: Context): OfflineMediaDatabase = - Room.databaseBuilder(context, OfflineMediaDatabase::class.java, "offline_media_database") - .fallbackToDestructiveMigration() - .build() - - @Provides - @OfflineDatabase - fun provideOfflineMovieDao(@OfflineDatabase db: OfflineMediaDatabase) = db.movieDao() - - @Provides - @OfflineDatabase - fun provideOfflineSeriesDao(@OfflineDatabase db: OfflineMediaDatabase) = db.seriesDao() - - @Provides - @OfflineDatabase - fun provideOfflineSeasonDao(@OfflineDatabase db: OfflineMediaDatabase) = db.seasonDao() - - @Provides - @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 - @Singleton - @OnlineDatabase - fun provideOnlineDataSource( - @OnlineDatabase database: MediaDatabase, - @OnlineDatabase movieDao: MovieDao, - @OnlineDatabase seriesDao: SeriesDao, - @OnlineDatabase seasonDao: SeasonDao, - @OnlineDatabase episodeDao: EpisodeDao, - @OnlineDatabase castMemberDao: CastMemberDao, - @OnlineDatabase libraryDao: LibraryDao - ): RoomMediaLocalDataSource = RoomMediaLocalDataSource( - database, movieDao, seriesDao, seasonDao, episodeDao, castMemberDao, libraryDao - ) - - @Provides - @Singleton - @OfflineDatabase - fun provideOfflineDataSource( - @OfflineDatabase database: OfflineMediaDatabase, - @OfflineDatabase movieDao: MovieDao, - @OfflineDatabase seriesDao: SeriesDao, - @OfflineDatabase seasonDao: SeasonDao, - @OfflineDatabase episodeDao: EpisodeDao, - @OfflineDatabase castMemberDao: CastMemberDao, - @OfflineDatabase libraryDao: LibraryDao - ): OfflineRoomMediaLocalDataSource = OfflineRoomMediaLocalDataSource( - database, movieDao, seriesDao, seasonDao, episodeDao, castMemberDao, libraryDao - ) - - // Default (unqualified) data source for backward compatibility - @Provides - @Singleton - fun provideDefaultDataSource( - @OnlineDatabase dataSource: RoomMediaLocalDataSource - ): RoomMediaLocalDataSource = dataSource -} diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/OfflineMediaDatabase.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/OfflineMediaDatabase.kt deleted file mode 100644 index 6d61bde..0000000 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/OfflineMediaDatabase.kt +++ /dev/null @@ -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 -} diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/RoomMediaLocalDataSource.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/RoomMediaLocalDataSource.kt deleted file mode 100644 index 3d91762..0000000 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/RoomMediaLocalDataSource.kt +++ /dev/null @@ -1,364 +0,0 @@ -package hu.bbara.purefin.core.data.local.room - -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.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 - -@Singleton -class RoomMediaLocalDataSource( - private val database: MediaDatabase, - private val movieDao: MovieDao, - 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> = libraryDao.observeAllWithContent() - .map { relation -> - relation.map { libraryEntity -> - libraryEntity.library.toDomain( - movies = libraryEntity.movies.map { it.toDomain(listOf()) }, - series = libraryEntity.series.map { it.toDomain(listOf(), listOf()) } - ) - } - } - - val moviesFlow: Flow> = movieDao.observeAll() - .map { entities -> entities.associate { it.id to it.toDomain(cast = emptyList()) } } - - val seriesFlow: Flow> = seriesDao.observeAll() - .map { entities -> - entities.associate { it.id to it.toDomain(seasons = emptyList(), cast = emptyList()) } - } - - val episodesFlow: Flow> = episodeDao.observeAll() - .map { entities -> entities.associate { it.id to it.toDomain(cast = emptyList()) } } - - // Full content Flow for series detail screen (scoped to one series) - fun observeSeriesWithContent(seriesId: UUID): Flow = - seriesDao.observeWithContent(seriesId).map { relation -> - relation?.let { - it.series.toDomain( - seasons = it.seasons.map { swe -> - swe.season.toDomain( - episodes = swe.episodes.map { ep -> ep.toDomain(cast = emptyList()) } - ) - }, - cast = emptyList() - ) - } - } - - suspend fun saveLibraries(libraries: List) { - database.withTransaction { - libraryDao.deleteAll() - libraryDao.upsertAll(libraries.map { it.toEntity() }) - } - } - - suspend fun saveMovies(movies: List) { - database.withTransaction { - movieDao.upsertAll(movies.map { it.toEntity() }) - } - } - - suspend fun saveSeries(seriesList: List) { - database.withTransaction { - seriesDao.upsertAll(seriesList.map { it.toEntity() }) - } - } - - suspend fun saveSeriesContent(series: Series) { - database.withTransaction { - // First ensure the series exists before adding seasons/episodes/cast - seriesDao.upsert(series.toEntity()) - - episodeDao.deleteBySeriesId(series.id) - seasonDao.deleteBySeriesId(series.id) - - series.seasons.forEach { season -> - seasonDao.upsert(season.toEntity()) - season.episodes.forEach { episode -> - episodeDao.upsert(episode.toEntity()) - } - } - } - } - - suspend fun saveEpisode(episode: Episode) { - database.withTransaction { - seriesDao.getById(episode.seriesId) - ?: throw RuntimeException("Cannot add episode without series. Episode: $episode") - - episodeDao.upsert(episode.toEntity()) - } - } - - suspend fun getMovies(): List { - val movies = movieDao.getAll() - return movies.map { entity -> - val cast = castMemberDao.getByMovieId(entity.id).map { it.toDomain() } - entity.toDomain(cast) - } - } - - 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) - } - - suspend fun getSeries(): List { - return seriesDao.getAll().mapNotNull { entity -> getSeriesInternal(entity.id, includeContent = false) } - } - - suspend fun getSeriesBasic(id: UUID): Series? = getSeriesInternal(id, includeContent = false) - - suspend fun getSeriesWithContent(id: UUID): Series? = getSeriesInternal(id, includeContent = true) - - 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) - } - seasonEntity.toDomain(episodes) - } - } else emptyList() - return entity.toDomain(seasons, cast) - } - - 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) - } - return seasonEntity.toDomain(episodes) - } - - suspend fun getSeasons(seriesId: UUID): List { - 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) - } - seasonEntity.toDomain(episodes) - } - } - - 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) - } - - 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) - } - - suspend fun updateWatchProgress(mediaId: UUID, progress: Double?, watched: Boolean) { - movieDao.getById(mediaId)?.let { - movieDao.updateProgress(mediaId, progress, watched) - return - } - - episodeDao.getById(mediaId)?.let { episode -> - database.withTransaction { - episodeDao.updateProgress(mediaId, progress, watched) - val seasonUnwatched = episodeDao.countUnwatchedBySeason(episode.seasonId) - seasonDao.updateUnwatchedCount(episode.seasonId, seasonUnwatched) - val seriesUnwatched = episodeDao.countUnwatchedBySeries(episode.seriesId) - seriesDao.updateUnwatchedCount(episode.seriesId, seriesUnwatched) - } - } - } - - suspend fun getEpisodesBySeries(seriesId: UUID): List { - return episodeDao.getBySeriesId(seriesId).map { episodeEntity -> - val cast = castMemberDao.getByEpisodeId(episodeEntity.id).map { it.toDomain() } - episodeEntity.toDomain(cast) - } - } - - 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, movies: List) = 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, - title = title, - progress = progress, - watched = watched, - year = year, - rating = rating, - runtime = runtime, - format = format, - synopsis = synopsis, - heroImageUrl = heroImageUrl, - audioTrack = audioTrack, - subtitles = subtitles - ) - - private fun Series.toEntity() = SeriesEntity( - id = id, - libraryId = libraryId, - name = name, - synopsis = synopsis, - year = year, - heroImageUrl = heroImageUrl, - unwatchedEpisodeCount = unwatchedEpisodeCount, - seasonCount = seasonCount - ) - - private fun Season.toEntity() = SeasonEntity( - id = id, - seriesId = seriesId, - name = name, - index = index, - unwatchedEpisodeCount = unwatchedEpisodeCount, - episodeCount = episodeCount - ) - - private fun Episode.toEntity() = EpisodeEntity( - id = id, - seriesId = seriesId, - seasonId = seasonId, - index = index, - title = title, - synopsis = synopsis, - releaseDate = releaseDate, - rating = rating, - runtime = runtime, - progress = progress, - watched = watched, - format = format, - heroImageUrl = heroImageUrl - ) - - private fun MovieEntity.toDomain(cast: List) = Movie( - id = id, - libraryId = libraryId, - title = title, - progress = progress, - watched = watched, - year = year, - rating = rating, - runtime = runtime, - format = format, - synopsis = synopsis, - heroImageUrl = heroImageUrl, - audioTrack = audioTrack, - subtitles = subtitles, - cast = cast - ) - - private fun SeriesEntity.toDomain(seasons: List, cast: List) = Series( - id = id, - libraryId = libraryId, - name = name, - synopsis = synopsis, - year = year, - heroImageUrl = heroImageUrl, - unwatchedEpisodeCount = unwatchedEpisodeCount, - seasonCount = seasonCount, - seasons = seasons, - cast = cast - ) - - private fun SeasonEntity.toDomain(episodes: List) = Season( - id = id, - seriesId = seriesId, - name = name, - index = index, - unwatchedEpisodeCount = unwatchedEpisodeCount, - episodeCount = episodeCount, - episodes = episodes - ) - - private fun EpisodeEntity.toDomain(cast: List) = Episode( - id = id, - seriesId = seriesId, - seasonId = seasonId, - index = index, - title = title, - synopsis = synopsis, - releaseDate = releaseDate, - rating = rating, - runtime = runtime, - progress = progress, - 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 - ) -} diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/room/MediaDatabaseModule.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/room/MediaDatabaseModule.kt new file mode 100644 index 0000000..20cd441 --- /dev/null +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/room/MediaDatabaseModule.kt @@ -0,0 +1,53 @@ +package hu.bbara.purefin.core.data.room + +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.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.offline.OfflineMediaDatabase +import hu.bbara.purefin.core.data.room.offline.OfflineRoomMediaLocalDataSource +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object MediaDatabaseModule { + + // Offline Database and DAOs + @Provides + @Singleton + fun provideOfflineDatabase(@ApplicationContext context: Context): OfflineMediaDatabase = + Room.databaseBuilder(context, OfflineMediaDatabase::class.java, "offline_media_database") + .fallbackToDestructiveMigration() + .build() + + @Provides + fun provideOfflineMovieDao(db: OfflineMediaDatabase) = db.movieDao() + + @Provides + fun provideOfflineSeriesDao(db: OfflineMediaDatabase) = db.seriesDao() + + @Provides + fun provideOfflineSeasonDao(db: OfflineMediaDatabase) = db.seasonDao() + + @Provides + fun provideOfflineEpisodeDao(db: OfflineMediaDatabase) = db.episodeDao() + + @Provides + @Singleton + fun provideOfflineDataSource( + database: OfflineMediaDatabase, + movieDao: MovieDao, + seriesDao: SeriesDao, + seasonDao: SeasonDao, + episodeDao: EpisodeDao + ): OfflineRoomMediaLocalDataSource = OfflineRoomMediaLocalDataSource( + database, movieDao, seriesDao, seasonDao, episodeDao + ) +} \ No newline at end of file diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/RoomRelations.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/room/RoomRelations.kt similarity index 70% rename from core/data/src/main/java/hu/bbara/purefin/core/data/local/room/RoomRelations.kt rename to core/data/src/main/java/hu/bbara/purefin/core/data/room/RoomRelations.kt index 6178021..838635d 100644 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/RoomRelations.kt +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/room/RoomRelations.kt @@ -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, diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/UuidConverters.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/room/UuidConverters.kt similarity index 86% rename from core/data/src/main/java/hu/bbara/purefin/core/data/local/room/UuidConverters.kt rename to core/data/src/main/java/hu/bbara/purefin/core/data/room/UuidConverters.kt index d1f8cdb..3761c4c 100644 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/UuidConverters.kt +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/room/UuidConverters.kt @@ -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 @@ -12,4 +12,4 @@ class UuidConverters { @TypeConverter fun uuidToString(uuid: UUID?): String? = uuid?.toString() -} +} \ No newline at end of file diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/dao/CastMemberDao.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/room/dao/CastMemberDao.kt similarity index 89% rename from core/data/src/main/java/hu/bbara/purefin/core/data/local/room/dao/CastMemberDao.kt rename to core/data/src/main/java/hu/bbara/purefin/core/data/room/dao/CastMemberDao.kt index ef496b6..f45ce93 100644 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/dao/CastMemberDao.kt +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/room/dao/CastMemberDao.kt @@ -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 diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/dao/EpisodeDao.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/room/dao/EpisodeDao.kt similarity index 92% rename from core/data/src/main/java/hu/bbara/purefin/core/data/local/room/dao/EpisodeDao.kt rename to core/data/src/main/java/hu/bbara/purefin/core/data/room/dao/EpisodeDao.kt index 30fff42..24729f6 100644 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/dao/EpisodeDao.kt +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/room/dao/EpisodeDao.kt @@ -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 diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/dao/LibraryDao.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/room/dao/LibraryDao.kt similarity index 77% rename from core/data/src/main/java/hu/bbara/purefin/core/data/local/room/dao/LibraryDao.kt rename to core/data/src/main/java/hu/bbara/purefin/core/data/room/dao/LibraryDao.kt index 6293622..356401d 100644 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/dao/LibraryDao.kt +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/room/dao/LibraryDao.kt @@ -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 diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/dao/MovieDao.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/room/dao/MovieDao.kt similarity index 88% rename from core/data/src/main/java/hu/bbara/purefin/core/data/local/room/dao/MovieDao.kt rename to core/data/src/main/java/hu/bbara/purefin/core/data/room/dao/MovieDao.kt index 17da509..2056786 100644 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/dao/MovieDao.kt +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/room/dao/MovieDao.kt @@ -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 diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/dao/SeasonDao.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/room/dao/SeasonDao.kt similarity index 87% rename from core/data/src/main/java/hu/bbara/purefin/core/data/local/room/dao/SeasonDao.kt rename to core/data/src/main/java/hu/bbara/purefin/core/data/room/dao/SeasonDao.kt index 24129f2..ad361af 100644 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/dao/SeasonDao.kt +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/room/dao/SeasonDao.kt @@ -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 diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/dao/SeriesDao.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/room/dao/SeriesDao.kt similarity index 83% rename from core/data/src/main/java/hu/bbara/purefin/core/data/local/room/dao/SeriesDao.kt rename to core/data/src/main/java/hu/bbara/purefin/core/data/room/dao/SeriesDao.kt index 396dc9e..cc200a7 100644 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/dao/SeriesDao.kt +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/room/dao/SeriesDao.kt @@ -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 diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/CastMemberEntity.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/room/entity/CastMemberEntity.kt similarity index 91% rename from core/data/src/main/java/hu/bbara/purefin/core/data/local/room/CastMemberEntity.kt rename to core/data/src/main/java/hu/bbara/purefin/core/data/room/entity/CastMemberEntity.kt index 9c33f32..f603634 100644 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/CastMemberEntity.kt +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/room/entity/CastMemberEntity.kt @@ -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 diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/EpisodeEntity.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/room/entity/EpisodeEntity.kt similarity index 94% rename from core/data/src/main/java/hu/bbara/purefin/core/data/local/room/EpisodeEntity.kt rename to core/data/src/main/java/hu/bbara/purefin/core/data/room/entity/EpisodeEntity.kt index 617fa09..5aadb83 100644 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/EpisodeEntity.kt +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/room/entity/EpisodeEntity.kt @@ -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 diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/LibraryEntity.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/room/entity/LibraryEntity.kt similarity index 82% rename from core/data/src/main/java/hu/bbara/purefin/core/data/local/room/LibraryEntity.kt rename to core/data/src/main/java/hu/bbara/purefin/core/data/room/entity/LibraryEntity.kt index 056923c..d548dc5 100644 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/LibraryEntity.kt +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/room/entity/LibraryEntity.kt @@ -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 diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/MovieEntity.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/room/entity/MovieEntity.kt similarity index 71% rename from core/data/src/main/java/hu/bbara/purefin/core/data/local/room/MovieEntity.kt rename to core/data/src/main/java/hu/bbara/purefin/core/data/room/entity/MovieEntity.kt index c1236bb..cf72c6b 100644 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/MovieEntity.kt +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/room/entity/MovieEntity.kt @@ -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 @@ -8,13 +8,6 @@ import java.util.UUID @Entity( tableName = "movies", - foreignKeys = [ - ForeignKey( - entity = LibraryEntity::class, - parentColumns = ["id"], - childColumns = ["libraryId"], - ), - ], indices = [Index("libraryId")] ) data class MovieEntity( diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/SeasonEntity.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/room/entity/SeasonEntity.kt similarity index 92% rename from core/data/src/main/java/hu/bbara/purefin/core/data/local/room/SeasonEntity.kt rename to core/data/src/main/java/hu/bbara/purefin/core/data/room/entity/SeasonEntity.kt index 2cc38fe..33dc6cd 100644 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/SeasonEntity.kt +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/room/entity/SeasonEntity.kt @@ -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 diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/SeriesEntity.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/room/entity/SeriesEntity.kt similarity index 67% rename from core/data/src/main/java/hu/bbara/purefin/core/data/local/room/SeriesEntity.kt rename to core/data/src/main/java/hu/bbara/purefin/core/data/room/entity/SeriesEntity.kt index f5ff477..cd3ba8a 100644 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/SeriesEntity.kt +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/room/entity/SeriesEntity.kt @@ -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 @@ -8,13 +8,6 @@ import java.util.UUID @Entity( tableName = "series", - foreignKeys = [ - ForeignKey( - entity = LibraryEntity::class, - parentColumns = ["id"], - childColumns = ["libraryId"] - ), - ], indices = [Index("libraryId")] ) data class SeriesEntity( diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/room/offline/OfflineMediaDatabase.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/room/offline/OfflineMediaDatabase.kt new file mode 100644 index 0000000..bdf5b59 --- /dev/null +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/room/offline/OfflineMediaDatabase.kt @@ -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 = 4, + 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 +} \ No newline at end of file diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/OfflineRoomMediaLocalDataSource.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/room/offline/OfflineRoomMediaLocalDataSource.kt similarity index 64% rename from core/data/src/main/java/hu/bbara/purefin/core/data/local/room/OfflineRoomMediaLocalDataSource.kt rename to core/data/src/main/java/hu/bbara/purefin/core/data/room/offline/OfflineRoomMediaLocalDataSource.kt index 4a9bb75..1826e60 100644 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/local/room/OfflineRoomMediaLocalDataSource.kt +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/room/offline/OfflineRoomMediaLocalDataSource.kt @@ -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> = 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> = 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> = 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> = 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 = @@ -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) { - database.withTransaction { - libraryDao.deleteAll() - libraryDao.upsertAll(libraries.map { it.toEntity() }) - } - } - suspend fun saveMovies(movies: List) { database.withTransaction { movieDao.upsertAll(movies.map { it.toEntity() }) @@ -115,15 +92,13 @@ class OfflineRoomMediaLocalDataSource( suspend fun getMovies(): List { 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 { @@ -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 { 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 { 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, movies: List) = 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) = 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, cast: List) = Series( + private fun SeriesEntity.toDomain(seasons: List) = 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) = Season( @@ -318,7 +264,7 @@ class OfflineRoomMediaLocalDataSource( episodes = episodes ) - private fun EpisodeEntity.toDomain(cast: List) = 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 + cast = emptyList() ) - - 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 - ) -} +} \ No newline at end of file diff --git a/feature/download/src/main/java/hu/bbara/purefin/feature/download/MediaDownloadManager.kt b/feature/download/src/main/java/hu/bbara/purefin/feature/download/MediaDownloadManager.kt index c0513d2..da6f7bc 100644 --- a/feature/download/src/main/java/hu/bbara/purefin/feature/download/MediaDownloadManager.kt +++ b/feature/download/src/main/java/hu/bbara/purefin/feature/download/MediaDownloadManager.kt @@ -11,9 +11,8 @@ 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.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 @@ -33,8 +32,8 @@ class MediaDownloadManager @Inject constructor( @ApplicationContext private val context: Context, private val downloadManager: DownloadManager, private val jellyfinApiClient: JellyfinApiClient, - @OfflineDatabase private val offlineDataSource: OfflineRoomMediaLocalDataSource, - @OfflineDatabase private val movieDao: MovieDao, + private val offlineDataSource: OfflineRoomMediaLocalDataSource, + private val movieDao: MovieDao, private val userSessionRepository: UserSessionRepository ) {