Fix issue with scrolling to the right episode if there are specials in the season (#1827)

## Description
Fixes an edge case where clicking on an episode to go to Series Overview
wouldn't scroll to the episode if there is a special (season 0) episodes
in the episode list.

Dev note: to simplify things, if the entire dataset is cached, then just
search with `indexOf`. Often the data is cached because 20 seasons or
100 episodes are fetched initially which covers many shows.

### Related issues
Follow up to #1708 

### Testing
Emulator and unit tests

## Screenshots
N/A

## AI or LLM usage
None
This commit is contained in:
Damontecres 2026-08-12 16:25:30 -04:00 • committed by GitHub
parent c6d81dada8
commit 548ec09af3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 123 additions and 37 deletions

View file

@ -177,10 +177,11 @@ class SeriesViewModel
viewModelScope.launchIO {
val index =
(seasons as? ApiRequestPager<*>)?.let {
findIndexByNumberOrId(
findIndexByNumberOrIdFast(
seasonEpisodeIds.seasonNumber,
seasonEpisodeIds.seasonId,
it,
null,
)
} ?: 0
Timber.v("Got initial season index: $index")
@ -313,6 +314,7 @@ class SeriesViewModel
includeItemTypes = listOf(BaseItemKind.SEASON),
sortBy = listOf(ItemSortBy.INDEX_NUMBER),
sortOrder = listOf(SortOrder.ASCENDING),
enableUserData = seriesPageType == SeriesPageType.DETAILS,
fields =
if (seriesPageType == SeriesPageType.DETAILS) {
listOf(
@ -331,16 +333,8 @@ class SeriesViewModel
request,
GetItemsRequestHandler,
viewModelScope,
pageSize = 10,
pageSize = 20,
).init(seasonNum ?: 0)
// val seasons =
// GetItemsRequestHandler.execute(api, request).content.items.map {
// BaseItem.from(
// it,
// api,
// )
// }
// Timber.v("Loaded ${seasons.size} seasons for series ${series.id}")
pager
}
@ -362,6 +356,7 @@ class SeriesViewModel
ItemFields.CUSTOM_RATING,
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
ItemFields.CAN_DELETE,
ItemFields.PARENT_ID,
),
)
Timber.v(
@ -373,10 +368,10 @@ class SeriesViewModel
pager.init(episodeNumber ?: 0)
val initialIndex =
if (episodeId != null || episodeNumber != null) {
findIndexByNumberOrId(episodeNumber, episodeId, pager)
findIndexByNumberOrIdFast(episodeNumber, episodeId, pager, seasonId)
.coerceAtLeast(0)
} else {
// Force the first page to to be fetched
// Force the first page to be fetched
if (pager.isNotEmpty()) {
pager.getBlocking(0)
}
@ -728,6 +723,32 @@ private fun checkNumberOrId(
equalsNotNull(indexNumber, targetNum)
}
/**
* Find the index in the [list] where the item's `indexNumber`==[targetNum] or `id`==[targetId]
*
* If the list is smaller than its page size, then the entire dataset is cached and
* [BlockingList.indexOfBlocking] is used, otherwise this function calls [findIndexByNumberOrId]
*/
suspend fun findIndexByNumberOrIdFast(
targetNum: Int?,
targetId: UUID?,
list: ApiRequestPager<*>,
parentId: UUID?,
): Int =
if (list.size <= list.pageSize) {
Timber.v("Using findIndexByNumberOrIdFast indexOfBlocking method")
list.indexOfBlocking {
checkNumberOrId(targetNum, targetId, it?.indexNumber, it?.id) &&
if (parentId != null) {
it?.data?.parentId == parentId
} else {
true
}
}
} else {
findIndexByNumberOrId(targetNum, targetId, list as BlockingList<BaseItem?>, parentId)
}
/**
* Find the index in the [list] where the item's `indexNumber`==[targetNum] or `id`==[targetId]
*
@ -743,7 +764,9 @@ suspend fun findIndexByNumberOrId(
targetNum: Int?,
targetId: UUID?,
list: BlockingList<BaseItem?>,
parentId: UUID? = null,
): Int {
Timber.v("Using findIndexByNumberOrId")
// Adjust for 1-based numbers
val listIndex = targetNum?.minus(1)?.coerceAtLeast(0)
val index =
@ -754,36 +777,55 @@ suspend fun findIndexByNumberOrId(
checkNumberOrId(targetNum, targetId, it?.indexNumber, it?.id)
}.coerceAtLeast(0)
} else if (listIndex != null && listIndex in list.indices) {
// Start searching from the target number and choose direction from there
val num = list.getBlocking(listIndex)?.indexNumber
if (num.lt(targetNum)) {
for (i in listIndex + 1 until list.size) {
val item = list.getBlocking(i)
if (checkNumberOrId(targetNum, targetId, item?.indexNumber, item?.id)) {
return i
}
}
return 0
} else if (num.gt(targetNum)) {
for (i in listIndex - 1 downTo 0) {
val item = list.getBlocking(i)
if (checkNumberOrId(targetNum, targetId, item?.indexNumber, item?.id)) {
return i
}
}
return 0
} else {
list
.indexOfBlocking {
checkNumberOrId(targetNum, targetId, it?.indexNumber, it?.id)
}.coerceAtLeast(0)
}
searchList(listIndex, targetNum, targetId, list, parentId)
} else {
0
}
return index
}
private suspend fun searchList(
listIndex: Int,
targetNum: Int,
targetId: UUID?,
list: BlockingList<BaseItem?>,
parentId: UUID?,
): Int {
val item = list.getBlocking(listIndex)
if (parentId != null && item?.data?.parentId != parentId) {
return if (listIndex - 1 in list.indices) {
searchList(listIndex - 1, targetNum, targetId, list, parentId)
} else if (listIndex + 1 in list.indices) {
searchList(listIndex + 1, targetNum, targetId, list, parentId)
} else {
0
}
}
val num = item?.indexNumber
if (num.lt(targetNum)) {
for (i in listIndex + 1 until list.size) {
val item = list.getBlocking(i)
if (checkNumberOrId(targetNum, targetId, item?.indexNumber, item?.id)) {
return i
}
}
return 0
} else if (num.gt(targetNum)) {
for (i in listIndex - 1 downTo 0) {
val item = list.getBlocking(i)
if (checkNumberOrId(targetNum, targetId, item?.indexNumber, item?.id)) {
return i
}
}
return 0
} else {
return list
.indexOfBlocking {
checkNumberOrId(targetNum, targetId, it?.indexNumber, it?.id)
}.coerceAtLeast(0)
}
}
data class SeriesState(
val series: DataLoadingState<BaseItem> = DataLoadingState.Pending,
val seasons: List<BaseItem?> = emptyList(),

View file

@ -24,7 +24,7 @@ import java.util.function.Predicate
*/
abstract class RequestPager<T>(
protected val scope: CoroutineScope,
protected val pageSize: Int = DEFAULT_PAGE_SIZE,
val pageSize: Int = DEFAULT_PAGE_SIZE,
cacheSize: Long = 8,
) : AbstractList<T?>(),
BlockingList<T?> {

View file

@ -11,6 +11,9 @@ import org.junit.Assert
import org.junit.Test
class TestFindIndexByNumberOrId {
val season0Id = UUID.randomUUID()
val season2Id = UUID.randomUUID()
fun create(
indexNumber: Int,
parentIndexNumber: Int,
@ -20,6 +23,12 @@ class TestFindIndexByNumberOrId {
type = BaseItemKind.EPISODE,
indexNumber = indexNumber,
parentIndexNumber = parentIndexNumber,
parentId =
when (parentIndexNumber) {
0 -> season0Id
2 -> season2Id
else -> null
},
),
)
@ -151,4 +160,39 @@ class TestFindIndexByNumberOrId {
Assert.assertEquals(2, index)
}
}
@Test
fun `Test with special in middle`() =
runTest {
val episodes =
BlockingList.of<BaseItem?>(
listOf(
epS02E01,
epS02E02,
epS02E03,
create(6, 0),
epS02E04,
epS02E05,
epS02E06,
),
)
findIndexByNumberOrId(
targetNum = 3,
targetId = epS02E03.id,
list = episodes,
parentId = season2Id,
).let { index ->
Assert.assertEquals(2, index)
}
findIndexByNumberOrId(
targetNum = 4,
targetId = epS02E04.id,
list = episodes,
parentId = season2Id,
).let { index ->
Assert.assertEquals(4, index)
}
}
}