diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ReadProgressDtoDao.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ReadProgressDtoDao.kt new file mode 100644 index 000000000..d416aa462 --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ReadProgressDtoDao.kt @@ -0,0 +1,96 @@ +package org.gotson.komga.infrastructure.jooq + +import org.gotson.komga.interfaces.rest.dto.TachiyomiReadProgressDto +import org.gotson.komga.interfaces.rest.persistence.ReadProgressDtoRepository +import org.gotson.komga.jooq.Tables +import org.jooq.Condition +import org.jooq.DSLContext +import org.jooq.Record +import org.jooq.Record2 +import org.jooq.impl.DSL.rowNumber +import org.springframework.stereotype.Component + +@Component +class ReadProgressDtoDao( + private val dsl: DSLContext +) : ReadProgressDtoRepository { + + private val rlb = Tables.READLIST_BOOK + private val b = Tables.BOOK + private val d = Tables.BOOK_METADATA + private val r = Tables.READ_PROGRESS + + override fun getProgressBySeries(seriesId: String, userId: String): TachiyomiReadProgressDto { + val indexedReadProgress = dsl.select( + rowNumber().over().orderBy(d.NUMBER_SORT), + r.COMPLETED, + ) + .from(b) + .leftJoin(r).on(b.ID.eq(r.BOOK_ID)).and(readProgressCondition(userId)) + .leftJoin(d).on(b.ID.eq(d.BOOK_ID)) + .where(b.SERIES_ID.eq(seriesId)) + .orderBy(d.NUMBER_SORT) + .fetch() + .toList() + + val booksCountRecord = dsl + .select(SeriesDtoDao.countUnread.`as`(BOOKS_UNREAD_COUNT)) + .select(SeriesDtoDao.countRead.`as`(BOOKS_READ_COUNT)) + .select(SeriesDtoDao.countInProgress.`as`(BOOKS_IN_PROGRESS_COUNT)) + .from(b) + .leftJoin(r).on(b.ID.eq(r.BOOK_ID)).and(readProgressCondition(userId)) + .where(b.SERIES_ID.eq(seriesId)) + .fetch() + .first() + + return booksCountToDto(booksCountRecord, indexedReadProgress) + } + + override fun getProgressByReadList(readListId: String, userId: String): TachiyomiReadProgressDto { + val indexedReadProgress = dsl.select( + rowNumber().over().orderBy(rlb.NUMBER), + r.COMPLETED, + ) + .from(b) + .leftJoin(r).on(b.ID.eq(r.BOOK_ID)).and(readProgressCondition(userId)) + .leftJoin(rlb).on(b.ID.eq(rlb.BOOK_ID)) + .where(rlb.READLIST_ID.eq(readListId)) + .orderBy(rlb.NUMBER) + .fetch() + .toList() + + val booksCountRecord = dsl + .select(SeriesDtoDao.countUnread.`as`(BOOKS_UNREAD_COUNT)) + .select(SeriesDtoDao.countRead.`as`(BOOKS_READ_COUNT)) + .select(SeriesDtoDao.countInProgress.`as`(BOOKS_IN_PROGRESS_COUNT)) + .from(b) + .leftJoin(r).on(b.ID.eq(r.BOOK_ID)).and(readProgressCondition(userId)) + .leftJoin(rlb).on(b.ID.eq(rlb.BOOK_ID)) + .where(rlb.READLIST_ID.eq(readListId)) + .fetch() + .first() + + return booksCountToDto(booksCountRecord, indexedReadProgress) + } + + private fun booksCountToDto(booksCountRecord: Record, indexedReadProgress: List>): TachiyomiReadProgressDto { + val booksUnreadCount = booksCountRecord.get(BOOKS_UNREAD_COUNT, Int::class.java) + val booksReadCount = booksCountRecord.get(BOOKS_READ_COUNT, Int::class.java) + val booksInProgressCount = booksCountRecord.get(BOOKS_IN_PROGRESS_COUNT, Int::class.java) + + val lastReadContinuousIndex = indexedReadProgress + .takeWhile { it.component2() == true } + .lastOrNull() + ?.component1() ?: 0 + + return TachiyomiReadProgressDto( + booksCount = booksUnreadCount + booksReadCount + booksInProgressCount, + booksUnreadCount = booksUnreadCount, + booksInProgressCount = booksInProgressCount, + booksReadCount = booksReadCount, + lastReadContinuousIndex = lastReadContinuousIndex, + ) + } + + private fun readProgressCondition(userId: String): Condition = r.USER_ID.eq(userId).or(r.USER_ID.isNull) +} diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/ReadListController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/ReadListController.kt index 573c03382..015503c69 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/ReadListController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/ReadListController.kt @@ -18,14 +18,14 @@ import org.gotson.komga.infrastructure.swagger.PageableWithoutSortAsQueryParam import org.gotson.komga.interfaces.rest.dto.BookDto import org.gotson.komga.interfaces.rest.dto.ReadListCreationDto import org.gotson.komga.interfaces.rest.dto.ReadListDto -import org.gotson.komga.interfaces.rest.dto.ReadListProgressDto import org.gotson.komga.interfaces.rest.dto.ReadListRequestResultDto import org.gotson.komga.interfaces.rest.dto.ReadListUpdateDto +import org.gotson.komga.interfaces.rest.dto.TachiyomiReadProgressDto import org.gotson.komga.interfaces.rest.dto.TachiyomiReadProgressUpdateDto import org.gotson.komga.interfaces.rest.dto.restrictUrl import org.gotson.komga.interfaces.rest.dto.toDto import org.gotson.komga.interfaces.rest.persistence.BookDtoRepository -import org.gotson.komga.interfaces.rest.persistence.ReadListDtoRepository +import org.gotson.komga.interfaces.rest.persistence.ReadProgressDtoRepository import org.springframework.data.domain.Page import org.springframework.data.domain.PageRequest import org.springframework.data.domain.Pageable @@ -41,6 +41,7 @@ import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PatchMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.PutMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam @@ -59,7 +60,7 @@ class ReadListController( private val readListRepository: ReadListRepository, private val readListLifecycle: ReadListLifecycle, private val bookDtoRepository: BookDtoRepository, - private val readListDtoRepository: ReadListDtoRepository, + private val readProgressDtoRepository: ReadProgressDtoRepository, private val bookLifecycle: BookLifecycle, ) { @@ -242,18 +243,18 @@ class ReadListController( ?.restrictUrl(!principal.user.roleAdmin) } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) - @GetMapping("{id}/read-progress") + @GetMapping("{id}/read-progress/tachiyomi") fun getReadProgress( @PathVariable id: String, @AuthenticationPrincipal principal: KomgaPrincipal - ): ReadListProgressDto = + ): TachiyomiReadProgressDto = readListRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null))?.let { readList -> - readListDtoRepository.getProgress(readList.id, principal.user.id) + readProgressDtoRepository.getProgressByReadList(readList.id, principal.user.id) } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) - @PatchMapping("{id}/read-progress/tachiyomi") + @PutMapping("{id}/read-progress/tachiyomi") @ResponseStatus(HttpStatus.NO_CONTENT) - fun markReadProgress( + fun markReadProgressTachiyomi( @PathVariable id: String, @Valid @RequestBody readProgress: TachiyomiReadProgressUpdateDto, @AuthenticationPrincipal principal: KomgaPrincipal @@ -264,12 +265,8 @@ class ReadListController( principal.user.id, principal.user.getAuthorizedLibraryIds(null), UnpagedSorted(Sort.by(Sort.Order.asc("readList.number"))) - ).forEachIndexed { index, book -> - if (index < readProgress.lastBookRead) - bookLifecycle.markReadProgressCompleted(book.id, principal.user) - else - bookLifecycle.deleteReadProgress(book.id, principal.user) - } + ).filterIndexed { index, _ -> index < readProgress.lastBookRead } + .forEach { book -> bookLifecycle.markReadProgressCompleted(book.id, principal.user) } } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } } 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 f1d951788..cc02fc0c0 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 @@ -35,10 +35,12 @@ import org.gotson.komga.interfaces.rest.dto.BookDto import org.gotson.komga.interfaces.rest.dto.CollectionDto import org.gotson.komga.interfaces.rest.dto.SeriesDto import org.gotson.komga.interfaces.rest.dto.SeriesMetadataUpdateDto +import org.gotson.komga.interfaces.rest.dto.TachiyomiReadProgressDto import org.gotson.komga.interfaces.rest.dto.TachiyomiReadProgressUpdateDto import org.gotson.komga.interfaces.rest.dto.restrictUrl import org.gotson.komga.interfaces.rest.dto.toDto import org.gotson.komga.interfaces.rest.persistence.BookDtoRepository +import org.gotson.komga.interfaces.rest.persistence.ReadProgressDtoRepository import org.gotson.komga.interfaces.rest.persistence.SeriesDtoRepository import org.springframework.core.io.FileSystemResource import org.springframework.data.domain.Page @@ -57,6 +59,7 @@ import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PatchMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.PutMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam @@ -81,7 +84,8 @@ class SeriesController( private val bookLifecycle: BookLifecycle, private val bookRepository: BookRepository, private val bookDtoRepository: BookDtoRepository, - private val collectionRepository: SeriesCollectionRepository + private val collectionRepository: SeriesCollectionRepository, + private val readProgressDtoRepository: ReadProgressDtoRepository, ) { @PageableAsQueryParam @@ -364,29 +368,6 @@ class SeriesController( } } - @PatchMapping("{seriesId}/read-progress/tachiyomi") - @ResponseStatus(HttpStatus.NO_CONTENT) - fun markReadProgress( - @PathVariable seriesId: String, - @Valid @RequestBody readProgress: TachiyomiReadProgressUpdateDto, - @AuthenticationPrincipal principal: KomgaPrincipal - ) { - seriesRepository.getLibraryId(seriesId)?.let { - if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN) - } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) - - bookDtoRepository.findAll( - BookSearchWithReadProgress(seriesIds = listOf(seriesId)), - principal.user.id, - UnpagedSorted(Sort.by(Sort.Order.asc("metadata.numberSort"))), - ).forEachIndexed { index, book -> - if (index < readProgress.lastBookRead) - bookLifecycle.markReadProgressCompleted(book.id, principal.user) - else - bookLifecycle.deleteReadProgress(book.id, principal.user) - } - } - @DeleteMapping("{seriesId}/read-progress") @ResponseStatus(HttpStatus.NO_CONTENT) fun markAsUnread( @@ -402,6 +383,35 @@ class SeriesController( } } + @GetMapping("{seriesId}/read-progress/tachiyomi") + fun getReadProgressTachiyomi( + @PathVariable seriesId: String, + @AuthenticationPrincipal principal: KomgaPrincipal, + ): TachiyomiReadProgressDto = + seriesRepository.getLibraryId(seriesId)?.let { + if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN) + return readProgressDtoRepository.getProgressBySeries(seriesId, principal.user.id) + } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + + @PutMapping("{seriesId}/read-progress/tachiyomi") + @ResponseStatus(HttpStatus.NO_CONTENT) + fun markReadProgressTachiyomi( + @PathVariable seriesId: String, + @Valid @RequestBody readProgress: TachiyomiReadProgressUpdateDto, + @AuthenticationPrincipal principal: KomgaPrincipal, + ) { + seriesRepository.getLibraryId(seriesId)?.let { + if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN) + } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + + bookDtoRepository.findAll( + BookSearchWithReadProgress(seriesIds = listOf(seriesId)), + principal.user.id, + UnpagedSorted(Sort.by(Sort.Order.asc("metadata.numberSort"))), + ).filterIndexed { index, _ -> index < readProgress.lastBookRead } + .forEach { book -> bookLifecycle.markReadProgressCompleted(book.id, principal.user) } + } + @GetMapping("{seriesId}/file", produces = [MediaType.APPLICATION_OCTET_STREAM_VALUE]) @PreAuthorize("hasRole('$ROLE_FILE_DOWNLOAD')") fun getSeriesFile( diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/ReadListDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/ReadListDto.kt index b90cd68c2..a74bea91f 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/ReadListDto.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/ReadListDto.kt @@ -18,13 +18,6 @@ data class ReadListDto( val filtered: Boolean ) -data class ReadListProgressDto( - val booksCount: Int, - val booksReadCount: Int, - val booksUnreadCount: Int, - val booksInProgressCount: Int, -) - fun ReadList.toDto() = ReadListDto( id = id, diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/ReadListDtoDao.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/ReadListDtoDao.kt deleted file mode 100644 index 140faf099..000000000 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/ReadListDtoDao.kt +++ /dev/null @@ -1,49 +0,0 @@ -package org.gotson.komga.interfaces.rest.dto - -import org.gotson.komga.infrastructure.jooq.BOOKS_IN_PROGRESS_COUNT -import org.gotson.komga.infrastructure.jooq.BOOKS_READ_COUNT -import org.gotson.komga.infrastructure.jooq.BOOKS_UNREAD_COUNT -import org.gotson.komga.infrastructure.jooq.SeriesDtoDao -import org.gotson.komga.interfaces.rest.persistence.ReadListDtoRepository -import org.gotson.komga.jooq.Tables -import org.jooq.Condition -import org.jooq.DSLContext -import org.springframework.stereotype.Component - -@Component -class ReadListDtoDao( - private val dsl: DSLContext -) : ReadListDtoRepository { - - private val rl = Tables.READLIST - private val rlb = Tables.READLIST_BOOK - private val b = Tables.BOOK - private val r = Tables.READ_PROGRESS - - override fun getProgress(readListId: String, userId: String): ReadListProgressDto { - - val booksCountRecord = dsl - .select(SeriesDtoDao.countUnread.`as`(BOOKS_UNREAD_COUNT)) - .select(SeriesDtoDao.countRead.`as`(BOOKS_READ_COUNT)) - .select(SeriesDtoDao.countInProgress.`as`(BOOKS_IN_PROGRESS_COUNT)) - .from(b) - .leftJoin(r).on(b.ID.eq(r.BOOK_ID)).and(readProgressCondition(userId)) - .leftJoin(rlb).on(b.ID.eq(rlb.BOOK_ID)) - .where(rlb.READLIST_ID.eq(readListId)) - .fetch() - .first() - - val booksUnreadCount = booksCountRecord.get(BOOKS_UNREAD_COUNT, Int::class.java) - val booksReadCount = booksCountRecord.get(BOOKS_READ_COUNT, Int::class.java) - val booksInProgressCount = booksCountRecord.get(BOOKS_IN_PROGRESS_COUNT, Int::class.java) - - return ReadListProgressDto( - booksCount = booksUnreadCount + booksReadCount + booksInProgressCount, - booksUnreadCount = booksUnreadCount, - booksInProgressCount = booksInProgressCount, - booksReadCount = booksReadCount, - ) - } - - private fun readProgressCondition(userId: String): Condition = r.USER_ID.eq(userId).or(r.USER_ID.isNull) -} diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/TachiyomiReadProgressDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/TachiyomiReadProgressDto.kt new file mode 100644 index 000000000..5f085fb9e --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/TachiyomiReadProgressDto.kt @@ -0,0 +1,9 @@ +package org.gotson.komga.interfaces.rest.dto + +data class TachiyomiReadProgressDto( + val booksCount: Int, + val booksReadCount: Int, + val booksUnreadCount: Int, + val booksInProgressCount: Int, + val lastReadContinuousIndex: Int, +) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/persistence/ReadListDtoRepository.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/persistence/ReadListDtoRepository.kt deleted file mode 100644 index 97806c15d..000000000 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/persistence/ReadListDtoRepository.kt +++ /dev/null @@ -1,7 +0,0 @@ -package org.gotson.komga.interfaces.rest.persistence - -import org.gotson.komga.interfaces.rest.dto.ReadListProgressDto - -interface ReadListDtoRepository { - fun getProgress(readListId: String, userId: String,): ReadListProgressDto -} diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/persistence/ReadProgressDtoRepository.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/persistence/ReadProgressDtoRepository.kt new file mode 100644 index 000000000..ac6721f9f --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/persistence/ReadProgressDtoRepository.kt @@ -0,0 +1,8 @@ +package org.gotson.komga.interfaces.rest.persistence + +import org.gotson.komga.interfaces.rest.dto.TachiyomiReadProgressDto + +interface ReadProgressDtoRepository { + fun getProgressBySeries(seriesId: String, userId: String,): TachiyomiReadProgressDto + fun getProgressByReadList(readListId: String, userId: String,): TachiyomiReadProgressDto +}