mirror of
https://github.com/bbara04/Purefin.git
synced 2026-04-01 01:30:08 +02:00
refactor: replace Room backing store in InMemoryMediaRepository with MutableStateFlows
- Remove RoomMediaLocalDataSource dependency from InMemoryMediaRepository;
libraries, movies, series, and episodes are now held in MutableStateFlows
- observeSeriesWithContent derives from _series flow via map {}
- updateWatchProgress mutates _movies / _episodes in-memory
- ensureSeriesContentLoaded checks and updates _series in-memory
- Reorganize Room package: move entities/DAOs from local/room to room/entity and room/dao
This commit is contained in:
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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.cache.HomeCache
|
||||||
import hu.bbara.purefin.core.data.client.JellyfinApiClient
|
import hu.bbara.purefin.core.data.client.JellyfinApiClient
|
||||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
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.data.session.UserSessionRepository
|
||||||
import hu.bbara.purefin.core.model.Episode
|
import hu.bbara.purefin.core.model.Episode
|
||||||
import hu.bbara.purefin.core.model.Library
|
import hu.bbara.purefin.core.model.Library
|
||||||
@@ -22,11 +19,11 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.first
|
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.launch
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
@@ -45,8 +42,6 @@ import javax.inject.Singleton
|
|||||||
class InMemoryMediaRepository @Inject constructor(
|
class InMemoryMediaRepository @Inject constructor(
|
||||||
val userSessionRepository: UserSessionRepository,
|
val userSessionRepository: UserSessionRepository,
|
||||||
val jellyfinApiClient: JellyfinApiClient,
|
val jellyfinApiClient: JellyfinApiClient,
|
||||||
private val localDataSource: RoomMediaLocalDataSource,
|
|
||||||
@OfflineDatabase private val offlineDataSource: OfflineRoomMediaLocalDataSource,
|
|
||||||
private val homeCacheDataStore: DataStore<HomeCache>
|
private val homeCacheDataStore: DataStore<HomeCache>
|
||||||
) : MediaRepository {
|
) : MediaRepository {
|
||||||
|
|
||||||
@@ -58,23 +53,24 @@ class InMemoryMediaRepository @Inject constructor(
|
|||||||
private val _state: MutableStateFlow<MediaRepositoryState> = MutableStateFlow(MediaRepositoryState.Loading)
|
private val _state: MutableStateFlow<MediaRepositoryState> = MutableStateFlow(MediaRepositoryState.Loading)
|
||||||
override val state: StateFlow<MediaRepositoryState> = _state.asStateFlow()
|
override val state: StateFlow<MediaRepositoryState> = _state.asStateFlow()
|
||||||
|
|
||||||
override val libraries: StateFlow<List<Library>> = localDataSource.librariesFlow
|
private val _libraries: MutableStateFlow<List<Library>> = MutableStateFlow(emptyList())
|
||||||
.stateIn(scope, SharingStarted.Eagerly, emptyList())
|
override val libraries: StateFlow<List<Library>> = _libraries.asStateFlow()
|
||||||
override val movies: StateFlow<Map<UUID, Movie>> = localDataSource.moviesFlow
|
|
||||||
.stateIn(scope, SharingStarted.Eagerly, emptyMap())
|
|
||||||
|
|
||||||
override val series: StateFlow<Map<UUID, Series>> = localDataSource.seriesFlow
|
private val _movies: MutableStateFlow<Map<UUID, Movie>> = MutableStateFlow(emptyMap())
|
||||||
.stateIn(scope, SharingStarted.Eagerly, emptyMap())
|
override val movies: StateFlow<Map<UUID, Movie>> = _movies.asStateFlow()
|
||||||
|
|
||||||
override val episodes: StateFlow<Map<UUID, Episode>> = localDataSource.episodesFlow
|
private val _series: MutableStateFlow<Map<UUID, Series>> = MutableStateFlow(emptyMap())
|
||||||
.stateIn(scope, SharingStarted.Eagerly, emptyMap())
|
override val series: StateFlow<Map<UUID, Series>> = _series.asStateFlow()
|
||||||
|
|
||||||
|
private val _episodes: MutableStateFlow<Map<UUID, Episode>> = MutableStateFlow(emptyMap())
|
||||||
|
override val episodes: StateFlow<Map<UUID, Episode>> = _episodes.asStateFlow()
|
||||||
|
|
||||||
override fun observeSeriesWithContent(seriesId: UUID): Flow<Series?> {
|
override fun observeSeriesWithContent(seriesId: UUID): Flow<Series?> {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
awaitReady()
|
awaitReady()
|
||||||
ensureSeriesContentLoaded(seriesId)
|
ensureSeriesContentLoaded(seriesId)
|
||||||
}
|
}
|
||||||
return localDataSource.observeSeriesWithContent(seriesId)
|
return _series.map { it[seriesId] }
|
||||||
}
|
}
|
||||||
|
|
||||||
private val _continueWatching: MutableStateFlow<List<Media>> = MutableStateFlow(emptyList())
|
private val _continueWatching: MutableStateFlow<List<Media>> = MutableStateFlow(emptyList())
|
||||||
@@ -181,20 +177,19 @@ class InMemoryMediaRepository @Inject constructor(
|
|||||||
//TODO add support for playlists
|
//TODO add support for playlists
|
||||||
val filteredLibraries =
|
val filteredLibraries =
|
||||||
librariesItem.filter { it.collectionType == CollectionType.MOVIES || it.collectionType == CollectionType.TVSHOWS }
|
librariesItem.filter { it.collectionType == CollectionType.MOVIES || it.collectionType == CollectionType.TVSHOWS }
|
||||||
val emptyLibraries = filteredLibraries.map {
|
val emptyLibraries = filteredLibraries.map { it.toLibrary() }
|
||||||
it.toLibrary()
|
_libraries.value = emptyLibraries
|
||||||
}
|
|
||||||
localDataSource.saveLibraries(emptyLibraries)
|
|
||||||
offlineDataSource.saveLibraries(emptyLibraries)
|
|
||||||
|
|
||||||
val filledLibraries = emptyLibraries.map { library ->
|
val filledLibraries = emptyLibraries.map { library ->
|
||||||
return@map loadLibrary(library)
|
return@map loadLibrary(library)
|
||||||
}
|
}
|
||||||
|
_libraries.value = filledLibraries
|
||||||
|
|
||||||
val movies = filledLibraries.filter { it.type == CollectionType.MOVIES }.flatMap { it.movies.orEmpty() }
|
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() }
|
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 {
|
suspend fun loadLibrary(library: Library): Library {
|
||||||
@@ -216,7 +211,7 @@ class InMemoryMediaRepository @Inject constructor(
|
|||||||
val movieItem = jellyfinApiClient.getItemInfo(movie.id)
|
val movieItem = jellyfinApiClient.getItemInfo(movie.id)
|
||||||
?: throw RuntimeException("Movie not found")
|
?: throw RuntimeException("Movie not found")
|
||||||
val updatedMovie = movieItem.toMovie(serverUrl(), movie.libraryId)
|
val updatedMovie = movieItem.toMovie(serverUrl(), movie.libraryId)
|
||||||
localDataSource.saveMovies(listOf(updatedMovie))
|
_movies.update { it + (updatedMovie.id to updatedMovie) }
|
||||||
return updatedMovie
|
return updatedMovie
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,7 +219,7 @@ class InMemoryMediaRepository @Inject constructor(
|
|||||||
val seriesItem = jellyfinApiClient.getItemInfo(series.id)
|
val seriesItem = jellyfinApiClient.getItemInfo(series.id)
|
||||||
?: throw RuntimeException("Series not found")
|
?: throw RuntimeException("Series not found")
|
||||||
val updatedSeries = seriesItem.toSeries(serverUrl(), series.libraryId)
|
val updatedSeries = seriesItem.toSeries(serverUrl(), series.libraryId)
|
||||||
localDataSource.saveSeries(listOf(updatedSeries))
|
_series.update { it + (updatedSeries.id to updatedSeries) }
|
||||||
return updatedSeries
|
return updatedSeries
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,7 +242,7 @@ class InMemoryMediaRepository @Inject constructor(
|
|||||||
when (item.type) {
|
when (item.type) {
|
||||||
BaseItemKind.EPISODE -> {
|
BaseItemKind.EPISODE -> {
|
||||||
val episode = item.toEpisode(serverUrl())
|
val episode = item.toEpisode(serverUrl())
|
||||||
localDataSource.saveEpisode(episode)
|
_episodes.update { it + (episode.id to episode) }
|
||||||
}
|
}
|
||||||
else -> { /* Do nothing */ }
|
else -> { /* Do nothing */ }
|
||||||
}
|
}
|
||||||
@@ -267,7 +262,7 @@ class InMemoryMediaRepository @Inject constructor(
|
|||||||
// Load episodes
|
// Load episodes
|
||||||
nextUpItems.forEach { item ->
|
nextUpItems.forEach { item ->
|
||||||
val episode = item.toEpisode(serverUrl())
|
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) {
|
private suspend fun ensureSeriesContentLoaded(seriesId: UUID) {
|
||||||
awaitReady()
|
awaitReady()
|
||||||
// Skip if content is already cached in Room
|
// Skip if content is already loaded in-memory
|
||||||
localDataSource.getSeriesWithContent(seriesId)?.takeIf { it.seasons.isNotEmpty() }?.let {
|
_series.value[seriesId]?.takeIf { it.seasons.isNotEmpty() }?.let { return }
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val series = this.series.value[seriesId] ?: throw RuntimeException("Series not found")
|
val series = this.series.value[seriesId] ?: throw RuntimeException("Series not found")
|
||||||
|
|
||||||
@@ -328,16 +321,30 @@ class InMemoryMediaRepository @Inject constructor(
|
|||||||
season.copy(episodes = episodes)
|
season.copy(episodes = episodes)
|
||||||
}
|
}
|
||||||
val updatedSeries = series.copy(seasons = filledSeasons)
|
val updatedSeries = series.copy(seasons = filledSeasons)
|
||||||
localDataSource.saveSeries(listOf(updatedSeries))
|
_series.update { it + (updatedSeries.id to updatedSeries) }
|
||||||
localDataSource.saveSeriesContent(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) {
|
override suspend fun updateWatchProgress(mediaId: UUID, positionMs: Long, durationMs: Long) {
|
||||||
if (durationMs <= 0) return
|
if (durationMs <= 0) return
|
||||||
val progressPercent = (positionMs.toDouble() / durationMs.toDouble()) * 100.0
|
val progressPercent = (positionMs.toDouble() / durationMs.toDouble()) * 100.0
|
||||||
val watched = progressPercent >= 90.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 {
|
companion object {
|
||||||
|
|||||||
@@ -4,22 +4,12 @@ import dagger.Binds
|
|||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import hu.bbara.purefin.core.data.local.room.OfflineRepository
|
|
||||||
import hu.bbara.purefin.core.data.local.room.OnlineRepository
|
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
abstract class MediaRepositoryModule {
|
abstract class MediaRepositoryModule {
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@OnlineRepository
|
abstract fun bindInMemoryMediaRepository(impl: InMemoryMediaRepository): MediaRepository
|
||||||
abstract fun bindOnlineMediaRepository(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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,15 @@
|
|||||||
package hu.bbara.purefin.core.data
|
package hu.bbara.purefin.core.data
|
||||||
|
|
||||||
import hu.bbara.purefin.core.data.local.room.OfflineDatabase
|
import hu.bbara.purefin.core.data.room.offline.OfflineRoomMediaLocalDataSource
|
||||||
import hu.bbara.purefin.core.data.local.room.OfflineRoomMediaLocalDataSource
|
|
||||||
import hu.bbara.purefin.core.model.Episode
|
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.Movie
|
||||||
import hu.bbara.purefin.core.model.Series
|
import hu.bbara.purefin.core.model.Series
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -27,41 +21,24 @@ import javax.inject.Singleton
|
|||||||
*/
|
*/
|
||||||
@Singleton
|
@Singleton
|
||||||
class OfflineMediaRepository @Inject constructor(
|
class OfflineMediaRepository @Inject constructor(
|
||||||
@OfflineDatabase private val localDataSource: OfflineRoomMediaLocalDataSource
|
private val localDataSource: OfflineRoomMediaLocalDataSource
|
||||||
) : MediaRepository {
|
) {
|
||||||
|
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|
||||||
// Offline repository is always ready (no network loading required)
|
val movies: StateFlow<Map<UUID, Movie>> = localDataSource.moviesFlow
|
||||||
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
|
|
||||||
.stateIn(scope, SharingStarted.Eagerly, emptyMap())
|
.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())
|
.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())
|
.stateIn(scope, SharingStarted.Eagerly, emptyMap())
|
||||||
|
|
||||||
// Offline mode doesn't support these server-side features
|
fun observeSeriesWithContent(seriesId: UUID): Flow<Series?> {
|
||||||
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?> {
|
|
||||||
return localDataSource.observeSeriesWithContent(seriesId)
|
return localDataSource.observeSeriesWithContent(seriesId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun ensureReady() {
|
suspend fun updateWatchProgress(mediaId: UUID, positionMs: Long, durationMs: Long) {
|
||||||
// Offline repository is always ready - no initialization needed
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun updateWatchProgress(mediaId: UUID, positionMs: Long, durationMs: Long) {
|
|
||||||
if (durationMs <= 0) return
|
if (durationMs <= 0) return
|
||||||
val progressPercent = (positionMs.toDouble() / durationMs.toDouble()) * 100.0
|
val progressPercent = (positionMs.toDouble() / durationMs.toDouble()) * 100.0
|
||||||
val watched = progressPercent >= 90.0
|
val watched = progressPercent >= 90.0
|
||||||
@@ -69,7 +46,4 @@ class OfflineMediaRepository @Inject constructor(
|
|||||||
localDataSource.updateWatchProgress(mediaId, progressPercent, watched)
|
localDataSource.updateWatchProgress(mediaId, progressPercent, watched)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun refreshHomeData() {
|
|
||||||
// No-op for offline repository - no network refresh available
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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<List<Library>> = 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<Map<UUID, Movie>> = movieDao.observeAll()
|
|
||||||
.map { entities -> entities.associate { it.id to it.toDomain(cast = emptyList()) } }
|
|
||||||
|
|
||||||
val seriesFlow: Flow<Map<UUID, Series>> = seriesDao.observeAll()
|
|
||||||
.map { entities ->
|
|
||||||
entities.associate { it.id to it.toDomain(seasons = emptyList(), cast = emptyList()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
val episodesFlow: Flow<Map<UUID, Episode>> = 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<Series?> =
|
|
||||||
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<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() })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun saveSeries(seriesList: List<Series>) {
|
|
||||||
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<Movie> {
|
|
||||||
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<Series> {
|
|
||||||
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<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)
|
|
||||||
}
|
|
||||||
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<Episode> {
|
|
||||||
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<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,
|
|
||||||
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<CastMember>) = 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<Season>, cast: List<CastMember>) = 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<Episode>) = Season(
|
|
||||||
id = id,
|
|
||||||
seriesId = seriesId,
|
|
||||||
name = name,
|
|
||||||
index = index,
|
|
||||||
unwatchedEpisodeCount = unwatchedEpisodeCount,
|
|
||||||
episodeCount = episodeCount,
|
|
||||||
episodes = episodes
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun EpisodeEntity.toDomain(cast: List<CastMember>) = 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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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.Embedded
|
||||||
import androidx.room.Relation
|
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(
|
data class SeasonWithEpisodes(
|
||||||
@Embedded val season: SeasonEntity,
|
@Embedded val season: SeasonEntity,
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hu.bbara.purefin.core.data.local.room
|
package hu.bbara.purefin.core.data.room
|
||||||
|
|
||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
@@ -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.Dao
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import androidx.room.Upsert
|
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
|
import java.util.UUID
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
@@ -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.Dao
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import androidx.room.Upsert
|
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 kotlinx.coroutines.flow.Flow
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@@ -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.Dao
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import androidx.room.Upsert
|
import androidx.room.Upsert
|
||||||
import hu.bbara.purefin.core.data.local.room.LibraryEntity
|
import hu.bbara.purefin.core.data.room.LibraryWithContent
|
||||||
import hu.bbara.purefin.core.data.local.room.LibraryWithContent
|
import hu.bbara.purefin.core.data.room.entity.LibraryEntity
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
@@ -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.Dao
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import androidx.room.Upsert
|
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 kotlinx.coroutines.flow.Flow
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@@ -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.Dao
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import androidx.room.Upsert
|
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
|
import java.util.UUID
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
@@ -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.Dao
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import androidx.room.Transaction
|
import androidx.room.Transaction
|
||||||
import androidx.room.Upsert
|
import androidx.room.Upsert
|
||||||
import hu.bbara.purefin.core.data.local.room.SeriesEntity
|
import hu.bbara.purefin.core.data.room.SeriesWithSeasonsAndEpisodes
|
||||||
import hu.bbara.purefin.core.data.local.room.SeriesWithSeasonsAndEpisodes
|
import hu.bbara.purefin.core.data.room.entity.SeriesEntity
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@@ -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.Entity
|
||||||
import androidx.room.Index
|
import androidx.room.Index
|
||||||
@@ -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.Entity
|
||||||
import androidx.room.ForeignKey
|
import androidx.room.ForeignKey
|
||||||
@@ -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.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
@@ -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.Entity
|
||||||
import androidx.room.ForeignKey
|
import androidx.room.ForeignKey
|
||||||
@@ -8,13 +8,6 @@ import java.util.UUID
|
|||||||
|
|
||||||
@Entity(
|
@Entity(
|
||||||
tableName = "movies",
|
tableName = "movies",
|
||||||
foreignKeys = [
|
|
||||||
ForeignKey(
|
|
||||||
entity = LibraryEntity::class,
|
|
||||||
parentColumns = ["id"],
|
|
||||||
childColumns = ["libraryId"],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
indices = [Index("libraryId")]
|
indices = [Index("libraryId")]
|
||||||
)
|
)
|
||||||
data class MovieEntity(
|
data class MovieEntity(
|
||||||
@@ -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.Entity
|
||||||
import androidx.room.ForeignKey
|
import androidx.room.ForeignKey
|
||||||
@@ -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.Entity
|
||||||
import androidx.room.ForeignKey
|
import androidx.room.ForeignKey
|
||||||
@@ -8,13 +8,6 @@ import java.util.UUID
|
|||||||
|
|
||||||
@Entity(
|
@Entity(
|
||||||
tableName = "series",
|
tableName = "series",
|
||||||
foreignKeys = [
|
|
||||||
ForeignKey(
|
|
||||||
entity = LibraryEntity::class,
|
|
||||||
parentColumns = ["id"],
|
|
||||||
childColumns = ["libraryId"]
|
|
||||||
),
|
|
||||||
],
|
|
||||||
indices = [Index("libraryId")]
|
indices = [Index("libraryId")]
|
||||||
)
|
)
|
||||||
data class SeriesEntity(
|
data class SeriesEntity(
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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 androidx.room.withTransaction
|
||||||
import hu.bbara.purefin.core.data.local.room.dao.CastMemberDao
|
import hu.bbara.purefin.core.data.room.dao.EpisodeDao
|
||||||
import hu.bbara.purefin.core.data.local.room.dao.EpisodeDao
|
import hu.bbara.purefin.core.data.room.dao.MovieDao
|
||||||
import hu.bbara.purefin.core.data.local.room.dao.LibraryDao
|
import hu.bbara.purefin.core.data.room.dao.SeasonDao
|
||||||
import hu.bbara.purefin.core.data.local.room.dao.MovieDao
|
import hu.bbara.purefin.core.data.room.dao.SeriesDao
|
||||||
import hu.bbara.purefin.core.data.local.room.dao.SeasonDao
|
import hu.bbara.purefin.core.data.room.entity.EpisodeEntity
|
||||||
import hu.bbara.purefin.core.data.local.room.dao.SeriesDao
|
import hu.bbara.purefin.core.data.room.entity.MovieEntity
|
||||||
import hu.bbara.purefin.core.model.CastMember
|
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.Episode
|
||||||
import hu.bbara.purefin.core.model.Library
|
|
||||||
import hu.bbara.purefin.core.model.Movie
|
import hu.bbara.purefin.core.model.Movie
|
||||||
import hu.bbara.purefin.core.model.Season
|
import hu.bbara.purefin.core.model.Season
|
||||||
import hu.bbara.purefin.core.model.Series
|
import hu.bbara.purefin.core.model.Series
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import org.jellyfin.sdk.model.api.CollectionType
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -26,31 +25,18 @@ class OfflineRoomMediaLocalDataSource(
|
|||||||
private val seriesDao: SeriesDao,
|
private val seriesDao: SeriesDao,
|
||||||
private val seasonDao: SeasonDao,
|
private val seasonDao: SeasonDao,
|
||||||
private val episodeDao: EpisodeDao,
|
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()
|
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()
|
val seriesFlow: Flow<Map<UUID, Series>> = seriesDao.observeAll()
|
||||||
.map { entities ->
|
.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()
|
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)
|
// Full content Flow for series detail screen (scoped to one series)
|
||||||
fun observeSeriesWithContent(seriesId: UUID): Flow<Series?> =
|
fun observeSeriesWithContent(seriesId: UUID): Flow<Series?> =
|
||||||
@@ -59,21 +45,12 @@ class OfflineRoomMediaLocalDataSource(
|
|||||||
it.series.toDomain(
|
it.series.toDomain(
|
||||||
seasons = it.seasons.map { swe ->
|
seasons = it.seasons.map { swe ->
|
||||||
swe.season.toDomain(
|
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>) {
|
suspend fun saveMovies(movies: List<Movie>) {
|
||||||
database.withTransaction {
|
database.withTransaction {
|
||||||
movieDao.upsertAll(movies.map { it.toEntity() })
|
movieDao.upsertAll(movies.map { it.toEntity() })
|
||||||
@@ -115,15 +92,13 @@ class OfflineRoomMediaLocalDataSource(
|
|||||||
suspend fun getMovies(): List<Movie> {
|
suspend fun getMovies(): List<Movie> {
|
||||||
val movies = movieDao.getAll()
|
val movies = movieDao.getAll()
|
||||||
return movies.map { entity ->
|
return movies.map { entity ->
|
||||||
val cast = castMemberDao.getByMovieId(entity.id).map { it.toDomain() }
|
entity.toDomain()
|
||||||
entity.toDomain(cast)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getMovie(id: UUID): Movie? {
|
suspend fun getMovie(id: UUID): Movie? {
|
||||||
val entity = movieDao.getById(id) ?: return null
|
val entity = movieDao.getById(id) ?: return null
|
||||||
val cast = castMemberDao.getByMovieId(id).map { it.toDomain() }
|
return entity.toDomain()
|
||||||
return entity.toDomain(cast)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getSeries(): List<Series> {
|
suspend fun getSeries(): List<Series> {
|
||||||
@@ -136,24 +111,21 @@ class OfflineRoomMediaLocalDataSource(
|
|||||||
|
|
||||||
private suspend fun getSeriesInternal(id: UUID, includeContent: Boolean): Series? {
|
private suspend fun getSeriesInternal(id: UUID, includeContent: Boolean): Series? {
|
||||||
val entity = seriesDao.getById(id) ?: return null
|
val entity = seriesDao.getById(id) ?: return null
|
||||||
val cast = castMemberDao.getBySeriesId(id).map { it.toDomain() }
|
|
||||||
val seasons = if (includeContent) {
|
val seasons = if (includeContent) {
|
||||||
seasonDao.getBySeriesId(id).map { seasonEntity ->
|
seasonDao.getBySeriesId(id).map { seasonEntity ->
|
||||||
val episodes = episodeDao.getBySeasonId(seasonEntity.id).map { episodeEntity ->
|
val episodes = episodeDao.getBySeasonId(seasonEntity.id).map { episodeEntity ->
|
||||||
val episodeCast = castMemberDao.getByEpisodeId(episodeEntity.id).map { it.toDomain() }
|
episodeEntity.toDomain()
|
||||||
episodeEntity.toDomain(episodeCast)
|
|
||||||
}
|
}
|
||||||
seasonEntity.toDomain(episodes)
|
seasonEntity.toDomain(episodes)
|
||||||
}
|
}
|
||||||
} else emptyList()
|
} else emptyList()
|
||||||
return entity.toDomain(seasons, cast)
|
return entity.toDomain(seasons)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getSeason(seriesId: UUID, seasonId: UUID): Season? {
|
suspend fun getSeason(seriesId: UUID, seasonId: UUID): Season? {
|
||||||
val seasonEntity = seasonDao.getById(seasonId) ?: return null
|
val seasonEntity = seasonDao.getById(seasonId) ?: return null
|
||||||
val episodes = episodeDao.getBySeasonId(seasonId).map { episodeEntity ->
|
val episodes = episodeDao.getBySeasonId(seasonId).map { episodeEntity ->
|
||||||
val episodeCast = castMemberDao.getByEpisodeId(episodeEntity.id).map { it.toDomain() }
|
episodeEntity.toDomain()
|
||||||
episodeEntity.toDomain(episodeCast)
|
|
||||||
}
|
}
|
||||||
return seasonEntity.toDomain(episodes)
|
return seasonEntity.toDomain(episodes)
|
||||||
}
|
}
|
||||||
@@ -161,8 +133,7 @@ class OfflineRoomMediaLocalDataSource(
|
|||||||
suspend fun getSeasons(seriesId: UUID): List<Season> {
|
suspend fun getSeasons(seriesId: UUID): List<Season> {
|
||||||
return seasonDao.getBySeriesId(seriesId).map { seasonEntity ->
|
return seasonDao.getBySeriesId(seriesId).map { seasonEntity ->
|
||||||
val episodes = episodeDao.getBySeasonId(seasonEntity.id).map { episodeEntity ->
|
val episodes = episodeDao.getBySeasonId(seasonEntity.id).map { episodeEntity ->
|
||||||
val episodeCast = castMemberDao.getByEpisodeId(episodeEntity.id).map { it.toDomain() }
|
episodeEntity.toDomain()
|
||||||
episodeEntity.toDomain(episodeCast)
|
|
||||||
}
|
}
|
||||||
seasonEntity.toDomain(episodes)
|
seasonEntity.toDomain(episodes)
|
||||||
}
|
}
|
||||||
@@ -170,14 +141,12 @@ class OfflineRoomMediaLocalDataSource(
|
|||||||
|
|
||||||
suspend fun getEpisode(seriesId: UUID, seasonId: UUID, episodeId: UUID): Episode? {
|
suspend fun getEpisode(seriesId: UUID, seasonId: UUID, episodeId: UUID): Episode? {
|
||||||
val episodeEntity = episodeDao.getById(episodeId) ?: return null
|
val episodeEntity = episodeDao.getById(episodeId) ?: return null
|
||||||
val cast = castMemberDao.getByEpisodeId(episodeId).map { it.toDomain() }
|
return episodeEntity.toDomain()
|
||||||
return episodeEntity.toDomain(cast)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getEpisodeById(episodeId: UUID): Episode? {
|
suspend fun getEpisodeById(episodeId: UUID): Episode? {
|
||||||
val episodeEntity = episodeDao.getById(episodeId) ?: return null
|
val episodeEntity = episodeDao.getById(episodeId) ?: return null
|
||||||
val cast = castMemberDao.getByEpisodeId(episodeId).map { it.toDomain() }
|
return episodeEntity.toDomain()
|
||||||
return episodeEntity.toDomain(cast)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateWatchProgress(mediaId: UUID, progress: Double?, watched: Boolean) {
|
suspend fun updateWatchProgress(mediaId: UUID, progress: Double?, watched: Boolean) {
|
||||||
@@ -199,33 +168,10 @@ class OfflineRoomMediaLocalDataSource(
|
|||||||
|
|
||||||
suspend fun getEpisodesBySeries(seriesId: UUID): List<Episode> {
|
suspend fun getEpisodesBySeries(seriesId: UUID): List<Episode> {
|
||||||
return episodeDao.getBySeriesId(seriesId).map { episodeEntity ->
|
return episodeDao.getBySeriesId(seriesId).map { episodeEntity ->
|
||||||
val cast = castMemberDao.getByEpisodeId(episodeEntity.id).map { it.toDomain() }
|
episodeEntity.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<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(
|
private fun Movie.toEntity() = MovieEntity(
|
||||||
id = id,
|
id = id,
|
||||||
libraryId = libraryId,
|
libraryId = libraryId,
|
||||||
@@ -278,7 +224,7 @@ class OfflineRoomMediaLocalDataSource(
|
|||||||
heroImageUrl = heroImageUrl
|
heroImageUrl = heroImageUrl
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun MovieEntity.toDomain(cast: List<CastMember>) = Movie(
|
private fun MovieEntity.toDomain() = Movie(
|
||||||
id = id,
|
id = id,
|
||||||
libraryId = libraryId,
|
libraryId = libraryId,
|
||||||
title = title,
|
title = title,
|
||||||
@@ -292,10 +238,10 @@ class OfflineRoomMediaLocalDataSource(
|
|||||||
heroImageUrl = heroImageUrl,
|
heroImageUrl = heroImageUrl,
|
||||||
audioTrack = audioTrack,
|
audioTrack = audioTrack,
|
||||||
subtitles = subtitles,
|
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,
|
id = id,
|
||||||
libraryId = libraryId,
|
libraryId = libraryId,
|
||||||
name = name,
|
name = name,
|
||||||
@@ -305,7 +251,7 @@ class OfflineRoomMediaLocalDataSource(
|
|||||||
unwatchedEpisodeCount = unwatchedEpisodeCount,
|
unwatchedEpisodeCount = unwatchedEpisodeCount,
|
||||||
seasonCount = seasonCount,
|
seasonCount = seasonCount,
|
||||||
seasons = seasons,
|
seasons = seasons,
|
||||||
cast = cast
|
cast = emptyList()
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun SeasonEntity.toDomain(episodes: List<Episode>) = Season(
|
private fun SeasonEntity.toDomain(episodes: List<Episode>) = Season(
|
||||||
@@ -318,7 +264,7 @@ class OfflineRoomMediaLocalDataSource(
|
|||||||
episodes = episodes
|
episodes = episodes
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun EpisodeEntity.toDomain(cast: List<CastMember>) = Episode(
|
private fun EpisodeEntity.toDomain() = Episode(
|
||||||
id = id,
|
id = id,
|
||||||
seriesId = seriesId,
|
seriesId = seriesId,
|
||||||
seasonId = seasonId,
|
seasonId = seasonId,
|
||||||
@@ -332,33 +278,6 @@ class OfflineRoomMediaLocalDataSource(
|
|||||||
watched = watched,
|
watched = watched,
|
||||||
format = format,
|
format = format,
|
||||||
heroImageUrl = heroImageUrl,
|
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
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -11,9 +11,8 @@ import androidx.media3.exoplayer.offline.DownloadRequest
|
|||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import hu.bbara.purefin.core.data.client.JellyfinApiClient
|
import hu.bbara.purefin.core.data.client.JellyfinApiClient
|
||||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
||||||
import hu.bbara.purefin.core.data.local.room.OfflineDatabase
|
import hu.bbara.purefin.core.data.room.dao.MovieDao
|
||||||
import hu.bbara.purefin.core.data.local.room.OfflineRoomMediaLocalDataSource
|
import hu.bbara.purefin.core.data.room.offline.OfflineRoomMediaLocalDataSource
|
||||||
import hu.bbara.purefin.core.data.local.room.dao.MovieDao
|
|
||||||
import hu.bbara.purefin.core.data.session.UserSessionRepository
|
import hu.bbara.purefin.core.data.session.UserSessionRepository
|
||||||
import hu.bbara.purefin.core.model.Movie
|
import hu.bbara.purefin.core.model.Movie
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -33,8 +32,8 @@ class MediaDownloadManager @Inject constructor(
|
|||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
private val downloadManager: DownloadManager,
|
private val downloadManager: DownloadManager,
|
||||||
private val jellyfinApiClient: JellyfinApiClient,
|
private val jellyfinApiClient: JellyfinApiClient,
|
||||||
@OfflineDatabase private val offlineDataSource: OfflineRoomMediaLocalDataSource,
|
private val offlineDataSource: OfflineRoomMediaLocalDataSource,
|
||||||
@OfflineDatabase private val movieDao: MovieDao,
|
private val movieDao: MovieDao,
|
||||||
private val userSessionRepository: UserSessionRepository
|
private val userSessionRepository: UserSessionRepository
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user