diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/main/BookDtoDao.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/main/BookDtoDao.kt index 7231899da..1923470c0 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/main/BookDtoDao.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/main/BookDtoDao.kt @@ -323,11 +323,18 @@ class BookDtoDao( val seriesId = record.get(0, String::class.java) val numberSort = record.get(1, Float::class.java) + val orderBy = + if (next) { + listOf(d.NUMBER_SORT.asc(), b.ID.asc()) + } else { + listOf(d.NUMBER_SORT.desc(), b.ID.desc()) + } + return dslRO .selectBase(userId) .where(b.SERIES_ID.eq(seriesId)) - .orderBy(d.NUMBER_SORT.let { if (next) it.asc() else it.desc() }) - .seek(numberSort) + .orderBy(orderBy) + .seek(numberSort, bookId) .limit(1) .fetchAndMap(dslRO) .firstOrNull() diff --git a/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/BookControllerTest.kt b/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/BookControllerTest.kt index 03fe5b538..7ba7243ea 100644 --- a/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/BookControllerTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/interfaces/api/rest/BookControllerTest.kt @@ -727,6 +727,50 @@ class BookControllerTest( .get("/api/v1/books/${book3.id}/next") .andExpect { status { isNotFound() } } } + + @Test + @WithMockCustomUser + fun `given series with duplicate numberSort when getting siblings then order is stable`() { + val book1 = makeBook("1", libraryId = library.id, id = "1") + val book2 = makeBook("2", libraryId = library.id, id = "2") + val book3 = makeBook("3", libraryId = library.id, id = "3") + makeSeries(name = "series", libraryId = library.id).let { series -> + seriesLifecycle.createSeries(series).also { created -> + val books = listOf(book1, book2, book3) + seriesLifecycle.addBooks(created, books) + seriesLifecycle.sortBooks(created) + } + } + + bookMetadataRepository.findById(book1.id).let { metadata -> + bookMetadataRepository.update(metadata.copy(numberSort = 1F)) + } + bookMetadataRepository.findById(book2.id).let { metadata -> + bookMetadataRepository.update(metadata.copy(numberSort = 1F)) + } + bookMetadataRepository.findById(book3.id).let { metadata -> + bookMetadataRepository.update(metadata.copy(numberSort = 2F)) + } + + mockMvc + .get("/api/v1/books/${book1.id}/next") + .andExpect { + status { isOk() } + jsonPath("$.name") { value("2") } + } + mockMvc + .get("/api/v1/books/${book2.id}/previous") + .andExpect { + status { isOk() } + jsonPath("$.name") { value("1") } + } + mockMvc + .get("/api/v1/books/${book2.id}/next") + .andExpect { + status { isOk() } + jsonPath("$.name") { value("3") } + } + } } @Nested