Apply combined continue/next up, if enabled, on library recommended tabs (#533)

## Description
Respect the combine Continue Watching & Next Up setting on TV Show
library recommended tabs.

This uses the same implementation on the home screen, but only includes
items for that particular tv show library.

### Related issues
Fixes #532
This commit is contained in:
Ray 2025-12-21 19:00:59 -05:00 committed by GitHub
parent 11dda19b8a
commit 6df89fe8c9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 96 additions and 59 deletions

View file

@ -9,14 +9,15 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.viewModelScope
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.preferences.AppPreference
import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.DatePlayedService
import com.github.damontecres.wholphin.services.FavoriteWatchManager
import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.ui.SlimItemFields
import com.github.damontecres.wholphin.ui.data.RowColumn
import com.github.damontecres.wholphin.ui.main.buildCombinedNextUp
import com.github.damontecres.wholphin.ui.setValueOnMain
import com.github.damontecres.wholphin.ui.toBaseItems
import com.github.damontecres.wholphin.util.ExceptionHandler
@ -57,6 +58,7 @@ class RecommendedTvShowViewModel
private val api: ApiClient,
private val serverRepository: ServerRepository,
private val preferencesDataStore: DataStore<AppPreferences>,
private val datePlayedService: DatePlayedService,
@Assisted val parentId: UUID,
navigationManager: NavigationManager,
favoriteWatchManager: FavoriteWatchManager,
@ -78,17 +80,17 @@ class RecommendedTvShowViewModel
override fun init() {
viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) {
val itemsPerRow =
preferencesDataStore.data
.firstOrNull()
?.homePagePreferences
?.maxItemsPerRow
?: AppPreference.HomePageItems.defaultValue.toInt()
val preferences =
preferencesDataStore.data.firstOrNull() ?: AppPreferences.getDefaultInstance()
val combineNextUp = preferences.homePagePreferences.combineContinueNext
val itemsPerRow = preferences.homePagePreferences.maxItemsPerRow
val userId = serverRepository.currentUser.value?.id
try {
val resumeItemsDeferred =
viewModelScope.async(Dispatchers.IO) {
val resumeItemsRequest =
GetResumeItemsRequest(
userId = userId,
parentId = parentId,
fields = SlimItemFields,
includeItemTypes = listOf(BaseItemKind.EPISODE),
@ -106,12 +108,14 @@ class RecommendedTvShowViewModel
viewModelScope.async(Dispatchers.IO) {
val nextUpRequest =
GetNextUpRequest(
parentId = parentId,
userId = userId,
fields = SlimItemFields,
enableUserData = true,
startIndex = 0,
imageTypeLimit = 1,
parentId = parentId,
limit = itemsPerRow,
enableTotalRecordCount = false,
enableResumable = false,
enableUserData = true,
enableRewatching = preferences.homePagePreferences.enableRewatchingNextUp,
)
GetNextUpRequestHandler
@ -119,21 +123,45 @@ class RecommendedTvShowViewModel
.toBaseItems(api, true)
}
val resumeItems = resumeItemsDeferred.await()
update(
R.string.continue_watching,
HomeRowLoadingState.Success(
context.getString(R.string.continue_watching),
resumeItems,
),
)
val nextUpItems = nextUpItemsDeferred.await()
update(
R.string.next_up,
HomeRowLoadingState.Success(
context.getString(R.string.next_up),
nextUpItems,
),
)
if (combineNextUp) {
val combined =
buildCombinedNextUp(
viewModelScope,
datePlayedService,
resumeItems,
nextUpItems,
)
update(
R.string.continue_watching,
HomeRowLoadingState.Success(
context.getString(R.string.continue_watching),
combined,
),
)
update(
R.string.next_up,
HomeRowLoadingState.Success(
context.getString(R.string.next_up),
listOf(),
),
)
} else {
update(
R.string.continue_watching,
HomeRowLoadingState.Success(
context.getString(R.string.continue_watching),
resumeItems,
),
)
update(
R.string.next_up,
HomeRowLoadingState.Success(
context.getString(R.string.next_up),
nextUpItems,
),
)
}
if (resumeItems.isNotEmpty() || nextUpItems.isNotEmpty()) {
loading.setValueOnMain(LoadingState.Success)

View file

@ -24,6 +24,7 @@ import com.github.damontecres.wholphin.util.LoadingState
import com.github.damontecres.wholphin.util.supportItemKinds
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
@ -109,7 +110,13 @@ class HomeViewModel
val watching =
buildList {
if (prefs.combineContinueNext) {
val items = buildCombined(resume, nextUp)
val items =
buildCombinedNextUp(
viewModelScope,
datePlayedService,
resume,
nextUp,
)
add(
HomeRowLoadingState.Success(
title = context.getString(R.string.continue_watching),
@ -267,39 +274,6 @@ class HomeViewModel
latestRows.setValueOnMain(rows)
}
private suspend fun buildCombined(
resume: List<BaseItem>,
nextUp: List<BaseItem>,
): List<BaseItem> =
withContext(Dispatchers.IO) {
val start = System.currentTimeMillis()
val semaphore = Semaphore(3)
val deferred =
nextUp
.filter { it.data.seriesId != null }
.map { item ->
viewModelScope.async(Dispatchers.IO) {
try {
semaphore.withPermit {
datePlayedService.getLastPlayed(item)
}
} catch (ex: Exception) {
Timber.e(ex, "Error fetching %s", item.id)
null
}
}
}
val nextUpLastPlayed = deferred.awaitAll()
val timestamps = mutableMapOf<UUID, LocalDateTime?>()
nextUp.map { it.id }.zip(nextUpLastPlayed).toMap(timestamps)
resume.forEach { timestamps[it.id] = it.data.userData?.lastPlayedDate }
val result = (resume + nextUp).sortedByDescending { timestamps[it.id] }
val duration = (System.currentTimeMillis() - start).milliseconds
Timber.v("buildCombined took %s", duration)
return@withContext result
}
fun setWatched(
itemId: UUID,
played: Boolean,
@ -340,3 +314,38 @@ data class LatestData(
val title: String,
val request: GetLatestMediaRequest,
)
suspend fun buildCombinedNextUp(
scope: CoroutineScope,
datePlayedService: DatePlayedService,
resume: List<BaseItem>,
nextUp: List<BaseItem>,
): List<BaseItem> =
withContext(Dispatchers.IO) {
val start = System.currentTimeMillis()
val semaphore = Semaphore(3)
val deferred =
nextUp
.filter { it.data.seriesId != null }
.map { item ->
scope.async(Dispatchers.IO) {
try {
semaphore.withPermit {
datePlayedService.getLastPlayed(item)
}
} catch (ex: Exception) {
Timber.e(ex, "Error fetching %s", item.id)
null
}
}
}
val nextUpLastPlayed = deferred.awaitAll()
val timestamps = mutableMapOf<UUID, LocalDateTime?>()
nextUp.map { it.id }.zip(nextUpLastPlayed).toMap(timestamps)
resume.forEach { timestamps[it.id] = it.data.userData?.lastPlayedDate }
val result = (resume + nextUp).sortedByDescending { timestamps[it.id] }
val duration = (System.currentTimeMillis() - start).milliseconds
Timber.v("buildCombined took %s", duration)
return@withContext result
}