mirror of
https://github.com/gotson/komga.git
synced 2025-12-22 00:13:30 +01:00
parent
f5948bd478
commit
1b6a030ab5
5 changed files with 254 additions and 132 deletions
|
|
@ -35,6 +35,7 @@ class BookDtoDao(
|
||||||
private val d = Tables.BOOK_METADATA
|
private val d = Tables.BOOK_METADATA
|
||||||
private val r = Tables.READ_PROGRESS
|
private val r = Tables.READ_PROGRESS
|
||||||
private val a = Tables.BOOK_METADATA_AUTHOR
|
private val a = Tables.BOOK_METADATA_AUTHOR
|
||||||
|
private val s = Tables.SERIES
|
||||||
|
|
||||||
private val mediaFields = m.fields().filterNot { it.name == m.THUMBNAIL.name }.toTypedArray()
|
private val mediaFields = m.fields().filterNot { it.name == m.THUMBNAIL.name }.toTypedArray()
|
||||||
|
|
||||||
|
|
@ -86,6 +87,43 @@ class BookDtoDao(
|
||||||
|
|
||||||
override fun findNextInSeries(bookId: Long, userId: Long): BookDto? = findSibling(bookId, userId, next = true)
|
override fun findNextInSeries(bookId: Long, userId: Long): BookDto? = findSibling(bookId, userId, next = true)
|
||||||
|
|
||||||
|
|
||||||
|
override fun findOnDeck(libraryIds: Collection<Long>, userId: Long, pageable: Pageable): Page<BookDto> {
|
||||||
|
val conditions = if (libraryIds.isEmpty()) DSL.trueCondition() else s.LIBRARY_ID.`in`(libraryIds)
|
||||||
|
|
||||||
|
val seriesIds = dsl.select(s.ID)
|
||||||
|
.from(s)
|
||||||
|
.leftJoin(b).on(s.ID.eq(b.SERIES_ID))
|
||||||
|
.leftJoin(r).on(b.ID.eq(r.BOOK_ID))
|
||||||
|
.and(readProgressCondition(userId))
|
||||||
|
.where(conditions)
|
||||||
|
.groupBy(s.ID)
|
||||||
|
.having(SeriesDtoDao.countUnread.ge(1.toBigDecimal()))
|
||||||
|
.and(SeriesDtoDao.countRead.ge(1.toBigDecimal()))
|
||||||
|
.and(SeriesDtoDao.countInProgress.eq(0.toBigDecimal()))
|
||||||
|
.orderBy(DSL.max(r.LAST_MODIFIED_DATE).desc())
|
||||||
|
.fetchInto(Long::class.java)
|
||||||
|
|
||||||
|
val dtos = seriesIds
|
||||||
|
.drop(pageable.pageNumber * pageable.pageSize)
|
||||||
|
.take(pageable.pageSize)
|
||||||
|
.mapNotNull { seriesId ->
|
||||||
|
selectBase(userId)
|
||||||
|
.where(b.SERIES_ID.eq(seriesId))
|
||||||
|
.and(r.COMPLETED.isNull)
|
||||||
|
.orderBy(d.NUMBER_SORT.asc())
|
||||||
|
.limit(1)
|
||||||
|
.fetchAndMap()
|
||||||
|
.firstOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
return PageImpl(
|
||||||
|
dtos,
|
||||||
|
PageRequest.of(pageable.pageNumber, pageable.pageSize, pageable.sort),
|
||||||
|
seriesIds.size.toLong()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun readProgressCondition(userId: Long): Condition = r.USER_ID.eq(userId).or(r.USER_ID.isNull)
|
private fun readProgressCondition(userId: Long): Condition = r.USER_ID.eq(userId).or(r.USER_ID.isNull)
|
||||||
|
|
||||||
private fun findSibling(bookId: Long, userId: Long, next: Boolean): BookDto? {
|
private fun findSibling(bookId: Long, userId: Long, next: Boolean): BookDto? {
|
||||||
|
|
|
||||||
|
|
@ -34,20 +34,22 @@ class SeriesDtoDao(
|
||||||
private val dsl: DSLContext
|
private val dsl: DSLContext
|
||||||
) : SeriesDtoRepository {
|
) : SeriesDtoRepository {
|
||||||
|
|
||||||
private val s = Tables.SERIES
|
companion object {
|
||||||
private val b = Tables.BOOK
|
private val s = Tables.SERIES
|
||||||
private val d = Tables.SERIES_METADATA
|
private val b = Tables.BOOK
|
||||||
private val r = Tables.READ_PROGRESS
|
private val d = Tables.SERIES_METADATA
|
||||||
|
private val r = Tables.READ_PROGRESS
|
||||||
|
|
||||||
|
val countUnread: AggregateFunction<BigDecimal> = DSL.sum(DSL.`when`(r.COMPLETED.isNull, 1).otherwise(0))
|
||||||
|
val countRead: AggregateFunction<BigDecimal> = DSL.sum(DSL.`when`(r.COMPLETED.isTrue, 1).otherwise(0))
|
||||||
|
val countInProgress: AggregateFunction<BigDecimal> = DSL.sum(DSL.`when`(r.COMPLETED.isFalse, 1).otherwise(0))
|
||||||
|
}
|
||||||
|
|
||||||
private val groupFields = arrayOf(
|
private val groupFields = arrayOf(
|
||||||
*s.fields(),
|
*s.fields(),
|
||||||
*d.fields()
|
*d.fields()
|
||||||
)
|
)
|
||||||
|
|
||||||
val countUnread: AggregateFunction<BigDecimal> = DSL.sum(DSL.`when`(r.COMPLETED.isNull, 1).otherwise(0))
|
|
||||||
val countRead: AggregateFunction<BigDecimal> = DSL.sum(DSL.`when`(r.COMPLETED.isTrue, 1).otherwise(0))
|
|
||||||
val countInProgress: AggregateFunction<BigDecimal> = DSL.sum(DSL.`when`(r.COMPLETED.isFalse, 1).otherwise(0))
|
|
||||||
|
|
||||||
private val sorts = mapOf(
|
private val sorts = mapOf(
|
||||||
"metadata.titleSort" to DSL.lower(d.TITLE_SORT),
|
"metadata.titleSort" to DSL.lower(d.TITLE_SORT),
|
||||||
"createdDate" to s.CREATED_DATE,
|
"createdDate" to s.CREATED_DATE,
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,22 @@ class BookController(
|
||||||
).map { it.restrictUrl(!principal.user.roleAdmin) }
|
).map { it.restrictUrl(!principal.user.roleAdmin) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(description = "Return first unread book of series with at least one book read and no books in progress.")
|
||||||
|
@PageableWithoutSortAsQueryParam
|
||||||
|
@GetMapping("api/v1/books/ondeck")
|
||||||
|
fun getBooksOnDeck(
|
||||||
|
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||||
|
@Parameter(hidden = true) page: Pageable
|
||||||
|
): Page<BookDto> {
|
||||||
|
val libraryIds = if (principal.user.sharedAllLibraries) emptyList<Long>() else principal.user.sharedLibrariesIds
|
||||||
|
|
||||||
|
return bookDtoRepository.findOnDeck(
|
||||||
|
libraryIds,
|
||||||
|
principal.user.id,
|
||||||
|
page
|
||||||
|
).map { it.restrictUrl(!principal.user.roleAdmin) }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@GetMapping("api/v1/books/{bookId}")
|
@GetMapping("api/v1/books/{bookId}")
|
||||||
fun getOneBook(
|
fun getOneBook(
|
||||||
|
|
|
||||||
|
|
@ -10,4 +10,5 @@ interface BookDtoRepository {
|
||||||
fun findByIdOrNull(bookId: Long, userId: Long): BookDto?
|
fun findByIdOrNull(bookId: Long, userId: Long): BookDto?
|
||||||
fun findPreviousInSeries(bookId: Long, userId: Long): BookDto?
|
fun findPreviousInSeries(bookId: Long, userId: Long): BookDto?
|
||||||
fun findNextInSeries(bookId: Long, userId: Long): BookDto?
|
fun findNextInSeries(bookId: Long, userId: Long): BookDto?
|
||||||
|
fun findOnDeck(libraryIds: Collection<Long>, userId: Long, pageable: Pageable): Page<BookDto>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import org.gotson.komga.domain.service.SeriesLifecycle
|
||||||
import org.junit.jupiter.api.AfterAll
|
import org.junit.jupiter.api.AfterAll
|
||||||
import org.junit.jupiter.api.AfterEach
|
import org.junit.jupiter.api.AfterEach
|
||||||
import org.junit.jupiter.api.BeforeAll
|
import org.junit.jupiter.api.BeforeAll
|
||||||
|
import org.junit.jupiter.api.Nested
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.extension.ExtendWith
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
|
@ -83,142 +84,206 @@ class BookDtoDaoTest(
|
||||||
books.elementAt(1).let { readProgressRepository.save(ReadProgress(it.id, user.id, 5, true)) }
|
books.elementAt(1).let { readProgressRepository.save(ReadProgress(it.id, user.id, 5, true)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Nested
|
||||||
fun `given books in various read status when searching for read books then only read books are returned`() {
|
inner class ReadProgress {
|
||||||
// given
|
@Test
|
||||||
setupBooks()
|
fun `given books in various read status when searching for read books then only read books are returned`() {
|
||||||
|
// given
|
||||||
|
setupBooks()
|
||||||
|
|
||||||
// when
|
// when
|
||||||
val found = bookDtoDao.findAll(
|
val found = bookDtoDao.findAll(
|
||||||
BookSearchWithReadProgress(readStatus = listOf(ReadStatus.READ)),
|
BookSearchWithReadProgress(readStatus = listOf(ReadStatus.READ)),
|
||||||
user.id,
|
user.id,
|
||||||
PageRequest.of(0, 20)
|
PageRequest.of(0, 20)
|
||||||
)
|
)
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(found).hasSize(1)
|
assertThat(found).hasSize(1)
|
||||||
assertThat(found.first().readProgress?.completed).isTrue()
|
assertThat(found.first().readProgress?.completed).isTrue()
|
||||||
assertThat(found.first().name).isEqualTo("2")
|
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Nested
|
||||||
fun `given books in various read status when searching for unread books then only unread books are returned`() {
|
inner class OnDeck {
|
||||||
// given
|
@Test
|
||||||
setupBooks()
|
fun `given series with in progress books status when searching for on deck then nothing is returned`() {
|
||||||
|
// given
|
||||||
|
setupBooks()
|
||||||
|
|
||||||
// when
|
// when
|
||||||
val found = bookDtoDao.findAll(
|
val found = bookDtoDao.findOnDeck(
|
||||||
BookSearchWithReadProgress(readStatus = listOf(ReadStatus.UNREAD)),
|
emptyList(),
|
||||||
user.id,
|
user.id,
|
||||||
PageRequest.of(0, 20)
|
PageRequest.of(0, 20)
|
||||||
)
|
)
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(found).hasSize(1)
|
assertThat(found).isEmpty()
|
||||||
assertThat(found.first().readProgress).isNull()
|
}
|
||||||
assertThat(found.first().name).isEqualTo("3")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given books in various read status when searching for in progress books then only in progress books are returned`() {
|
fun `given series with only unread books when searching for on deck then no books are returned`() {
|
||||||
// given
|
// given
|
||||||
setupBooks()
|
seriesLifecycle.addBooks(series,
|
||||||
|
(1..3).map {
|
||||||
|
makeBook("$it", seriesId = series.id, libraryId = library.id)
|
||||||
|
})
|
||||||
|
|
||||||
// when
|
// when
|
||||||
val found = bookDtoDao.findAll(
|
val found = bookDtoDao.findOnDeck(
|
||||||
BookSearchWithReadProgress(readStatus = listOf(ReadStatus.IN_PROGRESS)),
|
emptyList(),
|
||||||
user.id,
|
user.id,
|
||||||
PageRequest.of(0, 20)
|
PageRequest.of(0, 20)
|
||||||
)
|
)
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(found).hasSize(1)
|
assertThat(found).hasSize(0)
|
||||||
assertThat(found.first().readProgress?.completed).isFalse()
|
}
|
||||||
assertThat(found.first().name).isEqualTo("1")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given books in various read status when searching for read and unread books then only matching books are returned`() {
|
fun `given series with some unread books when searching for on deck then first unread book of series is returned`() {
|
||||||
// given
|
// given
|
||||||
setupBooks()
|
seriesLifecycle.addBooks(series,
|
||||||
|
(1..3).map {
|
||||||
|
makeBook("$it", seriesId = series.id, libraryId = library.id)
|
||||||
|
})
|
||||||
|
|
||||||
// when
|
val books = bookRepository.findAll().sortedBy { it.name }
|
||||||
val found = bookDtoDao.findAll(
|
books.elementAt(0).let { readProgressRepository.save(ReadProgress(it.id, user.id, 5, true)) }
|
||||||
BookSearchWithReadProgress(readStatus = listOf(ReadStatus.READ, ReadStatus.UNREAD)),
|
|
||||||
user.id,
|
|
||||||
PageRequest.of(0, 20)
|
|
||||||
)
|
|
||||||
|
|
||||||
// then
|
// when
|
||||||
assertThat(found).hasSize(2)
|
val found = bookDtoDao.findOnDeck(
|
||||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("2", "3")
|
emptyList(),
|
||||||
}
|
user.id,
|
||||||
|
PageRequest.of(0, 20)
|
||||||
|
)
|
||||||
|
|
||||||
@Test
|
// then
|
||||||
fun `given books in various read status when searching for read and in progress books then only matching books are returned`() {
|
assertThat(found).hasSize(1)
|
||||||
// given
|
assertThat(found.first().name).isEqualTo("2")
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue