Skip to content

Commit

Permalink
feat(api): collections management
Browse files Browse the repository at this point in the history
related to gotson#30
  • Loading branch information
gotson committed Jun 19, 2020
1 parent 1650aec commit c2f9403
Show file tree
Hide file tree
Showing 19 changed files with 1,268 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
create table collection
(
id bigint not null,
name varchar not null,
ordered boolean not null default false,
series_count int not null,
created_date timestamp not null default now(),
last_modified_date timestamp not null default now(),
primary key (id)
);

create table collection_series
(
collection_id bigint not null,
series_id bigint not null,
number integer not null
);

alter table collection_series
add constraint fk_collection_series_collection_collection_id foreign key (collection_id) references collection (id);

alter table collection_series
add constraint fk_collection_series_series_series_id foreign key (series_id) references series (id);
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.gotson.komga.domain.model

import java.time.LocalDateTime

data class SeriesCollection(
val name: String,
val ordered: Boolean = false,

val seriesIds: List<Long> = emptyList(),

val id: Long = 0,

override val createdDate: LocalDateTime = LocalDateTime.now(),
override val lastModifiedDate: LocalDateTime = LocalDateTime.now(),

/**
* Indicates that the seriesIds have been filtered and is not exhaustive.
*/
val filtered: Boolean = false
) : Auditable()
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.gotson.komga.domain.persistence

import org.gotson.komga.domain.model.SeriesCollection

