Skip to content

Commit

Permalink
feat(api): add new book search condition for posters
Browse files Browse the repository at this point in the history
Refs: #1829
  • Loading branch information
gotson committed Jan 20, 2025
1 parent ffc397f commit 70bcb8f
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -162,4 +162,21 @@ class SearchCondition {
val name: String? = null,
val role: String? = null,
)

data class Poster(
@JsonProperty("poster")
val operator: SearchOperator.Equality<PosterMatch>,
) : Book

@JsonInclude(JsonInclude.Include.NON_NULL)
data class PosterMatch(
val type: Type? = null,
val selected: Boolean? = null,
) {
enum class Type {
GENERATED,
SIDECAR,
USER_UPLOADED,
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())),
),
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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")
}
}
}

0 comments on commit 70bcb8f

Please sign in to comment.