Fixes to season/episode search with duplicates (#1946)
## Description Fixes cases where the server has duplicate season IDs causing the season index search to fail and default to the first season in the list. ### Related issues Fixes #1837 ### Testing Emulator & unit tests ## Screenshots N/A ## AI or LLM usage None
This commit is contained in:
parent
1778bdb34c
commit
4a567cbcf4
3 changed files with 108 additions and 35 deletions
|
|
@ -92,7 +92,7 @@ fun SeriesOverview(
|
|||
LaunchedEffect(Unit) {
|
||||
if (state.seasons.isNotEmpty()) {
|
||||
state.seasons.getOrNull(position.seasonTabIndex)?.let {
|
||||
viewModel.loadEpisodes(it.id)
|
||||
viewModel.loadEpisodes(it.id, it.indexNumber)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -210,7 +210,7 @@ fun SeriesOverview(
|
|||
onChangeSeason = { index ->
|
||||
if (index != position.seasonTabIndex) {
|
||||
state.seasons.getOrNull(index)?.let { season ->
|
||||
viewModel.loadEpisodes(season.id)
|
||||
viewModel.loadEpisodes(season.id, season.indexNumber)
|
||||
viewModel.position.update {
|
||||
SeriesOverviewPosition(index, 0)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -143,6 +143,7 @@ class SeriesViewModel
|
|||
if (seasonEpisodeIds != null) {
|
||||
loadEpisodesInternal(
|
||||
seasonEpisodeIds.seasonId,
|
||||
seasonEpisodeIds.seasonNumber,
|
||||
seasonEpisodeIds.episodeId,
|
||||
seasonEpisodeIds.episodeNumber,
|
||||
)
|
||||
|
|
@ -150,6 +151,7 @@ class SeriesViewModel
|
|||
seasonsDeferred.await().firstOrNull()?.let {
|
||||
loadEpisodesInternal(
|
||||
it.id,
|
||||
it.indexNumber,
|
||||
null,
|
||||
null,
|
||||
)
|
||||
|
|
@ -178,10 +180,11 @@ class SeriesViewModel
|
|||
val index =
|
||||
(seasons as? ApiRequestPager<*>)?.let {
|
||||
findIndexByNumberOrIdFast(
|
||||
seasonEpisodeIds.seasonNumber,
|
||||
seasonEpisodeIds.seasonId,
|
||||
it,
|
||||
null,
|
||||
targetNum = seasonEpisodeIds.seasonNumber,
|
||||
targetId = seasonEpisodeIds.seasonId,
|
||||
list = it,
|
||||
parentId = null,
|
||||
parentIndex = null,
|
||||
)
|
||||
} ?: 0
|
||||
Timber.v("Got initial season index: $index")
|
||||
|
|
@ -340,6 +343,7 @@ class SeriesViewModel
|
|||
|
||||
private suspend fun loadEpisodesInternal(
|
||||
seasonId: UUID,
|
||||
seasonNum: Int?,
|
||||
episodeId: UUID?,
|
||||
episodeNumber: Int?,
|
||||
): EpisodeList {
|
||||
|
|
@ -368,8 +372,13 @@ class SeriesViewModel
|
|||
pager.init(episodeNumber ?: 0)
|
||||
val initialIndex =
|
||||
if (episodeId != null || episodeNumber != null) {
|
||||
findIndexByNumberOrIdFast(episodeNumber, episodeId, pager, seasonId)
|
||||
.coerceAtLeast(0)
|
||||
findIndexByNumberOrIdFast(
|
||||
targetNum = episodeNumber,
|
||||
targetId = episodeId,
|
||||
list = pager,
|
||||
parentId = seasonId,
|
||||
parentIndex = seasonNum,
|
||||
).coerceAtLeast(0)
|
||||
} else {
|
||||
// Force the first page to be fetched
|
||||
if (pager.isNotEmpty()) {
|
||||
|
|
@ -381,7 +390,10 @@ class SeriesViewModel
|
|||
return EpisodeList.Success(seasonId, pager, initialIndex)
|
||||
}
|
||||
|
||||
fun loadEpisodes(seasonId: UUID) {
|
||||
fun loadEpisodes(
|
||||
seasonId: UUID,
|
||||
seasonNum: Int?,
|
||||
) {
|
||||
val currentEpisodes = (state.value.episodes as? EpisodeList.Success)
|
||||
if (currentEpisodes == null || currentEpisodes.seasonId != seasonId) {
|
||||
_state.update {
|
||||
|
|
@ -395,7 +407,7 @@ class SeriesViewModel
|
|||
viewModelScope.launchIO(ExceptionHandler(true)) {
|
||||
val episodes =
|
||||
try {
|
||||
loadEpisodesInternal(seasonId, null, null)
|
||||
loadEpisodesInternal(seasonId, seasonNum, null, null)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Error loading episodes for $seriesId for season $seasonId")
|
||||
EpisodeList.Error(e)
|
||||
|
|
@ -716,12 +728,18 @@ private fun checkNumberOrId(
|
|||
targetId: UUID?,
|
||||
indexNumber: Int?,
|
||||
id: UUID?,
|
||||
): Boolean =
|
||||
if (targetId != null) {
|
||||
equalsNotNull(targetId, id)
|
||||
} else {
|
||||
equalsNotNull(indexNumber, targetNum)
|
||||
}
|
||||
): Boolean = equalsNotNull(targetId, id) || equalsNotNull(indexNumber, targetNum)
|
||||
|
||||
private fun checkParent(
|
||||
parentId: UUID?,
|
||||
parentIndex: Int?,
|
||||
item: BaseItem?,
|
||||
) = if (parentId != null || parentIndex != null) {
|
||||
equalsNotNull(parentId, item?.data?.parentId) ||
|
||||
equalsNotNull(parentIndex, item?.data?.parentIndexNumber)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the index in the [list] where the item's `indexNumber`==[targetNum] or `id`==[targetId]
|
||||
|
|
@ -734,19 +752,22 @@ suspend fun findIndexByNumberOrIdFast(
|
|||
targetId: UUID?,
|
||||
list: ApiRequestPager<*>,
|
||||
parentId: UUID?,
|
||||
parentIndex: Int?,
|
||||
): 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
|
||||
}
|
||||
checkParent(parentId, parentIndex, it)
|
||||
}
|
||||
} else {
|
||||
findIndexByNumberOrId(targetNum, targetId, list as BlockingList<BaseItem?>, parentId)
|
||||
findIndexByNumberOrId(
|
||||
targetNum,
|
||||
targetId,
|
||||
list as BlockingList<BaseItem?>,
|
||||
parentId,
|
||||
parentIndex,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -765,6 +786,7 @@ suspend fun findIndexByNumberOrId(
|
|||
targetId: UUID?,
|
||||
list: BlockingList<BaseItem?>,
|
||||
parentId: UUID? = null,
|
||||
parentIndex: Int? = null,
|
||||
): Int {
|
||||
Timber.v("Using findIndexByNumberOrId")
|
||||
// Adjust for 1-based numbers
|
||||
|
|
@ -777,7 +799,7 @@ suspend fun findIndexByNumberOrId(
|
|||
checkNumberOrId(targetNum, targetId, it?.indexNumber, it?.id)
|
||||
}.coerceAtLeast(0)
|
||||
} else if (listIndex != null && listIndex in list.indices) {
|
||||
searchList(listIndex, targetNum, targetId, list, parentId)
|
||||
searchList(listIndex, targetNum, targetId, list, parentId, parentIndex)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
|
@ -790,22 +812,42 @@ private suspend fun searchList(
|
|||
targetId: UUID?,
|
||||
list: BlockingList<BaseItem?>,
|
||||
parentId: UUID?,
|
||||
parentIndex: Int?,
|
||||
): 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
|
||||
var index = listIndex
|
||||
var item = list.getBlocking(index)
|
||||
if ((parentId != null || parentIndex != null) && !checkParent(parentId, parentIndex, item)) {
|
||||
var leftIndex = listIndex - 1
|
||||
var rightIndex = listIndex + 1
|
||||
// Search before & after to find an episode with the right parent
|
||||
while (leftIndex in list.indices || rightIndex in list.indices) {
|
||||
if (leftIndex in list.indices) {
|
||||
val left = list.getBlocking(leftIndex)
|
||||
if (checkParent(parentId, parentIndex, left)) {
|
||||
index = leftIndex
|
||||
break
|
||||
}
|
||||
}
|
||||
if (rightIndex in list.indices) {
|
||||
val right = list.getBlocking(rightIndex)
|
||||
if (checkParent(parentId, parentIndex, right)) {
|
||||
index = rightIndex
|
||||
break
|
||||
}
|
||||
}
|
||||
leftIndex--
|
||||
rightIndex++
|
||||
}
|
||||
// Search for the right episode starting from this new one
|
||||
item = list.getBlocking(index)
|
||||
}
|
||||
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)) {
|
||||
if (checkNumberOrId(targetNum, targetId, item?.indexNumber, item?.id) &&
|
||||
checkParent(parentId, parentIndex, item)
|
||||
) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
|
@ -813,15 +855,22 @@ private suspend fun searchList(
|
|||
} 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)) {
|
||||
if (checkNumberOrId(targetNum, targetId, item?.indexNumber, item?.id) &&
|
||||
checkParent(parentId, parentIndex, item)
|
||||
) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return 0
|
||||
} else if (checkNumberOrId(targetNum, targetId, item?.indexNumber, item?.id) &&
|
||||
checkParent(parentId, parentIndex, item)
|
||||
) {
|
||||
return index
|
||||
} else {
|
||||
return list
|
||||
.indexOfBlocking {
|
||||
checkNumberOrId(targetNum, targetId, it?.indexNumber, it?.id)
|
||||
checkNumberOrId(targetNum, targetId, it?.indexNumber, it?.id) &&
|
||||
checkParent(parentId, parentIndex, item)
|
||||
}.coerceAtLeast(0)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -156,7 +156,12 @@ class TestFindIndexByNumberOrId {
|
|||
fun `Test with special matching number`() =
|
||||
runTest {
|
||||
val list = listOf(epS02E01, epS00E02, epS02E02)
|
||||
findIndexByNumberOrId(targetNum = 2, targetId = epS02E02.id, list = BlockingList.of(list)).let { index ->
|
||||
findIndexByNumberOrId(
|
||||
targetNum = 2,
|
||||
targetId = epS02E02.id,
|
||||
list = BlockingList.of(list),
|
||||
parentIndex = 2,
|
||||
).let { index ->
|
||||
Assert.assertEquals(2, index)
|
||||
}
|
||||
}
|
||||
|
|
@ -195,4 +200,23 @@ class TestFindIndexByNumberOrId {
|
|||
Assert.assertEquals(4, index)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test with non-matching parent ID`() =
|
||||
runTest {
|
||||
val altSeason2Id = UUID.randomUUID()
|
||||
Assert.assertNotEquals(altSeason2Id, season2Id)
|
||||
Assert.assertNotEquals(altSeason2Id, epS02E02.data.parentId)
|
||||
|
||||
val list = listOf(epS02E01, epS02E02)
|
||||
findIndexByNumberOrId(
|
||||
targetNum = 2,
|
||||
targetId = epS02E02.id,
|
||||
list = BlockingList.of(list),
|
||||
parentId = altSeason2Id,
|
||||
parentIndex = 2,
|
||||
).let { index ->
|
||||
Assert.assertEquals(1, index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue