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
|
|
@ -0,0 +1,124 @@
|
|||
package com.github.damontecres.wholphin.services
|
||||
|
||||
import android.content.Context
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.preferences.AppPreference
|
||||
import com.github.damontecres.wholphin.util.GetEpisodesRequestHandler
|
||||
import com.google.common.cache.CacheBuilder
|
||||
import com.google.common.cache.CacheLoader
|
||||
import dagger.hilt.android.qualifiers.ActivityContext
|
||||
import dagger.hilt.android.scopes.ActivityScoped
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.exception.InvalidStatusException
|
||||
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.request.GetEpisodesRequest
|
||||
import timber.log.Timber
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class DatePlayedService
|
||||
@Inject
|
||||
constructor(
|
||||
private val api: ApiClient,
|
||||
) {
|
||||
private val datePlayedCache =
|
||||
CacheBuilder
|
||||
.newBuilder()
|
||||
.maximumSize(AppPreference.HomePageItems.max)
|
||||
.expireAfterWrite(2, TimeUnit.HOURS)
|
||||
.build<SeriesItemId, LocalDateTime?>(
|
||||
object :
|
||||
CacheLoader<SeriesItemId, LocalDateTime>() {
|
||||
override fun load(key: SeriesItemId): LocalDateTime = getLastPlayed(key)
|
||||
},
|
||||
)
|
||||
|
||||
private fun getLastPlayed(key: SeriesItemId): LocalDateTime {
|
||||
val request =
|
||||
GetEpisodesRequest(
|
||||
seriesId = key.seriesId,
|
||||
adjacentTo = key.itemId,
|
||||
limit = 1,
|
||||
)
|
||||
return try {
|
||||
val result =
|
||||
runBlocking {
|
||||
GetEpisodesRequestHandler
|
||||
.execute(
|
||||
api,
|
||||
request,
|
||||
).content.items
|
||||
}
|
||||
result.firstOrNull()?.userData?.lastPlayedDate ?: LocalDateTime.MIN
|
||||
} catch (ex: InvalidStatusException) {
|
||||
Timber.w("Error fetching %s: %s", key, ex.localizedMessage)
|
||||
LocalDateTime.MIN
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getLastPlayed(item: BaseItem): LocalDateTime? {
|
||||
val seriesId = item.data.seriesId
|
||||
return if (seriesId != null) {
|
||||
datePlayedCache.get(SeriesItemId(seriesId, item.id))
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun invalidate(item: BaseItem) {
|
||||
item.data.seriesId?.let { seriesId ->
|
||||
Timber.d("Invalidating %s", seriesId)
|
||||
datePlayedCache.asMap().keys.removeIf { it.seriesId == seriesId }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun invalidate(itemId: UUID) {
|
||||
val seriesId =
|
||||
api.userLibraryApi.getItem(itemId = itemId).content.let {
|
||||
if (it.type == BaseItemKind.SERIES) {
|
||||
itemId
|
||||
} else {
|
||||
it.seriesId
|
||||
}
|
||||
}
|
||||
if (seriesId != null) {
|
||||
Timber.d("Invalidating %s", seriesId)
|
||||
datePlayedCache.asMap().keys.removeIf { it.seriesId == seriesId }
|
||||
}
|
||||
}
|
||||
|
||||
fun invalidateAll() {
|
||||
Timber.d("invalidateAll")
|
||||
datePlayedCache.invalidateAll()
|
||||
}
|
||||
}
|
||||
|
||||
@ActivityScoped
|
||||
class DatePlayedInvalidationService
|
||||
@Inject
|
||||
constructor(
|
||||
@param:ActivityContext private val context: Context,
|
||||
private val serverRepository: ServerRepository,
|
||||
private val datePlayedService: DatePlayedService,
|
||||
) {
|
||||
private val activity = (context as AppCompatActivity)
|
||||
|
||||
init {
|
||||
serverRepository.current.observe(activity) {
|
||||
datePlayedService.invalidateAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class SeriesItemId(
|
||||
val seriesId: UUID,
|
||||
val itemId: UUID,
|
||||
)
|
||||
|
|
@ -5,39 +5,35 @@ import org.jellyfin.sdk.api.client.extensions.playStateApi
|
|||
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
||||
import org.jellyfin.sdk.model.api.UserItemDataDto
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
interface FavoriteWatchManager {
|
||||
suspend fun setWatched(
|
||||
itemId: UUID,
|
||||
played: Boolean,
|
||||
): UserItemDataDto
|
||||
|
||||
suspend fun setFavorite(
|
||||
itemId: UUID,
|
||||
favorite: Boolean,
|
||||
): UserItemDataDto
|
||||
}
|
||||
|
||||
class FavoriteWatchManagerImpl(
|
||||
private val api: ApiClient,
|
||||
) : FavoriteWatchManager {
|
||||
override suspend fun setWatched(
|
||||
itemId: UUID,
|
||||
played: Boolean,
|
||||
): UserItemDataDto =
|
||||
if (played) {
|
||||
api.playStateApi.markPlayedItem(itemId).content
|
||||
} else {
|
||||
api.playStateApi.markUnplayedItem(itemId).content
|
||||
@Singleton
|
||||
class FavoriteWatchManager
|
||||
@Inject
|
||||
constructor(
|
||||
private val api: ApiClient,
|
||||
private val datePlayedService: DatePlayedService,
|
||||
) {
|
||||
suspend fun setWatched(
|
||||
itemId: UUID,
|
||||
played: Boolean,
|
||||
): UserItemDataDto {
|
||||
datePlayedService.invalidate(itemId)
|
||||
return if (played) {
|
||||
api.playStateApi.markPlayedItem(itemId).content
|
||||
} else {
|
||||
api.playStateApi.markUnplayedItem(itemId).content
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setFavorite(
|
||||
itemId: UUID,
|
||||
favorite: Boolean,
|
||||
): UserItemDataDto =
|
||||
if (favorite) {
|
||||
api.userLibraryApi.markFavoriteItem(itemId).content
|
||||
} else {
|
||||
api.userLibraryApi.unmarkFavoriteItem(itemId).content
|
||||
}
|
||||
}
|
||||
suspend fun setFavorite(
|
||||
itemId: UUID,
|
||||
favorite: Boolean,
|
||||
): UserItemDataDto =
|
||||
if (favorite) {
|
||||
api.userLibraryApi.markFavoriteItem(itemId).content
|
||||
} else {
|
||||
api.userLibraryApi.unmarkFavoriteItem(itemId).content
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@ import com.github.damontecres.wholphin.data.ServerRepository
|
|||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.preferences.updateInterfacePreferences
|
||||
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
||||
import com.github.damontecres.wholphin.services.FavoriteWatchManagerImpl
|
||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||
import com.github.damontecres.wholphin.util.RememberTabManager
|
||||
import dagger.Module
|
||||
|
|
@ -25,7 +23,6 @@ import kotlinx.coroutines.SupervisorJob
|
|||
import kotlinx.coroutines.launch
|
||||
import okhttp3.OkHttpClient
|
||||
import org.jellyfin.sdk.Jellyfin
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.util.AuthorizationHeaderBuilder
|
||||
import org.jellyfin.sdk.api.okhttp.OkHttpFactory
|
||||
import org.jellyfin.sdk.createJellyfin
|
||||
|
|
@ -185,8 +182,4 @@ object AppModule {
|
|||
@Singleton
|
||||
@IoCoroutineScope
|
||||
fun ioCoroutineScope(): CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun favoriteWatchManager(api: ApiClient): FavoriteWatchManager = FavoriteWatchManagerImpl(api)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue