mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-09 16:11:20 +02:00
Option to combine continue watching & next up rows (#192)
A real implementation for combining the `Continue Watching` and `Next Up` rows on the home page. It requires a query for every next up item, so it could become slow especially for many items and larger servers. The app makes a best effort to cache the information. The order is determined by querying each episode in next up to associate it to its immediately previous episode's last played date. Then both continue watching & next up items are reversed sorted by the last played data. So this only makes sense if you watch series in order. Closes #19
This commit is contained in:
parent
46415fcdd0
commit
cda5b118a3
6 changed files with 232 additions and 63 deletions
|
|
@ -9,6 +9,7 @@ import com.github.damontecres.wholphin.data.NavDrawerItemRepository
|
|||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
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
|
||||
|
|
@ -23,7 +24,11 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
|||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.itemsApi
|
||||
|
|
@ -38,8 +43,10 @@ import org.jellyfin.sdk.model.api.request.GetLatestMediaRequest
|
|||
import org.jellyfin.sdk.model.api.request.GetNextUpRequest
|
||||
import org.jellyfin.sdk.model.api.request.GetResumeItemsRequest
|
||||
import timber.log.Timber
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@HiltViewModel
|
||||
class HomeViewModel
|
||||
|
|
@ -51,6 +58,7 @@ class HomeViewModel
|
|||
val serverRepository: ServerRepository,
|
||||
val navDrawerItemRepository: NavDrawerItemRepository,
|
||||
private val favoriteWatchManager: FavoriteWatchManager,
|
||||
private val datePlayedService: DatePlayedService,
|
||||
) : ViewModel() {
|
||||
val loadingState = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||
val refreshState = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||
|
|
@ -59,6 +67,10 @@ class HomeViewModel
|
|||
|
||||
private lateinit var preferences: UserPreferences
|
||||
|
||||
init {
|
||||
datePlayedService.invalidateAll()
|
||||
}
|
||||
|
||||
fun init(preferences: UserPreferences): Job {
|
||||
val reload = loadingState.value != LoadingState.Success
|
||||
if (reload) {
|
||||
|
|
@ -84,31 +96,41 @@ class HomeViewModel
|
|||
.filter { it is ServerNavDrawerItem }
|
||||
.map { (it as ServerNavDrawerItem).itemId }
|
||||
// TODO data is fetched all together which may be slow for large servers
|
||||
val resume = getResume(userDto.id, limit, !prefs.combineContinueNext)
|
||||
val resume = getResume(userDto.id, limit, true)
|
||||
val nextUp =
|
||||
getNextUp(
|
||||
userDto.id,
|
||||
limit,
|
||||
prefs.enableRewatchingNextUp,
|
||||
prefs.combineContinueNext,
|
||||
false,
|
||||
)
|
||||
val watching =
|
||||
buildList {
|
||||
if (resume.isNotEmpty()) {
|
||||
if (prefs.combineContinueNext) {
|
||||
val items = buildCombined(resume, nextUp)
|
||||
add(
|
||||
HomeRowLoadingState.Success(
|
||||
title = context.getString(R.string.continue_watching),
|
||||
items = resume,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (nextUp.isNotEmpty()) {
|
||||
add(
|
||||
HomeRowLoadingState.Success(
|
||||
title = context.getString(R.string.next_up),
|
||||
items = nextUp,
|
||||
items = items,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
if (resume.isNotEmpty()) {
|
||||
add(
|
||||
HomeRowLoadingState.Success(
|
||||
title = context.getString(R.string.continue_watching),
|
||||
items = resume,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (nextUp.isNotEmpty()) {
|
||||
add(
|
||||
HomeRowLoadingState.Success(
|
||||
title = context.getString(R.string.next_up),
|
||||
items = nextUp,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -255,6 +277,37 @@ class HomeViewModel
|
|||
latestRows.setValueOnMain(rows)
|
||||
}
|
||||
|
||||
private suspend fun buildCombined(
|
||||
resume: List<BaseItem>,
|
||||
nextUp: List<BaseItem>,
|
||||
): List<BaseItem> {
|
||||
val start = System.currentTimeMillis()
|
||||
val semaphore = Semaphore(3)
|
||||
val deferred =
|
||||
nextUp
|
||||
.filter { it.data.seriesId != null }
|
||||
.map { item ->
|
||||
semaphore.withPermit {
|
||||
viewModelScope.async {
|
||||
try {
|
||||
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 result
|
||||
}
|
||||
|
||||
fun setWatched(
|
||||
itemId: UUID,
|
||||
played: Boolean,
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import com.github.damontecres.wholphin.preferences.PlayerBackend
|
|||
import com.github.damontecres.wholphin.preferences.ShowNextUpWhen
|
||||
import com.github.damontecres.wholphin.preferences.SkipSegmentBehavior
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.services.DatePlayedService
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
import com.github.damontecres.wholphin.services.PlayerFactory
|
||||
import com.github.damontecres.wholphin.services.PlaylistCreator
|
||||
|
|
@ -118,6 +119,7 @@ class PlaybackViewModel
|
|||
val itemPlaybackRepository: ItemPlaybackRepository,
|
||||
val appPreferences: DataStore<AppPreferences>,
|
||||
private val playerFactory: PlayerFactory,
|
||||
private val datePlayedService: DatePlayedService,
|
||||
) : ViewModel(),
|
||||
Player.Listener {
|
||||
val player by lazy {
|
||||
|
|
@ -235,6 +237,7 @@ class PlaybackViewModel
|
|||
): Boolean =
|
||||
withContext(Dispatchers.IO) {
|
||||
Timber.i("Playing ${item.id}")
|
||||
datePlayedService.invalidate(item)
|
||||
autoSkippedSegments.clear()
|
||||
if (item.type !in supportItemKinds) {
|
||||
showToast(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue