mirror of
https://github.com/gotson/komga.git
synced 2025-01-09 04:08:00 +08:00
feat(opds): do not show soft deleted books/series
This commit is contained in:
parent
d946600a64
commit
5b6b817085
@ -4,7 +4,8 @@ open class BookSearch(
|
||||
val libraryIds: Collection<String>? = null,
|
||||
val seriesIds: Collection<String>? = null,
|
||||
val searchTerm: String? = null,
|
||||
val mediaStatus: Collection<Media.Status>? = null
|
||||
val mediaStatus: Collection<Media.Status>? = null,
|
||||
val deleted: Boolean? = null,
|
||||
)
|
||||
|
||||
class BookSearchWithReadProgress(
|
||||
|
@ -5,7 +5,8 @@ open class SeriesSearch(
|
||||
val collectionIds: Collection<String>? = null,
|
||||
val searchTerm: String? = null,
|
||||
val metadataStatus: Collection<SeriesMetadata.Status>? = null,
|
||||
val publishers: Collection<String>? = null
|
||||
val publishers: Collection<String>? = null,
|
||||
val deleted: Boolean? = null,
|
||||
)
|
||||
|
||||
class SeriesSearchWithReadProgress(
|
||||
|
@ -271,6 +271,8 @@ class BookDao(
|
||||
if (!seriesIds.isNullOrEmpty()) c = c.and(b.SERIES_ID.`in`(seriesIds))
|
||||
searchTerm?.let { c = c.and(d.TITLE.containsIgnoreCase(it)) }
|
||||
if (!mediaStatus.isNullOrEmpty()) c = c.and(m.STATUS.`in`(mediaStatus))
|
||||
if (deleted == true) c = c.and(b.DELETED_DATE.isNotNull)
|
||||
if (deleted == false) c = c.and(b.DELETED_DATE.isNull)
|
||||
|
||||
return c
|
||||
}
|
||||
|
@ -129,6 +129,8 @@ class SeriesDao(
|
||||
searchTerm?.let { c = c.and(d.TITLE.containsIgnoreCase(it)) }
|
||||
if (!metadataStatus.isNullOrEmpty()) c = c.and(d.STATUS.`in`(metadataStatus))
|
||||
if (!publishers.isNullOrEmpty()) c = c.and(DSL.lower(d.PUBLISHER).`in`(publishers.map { it.lowercase() }))
|
||||
if (deleted == true) c = c.and(s.DELETED_DATE.isNotNull)
|
||||
if (deleted == false) c = c.and(s.DELETED_DATE.isNull)
|
||||
|
||||
return c
|
||||
}
|
||||
|
@ -189,7 +189,8 @@ class OpdsController(
|
||||
val seriesSearch = SeriesSearch(
|
||||
libraryIds = principal.user.getAuthorizedLibraryIds(null),
|
||||
searchTerm = searchTerm,
|
||||
publishers = publishers
|
||||
publishers = publishers,
|
||||
deleted = false,
|
||||
)
|
||||
|
||||
val entries = seriesRepository.findAll(seriesSearch)
|
||||
@ -215,7 +216,8 @@ class OpdsController(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal
|
||||
): OpdsFeed {
|
||||
val seriesSearch = SeriesSearch(
|
||||
libraryIds = principal.user.getAuthorizedLibraryIds(null)
|
||||
libraryIds = principal.user.getAuthorizedLibraryIds(null),
|
||||
deleted = false,
|
||||
)
|
||||
|
||||
val entries = seriesRepository.findAll(seriesSearch)
|
||||
@ -243,7 +245,8 @@ class OpdsController(
|
||||
): OpdsFeed {
|
||||
val bookSearch = BookSearch(
|
||||
libraryIds = principal.user.getAuthorizedLibraryIds(null),
|
||||
mediaStatus = setOf(Media.Status.READY)
|
||||
mediaStatus = setOf(Media.Status.READY),
|
||||
deleted = false,
|
||||
)
|
||||
val pageRequest = PageRequest.of(0, 50, Sort.by(Sort.Order.desc("createdDate")))
|
||||
|
||||
@ -376,7 +379,8 @@ class OpdsController(
|
||||
val books = bookRepository.findAll(
|
||||
BookSearch(
|
||||
seriesIds = listOf(id),
|
||||
mediaStatus = setOf(Media.Status.READY)
|
||||
mediaStatus = setOf(Media.Status.READY),
|
||||
deleted = false,
|
||||
)
|
||||
)
|
||||
val metadata = seriesMetadataRepository.findById(series.id)
|
||||
@ -407,7 +411,10 @@ class OpdsController(
|
||||
libraryRepository.findByIdOrNull(id)?.let { library ->
|
||||
if (!principal.user.canAccessLibrary(library)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
|
||||
val seriesSearch = SeriesSearch(libraryIds = setOf(library.id))
|
||||
val seriesSearch = SeriesSearch(
|
||||
libraryIds = setOf(library.id),
|
||||
deleted = false,
|
||||
)
|
||||
|
||||
val entries = seriesRepository.findAll(seriesSearch)
|
||||
.map { SeriesWithInfo(it, seriesMetadataRepository.findById(it.id)) }
|
||||
@ -434,6 +441,7 @@ class OpdsController(
|
||||
): OpdsFeed {
|
||||
return collectionRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null))?.let { collection ->
|
||||
val series = collection.seriesIds.mapNotNull { seriesRepository.findByIdOrNull(it) }
|
||||
.filterNot { it.deletedDate != null }
|
||||
.map { SeriesWithInfo(it, seriesMetadataRepository.findById(it.id)) }
|
||||
|
||||
val sorted =
|
||||
@ -465,6 +473,7 @@ class OpdsController(
|
||||
): OpdsFeed {
|
||||
return readListRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null))?.let { readList ->
|
||||
val books = readList.bookIds.values.mapNotNull { bookRepository.findByIdOrNull(it) }
|
||||
.filterNot { it.deletedDate != null }
|
||||
.map { BookWithInfo(it, mediaRepository.findById(it.id), bookMetadataRepository.findById(it.id)) }
|
||||
|
||||
val entries = books.mapIndexed { index, it ->
|
||||
|
@ -0,0 +1,260 @@
|
||||
package org.gotson.komga.interfaces.opds
|
||||
|
||||
import org.gotson.komga.domain.model.BookPage
|
||||
import org.gotson.komga.domain.model.KomgaUser
|
||||
import org.gotson.komga.domain.model.Media
|
||||
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.MediaRepository
|
||||
import org.gotson.komga.domain.persistence.SeriesMetadataRepository
|
||||
import org.gotson.komga.domain.persistence.SeriesRepository
|
||||
import org.gotson.komga.domain.service.KomgaUserLifecycle
|
||||
import org.gotson.komga.domain.service.LibraryLifecycle
|
||||
import org.gotson.komga.domain.service.SeriesLifecycle
|
||||
import org.gotson.komga.interfaces.rest.WithMockCustomUser
|
||||
import org.hamcrest.Matchers
|
||||
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
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||
import org.springframework.test.web.servlet.MockMvc
|
||||
import org.springframework.test.web.servlet.get
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@ExtendWith(SpringExtension::class)
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc(printOnlyOnFailure = false)
|
||||
class OpdsControllerTest(
|
||||
@Autowired private val seriesRepository: SeriesRepository,
|
||||
@Autowired private val seriesLifecycle: SeriesLifecycle,
|
||||
@Autowired private val seriesMetadataRepository: SeriesMetadataRepository,
|
||||
@Autowired private val libraryRepository: LibraryRepository,
|
||||
@Autowired private val libraryLifecycle: LibraryLifecycle,
|
||||
@Autowired private val bookRepository: BookRepository,
|
||||
@Autowired private val mediaRepository: MediaRepository,
|
||||
@Autowired private val userRepository: KomgaUserRepository,
|
||||
@Autowired private val userLifecycle: KomgaUserLifecycle,
|
||||
@Autowired private val mockMvc: MockMvc
|
||||
) {
|
||||
|
||||
private val library = makeLibrary(id = "1")
|
||||
private val user = KomgaUser("user@example.org", "", false, id = "1")
|
||||
private val user2 = KomgaUser("user2@example.org", "", false, id = "2")
|
||||
|
||||
@BeforeAll
|
||||
fun `setup library`() {
|
||||
libraryRepository.insert(library)
|
||||
userRepository.insert(user)
|
||||
userRepository.insert(user2)
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun teardown() {
|
||||
userRepository.findAll().forEach {
|
||||
userLifecycle.deleteUser(it)
|
||||
}
|
||||
libraryRepository.findAll().forEach {
|
||||
libraryLifecycle.deleteLibrary(it)
|
||||
}
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun `clear repository`() {
|
||||
seriesLifecycle.deleteMany(seriesRepository.findAll())
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class LimitedUser {
|
||||
@Test
|
||||
@WithMockCustomUser(sharedAllLibraries = false, sharedLibraries = ["1"])
|
||||
fun `given user with access to a single library when getting series then only gets series from this library`() {
|
||||
val createdSeries = makeSeries(name = "series", libraryId = library.id).also { series ->
|
||||
seriesLifecycle.createSeries(series).let { created ->
|
||||
val books = listOf(makeBook("1", libraryId = library.id))
|
||||
seriesLifecycle.addBooks(created, books)
|
||||
}
|
||||
}
|
||||
|
||||
val otherLibrary = makeLibrary("other")
|
||||
libraryRepository.insert(otherLibrary)
|
||||
makeSeries(name = "otherSeries", libraryId = otherLibrary.id).let { series ->
|
||||
seriesLifecycle.createSeries(series).let { created ->
|
||||
val otherBooks = listOf(makeBook("2", libraryId = otherLibrary.id))
|
||||
seriesLifecycle.addBooks(created, otherBooks)
|
||||
}
|
||||
}
|
||||
|
||||
mockMvc.get("/opds/v1.2/series")
|
||||
.andExpect {
|
||||
status { isOk() }
|
||||
xpath("/feed/entry/id") {
|
||||
nodeCount(1)
|
||||
string(createdSeries.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class SeriesSort {
|
||||
@Test
|
||||
@WithMockCustomUser
|
||||
fun `given series with titleSort when requesting via opds then series are sorted by titleSort`() {
|
||||
val alphaC = seriesLifecycle.createSeries(makeSeries("TheAlpha", libraryId = library.id))
|
||||
seriesMetadataRepository.findById(alphaC.id).let {
|
||||
seriesMetadataRepository.update(it.copy(titleSort = "Alpha, The"))
|
||||
}
|
||||
seriesLifecycle.createSeries(makeSeries("Beta", libraryId = library.id))
|
||||
|
||||
mockMvc.get("/opds/v1.2/series")
|
||||
.andExpect {
|
||||
status { isOk() }
|
||||
xpath("/feed/entry[1]/title") { string("TheAlpha") }
|
||||
xpath("/feed/entry[2]/title") { string("Beta") }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockCustomUser
|
||||
fun `given series when requesting via opds then series are sorted insensitive of case`() {
|
||||
listOf("a", "b", "B", "C")
|
||||
.map { name -> makeSeries(name, libraryId = library.id) }
|
||||
.forEach {
|
||||
seriesLifecycle.createSeries(it)
|
||||
}
|
||||
|
||||
mockMvc.get("/opds/v1.2/series")
|
||||
.andExpect {
|
||||
status { isOk() }
|
||||
xpath("/feed/entry[1]/title") { string("a") }
|
||||
xpath("/feed/entry[2]/title") { string(Matchers.equalToIgnoringCase("b")) }
|
||||
xpath("/feed/entry[3]/title") { string(Matchers.equalToIgnoringCase("b")) }
|
||||
xpath("/feed/entry[4]/title") { string("C") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class SeriesStatus {
|
||||
@Test
|
||||
@WithMockCustomUser
|
||||
fun `given series when requesting via opds then deleted series are not returned`() {
|
||||
seriesLifecycle.createSeries(makeSeries("Alpha", libraryId = library.id)).also {
|
||||
seriesRepository.update(it.copy(deletedDate = LocalDateTime.now()))
|
||||
}
|
||||
seriesLifecycle.createSeries(makeSeries("Beta", libraryId = library.id))
|
||||
|
||||
mockMvc.get("/opds/v1.2/series")
|
||||
.andExpect {
|
||||
status { isOk() }
|
||||
xpath("/feed/entry") { nodeCount(1) }
|
||||
xpath("/feed/entry[1]/title") { string("Beta") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class BookOrdering {
|
||||
@Test
|
||||
@WithMockCustomUser
|
||||
fun `given books with unordered index when requesting via opds then books are ordered`() {
|
||||
val createdSeries = makeSeries(name = "series", libraryId = library.id).let { series ->
|
||||
seriesLifecycle.createSeries(series).also { created ->
|
||||
val books = listOf(makeBook("1", libraryId = library.id), makeBook("3", libraryId = library.id))
|
||||
seriesLifecycle.addBooks(created, books)
|
||||
}
|
||||
}
|
||||
|
||||
val addedBook = makeBook("2", libraryId = library.id)
|
||||
seriesLifecycle.addBooks(createdSeries, listOf(addedBook))
|
||||
seriesLifecycle.sortBooks(createdSeries)
|
||||
|
||||
bookRepository.findAll().forEach {
|
||||
mediaRepository.findById(it.id).let { media ->
|
||||
mediaRepository.update(media.copy(status = Media.Status.READY, pages = listOf(BookPage("1.jpg", "image/jpeg"))))
|
||||
}
|
||||
}
|
||||
|
||||
mockMvc.get("/opds/v1.2/series/${createdSeries.id}")
|
||||
.andExpect {
|
||||
status { isOk() }
|
||||
xpath("/feed/entry[1]/title") { string("1") }
|
||||
xpath("/feed/entry[2]/title") { string("2") }
|
||||
xpath("/feed/entry[3]/title") { string("3") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class BookStatus {
|
||||
@Test
|
||||
@WithMockCustomUser
|
||||
fun `given books not ready when requesting via opds then no books are returned`() {
|
||||
val createdSeries = makeSeries(name = "series", libraryId = library.id).let { series ->
|
||||
seriesLifecycle.createSeries(series).also { created ->
|
||||
val books = listOf(makeBook("1", libraryId = library.id), makeBook("3", libraryId = library.id))
|
||||
seriesLifecycle.addBooks(created, books)
|
||||
}
|
||||
}
|
||||
|
||||
bookRepository.findAll().forEach {
|
||||
mediaRepository.findById(it.id).let { media ->
|
||||
mediaRepository.update(media.copy(status = Media.Status.READY, pages = listOf(BookPage("1.jpg", "image/jpeg"))))
|
||||
}
|
||||
}
|
||||
|
||||
val addedBook = makeBook("2", libraryId = library.id)
|
||||
seriesLifecycle.addBooks(createdSeries, listOf(addedBook))
|
||||
seriesLifecycle.sortBooks(createdSeries)
|
||||
|
||||
mockMvc.get("/opds/v1.2/series/${createdSeries.id}")
|
||||
.andExpect {
|
||||
status { isOk() }
|
||||
xpath("/feed/entry") { nodeCount(2) }
|
||||
xpath("/feed/entry[1]/title") { string("1") }
|
||||
xpath("/feed/entry[2]/title") { string("3") }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockCustomUser
|
||||
fun `given deleted ready books when requesting via opds then no books are returned`() {
|
||||
val createdSeries = makeSeries(name = "series", libraryId = library.id).let { series ->
|
||||
seriesLifecycle.createSeries(series).also { created ->
|
||||
val books = listOf(makeBook("1", libraryId = library.id), makeBook("3", libraryId = library.id))
|
||||
seriesLifecycle.addBooks(created, books)
|
||||
}
|
||||
}
|
||||
|
||||
val addedBook = makeBook("2", libraryId = library.id)
|
||||
seriesLifecycle.addBooks(createdSeries, listOf(addedBook))
|
||||
seriesLifecycle.sortBooks(createdSeries)
|
||||
|
||||
bookRepository.findAll().forEach {
|
||||
mediaRepository.findById(it.id).let { media ->
|
||||
mediaRepository.update(media.copy(status = Media.Status.READY, pages = listOf(BookPage("1.jpg", "image/jpeg"))))
|
||||
}
|
||||
if (it.id == addedBook.id)
|
||||
bookRepository.update(it.copy(deletedDate = LocalDateTime.now()))
|
||||
}
|
||||
|
||||
mockMvc.get("/opds/v1.2/series/${createdSeries.id}")
|
||||
.andExpect {
|
||||
status { isOk() }
|
||||
xpath("/feed/entry") { nodeCount(2) }
|
||||
xpath("/feed/entry[1]/title") { string("1") }
|
||||
xpath("/feed/entry[2]/title") { string("3") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user