mirror of
https://github.com/gotson/komga.git
synced 2025-12-21 16:03:03 +01:00
parent
1fc893ecb3
commit
7f3c49280b
7 changed files with 271 additions and 14 deletions
|
|
@ -1,8 +1,20 @@
|
|||
package org.gotson.komga.domain.model
|
||||
|
||||
data class BookSearch(
|
||||
open class BookSearch(
|
||||
val libraryIds: Collection<Long> = emptyList(),
|
||||
val seriesIds: Collection<Long> = emptyList(),
|
||||
val searchTerm: String? = null,
|
||||
val mediaStatus: Collection<Media.Status> = emptyList()
|
||||
)
|
||||
|
||||
class BookSearchWithReadProgress(
|
||||
libraryIds: Collection<Long> = emptyList(),
|
||||
seriesIds: Collection<Long> = emptyList(),
|
||||
searchTerm: String? = null,
|
||||
mediaStatus: Collection<Media.Status> = emptyList(),
|
||||
val readStatus: Collection<ReadStatus> = emptyList()
|
||||
) : BookSearch(libraryIds, seriesIds, searchTerm, mediaStatus)
|
||||
|
||||
enum class ReadStatus {
|
||||
UNREAD, READ, IN_PROGRESS
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package org.gotson.komga.infrastructure.jooq
|
||||
|
||||
import org.gotson.komga.domain.model.BookSearch
|
||||
import org.gotson.komga.domain.model.BookSearchWithReadProgress
|
||||
import org.gotson.komga.domain.model.ReadStatus
|
||||
import org.gotson.komga.interfaces.rest.dto.AuthorDto
|
||||
import org.gotson.komga.interfaces.rest.dto.BookDto
|
||||
import org.gotson.komga.interfaces.rest.dto.BookMetadataDto
|
||||
|
|
@ -39,19 +40,24 @@ class BookDtoDao(
|
|||
|
||||
private val sorts = mapOf(
|
||||
"metadata.numberSort" to d.NUMBER_SORT,
|
||||
"created" to b.CREATED_DATE,
|
||||
"createdDate" to b.CREATED_DATE,
|
||||
"lastModified" to b.LAST_MODIFIED_DATE,
|
||||
"lastModifiedDate" to b.LAST_MODIFIED_DATE,
|
||||
"fileSize" to b.FILE_SIZE
|
||||
"fileSize" to b.FILE_SIZE,
|
||||
"readProgress.lastModified" to r.LAST_MODIFIED_DATE
|
||||
)
|
||||
|
||||
override fun findAll(search: BookSearch, userId: Long, pageable: Pageable): Page<BookDto> {
|
||||
override fun findAll(search: BookSearchWithReadProgress, userId: Long, pageable: Pageable): Page<BookDto> {
|
||||
val conditions = search.toCondition()
|
||||
|
||||
val count = dsl.selectCount()
|
||||
.from(b)
|
||||
.leftJoin(m).on(b.ID.eq(m.BOOK_ID))
|
||||
.leftJoin(d).on(b.ID.eq(d.BOOK_ID))
|
||||
.leftJoin(r).on(b.ID.eq(r.BOOK_ID))
|
||||
.where(conditions)
|
||||
.and(readProgressCondition(userId))
|
||||
.fetchOne(0, Long::class.java)
|
||||
|
||||
val orderBy = pageable.sort.toOrderBy(sorts)
|
||||
|
|
@ -131,7 +137,7 @@ class BookDtoDao(
|
|||
br.toDto(mr.toDto(), dr.toDto(authors), if (rr.userId != null) rr.toDto() else null)
|
||||
}
|
||||
|
||||
private fun BookSearch.toCondition(): Condition {
|
||||
private fun BookSearchWithReadProgress.toCondition(): Condition {
|
||||
var c: Condition = DSL.trueCondition()
|
||||
|
||||
if (libraryIds.isNotEmpty()) c = c.and(b.LIBRARY_ID.`in`(libraryIds))
|
||||
|
|
@ -139,6 +145,18 @@ class BookDtoDao(
|
|||
searchTerm?.let { c = c.and(d.TITLE.containsIgnoreCase(it)) }
|
||||
if (mediaStatus.isNotEmpty()) c = c.and(m.STATUS.`in`(mediaStatus))
|
||||
|
||||
if (readStatus.isNotEmpty()) {
|
||||
val cr = readStatus.map {
|
||||
when (it) {
|
||||
ReadStatus.UNREAD -> r.COMPLETED.isNull
|
||||
ReadStatus.READ -> r.COMPLETED.isTrue
|
||||
ReadStatus.IN_PROGRESS -> r.COMPLETED.isFalse
|
||||
}
|
||||
}.reduce { acc, condition -> acc.or(condition) }
|
||||
|
||||
c = c.and(cr)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,10 +8,11 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse
|
|||
import mu.KotlinLogging
|
||||
import org.gotson.komga.application.tasks.TaskReceiver
|
||||
import org.gotson.komga.domain.model.Author
|
||||
import org.gotson.komga.domain.model.BookSearch
|
||||
import org.gotson.komga.domain.model.BookSearchWithReadProgress
|
||||
import org.gotson.komga.domain.model.ImageConversionException
|
||||
import org.gotson.komga.domain.model.Media
|
||||
import org.gotson.komga.domain.model.MediaNotReadyException
|
||||
import org.gotson.komga.domain.model.ReadStatus
|
||||
import org.gotson.komga.domain.persistence.BookMetadataRepository
|
||||
import org.gotson.komga.domain.persistence.BookRepository
|
||||
import org.gotson.komga.domain.persistence.MediaRepository
|
||||
|
|
@ -77,6 +78,7 @@ class BookController(
|
|||
@RequestParam(name = "search", required = false) searchTerm: String?,
|
||||
@RequestParam(name = "library_id", required = false) libraryIds: List<Long>?,
|
||||
@RequestParam(name = "media_status", required = false) mediaStatus: List<Media.Status>?,
|
||||
@RequestParam(name = "read_status", required = false) readStatus: List<ReadStatus>?,
|
||||
@Parameter(hidden = true) page: Pageable
|
||||
): Page<BookDto> {
|
||||
val pageRequest = PageRequest.of(
|
||||
|
|
@ -86,10 +88,11 @@ class BookController(
|
|||
else Sort.by(Sort.Order.asc("metadata.title").ignoreCase())
|
||||
)
|
||||
|
||||
val bookSearch = BookSearch(
|
||||
val bookSearch = BookSearchWithReadProgress(
|
||||
libraryIds = principal.user.getAuthorizedLibraryIds(libraryIds),
|
||||
searchTerm = searchTerm,
|
||||
mediaStatus = mediaStatus ?: emptyList()
|
||||
mediaStatus = mediaStatus ?: emptyList(),
|
||||
readStatus = readStatus ?: emptyList()
|
||||
)
|
||||
|
||||
return bookDtoRepository.findAll(bookSearch, principal.user.id, pageRequest)
|
||||
|
|
@ -113,7 +116,7 @@ class BookController(
|
|||
val libraryIds = if (principal.user.sharedAllLibraries) emptyList<Long>() else principal.user.sharedLibrariesIds
|
||||
|
||||
return bookDtoRepository.findAll(
|
||||
BookSearch(
|
||||
BookSearchWithReadProgress(
|
||||
libraryIds = libraryIds
|
||||
),
|
||||
principal.user.id,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import io.swagger.v3.oas.annotations.media.Schema
|
|||
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
||||
import mu.KotlinLogging
|
||||
import org.gotson.komga.application.tasks.TaskReceiver
|
||||
import org.gotson.komga.domain.model.BookSearch
|
||||
import org.gotson.komga.domain.model.BookSearchWithReadProgress
|
||||
import org.gotson.komga.domain.model.Media
|
||||
import org.gotson.komga.domain.model.SeriesMetadata
|
||||
import org.gotson.komga.domain.model.SeriesSearch
|
||||
|
|
@ -198,7 +198,7 @@ class SeriesController(
|
|||
)
|
||||
|
||||
return bookDtoRepository.findAll(
|
||||
BookSearch(
|
||||
BookSearchWithReadProgress(
|
||||
seriesIds = listOf(seriesId),
|
||||
mediaStatus = mediaStatus ?: emptyList()
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
package org.gotson.komga.interfaces.rest.persistence
|
||||
|
||||
import org.gotson.komga.domain.model.BookSearch
|
||||
import org.gotson.komga.domain.model.BookSearchWithReadProgress
|
||||
import org.gotson.komga.interfaces.rest.dto.BookDto
|
||||
import org.springframework.data.domain.Page
|
||||
import org.springframework.data.domain.Pageable
|
||||
|
||||
interface BookDtoRepository {
|
||||
fun findAll(search: BookSearch, userId: Long, pageable: Pageable): Page<BookDto>
|
||||
fun findAll(search: BookSearchWithReadProgress, userId: Long, pageable: Pageable): Page<BookDto>
|
||||
fun findByIdOrNull(bookId: Long, userId: Long): BookDto?
|
||||
fun findPreviousInSeries(bookId: Long, userId: Long): BookDto?
|
||||
fun findNextInSeries(bookId: Long, userId: Long): BookDto?
|
||||
|
|
|
|||
|
|
@ -0,0 +1,224 @@
|
|||
package org.gotson.komga.infrastructure.jooq
|
||||
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.gotson.komga.domain.model.BookSearchWithReadProgress
|
||||
import org.gotson.komga.domain.model.KomgaUser
|
||||
import org.gotson.komga.domain.model.ReadProgress
|
||||
import org.gotson.komga.domain.model.ReadStatus
|
||||
import org.gotson.komga.domain.model.makeBook
|
||||
import org.gotson.komga.domain.model.makeLibrary
|
||||
import org.gotson.komga.domain.model.makeSeries
|
||||
import org.gotson.komga.domain.persistence.BookRepository
|
||||
import org.gotson.komga.domain.persistence.KomgaUserRepository
|
||||
import org.gotson.komga.domain.persistence.LibraryRepository
|
||||
import org.gotson.komga.domain.persistence.ReadProgressRepository
|
||||
import org.gotson.komga.domain.persistence.SeriesRepository
|
||||
import org.gotson.komga.domain.service.BookLifecycle
|
||||
import org.gotson.komga.domain.service.KomgaUserLifecycle
|
||||
import org.gotson.komga.domain.service.LibraryLifecycle
|
||||
import org.gotson.komga.domain.service.SeriesLifecycle
|
||||
import org.junit.jupiter.api.AfterAll
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeAll
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.data.domain.PageRequest
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||
|
||||
@ExtendWith(SpringExtension::class)
|
||||
@SpringBootTest
|
||||
@AutoConfigureTestDatabase
|
||||
class BookDtoDaoTest(
|
||||
@Autowired private val bookDtoDao: BookDtoDao,
|
||||
@Autowired private val bookRepository: BookRepository,
|
||||
@Autowired private val bookLifecycle: BookLifecycle,
|
||||
@Autowired private val seriesRepository: SeriesRepository,
|
||||
@Autowired private val seriesLifecycle: SeriesLifecycle,
|
||||
@Autowired private val libraryRepository: LibraryRepository,
|
||||
@Autowired private val libraryLifecycle: LibraryLifecycle,
|
||||
@Autowired private val readProgressRepository: ReadProgressRepository,
|
||||
@Autowired private val userRepository: KomgaUserRepository,
|
||||
@Autowired private val userLifecycle: KomgaUserLifecycle
|
||||
) {
|
||||
|
||||
private var library = makeLibrary()
|
||||
private var series = makeSeries("Series")
|
||||
private var user = KomgaUser("user@example.org", "", false)
|
||||
|
||||
@BeforeAll
|
||||
fun setup() {
|
||||
library = libraryRepository.insert(library)
|
||||
series = seriesLifecycle.createSeries(series.copy(libraryId = library.id))
|
||||
user = userRepository.save(user)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun deleteBooks() {
|
||||
bookRepository.findAll().forEach {
|
||||
bookLifecycle.delete(it.id)
|
||||
}
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun tearDown() {
|
||||
userRepository.findAll().forEach {
|
||||
userLifecycle.deleteUser(it)
|
||||
}
|
||||
libraryRepository.findAll().forEach {
|
||||
libraryLifecycle.deleteLibrary(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupBooks() {
|
||||
seriesLifecycle.addBooks(series,
|
||||
(1..3).map {
|
||||
makeBook("$it", seriesId = series.id, libraryId = library.id)
|
||||
})
|
||||
|
||||
val books = bookRepository.findAll().sortedBy { it.name }
|
||||
books.elementAt(0).let { readProgressRepository.save(ReadProgress(it.id, user.id, 5, false)) }
|
||||
books.elementAt(1).let { readProgressRepository.save(ReadProgress(it.id, user.id, 5, true)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given books in various read status when searching for read books then only read books are returned`() {
|
||||
// given
|
||||
setupBooks()
|
||||
|
||||
// when
|
||||
val found = bookDtoDao.findAll(
|
||||
BookSearchWithReadProgress(readStatus = listOf(ReadStatus.READ)),
|
||||
user.id,
|
||||
PageRequest.of(0, 20)
|
||||
)
|
||||
|
||||
// then
|
||||
assertThat(found).hasSize(1)
|
||||
assertThat(found.first().readProgress?.completed).isTrue()
|
||||
assertThat(found.first().name).isEqualTo("2")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given books in various read status when searching for unread books then only unread books are returned`() {
|
||||
// given
|
||||
setupBooks()
|
||||
|
||||
// when
|
||||
val found = bookDtoDao.findAll(
|
||||
BookSearchWithReadProgress(readStatus = listOf(ReadStatus.UNREAD)),
|
||||
user.id,
|
||||
PageRequest.of(0, 20)
|
||||
)
|
||||
|
||||
// then
|
||||
assertThat(found).hasSize(1)
|
||||
assertThat(found.first().readProgress).isNull()
|
||||
assertThat(found.first().name).isEqualTo("3")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given books in various read status when searching for in progress books then only in progress books are returned`() {
|
||||
// given
|
||||
setupBooks()
|
||||
|
||||
// when
|
||||
val found = bookDtoDao.findAll(
|
||||
BookSearchWithReadProgress(readStatus = listOf(ReadStatus.IN_PROGRESS)),
|
||||
user.id,
|
||||
PageRequest.of(0, 20)
|
||||
)
|
||||
|
||||
// then
|
||||
assertThat(found).hasSize(1)
|
||||
assertThat(found.first().readProgress?.completed).isFalse()
|
||||
assertThat(found.first().name).isEqualTo("1")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given books in various read status when searching for read and unread books then only matching books are returned`() {
|
||||
// given
|
||||
setupBooks()
|
||||
|
||||
// when
|
||||
val found = bookDtoDao.findAll(
|
||||
BookSearchWithReadProgress(readStatus = listOf(ReadStatus.READ, ReadStatus.UNREAD)),
|
||||
user.id,
|
||||
PageRequest.of(0, 20)
|
||||
)
|
||||
|
||||
// then
|
||||
assertThat(found).hasSize(2)
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("2", "3")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given books in various read status when searching for read and in progress books then only matching books are returned`() {
|
||||
// given
|
||||
setupBooks()
|
||||
|
||||
// when
|
||||
val found = bookDtoDao.findAll(
|
||||
BookSearchWithReadProgress(readStatus = listOf(ReadStatus.READ, ReadStatus.IN_PROGRESS)),
|
||||
user.id,
|
||||
PageRequest.of(0, 20)
|
||||
)
|
||||
|
||||
// then
|
||||
assertThat(found).hasSize(2)
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("2", "1")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given books in various read status when searching for unread and in progress books then only matching books are returned`() {
|
||||
// given
|
||||
setupBooks()
|
||||
|
||||
// when
|
||||
val found = bookDtoDao.findAll(
|
||||
BookSearchWithReadProgress(readStatus = listOf(ReadStatus.UNREAD, ReadStatus.IN_PROGRESS)),
|
||||
user.id,
|
||||
PageRequest.of(0, 20)
|
||||
)
|
||||
|
||||
// then
|
||||
assertThat(found).hasSize(2)
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("3", "1")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given books in various read status when searching for read and unread and in progress books then only matching books are returned`() {
|
||||
// given
|
||||
setupBooks()
|
||||
|
||||
// when
|
||||
val found = bookDtoDao.findAll(
|
||||
BookSearchWithReadProgress(readStatus = listOf(ReadStatus.UNREAD, ReadStatus.IN_PROGRESS, ReadStatus.READ)),
|
||||
user.id,
|
||||
PageRequest.of(0, 20)
|
||||
)
|
||||
|
||||
// then
|
||||
assertThat(found).hasSize(3)
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("3", "1", "2")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given books in various read status when searching without read progress then all books are returned`() {
|
||||
// given
|
||||
setupBooks()
|
||||
|
||||
// when
|
||||
val found = bookDtoDao.findAll(
|
||||
BookSearchWithReadProgress(),
|
||||
user.id,
|
||||
PageRequest.of(0, 20)
|
||||
)
|
||||
|
||||
// then
|
||||
assertThat(found).hasSize(3)
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("3", "1", "2")
|
||||
}
|
||||
}
|
||||
|
|
@ -82,7 +82,7 @@ class BookControllerTest(
|
|||
}
|
||||
|
||||
@AfterAll
|
||||
fun `teardown`() {
|
||||
fun teardown() {
|
||||
userRepository.findAll().forEach {
|
||||
userLifecycle.deleteUser(it)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue