Add settings for max days in next up (#850)

## Description
Add a settings to set the maximum amount of time items can appear in
Next Up.

Options range from 7 days to a year, or no limit (current & default
behavior)

### Related issues
Closes #662 

### Testing
Tested on the emulator and verified URLs, added unit tests

## Screenshots
N/A, just a new slider bar setting

## AI or LLM usage
None
This commit is contained in:
Ray 2026-02-09 15:36:20 -05:00 committed by GitHub
parent 63b37ef3f4
commit e7da5ca007
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 208 additions and 7 deletions

View file

@ -186,6 +186,45 @@ sealed interface AppPreference<Pref, T> {
summarizer = { value -> value?.toString() },
)
val MaxDaysNextUpOptions = listOf(7, 14, 30, 60, 90, 180, 365)
val MaxDaysNextUp =
AppSliderPreference<AppPreferences>(
title = R.string.max_days_next_up,
defaultValue = -1,
min = 0,
// Max is "no limit" stored as -1
max = MaxDaysNextUpOptions.lastIndex + 1L,
interval = 1,
getter = {
MaxDaysNextUpOptions
.indexOf(it.homePagePreferences.maxDaysNextUp)
.takeIf { it in MaxDaysNextUpOptions.indices }
?.toLong()
?: MaxDaysNextUpOptions.size.toLong()
},
setter = { prefs, index ->
prefs.updateHomePagePreferences {
maxDaysNextUp = MaxDaysNextUpOptions.getOrNull(index.toInt()) ?: -1
}
},
summarizer = { value ->
if (value != null) {
val v = MaxDaysNextUpOptions.getOrNull(value.toInt()) ?: -1
if (v == -1) {
WholphinApplication.instance.getString(R.string.no_limit)
} else {
WholphinApplication.instance.resources.getQuantityString(
R.plurals.days,
v,
v.toString(),
)
}
} else {
null
}
},
)
val CombineContinueNext =
AppSwitchPreference<AppPreferences>(
title = R.string.combine_continue_next,
@ -956,6 +995,7 @@ val basicPreferences =
AppPreference.HomePageItems,
AppPreference.CombineContinueNext,
AppPreference.RewatchNextUp,
AppPreference.MaxDaysNextUp,
AppPreference.PlayThemeMusic,
AppPreference.RememberSelectedTab,
AppPreference.SubtitleStyle,

View file

@ -82,6 +82,7 @@ class AppPreferencesSerializer
maxItemsPerRow = AppPreference.HomePageItems.defaultValue.toInt()
enableRewatchingNextUp = AppPreference.RewatchNextUp.defaultValue
combineContinueNext = AppPreference.CombineContinueNext.defaultValue
maxDaysNextUp = AppPreference.MaxDaysNextUp.defaultValue.toInt()
}.build()
interfacePreferences =
InterfacePreferences

View file

@ -10,6 +10,7 @@ import com.github.damontecres.wholphin.preferences.AppPreference
import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.update
import com.github.damontecres.wholphin.preferences.updateAdvancedPreferences
import com.github.damontecres.wholphin.preferences.updateHomePagePreferences
import com.github.damontecres.wholphin.preferences.updateInterfacePreferences
import com.github.damontecres.wholphin.preferences.updateLiveTvPreferences
import com.github.damontecres.wholphin.preferences.updateMpvOptions
@ -237,4 +238,12 @@ suspend fun upgradeApp(
}
}
}
if (previous.isEqualOrBefore(Version.fromString("0.4.1-14-g0"))) {
appPreferences.updateData {
it.updateHomePagePreferences {
maxDaysNextUp = AppPreference.MaxDaysNextUp.defaultValue.toInt()
}
}
}
}

View file

