feat(api): on deck books

related to #131
This commit is contained in:
Gauthier Roebroeck 2020-06-08 10:45:15 +08:00
parent f5948bd478
commit 1b6a030ab5
5 changed files with 254 additions and 132 deletions

View File

@ -35,6 +35,7 @@ class BookDtoDao(
private val d = Tables.BOOK_METADATA
private val r = Tables.READ_PROGRESS
private val a = Tables.BOOK_METADATA_AUTHOR
private val s = Tables.SERIES
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 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 findSibling(bookId: Long, userId: Long, next: Boolean): BookDto? {

View File

@ -34,20 +34,22 @@ class SeriesDtoDao(
private val dsl: DSLContext
) : SeriesDtoRepository {
private val s = Tables.SERIES
private val b = Tables.BOOK
private val d = Tables.SERIES_METADATA
private val r = Tables.READ_PROGRESS
companion object {
private val s = Tables.SERIES
private val b = Tables.BOOK
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(
*s.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(
"metadata.titleSort" to DSL.lower(d.TITLE_SORT),
"createdDate" to s.CREATED_DATE,

View File

@ -124,6 +124,22 @@ class BookController(
).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}")
fun getOneBook(

View File

@ -10,4 +10,5 @@ interface BookDtoRepository {
fun findByIdOrNull(bookId: Long, userId: Long): BookDto?
fun findPreviousInSeries(bookId: Long, userId: Long): BookDto?
fun findNextInSeries(bookId: Long, userId: Long): BookDto?
fun findOnDeck(libraryIds: Collection<Long>, userId: Long, pageable: Pageable): Page<BookDto>
}

View File

@ -20,6 +20,7 @@ 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.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
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)) }
}
@Test
fun `given books in various read status when searching for read books then only read books are returned`() {
// given
setupBooks()
@Nested
inner class ReadProgress {
@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)
)
// 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")
// 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")
}
}
@Test
fun `given books in various read status when searching for unread books then only unread books are returned`() {
// given
setupBooks()
@Nested
inner class OnDeck {
@Test
fun `given series with in progress books status when searching for on deck then nothing is returned`() {
// given
setupBooks()
// when
val found = bookDtoDao.findAll(
BookSearchWithReadProgress(readStatus = listOf(ReadStatus.UNREAD)),
user.id,
PageRequest.of(0, 20)
)
// when
val found = bookDtoDao.findOnDeck(
emptyList(),
user.id,
PageRequest.of(0, 20)
)
// then
assertThat(found).hasSize(1)
assertThat(found.first().readProgress).isNull()
assertThat(found.first().name).isEqualTo("3")
}
// then
assertThat(found).isEmpty()
}
@Test
fun `given books in various read status when searching for in progress books then only in progress books are returned`() {
// given
setupBooks()
@Test
fun `given series with only unread books when searching for on deck then no books are returned`() {
// given
seriesLifecycle.addBooks(series,
(1..3).map {
makeBook("$it", seriesId = series.id, libraryId = library.id)
})
// when
val found = bookDtoDao.findAll(
BookSearchWithReadProgress(readStatus = listOf(ReadStatus.IN_PROGRESS)),
user.id,
PageRequest.of(0, 20)
)
// when
val found = bookDtoDao.findOnDeck(
emptyList(),
user.id,
PageRequest.of(0, 20)
)
// then
assertThat(found).hasSize(1)
assertThat(found.first().readProgress?.completed).isFalse()
assertThat(found.first().name).isEqualTo("1")
}
// then
assertThat(found).hasSize(0)
}
@Test
fun `given books in various read status when searching for read and unread books then only matching books are returned`() {
// given
setupBooks()
@Test
fun `given series with some unread books when searching for on deck then first unread book of series is returned`() {
// given
seriesLifecycle.addBooks(series,
(1..3).map {
makeBook("$it", seriesId = series.id, libraryId = library.id)
})
// when
val found = bookDtoDao.findAll(
BookSearchWithReadProgress(readStatus = listOf(ReadStatus.READ, ReadStatus.UNREAD)),
user.id,
PageRequest.of(0, 20)
)
val books = bookRepository.findAll().sortedBy { it.name }
books.elementAt(0).let { readProgressRepository.save(ReadProgress(it.id, user.id, 5, true)) }
// then
assertThat(found).hasSize(2)
assertThat(found.map { it.name }).containsExactlyInAnyOrder("2", "3")
}
// when
val found = bookDtoDao.findOnDeck(
emptyList(),
user.id,
PageRequest.of(0, 20)
)
@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")
// then
assertThat(found).hasSize(1)
assertThat(found.first().name).isEqualTo("2")
}
}
}