mirror of
https://github.com/gotson/komga.git
synced 2025-01-09 04:08:00 +08:00
parent
f5948bd478
commit
1b6a030ab5
@ -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? {
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user