@ -75,7 +75,10 @@ class LatestNextUpService
limit: Int,
enableRewatching: Boolean,
enableResumable: Boolean,
maxDays: Int,
): List<BaseItem> {
val nextUpDateCutoff =
maxDays.takeIf { it > 0 }?.let { LocalDateTime.now().minusDays(it.toLong()) }
val request =
GetNextUpRequest(
userId = userId,
@ -86,6 +89,7 @@ class LatestNextUpService
enableResumable = enableResumable,
enableUserData = true,
enableRewatching = enableRewatching,
nextUpDateCutoff = nextUpDateCutoff,
)
val nextUp =
api.tvShowsApi

View file

@ -81,6 +81,7 @@ class TvProviderWorker
userId,
prefs.homePagePreferences.enableRewatchingNextUp,
prefs.homePagePreferences.combineContinueNext,
prefs.homePagePreferences.maxDaysNextUp,
)
val potentialItemsToAddIds = potentialItemsToAdd.map { it.id.toString() }
@ -145,12 +146,13 @@ class TvProviderWorker
userId: UUID,
enableRewatching: Boolean,
combineContinueNext: Boolean,
maxDaysNextUp: Int,
): List<BaseItem> {
val resumeItems = latestNextUpService.getResume(userId, 10, true)
val seriesIds = resumeItems.mapNotNull { it.data.seriesId }
val nextUpItems =
latestNextUpService
.getNextUp(userId, 10, enableRewatching, false)
.getNextUp(userId, 10, enableRewatching, false, maxDaysNextUp)
.filter { it.data.seriesId != null && it.data.seriesId !in seriesIds }
return if (combineContinueNext) {
latestNextUpService.buildCombined(resumeItems, nextUpItems)

View file

@ -98,6 +98,7 @@ class HomeViewModel
limit,
prefs.enableRewatchingNextUp,
false,
prefs.maxDaysNextUp,
)
val watching =
buildList {

View file

@ -65,7 +65,7 @@ fun SliderPreference(
}
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
modifier =
Modifier

View file

@ -82,6 +82,7 @@ message HomePagePreferences{
int32 max_items_per_row = 1;
bool enable_rewatching_next_up = 2;
bool combine_continue_next = 3;
int32 max_days_next_up = 4;
}
enum ThemeSongVolume {

View file

@ -260,6 +260,11 @@
<item quantity="one">%d hour</item>
<item quantity="other">%d hours</item>
</plurals>
<plurals name="days">
<item quantity="zero">%s days</item>
<item quantity="one">%s day</item>
<item quantity="other">%s days</item>
</plurals>
<plurals name="items">
<item quantity="zero">%d items</item>
<item quantity="one">%d item</item>
@ -485,6 +490,8 @@
<string name="slideshow_duration">Slideshow duration</string>
<string name="play_videos_during_slideshow">Play videos during slideshow</string>
<string name="send_media_info_log_to_server">Send media info log to server</string>
<string name="no_limit">No limit</string>
<string name="max_days_next_up">Max days in Next Up</string>
<string-array name="theme_song_volume">
<item>Disabled</item>

View file

@ -78,11 +78,6 @@ class SuggestionsWorkerTest {
every { homePagePreferences } returns mockk { every { maxItemsPerRow } returns 25 }
}
private fun mockQueryResult(items: List<BaseItemDto> = emptyList()) =
mockk<org.jellyfin.sdk.api.client.Response<BaseItemDtoQueryResult>>(relaxed = true) {
every { content } returns mockk { every { this@mockk.items } returns items }
}
@Test
fun returns_failure_on_invalid_input() =
runTest {
@ -231,3 +226,8 @@ class SuggestionsWorkerTest {
assertEquals(ListenableWorker.Result.retry(), result)
}
}
fun mockQueryResult(items: List<BaseItemDto> = emptyList()) =
mockk<org.jellyfin.sdk.api.client.Response<BaseItemDtoQueryResult>>(relaxed = true) {
every { content } returns mockk { every { this@mockk.items } returns items }
}

View file

@ -0,0 +1,136 @@
package com.github.damontecres.wholphin.test
import android.content.Context
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.github.damontecres.wholphin.preferences.AppPreference
import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.updateHomePagePreferences
import com.github.damontecres.wholphin.services.DatePlayedService
import com.github.damontecres.wholphin.services.LatestNextUpService
import com.github.damontecres.wholphin.services.mockQueryResult
import io.mockk.CapturingSlot
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.runTest
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.tvShowsApi
import org.jellyfin.sdk.api.operations.TvShowsApi
import org.jellyfin.sdk.model.UUID
import org.jellyfin.sdk.model.api.request.GetNextUpRequest
import org.junit.Assert
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.time.LocalDate
class NextUpTest {
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
private val testDispatcher = StandardTestDispatcher()
private val mockTvShowsApi = mockk<TvShowsApi>()
private val mockApi = mockk<ApiClient>(relaxed = true)
private val mockContext = mockk<Context>()
private val mockDatePlayedService = mockk<DatePlayedService>()
private val latestNextUpService =
LatestNextUpService(mockContext, mockApi, mockDatePlayedService)
@Before
fun setUp() {
every { mockApi.tvShowsApi } returns mockTvShowsApi
}
@Test
fun `Test max 30 days in next up`() =
runTest {
val maxDays = 30
val nextUpSlot = CapturingSlot<GetNextUpRequest>()
coEvery { mockTvShowsApi.getNextUp(capture(nextUpSlot)) } returns mockQueryResult()
latestNextUpService.getNextUp(
userId = UUID.randomUUID(),
limit = 10,
enableRewatching = true,
enableResumable = true,
maxDays = maxDays,
)
Assert.assertEquals(10, nextUpSlot.captured.limit)
val expected = LocalDate.now().minusDays(maxDays.toLong())
Assert.assertEquals(expected, nextUpSlot.captured.nextUpDateCutoff?.toLocalDate())
}
@Test
fun `Test no limit in next up`() =
runTest {
val nextUpSlot = CapturingSlot<GetNextUpRequest>()
coEvery { mockTvShowsApi.getNextUp(capture(nextUpSlot)) } returns mockQueryResult()
latestNextUpService.getNextUp(
userId = UUID.randomUUID(),
limit = 10,
enableRewatching = true,
enableResumable = true,
maxDays = -1,
)
Assert.assertEquals(10, nextUpSlot.captured.limit)
Assert.assertNull(nextUpSlot.captured.nextUpDateCutoff)
}
@Test
fun `Test storing preference`() {
AppPreference.MaxDaysNextUp.setter.invoke(AppPreferences.getDefaultInstance(), 0).let {
Assert.assertEquals(7, it.homePagePreferences.maxDaysNextUp)
}
AppPreference.MaxDaysNextUp.setter
.invoke(
AppPreferences.getDefaultInstance(),
AppPreference.MaxDaysNextUpOptions.lastIndex.toLong(),
).let {
Assert.assertEquals(365, it.homePagePreferences.maxDaysNextUp)
}
AppPreference.MaxDaysNextUp.setter
.invoke(AppPreferences.getDefaultInstance(), 3)
.let {
Assert.assertEquals(60, it.homePagePreferences.maxDaysNextUp)
}
AppPreference.MaxDaysNextUp.setter
.invoke(
AppPreferences.getDefaultInstance(),
AppPreference.MaxDaysNextUpOptions.lastIndex + 1L,
).let {
Assert.assertEquals(-1, it.homePagePreferences.maxDaysNextUp)
}
}
@Test
fun `Test getting preference`() {
AppPreferences
.getDefaultInstance()
.updateHomePagePreferences { maxDaysNextUp = 7 }
.let {
val result = AppPreference.MaxDaysNextUp.getter.invoke(it)
Assert.assertEquals(0, result)
}
AppPreferences
.getDefaultInstance()
.updateHomePagePreferences { maxDaysNextUp = 60 }
.let {
val result = AppPreference.MaxDaysNextUp.getter.invoke(it)
Assert.assertEquals(3, result)
}
AppPreferences
.getDefaultInstance()
.updateHomePagePreferences { maxDaysNextUp = -1 }
.let {
val result = AppPreference.MaxDaysNextUp.getter.invoke(it)
Assert.assertEquals(AppPreference.MaxDaysNextUpOptions.lastIndex + 1L, result)
}
}
}