feat: introduce core data models and interfaces for playback and downloads

This commit is contained in:
2026-04-15 20:02:21 +02:00
parent b67c8076f1
commit ad930e899f
180 changed files with 1363 additions and 960 deletions

View File

@@ -52,9 +52,19 @@ kotlin {
dependencies { dependencies {
implementation(project(":core:model")) implementation(project(":core:model"))
implementation(project(":core:image"))
implementation(project(":core:data")) implementation(project(":core:data"))
implementation(project(":core:navigation"))
implementation(project(":core:download"))
implementation(project(":core:player")) implementation(project(":core:player"))
implementation(project(":feature:shared")) implementation(project(":data:jellyfin"))
implementation(project(":data:offline"))
implementation(project(":data:catalog"))
implementation(project(":feature:browse"))
implementation(project(":feature:search"))
implementation(project(":feature:content"))
implementation(project(":feature:downloads"))
implementation(project(":feature:login"))
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.androidx.lifecycle.viewmodel.compose)
@@ -83,8 +93,6 @@ dependencies {
implementation(libs.media3.datasource.okhttp) implementation(libs.media3.datasource.okhttp)
implementation(libs.androidx.navigation3.runtime) implementation(libs.androidx.navigation3.runtime)
implementation(libs.androidx.navigation3.ui) implementation(libs.androidx.navigation3.ui)
implementation(libs.androidx.room.ktx)
ksp(libs.androidx.room.compiler)
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)

View File

@@ -18,10 +18,10 @@ import androidx.compose.ui.input.key.Key
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import hu.bbara.purefin.core.model.Episode import hu.bbara.purefin.core.model.Episode
import hu.bbara.purefin.core.model.Movie import hu.bbara.purefin.core.model.Movie
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
import hu.bbara.purefin.feature.shared.home.LibraryItem import hu.bbara.purefin.feature.browse.home.LibraryItem
import hu.bbara.purefin.feature.shared.home.NextUpItem import hu.bbara.purefin.feature.browse.home.NextUpItem
import hu.bbara.purefin.feature.shared.home.PosterItem import hu.bbara.purefin.feature.browse.home.PosterItem
import hu.bbara.purefin.tv.home.TvHomeScreen import hu.bbara.purefin.tv.home.TvHomeScreen
import hu.bbara.purefin.ui.theme.AppTheme import hu.bbara.purefin.ui.theme.AppTheme
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind

View File

@@ -4,7 +4,7 @@ import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import hu.bbara.purefin.feature.shared.home.LibraryItem import hu.bbara.purefin.feature.browse.home.LibraryItem
import hu.bbara.purefin.ui.theme.AppTheme import hu.bbara.purefin.ui.theme.AppTheme
import org.jellyfin.sdk.model.api.CollectionType import org.jellyfin.sdk.model.api.CollectionType
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals

View File

@@ -20,13 +20,13 @@ import hu.bbara.purefin.common.ui.components.MediaDetailPlaybackSection
import hu.bbara.purefin.common.ui.components.MediaDetailSectionTitle import hu.bbara.purefin.common.ui.components.MediaDetailSectionTitle
import hu.bbara.purefin.common.ui.components.TvMediaDetailBodyBox import hu.bbara.purefin.common.ui.components.TvMediaDetailBodyBox
import hu.bbara.purefin.common.ui.components.TvMediaDetailScaffold import hu.bbara.purefin.common.ui.components.TvMediaDetailScaffold
import hu.bbara.purefin.core.data.image.JellyfinImageHelper import hu.bbara.purefin.core.image.ImageUrlBuilder
import hu.bbara.purefin.feature.shared.navigation.EpisodeDto import hu.bbara.purefin.core.navigation.EpisodeDto
import hu.bbara.purefin.tv.navigation.LocalNavigationManager import hu.bbara.purefin.tv.navigation.LocalNavigationManager
import hu.bbara.purefin.feature.shared.navigation.Route import hu.bbara.purefin.core.navigation.Route
import hu.bbara.purefin.core.model.Episode import hu.bbara.purefin.core.model.Episode
import hu.bbara.purefin.feature.shared.content.episode.EpisodeScreenViewModel import hu.bbara.purefin.feature.content.episode.EpisodeScreenViewModel
import org.jellyfin.sdk.model.api.ImageType import hu.bbara.purefin.core.image.ArtworkKind
@Composable @Composable
fun TvEpisodeScreen( fun TvEpisodeScreen(
@@ -87,7 +87,7 @@ internal fun TvEpisodeScreenContent(
) { ) {
item { item {
TvMediaDetailBodyBox( TvMediaDetailBodyBox(
backgroundImageUrl = JellyfinImageHelper.finishImageUrl(episode.imageUrlPrefix, ImageType.PRIMARY), backgroundImageUrl = ImageUrlBuilder.finishImageUrl(episode.imageUrlPrefix, ArtworkKind.PRIMARY),
modifier = it modifier = it
) { ) {
TvEpisodeHeroSection( TvEpisodeHeroSection(

View File

@@ -21,9 +21,9 @@ import hu.bbara.purefin.common.ui.components.MediaDetailSectionTitle
import hu.bbara.purefin.common.ui.components.TvMediaDetailBodyBox import hu.bbara.purefin.common.ui.components.TvMediaDetailBodyBox
import hu.bbara.purefin.common.ui.components.TvMediaDetailScaffold import hu.bbara.purefin.common.ui.components.TvMediaDetailScaffold
import hu.bbara.purefin.common.ui.components.tvMediaDetailBackgroundImageUrl import hu.bbara.purefin.common.ui.components.tvMediaDetailBackgroundImageUrl
import hu.bbara.purefin.feature.shared.navigation.MovieDto import hu.bbara.purefin.core.navigation.MovieDto
import hu.bbara.purefin.core.model.Movie import hu.bbara.purefin.core.model.Movie
import hu.bbara.purefin.feature.shared.content.movie.MovieScreenViewModel import hu.bbara.purefin.feature.content.movie.MovieScreenViewModel
@Composable @Composable
fun TvMovieScreen( fun TvMovieScreen(

View File

@@ -59,13 +59,13 @@ import hu.bbara.purefin.common.ui.components.MediaProgressBar
import hu.bbara.purefin.common.ui.components.MediaResumeButton import hu.bbara.purefin.common.ui.components.MediaResumeButton
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
import hu.bbara.purefin.common.ui.components.WatchStateIndicator import hu.bbara.purefin.common.ui.components.WatchStateIndicator
import hu.bbara.purefin.core.data.image.JellyfinImageHelper import hu.bbara.purefin.core.image.ImageUrlBuilder
import hu.bbara.purefin.core.model.CastMember import hu.bbara.purefin.core.model.CastMember
import hu.bbara.purefin.core.model.Episode import hu.bbara.purefin.core.model.Episode
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 hu.bbara.purefin.feature.shared.content.series.SeriesViewModel import hu.bbara.purefin.feature.content.series.SeriesViewModel
import org.jellyfin.sdk.model.api.ImageType import hu.bbara.purefin.core.image.ArtworkKind
internal const val SeriesPlayButtonTag = "series-play-button" internal const val SeriesPlayButtonTag = "series-play-button"
internal const val SeriesFirstSeasonTabTag = "series-first-season-tab" internal const val SeriesFirstSeasonTabTag = "series-first-season-tab"
@@ -296,7 +296,7 @@ private fun TvEpisodeCard(
) )
) { ) {
PurefinAsyncImage( PurefinAsyncImage(
model = JellyfinImageHelper.finishImageUrl(episode.imageUrlPrefix, ImageType.PRIMARY), model = ImageUrlBuilder.finishImageUrl(episode.imageUrlPrefix, ArtworkKind.PRIMARY),
contentDescription = null, contentDescription = null,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop contentScale = ContentScale.Crop

View File

@@ -22,10 +22,10 @@ import hu.bbara.purefin.common.ui.components.MediaDetailSectionTitle
import hu.bbara.purefin.common.ui.components.TvMediaDetailBodyBox import hu.bbara.purefin.common.ui.components.TvMediaDetailBodyBox
import hu.bbara.purefin.common.ui.components.TvMediaDetailScaffold import hu.bbara.purefin.common.ui.components.TvMediaDetailScaffold
import hu.bbara.purefin.common.ui.components.tvMediaDetailBackgroundImageUrl import hu.bbara.purefin.common.ui.components.tvMediaDetailBackgroundImageUrl
import hu.bbara.purefin.feature.shared.navigation.SeriesDto import hu.bbara.purefin.core.navigation.SeriesDto
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 hu.bbara.purefin.feature.shared.content.series.SeriesViewModel import hu.bbara.purefin.feature.content.series.SeriesViewModel
import java.util.UUID import java.util.UUID
@Composable @Composable

View File

@@ -32,8 +32,8 @@ import androidx.compose.ui.unit.sp
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
import hu.bbara.purefin.common.ui.components.UnwatchedEpisodeIndicator import hu.bbara.purefin.common.ui.components.UnwatchedEpisodeIndicator
import hu.bbara.purefin.common.ui.components.WatchStateIndicator import hu.bbara.purefin.common.ui.components.WatchStateIndicator
import hu.bbara.purefin.feature.shared.home.FocusableItem import hu.bbara.purefin.feature.browse.home.FocusableItem
import hu.bbara.purefin.feature.shared.home.PosterItem import hu.bbara.purefin.feature.browse.home.PosterItem
import java.util.UUID import java.util.UUID
import hu.bbara.purefin.core.model.MediaKind import hu.bbara.purefin.core.model.MediaKind

View File

@@ -32,8 +32,8 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import hu.bbara.purefin.common.ui.MediaSynopsis import hu.bbara.purefin.common.ui.MediaSynopsis
import hu.bbara.purefin.core.data.image.JellyfinImageHelper import hu.bbara.purefin.core.image.ImageUrlBuilder
import org.jellyfin.sdk.model.api.ImageType import hu.bbara.purefin.core.image.ArtworkKind
internal val MediaDetailHorizontalPadding = 48.dp internal val MediaDetailHorizontalPadding = 48.dp
private val MediaDetailHeaderTopPadding = 104.dp private val MediaDetailHeaderTopPadding = 104.dp
@@ -151,8 +151,8 @@ internal fun TvMediaDetailBodyBox(
} }
internal fun tvMediaDetailBackgroundImageUrl(imageUrlPrefix: String?): String { internal fun tvMediaDetailBackgroundImageUrl(imageUrlPrefix: String?): String {
val primaryImageUrl = JellyfinImageHelper.finishImageUrl(imageUrlPrefix, ImageType.PRIMARY) val primaryImageUrl = ImageUrlBuilder.finishImageUrl(imageUrlPrefix, ArtworkKind.PRIMARY)
val backdropImageUrl = JellyfinImageHelper.finishImageUrl(imageUrlPrefix, ImageType.BACKDROP) val backdropImageUrl = ImageUrlBuilder.finishImageUrl(imageUrlPrefix, ArtworkKind.BACKDROP)
return backdropImageUrl.ifBlank { primaryImageUrl } return backdropImageUrl.ifBlank { primaryImageUrl }
} }

View File

@@ -41,7 +41,7 @@ import hu.bbara.purefin.common.ui.PurefinComplexTextField
import hu.bbara.purefin.common.ui.PurefinPasswordField import hu.bbara.purefin.common.ui.PurefinPasswordField
import hu.bbara.purefin.common.ui.PurefinTextButton import hu.bbara.purefin.common.ui.PurefinTextButton
import hu.bbara.purefin.common.ui.PurefinWaitingScreen import hu.bbara.purefin.common.ui.PurefinWaitingScreen
import hu.bbara.purefin.feature.shared.login.LoginViewModel import hu.bbara.purefin.feature.login.LoginViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable @Composable

View File

@@ -37,13 +37,13 @@ import coil3.request.crossfade
import coil3.util.DebugLogger import coil3.util.DebugLogger
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import hu.bbara.purefin.common.ui.PurefinWaitingScreen import hu.bbara.purefin.common.ui.PurefinWaitingScreen
import hu.bbara.purefin.core.data.client.JellyfinApiClient import hu.bbara.purefin.core.data.SessionBootstrapper
import hu.bbara.purefin.core.data.client.JellyfinAuthInterceptor import hu.bbara.purefin.data.jellyfin.client.JellyfinAuthInterceptor
import hu.bbara.purefin.core.data.session.UserSessionRepository import hu.bbara.purefin.core.data.session.UserSessionRepository
import hu.bbara.purefin.feature.shared.navigation.NavigationCommand
import hu.bbara.purefin.feature.shared.navigation.NavigationManager
import hu.bbara.purefin.feature.shared.navigation.Route
import hu.bbara.purefin.login.ui.LoginScreen import hu.bbara.purefin.login.ui.LoginScreen
import hu.bbara.purefin.core.navigation.NavigationCommand
import hu.bbara.purefin.core.navigation.NavigationManager
import hu.bbara.purefin.core.navigation.Route
import hu.bbara.purefin.tv.navigation.LocalNavigationBackStack import hu.bbara.purefin.tv.navigation.LocalNavigationBackStack
import hu.bbara.purefin.tv.navigation.LocalNavigationManager import hu.bbara.purefin.tv.navigation.LocalNavigationManager
import hu.bbara.purefin.ui.theme.AppTheme import hu.bbara.purefin.ui.theme.AppTheme
@@ -66,7 +66,7 @@ class TvActivity : ComponentActivity() {
lateinit var navigationManager: NavigationManager lateinit var navigationManager: NavigationManager
@Inject @Inject
lateinit var jellyfinApiClient: JellyfinApiClient lateinit var sessionBootstrapper: SessionBootstrapper
@Inject @Inject
lateinit var authInterceptor: JellyfinAuthInterceptor lateinit var authInterceptor: JellyfinAuthInterceptor
@@ -94,7 +94,7 @@ class TvActivity : ComponentActivity() {
} }
private suspend fun init() { private suspend fun init() {
jellyfinApiClient.updateApiClient() sessionBootstrapper.initialize()
} }
private fun configureImageLoader() { private fun configureImageLoader() {

View File

@@ -15,7 +15,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.lifecycle.compose.LifecycleResumeEffect
import hu.bbara.purefin.feature.shared.home.AppViewModel import hu.bbara.purefin.feature.browse.home.AppViewModel
import hu.bbara.purefin.tv.home.TvHomeScreen import hu.bbara.purefin.tv.home.TvHomeScreen
import hu.bbara.purefin.tv.home.ui.TvDrawerDestination import hu.bbara.purefin.tv.home.ui.TvDrawerDestination
import hu.bbara.purefin.tv.home.ui.TvDrawerDestinationItem import hu.bbara.purefin.tv.home.ui.TvDrawerDestinationItem

View File

@@ -1,56 +0,0 @@
package hu.bbara.purefin.tv.di
import androidx.media3.datasource.DataSource
import androidx.media3.datasource.okhttp.OkHttpDataSource
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import hu.bbara.purefin.core.data.download.DownloadState
import hu.bbara.purefin.core.data.download.MediaDownloadController
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import java.util.UUID
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object TvPlaybackModule {
@Provides
@Singleton
fun providePlaybackDataSourceFactory(
okHttpDataSourceFactory: OkHttpDataSource.Factory
): DataSource.Factory {
return okHttpDataSourceFactory
}
@Provides
@Singleton
fun provideMediaDownloadController(): MediaDownloadController {
return TvNoOpMediaDownloadController
}
}
internal object TvNoOpMediaDownloadController : MediaDownloadController {
private val notDownloaded = MutableStateFlow<DownloadState>(DownloadState.NotDownloaded)
override fun observeActiveDownloads(): Flow<Map<String, Float>> = flowOf(emptyMap())
override fun observeDownloadState(contentId: String) = notDownloaded
override suspend fun downloadMovie(movieId: UUID) = Unit
override suspend fun cancelDownload(movieId: UUID) = Unit
override suspend fun downloadEpisode(episodeId: UUID) = Unit
override suspend fun downloadEpisodes(episodeIds: List<UUID>) = Unit
override suspend fun cancelEpisodeDownload(episodeId: UUID) = Unit
override suspend fun enableSmartDownload(seriesId: UUID) = Unit
override suspend fun syncSmartDownloads() = Unit
}

View File

@@ -4,7 +4,7 @@ import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import hu.bbara.purefin.core.data.client.PlaybackProfileFamily import hu.bbara.purefin.core.data.PlaybackProfileFamily
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)

View File

@@ -9,10 +9,10 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
import hu.bbara.purefin.feature.shared.home.LibraryItem import hu.bbara.purefin.feature.browse.home.LibraryItem
import hu.bbara.purefin.feature.shared.home.NextUpItem import hu.bbara.purefin.feature.browse.home.NextUpItem
import hu.bbara.purefin.feature.shared.home.PosterItem import hu.bbara.purefin.feature.browse.home.PosterItem
import hu.bbara.purefin.tv.home.ui.TvFocusedItemHero import hu.bbara.purefin.tv.home.ui.TvFocusedItemHero
import hu.bbara.purefin.tv.home.ui.TvHomeContent import hu.bbara.purefin.tv.home.ui.TvHomeContent
import hu.bbara.purefin.tv.home.ui.rememberTvHomeHeroState import hu.bbara.purefin.tv.home.ui.rememberTvHomeHeroState

View File

@@ -1,16 +1,16 @@
package hu.bbara.purefin.tv.home.ui package hu.bbara.purefin.tv.home.ui
import hu.bbara.purefin.core.data.image.JellyfinImageHelper import hu.bbara.purefin.core.image.ImageUrlBuilder
import hu.bbara.purefin.core.model.Episode import hu.bbara.purefin.core.model.Episode
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 hu.bbara.purefin.feature.shared.home.ContinueWatchingItem import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
import hu.bbara.purefin.feature.shared.home.FocusableItem import hu.bbara.purefin.feature.browse.home.FocusableItem
import hu.bbara.purefin.feature.shared.home.NextUpItem import hu.bbara.purefin.feature.browse.home.NextUpItem
import hu.bbara.purefin.feature.shared.home.PosterItem import hu.bbara.purefin.feature.browse.home.PosterItem
import java.util.UUID import java.util.UUID
import hu.bbara.purefin.core.model.MediaKind import hu.bbara.purefin.core.model.MediaKind
import org.jellyfin.sdk.model.api.ImageType import hu.bbara.purefin.core.image.ArtworkKind
import kotlin.math.roundToInt import kotlin.math.roundToInt
internal data class TvFocusedHeroModel( internal data class TvFocusedHeroModel(
@@ -42,7 +42,7 @@ internal fun FocusableItem.toTvFocusedHeroModel(): TvFocusedHeroModel {
} }
internal fun backdropImageUrl(imageUrlPrefix: String?, fallbackImageUrl: String): String { internal fun backdropImageUrl(imageUrlPrefix: String?, fallbackImageUrl: String): String {
val backdropImageUrl = JellyfinImageHelper.finishImageUrl(imageUrlPrefix, ImageType.BACKDROP) val backdropImageUrl = ImageUrlBuilder.finishImageUrl(imageUrlPrefix, ArtworkKind.BACKDROP)
return backdropImageUrl.ifBlank { fallbackImageUrl } return backdropImageUrl.ifBlank { fallbackImageUrl }
} }
@@ -51,7 +51,7 @@ private fun Movie.toTvFocusedHeroModel(): TvFocusedHeroModel {
id = id, id = id,
backdropImageUrl = backdropImageUrl( backdropImageUrl = backdropImageUrl(
imageUrlPrefix = imageUrlPrefix, imageUrlPrefix = imageUrlPrefix,
fallbackImageUrl = JellyfinImageHelper.finishImageUrl(imageUrlPrefix, ImageType.PRIMARY) fallbackImageUrl = ImageUrlBuilder.finishImageUrl(imageUrlPrefix, ArtworkKind.PRIMARY)
), ),
title = title, title = title,
metadata = listOf(year, rating, runtime, format).compactMetadata(), metadata = listOf(year, rating, runtime, format).compactMetadata(),
@@ -63,7 +63,7 @@ private fun Episode.toTvFocusedHeroModel(): TvFocusedHeroModel {
id = id, id = id,
backdropImageUrl = backdropImageUrl( backdropImageUrl = backdropImageUrl(
imageUrlPrefix = imageUrlPrefix, imageUrlPrefix = imageUrlPrefix,
fallbackImageUrl = JellyfinImageHelper.finishImageUrl(imageUrlPrefix, ImageType.PRIMARY) fallbackImageUrl = ImageUrlBuilder.finishImageUrl(imageUrlPrefix, ArtworkKind.PRIMARY)
), ),
title = title, title = title,
metadata = listOf( metadata = listOf(
@@ -87,7 +87,7 @@ private fun Series.toTvFocusedHeroModel(): TvFocusedHeroModel {
id = id, id = id,
backdropImageUrl = backdropImageUrl( backdropImageUrl = backdropImageUrl(
imageUrlPrefix = imageUrlPrefix, imageUrlPrefix = imageUrlPrefix,
fallbackImageUrl = JellyfinImageHelper.finishImageUrl(imageUrlPrefix, ImageType.PRIMARY) fallbackImageUrl = ImageUrlBuilder.finishImageUrl(imageUrlPrefix, ArtworkKind.PRIMARY)
), ),
title = name, title = name,
metadata = listOf(year, seasonLabel(seasonCount), unwatchedText).compactMetadata(), metadata = listOf(year, seasonLabel(seasonCount), unwatchedText).compactMetadata(),

View File

@@ -24,11 +24,11 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
import hu.bbara.purefin.feature.shared.home.FocusableItem import hu.bbara.purefin.feature.browse.home.FocusableItem
import hu.bbara.purefin.feature.shared.home.LibraryItem import hu.bbara.purefin.feature.browse.home.LibraryItem
import hu.bbara.purefin.feature.shared.home.NextUpItem import hu.bbara.purefin.feature.browse.home.NextUpItem
import hu.bbara.purefin.feature.shared.home.PosterItem import hu.bbara.purefin.feature.browse.home.PosterItem
import java.util.UUID import java.util.UUID
internal const val TvHomeInitialFocusTag = "tv-home-initial-focus-item" internal const val TvHomeInitialFocusTag = "tv-home-initial-focus-item"

View File

@@ -5,11 +5,11 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
import hu.bbara.purefin.feature.shared.home.FocusableItem import hu.bbara.purefin.feature.browse.home.FocusableItem
import hu.bbara.purefin.feature.shared.home.LibraryItem import hu.bbara.purefin.feature.browse.home.LibraryItem
import hu.bbara.purefin.feature.shared.home.NextUpItem import hu.bbara.purefin.feature.browse.home.NextUpItem
import hu.bbara.purefin.feature.shared.home.PosterItem import hu.bbara.purefin.feature.browse.home.PosterItem
import java.util.UUID import java.util.UUID
internal data class TvHomeItemRegistry( internal data class TvHomeItemRegistry(

View File

@@ -47,14 +47,14 @@ import androidx.compose.ui.unit.sp
import hu.bbara.purefin.common.ui.PosterCard import hu.bbara.purefin.common.ui.PosterCard
import hu.bbara.purefin.common.ui.components.MediaProgressBar import hu.bbara.purefin.common.ui.components.MediaProgressBar
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
import hu.bbara.purefin.core.data.image.JellyfinImageHelper import hu.bbara.purefin.core.image.ImageUrlBuilder
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
import hu.bbara.purefin.feature.shared.home.FocusableItem import hu.bbara.purefin.feature.browse.home.FocusableItem
import hu.bbara.purefin.feature.shared.home.NextUpItem import hu.bbara.purefin.feature.browse.home.NextUpItem
import hu.bbara.purefin.feature.shared.home.PosterItem import hu.bbara.purefin.feature.browse.home.PosterItem
import java.util.UUID import java.util.UUID
import hu.bbara.purefin.core.model.MediaKind import hu.bbara.purefin.core.model.MediaKind
import org.jellyfin.sdk.model.api.ImageType import hu.bbara.purefin.core.image.ArtworkKind
private val TvHomeSectionsThumbShape = RoundedCornerShape(20.dp) private val TvHomeSectionsThumbShape = RoundedCornerShape(20.dp)
private val TvHomeSectionsHorizontalPadding = 32.dp private val TvHomeSectionsHorizontalPadding = 32.dp
@@ -93,14 +93,14 @@ fun TvContinueWatchingSection(
title = item.primaryText, title = item.primaryText,
supporting = item.secondaryText, supporting = item.secondaryText,
imageUrl = when (item.type) { imageUrl = when (item.type) {
MediaKind.MOVIE -> JellyfinImageHelper.finishImageUrl( MediaKind.MOVIE -> ImageUrlBuilder.finishImageUrl(
prefixImageUrl = item.movie?.imageUrlPrefix, prefixImageUrl = item.movie?.imageUrlPrefix,
imageType = ImageType.PRIMARY artworkKind = ArtworkKind.PRIMARY
) )
MediaKind.EPISODE -> JellyfinImageHelper.finishImageUrl( MediaKind.EPISODE -> ImageUrlBuilder.finishImageUrl(
prefixImageUrl = item.episode?.imageUrlPrefix, prefixImageUrl = item.episode?.imageUrlPrefix,
imageType = ImageType.PRIMARY artworkKind = ArtworkKind.PRIMARY
) )
else -> null else -> null

View File

@@ -44,7 +44,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
import hu.bbara.purefin.feature.shared.home.LibraryItem import hu.bbara.purefin.feature.browse.home.LibraryItem
import hu.bbara.purefin.core.model.LibraryKind import hu.bbara.purefin.core.model.LibraryKind
internal const val TvLibrariesOverviewItemTagPrefix = "tv-libraries-overview-item-" internal const val TvLibrariesOverviewItemTagPrefix = "tv-libraries-overview-item-"

View File

@@ -24,9 +24,9 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import hu.bbara.purefin.common.ui.PosterCard import hu.bbara.purefin.common.ui.PosterCard
import hu.bbara.purefin.common.ui.components.PurefinIconButton import hu.bbara.purefin.common.ui.components.PurefinIconButton
import hu.bbara.purefin.feature.shared.navigation.LibraryDto import hu.bbara.purefin.core.navigation.LibraryDto
import hu.bbara.purefin.feature.shared.home.PosterItem import hu.bbara.purefin.feature.browse.home.PosterItem
import hu.bbara.purefin.feature.shared.library.LibraryViewModel import hu.bbara.purefin.feature.browse.library.LibraryViewModel
import java.util.UUID import java.util.UUID
@Composable @Composable

View File

@@ -3,8 +3,8 @@ package hu.bbara.purefin.tv.navigation
import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.runtime.staticCompositionLocalOf
import hu.bbara.purefin.feature.shared.navigation.NavigationManager import hu.bbara.purefin.core.navigation.NavigationManager
import hu.bbara.purefin.feature.shared.navigation.Route import hu.bbara.purefin.core.navigation.Route
val LocalNavigationManager: ProvidableCompositionLocal<NavigationManager> = val LocalNavigationManager: ProvidableCompositionLocal<NavigationManager> =
staticCompositionLocalOf { error("NavigationManager not provided") } staticCompositionLocalOf { error("NavigationManager not provided") }

View File

@@ -6,7 +6,7 @@ import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityRetainedComponent import dagger.hilt.android.components.ActivityRetainedComponent
import dagger.multibindings.IntoSet import dagger.multibindings.IntoSet
import hu.bbara.purefin.feature.shared.navigation.Route import hu.bbara.purefin.core.navigation.Route
@Module @Module
@InstallIn(ActivityRetainedComponent::class) @InstallIn(ActivityRetainedComponent::class)

View File

@@ -4,7 +4,7 @@ import androidx.navigation3.runtime.EntryProviderScope
import hu.bbara.purefin.app.content.episode.TvEpisodeScreen import hu.bbara.purefin.app.content.episode.TvEpisodeScreen
import hu.bbara.purefin.app.content.movie.TvMovieScreen import hu.bbara.purefin.app.content.movie.TvMovieScreen
import hu.bbara.purefin.app.content.series.TvSeriesScreen import hu.bbara.purefin.app.content.series.TvSeriesScreen
import hu.bbara.purefin.feature.shared.navigation.Route import hu.bbara.purefin.core.navigation.Route
import hu.bbara.purefin.login.ui.LoginScreen import hu.bbara.purefin.login.ui.LoginScreen
import hu.bbara.purefin.tv.TvAppScreen import hu.bbara.purefin.tv.TvAppScreen
import hu.bbara.purefin.tv.library.ui.TvLibraryScreen import hu.bbara.purefin.tv.library.ui.TvLibraryScreen

View File

@@ -1,19 +0,0 @@
package hu.bbara.purefin.tv.di
import hu.bbara.purefin.core.data.download.DownloadState
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
class TvNoOpMediaDownloadControllerTest {
@Test
fun `tv no-op download controller always reports no downloads`() = runBlocking {
assertEquals(emptyMap<String, Float>(), TvNoOpMediaDownloadController.observeActiveDownloads().first())
assertEquals(
DownloadState.NotDownloaded,
TvNoOpMediaDownloadController.observeDownloadState("movie-1").value
)
}
}

View File

@@ -1,14 +1,14 @@
package hu.bbara.purefin.tv.home.ui package hu.bbara.purefin.tv.home.ui
import hu.bbara.purefin.core.data.image.JellyfinImageHelper import hu.bbara.purefin.core.image.ImageUrlBuilder
import hu.bbara.purefin.core.model.Episode import hu.bbara.purefin.core.model.Episode
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 hu.bbara.purefin.feature.shared.home.ContinueWatchingItem import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
import hu.bbara.purefin.feature.shared.home.NextUpItem import hu.bbara.purefin.feature.browse.home.NextUpItem
import hu.bbara.purefin.feature.shared.home.PosterItem import hu.bbara.purefin.feature.browse.home.PosterItem
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.ImageType import hu.bbara.purefin.core.image.ArtworkKind
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
import org.junit.Test import org.junit.Test
@@ -33,7 +33,7 @@ class TvFocusedItemHeroTest {
assertEquals("42%", hero.progressLabel) assertEquals("42%", hero.progressLabel)
assertNull(hero.watchedText) assertNull(hero.watchedText)
assertEquals( assertEquals(
JellyfinImageHelper.finishImageUrl(movie.imageUrlPrefix, ImageType.BACKDROP), ImageUrlBuilder.finishImageUrl(movie.imageUrlPrefix, ArtworkKind.BACKDROP),
hero.backdropImageUrl hero.backdropImageUrl
) )
} }

View File

@@ -2,10 +2,10 @@ package hu.bbara.purefin.tv.home.ui
import hu.bbara.purefin.core.model.Episode import hu.bbara.purefin.core.model.Episode
import hu.bbara.purefin.core.model.Movie import hu.bbara.purefin.core.model.Movie
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
import hu.bbara.purefin.feature.shared.home.LibraryItem import hu.bbara.purefin.feature.browse.home.LibraryItem
import hu.bbara.purefin.feature.shared.home.NextUpItem import hu.bbara.purefin.feature.browse.home.NextUpItem
import hu.bbara.purefin.feature.shared.home.PosterItem import hu.bbara.purefin.feature.browse.home.PosterItem
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.CollectionType import org.jellyfin.sdk.model.api.CollectionType
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals

View File

@@ -51,10 +51,19 @@ kotlin {
dependencies { dependencies {
implementation(project(":core:model")) implementation(project(":core:model"))
implementation(project(":core:image"))
implementation(project(":core:data")) implementation(project(":core:data"))
implementation(project(":feature:download")) implementation(project(":core:navigation"))
implementation(project(":core:download"))
implementation(project(":core:player")) implementation(project(":core:player"))
implementation(project(":feature:shared")) implementation(project(":data:jellyfin"))
implementation(project(":data:offline"))
implementation(project(":data:catalog"))
implementation(project(":feature:browse"))
implementation(project(":feature:search"))
implementation(project(":feature:content"))
implementation(project(":feature:downloads"))
implementation(project(":feature:login"))
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.androidx.lifecycle.viewmodel.compose)
@@ -83,8 +92,6 @@ dependencies {
implementation(libs.media3.datasource.okhttp) implementation(libs.media3.datasource.okhttp)
implementation(libs.androidx.navigation3.runtime) implementation(libs.androidx.navigation3.runtime)
implementation(libs.androidx.navigation3.ui) implementation(libs.androidx.navigation3.ui)
implementation(libs.androidx.room.ktx)
ksp(libs.androidx.room.compiler)
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)

View File

@@ -33,7 +33,7 @@
android:exported="false" android:exported="false"
android:theme="@style/Theme.Purefin" /> android:theme="@style/Theme.Purefin" />
<service <service
android:name="hu.bbara.purefin.feature.download.PurefinDownloadService" android:name="hu.bbara.purefin.core.download.PurefinDownloadService"
android:exported="false" android:exported="false"
android:foregroundServiceType="dataSync" /> android:foregroundServiceType="dataSync" />
</application> </application>

View File

@@ -36,15 +36,15 @@ import coil3.network.okhttp.OkHttpNetworkFetcherFactory
import coil3.request.crossfade import coil3.request.crossfade
import coil3.util.DebugLogger import coil3.util.DebugLogger
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen import hu.bbara.purefin.core.data.SessionBootstrapper
import hu.bbara.purefin.core.data.client.JellyfinApiClient import hu.bbara.purefin.data.jellyfin.client.JellyfinAuthInterceptor
import hu.bbara.purefin.core.data.client.JellyfinAuthInterceptor
import hu.bbara.purefin.core.data.session.UserSessionRepository import hu.bbara.purefin.core.data.session.UserSessionRepository
import hu.bbara.purefin.feature.shared.navigation.NavigationCommand
import hu.bbara.purefin.feature.shared.navigation.NavigationManager
import hu.bbara.purefin.feature.shared.navigation.Route
import hu.bbara.purefin.navigation.LocalNavigationBackStack import hu.bbara.purefin.navigation.LocalNavigationBackStack
import hu.bbara.purefin.navigation.LocalNavigationManager import hu.bbara.purefin.navigation.LocalNavigationManager
import hu.bbara.purefin.core.navigation.NavigationCommand
import hu.bbara.purefin.core.navigation.NavigationManager
import hu.bbara.purefin.core.navigation.Route
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
import hu.bbara.purefin.ui.screen.login.LoginScreen import hu.bbara.purefin.ui.screen.login.LoginScreen
import hu.bbara.purefin.ui.theme.AppTheme import hu.bbara.purefin.ui.theme.AppTheme
import hu.bbara.purefin.ui.theme.backgroundDark import hu.bbara.purefin.ui.theme.backgroundDark
@@ -65,7 +65,7 @@ class PurefinActivity : ComponentActivity() {
lateinit var navigationManager: NavigationManager lateinit var navigationManager: NavigationManager
@Inject @Inject
lateinit var jellyfinApiClient: JellyfinApiClient lateinit var sessionBootstrapper: SessionBootstrapper
@Inject @Inject
lateinit var authInterceptor: JellyfinAuthInterceptor lateinit var authInterceptor: JellyfinAuthInterceptor
@@ -93,7 +93,7 @@ class PurefinActivity : ComponentActivity() {
} }
private suspend fun init() { private suspend fun init() {
jellyfinApiClient.updateApiClient() sessionBootstrapper.initialize()
} }
private fun configureImageLoader() { private fun configureImageLoader() {

View File

@@ -6,7 +6,7 @@ import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityRetainedComponent import dagger.hilt.android.components.ActivityRetainedComponent
import dagger.multibindings.IntoSet import dagger.multibindings.IntoSet
import hu.bbara.purefin.feature.shared.navigation.Route import hu.bbara.purefin.core.navigation.Route
import hu.bbara.purefin.navigation.appRouteEntryBuilder import hu.bbara.purefin.navigation.appRouteEntryBuilder
@Module @Module

View File

@@ -4,7 +4,7 @@ import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import hu.bbara.purefin.core.data.client.PlaybackProfileFamily import hu.bbara.purefin.core.data.PlaybackProfileFamily
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)

View File

@@ -3,8 +3,8 @@ package hu.bbara.purefin.navigation
import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.runtime.staticCompositionLocalOf
import hu.bbara.purefin.feature.shared.navigation.NavigationManager import hu.bbara.purefin.core.navigation.NavigationManager
import hu.bbara.purefin.feature.shared.navigation.Route import hu.bbara.purefin.core.navigation.Route
val LocalNavigationManager: ProvidableCompositionLocal<NavigationManager> = val LocalNavigationManager: ProvidableCompositionLocal<NavigationManager> =
staticCompositionLocalOf { error("NavigationManager not provided") } staticCompositionLocalOf { error("NavigationManager not provided") }

View File

@@ -1,7 +1,7 @@
package hu.bbara.purefin.navigation package hu.bbara.purefin.navigation
import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.EntryProviderScope
import hu.bbara.purefin.feature.shared.navigation.Route import hu.bbara.purefin.core.navigation.Route
import hu.bbara.purefin.ui.screen.library.LibraryScreen import hu.bbara.purefin.ui.screen.library.LibraryScreen
import hu.bbara.purefin.ui.screen.login.LoginScreen import hu.bbara.purefin.ui.screen.login.LoginScreen
import hu.bbara.purefin.ui.screen.AppScreen import hu.bbara.purefin.ui.screen.AppScreen

View File

@@ -27,7 +27,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import hu.bbara.purefin.feature.shared.search.SearchViewModel import hu.bbara.purefin.feature.search.SearchViewModel
@Composable @Composable
fun DefaultTopBar( fun DefaultTopBar(

View File

@@ -23,7 +23,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import coil3.request.ImageRequest import coil3.request.ImageRequest
import hu.bbara.purefin.feature.shared.home.PosterItem import hu.bbara.purefin.feature.browse.home.PosterItem
import hu.bbara.purefin.ui.common.badge.UnwatchedEpisodeIndicator import hu.bbara.purefin.ui.common.badge.UnwatchedEpisodeIndicator
import hu.bbara.purefin.ui.common.badge.WatchStateIndicator import hu.bbara.purefin.ui.common.badge.WatchStateIndicator
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage import hu.bbara.purefin.ui.common.image.PurefinAsyncImage

View File

@@ -10,7 +10,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.lifecycle.compose.LifecycleResumeEffect
import hu.bbara.purefin.feature.shared.home.AppViewModel import hu.bbara.purefin.feature.browse.home.AppViewModel
import hu.bbara.purefin.ui.screen.download.DownloadsScreen import hu.bbara.purefin.ui.screen.download.DownloadsScreen
import hu.bbara.purefin.ui.screen.home.HomeScreen import hu.bbara.purefin.ui.screen.home.HomeScreen
import hu.bbara.purefin.ui.screen.home.components.HomeNavItem import hu.bbara.purefin.ui.screen.home.components.HomeNavItem

View File

@@ -27,7 +27,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
import hu.bbara.purefin.feature.shared.download.ActiveDownloadItem import hu.bbara.purefin.feature.downloads.ActiveDownloadItem
@Composable @Composable
internal fun DownloadingItemRow( internal fun DownloadingItemRow(

View File

@@ -24,7 +24,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import hu.bbara.purefin.ui.common.card.PosterCard import hu.bbara.purefin.ui.common.card.PosterCard
import hu.bbara.purefin.feature.shared.download.DownloadsViewModel import hu.bbara.purefin.feature.downloads.DownloadsViewModel
@Composable @Composable
fun DownloadsContent( fun DownloadsContent(

View File

@@ -37,19 +37,19 @@ import androidx.hilt.navigation.compose.hiltViewModel
import hu.bbara.purefin.ui.common.media.MediaMetaChip import hu.bbara.purefin.ui.common.media.MediaMetaChip
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
import hu.bbara.purefin.ui.common.media.MediaHero import hu.bbara.purefin.ui.common.media.MediaHero
import hu.bbara.purefin.core.data.download.DownloadState import hu.bbara.purefin.core.download.DownloadState
import hu.bbara.purefin.core.data.image.JellyfinImageHelper import hu.bbara.purefin.core.image.ImageUrlBuilder
import hu.bbara.purefin.feature.shared.navigation.EpisodeDto import hu.bbara.purefin.core.navigation.EpisodeDto
import hu.bbara.purefin.navigation.LocalNavigationBackStack import hu.bbara.purefin.navigation.LocalNavigationBackStack
import hu.bbara.purefin.feature.shared.navigation.Route import hu.bbara.purefin.core.navigation.Route
import hu.bbara.purefin.core.model.CastMember import hu.bbara.purefin.core.model.CastMember
import hu.bbara.purefin.core.model.Episode import hu.bbara.purefin.core.model.Episode
import hu.bbara.purefin.feature.shared.content.episode.EpisodeScreenViewModel import hu.bbara.purefin.feature.content.episode.EpisodeScreenViewModel
import hu.bbara.purefin.ui.screen.episode.components.EpisodeDetails import hu.bbara.purefin.ui.screen.episode.components.EpisodeDetails
import hu.bbara.purefin.ui.screen.episode.components.EpisodeTopBar import hu.bbara.purefin.ui.screen.episode.components.EpisodeTopBar
import hu.bbara.purefin.ui.screen.episode.components.EpisodeTopBarShortcut import hu.bbara.purefin.ui.screen.episode.components.EpisodeTopBarShortcut
import hu.bbara.purefin.ui.theme.AppTheme import hu.bbara.purefin.ui.theme.AppTheme
import org.jellyfin.sdk.model.api.ImageType import hu.bbara.purefin.core.image.ArtworkKind
import java.util.UUID import java.util.UUID
@Composable @Composable
@@ -167,7 +167,7 @@ private fun EpisodeHeroSection(
.height(sectionHeight) .height(sectionHeight)
) { ) {
MediaHero( MediaHero(
imageUrl = JellyfinImageHelper.finishImageUrl(episode.imageUrlPrefix, ImageType.PRIMARY), imageUrl = ImageUrlBuilder.finishImageUrl(episode.imageUrlPrefix, ArtworkKind.PRIMARY),
backgroundColor = scheme.background, backgroundColor = scheme.background,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) )

View File

@@ -40,7 +40,7 @@ import hu.bbara.purefin.ui.common.button.GhostIconButton
import hu.bbara.purefin.ui.common.button.MediaActionButton import hu.bbara.purefin.ui.common.button.MediaActionButton
import hu.bbara.purefin.ui.common.media.MediaPlaybackSettings import hu.bbara.purefin.ui.common.media.MediaPlaybackSettings
import hu.bbara.purefin.ui.common.button.MediaResumeButton import hu.bbara.purefin.ui.common.button.MediaResumeButton
import hu.bbara.purefin.core.data.download.DownloadState import hu.bbara.purefin.core.download.DownloadState
import hu.bbara.purefin.core.model.Episode import hu.bbara.purefin.core.model.Episode
import hu.bbara.purefin.player.PlayerActivity import hu.bbara.purefin.player.PlayerActivity

View File

@@ -16,11 +16,11 @@ import androidx.compose.ui.Modifier
import hu.bbara.purefin.ui.screen.home.components.HomeContent import hu.bbara.purefin.ui.screen.home.components.HomeContent
import hu.bbara.purefin.ui.screen.home.components.HomeTopBar import hu.bbara.purefin.ui.screen.home.components.HomeTopBar
import hu.bbara.purefin.ui.screen.home.components.search.HomeSearchOverlay import hu.bbara.purefin.ui.screen.home.components.search.HomeSearchOverlay
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
import hu.bbara.purefin.feature.shared.home.LibraryItem import hu.bbara.purefin.feature.browse.home.LibraryItem
import hu.bbara.purefin.feature.shared.home.NextUpItem import hu.bbara.purefin.feature.browse.home.NextUpItem
import hu.bbara.purefin.feature.shared.home.PosterItem import hu.bbara.purefin.feature.browse.home.PosterItem
import hu.bbara.purefin.feature.shared.home.SuggestedItem import hu.bbara.purefin.feature.browse.home.SuggestedItem
import hu.bbara.purefin.ui.screen.AppBottomBar import hu.bbara.purefin.ui.screen.AppBottomBar
import java.util.UUID import java.util.UUID

View File

@@ -22,11 +22,11 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
import hu.bbara.purefin.feature.shared.home.LibraryItem import hu.bbara.purefin.feature.browse.home.LibraryItem
import hu.bbara.purefin.feature.shared.home.NextUpItem import hu.bbara.purefin.feature.browse.home.NextUpItem
import hu.bbara.purefin.feature.shared.home.PosterItem import hu.bbara.purefin.feature.browse.home.PosterItem
import hu.bbara.purefin.feature.shared.home.SuggestedItem import hu.bbara.purefin.feature.browse.home.SuggestedItem
import hu.bbara.purefin.ui.screen.home.components.continuewatching.ContinueWatchingSection import hu.bbara.purefin.ui.screen.home.components.continuewatching.ContinueWatchingSection
import hu.bbara.purefin.ui.screen.home.components.featured.SuggestionsSection import hu.bbara.purefin.ui.screen.home.components.featured.SuggestionsSection
import hu.bbara.purefin.ui.screen.home.components.library.LibraryPosterSection import hu.bbara.purefin.ui.screen.home.components.library.LibraryPosterSection

View File

@@ -25,11 +25,11 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import hu.bbara.purefin.ui.common.bar.MediaProgressBar import hu.bbara.purefin.ui.common.bar.MediaProgressBar
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
import hu.bbara.purefin.core.data.image.JellyfinImageHelper import hu.bbara.purefin.core.image.ImageUrlBuilder
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
import java.util.UUID import java.util.UUID
import hu.bbara.purefin.core.model.MediaKind import hu.bbara.purefin.core.model.MediaKind
import org.jellyfin.sdk.model.api.ImageType import hu.bbara.purefin.core.image.ArtworkKind
@Composable @Composable
internal fun ContinueWatchingCard( internal fun ContinueWatchingCard(
@@ -53,13 +53,13 @@ internal fun ContinueWatchingCard(
else -> "" else -> ""
} }
val imageUrl = when (item.type) { val imageUrl = when (item.type) {
MediaKind.MOVIE -> JellyfinImageHelper.finishImageUrl( MediaKind.MOVIE -> ImageUrlBuilder.finishImageUrl(
prefixImageUrl = item.movie?.imageUrlPrefix, prefixImageUrl = item.movie?.imageUrlPrefix,
imageType = ImageType.PRIMARY artworkKind = ArtworkKind.PRIMARY
) )
MediaKind.EPISODE -> JellyfinImageHelper.finishImageUrl( MediaKind.EPISODE -> ImageUrlBuilder.finishImageUrl(
prefixImageUrl = item.episode?.imageUrlPrefix, prefixImageUrl = item.episode?.imageUrlPrefix,
imageType = ImageType.PRIMARY artworkKind = ArtworkKind.PRIMARY
) )
else -> null else -> null
} }

View File

@@ -10,7 +10,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import hu.bbara.purefin.ui.common.header.SectionHeader import hu.bbara.purefin.ui.common.header.SectionHeader
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem import hu.bbara.purefin.feature.browse.home.ContinueWatchingItem
import java.util.UUID import java.util.UUID
@Composable @Composable

View File

@@ -26,7 +26,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import hu.bbara.purefin.ui.common.bar.MediaProgressBar import hu.bbara.purefin.ui.common.bar.MediaProgressBar
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
import hu.bbara.purefin.feature.shared.home.SuggestedItem import hu.bbara.purefin.feature.browse.home.SuggestedItem
@Composable @Composable
internal fun SuggestionCard( internal fun SuggestionCard(

View File

@@ -22,7 +22,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import hu.bbara.purefin.feature.shared.home.SuggestedItem import hu.bbara.purefin.feature.browse.home.SuggestedItem
@Composable @Composable
fun SuggestionsSection( fun SuggestionsSection(

View File

@@ -26,7 +26,7 @@ import androidx.compose.ui.unit.dp
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
import hu.bbara.purefin.ui.common.badge.UnwatchedEpisodeIndicator import hu.bbara.purefin.ui.common.badge.UnwatchedEpisodeIndicator
import hu.bbara.purefin.ui.common.badge.WatchStateIndicator import hu.bbara.purefin.ui.common.badge.WatchStateIndicator
import hu.bbara.purefin.feature.shared.home.PosterItem import hu.bbara.purefin.feature.browse.home.PosterItem
import java.util.UUID import java.util.UUID
import hu.bbara.purefin.core.model.MediaKind import hu.bbara.purefin.core.model.MediaKind

View File

@@ -9,8 +9,8 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import hu.bbara.purefin.feature.shared.home.LibraryItem import hu.bbara.purefin.feature.browse.home.LibraryItem
import hu.bbara.purefin.feature.shared.home.PosterItem import hu.bbara.purefin.feature.browse.home.PosterItem
import hu.bbara.purefin.ui.common.header.SectionHeader import hu.bbara.purefin.ui.common.header.SectionHeader
import java.util.UUID import java.util.UUID

View File

@@ -23,10 +23,10 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
import hu.bbara.purefin.core.data.image.JellyfinImageHelper import hu.bbara.purefin.core.image.ImageUrlBuilder
import hu.bbara.purefin.feature.shared.home.NextUpItem import hu.bbara.purefin.feature.browse.home.NextUpItem
import java.util.UUID import java.util.UUID
import org.jellyfin.sdk.model.api.ImageType import hu.bbara.purefin.core.image.ArtworkKind
@Composable @Composable
internal fun NextUpCard( internal fun NextUpCard(
@@ -57,7 +57,7 @@ internal fun NextUpCard(
.background(scheme.surface) .background(scheme.surface)
) { ) {
PurefinAsyncImage( PurefinAsyncImage(
model = JellyfinImageHelper.finishImageUrl(item.episode.imageUrlPrefix, ImageType.PRIMARY), model = ImageUrlBuilder.finishImageUrl(item.episode.imageUrlPrefix, ArtworkKind.PRIMARY),
contentDescription = item.primaryText, contentDescription = item.primaryText,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop contentScale = ContentScale.Crop

View File

@@ -10,7 +10,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import hu.bbara.purefin.ui.common.header.SectionHeader import hu.bbara.purefin.ui.common.header.SectionHeader
import hu.bbara.purefin.feature.shared.home.NextUpItem import hu.bbara.purefin.feature.browse.home.NextUpItem
import java.util.UUID import java.util.UUID
@Composable @Composable

View File

@@ -35,7 +35,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import hu.bbara.purefin.feature.shared.search.SearchViewModel import hu.bbara.purefin.feature.search.SearchViewModel
import java.util.UUID import java.util.UUID
import hu.bbara.purefin.core.model.MediaKind import hu.bbara.purefin.core.model.MediaKind

View File

@@ -18,7 +18,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import hu.bbara.purefin.core.model.MediaKind import hu.bbara.purefin.core.model.MediaKind
import hu.bbara.purefin.feature.shared.search.SearchResult import hu.bbara.purefin.feature.search.SearchResult
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
@Composable @Composable

View File

@@ -23,9 +23,9 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import hu.bbara.purefin.ui.common.card.PosterCard import hu.bbara.purefin.ui.common.card.PosterCard
import hu.bbara.purefin.ui.common.button.PurefinIconButton import hu.bbara.purefin.ui.common.button.PurefinIconButton
import hu.bbara.purefin.feature.shared.navigation.LibraryDto import hu.bbara.purefin.core.navigation.LibraryDto
import hu.bbara.purefin.feature.shared.home.PosterItem import hu.bbara.purefin.feature.browse.home.PosterItem
import hu.bbara.purefin.feature.shared.library.LibraryViewModel import hu.bbara.purefin.feature.browse.library.LibraryViewModel
@Composable @Composable
fun LibraryScreen( fun LibraryScreen(

View File

@@ -41,7 +41,7 @@ import hu.bbara.purefin.ui.common.textfield.PurefinComplexTextField
import hu.bbara.purefin.ui.common.textfield.PurefinPasswordField import hu.bbara.purefin.ui.common.textfield.PurefinPasswordField
import hu.bbara.purefin.ui.common.button.PurefinTextButton import hu.bbara.purefin.ui.common.button.PurefinTextButton
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
import hu.bbara.purefin.feature.shared.login.LoginViewModel import hu.bbara.purefin.feature.login.LoginViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable @Composable

View File

@@ -35,16 +35,16 @@ import androidx.hilt.navigation.compose.hiltViewModel
import hu.bbara.purefin.ui.common.media.MediaMetaChip import hu.bbara.purefin.ui.common.media.MediaMetaChip
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
import hu.bbara.purefin.ui.common.media.MediaHero import hu.bbara.purefin.ui.common.media.MediaHero
import hu.bbara.purefin.core.data.download.DownloadState import hu.bbara.purefin.core.download.DownloadState
import hu.bbara.purefin.core.data.image.JellyfinImageHelper import hu.bbara.purefin.core.image.ImageUrlBuilder
import hu.bbara.purefin.feature.shared.navigation.MovieDto import hu.bbara.purefin.core.navigation.MovieDto
import hu.bbara.purefin.core.model.CastMember import hu.bbara.purefin.core.model.CastMember
import hu.bbara.purefin.core.model.Movie import hu.bbara.purefin.core.model.Movie
import hu.bbara.purefin.feature.shared.content.movie.MovieScreenViewModel import hu.bbara.purefin.feature.content.movie.MovieScreenViewModel
import hu.bbara.purefin.ui.screen.movie.components.MovieDetails import hu.bbara.purefin.ui.screen.movie.components.MovieDetails
import hu.bbara.purefin.ui.screen.movie.components.MovieTopBar import hu.bbara.purefin.ui.screen.movie.components.MovieTopBar
import hu.bbara.purefin.ui.theme.AppTheme import hu.bbara.purefin.ui.theme.AppTheme
import org.jellyfin.sdk.model.api.ImageType import hu.bbara.purefin.core.image.ArtworkKind
import java.util.UUID import java.util.UUID
@Composable @Composable
@@ -139,7 +139,7 @@ fun MediaHeroSection(
.height(sectionHeight) .height(sectionHeight)
) { ) {
MediaHero( MediaHero(
imageUrl = JellyfinImageHelper.finishImageUrl(movie.imageUrlPrefix, ImageType.PRIMARY), imageUrl = ImageUrlBuilder.finishImageUrl(movie.imageUrlPrefix, ArtworkKind.PRIMARY),
backgroundColor = MaterialTheme.colorScheme.background, backgroundColor = MaterialTheme.colorScheme.background,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) )

View File

@@ -35,7 +35,7 @@ import hu.bbara.purefin.ui.common.button.GhostIconButton
import hu.bbara.purefin.ui.common.button.MediaActionButton import hu.bbara.purefin.ui.common.button.MediaActionButton
import hu.bbara.purefin.ui.common.media.MediaPlaybackSettings import hu.bbara.purefin.ui.common.media.MediaPlaybackSettings
import hu.bbara.purefin.ui.common.button.MediaResumeButton import hu.bbara.purefin.ui.common.button.MediaResumeButton
import hu.bbara.purefin.core.data.download.DownloadState import hu.bbara.purefin.core.download.DownloadState
import hu.bbara.purefin.core.model.Movie import hu.bbara.purefin.core.model.Movie
import hu.bbara.purefin.player.PlayerActivity import hu.bbara.purefin.player.PlayerActivity

View File

@@ -33,14 +33,14 @@ import androidx.hilt.navigation.compose.hiltViewModel
import hu.bbara.purefin.ui.common.media.MediaSynopsis import hu.bbara.purefin.ui.common.media.MediaSynopsis
import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen
import hu.bbara.purefin.ui.common.media.MediaHero import hu.bbara.purefin.ui.common.media.MediaHero
import hu.bbara.purefin.core.data.download.DownloadState import hu.bbara.purefin.core.download.DownloadState
import hu.bbara.purefin.core.data.image.JellyfinImageHelper import hu.bbara.purefin.core.image.ImageUrlBuilder
import hu.bbara.purefin.feature.shared.navigation.SeriesDto import hu.bbara.purefin.core.navigation.SeriesDto
import hu.bbara.purefin.core.model.CastMember import hu.bbara.purefin.core.model.CastMember
import hu.bbara.purefin.core.model.Episode import hu.bbara.purefin.core.model.Episode
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 hu.bbara.purefin.feature.shared.content.series.SeriesViewModel import hu.bbara.purefin.feature.content.series.SeriesViewModel
import hu.bbara.purefin.ui.screen.series.components.CastRow import hu.bbara.purefin.ui.screen.series.components.CastRow
import hu.bbara.purefin.ui.screen.series.components.EpisodeCarousel import hu.bbara.purefin.ui.screen.series.components.EpisodeCarousel
import hu.bbara.purefin.ui.screen.series.components.SeasonTabs import hu.bbara.purefin.ui.screen.series.components.SeasonTabs
@@ -49,7 +49,7 @@ import hu.bbara.purefin.ui.screen.series.components.SeriesDownloadOption
import hu.bbara.purefin.ui.screen.series.components.SeriesMetaChips import hu.bbara.purefin.ui.screen.series.components.SeriesMetaChips
import hu.bbara.purefin.ui.screen.series.components.SeriesTopBar import hu.bbara.purefin.ui.screen.series.components.SeriesTopBar
import hu.bbara.purefin.ui.theme.AppTheme import hu.bbara.purefin.ui.theme.AppTheme
import org.jellyfin.sdk.model.api.ImageType import hu.bbara.purefin.core.image.ArtworkKind
import java.util.UUID import java.util.UUID
@Composable @Composable
@@ -207,7 +207,7 @@ private fun SeriesHeroSection(
.height(sectionHeight) .height(sectionHeight)
) { ) {
MediaHero( MediaHero(
imageUrl = JellyfinImageHelper.finishImageUrl(series.imageUrlPrefix, ImageType.PRIMARY), imageUrl = ImageUrlBuilder.finishImageUrl(series.imageUrlPrefix, ArtworkKind.PRIMARY),
backgroundColor = scheme.background, backgroundColor = scheme.background,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) )

View File

@@ -65,18 +65,18 @@ import hu.bbara.purefin.ui.common.bar.MediaProgressBar
import hu.bbara.purefin.ui.common.button.MediaResumeButton import hu.bbara.purefin.ui.common.button.MediaResumeButton
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
import hu.bbara.purefin.ui.common.badge.WatchStateIndicator import hu.bbara.purefin.ui.common.badge.WatchStateIndicator
import hu.bbara.purefin.core.data.download.DownloadState import hu.bbara.purefin.core.download.DownloadState
import hu.bbara.purefin.core.data.image.JellyfinImageHelper import hu.bbara.purefin.core.image.ImageUrlBuilder
import hu.bbara.purefin.feature.shared.navigation.EpisodeDto import hu.bbara.purefin.core.navigation.EpisodeDto
import hu.bbara.purefin.navigation.LocalNavigationManager import hu.bbara.purefin.navigation.LocalNavigationManager
import hu.bbara.purefin.feature.shared.navigation.Route import hu.bbara.purefin.core.navigation.Route
import hu.bbara.purefin.core.model.CastMember import hu.bbara.purefin.core.model.CastMember
import hu.bbara.purefin.core.model.Episode import hu.bbara.purefin.core.model.Episode
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 hu.bbara.purefin.feature.shared.content.series.SeriesViewModel import hu.bbara.purefin.feature.content.series.SeriesViewModel
import hu.bbara.purefin.player.PlayerActivity import hu.bbara.purefin.player.PlayerActivity
import org.jellyfin.sdk.model.api.ImageType import hu.bbara.purefin.core.image.ArtworkKind
@Composable @Composable
internal fun SeriesTopBar( internal fun SeriesTopBar(
@@ -349,7 +349,7 @@ private fun EpisodeCard(
.border(1.dp, scheme.outlineVariant, RoundedCornerShape(12.dp)) .border(1.dp, scheme.outlineVariant, RoundedCornerShape(12.dp))
) { ) {
PurefinAsyncImage( PurefinAsyncImage(
model = JellyfinImageHelper.finishImageUrl(episode.imageUrlPrefix, ImageType.PRIMARY), model = ImageUrlBuilder.finishImageUrl(episode.imageUrlPrefix, ArtworkKind.PRIMARY),
contentDescription = null, contentDescription = null,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop contentScale = ContentScale.Crop

View File

@@ -3,9 +3,6 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins { plugins {
alias(libs.plugins.android.library) alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.hilt)
alias(libs.plugins.ksp)
} }
android { android {
@@ -30,15 +27,7 @@ kotlin {
dependencies { dependencies {
implementation(project(":core:model")) implementation(project(":core:model"))
implementation(libs.jellyfin.core)
implementation(libs.media3.common) implementation(libs.media3.common)
implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.coroutines.core)
implementation(libs.hilt)
ksp(libs.hilt.compiler)
implementation(libs.datastore)
implementation(libs.okhttp)
implementation(libs.logging.interceptor)
implementation(libs.androidx.room.ktx)
ksp(libs.androidx.room.compiler)
testImplementation(libs.junit) testImplementation(libs.junit)
} }

View File

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

View File

@@ -0,0 +1,32 @@
package hu.bbara.purefin.core.data
import hu.bbara.purefin.core.model.Episode
import hu.bbara.purefin.core.model.Movie
import hu.bbara.purefin.core.model.Season
import hu.bbara.purefin.core.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

@@ -1,16 +0,0 @@
package hu.bbara.purefin.core.data
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
abstract class NetworkModule {
@Binds
@Singleton
abstract fun bindNetworkMonitor(impl: ConnectivityNetworkMonitor): NetworkMonitor
}

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
package hu.bbara.purefin.core.data package hu.bbara.purefin.core.data
import hu.bbara.purefin.core.data.client.PlaybackReportContext
import java.util.UUID import java.util.UUID
interface PlaybackProgressReporter { interface PlaybackProgressReporter {

View File

@@ -0,0 +1,10 @@
package hu.bbara.purefin.core.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.core.data
interface SessionBootstrapper {
suspend fun initialize()
}

View File

@@ -1,19 +0,0 @@
package hu.bbara.purefin.core.data.client
import org.jellyfin.sdk.model.api.MediaSourceInfo
import org.jellyfin.sdk.model.api.PlayMethod
data class PlaybackDecision(
val url: String,
val mediaSource: MediaSourceInfo,
val reportContext: PlaybackReportContext
)
data class PlaybackReportContext(
val playMethod: PlayMethod,
val mediaSourceId: String?,
val audioStreamIndex: Int?,
val subtitleStreamIndex: Int?,
val liveStreamId: String?,
val playSessionId: String?,
)

View File

@@ -1,12 +0,0 @@
package hu.bbara.purefin.core.data.domain.usecase
import hu.bbara.purefin.core.data.HomeRepository
import javax.inject.Inject
class RefreshHomeDataUseCase @Inject constructor(
private val repository: HomeRepository
) {
suspend operator fun invoke() {
repository.refreshHomeData()
}
}

View File

@@ -1,45 +0,0 @@
package hu.bbara.purefin.core.data.image
import java.util.UUID
import org.jellyfin.sdk.model.api.ImageType
class JellyfinImageHelper {
companion object {
fun toImageUrl(url: String, itemId: UUID, type: ImageType): String {
if (url.isEmpty()) {
return ""
}
return StringBuilder()
.append(url)
.append("/Items/")
.append(itemId)
.append("/Images/")
.append(type.serialName)
.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?, imageType: ImageType): String {
if (prefixImageUrl.isNullOrEmpty()) {
return ""
}
return StringBuilder()
.append(prefixImageUrl)
.append(imageType.serialName)
.toString()
}
}
}

View File

@@ -1,36 +0,0 @@
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.dao.SmartDownloadDao
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.data.room.entity.SmartDownloadEntity
@Database(
entities = [
MovieEntity::class,
SeriesEntity::class,
SeasonEntity::class,
EpisodeEntity::class,
SmartDownloadEntity::class,
],
version = 9,
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 smartDownloadDao(): SmartDownloadDao
}

View File

@@ -1,54 +1,21 @@
package hu.bbara.purefin.core.data.session package hu.bbara.purefin.core.data.session
import androidx.datastore.core.DataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import java.util.UUID import java.util.UUID
import javax.inject.Inject import kotlinx.coroutines.flow.Flow
class UserSessionRepository @Inject constructor( interface UserSessionRepository {
private val userSessionDataStore: DataStore<UserSession> val serverUrl: Flow<String>
) { suspend fun setServerUrl(serverUrl: String)
val session: Flow<UserSession> = userSessionDataStore.data
val serverUrl: Flow<String> = session val accessToken: Flow<String>
.map { it.url } suspend fun setAccessToken(accessToken: String)
suspend fun setServerUrl(serverUrl: String) { val userId: Flow<UUID?>
userSessionDataStore.updateData { suspend fun setUserId(userId: UUID?)
it.copy(url = serverUrl) suspend fun getUserId(): UUID?
}
}
val accessToken: Flow<String> = session val isLoggedIn: Flow<Boolean>
.map { it.accessToken } suspend fun setLoggedIn(isLoggedIn: Boolean)
suspend fun setAccessToken(accessToken: String) { val isOfflineMode: Flow<Boolean>
userSessionDataStore.updateData {
it.copy(accessToken = accessToken)
}
}
val userId: Flow<UUID?> = session
.map { it.userId }
suspend fun setUserId(userId: UUID?) {
userSessionDataStore.updateData {
it.copy(userId = userId)
}
}
suspend fun getUserId(): UUID? = userId.first()
val isLoggedIn: Flow<Boolean> = session.map { it.loggedIn }.distinctUntilChanged()
suspend fun setLoggedIn(isLoggedIn: Boolean) {
userSessionDataStore.updateData {
it.copy(loggedIn = isLoggedIn)
}
}
val isOfflineMode: Flow<Boolean> = session.map { it.isOfflineMode }.distinctUntilChanged()
} }

View File

@@ -8,7 +8,7 @@ plugins {
} }
android { android {
namespace = "hu.bbara.purefin.feature.download" namespace = "hu.bbara.purefin.core.download"
compileSdk = 36 compileSdk = 36
defaultConfig { defaultConfig {
@@ -30,11 +30,11 @@ kotlin {
dependencies { dependencies {
implementation(project(":core:model")) implementation(project(":core:model"))
implementation(project(":core:data")) implementation(project(":core:data"))
implementation(libs.kotlinx.coroutines.core)
implementation(libs.hilt) implementation(libs.hilt)
ksp(libs.hilt.compiler) ksp(libs.hilt.compiler)
implementation(libs.medi3.exoplayer) implementation(libs.medi3.exoplayer)
implementation(libs.media3.datasource.okhttp) implementation(libs.media3.datasource.okhttp)
implementation(libs.okhttp) implementation(libs.okhttp)
implementation(libs.jellyfin.core)
testImplementation(libs.junit) testImplementation(libs.junit)
} }

View File

@@ -1,10 +1,10 @@
package hu.bbara.purefin.feature.download package hu.bbara.purefin.core.download
import dagger.Binds 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.download.MediaDownloadController import hu.bbara.purefin.core.download.MediaDownloadController
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module

View File

@@ -1,4 +1,4 @@
package hu.bbara.purefin.feature.download package hu.bbara.purefin.core.download
import android.content.Context import android.content.Context
import androidx.annotation.OptIn import androidx.annotation.OptIn

View File

@@ -1,4 +1,4 @@
package hu.bbara.purefin.core.data.download package hu.bbara.purefin.core.download
sealed class DownloadState { sealed class DownloadState {
data object NotDownloaded : DownloadState() data object NotDownloaded : DownloadState()

View File

@@ -1,4 +1,4 @@
package hu.bbara.purefin.core.data.download package hu.bbara.purefin.core.download
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow

View File

@@ -1,4 +1,4 @@
package hu.bbara.purefin.feature.download package hu.bbara.purefin.core.download
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
@@ -9,18 +9,9 @@ import androidx.media3.exoplayer.offline.Download
import androidx.media3.exoplayer.offline.DownloadManager import androidx.media3.exoplayer.offline.DownloadManager
import androidx.media3.exoplayer.offline.DownloadRequest import androidx.media3.exoplayer.offline.DownloadRequest
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import hu.bbara.purefin.core.data.DownloadMediaSourceResolver
import hu.bbara.purefin.core.data.OfflineCatalogStore import hu.bbara.purefin.core.data.OfflineCatalogStore
import hu.bbara.purefin.core.data.SmartDownloadStore import hu.bbara.purefin.core.data.SmartDownloadStore
import hu.bbara.purefin.core.data.client.playbackCustomCacheKey
import hu.bbara.purefin.core.data.client.JellyfinApiClient
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
import hu.bbara.purefin.core.data.download.DownloadState
import hu.bbara.purefin.core.data.download.MediaDownloadController
import hu.bbara.purefin.core.data.session.UserSessionRepository
import hu.bbara.purefin.core.model.Episode
import hu.bbara.purefin.core.model.Movie
import hu.bbara.purefin.core.model.Season
import hu.bbara.purefin.core.model.Series
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -34,10 +25,6 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.UUID import java.util.UUID
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.ImageType
import org.jellyfin.sdk.model.api.MediaSourceInfo
import org.jellyfin.sdk.model.api.PlayMethod
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -47,10 +34,9 @@ import javax.inject.Singleton
class MediaDownloadManager @Inject constructor( 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 downloadMediaSourceResolver: DownloadMediaSourceResolver,
private val offlineCatalogStore: OfflineCatalogStore, private val offlineCatalogStore: OfflineCatalogStore,
private val smartDownloadStore: SmartDownloadStore, private val smartDownloadStore: SmartDownloadStore,
private val userSessionRepository: UserSessionRepository,
) : MediaDownloadController { ) : MediaDownloadController {
private val stateFlows = ConcurrentHashMap<String, MutableStateFlow<DownloadState>>() private val stateFlows = ConcurrentHashMap<String, MutableStateFlow<DownloadState>>()
@@ -127,53 +113,19 @@ class MediaDownloadManager @Inject constructor(
override suspend fun downloadMovie(movieId: UUID) { override suspend fun downloadMovie(movieId: UUID) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
try { try {
val sources = jellyfinApiClient.getMediaSources(movieId) val source = downloadMediaSourceResolver.resolveMovieDownload(movieId) ?: run {
val source = sources.firstOrNull() ?: run { Log.e(TAG, "No downloadable movie source for $movieId")
Log.e(TAG, "No media sources for $movieId")
return@withContext
}
val shouldTranscode = source.shouldUseTranscodingUrl()
val url = jellyfinApiClient.getMediaPlaybackUrl(movieId, source) ?: run {
Log.e(TAG, "No playback URL for $movieId")
return@withContext return@withContext
} }
val itemInfo = jellyfinApiClient.getItemInfo(movieId) ?: run { offlineCatalogStore.saveMovies(listOf(source.movie))
Log.e(TAG, "No item info for $movieId")
return@withContext
}
val serverUrl = userSessionRepository.serverUrl.first().trim() Log.d(TAG, "Starting download for '${source.movie.title}' from: ${source.playbackUrl}")
val movie = Movie(
id = itemInfo.id,
libraryId = itemInfo.parentId ?: UUID.randomUUID(),
title = itemInfo.name ?: "Unknown title",
progress = itemInfo.userData?.playedPercentage,
watched = itemInfo.userData?.played ?: false,
year = itemInfo.productionYear?.toString()
?: itemInfo.premiereDate?.year?.toString().orEmpty(),
rating = itemInfo.officialRating ?: "NR",
runtime = formatRuntime(itemInfo.runTimeTicks),
synopsis = itemInfo.overview ?: "No synopsis available",
format = itemInfo.container?.uppercase() ?: "VIDEO",
imageUrlPrefix = JellyfinImageHelper.toPrefixImageUrl(
url = serverUrl,
itemId = itemInfo.id
),
subtitles = "ENG",
audioTrack = "ENG",
cast = emptyList()
)
offlineCatalogStore.saveMovies(listOf(movie))
Log.d(TAG, "Starting download for '${movie.title}' from: $url")
val request = buildDownloadRequest( val request = buildDownloadRequest(
mediaId = movieId, mediaId = movieId,
playbackUrl = url, playbackUrl = source.playbackUrl,
title = movie.title, title = source.movie.title,
isDirectPlay = !shouldTranscode customCacheKey = source.customCacheKey,
) )
PurefinDownloadService.sendAddDownload(context, request) PurefinDownloadService.sendAddDownload(context, request)
Log.d(TAG, "Download request sent for $movieId") Log.d(TAG, "Download request sent for $movieId")
@@ -198,50 +150,27 @@ class MediaDownloadManager @Inject constructor(
override suspend fun downloadEpisode(episodeId: UUID) { override suspend fun downloadEpisode(episodeId: UUID) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
try { try {
val serverUrl = userSessionRepository.serverUrl.first().trim() val source = downloadMediaSourceResolver.resolveEpisodeDownload(episodeId) ?: run {
val sources = jellyfinApiClient.getMediaSources(episodeId) Log.e(TAG, "No downloadable episode source for $episodeId")
val source = sources.firstOrNull() ?: run {
Log.e(TAG, "No media sources for episode $episodeId")
return@withContext
}
val shouldTranscode = source.shouldUseTranscodingUrl()
val url = jellyfinApiClient.getMediaPlaybackUrl(episodeId, source) ?: run {
Log.e(TAG, "No playback URL for episode $episodeId")
return@withContext return@withContext
} }
val episode = jellyfinApiClient.getItemInfo(episodeId)?.toEpisode(serverUrl) ?: run { if (offlineCatalogStore.getSeriesBasic(source.series.id) == null) {
Log.e(TAG, "Episode not found $episodeId") offlineCatalogStore.saveSeries(listOf(source.series))
return@withContext
} }
val series = jellyfinApiClient.getItemInfo(episode.seriesId)?.toSeries(serverUrl) ?: run { if (offlineCatalogStore.getSeason(source.season.id) == null) {
Log.e(TAG, "Series not found ${episode.seriesId}") offlineCatalogStore.saveSeason(source.season)
return@withContext
} }
val season = jellyfinApiClient.getItemInfo(episode.seasonId)?.toSeason(series.id) ?: run { offlineCatalogStore.saveEpisode(source.episode)
Log.e(TAG, "Season not found ${episode.seasonId}")
return@withContext
}
if (offlineCatalogStore.getSeriesBasic(series.id) == null) { Log.d(TAG, "Starting download for episode '${source.episode.title}' from: ${source.playbackUrl}")
offlineCatalogStore.saveSeries(listOf(series))
}
if (offlineCatalogStore.getSeason(season.id) == null) {
offlineCatalogStore.saveSeason(season)
}
offlineCatalogStore.saveEpisode(episode)
Log.d(TAG, "Starting download for episode '${episode.title}' from: $url")
val request = buildDownloadRequest( val request = buildDownloadRequest(
mediaId = episodeId, mediaId = episodeId,
playbackUrl = url, playbackUrl = source.playbackUrl,
title = episode.title, title = source.episode.title,
isDirectPlay = !shouldTranscode customCacheKey = source.customCacheKey,
) )
PurefinDownloadService.sendAddDownload(context, request) PurefinDownloadService.sendAddDownload(context, request)
Log.d(TAG, "Download request sent for episode $episodeId") Log.d(TAG, "Download request sent for episode $episodeId")
@@ -299,17 +228,13 @@ class MediaDownloadManager @Inject constructor(
private suspend fun syncSmartDownloadsForSeries(seriesId: UUID) { private suspend fun syncSmartDownloadsForSeries(seriesId: UUID) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val serverUrl = userSessionRepository.serverUrl.first().trim()
// 1. Get currently downloaded episodes for this series // 1. Get currently downloaded episodes for this series
val downloadedEpisodes = offlineCatalogStore.getEpisodesBySeries(seriesId) val downloadedEpisodes = offlineCatalogStore.getEpisodesBySeries(seriesId)
// 2. Check watched status from server and delete watched downloads // 2. Check watched status from server and delete watched downloads
val unwatchedDownloaded = mutableListOf<UUID>() val unwatchedDownloaded = mutableListOf<UUID>()
for (episode in downloadedEpisodes) { for (episode in downloadedEpisodes) {
val itemInfo = jellyfinApiClient.getItemInfo(episode.id) if (downloadMediaSourceResolver.isEpisodeWatched(episode.id)) {
val isWatched = itemInfo?.userData?.played ?: false
if (isWatched) {
Log.d(TAG, "Smart download: removing watched episode ${episode.title}") Log.d(TAG, "Smart download: removing watched episode ${episode.title}")
cancelEpisodeDownload(episode.id) cancelEpisodeDownload(episode.id)
} else { } else {
@@ -317,22 +242,15 @@ class MediaDownloadManager @Inject constructor(
} }
} }
// 3. Get all episodes of the series from the server in order // 3. Find unwatched episodes not already downloaded
val seasons = jellyfinApiClient.getSeasons(seriesId)
val allEpisodes = mutableListOf<BaseItemDto>()
for (season in seasons) {
val episodes = jellyfinApiClient.getEpisodesInSeason(seriesId, season.id)
allEpisodes.addAll(episodes)
}
// 4. Find unwatched episodes not already downloaded
val needed = SMART_DOWNLOAD_COUNT - unwatchedDownloaded.size val needed = SMART_DOWNLOAD_COUNT - unwatchedDownloaded.size
if (needed <= 0) return@withContext if (needed <= 0) return@withContext
val toDownload = allEpisodes val toDownload = downloadMediaSourceResolver.getUnwatchedEpisodeIds(
.filter { ep -> ep.userData?.played != true && ep.id !in unwatchedDownloaded } seriesId = seriesId,
.take(needed) excludedEpisodeIds = unwatchedDownloaded.toSet(),
.map { it.id } limit = needed,
)
if (toDownload.isNotEmpty()) { if (toDownload.isNotEmpty()) {
Log.d(TAG, "Smart download: queuing ${toDownload.size} episodes for series $seriesId") Log.d(TAG, "Smart download: queuing ${toDownload.size} episodes for series $seriesId")
@@ -349,18 +267,12 @@ class MediaDownloadManager @Inject constructor(
mediaId: UUID, mediaId: UUID,
playbackUrl: String, playbackUrl: String,
title: String, title: String,
isDirectPlay: Boolean customCacheKey: String?,
): DownloadRequest { ): DownloadRequest {
val builder = DownloadRequest.Builder(mediaId.toString(), playbackUrl.toUri()) val builder = DownloadRequest.Builder(mediaId.toString(), playbackUrl.toUri())
.setData(title.toByteArray(Charsets.UTF_8)) .setData(title.toByteArray(Charsets.UTF_8))
if (isDirectPlay) { customCacheKey?.let(builder::setCustomCacheKey)
playbackCustomCacheKey(
mediaId = mediaId.toString(),
playbackUrl = playbackUrl,
playMethod = PlayMethod.DIRECT_PLAY
)?.let(builder::setCustomCacheKey)
}
return builder.build() return builder.build()
} }
@@ -375,73 +287,8 @@ class MediaDownloadManager @Inject constructor(
else -> DownloadState.NotDownloaded else -> DownloadState.NotDownloaded
} }
private fun formatRuntime(ticks: Long?): String {
if (ticks == null || ticks <= 0) return ""
val totalSeconds = ticks / 10_000_000
val hours = java.util.concurrent.TimeUnit.SECONDS.toHours(totalSeconds)
val minutes = java.util.concurrent.TimeUnit.SECONDS.toMinutes(totalSeconds) % 60
return if (hours > 0) "${hours}h ${minutes}m" else "${minutes}m"
}
private fun BaseItemDto.toEpisode(serverUrl: String): Episode {
return Episode(
id = id,
seriesId = seriesId!!,
seasonId = parentId!!,
title = name ?: "Unknown title",
index = indexNumber ?: 0,
releaseDate = productionYear?.toString() ?: "",
rating = officialRating ?: "NR",
runtime = formatRuntime(runTimeTicks),
progress = userData?.playedPercentage,
watched = userData?.played ?: false,
format = container?.uppercase() ?: "VIDEO",
synopsis = overview ?: "No synopsis available.",
imageUrlPrefix = JellyfinImageHelper.toPrefixImageUrl(
url = serverUrl,
itemId = id
),
cast = emptyList()
)
}
private fun BaseItemDto.toSeries(serverUrl: String): Series {
return Series(
id = id,
libraryId = parentId ?: java.util.UUID.randomUUID(),
name = name ?: "Unknown",
synopsis = overview ?: "No synopsis available",
year = productionYear?.toString() ?: premiereDate?.year?.toString().orEmpty(),
imageUrlPrefix = JellyfinImageHelper.toPrefixImageUrl(
url = serverUrl,
itemId = id
),
unwatchedEpisodeCount = userData?.unplayedItemCount ?: 0,
seasonCount = childCount ?: 0,
seasons = emptyList(),
cast = emptyList()
)
}
private fun BaseItemDto.toSeason(seriesId: UUID): Season {
return Season(
id = id,
seriesId = this.seriesId ?: seriesId,
name = name ?: "Unknown",
index = indexNumber ?: 0,
unwatchedEpisodeCount = userData?.unplayedItemCount ?: 0,
episodeCount = childCount ?: 0,
episodes = emptyList()
)
}
companion object { companion object {
private const val TAG = "MediaDownloadManager" private const val TAG = "MediaDownloadManager"
private const val SMART_DOWNLOAD_COUNT = 5 private const val SMART_DOWNLOAD_COUNT = 5
} }
} }
private fun MediaSourceInfo.shouldUseTranscodingUrl(): Boolean {
return supportsTranscoding == true &&
(supportsDirectPlay == false || transcodingUrl != null)
}

View File

@@ -1,4 +1,4 @@
package hu.bbara.purefin.feature.download package hu.bbara.purefin.core.download
import android.app.Notification import android.app.Notification
import android.content.Context import android.content.Context
@@ -50,7 +50,7 @@ class PurefinDownloadService : DownloadService(
if (activeDownloads.isEmpty()) { if (activeDownloads.isEmpty()) {
return entryPoint.downloadNotificationHelper().buildProgressNotification( return entryPoint.downloadNotificationHelper().buildProgressNotification(
this, this,
R.drawable.ic_launcher_foreground, android.R.drawable.stat_sys_download,
null, null,
null, null,
downloads, downloads,
@@ -95,7 +95,7 @@ class PurefinDownloadService : DownloadService(
} }
return NotificationCompat.Builder(this, DOWNLOAD_CHANNEL_ID) return NotificationCompat.Builder(this, DOWNLOAD_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_launcher_foreground) .setSmallIcon(android.R.drawable.stat_sys_download)
.setContentTitle(title) .setContentTitle(title)
.setContentText(contentText) .setContentText(contentText)
.setProgress(100, percent.toInt(), false) .setProgress(100, percent.toInt(), false)

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="download_channel_name">Downloads</string> <string name="download_channel_name">Downloads</string>
<string name="download_channel_description">Media download progress</string> <string name="download_channel_description">Purefin offline downloads</string>
</resources> </resources>

View File

@@ -1,4 +1,4 @@
package hu.bbara.purefin.feature.download package hu.bbara.purefin.core.download
import androidx.media3.datasource.DataSource import androidx.media3.datasource.DataSource
import androidx.media3.datasource.cache.CacheDataSource import androidx.media3.datasource.cache.CacheDataSource

View File

@@ -0,0 +1,16 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.kotlin.jvm)
}
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
jvmToolchain(11)
}
dependencies {
implementation(project(":core:model"))
}

View File

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

View File

@@ -0,0 +1,40 @@
package hu.bbara.purefin.core.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

@@ -0,0 +1,38 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.hilt)
alias(libs.plugins.ksp)
}
android {
namespace = "hu.bbara.purefin.core.navigation"
compileSdk = 36
defaultConfig {
minSdk = 29
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
}
dependencies {
implementation(libs.androidx.navigation3.runtime)
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.hilt)
ksp(libs.hilt.compiler)
testImplementation(libs.junit)
}

View File

@@ -0,0 +1,15 @@
package hu.bbara.purefin.core.navigation
import java.util.UUID
import kotlinx.serialization.Serializable
import hu.bbara.purefin.core.navigation.UuidSerializer
@Serializable
data class EpisodeDto(
@Serializable(with = UuidSerializer::class)
val id: UUID,
@Serializable(with = UuidSerializer::class)
val seasonId: UUID,
@Serializable(with = UuidSerializer::class)
val seriesId: UUID,
)

View File

@@ -0,0 +1,12 @@
package hu.bbara.purefin.core.navigation
import java.util.UUID
import kotlinx.serialization.Serializable
import hu.bbara.purefin.core.navigation.UuidSerializer
@Serializable
data class LibraryDto(
@Serializable(with = UuidSerializer::class)
val id: UUID,
val name: String,
)

View File

@@ -0,0 +1,11 @@
package hu.bbara.purefin.core.navigation
import java.util.UUID
import kotlinx.serialization.Serializable
import hu.bbara.purefin.core.navigation.UuidSerializer
@Serializable
data class MovieDto(
@Serializable(with = UuidSerializer::class)
val id: UUID,
)

View File

@@ -1,4 +1,4 @@
package hu.bbara.purefin.feature.shared.navigation package hu.bbara.purefin.core.navigation
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow

View File

@@ -1,4 +1,4 @@
package hu.bbara.purefin.feature.shared.navigation package hu.bbara.purefin.core.navigation
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides

View File

@@ -1,4 +1,4 @@
package hu.bbara.purefin.feature.shared.navigation package hu.bbara.purefin.core.navigation
import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.NavKey
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable

View File

@@ -0,0 +1,11 @@
package hu.bbara.purefin.core.navigation
import java.util.UUID
import kotlinx.serialization.Serializable
import hu.bbara.purefin.core.navigation.UuidSerializer
@Serializable
data class SeriesDto(
@Serializable(with = UuidSerializer::class)
val id: UUID,
)

View File

@@ -0,0 +1,20 @@
package hu.bbara.purefin.core.navigation
import java.util.UUID
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
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())
}
}

View File

@@ -1,4 +1,4 @@
package hu.bbara.purefin.feature.shared.navigation package hu.bbara.purefin.core.navigation
import java.util.UUID import java.util.UUID
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString

View File

@@ -31,6 +31,7 @@ kotlin {
dependencies { dependencies {
implementation(project(":core:model")) implementation(project(":core:model"))
implementation(project(":core:data")) implementation(project(":core:data"))
implementation(project(":core:image"))
implementation(libs.hilt) implementation(libs.hilt)
ksp(libs.hilt.compiler) ksp(libs.hilt.compiler)
implementation(libs.androidx.lifecycle.viewmodel.ktx) implementation(libs.androidx.lifecycle.viewmodel.ktx)

View File

@@ -9,7 +9,7 @@ import androidx.media3.common.TrackSelectionOverride
import androidx.media3.common.Tracks import androidx.media3.common.Tracks
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import dagger.hilt.android.scopes.ViewModelScoped import dagger.hilt.android.scopes.ViewModelScoped
import hu.bbara.purefin.core.data.client.PlaybackReportContext import hu.bbara.purefin.core.data.PlaybackReportContext
import hu.bbara.purefin.core.player.model.QueueItemUi import hu.bbara.purefin.core.player.model.QueueItemUi
import hu.bbara.purefin.core.player.model.TrackOption import hu.bbara.purefin.core.player.model.TrackOption
import hu.bbara.purefin.core.player.model.TrackType import hu.bbara.purefin.core.player.model.TrackType

View File

@@ -4,7 +4,7 @@ import android.util.Log
import dagger.hilt.android.scopes.ViewModelScoped import dagger.hilt.android.scopes.ViewModelScoped
import hu.bbara.purefin.core.data.MediaProgressWriter import hu.bbara.purefin.core.data.MediaProgressWriter
import hu.bbara.purefin.core.data.PlaybackProgressReporter import hu.bbara.purefin.core.data.PlaybackProgressReporter
import hu.bbara.purefin.core.data.client.PlaybackReportContext import hu.bbara.purefin.core.data.PlaybackReportContext
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job

View File

@@ -0,0 +1,41 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.hilt)
alias(libs.plugins.ksp)
}
android {
namespace = "hu.bbara.purefin.data.catalog"
compileSdk = 36
defaultConfig {
minSdk = 29
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
}
dependencies {
implementation(project(":core:model"))
implementation(project(":core:data"))
implementation(project(":core:image"))
implementation(project(":data:jellyfin"))
implementation(project(":data:offline"))
implementation(libs.jellyfin.core)
implementation(libs.hilt)
ksp(libs.hilt.compiler)
implementation(libs.datastore)
testImplementation(libs.junit)
}

View File

@@ -1,5 +1,8 @@
package hu.bbara.purefin.core.data package hu.bbara.purefin.data.catalog
import hu.bbara.purefin.core.data.MediaCatalogReader
import hu.bbara.purefin.core.data.MediaProgressWriter
import hu.bbara.purefin.core.data.NetworkMonitor
import hu.bbara.purefin.core.model.Episode import hu.bbara.purefin.core.model.Episode
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

Some files were not shown because too many files have changed in this diff Show More