Move data loading to HomeSettingsService

This commit is contained in:
Damontecres 2026-01-24 21:12:46 -05:00
parent a174d981a8
commit 9834991392
No known key found for this signature in database
3 changed files with 275 additions and 262 deletions

View file

@ -3,15 +3,24 @@ package com.github.damontecres.wholphin.services
import android.content.Context
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.NavDrawerItemRepository
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.HomePageResolvedSettings
import com.github.damontecres.wholphin.data.model.HomePageSettings
import com.github.damontecres.wholphin.data.model.HomeRowConfig
import com.github.damontecres.wholphin.data.model.HomeRowConfigDisplay
import com.github.damontecres.wholphin.data.model.HomeRowViewOptions
import com.github.damontecres.wholphin.preferences.HomePagePreferences
import com.github.damontecres.wholphin.ui.DefaultItemFields
import com.github.damontecres.wholphin.ui.SlimItemFields
import com.github.damontecres.wholphin.ui.components.getGenreImageMap
import com.github.damontecres.wholphin.ui.main.settings.Library
import com.github.damontecres.wholphin.ui.nav.ServerNavDrawerItem
import com.github.damontecres.wholphin.ui.toServerString
import com.github.damontecres.wholphin.util.GetGenresRequestHandler
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
import com.github.damontecres.wholphin.util.HomeRowLoadingState
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.serialization.ExperimentalSerializationApi
@ -22,6 +31,12 @@ import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.displayPreferencesApi
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
import org.jellyfin.sdk.model.UUID
import org.jellyfin.sdk.model.api.ItemSortBy
import org.jellyfin.sdk.model.api.SortOrder
import org.jellyfin.sdk.model.api.UserDto
import org.jellyfin.sdk.model.api.request.GetGenresRequest
import org.jellyfin.sdk.model.api.request.GetItemsRequest
import org.jellyfin.sdk.model.api.request.GetLatestMediaRequest
import org.jellyfin.sdk.model.serializer.toUUID
import timber.log.Timber
import java.io.File
@ -36,6 +51,8 @@ class HomeSettingsService
private val api: ApiClient,
private val userPreferencesService: UserPreferencesService,
private val navDrawerItemRepository: NavDrawerItemRepository,
private val latestNextUpService: LatestNextUpService,
private val imageUrlService: ImageUrlService,
) {
val jsonParser =
Json {
@ -285,6 +302,244 @@ class HomeSettingsService
}
}
suspend fun fetchDataForRow(
row: HomeRowConfig,
scope: CoroutineScope,
prefs: HomePagePreferences,
userDto: UserDto,
libraries: List<Library>,
limit: Int = prefs.maxItemsPerRow,
): List<HomeRowLoadingState> =
when (row) {
is HomeRowConfig.ContinueWatching -> {
val resume = latestNextUpService.getResume(userDto.id, limit, true)
listOf(
HomeRowLoadingState.Success(
title = context.getString(R.string.continue_watching),
items = resume,
viewOptions = row.viewOptions,
),
)
}
is HomeRowConfig.NextUp -> {
val nextUp =
latestNextUpService.getNextUp(
userDto.id,
limit,
prefs.enableRewatchingNextUp,
false,
)
listOf(
HomeRowLoadingState.Success(
title = context.getString(R.string.next_up),
items = nextUp,
viewOptions = row.viewOptions,
),
)
}
is HomeRowConfig.ContinueWatchingCombined -> {
val resume =
latestNextUpService.getResume(userDto.id, limit, true)
val nextUp =
latestNextUpService.getNextUp(
userDto.id,
limit,
prefs.enableRewatchingNextUp,
false,
)
listOf(
HomeRowLoadingState.Success(
title = context.getString(R.string.continue_watching),
items =
latestNextUpService.buildCombined(
resume,
nextUp,
),
viewOptions = row.viewOptions,
),
)
}
is HomeRowConfig.Genres -> {
val request =
GetGenresRequest(
parentId = row.parentId,
userId = userDto.id,
limit = limit,
)
val items =
GetGenresRequestHandler
.execute(api, request)
.content.items
val genreIds = items.map { it.id }
val genreImages =
getGenreImageMap(
api = api,
scope = scope,
imageUrlService = imageUrlService,
genres = genreIds,
parentId = row.parentId,
includeItemTypes = null,
cardWidthPx = null,
)
val genres =
items.map {
BaseItem(it, false, genreImages[it.id])
}
val name =
libraries
.firstOrNull { it.itemId == row.parentId }
?.name
val title =
name?.let { context.getString(R.string.genres_in, it) }
?: context.getString(R.string.genres)
listOf(
HomeRowLoadingState.Success(
title,
genres,
viewOptions = row.viewOptions,
),
)
}
is HomeRowConfig.RecentlyAdded -> {
val name =
libraries
.firstOrNull { it.itemId == row.parentId }
?.name
val title =
name?.let { context.getString(R.string.recently_added_in, it) }
?: context.getString(R.string.recently_added)
val request =
GetLatestMediaRequest(
fields = SlimItemFields,
imageTypeLimit = 1,
parentId = row.parentId,
groupItems = true,
limit = limit,
isPlayed = null, // Server will handle user's preference
)
val latest =
api.userLibraryApi
.getLatestMedia(request)
.content
.map { BaseItem.Companion.from(it, api, true) }
.let {
HomeRowLoadingState.Success(
title,
it,
row.viewOptions,
)
}
listOf(latest)
}
is HomeRowConfig.RecentlyReleased -> {
val name =
libraries
.firstOrNull { it.itemId == row.parentId }
?.name
val title =
name?.let {
context.getString(R.string.recently_released_in, it)
} ?: context.getString(R.string.recently_released)
val request =
GetItemsRequest(
parentId = row.parentId,
limit = limit,
sortBy = listOf(ItemSortBy.PREMIERE_DATE),
sortOrder = listOf(SortOrder.DESCENDING),
fields = DefaultItemFields,
recursive = true,
)
GetItemsRequestHandler
.execute(api, request)
.content.items
.map { BaseItem.Companion.from(it, api, true) }
.let {
listOf(
HomeRowLoadingState.Success(
title,
it,
row.viewOptions,
),
)
}
}
is HomeRowConfig.ByParent -> {
val request =
GetItemsRequest(
userId = userDto.id,
parentId = row.parentId,
recursive = row.recursive,
sortBy = row.sort?.let { listOf(it.sort) },
sortOrder = row.sort?.let { listOf(it.direction) },
limit = limit,
fields = DefaultItemFields,
)
val name =
api.userLibraryApi
.getItem(itemId = row.parentId)
.content.name
GetItemsRequestHandler
.execute(api, request)
.content.items
.map { BaseItem(it, true) }
.let {
listOf(
HomeRowLoadingState.Success(
name ?: context.getString(R.string.collection),
it,
row.viewOptions,
),
)
}
}
is HomeRowConfig.GetItems -> {
val request =
row.getItems.let {
if (it.limit == null) {
it.copy(
userId = userDto.id,
limit = limit,
)
} else {
it.copy(
userId = userDto.id,
)
}
}
val name =
if (row.name == null && request.parentId != null) {
// If a name was not provided, use the parent's name if available
api.userLibraryApi
.getItem(itemId = request.parentId!!)
.content.name
} else {
row.name
}
GetItemsRequestHandler
.execute(api, request)
.content.items
.map { BaseItem.Companion.from(it, api, true) }
.let {
listOf(
HomeRowLoadingState.Success(
name ?: context.getString(R.string.collection),
it,
row.viewOptions,
),
)
}
}
}
companion object {
const val DISPLAY_PREF_ID = "default"
const val CUSTOM_PREF_ID = "home_settings"

View file

@ -41,6 +41,7 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
@ -111,6 +112,7 @@ class GenreViewModel
val genreToUrl =
getGenreImageMap(
api = api,
scope = viewModelScope,
imageUrlService = imageUrlService,
genres = genres.map { it.id },
parentId = itemId,
@ -141,8 +143,9 @@ class GenreViewModel
}
}
suspend fun ViewModel.getGenreImageMap(
suspend fun getGenreImageMap(
api: ApiClient,
scope: CoroutineScope,
imageUrlService: ImageUrlService,
genres: List<UUID>,
parentId: UUID,
@ -153,7 +156,7 @@ suspend fun ViewModel.getGenreImageMap(
val semaphore = Semaphore(4)
genres
.map { genreId ->
viewModelScope.async(Dispatchers.IO) {
scope.async(Dispatchers.IO) {
semaphore.withPermit {
val item =
GetItemsRequestHandler

View file

@ -14,20 +14,12 @@ import com.github.damontecres.wholphin.data.model.HomePageSettings
import com.github.damontecres.wholphin.data.model.HomeRowConfig
import com.github.damontecres.wholphin.data.model.HomeRowConfigDisplay
import com.github.damontecres.wholphin.data.model.HomeRowViewOptions
import com.github.damontecres.wholphin.preferences.HomePagePreferences
import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.HomeSettingsService
import com.github.damontecres.wholphin.services.ImageUrlService
import com.github.damontecres.wholphin.services.LatestNextUpService
import com.github.damontecres.wholphin.services.UserPreferencesService
import com.github.damontecres.wholphin.ui.DefaultItemFields
import com.github.damontecres.wholphin.ui.SlimItemFields
import com.github.damontecres.wholphin.ui.components.getGenreImageMap
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.nav.ServerNavDrawerItem
import com.github.damontecres.wholphin.ui.showToast
import com.github.damontecres.wholphin.util.GetGenresRequestHandler
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
import com.github.damontecres.wholphin.util.HomeRowLoadingState
import com.github.damontecres.wholphin.util.LoadingState
import dagger.hilt.android.lifecycle.HiltViewModel
@ -36,15 +28,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
import org.jellyfin.sdk.model.api.CollectionType
import org.jellyfin.sdk.model.api.ItemSortBy
import org.jellyfin.sdk.model.api.SortOrder
import org.jellyfin.sdk.model.api.UserDto
import org.jellyfin.sdk.model.api.request.GetGenresRequest
import org.jellyfin.sdk.model.api.request.GetItemsRequest
import org.jellyfin.sdk.model.api.request.GetLatestMediaRequest
import timber.log.Timber
import java.util.UUID
import javax.inject.Inject
@ -54,14 +38,11 @@ class HomeSettingsViewModel
@Inject
constructor(
@param:ApplicationContext private val context: Context,
private val api: ApiClient,
private val homeSettingsService: HomeSettingsService,
private val serverRepository: ServerRepository,
private val userPreferencesService: UserPreferencesService,
private val navDrawerItemRepository: NavDrawerItemRepository,
private val backdropService: BackdropService,
private val latestNextUpService: LatestNextUpService,
private val imageUrlService: ImageUrlService,
) : ViewModel() {
private val _state = MutableStateFlow(HomePageSettingsState.EMPTY)
val state: StateFlow<HomePageSettingsState> = _state
@ -102,12 +83,21 @@ class HomeSettingsViewModel
val rows =
serverRepository.currentUserDto.value?.let { userDto ->
val prefs = userPreferencesService.getCurrent().appPreferences.homePagePreferences
state.value.rows
.map { it.config }
.map { row ->
// TODO parallelize
parseRow(prefs, userDto, row, limit)
}.flatten()
state.value.let { state ->
state.rows
.map { it.config }
.map { row ->
// TODO parallelize
homeSettingsService.fetchDataForRow(
row = row,
scope = viewModelScope,
prefs = prefs,
userDto = userDto,
libraries = state.libraries,
limit = limit,
)
}.flatten()
}
}
rows?.let { rows ->
_state.update {
@ -116,241 +106,6 @@ class HomeSettingsViewModel
}
}
private suspend fun parseRow(
prefs: HomePagePreferences,
userDto: UserDto,
row: HomeRowConfig,
limit: Int,
): List<HomeRowLoadingState> =
when (row) {
is HomeRowConfig.ContinueWatching -> {
val resume = latestNextUpService.getResume(userDto.id, limit, true)
listOf(
HomeRowLoadingState.Success(
title = context.getString(R.string.continue_watching),
items = resume,
viewOptions = row.viewOptions,
),
)
}
is HomeRowConfig.NextUp -> {
val nextUp =
latestNextUpService.getNextUp(
userDto.id,
limit,
prefs.enableRewatchingNextUp,
false,
)
listOf(
HomeRowLoadingState.Success(
title = context.getString(R.string.next_up),
items = nextUp,
viewOptions = row.viewOptions,
),
)
}
is HomeRowConfig.ContinueWatchingCombined -> {
val resume =
latestNextUpService.getResume(userDto.id, limit, true)
val nextUp =
latestNextUpService.getNextUp(
userDto.id,
limit,
prefs.enableRewatchingNextUp,
false,
)
listOf(
HomeRowLoadingState.Success(
title = context.getString(R.string.continue_watching),
items =
latestNextUpService.buildCombined(
resume,
nextUp,
),
viewOptions = row.viewOptions,
),
)
}
is HomeRowConfig.Genres -> {
val request =
GetGenresRequest(
parentId = row.parentId,
userId = userDto.id,
limit = limit,
)
val items =
GetGenresRequestHandler
.execute(api, request)
.content.items
val genreIds = items.map { it.id }
val genreImages =
getGenreImageMap(
api = api,
imageUrlService = imageUrlService,
genres = genreIds,
parentId = row.parentId,
includeItemTypes = null,
cardWidthPx = null,
)
val genres =
items.map {
BaseItem(it, false, genreImages[it.id])
}
val name =
_state.value.libraries
.firstOrNull { it.itemId == row.parentId }
?.name
val title =
name?.let { context.getString(R.string.genres_in, it) }
?: context.getString(R.string.genres)
listOf(
HomeRowLoadingState.Success(
title,
genres,
viewOptions = row.viewOptions,
),
)
}
is HomeRowConfig.RecentlyAdded -> {
val name =
_state.value.libraries
.firstOrNull { it.itemId == row.parentId }
?.name
val title =
name?.let { context.getString(R.string.recently_added_in, it) }
?: context.getString(R.string.recently_added)
val request =
GetLatestMediaRequest(
fields = SlimItemFields,
imageTypeLimit = 1,
parentId = row.parentId,
groupItems = true,
limit = limit,
isPlayed = null, // Server will handle user's preference
)
val latest =
api.userLibraryApi
.getLatestMedia(request)
.content
.map { BaseItem.Companion.from(it, api, true) }
.let {
HomeRowLoadingState.Success(
title,
it,
row.viewOptions,
)
}
listOf(latest)
}
is HomeRowConfig.RecentlyReleased -> {
val name =
_state.value.libraries
.firstOrNull { it.itemId == row.parentId }
?.name
val title =
name?.let {
context.getString(R.string.recently_released_in, it)
} ?: context.getString(R.string.recently_released)
val request =
GetItemsRequest(
parentId = row.parentId,
limit = limit,
sortBy = listOf(ItemSortBy.PREMIERE_DATE),
sortOrder = listOf(SortOrder.DESCENDING),
fields = DefaultItemFields,
recursive = true,
)
GetItemsRequestHandler
.execute(api, request)
.content.items
.map { BaseItem.Companion.from(it, api, true) }
.let {
listOf(
HomeRowLoadingState.Success(
title,
it,
row.viewOptions,
),
)
}
}
is HomeRowConfig.ByParent -> {
val request =
GetItemsRequest(
userId = userDto.id,
parentId = row.parentId,
recursive = row.recursive,
sortBy = row.sort?.let { listOf(it.sort) },
sortOrder = row.sort?.let { listOf(it.direction) },
limit = limit,
fields = DefaultItemFields,
)
val name =
api.userLibraryApi
.getItem(itemId = row.parentId)
.content.name
GetItemsRequestHandler
.execute(api, request)
.content.items
.map { BaseItem(it, true) }
.let {
listOf(
HomeRowLoadingState.Success(
name ?: context.getString(R.string.collection),
it,
row.viewOptions,
),
)
}
}
is HomeRowConfig.GetItems -> {
val request =
row.getItems.let {
if (it.limit == null) {
it.copy(
userId = userDto.id,
limit = limit,
)
} else {
it.copy(
userId = userDto.id,
)
}
}
val name =
if (row.name == null && request.parentId != null) {
// If a name was not provided, use the parent's name if available
api.userLibraryApi
.getItem(itemId = request.parentId!!)
.content.name
} else {
row.name
}
GetItemsRequestHandler
.execute(api, request)
.content.items
.map { BaseItem.Companion.from(it, api, true) }
.let {
listOf(
HomeRowLoadingState.Success(
name ?: context.getString(R.string.collection),
it,
row.viewOptions,
),
)
}
}
}
private fun <T> List<T>.move(
direction: MoveDirection,
index: Int,