mirror of
https://github.com/gotson/komga.git
synced 2026-05-08 21:00:16 +02:00
perf: precompute series book counts
This commit is contained in:
parent
ae671709fe
commit
c3b352aca0
13 changed files with 183 additions and 79 deletions
|
|
@ -0,0 +1,29 @@
|
||||||
|
alter table SERIES
|
||||||
|
add column BOOK_COUNT int NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
update SERIES
|
||||||
|
set BOOK_COUNT = (
|
||||||
|
SELECT COUNT(b.ID)
|
||||||
|
FROM BOOK b
|
||||||
|
WHERE b.SERIES_ID = SERIES.ID
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE READ_PROGRESS_SERIES
|
||||||
|
(
|
||||||
|
SERIES_ID varchar NOT NULL,
|
||||||
|
USER_ID varchar NOT NULL,
|
||||||
|
READ_COUNT int NOT NULL,
|
||||||
|
IN_PROGRESS_COUNT int NOT NULL,
|
||||||
|
PRIMARY KEY (SERIES_ID, USER_ID),
|
||||||
|
FOREIGN KEY (SERIES_ID) REFERENCES SERIES (ID),
|
||||||
|
FOREIGN KEY (USER_ID) REFERENCES USER (ID)
|
||||||
|
);
|
||||||
|
|
||||||
|
insert into READ_PROGRESS_SERIES
|
||||||
|
select BOOK.SERIES_ID,
|
||||||
|
READ_PROGRESS.USER_ID,
|
||||||
|
sum(case when READ_PROGRESS.COMPLETED = 1 then 1 else 0 end) as READ_COUNT,
|
||||||
|
sum(case when READ_PROGRESS.COMPLETED = 0 then 1 else 0 end) as IN_PROGRESS_COUNT
|
||||||
|
from BOOK
|
||||||
|
inner join READ_PROGRESS on (BOOK.ID = READ_PROGRESS.BOOK_ID)
|
||||||
|
group by BOOK.SERIES_ID, READ_PROGRESS.USER_ID;
|
||||||
|
|
@ -13,6 +13,7 @@ data class Series(
|
||||||
|
|
||||||
val id: String = TsidCreator.getTsid256().toString(),
|
val id: String = TsidCreator.getTsid256().toString(),
|
||||||
val libraryId: String = "",
|
val libraryId: String = "",
|
||||||
|
val bookCount: Int = 0,
|
||||||
|
|
||||||
override val createdDate: LocalDateTime = LocalDateTime.now(),
|
override val createdDate: LocalDateTime = LocalDateTime.now(),
|
||||||
override val lastModifiedDate: LocalDateTime = LocalDateTime.now()
|
override val lastModifiedDate: LocalDateTime = LocalDateTime.now()
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import org.gotson.komga.domain.model.Media
|
||||||
|
|
||||||
interface MediaRepository {
|
interface MediaRepository {
|
||||||
fun findById(bookId: String): Media
|
fun findById(bookId: String): Media
|
||||||
|
fun getPagesSize(bookId: String): Int
|
||||||
|
fun getPagesSizes(bookIds: Collection<String>): Collection<Pair<String, Int>>
|
||||||
|
|
||||||
fun insert(media: Media)
|
fun insert(media: Media)
|
||||||
fun insertMany(medias: Collection<Media>)
|
fun insertMany(medias: Collection<Media>)
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,12 @@ interface ReadProgressRepository {
|
||||||
fun findByBookId(bookId: String): Collection<ReadProgress>
|
fun findByBookId(bookId: String): Collection<ReadProgress>
|
||||||
|
|
||||||
fun save(readProgress: ReadProgress)
|
fun save(readProgress: ReadProgress)
|
||||||
|
fun saveAll(readProgresses: Collection<ReadProgress>)
|
||||||
|
|
||||||
fun delete(bookId: String, userId: String)
|
fun delete(bookId: String, userId: String)
|
||||||
fun deleteByUserId(userId: String)
|
fun deleteByUserId(userId: String)
|
||||||
fun deleteByBookId(bookId: String)
|
fun deleteByBookId(bookId: String)
|
||||||
fun deleteByBookIds(bookIds: Collection<String>)
|
fun deleteByBookIds(bookIds: Collection<String>)
|
||||||
|
fun deleteByBookIdsAndUserId(bookIds: Collection<String>, userId: String)
|
||||||
fun deleteAll()
|
fun deleteAll()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -214,10 +214,10 @@ class BookLifecycle(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun markReadProgress(book: Book, user: KomgaUser, page: Int) {
|
fun markReadProgress(book: Book, user: KomgaUser, page: Int) {
|
||||||
val media = mediaRepository.findById(book.id)
|
val pages = mediaRepository.getPagesSize(book.id)
|
||||||
require(page >= 1 && page <= media.pages.size) { "Page argument ($page) must be within 1 and book page count (${media.pages.size})" }
|
require(page in 1..pages) { "Page argument ($page) must be within 1 and book page count ($pages)" }
|
||||||
|
|
||||||
readProgressRepository.save(ReadProgress(book.id, user.id, page, page == media.pages.size))
|
readProgressRepository.save(ReadProgress(book.id, user.id, page, page == pages))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun markReadProgressCompleted(bookId: String, user: KomgaUser) {
|
fun markReadProgressCompleted(bookId: String, user: KomgaUser) {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,9 @@ import org.gotson.komga.domain.model.Book
|
||||||
import org.gotson.komga.domain.model.BookMetadata
|
import org.gotson.komga.domain.model.BookMetadata
|
||||||
import org.gotson.komga.domain.model.BookMetadataAggregation
|
import org.gotson.komga.domain.model.BookMetadataAggregation
|
||||||
import org.gotson.komga.domain.model.BookMetadataPatchCapability
|
import org.gotson.komga.domain.model.BookMetadataPatchCapability
|
||||||
|
import org.gotson.komga.domain.model.KomgaUser
|
||||||
import org.gotson.komga.domain.model.Media
|
import org.gotson.komga.domain.model.Media
|
||||||
|
import org.gotson.komga.domain.model.ReadProgress
|
||||||
import org.gotson.komga.domain.model.Series
|
import org.gotson.komga.domain.model.Series
|
||||||
import org.gotson.komga.domain.model.SeriesMetadata
|
import org.gotson.komga.domain.model.SeriesMetadata
|
||||||
import org.gotson.komga.domain.model.ThumbnailSeries
|
import org.gotson.komga.domain.model.ThumbnailSeries
|
||||||
|
|
@ -16,6 +18,7 @@ import org.gotson.komga.domain.persistence.BookMetadataAggregationRepository
|
||||||
import org.gotson.komga.domain.persistence.BookMetadataRepository
|
import org.gotson.komga.domain.persistence.BookMetadataRepository
|
||||||
import org.gotson.komga.domain.persistence.BookRepository
|
import org.gotson.komga.domain.persistence.BookRepository
|
||||||
import org.gotson.komga.domain.persistence.MediaRepository
|
import org.gotson.komga.domain.persistence.MediaRepository
|
||||||
|
import org.gotson.komga.domain.persistence.ReadProgressRepository
|
||||||
import org.gotson.komga.domain.persistence.SeriesCollectionRepository
|
import org.gotson.komga.domain.persistence.SeriesCollectionRepository
|
||||||
import org.gotson.komga.domain.persistence.SeriesMetadataRepository
|
import org.gotson.komga.domain.persistence.SeriesMetadataRepository
|
||||||
import org.gotson.komga.domain.persistence.SeriesRepository
|
import org.gotson.komga.domain.persistence.SeriesRepository
|
||||||
|
|
@ -24,7 +27,6 @@ import org.springframework.stereotype.Service
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
import java.util.Comparator
|
|
||||||
|
|
||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
private val natSortComparator: Comparator<String> = CaseInsensitiveSimpleNaturalComparator.getInstance()
|
private val natSortComparator: Comparator<String> = CaseInsensitiveSimpleNaturalComparator.getInstance()
|
||||||
|
|
@ -40,6 +42,7 @@ class SeriesLifecycle(
|
||||||
private val seriesMetadataRepository: SeriesMetadataRepository,
|
private val seriesMetadataRepository: SeriesMetadataRepository,
|
||||||
private val bookMetadataAggregationRepository: BookMetadataAggregationRepository,
|
private val bookMetadataAggregationRepository: BookMetadataAggregationRepository,
|
||||||
private val collectionRepository: SeriesCollectionRepository,
|
private val collectionRepository: SeriesCollectionRepository,
|
||||||
|
private val readProgressRepository: ReadProgressRepository,
|
||||||
private val taskReceiver: TaskReceiver
|
private val taskReceiver: TaskReceiver
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|
@ -69,13 +72,16 @@ class SeriesLifecycle(
|
||||||
}
|
}
|
||||||
bookMetadataRepository.updateMany(oldToNew.map { it.second })
|
bookMetadataRepository.updateMany(oldToNew.map { it.second })
|
||||||
|
|
||||||
// refresh metadata to reimport book number, else the series resorting would overwritei t
|
// refresh metadata to reimport book number, else the series resorting would overwrite it
|
||||||
oldToNew.forEach { (old, new) ->
|
oldToNew.forEach { (old, new) ->
|
||||||
if (old.number != new.number || old.numberSort != new.numberSort) {
|
if (old.number != new.number || old.numberSort != new.numberSort) {
|
||||||
logger.debug { "Metadata numbering has changed, refreshing metadata for book ${new.bookId} " }
|
logger.debug { "Metadata numbering has changed, refreshing metadata for book ${new.bookId} " }
|
||||||
taskReceiver.refreshBookMetadata(new.bookId, listOf(BookMetadataPatchCapability.NUMBER, BookMetadataPatchCapability.NUMBER_SORT))
|
taskReceiver.refreshBookMetadata(new.bookId, listOf(BookMetadataPatchCapability.NUMBER, BookMetadataPatchCapability.NUMBER_SORT))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update book count for series
|
||||||
|
seriesRepository.update(series.copy(bookCount = books.size))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addBooks(series: Series, booksToAdd: Collection<Book>) {
|
fun addBooks(series: Series, booksToAdd: Collection<Book>) {
|
||||||
|
|
@ -147,6 +153,17 @@ class SeriesLifecycle(
|
||||||
seriesRepository.deleteAll(seriesIds)
|
seriesRepository.deleteAll(seriesIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun markReadProgressCompleted(seriesId: String, user: KomgaUser) {
|
||||||
|
val progresses = mediaRepository.getPagesSizes(bookRepository.findAllIdBySeriesId(seriesId))
|
||||||
|
.map { (bookId, pageSize) -> ReadProgress(bookId, user.id, pageSize, true) }
|
||||||
|
|
||||||
|
readProgressRepository.saveAll(progresses)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteReadProgress(seriesId: String, user: KomgaUser) {
|
||||||
|
readProgressRepository.deleteByBookIdsAndUserId(bookRepository.findAllIdBySeriesId(seriesId), user.id)
|
||||||
|
}
|
||||||
|
|
||||||
fun getThumbnail(seriesId: String): ThumbnailSeries? {
|
fun getThumbnail(seriesId: String): ThumbnailSeries? {
|
||||||
val selected = thumbnailsSeriesRepository.findSelectedBySeriesId(seriesId)
|
val selected = thumbnailsSeriesRepository.findSelectedBySeriesId(seriesId)
|
||||||
|
|
||||||
|
|
@ -205,6 +222,5 @@ class SeriesLifecycle(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ThumbnailSeries.exists(): Boolean = Files.exists(Paths.get(url.toURI()))
|
private fun ThumbnailSeries.exists(): Boolean = Files.exists(Paths.get(url.toURI()))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,20 @@ class MediaDao(
|
||||||
override fun findById(bookId: String): Media =
|
override fun findById(bookId: String): Media =
|
||||||
find(dsl, bookId)
|
find(dsl, bookId)
|
||||||
|
|
||||||
|
override fun getPagesSize(bookId: String): Int =
|
||||||
|
dsl.select(m.PAGE_COUNT)
|
||||||
|
.from(m)
|
||||||
|
.where(m.BOOK_ID.eq(bookId))
|
||||||
|
.fetch(m.PAGE_COUNT)
|
||||||
|
.first()
|
||||||
|
|
||||||
|
override fun getPagesSizes(bookIds: Collection<String>): Collection<Pair<String, Int>> =
|
||||||
|
dsl.select(m.BOOK_ID, m.PAGE_COUNT)
|
||||||
|
.from(m)
|
||||||
|
.where(m.BOOK_ID.`in`(bookIds))
|
||||||
|
.fetch()
|
||||||
|
.map { Pair(it[m.BOOK_ID], it[m.PAGE_COUNT]) }
|
||||||
|
|
||||||
private fun find(dsl: DSLContext, bookId: String): Media =
|
private fun find(dsl: DSLContext, bookId: String): Media =
|
||||||
dsl.select(*groupFields)
|
dsl.select(*groupFields)
|
||||||
.from(m)
|
.from(m)
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import org.gotson.komga.domain.persistence.ReadProgressRepository
|
||||||
import org.gotson.komga.jooq.Tables
|
import org.gotson.komga.jooq.Tables
|
||||||
import org.gotson.komga.jooq.tables.records.ReadProgressRecord
|
import org.gotson.komga.jooq.tables.records.ReadProgressRecord
|
||||||
import org.jooq.DSLContext
|
import org.jooq.DSLContext
|
||||||
|
import org.jooq.Query
|
||||||
|
import org.jooq.impl.DSL
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
|
|
@ -15,6 +17,8 @@ class ReadProgressDao(
|
||||||
) : ReadProgressRepository {
|
) : ReadProgressRepository {
|
||||||
|
|
||||||
private val r = Tables.READ_PROGRESS
|
private val r = Tables.READ_PROGRESS
|
||||||
|
private val rs = Tables.READ_PROGRESS_SERIES
|
||||||
|
private val b = Tables.BOOK
|
||||||
|
|
||||||
override fun findAll(): Collection<ReadProgress> =
|
override fun findAll(): Collection<ReadProgress> =
|
||||||
dsl.selectFrom(r)
|
dsl.selectFrom(r)
|
||||||
|
|
@ -40,41 +44,96 @@ class ReadProgressDao(
|
||||||
.map { it.toDomain() }
|
.map { it.toDomain() }
|
||||||
|
|
||||||
override fun save(readProgress: ReadProgress) {
|
override fun save(readProgress: ReadProgress) {
|
||||||
dsl.insertInto(r, r.BOOK_ID, r.USER_ID, r.PAGE, r.COMPLETED)
|
dsl.transaction { config ->
|
||||||
|
config.dsl().saveQuery(readProgress).execute()
|
||||||
|
config.dsl().aggregateSeriesProgress(listOf(readProgress.bookId), readProgress.userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun saveAll(readProgresses: Collection<ReadProgress>) {
|
||||||
|
dsl.transaction { config ->
|
||||||
|
val queries = readProgresses.map { config.dsl().saveQuery(it) }
|
||||||
|
config.dsl().batch(queries).execute()
|
||||||
|
|
||||||
|
readProgresses.groupBy { it.userId }
|
||||||
|
.forEach { (userId, readProgresses) ->
|
||||||
|
config.dsl().aggregateSeriesProgress(readProgresses.map { it.bookId }, userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun DSLContext.saveQuery(readProgress: ReadProgress): Query =
|
||||||
|
this.insertInto(r, r.BOOK_ID, r.USER_ID, r.PAGE, r.COMPLETED)
|
||||||
.values(readProgress.bookId, readProgress.userId, readProgress.page, readProgress.completed)
|
.values(readProgress.bookId, readProgress.userId, readProgress.page, readProgress.completed)
|
||||||
.onDuplicateKeyUpdate()
|
.onDuplicateKeyUpdate()
|
||||||
.set(r.PAGE, readProgress.page)
|
.set(r.PAGE, readProgress.page)
|
||||||
.set(r.COMPLETED, readProgress.completed)
|
.set(r.COMPLETED, readProgress.completed)
|
||||||
.set(r.LAST_MODIFIED_DATE, LocalDateTime.now(ZoneId.of("Z")))
|
.set(r.LAST_MODIFIED_DATE, LocalDateTime.now(ZoneId.of("Z")))
|
||||||
.execute()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun delete(bookId: String, userId: String) {
|
override fun delete(bookId: String, userId: String) {
|
||||||
dsl.deleteFrom(r)
|
dsl.transaction { config ->
|
||||||
.where(r.BOOK_ID.eq(bookId).and(r.USER_ID.eq(userId)))
|
config.dsl().deleteFrom(r).where(r.BOOK_ID.eq(bookId).and(r.USER_ID.eq(userId))).execute()
|
||||||
.execute()
|
config.dsl().aggregateSeriesProgress(listOf(bookId), userId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun deleteByUserId(userId: String) {
|
override fun deleteByUserId(userId: String) {
|
||||||
dsl.deleteFrom(r)
|
dsl.transaction { config ->
|
||||||
.where(r.USER_ID.eq(userId))
|
config.dsl().deleteFrom(r).where(r.USER_ID.eq(userId)).execute()
|
||||||
.execute()
|
config.dsl().deleteFrom(rs).where(rs.USER_ID.eq(userId)).execute()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun deleteByBookId(bookId: String) {
|
override fun deleteByBookId(bookId: String) {
|
||||||
dsl.deleteFrom(r)
|
dsl.transaction { config ->
|
||||||
.where(r.BOOK_ID.eq(bookId))
|
config.dsl().deleteFrom(r).where(r.BOOK_ID.eq(bookId)).execute()
|
||||||
.execute()
|
config.dsl().aggregateSeriesProgress(listOf(bookId))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun deleteByBookIds(bookIds: Collection<String>) {
|
override fun deleteByBookIds(bookIds: Collection<String>) {
|
||||||
dsl.deleteFrom(r)
|
dsl.transaction { config ->
|
||||||
.where(r.BOOK_ID.`in`(bookIds))
|
config.dsl().deleteFrom(r).where(r.BOOK_ID.`in`(bookIds)).execute()
|
||||||
.execute()
|
config.dsl().aggregateSeriesProgress(bookIds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deleteByBookIdsAndUserId(bookIds: Collection<String>, userId: String) {
|
||||||
|
dsl.transaction { config ->
|
||||||
|
config.dsl().deleteFrom(r).where(r.BOOK_ID.`in`(bookIds)).and(r.USER_ID.eq(userId)).execute()
|
||||||
|
config.dsl().aggregateSeriesProgress(bookIds, userId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun deleteAll() {
|
override fun deleteAll() {
|
||||||
dsl.deleteFrom(r).execute()
|
dsl.transaction { config ->
|
||||||
|
config.dsl().deleteFrom(r).execute()
|
||||||
|
config.dsl().deleteFrom(rs).execute()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun DSLContext.aggregateSeriesProgress(bookIds: Collection<String>, userId: String? = null) {
|
||||||
|
val seriesIds = this.select(b.SERIES_ID)
|
||||||
|
.from(b)
|
||||||
|
.where(b.ID.`in`(bookIds))
|
||||||
|
.fetch(b.SERIES_ID)
|
||||||
|
|
||||||
|
this.deleteFrom(rs)
|
||||||
|
.where(rs.SERIES_ID.`in`(seriesIds))
|
||||||
|
.apply { userId?.let { and(rs.USER_ID.eq(it)) } }
|
||||||
|
.execute()
|
||||||
|
|
||||||
|
this.insertInto(rs)
|
||||||
|
.select(
|
||||||
|
this.select(b.SERIES_ID, r.USER_ID)
|
||||||
|
.select(DSL.sum(DSL.`when`(r.COMPLETED.isTrue, 1).otherwise(0)))
|
||||||
|
.select(DSL.sum(DSL.`when`(r.COMPLETED.isFalse, 1).otherwise(0)))
|
||||||
|
.from(b)
|
||||||
|
.innerJoin(r).on(b.ID.eq(r.BOOK_ID))
|
||||||
|
.where(b.SERIES_ID.`in`(seriesIds))
|
||||||
|
.apply { userId?.let { and(r.USER_ID.eq(it)) } }
|
||||||
|
.groupBy(b.SERIES_ID, r.USER_ID)
|
||||||
|
).execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ReadProgressRecord.toDomain() =
|
private fun ReadProgressRecord.toDomain() =
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,7 @@ class SeriesDao(
|
||||||
.set(s.URL, series.url.toString())
|
.set(s.URL, series.url.toString())
|
||||||
.set(s.FILE_LAST_MODIFIED, series.fileLastModified)
|
.set(s.FILE_LAST_MODIFIED, series.fileLastModified)
|
||||||
.set(s.LIBRARY_ID, series.libraryId)
|
.set(s.LIBRARY_ID, series.libraryId)
|
||||||
|
.set(s.BOOK_COUNT, series.bookCount)
|
||||||
.set(s.LAST_MODIFIED_DATE, LocalDateTime.now(ZoneId.of("Z")))
|
.set(s.LAST_MODIFIED_DATE, LocalDateTime.now(ZoneId.of("Z")))
|
||||||
.where(s.ID.eq(series.id))
|
.where(s.ID.eq(series.id))
|
||||||
.execute()
|
.execute()
|
||||||
|
|
@ -143,6 +144,7 @@ class SeriesDao(
|
||||||
fileLastModified = fileLastModified,
|
fileLastModified = fileLastModified,
|
||||||
id = id,
|
id = id,
|
||||||
libraryId = libraryId,
|
libraryId = libraryId,
|
||||||
|
bookCount = bookCount,
|
||||||
createdDate = createdDate.toCurrentTimeZone(),
|
createdDate = createdDate.toCurrentTimeZone(),
|
||||||
lastModifiedDate = lastModifiedDate.toCurrentTimeZone()
|
lastModifiedDate = lastModifiedDate.toCurrentTimeZone()
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,6 @@ import org.jooq.Record
|
||||||
import org.jooq.ResultQuery
|
import org.jooq.ResultQuery
|
||||||
import org.jooq.SelectOnConditionStep
|
import org.jooq.SelectOnConditionStep
|
||||||
import org.jooq.impl.DSL
|
import org.jooq.impl.DSL
|
||||||
import org.jooq.impl.DSL.field
|
|
||||||
import org.jooq.impl.DSL.inline
|
|
||||||
import org.jooq.impl.DSL.lower
|
import org.jooq.impl.DSL.lower
|
||||||
import org.springframework.data.domain.Page
|
import org.springframework.data.domain.Page
|
||||||
import org.springframework.data.domain.PageImpl
|
import org.springframework.data.domain.PageImpl
|
||||||
|
|
@ -43,9 +41,9 @@ class SeriesDtoDao(
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val s = Tables.SERIES
|
private val s = Tables.SERIES
|
||||||
private val b = Tables.BOOK
|
|
||||||
private val d = Tables.SERIES_METADATA
|
private val d = Tables.SERIES_METADATA
|
||||||
private val r = Tables.READ_PROGRESS
|
private val r = Tables.READ_PROGRESS
|
||||||
|
private val rs = Tables.READ_PROGRESS_SERIES
|
||||||
private val cs = Tables.COLLECTION_SERIES
|
private val cs = Tables.COLLECTION_SERIES
|
||||||
private val g = Tables.SERIES_METADATA_GENRE
|
private val g = Tables.SERIES_METADATA_GENRE
|
||||||
private val st = Tables.SERIES_METADATA_TAG
|
private val st = Tables.SERIES_METADATA_TAG
|
||||||
|
|
@ -61,24 +59,24 @@ class SeriesDtoDao(
|
||||||
*s.fields(),
|
*s.fields(),
|
||||||
*d.fields(),
|
*d.fields(),
|
||||||
*bma.fields(),
|
*bma.fields(),
|
||||||
|
*rs.fields(),
|
||||||
)
|
)
|
||||||
|
|
||||||
private val sorts = mapOf(
|
private val sorts = mapOf(
|
||||||
"metadata.titleSort" to DSL.lower(d.TITLE_SORT),
|
"metadata.titleSort" to lower(d.TITLE_SORT),
|
||||||
"createdDate" to s.CREATED_DATE,
|
"createdDate" to s.CREATED_DATE,
|
||||||
"created" to s.CREATED_DATE,
|
"created" to s.CREATED_DATE,
|
||||||
"lastModifiedDate" to s.LAST_MODIFIED_DATE,
|
"lastModifiedDate" to s.LAST_MODIFIED_DATE,
|
||||||
"lastModified" to s.LAST_MODIFIED_DATE,
|
"lastModified" to s.LAST_MODIFIED_DATE,
|
||||||
"collection.number" to cs.NUMBER,
|
"collection.number" to cs.NUMBER,
|
||||||
"name" to s.NAME,
|
"name" to s.NAME,
|
||||||
"booksCount" to field(BOOKS_COUNT)
|
"booksCount" to s.BOOK_COUNT,
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun findAll(search: SeriesSearchWithReadProgress, userId: String, pageable: Pageable): Page<SeriesDto> {
|
override fun findAll(search: SeriesSearchWithReadProgress, userId: String, pageable: Pageable): Page<SeriesDto> {
|
||||||
val conditions = search.toCondition()
|
val conditions = search.toCondition()
|
||||||
val having = search.readStatus?.toCondition() ?: DSL.trueCondition()
|
|
||||||
|
|
||||||
return findAll(conditions, having, userId, pageable, search.toJoinConditions())
|
return findAll(conditions, userId, pageable, search.toJoinConditions())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findByCollectionId(
|
override fun findByCollectionId(
|
||||||
|
|
@ -88,10 +86,9 @@ class SeriesDtoDao(
|
||||||
pageable: Pageable
|
pageable: Pageable
|
||||||
): Page<SeriesDto> {
|
): Page<SeriesDto> {
|
||||||
val conditions = search.toCondition().and(cs.COLLECTION_ID.eq(collectionId))
|
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)
|
val joinConditions = search.toJoinConditions().copy(selectCollectionNumber = true, collection = true)
|
||||||
|
|
||||||
return findAll(conditions, having, userId, pageable, joinConditions)
|
return findAll(conditions, userId, pageable, joinConditions)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findRecentlyUpdated(
|
override fun findRecentlyUpdated(
|
||||||
|
|
@ -102,9 +99,7 @@ class SeriesDtoDao(
|
||||||
val conditions = search.toCondition()
|
val conditions = search.toCondition()
|
||||||
.and(s.CREATED_DATE.ne(s.LAST_MODIFIED_DATE))
|
.and(s.CREATED_DATE.ne(s.LAST_MODIFIED_DATE))
|
||||||
|
|
||||||
val having = search.readStatus?.toCondition() ?: DSL.trueCondition()
|
return findAll(conditions, userId, pageable, search.toJoinConditions())
|
||||||
|
|
||||||
return findAll(conditions, having, userId, pageable, search.toJoinConditions())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findByIdOrNull(seriesId: String, userId: String): SeriesDto? =
|
override fun findByIdOrNull(seriesId: String, userId: String): SeriesDto? =
|
||||||
|
|
@ -119,16 +114,11 @@ class SeriesDtoDao(
|
||||||
joinConditions: JoinConditions = JoinConditions()
|
joinConditions: JoinConditions = JoinConditions()
|
||||||
): SelectOnConditionStep<Record> =
|
): SelectOnConditionStep<Record> =
|
||||||
dsl.selectDistinct(*groupFields)
|
dsl.selectDistinct(*groupFields)
|
||||||
.select(DSL.countDistinct(b.ID).`as`(BOOKS_COUNT))
|
|
||||||
.select(countUnread.`as`(BOOKS_UNREAD_COUNT))
|
|
||||||
.select(countRead.`as`(BOOKS_READ_COUNT))
|
|
||||||
.select(countInProgress.`as`(BOOKS_IN_PROGRESS_COUNT))
|
|
||||||
.apply { if (joinConditions.selectCollectionNumber) select(cs.NUMBER) }
|
.apply { if (joinConditions.selectCollectionNumber) select(cs.NUMBER) }
|
||||||
.from(s)
|
.from(s)
|
||||||
.leftJoin(b).on(s.ID.eq(b.SERIES_ID))
|
|
||||||
.leftJoin(d).on(s.ID.eq(d.SERIES_ID))
|
.leftJoin(d).on(s.ID.eq(d.SERIES_ID))
|
||||||
.leftJoin(bma).on(s.ID.eq(bma.SERIES_ID))
|
.leftJoin(bma).on(s.ID.eq(bma.SERIES_ID))
|
||||||
.leftJoin(r).on(b.ID.eq(r.BOOK_ID)).and(readProgressCondition(userId))
|
.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.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.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.collection) leftJoin(cs).on(s.ID.eq(cs.SERIES_ID)) }
|
||||||
|
|
@ -136,24 +126,20 @@ class SeriesDtoDao(
|
||||||
|
|
||||||
private fun findAll(
|
private fun findAll(
|
||||||
conditions: Condition,
|
conditions: Condition,
|
||||||
having: Condition,
|
|
||||||
userId: String,
|
userId: String,
|
||||||
pageable: Pageable,
|
pageable: Pageable,
|
||||||
joinConditions: JoinConditions = JoinConditions()
|
joinConditions: JoinConditions = JoinConditions()
|
||||||
): Page<SeriesDto> {
|
): Page<SeriesDto> {
|
||||||
val count = dsl.selectDistinct(s.ID)
|
val count = dsl.select(s.ID)
|
||||||
.from(s)
|
.from(s)
|
||||||
.leftJoin(b).on(s.ID.eq(b.SERIES_ID))
|
|
||||||
.leftJoin(d).on(s.ID.eq(d.SERIES_ID))
|
.leftJoin(d).on(s.ID.eq(d.SERIES_ID))
|
||||||
.leftJoin(bma).on(s.ID.eq(bma.SERIES_ID))
|
.leftJoin(bma).on(s.ID.eq(bma.SERIES_ID))
|
||||||
.leftJoin(r).on(b.ID.eq(r.BOOK_ID)).and(readProgressCondition(userId))
|
.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.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.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.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.aggregationAuthor) leftJoin(bmaa).on(s.ID.eq(bmaa.SERIES_ID)) }
|
||||||
.where(conditions)
|
.where(conditions)
|
||||||
.groupBy(s.ID)
|
|
||||||
.having(having)
|
|
||||||
.fetch()
|
.fetch()
|
||||||
.size
|
.size
|
||||||
|
|
||||||
|
|
@ -161,8 +147,6 @@ class SeriesDtoDao(
|
||||||
|
|
||||||
val dtos = selectBase(userId, joinConditions)
|
val dtos = selectBase(userId, joinConditions)
|
||||||
.where(conditions)
|
.where(conditions)
|
||||||
.groupBy(*groupFields)
|
|
||||||
.having(having)
|
|
||||||
.orderBy(orderBy)
|
.orderBy(orderBy)
|
||||||
.apply { if (pageable.isPaged) limit(pageable.pageSize).offset(pageable.offset) }
|
.apply { if (pageable.isPaged) limit(pageable.pageSize).offset(pageable.offset) }
|
||||||
.fetchAndMap()
|
.fetchAndMap()
|
||||||
|
|
@ -176,7 +160,7 @@ class SeriesDtoDao(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun readProgressCondition(userId: String): Condition = r.USER_ID.eq(userId).or(r.USER_ID.isNull)
|
private fun readProgressConditionSeries(userId: String): Condition = rs.USER_ID.eq(userId).or(rs.USER_ID.isNull)
|
||||||
|
|
||||||
private fun ResultQuery<Record>.fetchAndMap() =
|
private fun ResultQuery<Record>.fetchAndMap() =
|
||||||
fetch()
|
fetch()
|
||||||
|
|
@ -184,10 +168,10 @@ class SeriesDtoDao(
|
||||||
val sr = rec.into(s)
|
val sr = rec.into(s)
|
||||||
val dr = rec.into(d)
|
val dr = rec.into(d)
|
||||||
val bmar = rec.into(bma)
|
val bmar = rec.into(bma)
|
||||||
val booksCount = rec.get(BOOKS_COUNT, Int::class.java)
|
val rsr = rec.into(rs)
|
||||||
val booksUnreadCount = rec.get(BOOKS_UNREAD_COUNT, Int::class.java)
|
val booksReadCount = rsr.readCount ?: 0
|
||||||
val booksReadCount = rec.get(BOOKS_READ_COUNT, Int::class.java)
|
val booksInProgressCount = rsr.inProgressCount ?: 0
|
||||||
val booksInProgressCount = rec.get(BOOKS_IN_PROGRESS_COUNT, Int::class.java)
|
val booksUnreadCount = sr.bookCount - booksReadCount - booksInProgressCount
|
||||||
|
|
||||||
val genres = dsl.select(g.GENRE)
|
val genres = dsl.select(g.GENRE)
|
||||||
.from(g)
|
.from(g)
|
||||||
|
|
@ -206,7 +190,7 @@ class SeriesDtoDao(
|
||||||
.map { AuthorDto(it.name, it.role) }
|
.map { AuthorDto(it.name, it.role) }
|
||||||
|
|
||||||
sr.toDto(
|
sr.toDto(
|
||||||
booksCount,
|
sr.bookCount,
|
||||||
booksReadCount,
|
booksReadCount,
|
||||||
booksUnreadCount,
|
booksUnreadCount,
|
||||||
booksInProgressCount,
|
booksInProgressCount,
|
||||||
|
|
@ -242,6 +226,16 @@ class SeriesDtoDao(
|
||||||
}
|
}
|
||||||
c = c.and(ca)
|
c = c.and(ca)
|
||||||
}
|
}
|
||||||
|
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
|
return c
|
||||||
}
|
}
|
||||||
|
|
@ -262,15 +256,6 @@ class SeriesDtoDao(
|
||||||
val aggregationAuthor: Boolean = false,
|
val aggregationAuthor: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun Collection<ReadStatus>.toCondition(): Condition =
|
|
||||||
map {
|
|
||||||
when (it) {
|
|
||||||
ReadStatus.UNREAD -> countUnread.ge(inline(1.toBigDecimal()))
|
|
||||||
ReadStatus.READ -> countRead.ge(inline(1.toBigDecimal()))
|
|
||||||
ReadStatus.IN_PROGRESS -> countInProgress.ge(inline(1.toBigDecimal()))
|
|
||||||
}
|
|
||||||
}.reduce { acc, condition -> acc.or(condition) }
|
|
||||||
|
|
||||||
private fun SeriesRecord.toDto(
|
private fun SeriesRecord.toDto(
|
||||||
booksCount: Int,
|
booksCount: Int,
|
||||||
booksReadCount: Int,
|
booksReadCount: Int,
|
||||||
|
|
|
||||||
|
|
@ -363,9 +363,7 @@ class SeriesController(
|
||||||
if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
|
|
||||||
bookRepository.findAllIdBySeriesId(seriesId).forEach {
|
seriesLifecycle.markReadProgressCompleted(seriesId, principal.user)
|
||||||
bookLifecycle.markReadProgressCompleted(it, principal.user)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("{seriesId}/read-progress")
|
@DeleteMapping("{seriesId}/read-progress")
|
||||||
|
|
@ -378,9 +376,7 @@ class SeriesController(
|
||||||
if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
if (!principal.user.canAccessLibrary(it)) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||||
|
|
||||||
bookRepository.findAllIdBySeriesId(seriesId).forEach {
|
seriesLifecycle.deleteReadProgress(seriesId, principal.user)
|
||||||
bookLifecycle.deleteReadProgress(it, principal.user)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("{seriesId}/read-progress/tachiyomi")
|
@GetMapping("{seriesId}/read-progress/tachiyomi")
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,7 @@ class SeriesDtoDaoTest(
|
||||||
makeBook("$it", seriesId = created.id, libraryId = library.id)
|
makeBook("$it", seriesId = created.id, libraryId = library.id)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
seriesLifecycle.sortBooks(created)
|
||||||
}
|
}
|
||||||
|
|
||||||
val series = seriesRepository.findAll().sortedBy { it.name }
|
val series = seriesRepository.findAll().sortedBy { it.name }
|
||||||
|
|
@ -107,13 +108,10 @@ class SeriesDtoDaoTest(
|
||||||
).sortedBy { it.name }
|
).sortedBy { it.name }
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(found).hasSize(2)
|
assertThat(found).hasSize(1)
|
||||||
|
|
||||||
assertThat(found.first().booksReadCount).isEqualTo(3)
|
assertThat(found.first().booksReadCount).isEqualTo(3)
|
||||||
assertThat(found.first().name).isEqualTo("2")
|
assertThat(found.first().name).isEqualTo("2")
|
||||||
|
|
||||||
assertThat(found.last().booksReadCount).isEqualTo(1)
|
|
||||||
assertThat(found.last().name).isEqualTo("4")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -129,13 +127,10 @@ class SeriesDtoDaoTest(
|
||||||
).sortedBy { it.name }
|
).sortedBy { it.name }
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(found).hasSize(2)
|
assertThat(found).hasSize(1)
|
||||||
|
|
||||||
assertThat(found.first().booksUnreadCount).isEqualTo(3)
|
assertThat(found.first().booksUnreadCount).isEqualTo(3)
|
||||||
assertThat(found.first().name).isEqualTo("3")
|
assertThat(found.first().name).isEqualTo("3")
|
||||||
|
|
||||||
assertThat(found.last().booksUnreadCount).isEqualTo(1)
|
|
||||||
assertThat(found.last().name).isEqualTo("4")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -173,8 +168,8 @@ class SeriesDtoDaoTest(
|
||||||
).sortedBy { it.name }
|
).sortedBy { it.name }
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(found).hasSize(3)
|
assertThat(found).hasSize(2)
|
||||||
assertThat(found.map { it.name }).containsExactlyInAnyOrder("2", "3", "4")
|
assertThat(found.map { it.name }).containsExactlyInAnyOrder("2", "3")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -607,6 +607,7 @@ class SeriesControllerTest(
|
||||||
seriesLifecycle.createSeries(series).also { created ->
|
seriesLifecycle.createSeries(series).also { created ->
|
||||||
val books = listOf(makeBook("1.cbr", libraryId = library.id), makeBook("2.cbr", libraryId = library.id))
|
val books = listOf(makeBook("1.cbr", libraryId = library.id), makeBook("2.cbr", libraryId = library.id))
|
||||||
seriesLifecycle.addBooks(created, books)
|
seriesLifecycle.addBooks(created, books)
|
||||||
|
seriesLifecycle.sortBooks(created)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -649,6 +650,7 @@ class SeriesControllerTest(
|
||||||
seriesLifecycle.createSeries(series).also { created ->
|
seriesLifecycle.createSeries(series).also { created ->
|
||||||
val books = listOf(makeBook("1.cbr", libraryId = library.id), makeBook("2.cbr", libraryId = library.id))
|
val books = listOf(makeBook("1.cbr", libraryId = library.id), makeBook("2.cbr", libraryId = library.id))
|
||||||
seriesLifecycle.addBooks(created, books)
|
seriesLifecycle.addBooks(created, books)
|
||||||
|
seriesLifecycle.sortBooks(created)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -700,6 +702,7 @@ class SeriesControllerTest(
|
||||||
makeBook("3.cbr", libraryId = library.id)
|
makeBook("3.cbr", libraryId = library.id)
|
||||||
)
|
)
|
||||||
seriesLifecycle.addBooks(created, books)
|
seriesLifecycle.addBooks(created, books)
|
||||||
|
seriesLifecycle.sortBooks(created)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue