refactor: change dependency structure. core should not depend on data. Move repository interfaces to core. Move hilt bindings in the data model.

This commit is contained in:
2026-04-24 19:02:26 +02:00
parent 354314710b
commit 2433ececac
44 changed files with 73 additions and 53 deletions

View File

@@ -0,0 +1,11 @@
package hu.bbara.purefin
import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Online
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Offline

View File

@@ -0,0 +1,5 @@
package hu.bbara.purefin.data
interface AuthenticationRepository {
suspend fun login(url: String, username: String, password: String): Boolean
}

View File

@@ -1,7 +1,7 @@
package hu.bbara.purefin.data
import hu.bbara.purefin.data.catalog.InMemoryMediaRepository
import hu.bbara.purefin.data.catalog.OfflineMediaRepository
import hu.bbara.purefin.Offline
import hu.bbara.purefin.Online
import hu.bbara.purefin.model.Episode
import hu.bbara.purefin.model.Movie
import hu.bbara.purefin.model.Series
@@ -22,8 +22,8 @@ import javax.inject.Singleton
@OptIn(ExperimentalCoroutinesApi::class)
@Singleton
class CompositeMediaRepository @Inject constructor(
private val offlineRepository: OfflineMediaRepository,
private val onlineRepository: InMemoryMediaRepository,
@Offline private val offlineRepository: MediaRepository,
@Online private val onlineRepository: MediaRepository,
) : MediaCatalogReader, MediaProgressWriter {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

View File

@@ -0,0 +1,32 @@
package hu.bbara.purefin.data
import hu.bbara.purefin.model.Episode
import hu.bbara.purefin.model.Movie
import hu.bbara.purefin.model.Season
import hu.bbara.purefin.model.Series
import java.util.UUID
interface DownloadMediaSourceResolver {
suspend fun resolveMovieDownload(movieId: UUID): MovieDownloadSource?
suspend fun resolveEpisodeDownload(episodeId: UUID): EpisodeDownloadSource?
suspend fun isEpisodeWatched(episodeId: UUID): Boolean
suspend fun getUnwatchedEpisodeIds(
seriesId: UUID,
excludedEpisodeIds: Set<UUID>,
limit: Int,
): List<UUID>
}
data class MovieDownloadSource(
val movie: Movie,
val playbackUrl: String,
val customCacheKey: String?,
)
data class EpisodeDownloadSource(
val episode: Episode,
val series: Series,
val season: Season,
val playbackUrl: String,
val customCacheKey: String?,
)

View File

@@ -0,0 +1,16 @@
package hu.bbara.purefin.data
import hu.bbara.purefin.model.Library
import hu.bbara.purefin.model.Media
import kotlinx.coroutines.flow.StateFlow
import java.util.UUID
interface HomeRepository {
val libraries: StateFlow<List<Library>>
val suggestions: StateFlow<List<Media>>
val continueWatching: StateFlow<List<Media>>
val nextUp: StateFlow<List<Media>>
val latestLibraryContent: StateFlow<Map<UUID, List<Media>>>
fun ensureReady()
fun refreshHomeData()
}

View File

@@ -0,0 +1,16 @@
package hu.bbara.purefin.data
import hu.bbara.purefin.model.Episode
import hu.bbara.purefin.model.Movie
import hu.bbara.purefin.model.Series
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import java.util.UUID
//TODO rename MediaRepository
interface MediaCatalogReader {
val movies: StateFlow<Map<UUID, Movie>>
val series: StateFlow<Map<UUID, Series>>
val episodes: StateFlow<Map<UUID, Episode>>
fun observeSeriesWithContent(seriesId: UUID): Flow<Series?>
}

View File

@@ -0,0 +1,7 @@
package hu.bbara.purefin.data
import java.util.UUID
interface MediaProgressWriter {
suspend fun updateWatchProgress(mediaId: UUID, positionMs: Long, durationMs: Long)
}

View File

@@ -0,0 +1,4 @@
package hu.bbara.purefin.data
interface MediaRepository : MediaCatalogReader, MediaProgressWriter {
}

View File

@@ -0,0 +1,7 @@
package hu.bbara.purefin.data
import kotlinx.coroutines.flow.Flow
interface NetworkMonitor {
val isOnline: Flow<Boolean>
}

View File

@@ -0,0 +1,13 @@
package hu.bbara.purefin.data
import hu.bbara.purefin.model.Episode
import hu.bbara.purefin.model.Movie
import hu.bbara.purefin.model.Series
import kotlinx.coroutines.flow.StateFlow
import java.util.UUID
interface OfflineCatalogReader {
val movies: StateFlow<Map<UUID, Movie>>
val series: StateFlow<Map<UUID, Series>>
val episodes: StateFlow<Map<UUID, Episode>>
}

View File

@@ -0,0 +1,19 @@
package hu.bbara.purefin.data
import hu.bbara.purefin.model.Episode
import hu.bbara.purefin.model.Movie
import hu.bbara.purefin.model.Season
import hu.bbara.purefin.model.Series
import java.util.UUID
interface OfflineCatalogStore {
suspend fun saveMovies(movies: List<Movie>)
suspend fun saveSeries(series: List<Series>)
suspend fun saveSeason(season: Season)
suspend fun saveEpisode(episode: Episode)
suspend fun getSeriesBasic(seriesId: UUID): Series?
suspend fun getSeason(seasonId: UUID): Season?
suspend fun deleteMovie(movieId: UUID)
suspend fun deleteEpisodeAndCleanup(episodeId: UUID)
suspend fun getEpisodesBySeries(seriesId: UUID): List<Episode>
}

View File

@@ -0,0 +1,15 @@
package hu.bbara.purefin.data
import androidx.media3.common.MediaItem
import hu.bbara.purefin.model.MediaSegment
import java.util.UUID
interface PlayableMediaRepository {
suspend fun getMediaItem(mediaId: UUID): Pair<MediaItem, Long?>?
suspend fun getMediaSegments(mediaId: UUID): List<MediaSegment>
suspend fun getNextUpMediaItems(
episodeId: UUID,
existingIds: Set<String>,
count: Int = 9,
): List<MediaItem>
}

View File

@@ -0,0 +1,7 @@
package hu.bbara.purefin.data
enum class PlaybackMethod {
DIRECT_PLAY,
DIRECT_STREAM,
TRANSCODE,
}

View File

@@ -0,0 +1,6 @@
package hu.bbara.purefin.data
enum class PlaybackProfileFamily {
MOBILE,
TV,
}

View File

@@ -0,0 +1,14 @@
package hu.bbara.purefin.data
import java.util.UUID
interface PlaybackProgressReporter {
suspend fun reportPlaybackStart(itemId: UUID, positionTicks: Long, reportContext: PlaybackReportContext)
suspend fun reportPlaybackProgress(
itemId: UUID,
positionTicks: Long,
isPaused: Boolean,
reportContext: PlaybackReportContext,
)
suspend fun reportPlaybackStopped(itemId: UUID, positionTicks: Long, reportContext: PlaybackReportContext)
}

View File

@@ -0,0 +1,10 @@
package hu.bbara.purefin.data
data class PlaybackReportContext(
val playMethod: PlaybackMethod,
val mediaSourceId: String?,
val audioStreamIndex: Int?,
val subtitleStreamIndex: Int?,
val liveStreamId: String?,
val playSessionId: String?,
)

View File

@@ -0,0 +1,5 @@
package hu.bbara.purefin.data
interface SessionBootstrapper {
suspend fun initialize()
}

View File

@@ -0,0 +1,11 @@
package hu.bbara.purefin.data
import kotlinx.coroutines.flow.Flow
import java.util.UUID
interface SmartDownloadStore {
suspend fun enable(seriesId: UUID)
suspend fun disable(seriesId: UUID)
fun observe(seriesId: UUID): Flow<Boolean>
suspend fun getEnabledSeriesIds(): List<UUID>
}

View File

@@ -0,0 +1,21 @@
package hu.bbara.purefin.data
import kotlinx.coroutines.flow.Flow
import java.util.UUID
interface UserSessionRepository {
val serverUrl: Flow<String>
suspend fun setServerUrl(serverUrl: String)
val accessToken: Flow<String>
suspend fun setAccessToken(accessToken: String)
val userId: Flow<UUID?>
suspend fun setUserId(userId: UUID?)
suspend fun getUserId(): UUID?
val isLoggedIn: Flow<Boolean>
suspend fun setLoggedIn(isLoggedIn: Boolean)
val isOfflineMode: Flow<Boolean>
}

View File

@@ -5,7 +5,7 @@ import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import hu.bbara.purefin.data.HomeRepository
import hu.bbara.purefin.data.MediaCatalogReader
import hu.bbara.purefin.data.session.UserSessionRepository
import hu.bbara.purefin.data.UserSessionRepository
import hu.bbara.purefin.download.MediaDownloadController
import hu.bbara.purefin.model.LibraryKind
import hu.bbara.purefin.model.Media

View File

@@ -3,7 +3,8 @@ package hu.bbara.purefin.feature.downloads
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import hu.bbara.purefin.data.OfflineCatalogReader
import hu.bbara.purefin.Offline
import hu.bbara.purefin.data.MediaRepository
import hu.bbara.purefin.download.MediaDownloadController
import hu.bbara.purefin.image.ArtworkKind
import hu.bbara.purefin.image.ImageUrlBuilder
@@ -22,7 +23,7 @@ import javax.inject.Inject
@HiltViewModel
class DownloadsViewModel @Inject constructor(
private val offlineCatalogReader: OfflineCatalogReader,
@Offline private val offlineCatalogReader: MediaRepository,
private val navigationManager: NavigationManager,
private val downloadManager: MediaDownloadController,
) : ViewModel() {

View File

@@ -4,7 +4,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import hu.bbara.purefin.data.AuthenticationRepository
import hu.bbara.purefin.data.session.UserSessionRepository
import hu.bbara.purefin.data.UserSessionRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow

View File

@@ -4,8 +4,8 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import hu.bbara.purefin.data.MediaCatalogReader
import hu.bbara.purefin.data.UserSessionRepository
import hu.bbara.purefin.image.ImageUrlBuilder
import hu.bbara.purefin.data.session.UserSessionRepository
import java.util.UUID
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.MutableStateFlow

View File

@@ -0,0 +1,8 @@
package hu.bbara.purefin.image
enum class ArtworkKind(
internal val pathSegment: String,
) {
PRIMARY("Primary"),
BACKDROP("Backdrop"),
}

View File

@@ -0,0 +1,40 @@
package hu.bbara.purefin.image
import java.util.UUID
object ImageUrlBuilder {
fun toImageUrl(url: String, itemId: UUID, artworkKind: ArtworkKind): String {
if (url.isEmpty()) {
return ""
}
return StringBuilder()
.append(url)
.append("/Items/")
.append(itemId)
.append("/Images/")
.append(artworkKind.pathSegment)
.toString()
}
fun toPrefixImageUrl(url: String, itemId: UUID): String {
if (url.isEmpty()) {
return ""
}
return StringBuilder()
.append(url)
.append("/Items/")
.append(itemId)
.append("/Images/")
.toString()
}
fun finishImageUrl(prefixImageUrl: String?, artworkKind: ArtworkKind): String {
if (prefixImageUrl.isNullOrEmpty()) {
return ""
}
return StringBuilder()
.append(prefixImageUrl)
.append(artworkKind.pathSegment)
.toString()
}
}

View File

@@ -1,6 +1,6 @@
package hu.bbara.purefin.jellyfin
import hu.bbara.purefin.data.session.UserSessionRepository
import hu.bbara.purefin.data.UserSessionRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob

View File

@@ -5,19 +5,14 @@ import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import hu.bbara.purefin.data.CompositeMediaRepository
import hu.bbara.purefin.data.HomeRepository
import hu.bbara.purefin.data.MediaCatalogReader
import hu.bbara.purefin.data.MediaProgressWriter
import hu.bbara.purefin.data.catalog.InMemoryAppContentRepository
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
abstract fun bindHomeRepository(impl: InMemoryAppContentRepository): HomeRepository
@Binds
abstract fun bindMediaCatalogReader(impl: CompositeMediaRepository): MediaCatalogReader

View File

@@ -1,8 +1,7 @@
package hu.bbara.purefin.navigation
import java.util.UUID
import kotlinx.serialization.Serializable
import hu.bbara.purefin.navigation.UuidSerializer
import java.util.UUID
@Serializable
data class EpisodeDto(

View File

@@ -0,0 +1,20 @@
package hu.bbara.purefin.navigation
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.util.UUID
object UuidSerializer : KSerializer<UUID> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): UUID = UUID.fromString(decoder.decodeString())
override fun serialize(encoder: Encoder, value: UUID) {
encoder.encodeString(value.toString())
}
}