feat: add/rearrange metadata fields

existing fields moved from book to series: publisher, age rating, reading direction
new book fields: tags
new series fields: tags, genres, language, summary

closes #276
This commit is contained in:
Gauthier Roebroeck 2020-08-24 14:40:22 +08:00
parent d8db46c589
commit 9e406e3316
37 changed files with 1084 additions and 325 deletions

View file

@ -70,6 +70,8 @@ dependencies {
implementation("commons-io:commons-io:2.7")
implementation("org.apache.commons:commons-lang3:3.11")
implementation("com.ibm.icu:icu4j:67.1")
implementation("org.apache.tika:tika-core:1.24.1")
implementation("org.apache.commons:commons-compress:1.20")
implementation("com.github.junrar:junrar:7.2.0")

View file

@ -0,0 +1,48 @@
package db.migration.sqlite
import org.flywaydb.core.api.migration.BaseJavaMigration
import org.flywaydb.core.api.migration.Context
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.jdbc.datasource.SingleConnectionDataSource
class V20200820150923__metadata_fields_part_2 : BaseJavaMigration() {
override fun migrate(context: Context) {
val jdbcTemplate = JdbcTemplate(SingleConnectionDataSource(context.connection, true))
val bookMetadata = jdbcTemplate.queryForList(
"""select m.AGE_RATING, m.AGE_RATING_LOCK, m.PUBLISHER, m.PUBLISHER_LOCK, m.READING_DIRECTION, m.READING_DIRECTION_LOCK, b.SERIES_ID, m.NUMBER_SORT
from BOOK_METADATA m
left join BOOK B on B.ID = m.BOOK_ID"""
)
if (bookMetadata.isNotEmpty()) {
val parameters = bookMetadata
.groupBy { it["SERIES_ID"] }
.map { (seriesId, v) ->
val ageRating = v.mapNotNull { it["AGE_RATING"] as Int? }.max()
val ageRatingLock = v.mapNotNull { it["AGE_RATING_LOCK"] as Int? }.max()
val publisher =
v.filter { (it["PUBLISHER"] as String).isNotEmpty() }
.sortedByDescending { it["NUMBER_SORT"] as Double? }
.map { it["PUBLISHER"] as String }
.firstOrNull()
val publisherLock = v.mapNotNull { it["PUBLISHER_LOCK"] as Int? }.max()
val readingDir =
v.mapNotNull { it["READING_DIRECTION"] as String? }
.groupingBy { it }
.eachCount()
.maxBy { it.value }?.key
val readingDirLock = v.mapNotNull { it["READING_DIRECTION_LOCK"] as Int? }.max()
arrayOf(ageRating, ageRatingLock, publisher, publisherLock, readingDir, readingDirLock, seriesId)
}
jdbcTemplate.batchUpdate(
"UPDATE SERIES_METADATA SET AGE_RATING = ?, AGE_RATING_LOCK = ?, PUBLISHER = ?, PUBLISHER_LOCK = ?, READING_DIRECTION = ?, READING_DIRECTION_LOCK = ? WHERE SERIES_ID = ?",
parameters
)
}
}
}

View file

@ -0,0 +1,52 @@
alter table series_metadata
add column PUBLISHER varchar NOT NULL DEFAULT '';
alter table series_metadata
add column PUBLISHER_LOCK boolean NOT NULL DEFAULT 0;
alter table series_metadata
add column READING_DIRECTION varchar NULL;
alter table series_metadata
add column READING_DIRECTION_LOCK boolean NOT NULL DEFAULT 0;
alter table series_metadata
add column AGE_RATING int NULL;
alter table series_metadata
add column AGE_RATING_LOCK boolean NOT NULL DEFAULT 0;
alter table SERIES_METADATA
add column SUMMARY varchar NOT NULL DEFAULT '';
alter table SERIES_METADATA
add column SUMMARY_LOCK boolean NOT NULL DEFAULT 0;
alter table SERIES_METADATA
add column LANGUAGE varchar NOT NULL DEFAULT '';
alter table SERIES_METADATA
add column LANGUAGE_LOCK boolean NOT NULL DEFAULT 0;
alter table SERIES_METADATA
add column GENRES_LOCK boolean NOT NULL DEFAULT 0;
alter table SERIES_METADATA
add column TAGS_LOCK boolean NOT NULL DEFAULT 0;
CREATE TABLE SERIES_METADATA_GENRE
(
GENRE varchar NOT NULL,
SERIES_ID varchar NOT NULL,
FOREIGN KEY (SERIES_ID) REFERENCES SERIES (ID)
);
CREATE TABLE SERIES_METADATA_TAG
(
TAG varchar NOT NULL,
SERIES_ID varchar NOT NULL,
FOREIGN KEY (SERIES_ID) REFERENCES SERIES (ID)
);
CREATE TABLE BOOK_METADATA_TAG
(
TAG varchar NOT NULL,
BOOK_ID varchar NOT NULL,
FOREIGN KEY (BOOK_ID) REFERENCES BOOK (ID)
);

View file

@ -0,0 +1,47 @@
PRAGMA foreign_keys= OFF;
ALTER TABLE BOOK_METADATA
RENAME TO _BOOK_METADATA_OLD;
CREATE TABLE BOOK_METADATA
(
CREATED_DATE datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
LAST_MODIFIED_DATE datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
NUMBER varchar NOT NULL,
NUMBER_LOCK boolean NOT NULL DEFAULT 0,
NUMBER_SORT real NOT NULL,
NUMBER_SORT_LOCK boolean NOT NULL DEFAULT 0,
RELEASE_DATE date NULL,
RELEASE_DATE_LOCK boolean NOT NULL DEFAULT 0,
SUMMARY varchar NOT NULL DEFAULT '',
SUMMARY_LOCK boolean NOT NULL DEFAULT 0,
TITLE varchar NOT NULL,
TITLE_LOCK boolean NOT NULL DEFAULT 0,
AUTHORS_LOCK boolean NOT NULL DEFAULT 0,
TAGS_LOCK boolean NOT NULL DEFAULT 0,
BOOK_ID varchar NOT NULL PRIMARY KEY,
FOREIGN KEY (BOOK_ID) REFERENCES BOOK (ID)
);
INSERT INTO BOOK_METADATA (CREATED_DATE, LAST_MODIFIED_DATE, NUMBER, NUMBER_LOCK, NUMBER_SORT, NUMBER_SORT_LOCK,
RELEASE_DATE, RELEASE_DATE_LOCK, SUMMARY, SUMMARY_LOCK, TITLE, TITLE_LOCK, AUTHORS_LOCK,
BOOK_ID)
SELECT CREATED_DATE,
LAST_MODIFIED_DATE,
NUMBER,
NUMBER_LOCK,
NUMBER_SORT,
NUMBER_SORT_LOCK,
RELEASE_DATE,
RELEASE_DATE_LOCK,
SUMMARY,
SUMMARY_LOCK,
TITLE,
TITLE_LOCK,
AUTHORS_LOCK,
BOOK_ID
FROM _BOOK_METADATA_OLD;
DROP TABLE _BOOK_METADATA_OLD;
PRAGMA foreign_keys= ON;

View file

@ -8,21 +8,17 @@ class BookMetadata(
summary: String = "",
number: String,
val numberSort: Float,
val readingDirection: ReadingDirection? = null,
publisher: String = "",
val ageRating: Int? = null,
val releaseDate: LocalDate? = null,
val authors: List<Author> = emptyList(),
val tags: Set<String> = emptySet(),
val titleLock: Boolean = false,
val summaryLock: Boolean = false,
val numberLock: Boolean = false,
val numberSortLock: Boolean = false,
val readingDirectionLock: Boolean = false,
val publisherLock: Boolean = false,
val ageRatingLock: Boolean = false,
val releaseDateLock: Boolean = false,
val authorsLock: Boolean = false,
val tagsLock: Boolean = false,
val bookId: String = "",
@ -33,27 +29,22 @@ class BookMetadata(
val title = title.trim()
val summary = summary.trim()
val number = number.trim()
val publisher = publisher.trim()
fun copy(
title: String = this.title,
summary: String = this.summary,
number: String = this.number,
numberSort: Float = this.numberSort,
readingDirection: ReadingDirection? = this.readingDirection,
publisher: String = this.publisher,
ageRating: Int? = this.ageRating,
releaseDate: LocalDate? = this.releaseDate,
authors: List<Author> = this.authors.toList(),
tags: Set<String> = this.tags,
titleLock: Boolean = this.titleLock,
summaryLock: Boolean = this.summaryLock,
numberLock: Boolean = this.numberLock,
numberSortLock: Boolean = this.numberSortLock,
readingDirectionLock: Boolean = this.readingDirectionLock,
publisherLock: Boolean = this.publisherLock,
ageRatingLock: Boolean = this.ageRatingLock,
releaseDateLock: Boolean = this.releaseDateLock,
authorsLock: Boolean = this.authorsLock,
tagsLock: Boolean = this.tagsLock,
bookId: String = this.bookId,
createdDate: LocalDateTime = this.createdDate,
lastModifiedDate: LocalDateTime = this.lastModifiedDate
@ -63,32 +54,21 @@ class BookMetadata(
summary = summary,
number = number,
numberSort = numberSort,
readingDirection = readingDirection,
publisher = publisher,
ageRating = ageRating,
releaseDate = releaseDate,
authors = authors,
tags = tags,
titleLock = titleLock,
summaryLock = summaryLock,
numberLock = numberLock,
numberSortLock = numberSortLock,
readingDirectionLock = readingDirectionLock,
publisherLock = publisherLock,
ageRatingLock = ageRatingLock,
releaseDateLock = releaseDateLock,
authorsLock = authorsLock,
tagsLock = tagsLock,
bookId = bookId,
createdDate = createdDate,
lastModifiedDate = lastModifiedDate
)
enum class ReadingDirection {
LEFT_TO_RIGHT,
RIGHT_TO_LEFT,
VERTICAL,
WEBTOON
}
override fun toString(): String =
"BookMetadata(numberSort=$numberSort, readingDirection=$readingDirection, ageRating=$ageRating, releaseDate=$releaseDate, authors=$authors, titleLock=$titleLock, summaryLock=$summaryLock, numberLock=$numberLock, numberSortLock=$numberSortLock, readingDirectionLock=$readingDirectionLock, publisherLock=$publisherLock, ageRatingLock=$ageRatingLock, releaseDateLock=$releaseDateLock, authorsLock=$authorsLock, bookId=$bookId, createdDate=$createdDate, lastModifiedDate=$lastModifiedDate, title='$title', summary='$summary', number='$number', publisher='$publisher')"
"BookMetadata(numberSort=$numberSort, releaseDate=$releaseDate, authors=$authors, titleLock=$titleLock, summaryLock=$summaryLock, numberLock=$numberLock, numberSortLock=$numberSortLock, releaseDateLock=$releaseDateLock, authorsLock=$authorsLock, bookId=$bookId, createdDate=$createdDate, lastModifiedDate=$lastModifiedDate, title='$title', summary='$summary', number='$number')"
}

View file

@ -7,9 +7,6 @@ data class BookMetadataPatch(
val summary: String?,
val number: String?,
val numberSort: Float?,
val readingDirection: BookMetadata.ReadingDirection?,
val publisher: String?,
val ageRating: Int?,
val releaseDate: LocalDate?,
val authors: List<Author>?,

View file

@ -6,10 +6,24 @@ class SeriesMetadata(
val status: Status = Status.ONGOING,
title: String,
titleSort: String = title,
summary: String = "",
val readingDirection: ReadingDirection? = null,
publisher: String = "",
val ageRating: Int? = null,
val language: String = "",
val genres: Set<String> = emptySet(),
val tags: Set<String> = emptySet(),
val statusLock: Boolean = false,
val titleLock: Boolean = false,
val titleSortLock: Boolean = false,
val summaryLock: Boolean = false,
val readingDirectionLock: Boolean = false,
val publisherLock: Boolean = false,
val ageRatingLock: Boolean = false,
val languageLock: Boolean = false,
val genresLock: Boolean = false,
val tagsLock: Boolean = false,
val seriesId: String = "",
@ -18,14 +32,30 @@ class SeriesMetadata(
) : Auditable() {
val title = title.trim()
val titleSort = titleSort.trim()
val summary = summary.trim()
val publisher = publisher.trim()
fun copy(
status: Status = this.status,
title: String = this.title,
titleSort: String = this.titleSort,
summary: String = this.summary,
readingDirection: ReadingDirection? = this.readingDirection,
publisher: String = this.publisher,
ageRating: Int? = this.ageRating,
language: String = this.language,
genres: Set<String> = this.genres,
tags: Set<String> = this.tags,
statusLock: Boolean = this.statusLock,
titleLock: Boolean = this.titleLock,
titleSortLock: Boolean = this.titleSortLock,
summaryLock: Boolean = this.summaryLock,
readingDirectionLock: Boolean = this.readingDirectionLock,
publisherLock: Boolean = this.publisherLock,
ageRatingLock: Boolean = this.ageRatingLock,
languageLock: Boolean = this.languageLock,
genresLock: Boolean = this.genresLock,
tagsLock: Boolean = this.tagsLock,
seriesId: String = this.seriesId,
createdDate: LocalDateTime = this.createdDate,
lastModifiedDate: LocalDateTime = this.lastModifiedDate
@ -34,9 +64,23 @@ class SeriesMetadata(
status = status,
title = title,
titleSort = titleSort,
summary = summary,
readingDirection = readingDirection,
publisher = publisher,
ageRating = ageRating,
language = language,
genres = genres,
tags = tags,
statusLock = statusLock,
titleLock = titleLock,
titleSortLock = titleSortLock,
summaryLock = summaryLock,
readingDirectionLock = readingDirectionLock,
publisherLock = publisherLock,
ageRatingLock = ageRatingLock,
languageLock = languageLock,
genresLock = genresLock,
tagsLock = tagsLock,
seriesId = seriesId,
createdDate = createdDate,
lastModifiedDate = lastModifiedDate
@ -46,6 +90,14 @@ class SeriesMetadata(
ENDED, ONGOING, ABANDONED, HIATUS
}
override fun toString(): String =
"SeriesMetadata(status=$status, statusLock=$statusLock, titleLock=$titleLock, titleSortLock=$titleSortLock, seriesId=$seriesId, createdDate=$createdDate, lastModifiedDate=$lastModifiedDate, title='$title', titleSort='$titleSort')"
enum class ReadingDirection {
LEFT_TO_RIGHT,
RIGHT_TO_LEFT,
VERTICAL,
WEBTOON
}
override fun toString(): String {
return "SeriesMetadata(status=$status, readingDirection=$readingDirection, ageRating=$ageRating, language=$language, genres=$genres, statusLock=$statusLock, titleLock=$titleLock, titleSortLock=$titleSortLock, readingDirectionLock=$readingDirectionLock, publisherLock=$publisherLock, ageRatingLock=$ageRatingLock, languageLock=$languageLock, genresLock=$genresLock, seriesId='$seriesId', createdDate=$createdDate, lastModifiedDate=$lastModifiedDate, title='$title', titleSort='$titleSort', summary='$summary', publisher='$publisher')"
}
}

View file

@ -4,6 +4,12 @@ data class SeriesMetadataPatch(
val title: String?,
val titleSort: String?,
val status: SeriesMetadata.Status?,
val summary: String?,
val readingDirection: SeriesMetadata.ReadingDirection?,
val publisher: String?,
val ageRating: Int?,
val language: String?,
val genres: Set<String>,
val collections: List<String> = emptyList()
val collections: List<String>
)

View file

@ -7,8 +7,6 @@ interface BookMetadataRepository {
fun findByIdOrNull(bookId: String): BookMetadata?
fun findByIds(bookIds: Collection<String>): Collection<BookMetadata>
fun findAuthorsByName(search: String): List<String>
fun insert(metadata: BookMetadata)
fun insertMany(metadatas: Collection<BookMetadata>)
fun update(metadata: BookMetadata)

View file

@ -0,0 +1,7 @@
package org.gotson.komga.domain.persistence
interface ReferentialRepository {
fun findAuthorsByName(search: String): List<String>
fun findAllGenres(): Set<String>
fun findAllTags(): Set<String>
}

View file

@ -6,10 +6,11 @@ interface SeriesMetadataRepository {
fun findById(seriesId: String): SeriesMetadata
fun findByIdOrNull(seriesId: String): SeriesMetadata?
fun insert(metadata: SeriesMetadata): SeriesMetadata
fun insert(metadata: SeriesMetadata)
fun update(metadata: SeriesMetadata)
fun delete(seriesId: String)
fun delete(seriesIds: Collection<String>)
fun count(): Long
}

View file

@ -23,10 +23,7 @@ class MetadataApplier {
summary = getIfNotLocked(summary, patch.summary, summaryLock),
number = getIfNotLocked(number, patch.number, numberLock),
numberSort = getIfNotLocked(numberSort, patch.numberSort, numberSortLock),
readingDirection = getIfNotLocked(readingDirection, patch.readingDirection, readingDirectionLock),
releaseDate = getIfNotLocked(releaseDate, patch.releaseDate, releaseDateLock),
ageRating = getIfNotLocked(ageRating, patch.ageRating, ageRatingLock),
publisher = getIfNotLocked(publisher, patch.publisher, publisherLock),
authors = getIfNotLocked(authors, patch.authors, authorsLock)
)
}
@ -36,7 +33,13 @@ class MetadataApplier {
copy(
status = getIfNotLocked(status, patch.status, statusLock),
title = getIfNotLocked(title, patch.title, titleLock),
titleSort = getIfNotLocked(titleSort, patch.titleSort, titleSortLock)
titleSort = getIfNotLocked(titleSort, patch.titleSort, titleSortLock),
summary = getIfNotLocked(summary, patch.summary, summaryLock),
readingDirection = getIfNotLocked(readingDirection, patch.readingDirection, readingDirectionLock),
ageRating = getIfNotLocked(ageRating, patch.ageRating, ageRatingLock),
publisher = getIfNotLocked(publisher, patch.publisher, publisherLock),
language = getIfNotLocked(language, patch.language, languageLock),
genres = getIfNotLocked(genres, patch.genres, genresLock)
)
}

View file

@ -134,15 +134,19 @@ class MetadataLifecycle(
// handle series metadata
if ((provider is ComicInfoProvider && library.importComicInfoSeries) ||
(provider is EpubMetadataProvider && library.importEpubSeries)) {
val title = patches.uniqueOrNull { it.title }
val titleSort = patches.uniqueOrNull { it.titleSort }
val status = patches.uniqueOrNull { it.status }
if (title == null) logger.debug { "Ignoring title, values are not unique within series books" }
if (titleSort == null) logger.debug { "Ignoring sort title, values are not unique within series books" }
if (status == null) logger.debug { "Ignoring status, values are not unique within series books" }
val aggregatedPatch = SeriesMetadataPatch(title, titleSort, status)
val aggregatedPatch = SeriesMetadataPatch(
title = patches.mostFrequent { it.title },
titleSort = patches.mostFrequent { it.titleSort },
status = patches.mostFrequent { it.status },
genres = patches.flatMap { it.genres }.toSet(),
language = patches.mostFrequent { it.language },
summary = null,
readingDirection = patches.mostFrequent { it.readingDirection },
ageRating = patches.mapNotNull { it.ageRating }.max(),
publisher = patches.mostFrequent { it.publisher },
collections = emptyList()
)
seriesMetadataRepository.findById(series.id).let {
logger.debug { "Apply metadata for series: $series" }
@ -188,13 +192,12 @@ class MetadataLifecycle(
}
}
private fun <T, R : Any> Iterable<T>.uniqueOrNull(transform: (T) -> R?): R? {
private fun <T, R : Any> Iterable<T>.mostFrequent(transform: (T) -> R?): R? {
return this
.mapNotNull(transform)
.distinct()
.let {
if (it.size == 1) it.first() else null
}
.groupingBy { it }
.eachCount()
.maxBy { it.value }?.key
}
}

View file

@ -118,6 +118,7 @@ class SeriesLifecycle(
collectionRepository.removeSeriesFromAll(seriesId)
thumbnailsSeriesRepository.deleteBySeriesId(seriesId)
seriesMetadataRepository.delete(seriesId)
seriesRepository.delete(seriesId)
}
@ -130,6 +131,7 @@ class SeriesLifecycle(
collectionRepository.removeSeriesFromAll(seriesIds)
thumbnailsSeriesRepository.deleteBySeriesIds(seriesIds)
seriesMetadataRepository.delete(seriesIds)
seriesRepository.deleteAll(seriesIds)
}

View file

@ -40,6 +40,7 @@ class BookDtoDao(
private val a = Tables.BOOK_METADATA_AUTHOR
private val s = Tables.SERIES
private val rlb = Tables.READLIST_BOOK
private val bt = Tables.BOOK_METADATA_TAG
private val sorts = mapOf(
"name" to DSL.lower(b.NAME),
@ -194,7 +195,12 @@ class BookDtoDao(
.filter { it.name != null }
.map { AuthorDto(it.name, it.role) }
br.toDto(mr.toDto(), dr.toDto(authors), if (rr.userId != null) rr.toDto() else null)
val tags = dsl.select(bt.TAG)
.from(bt)
.where(bt.BOOK_ID.eq(br.id))
.fetchSet(bt.TAG)
br.toDto(mr.toDto(), dr.toDto(authors, tags), if (rr.userId != null) rr.toDto() else null)
}
private fun BookSearchWithReadProgress.toCondition(): Condition {
@ -245,7 +251,7 @@ class BookDtoDao(
comment = comment ?: ""
)
private fun BookMetadataRecord.toDto(authors: List<AuthorDto>) =
private fun BookMetadataRecord.toDto(authors: List<AuthorDto>, tags: Set<String>) =
BookMetadataDto(
title = title,
titleLock = titleLock,
@ -255,16 +261,14 @@ class BookDtoDao(
numberLock = numberLock,
numberSort = numberSort,
numberSortLock = numberSortLock,
readingDirection = readingDirection ?: "",
readingDirectionLock = readingDirectionLock,
publisher = publisher,
publisherLock = publisherLock,
ageRating = ageRating,
ageRatingLock = ageRatingLock,
releaseDate = releaseDate,
releaseDateLock = releaseDateLock,
authors = authors,
authorsLock = authorsLock
authorsLock = authorsLock,
tags = tags,
tagsLock = tagsLock,
created = createdDate,
lastModified = lastModifiedDate
)
private fun ReadProgressRecord.toDto() =

View file

@ -7,6 +7,7 @@ import org.gotson.komga.jooq.Tables
import org.gotson.komga.jooq.tables.records.BookMetadataAuthorRecord
import org.gotson.komga.jooq.tables.records.BookMetadataRecord
import org.jooq.DSLContext
import org.jooq.impl.DSL
import org.springframework.stereotype.Component
import java.time.LocalDateTime
import java.time.ZoneId
@ -18,6 +19,7 @@ class BookMetadataDao(
private val d = Tables.BOOK_METADATA
private val a = Tables.BOOK_METADATA_AUTHOR
private val bt = Tables.BOOK_METADATA_TAG
private val groupFields = arrayOf(*d.fields(), *a.fields())
@ -39,16 +41,16 @@ class BookMetadataDao(
.fetchGroups(
{ it.into(d) }, { it.into(a) }
).map { (dr, ar) ->
dr.toDomain(ar.filterNot { it.name == null }.map { it.toDomain() })
dr.toDomain(ar.filterNot { it.name == null }.map { it.toDomain() }, findTags(dr.bookId))
}
override fun findAuthorsByName(search: String): List<String> {
return dsl.selectDistinct(a.NAME)
.from(a)
.where(a.NAME.containsIgnoreCase(search))
.orderBy(a.NAME)
.fetch(a.NAME)
}
private fun findTags(bookId: String) =
dsl.select(bt.TAG)
.from(bt)
.where(bt.BOOK_ID.eq(bookId))
.fetchInto(bt)
.mapNotNull { it.tag }
.toSet()
override fun insert(metadata: BookMetadata) {
insertMany(listOf(metadata))
@ -69,16 +71,11 @@ class BookMetadataDao(
d.NUMBER_LOCK,
d.NUMBER_SORT,
d.NUMBER_SORT_LOCK,
d.READING_DIRECTION,
d.READING_DIRECTION_LOCK,
d.PUBLISHER,
d.PUBLISHER_LOCK,
d.AGE_RATING,
d.AGE_RATING_LOCK,
d.RELEASE_DATE,
d.RELEASE_DATE_LOCK,
d.AUTHORS_LOCK
).values(null as String?, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null)
d.AUTHORS_LOCK,
d.TAGS_LOCK
).values(null as String?, null, null, null, null, null, null, null, null, null, null, null, null)
).also { step ->
metadatas.forEach {
step.bind(
@ -91,20 +88,16 @@ class BookMetadataDao(
it.numberLock,
it.numberSort,
it.numberSortLock,
it.readingDirection?.toString(),
it.readingDirectionLock,
it.publisher,
it.publisherLock,
it.ageRating,
it.ageRatingLock,
it.releaseDate,
it.releaseDateLock,
it.authorsLock
it.authorsLock,
it.tagsLock
)
}
}.execute()
insertAuthors(config.dsl(), metadatas)
insertTags(config.dsl(), metadatas)
}
}
}
@ -122,33 +115,36 @@ class BookMetadataDao(
}
private fun updateMetadata(dsl: DSLContext, metadata: BookMetadata) {
dsl.update(d)
.set(d.TITLE, metadata.title)
.set(d.TITLE_LOCK, metadata.titleLock)
.set(d.SUMMARY, metadata.summary)
.set(d.SUMMARY_LOCK, metadata.summaryLock)
.set(d.NUMBER, metadata.number)
.set(d.NUMBER_LOCK, metadata.numberLock)
.set(d.NUMBER_SORT, metadata.numberSort)
.set(d.NUMBER_SORT_LOCK, metadata.numberSortLock)
.set(d.READING_DIRECTION, metadata.readingDirection?.toString())
.set(d.READING_DIRECTION_LOCK, metadata.readingDirectionLock)
.set(d.PUBLISHER, metadata.publisher)
.set(d.PUBLISHER_LOCK, metadata.publisherLock)
.set(d.AGE_RATING, metadata.ageRating)
.set(d.AGE_RATING_LOCK, metadata.ageRatingLock)
.set(d.RELEASE_DATE, metadata.releaseDate)
.set(d.RELEASE_DATE_LOCK, metadata.releaseDateLock)
.set(d.AUTHORS_LOCK, metadata.authorsLock)
.set(d.LAST_MODIFIED_DATE, LocalDateTime.now(ZoneId.of("Z")))
.where(d.BOOK_ID.eq(metadata.bookId))
.execute()
dsl.transaction { config ->
with(config.dsl()) {
update(d)
.set(d.TITLE, metadata.title)
.set(d.TITLE_LOCK, metadata.titleLock)
.set(d.SUMMARY, metadata.summary)
.set(d.SUMMARY_LOCK, metadata.summaryLock)
.set(d.NUMBER, metadata.number)
.set(d.NUMBER_LOCK, metadata.numberLock)
.set(d.NUMBER_SORT, metadata.numberSort)
.set(d.NUMBER_SORT_LOCK, metadata.numberSortLock)
.set(d.RELEASE_DATE, metadata.releaseDate)
.set(d.RELEASE_DATE_LOCK, metadata.releaseDateLock)
.set(d.AUTHORS_LOCK, metadata.authorsLock)
.set(d.TAGS_LOCK, metadata.tagsLock)
.set(d.LAST_MODIFIED_DATE, LocalDateTime.now(ZoneId.of("Z")))
.where(d.BOOK_ID.eq(metadata.bookId))
.execute()
dsl.deleteFrom(a)
.where(a.BOOK_ID.eq(metadata.bookId))
.execute()
deleteFrom(a)
.where(a.BOOK_ID.eq(metadata.bookId))
.execute()
deleteFrom(bt)
.where(bt.BOOK_ID.eq(metadata.bookId))
.execute()
insertAuthors(dsl, listOf(metadata))
insertAuthors(this, listOf(metadata))
insertTags(config.dsl(), listOf(metadata))
}
}
}
private fun insertAuthors(dsl: DSLContext, metadatas: Collection<BookMetadata>) {
@ -166,10 +162,26 @@ class BookMetadataDao(
}
}
private fun insertTags(dsl: DSLContext, metadatas: Collection<BookMetadata>) {
if (metadatas.any { it.tags.isNotEmpty() }) {
dsl.batch(
dsl.insertInto(bt, bt.BOOK_ID, bt.TAG)
.values(null as String?, null)
).also { step ->
metadatas.forEach { metadata ->
metadata.tags.forEach {
step.bind(metadata.bookId, it)
}
}
}.execute()
}
}
override fun delete(bookId: String) {
dsl.transaction { config ->
with(config.dsl()) {
deleteFrom(a).where(a.BOOK_ID.eq(bookId)).execute()
deleteFrom(bt).where(bt.BOOK_ID.eq(bookId)).execute()
deleteFrom(d).where(d.BOOK_ID.eq(bookId)).execute()
}
}
@ -179,24 +191,21 @@ class BookMetadataDao(
dsl.transaction { config ->
with(config.dsl()) {
deleteFrom(a).where(a.BOOK_ID.`in`(bookIds)).execute()
deleteFrom(bt).where(bt.BOOK_ID.`in`(bookIds)).execute()
deleteFrom(d).where(d.BOOK_ID.`in`(bookIds)).execute()
}
}
}
private fun BookMetadataRecord.toDomain(authors: Collection<Author>) =
private fun BookMetadataRecord.toDomain(authors: List<Author>, tags: Set<String>) =
BookMetadata(
title = title,
summary = summary,
number = number,
numberSort = numberSort,
readingDirection = readingDirection?.let {
BookMetadata.ReadingDirection.valueOf(readingDirection)
},
publisher = publisher,
ageRating = ageRating,
releaseDate = releaseDate,
authors = authors.toMutableList(),
authors = authors,
tags = tags,
bookId = bookId,
@ -207,11 +216,9 @@ class BookMetadataDao(
summaryLock = summaryLock,
numberLock = numberLock,
numberSortLock = numberSortLock,
readingDirectionLock = readingDirectionLock,
publisherLock = publisherLock,
ageRatingLock = ageRatingLock,
releaseDateLock = releaseDateLock,
authorsLock = authorsLock
authorsLock = authorsLock,
tagsLock = tagsLock
)
private fun BookMetadataAuthorRecord.toDomain() =

View file

@ -0,0 +1,45 @@
package org.gotson.komga.infrastructure.jooq
import org.gotson.komga.domain.persistence.ReferentialRepository
import org.gotson.komga.jooq.Tables
import org.jooq.DSLContext
import org.jooq.impl.DSL
import org.jooq.impl.DSL.field
import org.jooq.impl.DSL.lower
import org.jooq.impl.DSL.one
import org.jooq.impl.DSL.select
import org.springframework.stereotype.Component
@Component
class ReferentialDao(
private val dsl: DSLContext
) : ReferentialRepository {
private val a = Tables.BOOK_METADATA_AUTHOR
private val g = Tables.SERIES_METADATA_GENRE
private val bt = Tables.BOOK_METADATA_TAG
private val st = Tables.SERIES_METADATA_TAG
override fun findAuthorsByName(search: String): List<String> =
dsl.selectDistinct(a.NAME)
.from(a)
.where(a.NAME.containsIgnoreCase(search))
.orderBy(a.NAME)
.fetch(a.NAME)
override fun findAllGenres(): Set<String> =
dsl.selectDistinct(g.GENRE)
.from(g)
.orderBy(lower(g.GENRE))
.fetchSet(g.GENRE)
override fun findAllTags(): Set<String> =
dsl.select(bt.TAG.`as`("tag"))
.from(bt)
.union(
select(st.TAG.`as`("tag")).from(st)
)
.fetchSet(0, String::class.java)
.sortedBy { it.toLowerCase() }
.toSet()
}

View file

@ -98,7 +98,6 @@ class SeriesDao(
dsl.transaction { config ->
with(config.dsl())
{
deleteFrom(d).where(d.SERIES_ID.eq(seriesId)).execute()
deleteFrom(s).where(s.ID.eq(seriesId)).execute()
}
}
@ -108,7 +107,6 @@ class SeriesDao(
dsl.transaction { config ->
with(config.dsl())
{
deleteFrom(d).execute()
deleteFrom(s).execute()
}
}
@ -118,7 +116,6 @@ class SeriesDao(
dsl.transaction { config ->
with(config.dsl())
{
deleteFrom(d).where(d.SERIES_ID.`in`(seriesIds)).execute()
deleteFrom(s).where(s.ID.`in`(seriesIds)).execute()
}
}

View file

@ -42,6 +42,8 @@ class SeriesDtoDao(
private val d = Tables.SERIES_METADATA
private val r = Tables.READ_PROGRESS
private val cs = Tables.COLLECTION_SERIES
private val g = Tables.SERIES_METADATA_GENRE
private val st = Tables.SERIES_METADATA_TAG
val countUnread: AggregateFunction<BigDecimal> = DSL.sum(DSL.`when`(r.COMPLETED.isNull, 1).otherwise(0))
val countRead: AggregateFunction<BigDecimal> = DSL.sum(DSL.`when`(r.COMPLETED.isTrue, 1).otherwise(0))
@ -161,7 +163,18 @@ class SeriesDtoDao(
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)
sr.toDto(booksCount, booksReadCount, booksUnreadCount, booksInProgressCount, dr.toDto())
val genres = dsl.select(g.GENRE)
.from(g)
.where(g.SERIES_ID.eq(sr.id))
.fetchSet(g.GENRE)
val tags = dsl.select(st.TAG)
.from(st)
.where(st.SERIES_ID.eq(sr.id))
.fetchSet(st.TAG)
sr.toDto(booksCount, booksReadCount, booksUnreadCount, booksInProgressCount, dr.toDto(genres, tags))
}
private fun SeriesSearchWithReadProgress.toCondition(): Condition {
@ -200,7 +213,7 @@ class SeriesDtoDao(
metadata = metadata
)
private fun SeriesMetadataRecord.toDto() =
private fun SeriesMetadataRecord.toDto(genres: Set<String>, tags: Set<String>) =
SeriesMetadataDto(
status = status,
statusLock = statusLock,
@ -209,7 +222,21 @@ class SeriesDtoDao(
title = title,
titleLock = titleLock,
titleSort = titleSort,
titleSortLock = titleSortLock
titleSortLock = titleSortLock,
summary = summary,
summaryLock = summaryLock,
readingDirection = readingDirection ?: "",
readingDirectionLock = readingDirectionLock,
publisher = publisher,
publisherLock = publisherLock,
ageRating = ageRating,
ageRatingLock = ageRatingLock,
language = language,
languageLock = languageLock,
genres = genres,
genresLock = genresLock,
tags = tags,
tagsLock = tagsLock
)
}

View file

@ -5,6 +5,7 @@ import org.gotson.komga.domain.persistence.SeriesMetadataRepository
import org.gotson.komga.jooq.Tables
import org.gotson.komga.jooq.tables.records.SeriesMetadataRecord
import org.jooq.DSLContext
import org.jooq.impl.DSL.lower
import org.springframework.stereotype.Component
import java.time.LocalDateTime
import java.time.ZoneId
@ -15,63 +16,180 @@ class SeriesMetadataDao(
) : SeriesMetadataRepository {
private val d = Tables.SERIES_METADATA
private val g = Tables.SERIES_METADATA_GENRE
private val st = Tables.SERIES_METADATA_TAG
override fun findById(seriesId: String): SeriesMetadata =
findOne(seriesId).toDomain()
findOne(seriesId).toDomain(findGenres(seriesId), findTags(seriesId))
override fun findByIdOrNull(seriesId: String): SeriesMetadata? =
findOne(seriesId)?.toDomain()
findOne(seriesId)?.toDomain(findGenres(seriesId), findTags(seriesId))
private fun findOne(seriesId: String) =
dsl.selectFrom(d)
.where(d.SERIES_ID.eq(seriesId))
.fetchOneInto(d)
override fun insert(metadata: SeriesMetadata): SeriesMetadata {
dsl.insertInto(d)
.set(d.SERIES_ID, metadata.seriesId)
.set(d.STATUS, metadata.status.toString())
.set(d.TITLE, metadata.title)
.set(d.TITLE_SORT, metadata.titleSort)
.set(d.STATUS_LOCK, metadata.statusLock)
.set(d.TITLE_LOCK, metadata.titleLock)
.set(d.TITLE_SORT_LOCK, metadata.titleSortLock)
.execute()
private fun findGenres(seriesId: String) =
dsl.select(g.GENRE)
.from(g)
.where(g.SERIES_ID.eq(seriesId))
.fetchInto(g)
.mapNotNull { it.genre }
.toSet()
return findById(metadata.seriesId)
private fun findTags(seriesId: String) =
dsl.select(st.TAG)
.from(st)
.where(st.SERIES_ID.eq(seriesId))
.fetchInto(st)
.mapNotNull { it.tag }
.toSet()
override fun insert(metadata: SeriesMetadata) {
dsl.transaction { config ->
config.dsl().insertInto(d)
.set(d.SERIES_ID, metadata.seriesId)
.set(d.STATUS, metadata.status.toString())
.set(d.TITLE, metadata.title)
.set(d.TITLE_SORT, metadata.titleSort)
.set(d.SUMMARY, metadata.summary)
.set(d.READING_DIRECTION, metadata.readingDirection?.toString())
.set(d.PUBLISHER, metadata.publisher)
.set(d.AGE_RATING, metadata.ageRating)
.set(d.LANGUAGE, metadata.language)
.set(d.STATUS_LOCK, metadata.statusLock)
.set(d.TITLE_LOCK, metadata.titleLock)
.set(d.TITLE_SORT_LOCK, metadata.titleSortLock)
.set(d.SUMMARY_LOCK, metadata.summaryLock)
.set(d.READING_DIRECTION_LOCK, metadata.readingDirectionLock)
.set(d.PUBLISHER_LOCK, metadata.publisherLock)
.set(d.AGE_RATING_LOCK, metadata.ageRatingLock)
.set(d.LANGUAGE_LOCK, metadata.languageLock)
.set(d.GENRES_LOCK, metadata.genresLock)
.set(d.TAGS_LOCK, metadata.tagsLock)
.execute()
insertGenres(config.dsl(), metadata)
insertTags(config.dsl(), metadata)
}
}
override fun update(metadata: SeriesMetadata) {
dsl.update(d)
.set(d.STATUS, metadata.status.toString())
.set(d.TITLE, metadata.title)
.set(d.TITLE_SORT, metadata.titleSort)
.set(d.STATUS_LOCK, metadata.statusLock)
.set(d.TITLE_LOCK, metadata.titleLock)
.set(d.TITLE_SORT_LOCK, metadata.titleSortLock)
.set(d.LAST_MODIFIED_DATE, LocalDateTime.now(ZoneId.of("Z")))
.where(d.SERIES_ID.eq(metadata.seriesId))
.execute()
dsl.transaction { config ->
config.dsl().update(d)
.set(d.STATUS, metadata.status.toString())
.set(d.TITLE, metadata.title)
.set(d.TITLE_SORT, metadata.titleSort)
.set(d.SUMMARY, metadata.summary)
.set(d.READING_DIRECTION, metadata.readingDirection?.toString())
.set(d.PUBLISHER, metadata.publisher)
.set(d.AGE_RATING, metadata.ageRating)
.set(d.LANGUAGE, metadata.language)
.set(d.STATUS_LOCK, metadata.statusLock)
.set(d.TITLE_LOCK, metadata.titleLock)
.set(d.TITLE_SORT_LOCK, metadata.titleSortLock)
.set(d.SUMMARY_LOCK, metadata.summaryLock)
.set(d.READING_DIRECTION_LOCK, metadata.readingDirectionLock)
.set(d.PUBLISHER_LOCK, metadata.publisherLock)
.set(d.AGE_RATING_LOCK, metadata.ageRatingLock)
.set(d.LANGUAGE_LOCK, metadata.languageLock)
.set(d.GENRES_LOCK, metadata.genresLock)
.set(d.TAGS_LOCK, metadata.tagsLock)
.set(d.LAST_MODIFIED_DATE, LocalDateTime.now(ZoneId.of("Z")))
.where(d.SERIES_ID.eq(metadata.seriesId))
.execute()
config.dsl().deleteFrom(g)
.where(g.SERIES_ID.eq(metadata.seriesId))
.execute()
config.dsl().deleteFrom(st)
.where(st.SERIES_ID.eq(metadata.seriesId))
.execute()
insertGenres(config.dsl(), metadata)
insertTags(config.dsl(), metadata)
}
}
private fun insertGenres(dsl: DSLContext, metadata: SeriesMetadata) {
if (metadata.genres.isNotEmpty()) {
dsl.batch(
dsl.insertInto(g, g.SERIES_ID, g.GENRE)
.values(null as String?, null)
).also { step ->
metadata.genres.forEach {
step.bind(metadata.seriesId, it)
}
}.execute()
}
}
private fun insertTags(dsl: DSLContext, metadata: SeriesMetadata) {
if (metadata.tags.isNotEmpty()) {
dsl.batch(
dsl.insertInto(st, st.SERIES_ID, st.TAG)
.values(null as String?, null)
).also { step ->
metadata.tags.forEach {
step.bind(metadata.seriesId, it)
}
}.execute()
}
}
override fun delete(seriesId: String) {
dsl.deleteFrom(d)
.where(d.SERIES_ID.eq(seriesId))
.execute()
dsl.transaction { config ->
with(config.dsl()) {
deleteFrom(g).where(g.SERIES_ID.eq(seriesId)).execute()
deleteFrom(st).where(st.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(g).where(g.SERIES_ID.`in`(seriesIds)).execute()
deleteFrom(st).where(st.SERIES_ID.`in`(seriesIds)).execute()
deleteFrom(d).where(d.SERIES_ID.`in`(seriesIds)).execute()
}
}
}
override fun count(): Long = dsl.fetchCount(d).toLong()
private fun SeriesMetadataRecord.toDomain() =
private fun SeriesMetadataRecord.toDomain(genres: Set<String>, tags: Set<String>) =
SeriesMetadata(
status = SeriesMetadata.Status.valueOf(status),
title = title,
titleSort = titleSort,
seriesId = seriesId,
summary = summary,
readingDirection = readingDirection?.let {
SeriesMetadata.ReadingDirection.valueOf(readingDirection)
},
publisher = publisher,
ageRating = ageRating,
language = language,
genres = genres,
tags = tags,
statusLock = statusLock,
titleLock = titleLock,
titleSortLock = titleSortLock,
summaryLock = summaryLock,
readingDirectionLock = readingDirectionLock,
publisherLock = publisherLock,
ageRatingLock = ageRatingLock,
languageLock = languageLock,
genresLock = genresLock,
tagsLock = tagsLock,
seriesId = seriesId,
createdDate = createdDate.toCurrentTimeZone(),
lastModifiedDate = lastModifiedDate.toCurrentTimeZone()
)

View file

@ -4,15 +4,16 @@ import com.fasterxml.jackson.dataformat.xml.XmlMapper
import mu.KotlinLogging
import org.gotson.komga.domain.model.Author
import org.gotson.komga.domain.model.Book
import org.gotson.komga.domain.model.BookMetadata
import org.gotson.komga.domain.model.BookMetadataPatch
import org.gotson.komga.domain.model.Media
import org.gotson.komga.domain.model.SeriesMetadata
import org.gotson.komga.domain.model.SeriesMetadataPatch
import org.gotson.komga.domain.service.BookAnalyzer
import org.gotson.komga.infrastructure.metadata.BookMetadataProvider
import org.gotson.komga.infrastructure.metadata.SeriesMetadataProvider
import org.gotson.komga.infrastructure.metadata.comicinfo.dto.ComicInfo
import org.gotson.komga.infrastructure.metadata.comicinfo.dto.Manga
import org.gotson.komga.infrastructure.validation.BCP47TagValidator
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import java.time.LocalDate
@ -42,20 +43,11 @@ class ComicInfoProvider(
comicInfo.coverArtist?.let { authors += it.splitWithRole("cover") }
comicInfo.editor?.let { authors += it.splitWithRole("editor") }
val readingDirection = when (comicInfo.manga) {
Manga.NO -> BookMetadata.ReadingDirection.LEFT_TO_RIGHT
Manga.YES_AND_RIGHT_TO_LEFT -> BookMetadata.ReadingDirection.RIGHT_TO_LEFT
else -> null
}
return BookMetadataPatch(
title = comicInfo.title,
summary = comicInfo.summary,
number = comicInfo.number,
numberSort = comicInfo.number?.toFloatOrNull(),
readingDirection = readingDirection,
publisher = comicInfo.publisher,
ageRating = comicInfo.ageRating?.ageRating,
releaseDate = releaseDate,
authors = authors.ifEmpty { null },
readList = comicInfo.alternateSeries ?: comicInfo.storyArc,
@ -67,11 +59,25 @@ class ComicInfoProvider(
override fun getSeriesMetadataFromBook(book: Book, media: Media): SeriesMetadataPatch? {
getComicInfo(book, media)?.let { comicInfo ->
val readingDirection = when (comicInfo.manga) {
Manga.NO -> SeriesMetadata.ReadingDirection.LEFT_TO_RIGHT
Manga.YES_AND_RIGHT_TO_LEFT -> SeriesMetadata.ReadingDirection.RIGHT_TO_LEFT
else -> null
}
val genres = comicInfo.genre?.split(',')?.map { it.trim() }?.toSet() ?: emptySet()
return SeriesMetadataPatch(
comicInfo.series,
comicInfo.series,
null,
listOfNotNull(comicInfo.seriesGroup)
title = comicInfo.series,
titleSort = comicInfo.series,
status = null,
summary = null,
readingDirection = readingDirection,
publisher = comicInfo.publisher,
ageRating = comicInfo.ageRating?.ageRating,
language = if(comicInfo.languageISO != null && BCP47TagValidator.isValid(comicInfo.languageISO!!)) comicInfo.languageISO else null,
genres = genres,
collections = listOfNotNull(comicInfo.seriesGroup)
)
}
return null

View file

@ -2,13 +2,14 @@ package org.gotson.komga.infrastructure.metadata.epub
import org.gotson.komga.domain.model.Author
import org.gotson.komga.domain.model.Book
import org.gotson.komga.domain.model.BookMetadata
import org.gotson.komga.domain.model.BookMetadataPatch
import org.gotson.komga.domain.model.Media
import org.gotson.komga.domain.model.SeriesMetadata
import org.gotson.komga.domain.model.SeriesMetadataPatch
import org.gotson.komga.infrastructure.mediacontainer.EpubExtractor
import org.gotson.komga.infrastructure.metadata.BookMetadataProvider
import org.gotson.komga.infrastructure.metadata.SeriesMetadataProvider
import org.gotson.komga.infrastructure.validation.BCP47TagValidator
import org.jsoup.Jsoup
import org.springframework.stereotype.Service
import java.time.LocalDate
@ -34,18 +35,9 @@ class EpubMetadataProvider(
val opf = Jsoup.parse(packageFile)
val title = opf.selectFirst("metadata > dc|title")?.text()
val publisher = opf.selectFirst("metadata > dc|publisher")?.text()
val description = opf.selectFirst("metadata > dc|description")?.text()
val date = opf.selectFirst("metadata > dc|date")?.text()?.let { parseDate(it) }
val direction = opf.getElementsByTag("spine").first().attr("page-progression-direction")?.let {
when (it) {
"rtl" -> BookMetadata.ReadingDirection.RIGHT_TO_LEFT
"ltr" -> BookMetadata.ReadingDirection.LEFT_TO_RIGHT
else -> null
}
}
val creatorRefines = opf.select("metadata > meta[property=role][scheme=marc:relators]")
.associate { it.attr("refines").removePrefix("#") to it.text() }
val authors = opf.select("metadata > dc|creator")
@ -63,9 +55,6 @@ class EpubMetadataProvider(
summary = description,
number = null,
numberSort = null,
readingDirection = direction,
publisher = publisher,
ageRating = null,
releaseDate = date,
authors = authors,
readList = null,
@ -81,8 +70,30 @@ class EpubMetadataProvider(
val opf = Jsoup.parse(packageFile)
val series = opf.selectFirst("metadata > meta[property=belongs-to-collection]")?.text()
val publisher = opf.selectFirst("metadata > dc|publisher")?.text()
val language = opf.selectFirst("metadata > dc|language")?.text()
val genre = opf.selectFirst("metadata > dc|subject")?.text()
return SeriesMetadataPatch(series, series, null)
val direction = opf.getElementsByTag("spine").first().attr("page-progression-direction")?.let {
when (it) {
"rtl" -> SeriesMetadata.ReadingDirection.RIGHT_TO_LEFT
"ltr" -> SeriesMetadata.ReadingDirection.LEFT_TO_RIGHT
else -> null
}
}
return SeriesMetadataPatch(
title = series,
titleSort = series,
status = null,
readingDirection = direction,
publisher = publisher,
ageRating = null,
summary = null,
language = if(language != null && BCP47TagValidator.isValid(language)) language else null,
genres = if(genre != null) setOf(genre) else emptySet(),
collections = emptyList()
)
}
return null
}

View file

@ -0,0 +1,35 @@
package org.gotson.komga.infrastructure.validation
import com.ibm.icu.util.ULocale
import javax.validation.Constraint
import javax.validation.ConstraintValidator
import javax.validation.ConstraintValidatorContext
import kotlin.reflect.KClass
@Constraint(validatedBy = [BCP47Validator::class])
@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.FIELD, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.ANNOTATION_CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class BCP47(
val message: String = "Must be a valid BCP 47 language tag",
val groups: Array<KClass<out Any>> = [],
val payload: Array<KClass<out Any>> = []
)
class BCP47Validator : ConstraintValidator<BCP47, String> {
override fun isValid(value: String?, context: ConstraintValidatorContext?): Boolean {
if (value == null) return false
return BCP47TagValidator.isValid(value)
}
}
object BCP47TagValidator {
val languages by lazy { ULocale.getISOLanguages().toSet() }
fun isValid(value: String): Boolean {
return ULocale.forLanguageTag(value).let {
it.language.isNotBlank() && languages.contains(it.language)
}
}
}

View file

@ -0,0 +1,25 @@
package org.gotson.komga.infrastructure.validation
import com.ibm.icu.util.ULocale
import javax.validation.Constraint
import javax.validation.ConstraintValidator
import javax.validation.ConstraintValidatorContext
import kotlin.reflect.KClass
@Constraint(validatedBy = [BlankValidator::class])
@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.FIELD, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.ANNOTATION_CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class Blank(
val message: String = "Must be blank",
val groups: Array<KClass<out Any>> = [],
val payload: Array<KClass<out Any>> = []
)
class BlankValidator : ConstraintValidator<Blank, String> {
override fun isValid(value: String?, context: ConstraintValidatorContext?): Boolean {
if (value == null) return false
return value.isBlank()
}
}

View file

@ -0,0 +1,22 @@
package org.gotson.komga.infrastructure.validation
import org.hibernate.validator.constraints.CompositionType
import org.hibernate.validator.constraints.ConstraintComposition
import javax.validation.Constraint
import javax.validation.constraints.NotBlank
import javax.validation.constraints.Null
import kotlin.reflect.KClass
@ConstraintComposition(CompositionType.OR)
@Constraint(validatedBy = [])
@Null
@Blank
@BCP47
@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.FIELD, AnnotationTarget.PROPERTY_GETTER)
@Retention(AnnotationRetention.RUNTIME)
annotation class NullOrBlankOrBCP47(
val message: String = "Must be null or blank or valid BCP 47 language tag",
val groups: Array<KClass<out Any>> = [],
val payload: Array<KClass<out Any>> = []
)

View file

@ -422,18 +422,16 @@ class BookController(
numberLock = numberLock ?: existing.numberLock,
numberSort = numberSort ?: existing.numberSort,
numberSortLock = numberSortLock ?: existing.numberSortLock,
readingDirection = if (isSet("readingDirection")) readingDirection else existing.readingDirection,
readingDirectionLock = readingDirectionLock ?: existing.readingDirectionLock,
publisher = publisher ?: existing.publisher,
publisherLock = publisherLock ?: existing.publisherLock,
ageRating = if (isSet("ageRating")) ageRating else existing.ageRating,
ageRatingLock = ageRatingLock ?: existing.ageRatingLock,
releaseDate = if (isSet("releaseDate")) releaseDate else existing.releaseDate,
releaseDateLock = releaseDateLock ?: existing.releaseDateLock,
authors = if (isSet("authors")) {
if (authors != null) authors!!.map { Author(it.name ?: "", it.role ?: "") } else emptyList()
} else existing.authors,
authorsLock = authorsLock ?: existing.authorsLock
authorsLock = authorsLock ?: existing.authorsLock,
tags = if(isSet("tags")) {
if(tags != null) tags!! else emptySet()
} else existing.tags,
tagsLock = tagsLock ?: existing.tagsLock
)
}
bookMetadataRepository.update(updated)

View file

@ -1,6 +1,6 @@
package org.gotson.komga.interfaces.rest
import org.gotson.komga.domain.persistence.BookMetadataRepository
import org.gotson.komga.domain.persistence.ReferentialRepository
import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
@ -11,12 +11,20 @@ import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("api/v1", produces = [MediaType.APPLICATION_JSON_VALUE])
class ReferentialController(
private val bookMetadataRepository: BookMetadataRepository
private val referentialRepository: ReferentialRepository
) {
@GetMapping("/authors")
fun getAuthors(
@RequestParam(name = "search", defaultValue = "") search: String
): List<String> =
bookMetadataRepository.findAuthorsByName(search)
referentialRepository.findAuthorsByName(search)
@GetMapping("/genres")
fun getGenres(): Set<String> =
referentialRepository.findAllGenres()
@GetMapping("/tags")
fun getTags(): Set<String> =
referentialRepository.findAllTags()
}

View file

@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.responses.ApiResponse
import mu.KotlinLogging
import org.gotson.komga.application.tasks.TaskReceiver
import org.gotson.komga.domain.model.Author
import org.gotson.komga.domain.model.BookSearchWithReadProgress
import org.gotson.komga.domain.model.Media
import org.gotson.komga.domain.model.ROLE_ADMIN
@ -286,7 +287,25 @@ class SeriesController(
title = title ?: existing.title,
titleLock = titleLock ?: existing.titleLock,
titleSort = titleSort ?: existing.titleSort,
titleSortLock = titleSortLock ?: existing.titleSortLock
titleSortLock = titleSortLock ?: existing.titleSortLock,
summary = summary ?: existing.summary,
summaryLock = summaryLock ?: existing.summaryLock,
language = language ?: existing.language,
languageLock = languageLock ?: existing.languageLock,
readingDirection = if (isSet("readingDirection")) readingDirection else existing.readingDirection,
readingDirectionLock = readingDirectionLock ?: existing.readingDirectionLock,
publisher = publisher ?: existing.publisher,
publisherLock = publisherLock ?: existing.publisherLock,
ageRating = if (isSet("ageRating")) ageRating else existing.ageRating,
ageRatingLock = ageRatingLock ?: existing.ageRatingLock,
genres = if (isSet("genres")) {
if (genres != null) genres!! else emptySet()
} else existing.genres,
genresLock = genresLock ?: existing.genresLock,
tags = if(isSet("tags")) {
if(tags != null) tags!! else emptySet()
} else existing.tags,
tagsLock = tagsLock ?: existing.tagsLock
)
}
seriesMetadataRepository.update(updated)

View file

@ -45,17 +45,18 @@ data class BookMetadataDto(
val numberLock: Boolean,
val numberSort: Float,
val numberSortLock: Boolean,
val readingDirection: String,
val readingDirectionLock: Boolean,
val publisher: String,
val publisherLock: Boolean,
val ageRating: Int?,
val ageRatingLock: Boolean,
@JsonFormat(pattern = "yyyy-MM-dd")
val releaseDate: LocalDate?,
val releaseDateLock: Boolean,
val authors: List<AuthorDto>,
val authorsLock: Boolean
val authorsLock: Boolean,
val tags: Set<String>,
val tagsLock: Boolean,
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
val created: LocalDateTime,
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
val lastModified: LocalDateTime
)
data class AuthorDto(

View file

@ -1,6 +1,6 @@
package org.gotson.komga.interfaces.rest.dto
import org.gotson.komga.domain.model.BookMetadata
import org.gotson.komga.domain.model.SeriesMetadata
import org.gotson.komga.infrastructure.validation.NullOrNotBlank
import java.time.LocalDate
import javax.validation.Valid
@ -30,25 +30,6 @@ class BookMetadataUpdateDto {
var numberSortLock: Boolean? = null
var readingDirection: BookMetadata.ReadingDirection?
by Delegates.observable<BookMetadata.ReadingDirection?>(null) { prop, _, _ ->
isSet[prop.name] = true
}
var readingDirectionLock: Boolean? = null
var publisher: String? = null
var publisherLock: Boolean? = null
@get:PositiveOrZero
var ageRating: Int?
by Delegates.observable<Int?>(null) { prop, _, _ ->
isSet[prop.name] = true
}
var ageRatingLock: Boolean? = null
var releaseDate: LocalDate?
by Delegates.observable<LocalDate?>(null) { prop, _, _ ->
isSet[prop.name] = true
@ -63,6 +44,13 @@ class BookMetadataUpdateDto {
}
var authorsLock: Boolean? = null
var tags: Set<String>?
by Delegates.observable<Set<String>?>(null) { prop, _, _ ->
isSet[prop.name] = true
}
var tagsLock: Boolean? = null
}
class AuthorUpdateDto {

View file

@ -27,12 +27,27 @@ fun SeriesDto.restrictUrl(restrict: Boolean) =
data class SeriesMetadataDto(
val status: String,
val statusLock: Boolean,
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
val created: LocalDateTime,
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
val lastModified: LocalDateTime,
val title: String,
val titleLock: Boolean,
val titleSort: String,
val titleSortLock: Boolean
val titleSortLock: Boolean,
val summary: String,
val summaryLock: Boolean,
val readingDirection: String,
val readingDirectionLock: Boolean,
val publisher: String,
val publisherLock: Boolean,
val ageRating: Int?,
val ageRatingLock: Boolean,
val language: String,
val languageLock: Boolean,
val genres: Set<String>,
val genresLock: Boolean,
val tags: Set<String>,
val tagsLock: Boolean,
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
val created: LocalDateTime,
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
val lastModified: LocalDateTime
)

View file

@ -1,15 +1,69 @@
package org.gotson.komga.interfaces.rest.dto
import org.gotson.komga.domain.model.SeriesMetadata
import org.gotson.komga.infrastructure.validation.NullOrBlankOrBCP47
import org.gotson.komga.infrastructure.validation.NullOrNotBlank
import javax.validation.Valid
import javax.validation.constraints.PositiveOrZero
import kotlin.properties.Delegates
class SeriesMetadataUpdateDto {
private val isSet = mutableMapOf<String, Boolean>()
fun isSet(prop: String) = isSet.getOrDefault(prop, false)
val status: SeriesMetadata.Status? = null
val statusLock: Boolean? = null
data class SeriesMetadataUpdateDto(
val status: SeriesMetadata.Status?,
val statusLock: Boolean?,
@get:NullOrNotBlank
val title: String?,
val titleLock: Boolean?,
val title: String? = null
val titleLock: Boolean? = null
@get:NullOrNotBlank
val titleSort: String?,
val titleSortLock: Boolean?
)
val titleSort: String? = null
val titleSortLock: Boolean? = null
var summary: String? = null
var summaryLock: Boolean? = null
var publisher: String? = null
var publisherLock: Boolean? = null
var readingDirection: SeriesMetadata.ReadingDirection?
by Delegates.observable<SeriesMetadata.ReadingDirection?>(null) { prop, _, _ ->
isSet[prop.name] = true
}
var readingDirectionLock: Boolean? = null
@get:PositiveOrZero
var ageRating: Int?
by Delegates.observable<Int?>(null) { prop, _, _ ->
isSet[prop.name] = true
}
var ageRatingLock: Boolean? = null
@get:NullOrBlankOrBCP47
var language: String? = null
var languageLock: Boolean? = null
var genres: Set<String>?
by Delegates.observable<Set<String>?>(null) { prop, _, _ ->
isSet[prop.name] = true
}
var genresLock: Boolean? = null
var tags: Set<String>?
by Delegates.observable<Set<String>?>(null) { prop, _, _ ->
isSet[prop.name] = true
}
var tagsLock: Boolean? = null
}

View file

@ -4,6 +4,7 @@ import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.catchThrowable
import org.gotson.komga.domain.model.Author
import org.gotson.komga.domain.model.BookMetadata
import org.gotson.komga.domain.model.SeriesMetadata
import org.gotson.komga.domain.model.makeBook
import org.gotson.komga.domain.model.makeLibrary
import org.gotson.komga.domain.model.makeSeries
@ -64,21 +65,17 @@ class BookMetadataDaoTest(
summary = "Summary",
number = "1",
numberSort = 1F,
readingDirection = BookMetadata.ReadingDirection.LEFT_TO_RIGHT,
publisher = "publisher",
ageRating = 18,
releaseDate = LocalDate.now(),
authors = mutableListOf(Author("author", "role")),
authors = listOf(Author("author", "role")),
tags = setOf("tag", "another"),
bookId = book.id,
titleLock = true,
summaryLock = true,
numberLock = true,
numberSortLock = true,
readingDirectionLock = true,
publisherLock = true,
ageRatingLock = true,
releaseDateLock = true,
authorsLock = true
authorsLock = true,
tagsLock = true
)
bookMetadataDao.insert(metadata)
@ -92,25 +89,21 @@ class BookMetadataDaoTest(
assertThat(created.summary).isEqualTo(metadata.summary)
assertThat(created.number).isEqualTo(metadata.number)
assertThat(created.numberSort).isEqualTo(metadata.numberSort)
assertThat(created.readingDirection).isEqualTo(metadata.readingDirection)
assertThat(created.publisher).isEqualTo(metadata.publisher)
assertThat(created.ageRating).isEqualTo(metadata.ageRating)
assertThat(created.releaseDate).isEqualTo(metadata.releaseDate)
assertThat(created.authors).hasSize(1)
with(created.authors.first()) {
assertThat(name).isEqualTo(metadata.authors.first().name)
assertThat(role).isEqualTo(metadata.authors.first().role)
}
assertThat(created.tags).containsAll(metadata.tags)
assertThat(created.titleLock).isEqualTo(metadata.titleLock)
assertThat(created.summaryLock).isEqualTo(metadata.summaryLock)
assertThat(created.numberLock).isEqualTo(metadata.numberLock)
assertThat(created.numberSortLock).isEqualTo(metadata.numberSortLock)
assertThat(created.readingDirectionLock).isEqualTo(metadata.readingDirectionLock)
assertThat(created.publisherLock).isEqualTo(metadata.publisherLock)
assertThat(created.ageRatingLock).isEqualTo(metadata.ageRatingLock)
assertThat(created.releaseDateLock).isEqualTo(metadata.releaseDateLock)
assertThat(created.authorsLock).isEqualTo(metadata.authorsLock)
assertThat(created.tagsLock).isEqualTo(metadata.tagsLock)
}
@Test
@ -131,21 +124,17 @@ class BookMetadataDaoTest(
assertThat(created.summary).isBlank()
assertThat(created.number).isEqualTo(metadata.number)
assertThat(created.numberSort).isEqualTo(metadata.numberSort)
assertThat(created.readingDirection).isNull()
assertThat(created.publisher).isBlank()
assertThat(created.ageRating).isNull()
assertThat(created.releaseDate).isNull()
assertThat(created.authors).isEmpty()
assertThat(created.tags).isEmpty()
assertThat(created.titleLock).isFalse()
assertThat(created.summaryLock).isFalse()
assertThat(created.numberLock).isFalse()
assertThat(created.numberSortLock).isFalse()
assertThat(created.readingDirectionLock).isFalse()
assertThat(created.publisherLock).isFalse()
assertThat(created.ageRatingLock).isFalse()
assertThat(created.releaseDateLock).isFalse()
assertThat(created.authorsLock).isFalse()
assertThat(created.tagsLock).isFalse()
}
@Test
@ -155,11 +144,9 @@ class BookMetadataDaoTest(
summary = "Summary",
number = "1",
numberSort = 1F,
readingDirection = BookMetadata.ReadingDirection.LEFT_TO_RIGHT,
publisher = "publisher",
ageRating = 18,
releaseDate = LocalDate.now(),
authors = mutableListOf(Author("author", "role")),
authors = listOf(Author("author", "role")),
tags = setOf("tag"),
bookId = book.id
)
bookMetadataDao.insert(metadata)
@ -171,20 +158,16 @@ class BookMetadataDaoTest(
summary = "SummaryUpdated",
number = "2",
numberSort = 2F,
readingDirection = BookMetadata.ReadingDirection.RIGHT_TO_LEFT,
publisher = "publisher2",
ageRating = 15,
releaseDate = LocalDate.now(),
authors = mutableListOf(Author("author2", "role2")),
authors = listOf(Author("author2", "role2")),
tags = setOf("another"),
titleLock = true,
summaryLock = true,
numberLock = true,
numberSortLock = true,
readingDirectionLock = true,
publisherLock = true,
ageRatingLock = true,
releaseDateLock = true,
authorsLock = true
authorsLock = true,
tagsLock = true
)
}
@ -201,20 +184,16 @@ class BookMetadataDaoTest(
assertThat(modified.summary).isEqualTo(updated.summary)
assertThat(modified.number).isEqualTo(updated.number)
assertThat(modified.numberSort).isEqualTo(updated.numberSort)
assertThat(modified.readingDirection).isEqualTo(updated.readingDirection)
assertThat(modified.publisher).isEqualTo(updated.publisher)
assertThat(modified.ageRating).isEqualTo(updated.ageRating)
assertThat(modified.titleLock).isEqualTo(updated.titleLock)
assertThat(modified.summaryLock).isEqualTo(updated.summaryLock)
assertThat(modified.numberLock).isEqualTo(updated.numberLock)
assertThat(modified.numberSortLock).isEqualTo(updated.numberSortLock)
assertThat(modified.readingDirectionLock).isEqualTo(updated.readingDirectionLock)
assertThat(modified.publisherLock).isEqualTo(updated.publisherLock)
assertThat(modified.ageRatingLock).isEqualTo(updated.ageRatingLock)
assertThat(modified.releaseDateLock).isEqualTo(updated.releaseDateLock)
assertThat(modified.authorsLock).isEqualTo(updated.authorsLock)
assertThat(modified.tagsLock).isEqualTo(updated.tagsLock)
assertThat(modified.tags).containsAll(updated.tags)
assertThat(modified.authors.first().name).isEqualTo(updated.authors.first().name)
assertThat(modified.authors.first().role).isEqualTo(updated.authors.first().role)
}
@ -226,11 +205,8 @@ class BookMetadataDaoTest(
summary = "Summary",
number = "1",
numberSort = 1F,
readingDirection = BookMetadata.ReadingDirection.LEFT_TO_RIGHT,
publisher = "publisher",
ageRating = 18,
releaseDate = LocalDate.now(),
authors = mutableListOf(Author("author", "role")),
authors = listOf(Author("author", "role")),
bookId = book.id
)
bookMetadataDao.insert(metadata)

View file

@ -34,6 +34,9 @@ class SeriesMetadataDaoTest(
@AfterEach
fun deleteSeries() {
seriesRepository.findAll().forEach {
seriesMetadataDao.delete(it.id)
}
seriesRepository.deleteAll()
}
@ -52,20 +55,93 @@ class SeriesMetadataDaoTest(
status = SeriesMetadata.Status.ENDED,
title = "Series",
titleSort = "Series, The",
summary = "Summary",
readingDirection = SeriesMetadata.ReadingDirection.LEFT_TO_RIGHT,
publisher = "publisher",
ageRating = 18,
genres = setOf("Action", "Adventure"),
tags = setOf("tag","another"),
language = "en",
titleLock = true,
titleSortLock = true,
summaryLock = true,
readingDirectionLock = true,
publisherLock = true,
ageRatingLock = true,
genresLock = true,
languageLock = true,
tagsLock = true,
seriesId = series.id
)
val created = seriesMetadataDao.insert(metadata)
seriesMetadataDao.insert(metadata)
val created = seriesMetadataDao.findById(metadata.seriesId)
assertThat(created.seriesId).isEqualTo(series.id)
assertThat(created.createdDate).isCloseTo(now, offset)
assertThat(created.lastModifiedDate).isCloseTo(now, offset)
assertThat(created.title).isEqualTo("Series")
assertThat(created.titleSort).isEqualTo("Series, The")
assertThat(created.title).isEqualTo(metadata.title)
assertThat(created.titleSort).isEqualTo(metadata.titleSort)
assertThat(created.summary).isEqualTo(metadata.summary)
assertThat(created.status).isEqualTo(SeriesMetadata.Status.ENDED)
assertThat(created.readingDirection).isEqualTo(metadata.readingDirection)
assertThat(created.publisher).isEqualTo(metadata.publisher)
assertThat(created.ageRating).isEqualTo(metadata.ageRating)
assertThat(created.language).isEqualTo(metadata.language)
assertThat(created.genres).containsAll(metadata.genres)
assertThat(created.tags).containsAll(metadata.tags)
assertThat(created.titleLock).isEqualTo(metadata.titleLock)
assertThat(created.titleSortLock).isEqualTo(metadata.titleSortLock)
assertThat(created.statusLock).isEqualTo(metadata.statusLock)
assertThat(created.summaryLock).isEqualTo(metadata.summaryLock)
assertThat(created.readingDirectionLock).isEqualTo(metadata.readingDirectionLock)
assertThat(created.publisherLock).isEqualTo(metadata.publisherLock)
assertThat(created.ageRatingLock).isEqualTo(metadata.ageRatingLock)
assertThat(created.genresLock).isEqualTo(metadata.genresLock)
assertThat(created.languageLock).isEqualTo(metadata.languageLock)
assertThat(created.tagsLock).isEqualTo(metadata.tagsLock)
}
@Test
fun `given a minimum seriesMetadata when inserting then it is persisted`() {
val series = makeSeries("Series", libraryId = library.id).also { seriesRepository.insert(it) }
val now = LocalDateTime.now()
val metadata = SeriesMetadata(
title = "Series",
seriesId = series.id
)
seriesMetadataDao.insert(metadata)
val created = seriesMetadataDao.findById(metadata.seriesId)
assertThat(created.seriesId).isEqualTo(series.id)
assertThat(created.createdDate).isCloseTo(now, offset)
assertThat(created.lastModifiedDate).isCloseTo(now, offset)
assertThat(created.title).isEqualTo(metadata.title)
assertThat(created.titleSort).isEqualTo(metadata.title)
assertThat(created.summary).isBlank()
assertThat(created.status).isEqualTo(SeriesMetadata.Status.ONGOING)
assertThat(created.readingDirection).isNull()
assertThat(created.publisher).isBlank()
assertThat(created.language).isBlank()
assertThat(created.ageRating).isNull()
assertThat(created.genres).isEmpty()
assertThat(created.tags).isEmpty()
assertThat(created.titleLock).isFalse()
assertThat(created.titleSortLock).isFalse()
assertThat(created.statusLock).isFalse()
assertThat(created.summaryLock).isFalse()
assertThat(created.readingDirectionLock).isFalse()
assertThat(created.publisherLock).isFalse()
assertThat(created.ageRatingLock).isFalse()
assertThat(created.genresLock).isFalse()
assertThat(created.languageLock).isFalse()
assertThat(created.tagsLock).isFalse()
}
@Test
@ -109,9 +185,17 @@ class SeriesMetadataDaoTest(
status = SeriesMetadata.Status.ENDED,
title = "Series",
titleSort = "Series, The",
summary = "Summary",
readingDirection = SeriesMetadata.ReadingDirection.LEFT_TO_RIGHT,
publisher = "publisher",
ageRating = 18,
language = "en",
genres = setOf("Action"),
tags = setOf("tag"),
seriesId = series.id
)
val created = seriesMetadataDao.insert(metadata)
seriesMetadataDao.insert(metadata)
val created = seriesMetadataDao.findById(metadata.seriesId)
val modificationDate = LocalDateTime.now()
@ -121,9 +205,23 @@ class SeriesMetadataDaoTest(
status = SeriesMetadata.Status.HIATUS,
title = "Changed",
titleSort = "Changed, The",
summary = "SummaryUpdated",
readingDirection = SeriesMetadata.ReadingDirection.RIGHT_TO_LEFT,
publisher = "publisher2",
ageRating = 15,
language = "jp",
genres = setOf("Adventure"),
tags = setOf("Another"),
statusLock = true,
titleLock = true,
titleSortLock = true
titleSortLock = true,
summaryLock = true,
readingDirectionLock = true,
publisherLock = true,
ageRatingLock = true,
languageLock = true,
genresLock = true,
tagsLock = true
)
}
@ -135,11 +233,26 @@ class SeriesMetadataDaoTest(
assertThat(modified.lastModifiedDate)
.isCloseTo(modificationDate, offset)
.isNotEqualTo(modified.createdDate)
assertThat(modified.title).isEqualTo("Changed")
assertThat(modified.titleSort).isEqualTo("Changed, The")
assertThat(modified.status).isEqualTo(SeriesMetadata.Status.HIATUS)
assertThat(modified.title).isEqualTo(updated.title)
assertThat(modified.titleSort).isEqualTo(updated.titleSort)
assertThat(modified.summary).isEqualTo(updated.summary)
assertThat(modified.status).isEqualTo(updated.status)
assertThat(modified.readingDirection).isEqualTo(updated.readingDirection)
assertThat(modified.publisher).isEqualTo(updated.publisher)
assertThat(modified.ageRating).isEqualTo(updated.ageRating)
assertThat(modified.language).isEqualTo(updated.language)
assertThat(modified.genres).containsAll(updated.genres)
assertThat(modified.tags).containsAll(updated.tags)
assertThat(modified.titleLock).isTrue()
assertThat(modified.titleSortLock).isTrue()
assertThat(modified.statusLock).isTrue()
assertThat(modified.summaryLock).isTrue()
assertThat(modified.readingDirectionLock).isTrue()
assertThat(modified.ageRatingLock).isTrue()
assertThat(modified.languageLock).isTrue()
assertThat(modified.genresLock).isTrue()
assertThat(modified.publisherLock).isTrue()
assertThat(modified.tagsLock).isTrue()
}
}

View file

@ -5,10 +5,12 @@ import io.mockk.every
import io.mockk.mockk
import org.assertj.core.api.Assertions.assertThat
import org.gotson.komga.domain.model.Media
import org.gotson.komga.domain.model.SeriesMetadata
import org.gotson.komga.domain.model.makeBook
import org.gotson.komga.domain.service.BookAnalyzer
import org.gotson.komga.infrastructure.metadata.comicinfo.dto.AgeRating
import org.gotson.komga.infrastructure.metadata.comicinfo.dto.ComicInfo
import org.gotson.komga.infrastructure.metadata.comicinfo.dto.Manga
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import java.time.LocalDate
@ -38,10 +40,10 @@ class ComicInfoProviderTest {
title = "title"
summary = "summary"
number = "010"
publisher = "publisher"
ageRating = AgeRating.MA_15
year = 2020
month = 2
alternateSeries = "story arc"
alternateNumber = "5"
}
every { mockMapper.readValue(any<ByteArray>(), ComicInfo::class.java) } returns comicInfo
@ -53,10 +55,9 @@ class ComicInfoProviderTest {
assertThat(summary).isEqualTo("summary")
assertThat(number).isEqualTo("010")
assertThat(numberSort).isEqualTo(10F)
assertThat(publisher).isEqualTo("publisher")
assertThat(ageRating).isEqualTo(15)
assertThat(readList).isEqualTo("story arc")
assertThat(readListNumber).isEqualTo(5)
assertThat(releaseDate).isEqualTo(LocalDate.of(2020, 2, 1))
assertThat(readingDirection).isNull()
}
}
@ -154,6 +155,11 @@ class ComicInfoProviderTest {
val comicInfo = ComicInfo().apply {
series = "series"
seriesGroup = "collection"
publisher = "publisher"
ageRating = AgeRating.MA_15
manga = Manga.YES_AND_RIGHT_TO_LEFT
languageISO = "en"
genre = "Action, Adventure"
}
every { mockMapper.readValue(any<ByteArray>(), ComicInfo::class.java) } returns comicInfo
@ -165,6 +171,27 @@ class ComicInfoProviderTest {
assertThat(titleSort).isEqualTo("series")
assertThat(status).isNull()
assertThat(collections).containsExactly("collection")
assertThat(publisher).isEqualTo("publisher")
assertThat(ageRating).isEqualTo(15)
assertThat(readingDirection).isEqualTo(SeriesMetadata.ReadingDirection.RIGHT_TO_LEFT)
assertThat(language).isEqualTo("en")
assertThat(summary).isBlank()
assertThat(genres).containsExactlyInAnyOrder("Action", "Adventure")
}
}
@Test
fun `given comicInfo with incorrect values when getting series metadata then metadata patch is valid`() {
val comicInfo = ComicInfo().apply {
languageISO = "japanese"
}
every { mockMapper.readValue(any<ByteArray>(), ComicInfo::class.java) } returns comicInfo
val patch = comicInfoProvider.getSeriesMetadataFromBook(book, media)!!
with(patch) {
assertThat(language).isNull()
}
}

View file

@ -3,12 +3,12 @@ package org.gotson.komga.interfaces.rest
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.groups.Tuple.tuple
import org.gotson.komga.domain.model.Author
import org.gotson.komga.domain.model.BookMetadata
import org.gotson.komga.domain.model.BookPage
import org.gotson.komga.domain.model.BookSearch
import org.gotson.komga.domain.model.KomgaUser
import org.gotson.komga.domain.model.Media
import org.gotson.komga.domain.model.ROLE_ADMIN
import org.gotson.komga.domain.model.SeriesMetadata
import org.gotson.komga.domain.model.ThumbnailBook
import org.gotson.komga.domain.model.makeBook
import org.gotson.komga.domain.model.makeLibrary
@ -577,8 +577,7 @@ class BookControllerTest(
@ValueSource(strings = [
"""{"title":""}""",
"""{"number":""}""",
"""{"authors":"[{"name":""}]"}""",
"""{"ageRating":-1}"""
"""{"authors":"[{"name":""}]"}"""
])
@WithMockCustomUser(roles = [ROLE_ADMIN])
fun `given invalid json when updating metadata then raise validation error`(jsonString: String) {
@ -612,12 +611,6 @@ class BookControllerTest(
"numberLock":true,
"numberSort": 1.0,
"numberSortLock":true,
"readingDirection":"LEFT_TO_RIGHT",
"readingDirectionLock":true,
"publisher":"newPublisher",
"publisherLock":true,
"ageRating":12,
"ageRatingLock":true,
"releaseDate":"2020-01-01",
"releaseDateLock":true,
"authors":[
@ -630,7 +623,9 @@ class BookControllerTest(
"role":"newAuthorRole2"
}
],
"authorsLock":true
"authorsLock":true,
"tags":["tag"],
"tagsLock":true
}
""".trimIndent()
@ -647,9 +642,6 @@ class BookControllerTest(
assertThat(summary).isEqualTo("newSummary")
assertThat(number).isEqualTo("newNumber")
assertThat(numberSort).isEqualTo(1F)
assertThat(readingDirection).isEqualTo(BookMetadata.ReadingDirection.LEFT_TO_RIGHT)
assertThat(publisher).isEqualTo("newPublisher")
assertThat(ageRating).isEqualTo(12)
assertThat(releaseDate).isEqualTo(LocalDate.of(2020, 1, 1))
assertThat(authors)
.hasSize(2)
@ -658,16 +650,15 @@ class BookControllerTest(
tuple("newAuthor", "newauthorrole"),
tuple("newAuthor2", "newauthorrole2")
)
assertThat(tags).containsExactly("tag")
assertThat(titleLock).isEqualTo(true)
assertThat(summaryLock).isEqualTo(true)
assertThat(numberLock).isEqualTo(true)
assertThat(numberSortLock).isEqualTo(true)
assertThat(readingDirectionLock).isEqualTo(true)
assertThat(publisherLock).isEqualTo(true)
assertThat(ageRatingLock).isEqualTo(true)
assertThat(releaseDateLock).isEqualTo(true)
assertThat(authorsLock).isEqualTo(true)
assertThat(tagsLock).isEqualTo(true)
}
}
@ -686,10 +677,9 @@ class BookControllerTest(
val bookId = bookRepository.findAll().first().id
bookMetadataRepository.findById(bookId).let { metadata ->
val updated = metadata.copy(
ageRating = 12,
readingDirection = BookMetadata.ReadingDirection.LEFT_TO_RIGHT,
authors = metadata.authors.toMutableList().also { it.add(Author("Author", "role")) },
releaseDate = testDate
releaseDate = testDate,
tags = setOf("tag")
)
bookMetadataRepository.update(updated)
@ -697,18 +687,15 @@ class BookControllerTest(
val metadata = bookMetadataRepository.findById(bookId)
with(metadata) {
assertThat(readingDirection).isEqualTo(BookMetadata.ReadingDirection.LEFT_TO_RIGHT)
assertThat(ageRating).isEqualTo(12)
assertThat(authors).hasSize(1)
assertThat(releaseDate).isEqualTo(testDate)
}
val jsonString = """
{
"readingDirection":null,
"ageRating":null,
"authors":null,
"releaseDate":null
"releaseDate":null,
"tags":null
}
""".trimIndent()
@ -721,10 +708,9 @@ class BookControllerTest(
val updatedMetadata = bookMetadataRepository.findById(bookId)
with(updatedMetadata) {
assertThat(readingDirection).isNull()
assertThat(ageRating).isNull()
assertThat(authors).isEmpty()
assertThat(releaseDate).isNull()
assertThat(tags).isEmpty()
}
}
@ -743,8 +729,6 @@ class BookControllerTest(
val bookId = bookRepository.findAll().first().id
bookMetadataRepository.findById(bookId).let { metadata ->
val updated = metadata.copy(
ageRating = 12,
readingDirection = BookMetadata.ReadingDirection.LEFT_TO_RIGHT,
authors = metadata.authors.toMutableList().also { it.add(Author("Author", "role")) },
releaseDate = testDate,
summary = "summary",
@ -752,7 +736,6 @@ class BookControllerTest(
numberLock = true,
numberSort = 2F,
numberSortLock = true,
publisher = "publisher",
title = "title"
)
@ -773,14 +756,11 @@ class BookControllerTest(
val metadata = bookMetadataRepository.findById(bookId)
with(metadata) {
assertThat(readingDirection).isEqualTo(BookMetadata.ReadingDirection.LEFT_TO_RIGHT)
assertThat(ageRating).isEqualTo(12)
assertThat(authors).hasSize(1)
assertThat(releaseDate).isEqualTo(testDate)
assertThat(summary).isEqualTo("summary")
assertThat(number).isEqualTo("number")
assertThat(numberSort).isEqualTo(2F)
assertThat(publisher).isEqualTo("publisher")
assertThat(title).isEqualTo("title")
}
}

View file

@ -43,6 +43,7 @@ import org.springframework.test.web.servlet.delete
import org.springframework.test.web.servlet.get
import org.springframework.test.web.servlet.patch
import org.springframework.test.web.servlet.post
import java.time.LocalDate
import kotlin.random.Random
@ExtendWith(SpringExtension::class)
@ -353,7 +354,9 @@ class SeriesControllerTest(
@ParameterizedTest
@ValueSource(strings = [
"""{"title":""}""",
"""{"titleSort":""}"""
"""{"titleSort":""}""",
"""{"ageRating":-1}""",
"""{"language":"japanese"}"""
])
@WithMockCustomUser(roles = [ROLE_ADMIN])
fun `given invalid json when updating metadata then raise validation error`(jsonString: String) {
@ -378,11 +381,25 @@ class SeriesControllerTest(
val jsonString = """
{
"title":"newTitle",
"titleSort":"newTitleSort",
"status":"HIATUS",
"titleLock":true,
"titleSort":"newTitleSort",
"titleSortLock":true,
"statusLock":true
"status":"HIATUS",
"statusLock":true,
"summary":"newSummary",
"summaryLock":true,
"readingDirection":"LEFT_TO_RIGHT",
"readingDirectionLock":true,
"ageRating":12,
"ageRatingLock":true,
"publisher":"newPublisher",
"publisherLock":true,
"language":"ja",
"languageLock":true,
"genres":["Action"],
"genresLock":true,
"tags":["tag"],
"tagsLock":true
}
""".trimIndent()
@ -398,9 +415,77 @@ class SeriesControllerTest(
assertThat(title).isEqualTo("newTitle")
assertThat(titleSort).isEqualTo("newTitleSort")
assertThat(status).isEqualTo(SeriesMetadata.Status.HIATUS)
assertThat(readingDirection).isEqualTo(SeriesMetadata.ReadingDirection.LEFT_TO_RIGHT)
assertThat(publisher).isEqualTo("newPublisher")
assertThat(summary).isEqualTo("newSummary")
assertThat(language).isEqualTo("ja")
assertThat(ageRating).isEqualTo(12)
assertThat(genres).containsExactly("Action")
assertThat(tags).containsExactly("tag")
assertThat(titleLock).isEqualTo(true)
assertThat(titleSortLock).isEqualTo(true)
assertThat(statusLock).isEqualTo(true)
assertThat(readingDirectionLock).isEqualTo(true)
assertThat(publisherLock).isEqualTo(true)
assertThat(ageRatingLock).isEqualTo(true)
assertThat(languageLock).isEqualTo(true)
assertThat(summaryLock).isEqualTo(true)
assertThat(genresLock).isEqualTo(true)
assertThat(tagsLock).isEqualTo(true)
}
}
@Test
@WithMockCustomUser(roles = [ROLE_ADMIN])
fun `given json with null fields when updating metadata then fields with null are unset`() {
val createdSeries = makeSeries(name = "series", libraryId = library.id).let { series ->
seriesLifecycle.createSeries(series).also { created ->
val books = listOf(makeBook("1", libraryId = library.id))
seriesLifecycle.addBooks(created, books)
}
}
seriesMetadataRepository.findById(createdSeries.id).let { metadata ->
val updated = metadata.copy(
ageRating = 12,
readingDirection = SeriesMetadata.ReadingDirection.LEFT_TO_RIGHT,
genres = setOf("Action"),
tags = setOf("tag")
)
seriesMetadataRepository.update(updated)
}
val metadata = seriesMetadataRepository.findById(createdSeries.id)
with(metadata) {
assertThat(readingDirection).isEqualTo(SeriesMetadata.ReadingDirection.LEFT_TO_RIGHT)
assertThat(ageRating).isEqualTo(12)
assertThat(genres).hasSize(1)
}
val jsonString = """
{
"readingDirection":null,
"ageRating":null,
"genres":null,
"tags":null
}
""".trimIndent()
mockMvc.patch("/api/v1/series/${createdSeries.id}/metadata") {
contentType = MediaType.APPLICATION_JSON
content = jsonString
}.andExpect {
status { isNoContent }
}
val updatedMetadata = seriesMetadataRepository.findById(createdSeries.id)
with(updatedMetadata) {
assertThat(readingDirection).isNull()
assertThat(ageRating).isNull()
assertThat(genres).isEmpty()
assertThat(tags).isEmpty()
}
}
}