diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesDtoDao.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesDtoDao.kt index e2f2106cc..a68262a3f 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesDtoDao.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesDtoDao.kt @@ -28,6 +28,7 @@ class SeriesDtoDao( private val s = Tables.SERIES private val b = Tables.BOOK private val d = Tables.SERIES_METADATA + private val r = Tables.READ_PROGRESS private val groupFields = arrayOf( *s.fields(), @@ -40,28 +41,29 @@ class SeriesDtoDao( "lastModifiedDate" to s.LAST_MODIFIED_DATE ) - override fun findAll(search: SeriesSearch, pageable: Pageable): Page { + override fun findAll(search: SeriesSearch, userId: Long, pageable: Pageable): Page { val conditions = search.toCondition() - return findAll(conditions, pageable) + return findAll(conditions, userId, pageable) } - override fun findRecentlyUpdated(search: SeriesSearch, pageable: Pageable): Page { + override fun findRecentlyUpdated(search: SeriesSearch, userId: Long, pageable: Pageable): Page { val conditions = search.toCondition() .and(s.CREATED_DATE.ne(s.LAST_MODIFIED_DATE)) - return findAll(conditions, pageable) + return findAll(conditions, userId, pageable) } - override fun findByIdOrNull(seriesId: Long): SeriesDto? = + override fun findByIdOrNull(seriesId: Long, userId: Long): SeriesDto? = selectBase() .where(s.ID.eq(seriesId)) + .and(readProgressCondition(userId)) .groupBy(*groupFields) .fetchAndMap() .firstOrNull() - private fun findAll(conditions: Condition, pageable: Pageable): Page { + private fun findAll(conditions: Condition, userId: Long, pageable: Pageable): Page { val count = dsl.selectCount() .from(s) .leftJoin(d).on(s.ID.eq(d.SERIES_ID)) @@ -72,6 +74,7 @@ class SeriesDtoDao( val dtos = selectBase() .where(conditions) + .and(readProgressCondition(userId)) .groupBy(*groupFields) .orderBy(orderBy) .limit(pageable.pageSize) @@ -87,18 +90,23 @@ class SeriesDtoDao( private fun selectBase() = dsl.select(*groupFields) - .select(DSL.count(b.ID).`as`("bookCount")) + .select(DSL.count(b.ID).`as`("booksCount")) + .select(DSL.count(r.COMPLETED).`as`("booksReadCount")) .from(s) .leftJoin(b).on(s.ID.eq(b.SERIES_ID)) .leftJoin(d).on(s.ID.eq(d.SERIES_ID)) + .leftJoin(r).on(b.ID.eq(r.BOOK_ID)) + + private fun readProgressCondition(userId: Long): Condition = r.USER_ID.eq(userId).or(r.USER_ID.isNull) private fun ResultQuery.fetchAndMap() = fetch() .map { r -> val sr = r.into(s) val dr = r.into(d) - val bookCount = r["bookCount"] as Int - sr.toDto(bookCount, dr.toDto()) + val booksCount = r["booksCount"] as Int + val booksReadCount = r["booksReadCount"] as Int + sr.toDto(booksCount, booksReadCount, dr.toDto()) } private fun SeriesSearch.toCondition(): Condition { @@ -111,7 +119,7 @@ class SeriesDtoDao( return c } - private fun SeriesRecord.toDto(bookCount: Int, metadata: SeriesMetadataDto) = + private fun SeriesRecord.toDto(booksCount: Int, booksReadCount: Int, metadata: SeriesMetadataDto) = SeriesDto( id = id, libraryId = libraryId, @@ -120,7 +128,8 @@ class SeriesDtoDao( created = createdDate.toUTC(), lastModified = lastModifiedDate.toUTC(), fileLastModified = fileLastModified.toUTC(), - booksCount = bookCount, + booksCount = booksCount, + booksReadCount = booksReadCount, metadata = metadata ) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/SeriesController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/SeriesController.kt index 069d749dd..979b694a3 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/SeriesController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/SeriesController.kt @@ -83,7 +83,7 @@ class SeriesController( metadataStatus = metadataStatus ?: emptyList() ) - return seriesDtoRepository.findAll(seriesSearch, pageRequest) + return seriesDtoRepository.findAll(seriesSearch, principal.user.id, pageRequest) .map { it.restrictUrl(!principal.user.roleAdmin) } } @@ -104,6 +104,7 @@ class SeriesController( return seriesDtoRepository.findAll( SeriesSearch(libraryIds = libraryIds), + principal.user.id, pageRequest ).map { it.restrictUrl(!principal.user.roleAdmin) } } @@ -125,6 +126,7 @@ class SeriesController( return seriesDtoRepository.findAll( SeriesSearch(libraryIds = libraryIds), + principal.user.id, pageRequest ).map { it.restrictUrl(!principal.user.roleAdmin) } } @@ -146,6 +148,7 @@ class SeriesController( return seriesDtoRepository.findRecentlyUpdated( SeriesSearch(libraryIds = libraryIds), + principal.user.id, pageRequest ).map { it.restrictUrl(!principal.user.roleAdmin) } } @@ -155,7 +158,7 @@ class SeriesController( @AuthenticationPrincipal principal: KomgaPrincipal, @PathVariable(name = "seriesId") id: Long ): SeriesDto = - seriesDtoRepository.findByIdOrNull(id)?.let { + seriesDtoRepository.findByIdOrNull(id, principal.user.id)?.let { if (!principal.user.canAccessLibrary(it.libraryId)) throw ResponseStatusException(HttpStatus.UNAUTHORIZED) it.restrictUrl(!principal.user.roleAdmin) } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) @@ -227,7 +230,8 @@ class SeriesController( fun updateMetadata( @PathVariable seriesId: Long, @Parameter(description = "Metadata fields to update. Set a field to null to unset the metadata. You can omit fields you don't want to update.") - @Valid @RequestBody newMetadata: SeriesMetadataUpdateDto + @Valid @RequestBody newMetadata: SeriesMetadataUpdateDto, + @AuthenticationPrincipal principal: KomgaPrincipal ): SeriesDto = seriesMetadataRepository.findByIdOrNull(seriesId)?.let { existing -> val updated = with(newMetadata) { @@ -241,7 +245,7 @@ class SeriesController( ) } seriesMetadataRepository.update(updated) - seriesDtoRepository.findByIdOrNull(seriesId)!! + seriesDtoRepository.findByIdOrNull(seriesId, principal.user.id)!! } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) @PostMapping("{seriesId}/read-progress") diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/SeriesDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/SeriesDto.kt index 4bb0c1140..8a179f66f 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/SeriesDto.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/SeriesDto.kt @@ -15,8 +15,11 @@ data class SeriesDto( @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") val fileLastModified: LocalDateTime, val booksCount: Int, + val booksReadCount: Int, val metadata: SeriesMetadataDto -) +) { + val booksUnreadCount: Int = booksCount - booksReadCount +} fun SeriesDto.restrictUrl(restrict: Boolean) = if (restrict) copy(url = "") else this diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/persistence/SeriesDtoRepository.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/persistence/SeriesDtoRepository.kt index a476e1d44..f8eb46fc4 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/persistence/SeriesDtoRepository.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/persistence/SeriesDtoRepository.kt @@ -6,7 +6,7 @@ import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable interface SeriesDtoRepository { - fun findAll(search: SeriesSearch, pageable: Pageable): Page - fun findRecentlyUpdated(search: SeriesSearch, pageable: Pageable): Page - fun findByIdOrNull(seriesId: Long): SeriesDto? + fun findAll(search: SeriesSearch, userId: Long, pageable: Pageable): Page + fun findRecentlyUpdated(search: SeriesSearch, userId: Long, pageable: Pageable): Page + fun findByIdOrNull(seriesId: Long, userId: Long): SeriesDto? } diff --git a/komga/src/test/kotlin/org/gotson/komga/interfaces/rest/SeriesControllerTest.kt b/komga/src/test/kotlin/org/gotson/komga/interfaces/rest/SeriesControllerTest.kt index f5af7a4ad..88b669d4f 100644 --- a/komga/src/test/kotlin/org/gotson/komga/interfaces/rest/SeriesControllerTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/interfaces/rest/SeriesControllerTest.kt @@ -510,6 +510,13 @@ class SeriesControllerTest( status { isNoContent } } + mockMvc.get("/api/v1/series/${series.id}") + .andExpect { + status { isOk } + jsonPath("$.booksUnreadCount") { value(0) } + jsonPath("$.booksReadCount") { value(2) } + } + mockMvc.get("/api/v1/series/${series.id}/books") .andExpect { status { isOk } @@ -548,6 +555,13 @@ class SeriesControllerTest( status { isNoContent } } + mockMvc.get("/api/v1/series/${series.id}") + .andExpect { + status { isOk } + jsonPath("$.booksUnreadCount") { value(2) } + jsonPath("$.booksReadCount") { value(0) } + } + mockMvc.get("/api/v1/series/${series.id}/books") .andExpect { status { isOk }