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 androidx.lifecycle.viewModelScope
import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.ServerRepository 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.AppPreferences
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.BackdropService 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.FavoriteWatchManager
import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.ui.SlimItemFields import com.github.damontecres.wholphin.ui.SlimItemFields
import com.github.damontecres.wholphin.ui.data.RowColumn 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.setValueOnMain
import com.github.damontecres.wholphin.ui.toBaseItems import com.github.damontecres.wholphin.ui.toBaseItems
import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.ExceptionHandler
@ -57,6 +58,7 @@ class RecommendedTvShowViewModel
private val api: ApiClient, private val api: ApiClient,
private val serverRepository: ServerRepository, private val serverRepository: ServerRepository,
private val preferencesDataStore: DataStore<AppPreferences>, private val preferencesDataStore: DataStore<AppPreferences>,
private val datePlayedService: DatePlayedService,
@Assisted val parentId: UUID, @Assisted val parentId: UUID,
navigationManager: NavigationManager, navigationManager: NavigationManager,
favoriteWatchManager: FavoriteWatchManager, favoriteWatchManager: FavoriteWatchManager,
@ -78,17 +80,17 @@ class RecommendedTvShowViewModel
override fun init() { override fun init() {
viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) { viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) {
val itemsPerRow = val preferences =
preferencesDataStore.data preferencesDataStore.data.firstOrNull() ?: AppPreferences.getDefaultInstance()
.firstOrNull() val combineNextUp = preferences.homePagePreferences.combineContinueNext
?.homePagePreferences val itemsPerRow = preferences.homePagePreferences.maxItemsPerRow
?.maxItemsPerRow val userId = serverRepository.currentUser.value?.id
?: AppPreference.HomePageItems.defaultValue.toInt()
try { try {
val resumeItemsDeferred = val resumeItemsDeferred =
viewModelScope.async(Dispatchers.IO) { viewModelScope.async(Dispatchers.IO) {
val resumeItemsRequest = val resumeItemsRequest =
GetResumeItemsRequest( GetResumeItemsRequest(
userId = userId,
parentId = parentId, parentId = parentId,
fields = SlimItemFields, fields = SlimItemFields,
includeItemTypes = listOf(BaseItemKind.EPISODE), includeItemTypes = listOf(BaseItemKind.EPISODE),
@ -106,12 +108,14 @@ class RecommendedTvShowViewModel
viewModelScope.async(Dispatchers.IO) { viewModelScope.async(Dispatchers.IO) {
val nextUpRequest = val nextUpRequest =
GetNextUpRequest( GetNextUpRequest(
parentId = parentId, userId = userId,
fields = SlimItemFields, fields = SlimItemFields,
enableUserData = true, imageTypeLimit = 1,
startIndex = 0, parentId = parentId,
limit = itemsPerRow, limit = itemsPerRow,
enableTotalRecordCount = false, enableResumable = false,
enableUserData = true,
enableRewatching = preferences.homePagePreferences.enableRewatchingNextUp,
) )
GetNextUpRequestHandler GetNextUpRequestHandler
@ -119,21 +123,45 @@ class RecommendedTvShowViewModel
.toBaseItems(api, true) .toBaseItems(api, true)
} }
val resumeItems = resumeItemsDeferred.await() val resumeItems = resumeItemsDeferred.await()
update(
R.string.continue_watching,
HomeRowLoadingState.Success(
context.getString(R.string.continue_watching),
resumeItems,
),
)
val nextUpItems = nextUpItemsDeferred.await() val nextUpItems = nextUpItemsDeferred.await()
update( if (combineNextUp) {
R.string.next_up, val combined =
HomeRowLoadingState.Success( buildCombinedNextUp(
context.getString(R.string.next_up), viewModelScope,
nextUpItems, 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()) { if (resumeItems.isNotEmpty() || nextUpItems.isNotEmpty()) {
loading.setValueOnMain(LoadingState.Success) 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 com.github.damontecres.wholphin.util.supportItemKinds
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.async import kotlinx.coroutines.async
@ -109,7 +110,13 @@ class HomeViewModel
val watching = val watching =
buildList { buildList {
if (prefs.combineContinueNext) { if (prefs.combineContinueNext) {
val items = buildCombined(resume, nextUp) val items =
buildCombinedNextUp(
viewModelScope,
datePlayedService,
resume,
nextUp,
)
add( add(
HomeRowLoadingState.Success( HomeRowLoadingState.Success(
title = context.getString(R.string.continue_watching), title = context.getString(R.string.continue_watching),
@ -267,39 +274,6 @@ class HomeViewModel
latestRows.setValueOnMain(rows) 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( fun setWatched(
itemId: UUID, itemId: UUID,
played: Boolean, played: Boolean,
@ -340,3 +314,38 @@ data class LatestData(
val title: String, val title: String,
val request: GetLatestMediaRequest, 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
}