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

View file

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

View file

@ -11,6 +11,9 @@ import org.junit.Assert
import org.junit.Test import org.junit.Test
class TestFindIndexByNumberOrId { class TestFindIndexByNumberOrId {
val season0Id = UUID.randomUUID()
val season2Id = UUID.randomUUID()
fun create( fun create(
indexNumber: Int, indexNumber: Int,
parentIndexNumber: Int, parentIndexNumber: Int,
@ -20,6 +23,12 @@ class TestFindIndexByNumberOrId {
type = BaseItemKind.EPISODE, type = BaseItemKind.EPISODE,
indexNumber = indexNumber, indexNumber = indexNumber,
parentIndexNumber = parentIndexNumber, parentIndexNumber = parentIndexNumber,
parentId =
when (parentIndexNumber) {
0 -> season0Id
2 -> season2Id
else -> null
},
), ),
) )
@ -151,4 +160,39 @@ class TestFindIndexByNumberOrId {
Assert.assertEquals(2, index) 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)
}
}
} }