mirror of
https://github.com/gotson/komga.git
synced 2025-12-21 16:03:03 +01:00
feat(api): find previous/next book in readlist
this also works for users with limited access to libraries
This commit is contained in:
parent
cdca78b38f
commit
bcfb203f74
4 changed files with 215 additions and 11 deletions
|
|
@ -121,9 +121,28 @@ class BookDtoDao(
|
|||
.fetchAndMap()
|
||||
.firstOrNull()
|
||||
|
||||
override fun findPreviousInSeries(bookId: String, userId: String): BookDto? = findSibling(bookId, userId, next = false)
|
||||
override fun findPreviousInSeries(bookId: String, userId: String): BookDto? =
|
||||
findSiblingSeries(bookId, userId, next = false)
|
||||
|
||||
override fun findNextInSeries(bookId: String, userId: String): BookDto? = findSibling(bookId, userId, next = true)
|
||||
override fun findNextInSeries(bookId: String, userId: String): BookDto? =
|
||||
findSiblingSeries(bookId, userId, next = true)
|
||||
|
||||
|
||||
override fun findPreviousInReadList(
|
||||
readListId: String,
|
||||
bookId: String,
|
||||
userId: String,
|
||||
filterOnLibraryIds: Collection<String>?
|
||||
): BookDto? =
|
||||
findSiblingReadList(readListId, bookId, userId, filterOnLibraryIds, next = false)
|
||||
|
||||
override fun findNextInReadList(
|
||||
readListId: String,
|
||||
bookId: String,
|
||||
userId: String,
|
||||
filterOnLibraryIds: Collection<String>?
|
||||
): BookDto? =
|
||||
findSiblingReadList(readListId, bookId, userId, filterOnLibraryIds, next = true)
|
||||
|
||||
|
||||
override fun findOnDeck(libraryIds: Collection<String>, userId: String, pageable: Pageable): Page<BookDto> {
|
||||
|
|
@ -163,7 +182,7 @@ class BookDtoDao(
|
|||
|
||||
private fun readProgressCondition(userId: String): Condition = r.USER_ID.eq(userId).or(r.USER_ID.isNull)
|
||||
|
||||
private fun findSibling(bookId: String, userId: String, next: Boolean): BookDto? {
|
||||
private fun findSiblingSeries(bookId: String, userId: String, next: Boolean): BookDto? {
|
||||
val record = dsl.select(b.SERIES_ID, d.NUMBER_SORT)
|
||||
.from(b)
|
||||
.leftJoin(d).on(b.ID.eq(d.BOOK_ID))
|
||||
|
|
@ -181,6 +200,31 @@ class BookDtoDao(
|
|||
.firstOrNull()
|
||||
}
|
||||
|
||||
private fun findSiblingReadList(
|
||||
readListId: String,
|
||||
bookId: String,
|
||||
userId: String,
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
next: Boolean
|
||||
): BookDto? {
|
||||
val numberSort = dsl.select(rlb.NUMBER)
|
||||
.from(b)
|
||||
.leftJoin(rlb).on(b.ID.eq(rlb.BOOK_ID))
|
||||
.where(b.ID.eq(bookId))
|
||||
.and(rlb.READLIST_ID.eq(readListId))
|
||||
.apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } }
|
||||
.fetchOne(0, Int::class.java)
|
||||
|
||||
return selectBase(userId, JoinConditions(selectReadListNumber = true))
|
||||
.where(rlb.READLIST_ID.eq(readListId))
|
||||
.apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } }
|
||||
.orderBy(rlb.NUMBER.let { if (next) it.asc() else it.desc() })
|
||||
.seek(numberSort)
|
||||
.limit(1)
|
||||
.fetchAndMap()
|
||||
.firstOrNull()
|
||||
}
|
||||
|
||||
private fun selectBase(userId: String, joinConditions: JoinConditions = JoinConditions()) =
|
||||
dsl.selectDistinct(
|
||||
*b.fields(),
|
||||
|
|
|
|||
|
|
@ -73,10 +73,28 @@ class ReadListController(
|
|||
)
|
||||
|
||||
return when {
|
||||
principal.user.sharedAllLibraries && libraryIds == null -> readListRepository.findAll(searchTerm, pageable = pageRequest)
|
||||
principal.user.sharedAllLibraries && libraryIds != null -> readListRepository.findAllByLibraries(libraryIds, null, searchTerm, pageable = pageRequest)
|
||||
!principal.user.sharedAllLibraries && libraryIds != null -> readListRepository.findAllByLibraries(libraryIds, principal.user.sharedLibrariesIds, searchTerm, pageable = pageRequest)
|
||||
else -> readListRepository.findAllByLibraries(principal.user.sharedLibrariesIds, principal.user.sharedLibrariesIds, searchTerm, pageable = pageRequest)
|
||||
principal.user.sharedAllLibraries && libraryIds == null -> readListRepository.findAll(
|
||||
searchTerm,
|
||||
pageable = pageRequest
|
||||
)
|
||||
principal.user.sharedAllLibraries && libraryIds != null -> readListRepository.findAllByLibraries(
|
||||
libraryIds,
|
||||
null,
|
||||
searchTerm,
|
||||
pageable = pageRequest
|
||||
)
|
||||
!principal.user.sharedAllLibraries && libraryIds != null -> readListRepository.findAllByLibraries(
|
||||
libraryIds,
|
||||
principal.user.sharedLibrariesIds,
|
||||
searchTerm,
|
||||
pageable = pageRequest
|
||||
)
|
||||
else -> readListRepository.findAllByLibraries(
|
||||
principal.user.sharedLibrariesIds,
|
||||
principal.user.sharedLibrariesIds,
|
||||
searchTerm,
|
||||
pageable = pageRequest
|
||||
)
|
||||
}.map { it.toDto() }
|
||||
}
|
||||
|
||||
|
|
@ -108,10 +126,12 @@ class ReadListController(
|
|||
@Valid @RequestBody readList: ReadListCreationDto
|
||||
): ReadListDto =
|
||||
try {
|
||||
readListLifecycle.addReadList(ReadList(
|
||||
name = readList.name,
|
||||
bookIds = readList.bookIds.toIndexedMap()
|
||||
)).toDto()
|
||||
readListLifecycle.addReadList(
|
||||
ReadList(
|
||||
name = readList.name,
|
||||
bookIds = readList.bookIds.toIndexedMap()
|
||||
)
|
||||
).toDto()
|
||||
} catch (e: DuplicateNameException) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, e.message)
|
||||
}
|
||||
|
|
@ -174,5 +194,37 @@ class ReadListController(
|
|||
)
|
||||
.map { it.restrictUrl(!principal.user.roleAdmin) }
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
|
||||
@GetMapping("{id}/books/{bookId}/previous")
|
||||
fun getBookSiblingPrevious(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@PathVariable id: String,
|
||||
@PathVariable bookId: String
|
||||
): BookDto =
|
||||
readListRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null))?.let { readList ->
|
||||
bookDtoRepository.findPreviousInReadList(
|
||||
id,
|
||||
bookId,
|
||||
principal.user.id,
|
||||
principal.user.getAuthorizedLibraryIds(null)
|
||||
)
|
||||
?.restrictUrl(!principal.user.roleAdmin)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
|
||||
@GetMapping("{id}/books/{bookId}/next")
|
||||
fun getBookSiblingNext(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@PathVariable id: String,
|
||||
@PathVariable bookId: String
|
||||
): BookDto =
|
||||
readListRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null))?.let { readList ->
|
||||
bookDtoRepository.findNextInReadList(
|
||||
id,
|
||||
bookId,
|
||||
principal.user.id,
|
||||
principal.user.getAuthorizedLibraryIds(null)
|
||||
)
|
||||
?.restrictUrl(!principal.user.roleAdmin)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@ import org.springframework.data.domain.Pageable
|
|||
|
||||
interface BookDtoRepository {
|
||||
fun findAll(search: BookSearchWithReadProgress, userId: String, pageable: Pageable): Page<BookDto>
|
||||
|
||||
/**
|
||||
* Find books that are part of a readlist, optionally filtered by library
|
||||
*/
|
||||
fun findByReadListId(
|
||||
readListId: String,
|
||||
userId: String,
|
||||
|
|
@ -15,7 +19,23 @@ interface BookDtoRepository {
|
|||
): Page<BookDto>
|
||||
|
||||
fun findByIdOrNull(bookId: String, userId: String): BookDto?
|
||||
|
||||
fun findPreviousInSeries(bookId: String, userId: String): BookDto?
|
||||
fun findNextInSeries(bookId: String, userId: String): BookDto?
|
||||
|
||||
fun findPreviousInReadList(
|
||||
readListId: String,
|
||||
bookId: String,
|
||||
userId: String,
|
||||
filterOnLibraryIds: Collection<String>?
|
||||
): BookDto?
|
||||
|
||||
fun findNextInReadList(
|
||||
readListId: String,
|
||||
bookId: String,
|
||||
userId: String,
|
||||
filterOnLibraryIds: Collection<String>?
|
||||
): BookDto?
|
||||
|
||||
fun findOnDeck(libraryIds: Collection<String>, userId: String, pageable: Pageable): Page<BookDto>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -202,6 +202,94 @@ class ReadListControllerTest(
|
|||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class Siblings {
|
||||
@Test
|
||||
@WithMockCustomUser
|
||||
fun `given user with access to all libraries when getting book siblings then it is returned or not found`() {
|
||||
makeReadLists()
|
||||
|
||||
val first = booksLibrary1.first().id // Book_1
|
||||
val second = booksLibrary1[1].id // Book_2
|
||||
val last = booksLibrary2.last().id // Book_10
|
||||
|
||||
mockMvc.get("/api/v1/readlists/${rlLibBoth.id}/books/${first}/previous")
|
||||
.andExpect { status { isNotFound() } }
|
||||
mockMvc.get("/api/v1/readlists/${rlLibBoth.id}/books/${first}/next")
|
||||
.andExpect {
|
||||
status { isOk() }
|
||||
jsonPath("$.name") { value("Book_2") }
|
||||
}
|
||||
|
||||
mockMvc.get("/api/v1/readlists/${rlLibBoth.id}/books/${second}/previous")
|
||||
.andExpect {
|
||||
status { isOk() }
|
||||
jsonPath("$.name") { value("Book_1") }
|
||||
}
|
||||
mockMvc.get("/api/v1/readlists/${rlLibBoth.id}/books/${second}/next")
|
||||
.andExpect {
|
||||
status { isOk() }
|
||||
jsonPath("$.name") { value("Book_3") }
|
||||
}
|
||||
|
||||
mockMvc.get("/api/v1/readlists/${rlLibBoth.id}/books/${last}/previous")
|
||||
.andExpect {
|
||||
status { isOk() }
|
||||
jsonPath("$.name") { value("Book_9") }
|
||||
}
|
||||
mockMvc.get("/api/v1/readlists/${rlLibBoth.id}/books/${last}/next")
|
||||
.andExpect { status { isNotFound() } }
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockCustomUser(sharedAllLibraries = false, sharedLibraries = ["1"])
|
||||
fun `given user with access to a single library when getting book siblings then it takes into account the library filter`() {
|
||||
makeReadLists()
|
||||
|
||||
val first = booksLibrary1.first().id // Book_1
|
||||
val second = booksLibrary1[1].id // Book_2
|
||||
val last = booksLibrary1.last().id // Book_5
|
||||
|
||||
mockMvc.get("/api/v1/readlists/${rlLibBoth.id}/books/${first}/previous")
|
||||
.andExpect { status { isNotFound() } }
|
||||
mockMvc.get("/api/v1/readlists/${rlLibBoth.id}/books/${first}/next")
|
||||
.andExpect {
|
||||
status { isOk() }
|
||||
jsonPath("$.name") { value("Book_2") }
|
||||
}
|
||||
|
||||
mockMvc.get("/api/v1/readlists/${rlLibBoth.id}/books/${second}/previous")
|
||||
.andExpect {
|
||||
status { isOk() }
|
||||
jsonPath("$.name") { value("Book_1") }
|
||||
}
|
||||
mockMvc.get("/api/v1/readlists/${rlLibBoth.id}/books/${second}/next")
|
||||
.andExpect {
|
||||
status { isOk() }
|
||||
jsonPath("$.name") { value("Book_3") }
|
||||
}
|
||||
|
||||
mockMvc.get("/api/v1/readlists/${rlLibBoth.id}/books/${last}/previous")
|
||||
.andExpect {
|
||||
status { isOk() }
|
||||
jsonPath("$.name") { value("Book_4") }
|
||||
}
|
||||
mockMvc.get("/api/v1/readlists/${rlLibBoth.id}/books/${last}/next")
|
||||
.andExpect { status { isNotFound() } }
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockCustomUser(sharedAllLibraries = false, sharedLibraries = ["1"])
|
||||
fun `given user with access to a single library when getting books from single read list from another library then return not found`() {
|
||||
makeReadLists()
|
||||
|
||||
mockMvc.get("/api/v1/readlists/${rlLib2.id}/books")
|
||||
.andExpect {
|
||||
status { isNotFound() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class Creation {
|
||||
@Test
|
||||
|
|
|
|||
Loading…
Reference in a new issue