perf: precompute series book counts

This commit is contained in:
Gauthier Roebroeck 2021-05-27 09:50:30 +08:00
parent ae671709fe
commit c3b352aca0
13 changed files with 183 additions and 79 deletions

View file

@ -0,0 +1,29 @@
alter table SERIES
add column BOOK_COUNT int NOT NULL DEFAULT 0;
update SERIES
set BOOK_COUNT = (
SELECT COUNT(b.ID)
FROM BOOK b
WHERE b.SERIES_ID = SERIES.ID
);
CREATE TABLE READ_PROGRESS_SERIES
(
SERIES_ID varchar NOT NULL,
USER_ID varchar NOT NULL,
READ_COUNT int NOT NULL,
IN_PROGRESS_COUNT int NOT NULL,
PRIMARY KEY (SERIES_ID, USER_ID),
FOREIGN KEY (SERIES_ID) REFERENCES SERIES (ID),
FOREIGN KEY (USER_ID) REFERENCES USER (ID)
);
insert into READ_PROGRESS_SERIES
select BOOK.SERIES_ID,
READ_PROGRESS.USER_ID,
sum(case when READ_PROGRESS.COMPLETED = 1 then 1 else 0 end) as READ_COUNT,
sum(case when READ_PROGRESS.COMPLETED = 0 then 1 else 0 end) as IN_PROGRESS_COUNT
from BOOK
inner join READ_PROGRESS on (BOOK.ID = READ_PROGRESS.BOOK_ID)
group by BOOK.SERIES_ID, READ_PROGRESS.USER_ID;

View file

