From 70bcb8f417e84da16f1b0f674dc3fe0b5839fa02 Mon Sep 17 00:00:00 2001 From: Gauthier Roebroeck Date: Mon, 20 Jan 2025 13:07:54 +0800 Subject: [PATCH] feat(api): add new book search condition for posters Refs: #1829 --- .../komga/domain/model/SearchCondition.kt | 17 +++ .../infrastructure/jooq/BookSearchHelper.kt | 33 +++++ .../komga/domain/model/BookSearchTest.kt | 4 + .../jooq/main/BookSearchTest.kt | 119 ++++++++++++++++++ 4 files changed, 173 insertions(+) diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/SearchCondition.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/SearchCondition.kt index 15df714ca..f9bf2ca13 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/model/SearchCondition.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/SearchCondition.kt @@ -162,4 +162,21 @@ class SearchCondition { val name: String? = null, val role: String? = null, ) + + data class Poster( + @JsonProperty("poster") + val operator: SearchOperator.Equality, + ) : Book + + @JsonInclude(JsonInclude.Include.NON_NULL) + data class PosterMatch( + val type: Type? = null, + val selected: Boolean? = null, + ) { + enum class Type { + GENERATED, + SIDECAR, + USER_UPLOADED, + } + } } diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/BookSearchHelper.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/BookSearchHelper.kt index 0b157ed90..789ef1352 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/BookSearchHelper.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/BookSearchHelper.kt @@ -64,6 +64,7 @@ class BookSearchHelper( rlbAlias(searchCondition.operator.value) .READLIST_ID .eq(searchCondition.operator.value) to setOf(RequiredJoin.ReadList(searchCondition.operator.value)) + is SearchOperator.IsNot -> { val inner = { readListId: String -> DSL @@ -198,6 +199,38 @@ class BookSearchHelper( } to emptySet() } + is SearchCondition.Poster -> + Tables.BOOK.ID.let { field -> + val inner = { type: SearchCondition.PosterMatch.Type?, selected: Boolean? -> + DSL + .select(Tables.THUMBNAIL_BOOK.BOOK_ID) + .from(Tables.THUMBNAIL_BOOK) + .where(DSL.noCondition()) + .apply { + if (type != null) + and(Tables.THUMBNAIL_BOOK.TYPE.equalIgnoreCase(type.name)) + if (selected != null && selected) + and(Tables.THUMBNAIL_BOOK.SELECTED.isTrue) + if (selected != null && !selected) + and(Tables.THUMBNAIL_BOOK.SELECTED.isFalse) + } + } + when (searchCondition.operator) { + is SearchOperator.Is -> { + if (searchCondition.operator.value.type == null && searchCondition.operator.value.selected == null) + DSL.noCondition() + else + field.`in`(inner(searchCondition.operator.value.type, searchCondition.operator.value.selected)) + } + + is SearchOperator.IsNot -> + if (searchCondition.operator.value.type == null && searchCondition.operator.value.selected == null) + DSL.noCondition() + else + field.notIn(inner(searchCondition.operator.value.type, searchCondition.operator.value.selected)) + } to emptySet() + } + is SearchCondition.OneShot -> searchCondition.operator.toCondition(Tables.BOOK.ONESHOT) to emptySet() null -> DSL.noCondition() to emptySet() diff --git a/komga/src/test/kotlin/org/gotson/komga/domain/model/BookSearchTest.kt b/komga/src/test/kotlin/org/gotson/komga/domain/model/BookSearchTest.kt index fda4ae9cc..646dd378b 100644 --- a/komga/src/test/kotlin/org/gotson/komga/domain/model/BookSearchTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/domain/model/BookSearchTest.kt @@ -63,6 +63,10 @@ class BookSearchTest( SearchCondition.Author(SearchOperator.IsNot(AuthorMatch())), SearchCondition.OneShot(SearchOperator.IsFalse), SearchCondition.OneShot(SearchOperator.IsTrue), + SearchCondition.Poster(SearchOperator.Is(SearchCondition.PosterMatch(type = SearchCondition.PosterMatch.Type.GENERATED, selected = false))), + SearchCondition.Poster(SearchOperator.Is(SearchCondition.PosterMatch(selected = true))), + SearchCondition.Poster(SearchOperator.IsNot(SearchCondition.PosterMatch(type = SearchCondition.PosterMatch.Type.SIDECAR))), + SearchCondition.Poster(SearchOperator.IsNot(SearchCondition.PosterMatch())), ), ) diff --git a/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/main/BookSearchTest.kt b/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/main/BookSearchTest.kt index f4d824d61..73686015f 100644 --- a/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/main/BookSearchTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/main/BookSearchTest.kt @@ -8,7 +8,9 @@ import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.catchThrowable import org.gotson.komga.domain.model.Author import org.gotson.komga.domain.model.BookSearch +import org.gotson.komga.domain.model.Dimension import org.gotson.komga.domain.model.KomgaUser +import org.gotson.komga.domain.model.MarkSelectedPreference import org.gotson.komga.domain.model.Media import org.gotson.komga.domain.model.MediaProfile import org.gotson.komga.domain.model.MediaType @@ -19,6 +21,7 @@ import org.gotson.komga.domain.model.SearchCondition import org.gotson.komga.domain.model.SearchCondition.AuthorMatch import org.gotson.komga.domain.model.SearchContext import org.gotson.komga.domain.model.SearchOperator +import org.gotson.komga.domain.model.ThumbnailBook import org.gotson.komga.domain.model.makeBook import org.gotson.komga.domain.model.makeLibrary import org.gotson.komga.domain.model.makeSeries @@ -970,4 +973,120 @@ class BookSearchTest( assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("2") } } + + @Test + fun `given some books when searching by poster then results are accurate`() { + // book with GENERATED selected + makeBook("1", libraryId = library1.id, seriesId = series1.id).let { book -> + seriesLifecycle.addBooks(series1, listOf(book)) + bookLifecycle.addThumbnailForBook(ThumbnailBook(bookId = book.id, type = ThumbnailBook.Type.GENERATED, mediaType = "image/jpeg", fileSize = 0L, dimension = Dimension(0, 0)), MarkSelectedPreference.YES) + } + // book with GENERATED not selected, SIDECAR selected + makeBook("2", libraryId = library2.id, seriesId = series2.id).let { book -> + seriesLifecycle.addBooks(series2, listOf(book)) + bookLifecycle.addThumbnailForBook(ThumbnailBook(bookId = book.id, type = ThumbnailBook.Type.GENERATED, mediaType = "image/jpeg", fileSize = 0L, dimension = Dimension(0, 0)), MarkSelectedPreference.YES) + bookLifecycle.addThumbnailForBook(ThumbnailBook(bookId = book.id, type = ThumbnailBook.Type.SIDECAR, mediaType = "image/jpeg", fileSize = 0L, dimension = Dimension(0, 0)), MarkSelectedPreference.YES) + } + // book with GENERATED not selected, USER_UPLOADED selected + makeBook("3", libraryId = library2.id, seriesId = series2.id).let { book -> + seriesLifecycle.addBooks(series2, listOf(book)) + bookLifecycle.addThumbnailForBook(ThumbnailBook(bookId = book.id, type = ThumbnailBook.Type.GENERATED, mediaType = "image/jpeg", fileSize = 0L, dimension = Dimension(0, 0)), MarkSelectedPreference.YES) + bookLifecycle.addThumbnailForBook(ThumbnailBook(bookId = book.id, type = ThumbnailBook.Type.USER_UPLOADED, mediaType = "image/jpeg", fileSize = 0L, dimension = Dimension(0, 0)), MarkSelectedPreference.YES) + } + // book without poster + makeBook("4", libraryId = library2.id, seriesId = series2.id).let { book -> + seriesLifecycle.addBooks(series2, listOf(book)) + } + + // books with a poster of type GENERATED + run { + val search = + BookSearch( + SearchCondition.Poster(SearchOperator.Is(SearchCondition.PosterMatch(SearchCondition.PosterMatch.Type.GENERATED))), + ) + val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content + val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content + + assertThat(found.map { it.name }).containsExactlyInAnyOrder("1", "2", "3") + assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1", "2", "3") + } + + // books with a poster of type GENERATED, selected + run { + val search = + BookSearch( + SearchCondition.Poster(SearchOperator.Is(SearchCondition.PosterMatch(SearchCondition.PosterMatch.Type.GENERATED, true))), + ) + val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content + val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content + + assertThat(found.map { it.name }).containsExactlyInAnyOrder("1") + assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1") + } + + // books with any poster not selected + run { + val search = + BookSearch( + SearchCondition.Poster(SearchOperator.Is(SearchCondition.PosterMatch(selected = false))), + ) + val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content + val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content + + assertThat(found.map { it.name }).containsExactlyInAnyOrder("2", "3") + assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("2", "3") + } + + // books without a poster of type SIDECAR + run { + val search = + BookSearch( + SearchCondition.Poster(SearchOperator.IsNot(SearchCondition.PosterMatch(SearchCondition.PosterMatch.Type.SIDECAR))), + ) + val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content + val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content + + assertThat(found.map { it.name }).containsExactlyInAnyOrder("1", "3", "4") + assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1", "3", "4") + } + + // books without a poster of type GENERATED + run { + val search = + BookSearch( + SearchCondition.Poster(SearchOperator.IsNot(SearchCondition.PosterMatch(SearchCondition.PosterMatch.Type.GENERATED))), + ) + val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content + val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content + + assertThat(found.map { it.name }).containsExactlyInAnyOrder("4") + assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("4") + } + + // books without selected poster + run { + val search = + BookSearch( + SearchCondition.Poster(SearchOperator.IsNot(SearchCondition.PosterMatch(selected = true))), + ) + val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content + val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content + + assertThat(found.map { it.name }).containsExactlyInAnyOrder("4") + assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("4") + } + + // empty PosterMatch does not apply any condition + run { + val search = + BookSearch( + SearchCondition.Poster(SearchOperator.Is(SearchCondition.PosterMatch())), + ) + val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content + val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content + + assertThat(found.map { it.name }).containsExactlyInAnyOrder("1", "2", "3", "4") + assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1", "2", "3", "4") + } + } }