diff --git a/komga/build.gradle.kts b/komga/build.gradle.kts index 4712127c0..7f44cd08e 100644 --- a/komga/build.gradle.kts +++ b/komga/build.gradle.kts @@ -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") diff --git a/komga/src/flyway/kotlin/db/migration/sqlite/V20200820150923__metadata_fields_part_2.kt b/komga/src/flyway/kotlin/db/migration/sqlite/V20200820150923__metadata_fields_part_2.kt new file mode 100644 index 000000000..7b925d07e --- /dev/null +++ b/komga/src/flyway/kotlin/db/migration/sqlite/V20200820150923__metadata_fields_part_2.kt @@ -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 + ) + } + } +} diff --git a/komga/src/flyway/resources/db/migration/sqlite/V20200820141405__metadata_fields_part_1.sql b/komga/src/flyway/resources/db/migration/sqlite/V20200820141405__metadata_fields_part_1.sql new file mode 100644 index 000000000..dadeb6a1a --- /dev/null +++ b/komga/src/flyway/resources/db/migration/sqlite/V20200820141405__metadata_fields_part_1.sql @@ -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) +); diff --git a/komga/src/flyway/resources/db/migration/sqlite/V20200820154318__metadata_fields_part_3.sql b/komga/src/flyway/resources/db/migration/sqlite/V20200820154318__metadata_fields_part_3.sql new file mode 100644 index 000000000..ba8facba1 --- /dev/null +++ b/komga/src/flyway/resources/db/migration/sqlite/V20200820154318__metadata_fields_part_3.sql @@ -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; diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/BookMetadata.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/BookMetadata.kt index 198a2418f..ef0e24f52 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/model/BookMetadata.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/BookMetadata.kt @@ -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 = emptyList(), + val tags: Set = 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 = this.authors.toList(), + tags: Set = 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')" } diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/BookMetadataPatch.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/BookMetadataPatch.kt index ce37a9f5f..112fba0cf 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/model/BookMetadataPatch.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/BookMetadataPatch.kt @@ -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?, diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/SeriesMetadata.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/SeriesMetadata.kt index fd6095ecd..e093e3462 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/model/SeriesMetadata.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/SeriesMetadata.kt @@ -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 = emptySet(), + val tags: Set = 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 = this.genres, + tags: Set = 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')" + } } diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/SeriesMetadataPatch.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/SeriesMetadataPatch.kt index a86fed28f..67389800b 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/model/SeriesMetadataPatch.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/SeriesMetadataPatch.kt @@ -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, - val collections: List = emptyList() + val collections: List ) diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/persistence/BookMetadataRepository.kt b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/BookMetadataRepository.kt index a71d31438..6b5223c3a 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/persistence/BookMetadataRepository.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/BookMetadataRepository.kt @@ -7,8 +7,6 @@ interface BookMetadataRepository { fun findByIdOrNull(bookId: String): BookMetadata? fun findByIds(bookIds: Collection): Collection - fun findAuthorsByName(search: String): List - fun insert(metadata: BookMetadata) fun insertMany(metadatas: Collection) fun update(metadata: BookMetadata) diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/persistence/ReferentialRepository.kt b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/ReferentialRepository.kt new file mode 100644 index 000000000..56766d4c4 --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/ReferentialRepository.kt @@ -0,0 +1,7 @@ +package org.gotson.komga.domain.persistence + +interface ReferentialRepository { + fun findAuthorsByName(search: String): List + fun findAllGenres(): Set + fun findAllTags(): Set +} diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/persistence/SeriesMetadataRepository.kt b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/SeriesMetadataRepository.kt index 1870807a6..0b2c9a655 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/persistence/SeriesMetadataRepository.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/SeriesMetadataRepository.kt @@ -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) fun count(): Long } diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/MetadataApplier.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/MetadataApplier.kt index 3302aef98..de5ceec75 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/MetadataApplier.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/MetadataApplier.kt @@ -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) ) } diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/MetadataLifecycle.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/MetadataLifecycle.kt index 6efc29769..01e464656 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/MetadataLifecycle.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/MetadataLifecycle.kt @@ -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 Iterable.uniqueOrNull(transform: (T) -> R?): R? { + private fun Iterable.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 } } diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/SeriesLifecycle.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/SeriesLifecycle.kt index 44776b7de..dc9e8a62f 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/SeriesLifecycle.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/SeriesLifecycle.kt @@ -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) } diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/BookDtoDao.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/BookDtoDao.kt index de4bd10f9..b63dc2f59 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/BookDtoDao.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/BookDtoDao.kt @@ -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) = + private fun BookMetadataRecord.toDto(authors: List, tags: Set) = 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() = diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/BookMetadataDao.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/BookMetadataDao.kt index 46d7699b4..02ad135da 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/BookMetadataDao.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/BookMetadataDao.kt @@ -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 { - 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) { @@ -166,10 +162,26 @@ class BookMetadataDao( } } + private fun insertTags(dsl: DSLContext, metadatas: Collection) { + 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) = + private fun BookMetadataRecord.toDomain(authors: List, tags: Set) = 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() = diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ReferentialDao.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ReferentialDao.kt new file mode 100644 index 000000000..5c5fc328a --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ReferentialDao.kt @@ -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 = + dsl.selectDistinct(a.NAME) + .from(a) + .where(a.NAME.containsIgnoreCase(search)) + .orderBy(a.NAME) + .fetch(a.NAME) + + override fun findAllGenres(): Set = + dsl.selectDistinct(g.GENRE) + .from(g) + .orderBy(lower(g.GENRE)) + .fetchSet(g.GENRE) + + override fun findAllTags(): Set = + 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() +} diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesDao.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesDao.kt index c189ce7c1..51a0e8af4 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesDao.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesDao.kt @@ -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() } } diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesDtoDao.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesDtoDao.kt index 7bc23bacf..f6f0c62f8 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesDtoDao.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesDtoDao.kt @@ -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 = DSL.sum(DSL.`when`(r.COMPLETED.isNull, 1).otherwise(0)) val countRead: AggregateFunction = 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, tags: Set) = 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 ) } diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesMetadataDao.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesMetadataDao.kt index 691be1ebc..6f67f333e 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesMetadataDao.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesMetadataDao.kt @@ -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) { + 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, tags: Set) = 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() ) diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicinfo/ComicInfoProvider.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicinfo/ComicInfoProvider.kt index 229c3e619..b45f579b4 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicinfo/ComicInfoProvider.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicinfo/ComicInfoProvider.kt @@ -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 diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/epub/EpubMetadataProvider.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/epub/EpubMetadataProvider.kt index 484a87f5b..8eccc5164 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/epub/EpubMetadataProvider.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/epub/EpubMetadataProvider.kt @@ -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 } diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/validation/BCP47.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/validation/BCP47.kt new file mode 100644 index 000000000..4eeeabdac --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/validation/BCP47.kt @@ -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> = [], + val payload: Array> = [] +) + +class BCP47Validator : ConstraintValidator { + + 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) + } + } +} diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/validation/Blank.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/validation/Blank.kt new file mode 100644 index 000000000..7c919e5cf --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/validation/Blank.kt @@ -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> = [], + val payload: Array> = [] +) + +class BlankValidator : ConstraintValidator { + + override fun isValid(value: String?, context: ConstraintValidatorContext?): Boolean { + if (value == null) return false + return value.isBlank() + } +} diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/validation/NullOrBlankOrBCP47.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/validation/NullOrBlankOrBCP47.kt new file mode 100644 index 000000000..86a7e1e86 --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/validation/NullOrBlankOrBCP47.kt @@ -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> = [], + val payload: Array> = [] +) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/BookController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/BookController.kt index 8600a856f..a9b1eef98 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/BookController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/BookController.kt @@ -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) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/ReferentialController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/ReferentialController.kt index 050fd7650..e449de71f 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/ReferentialController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/ReferentialController.kt @@ -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 = - bookMetadataRepository.findAuthorsByName(search) + referentialRepository.findAuthorsByName(search) + + @GetMapping("/genres") + fun getGenres(): Set = + referentialRepository.findAllGenres() + + @GetMapping("/tags") + fun getTags(): Set = + referentialRepository.findAllTags() } diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/SeriesController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/SeriesController.kt index 0fe574133..6296d0488 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/SeriesController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/SeriesController.kt @@ -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) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/BookDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/BookDto.kt index b09c46596..9a3cc8422 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/BookDto.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/BookDto.kt @@ -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, - val authorsLock: Boolean + val authorsLock: Boolean, + val tags: Set, + 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( diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/BookMetadataUpdateDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/BookMetadataUpdateDto.kt index 4962f44b5..699caa0aa 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/BookMetadataUpdateDto.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/BookMetadataUpdateDto.kt @@ -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(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(null) { prop, _, _ -> - isSet[prop.name] = true - } - - var ageRatingLock: Boolean? = null - var releaseDate: LocalDate? by Delegates.observable(null) { prop, _, _ -> isSet[prop.name] = true @@ -63,6 +44,13 @@ class BookMetadataUpdateDto { } var authorsLock: Boolean? = null + + var tags: Set? + by Delegates.observable?>(null) { prop, _, _ -> + isSet[prop.name] = true + } + + var tagsLock: Boolean? = null } class AuthorUpdateDto { diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/SeriesDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/SeriesDto.kt index d8c760c73..629d7db3e 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/SeriesDto.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/SeriesDto.kt @@ -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, + val genresLock: Boolean, + val tags: Set, + 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 ) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/SeriesMetadataUpdateDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/SeriesMetadataUpdateDto.kt index 99d90b7a0..91448513d 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/SeriesMetadataUpdateDto.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/SeriesMetadataUpdateDto.kt @@ -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() + 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(null) { prop, _, _ -> + isSet[prop.name] = true + } + + var readingDirectionLock: Boolean? = null + + @get:PositiveOrZero + var ageRating: Int? + by Delegates.observable(null) { prop, _, _ -> + isSet[prop.name] = true + } + + var ageRatingLock: Boolean? = null + + @get:NullOrBlankOrBCP47 + var language: String? = null + + var languageLock: Boolean? = null + + var genres: Set? + by Delegates.observable?>(null) { prop, _, _ -> + isSet[prop.name] = true + } + + var genresLock: Boolean? = null + + var tags: Set? + by Delegates.observable?>(null) { prop, _, _ -> + isSet[prop.name] = true + } + + var tagsLock: Boolean? = null +} diff --git a/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/BookMetadataDaoTest.kt b/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/BookMetadataDaoTest.kt index 63fbf0fc5..8399641c6 100644 --- a/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/BookMetadataDaoTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/BookMetadataDaoTest.kt @@ -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) diff --git a/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/SeriesMetadataDaoTest.kt b/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/SeriesMetadataDaoTest.kt index 7a69932f1..6cf0f8ff6 100644 --- a/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/SeriesMetadataDaoTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/SeriesMetadataDaoTest.kt @@ -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() } } diff --git a/komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/comicinfo/ComicInfoProviderTest.kt b/komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/comicinfo/ComicInfoProviderTest.kt index 4b28fd8b4..2df3c0853 100644 --- a/komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/comicinfo/ComicInfoProviderTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/comicinfo/ComicInfoProviderTest.kt @@ -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(), 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(), 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(), ComicInfo::class.java) } returns comicInfo + + val patch = comicInfoProvider.getSeriesMetadataFromBook(book, media)!! + + with(patch) { + assertThat(language).isNull() } } diff --git a/komga/src/test/kotlin/org/gotson/komga/interfaces/rest/BookControllerTest.kt b/komga/src/test/kotlin/org/gotson/komga/interfaces/rest/BookControllerTest.kt index e5f62bc86..517617c89 100644 --- a/komga/src/test/kotlin/org/gotson/komga/interfaces/rest/BookControllerTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/interfaces/rest/BookControllerTest.kt @@ -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") } } diff --git a/komga/src/test/kotlin/org/gotson/komga/interfaces/rest/SeriesControllerTest.kt b/komga/src/test/kotlin/org/gotson/komga/interfaces/rest/SeriesControllerTest.kt index 9d8917f26..b61ea21eb 100644 --- a/komga/src/test/kotlin/org/gotson/komga/interfaces/rest/SeriesControllerTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/interfaces/rest/SeriesControllerTest.kt @@ -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() } } }