@ -13,6 +13,7 @@ data class Series(
val id: String = TsidCreator.getTsid256().toString(),
val libraryId: String = "",
val bookCount: Int = 0,
override val createdDate: LocalDateTime = LocalDateTime.now(),
override val lastModifiedDate: LocalDateTime = LocalDateTime.now()

View file

@ -4,6 +4,8 @@ import org.gotson.komga.domain.model.Media
interface MediaRepository {
fun findById(bookId: String): Media
fun getPagesSize(bookId: String): Int
fun getPagesSizes(bookIds: Collection<String>): Collection<Pair<String, Int>>
fun insert(media: Media)
fun insertMany(medias: Collection<Media>)

View file

@ -9,10 +9,12 @@ interface ReadProgressRepository {
fun findByBookId(bookId: String): Collection<ReadProgress>
fun save(readProgress: ReadProgress)
fun saveAll(readProgresses: Collection<ReadProgress>)
fun delete(bookId: String, userId: String)
fun deleteByUserId(userId: String)
fun deleteByBookId(bookId: String)
fun deleteByBookIds(bookIds: Collection<String>)
fun deleteByBookIdsAndUserId(bookIds: Collection<String>, userId: String)
fun deleteAll()
}

View file

@ -214,10 +214,10 @@ class BookLifecycle(
}
fun markReadProgress(book: Book, user: KomgaUser, page: Int) {
val media = mediaRepository.findById(book.id)
require(page >= 1 && page <= media.pages.size) { "Page argument ($page) must be within 1 and book page count (${media.pages.size})" }
val pages = mediaRepository.getPagesSize(book.id)
require(page in 1..pages) { "Page argument ($page) must be within 1 and book page count ($pages)" }
readProgressRepository.save(ReadProgress(book.id, user.id, page, page == media.pages.size))
readProgressRepository.save(ReadProgress(book.id, user.id, page, page == pages))
}
fun markReadProgressCompleted(bookId: String, user: KomgaUser) {

View file

@ -8,7 +8,9 @@ import org.gotson.komga.domain.model.Book
import org.gotson.komga.domain.model.BookMetadata
import org.gotson.komga.domain.model.BookMetadataAggregation
import org.gotson.komga.domain.model.BookMetadataPatchCapability
import org.gotson.komga.domain.model.KomgaUser
import org.gotson.komga.domain.model.Media
import org.gotson.komga.domain.model.ReadProgress
import org.gotson.komga.domain.model.Series
import org.gotson.komga.domain.model.SeriesMetadata
import org.gotson.komga.domain.model.ThumbnailSeries
@ -16,6 +18,7 @@ import org.gotson.komga.domain.persistence.BookMetadataAggregationRepository
import org.gotson.komga.domain.persistence.BookMetadataRepository
import org.gotson.komga.domain.persistence.BookRepository
import org.gotson.komga.domain.persistence.MediaRepository
import org.gotson.komga.domain.persistence.ReadProgressRepository
import org.gotson.komga.domain.persistence.SeriesCollectionRepository
import org.gotson.komga.domain.persistence.SeriesMetadataRepository
import org.gotson.komga.domain.persistence.SeriesRepository
@ -24,7 +27,6 @@ import org.springframework.stereotype.Service
import java.io.File
import java.nio.file.Files
import java.nio.file.Paths
import java.util.Comparator
private val logger = KotlinLogging.logger {}
private val natSortComparator: Comparator<String> = CaseInsensitiveSimpleNaturalComparator.getInstance()
@ -40,6 +42,7 @@ class SeriesLifecycle(
private val seriesMetadataRepository: SeriesMetadataRepository,
private val bookMetadataAggregationRepository: BookMetadataAggregationRepository,
private val collectionRepository: SeriesCollectionRepository,
private val readProgressRepository: ReadProgressRepository,
private val taskReceiver: TaskReceiver
) {
@ -69,13 +72,16 @@ class SeriesLifecycle(
}
bookMetadataRepository.updateMany(oldToNew.map { it.second })
// refresh metadata to reimport book number, else the series resorting would overwritei t
// refresh metadata to reimport book number, else the series resorting would overwrite it
oldToNew.forEach { (old, new) ->
if (old.number != new.number || old.numberSort != new.numberSort) {
logger.debug { "Metadata numbering has changed, refreshing metadata for book ${new.bookId} " }
taskReceiver.refreshBookMetadata(new.bookId, listOf(BookMetadataPatchCapability.NUMBER, BookMetadataPatchCapability.NUMBER_SORT))
}
}
// update book count for series
seriesRepository.update(series.copy(bookCount = books.size))
}
fun addBooks(series: Series, booksToAdd: Collection<Book>) {
@ -147,6 +153,17 @@ class SeriesLifecycle(
seriesRepository.deleteAll(seriesIds)
}
fun markReadProgressCompleted(seriesId: String, user: KomgaUser) {
val progresses = mediaRepository.getPagesSizes(bookRepository.findAllIdBySeriesId(seriesId))
.map { (bookId, pageSize) -> ReadProgress(bookId, user.id, pageSize, true) }
readProgressRepository.saveAll(progresses)
}
fun deleteReadProgress(seriesId: String, user: KomgaUser) {
readProgressRepository.deleteByBookIdsAndUserId(bookRepository.findAllIdBySeriesId(seriesId), user.id)
}
fun getThumbnail(seriesId: String): ThumbnailSeries? {
val selected = thumbnailsSeriesRepository.findSelectedBySeriesId(seriesId)
@ -205,6 +222,5 @@ class SeriesLifecycle(
}
}
}
private fun ThumbnailSeries.exists(): Boolean = Files.exists(Paths.get(url.toURI()))
}

View file

@ -26,6 +26,20 @@ class MediaDao(
override fun findById(bookId: String): Media =
find(dsl, bookId)
override fun getPagesSize(bookId: String): Int =
dsl.select(m.PAGE_COUNT)
.from(m)
.where(m.BOOK_ID.eq(bookId))
.fetch(m.PAGE_COUNT)
.first()
override fun getPagesSizes(bookIds: Collection<String>): Collection<Pair<String, Int>> =
dsl.select(m.BOOK_ID, m.PAGE_COUNT)
.from(m)
.where(m.BOOK_ID.`in`(bookIds))
.fetch()
.map { Pair(it[m.BOOK_ID], it[m.PAGE_COUNT]) }
private fun find(dsl: DSLContext, bookId: String): Media =
dsl.select(*groupFields)
.from(m)

View file

@ -5,6 +5,8 @@ import org.gotson.komga.domain.persistence.ReadProgressRepository
import org.gotson.komga.jooq.Tables
import org.gotson.komga.jooq.tables.records.ReadProgressRecord
import org.jooq.DSLContext
import org.jooq.Query
import org.jooq.impl.DSL
import org.springframework.stereotype.Component
import java.time.LocalDateTime
import java.time.ZoneId
@ -15,6 +17,8 @@ class ReadProgressDao(
) : ReadProgressRepository {
private val r = Tables.READ_PROGRESS
private val rs = Tables.READ_PROGRESS_SERIES
private val b = Tables.BOOK
override fun findAll(): Collection<ReadProgress> =
dsl.selectFrom(r)
@ -40,41 +44,96 @@ class ReadProgressDao(
.map { it.toDomain() }
override fun save(readProgress: ReadProgress) {
dsl.insertInto(r, r.BOOK_ID, r.USER_ID, r.PAGE, r.COMPLETED)
dsl.transaction { config ->
config.dsl().saveQuery(readProgress).execute()
config.dsl().aggregateSeriesProgress(listOf(readProgress.bookId), readProgress.userId)
}
}
override fun saveAll(readProgresses: Collection<ReadProgress>) {
dsl.transaction { config ->
val queries = readProgresses.map { config.dsl().saveQuery(it) }
config.dsl().batch(queries).execute()
readProgresses.groupBy { it.userId }
.forEach { (userId, readProgresses) ->
config.dsl().aggregateSeriesProgress(readProgresses.map { it.bookId }, userId)
}
}
}
private fun DSLContext.saveQuery(readProgress: ReadProgress): Query =
this.insertInto(r, r.BOOK_ID, r.USER_ID, r.PAGE, r.COMPLETED)
.values(readProgress.bookId, readProgress.userId, readProgress.page, readProgress.completed)
.onDuplicateKeyUpdate()
.set(r.PAGE, readProgress.page)
.set(r.COMPLETED, readProgress.completed)
.set(r.LAST_MODIFIED_DATE, LocalDateTime.now(ZoneId.of("Z")))
.execute()
}
override fun delete(bookId: String, userId: String) {
dsl.deleteFrom(r)
.where(r.BOOK_ID.eq(bookId).and(r.USER_ID.eq(userId)))
.execute()
dsl.transaction { config ->
config.dsl().deleteFrom(r).where(r.BOOK_ID.eq(bookId).and(r.USER_ID.eq(userId))).execute()
config.dsl().aggregateSeriesProgress(listOf(bookId), userId)
}
}
override fun deleteByUserId(userId: String) {
dsl.deleteFrom(r)
.where(r.USER_ID.eq(userId))
.execute()
dsl.transaction { config ->
config.dsl().deleteFrom(r).where(r.USER_ID.eq(userId)).execute()
config.dsl().deleteFrom(rs).where(rs.USER_ID.eq(userId)).execute()
}
}
override fun deleteByBookId(bookId: String) {
dsl.deleteFrom(r)
.where(r.BOOK_ID.eq(bookId))
.execute()
dsl.transaction { config ->
config.dsl().deleteFrom(r).where(r.BOOK_ID.eq(bookId)).execute()
config.dsl().aggregateSeriesProgress(listOf(bookId))
}
}
override fun deleteByBookIds(bookIds: Collection<String>) {
dsl.deleteFrom(r)
.where(r.BOOK_ID.`in`(bookIds))
.execute()
dsl.transaction { config ->
config.dsl().deleteFrom(r).where(r.BOOK_ID.`in`(bookIds)).execute()
config.dsl().aggregateSeriesProgress(bookIds)
}
}
override fun deleteByBookIdsAndUserId(bookIds: Collection<String>, userId: String) {
dsl.transaction { config ->
config.dsl().deleteFrom(r).where(r.BOOK_ID.`in`(bookIds)).and(r.USER_ID.eq(userId)).execute()
config.dsl().aggregateSeriesProgress(bookIds, userId)
}
}
override fun deleteAll() {
dsl.deleteFrom(r).execute()
dsl.transaction { config ->
config.dsl().deleteFrom(r).execute()
config.dsl().deleteFrom(rs).execute()
}
}
private fun DSLContext.aggregateSeriesProgress(bookIds: Collection<String>, userId: String? = null) {
val seriesIds = this.select(b.SERIES_ID)
.from(b)
.where(b.ID.`in`(bookIds))
.fetch(b.SERIES_ID)
this.deleteFrom(rs)
.where(rs.SERIES_ID.`in`(seriesIds))
.apply { userId?.let { and(rs.USER_ID.eq(it)) } }
.execute()
this.insertInto(rs)
.select(
this.select(b.SERIES_ID, r.USER_ID)
.select(DSL.sum(DSL.`when`(r.COMPLETED.isTrue, 1).otherwise(0)))
.select(DSL.sum(DSL.`when`(r.COMPLETED.isFalse, 1).otherwise(0)))
.from(b)
.innerJoin(r).on(b.ID.eq(r.BOOK_ID))
.where(b.SERIES_ID.`in`(seriesIds))
.apply { userId?.let { and(r.USER_ID.eq(it)) } }
.groupBy(b.SERIES_ID, r.USER_ID)
).execute()
}
private fun ReadProgressRecord.toDomain() =

View file

@ -93,6 +93,7 @@ class SeriesDao(
.set(s.URL, series.url.toString())
.set(s.FILE_LAST_MODIFIED, series.fileLastModified)
.set(s.LIBRARY_ID, series.libraryId)
.set(s.BOOK_COUNT, series.bookCount)
.set(s.LAST_MODIFIED_DATE, LocalDateTime.now(ZoneId.of("Z")))
.where(s.ID.eq(series.id))
.execute()
@ -143,6 +144,7 @@ class SeriesDao(
fileLastModified = fileLastModified,
id = id,
libraryId = libraryId,
bookCount = bookCount,
createdDate = createdDate.toCurrentTimeZone(),
lastModifiedDate = lastModifiedDate.toCurrentTimeZone()
)

View file

@ -19,8 +19,6 @@ import org.jooq.Record
import org.jooq.ResultQuery
import org.jooq.SelectOnConditionStep
import org.jooq.impl.DSL
import org.jooq.impl.DSL.field
import org.jooq.impl.DSL.inline
import org.jooq.impl.DSL.lower
import org.springframework.data.domain.Page
import org.springframework.data.domain.PageImpl
@ -43,9 +41,9 @@ class SeriesDtoDao(
companion object {
private val s = Tables.SERIES
private val b = Tables.BOOK
private val d = Tables.SERIES_METADATA
private val r = Tables.READ_PROGRESS
private val rs = Tables.READ_PROGRESS_SERIES
private val cs = Tables.COLLECTION_SERIES
private val g = Tables.SERIES_METADATA_GENRE
private val st = Tables.SERIES_METADATA_TAG
@ -61,24 +59,24 @@ class SeriesDtoDao(
*s.fields(),
*d.fields(),
*bma.fields(),
*rs.fields(),
)
private val sorts = mapOf(
"metadata.titleSort" to DSL.lower(d.TITLE_SORT),
"metadata.titleSort" to lower(d.TITLE_SORT),
"createdDate" to s.CREATED_DATE,
"created" to s.CREATED_DATE,
"lastModifiedDate" to s.LAST_MODIFIED_DATE,
"lastModified" to s.LAST_MODIFIED_DATE,
"collection.number" to cs.NUMBER,
"name" to s.NAME,
"booksCount" to field(BOOKS_COUNT)
"booksCount" to s.BOOK_COUNT,
)
override fun findAll(search: SeriesSearchWithReadProgress, userId: String, pageable: Pageable): Page<SeriesDto> {
val conditions = search.toCondition()
val having = search.readStatus?.toCondition() ?: DSL.trueCondition()
return findAll(conditions, having, userId, pageable, search.toJoinConditions())
return findAll(conditions, userId, pageable, search.toJoinConditions())
}
override fun findByCollectionId(
@ -88,10 +86,9 @@ class SeriesDtoDao(
pageable: Pageable
): Page<SeriesDto> {
val conditions = search.toCondition().and(cs.COLLECTION_ID.eq(collectionId))
val having = search.readStatus?.toCondition() ?: DSL.trueCondition()
val joinConditions = search.toJoinConditions().copy(selectCollectionNumber = true, collection = true)
return findAll(conditions, having, userId, pageable, joinConditions)
return findAll(conditions, userId, pageable, joinConditions)
}
override fun findRecentlyUpdated(
@ -102,9 +99,7 @@ class SeriesDtoDao(
val conditions = search.toCondition()
.and(s.CREATED_DATE.ne(s.LAST_MODIFIED_DATE))
val having = search.readStatus?.toCondition() ?: DSL.trueCondition()
return findAll(conditions, having, userId, pageable, search.toJoinConditions())
return findAll(conditions, userId, pageable, search.toJoinConditions())
}
override fun findByIdOrNull(seriesId: String, userId: String): SeriesDto? =
@ -119,16 +114,11 @@ class SeriesDtoDao(
joinConditions: JoinConditions = JoinConditions()
): SelectOnConditionStep<Record> =
dsl.selectDistinct(*groupFields)
.select(DSL.countDistinct(b.ID).`as`(BOOKS_COUNT))
.select(countUnread.`as`(BOOKS_UNREAD_COUNT))
.select(countRead.`as`(BOOKS_READ_COUNT))
.select(countInProgress.`as`(BOOKS_IN_PROGRESS_COUNT))
.apply { if (joinConditions.selectCollectionNumber) select(cs.NUMBER) }
.from(s)
.leftJoin(b).on(s.ID.eq(b.SERIES_ID))
.leftJoin(d).on(s.ID.eq(d.SERIES_ID))
.leftJoin(bma).on(s.ID.eq(bma.SERIES_ID))
.leftJoin(r).on(b.ID.eq(r.BOOK_ID)).and(readProgressCondition(userId))
.leftJoin(rs).on(s.ID.eq(rs.SERIES_ID)).and(readProgressConditionSeries(userId))
.apply { if (joinConditions.genre) leftJoin(g).on(s.ID.eq(g.SERIES_ID)) }
.apply { if (joinConditions.tag) leftJoin(st).on(s.ID.eq(st.SERIES_ID)) }
.apply { if (joinConditions.collection) leftJoin(cs).on(s.ID.eq(cs.SERIES_ID)) }
@ -136,24 +126,20 @@ class SeriesDtoDao(
private fun findAll(
conditions: Condition,
having: Condition,
userId: String,
pageable: Pageable,
joinConditions: JoinConditions = JoinConditions()
): Page<SeriesDto> {
val count = dsl.selectDistinct(s.ID)
val count = dsl.select(s.ID)
.from(s)
.leftJoin(b).on(s.ID.eq(b.SERIES_ID))
.leftJoin(d).on(s.ID.eq(d.SERIES_ID))
.leftJoin(bma).on(s.ID.eq(bma.SERIES_ID))
.leftJoin(r).on(b.ID.eq(r.BOOK_ID)).and(readProgressCondition(userId))
.leftJoin(rs).on(s.ID.eq(rs.SERIES_ID)).and(readProgressConditionSeries(userId))
.apply { if (joinConditions.genre) leftJoin(g).on(s.ID.eq(g.SERIES_ID)) }
.apply { if (joinConditions.tag) leftJoin(st).on(s.ID.eq(st.SERIES_ID)) }
.apply { if (joinConditions.collection) leftJoin(cs).on(s.ID.eq(cs.SERIES_ID)) }
.apply { if (joinConditions.aggregationAuthor) leftJoin(bmaa).on(s.ID.eq(bmaa.SERIES_ID)) }
.where(conditions)
.groupBy(s.ID)
.having(having)
.fetch()
.size
@ -161,8 +147,6 @@ class SeriesDtoDao(
val dtos = selectBase(userId, joinConditions)
.where(conditions)
.groupBy(*groupFields)
.having(having)
.orderBy(orderBy)
.apply { if (pageable.isPaged) limit(pageable.pageSize).offset(pageable.offset) }
.fetchAndMap()
@ -176,7 +160,7 @@ class SeriesDtoDao(
)
}
private fun readProgressCondition(userId: String): Condition = r.USER_ID.eq(userId).or(r.USER_ID.isNull)
private fun readProgressConditionSeries(userId: String): Condition = rs.USER_ID.eq(userId).or(rs.USER_ID.isNull)
private fun ResultQuery<Record>.fetchAndMap() =
fetch()
@ -184,10 +168,10 @@ class SeriesDtoDao(
val sr = rec.into(s)
val dr = rec.into(d)
val bmar = rec.into(bma)
val booksCount = rec.get(BOOKS_COUNT, Int::class.java)
val booksUnreadCount = rec.get(BOOKS_UNREAD_COUNT, Int::class.java)
val booksReadCount = rec.get(BOOKS_READ_COUNT, Int::class.java)
val booksInProgressCount = rec.get(BOOKS_IN_PROGRESS_COUNT, Int::class.java)
val rsr = rec.into(rs)
val booksReadCount = rsr.readCount ?: 0
val booksInProgressCount = rsr.inProgressCount ?: 0
val booksUnreadCount = sr.bookCount - booksReadCount - booksInProgressCount
val genres = dsl.select(g.GENRE)
.from(g)
@ -206,7 +190,7 @@ class SeriesDtoDao(
.map { AuthorDto(it.name, it.role) }
sr.toDto(
booksCount,
sr.bookCount,
booksReadCount,
booksUnreadCount,
booksInProgressCount,
@ -242,6 +226,16 @@ class SeriesDtoDao(
}
c = c.and(ca)
}
if (!readStatus.isNullOrEmpty()) {
val cr = readStatus.map {
when (it) {
ReadStatus.UNREAD -> rs.READ_COUNT.isNull
ReadStatus.READ -> rs.READ_COUNT.eq(s.BOOK_COUNT)
ReadStatus.IN_PROGRESS -> rs.READ_COUNT.ne(s.BOOK_COUNT)
}
}.reduce { acc, condition -> acc.or(condition) }
c = c.and(cr)
}
return c
}
@ -262,15 +256,6 @@ class SeriesDtoDao(
val aggregationAuthor: Boolean = false,
)
private fun Collection<ReadStatus>.toCondition(): Condition =
map {
when (it) {
ReadStatus.UNREAD -> countUnread.ge(inline(1.toBigDecimal()))
ReadStatus.READ -> countRead.ge(inline(1.toBigDecimal()))
ReadStatus.IN_PROGRESS -> countInProgress.ge(inline(1.toBigDecimal()))
}
}.reduce { acc, condition -> acc.or(condition) }
private fun SeriesRecord.toDto(
booksCount: Int,
booksReadCount: Int,

View file

@ -363,9 +363,7 @@ class SeriesController(
if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
bookRepository.findAllIdBySeriesId(seriesId).forEach {
bookLifecycle.markReadProgressCompleted(it, principal.user)
}
seriesLifecycle.markReadProgressCompleted(seriesId, principal.user)
}
@DeleteMapping("{seriesId}/read-progress")
@ -378,9 +376,7 @@ class SeriesController(
if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
bookRepository.findAllIdBySeriesId(seriesId).forEach {
bookLifecycle.deleteReadProgress(it, principal.user)
}
seriesLifecycle.deleteReadProgress(seriesId, principal.user)
}
@GetMapping("{seriesId}/read-progress/tachiyomi")

View file

@ -74,6 +74,7 @@ class SeriesDtoDaoTest(
makeBook("$it", seriesId = created.id, libraryId = library.id)
}
)
seriesLifecycle.sortBooks(created)
}
val series = seriesRepository.findAll().sortedBy { it.name }
@ -107,13 +108,10 @@ class SeriesDtoDaoTest(
).sortedBy { it.name }
// then
assertThat(found).hasSize(2)
assertThat(found).hasSize(1)
assertThat(found.first().booksReadCount).isEqualTo(3)
assertThat(found.first().name).isEqualTo("2")
assertThat(found.last().booksReadCount).isEqualTo(1)
assertThat(found.last().name).isEqualTo("4")
}
@Test
@ -129,13 +127,10 @@ class SeriesDtoDaoTest(
).sortedBy { it.name }
// then
assertThat(found).hasSize(2)
assertThat(found).hasSize(1)
assertThat(found.first().booksUnreadCount).isEqualTo(3)
assertThat(found.first().name).isEqualTo("3")
assertThat(found.last().booksUnreadCount).isEqualTo(1)
assertThat(found.last().name).isEqualTo("4")
}
@Test
@ -173,8 +168,8 @@ class SeriesDtoDaoTest(
).sortedBy { it.name }
// then
assertThat(found).hasSize(3)
assertThat(found.map { it.name }).containsExactlyInAnyOrder("2", "3", "4")
assertThat(found).hasSize(2)
assertThat(found.map { it.name }).containsExactlyInAnyOrder("2", "3")
}
@Test

View file

@ -607,6 +607,7 @@ class SeriesControllerTest(
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(makeBook("1.cbr", libraryId = library.id), makeBook("2.cbr", libraryId = library.id))
seriesLifecycle.addBooks(created, books)
seriesLifecycle.sortBooks(created)
}
}
@ -649,6 +650,7 @@ class SeriesControllerTest(
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(makeBook("1.cbr", libraryId = library.id), makeBook("2.cbr", libraryId = library.id))
seriesLifecycle.addBooks(created, books)
seriesLifecycle.sortBooks(created)
}
}
@ -700,6 +702,7 @@ class SeriesControllerTest(
makeBook("3.cbr", libraryId = library.id)
)
seriesLifecycle.addBooks(created, books)
seriesLifecycle.sortBooks(created)
}
}