Skip to content

Commit

Permalink
feat: aggregate book information at series level
Browse files Browse the repository at this point in the history
  • Loading branch information
gotson committed Jan 11, 2021
1 parent fc27ec8 commit eb029d9
Show file tree
Hide file tree
Showing 15 changed files with 516 additions and 5 deletions.
@@ -0,0 +1,20 @@
CREATE TABLE BOOK_METADATA_AGGREGATION
(
CREATED_DATE datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
LAST_MODIFIED_DATE datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
RELEASE_DATE date NULL,
SUMMARY varchar NOT NULL DEFAULT '',
SUMMARY_NUMBER varchar NOT NULL DEFAULT '',
SERIES_ID varchar NOT NULL PRIMARY KEY,
FOREIGN KEY (SERIES_ID) REFERENCES SERIES (ID)
);
CREATE TABLE BOOK_METADATA_AGGREGATION_AUTHOR
(
NAME varchar NOT NULL,
ROLE varchar NOT NULL,
SERIES_ID varchar NOT NULL,
FOREIGN KEY (SERIES_ID) REFERENCES SERIES (ID)
);
INSERT INTO BOOK_METADATA_AGGREGATION(SERIES_ID)
SELECT ID
from SERIES;
Expand Up @@ -24,4 +24,8 @@ sealed class Task : Serializable {
data class RefreshSeriesMetadata(val seriesId: String) : Task() {
override fun uniqueId() = "REFRESH_SERIES_METADATA_$seriesId"
}

data class AggregateSeriesMetadata(val seriesId: String) : Task() {
override fun uniqueId() = "AGGREGATE_SERIES_METADATA_$seriesId"
}
}
Expand Up @@ -60,6 +60,12 @@ class TaskHandler(
is Task.RefreshSeriesMetadata ->
seriesRepository.findByIdOrNull(task.seriesId)?.let {
metadataLifecycle.refreshMetadata(it)
taskReceiver.aggregateSeriesMetadata(it.id)
} ?: logger.warn { "Cannot execute task $task: Series does not exist" }

is Task.AggregateSeriesMetadata ->
seriesRepository.findByIdOrNull(task.seriesId)?.let {
metadataLifecycle.aggregateMetadata(it)
} ?: logger.warn { "Cannot execute task $task: Series does not exist" }
}
}.also {
Expand Down
Expand Up @@ -66,6 +66,10 @@ class TaskReceiver(
submitTask(Task.RefreshSeriesMetadata(seriesId))
}

fun aggregateSeriesMetadata(seriesId: String) {
submitTask(Task.AggregateSeriesMetadata(seriesId))
}

private fun submitTask(task: Task) {
logger.info { "Sending task: $task" }
jmsTemplate.convertAndSend(QUEUE_TASKS, task) {
Expand Down
@@ -0,0 +1,16 @@
package org.gotson.komga.domain.model

import java.time.LocalDate
import java.time.LocalDateTime

data class BookMetadataAggregation(
val authors: List<Author> = emptyList(),
val releaseDate: LocalDate? = null,
val summary: String = "",
val summaryNumber: String = "",

val seriesId: String = "",

override val createdDate: LocalDateTime = LocalDateTime.now(),
override val lastModifiedDate: LocalDateTime = LocalDateTime.now()
) : Auditable()
@@ -0,0 +1,16 @@
package org.gotson.komga.domain.persistence

import org.gotson.komga.domain.model.BookMetadataAggregation

interface BookMetadataAggregationRepository {
fun findById(seriesId: String): BookMetadataAggregation
fun findByIdOrNull(seriesId: String): BookMetadataAggregation?

fun insert(metadata: BookMetadataAggregation)
fun update(metadata: BookMetadataAggregation)

fun delete(seriesId: String)
fun delete(seriesIds: Collection<String>)

fun count(): Long
}
@@ -0,0 +1,19 @@
package org.gotson.komga.domain.service

import org.gotson.komga.domain.model.BookMetadata
import org.gotson.komga.domain.model.BookMetadataAggregation
import org.springframework.stereotype.Service

@Service
class MetadataAggregator {

fun aggregate(metadatas: Collection<BookMetadata>) : BookMetadataAggregation {
val authors = metadatas.flatMap { it.authors }.distinctBy { "${it.role}__${it.name}" }
val (summary, summaryNumber) = metadatas.sortedBy { it.numberSort }.find { it.summary.isNotBlank() }?.let {
it.summary to it.number
} ?: "" to ""
val releaseDate = metadatas.mapNotNull { it.releaseDate }.minOrNull()

return BookMetadataAggregation(authors = authors, releaseDate = releaseDate, summary = summary, summaryNumber = summaryNumber)
}
}
Expand Up @@ -6,6 +6,7 @@ import org.gotson.komga.domain.model.ReadList
import org.gotson.komga.domain.model.Series
import org.gotson.komga.domain.model.SeriesCollection
import org.gotson.komga.domain.model.SeriesMetadataPatch
import org.gotson.komga.domain.persistence.BookMetadataAggregationRepository
import org.gotson.komga.domain.persistence.BookMetadataRepository
import org.gotson.komga.domain.persistence.BookRepository
import org.gotson.komga.domain.persistence.LibraryRepository
Expand All @@ -27,9 +28,11 @@ class MetadataLifecycle(
private val bookMetadataProviders: List<BookMetadataProvider>,
private val seriesMetadataProviders: List<SeriesMetadataProvider>,
private val metadataApplier: MetadataApplier,
private val metadataAggregator: MetadataAggregator,
private val mediaRepository: MediaRepository,
private val bookMetadataRepository: BookMetadataRepository,
private val seriesMetadataRepository: SeriesMetadataRepository,
private val bookMetadataAggregationRepository: BookMetadataAggregationRepository,
private val libraryRepository: LibraryRepository,
private val bookRepository: BookRepository,
private val bookLifecycle: BookLifecycle,
Expand Down Expand Up @@ -192,6 +195,15 @@ class MetadataLifecycle(
}
}

fun aggregateMetadata(series: Series){
logger.info { "Aggregate book metadata for series: $series" }

val metadatas = bookMetadataRepository.findByIds(bookRepository.findAllIdBySeriesId(series.id))
val aggregation = metadataAggregator.aggregate(metadatas).copy(seriesId = series.id)

bookMetadataAggregationRepository.update(aggregation)
}

private fun <T, R : Any> Iterable<T>.mostFrequent(transform: (T) -> R?): R? {
return this
.mapNotNull(transform)
Expand Down
Expand Up @@ -6,10 +6,12 @@ import org.apache.commons.lang3.StringUtils
import org.gotson.komga.application.tasks.TaskReceiver
import org.gotson.komga.domain.model.Book
import org.gotson.komga.domain.model.BookMetadata
import org.gotson.komga.domain.model.BookMetadataAggregation
import org.gotson.komga.domain.model.Media
import org.gotson.komga.domain.model.Series
import org.gotson.komga.domain.model.SeriesMetadata
import org.gotson.komga.domain.model.ThumbnailSeries
import org.gotson.komga.domain.persistence.BookMetadataAggregationRepository
import org.gotson.komga.domain.persistence.BookMetadataRepository
import org.gotson.komga.domain.persistence.BookRepository
import org.gotson.komga.domain.persistence.MediaRepository
Expand All @@ -35,6 +37,7 @@ class SeriesLifecycle(
private val seriesRepository: SeriesRepository,
private val thumbnailsSeriesRepository: ThumbnailSeriesRepository,
private val seriesMetadataRepository: SeriesMetadataRepository,
private val bookMetadataAggregationRepository: BookMetadataAggregationRepository,
private val collectionRepository: SeriesCollectionRepository,
private val taskReceiver: TaskReceiver
) {
Expand Down Expand Up @@ -107,6 +110,10 @@ class SeriesLifecycle(
)
)

bookMetadataAggregationRepository.insert(
BookMetadataAggregation(seriesId = series.id)
)

return seriesRepository.findByIdOrNull(series.id)!!
}

Expand All @@ -119,6 +126,7 @@ class SeriesLifecycle(
collectionRepository.removeSeriesFromAll(seriesId)
thumbnailsSeriesRepository.deleteBySeriesId(seriesId)
seriesMetadataRepository.delete(seriesId)
bookMetadataAggregationRepository.delete(seriesId)

seriesRepository.delete(seriesId)
}
Expand All @@ -132,6 +140,7 @@ class SeriesLifecycle(
collectionRepository.removeSeriesFromAll(seriesIds)
thumbnailsSeriesRepository.deleteBySeriesIds(seriesIds)
seriesMetadataRepository.delete(seriesIds)
bookMetadataAggregationRepository.delete(seriesIds)

seriesRepository.deleteAll(seriesIds)
}
Expand Down
@@ -0,0 +1,124 @@
package org.gotson.komga.infrastructure.jooq

import org.gotson.komga.domain.model.Author
import org.gotson.komga.domain.model.BookMetadataAggregation
import org.gotson.komga.domain.persistence.BookMetadataAggregationRepository
import org.gotson.komga.jooq.Tables
import org.gotson.komga.jooq.tables.records.BookMetadataAggregationAuthorRecord
import org.gotson.komga.jooq.tables.records.BookMetadataAggregationRecord
import org.jooq.DSLContext
import org.springframework.stereotype.Component
import java.time.LocalDateTime
import java.time.ZoneId

@Component
class BookMetadataAggregationDao(
private val dsl: DSLContext
) : BookMetadataAggregationRepository {

private val d = Tables.BOOK_METADATA_AGGREGATION
private val a = Tables.BOOK_METADATA_AGGREGATION_AUTHOR

private val groupFields = arrayOf(*d.fields(), *a.fields())

override fun findById(seriesId: String): BookMetadataAggregation =
findOne(listOf(seriesId)).first()

override fun findByIdOrNull(seriesId: String): BookMetadataAggregation? =
findOne(listOf(seriesId)).firstOrNull()

private fun findOne(seriesIds: Collection<String>) =
dsl.select(*groupFields)
.from(d)
.leftJoin(a).on(d.SERIES_ID.eq(a.SERIES_ID))
.where(d.SERIES_ID.`in`(seriesIds))
.groupBy(*groupFields)
.fetchGroups(
{ it.into(d) }, { it.into(a) }
).map { (dr, ar) ->
dr.toDomain(ar.filterNot { it.name == null }.map { it.toDomain() })
}

override fun insert(metadata: BookMetadataAggregation) {
dsl.transaction { config ->
config.dsl().insertInto(d)
.set(d.SERIES_ID, metadata.seriesId)
.set(d.RELEASE_DATE, metadata.releaseDate)
.set(d.SUMMARY, metadata.summary)
.set(d.SUMMARY_NUMBER, metadata.summaryNumber)
.execute()

insertAuthors(config.dsl(), metadata)
}
}

override fun update(metadata: BookMetadataAggregation) {
dsl.transaction { config ->
config.dsl().update(d)
.set(d.SUMMARY, metadata.summary)
.set(d.SUMMARY_NUMBER, metadata.summaryNumber)
.set(d.RELEASE_DATE, metadata.releaseDate)
.set(d.LAST_MODIFIED_DATE, LocalDateTime.now(ZoneId.of("Z")))
.where(d.SERIES_ID.eq(metadata.seriesId))
.execute()

config.dsl().deleteFrom(a)
.where(a.SERIES_ID.eq(metadata.seriesId))
.execute()

insertAuthors(config.dsl(), metadata)
}
}

private fun insertAuthors(dsl: DSLContext, metadata: BookMetadataAggregation) {
if (metadata.authors.isNotEmpty()) {
dsl.batch(
dsl.insertInto(a, a.SERIES_ID, a.NAME, a.ROLE)
.values(null as String?, null, null)
).also { step ->
metadata.authors.forEach {
step.bind(metadata.seriesId, it.name, it.role)
}
}.execute()
}
}

override fun delete(seriesId: String) {
dsl.transaction { config ->
with(config.dsl()) {
deleteFrom(a).where(a.SERIES_ID.eq(seriesId)).execute()
deleteFrom(d).where(d.SERIES_ID.eq(seriesId)).execute()
}
}
}

override fun delete(seriesIds: Collection<String>) {
dsl.transaction { config ->
with(config.dsl()) {
deleteFrom(a).where(a.SERIES_ID.`in`(seriesIds)).execute()
deleteFrom(d).where(d.SERIES_ID.`in`(seriesIds)).execute()
}
}
}

override fun count(): Long = dsl.fetchCount(d).toLong()

private fun BookMetadataAggregationRecord.toDomain(authors: List<Author>) =
BookMetadataAggregation(
authors = authors,
releaseDate = releaseDate,
summary = summary,
summaryNumber = summaryNumber,

seriesId = seriesId,

createdDate = createdDate.toCurrentTimeZone(),
lastModifiedDate = lastModifiedDate.toCurrentTimeZone()
)

private fun BookMetadataAggregationAuthorRecord.toDomain() =
Author(
name = name,
role = role
)
}

0 comments on commit eb029d9

Please sign in to comment.