fix(api): better handling of tachiyomi tracking

read progress returns the last book read in a continuous fashion
don't mark books as unread when tachiyomi updates progress, only read
This commit is contained in:
Gauthier Roebroeck 2021-05-14 09:50:49 +08:00
parent 571d54a526
commit a7ab0da025
8 changed files with 158 additions and 101 deletions

View file

@ -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<Record2<Int, Boolean>>): 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)
}

View file

@ -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)
}
}

View file

@ -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(

View file

@ -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,

View file

@ -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)
}

View file

@ -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,
)

View file

@ -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
}

View file

@ -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
}