interface SeriesCollectionRepository {
fun findByIdOrNull(collectionId: Long): SeriesCollection?
fun findAll(): Collection<SeriesCollection>

/**
* Find one SeriesCollection by collectionId,
* optionally with only seriesId filtered by the provided filterOnLibraryIds.
*/
fun findByIdOrNull(collectionId: Long, filterOnLibraryIds: Collection<Long>?): SeriesCollection?

/**
* Find all SeriesCollection with at least one Series belonging to the provided belongsToLibraryIds,
* optionally with only seriesId filtered by the provided filterOnLibraryIds.
*/
fun findAllByLibraries(belongsToLibraryIds: Collection<Long>, filterOnLibraryIds: Collection<Long>?): Collection<SeriesCollection>

/**
* Find all SeriesCollection that contains the provided containsSeriesId,
* optionally with only seriesId filtered by the provided filterOnLibraryIds.
*/
fun findAllBySeries(containsSeriesId: Long, filterOnLibraryIds: Collection<Long>?): Collection<SeriesCollection>

fun insert(collection: SeriesCollection): SeriesCollection
fun update(collection: SeriesCollection)

fun removeSeriesFromAll(seriesId: Long)

fun delete(collectionId: Long)
fun deleteAll()

fun existsByName(name: String): Boolean
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.gotson.komga.domain.service

import mu.KotlinLogging
import org.gotson.komga.domain.model.DuplicateNameException
import org.gotson.komga.domain.model.SeriesCollection
import org.gotson.komga.domain.persistence.SeriesCollectionRepository
import org.springframework.stereotype.Service

private val logger = KotlinLogging.logger {}

@Service
class SeriesCollectionLifecycle(
private val collectionRepository: SeriesCollectionRepository
) {

@Throws(
DuplicateNameException::class
)
fun addCollection(collection: SeriesCollection): SeriesCollection {
logger.info { "Adding new collection: $collection" }

if (collectionRepository.existsByName(collection.name))
throw DuplicateNameException("Collection name already exists")

return collectionRepository.insert(collection)
}

fun updateCollection(toUpdate: SeriesCollection) {
val existing = collectionRepository.findByIdOrNull(toUpdate.id)
?: throw IllegalArgumentException("Cannot update collection that does not exist")

if (existing.name != toUpdate.name && collectionRepository.existsByName(toUpdate.name))
throw DuplicateNameException("Collection name already exists")

collectionRepository.update(toUpdate)
}

fun deleteCollection(collectionId: Long) {
collectionRepository.delete(collectionId)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import org.gotson.komga.domain.model.SeriesMetadata
import org.gotson.komga.domain.persistence.BookMetadataRepository
import org.gotson.komga.domain.persistence.BookRepository
import org.gotson.komga.domain.persistence.MediaRepository
import org.gotson.komga.domain.persistence.SeriesCollectionRepository
import org.gotson.komga.domain.persistence.SeriesMetadataRepository
import org.gotson.komga.domain.persistence.SeriesRepository
import org.springframework.stereotype.Service
Expand All @@ -26,7 +27,8 @@ class SeriesLifecycle(
private val mediaRepository: MediaRepository,
private val bookMetadataRepository: BookMetadataRepository,
private val seriesRepository: SeriesRepository,
private val seriesMetadataRepository: SeriesMetadataRepository
private val seriesMetadataRepository: SeriesMetadataRepository,
private val collectionRepository: SeriesCollectionRepository
) {

fun sortBooks(series: Series) {
Expand Down Expand Up @@ -90,6 +92,8 @@ class SeriesLifecycle(
bookLifecycle.delete(it.id)
}

collectionRepository.removeSeriesFromAll(seriesId)

seriesRepository.delete(seriesId)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package org.gotson.komga.infrastructure.image

import net.coobird.thumbnailator.Thumbnails
import org.springframework.stereotype.Service
import java.awt.image.BufferedImage
import java.io.ByteArrayOutputStream
import javax.imageio.ImageIO


@Service
class MosaicGenerator {

fun createMosaic(images: List<ByteArray>): ByteArray {
val thumbs = images.map { resize(it, 150) }

return ByteArrayOutputStream().use { baos ->
val mosaic = BufferedImage(212, 300, BufferedImage.TYPE_INT_RGB)
mosaic.createGraphics().apply {
listOf(
0 to 0,
106 to 0,
0 to 150,
106 to 150
).forEachIndexed { index, (x, y) ->
thumbs.getOrNull(index)?.let { drawImage(it, x, y, null) }
}
}

ImageIO.write(mosaic, "jpeg", baos)

baos.toByteArray()
}
}

private fun resize(imageBytes: ByteArray, size: Int) =
Thumbnails.of(imageBytes.inputStream())
.size(size, size)
.outputFormat("jpeg")
.asBufferedImage()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package org.gotson.komga.infrastructure.jooq

import org.gotson.komga.domain.model.SeriesCollection
import org.gotson.komga.domain.persistence.SeriesCollectionRepository
import org.gotson.komga.jooq.Sequences
import org.gotson.komga.jooq.Tables
import org.gotson.komga.jooq.tables.records.CollectionRecord
import org.jooq.DSLContext
import org.jooq.Record
import org.jooq.ResultQuery
import org.springframework.stereotype.Component
import java.time.LocalDateTime

@Component
class SeriesCollectionDao(
private val dsl: DSLContext
) : SeriesCollectionRepository {

private val c = Tables.COLLECTION
private val cs = Tables.COLLECTION_SERIES
private val s = Tables.SERIES

private val groupFields = arrayOf(*c.fields(), *cs.fields())


override fun findByIdOrNull(collectionId: Long): SeriesCollection? =
selectBase()
.where(c.ID.eq(collectionId))
.groupBy(*groupFields)
.orderBy(cs.NUMBER.asc())
.fetchAndMap()
.firstOrNull()

override fun findByIdOrNull(collectionId: Long, filterOnLibraryIds: Collection<Long>?): SeriesCollection? =
selectBase()
.where(c.ID.eq(collectionId))
.also { step ->
filterOnLibraryIds?.let { step.and(s.LIBRARY_ID.`in`(it)) }
}
.groupBy(*groupFields)
.orderBy(cs.NUMBER.asc())
.fetchAndMap()
.firstOrNull()

override fun findAll(): Collection<SeriesCollection> =
selectBase()
.groupBy(*groupFields)
.orderBy(cs.NUMBER.asc())
.fetchAndMap()

override fun findAllByLibraries(belongsToLibraryIds: Collection<Long>, filterOnLibraryIds: Collection<Long>?): Collection<SeriesCollection> {
val ids = dsl.select(c.ID)
.from(c)
.leftJoin(cs).on(c.ID.eq(cs.COLLECTION_ID))
.leftJoin(s).on(cs.SERIES_ID.eq(s.ID))
.where(s.LIBRARY_ID.`in`(belongsToLibraryIds))
.fetch(0, Long::class.java)

return selectBase()
.where(c.ID.`in`(ids))
.also { step ->
filterOnLibraryIds?.let { step.and(s.LIBRARY_ID.`in`(it)) }
}
.groupBy(*groupFields)
.orderBy(cs.NUMBER.asc())
.fetchAndMap()
}

override fun findAllBySeries(containsSeriesId: Long, filterOnLibraryIds: Collection<Long>?): Collection<SeriesCollection> {
val ids = dsl.select(c.ID)
.from(c)
.leftJoin(cs).on(c.ID.eq(cs.COLLECTION_ID))
.where(cs.SERIES_ID.eq(containsSeriesId))
.fetch(0, Long::class.java)

return selectBase()
.where(c.ID.`in`(ids))
.also { step ->
filterOnLibraryIds?.let { step.and(s.LIBRARY_ID.`in`(it)) }
}
.groupBy(*groupFields)
.orderBy(cs.NUMBER.asc())
.fetchAndMap()
}

private fun selectBase() =
dsl.select(*groupFields)
.from(c)
.leftJoin(cs).on(c.ID.eq(cs.COLLECTION_ID))
.leftJoin(s).on(cs.SERIES_ID.eq(s.ID))

private fun ResultQuery<Record>.fetchAndMap() =
fetchGroups({ it.into(c) }, { it.into(cs) })
.map { (cr, csr) ->
val seriesIds = csr.map { it.seriesId }
cr.toDomain(seriesIds)
}

override fun insert(collection: SeriesCollection): SeriesCollection {
val id = dsl.nextval(Sequences.HIBERNATE_SEQUENCE)
val insert = collection.copy(id = id)

dsl.insertInto(c)
.set(c.ID, insert.id)
.set(c.NAME, insert.name)
.set(c.ORDERED, insert.ordered)
.set(c.SERIES_COUNT, collection.seriesIds.size)
.execute()

insertSeries(insert)

return findByIdOrNull(id)!!
}


private fun insertSeries(collection: SeriesCollection) {
collection.seriesIds.forEachIndexed { index, id ->
dsl.insertInto(cs)
.set(cs.COLLECTION_ID, collection.id)
.set(cs.SERIES_ID, id)
.set(cs.NUMBER, index)
.execute()
}
}

override fun update(collection: SeriesCollection) {
dsl.transaction { config ->
with(config.dsl())
{
update(c)
.set(c.NAME, collection.name)
.set(c.ORDERED, collection.ordered)
.set(c.SERIES_COUNT, collection.seriesIds.size)
.set(c.LAST_MODIFIED_DATE, LocalDateTime.now())
.where(c.ID.eq(collection.id))
.execute()

deleteFrom(cs).where(cs.COLLECTION_ID.eq(collection.id)).execute()

insertSeries(collection)
}
}
}

override fun removeSeriesFromAll(seriesId: Long) {
dsl.deleteFrom(cs)
.where(cs.SERIES_ID.eq(seriesId))
.execute()
}

override fun delete(collectionId: Long) {
dsl.transaction { config ->
with(config.dsl())
{
deleteFrom(cs).where(cs.COLLECTION_ID.eq(collectionId)).execute()
deleteFrom(c).where(c.ID.eq(collectionId)).execute()
}
}
}

override fun deleteAll() {
dsl.transaction { config ->
with(config.dsl())
{
deleteFrom(cs).execute()
deleteFrom(c).execute()
}
}
}

override fun existsByName(name: String): Boolean =
dsl.fetchExists(
dsl.selectFrom(c)
.where(c.NAME.equalIgnoreCase(name))
)


private fun CollectionRecord.toDomain(seriesIds: List<Long>) =
SeriesCollection(
name = name,
ordered = ordered,
seriesIds = seriesIds,
id = id,
createdDate = createdDate,
lastModifiedDate = lastModifiedDate,
filtered = seriesCount != seriesIds.size
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ class SeriesDtoDao(
.fetchAndMap()
.firstOrNull()

override fun findByIds(seriesIds: Collection<Long>, userId: Long): List<SeriesDto> =
selectBase(userId)
.where(s.ID.`in`(seriesIds))
.groupBy(*groupFields)
.fetchAndMap()


private fun findAll(conditions: Condition, having: Condition, userId: Long, pageable: Pageable): Page<SeriesDto> {
val count = dsl.select(s.ID)
Expand Down

0 comments on commit c2f9403

Please sign in to comment.