mirror of
https://github.com/gotson/komga.git
synced 2026-05-04 18:42:10 +02:00
feat(api): add new series list API using search condition criteria DSL
add book search condition criteria DSL
This commit is contained in:
parent
7fa42f5899
commit
3bfc7981e5
54 changed files with 4276 additions and 953 deletions
|
|
@ -4,10 +4,12 @@ import io.github.oshai.kotlinlogging.KotlinLogging
|
|||
import org.gotson.komga.domain.model.Book
|
||||
import org.gotson.komga.domain.model.BookMetadataPatchCapability
|
||||
import org.gotson.komga.domain.model.BookPageNumbered
|
||||
import org.gotson.komga.domain.model.BookSearch
|
||||
import org.gotson.komga.domain.model.CopyMode
|
||||
import org.gotson.komga.domain.model.Library
|
||||
import org.gotson.komga.domain.model.Media
|
||||
import org.gotson.komga.domain.model.SearchCondition
|
||||
import org.gotson.komga.domain.model.SearchContext
|
||||
import org.gotson.komga.domain.model.SearchOperator
|
||||
import org.gotson.komga.domain.persistence.BookRepository
|
||||
import org.gotson.komga.domain.service.BookConverter
|
||||
import org.gotson.komga.infrastructure.jooq.UnpagedSorted
|
||||
|
|
@ -43,10 +45,14 @@ class TaskEmitter(
|
|||
fun analyzeUnknownAndOutdatedBooks(library: Library) {
|
||||
bookRepository
|
||||
.findAll(
|
||||
BookSearch(
|
||||
libraryIds = listOf(library.id),
|
||||
mediaStatus = listOf(Media.Status.UNKNOWN, Media.Status.OUTDATED),
|
||||
SearchCondition.AllOfBook(
|
||||
SearchCondition.LibraryId(SearchOperator.Is(library.id)),
|
||||
SearchCondition.AnyOfBook(
|
||||
SearchCondition.MediaStatus(SearchOperator.Is(Media.Status.UNKNOWN)),
|
||||
SearchCondition.MediaStatus(SearchOperator.Is(Media.Status.OUTDATED)),
|
||||
),
|
||||
),
|
||||
SearchContext.empty(),
|
||||
UnpagedSorted(Sort.by(Sort.Order.asc("seriesId"), Sort.Order.asc("number"))),
|
||||
)
|
||||
.content
|
||||
|
|
|
|||
|
|
@ -1,34 +1,9 @@
|
|||
package org.gotson.komga.domain.model
|
||||
|
||||
import java.time.LocalDate
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
|
||||
open class BookSearch(
|
||||
val libraryIds: Collection<String>? = null,
|
||||
val seriesIds: Collection<String>? = null,
|
||||
val searchTerm: String? = null,
|
||||
val mediaStatus: Collection<Media.Status>? = null,
|
||||
val mediaProfile: Collection<MediaProfile>? = null,
|
||||
val deleted: Boolean? = null,
|
||||
val releasedAfter: LocalDate? = null,
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
data class BookSearch(
|
||||
val condition: SearchCondition.Book? = null,
|
||||
val fullTextSearch: String? = null,
|
||||
)
|
||||
|
||||
class BookSearchWithReadProgress(
|
||||
libraryIds: Collection<String>? = null,
|
||||
seriesIds: Collection<String>? = null,
|
||||
searchTerm: String? = null,
|
||||
mediaStatus: Collection<Media.Status>? = null,
|
||||
mediaProfile: Collection<MediaProfile>? = null,
|
||||
deleted: Boolean? = null,
|
||||
releasedAfter: LocalDate? = null,
|
||||
val tags: Collection<String>? = null,
|
||||
val readStatus: Collection<ReadStatus>? = null,
|
||||
val authors: Collection<Author>? = null,
|
||||
) : BookSearch(
|
||||
libraryIds = libraryIds,
|
||||
seriesIds = seriesIds,
|
||||
searchTerm = searchTerm,
|
||||
mediaStatus = mediaStatus,
|
||||
mediaProfile = mediaProfile,
|
||||
deleted = deleted,
|
||||
releasedAfter = releasedAfter,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ data class KomgaUser(
|
|||
|
||||
/**
|
||||
* Return the list of LibraryIds this user is authorized to view, intersecting the provided list of LibraryIds.
|
||||
*
|
||||
* @param libraryIds an optional list of LibraryIds to filter on
|
||||
* @return a list of authorised LibraryIds, or null if the user is authorized to see all libraries
|
||||
*/
|
||||
fun getAuthorizedLibraryIds(libraryIds: Collection<String>?): Collection<String>? =
|
||||
|
|
|
|||
|
|
@ -0,0 +1,157 @@
|
|||
package org.gotson.komga.domain.model
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo
|
||||
|
||||
class SearchCondition {
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
|
||||
sealed interface Book
|
||||
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
|
||||
sealed interface Series
|
||||
|
||||
data class AnyOfBook(
|
||||
@JsonProperty("anyOf")
|
||||
val conditions: List<Book>,
|
||||
) : Book {
|
||||
constructor(vararg args: Book) : this(args.toList())
|
||||
}
|
||||
|
||||
data class AllOfBook(
|
||||
@JsonProperty("allOf")
|
||||
val conditions: List<Book>,
|
||||
) : Book {
|
||||
constructor(vararg args: Book) : this(args.toList())
|
||||
}
|
||||
|
||||
data class AnyOfSeries(
|
||||
@JsonProperty("anyOf")
|
||||
val conditions: List<Series>,
|
||||
) : Series {
|
||||
constructor(vararg args: Series) : this(args.toList())
|
||||
}
|
||||
|
||||
data class AllOfSeries(
|
||||
@JsonProperty("allOf")
|
||||
val conditions: List<Series>,
|
||||
) : Series {
|
||||
constructor(vararg args: Series) : this(args.toList())
|
||||
}
|
||||
|
||||
data class LibraryId(
|
||||
@JsonProperty("libraryId")
|
||||
val operator: SearchOperator.Equality<String>,
|
||||
) : Book, Series
|
||||
|
||||
data class CollectionId(
|
||||
@JsonProperty("collectionId")
|
||||
val operator: SearchOperator.Equality<String>,
|
||||
) : Series
|
||||
|
||||
data class ReadListId(
|
||||
@JsonProperty("readListId")
|
||||
val operator: SearchOperator.Equality<String>,
|
||||
) : Book
|
||||
|
||||
data class SeriesId(
|
||||
@JsonProperty("seriesId")
|
||||
val operator: SearchOperator.Equality<String>,
|
||||
) : Book
|
||||
|
||||
data class Deleted(
|
||||
@JsonProperty("deleted")
|
||||
val operator: SearchOperator.Boolean,
|
||||
) : Book, Series
|
||||
|
||||
data class Complete(
|
||||
@JsonProperty("complete")
|
||||
val operator: SearchOperator.Boolean,
|
||||
) : Series
|
||||
|
||||
data class OneShot(
|
||||
@JsonProperty("oneShot")
|
||||
val operator: SearchOperator.Boolean,
|
||||
) : Book, Series
|
||||
|
||||
data class Title(
|
||||
@JsonProperty("title")
|
||||
val operator: SearchOperator.StringOp,
|
||||
) : Book, Series
|
||||
|
||||
data class TitleSort(
|
||||
@JsonProperty("titleSort")
|
||||
val operator: SearchOperator.StringOp,
|
||||
) : Series
|
||||
|
||||
data class ReleaseDate(
|
||||
@JsonProperty("releaseDate")
|
||||
val operator: SearchOperator.Date,
|
||||
) : Book, Series
|
||||
|
||||
data class NumberSort(
|
||||
@JsonProperty("numberSort")
|
||||
val operator: SearchOperator.Numeric<Float>,
|
||||
) : Book
|
||||
|
||||
data class Tag(
|
||||
@JsonProperty("tag")
|
||||
val operator: SearchOperator.Equality<String>,
|
||||
) : Book, Series
|
||||
|
||||
data class SharingLabel(
|
||||
@JsonProperty("sharingLabel")
|
||||
val operator: SearchOperator.Equality<String>,
|
||||
) : Series
|
||||
|
||||
data class Publisher(
|
||||
@JsonProperty("publisher")
|
||||
val operator: SearchOperator.Equality<String>,
|
||||
) : Series
|
||||
|
||||
data class Language(
|
||||
@JsonProperty("language")
|
||||
val operator: SearchOperator.Equality<String>,
|
||||
) : Series
|
||||
|
||||
data class Genre(
|
||||
@JsonProperty("genre")
|
||||
val operator: SearchOperator.Equality<String>,
|
||||
) : Series
|
||||
|
||||
data class AgeRating(
|
||||
@JsonProperty("ageRating")
|
||||
val operator: SearchOperator.NumericNullable<Int>,
|
||||
) : Series
|
||||
|
||||
data class ReadStatus(
|
||||
@JsonProperty("readStatus")
|
||||
val operator: SearchOperator.Equality<org.gotson.komga.domain.model.ReadStatus>,
|
||||
) : Book, Series
|
||||
|
||||
data class MediaStatus(
|
||||
@JsonProperty("mediaStatus")
|
||||
val operator: SearchOperator.Equality<Media.Status>,
|
||||
) : Book
|
||||
|
||||
data class SeriesStatus(
|
||||
@JsonProperty("seriesStatus")
|
||||
val operator: SearchOperator.Equality<SeriesMetadata.Status>,
|
||||
) : Series
|
||||
|
||||
data class MediaProfile(
|
||||
@JsonProperty("mediaProfile")
|
||||
val operator: SearchOperator.Equality<org.gotson.komga.domain.model.MediaProfile>,
|
||||
) : Book
|
||||
|
||||
data class Author(
|
||||
@JsonProperty("author")
|
||||
val operator: SearchOperator.Equality<AuthorMatch>,
|
||||
) : Book, Series
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
data class AuthorMatch(
|
||||
val name: String? = null,
|
||||
val role: String? = null,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package org.gotson.komga.domain.model
|
||||
|
||||
class SearchContext private constructor(val userId: String?, val restrictions: ContentRestrictions, val libraryIds: Collection<String>?) {
|
||||
constructor(user: KomgaUser?) : this(user?.id, user?.restrictions ?: ContentRestrictions(), user?.getAuthorizedLibraryIds(null))
|
||||
|
||||
companion object {
|
||||
fun empty() = SearchContext(null)
|
||||
|
||||
fun ofAnonymousUser() = SearchContext("UNUSED", ContentRestrictions(), null)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package org.gotson.komga.domain.model
|
||||
|
||||
@Deprecated("use SearchOperator.BeginsWith instead")
|
||||
enum class SearchField {
|
||||
TITLE,
|
||||
TITLE_SORT,
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
package org.gotson.komga.domain.model
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo
|
||||
import com.fasterxml.jackson.annotation.JsonTypeName
|
||||
import java.time.Duration
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
class SearchOperator {
|
||||
@JsonTypeInfo(
|
||||
use = JsonTypeInfo.Id.NAME,
|
||||
include = JsonTypeInfo.As.PROPERTY,
|
||||
property = "operator",
|
||||
)
|
||||
sealed interface Equality<T>
|
||||
|
||||
@JsonTypeInfo(
|
||||
use = JsonTypeInfo.Id.NAME,
|
||||
include = JsonTypeInfo.As.PROPERTY,
|
||||
property = "operator",
|
||||
)
|
||||
sealed interface StringOp
|
||||
|
||||
@JsonTypeInfo(
|
||||
use = JsonTypeInfo.Id.NAME,
|
||||
include = JsonTypeInfo.As.PROPERTY,
|
||||
property = "operator",
|
||||
)
|
||||
sealed interface Numeric<T>
|
||||
|
||||
@JsonTypeInfo(
|
||||
use = JsonTypeInfo.Id.NAME,
|
||||
include = JsonTypeInfo.As.PROPERTY,
|
||||
property = "operator",
|
||||
)
|
||||
sealed interface NumericNullable<T>
|
||||
|
||||
@JsonTypeInfo(
|
||||
use = JsonTypeInfo.Id.NAME,
|
||||
include = JsonTypeInfo.As.PROPERTY,
|
||||
property = "operator",
|
||||
)
|
||||
sealed interface Date
|
||||
|
||||
@JsonTypeInfo(
|
||||
use = JsonTypeInfo.Id.NAME,
|
||||
include = JsonTypeInfo.As.PROPERTY,
|
||||
property = "operator",
|
||||
)
|
||||
sealed interface Boolean
|
||||
|
||||
@JsonTypeName("is")
|
||||
data class Is<T>(val value: T) : Equality<T>, StringOp, Numeric<T>, NumericNullable<T>
|
||||
|
||||
@JsonTypeName("isNot")
|
||||
data class IsNot<T>(val value: T) : Equality<T>, StringOp, Numeric<T>, NumericNullable<T>
|
||||
|
||||
@JsonTypeName("contains")
|
||||
data class Contains(val value: String) : StringOp
|
||||
|
||||
@JsonTypeName("doesNotContain")
|
||||
data class DoesNotContain(val value: String) : StringOp
|
||||
|
||||
@JsonTypeName("beginsWith")
|
||||
data class BeginsWith(val value: String) : StringOp
|
||||
|
||||
@JsonTypeName("doesNotBeginWith")
|
||||
data class DoesNotBeginWith(val value: String) : StringOp
|
||||
|
||||
@JsonTypeName("endsWith")
|
||||
data class EndsWith(val value: String) : StringOp
|
||||
|
||||
@JsonTypeName("doesNotEndWith")
|
||||
data class DoesNotEndWith(val value: String) : StringOp
|
||||
|
||||
@JsonTypeName("greaterThan")
|
||||
data class GreaterThan<T>(val value: T) : Numeric<T>, NumericNullable<T>
|
||||
|
||||
@JsonTypeName("lessThan")
|
||||
data class LessThan<T>(val value: T) : Numeric<T>, NumericNullable<T>
|
||||
|
||||
@JsonTypeName("before")
|
||||
data class Before(val dateTime: ZonedDateTime) : Date
|
||||
|
||||
@JsonTypeName("after")
|
||||
data class After(val dateTime: ZonedDateTime) : Date
|
||||
|
||||
@JsonTypeName("isInTheLast")
|
||||
data class IsInTheLast(val duration: Duration) : Date
|
||||
|
||||
@JsonTypeName("isNotInTheLast")
|
||||
data class IsNotInTheLast(val duration: Duration) : Date
|
||||
|
||||
@JsonTypeName("isTrue")
|
||||
data object IsTrue : Boolean
|
||||
|
||||
@JsonTypeName("isFalse")
|
||||
data object IsFalse : Boolean
|
||||
|
||||
@JsonTypeName("isNull")
|
||||
data object IsNull : Date
|
||||
|
||||
@JsonTypeName("isNotNull")
|
||||
data object IsNotNull : Date
|
||||
|
||||
@JsonTypeName("isNull")
|
||||
class IsNullT<T> : NumericNullable<T> {
|
||||
override fun equals(other: Any?): kotlin.Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return javaClass.hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
@JsonTypeName("isNotNull")
|
||||
class IsNotNullT<T> : NumericNullable<T> {
|
||||
override fun equals(other: Any?): kotlin.Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return javaClass.hashCode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +1,13 @@
|
|||
package org.gotson.komga.domain.model
|
||||
|
||||
open class SeriesSearch(
|
||||
val libraryIds: Collection<String>? = null,
|
||||
val collectionIds: Collection<String>? = null,
|
||||
val searchTerm: String? = null,
|
||||
val searchRegex: Pair<String, SearchField>? = null,
|
||||
val metadataStatus: Collection<SeriesMetadata.Status>? = null,
|
||||
val publishers: Collection<String>? = null,
|
||||
val deleted: Boolean? = null,
|
||||
val complete: Boolean? = null,
|
||||
val oneshot: Boolean? = null,
|
||||
) {
|
||||
enum class SearchField {
|
||||
NAME,
|
||||
TITLE,
|
||||
TITLE_SORT,
|
||||
}
|
||||
}
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
|
||||
class SeriesSearchWithReadProgress(
|
||||
libraryIds: Collection<String>? = null,
|
||||
collectionIds: Collection<String>? = null,
|
||||
searchTerm: String? = null,
|
||||
searchRegex: Pair<String, SearchField>? = null,
|
||||
metadataStatus: Collection<SeriesMetadata.Status>? = null,
|
||||
publishers: Collection<String>? = null,
|
||||
deleted: Boolean? = null,
|
||||
complete: Boolean? = null,
|
||||
oneshot: Boolean? = null,
|
||||
val languages: Collection<String>? = null,
|
||||
val genres: Collection<String>? = null,
|
||||
val tags: Collection<String>? = null,
|
||||
val ageRatings: Collection<Int?>? = null,
|
||||
val releaseYears: Collection<String>? = null,
|
||||
val readStatus: Collection<ReadStatus>? = null,
|
||||
val authors: Collection<Author>? = null,
|
||||
val sharingLabels: Collection<String>? = null,
|
||||
) : SeriesSearch(
|
||||
libraryIds = libraryIds,
|
||||
collectionIds = collectionIds,
|
||||
searchTerm = searchTerm,
|
||||
searchRegex = searchRegex,
|
||||
metadataStatus = metadataStatus,
|
||||
publishers = publishers,
|
||||
deleted = deleted,
|
||||
complete = complete,
|
||||
oneshot = oneshot,
|
||||
)
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
data class SeriesSearch(
|
||||
val condition: SearchCondition.Series? = null,
|
||||
val fullTextSearch: String? = null,
|
||||
@JsonIgnore
|
||||
@Deprecated("Used for backward compatibility only, use SearchOperator.BeginsWith instead")
|
||||
val regexSearch: Pair<String, SearchField>? = null,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
package org.gotson.komga.domain.persistence
|
||||
|
||||
import org.gotson.komga.domain.model.Book
|
||||
import org.gotson.komga.domain.model.BookSearch
|
||||
import org.gotson.komga.domain.model.SearchCondition
|
||||
import org.gotson.komga.domain.model.SearchContext
|
||||
import org.springframework.data.domain.Page
|
||||
import org.springframework.data.domain.Pageable
|
||||
import org.springframework.data.domain.Sort
|
||||
import java.math.BigDecimal
|
||||
import java.net.URL
|
||||
|
||||
|
|
@ -27,10 +27,9 @@ interface BookRepository {
|
|||
urls: Collection<URL>,
|
||||
): Collection<Book>
|
||||
|
||||
fun findAll(bookSearch: BookSearch): Collection<Book>
|
||||
|
||||
fun findAll(
|
||||
bookSearch: BookSearch,
|
||||
searchCondition: SearchCondition.Book?,
|
||||
searchContext: SearchContext,
|
||||
pageable: Pageable,
|
||||
): Page<Book>
|
||||
|
||||
|
|
@ -64,15 +63,8 @@ interface BookRepository {
|
|||
|
||||
fun findAllIdsBySeriesId(seriesId: String): Collection<String>
|
||||
|
||||
fun findAllIdsBySeriesIds(seriesIds: Collection<String>): Collection<String>
|
||||
|
||||
fun findAllIdsByLibraryId(libraryId: String): Collection<String>
|
||||
|
||||
fun findAllIds(
|
||||
bookSearch: BookSearch,
|
||||
sort: Sort,
|
||||
): Collection<String>
|
||||
|
||||
fun existsById(bookId: String): Boolean
|
||||
|
||||
fun insert(book: Book)
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ interface MediaRepository {
|
|||
|
||||
fun delete(bookId: String)
|
||||
|
||||
fun deleteByBookIds(bookIds: Collection<String>)
|
||||
fun delete(bookIds: Collection<String>)
|
||||
|
||||
fun count(): Long
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
package org.gotson.komga.domain.persistence
|
||||
|
||||
import org.gotson.komga.domain.model.SearchCondition
|
||||
import org.gotson.komga.domain.model.SearchContext
|
||||
import org.gotson.komga.domain.model.Series
|
||||
import org.gotson.komga.domain.model.SeriesSearch
|
||||
import org.springframework.data.domain.Page
|
||||
import org.springframework.data.domain.Pageable
|
||||
import java.net.URL
|
||||
|
||||
interface SeriesRepository {
|
||||
|
|
@ -23,7 +26,11 @@ interface SeriesRepository {
|
|||
|
||||
fun findAllByTitleContaining(title: String): Collection<Series>
|
||||
|
||||
fun findAll(search: SeriesSearch): Collection<Series>
|
||||
fun findAll(
|
||||
searchCondition: SearchCondition.Series?,
|
||||
searchContext: SearchContext,
|
||||
pageable: Pageable,
|
||||
): Page<Series>
|
||||
|
||||
fun getLibraryId(seriesId: String): String?
|
||||
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
package org.gotson.komga.domain.persistence
|
||||
|
||||
import org.gotson.komga.domain.model.BookSearch
|
||||
import org.gotson.komga.domain.model.KomgaUser
|
||||
import org.gotson.komga.domain.model.SearchContext
|
||||
import org.gotson.komga.domain.model.SyncPoint
|
||||
import org.springframework.data.domain.Page
|
||||
import org.springframework.data.domain.Pageable
|
||||
|
||||
interface SyncPointRepository {
|
||||
fun create(
|
||||
user: KomgaUser,
|
||||
apiKeyId: String?,
|
||||
search: BookSearch,
|
||||
context: SearchContext,
|
||||
): SyncPoint
|
||||
|
||||
fun addOnDeck(
|
||||
syncPointId: String,
|
||||
user: KomgaUser,
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
context: SearchContext,
|
||||
filterOnLibraryIds: List<String>?,
|
||||
)
|
||||
|
||||
fun findByIdOrNull(syncPointId: String): SyncPoint?
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package org.gotson.komga.domain.service
|
|||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.gotson.komga.domain.model.Book
|
||||
import org.gotson.komga.domain.model.BookAction
|
||||
import org.gotson.komga.domain.model.BookSearch
|
||||
import org.gotson.komga.domain.model.BookWithMedia
|
||||
import org.gotson.komga.domain.model.DomainEvent
|
||||
import org.gotson.komga.domain.model.HistoricalEvent
|
||||
|
|
@ -17,6 +16,9 @@ import org.gotson.komga.domain.model.MediaProfile
|
|||
import org.gotson.komga.domain.model.NoThumbnailFoundException
|
||||
import org.gotson.komga.domain.model.R2Progression
|
||||
import org.gotson.komga.domain.model.ReadProgress
|
||||
import org.gotson.komga.domain.model.SearchCondition
|
||||
import org.gotson.komga.domain.model.SearchContext
|
||||
import org.gotson.komga.domain.model.SearchOperator
|
||||
import org.gotson.komga.domain.model.ThumbnailBook
|
||||
import org.gotson.komga.domain.model.TypedBytes
|
||||
import org.gotson.komga.domain.persistence.BookMetadataRepository
|
||||
|
|
@ -34,7 +36,7 @@ import org.gotson.komga.infrastructure.image.ImageType
|
|||
import org.gotson.komga.language.toCurrentTimeZone
|
||||
import org.springframework.beans.factory.annotation.Qualifier
|
||||
import org.springframework.context.ApplicationEventPublisher
|
||||
import org.springframework.data.domain.Sort
|
||||
import org.springframework.data.domain.Pageable
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.support.TransactionTemplate
|
||||
import org.springframework.web.util.UriUtils
|
||||
|
|
@ -280,7 +282,7 @@ class BookLifecycle(
|
|||
return if (forBiggerResultOnly) {
|
||||
thumbnailBookRepository.findAllBookIdsByThumbnailTypeAndDimensionSmallerThan(ThumbnailBook.Type.GENERATED, komgaSettingsProvider.thumbnailSize.maxEdge)
|
||||
} else {
|
||||
bookRepository.findAllIds(BookSearch(deleted = false), Sort.unsorted())
|
||||
bookRepository.findAll(SearchCondition.Deleted(SearchOperator.IsFalse), SearchContext.empty(), Pageable.unpaged()).content.map { it.id }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -374,7 +376,7 @@ class BookLifecycle(
|
|||
readProgressRepository.deleteByBookIds(bookIds)
|
||||
readListRepository.removeBooksFromAll(bookIds)
|
||||
|
||||
mediaRepository.deleteByBookIds(bookIds)
|
||||
mediaRepository.delete(bookIds)
|
||||
thumbnailBookRepository.deleteByBookIds(bookIds)
|
||||
bookMetadataRepository.delete(bookIds)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,13 +4,14 @@ import io.github.oshai.kotlinlogging.KotlinLogging
|
|||
import org.gotson.komga.application.tasks.TaskEmitter
|
||||
import org.gotson.komga.domain.model.Book
|
||||
import org.gotson.komga.domain.model.BookMetadataPatchCapability
|
||||
import org.gotson.komga.domain.model.BookSearch
|
||||
import org.gotson.komga.domain.model.DirectoryNotFoundException
|
||||
import org.gotson.komga.domain.model.DomainEvent
|
||||
import org.gotson.komga.domain.model.Library
|
||||
import org.gotson.komga.domain.model.Media
|
||||
import org.gotson.komga.domain.model.SearchCondition
|
||||
import org.gotson.komga.domain.model.SearchContext
|
||||
import org.gotson.komga.domain.model.SearchOperator
|
||||
import org.gotson.komga.domain.model.Series
|
||||
import org.gotson.komga.domain.model.SeriesSearch
|
||||
import org.gotson.komga.domain.model.Sidecar
|
||||
import org.gotson.komga.domain.model.ThumbnailBook
|
||||
import org.gotson.komga.domain.model.ThumbnailSeries
|
||||
|
|
@ -31,6 +32,7 @@ import org.gotson.komga.infrastructure.hash.Hasher
|
|||
import org.gotson.komga.language.notEquals
|
||||
import org.gotson.komga.language.toIndexedMap
|
||||
import org.springframework.context.ApplicationEventPublisher
|
||||
import org.springframework.data.domain.Pageable
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.support.TransactionTemplate
|
||||
import java.nio.file.Paths
|
||||
|
|
@ -279,7 +281,7 @@ class LibraryContentLifecycle(
|
|||
val bookSizes = newBooks.map { it.fileSize }
|
||||
|
||||
val deletedCandidates =
|
||||
seriesRepository.findAll(SeriesSearch(deleted = true))
|
||||
seriesRepository.findAll(SearchCondition.Deleted(SearchOperator.IsTrue), SearchContext.empty(), Pageable.unpaged()).content
|
||||
.mapNotNull { deletedCandidate ->
|
||||
val deletedBooks = bookRepository.findAllBySeriesId(deletedCandidate.id)
|
||||
val deletedBooksSizes = deletedBooks.map { it.fileSize }
|
||||
|
|
@ -419,10 +421,26 @@ class LibraryContentLifecycle(
|
|||
fun emptyTrash(library: Library) {
|
||||
logger.info { "Empty trash for library: $library" }
|
||||
|
||||
val seriesToDelete = seriesRepository.findAll(SeriesSearch(libraryIds = listOf(library.id), deleted = true))
|
||||
val seriesToDelete =
|
||||
seriesRepository.findAll(
|
||||
SearchCondition.AllOfSeries(
|
||||
SearchCondition.LibraryId(SearchOperator.Is(library.id)),
|
||||
SearchCondition.Deleted(SearchOperator.IsTrue),
|
||||
),
|
||||
SearchContext.empty(),
|
||||
Pageable.unpaged(),
|
||||
).content
|
||||
seriesLifecycle.deleteMany(seriesToDelete)
|
||||
|
||||
val booksToDelete = bookRepository.findAll(BookSearch(libraryIds = listOf(library.id), deleted = true))
|
||||
val booksToDelete =
|
||||
bookRepository.findAll(
|
||||
SearchCondition.AllOfBook(
|
||||
SearchCondition.LibraryId(SearchOperator.Is(library.id)),
|
||||
SearchCondition.Deleted(SearchOperator.IsTrue),
|
||||
),
|
||||
SearchContext.empty(),
|
||||
Pageable.unpaged(),
|
||||
).content
|
||||
bookLifecycle.deleteMany(booksToDelete)
|
||||
booksToDelete.map { it.seriesId }.distinct().forEach { seriesId ->
|
||||
seriesRepository.findByIdOrNull(seriesId)?.let { seriesLifecycle.sortBooks(it) }
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import org.gotson.komga.domain.model.BookSearch
|
|||
import org.gotson.komga.domain.model.KomgaUser
|
||||
import org.gotson.komga.domain.model.Media
|
||||
import org.gotson.komga.domain.model.MediaProfile
|
||||
import org.gotson.komga.domain.model.SearchCondition
|
||||
import org.gotson.komga.domain.model.SearchContext
|
||||
import org.gotson.komga.domain.model.SearchOperator
|
||||
import org.gotson.komga.domain.model.SyncPoint
|
||||
import org.gotson.komga.domain.persistence.SyncPointRepository
|
||||
import org.springframework.data.domain.Page
|
||||
|
|
@ -19,20 +22,32 @@ class SyncPointLifecycle(
|
|||
apiKeyId: String?,
|
||||
libraryIds: List<String>?,
|
||||
): SyncPoint {
|
||||
val authorizedLibraryIds = user.getAuthorizedLibraryIds(libraryIds)
|
||||
val context = SearchContext(user)
|
||||
|
||||
val syncPoint =
|
||||
syncPointRepository.create(
|
||||
user,
|
||||
syncPointRepository.
|
||||
create(
|
||||
apiKeyId,
|
||||
BookSearch(
|
||||
libraryIds = authorizedLibraryIds,
|
||||
mediaStatus = setOf(Media.Status.READY),
|
||||
mediaProfile = listOf(MediaProfile.EPUB),
|
||||
deleted = false,
|
||||
SearchCondition.AllOfBook(
|
||||
buildList {
|
||||
libraryIds?.let {
|
||||
add(
|
||||
SearchCondition.AnyOfBook(
|
||||
it.map { libraryId -> SearchCondition.LibraryId(SearchOperator.Is(libraryId)) },
|
||||
),
|
||||
)
|
||||
}
|
||||
add(SearchCondition.MediaStatus(SearchOperator.Is(Media.Status.READY)))
|
||||
add(SearchCondition.MediaProfile(SearchOperator.Is(MediaProfile.EPUB)))
|
||||
add(SearchCondition.Deleted(SearchOperator.IsFalse))
|
||||
},
|
||||
),
|
||||
),
|
||||
context,
|
||||
)
|
||||
|
||||
syncPointRepository.addOnDeck(syncPoint.id, user, authorizedLibraryIds)
|
||||
syncPointRepository.addOnDeck(syncPoint.id, context, libraryIds)
|
||||
|
||||
return syncPoint
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ class Hasher {
|
|||
}
|
||||
|
||||
@OptIn(ExperimentalUnsignedTypes::class)
|
||||
private fun ByteArray.toHexString(): String =
|
||||
fun ByteArray.toHexString(): String =
|
||||
asUByteArray().joinToString("") {
|
||||
it.toString(16).padStart(2, '0')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,182 @@
|
|||
package org.gotson.komga.infrastructure.jooq
|
||||
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.gotson.komga.domain.model.Media
|
||||
import org.gotson.komga.domain.model.MediaType
|
||||
import org.gotson.komga.domain.model.ReadStatus
|
||||
import org.gotson.komga.domain.model.SearchCondition
|
||||
import org.gotson.komga.domain.model.SearchContext
|
||||
import org.gotson.komga.domain.model.SearchOperator
|
||||
import org.gotson.komga.infrastructure.datasource.SqliteUdfDataSource
|
||||
import org.gotson.komga.infrastructure.jooq.RequiredJoin.ReadProgress
|
||||
import org.gotson.komga.jooq.main.Tables
|
||||
import org.jooq.Condition
|
||||
import org.jooq.impl.DSL
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
/**
|
||||
* Helper class to generate a Jooq Condition from a [SearchCondition.Book] and [SearchContext]
|
||||
*/
|
||||
class BookSearchHelper(
|
||||
val context: SearchContext,
|
||||
) : ContentRestrictionsSearchHelper() {
|
||||
fun toCondition(searchCondition: SearchCondition.Book?): Pair<Condition, Set<RequiredJoin>> {
|
||||
val base = toCondition()
|
||||
val search = toConditionInternal(searchCondition)
|
||||
return search.first.and(base.first) to (search.second + base.second)
|
||||
}
|
||||
|
||||
fun toCondition(): Pair<Condition, Set<RequiredJoin>> {
|
||||
val restrictions = toConditionInternal(context.restrictions)
|
||||
val authorizedLibraries = toConditionInternal(context.libraryIds)
|
||||
return restrictions.first.and(authorizedLibraries.first) to (restrictions.second + authorizedLibraries.second)
|
||||
}
|
||||
|
||||
private fun toConditionInternal(libraryIds: Collection<String>?): Pair<Condition, Set<RequiredJoin>> {
|
||||
if (libraryIds == null) return DSL.noCondition() to emptySet()
|
||||
if (libraryIds.isEmpty()) return DSL.falseCondition() to emptySet()
|
||||
return toConditionInternal(SearchCondition.AnyOfBook(libraryIds.map { SearchCondition.LibraryId(SearchOperator.Is(it)) }))
|
||||
}
|
||||
|
||||
private fun toConditionInternal(searchCondition: SearchCondition.Book?): Pair<Condition, Set<RequiredJoin>> {
|
||||
return when (searchCondition) {
|
||||
is SearchCondition.AllOfBook ->
|
||||
searchCondition.conditions.fold(DSL.noCondition() to emptySet()) { acc: Pair<Condition, Set<RequiredJoin>>, cond: SearchCondition.Book ->
|
||||
val bookCondition = toConditionInternal(cond)
|
||||
acc.first.and(bookCondition.first) to (acc.second + bookCondition.second)
|
||||
}
|
||||
|
||||
is SearchCondition.AnyOfBook ->
|
||||
searchCondition.conditions.fold(DSL.noCondition() to emptySet()) { acc: Pair<Condition, Set<RequiredJoin>>, cond: SearchCondition.Book ->
|
||||
val bookCondition = toConditionInternal(cond)
|
||||
acc.first.or(bookCondition.first) to (acc.second + bookCondition.second)
|
||||
}
|
||||
|
||||
is SearchCondition.LibraryId -> searchCondition.operator.toCondition(Tables.BOOK.LIBRARY_ID) to emptySet()
|
||||
|
||||
is SearchCondition.SeriesId -> searchCondition.operator.toCondition(Tables.BOOK.SERIES_ID) to emptySet()
|
||||
|
||||
is SearchCondition.ReadListId ->
|
||||
Tables.BOOK.ID.let { field ->
|
||||
val inner = { readListId: String ->
|
||||
DSL.select(Tables.READLIST_BOOK.BOOK_ID)
|
||||
.from(Tables.READLIST_BOOK)
|
||||
.where(Tables.READLIST_BOOK.READLIST_ID.eq(readListId))
|
||||
}
|
||||
when (searchCondition.operator) {
|
||||
is SearchOperator.Is -> field.`in`(inner(searchCondition.operator.value))
|
||||
is SearchOperator.IsNot -> field.notIn(inner(searchCondition.operator.value))
|
||||
}
|
||||
} to emptySet()
|
||||
|
||||
is SearchCondition.Title ->
|
||||
searchCondition.operator.toCondition(Tables.BOOK_METADATA.TITLE) to
|
||||
setOf(
|
||||
RequiredJoin.BookMetadata,
|
||||
)
|
||||
|
||||
is SearchCondition.Deleted ->
|
||||
Tables.BOOK.DELETED_DATE.let {
|
||||
when (searchCondition.operator) {
|
||||
SearchOperator.IsFalse -> it.isNull
|
||||
SearchOperator.IsTrue -> it.isNotNull
|
||||
}
|
||||
} to emptySet()
|
||||
|
||||
is SearchCondition.ReleaseDate ->
|
||||
searchCondition.operator.toCondition(Tables.BOOK_METADATA.RELEASE_DATE) to
|
||||
setOf(
|
||||
RequiredJoin.BookMetadata,
|
||||
)
|
||||
|
||||
is SearchCondition.NumberSort ->
|
||||
searchCondition.operator.toCondition(Tables.BOOK_METADATA.NUMBER_SORT) to
|
||||
setOf(
|
||||
RequiredJoin.BookMetadata,
|
||||
)
|
||||
|
||||
is SearchCondition.ReadStatus ->
|
||||
if (context.userId == null) {
|
||||
logger.warn { "SearchCondition.ReadStatus without userId in search context" }
|
||||
DSL.falseCondition() to emptySet()
|
||||
} else {
|
||||
Tables.READ_PROGRESS.COMPLETED.let {
|
||||
when (searchCondition.operator) {
|
||||
is SearchOperator.Is -> {
|
||||
when (searchCondition.operator.value) {
|
||||
ReadStatus.UNREAD -> it.isNull
|
||||
ReadStatus.READ -> it.isTrue
|
||||
ReadStatus.IN_PROGRESS -> it.isFalse
|
||||
}
|
||||
}
|
||||
|
||||
is SearchOperator.IsNot ->
|
||||
when (searchCondition.operator.value) {
|
||||
ReadStatus.UNREAD -> it.isNotNull
|
||||
ReadStatus.READ -> it.isNull.or(it.isFalse)
|
||||
ReadStatus.IN_PROGRESS -> it.isTrue.or(it.isNull)
|
||||
}
|
||||
}
|
||||
} to setOf(ReadProgress(context.userId))
|
||||
}
|
||||
|
||||
is SearchCondition.MediaStatus ->
|
||||
searchCondition.operator.toCondition(Tables.MEDIA.STATUS, Media.Status::name) to
|
||||
setOf(
|
||||
RequiredJoin.Media,
|
||||
)
|
||||
|
||||
is SearchCondition.MediaProfile ->
|
||||
Tables.MEDIA.MEDIA_TYPE.let { field ->
|
||||
when (searchCondition.operator) {
|
||||
is SearchOperator.Is -> field.`in`(MediaType.matchingMediaProfile(searchCondition.operator.value).map { it.type }.toSet())
|
||||
is SearchOperator.IsNot -> field.notIn(MediaType.matchingMediaProfile(searchCondition.operator.value).map { it.type }.toSet())
|
||||
}
|
||||
} to setOf(RequiredJoin.Media)
|
||||
|
||||
is SearchCondition.Tag ->
|
||||
Tables.BOOK.ID.let { field ->
|
||||
val inner = { tag: String ->
|
||||
DSL.select(Tables.BOOK_METADATA_TAG.BOOK_ID)
|
||||
.from(Tables.BOOK_METADATA_TAG)
|
||||
.where(Tables.BOOK_METADATA_TAG.TAG.collate(SqliteUdfDataSource.COLLATION_UNICODE_3).equalIgnoreCase(tag))
|
||||
}
|
||||
when (searchCondition.operator) {
|
||||
is SearchOperator.Is -> field.`in`(inner(searchCondition.operator.value))
|
||||
is SearchOperator.IsNot -> field.notIn(inner(searchCondition.operator.value))
|
||||
}
|
||||
} to emptySet()
|
||||
|
||||
is SearchCondition.Author ->
|
||||
Tables.BOOK.ID.let { field ->
|
||||
val inner = { name: String?, role: String? ->
|
||||
DSL.select(Tables.BOOK_METADATA_AUTHOR.BOOK_ID)
|
||||
.from(Tables.BOOK_METADATA_AUTHOR)
|
||||
.where(DSL.noCondition())
|
||||
.apply { if (name != null) and(Tables.BOOK_METADATA_AUTHOR.NAME.collate(SqliteUdfDataSource.COLLATION_UNICODE_3).equalIgnoreCase(name)) }
|
||||
.apply { if (role != null) and(Tables.BOOK_METADATA_AUTHOR.ROLE.collate(SqliteUdfDataSource.COLLATION_UNICODE_3).equalIgnoreCase(role)) }
|
||||
}
|
||||
when (searchCondition.operator) {
|
||||
is SearchOperator.Is -> {
|
||||
if (searchCondition.operator.value.name == null && searchCondition.operator.value.role == null)
|
||||
DSL.noCondition()
|
||||
else
|
||||
field.`in`(inner(searchCondition.operator.value.name, searchCondition.operator.value.role))
|
||||
}
|
||||
|
||||
is SearchOperator.IsNot -> {
|
||||
if (searchCondition.operator.value.name == null && searchCondition.operator.value.role == null)
|
||||
DSL.noCondition()
|
||||
else
|
||||
field.notIn(inner(searchCondition.operator.value.name, searchCondition.operator.value.role))
|
||||
}
|
||||
} to emptySet()
|
||||
}
|
||||
|
||||
is SearchCondition.OneShot -> searchCondition.operator.toCondition(Tables.BOOK.ONESHOT) to emptySet()
|
||||
|
||||
null -> DSL.noCondition() to emptySet()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package org.gotson.komga.infrastructure.jooq
|
||||
|
||||
import org.gotson.komga.domain.model.AllowExclude
|
||||
import org.gotson.komga.domain.model.ContentRestrictions
|
||||
import org.gotson.komga.jooq.main.Tables
|
||||
import org.jooq.Condition
|
||||
import org.jooq.impl.DSL
|
||||
|
||||
abstract class ContentRestrictionsSearchHelper {
|
||||
protected fun toConditionInternal(restrictions: ContentRestrictions): Pair<Condition, Set<RequiredJoin>> {
|
||||
val ageAllowed =
|
||||
if (restrictions.ageRestriction?.restriction == AllowExclude.ALLOW_ONLY) {
|
||||
Tables.SERIES_METADATA.AGE_RATING.isNotNull.and(Tables.SERIES_METADATA.AGE_RATING.lessOrEqual(restrictions.ageRestriction.age)) to setOf(RequiredJoin.SeriesMetadata)
|
||||
} else {
|
||||
DSL.noCondition() to emptySet()
|
||||
}
|
||||
|
||||
val labelAllowed =
|
||||
if (restrictions.labelsAllow.isNotEmpty())
|
||||
Tables.SERIES_METADATA.SERIES_ID.`in`(
|
||||
DSL.select(Tables.SERIES_METADATA_SHARING.SERIES_ID)
|
||||
.from(Tables.SERIES_METADATA_SHARING)
|
||||
.where(Tables.SERIES_METADATA_SHARING.LABEL.`in`(restrictions.labelsAllow)),
|
||||
) to setOf(RequiredJoin.SeriesMetadata)
|
||||
else
|
||||
DSL.noCondition() to emptySet()
|
||||
|
||||
val ageDenied =
|
||||
if (restrictions.ageRestriction?.restriction == AllowExclude.EXCLUDE)
|
||||
Tables.SERIES_METADATA.AGE_RATING.isNull.or(Tables.SERIES_METADATA.AGE_RATING.lessThan(restrictions.ageRestriction.age)) to setOf(RequiredJoin.SeriesMetadata)
|
||||
else
|
||||
DSL.noCondition() to emptySet()
|
||||
|
||||
val labelDenied =
|
||||
if (restrictions.labelsExclude.isNotEmpty())
|
||||
Tables.SERIES_METADATA.SERIES_ID.notIn(
|
||||
DSL.select(Tables.SERIES_METADATA_SHARING.SERIES_ID)
|
||||
.from(Tables.SERIES_METADATA_SHARING)
|
||||
.where(Tables.SERIES_METADATA_SHARING.LABEL.`in`(restrictions.labelsExclude)),
|
||||
) to setOf(RequiredJoin.SeriesMetadata)
|
||||
else
|
||||
DSL.noCondition() to emptySet()
|
||||
|
||||
return ageAllowed.first.or(labelAllowed.first)
|
||||
.and(ageDenied.first.and(labelDenied.first)) to (ageAllowed.second + labelAllowed.second + ageDenied.second + labelDenied.second)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package org.gotson.komga.infrastructure.jooq
|
||||
|
||||
/**
|
||||
* An indication that some tables need to be joined for query conditions to work
|
||||
*/
|
||||
sealed class RequiredJoin {
|
||||
data object BookMetadata : RequiredJoin()
|
||||
|
||||
data object Media : RequiredJoin()
|
||||
|
||||
data class ReadProgress(val userId: String) : RequiredJoin()
|
||||
|
||||
data object BookMetadataAggregation : RequiredJoin()
|
||||
|
||||
data object SeriesMetadata : RequiredJoin()
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package org.gotson.komga.infrastructure.jooq
|
||||
|
||||
import org.gotson.komga.domain.model.SearchOperator
|
||||
import org.jooq.Field
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneOffset
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
fun SearchOperator.Equality<String>.toCondition(
|
||||
field: Field<String>,
|
||||
ignoreCase: Boolean = false,
|
||||
) =
|
||||
when (this) {
|
||||
is SearchOperator.Is -> if (ignoreCase) field.equalIgnoreCase(this.value) else field.eq(this.value)
|
||||
is SearchOperator.IsNot -> if (ignoreCase) field.notEqualIgnoreCase(this.value) else field.ne(this.value)
|
||||
}
|
||||
|
||||
fun <T> SearchOperator.Equality<T>.toCondition(field: Field<T>) =
|
||||
when (this) {
|
||||
is SearchOperator.Is -> field.eq(this.value)
|
||||
is SearchOperator.IsNot -> field.ne(this.value)
|
||||
}
|
||||
|
||||
fun <T> SearchOperator.Equality<T>.toCondition(
|
||||
field: Field<String>,
|
||||
converter: (T) -> String,
|
||||
) =
|
||||
when (this) {
|
||||
is SearchOperator.Is -> field.eq(converter(this.value))
|
||||
is SearchOperator.IsNot -> field.ne(converter(this.value))
|
||||
}
|
||||
|
||||
fun SearchOperator.StringOp.toCondition(field: Field<String>) =
|
||||
when (this) {
|
||||
is SearchOperator.BeginsWith -> field.startsWithIgnoreCase(value)
|
||||
is SearchOperator.DoesNotBeginWith -> field.startsWithIgnoreCase(value).not()
|
||||
is SearchOperator.EndsWith -> field.endsWithIgnoreCase(value)
|
||||
is SearchOperator.DoesNotEndWith -> field.endsWithIgnoreCase(value).not()
|
||||
is SearchOperator.Contains -> field.containsIgnoreCase(value)
|
||||
is SearchOperator.DoesNotContain -> field.notContainsIgnoreCase(value)
|
||||
is SearchOperator.Is<*> -> field.equalIgnoreCase(value as String)
|
||||
is SearchOperator.IsNot<*> -> field.notEqualIgnoreCase(value as String)
|
||||
}
|
||||
|
||||
fun SearchOperator.Date.toCondition(field: Field<LocalDate>) =
|
||||
when (this) {
|
||||
is SearchOperator.After -> field.gt(dateTime.withZoneSameInstant(ZoneOffset.UTC).toLocalDate())
|
||||
is SearchOperator.Before -> field.lt(dateTime.withZoneSameInstant(ZoneOffset.UTC).toLocalDate())
|
||||
is SearchOperator.IsInTheLast -> field.gt(LocalDate.now(ZoneOffset.UTC).minus(duration.toDays(), ChronoUnit.DAYS))
|
||||
is SearchOperator.IsNotInTheLast -> field.lt(LocalDate.now(ZoneOffset.UTC).minus(duration.toDays(), ChronoUnit.DAYS))
|
||||
SearchOperator.IsNull -> field.isNull
|
||||
SearchOperator.IsNotNull -> field.isNotNull
|
||||
}
|
||||
|
||||
fun SearchOperator.NumericNullable<Int>.toCondition(field: Field<Int>) =
|
||||
when (this) {
|
||||
is SearchOperator.Is<*> -> field.eq(value as Int)
|
||||
is SearchOperator.IsNot<*> -> field.ne(value as Int).or(field.isNull)
|
||||
is SearchOperator.GreaterThan -> field.gt(value)
|
||||
is SearchOperator.LessThan -> field.lt(value)
|
||||
is SearchOperator.IsNullT -> field.isNull
|
||||
is SearchOperator.IsNotNullT -> field.isNotNull
|
||||
}
|
||||
|
||||
fun SearchOperator.Numeric<Float>.toCondition(field: Field<Float>) =
|
||||
when (this) {
|
||||
is SearchOperator.Is<*> -> field.eq(value as Float)
|
||||
is SearchOperator.IsNot<*> -> field.ne(value as Float)
|
||||
is SearchOperator.GreaterThan -> field.gt(value)
|
||||
is SearchOperator.LessThan -> field.lt(value)
|
||||
}
|
||||
|
||||
fun SearchOperator.Boolean.toCondition(field: Field<Boolean>) =
|
||||
when (this) {
|
||||
SearchOperator.IsTrue -> field.isTrue
|
||||
SearchOperator.IsFalse -> field.isFalse
|
||||
}
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
package org.gotson.komga.infrastructure.jooq
|
||||
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.gotson.komga.domain.model.ReadStatus
|
||||
import org.gotson.komga.domain.model.SearchCondition
|
||||
import org.gotson.komga.domain.model.SearchContext
|
||||
import org.gotson.komga.domain.model.SearchOperator
|
||||
import org.gotson.komga.domain.model.SeriesMetadata
|
||||
import org.gotson.komga.infrastructure.datasource.SqliteUdfDataSource
|
||||
import org.gotson.komga.jooq.main.Tables
|
||||
import org.jooq.Condition
|
||||
import org.jooq.impl.DSL
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
/**
|
||||
* Helper class to generate a Jooq Condition from a [SearchCondition.Series] and [SearchContext]
|
||||
*/
|
||||
class SeriesSearchHelper(
|
||||
val context: SearchContext,
|
||||
) : ContentRestrictionsSearchHelper() {
|
||||
fun toCondition(searchCondition: SearchCondition.Series?): Pair<Condition, Set<RequiredJoin>> {
|
||||
val base = toCondition()
|
||||
val search = toConditionInternal(searchCondition)
|
||||
return search.first.and(base.first) to (search.second + base.second)
|
||||
}
|
||||
|
||||
fun toCondition(): Pair<Condition, Set<RequiredJoin>> {
|
||||
val restrictions = toConditionInternal(context.restrictions)
|
||||
val authorizedLibraries = toConditionInternal(context.libraryIds)
|
||||
return restrictions.first.and(authorizedLibraries.first) to (restrictions.second + authorizedLibraries.second)
|
||||
}
|
||||
|
||||
private fun toConditionInternal(libraryIds: Collection<String>?): Pair<Condition, Set<RequiredJoin>> {
|
||||
if (libraryIds == null) return DSL.noCondition() to emptySet()
|
||||
if (libraryIds.isEmpty()) return DSL.falseCondition() to emptySet()
|
||||
return toConditionInternal(SearchCondition.AnyOfSeries(libraryIds.map { SearchCondition.LibraryId(SearchOperator.Is(it)) }))
|
||||
}
|
||||
|
||||
private fun toConditionInternal(searchCondition: SearchCondition.Series?): Pair<Condition, Set<RequiredJoin>> {
|
||||
return when (searchCondition) {
|
||||
is SearchCondition.AllOfSeries ->
|
||||
searchCondition.conditions.fold(DSL.noCondition() to emptySet()) { acc: Pair<Condition, Set<RequiredJoin>>, cond: SearchCondition.Series ->
|
||||
val seriesCondition = toConditionInternal(cond)
|
||||
acc.first.and(seriesCondition.first) to (acc.second + seriesCondition.second)
|
||||
}
|
||||
|
||||
is SearchCondition.AnyOfSeries ->
|
||||
searchCondition.conditions.fold(DSL.noCondition() to emptySet()) { acc: Pair<Condition, Set<RequiredJoin>>, cond: SearchCondition.Series ->
|
||||
val seriesCondition = toConditionInternal(cond)
|
||||
acc.first.or(seriesCondition.first) to (acc.second + seriesCondition.second)
|
||||
}
|
||||
|
||||
is SearchCondition.LibraryId -> searchCondition.operator.toCondition(Tables.SERIES.LIBRARY_ID) to emptySet()
|
||||
|
||||
is SearchCondition.Deleted ->
|
||||
Tables.SERIES.DELETED_DATE.let {
|
||||
when (searchCondition.operator) {
|
||||
SearchOperator.IsFalse -> it.isNull
|
||||
SearchOperator.IsTrue -> it.isNotNull
|
||||
}
|
||||
} to emptySet()
|
||||
|
||||
is SearchCondition.ReleaseDate ->
|
||||
searchCondition.operator.toCondition(Tables.BOOK_METADATA_AGGREGATION.RELEASE_DATE) to setOf(RequiredJoin.BookMetadataAggregation)
|
||||
|
||||
is SearchCondition.ReadStatus ->
|
||||
if (context.userId == null) {
|
||||
logger.warn { "SearchCondition.ReadStatus without userId in search context" }
|
||||
DSL.falseCondition() to emptySet()
|
||||
} else {
|
||||
Tables.READ_PROGRESS_SERIES.READ_COUNT.let { field ->
|
||||
when (searchCondition.operator) {
|
||||
is SearchOperator.Is -> {
|
||||
when (searchCondition.operator.value) {
|
||||
ReadStatus.UNREAD -> field.isNull
|
||||
ReadStatus.READ -> field.eq(Tables.SERIES.BOOK_COUNT)
|
||||
ReadStatus.IN_PROGRESS -> field.ne(Tables.SERIES.BOOK_COUNT)
|
||||
}
|
||||
}
|
||||
|
||||
is SearchOperator.IsNot ->
|
||||
when (searchCondition.operator.value) {
|
||||
ReadStatus.UNREAD -> field.isNotNull
|
||||
ReadStatus.READ -> field.ne(Tables.SERIES.BOOK_COUNT).or(field.isNull)
|
||||
ReadStatus.IN_PROGRESS -> field.eq(Tables.SERIES.BOOK_COUNT).or(field.isNull)
|
||||
}
|
||||
}
|
||||
} to setOf(RequiredJoin.ReadProgress(context.userId))
|
||||
}
|
||||
|
||||
is SearchCondition.SeriesStatus ->
|
||||
searchCondition.operator.toCondition(Tables.SERIES_METADATA.STATUS, SeriesMetadata.Status::name) to
|
||||
setOf(
|
||||
RequiredJoin.SeriesMetadata,
|
||||
)
|
||||
|
||||
is SearchCondition.Tag ->
|
||||
Tables.SERIES.ID.let { field ->
|
||||
val inner = { tag: String ->
|
||||
DSL.select(Tables.SERIES_METADATA_TAG.SERIES_ID)
|
||||
.from(Tables.SERIES_METADATA_TAG)
|
||||
.where(Tables.SERIES_METADATA_TAG.TAG.collate(SqliteUdfDataSource.COLLATION_UNICODE_3).equalIgnoreCase(tag))
|
||||
.union(
|
||||
DSL.select(Tables.BOOK_METADATA_AGGREGATION_TAG.SERIES_ID)
|
||||
.from(Tables.BOOK_METADATA_AGGREGATION_TAG)
|
||||
.where(Tables.BOOK_METADATA_AGGREGATION_TAG.TAG.collate(SqliteUdfDataSource.COLLATION_UNICODE_3).equalIgnoreCase(tag)),
|
||||
)
|
||||
}
|
||||
when (searchCondition.operator) {
|
||||
is SearchOperator.Is -> field.`in`(inner(searchCondition.operator.value))
|
||||
is SearchOperator.IsNot -> field.notIn(inner(searchCondition.operator.value))
|
||||
}
|
||||
} to emptySet()
|
||||
|
||||
is SearchCondition.Author ->
|
||||
Tables.SERIES.ID.let { field ->
|
||||
val inner = { name: String?, role: String? ->
|
||||
DSL.select(Tables.BOOK_METADATA_AGGREGATION_AUTHOR.SERIES_ID)
|
||||
.from(Tables.BOOK_METADATA_AGGREGATION_AUTHOR)
|
||||
.where(DSL.noCondition())
|
||||
.apply {
|
||||
if (name != null)
|
||||
and(
|
||||
Tables.BOOK_METADATA_AGGREGATION_AUTHOR.NAME.collate(
|
||||
SqliteUdfDataSource.COLLATION_UNICODE_3,
|
||||
).equalIgnoreCase(name),
|
||||
)
|
||||
}
|
||||
.apply {
|
||||
if (role != null)
|
||||
and(
|
||||
Tables.BOOK_METADATA_AGGREGATION_AUTHOR.ROLE.collate(
|
||||
SqliteUdfDataSource.COLLATION_UNICODE_3,
|
||||
).equalIgnoreCase(role),
|
||||
)
|
||||
}
|
||||
}
|
||||
when (searchCondition.operator) {
|
||||
is SearchOperator.Is -> {
|
||||
if (searchCondition.operator.value.name == null && searchCondition.operator.value.role == null)
|
||||
DSL.noCondition()
|
||||
else
|
||||
field.`in`(inner(searchCondition.operator.value.name, searchCondition.operator.value.role))
|
||||
}
|
||||
|
||||
is SearchOperator.IsNot -> {
|
||||
if (searchCondition.operator.value.name == null && searchCondition.operator.value.role == null)
|
||||
DSL.noCondition()
|
||||
else
|
||||
field.notIn(inner(searchCondition.operator.value.name, searchCondition.operator.value.role))
|
||||
}
|
||||
} to emptySet()
|
||||
}
|
||||
|
||||
is SearchCondition.OneShot -> searchCondition.operator.toCondition(Tables.SERIES.ONESHOT) to emptySet()
|
||||
|
||||
is SearchCondition.AgeRating -> searchCondition.operator.toCondition(Tables.SERIES_METADATA.AGE_RATING) to setOf(RequiredJoin.SeriesMetadata)
|
||||
|
||||
is SearchCondition.CollectionId ->
|
||||
Tables.SERIES.ID.let { field ->
|
||||
val inner = { collectionId: String ->
|
||||
DSL.select(Tables.COLLECTION_SERIES.SERIES_ID)
|
||||
.from(Tables.COLLECTION_SERIES)
|
||||
.where(Tables.COLLECTION_SERIES.COLLECTION_ID.eq(collectionId))
|
||||
}
|
||||
when (searchCondition.operator) {
|
||||
is SearchOperator.Is -> field.`in`(inner(searchCondition.operator.value))
|
||||
is SearchOperator.IsNot -> field.notIn(inner(searchCondition.operator.value))
|
||||
}
|
||||
} to emptySet()
|
||||
|
||||
is SearchCondition.Complete ->
|
||||
Tables.SERIES_METADATA.TOTAL_BOOK_COUNT.let { field ->
|
||||
when (searchCondition.operator) {
|
||||
SearchOperator.IsTrue -> field.isNotNull.and(field.eq(Tables.SERIES.BOOK_COUNT))
|
||||
SearchOperator.IsFalse -> field.isNotNull.and(field.ne(Tables.SERIES.BOOK_COUNT))
|
||||
} to setOf(RequiredJoin.SeriesMetadata)
|
||||
}
|
||||
|
||||
is SearchCondition.Genre ->
|
||||
Tables.SERIES.ID.let { field ->
|
||||
val inner = { genre: String ->
|
||||
DSL.select(Tables.SERIES_METADATA_GENRE.SERIES_ID)
|
||||
.from(Tables.SERIES_METADATA_GENRE)
|
||||
.where(Tables.SERIES_METADATA_GENRE.GENRE.collate(SqliteUdfDataSource.COLLATION_UNICODE_3).equalIgnoreCase(genre))
|
||||
}
|
||||
when (searchCondition.operator) {
|
||||
is SearchOperator.Is -> field.`in`(inner(searchCondition.operator.value))
|
||||
is SearchOperator.IsNot -> field.notIn(inner(searchCondition.operator.value))
|
||||
}
|
||||
} to emptySet()
|
||||
|
||||
is SearchCondition.Language -> searchCondition.operator.toCondition(Tables.SERIES_METADATA.LANGUAGE, true) to setOf(RequiredJoin.SeriesMetadata)
|
||||
|
||||
is SearchCondition.Publisher -> searchCondition.operator.toCondition(Tables.SERIES_METADATA.PUBLISHER, true) to setOf(RequiredJoin.SeriesMetadata)
|
||||
|
||||
is SearchCondition.SharingLabel ->
|
||||
Tables.SERIES.ID.let { field ->
|
||||
val inner = { label: String ->
|
||||
DSL.select(Tables.SERIES_METADATA_SHARING.SERIES_ID)
|
||||
.from(Tables.SERIES_METADATA_SHARING)
|
||||
.where(Tables.SERIES_METADATA_SHARING.LABEL.collate(SqliteUdfDataSource.COLLATION_UNICODE_3).equalIgnoreCase(label))
|
||||
}
|
||||
when (searchCondition.operator) {
|
||||
is SearchOperator.Is -> field.`in`(inner(searchCondition.operator.value))
|
||||
is SearchOperator.IsNot -> field.notIn(inner(searchCondition.operator.value))
|
||||
}
|
||||
} to emptySet()
|
||||
|
||||
is SearchCondition.Title ->
|
||||
searchCondition.operator.toCondition(Tables.SERIES_METADATA.TITLE) to
|
||||
setOf(
|
||||
RequiredJoin.SeriesMetadata,
|
||||
)
|
||||
|
||||
is SearchCondition.TitleSort ->
|
||||
searchCondition.operator.toCondition(Tables.SERIES_METADATA.TITLE_SORT) to
|
||||
setOf(
|
||||
RequiredJoin.SeriesMetadata,
|
||||
)
|
||||
|
||||
null -> DSL.noCondition() to emptySet()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,10 +2,8 @@ package org.gotson.komga.infrastructure.jooq
|
|||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import org.gotson.komga.domain.model.AllowExclude
|
||||
import org.gotson.komga.domain.model.BookSearch
|
||||
import org.gotson.komga.domain.model.ContentRestrictions
|
||||
import org.gotson.komga.domain.model.MediaExtension
|
||||
import org.gotson.komga.domain.model.MediaType
|
||||
import org.gotson.komga.infrastructure.datasource.SqliteUdfDataSource
|
||||
import org.gotson.komga.jooq.main.Tables
|
||||
import org.jooq.Condition
|
||||
|
|
@ -70,22 +68,7 @@ fun DSLContext.insertTempStrings(
|
|||
|
||||
fun DSLContext.selectTempStrings() = this.select(Tables.TEMP_STRING_LIST.STRING).from(Tables.TEMP_STRING_LIST)
|
||||
|
||||
fun BookSearch.toCondition(): Condition {
|
||||
var c: Condition = DSL.noCondition()
|
||||
|
||||
if (libraryIds != null) c = c.and(Tables.BOOK.LIBRARY_ID.`in`(libraryIds))
|
||||
if (!seriesIds.isNullOrEmpty()) c = c.and(Tables.BOOK.SERIES_ID.`in`(seriesIds))
|
||||
searchTerm?.let { c = c.and(Tables.BOOK_METADATA.TITLE.containsIgnoreCase(it)) }
|
||||
if (!mediaStatus.isNullOrEmpty()) c = c.and(Tables.MEDIA.STATUS.`in`(mediaStatus))
|
||||
if (!mediaProfile.isNullOrEmpty()) c = c.and(Tables.MEDIA.MEDIA_TYPE.`in`(mediaProfile.flatMap { profile -> MediaType.matchingMediaProfile(profile).map { it.type } }.toSet()))
|
||||
if (deleted == true) c = c.and(Tables.BOOK.DELETED_DATE.isNotNull)
|
||||
if (deleted == false) c = c.and(Tables.BOOK.DELETED_DATE.isNull)
|
||||
if (releasedAfter != null) c = c.and(Tables.BOOK_METADATA.RELEASE_DATE.gt(releasedAfter))
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
fun ContentRestrictions.toCondition(dsl: DSLContext): Condition {
|
||||
fun ContentRestrictions.toCondition(): Condition {
|
||||
val ageAllowed =
|
||||
if (ageRestriction?.restriction == AllowExclude.ALLOW_ONLY) {
|
||||
Tables.SERIES_METADATA.AGE_RATING.isNotNull.and(Tables.SERIES_METADATA.AGE_RATING.lessOrEqual(ageRestriction.age))
|
||||
|
|
@ -96,7 +79,7 @@ fun ContentRestrictions.toCondition(dsl: DSLContext): Condition {
|
|||
val labelAllowed =
|
||||
if (labelsAllow.isNotEmpty())
|
||||
Tables.SERIES_METADATA.SERIES_ID.`in`(
|
||||
dsl.select(Tables.SERIES_METADATA_SHARING.SERIES_ID)
|
||||
DSL.select(Tables.SERIES_METADATA_SHARING.SERIES_ID)
|
||||
.from(Tables.SERIES_METADATA_SHARING)
|
||||
.where(Tables.SERIES_METADATA_SHARING.LABEL.`in`(labelsAllow)),
|
||||
)
|
||||
|
|
@ -112,7 +95,7 @@ fun ContentRestrictions.toCondition(dsl: DSLContext): Condition {
|
|||
val labelDenied =
|
||||
if (labelsExclude.isNotEmpty())
|
||||
Tables.SERIES_METADATA.SERIES_ID.notIn(
|
||||
dsl.select(Tables.SERIES_METADATA_SHARING.SERIES_ID)
|
||||
DSL.select(Tables.SERIES_METADATA_SHARING.SERIES_ID)
|
||||
.from(Tables.SERIES_METADATA_SHARING)
|
||||
.where(Tables.SERIES_METADATA_SHARING.LABEL.`in`(labelsExclude)),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ class BookCommonDao(
|
|||
.innerJoin(sd).on(s.ID.eq(sd.SERIES_ID))
|
||||
.where(rs.IN_PROGRESS_COUNT.eq(0))
|
||||
.and(rs.READ_COUNT.ne(s.BOOK_COUNT))
|
||||
.and(restrictions.toCondition(dsl))
|
||||
.and(restrictions.toCondition())
|
||||
.apply { filterOnLibraryIds?.let<Collection<String>, Unit> { and(s.LIBRARY_ID.`in`(it)) } },
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,18 @@
|
|||
package org.gotson.komga.infrastructure.jooq.main
|
||||
|
||||
import org.gotson.komga.domain.model.Book
|
||||
import org.gotson.komga.domain.model.BookSearch
|
||||
import org.gotson.komga.domain.model.SearchCondition
|
||||
import org.gotson.komga.domain.model.SearchContext
|
||||
import org.gotson.komga.domain.persistence.BookRepository
|
||||
import org.gotson.komga.infrastructure.jooq.BookSearchHelper
|
||||
import org.gotson.komga.infrastructure.jooq.RequiredJoin
|
||||
import org.gotson.komga.infrastructure.jooq.insertTempStrings
|
||||
import org.gotson.komga.infrastructure.jooq.selectTempStrings
|
||||
import org.gotson.komga.infrastructure.jooq.toCondition
|
||||
import org.gotson.komga.infrastructure.jooq.toOrderBy
|
||||
import org.gotson.komga.jooq.main.Tables
|
||||
import org.gotson.komga.jooq.main.tables.records.BookRecord
|
||||
import org.gotson.komga.language.toCurrentTimeZone
|
||||
import org.jooq.Condition
|
||||
import org.jooq.DSLContext
|
||||
import org.jooq.impl.DSL
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
|
|
@ -33,7 +36,9 @@ class BookDao(
|
|||
private val b = Tables.BOOK
|
||||
private val m = Tables.MEDIA
|
||||
private val d = Tables.BOOK_METADATA
|
||||
private val sd = Tables.SERIES_METADATA
|
||||
private val r = Tables.READ_PROGRESS
|
||||
private val rlb = Tables.READLIST_BOOK
|
||||
|
||||
private val sorts =
|
||||
mapOf(
|
||||
|
|
@ -104,26 +109,35 @@ class BookDao(
|
|||
.fetchInto(b)
|
||||
.map { it.toDomain() }
|
||||
|
||||
override fun findAll(bookSearch: BookSearch): Collection<Book> =
|
||||
dsl.select(*b.fields())
|
||||
.from(b)
|
||||
.leftJoin(d).on(b.ID.eq(d.BOOK_ID))
|
||||
.leftJoin(m).on(b.ID.eq(m.BOOK_ID))
|
||||
.where(bookSearch.toCondition())
|
||||
.fetchInto(b)
|
||||
.map { it.toDomain() }
|
||||
|
||||
override fun findAll(
|
||||
bookSearch: BookSearch,
|
||||
searchCondition: SearchCondition.Book?,
|
||||
searchContext: SearchContext,
|
||||
pageable: Pageable,
|
||||
): Page<Book> {
|
||||
val conditions = bookSearch.toCondition()
|
||||
val bookCondition = BookSearchHelper(searchContext).toCondition(searchCondition)
|
||||
return findAll(bookCondition.first, bookCondition.second, pageable)
|
||||
}
|
||||
|
||||
private fun findAll(
|
||||
conditions: Condition,
|
||||
joins: Set<RequiredJoin>,
|
||||
pageable: Pageable,
|
||||
): PageImpl<Book> {
|
||||
val count =
|
||||
dsl.selectCount()
|
||||
.from(b)
|
||||
.leftJoin(d).on(b.ID.eq(d.BOOK_ID))
|
||||
.leftJoin(m).on(b.ID.eq(m.BOOK_ID))
|
||||
.apply {
|
||||
joins.forEach { join ->
|
||||
when (join) {
|
||||
RequiredJoin.BookMetadata -> innerJoin(d).on(b.ID.eq(d.BOOK_ID))
|
||||
RequiredJoin.SeriesMetadata -> innerJoin(sd).on(b.SERIES_ID.eq(sd.SERIES_ID))
|
||||
RequiredJoin.Media -> innerJoin(m).on(b.ID.eq(m.BOOK_ID))
|
||||
is RequiredJoin.ReadProgress -> leftJoin(r).on(b.ID.eq(r.BOOK_ID)).and(r.USER_ID.eq(join.userId))
|
||||
// shouldn't be required for books
|
||||
RequiredJoin.BookMetadataAggregation -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
.where(conditions)
|
||||
.fetchOne(0, Long::class.java) ?: 0
|
||||
|
||||
|
|
@ -132,8 +146,18 @@ class BookDao(
|
|||
val items =
|
||||
dsl.select(*b.fields())
|
||||
.from(b)
|
||||
.leftJoin(d).on(b.ID.eq(d.BOOK_ID))
|
||||
.leftJoin(m).on(b.ID.eq(m.BOOK_ID))
|
||||
.apply {
|
||||
joins.forEach { join ->
|
||||
when (join) {
|
||||
RequiredJoin.BookMetadata -> innerJoin(d).on(b.ID.eq(d.BOOK_ID))
|
||||
RequiredJoin.SeriesMetadata -> innerJoin(sd).on(b.SERIES_ID.eq(sd.SERIES_ID))
|
||||
RequiredJoin.Media -> innerJoin(m).on(b.ID.eq(m.BOOK_ID))
|
||||
is RequiredJoin.ReadProgress -> leftJoin(r).on(b.ID.eq(r.BOOK_ID)).and(r.USER_ID.eq(join.userId))
|
||||
// shouldn't be required for books
|
||||
RequiredJoin.BookMetadataAggregation -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
.where(conditions)
|
||||
.orderBy(orderBy)
|
||||
.apply { if (pageable.isPaged) limit(pageable.pageSize).offset(pageable.offset) }
|
||||
|
|
@ -201,35 +225,12 @@ class BookDao(
|
|||
.where(b.SERIES_ID.eq(seriesId))
|
||||
.fetch(b.ID)
|
||||
|
||||
override fun findAllIdsBySeriesIds(seriesIds: Collection<String>): Collection<String> =
|
||||
dsl.select(b.ID)
|
||||
.from(b)
|
||||
.where(b.SERIES_ID.`in`(seriesIds))
|
||||
.fetch(0, String::class.java)
|
||||
|
||||
override fun findAllIdsByLibraryId(libraryId: String): Collection<String> =
|
||||
dsl.select(b.ID)
|
||||
.from(b)
|
||||
.where(b.LIBRARY_ID.eq(libraryId))
|
||||
.fetch(b.ID)
|
||||
|
||||
override fun findAllIds(
|
||||
bookSearch: BookSearch,
|
||||
sort: Sort,
|
||||
): Collection<String> {
|
||||
val conditions = bookSearch.toCondition()
|
||||
|
||||
val orderBy = sort.toOrderBy(sorts)
|
||||
|
||||
return dsl.select(b.ID)
|
||||
.from(b)
|
||||
.leftJoin(m).on(b.ID.eq(m.BOOK_ID))
|
||||
.leftJoin(d).on(b.ID.eq(d.BOOK_ID))
|
||||
.where(conditions)
|
||||
.orderBy(orderBy)
|
||||
.fetch(b.ID)
|
||||
}
|
||||
|
||||
override fun existsById(bookId: String): Boolean =
|
||||
dsl.fetchExists(b, b.ID.eq(bookId))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
package org.gotson.komga.infrastructure.jooq.main
|
||||
|
||||
import org.gotson.komga.domain.model.BookSearchWithReadProgress
|
||||
import org.gotson.komga.domain.model.BookSearch
|
||||
import org.gotson.komga.domain.model.ContentRestrictions
|
||||
import org.gotson.komga.domain.model.MediaType
|
||||
import org.gotson.komga.domain.model.ReadList
|
||||
import org.gotson.komga.domain.model.ReadStatus
|
||||
import org.gotson.komga.domain.model.SearchContext
|
||||
import org.gotson.komga.infrastructure.datasource.SqliteUdfDataSource
|
||||
import org.gotson.komga.infrastructure.jooq.BookSearchHelper
|
||||
import org.gotson.komga.infrastructure.jooq.RequiredJoin
|
||||
import org.gotson.komga.infrastructure.jooq.insertTempStrings
|
||||
import org.gotson.komga.infrastructure.jooq.noCase
|
||||
import org.gotson.komga.infrastructure.jooq.selectTempStrings
|
||||
|
|
@ -33,6 +34,7 @@ import org.jooq.Condition
|
|||
import org.jooq.DSLContext
|
||||
import org.jooq.Record
|
||||
import org.jooq.ResultQuery
|
||||
import org.jooq.SelectOnConditionStep
|
||||
import org.jooq.impl.DSL
|
||||
import org.jooq.impl.DSL.falseCondition
|
||||
import org.jooq.impl.DSL.noCondition
|
||||
|
|
@ -58,9 +60,7 @@ class BookDtoDao(
|
|||
private val m = Tables.MEDIA
|
||||
private val d = Tables.BOOK_METADATA
|
||||
private val r = Tables.READ_PROGRESS
|
||||
private val rs = Tables.READ_PROGRESS_SERIES
|
||||
private val a = Tables.BOOK_METADATA_AUTHOR
|
||||
private val s = Tables.SERIES
|
||||
private val sd = Tables.SERIES_METADATA
|
||||
private val rlb = Tables.READLIST_BOOK
|
||||
private val bt = Tables.BOOK_METADATA_TAG
|
||||
|
|
@ -90,37 +90,31 @@ class BookDtoDao(
|
|||
"readList.number" to rlb.NUMBER,
|
||||
)
|
||||
|
||||
override fun findAll(pageable: Pageable): Page<BookDto> =
|
||||
findAll(BookSearch(), SearchContext.ofAnonymousUser(), pageable)
|
||||
|
||||
override fun findAll(
|
||||
search: BookSearchWithReadProgress,
|
||||
userId: String,
|
||||
context: SearchContext,
|
||||
pageable: Pageable,
|
||||
restrictions: ContentRestrictions,
|
||||
): Page<BookDto> {
|
||||
val conditions = search.toCondition().and(restrictions.toCondition(dsl))
|
||||
): Page<BookDto> = findAll(BookSearch(), context, pageable)
|
||||
|
||||
return findAll(conditions, userId, pageable, false, null, search.searchTerm)
|
||||
}
|
||||
|
||||
override fun findAllByReadListId(
|
||||
readListId: String,
|
||||
userId: String,
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
search: BookSearchWithReadProgress,
|
||||
override fun findAll(
|
||||
search: BookSearch,
|
||||
context: SearchContext,
|
||||
pageable: Pageable,
|
||||
restrictions: ContentRestrictions,
|
||||
): Page<BookDto> {
|
||||
val conditions = rlb.READLIST_ID.eq(readListId).and(search.toCondition()).and(restrictions.toCondition(dsl))
|
||||
requireNotNull(context.userId) { "Missing userId in search context" }
|
||||
|
||||
return findAll(conditions, userId, pageable, true, filterOnLibraryIds, search.searchTerm)
|
||||
val (conditions, joins) = BookSearchHelper(context).toCondition(search.condition)
|
||||
return findAll(conditions, context.userId, pageable, search.fullTextSearch, joins)
|
||||
}
|
||||
|
||||
private fun findAll(
|
||||
conditions: Condition,
|
||||
userId: String,
|
||||
pageable: Pageable,
|
||||
selectReadListNumber: Boolean = false,
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
searchTerm: String?,
|
||||
joins: Set<RequiredJoin>,
|
||||
): Page<BookDto> {
|
||||
val bookIds = luceneHelper.searchEntitiesIds(searchTerm, LuceneEntity.Book)
|
||||
|
||||
|
|
@ -153,18 +147,28 @@ class BookDtoDao(
|
|||
.leftJoin(d).on(b.ID.eq(d.BOOK_ID))
|
||||
.leftJoin(r).on(b.ID.eq(r.BOOK_ID)).and(readProgressCondition(userId))
|
||||
.leftJoin(sd).on(b.SERIES_ID.eq(sd.SERIES_ID))
|
||||
.apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } }
|
||||
.apply { if (selectReadListNumber) leftJoin(rlb).on(b.ID.eq(rlb.BOOK_ID)) }
|
||||
.apply {
|
||||
joins.forEach { join ->
|
||||
when (join) {
|
||||
// always joined
|
||||
RequiredJoin.BookMetadata -> Unit
|
||||
RequiredJoin.Media -> Unit
|
||||
is RequiredJoin.ReadProgress -> Unit
|
||||
// Series joins - not needed
|
||||
RequiredJoin.BookMetadataAggregation -> Unit
|
||||
RequiredJoin.SeriesMetadata -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
.where(conditions)
|
||||
.and(searchCondition)
|
||||
.groupBy(b.ID),
|
||||
)
|
||||
|
||||
val dtos =
|
||||
selectBase(userId, selectReadListNumber)
|
||||
selectBase(userId, joins, pageable.sort.any { it.property == "readList.number" })
|
||||
.where(conditions)
|
||||
.and(searchCondition)
|
||||
.apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } }
|
||||
.orderBy(orderBy)
|
||||
.apply { if (pageable.isPaged) limit(pageable.pageSize).offset(pageable.offset) }
|
||||
.fetchAndMap()
|
||||
|
|
@ -324,9 +328,9 @@ class BookDtoDao(
|
|||
.apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } }
|
||||
.fetchOne(rlb.NUMBER)
|
||||
|
||||
return selectBase(userId, true)
|
||||
return selectBase(userId, joinOnReadList = true)
|
||||
.where(rlb.READLIST_ID.eq(readList.id))
|
||||
.apply { if (restrictions.isRestricted) and(restrictions.toCondition(dsl)) }
|
||||
.apply { if (restrictions.isRestricted) and(restrictions.toCondition()) }
|
||||
.apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } }
|
||||
.orderBy(rlb.NUMBER.let { if (next) it.asc() else it.desc() })
|
||||
.seek(numberSort)
|
||||
|
|
@ -343,7 +347,7 @@ class BookDtoDao(
|
|||
.leftJoin(d).on(b.ID.eq(d.BOOK_ID))
|
||||
.apply { if (restrictions.isRestricted) leftJoin(sd).on(sd.SERIES_ID.eq(b.SERIES_ID)) }
|
||||
.where(rlb.READLIST_ID.eq(readList.id))
|
||||
.apply { if (restrictions.isRestricted) and(restrictions.toCondition(dsl)) }
|
||||
.apply { if (restrictions.isRestricted) and(restrictions.toCondition()) }
|
||||
.apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } }
|
||||
.orderBy(d.RELEASE_DATE)
|
||||
.fetch(b.ID)
|
||||
|
|
@ -363,21 +367,40 @@ class BookDtoDao(
|
|||
|
||||
private fun selectBase(
|
||||
userId: String,
|
||||
selectReadListNumber: Boolean = false,
|
||||
) =
|
||||
dsl.select(
|
||||
*b.fields(),
|
||||
*m.fields(),
|
||||
*d.fields(),
|
||||
*r.fields(),
|
||||
sd.TITLE,
|
||||
).apply { if (selectReadListNumber) select(rlb.NUMBER) }
|
||||
joins: Set<RequiredJoin> = emptySet(),
|
||||
joinOnReadList: Boolean = false,
|
||||
): SelectOnConditionStep<Record> {
|
||||
val selectFields =
|
||||
listOf(
|
||||
*b.fields(),
|
||||
*m.fields(),
|
||||
*d.fields(),
|
||||
*r.fields(),
|
||||
sd.TITLE,
|
||||
)
|
||||
|
||||
return dsl
|
||||
.let { if (joinOnReadList) it.selectDistinct(selectFields) else it.select(selectFields) }
|
||||
.from(b)
|
||||
.leftJoin(m).on(b.ID.eq(m.BOOK_ID))
|
||||
.leftJoin(d).on(b.ID.eq(d.BOOK_ID))
|
||||
.leftJoin(r).on(b.ID.eq(r.BOOK_ID)).and(readProgressCondition(userId))
|
||||
.leftJoin(sd).on(b.SERIES_ID.eq(sd.SERIES_ID))
|
||||
.apply { if (selectReadListNumber) leftJoin(rlb).on(b.ID.eq(rlb.BOOK_ID)) }
|
||||
.apply {
|
||||
if (joinOnReadList) leftJoin(rlb).on(b.ID.eq(rlb.BOOK_ID))
|
||||
joins.forEach { join ->
|
||||
when (join) {
|
||||
// always joined
|
||||
RequiredJoin.BookMetadata -> Unit
|
||||
RequiredJoin.Media -> Unit
|
||||
is RequiredJoin.ReadProgress -> Unit
|
||||
// Series joins - not needed
|
||||
RequiredJoin.BookMetadataAggregation -> Unit
|
||||
RequiredJoin.SeriesMetadata -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ResultQuery<Record>.fetchAndMap(): MutableList<BookDto> {
|
||||
val records = fetch()
|
||||
|
|
@ -417,42 +440,6 @@ class BookDtoDao(
|
|||
}
|
||||
}
|
||||
|
||||
private fun BookSearchWithReadProgress.toCondition(): Condition {
|
||||
var c: Condition = noCondition()
|
||||
|
||||
if (libraryIds != null) c = c.and(b.LIBRARY_ID.`in`(libraryIds))
|
||||
if (!seriesIds.isNullOrEmpty()) c = c.and(b.SERIES_ID.`in`(seriesIds))
|
||||
if (!mediaStatus.isNullOrEmpty()) c = c.and(m.STATUS.`in`(mediaStatus))
|
||||
if (!mediaProfile.isNullOrEmpty()) c = c.and(m.MEDIA_TYPE.`in`(mediaProfile.flatMap { profile -> MediaType.matchingMediaProfile(profile).map { it.type } }.toSet()))
|
||||
if (deleted == true) c = c.and(b.DELETED_DATE.isNotNull)
|
||||
if (deleted == false) c = c.and(b.DELETED_DATE.isNull)
|
||||
if (releasedAfter != null) c = c.and(d.RELEASE_DATE.gt(releasedAfter))
|
||||
if (!tags.isNullOrEmpty()) c = c.and(b.ID.`in`(dsl.select(bt.BOOK_ID).from(bt).where(bt.TAG.collate(SqliteUdfDataSource.COLLATION_UNICODE_3).`in`(tags))))
|
||||
|
||||
if (readStatus != null) {
|
||||
val cr =
|
||||
readStatus.map {
|
||||
when (it) {
|
||||
ReadStatus.UNREAD -> r.COMPLETED.isNull
|
||||
ReadStatus.READ -> r.COMPLETED.isTrue
|
||||
ReadStatus.IN_PROGRESS -> r.COMPLETED.isFalse
|
||||
}
|
||||
}.reduce { acc, condition -> acc.or(condition) }
|
||||
|
||||
c = c.and(cr)
|
||||
}
|
||||
|
||||
if (!authors.isNullOrEmpty()) {
|
||||
var ca = noCondition()
|
||||
authors.forEach {
|
||||
ca = ca.or(b.ID.`in`(dsl.select(a.BOOK_ID).from(a).where(a.NAME.equalIgnoreCase(it.name).and(a.ROLE.equalIgnoreCase(it.role)))))
|
||||
}
|
||||
c = c.and(ca)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
private fun BookRecord.toDto(
|
||||
media: MediaDto,
|
||||
metadata: BookMetadataDto,
|
||||
|
|
|
|||
|
|
@ -266,7 +266,7 @@ class MediaDao(
|
|||
}
|
||||
|
||||
@Transactional
|
||||
override fun deleteByBookIds(bookIds: Collection<String>) {
|
||||
override fun delete(bookIds: Collection<String>) {
|
||||
dsl.insertTempStrings(batchSize, bookIds)
|
||||
|
||||
dsl.deleteFrom(p).where(p.BOOK_ID.`in`(dsl.selectTempStrings())).execute()
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ class ReadListDao(
|
|||
selectBase(restrictions.isRestricted)
|
||||
.where(rl.ID.eq(readListId))
|
||||
.apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } }
|
||||
.apply { if (restrictions.isRestricted) and(restrictions.toCondition(dsl)) }
|
||||
.apply { if (restrictions.isRestricted) and(restrictions.toCondition()) }
|
||||
.fetchAndMap(filterOnLibraryIds, restrictions)
|
||||
.firstOrNull()
|
||||
|
||||
|
|
@ -74,7 +74,7 @@ class ReadListDao(
|
|||
searchCondition
|
||||
.and(b.LIBRARY_ID.inOrNoCondition(belongsToLibraryIds))
|
||||
.and(b.LIBRARY_ID.inOrNoCondition(filterOnLibraryIds))
|
||||
.and(restrictions.toCondition(dsl))
|
||||
.and(restrictions.toCondition())
|
||||
|
||||
val queryIds =
|
||||
if (belongsToLibraryIds == null && filterOnLibraryIds == null && !restrictions.isRestricted)
|
||||
|
|
@ -131,12 +131,12 @@ class ReadListDao(
|
|||
.leftJoin(rlb).on(rl.ID.eq(rlb.READLIST_ID))
|
||||
.apply { if (restrictions.isRestricted) leftJoin(b).on(rlb.BOOK_ID.eq(b.ID)).leftJoin(sd).on(sd.SERIES_ID.eq(b.SERIES_ID)) }
|
||||
.where(rlb.BOOK_ID.eq(containsBookId))
|
||||
.apply { if (restrictions.isRestricted) and(restrictions.toCondition(dsl)) }
|
||||
.apply { if (restrictions.isRestricted) and(restrictions.toCondition()) }
|
||||
|
||||
return selectBase(restrictions.isRestricted)
|
||||
.where(rl.ID.`in`(queryIds))
|
||||
.apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } }
|
||||
.apply { if (restrictions.isRestricted) and(restrictions.toCondition(dsl)) }
|
||||
.apply { if (restrictions.isRestricted) and(restrictions.toCondition()) }
|
||||
.fetchAndMap(filterOnLibraryIds, restrictions)
|
||||
}
|
||||
|
||||
|
|
@ -178,7 +178,7 @@ class ReadListDao(
|
|||
.apply { if (restrictions.isRestricted) leftJoin(sd).on(sd.SERIES_ID.eq(b.SERIES_ID)) }
|
||||
.where(rlb.READLIST_ID.eq(rr.id))
|
||||
.apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } }
|
||||
.apply { if (restrictions.isRestricted) and(restrictions.toCondition(dsl)) }
|
||||
.apply { if (restrictions.isRestricted) and(restrictions.toCondition()) }
|
||||
.orderBy(rlb.NUMBER.asc())
|
||||
.fetchInto(rlb)
|
||||
.mapNotNull { it.number to it.bookId }
|
||||
|
|
|
|||
|
|
@ -503,7 +503,7 @@ class ReferentialDao(
|
|||
.orderBy(sd.PUBLISHER.collate(SqliteUdfDataSource.COLLATION_UNICODE_3))
|
||||
.fetchSet(sd.PUBLISHER)
|
||||
|
||||
override fun findAllAgeRatings(filterOnLibraryIds: Collection<String>?): Set<Int> =
|
||||
override fun findAllAgeRatings(filterOnLibraryIds: Collection<String>?): Set<Int?> =
|
||||
dsl.selectDistinct(sd.AGE_RATING)
|
||||
.from(sd)
|
||||
.apply {
|
||||
|
|
@ -518,7 +518,7 @@ class ReferentialDao(
|
|||
override fun findAllAgeRatingsByLibrary(
|
||||
libraryId: String,
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
): Set<Int> =
|
||||
): Set<Int?> =
|
||||
dsl.selectDistinct(sd.AGE_RATING)
|
||||
.from(sd)
|
||||
.leftJoin(s).on(sd.SERIES_ID.eq(s.ID))
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ class SeriesCollectionDao(
|
|||
selectBase(restrictions.isRestricted)
|
||||
.where(c.ID.eq(collectionId))
|
||||
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
|
||||
.apply { if (restrictions.isRestricted) and(restrictions.toCondition(dsl)) }
|
||||
.apply { if (restrictions.isRestricted) and(restrictions.toCondition()) }
|
||||
.fetchAndMap(filterOnLibraryIds, restrictions)
|
||||
.firstOrNull()
|
||||
|
||||
|
|
@ -71,7 +71,7 @@ class SeriesCollectionDao(
|
|||
searchCondition
|
||||
.and(s.LIBRARY_ID.inOrNoCondition(belongsToLibraryIds))
|
||||
.and(s.LIBRARY_ID.inOrNoCondition(filterOnLibraryIds))
|
||||
.and(restrictions.toCondition(dsl))
|
||||
.and(restrictions.toCondition())
|
||||
|
||||
val queryIds =
|
||||
if (belongsToLibraryIds == null && filterOnLibraryIds == null && !restrictions.isRestricted)
|
||||
|
|
@ -128,12 +128,12 @@ class SeriesCollectionDao(
|
|||
.leftJoin(cs).on(c.ID.eq(cs.COLLECTION_ID))
|
||||
.apply { if (restrictions.isRestricted) leftJoin(sd).on(cs.SERIES_ID.eq(sd.SERIES_ID)) }
|
||||
.where(cs.SERIES_ID.eq(containsSeriesId))
|
||||
.apply { if (restrictions.isRestricted) and(restrictions.toCondition(dsl)) }
|
||||
.apply { if (restrictions.isRestricted) and(restrictions.toCondition()) }
|
||||
|
||||
return selectBase(restrictions.isRestricted)
|
||||
.where(c.ID.`in`(queryIds))
|
||||
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
|
||||
.apply { if (restrictions.isRestricted) and(restrictions.toCondition(dsl)) }
|
||||
.apply { if (restrictions.isRestricted) and(restrictions.toCondition()) }
|
||||
.fetchAndMap(filterOnLibraryIds, restrictions)
|
||||
}
|
||||
|
||||
|
|
@ -175,7 +175,7 @@ class SeriesCollectionDao(
|
|||
.apply { if (restrictions.isRestricted) leftJoin(sd).on(cs.SERIES_ID.eq(sd.SERIES_ID)) }
|
||||
.where(cs.COLLECTION_ID.eq(cr.id))
|
||||
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
|
||||
.apply { if (restrictions.isRestricted) and(restrictions.toCondition(dsl)) }
|
||||
.apply { if (restrictions.isRestricted) and(restrictions.toCondition()) }
|
||||
.orderBy(cs.NUMBER.asc())
|
||||
.fetchInto(cs)
|
||||
.mapNotNull { it.seriesId }
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
package org.gotson.komga.infrastructure.jooq.main
|
||||
|
||||
import org.gotson.komga.domain.model.SearchCondition
|
||||
import org.gotson.komga.domain.model.SearchContext
|
||||
import org.gotson.komga.domain.model.Series
|
||||
import org.gotson.komga.domain.model.SeriesSearch
|
||||
import org.gotson.komga.domain.persistence.SeriesRepository
|
||||
import org.gotson.komga.infrastructure.datasource.SqliteUdfDataSource
|
||||
import org.gotson.komga.infrastructure.jooq.RequiredJoin
|
||||
import org.gotson.komga.infrastructure.jooq.SeriesSearchHelper
|
||||
import org.gotson.komga.infrastructure.jooq.insertTempStrings
|
||||
import org.gotson.komga.infrastructure.jooq.selectTempStrings
|
||||
import org.gotson.komga.jooq.main.Tables
|
||||
|
|
@ -13,6 +15,11 @@ import org.jooq.Condition
|
|||
import org.jooq.DSLContext
|
||||
import org.jooq.impl.DSL
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.data.domain.Page
|
||||
import org.springframework.data.domain.PageImpl
|
||||
import org.springframework.data.domain.PageRequest
|
||||
import org.springframework.data.domain.Pageable
|
||||
import org.springframework.data.domain.Sort
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import java.net.URL
|
||||
|
|
@ -26,8 +33,9 @@ class SeriesDao(
|
|||
) : SeriesRepository {
|
||||
private val s = Tables.SERIES
|
||||
private val d = Tables.SERIES_METADATA
|
||||
private val rs = Tables.READ_PROGRESS_SERIES
|
||||
private val cs = Tables.COLLECTION_SERIES
|
||||
private val l = Tables.LIBRARY
|
||||
private val bma = Tables.BOOK_METADATA_AGGREGATION
|
||||
|
||||
override fun findAll(): Collection<Series> =
|
||||
dsl.selectFrom(s)
|
||||
|
|
@ -93,16 +101,52 @@ class SeriesDao(
|
|||
.where(s.LIBRARY_ID.eq(libraryId))
|
||||
.fetch(s.ID)
|
||||
|
||||
override fun findAll(search: SeriesSearch): Collection<Series> {
|
||||
val conditions = search.toCondition()
|
||||
override fun findAll(
|
||||
searchCondition: SearchCondition.Series?,
|
||||
searchContext: SearchContext,
|
||||
pageable: Pageable,
|
||||
): Page<Series> {
|
||||
val (conditions, joins) = SeriesSearchHelper(searchContext).toCondition(searchCondition)
|
||||
return findAll(conditions, joins, pageable)
|
||||
}
|
||||
|
||||
return dsl.selectDistinct(*s.fields())
|
||||
.from(s)
|
||||
.leftJoin(cs).on(s.ID.eq(cs.SERIES_ID))
|
||||
.leftJoin(d).on(s.ID.eq(d.SERIES_ID))
|
||||
.where(conditions)
|
||||
.fetchInto(s)
|
||||
.map { it.toDomain() }
|
||||
private fun findAll(
|
||||
conditions: Condition,
|
||||
joins: Set<RequiredJoin>,
|
||||
pageable: Pageable,
|
||||
): Page<Series> {
|
||||
val query =
|
||||
dsl.selectDistinct(*s.fields())
|
||||
.from(s)
|
||||
.apply {
|
||||
joins.forEach { join ->
|
||||
when (join) {
|
||||
RequiredJoin.BookMetadataAggregation -> leftJoin(bma).on(s.ID.eq(bma.SERIES_ID))
|
||||
RequiredJoin.SeriesMetadata -> innerJoin(d).on(s.ID.eq(d.SERIES_ID))
|
||||
is RequiredJoin.ReadProgress -> leftJoin(rs).on(rs.SERIES_ID.eq(s.ID)).and(rs.USER_ID.eq(join.userId))
|
||||
// Book joins - not needed
|
||||
RequiredJoin.BookMetadata -> Unit
|
||||
RequiredJoin.Media -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
.where(conditions)
|
||||
|
||||
val count = dsl.fetchCount(query)
|
||||
val items =
|
||||
query
|
||||
.apply { if (pageable.isPaged) limit(pageable.pageSize).offset(pageable.offset) }
|
||||
.fetchInto(s)
|
||||
.map { it.toDomain() }
|
||||
|
||||
return PageImpl(
|
||||
items,
|
||||
if (pageable.isPaged)
|
||||
PageRequest.of(pageable.pageNumber, pageable.pageSize, Sort.unsorted())
|
||||
else
|
||||
PageRequest.of(0, maxOf(count, 20), Sort.unsorted()),
|
||||
count.toLong(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun insert(series: Series) {
|
||||
|
|
@ -157,28 +201,6 @@ class SeriesDao(
|
|||
.groupBy(s.LIBRARY_ID)
|
||||
.fetchMap(s.LIBRARY_ID, DSL.count(s.ID))
|
||||
|
||||
private fun SeriesSearch.toCondition(): Condition {
|
||||
var c: Condition = DSL.trueCondition()
|
||||
|
||||
if (libraryIds != null) c = c.and(s.LIBRARY_ID.`in`(libraryIds))
|
||||
if (!collectionIds.isNullOrEmpty()) c = c.and(cs.COLLECTION_ID.`in`(collectionIds))
|
||||
searchTerm?.let { c = c.and(d.TITLE.containsIgnoreCase(it)) }
|
||||
searchRegex?.let { c = c.and((it.second.toColumn()).likeRegex(it.first)) }
|
||||
if (!metadataStatus.isNullOrEmpty()) c = c.and(d.STATUS.`in`(metadataStatus))
|
||||
if (!publishers.isNullOrEmpty()) c = c.and(d.PUBLISHER.collate(SqliteUdfDataSource.COLLATION_UNICODE_3).`in`(publishers))
|
||||
if (deleted == true) c = c.and(s.DELETED_DATE.isNotNull)
|
||||
if (deleted == false) c = c.and(s.DELETED_DATE.isNull)
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
private fun SeriesSearch.SearchField.toColumn() =
|
||||
when (this) {
|
||||
SeriesSearch.SearchField.NAME -> s.NAME
|
||||
SeriesSearch.SearchField.TITLE -> d.TITLE
|
||||
SeriesSearch.SearchField.TITLE_SORT -> d.TITLE_SORT
|
||||
}
|
||||
|
||||
private fun SeriesRecord.toDomain() =
|
||||
Series(
|
||||
name = name,
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
package org.gotson.komga.infrastructure.jooq.main
|
||||
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.gotson.komga.domain.model.ContentRestrictions
|
||||
import org.gotson.komga.domain.model.ReadStatus
|
||||
import org.gotson.komga.domain.model.SearchContext
|
||||
import org.gotson.komga.domain.model.SearchField
|
||||
import org.gotson.komga.domain.model.SeriesSearch
|
||||
import org.gotson.komga.domain.model.SeriesSearchWithReadProgress
|
||||
import org.gotson.komga.infrastructure.datasource.SqliteUdfDataSource
|
||||
import org.gotson.komga.infrastructure.jooq.RequiredJoin
|
||||
import org.gotson.komga.infrastructure.jooq.SeriesSearchHelper
|
||||
import org.gotson.komga.infrastructure.jooq.inOrNoCondition
|
||||
import org.gotson.komga.infrastructure.jooq.insertTempStrings
|
||||
import org.gotson.komga.infrastructure.jooq.noCase
|
||||
import org.gotson.komga.infrastructure.jooq.selectTempStrings
|
||||
import org.gotson.komga.infrastructure.jooq.sortByValues
|
||||
import org.gotson.komga.infrastructure.jooq.toCondition
|
||||
import org.gotson.komga.infrastructure.jooq.toSortField
|
||||
import org.gotson.komga.infrastructure.search.LuceneEntity
|
||||
import org.gotson.komga.infrastructure.search.LuceneHelper
|
||||
|
|
@ -95,52 +95,50 @@ class SeriesDtoDao(
|
|||
"booksCount" to s.BOOK_COUNT,
|
||||
)
|
||||
|
||||
override fun findAll(pageable: Pageable): Page<SeriesDto> =
|
||||
findAll(SeriesSearch(), SearchContext.ofAnonymousUser(), pageable)
|
||||
|
||||
override fun findAll(
|
||||
search: SeriesSearchWithReadProgress,
|
||||
userId: String,
|
||||
context: SearchContext,
|
||||
pageable: Pageable,
|
||||
restrictions: ContentRestrictions,
|
||||
): Page<SeriesDto> {
|
||||
val conditions = search.toCondition().and(restrictions.toCondition(dsl))
|
||||
): Page<SeriesDto> = findAll(SeriesSearch(), context, pageable)
|
||||
|
||||
return findAll(conditions, userId, pageable, search.toJoinConditions(), search.searchTerm)
|
||||
}
|
||||
|
||||
override fun findAllByCollectionId(
|
||||
collectionId: String,
|
||||
search: SeriesSearchWithReadProgress,
|
||||
userId: String,
|
||||
override fun findAll(
|
||||
search: SeriesSearch,
|
||||
context: SearchContext,
|
||||
pageable: Pageable,
|
||||
restrictions: ContentRestrictions,
|
||||
): Page<SeriesDto> {
|
||||
val conditions = search.toCondition().and(restrictions.toCondition(dsl)).and(cs.COLLECTION_ID.eq(collectionId))
|
||||
val joinConditions = search.toJoinConditions().copy(selectCollectionNumber = true, collection = true)
|
||||
requireNotNull(context.userId) { "Missing userId in search context" }
|
||||
|
||||
return findAll(conditions, userId, pageable, joinConditions, search.searchTerm)
|
||||
val (conditions, joins) = SeriesSearchHelper(context).toCondition(search.condition)
|
||||
val conditionsRefined = conditions.and(search.regexSearch?.let { it.second.toColumn().likeRegex(it.first) } ?: DSL.noCondition())
|
||||
|
||||
return findAll(conditionsRefined, context.userId, pageable, joins, search.fullTextSearch)
|
||||
}
|
||||
|
||||
override fun findAllRecentlyUpdated(
|
||||
search: SeriesSearchWithReadProgress,
|
||||
userId: String,
|
||||
restrictions: ContentRestrictions,
|
||||
search: SeriesSearch,
|
||||
context: SearchContext,
|
||||
pageable: Pageable,
|
||||
): Page<SeriesDto> {
|
||||
val conditions =
|
||||
search.toCondition()
|
||||
.and(restrictions.toCondition(dsl))
|
||||
.and(s.CREATED_DATE.notEqual(s.LAST_MODIFIED_DATE))
|
||||
requireNotNull(context.userId) { "Missing userId in search context" }
|
||||
|
||||
return findAll(conditions, userId, pageable, search.toJoinConditions(), search.searchTerm)
|
||||
val (conditions, joins) = SeriesSearchHelper(context).toCondition(search.condition)
|
||||
val conditionsRefined = conditions.and(s.CREATED_DATE.notEqual(s.LAST_MODIFIED_DATE))
|
||||
|
||||
return findAll(conditionsRefined, context.userId, pageable, joins, search.fullTextSearch)
|
||||
}
|
||||
|
||||
override fun countByFirstCharacter(
|
||||
search: SeriesSearchWithReadProgress,
|
||||
userId: String,
|
||||
restrictions: ContentRestrictions,
|
||||
search: SeriesSearch,
|
||||
context: SearchContext,
|
||||
): List<GroupCountDto> {
|
||||
val conditions = search.toCondition().and(restrictions.toCondition(dsl))
|
||||
val joinConditions = search.toJoinConditions()
|
||||
val seriesIds = luceneHelper.searchEntitiesIds(search.searchTerm, LuceneEntity.Series)
|
||||
requireNotNull(context.userId) { "Missing userId in search context" }
|
||||
|
||||
val (conditions, joins) = SeriesSearchHelper(context).toCondition(search.condition)
|
||||
val conditionsRefined = conditions.and(search.regexSearch?.let { it.second.toColumn().likeRegex(it.first) } ?: DSL.noCondition())
|
||||
|
||||
val seriesIds = luceneHelper.searchEntitiesIds(search.fullTextSearch, LuceneEntity.Series)
|
||||
val searchCondition = s.ID.inOrNoCondition(seriesIds)
|
||||
|
||||
val firstChar = lower(substring(d.TITLE_SORT, 1, 1))
|
||||
|
|
@ -148,17 +146,21 @@ class SeriesDtoDao(
|
|||
.from(s)
|
||||
.leftJoin(d).on(s.ID.eq(d.SERIES_ID))
|
||||
.leftJoin(bma).on(s.ID.eq(bma.SERIES_ID))
|
||||
.leftJoin(rs).on(s.ID.eq(rs.SERIES_ID)).and(readProgressConditionSeries(userId))
|
||||
.apply { if (joinConditions.genre) leftJoin(g).on(s.ID.eq(g.SERIES_ID)) }
|
||||
.leftJoin(rs).on(s.ID.eq(rs.SERIES_ID)).and(readProgressConditionSeries(context.userId))
|
||||
.apply {
|
||||
if (joinConditions.tag)
|
||||
leftJoin(st).on(s.ID.eq(st.SERIES_ID))
|
||||
.leftJoin(bmat).on(s.ID.eq(bmat.SERIES_ID))
|
||||
joins.forEach { join ->
|
||||
when (join) {
|
||||
// always joined
|
||||
is RequiredJoin.ReadProgress -> Unit
|
||||
RequiredJoin.SeriesMetadata -> Unit
|
||||
// Book joins - not needed
|
||||
RequiredJoin.Media -> Unit
|
||||
RequiredJoin.BookMetadata -> Unit
|
||||
RequiredJoin.BookMetadataAggregation -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
.apply { if (joinConditions.collection) leftJoin(cs).on(s.ID.eq(cs.SERIES_ID)) }
|
||||
.apply { if (joinConditions.aggregationAuthor) leftJoin(bmaa).on(s.ID.eq(bmaa.SERIES_ID)) }
|
||||
.apply { if (joinConditions.sharingLabel) leftJoin(sl).on(s.ID.eq(sl.SERIES_ID)) }
|
||||
.where(conditions)
|
||||
.where(conditionsRefined)
|
||||
.and(searchCondition)
|
||||
.groupBy(firstChar)
|
||||
.map {
|
||||
|
|
@ -178,30 +180,36 @@ class SeriesDtoDao(
|
|||
|
||||
private fun selectBase(
|
||||
userId: String,
|
||||
joinConditions: JoinConditions = JoinConditions(),
|
||||
joins: Set<RequiredJoin> = emptySet(),
|
||||
joinOnCollection: Boolean = false,
|
||||
): SelectOnConditionStep<Record> =
|
||||
dsl.selectDistinct(*groupFields)
|
||||
.apply { if (joinConditions.selectCollectionNumber) select(cs.NUMBER) }
|
||||
dsl
|
||||
.let { if (joinOnCollection) it.selectDistinct(*groupFields) else it.select(*groupFields) }
|
||||
.from(s)
|
||||
.leftJoin(d).on(s.ID.eq(d.SERIES_ID))
|
||||
.leftJoin(bma).on(s.ID.eq(bma.SERIES_ID))
|
||||
.leftJoin(rs).on(s.ID.eq(rs.SERIES_ID)).and(readProgressConditionSeries(userId))
|
||||
.apply { if (joinConditions.genre) leftJoin(g).on(s.ID.eq(g.SERIES_ID)) }
|
||||
.apply {
|
||||
if (joinConditions.tag)
|
||||
leftJoin(st).on(s.ID.eq(st.SERIES_ID))
|
||||
.leftJoin(bmat).on(s.ID.eq(bmat.SERIES_ID))
|
||||
if (joinOnCollection)leftJoin(cs).on(s.ID.eq(cs.SERIES_ID))
|
||||
joins.forEach { join ->
|
||||
when (join) {
|
||||
// always joined
|
||||
is RequiredJoin.ReadProgress -> Unit
|
||||
RequiredJoin.SeriesMetadata -> Unit
|
||||
// Book joins - not needed
|
||||
RequiredJoin.BookMetadata -> Unit
|
||||
RequiredJoin.BookMetadataAggregation -> Unit
|
||||
RequiredJoin.Media -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
.apply { if (joinConditions.collection) leftJoin(cs).on(s.ID.eq(cs.SERIES_ID)) }
|
||||
.apply { if (joinConditions.aggregationAuthor) leftJoin(bmaa).on(s.ID.eq(bmaa.SERIES_ID)) }
|
||||
.apply { if (joinConditions.sharingLabel) leftJoin(sl).on(s.ID.eq(sl.SERIES_ID)) }
|
||||
|
||||
private fun findAll(
|
||||
conditions: Condition,
|
||||
userId: String,
|
||||
pageable: Pageable,
|
||||
joinConditions: JoinConditions = JoinConditions(),
|
||||
searchTerm: String?,
|
||||
joins: Set<RequiredJoin> = emptySet(),
|
||||
searchTerm: String? = null,
|
||||
): Page<SeriesDto> {
|
||||
val seriesIds = luceneHelper.searchEntitiesIds(searchTerm, LuceneEntity.Series)
|
||||
val searchCondition = s.ID.inOrNoCondition(seriesIds)
|
||||
|
|
@ -212,15 +220,19 @@ class SeriesDtoDao(
|
|||
.leftJoin(d).on(s.ID.eq(d.SERIES_ID))
|
||||
.leftJoin(bma).on(s.ID.eq(bma.SERIES_ID))
|
||||
.leftJoin(rs).on(s.ID.eq(rs.SERIES_ID)).and(readProgressConditionSeries(userId))
|
||||
.apply { if (joinConditions.genre) leftJoin(g).on(s.ID.eq(g.SERIES_ID)) }
|
||||
.apply {
|
||||
if (joinConditions.tag)
|
||||
leftJoin(st).on(s.ID.eq(st.SERIES_ID))
|
||||
.leftJoin(bmat).on(s.ID.eq(bmat.SERIES_ID))
|
||||
joins.forEach { join ->
|
||||
when (join) {
|
||||
// always joined
|
||||
is RequiredJoin.ReadProgress -> Unit
|
||||
RequiredJoin.SeriesMetadata -> Unit
|
||||
// Book joins - not needed
|
||||
RequiredJoin.BookMetadata -> Unit
|
||||
RequiredJoin.BookMetadataAggregation -> Unit
|
||||
RequiredJoin.Media -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
.apply { if (joinConditions.collection) leftJoin(cs).on(s.ID.eq(cs.SERIES_ID)) }
|
||||
.apply { if (joinConditions.aggregationAuthor) leftJoin(bmaa).on(s.ID.eq(bmaa.SERIES_ID)) }
|
||||
.apply { if (joinConditions.sharingLabel) leftJoin(sl).on(s.ID.eq(sl.SERIES_ID)) }
|
||||
.where(conditions)
|
||||
.and(searchCondition)
|
||||
.fetchOne(countDistinct(s.ID)) ?: 0
|
||||
|
|
@ -234,7 +246,7 @@ class SeriesDtoDao(
|
|||
}
|
||||
|
||||
val dtos =
|
||||
selectBase(userId, joinConditions)
|
||||
selectBase(userId, joins, pageable.sort.any { it.property == "collection.number" })
|
||||
.where(conditions)
|
||||
.and(searchCondition)
|
||||
.orderBy(orderBy)
|
||||
|
|
@ -325,77 +337,12 @@ class SeriesDtoDao(
|
|||
}
|
||||
}
|
||||
|
||||
private fun SeriesSearchWithReadProgress.toCondition(): Condition {
|
||||
var c = DSL.noCondition()
|
||||
|
||||
if (libraryIds != null) c = c.and(s.LIBRARY_ID.`in`(libraryIds))
|
||||
if (!collectionIds.isNullOrEmpty()) c = c.and(cs.COLLECTION_ID.`in`(collectionIds))
|
||||
searchRegex?.let { c = c.and((it.second.toColumn()).likeRegex(it.first)) }
|
||||
if (!metadataStatus.isNullOrEmpty()) c = c.and(d.STATUS.`in`(metadataStatus))
|
||||
if (!publishers.isNullOrEmpty()) c = c.and(d.PUBLISHER.collate(SqliteUdfDataSource.COLLATION_UNICODE_3).`in`(publishers))
|
||||
if (deleted == true) c = c.and(s.DELETED_DATE.isNotNull)
|
||||
if (deleted == false) c = c.and(s.DELETED_DATE.isNull)
|
||||
if (complete == false) c = c.and(d.TOTAL_BOOK_COUNT.isNotNull.and(d.TOTAL_BOOK_COUNT.ne(s.BOOK_COUNT)))
|
||||
if (complete == true) c = c.and(d.TOTAL_BOOK_COUNT.isNotNull.and(d.TOTAL_BOOK_COUNT.eq(s.BOOK_COUNT)))
|
||||
if (oneshot != null) c = c.and(s.ONESHOT.eq(oneshot))
|
||||
if (!languages.isNullOrEmpty()) c = c.and(d.LANGUAGE.collate(SqliteUdfDataSource.COLLATION_UNICODE_3).`in`(languages))
|
||||
if (!genres.isNullOrEmpty()) c = c.and(g.GENRE.collate(SqliteUdfDataSource.COLLATION_UNICODE_3).`in`(genres))
|
||||
if (!tags.isNullOrEmpty()) c = c.and(st.TAG.collate(SqliteUdfDataSource.COLLATION_UNICODE_3).`in`(tags).or(bmat.TAG.collate(SqliteUdfDataSource.COLLATION_UNICODE_3).`in`(tags)))
|
||||
if (!ageRatings.isNullOrEmpty()) {
|
||||
val c1 = if (ageRatings.contains(null)) d.AGE_RATING.isNull else DSL.noCondition()
|
||||
val c2 = if (ageRatings.filterNotNull().isNotEmpty()) d.AGE_RATING.`in`(ageRatings.filterNotNull()) else DSL.noCondition()
|
||||
c = c.and(c1.or(c2))
|
||||
}
|
||||
// cast to String is necessary for SQLite, else the years in the IN block are coerced to Int, even though YEAR for SQLite uses strftime (string)
|
||||
if (!releaseYears.isNullOrEmpty()) c = c.and(DSL.year(bma.RELEASE_DATE).cast(String::class.java).`in`(releaseYears))
|
||||
if (!authors.isNullOrEmpty()) {
|
||||
var ca = DSL.noCondition()
|
||||
authors.forEach {
|
||||
ca = ca.or(bmaa.NAME.equalIgnoreCase(it.name).and(bmaa.ROLE.equalIgnoreCase(it.role)))
|
||||
}
|
||||
c = c.and(ca)
|
||||
}
|
||||
if (!sharingLabels.isNullOrEmpty()) c = c.and(sl.LABEL.collate(SqliteUdfDataSource.COLLATION_UNICODE_3).`in`(sharingLabels))
|
||||
if (!readStatus.isNullOrEmpty()) {
|
||||
val cr =
|
||||
readStatus.map {
|
||||
when (it) {
|
||||
ReadStatus.UNREAD -> rs.READ_COUNT.isNull
|
||||
ReadStatus.READ -> rs.READ_COUNT.eq(s.BOOK_COUNT)
|
||||
ReadStatus.IN_PROGRESS -> rs.READ_COUNT.ne(s.BOOK_COUNT)
|
||||
}
|
||||
}.reduce { acc, condition -> acc.or(condition) }
|
||||
c = c.and(cr)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
private fun SeriesSearch.SearchField.toColumn() =
|
||||
private fun SearchField.toColumn() =
|
||||
when (this) {
|
||||
SeriesSearch.SearchField.NAME -> s.NAME
|
||||
SeriesSearch.SearchField.TITLE -> d.TITLE
|
||||
SeriesSearch.SearchField.TITLE_SORT -> d.TITLE_SORT
|
||||
SearchField.TITLE -> d.TITLE
|
||||
SearchField.TITLE_SORT -> d.TITLE_SORT
|
||||
}
|
||||
|
||||
private fun SeriesSearchWithReadProgress.toJoinConditions() =
|
||||
JoinConditions(
|
||||
genre = !genres.isNullOrEmpty(),
|
||||
tag = !tags.isNullOrEmpty(),
|
||||
collection = !collectionIds.isNullOrEmpty(),
|
||||
aggregationAuthor = !authors.isNullOrEmpty(),
|
||||
sharingLabel = !sharingLabels.isNullOrEmpty(),
|
||||
)
|
||||
|
||||
private data class JoinConditions(
|
||||
val selectCollectionNumber: Boolean = false,
|
||||
val genre: Boolean = false,
|
||||
val tag: Boolean = false,
|
||||
val collection: Boolean = false,
|
||||
val aggregationAuthor: Boolean = false,
|
||||
val sharingLabel: Boolean = false,
|
||||
)
|
||||
|
||||
private fun SeriesRecord.toDto(
|
||||
booksCount: Int,
|
||||
booksReadCount: Int,
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@ package org.gotson.komga.infrastructure.jooq.main
|
|||
|
||||
import com.github.f4b6a3.tsid.TsidCreator
|
||||
import org.gotson.komga.domain.model.BookSearch
|
||||
import org.gotson.komga.domain.model.KomgaUser
|
||||
import org.gotson.komga.domain.model.SearchContext
|
||||
import org.gotson.komga.domain.model.SyncPoint
|
||||
import org.gotson.komga.domain.model.SyncPoint.ReadList.Companion.ON_DECK_ID
|
||||
import org.gotson.komga.domain.persistence.SyncPointRepository
|
||||
import org.gotson.komga.infrastructure.jooq.toCondition
|
||||
import org.gotson.komga.infrastructure.jooq.BookSearchHelper
|
||||
import org.gotson.komga.infrastructure.jooq.RequiredJoin
|
||||
import org.gotson.komga.jooq.main.Tables
|
||||
import org.gotson.komga.language.toZonedDateTime
|
||||
import org.jooq.DSLContext
|
||||
|
|
@ -44,11 +45,13 @@ class SyncPointDao(
|
|||
|
||||
@Transactional
|
||||
override fun create(
|
||||
user: KomgaUser,
|
||||
apiKeyId: String?,
|
||||
search: BookSearch,
|
||||
context: SearchContext,
|
||||
): SyncPoint {
|
||||
val conditions = search.toCondition().and(user.restrictions.toCondition(dsl))
|
||||
requireNotNull(context.userId) { "userId is required to create a SyncPoint" }
|
||||
|
||||
val (condition, joins) = BookSearchHelper(context).toCondition(search.condition)
|
||||
|
||||
val syncPointId = TsidCreator.getTsid256().toString()
|
||||
val createdAt = LocalDateTime.now(ZoneId.of("Z"))
|
||||
|
|
@ -61,7 +64,7 @@ class SyncPointDao(
|
|||
sp.CREATED_DATE,
|
||||
).values(
|
||||
syncPointId,
|
||||
user.id,
|
||||
context.userId,
|
||||
apiKeyId,
|
||||
createdAt,
|
||||
).execute()
|
||||
|
|
@ -91,12 +94,24 @@ class SyncPointDao(
|
|||
r.LAST_MODIFIED_DATE,
|
||||
bt.ID,
|
||||
).from(b)
|
||||
.apply {
|
||||
joins.forEach {
|
||||
when (it) {
|
||||
// we don't have to handle those since we already join on those tables anyway, the 'when' is here for future proofing
|
||||
RequiredJoin.BookMetadata -> Unit
|
||||
RequiredJoin.SeriesMetadata -> Unit
|
||||
RequiredJoin.Media -> Unit
|
||||
is RequiredJoin.ReadProgress -> Unit
|
||||
RequiredJoin.BookMetadataAggregation -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
.join(m).on(b.ID.eq(m.BOOK_ID))
|
||||
.join(d).on(b.ID.eq(d.BOOK_ID))
|
||||
.join(sd).on(b.SERIES_ID.eq(sd.SERIES_ID))
|
||||
.leftJoin(r).on(b.ID.eq(r.BOOK_ID)).and(r.USER_ID.eq(user.id))
|
||||
.leftJoin(r).on(b.ID.eq(r.BOOK_ID)).and(r.USER_ID.eq(context.userId))
|
||||
.leftJoin(bt).on(b.ID.eq(bt.BOOK_ID)).and(bt.SELECTED.isTrue)
|
||||
.where(conditions),
|
||||
.where(condition),
|
||||
).execute()
|
||||
|
||||
return findByIdOrNull(syncPointId)!!
|
||||
|
|
@ -105,13 +120,15 @@ class SyncPointDao(
|
|||
@Transactional
|
||||
override fun addOnDeck(
|
||||
syncPointId: String,
|
||||
user: KomgaUser,
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
context: SearchContext,
|
||||
filterOnLibraryIds: List<String>?,
|
||||
) {
|
||||
requireNotNull(context.userId) { "Missing userId in search context" }
|
||||
|
||||
val createdAt = LocalDateTime.now(ZoneId.of("Z"))
|
||||
val onDeckFields: Array<Field<*>> = arrayOf(DSL.`val`(syncPointId), DSL.`val`(ON_DECK_ID), b.ID)
|
||||
|
||||
val (query, _, queryMostRecentDate) = bookCommonDao.getBooksOnDeckQuery(user.id, user.restrictions, filterOnLibraryIds, onDeckFields)
|
||||
val (query, _, queryMostRecentDate) = bookCommonDao.getBooksOnDeckQuery(context.userId, context.restrictions, filterOnLibraryIds, onDeckFields)
|
||||
|
||||
val count =
|
||||
dsl.insertInto(sprlb)
|
||||
|
|
|
|||
|
|
@ -3,11 +3,9 @@ package org.gotson.komga.infrastructure.search
|
|||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.apache.lucene.document.Document
|
||||
import org.apache.lucene.index.Term
|
||||
import org.gotson.komga.domain.model.BookSearchWithReadProgress
|
||||
import org.gotson.komga.domain.model.DomainEvent
|
||||
import org.gotson.komga.domain.model.ReadList
|
||||
import org.gotson.komga.domain.model.SeriesCollection
|
||||
import org.gotson.komga.domain.model.SeriesSearchWithReadProgress
|
||||
import org.gotson.komga.domain.persistence.ReadListRepository
|
||||
import org.gotson.komga.domain.persistence.SeriesCollectionRepository
|
||||
import org.gotson.komga.interfaces.api.persistence.BookDtoRepository
|
||||
|
|
@ -45,8 +43,8 @@ class SearchIndexLifecycle(
|
|||
|
||||
targetEntities.forEach {
|
||||
when (it) {
|
||||
LuceneEntity.Book -> rebuildIndex(it, { p: Pageable -> bookDtoRepository.findAll(BookSearchWithReadProgress(), "unused", p) }, { e: BookDto -> e.bookToDocument() })
|
||||
LuceneEntity.Series -> rebuildIndex(it, { p: Pageable -> seriesDtoRepository.findAll(SeriesSearchWithReadProgress(), "unused", p) }, { e: SeriesDto -> e.toDocument() })
|
||||
LuceneEntity.Book -> rebuildIndex(it, { p: Pageable -> bookDtoRepository.findAll(p) }, { e: BookDto -> e.bookToDocument() })
|
||||
LuceneEntity.Series -> rebuildIndex(it, { p: Pageable -> seriesDtoRepository.findAll(p) }, { e: SeriesDto -> e.toDocument() })
|
||||
LuceneEntity.Collection -> rebuildIndex(it, { p: Pageable -> collectionRepository.findAll(pageable = p) }, { e: SeriesCollection -> e.toDocument() })
|
||||
LuceneEntity.ReadList -> rebuildIndex(it, { p: Pageable -> readListRepository.findAll(pageable = p) }, { e: ReadList -> e.toDocument() })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package org.gotson.komga.infrastructure.web
|
|||
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@Target(AnnotationTarget.VALUE_PARAMETER)
|
||||
@Deprecated("was used only for search_regex which is deprecated")
|
||||
annotation class DelimitedPair(
|
||||
val parameterName: String,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,15 +6,18 @@ import io.swagger.v3.oas.annotations.media.Content
|
|||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
||||
import org.apache.commons.io.FilenameUtils
|
||||
import org.gotson.komga.domain.model.BookSearchWithReadProgress
|
||||
import org.gotson.komga.domain.model.BookSearch
|
||||
import org.gotson.komga.domain.model.Library
|
||||
import org.gotson.komga.domain.model.Media
|
||||
import org.gotson.komga.domain.model.MediaProfile
|
||||
import org.gotson.komga.domain.model.ROLE_PAGE_STREAMING
|
||||
import org.gotson.komga.domain.model.ReadList
|
||||
import org.gotson.komga.domain.model.ReadStatus
|
||||
import org.gotson.komga.domain.model.SearchCondition
|
||||
import org.gotson.komga.domain.model.SearchContext
|
||||
import org.gotson.komga.domain.model.SearchOperator
|
||||
import org.gotson.komga.domain.model.SeriesCollection
|
||||
import org.gotson.komga.domain.model.SeriesSearchWithReadProgress
|
||||
import org.gotson.komga.domain.model.SeriesSearch
|
||||
import org.gotson.komga.domain.model.ThumbnailBook
|
||||
import org.gotson.komga.domain.persistence.LibraryRepository
|
||||
import org.gotson.komga.domain.persistence.MediaRepository
|
||||
|
|
@ -281,19 +284,19 @@ class OpdsController(
|
|||
val pageable = PageRequest.of(page.pageNumber, page.pageSize, Sort.by(Sort.Order.desc("readProgress.readDate")))
|
||||
|
||||
val bookSearch =
|
||||
BookSearchWithReadProgress(
|
||||
libraryIds = principal.user.getAuthorizedLibraryIds(null),
|
||||
readStatus = setOf(ReadStatus.IN_PROGRESS),
|
||||
mediaStatus = setOf(Media.Status.READY),
|
||||
deleted = false,
|
||||
BookSearch(
|
||||
SearchCondition.AllOfBook(
|
||||
SearchCondition.ReadStatus(SearchOperator.Is(ReadStatus.IN_PROGRESS)),
|
||||
SearchCondition.MediaStatus(SearchOperator.Is(Media.Status.READY)),
|
||||
SearchCondition.Deleted(SearchOperator.IsFalse),
|
||||
),
|
||||
)
|
||||
|
||||
val bookPage =
|
||||
bookDtoRepository.findAll(
|
||||
bookSearch,
|
||||
principal.user.id,
|
||||
SearchContext(principal.user),
|
||||
pageable,
|
||||
principal.user.restrictions,
|
||||
)
|
||||
|
||||
val builder = uriBuilder(ROUTE_ON_DECK)
|
||||
|
|
@ -329,14 +332,17 @@ class OpdsController(
|
|||
val pageable = PageRequest.of(page.pageNumber, page.pageSize, sort)
|
||||
|
||||
val seriesSearch =
|
||||
SeriesSearchWithReadProgress(
|
||||
libraryIds = principal.user.getAuthorizedLibraryIds(null),
|
||||
searchTerm = searchTerm,
|
||||
publishers = publishers,
|
||||
deleted = false,
|
||||
SeriesSearch(
|
||||
SearchCondition.AllOfSeries(
|
||||
buildList {
|
||||
searchTerm?.let { add(SearchCondition.Title(SearchOperator.Contains(it))) }
|
||||
publishers?.let { add(SearchCondition.AnyOfSeries(it.map { publisher -> SearchCondition.Publisher(SearchOperator.Is(publisher)) })) }
|
||||
add(SearchCondition.Deleted(SearchOperator.IsFalse))
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
val seriesPage = seriesDtoRepository.findAll(seriesSearch, principal.user.id, pageable, principal.user.restrictions)
|
||||
val seriesPage = seriesDtoRepository.findAll(seriesSearch, SearchContext(principal.user), pageable)
|
||||
|
||||
val builder =
|
||||
uriBuilder(ROUTE_SERIES_ALL)
|
||||
|
|
@ -366,13 +372,9 @@ class OpdsController(
|
|||
): OpdsFeed {
|
||||
val pageable = PageRequest.of(page.pageNumber, page.pageSize, Sort.by(Sort.Order.desc("lastModified")))
|
||||
|
||||
val seriesSearch =
|
||||
SeriesSearchWithReadProgress(
|
||||
libraryIds = principal.user.getAuthorizedLibraryIds(null),
|
||||
deleted = false,
|
||||
)
|
||||
val seriesSearch = SeriesSearch(SearchCondition.Deleted(SearchOperator.IsFalse))
|
||||
|
||||
val seriesPage = seriesDtoRepository.findAll(seriesSearch, principal.user.id, pageable, principal.user.restrictions)
|
||||
val seriesPage = seriesDtoRepository.findAll(seriesSearch, SearchContext(principal.user), pageable)
|
||||
|
||||
val uriBuilder = uriBuilder(ROUTE_SERIES_LATEST)
|
||||
|
||||
|
|
@ -398,14 +400,15 @@ class OpdsController(
|
|||
@Parameter(hidden = true) page: Pageable,
|
||||
): OpdsFeed {
|
||||
val bookSearch =
|
||||
BookSearchWithReadProgress(
|
||||
libraryIds = principal.user.getAuthorizedLibraryIds(null),
|
||||
mediaStatus = setOf(Media.Status.READY),
|
||||
deleted = false,
|
||||
BookSearch(
|
||||
SearchCondition.AllOfBook(
|
||||
SearchCondition.MediaStatus(SearchOperator.Is(Media.Status.READY)),
|
||||
SearchCondition.Deleted(SearchOperator.IsFalse),
|
||||
),
|
||||
)
|
||||
val pageable = PageRequest.of(page.pageNumber, page.pageSize, Sort.by(Sort.Order.desc("createdDate")))
|
||||
|
||||
val bookPage = bookDtoRepository.findAll(bookSearch, principal.user.id, pageable, principal.user.restrictions)
|
||||
val bookPage = bookDtoRepository.findAll(bookSearch, SearchContext(principal.user), pageable)
|
||||
|
||||
val uriBuilder = uriBuilder(ROUTE_BOOKS_LATEST)
|
||||
|
||||
|
|
@ -545,15 +548,17 @@ class OpdsController(
|
|||
contentRestrictionChecker.checkContentRestriction(principal.user, series)
|
||||
|
||||
val bookSearch =
|
||||
BookSearchWithReadProgress(
|
||||
seriesIds = listOf(id),
|
||||
mediaStatus = setOf(Media.Status.READY),
|
||||
deleted = false,
|
||||
BookSearch(
|
||||
SearchCondition.AllOfBook(
|
||||
SearchCondition.SeriesId(SearchOperator.Is(series.id)),
|
||||
SearchCondition.MediaStatus(SearchOperator.Is(Media.Status.READY)),
|
||||
SearchCondition.Deleted(SearchOperator.IsFalse),
|
||||
),
|
||||
)
|
||||
val pageable = PageRequest.of(page.pageNumber, page.pageSize, Sort.by(Sort.Order.asc("metadata.numberSort")))
|
||||
|
||||
val entries =
|
||||
bookDtoRepository.findAll(bookSearch, principal.user.id, pageable, principal.user.restrictions)
|
||||
bookDtoRepository.findAll(bookSearch, SearchContext(principal.user), pageable)
|
||||
.map { it.toOpdsEntry(mediaRepository.findById(it.id)) }
|
||||
|
||||
val uriBuilder = uriBuilder("series/$id")
|
||||
|
|
@ -584,15 +589,17 @@ class OpdsController(
|
|||
if (!principal.user.canAccessLibrary(library)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
|
||||
val seriesSearch =
|
||||
SeriesSearchWithReadProgress(
|
||||
libraryIds = setOf(library.id),
|
||||
deleted = false,
|
||||
SeriesSearch(
|
||||
SearchCondition.AllOfSeries(
|
||||
SearchCondition.LibraryId(SearchOperator.Is(library.id)),
|
||||
SearchCondition.Deleted(SearchOperator.IsFalse),
|
||||
),
|
||||
)
|
||||
|
||||
val pageable = PageRequest.of(page.pageNumber, page.pageSize, Sort.by(Sort.Order.asc("metadata.titleSort")))
|
||||
|
||||
val entries =
|
||||
seriesDtoRepository.findAll(seriesSearch, principal.user.id, pageable, principal.user.restrictions)
|
||||
seriesDtoRepository.findAll(seriesSearch, SearchContext(principal.user), pageable)
|
||||
.map { it.toOpdsEntry() }
|
||||
|
||||
val uriBuilder = uriBuilder("libraries/$id")
|
||||
|
|
@ -628,13 +635,15 @@ class OpdsController(
|
|||
val pageable = PageRequest.of(page.pageNumber, page.pageSize, sort)
|
||||
|
||||
val seriesSearch =
|
||||
SeriesSearchWithReadProgress(
|
||||
libraryIds = principal.user.getAuthorizedLibraryIds(null),
|
||||
deleted = false,
|
||||
SeriesSearch(
|
||||
SearchCondition.AllOfSeries(
|
||||
SearchCondition.CollectionId(SearchOperator.Is(collection.id)),
|
||||
SearchCondition.Deleted(SearchOperator.IsFalse),
|
||||
),
|
||||
)
|
||||
|
||||
val entries =
|
||||
seriesDtoRepository.findAllByCollectionId(collection.id, seriesSearch, principal.user.id, pageable, principal.user.restrictions)
|
||||
seriesDtoRepository.findAll(seriesSearch, SearchContext(principal.user), pageable)
|
||||
.map { it.toOpdsEntry() }
|
||||
|
||||
val uriBuilder = uriBuilder("collections/$id")
|
||||
|
|
@ -670,20 +679,14 @@ class OpdsController(
|
|||
val pageable = PageRequest.of(page.pageNumber, page.pageSize, sort)
|
||||
|
||||
val bookSearch =
|
||||
BookSearchWithReadProgress(
|
||||
mediaStatus = setOf(Media.Status.READY),
|
||||
deleted = false,
|
||||
)
|
||||
|
||||
val booksPage =
|
||||
bookDtoRepository.findAllByReadListId(
|
||||
readList.id,
|
||||
principal.user.id,
|
||||
principal.user.getAuthorizedLibraryIds(null),
|
||||
bookSearch,
|
||||
pageable,
|
||||
principal.user.restrictions,
|
||||
BookSearch(
|
||||
SearchCondition.AllOfBook(
|
||||
SearchCondition.ReadListId(SearchOperator.Is(readList.id)),
|
||||
SearchCondition.MediaStatus(SearchOperator.Is(Media.Status.READY)),
|
||||
SearchCondition.Deleted(SearchOperator.IsFalse),
|
||||
),
|
||||
)
|
||||
val booksPage = bookDtoRepository.findAll(bookSearch, SearchContext(principal.user), pageable)
|
||||
|
||||
val entries =
|
||||
booksPage.map { bookDto ->
|
||||
|
|
|
|||
|
|
@ -4,15 +4,18 @@ import io.swagger.v3.oas.annotations.Parameter
|
|||
import io.swagger.v3.oas.annotations.media.Content
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
||||
import org.gotson.komga.domain.model.BookSearchWithReadProgress
|
||||
import org.gotson.komga.domain.model.BookSearch
|
||||
import org.gotson.komga.domain.model.KomgaUser
|
||||
import org.gotson.komga.domain.model.Library
|
||||
import org.gotson.komga.domain.model.Media
|
||||
import org.gotson.komga.domain.model.ROLE_PAGE_STREAMING
|
||||
import org.gotson.komga.domain.model.ReadList
|
||||
import org.gotson.komga.domain.model.ReadStatus
|
||||
import org.gotson.komga.domain.model.SearchCondition
|
||||
import org.gotson.komga.domain.model.SearchContext
|
||||
import org.gotson.komga.domain.model.SearchOperator
|
||||
import org.gotson.komga.domain.model.SeriesCollection
|
||||
import org.gotson.komga.domain.model.SeriesSearchWithReadProgress
|
||||
import org.gotson.komga.domain.model.SeriesSearch
|
||||
import org.gotson.komga.domain.persistence.LibraryRepository
|
||||
import org.gotson.komga.domain.persistence.ReadListRepository
|
||||
import org.gotson.komga.domain.persistence.ReferentialRepository
|
||||
|
|
@ -181,15 +184,17 @@ class Opds2Controller(
|
|||
|
||||
val keepReading =
|
||||
bookDtoRepository.findAll(
|
||||
BookSearchWithReadProgress(
|
||||
libraryIds = authorizedLibraryIds,
|
||||
readStatus = setOf(ReadStatus.IN_PROGRESS),
|
||||
mediaStatus = setOf(Media.Status.READY),
|
||||
deleted = false,
|
||||
BookSearch(
|
||||
SearchCondition.AllOfBook(
|
||||
buildList {
|
||||
if (library != null) add(SearchCondition.LibraryId(SearchOperator.Is(library.id)))
|
||||
add(SearchCondition.MediaStatus(SearchOperator.Is(Media.Status.READY)))
|
||||
add(SearchCondition.Deleted(SearchOperator.IsFalse))
|
||||
},
|
||||
),
|
||||
),
|
||||
principal.user.id,
|
||||
SearchContext(principal.user),
|
||||
PageRequest.of(0, RECOMMENDED_ITEMS_NUMBER, Sort.by(Sort.Order.desc("readProgress.readDate"))),
|
||||
principal.user.restrictions,
|
||||
).map { opdsGenerator.toOpdsPublicationDto(it) }
|
||||
|
||||
val onDeck =
|
||||
|
|
@ -202,26 +207,32 @@ class Opds2Controller(
|
|||
|
||||
val latestBooks =
|
||||
bookDtoRepository.findAll(
|
||||
BookSearchWithReadProgress(
|
||||
libraryIds = authorizedLibraryIds,
|
||||
mediaStatus = setOf(Media.Status.READY),
|
||||
deleted = false,
|
||||
BookSearch(
|
||||
SearchCondition.AllOfBook(
|
||||
buildList {
|
||||
if (library != null) add(SearchCondition.LibraryId(SearchOperator.Is(library.id)))
|
||||
add(SearchCondition.MediaStatus(SearchOperator.Is(Media.Status.READY)))
|
||||
add(SearchCondition.Deleted(SearchOperator.IsFalse))
|
||||
},
|
||||
),
|
||||
),
|
||||
principal.user.id,
|
||||
SearchContext(principal.user),
|
||||
PageRequest.of(0, RECOMMENDED_ITEMS_NUMBER, Sort.by(Sort.Order.desc("createdDate"))),
|
||||
principal.user.restrictions,
|
||||
).map { opdsGenerator.toOpdsPublicationDto(it) }
|
||||
|
||||
val latestSeries =
|
||||
seriesDtoRepository.findAll(
|
||||
SeriesSearchWithReadProgress(
|
||||
libraryIds = authorizedLibraryIds,
|
||||
deleted = false,
|
||||
oneshot = false,
|
||||
SeriesSearch(
|
||||
SearchCondition.AllOfSeries(
|
||||
buildList {
|
||||
if (library != null) add(SearchCondition.LibraryId(SearchOperator.Is(library.id)))
|
||||
add(SearchCondition.Deleted(SearchOperator.IsFalse))
|
||||
add(SearchCondition.OneShot(SearchOperator.IsFalse))
|
||||
},
|
||||
),
|
||||
),
|
||||
principal.user.id,
|
||||
SearchContext(principal.user),
|
||||
PageRequest.of(0, RECOMMENDED_ITEMS_NUMBER, Sort.by(Sort.Order.desc("lastModified"))),
|
||||
principal.user.restrictions,
|
||||
).map { it.toWPLinkDto() }
|
||||
|
||||
val uriBuilder = uriBuilder("libraries${if (library != null) "/${library.id}" else ""}")
|
||||
|
|
@ -285,19 +296,19 @@ class Opds2Controller(
|
|||
@PathVariable(name = "id", required = false) libraryId: String?,
|
||||
@Parameter(hidden = true) page: Pageable,
|
||||
): FeedDto {
|
||||
val (library, authorizedLibraryIds) = checkLibraryAccess(libraryId, principal)
|
||||
val (library, _) = checkLibraryAccess(libraryId, principal)
|
||||
|
||||
val entries =
|
||||
bookDtoRepository.findAll(
|
||||
BookSearchWithReadProgress(
|
||||
libraryIds = authorizedLibraryIds,
|
||||
readStatus = setOf(ReadStatus.IN_PROGRESS),
|
||||
mediaStatus = setOf(Media.Status.READY),
|
||||
deleted = false,
|
||||
BookSearch(
|
||||
SearchCondition.AllOfBook(
|
||||
SearchCondition.ReadStatus(SearchOperator.Is(ReadStatus.IN_PROGRESS)),
|
||||
SearchCondition.MediaStatus(SearchOperator.Is(Media.Status.READY)),
|
||||
SearchCondition.Deleted(SearchOperator.IsFalse),
|
||||
),
|
||||
),
|
||||
principal.user.id,
|
||||
SearchContext(principal.user),
|
||||
PageRequest.of(page.pageNumber, page.pageSize, Sort.by(Sort.Order.desc("readProgress.readDate"))),
|
||||
principal.user.restrictions,
|
||||
).map { opdsGenerator.toOpdsPublicationDto(it) }
|
||||
|
||||
val uriBuilder = uriBuilder("libraries${if (library != null) "/${library.id}" else ""}/keep-reading")
|
||||
|
|
@ -364,18 +375,18 @@ class Opds2Controller(
|
|||
@PathVariable(name = "id", required = false) libraryId: String?,
|
||||
@Parameter(hidden = true) page: Pageable,
|
||||
): FeedDto {
|
||||
val (library, authorizedLibraryIds) = checkLibraryAccess(libraryId, principal)
|
||||
val (library, _) = checkLibraryAccess(libraryId, principal)
|
||||
|
||||
val entries =
|
||||
bookDtoRepository.findAll(
|
||||
BookSearchWithReadProgress(
|
||||
libraryIds = authorizedLibraryIds,
|
||||
mediaStatus = setOf(Media.Status.READY),
|
||||
deleted = false,
|
||||
BookSearch(
|
||||
SearchCondition.AllOfBook(
|
||||
SearchCondition.MediaStatus(SearchOperator.Is(Media.Status.READY)),
|
||||
SearchCondition.Deleted(SearchOperator.IsFalse),
|
||||
),
|
||||
),
|
||||
principal.user.id,
|
||||
SearchContext(principal.user),
|
||||
PageRequest.of(page.pageNumber, page.pageSize, Sort.by(Sort.Order.desc("createdDate"))),
|
||||
principal.user.restrictions,
|
||||
).map { opdsGenerator.toOpdsPublicationDto(it) }
|
||||
|
||||
val uriBuilder = uriBuilder("libraries${if (library != null) "/${library.id}" else ""}/books/latest")
|
||||
|
|
@ -409,14 +420,17 @@ class Opds2Controller(
|
|||
|
||||
val entries =
|
||||
seriesDtoRepository.findAll(
|
||||
SeriesSearchWithReadProgress(
|
||||
libraryIds = authorizedLibraryIds,
|
||||
deleted = false,
|
||||
oneshot = false,
|
||||
SeriesSearch(
|
||||
SearchCondition.AllOfSeries(
|
||||
buildList {
|
||||
if (library != null) add(SearchCondition.LibraryId(SearchOperator.Is(library.id)))
|
||||
add(SearchCondition.Deleted(SearchOperator.IsFalse))
|
||||
add(SearchCondition.OneShot(SearchOperator.IsFalse))
|
||||
},
|
||||
),
|
||||
),
|
||||
principal.user.id,
|
||||
SearchContext(principal.user),
|
||||
PageRequest.of(page.pageNumber, page.pageSize, Sort.by(Sort.Order.desc("lastModified"))),
|
||||
principal.user.restrictions,
|
||||
).map { it.toWPLinkDto() }
|
||||
|
||||
val uriBuilder = uriBuilder("libraries${if (library != null) "/${library.id}" else ""}/books/latest")
|
||||
|
|
@ -450,15 +464,21 @@ class Opds2Controller(
|
|||
val (library, authorizedLibraryIds) = checkLibraryAccess(libraryId, principal)
|
||||
|
||||
val seriesSearch =
|
||||
SeriesSearchWithReadProgress(
|
||||
libraryIds = authorizedLibraryIds,
|
||||
publishers = publishers,
|
||||
deleted = false,
|
||||
SeriesSearch(
|
||||
SearchCondition.AllOfSeries(
|
||||
buildList {
|
||||
if (library != null) add(SearchCondition.LibraryId(SearchOperator.Is(library.id)))
|
||||
if (!publishers.isNullOrEmpty()) {
|
||||
add(SearchCondition.AllOfSeries(publishers.map { SearchCondition.Publisher(SearchOperator.Is(it)) }))
|
||||
}
|
||||
add(SearchCondition.Deleted(SearchOperator.IsFalse))
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
val pageable = PageRequest.of(page.pageNumber, page.pageSize, Sort.by(Sort.Order.asc("metadata.titleSort")))
|
||||
|
||||
val entries = seriesDtoRepository.findAll(seriesSearch, principal.user.id, pageable, principal.user.restrictions).map { it.toWPLinkDto() }
|
||||
val entries = seriesDtoRepository.findAll(seriesSearch, SearchContext(principal.user), pageable).map { it.toWPLinkDto() }
|
||||
|
||||
val uriBuilder = uriBuilder("libraries${if (library != null) "/${library.id}" else ""}/browse")
|
||||
|
||||
|
|
@ -551,12 +571,14 @@ class Opds2Controller(
|
|||
val pageable = PageRequest.of(page.pageNumber, page.pageSize, sort)
|
||||
|
||||
val seriesSearch =
|
||||
SeriesSearchWithReadProgress(
|
||||
libraryIds = principal.user.getAuthorizedLibraryIds(null),
|
||||
deleted = false,
|
||||
SeriesSearch(
|
||||
SearchCondition.AllOfSeries(
|
||||
SearchCondition.CollectionId(SearchOperator.Is(collection.id)),
|
||||
SearchCondition.Deleted(SearchOperator.IsFalse),
|
||||
),
|
||||
)
|
||||
|
||||
val entries = seriesDtoRepository.findAllByCollectionId(collection.id, seriesSearch, principal.user.id, pageable, principal.user.restrictions).map { it.toWPLinkDto() }
|
||||
val entries = seriesDtoRepository.findAll(seriesSearch, SearchContext(principal.user), pageable).map { it.toWPLinkDto() }
|
||||
|
||||
val uriBuilder = uriBuilder("collections/$id")
|
||||
|
||||
|
|
@ -634,20 +656,14 @@ class Opds2Controller(
|
|||
val pageable = PageRequest.of(page.pageNumber, page.pageSize, sort)
|
||||
|
||||
val bookSearch =
|
||||
BookSearchWithReadProgress(
|
||||
mediaStatus = setOf(Media.Status.READY),
|
||||
deleted = false,
|
||||
)
|
||||
|
||||
val booksPage =
|
||||
bookDtoRepository.findAllByReadListId(
|
||||
readList.id,
|
||||
principal.user.id,
|
||||
principal.user.getAuthorizedLibraryIds(null),
|
||||
bookSearch,
|
||||
pageable,
|
||||
principal.user.restrictions,
|
||||
BookSearch(
|
||||
SearchCondition.AllOfBook(
|
||||
SearchCondition.ReadListId(SearchOperator.Is(readList.id)),
|
||||
SearchCondition.MediaStatus(SearchOperator.Is(Media.Status.READY)),
|
||||
SearchCondition.Deleted(SearchOperator.IsFalse),
|
||||
),
|
||||
)
|
||||
val booksPage = bookDtoRepository.findAll(bookSearch, SearchContext(principal.user), pageable)
|
||||
|
||||
val entries = booksPage.map { opdsGenerator.toOpdsPublicationDto(it) }
|
||||
|
||||
|
|
@ -695,19 +711,23 @@ class Opds2Controller(
|
|||
@RequestParam(name = "tag", required = false) tag: String? = null,
|
||||
@Parameter(hidden = true) page: Pageable,
|
||||
): FeedDto =
|
||||
seriesDtoRepository.findByIdOrNull(id, "principal.user.id")?.let { series ->
|
||||
seriesDtoRepository.findByIdOrNull(id, principal.user.id)?.let { series ->
|
||||
contentRestrictionChecker.checkContentRestriction(principal.user, series)
|
||||
|
||||
val bookSearch =
|
||||
BookSearchWithReadProgress(
|
||||
seriesIds = listOf(id),
|
||||
mediaStatus = setOf(Media.Status.READY),
|
||||
tags = if (tag != null) listOf(tag) else null,
|
||||
deleted = false,
|
||||
BookSearch(
|
||||
SearchCondition.AllOfBook(
|
||||
buildList {
|
||||
add(SearchCondition.SeriesId(SearchOperator.Is(series.id)))
|
||||
add(SearchCondition.MediaStatus(SearchOperator.Is(Media.Status.READY)))
|
||||
add(SearchCondition.Deleted(SearchOperator.IsFalse))
|
||||
tag?.let { add(SearchCondition.Tag(SearchOperator.Is(it))) }
|
||||
},
|
||||
),
|
||||
)
|
||||
val pageable = PageRequest.of(page.pageNumber, page.pageSize, Sort.by(Sort.Order.asc("metadata.numberSort")))
|
||||
|
||||
val entries = bookDtoRepository.findAll(bookSearch, principal.user.id, pageable, principal.user.restrictions).map { opdsGenerator.toOpdsPublicationDto(it) }
|
||||
val entries = bookDtoRepository.findAll(bookSearch, SearchContext(principal.user), pageable).map { opdsGenerator.toOpdsPublicationDto(it) }
|
||||
|
||||
val uriBuilder = uriBuilder("series/$id")
|
||||
|
||||
|
|
@ -750,30 +770,39 @@ class Opds2Controller(
|
|||
@RequestParam(name = "query", required = false) query: String? = null,
|
||||
): FeedDto {
|
||||
val pageable = PageRequest.of(0, 20, Sort.by("relevance"))
|
||||
val queryTerms = query?.split("\\s+".toRegex())
|
||||
|
||||
val resultsSeries =
|
||||
seriesDtoRepository.findAll(
|
||||
SeriesSearchWithReadProgress(
|
||||
libraryIds = principal.user.getAuthorizedLibraryIds(null),
|
||||
searchTerm = query,
|
||||
oneshot = false,
|
||||
deleted = false,
|
||||
SeriesSearch(
|
||||
SearchCondition.AllOfSeries(
|
||||
buildList {
|
||||
add(SearchCondition.OneShot(SearchOperator.IsFalse))
|
||||
add(SearchCondition.Deleted(SearchOperator.IsFalse))
|
||||
if (!queryTerms.isNullOrEmpty()) {
|
||||
add(SearchCondition.AllOfSeries(queryTerms.map { SearchCondition.Title(SearchOperator.Contains(it)) }))
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
principal.user.id,
|
||||
SearchContext(principal.user),
|
||||
pageable,
|
||||
principal.user.restrictions,
|
||||
).map { it.toWPLinkDto() }
|
||||
|
||||
val resultsBooks =
|
||||
bookDtoRepository.findAll(
|
||||
BookSearchWithReadProgress(
|
||||
libraryIds = principal.user.getAuthorizedLibraryIds(null),
|
||||
searchTerm = query,
|
||||
deleted = false,
|
||||
BookSearch(
|
||||
SearchCondition.AllOfBook(
|
||||
buildList {
|
||||
add(SearchCondition.Deleted(SearchOperator.IsFalse))
|
||||
if (!queryTerms.isNullOrEmpty()) {
|
||||
add(SearchCondition.AllOfBook(queryTerms.map { SearchCondition.Title(SearchOperator.Contains(it)) }))
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
principal.user.id,
|
||||
SearchContext(principal.user),
|
||||
pageable,
|
||||
principal.user.restrictions,
|
||||
).map { opdsGenerator.toOpdsPublicationDto(it) }
|
||||
|
||||
val resultsCollections =
|
||||
|
|
|
|||
|
|
@ -1,30 +1,25 @@
|
|||
package org.gotson.komga.interfaces.api.persistence
|
||||
|
||||
import org.gotson.komga.domain.model.BookSearchWithReadProgress
|
||||
import org.gotson.komga.domain.model.BookSearch
|
||||
import org.gotson.komga.domain.model.ContentRestrictions
|
||||
import org.gotson.komga.domain.model.ReadList
|
||||
import org.gotson.komga.domain.model.SearchContext
|
||||
import org.gotson.komga.interfaces.api.rest.dto.BookDto
|
||||
import org.springframework.data.domain.Page
|
||||
import org.springframework.data.domain.Pageable
|
||||
|
||||
interface BookDtoRepository {
|
||||
fun findAll(pageable: Pageable): Page<BookDto>
|
||||
|
||||
fun findAll(
|
||||
search: BookSearchWithReadProgress,
|
||||
userId: String,
|
||||
context: SearchContext,
|
||||
pageable: Pageable,
|
||||
restrictions: ContentRestrictions = ContentRestrictions(),
|
||||
): Page<BookDto>
|
||||
|
||||
/**
|
||||
* Find books that are part of a readlist, optionally filtered by library
|
||||
*/
|
||||
fun findAllByReadListId(
|
||||
readListId: String,
|
||||
userId: String,
|
||||
filterOnLibraryIds: Collection<String>?,
|
||||
search: BookSearchWithReadProgress,
|
||||
fun findAll(
|
||||
search: BookSearch,
|
||||
context: SearchContext,
|
||||
pageable: Pageable,
|
||||
restrictions: ContentRestrictions = ContentRestrictions(),
|
||||
): Page<BookDto>
|
||||
|
||||
fun findByIdOrNull(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package org.gotson.komga.interfaces.api.persistence
|
||||
|
||||
import org.gotson.komga.domain.model.ContentRestrictions
|
||||
import org.gotson.komga.domain.model.SeriesSearchWithReadProgress
|
||||
import org.gotson.komga.domain.model.SearchContext
|
||||
import org.gotson.komga.domain.model.SeriesSearch
|
||||
import org.gotson.komga.interfaces.api.rest.dto.GroupCountDto
|
||||
import org.gotson.komga.interfaces.api.rest.dto.SeriesDto
|
||||
import org.springframework.data.domain.Page
|
||||
|
|
@ -14,30 +14,28 @@ interface SeriesDtoRepository {
|
|||
): SeriesDto?
|
||||
|
||||
fun findAll(
|
||||
search: SeriesSearchWithReadProgress,
|
||||
userId: String,
|
||||
pageable: Pageable,
|
||||
restrictions: ContentRestrictions = ContentRestrictions(),
|
||||
): Page<SeriesDto>
|
||||
|
||||
fun findAllByCollectionId(
|
||||
collectionId: String,
|
||||
search: SeriesSearchWithReadProgress,
|
||||
userId: String,
|
||||
fun findAll(
|
||||
context: SearchContext,
|
||||
pageable: Pageable,
|
||||
): Page<SeriesDto>
|
||||
|
||||
fun findAll(
|
||||
search: SeriesSearch,
|
||||
context: SearchContext,
|
||||
pageable: Pageable,
|
||||
restrictions: ContentRestrictions = ContentRestrictions(),
|
||||
): Page<SeriesDto>
|
||||
|
||||
fun findAllRecentlyUpdated(
|
||||
search: SeriesSearchWithReadProgress,
|
||||
userId: String,
|
||||
restrictions: ContentRestrictions,
|
||||
search: SeriesSearch,
|
||||
context: SearchContext,
|
||||
pageable: Pageable,
|
||||
): Page<SeriesDto>
|
||||
|
||||
fun countByFirstCharacter(
|
||||
search: SeriesSearchWithReadProgress,
|
||||
userId: String,
|
||||
restrictions: ContentRestrictions,
|
||||
search: SeriesSearch,
|
||||
context: SearchContext,
|
||||
): List<GroupCountDto>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import org.gotson.komga.application.tasks.HIGHEST_PRIORITY
|
|||
import org.gotson.komga.application.tasks.HIGH_PRIORITY
|
||||
import org.gotson.komga.application.tasks.LOWEST_PRIORITY
|
||||
import org.gotson.komga.application.tasks.TaskEmitter
|
||||
import org.gotson.komga.domain.model.BookSearchWithReadProgress
|
||||
import org.gotson.komga.domain.model.BookSearch
|
||||
import org.gotson.komga.domain.model.Dimension
|
||||
import org.gotson.komga.domain.model.DomainEvent
|
||||
import org.gotson.komga.domain.model.ImageConversionException
|
||||
|
|
@ -24,6 +24,9 @@ import org.gotson.komga.domain.model.MediaProfile
|
|||
import org.gotson.komga.domain.model.ROLE_ADMIN
|
||||
import org.gotson.komga.domain.model.ROLE_PAGE_STREAMING
|
||||
import org.gotson.komga.domain.model.ReadStatus
|
||||
import org.gotson.komga.domain.model.SearchCondition
|
||||
import org.gotson.komga.domain.model.SearchContext
|
||||
import org.gotson.komga.domain.model.SearchOperator
|
||||
import org.gotson.komga.domain.model.ThumbnailBook
|
||||
import org.gotson.komga.domain.persistence.BookMetadataRepository
|
||||
import org.gotson.komga.domain.persistence.BookRepository
|
||||
|
|
@ -92,6 +95,7 @@ import org.springframework.web.multipart.MultipartFile
|
|||
import org.springframework.web.server.ResponseStatusException
|
||||
import java.nio.file.NoSuchFileException
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneOffset
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
|
|
@ -148,16 +152,20 @@ class BookController(
|
|||
)
|
||||
|
||||
val bookSearch =
|
||||
BookSearchWithReadProgress(
|
||||
libraryIds = principal.user.getAuthorizedLibraryIds(libraryIds),
|
||||
searchTerm = searchTerm,
|
||||
mediaStatus = mediaStatus,
|
||||
readStatus = readStatus,
|
||||
releasedAfter = releasedAfter,
|
||||
tags = tags,
|
||||
BookSearch(
|
||||
SearchCondition.AllOfBook(
|
||||
buildList {
|
||||
if (!libraryIds.isNullOrEmpty()) add(SearchCondition.AnyOfBook(libraryIds.map { SearchCondition.LibraryId(SearchOperator.Is(it)) }))
|
||||
if (!mediaStatus.isNullOrEmpty()) add(SearchCondition.AnyOfBook(mediaStatus.map { SearchCondition.MediaStatus(SearchOperator.Is(it)) }))
|
||||
if (!readStatus.isNullOrEmpty()) add(SearchCondition.AnyOfBook(readStatus.map { SearchCondition.ReadStatus(SearchOperator.Is(it)) }))
|
||||
if (!tags.isNullOrEmpty()) add(SearchCondition.AnyOfBook(tags.map { SearchCondition.Tag(SearchOperator.Is(it)) }))
|
||||
releasedAfter?.let { add(SearchCondition.ReleaseDate(SearchOperator.After(it.atStartOfDay(ZoneOffset.UTC)))) }
|
||||
},
|
||||
),
|
||||
searchTerm,
|
||||
)
|
||||
|
||||
return bookDtoRepository.findAll(bookSearch, principal.user.id, pageRequest, principal.user.restrictions)
|
||||
return bookDtoRepository.findAll(bookSearch, SearchContext(principal.user), pageRequest)
|
||||
.map { it.restrictUrl(!principal.user.roleAdmin) }
|
||||
}
|
||||
|
||||
|
|
@ -182,12 +190,8 @@ class BookController(
|
|||
)
|
||||
|
||||
return bookDtoRepository.findAll(
|
||||
BookSearchWithReadProgress(
|
||||
libraryIds = principal.user.getAuthorizedLibraryIds(null),
|
||||
),
|
||||
principal.user.id,
|
||||
SearchContext(principal.user),
|
||||
pageRequest,
|
||||
principal.user.restrictions,
|
||||
).map { it.restrictUrl(!principal.user.roleAdmin) }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,12 +5,14 @@ import jakarta.validation.Valid
|
|||
import org.gotson.komga.application.tasks.HIGHEST_PRIORITY
|
||||
import org.gotson.komga.application.tasks.HIGH_PRIORITY
|
||||
import org.gotson.komga.application.tasks.TaskEmitter
|
||||
import org.gotson.komga.domain.model.BookSearch
|
||||
import org.gotson.komga.domain.model.DirectoryNotFoundException
|
||||
import org.gotson.komga.domain.model.DuplicateNameException
|
||||
import org.gotson.komga.domain.model.Library
|
||||
import org.gotson.komga.domain.model.PathContainedInPath
|
||||
import org.gotson.komga.domain.model.ROLE_ADMIN
|
||||
import org.gotson.komga.domain.model.SearchCondition
|
||||
import org.gotson.komga.domain.model.SearchContext
|
||||
import org.gotson.komga.domain.model.SearchOperator
|
||||
import org.gotson.komga.domain.persistence.BookRepository
|
||||
import org.gotson.komga.domain.persistence.LibraryRepository
|
||||
import org.gotson.komga.domain.persistence.SeriesRepository
|
||||
|
|
@ -22,6 +24,7 @@ import org.gotson.komga.interfaces.api.rest.dto.LibraryDto
|
|||
import org.gotson.komga.interfaces.api.rest.dto.LibraryUpdateDto
|
||||
import org.gotson.komga.interfaces.api.rest.dto.toDomain
|
||||
import org.gotson.komga.interfaces.api.rest.dto.toDto
|
||||
import org.springframework.data.domain.Pageable
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
|
|
@ -223,7 +226,13 @@ class LibraryController(
|
|||
fun analyze(
|
||||
@PathVariable libraryId: String,
|
||||
) {
|
||||
taskEmitter.analyzeBook(bookRepository.findAll(BookSearch(libraryIds = listOf(libraryId))), HIGH_PRIORITY)
|
||||
val books =
|
||||
bookRepository.findAll(
|
||||
SearchCondition.LibraryId(SearchOperator.Is(libraryId)),
|
||||
SearchContext.empty(),
|
||||
Pageable.unpaged(),
|
||||
).content
|
||||
taskEmitter.analyzeBook(books, HIGH_PRIORITY)
|
||||
}
|
||||
|
||||
@PostMapping("{libraryId}/metadata/refresh")
|
||||
|
|
@ -232,7 +241,12 @@ class LibraryController(
|
|||
fun refreshMetadata(
|
||||
@PathVariable libraryId: String,
|
||||
) {
|
||||
val books = bookRepository.findAll(BookSearch(libraryIds = listOf(libraryId)))
|
||||
val books =
|
||||
bookRepository.findAll(
|
||||
SearchCondition.LibraryId(SearchOperator.Is(libraryId)),
|
||||
SearchContext.empty(),
|
||||
Pageable.unpaged(),
|
||||
).content
|
||||
taskEmitter.refreshBookMetadata(books, priority = HIGH_PRIORITY)
|
||||
taskEmitter.refreshBookLocalArtwork(books, priority = HIGH_PRIORITY)
|
||||
taskEmitter.refreshSeriesLocalArtwork(seriesRepository.findAllIdsByLibraryId(libraryId), priority = HIGH_PRIORITY)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
|
|||
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.gotson.komga.domain.model.Author
|
||||
import org.gotson.komga.domain.model.BookSearchWithReadProgress
|
||||
import org.gotson.komga.domain.model.BookSearch
|
||||
import org.gotson.komga.domain.model.CodedException
|
||||
import org.gotson.komga.domain.model.Dimension
|
||||
import org.gotson.komga.domain.model.DomainEvent
|
||||
|
|
@ -22,6 +22,9 @@ import org.gotson.komga.domain.model.ROLE_ADMIN
|
|||
import org.gotson.komga.domain.model.ROLE_FILE_DOWNLOAD
|
||||
import org.gotson.komga.domain.model.ReadList
|
||||
import org.gotson.komga.domain.model.ReadStatus
|
||||
import org.gotson.komga.domain.model.SearchCondition
|
||||
import org.gotson.komga.domain.model.SearchContext
|
||||
import org.gotson.komga.domain.model.SearchOperator
|
||||
import org.gotson.komga.domain.model.ThumbnailReadList
|
||||
import org.gotson.komga.domain.persistence.BookRepository
|
||||
import org.gotson.komga.domain.persistence.ReadListRepository
|
||||
|
|
@ -329,23 +332,20 @@ class ReadListController(
|
|||
)
|
||||
|
||||
val bookSearch =
|
||||
BookSearchWithReadProgress(
|
||||
libraryIds = principal.user.getAuthorizedLibraryIds(libraryIds),
|
||||
readStatus = readStatus,
|
||||
mediaStatus = mediaStatus,
|
||||
deleted = deleted,
|
||||
tags = tags,
|
||||
authors = authors,
|
||||
BookSearch(
|
||||
SearchCondition.AllOfBook(
|
||||
buildList {
|
||||
add(SearchCondition.ReadListId(SearchOperator.Is(readList.id)))
|
||||
if (!libraryIds.isNullOrEmpty()) add(SearchCondition.AnyOfBook(libraryIds.map { SearchCondition.LibraryId(SearchOperator.Is(it)) }))
|
||||
if (!readStatus.isNullOrEmpty()) add(SearchCondition.AnyOfBook(readStatus.map { SearchCondition.ReadStatus(SearchOperator.Is(it)) }))
|
||||
if (!mediaStatus.isNullOrEmpty()) add(SearchCondition.AnyOfBook(mediaStatus.map { SearchCondition.MediaStatus(SearchOperator.Is(it)) }))
|
||||
if (!tags.isNullOrEmpty()) add(SearchCondition.AnyOfBook(tags.map { SearchCondition.Tag(SearchOperator.Is(it)) }))
|
||||
if (!authors.isNullOrEmpty()) add(SearchCondition.AnyOfBook(authors.map { SearchCondition.Author(SearchOperator.Is(SearchCondition.AuthorMatch(it.name, it.role))) }))
|
||||
deleted?.let { add(SearchCondition.Deleted(if (deleted) SearchOperator.IsTrue else SearchOperator.IsFalse)) }
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
bookDtoRepository.findAllByReadListId(
|
||||
readList.id,
|
||||
principal.user.id,
|
||||
principal.user.getAuthorizedLibraryIds(null),
|
||||
bookSearch,
|
||||
pageRequest,
|
||||
principal.user.restrictions,
|
||||
)
|
||||
bookDtoRepository.findAll(bookSearch, SearchContext(principal.user), pageRequest)
|
||||
.map { it.restrictUrl(!principal.user.roleAdmin) }
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
|
||||
|
|
@ -401,14 +401,8 @@ class ReadListController(
|
|||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
) {
|
||||
readListRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null))?.let { readList ->
|
||||
bookDtoRepository.findAllByReadListId(
|
||||
readList.id,
|
||||
principal.user.id,
|
||||
principal.user.getAuthorizedLibraryIds(null),
|
||||
BookSearchWithReadProgress(),
|
||||
UnpagedSorted(Sort.by(Sort.Order.asc("readList.number"))),
|
||||
principal.user.restrictions,
|
||||
).filterIndexed { index, _ -> index < readProgress.lastBookRead }
|
||||
bookDtoRepository.findAll(BookSearch(SearchCondition.ReadListId(SearchOperator.Is(readList.id))), SearchContext(principal.user), UnpagedSorted(Sort.by(Sort.Order.asc("readList.number"))))
|
||||
.filterIndexed { index, _ -> index < readProgress.lastBookRead }
|
||||
.forEach { book ->
|
||||
if (book.readProgress?.completed != true)
|
||||
bookLifecycle.markReadProgressCompleted(book.id, principal.user)
|
||||
|
|
|
|||
|
|
@ -12,9 +12,12 @@ import org.gotson.komga.domain.model.DomainEvent
|
|||
import org.gotson.komga.domain.model.DuplicateNameException
|
||||
import org.gotson.komga.domain.model.ROLE_ADMIN
|
||||
import org.gotson.komga.domain.model.ReadStatus
|
||||
import org.gotson.komga.domain.model.SearchCondition
|
||||
import org.gotson.komga.domain.model.SearchContext
|
||||
import org.gotson.komga.domain.model.SearchOperator
|
||||
import org.gotson.komga.domain.model.SeriesCollection
|
||||
import org.gotson.komga.domain.model.SeriesMetadata
|
||||
import org.gotson.komga.domain.model.SeriesSearchWithReadProgress
|
||||
import org.gotson.komga.domain.model.SeriesSearch
|
||||
import org.gotson.komga.domain.model.ThumbnailSeriesCollection
|
||||
import org.gotson.komga.domain.persistence.SeriesCollectionRepository
|
||||
import org.gotson.komga.domain.persistence.ThumbnailSeriesCollectionRepository
|
||||
|
|
@ -58,6 +61,8 @@ import org.springframework.web.bind.annotation.ResponseStatus
|
|||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
import java.time.ZoneOffset
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
|
@ -296,23 +301,38 @@ class SeriesCollectionController(
|
|||
sort,
|
||||
)
|
||||
|
||||
val seriesSearch =
|
||||
SeriesSearchWithReadProgress(
|
||||
libraryIds = principal.user.getAuthorizedLibraryIds(libraryIds),
|
||||
metadataStatus = metadataStatus,
|
||||
publishers = publishers,
|
||||
deleted = deleted,
|
||||
complete = complete,
|
||||
languages = languages,
|
||||
genres = genres,
|
||||
tags = tags,
|
||||
ageRatings = ageRatings?.map { it.toIntOrNull() },
|
||||
releaseYears = releaseYears,
|
||||
readStatus = readStatus,
|
||||
authors = authors,
|
||||
val search =
|
||||
SeriesSearch(
|
||||
SearchCondition.AllOfSeries(
|
||||
buildList {
|
||||
add(SearchCondition.CollectionId(SearchOperator.Is(collection.id)))
|
||||
if (!libraryIds.isNullOrEmpty()) add(SearchCondition.AnyOfSeries(libraryIds.map { SearchCondition.LibraryId(SearchOperator.Is(it)) }))
|
||||
deleted?.let { add(SearchCondition.Deleted(if (it) SearchOperator.IsTrue else SearchOperator.IsFalse)) }
|
||||
complete?.let { add(SearchCondition.Complete(if (it) SearchOperator.IsTrue else SearchOperator.IsFalse)) }
|
||||
if (!metadataStatus.isNullOrEmpty()) add(SearchCondition.AnyOfSeries(metadataStatus.map { SearchCondition.SeriesStatus(SearchOperator.Is(it)) }))
|
||||
if (!publishers.isNullOrEmpty()) add(SearchCondition.AnyOfSeries(publishers.map { SearchCondition.Publisher(SearchOperator.Is(it)) }))
|
||||
if (!languages.isNullOrEmpty()) add(SearchCondition.AnyOfSeries(languages.map { SearchCondition.Language(SearchOperator.Is(it)) }))
|
||||
if (!tags.isNullOrEmpty()) add(SearchCondition.AnyOfSeries(tags.map { SearchCondition.Tag(SearchOperator.Is(it)) }))
|
||||
if (!genres.isNullOrEmpty()) add(SearchCondition.AnyOfSeries(genres.map { SearchCondition.Genre(SearchOperator.Is(it)) }))
|
||||
if (!ageRatings.isNullOrEmpty()) add(SearchCondition.AnyOfSeries(ageRatings.map { it.toIntOrNull()?.let { ageRating -> SearchCondition.AgeRating(SearchOperator.Is(ageRating)) } ?: SearchCondition.AgeRating(SearchOperator.IsNullT()) }))
|
||||
if (!releaseYears.isNullOrEmpty())
|
||||
add(
|
||||
SearchCondition.AnyOfSeries(
|
||||
releaseYears.mapNotNull { it.toIntOrNull() }.map { releaseYear ->
|
||||
SearchCondition.AllOfSeries(
|
||||
SearchCondition.ReleaseDate(SearchOperator.After(ZonedDateTime.of(releaseYear - 1, 12, 31, 12, 0, 0, 0, ZoneOffset.UTC))),
|
||||
SearchCondition.ReleaseDate(SearchOperator.Before(ZonedDateTime.of(releaseYear + 1, 1, 1, 12, 0, 0, 0, ZoneOffset.UTC))),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
if (!readStatus.isNullOrEmpty()) add(SearchCondition.AnyOfSeries(readStatus.map { SearchCondition.ReadStatus(SearchOperator.Is(it)) }))
|
||||
if (!authors.isNullOrEmpty()) add(SearchCondition.AnyOfSeries(authors.map { SearchCondition.Author(SearchOperator.Is(SearchCondition.AuthorMatch(it.name, it.role))) }))
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
seriesDtoRepository.findAllByCollectionId(collection.id, seriesSearch, principal.user.id, pageRequest, principal.user.restrictions)
|
||||
seriesDtoRepository.findAll(search, SearchContext(principal.user), pageRequest)
|
||||
.map { it.restrictUrl(!principal.user.roleAdmin) }
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import org.gotson.komga.application.tasks.HIGH_PRIORITY
|
|||
import org.gotson.komga.application.tasks.TaskEmitter
|
||||
import org.gotson.komga.domain.model.AlternateTitle
|
||||
import org.gotson.komga.domain.model.Author
|
||||
import org.gotson.komga.domain.model.BookSearchWithReadProgress
|
||||
import org.gotson.komga.domain.model.BookSearch
|
||||
import org.gotson.komga.domain.model.Dimension
|
||||
import org.gotson.komga.domain.model.DomainEvent
|
||||
import org.gotson.komga.domain.model.KomgaUser
|
||||
|
|
@ -28,9 +28,12 @@ import org.gotson.komga.domain.model.MediaType.ZIP
|
|||
import org.gotson.komga.domain.model.ROLE_ADMIN
|
||||
import org.gotson.komga.domain.model.ROLE_FILE_DOWNLOAD
|
||||
import org.gotson.komga.domain.model.ReadStatus
|
||||
import org.gotson.komga.domain.model.SearchCondition
|
||||
import org.gotson.komga.domain.model.SearchContext
|
||||
import org.gotson.komga.domain.model.SearchField
|
||||
import org.gotson.komga.domain.model.SearchOperator
|
||||
import org.gotson.komga.domain.model.SeriesMetadata
|
||||
import org.gotson.komga.domain.model.SeriesSearch
|
||||
import org.gotson.komga.domain.model.SeriesSearchWithReadProgress
|
||||
import org.gotson.komga.domain.model.ThumbnailSeries
|
||||
import org.gotson.komga.domain.model.WebLink
|
||||
import org.gotson.komga.domain.persistence.BookRepository
|
||||
|
|
@ -93,6 +96,8 @@ import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBo
|
|||
import java.io.OutputStream
|
||||
import java.net.URI
|
||||
import java.nio.charset.StandardCharsets.UTF_8
|
||||
import java.time.ZoneOffset
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.zip.Deflater
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
|
@ -116,6 +121,7 @@ class SeriesController(
|
|||
private val thumbnailsSeriesRepository: ThumbnailSeriesRepository,
|
||||
private val contentRestrictionChecker: ContentRestrictionChecker,
|
||||
) {
|
||||
@Deprecated("use /v1/series/list instead")
|
||||
@PageableAsQueryParam
|
||||
@AuthorsAsQueryParam
|
||||
@Parameters(
|
||||
|
|
@ -169,37 +175,83 @@ class SeriesController(
|
|||
)
|
||||
|
||||
val seriesSearch =
|
||||
SeriesSearchWithReadProgress(
|
||||
libraryIds = principal.user.getAuthorizedLibraryIds(libraryIds),
|
||||
collectionIds = collectionIds,
|
||||
searchTerm = searchTerm,
|
||||
searchRegex =
|
||||
searchRegex?.let {
|
||||
when (it.second.lowercase()) {
|
||||
"title" -> Pair(it.first, SeriesSearch.SearchField.TITLE)
|
||||
"title_sort" -> Pair(it.first, SeriesSearch.SearchField.TITLE_SORT)
|
||||
else -> null
|
||||
}
|
||||
SeriesSearch(
|
||||
condition =
|
||||
SearchCondition.AllOfSeries(
|
||||
buildList {
|
||||
if (!libraryIds.isNullOrEmpty()) add(SearchCondition.AnyOfSeries(libraryIds.map { SearchCondition.LibraryId(SearchOperator.Is(it)) }))
|
||||
if (!collectionIds.isNullOrEmpty()) add(SearchCondition.AnyOfSeries(collectionIds.map { SearchCondition.CollectionId(SearchOperator.Is(it)) }))
|
||||
if (!metadataStatus.isNullOrEmpty()) add(SearchCondition.AnyOfSeries(metadataStatus.map { SearchCondition.SeriesStatus(SearchOperator.Is(it)) }))
|
||||
if (!publishers.isNullOrEmpty()) add(SearchCondition.AnyOfSeries(publishers.map { SearchCondition.Publisher(SearchOperator.Is(it)) }))
|
||||
if (!languages.isNullOrEmpty()) add(SearchCondition.AnyOfSeries(languages.map { SearchCondition.Language(SearchOperator.Is(it)) }))
|
||||
if (!genres.isNullOrEmpty()) add(SearchCondition.AnyOfSeries(genres.map { SearchCondition.Genre(SearchOperator.Is(it)) }))
|
||||
if (!tags.isNullOrEmpty()) add(SearchCondition.AnyOfSeries(tags.map { SearchCondition.Tag(SearchOperator.Is(it)) }))
|
||||
if (!readStatus.isNullOrEmpty()) add(SearchCondition.AnyOfSeries(readStatus.map { SearchCondition.ReadStatus(SearchOperator.Is(it)) }))
|
||||
if (!authors.isNullOrEmpty()) add(SearchCondition.AnyOfSeries(authors.map { SearchCondition.Author(SearchOperator.Is(SearchCondition.AuthorMatch(it.name, it.role))) }))
|
||||
if (!ageRatings.isNullOrEmpty()) add(SearchCondition.AnyOfSeries(ageRatings.map { it.toIntOrNull()?.let { ageRating -> SearchCondition.AgeRating(SearchOperator.Is(ageRating)) } ?: SearchCondition.AgeRating(SearchOperator.IsNullT()) }))
|
||||
if (!releaseYears.isNullOrEmpty())
|
||||
add(
|
||||
SearchCondition.AnyOfSeries(
|
||||
releaseYears.mapNotNull { it.toIntOrNull() }.map { releaseYear ->
|
||||
SearchCondition.AllOfSeries(
|
||||
SearchCondition.ReleaseDate(SearchOperator.After(ZonedDateTime.of(releaseYear - 1, 12, 31, 12, 0, 0, 0, ZoneOffset.UTC))),
|
||||
SearchCondition.ReleaseDate(SearchOperator.Before(ZonedDateTime.of(releaseYear + 1, 1, 1, 12, 0, 0, 0, ZoneOffset.UTC))),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
if (!sharingLabels.isNullOrEmpty()) add(SearchCondition.AnyOfSeries(sharingLabels.map { SearchCondition.SharingLabel(SearchOperator.Is(it)) }))
|
||||
oneshot?.let { add(SearchCondition.OneShot(if (it) SearchOperator.IsTrue else SearchOperator.IsFalse)) }
|
||||
complete?.let { add(SearchCondition.Complete(if (it) SearchOperator.IsTrue else SearchOperator.IsFalse)) }
|
||||
deleted?.let { add(SearchCondition.Deleted(if (it) SearchOperator.IsTrue else SearchOperator.IsFalse)) }
|
||||
},
|
||||
metadataStatus = metadataStatus,
|
||||
publishers = publishers,
|
||||
deleted = deleted,
|
||||
complete = complete,
|
||||
oneshot = oneshot,
|
||||
languages = languages,
|
||||
genres = genres,
|
||||
tags = tags,
|
||||
ageRatings = ageRatings?.map { it.toIntOrNull() },
|
||||
releaseYears = releaseYears,
|
||||
readStatus = readStatus,
|
||||
authors = authors,
|
||||
sharingLabels = sharingLabels,
|
||||
),
|
||||
fullTextSearch = searchTerm,
|
||||
regexSearch =
|
||||
searchRegex?.let {
|
||||
when (it.second.lowercase()) {
|
||||
"title" -> Pair(it.first, SearchField.TITLE)
|
||||
"title_sort" -> Pair(it.first, SearchField.TITLE_SORT)
|
||||
else -> null
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return seriesDtoRepository.findAll(seriesSearch, principal.user.id, pageRequest, principal.user.restrictions)
|
||||
return seriesDtoRepository.findAll(seriesSearch, SearchContext(principal.user), pageRequest)
|
||||
.map { it.restrictUrl(!principal.user.roleAdmin) }
|
||||
}
|
||||
|
||||
@PageableAsQueryParam
|
||||
@PostMapping("v1/series/list")
|
||||
fun getSeriesList(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@RequestBody search: SeriesSearch,
|
||||
@RequestParam(name = "unpaged", required = false) unpaged: Boolean = false,
|
||||
@Parameter(hidden = true) page: Pageable,
|
||||
): Page<SeriesDto> {
|
||||
val sort =
|
||||
when {
|
||||
page.sort.isSorted -> page.sort
|
||||
!search.fullTextSearch.isNullOrBlank() -> Sort.by("relevance")
|
||||
else -> Sort.unsorted()
|
||||
}
|
||||
|
||||
val pageRequest =
|
||||
if (unpaged)
|
||||
UnpagedSorted(sort)
|
||||
else
|
||||
PageRequest.of(
|
||||
page.pageNumber,
|
||||
page.pageSize,
|
||||
sort,
|
||||
)
|
||||
|
||||
return seriesDtoRepository.findAll(search, SearchContext(principal.user), pageRequest)
|
||||
.map { it.restrictUrl(!principal.user.roleAdmin) }
|
||||
}
|
||||
|
||||
@Deprecated("use /v1/series/list/alphabetical-groups instead")
|
||||
@AuthorsAsQueryParam
|
||||
@Parameters(
|
||||
Parameter(
|
||||
|
|
@ -234,36 +286,58 @@ class SeriesController(
|
|||
@Parameter(hidden = true) page: Pageable,
|
||||
): List<GroupCountDto> {
|
||||
val seriesSearch =
|
||||
SeriesSearchWithReadProgress(
|
||||
libraryIds = principal.user.getAuthorizedLibraryIds(libraryIds),
|
||||
collectionIds = collectionIds,
|
||||
searchTerm = searchTerm,
|
||||
searchRegex =
|
||||
searchRegex?.let {
|
||||
when (it.second.lowercase()) {
|
||||
"title" -> Pair(it.first, SeriesSearch.SearchField.TITLE)
|
||||
"title_sort" -> Pair(it.first, SeriesSearch.SearchField.TITLE_SORT)
|
||||
else -> null
|
||||
}
|
||||
SeriesSearch(
|
||||
condition =
|
||||
SearchCondition.AllOfSeries(
|
||||
buildList {
|
||||
if (!libraryIds.isNullOrEmpty()) add(SearchCondition.AnyOfSeries(libraryIds.map { SearchCondition.LibraryId(SearchOperator.Is(it)) }))
|
||||
if (!collectionIds.isNullOrEmpty()) add(SearchCondition.AnyOfSeries(collectionIds.map { SearchCondition.CollectionId(SearchOperator.Is(it)) }))
|
||||
if (!metadataStatus.isNullOrEmpty()) add(SearchCondition.AnyOfSeries(metadataStatus.map { SearchCondition.SeriesStatus(SearchOperator.Is(it)) }))
|
||||
if (!publishers.isNullOrEmpty()) add(SearchCondition.AnyOfSeries(publishers.map { SearchCondition.Publisher(SearchOperator.Is(it)) }))
|
||||
if (!languages.isNullOrEmpty()) add(SearchCondition.AnyOfSeries(languages.map { SearchCondition.Language(SearchOperator.Is(it)) }))
|
||||
if (!genres.isNullOrEmpty()) add(SearchCondition.AnyOfSeries(genres.map { SearchCondition.Genre(SearchOperator.Is(it)) }))
|
||||
if (!tags.isNullOrEmpty()) add(SearchCondition.AnyOfSeries(tags.map { SearchCondition.Tag(SearchOperator.Is(it)) }))
|
||||
if (!readStatus.isNullOrEmpty()) add(SearchCondition.AnyOfSeries(readStatus.map { SearchCondition.ReadStatus(SearchOperator.Is(it)) }))
|
||||
if (!authors.isNullOrEmpty()) add(SearchCondition.AnyOfSeries(authors.map { SearchCondition.Author(SearchOperator.Is(SearchCondition.AuthorMatch(it.name, it.role))) }))
|
||||
if (!ageRatings.isNullOrEmpty()) add(SearchCondition.AnyOfSeries(ageRatings.map { it.toIntOrNull()?.let { ageRating -> SearchCondition.AgeRating(SearchOperator.Is(ageRating)) } ?: SearchCondition.AgeRating(SearchOperator.IsNullT()) }))
|
||||
if (!releaseYears.isNullOrEmpty())
|
||||
add(
|
||||
SearchCondition.AnyOfSeries(
|
||||
releaseYears.mapNotNull { it.toIntOrNull() }.map { releaseYear ->
|
||||
SearchCondition.AllOfSeries(
|
||||
SearchCondition.ReleaseDate(SearchOperator.After(ZonedDateTime.of(releaseYear - 1, 12, 31, 12, 0, 0, 0, ZoneOffset.UTC))),
|
||||
SearchCondition.ReleaseDate(SearchOperator.Before(ZonedDateTime.of(releaseYear + 1, 1, 1, 12, 0, 0, 0, ZoneOffset.UTC))),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
if (!sharingLabels.isNullOrEmpty()) add(SearchCondition.AnyOfSeries(sharingLabels.map { SearchCondition.SharingLabel(SearchOperator.Is(it)) }))
|
||||
oneshot?.let { add(SearchCondition.OneShot(if (it) SearchOperator.IsTrue else SearchOperator.IsFalse)) }
|
||||
complete?.let { add(SearchCondition.Complete(if (it) SearchOperator.IsTrue else SearchOperator.IsFalse)) }
|
||||
deleted?.let { add(SearchCondition.Deleted(if (it) SearchOperator.IsTrue else SearchOperator.IsFalse)) }
|
||||
},
|
||||
metadataStatus = metadataStatus,
|
||||
publishers = publishers,
|
||||
deleted = deleted,
|
||||
complete = complete,
|
||||
oneshot = oneshot,
|
||||
languages = languages,
|
||||
genres = genres,
|
||||
tags = tags,
|
||||
ageRatings = ageRatings?.map { it.toIntOrNull() },
|
||||
releaseYears = releaseYears,
|
||||
readStatus = readStatus,
|
||||
authors = authors,
|
||||
sharingLabels = sharingLabels,
|
||||
),
|
||||
fullTextSearch = searchTerm,
|
||||
regexSearch =
|
||||
searchRegex?.let {
|
||||
when (it.second.lowercase()) {
|
||||
"title" -> Pair(it.first, SearchField.TITLE)
|
||||
"title_sort" -> Pair(it.first, SearchField.TITLE_SORT)
|
||||
else -> null
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return seriesDtoRepository.countByFirstCharacter(seriesSearch, principal.user.id, principal.user.restrictions)
|
||||
return seriesDtoRepository.countByFirstCharacter(seriesSearch, SearchContext(principal.user))
|
||||
}
|
||||
|
||||
@PostMapping("v1/series/list/alphabetical-groups")
|
||||
fun getSeriesListByAlphabeticalGroups(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@RequestBody search: SeriesSearch,
|
||||
): List<GroupCountDto> = seriesDtoRepository.countByFirstCharacter(search, SearchContext(principal.user))
|
||||
|
||||
@Operation(description = "Return recently added or updated series.")
|
||||
@PageableWithoutSortAsQueryParam
|
||||
@GetMapping("v1/series/latest")
|
||||
|
|
@ -288,14 +362,17 @@ class SeriesController(
|
|||
)
|
||||
|
||||
return seriesDtoRepository.findAll(
|
||||
SeriesSearchWithReadProgress(
|
||||
libraryIds = principal.user.getAuthorizedLibraryIds(libraryIds),
|
||||
deleted = deleted,
|
||||
oneshot = oneshot,
|
||||
SeriesSearch(
|
||||
SearchCondition.AllOfSeries(
|
||||
buildList {
|
||||
if (!libraryIds.isNullOrEmpty()) add(SearchCondition.AnyOfSeries(libraryIds.map { SearchCondition.LibraryId(SearchOperator.Is(it)) }))
|
||||
deleted?.let { add(SearchCondition.Deleted(if (it) SearchOperator.IsTrue else SearchOperator.IsFalse)) }
|
||||
oneshot?.let { add(SearchCondition.OneShot(if (it) SearchOperator.IsTrue else SearchOperator.IsFalse)) }
|
||||
},
|
||||
),
|
||||
),
|
||||
principal.user.id,
|
||||
SearchContext(principal.user),
|
||||
pageRequest,
|
||||
principal.user.restrictions,
|
||||
).map { it.restrictUrl(!principal.user.roleAdmin) }
|
||||
}
|
||||
|
||||
|
|
@ -323,14 +400,17 @@ class SeriesController(
|
|||
)
|
||||
|
||||
return seriesDtoRepository.findAll(
|
||||
SeriesSearchWithReadProgress(
|
||||
libraryIds = principal.user.getAuthorizedLibraryIds(libraryIds),
|
||||
deleted = deleted,
|
||||
oneshot = oneshot,
|
||||
SeriesSearch(
|
||||
SearchCondition.AllOfSeries(
|
||||
buildList {
|
||||
if (!libraryIds.isNullOrEmpty()) add(SearchCondition.AnyOfSeries(libraryIds.map { SearchCondition.LibraryId(SearchOperator.Is(it)) }))
|
||||
deleted?.let { add(SearchCondition.Deleted(if (it) SearchOperator.IsTrue else SearchOperator.IsFalse)) }
|
||||
oneshot?.let { add(SearchCondition.OneShot(if (it) SearchOperator.IsTrue else SearchOperator.IsFalse)) }
|
||||
},
|
||||
),
|
||||
),
|
||||
principal.user.id,
|
||||
SearchContext(principal.user),
|
||||
pageRequest,
|
||||
principal.user.restrictions,
|
||||
).map { it.restrictUrl(!principal.user.roleAdmin) }
|
||||
}
|
||||
|
||||
|
|
@ -358,13 +438,16 @@ class SeriesController(
|
|||
)
|
||||
|
||||
return seriesDtoRepository.findAllRecentlyUpdated(
|
||||
SeriesSearchWithReadProgress(
|
||||
libraryIds = principal.user.getAuthorizedLibraryIds(libraryIds),
|
||||
deleted = deleted,
|
||||
oneshot = oneshot,
|
||||
SeriesSearch(
|
||||
SearchCondition.AllOfSeries(
|
||||
buildList {
|
||||
if (!libraryIds.isNullOrEmpty()) add(SearchCondition.AnyOfSeries(libraryIds.map { SearchCondition.LibraryId(SearchOperator.Is(it)) }))
|
||||
deleted?.let { add(SearchCondition.Deleted(if (it) SearchOperator.IsTrue else SearchOperator.IsFalse)) }
|
||||
oneshot?.let { add(SearchCondition.OneShot(if (it) SearchOperator.IsTrue else SearchOperator.IsFalse)) }
|
||||
},
|
||||
),
|
||||
),
|
||||
principal.user.id,
|
||||
principal.user.restrictions,
|
||||
SearchContext(principal.user),
|
||||
pageRequest,
|
||||
).map { it.restrictUrl(!principal.user.roleAdmin) }
|
||||
}
|
||||
|
|
@ -505,16 +588,22 @@ class SeriesController(
|
|||
sort,
|
||||
)
|
||||
|
||||
val search =
|
||||
BookSearch(
|
||||
SearchCondition.AllOfBook(
|
||||
buildList {
|
||||
add(SearchCondition.SeriesId(SearchOperator.Is(seriesId)))
|
||||
if (!mediaStatus.isNullOrEmpty()) add(SearchCondition.AnyOfBook(mediaStatus.map { SearchCondition.MediaStatus(SearchOperator.Is(it)) }))
|
||||
if (!readStatus.isNullOrEmpty()) add(SearchCondition.AnyOfBook(readStatus.map { SearchCondition.ReadStatus(SearchOperator.Is(it)) }))
|
||||
if (!tags.isNullOrEmpty()) add(SearchCondition.AnyOfBook(tags.map { SearchCondition.Tag(SearchOperator.Is(it)) }))
|
||||
if (!authors.isNullOrEmpty()) add(SearchCondition.AnyOfBook(authors.map { SearchCondition.Author(SearchOperator.Is(SearchCondition.AuthorMatch(it.name, it.role))) }))
|
||||
deleted?.let { add(SearchCondition.Deleted(if (it) SearchOperator.IsTrue else SearchOperator.IsFalse)) }
|
||||
},
|
||||
),
|
||||
)
|
||||
return bookDtoRepository.findAll(
|
||||
BookSearchWithReadProgress(
|
||||
seriesIds = listOf(seriesId),
|
||||
mediaStatus = mediaStatus,
|
||||
deleted = deleted,
|
||||
readStatus = readStatus,
|
||||
tags = tags,
|
||||
authors = authors,
|
||||
),
|
||||
principal.user.id,
|
||||
search,
|
||||
SearchContext(principal.user),
|
||||
pageRequest,
|
||||
).map { it.restrictUrl(!principal.user.roleAdmin) }
|
||||
}
|
||||
|
|
@ -583,41 +672,41 @@ class SeriesController(
|
|||
ageRating = if (isSet("ageRating")) ageRating else existing.ageRating,
|
||||
ageRatingLock = ageRatingLock ?: existing.ageRatingLock,
|
||||
genres =
|
||||
if (isSet("genres")) {
|
||||
if (genres != null) genres!! else emptySet()
|
||||
} else {
|
||||
existing.genres
|
||||
},
|
||||
if (isSet("genres")) {
|
||||
if (genres != null) genres!! else emptySet()
|
||||
} else {
|
||||
existing.genres
|
||||
},
|
||||
genresLock = genresLock ?: existing.genresLock,
|
||||
tags =
|
||||
if (isSet("tags")) {
|
||||
if (tags != null) tags!! else emptySet()
|
||||
} else {
|
||||
existing.tags
|
||||
},
|
||||
if (isSet("tags")) {
|
||||
if (tags != null) tags!! else emptySet()
|
||||
} else {
|
||||
existing.tags
|
||||
},
|
||||
tagsLock = tagsLock ?: existing.tagsLock,
|
||||
totalBookCount = if (isSet("totalBookCount")) totalBookCount else existing.totalBookCount,
|
||||
totalBookCountLock = totalBookCountLock ?: existing.totalBookCountLock,
|
||||
sharingLabels =
|
||||
if (isSet("sharingLabels")) {
|
||||
if (sharingLabels != null) sharingLabels!! else emptySet()
|
||||
} else {
|
||||
existing.sharingLabels
|
||||
},
|
||||
if (isSet("sharingLabels")) {
|
||||
if (sharingLabels != null) sharingLabels!! else emptySet()
|
||||
} else {
|
||||
existing.sharingLabels
|
||||
},
|
||||
sharingLabelsLock = sharingLabelsLock ?: existing.sharingLabelsLock,
|
||||
links =
|
||||
if (isSet("links")) {
|
||||
if (links != null) links!!.map { WebLink(it.label!!, URI(it.url!!)) } else emptyList()
|
||||
} else {
|
||||
existing.links
|
||||
},
|
||||
if (isSet("links")) {
|
||||
if (links != null) links!!.map { WebLink(it.label!!, URI(it.url!!)) } else emptyList()
|
||||
} else {
|
||||
existing.links
|
||||
},
|
||||
linksLock = linksLock ?: existing.linksLock,
|
||||
alternateTitles =
|
||||
if (isSet("alternateTitles")) {
|
||||
if (alternateTitles != null) alternateTitles!!.map { AlternateTitle(it.label!!, it.title!!) } else emptyList()
|
||||
} else {
|
||||
existing.alternateTitles
|
||||
},
|
||||
if (isSet("alternateTitles")) {
|
||||
if (alternateTitles != null) alternateTitles!!.map { AlternateTitle(it.label!!, it.title!!) } else emptyList()
|
||||
} else {
|
||||
existing.alternateTitles
|
||||
},
|
||||
alternateTitlesLock = alternateTitlesLock ?: existing.alternateTitlesLock,
|
||||
)
|
||||
}
|
||||
|
|
@ -670,8 +759,8 @@ class SeriesController(
|
|||
principal.user.checkContentRestriction(seriesId)
|
||||
|
||||
bookDtoRepository.findAll(
|
||||
BookSearchWithReadProgress(seriesIds = listOf(seriesId)),
|
||||
principal.user.id,
|
||||
BookSearch(SearchCondition.SeriesId(SearchOperator.Is(seriesId))),
|
||||
SearchContext(principal.user),
|
||||
UnpagedSorted(Sort.by(Sort.Order.asc("metadata.numberSort"))),
|
||||
).toList().filter { book -> book.metadata.numberSort <= readProgress.lastBookNumberSortRead }
|
||||
.forEach { book ->
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
package org.gotson.komga.domain.model
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.gotson.komga.domain.model.SearchCondition.AuthorMatch
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import java.time.Duration
|
||||
import java.time.ZoneOffset
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
@SpringBootTest
|
||||
class BookSearchTest(
|
||||
@Autowired private val mapper: ObjectMapper,
|
||||
) {
|
||||
private val writer = mapper.writerWithDefaultPrettyPrinter()
|
||||
|
||||
@Test
|
||||
fun `given bookSearch entity when serializing then it looks alright`() {
|
||||
val search =
|
||||
BookSearch(
|
||||
SearchCondition.AllOfBook(
|
||||
SearchCondition.AnyOfBook(
|
||||
SearchCondition.LibraryId(SearchOperator.Is("library1")),
|
||||
SearchCondition.LibraryId(SearchOperator.IsNot("library1")),
|
||||
),
|
||||
SearchCondition.SeriesId(SearchOperator.Is("series1")),
|
||||
SearchCondition.SeriesId(SearchOperator.IsNot("series1")),
|
||||
SearchCondition.ReadListId(SearchOperator.Is("readList1")),
|
||||
SearchCondition.ReadListId(SearchOperator.IsNot("readList1")),
|
||||
SearchCondition.Deleted(SearchOperator.IsFalse),
|
||||
SearchCondition.Deleted(SearchOperator.IsTrue),
|
||||
SearchCondition.Title(SearchOperator.BeginsWith("abc")),
|
||||
SearchCondition.Title(SearchOperator.EndsWith("abc")),
|
||||
SearchCondition.Title(SearchOperator.Is("abc")),
|
||||
SearchCondition.Title(SearchOperator.IsNot("abc")),
|
||||
SearchCondition.Title(SearchOperator.Contains("abc")),
|
||||
SearchCondition.Title(SearchOperator.DoesNotContain("abc")),
|
||||
SearchCondition.ReleaseDate(SearchOperator.Before(ZonedDateTime.now(ZoneOffset.UTC).minusMonths(1))),
|
||||
SearchCondition.ReleaseDate(SearchOperator.After(ZonedDateTime.now(ZoneOffset.UTC).minusMonths(1))),
|
||||
SearchCondition.ReleaseDate(SearchOperator.IsNotInTheLast(Duration.ofDays(5))),
|
||||
SearchCondition.ReleaseDate(SearchOperator.IsInTheLast(Duration.ofDays(5))),
|
||||
SearchCondition.ReleaseDate(SearchOperator.IsNotNull),
|
||||
SearchCondition.ReleaseDate(SearchOperator.IsNull),
|
||||
SearchCondition.NumberSort(SearchOperator.GreaterThan(5F)),
|
||||
SearchCondition.NumberSort(SearchOperator.LessThan(5F)),
|
||||
SearchCondition.NumberSort(SearchOperator.Is(5F)),
|
||||
SearchCondition.NumberSort(SearchOperator.IsNot(5F)),
|
||||
SearchCondition.Tag(SearchOperator.Is("fiction")),
|
||||
SearchCondition.Tag(SearchOperator.IsNot("fantasy")),
|
||||
SearchCondition.ReadStatus(SearchOperator.IsNot(ReadStatus.READ)),
|
||||
SearchCondition.ReadStatus(SearchOperator.Is(ReadStatus.READ)),
|
||||
SearchCondition.MediaStatus(SearchOperator.Is(Media.Status.READY)),
|
||||
SearchCondition.MediaStatus(SearchOperator.IsNot(Media.Status.READY)),
|
||||
SearchCondition.MediaProfile(SearchOperator.Is(MediaProfile.PDF)),
|
||||
SearchCondition.MediaProfile(SearchOperator.IsNot(MediaProfile.PDF)),
|
||||
SearchCondition.Author(SearchOperator.Is(AuthorMatch("john", "writer"))),
|
||||
SearchCondition.Author(SearchOperator.IsNot(AuthorMatch("jack", "writer"))),
|
||||
SearchCondition.Author(SearchOperator.Is(AuthorMatch(role = "writer"))),
|
||||
SearchCondition.Author(SearchOperator.IsNot(AuthorMatch(name = "jim"))),
|
||||
SearchCondition.Author(SearchOperator.IsNot(AuthorMatch())),
|
||||
SearchCondition.OneShot(SearchOperator.IsFalse),
|
||||
SearchCondition.OneShot(SearchOperator.IsTrue),
|
||||
),
|
||||
)
|
||||
|
||||
val json = writer.writeValueAsString(search)
|
||||
|
||||
println(json)
|
||||
|
||||
val entity = mapper.readValue<BookSearch>(json)
|
||||
|
||||
assertThat(entity).isEqualTo(search)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package org.gotson.komga.domain.model
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.gotson.komga.domain.model.SearchCondition.AuthorMatch
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import java.time.Duration
|
||||
import java.time.ZoneOffset
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
@SpringBootTest
|
||||
class SeriesSearchTest(
|
||||
@Autowired private val mapper: ObjectMapper,
|
||||
) {
|
||||
private val writer = mapper.writerWithDefaultPrettyPrinter()
|
||||
|
||||
@Test
|
||||
fun `given seriesSearch entity when serializing then it looks alright`() {
|
||||
val search =
|
||||
SeriesSearch(
|
||||
SearchCondition.AllOfSeries(
|
||||
SearchCondition.AnyOfSeries(
|
||||
emptyList()
|
||||
),
|
||||
SearchCondition.AnyOfSeries(
|
||||
SearchCondition.LibraryId(SearchOperator.Is("library1")),
|
||||
SearchCondition.LibraryId(SearchOperator.IsNot("library1")),
|
||||
),
|
||||
SearchCondition.CollectionId(SearchOperator.Is("collection1")),
|
||||
SearchCondition.CollectionId(SearchOperator.IsNot("collection1")),
|
||||
SearchCondition.Deleted(SearchOperator.IsFalse),
|
||||
SearchCondition.Deleted(SearchOperator.IsTrue),
|
||||
SearchCondition.Complete(SearchOperator.IsTrue),
|
||||
SearchCondition.Complete(SearchOperator.IsFalse),
|
||||
SearchCondition.ReleaseDate(SearchOperator.Before(ZonedDateTime.now(ZoneOffset.UTC).minusMonths(1))),
|
||||
SearchCondition.ReleaseDate(SearchOperator.After(ZonedDateTime.now(ZoneOffset.UTC).minusMonths(1))),
|
||||
SearchCondition.ReleaseDate(SearchOperator.IsNotInTheLast(Duration.ofDays(5))),
|
||||
SearchCondition.ReleaseDate(SearchOperator.IsInTheLast(Duration.ofDays(5))),
|
||||
SearchCondition.ReleaseDate(SearchOperator.IsNotNull),
|
||||
SearchCondition.ReleaseDate(SearchOperator.IsNull),
|
||||
SearchCondition.AgeRating(SearchOperator.LessThan(5)),
|
||||
SearchCondition.AgeRating(SearchOperator.Is(5)),
|
||||
SearchCondition.AgeRating(SearchOperator.IsNullT()),
|
||||
SearchCondition.AgeRating(SearchOperator.IsNot(5)),
|
||||
SearchCondition.AgeRating(SearchOperator.IsNotNullT()),
|
||||
SearchCondition.Tag(SearchOperator.Is("fiction")),
|
||||
SearchCondition.Tag(SearchOperator.IsNot("fantasy")),
|
||||
SearchCondition.SharingLabel(SearchOperator.Is("label1")),
|
||||
SearchCondition.SharingLabel(SearchOperator.IsNot("label1")),
|
||||
SearchCondition.Publisher(SearchOperator.Is("publisher1")),
|
||||
SearchCondition.Publisher(SearchOperator.IsNot("publisher1")),
|
||||
SearchCondition.Language(SearchOperator.Is("en")),
|
||||
SearchCondition.Language(SearchOperator.IsNot("en")),
|
||||
SearchCondition.Genre(SearchOperator.Is("genre1")),
|
||||
SearchCondition.Genre(SearchOperator.IsNot("genre1")),
|
||||
SearchCondition.ReadStatus(SearchOperator.IsNot(ReadStatus.READ)),
|
||||
SearchCondition.ReadStatus(SearchOperator.Is(ReadStatus.READ)),
|
||||
SearchCondition.SeriesStatus(SearchOperator.Is(SeriesMetadata.Status.ENDED)),
|
||||
SearchCondition.SeriesStatus(SearchOperator.IsNot(SeriesMetadata.Status.ONGOING)),
|
||||
SearchCondition.Author(SearchOperator.Is(AuthorMatch("john", "writer"))),
|
||||
SearchCondition.Author(SearchOperator.IsNot(AuthorMatch("jack", "writer"))),
|
||||
SearchCondition.Author(SearchOperator.Is(AuthorMatch(role = "writer"))),
|
||||
SearchCondition.Author(SearchOperator.IsNot(AuthorMatch(name = "jim"))),
|
||||
SearchCondition.Author(SearchOperator.IsNot(AuthorMatch())),
|
||||
SearchCondition.OneShot(SearchOperator.IsFalse),
|
||||
SearchCondition.OneShot(SearchOperator.IsTrue),
|
||||
SearchCondition.Title(SearchOperator.Is("abc")),
|
||||
SearchCondition.Title(SearchOperator.IsNot("abc")),
|
||||
SearchCondition.Title(SearchOperator.Contains("abc")),
|
||||
SearchCondition.Title(SearchOperator.DoesNotContain("abc")),
|
||||
SearchCondition.Title(SearchOperator.BeginsWith("abc")),
|
||||
SearchCondition.Title(SearchOperator.DoesNotBeginWith("abc")),
|
||||
SearchCondition.Title(SearchOperator.EndsWith("abc")),
|
||||
SearchCondition.Title(SearchOperator.DoesNotEndWith("abc")),
|
||||
SearchCondition.TitleSort(SearchOperator.Is("abc")),
|
||||
SearchCondition.TitleSort(SearchOperator.IsNot("abc")),
|
||||
SearchCondition.TitleSort(SearchOperator.Contains("abc")),
|
||||
SearchCondition.TitleSort(SearchOperator.DoesNotContain("abc")),
|
||||
SearchCondition.TitleSort(SearchOperator.BeginsWith("abc")),
|
||||
SearchCondition.TitleSort(SearchOperator.DoesNotBeginWith("abc")),
|
||||
SearchCondition.TitleSort(SearchOperator.EndsWith("abc")),
|
||||
SearchCondition.TitleSort(SearchOperator.DoesNotEndWith("abc")),
|
||||
),
|
||||
)
|
||||
|
||||
val json = writer.writeValueAsString(search)
|
||||
|
||||
println(json)
|
||||
|
||||
val entity = mapper.readValue<SeriesSearch>(json)
|
||||
|
||||
assertThat(entity).isEqualTo(search)
|
||||
}
|
||||
}
|
||||
|
|
@ -190,12 +190,11 @@ class SyncPointLifecycleTest(
|
|||
val book2 = makeBook("valid2", libraryId = library1.id)
|
||||
val book3 = makeBook("valid3", libraryId = library1.id)
|
||||
|
||||
val series =
|
||||
makeSeries(name = "series1", libraryId = library1.id).also { series ->
|
||||
seriesLifecycle.createSeries(series).let { created ->
|
||||
seriesLifecycle.addBooks(created, listOf(book1, book2, book3))
|
||||
}
|
||||
makeSeries(name = "series1", libraryId = library1.id).also { series ->
|
||||
seriesLifecycle.createSeries(series).let { created ->
|
||||
seriesLifecycle.addBooks(created, listOf(book1, book2, book3))
|
||||
}
|
||||
}
|
||||
|
||||
bookRepository.findAll().forEach { mediaRepository.findById(it.id).let { media -> mediaRepository.update(media.copy(status = Media.Status.READY, mediaType = MediaType.EPUB.type)) } }
|
||||
|
||||
|
|
@ -231,12 +230,11 @@ class SyncPointLifecycleTest(
|
|||
val book3 = makeBook("valid3", libraryId = library1.id)
|
||||
val book4 = makeBook("no hash to hash", libraryId = library1.id)
|
||||
|
||||
val series =
|
||||
makeSeries(name = "series1", libraryId = library1.id).also { series ->
|
||||
seriesLifecycle.createSeries(series).let { created ->
|
||||
seriesLifecycle.addBooks(created, listOf(book1, book2, book3))
|
||||
}
|
||||
makeSeries(name = "series1", libraryId = library1.id).also { series ->
|
||||
seriesLifecycle.createSeries(series).let { created ->
|
||||
seriesLifecycle.addBooks(created, listOf(book1, book2, book3))
|
||||
}
|
||||
}
|
||||
|
||||
bookRepository.findAll().forEach { mediaRepository.findById(it.id).let { media -> mediaRepository.update(media.copy(status = Media.Status.READY, mediaType = MediaType.EPUB.type)) } }
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ package org.gotson.komga.infrastructure.jooq.main
|
|||
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.gotson.komga.domain.model.Book
|
||||
import org.gotson.komga.domain.model.BookSearch
|
||||
import org.gotson.komga.domain.model.SearchCondition
|
||||
import org.gotson.komga.domain.model.SearchContext
|
||||
import org.gotson.komga.domain.model.SearchOperator
|
||||
import org.gotson.komga.domain.model.makeBook
|
||||
import org.gotson.komga.domain.model.makeLibrary
|
||||
import org.gotson.komga.domain.model.makeSeries
|
||||
|
|
@ -15,6 +17,7 @@ import org.junit.jupiter.api.BeforeAll
|
|||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.data.domain.Pageable
|
||||
import java.net.URL
|
||||
import java.time.LocalDateTime
|
||||
|
||||
|
|
@ -159,11 +162,11 @@ class BookDaoTest(
|
|||
bookDao.insert(makeBook("2", libraryId = library.id, seriesId = series.id))
|
||||
|
||||
val search =
|
||||
BookSearch(
|
||||
libraryIds = listOf(library.id),
|
||||
seriesIds = listOf(series.id),
|
||||
SearchCondition.AllOfBook(
|
||||
SearchCondition.LibraryId(SearchOperator.Is(library.id)),
|
||||
SearchCondition.SeriesId(SearchOperator.Is(series.id)),
|
||||
)
|
||||
val found = bookDao.findAll(search)
|
||||
val found = bookDao.findAll(search, SearchContext.empty(), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found).hasSize(2)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,11 +7,14 @@ import io.mockk.just
|
|||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatCode
|
||||
import org.gotson.komga.domain.model.Author
|
||||
import org.gotson.komga.domain.model.BookSearchWithReadProgress
|
||||
import org.gotson.komga.domain.model.BookSearch
|
||||
import org.gotson.komga.domain.model.KomgaUser
|
||||
import org.gotson.komga.domain.model.Media
|
||||
import org.gotson.komga.domain.model.ReadProgress
|
||||
import org.gotson.komga.domain.model.ReadStatus
|
||||
import org.gotson.komga.domain.model.SearchCondition
|
||||
import org.gotson.komga.domain.model.SearchContext
|
||||
import org.gotson.komga.domain.model.SearchOperator
|
||||
import org.gotson.komga.domain.model.makeBook
|
||||
import org.gotson.komga.domain.model.makeLibrary
|
||||
import org.gotson.komga.domain.model.makeSeries
|
||||
|
|
@ -133,8 +136,13 @@ class BookDtoDaoTest(
|
|||
// when
|
||||
val page =
|
||||
bookDtoDao.findAll(
|
||||
BookSearchWithReadProgress(tags = setOf("tag1", "tag2")),
|
||||
user.id,
|
||||
BookSearch(
|
||||
SearchCondition.AnyOfBook(
|
||||
SearchCondition.Tag(SearchOperator.Is("tag1")),
|
||||
SearchCondition.Tag(SearchOperator.Is("tag2")),
|
||||
),
|
||||
),
|
||||
SearchContext(user),
|
||||
Pageable.unpaged(),
|
||||
)
|
||||
|
||||
|
|
@ -167,8 +175,13 @@ class BookDtoDaoTest(
|
|||
// when
|
||||
val page =
|
||||
bookDtoDao.findAll(
|
||||
BookSearchWithReadProgress(authors = listOf(Author("Mark", "writer"), Author("Jim", "inker"))),
|
||||
user.id,
|
||||
BookSearch(
|
||||
SearchCondition.AnyOfBook(
|
||||
SearchCondition.Author(SearchOperator.Is(SearchCondition.AuthorMatch("Mark", "writer"))),
|
||||
SearchCondition.Author(SearchOperator.Is(SearchCondition.AuthorMatch("Jim", "inker"))),
|
||||
),
|
||||
),
|
||||
SearchContext(user),
|
||||
Pageable.unpaged(),
|
||||
)
|
||||
|
||||
|
|
@ -189,8 +202,8 @@ class BookDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
bookDtoDao.findAll(
|
||||
BookSearchWithReadProgress(readStatus = listOf(ReadStatus.READ)),
|
||||
user.id,
|
||||
BookSearch(SearchCondition.ReadStatus(SearchOperator.Is(ReadStatus.READ))),
|
||||
SearchContext(user),
|
||||
PageRequest.of(0, 20),
|
||||
)
|
||||
|
||||
|
|
@ -208,8 +221,8 @@ class BookDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
bookDtoDao.findAll(
|
||||
BookSearchWithReadProgress(readStatus = listOf(ReadStatus.UNREAD)),
|
||||
user.id,
|
||||
BookSearch(SearchCondition.ReadStatus(SearchOperator.Is(ReadStatus.UNREAD))),
|
||||
SearchContext(user),
|
||||
PageRequest.of(0, 20),
|
||||
)
|
||||
|
||||
|
|
@ -227,8 +240,8 @@ class BookDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
bookDtoDao.findAll(
|
||||
BookSearchWithReadProgress(readStatus = listOf(ReadStatus.IN_PROGRESS)),
|
||||
user.id,
|
||||
BookSearch(SearchCondition.ReadStatus(SearchOperator.Is(ReadStatus.IN_PROGRESS))),
|
||||
SearchContext(user),
|
||||
PageRequest.of(0, 20),
|
||||
)
|
||||
|
||||
|
|
@ -246,8 +259,13 @@ class BookDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
bookDtoDao.findAll(
|
||||
BookSearchWithReadProgress(readStatus = listOf(ReadStatus.READ, ReadStatus.UNREAD)),
|
||||
user.id,
|
||||
BookSearch(
|
||||
SearchCondition.AnyOfBook(
|
||||
SearchCondition.ReadStatus(SearchOperator.Is(ReadStatus.READ)),
|
||||
SearchCondition.ReadStatus(SearchOperator.Is(ReadStatus.UNREAD)),
|
||||
),
|
||||
),
|
||||
SearchContext(user),
|
||||
PageRequest.of(0, 20),
|
||||
)
|
||||
|
||||
|
|
@ -264,8 +282,13 @@ class BookDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
bookDtoDao.findAll(
|
||||
BookSearchWithReadProgress(readStatus = listOf(ReadStatus.READ, ReadStatus.IN_PROGRESS)),
|
||||
user.id,
|
||||
BookSearch(
|
||||
SearchCondition.AnyOfBook(
|
||||
SearchCondition.ReadStatus(SearchOperator.Is(ReadStatus.READ)),
|
||||
SearchCondition.ReadStatus(SearchOperator.Is(ReadStatus.IN_PROGRESS)),
|
||||
),
|
||||
),
|
||||
SearchContext(user),
|
||||
PageRequest.of(0, 20),
|
||||
)
|
||||
|
||||
|
|
@ -282,8 +305,13 @@ class BookDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
bookDtoDao.findAll(
|
||||
BookSearchWithReadProgress(readStatus = listOf(ReadStatus.UNREAD, ReadStatus.IN_PROGRESS)),
|
||||
user.id,
|
||||
BookSearch(
|
||||
SearchCondition.AnyOfBook(
|
||||
SearchCondition.ReadStatus(SearchOperator.Is(ReadStatus.UNREAD)),
|
||||
SearchCondition.ReadStatus(SearchOperator.Is(ReadStatus.IN_PROGRESS)),
|
||||
),
|
||||
),
|
||||
SearchContext(user),
|
||||
PageRequest.of(0, 20),
|
||||
)
|
||||
|
||||
|
|
@ -300,8 +328,14 @@ class BookDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
bookDtoDao.findAll(
|
||||
BookSearchWithReadProgress(readStatus = listOf(ReadStatus.UNREAD, ReadStatus.IN_PROGRESS, ReadStatus.READ)),
|
||||
user.id,
|
||||
BookSearch(
|
||||
SearchCondition.AnyOfBook(
|
||||
SearchCondition.ReadStatus(SearchOperator.Is(ReadStatus.READ)),
|
||||
SearchCondition.ReadStatus(SearchOperator.Is(ReadStatus.IN_PROGRESS)),
|
||||
SearchCondition.ReadStatus(SearchOperator.Is(ReadStatus.UNREAD)),
|
||||
),
|
||||
),
|
||||
SearchContext(user),
|
||||
PageRequest.of(0, 20),
|
||||
)
|
||||
|
||||
|
|
@ -318,8 +352,8 @@ class BookDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
bookDtoDao.findAll(
|
||||
BookSearchWithReadProgress(),
|
||||
user.id,
|
||||
BookSearch(),
|
||||
SearchContext(user),
|
||||
PageRequest.of(0, 20),
|
||||
)
|
||||
|
||||
|
|
@ -418,8 +452,8 @@ class BookDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
bookDtoDao.findAll(
|
||||
BookSearchWithReadProgress(searchTerm = "batman"),
|
||||
user.id,
|
||||
BookSearch(fullTextSearch = "batman"),
|
||||
SearchContext(user),
|
||||
UnpagedSorted(Sort.by("relevance")),
|
||||
).content
|
||||
|
||||
|
|
@ -446,28 +480,28 @@ class BookDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
bookDtoDao.findAll(
|
||||
BookSearchWithReadProgress(searchTerm = "book"),
|
||||
user.id,
|
||||
BookSearch(fullTextSearch = "book"),
|
||||
SearchContext(user),
|
||||
UnpagedSorted(Sort.by("name")),
|
||||
).content
|
||||
val pages =
|
||||
(0..2).map {
|
||||
bookDtoDao.findAll(
|
||||
BookSearchWithReadProgress(searchTerm = "book"),
|
||||
user.id,
|
||||
BookSearch(fullTextSearch = "book"),
|
||||
SearchContext(user),
|
||||
PageRequest.of(it, 1, Sort.by("name")),
|
||||
)
|
||||
}
|
||||
val page0 =
|
||||
bookDtoDao.findAll(
|
||||
BookSearchWithReadProgress(searchTerm = "book"),
|
||||
user.id,
|
||||
BookSearch(fullTextSearch = "book"),
|
||||
SearchContext(user),
|
||||
PageRequest.of(0, 2, Sort.by("name")),
|
||||
)
|
||||
val page1 =
|
||||
bookDtoDao.findAll(
|
||||
BookSearchWithReadProgress(searchTerm = "book"),
|
||||
user.id,
|
||||
BookSearch(fullTextSearch = "book"),
|
||||
SearchContext(user),
|
||||
PageRequest.of(1, 2, Sort.by("name")),
|
||||
)
|
||||
|
||||
|
|
@ -511,8 +545,8 @@ class BookDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
bookDtoDao.findAll(
|
||||
BookSearchWithReadProgress(searchTerm = "eric"),
|
||||
user.id,
|
||||
BookSearch(fullTextSearch = "eric"),
|
||||
SearchContext(user),
|
||||
UnpagedSorted(Sort.by("relevance")),
|
||||
).content
|
||||
|
||||
|
|
@ -545,8 +579,8 @@ class BookDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
bookDtoDao.findAll(
|
||||
BookSearchWithReadProgress(searchTerm = "9782413016878"),
|
||||
user.id,
|
||||
BookSearch(fullTextSearch = "9782413016878"),
|
||||
SearchContext(user),
|
||||
UnpagedSorted(Sort.by("relevance")),
|
||||
).content
|
||||
|
||||
|
|
@ -576,8 +610,8 @@ class BookDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
bookDtoDao.findAll(
|
||||
BookSearchWithReadProgress(searchTerm = "tag:tag1"),
|
||||
user.id,
|
||||
BookSearch(fullTextSearch = "tag:tag1"),
|
||||
SearchContext(user),
|
||||
UnpagedSorted(Sort.by("relevance")),
|
||||
).content
|
||||
|
||||
|
|
@ -607,20 +641,20 @@ class BookDtoDaoTest(
|
|||
// when
|
||||
val foundGeneric =
|
||||
bookDtoDao.findAll(
|
||||
BookSearchWithReadProgress(searchTerm = "author:bob"),
|
||||
user.id,
|
||||
BookSearch(fullTextSearch = "author:bob"),
|
||||
SearchContext(user),
|
||||
UnpagedSorted(Sort.by("relevance")),
|
||||
).content
|
||||
val foundByRole =
|
||||
bookDtoDao.findAll(
|
||||
BookSearchWithReadProgress(searchTerm = "writer:bob"),
|
||||
user.id,
|
||||
BookSearch(fullTextSearch = "writer:bob"),
|
||||
SearchContext(user),
|
||||
UnpagedSorted(Sort.by("relevance")),
|
||||
).content
|
||||
val notFound =
|
||||
bookDtoDao.findAll(
|
||||
BookSearchWithReadProgress(searchTerm = "penciller:bob"),
|
||||
user.id,
|
||||
BookSearch(fullTextSearch = "penciller:bob"),
|
||||
SearchContext(user),
|
||||
UnpagedSorted(Sort.by("relevance")),
|
||||
).content
|
||||
|
||||
|
|
@ -648,8 +682,8 @@ class BookDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
bookDtoDao.findAll(
|
||||
BookSearchWithReadProgress(searchTerm = "release_date:1999"),
|
||||
user.id,
|
||||
BookSearch(fullTextSearch = "release_date:1999"),
|
||||
SearchContext(user),
|
||||
UnpagedSorted(Sort.by("relevance")),
|
||||
).content
|
||||
|
||||
|
|
@ -678,8 +712,8 @@ class BookDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
bookDtoDao.findAll(
|
||||
BookSearchWithReadProgress(searchTerm = "release_date:[1990 TO 2010]"),
|
||||
user.id,
|
||||
BookSearch(fullTextSearch = "release_date:[1990 TO 2010]"),
|
||||
SearchContext(user),
|
||||
UnpagedSorted(Sort.by("relevance")),
|
||||
).content
|
||||
|
||||
|
|
@ -704,8 +738,8 @@ class BookDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
bookDtoDao.findAll(
|
||||
BookSearchWithReadProgress(searchTerm = "status:error"),
|
||||
user.id,
|
||||
BookSearch(fullTextSearch = "status:error"),
|
||||
SearchContext(user),
|
||||
UnpagedSorted(Sort.by("relevance")),
|
||||
).content
|
||||
|
||||
|
|
@ -734,8 +768,8 @@ class BookDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
bookDtoDao.findAll(
|
||||
BookSearchWithReadProgress(searchTerm = "deleted:true"),
|
||||
user.id,
|
||||
BookSearch(fullTextSearch = "deleted:true"),
|
||||
SearchContext(user),
|
||||
UnpagedSorted(Sort.by("relevance")),
|
||||
).content
|
||||
|
||||
|
|
@ -761,8 +795,8 @@ class BookDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
bookDtoDao.findAll(
|
||||
BookSearchWithReadProgress(searchTerm = "s.w.o.r.d."),
|
||||
user.id,
|
||||
BookSearch(fullTextSearch = "s.w.o.r.d"),
|
||||
SearchContext(user),
|
||||
UnpagedSorted(Sort.by("relevance")),
|
||||
).content
|
||||
|
||||
|
|
@ -790,8 +824,8 @@ class BookDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
bookDtoDao.findAll(
|
||||
BookSearchWithReadProgress(searchTerm = "batman robin"),
|
||||
user.id,
|
||||
BookSearch(fullTextSearch = "batman robin"),
|
||||
SearchContext(user),
|
||||
UnpagedSorted(Sort.by("relevance")),
|
||||
).content
|
||||
|
||||
|
|
@ -818,8 +852,8 @@ class BookDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
bookDtoDao.findAll(
|
||||
BookSearchWithReadProgress(searchTerm = "x-men"),
|
||||
user.id,
|
||||
BookSearch(fullTextSearch = "x-men"),
|
||||
SearchContext(user),
|
||||
UnpagedSorted(Sort.by("relevance")),
|
||||
).content
|
||||
|
||||
|
|
@ -834,8 +868,8 @@ class BookDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
bookDtoDao.findAll(
|
||||
BookSearchWithReadProgress(searchTerm = "publisher:batman"),
|
||||
user.id,
|
||||
BookSearch(fullTextSearch = "publisher:batman"),
|
||||
SearchContext(user),
|
||||
UnpagedSorted(Sort.by("relevance")),
|
||||
).content
|
||||
|
||||
|
|
@ -860,8 +894,8 @@ class BookDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
bookDtoDao.findAll(
|
||||
BookSearchWithReadProgress(searchTerm = "不道德"),
|
||||
user.id,
|
||||
BookSearch(fullTextSearch = "不道德"),
|
||||
SearchContext(user),
|
||||
UnpagedSorted(Sort.by("relevance")),
|
||||
).content
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,923 @@
|
|||
package org.gotson.komga.infrastructure.jooq.main
|
||||
|
||||
import com.ninjasquad.springmockk.MockkBean
|
||||
import io.mockk.Runs
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.catchThrowable
|
||||
import org.gotson.komga.domain.model.Author
|
||||
import org.gotson.komga.domain.model.BookSearch
|
||||
import org.gotson.komga.domain.model.KomgaUser
|
||||
import org.gotson.komga.domain.model.Media
|
||||
import org.gotson.komga.domain.model.MediaProfile
|
||||
import org.gotson.komga.domain.model.MediaType
|
||||
import org.gotson.komga.domain.model.ReadList
|
||||
import org.gotson.komga.domain.model.ReadProgress
|
||||
import org.gotson.komga.domain.model.ReadStatus
|
||||
import org.gotson.komga.domain.model.SearchCondition
|
||||
import org.gotson.komga.domain.model.SearchCondition.AuthorMatch
|
||||
import org.gotson.komga.domain.model.SearchContext
|
||||
import org.gotson.komga.domain.model.SearchOperator
|
||||
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.BookMetadataRepository
|
||||
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.ReadListRepository
|
||||
import org.gotson.komga.domain.persistence.ReadProgressRepository
|
||||
import org.gotson.komga.domain.service.BookLifecycle
|
||||
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.infrastructure.jooq.UnpagedSorted
|
||||
import org.junit.jupiter.api.AfterAll
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeAll
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.context.ApplicationEventPublisher
|
||||
import org.springframework.data.domain.Pageable
|
||||
import org.springframework.data.domain.Sort
|
||||
import java.time.Duration
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
@SpringBootTest
|
||||
class BookSearchTest(
|
||||
@Autowired private val bookDao: BookDao,
|
||||
@Autowired private val bookDtoDao: BookDtoDao,
|
||||
@Autowired private val libraryRepository: LibraryRepository,
|
||||
@Autowired private val bookMetadataRepository: BookMetadataRepository,
|
||||
@Autowired private val mediaRepository: MediaRepository,
|
||||
@Autowired private val readProgressRepository: ReadProgressRepository,
|
||||
@Autowired private val userRepository: KomgaUserRepository,
|
||||
@Autowired private val bookRepository: BookRepository,
|
||||
@Autowired private val bookLifecycle: BookLifecycle,
|
||||
@Autowired private val seriesLifecycle: SeriesLifecycle,
|
||||
@Autowired private val libraryLifecycle: LibraryLifecycle,
|
||||
@Autowired private val userLifecycle: KomgaUserLifecycle,
|
||||
@Autowired private val readListRepository: ReadListRepository,
|
||||
) {
|
||||
private val library1 = makeLibrary()
|
||||
private val library2 = makeLibrary()
|
||||
private val series1 = makeSeries("Series 1").copy(libraryId = library1.id)
|
||||
private val series2 = makeSeries("Series 2").copy(libraryId = library2.id)
|
||||
private val user1 = KomgaUser("user1@example.org", "p", false)
|
||||
private val user2 = KomgaUser("user2@example.org", "p", false)
|
||||
|
||||
@MockkBean
|
||||
private lateinit var mockEventPublisher: ApplicationEventPublisher
|
||||
|
||||
@BeforeAll
|
||||
fun setup() {
|
||||
every { mockEventPublisher.publishEvent(any()) } just Runs
|
||||
libraryRepository.insert(library1)
|
||||
libraryRepository.insert(library2)
|
||||
seriesLifecycle.createSeries(series1)
|
||||
seriesLifecycle.createSeries(series2)
|
||||
userRepository.insert(user1)
|
||||
userRepository.insert(user2)
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
every { mockEventPublisher.publishEvent(any()) } just Runs
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun deleteBooks() {
|
||||
readListRepository.deleteAll()
|
||||
bookLifecycle.deleteMany(bookRepository.findAll())
|
||||
assertThat(bookDao.count()).isEqualTo(0)
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun tearDown() {
|
||||
every { mockEventPublisher.publishEvent(any()) } just Runs
|
||||
userRepository.findAll().forEach {
|
||||
userLifecycle.deleteUser(it)
|
||||
}
|
||||
libraryRepository.findAll().forEach {
|
||||
libraryLifecycle.deleteLibrary(it)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given some books when searching by library then results are accurate`() {
|
||||
val book1 = makeBook("1", libraryId = library1.id, seriesId = series1.id)
|
||||
val book2 = makeBook("2", libraryId = library2.id, seriesId = series2.id)
|
||||
seriesLifecycle.addBooks(series1, listOf(book1))
|
||||
seriesLifecycle.addBooks(series2, listOf(book2))
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.LibraryId(SearchOperator.Is(library1.id)))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), UnpagedSorted(Sort.by("readList.number"))).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), UnpagedSorted(Sort.by("readList.number"))).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("1")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1")
|
||||
}
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.LibraryId(SearchOperator.IsNot(library1.id)))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("2")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("2")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given some books when searching by series then results are accurate`() {
|
||||
val book1 = makeBook("1", libraryId = library1.id, seriesId = series1.id)
|
||||
val book2 = makeBook("2", libraryId = library2.id, seriesId = series2.id)
|
||||
seriesLifecycle.addBooks(series1, listOf(book1))
|
||||
seriesLifecycle.addBooks(series2, listOf(book2))
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.SeriesId(SearchOperator.Is(series1.id)))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("1")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1")
|
||||
}
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.SeriesId(SearchOperator.IsNot(series1.id)))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("2")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("2")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given some books when searching by read list then results are accurate`() {
|
||||
val book1 = makeBook("1", libraryId = library1.id, seriesId = series1.id)
|
||||
val book2 = makeBook("2", libraryId = library2.id, seriesId = series2.id)
|
||||
seriesLifecycle.addBooks(series1, listOf(book1))
|
||||
seriesLifecycle.addBooks(series2, listOf(book2))
|
||||
val readList = ReadList("rl1", bookIds = mapOf(1 to book1.id).toSortedMap())
|
||||
readListRepository.insert(readList)
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.ReadListId(SearchOperator.Is(readList.id)))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("1")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1")
|
||||
}
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.ReadListId(SearchOperator.IsNot(readList.id)))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("2")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("2")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given some books when searching by deleted then results are accurate`() {
|
||||
val book1 = makeBook("1", libraryId = library1.id, seriesId = series1.id).copy(deletedDate = LocalDateTime.now())
|
||||
val book2 = makeBook("2", libraryId = library2.id, seriesId = series2.id)
|
||||
seriesLifecycle.addBooks(series1, listOf(book1))
|
||||
seriesLifecycle.addBooks(series2, listOf(book2))
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.Deleted(SearchOperator.IsTrue))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("1")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1")
|
||||
}
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.Deleted(SearchOperator.IsFalse))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("2")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("2")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given some books when searching by title then results are accurate`() {
|
||||
makeBook("1", libraryId = library1.id, seriesId = series1.id).let { book ->
|
||||
seriesLifecycle.addBooks(series1, listOf(book))
|
||||
bookMetadataRepository.findById(book.id).let {
|
||||
bookMetadataRepository.update(it.copy(title = "Book 1"))
|
||||
}
|
||||
}
|
||||
makeBook("2", libraryId = library2.id, seriesId = series2.id).let { book ->
|
||||
seriesLifecycle.addBooks(series2, listOf(book))
|
||||
bookMetadataRepository.findById(book.id).let {
|
||||
bookMetadataRepository.update(it.copy(title = "Book 2"))
|
||||
}
|
||||
}
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.Title(SearchOperator.Is("boOK 1")))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("1")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1")
|
||||
}
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.Title(SearchOperator.IsNot("book 1")))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("2")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("2")
|
||||
}
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.Title(SearchOperator.Contains("book")))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("1", "2")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1", "2")
|
||||
}
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.Title(SearchOperator.DoesNotContain("1")))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("2")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("2")
|
||||
}
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.Title(SearchOperator.BeginsWith("book")))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("1", "2")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1", "2")
|
||||
}
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.Title(SearchOperator.EndsWith("1")))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("1")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given some books when searching by release date then results are accurate`() {
|
||||
makeBook("1", libraryId = library1.id, seriesId = series1.id).let { book ->
|
||||
seriesLifecycle.addBooks(series1, listOf(book))
|
||||
bookMetadataRepository.findById(book.id).let {
|
||||
bookMetadataRepository.update(it.copy(releaseDate = LocalDate.now().minusDays(5)))
|
||||
}
|
||||
}
|
||||
makeBook("2", libraryId = library2.id, seriesId = series2.id).let { book ->
|
||||
seriesLifecycle.addBooks(series2, listOf(book))
|
||||
}
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.ReleaseDate(SearchOperator.After(ZonedDateTime.now().minusDays(10))))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("1")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1")
|
||||
}
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.ReleaseDate(SearchOperator.Before(ZonedDateTime.now())))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("1")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1")
|
||||
}
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.ReleaseDate(SearchOperator.IsInTheLast(Duration.ofDays(10))))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("1")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1")
|
||||
}
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.ReleaseDate(SearchOperator.IsNotInTheLast(Duration.ofDays(10))))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found).isEmpty()
|
||||
assertThat(foundDto).isEmpty()
|
||||
}
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.ReleaseDate(SearchOperator.IsNotInTheLast(Duration.ofDays(1))))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("1")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1")
|
||||
}
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.ReleaseDate(SearchOperator.IsNull))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("2")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("2")
|
||||
}
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.ReleaseDate(SearchOperator.IsNotNull))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("1")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given some books when searching by number sort then results are accurate`() {
|
||||
makeBook("1", libraryId = library1.id, seriesId = series1.id).let { book ->
|
||||
seriesLifecycle.addBooks(series1, listOf(book))
|
||||
bookMetadataRepository.findById(book.id).let {
|
||||
bookMetadataRepository.update(it.copy(numberSort = 1F))
|
||||
}
|
||||
}
|
||||
makeBook("2", libraryId = library2.id, seriesId = series2.id).let { book ->
|
||||
seriesLifecycle.addBooks(series2, listOf(book))
|
||||
bookMetadataRepository.findById(book.id).let {
|
||||
bookMetadataRepository.update(it.copy(numberSort = 10.5F))
|
||||
}
|
||||
}
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.NumberSort(SearchOperator.Is(10.5F)))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("2")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("2")
|
||||
}
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.NumberSort(SearchOperator.IsNot(10.5F)))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("1")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1")
|
||||
}
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.NumberSort(SearchOperator.GreaterThan(0F)))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("1", "2")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1", "2")
|
||||
}
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.NumberSort(SearchOperator.GreaterThan(5F)))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("2")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("2")
|
||||
}
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.NumberSort(SearchOperator.LessThan(11F)))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("1", "2")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1", "2")
|
||||
}
|
||||
|
||||
run {
|
||||
val search =
|
||||
BookSearch(
|
||||
SearchCondition.AnyOfBook(
|
||||
listOf(
|
||||
SearchCondition.NumberSort(SearchOperator.Is(10.5F)),
|
||||
SearchCondition.NumberSort(SearchOperator.Is(1F)),
|
||||
),
|
||||
),
|
||||
)
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("1", "2")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1", "2")
|
||||
}
|
||||
|
||||
run {
|
||||
val search =
|
||||
BookSearch(
|
||||
SearchCondition.AllOfBook(
|
||||
listOf(
|
||||
SearchCondition.NumberSort(SearchOperator.Is(10.5F)),
|
||||
SearchCondition.NumberSort(SearchOperator.Is(1F)),
|
||||
),
|
||||
),
|
||||
)
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found).isEmpty()
|
||||
assertThat(foundDto).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given some books when searching by tag then results are accurate`() {
|
||||
makeBook("1", libraryId = library1.id, seriesId = series1.id).let { book ->
|
||||
seriesLifecycle.addBooks(series1, listOf(book))
|
||||
bookMetadataRepository.findById(book.id).let {
|
||||
bookMetadataRepository.update(it.copy(tags = setOf("fiction", "horror")))
|
||||
}
|
||||
}
|
||||
makeBook("2", libraryId = library2.id, seriesId = series2.id).let { book ->
|
||||
seriesLifecycle.addBooks(series2, listOf(book))
|
||||
bookMetadataRepository.findById(book.id).let {
|
||||
bookMetadataRepository.update(it.copy(tags = setOf("fiction")))
|
||||
}
|
||||
}
|
||||
makeBook("3", libraryId = library2.id, seriesId = series2.id).let { book ->
|
||||
seriesLifecycle.addBooks(series2, listOf(book))
|
||||
bookMetadataRepository.findById(book.id).let {
|
||||
bookMetadataRepository.update(it.copy(tags = setOf("fantasy")))
|
||||
}
|
||||
}
|
||||
makeBook("4", libraryId = library2.id, seriesId = series2.id).let { book ->
|
||||
seriesLifecycle.addBooks(series2, listOf(book))
|
||||
}
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.Tag(SearchOperator.Is("FICTION")))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("1", "2")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1", "2")
|
||||
}
|
||||
|
||||
run {
|
||||
val search =
|
||||
BookSearch(
|
||||
SearchCondition.AllOfBook(
|
||||
listOf(
|
||||
SearchCondition.Tag(SearchOperator.Is("FICTION")),
|
||||
SearchCondition.Tag(SearchOperator.Is("horror")),
|
||||
),
|
||||
),
|
||||
)
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("1")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1")
|
||||
}
|
||||
|
||||
run {
|
||||
val search =
|
||||
BookSearch(
|
||||
SearchCondition.AnyOfBook(
|
||||
listOf(
|
||||
SearchCondition.Tag(SearchOperator.Is("horror")),
|
||||
SearchCondition.Tag(SearchOperator.Is("notexist")),
|
||||
SearchCondition.Tag(SearchOperator.Is("fantasy")),
|
||||
),
|
||||
),
|
||||
)
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("1", "3")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1", "3")
|
||||
}
|
||||
|
||||
run {
|
||||
val search =
|
||||
BookSearch(
|
||||
SearchCondition.AllOfBook(
|
||||
listOf(
|
||||
SearchCondition.Tag(SearchOperator.Is("fiction")),
|
||||
SearchCondition.Tag(SearchOperator.IsNot("horror")),
|
||||
),
|
||||
),
|
||||
)
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("2")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("2")
|
||||
}
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.Tag(SearchOperator.IsNot("fiction")))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("3", "4")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("3", "4")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given some books when searching by media status then results are accurate`() {
|
||||
makeBook("1", libraryId = library1.id, seriesId = series1.id).let { book ->
|
||||
seriesLifecycle.addBooks(series1, listOf(book))
|
||||
mediaRepository.findById(book.id).let {
|
||||
mediaRepository.update(it.copy(status = Media.Status.READY))
|
||||
}
|
||||
}
|
||||
makeBook("2", libraryId = library2.id, seriesId = series2.id).let { book ->
|
||||
seriesLifecycle.addBooks(series2, listOf(book))
|
||||
mediaRepository.findById(book.id).let {
|
||||
mediaRepository.update(it.copy(status = Media.Status.ERROR))
|
||||
}
|
||||
}
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.MediaStatus(SearchOperator.Is(Media.Status.READY)))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("1")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1")
|
||||
}
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.MediaStatus(SearchOperator.IsNot(Media.Status.READY)))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("2")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("2")
|
||||
}
|
||||
|
||||
run {
|
||||
val search =
|
||||
BookSearch(
|
||||
SearchCondition.AllOfBook(
|
||||
listOf(
|
||||
SearchCondition.MediaStatus(SearchOperator.Is(Media.Status.READY)),
|
||||
SearchCondition.MediaStatus(SearchOperator.Is(Media.Status.ERROR)),
|
||||
),
|
||||
),
|
||||
)
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found).isEmpty()
|
||||
assertThat(foundDto).isEmpty()
|
||||
}
|
||||
|
||||
run {
|
||||
val search =
|
||||
BookSearch(
|
||||
SearchCondition.AnyOfBook(
|
||||
listOf(
|
||||
SearchCondition.MediaStatus(SearchOperator.Is(Media.Status.READY)),
|
||||
SearchCondition.MediaStatus(SearchOperator.Is(Media.Status.ERROR)),
|
||||
),
|
||||
),
|
||||
)
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("1", "2")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1", "2")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given some books when searching by media profile then results are accurate`() {
|
||||
makeBook("1", libraryId = library1.id, seriesId = series1.id).let { book ->
|
||||
seriesLifecycle.addBooks(series1, listOf(book))
|
||||
mediaRepository.findById(book.id).let {
|
||||
mediaRepository.update(it.copy(mediaType = MediaType.ZIP.type))
|
||||
}
|
||||
}
|
||||
makeBook("2", libraryId = library2.id, seriesId = series2.id).let { book ->
|
||||
seriesLifecycle.addBooks(series2, listOf(book))
|
||||
mediaRepository.findById(book.id).let {
|
||||
mediaRepository.update(it.copy(mediaType = MediaType.RAR_4.type))
|
||||
}
|
||||
}
|
||||
makeBook("3", libraryId = library2.id, seriesId = series2.id).let { book ->
|
||||
seriesLifecycle.addBooks(series2, listOf(book))
|
||||
mediaRepository.findById(book.id).let {
|
||||
mediaRepository.update(it.copy(mediaType = MediaType.EPUB.type))
|
||||
}
|
||||
}
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.MediaProfile(SearchOperator.Is(MediaProfile.DIVINA)))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("1", "2")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1", "2")
|
||||
}
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.MediaProfile(SearchOperator.IsNot(MediaProfile.EPUB)))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("1", "2")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1", "2")
|
||||
}
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.MediaProfile(SearchOperator.Is(MediaProfile.EPUB)))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("3")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("3")
|
||||
}
|
||||
|
||||
run {
|
||||
val search =
|
||||
BookSearch(
|
||||
SearchCondition.AllOfBook(
|
||||
listOf(
|
||||
SearchCondition.MediaProfile(SearchOperator.Is(MediaProfile.DIVINA)),
|
||||
SearchCondition.MediaProfile(SearchOperator.Is(MediaProfile.EPUB)),
|
||||
),
|
||||
),
|
||||
)
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found).isEmpty()
|
||||
assertThat(foundDto).isEmpty()
|
||||
}
|
||||
|
||||
run {
|
||||
val search =
|
||||
BookSearch(
|
||||
SearchCondition.AnyOfBook(
|
||||
listOf(
|
||||
SearchCondition.MediaProfile(SearchOperator.Is(MediaProfile.DIVINA)),
|
||||
SearchCondition.MediaProfile(SearchOperator.Is(MediaProfile.EPUB)),
|
||||
),
|
||||
),
|
||||
)
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("1", "2", "3")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1", "2", "3")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given some books when searching by read progress then results are accurate`() {
|
||||
makeBook("1", libraryId = library1.id, seriesId = series1.id).let { book ->
|
||||
seriesLifecycle.addBooks(series1, listOf(book))
|
||||
readProgressRepository.save(ReadProgress(book.id, user1.id, 5, false))
|
||||
}
|
||||
makeBook("2", libraryId = library2.id, seriesId = series2.id).let { book ->
|
||||
seriesLifecycle.addBooks(series2, listOf(book))
|
||||
readProgressRepository.save(ReadProgress(book.id, user1.id, 10, true))
|
||||
}
|
||||
makeBook("3", libraryId = library2.id, seriesId = series2.id).let { book ->
|
||||
seriesLifecycle.addBooks(series2, listOf(book))
|
||||
}
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.ReadStatus(SearchOperator.Is(ReadStatus.IN_PROGRESS)))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("1")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1")
|
||||
}
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.ReadStatus(SearchOperator.Is(ReadStatus.UNREAD)))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("3")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("3")
|
||||
}
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.ReadStatus(SearchOperator.Is(ReadStatus.READ)))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("2")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("2")
|
||||
}
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.ReadStatus(SearchOperator.Is(ReadStatus.READ)))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user2), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user2), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found).isEmpty()
|
||||
assertThat(foundDto).isEmpty()
|
||||
}
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.ReadStatus(SearchOperator.Is(ReadStatus.UNREAD)))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user2), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user2), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("1", "2", "3")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1", "2", "3")
|
||||
}
|
||||
|
||||
run {
|
||||
val search = BookSearch(SearchCondition.ReadStatus(SearchOperator.Is(ReadStatus.READ)))
|
||||
val found = bookDao.findAll(search.condition, SearchContext(null), Pageable.unpaged()).content
|
||||
val thrown = catchThrowable { bookDtoDao.findAll(search, SearchContext(null), Pageable.unpaged()) }
|
||||
|
||||
assertThat(found).isEmpty()
|
||||
assertThat(thrown).isInstanceOf(IllegalArgumentException::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given some books when searching by author then results are accurate`() {
|
||||
makeBook("1", libraryId = library1.id, seriesId = series1.id).let { book ->
|
||||
seriesLifecycle.addBooks(series1, listOf(book))
|
||||
bookMetadataRepository.findById(book.id).let {
|
||||
bookMetadataRepository.update(it.copy(authors = listOf(Author("john", "writer"), Author("jim", "cover"))))
|
||||
}
|
||||
}
|
||||
makeBook("2", libraryId = library2.id, seriesId = series2.id).let { book ->
|
||||
seriesLifecycle.addBooks(series2, listOf(book))
|
||||
bookMetadataRepository.findById(book.id).let {
|
||||
bookMetadataRepository.update(it.copy(authors = listOf(Author("john", "artist"), Author("amanda", "artist"))))
|
||||
}
|
||||
}
|
||||
makeBook("3", libraryId = library2.id, seriesId = series2.id).let { book ->
|
||||
seriesLifecycle.addBooks(series2, listOf(book))
|
||||
bookMetadataRepository.findById(book.id).let {
|
||||
bookMetadataRepository.update(it.copy(authors = listOf(Author("jack", "writer"))))
|
||||
}
|
||||
}
|
||||
makeBook("4", libraryId = library2.id, seriesId = series2.id).let { book ->
|
||||
seriesLifecycle.addBooks(series2, listOf(book))
|
||||
}
|
||||
|
||||
// books with an author named 'john'
|
||||
run {
|
||||
val search =
|
||||
BookSearch(
|
||||
SearchCondition.Author(SearchOperator.Is(AuthorMatch("john"))),
|
||||
)
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("1", "2")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1", "2")
|
||||
}
|
||||
|
||||
// books with an author named 'john' with the 'writer' role
|
||||
run {
|
||||
val search =
|
||||
BookSearch(
|
||||
SearchCondition.Author(SearchOperator.Is(AuthorMatch("john", "writer"))),
|
||||
)
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("1")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1")
|
||||
}
|
||||
|
||||
// books with any author with the 'writer' role
|
||||
run {
|
||||
val search =
|
||||
BookSearch(
|
||||
SearchCondition.Author(SearchOperator.Is(AuthorMatch(role = "writer"))),
|
||||
)
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("1", "3")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1", "3")
|
||||
}
|
||||
|
||||
// books without any author named 'john' with a 'writer' role
|
||||
run {
|
||||
val search =
|
||||
BookSearch(
|
||||
SearchCondition.Author(SearchOperator.IsNot(AuthorMatch("john", "writer"))),
|
||||
)
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("2", "3", "4")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("2", "3", "4")
|
||||
}
|
||||
|
||||
// books without any author named 'john'
|
||||
run {
|
||||
val search =
|
||||
BookSearch(
|
||||
SearchCondition.Author(SearchOperator.IsNot(AuthorMatch("jOhn"))),
|
||||
)
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("3", "4")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("3", "4")
|
||||
}
|
||||
|
||||
// books without any author with the 'writer' role
|
||||
run {
|
||||
val search =
|
||||
BookSearch(
|
||||
SearchCondition.Author(SearchOperator.IsNot(AuthorMatch(role = "writer"))),
|
||||
)
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("2", "4")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("2", "4")
|
||||
}
|
||||
|
||||
// empty AuthorMatch does not apply any condition
|
||||
run {
|
||||
val search =
|
||||
BookSearch(
|
||||
SearchCondition.Author(SearchOperator.Is(AuthorMatch())),
|
||||
)
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("1", "2", "3", "4")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1", "2", "3", "4")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given some books when searching by one-shot then results are accurate`() {
|
||||
makeBook("1", libraryId = library1.id, seriesId = series1.id).copy(oneshot = true).let { book ->
|
||||
seriesLifecycle.addBooks(series1, listOf(book))
|
||||
}
|
||||
makeBook("2", libraryId = library2.id, seriesId = series2.id).let { book ->
|
||||
seriesLifecycle.addBooks(series2, listOf(book))
|
||||
}
|
||||
|
||||
run {
|
||||
val search =
|
||||
BookSearch(
|
||||
SearchCondition.OneShot(SearchOperator.IsTrue),
|
||||
)
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("1")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("1")
|
||||
}
|
||||
|
||||
run {
|
||||
val search =
|
||||
BookSearch(
|
||||
SearchCondition.OneShot(SearchOperator.IsFalse),
|
||||
)
|
||||
val found = bookDao.findAll(search.condition, SearchContext(user1), Pageable.unpaged()).content
|
||||
val foundDto = bookDtoDao.findAll(search, SearchContext(user1), Pageable.unpaged()).content
|
||||
|
||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("2")
|
||||
assertThat(foundDto.map { it.name }).containsExactlyInAnyOrder("2")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ package org.gotson.komga.infrastructure.jooq.main
|
|||
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.gotson.komga.domain.model.Series
|
||||
import org.gotson.komga.domain.model.SeriesSearch
|
||||
import org.gotson.komga.domain.model.makeLibrary
|
||||
import org.gotson.komga.domain.persistence.LibraryRepository
|
||||
import org.gotson.komga.infrastructure.jooq.offset
|
||||
|
|
@ -200,43 +199,6 @@ class SeriesDaoTest(
|
|||
assertThat(found).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given existing series when searching then result is returned`() {
|
||||
val series =
|
||||
Series(
|
||||
name = "Series",
|
||||
url = URL("file://series"),
|
||||
fileLastModified = LocalDateTime.now(),
|
||||
libraryId = library.id,
|
||||
)
|
||||
|
||||
seriesDao.insert(series)
|
||||
|
||||
val search =
|
||||
SeriesSearch(
|
||||
libraryIds = listOf(library.id),
|
||||
)
|
||||
val found = seriesDao.findAll(search)
|
||||
|
||||
assertThat(found).hasSize(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given existing series when searching by regex then result is returned`() {
|
||||
val series =
|
||||
Series(
|
||||
name = "my Series",
|
||||
url = URL("file://series"),
|
||||
fileLastModified = LocalDateTime.now(),
|
||||
libraryId = library.id,
|
||||
)
|
||||
seriesDao.insert(series)
|
||||
|
||||
assertThat(seriesDao.findAll(SeriesSearch(searchRegex = Pair("^my", SeriesSearch.SearchField.NAME)))).hasSize(1)
|
||||
assertThat(seriesDao.findAll(SeriesSearch(searchRegex = Pair("ries$", SeriesSearch.SearchField.NAME)))).hasSize(1)
|
||||
assertThat(seriesDao.findAll(SeriesSearch(searchRegex = Pair("series", SeriesSearch.SearchField.NAME)))).hasSize(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given existing series when finding by libraryId then series are returned`() {
|
||||
val series =
|
||||
|
|
|
|||
|
|
@ -9,8 +9,11 @@ import org.gotson.komga.domain.model.Author
|
|||
import org.gotson.komga.domain.model.KomgaUser
|
||||
import org.gotson.komga.domain.model.ReadProgress
|
||||
import org.gotson.komga.domain.model.ReadStatus
|
||||
import org.gotson.komga.domain.model.SearchCondition
|
||||
import org.gotson.komga.domain.model.SearchContext
|
||||
import org.gotson.komga.domain.model.SearchOperator
|
||||
import org.gotson.komga.domain.model.SeriesMetadata
|
||||
import org.gotson.komga.domain.model.SeriesSearchWithReadProgress
|
||||
import org.gotson.komga.domain.model.SeriesSearch
|
||||
import org.gotson.komga.domain.model.makeBook
|
||||
import org.gotson.komga.domain.model.makeLibrary
|
||||
import org.gotson.komga.domain.model.makeSeries
|
||||
|
|
@ -133,8 +136,8 @@ class SeriesDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
seriesDtoDao.findAll(
|
||||
SeriesSearchWithReadProgress(readStatus = listOf(ReadStatus.READ)),
|
||||
user.id,
|
||||
SeriesSearch(SearchCondition.ReadStatus(SearchOperator.Is(ReadStatus.READ))),
|
||||
SearchContext(user),
|
||||
PageRequest.of(0, 20),
|
||||
).sortedBy { it.name }
|
||||
|
||||
|
|
@ -153,8 +156,8 @@ class SeriesDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
seriesDtoDao.findAll(
|
||||
SeriesSearchWithReadProgress(readStatus = listOf(ReadStatus.UNREAD)),
|
||||
user.id,
|
||||
SeriesSearch(SearchCondition.ReadStatus(SearchOperator.Is(ReadStatus.UNREAD))),
|
||||
SearchContext(user),
|
||||
PageRequest.of(0, 20),
|
||||
).sortedBy { it.name }
|
||||
|
||||
|
|
@ -173,8 +176,8 @@ class SeriesDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
seriesDtoDao.findAll(
|
||||
SeriesSearchWithReadProgress(readStatus = listOf(ReadStatus.IN_PROGRESS)),
|
||||
user.id,
|
||||
SeriesSearch(SearchCondition.ReadStatus(SearchOperator.Is(ReadStatus.IN_PROGRESS))),
|
||||
SearchContext(user),
|
||||
PageRequest.of(0, 20),
|
||||
).sortedBy { it.name }
|
||||
|
||||
|
|
@ -196,8 +199,13 @@ class SeriesDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
seriesDtoDao.findAll(
|
||||
SeriesSearchWithReadProgress(readStatus = listOf(ReadStatus.READ, ReadStatus.UNREAD)),
|
||||
user.id,
|
||||
SeriesSearch(
|
||||
SearchCondition.AnyOfSeries(
|
||||
SearchCondition.ReadStatus(SearchOperator.Is(ReadStatus.READ)),
|
||||
SearchCondition.ReadStatus(SearchOperator.Is(ReadStatus.UNREAD)),
|
||||
),
|
||||
),
|
||||
SearchContext(user),
|
||||
PageRequest.of(0, 20),
|
||||
).sortedBy { it.name }
|
||||
|
||||
|
|
@ -214,8 +222,13 @@ class SeriesDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
seriesDtoDao.findAll(
|
||||
SeriesSearchWithReadProgress(readStatus = listOf(ReadStatus.READ, ReadStatus.IN_PROGRESS)),
|
||||
user.id,
|
||||
SeriesSearch(
|
||||
SearchCondition.AnyOfSeries(
|
||||
SearchCondition.ReadStatus(SearchOperator.Is(ReadStatus.READ)),
|
||||
SearchCondition.ReadStatus(SearchOperator.Is(ReadStatus.IN_PROGRESS)),
|
||||
),
|
||||
),
|
||||
SearchContext(user),
|
||||
PageRequest.of(0, 20),
|
||||
).sortedBy { it.name }
|
||||
|
||||
|
|
@ -232,8 +245,13 @@ class SeriesDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
seriesDtoDao.findAll(
|
||||
SeriesSearchWithReadProgress(readStatus = listOf(ReadStatus.UNREAD, ReadStatus.IN_PROGRESS)),
|
||||
user.id,
|
||||
SeriesSearch(
|
||||
SearchCondition.AnyOfSeries(
|
||||
SearchCondition.ReadStatus(SearchOperator.Is(ReadStatus.UNREAD)),
|
||||
SearchCondition.ReadStatus(SearchOperator.Is(ReadStatus.IN_PROGRESS)),
|
||||
),
|
||||
),
|
||||
SearchContext(user),
|
||||
PageRequest.of(0, 20),
|
||||
).sortedBy { it.name }
|
||||
|
||||
|
|
@ -250,8 +268,14 @@ class SeriesDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
seriesDtoDao.findAll(
|
||||
SeriesSearchWithReadProgress(readStatus = listOf(ReadStatus.READ, ReadStatus.IN_PROGRESS, ReadStatus.UNREAD)),
|
||||
user.id,
|
||||
SeriesSearch(
|
||||
SearchCondition.AnyOfSeries(
|
||||
SearchCondition.ReadStatus(SearchOperator.Is(ReadStatus.READ)),
|
||||
SearchCondition.ReadStatus(SearchOperator.Is(ReadStatus.IN_PROGRESS)),
|
||||
SearchCondition.ReadStatus(SearchOperator.Is(ReadStatus.UNREAD)),
|
||||
),
|
||||
),
|
||||
SearchContext(user),
|
||||
PageRequest.of(0, 20),
|
||||
).sortedBy { it.name }
|
||||
|
||||
|
|
@ -268,8 +292,7 @@ class SeriesDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
seriesDtoDao.findAll(
|
||||
SeriesSearchWithReadProgress(),
|
||||
user.id,
|
||||
SearchContext(user),
|
||||
PageRequest.of(0, 20),
|
||||
).sortedBy { it.name }
|
||||
|
||||
|
|
@ -294,8 +317,8 @@ class SeriesDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
seriesDtoDao.findAll(
|
||||
SeriesSearchWithReadProgress(searchTerm = "batman"),
|
||||
user.id,
|
||||
SeriesSearch(fullTextSearch = "batman"),
|
||||
SearchContext(user),
|
||||
UnpagedSorted(Sort.by("relevance")),
|
||||
).content
|
||||
|
||||
|
|
@ -320,8 +343,8 @@ class SeriesDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
seriesDtoDao.findAll(
|
||||
SeriesSearchWithReadProgress(searchTerm = "publisher:vertigo"),
|
||||
user.id,
|
||||
SeriesSearch(fullTextSearch = "publisher:vertigo"),
|
||||
SearchContext(user),
|
||||
UnpagedSorted(Sort.by("relevance")),
|
||||
).content
|
||||
|
||||
|
|
@ -346,8 +369,8 @@ class SeriesDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
seriesDtoDao.findAll(
|
||||
SeriesSearchWithReadProgress(searchTerm = "status:hiatus"),
|
||||
user.id,
|
||||
SeriesSearch(fullTextSearch = "status:hiatus"),
|
||||
SearchContext(user),
|
||||
UnpagedSorted(Sort.by("relevance")),
|
||||
).content
|
||||
|
||||
|
|
@ -372,8 +395,8 @@ class SeriesDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
seriesDtoDao.findAll(
|
||||
SeriesSearchWithReadProgress(searchTerm = "reading_direction:left_to_right"),
|
||||
user.id,
|
||||
SeriesSearch(fullTextSearch = "reading_direction:left_to_right"),
|
||||
SearchContext(user),
|
||||
UnpagedSorted(Sort.by("relevance")),
|
||||
).content
|
||||
|
||||
|
|
@ -398,8 +421,8 @@ class SeriesDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
seriesDtoDao.findAll(
|
||||
SeriesSearchWithReadProgress(searchTerm = "age_rating:12"),
|
||||
user.id,
|
||||
SeriesSearch(fullTextSearch = "age_rating:12"),
|
||||
SearchContext(user),
|
||||
UnpagedSorted(Sort.by("relevance")),
|
||||
).content
|
||||
|
||||
|
|
@ -424,8 +447,8 @@ class SeriesDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
seriesDtoDao.findAll(
|
||||
SeriesSearchWithReadProgress(searchTerm = "language:en-us"),
|
||||
user.id,
|
||||
SeriesSearch(fullTextSearch = "language:en-us"),
|
||||
SearchContext(user),
|
||||
UnpagedSorted(Sort.by("relevance")),
|
||||
).content
|
||||
|
||||
|
|
@ -456,43 +479,43 @@ class SeriesDtoDaoTest(
|
|||
// when
|
||||
val foundByBookTag =
|
||||
seriesDtoDao.findAll(
|
||||
SeriesSearchWithReadProgress(searchTerm = "book_tag:booktag"),
|
||||
user.id,
|
||||
SeriesSearch(fullTextSearch = "book_tag:booktag"),
|
||||
SearchContext(user),
|
||||
UnpagedSorted(Sort.by("relevance")),
|
||||
).content
|
||||
|
||||
val notFoundByBookTag =
|
||||
seriesDtoDao.findAll(
|
||||
SeriesSearchWithReadProgress(searchTerm = "book_tag:seriestag"),
|
||||
user.id,
|
||||
SeriesSearch(fullTextSearch = "book_tag:seriestag"),
|
||||
SearchContext(user),
|
||||
UnpagedSorted(Sort.by("relevance")),
|
||||
).content
|
||||
|
||||
val foundBySeriesTag =
|
||||
seriesDtoDao.findAll(
|
||||
SeriesSearchWithReadProgress(searchTerm = "series_tag:seriestag"),
|
||||
user.id,
|
||||
SeriesSearch(fullTextSearch = "series_tag:seriestag"),
|
||||
SearchContext(user),
|
||||
UnpagedSorted(Sort.by("relevance")),
|
||||
).content
|
||||
|
||||
val notFoundBySeriesTag =
|
||||
seriesDtoDao.findAll(
|
||||
SeriesSearchWithReadProgress(searchTerm = "series_tag:booktag"),
|
||||
user.id,
|
||||
SeriesSearch(fullTextSearch = "series_tag:booktag"),
|
||||
SearchContext(user),
|
||||
UnpagedSorted(Sort.by("relevance")),
|
||||
).content
|
||||
|
||||
val foundByTagFromBook =
|
||||
seriesDtoDao.findAll(
|
||||
SeriesSearchWithReadProgress(searchTerm = "tag:booktag"),
|
||||
user.id,
|
||||
SeriesSearch(fullTextSearch = "tag:booktag"),
|
||||
SearchContext(user),
|
||||
UnpagedSorted(Sort.by("relevance")),
|
||||
).content
|
||||
|
||||
val foundByTagFromSeries =
|
||||
seriesDtoDao.findAll(
|
||||
SeriesSearchWithReadProgress(searchTerm = "tag:seriestag"),
|
||||
user.id,
|
||||
SeriesSearch(fullTextSearch = "tag:seriestag"),
|
||||
SearchContext(user),
|
||||
UnpagedSorted(Sort.by("relevance")),
|
||||
).content
|
||||
|
||||
|
|
@ -530,8 +553,8 @@ class SeriesDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
seriesDtoDao.findAll(
|
||||
SeriesSearchWithReadProgress(searchTerm = "genre:action"),
|
||||
user.id,
|
||||
SeriesSearch(fullTextSearch = "genre:action"),
|
||||
SearchContext(user),
|
||||
UnpagedSorted(Sort.by("relevance")),
|
||||
).content
|
||||
|
||||
|
|
@ -556,8 +579,8 @@ class SeriesDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
seriesDtoDao.findAll(
|
||||
SeriesSearchWithReadProgress(searchTerm = "total_book_count:5"),
|
||||
user.id,
|
||||
SeriesSearch(fullTextSearch = "total_book_count:5"),
|
||||
SearchContext(user),
|
||||
UnpagedSorted(Sort.by("relevance")),
|
||||
).content
|
||||
|
||||
|
|
@ -590,8 +613,8 @@ class SeriesDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
seriesDtoDao.findAll(
|
||||
SeriesSearchWithReadProgress(searchTerm = "book_count:2"),
|
||||
user.id,
|
||||
SeriesSearch(fullTextSearch = "book_count:2"),
|
||||
SearchContext(user),
|
||||
UnpagedSorted(Sort.by("relevance")),
|
||||
).content
|
||||
|
||||
|
|
@ -626,22 +649,22 @@ class SeriesDtoDaoTest(
|
|||
// when
|
||||
val foundGeneric =
|
||||
seriesDtoDao.findAll(
|
||||
SeriesSearchWithReadProgress(searchTerm = "author:david"),
|
||||
user.id,
|
||||
SeriesSearch(fullTextSearch = "author:david"),
|
||||
SearchContext(user),
|
||||
UnpagedSorted(Sort.by("relevance")),
|
||||
).content
|
||||
|
||||
val foundByRole =
|
||||
seriesDtoDao.findAll(
|
||||
SeriesSearchWithReadProgress(searchTerm = "penciller:david"),
|
||||
user.id,
|
||||
SeriesSearch(fullTextSearch = "penciller:david"),
|
||||
SearchContext(user),
|
||||
UnpagedSorted(Sort.by("relevance")),
|
||||
).content
|
||||
|
||||
val notFoundByRole =
|
||||
seriesDtoDao.findAll(
|
||||
SeriesSearchWithReadProgress(searchTerm = "writer:david"),
|
||||
user.id,
|
||||
SeriesSearch(fullTextSearch = "writer:david"),
|
||||
SearchContext(user),
|
||||
UnpagedSorted(Sort.by("relevance")),
|
||||
).content
|
||||
|
||||
|
|
@ -674,8 +697,8 @@ class SeriesDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
seriesDtoDao.findAll(
|
||||
SeriesSearchWithReadProgress(searchTerm = "release_date:1999"),
|
||||
user.id,
|
||||
SeriesSearch(fullTextSearch = "release_date:1999"),
|
||||
SearchContext(user),
|
||||
UnpagedSorted(Sort.by("relevance")),
|
||||
).content
|
||||
|
||||
|
|
@ -696,8 +719,8 @@ class SeriesDtoDaoTest(
|
|||
// when
|
||||
val found =
|
||||
seriesDtoDao.findAll(
|
||||
SeriesSearchWithReadProgress(searchTerm = "deleted:true"),
|
||||
user.id,
|
||||
SeriesSearch(fullTextSearch = "deleted:true"),
|
||||
SearchContext(user),
|
||||
UnpagedSorted(Sort.by("relevance")),
|
||||
).content
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -82,12 +82,11 @@ class KoboControllerTest(
|
|||
val book1 = makeBook("valid", libraryId = library1.id)
|
||||
val book2 = makeBook("valid", libraryId = library1.id)
|
||||
|
||||
val series =
|
||||
makeSeries(name = "series1", libraryId = library1.id).also { series ->
|
||||
seriesLifecycle.createSeries(series).let { created ->
|
||||
seriesLifecycle.addBooks(created, listOf(book1, book2))
|
||||
}
|
||||
makeSeries(name = "series1", libraryId = library1.id).also { series ->
|
||||
seriesLifecycle.createSeries(series).let { created ->
|
||||
seriesLifecycle.addBooks(created, listOf(book1, book2))
|
||||
}
|
||||
}
|
||||
|
||||
mediaRepository.findById(book1.id).let { media -> mediaRepository.update(media.copy(status = Media.Status.READY, mediaType = MediaType.EPUB.type)) }
|
||||
mediaRepository.findById(book2.id).let { media -> mediaRepository.update(media.copy(status = Media.Status.READY, mediaType = MediaType.EPUB.type)) }
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ class SeriesCollectionControllerTest(
|
|||
SeriesCollection(
|
||||
name = "Lib1+2",
|
||||
seriesIds = (seriesLibrary1 + seriesLibrary2).map { it.id },
|
||||
ordered = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -142,6 +143,12 @@ class SeriesCollectionControllerTest(
|
|||
jsonPath("$.seriesIds.length()") { value(10) }
|
||||
jsonPath("$.filtered") { value(false) }
|
||||
}
|
||||
|
||||
mockMvc.get("/api/v1/collections/${colLibBoth.id}/series")
|
||||
.andExpect {
|
||||
status { isOk() }
|
||||
jsonPath("$.content.length()") { value(10) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -950,7 +957,7 @@ class SeriesCollectionControllerTest(
|
|||
.andExpect {
|
||||
status { isOk() }
|
||||
jsonPath("$.name") { value("Lib1+2") }
|
||||
jsonPath("$.ordered") { value(false) }
|
||||
jsonPath("$.ordered") { value(true) }
|
||||
jsonPath("$.seriesIds.length()") { value(1) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue