mirror of
https://github.com/gotson/komga.git
synced 2025-12-21 07:56:57 +01:00
parent
f549067a8a
commit
bd64381a8e
10 changed files with 136 additions and 12 deletions
|
|
@ -13,5 +13,6 @@ class BookSearchWithReadProgress(
|
|||
searchTerm: String? = null,
|
||||
mediaStatus: Collection<Media.Status>? = null,
|
||||
val tags: Collection<String>? = null,
|
||||
val readStatus: Collection<ReadStatus>? = null
|
||||
val readStatus: Collection<ReadStatus>? = null,
|
||||
val authors: Collection<Author>? = null,
|
||||
) : BookSearch(libraryIds, seriesIds, searchTerm, mediaStatus)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@ class SeriesSearchWithReadProgress(
|
|||
val tags: Collection<String>? = null,
|
||||
val ageRatings: Collection<Int?>? = null,
|
||||
val releaseYears: Collection<String>? = null,
|
||||
val readStatus: Collection<ReadStatus>? = null
|
||||
val readStatus: Collection<ReadStatus>? = null,
|
||||
val authors: Collection<Author>? = null,
|
||||
) : SeriesSearch(
|
||||
libraryIds = libraryIds,
|
||||
collectionIds = collectionIds,
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ class BookDtoDao(
|
|||
.apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } }
|
||||
.apply { if (joinConditions.tag) leftJoin(bt).on(b.ID.eq(bt.BOOK_ID)) }
|
||||
.apply { if (joinConditions.selectReadListNumber) leftJoin(rlb).on(b.ID.eq(rlb.BOOK_ID)) }
|
||||
.apply { if(joinConditions.author) leftJoin(a).on(b.ID.eq(a.BOOK_ID)) }
|
||||
.where(conditions)
|
||||
.groupBy(b.ID)
|
||||
.fetch()
|
||||
|
|
@ -236,6 +237,7 @@ class BookDtoDao(
|
|||
.leftJoin(r).on(b.ID.eq(r.BOOK_ID)).and(readProgressCondition(userId))
|
||||
.apply { if (joinConditions.tag) leftJoin(bt).on(b.ID.eq(bt.BOOK_ID)) }
|
||||
.apply { if (joinConditions.selectReadListNumber) leftJoin(rlb).on(b.ID.eq(rlb.BOOK_ID)) }
|
||||
.apply { if(joinConditions.author) leftJoin(a).on(b.ID.eq(a.BOOK_ID)) }
|
||||
|
||||
private fun ResultQuery<Record>.fetchAndMap() =
|
||||
fetch()
|
||||
|
|
@ -280,17 +282,27 @@ class BookDtoDao(
|
|||
c = c.and(cr)
|
||||
}
|
||||
|
||||
if (!authors.isNullOrEmpty()) {
|
||||
var ca: Condition = DSL.falseCondition()
|
||||
authors.forEach {
|
||||
ca = ca.or(a.NAME.equalIgnoreCase(it.name).and(a.ROLE.equalIgnoreCase(it.role)))
|
||||
}
|
||||
c = c.and(ca)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
private fun BookSearchWithReadProgress.toJoinConditions() =
|
||||
JoinConditions(
|
||||
tag = !tags.isNullOrEmpty()
|
||||
tag = !tags.isNullOrEmpty(),
|
||||
author = !authors.isNullOrEmpty(),
|
||||
)
|
||||
|
||||
private data class JoinConditions(
|
||||
val selectReadListNumber: Boolean = false,
|
||||
val tag: Boolean = false
|
||||
val tag: Boolean = false,
|
||||
val author: Boolean = false,
|
||||
)
|
||||
|
||||
private fun BookRecord.toDto(media: MediaDto, metadata: BookMetadataDto, readProgress: ReadProgressDto?) =
|
||||
|
|
|
|||
|
|
@ -79,7 +79,12 @@ class SeriesDtoDao(
|
|||
return findAll(conditions, having, userId, pageable, search.toJoinConditions())
|
||||
}
|
||||
|
||||
override fun findByCollectionId(collectionId: String, search: SeriesSearchWithReadProgress, userId: String, pageable: Pageable): Page<SeriesDto> {
|
||||
override fun findByCollectionId(
|
||||
collectionId: String,
|
||||
search: SeriesSearchWithReadProgress,
|
||||
userId: String,
|
||||
pageable: Pageable
|
||||
): Page<SeriesDto> {
|
||||
val conditions = search.toCondition().and(cs.COLLECTION_ID.eq(collectionId))
|
||||
val having = search.readStatus?.toCondition() ?: DSL.trueCondition()
|
||||
val joinConditions = search.toJoinConditions().copy(selectCollectionNumber = true, collection = true)
|
||||
|
|
@ -87,7 +92,11 @@ class SeriesDtoDao(
|
|||
return findAll(conditions, having, userId, pageable, joinConditions)
|
||||
}
|
||||
|
||||
override fun findRecentlyUpdated(search: SeriesSearchWithReadProgress, userId: String, pageable: Pageable): Page<SeriesDto> {
|
||||
override fun findRecentlyUpdated(
|
||||
search: SeriesSearchWithReadProgress,
|
||||
userId: String,
|
||||
pageable: Pageable
|
||||
): Page<SeriesDto> {
|
||||
val conditions = search.toCondition()
|
||||
.and(s.CREATED_DATE.ne(s.LAST_MODIFIED_DATE))
|
||||
|
||||
|
|
@ -118,6 +127,7 @@ class SeriesDtoDao(
|
|||
.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)) }
|
||||
.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)) }
|
||||
|
||||
private fun findAll(
|
||||
conditions: Condition,
|
||||
|
|
@ -135,6 +145,7 @@ class SeriesDtoDao(
|
|||
.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)) }
|
||||
.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)) }
|
||||
.where(conditions)
|
||||
.groupBy(s.ID)
|
||||
.having(having)
|
||||
|
|
@ -200,7 +211,14 @@ class SeriesDtoDao(
|
|||
.filter { it.name != null }
|
||||
.map { AuthorDto(it.name, it.role) }
|
||||
|
||||
sr.toDto(booksCount, booksReadCount, booksUnreadCount, booksInProgressCount, dr.toDto(genres, tags), bmar.toDto(aggregatedAuthors))
|
||||
sr.toDto(
|
||||
booksCount,
|
||||
booksReadCount,
|
||||
booksUnreadCount,
|
||||
booksInProgressCount,
|
||||
dr.toDto(genres, tags),
|
||||
bmar.toDto(aggregatedAuthors)
|
||||
)
|
||||
}
|
||||
|
||||
private fun SeriesSearchWithReadProgress.toCondition(): Condition {
|
||||
|
|
@ -216,11 +234,20 @@ class SeriesDtoDao(
|
|||
if (!tags.isNullOrEmpty()) c = c.and(lower(st.TAG).`in`(tags.map { it.toLowerCase() }))
|
||||
if (!ageRatings.isNullOrEmpty()) {
|
||||
val c1 = if (ageRatings.contains(null)) d.AGE_RATING.isNull else DSL.falseCondition()
|
||||
val c2 = if (ageRatings.filterNotNull().isNotEmpty()) d.AGE_RATING.`in`(ageRatings.filterNotNull()) else DSL.falseCondition()
|
||||
val c2 = if (ageRatings.filterNotNull()
|
||||
.isNotEmpty()
|
||||
) d.AGE_RATING.`in`(ageRatings.filterNotNull()) else DSL.falseCondition()
|
||||
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: Condition = DSL.falseCondition()
|
||||
authors.forEach {
|
||||
ca = ca.or(bmaa.NAME.equalIgnoreCase(it.name).and(bmaa.ROLE.equalIgnoreCase(it.role)))
|
||||
}
|
||||
c = c.and(ca)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
|
@ -229,14 +256,16 @@ class SeriesDtoDao(
|
|||
JoinConditions(
|
||||
genre = !genres.isNullOrEmpty(),
|
||||
tag = !tags.isNullOrEmpty(),
|
||||
collection = !collectionIds.isNullOrEmpty()
|
||||
collection = !collectionIds.isNullOrEmpty(),
|
||||
aggregationAuthor = !authors.isNullOrEmpty(),
|
||||
)
|
||||
|
||||
private data class JoinConditions(
|
||||
val selectCollectionNumber: Boolean = false,
|
||||
val genre: Boolean = false,
|
||||
val tag: Boolean = false,
|
||||
val collection: Boolean = false
|
||||
val collection: Boolean = false,
|
||||
val aggregationAuthor: Boolean = false,
|
||||
)
|
||||
|
||||
private fun Collection<ReadStatus>.toCondition(): Condition =
|
||||
|
|
@ -248,7 +277,14 @@ class SeriesDtoDao(
|
|||
}
|
||||
}.reduce { acc, condition -> acc.or(condition) }
|
||||
|
||||
private fun SeriesRecord.toDto(booksCount: Int, booksReadCount: Int, booksUnreadCount: Int, booksInProgressCount: Int, metadata: SeriesMetadataDto, booksMetadata: BookMetadataAggregationDto) =
|
||||
private fun SeriesRecord.toDto(
|
||||
booksCount: Int,
|
||||
booksReadCount: Int,
|
||||
booksUnreadCount: Int,
|
||||
booksInProgressCount: Int,
|
||||
metadata: SeriesMetadataDto,
|
||||
booksMetadata: BookMetadataAggregationDto
|
||||
) =
|
||||
SeriesDto(
|
||||
id = id,
|
||||
libraryId = libraryId,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
package org.gotson.komga.infrastructure.swagger
|
||||
|
||||
import io.swagger.v3.oas.annotations.Parameter
|
||||
import io.swagger.v3.oas.annotations.Parameters
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema
|
||||
import io.swagger.v3.oas.annotations.media.Content
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
|
||||
@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.FUNCTION)
|
||||
@Parameters(
|
||||
Parameter(
|
||||
description = "Author criteria in the format: name,role. Multiple author criteria are supported.",
|
||||
`in` = ParameterIn.QUERY, name = "author", content = [Content(array = ArraySchema(schema = Schema(type = "string")))]
|
||||
)
|
||||
)
|
||||
annotation class AuthorsAsQueryParam
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package org.gotson.komga.infrastructure.web
|
||||
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@Target(AnnotationTarget.VALUE_PARAMETER)
|
||||
annotation class Authors()
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package org.gotson.komga.infrastructure.web
|
||||
|
||||
import org.gotson.komga.domain.model.Author
|
||||
import org.springframework.core.MethodParameter
|
||||
import org.springframework.web.bind.support.WebDataBinderFactory
|
||||
import org.springframework.web.context.request.NativeWebRequest
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver
|
||||
import org.springframework.web.method.support.ModelAndViewContainer
|
||||
|
||||
class AuthorsHandlerMethodArgumentResolver : HandlerMethodArgumentResolver {
|
||||
override fun supportsParameter(parameter: MethodParameter): Boolean =
|
||||
parameter.getParameterAnnotation(Authors::class.java) != null
|
||||
|
||||
override fun resolveArgument(
|
||||
parameter: MethodParameter,
|
||||
mavContainer: ModelAndViewContainer?,
|
||||
webRequest: NativeWebRequest,
|
||||
binderFactory: WebDataBinderFactory?
|
||||
): Any? {
|
||||
val param = webRequest.getParameterValues("author") ?: return null
|
||||
|
||||
// Single empty parameter, e.g "author="
|
||||
if (param.size == 1 && param[0].isNullOrBlank()) return null
|
||||
|
||||
return parseParameterIntoAuthors(param.toList())
|
||||
}
|
||||
|
||||
private fun parseParameterIntoAuthors(source: List<String>, delimiter: String = ","): List<Author> =
|
||||
source
|
||||
.filter { it.contains(delimiter) }
|
||||
.map { Author(name = it.substringBeforeLast(delimiter), role = it.substringAfterLast(delimiter)) }
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import org.springframework.http.CacheControl
|
|||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver
|
||||
import org.springframework.web.servlet.NoHandlerFoundException
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry
|
||||
|
|
@ -63,6 +64,10 @@ class WebMvcConfiguration : WebMvcConfigurer {
|
|||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun addArgumentResolvers(resolvers: MutableList<HandlerMethodArgumentResolver>) {
|
||||
resolvers.add(AuthorsHandlerMethodArgumentResolver())
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ 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 mu.KotlinLogging
|
||||
import org.gotson.komga.domain.model.Author
|
||||
import org.gotson.komga.domain.model.DuplicateNameException
|
||||
import org.gotson.komga.domain.model.ROLE_ADMIN
|
||||
import org.gotson.komga.domain.model.ReadStatus
|
||||
|
|
@ -15,7 +16,9 @@ import org.gotson.komga.domain.persistence.SeriesCollectionRepository
|
|||
import org.gotson.komga.domain.service.SeriesCollectionLifecycle
|
||||
import org.gotson.komga.infrastructure.jooq.UnpagedSorted
|
||||
import org.gotson.komga.infrastructure.security.KomgaPrincipal
|
||||
import org.gotson.komga.infrastructure.swagger.AuthorsAsQueryParam
|
||||
import org.gotson.komga.infrastructure.swagger.PageableWithoutSortAsQueryParam
|
||||
import org.gotson.komga.infrastructure.web.Authors
|
||||
import org.gotson.komga.interfaces.rest.dto.CollectionCreationDto
|
||||
import org.gotson.komga.interfaces.rest.dto.CollectionDto
|
||||
import org.gotson.komga.interfaces.rest.dto.CollectionUpdateDto
|
||||
|
|
@ -154,6 +157,7 @@ class SeriesCollectionController(
|
|||
}
|
||||
|
||||
@PageableWithoutSortAsQueryParam
|
||||
@AuthorsAsQueryParam
|
||||
@GetMapping("{id}/series")
|
||||
fun getSeriesForCollection(
|
||||
@PathVariable id: String,
|
||||
|
|
@ -168,6 +172,7 @@ class SeriesCollectionController(
|
|||
@RequestParam(name = "age_rating", required = false) ageRatings: List<String>?,
|
||||
@RequestParam(name = "release_year", required = false) release_years: List<String>?,
|
||||
@RequestParam(name = "unpaged", required = false) unpaged: Boolean = false,
|
||||
@Parameter(hidden = true) @Authors authors: List<Author>?,
|
||||
@Parameter(hidden = true) page: Pageable
|
||||
): Page<SeriesDto> =
|
||||
collectionRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null))?.let { collection ->
|
||||
|
|
@ -193,6 +198,7 @@ class SeriesCollectionController(
|
|||
tags = tags,
|
||||
ageRatings = ageRatings?.map { it.toIntOrNull() },
|
||||
releaseYears = release_years,
|
||||
authors = authors
|
||||
)
|
||||
|
||||
seriesDtoRepository.findByCollectionId(collection.id, seriesSearch, principal.user.id, pageRequest)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,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.application.tasks.TaskReceiver
|
||||
import org.gotson.komga.domain.model.Author
|
||||
import org.gotson.komga.domain.model.BookSearchWithReadProgress
|
||||
import org.gotson.komga.domain.model.Media
|
||||
import org.gotson.komga.domain.model.ROLE_ADMIN
|
||||
|
|
@ -25,8 +26,10 @@ import org.gotson.komga.domain.service.BookLifecycle
|
|||
import org.gotson.komga.domain.service.SeriesLifecycle
|
||||
import org.gotson.komga.infrastructure.jooq.UnpagedSorted
|
||||
import org.gotson.komga.infrastructure.security.KomgaPrincipal
|
||||
import org.gotson.komga.infrastructure.swagger.AuthorsAsQueryParam
|
||||
import org.gotson.komga.infrastructure.swagger.PageableAsQueryParam
|
||||
import org.gotson.komga.infrastructure.swagger.PageableWithoutSortAsQueryParam
|
||||
import org.gotson.komga.infrastructure.web.Authors
|
||||
import org.gotson.komga.interfaces.rest.dto.BookDto
|
||||
import org.gotson.komga.interfaces.rest.dto.CollectionDto
|
||||
import org.gotson.komga.interfaces.rest.dto.SeriesDto
|
||||
|
|
@ -80,6 +83,7 @@ class SeriesController(
|
|||
) {
|
||||
|
||||
@PageableAsQueryParam
|
||||
@AuthorsAsQueryParam
|
||||
@GetMapping
|
||||
fun getAllSeries(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
|
|
@ -95,6 +99,7 @@ class SeriesController(
|
|||
@RequestParam(name = "age_rating", required = false) ageRatings: List<String>?,
|
||||
@RequestParam(name = "release_year", required = false) release_years: List<String>?,
|
||||
@RequestParam(name = "unpaged", required = false) unpaged: Boolean = false,
|
||||
@Parameter(hidden = true) @Authors authors: List<Author>?,
|
||||
@Parameter(hidden = true) page: Pageable
|
||||
): Page<SeriesDto> {
|
||||
val sort =
|
||||
|
|
@ -121,6 +126,7 @@ class SeriesController(
|
|||
tags = tags,
|
||||
ageRatings = ageRatings?.map { it.toIntOrNull() },
|
||||
releaseYears = release_years,
|
||||
authors = authors
|
||||
)
|
||||
|
||||
return seriesDtoRepository.findAll(seriesSearch, principal.user.id, pageRequest)
|
||||
|
|
@ -229,6 +235,7 @@ class SeriesController(
|
|||
}
|
||||
|
||||
@PageableAsQueryParam
|
||||
@AuthorsAsQueryParam
|
||||
@GetMapping("{seriesId}/books")
|
||||
fun getAllBooksBySeries(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
|
|
@ -237,6 +244,7 @@ class SeriesController(
|
|||
@RequestParam(name = "read_status", required = false) readStatus: List<ReadStatus>?,
|
||||
@RequestParam(name = "tag", required = false) tags: List<String>?,
|
||||
@RequestParam(name = "unpaged", required = false) unpaged: Boolean = false,
|
||||
@Parameter(hidden = true) @Authors authors: List<Author>?,
|
||||
@Parameter(hidden = true) page: Pageable
|
||||
): Page<BookDto> {
|
||||
seriesRepository.getLibraryId(seriesId)?.let {
|
||||
|
|
@ -259,7 +267,8 @@ class SeriesController(
|
|||
seriesIds = listOf(seriesId),
|
||||
mediaStatus = mediaStatus,
|
||||
readStatus = readStatus,
|
||||
tags = tags
|
||||
tags = tags,
|
||||
authors = authors,
|
||||
),
|
||||
principal.user.id,
|
||||
pageRequest
|
||||
|
|
|
|||
Loading…
Reference in a new issue