diff --git a/komga/src/flyway/resources/db/migration/V20200702111235__library_metadata_import_flags.sql b/komga/src/flyway/resources/db/migration/V20200702111235__library_metadata_import_flags.sql new file mode 100644 index 000000000..57603ecd9 --- /dev/null +++ b/komga/src/flyway/resources/db/migration/V20200702111235__library_metadata_import_flags.sql @@ -0,0 +1,10 @@ +alter table library + add column import_comicinfo_book boolean default true; +alter table library + add column import_comicinfo_series boolean default true; +alter table library + add column import_comicinfo_collection boolean default true; +alter table library + add column import_epub_book boolean default true; +alter table library + add column import_epub_series boolean default true; diff --git a/komga/src/main/kotlin/org/gotson/komga/application/tasks/Task.kt b/komga/src/main/kotlin/org/gotson/komga/application/tasks/Task.kt index 95a1d8782..265bdcb4b 100644 --- a/komga/src/main/kotlin/org/gotson/komga/application/tasks/Task.kt +++ b/komga/src/main/kotlin/org/gotson/komga/application/tasks/Task.kt @@ -21,6 +21,10 @@ sealed class Task : Serializable { override fun uniqueId() = "REFRESH_BOOK_METADATA_$bookId" } + data class RefreshSeriesMetadata(val seriesId: Long) : Task() { + override fun uniqueId() = "REFRESH_SERIES_METADATA_$seriesId" + } + object BackupDatabase : Task() { override fun uniqueId(): String = "BACKUP_DATABASE" override fun toString(): String = "BackupDatabase" diff --git a/komga/src/main/kotlin/org/gotson/komga/application/tasks/TaskHandler.kt b/komga/src/main/kotlin/org/gotson/komga/application/tasks/TaskHandler.kt index 511069029..b6868d1cf 100644 --- a/komga/src/main/kotlin/org/gotson/komga/application/tasks/TaskHandler.kt +++ b/komga/src/main/kotlin/org/gotson/komga/application/tasks/TaskHandler.kt @@ -3,6 +3,7 @@ package org.gotson.komga.application.tasks import mu.KotlinLogging import org.gotson.komga.domain.persistence.BookRepository import org.gotson.komga.domain.persistence.LibraryRepository +import org.gotson.komga.domain.persistence.SeriesRepository import org.gotson.komga.domain.service.BookLifecycle import org.gotson.komga.domain.service.LibraryScanner import org.gotson.komga.domain.service.MetadataLifecycle @@ -20,6 +21,7 @@ class TaskHandler( private val taskReceiver: TaskReceiver, private val libraryRepository: LibraryRepository, private val bookRepository: BookRepository, + private val seriesRepository: SeriesRepository, private val libraryScanner: LibraryScanner, private val bookLifecycle: BookLifecycle, private val metadataLifecycle: MetadataLifecycle, @@ -53,8 +55,14 @@ class TaskHandler( is Task.RefreshBookMetadata -> bookRepository.findByIdOrNull(task.bookId)?.let { metadataLifecycle.refreshMetadata(it) + taskReceiver.refreshSeriesMetadata(it.seriesId) } ?: logger.warn { "Cannot execute task $task: Book does not exist" } + is Task.RefreshSeriesMetadata -> + seriesRepository.findByIdOrNull(task.seriesId)?.let { + metadataLifecycle.refreshMetadata(it) + } ?: logger.warn { "Cannot execute task $task: Series does not exist" } + is Task.BackupDatabase -> { databaseBackuper.backupDatabase() } diff --git a/komga/src/main/kotlin/org/gotson/komga/application/tasks/TaskReceiver.kt b/komga/src/main/kotlin/org/gotson/komga/application/tasks/TaskReceiver.kt index 9141e57fc..49bdeaa3a 100644 --- a/komga/src/main/kotlin/org/gotson/komga/application/tasks/TaskReceiver.kt +++ b/komga/src/main/kotlin/org/gotson/komga/application/tasks/TaskReceiver.kt @@ -60,6 +60,10 @@ class TaskReceiver( submitTask(Task.RefreshBookMetadata(book.id)) } + fun refreshSeriesMetadata(seriesId: Long) { + submitTask(Task.RefreshSeriesMetadata(seriesId)) + } + fun databaseBackup() { submitTask(Task.BackupDatabase) } 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 b29340927..c01d7d052 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 @@ -11,6 +11,5 @@ data class BookMetadataPatch( val publisher: String?, val ageRating: Int?, val releaseDate: LocalDate?, - val authors: List?, - val series: SeriesMetadataPatch? + val authors: List? ) diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/Library.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/Library.kt index 623b5841f..b7ca555b6 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/model/Library.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/Library.kt @@ -8,12 +8,17 @@ import java.time.LocalDateTime data class Library( val name: String, val root: URL, + val importComicInfoBook: Boolean = true, + val importComicInfoSeries: Boolean = true, + val importComicInfoCollection: Boolean = true, + val importEpubBook: Boolean = true, + val importEpubSeries: Boolean = true, + val id: Long = 0, + override val createdDate: LocalDateTime = LocalDateTime.now(), override val lastModifiedDate: LocalDateTime = LocalDateTime.now() ) : Auditable() { - constructor(name: String, root: String) : this(name, Paths.get(root).toUri().toURL()) - fun path(): Path = Paths.get(this.root.toURI()) } diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/persistence/LibraryRepository.kt b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/LibraryRepository.kt index 0772187fd..0ebe8a8e0 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/persistence/LibraryRepository.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/LibraryRepository.kt @@ -4,15 +4,15 @@ import org.gotson.komga.domain.model.Library interface LibraryRepository { fun findByIdOrNull(libraryId: Long): Library? + fun findById(libraryId: Long): Library fun findAll(): Collection fun findAllById(libraryIds: Collection): Collection - fun existsByName(name: String): Boolean - fun delete(libraryId: Long) fun deleteAll() fun insert(library: Library): Library + fun update(library: Library) fun count(): Long } diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/LibraryLifecycle.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/LibraryLifecycle.kt index 164b2b35c..483412347 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/LibraryLifecycle.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/LibraryLifecycle.kt @@ -31,26 +31,40 @@ class LibraryLifecycle( fun addLibrary(library: Library): Library { logger.info { "Adding new library: ${library.name} with root folder: ${library.root}" } + val existing = libraryRepository.findAll() + checkLibraryValidity(library, existing) + + return libraryRepository.insert(library).also { + taskReceiver.scanLibrary(it.id) + } + } + + fun updateLibrary(toUpdate: Library) { + logger.info { "Updating library: ${toUpdate.id}" } + + val existing = libraryRepository.findAll().filter { it.id != toUpdate.id } + checkLibraryValidity(toUpdate, existing) + + libraryRepository.update(toUpdate) + taskReceiver.scanLibrary(toUpdate.id) + } + + private fun checkLibraryValidity(library: Library, existing: Collection) { if (!Files.exists(library.path())) throw FileNotFoundException("Library root folder does not exist: ${library.root}") if (!Files.isDirectory(library.path())) throw DirectoryNotFoundException("Library root folder is not a folder: ${library.root}") - if (libraryRepository.existsByName(library.name)) + if (existing.map { it.name }.contains(library.name)) throw DuplicateNameException("Library name already exists") - libraryRepository.findAll().forEach { + existing.forEach { if (library.path().startsWith(it.path())) throw PathContainedInPath("Library path ${library.path()} is a child of existing library ${it.name}: ${it.path()}") if (it.path().startsWith(library.path())) throw PathContainedInPath("Library path ${library.path()} is a parent of existing library ${it.name}: ${it.path()}") } - - return libraryRepository.insert(library).let { - taskReceiver.scanLibrary(it.id) - it - } } fun deleteLibrary(library: Library) { 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 68afaf3bd..3442de955 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 @@ -2,10 +2,17 @@ package org.gotson.komga.domain.service import mu.KotlinLogging import org.gotson.komga.domain.model.Book +import org.gotson.komga.domain.model.Series +import org.gotson.komga.domain.model.SeriesMetadataPatch import org.gotson.komga.domain.persistence.BookMetadataRepository +import org.gotson.komga.domain.persistence.BookRepository +import org.gotson.komga.domain.persistence.LibraryRepository import org.gotson.komga.domain.persistence.MediaRepository import org.gotson.komga.domain.persistence.SeriesMetadataRepository import org.gotson.komga.infrastructure.metadata.BookMetadataProvider +import org.gotson.komga.infrastructure.metadata.SeriesMetadataProvider +import org.gotson.komga.infrastructure.metadata.comicinfo.ComicInfoProvider +import org.gotson.komga.infrastructure.metadata.epub.EpubMetadataProvider import org.springframework.stereotype.Service private val logger = KotlinLogging.logger {} @@ -13,33 +20,71 @@ private val logger = KotlinLogging.logger {} @Service class MetadataLifecycle( private val bookMetadataProviders: List, + private val seriesMetadataProviders: List, private val metadataApplier: MetadataApplier, private val mediaRepository: MediaRepository, private val bookMetadataRepository: BookMetadataRepository, - private val seriesMetadataRepository: SeriesMetadataRepository + private val seriesMetadataRepository: SeriesMetadataRepository, + private val libraryRepository: LibraryRepository, + private val bookRepository: BookRepository ) { fun refreshMetadata(book: Book) { logger.info { "Refresh metadata for book: $book" } val media = mediaRepository.findById(book.id) + val library = libraryRepository.findById(book.libraryId) + bookMetadataProviders.forEach { provider -> - provider.getBookMetadataFromBook(book, media)?.let { bPatch -> + when { + provider is ComicInfoProvider && !library.importComicInfoBook -> logger.info { "Library is not set to import book metadata from ComicInfo, skipping" } + provider is EpubMetadataProvider && !library.importEpubBook -> logger.info { "Library is not set to import book metadata from Epub, skipping" } + else -> { + logger.debug { "Provider: $provider" } + provider.getBookMetadataFromBook(book, media)?.let { bPatch -> - bookMetadataRepository.findById(book.id).let { - logger.debug { "Original metadata: $it" } - val patched = metadataApplier.apply(bPatch, it) - logger.debug { "Patched metadata: $patched" } + bookMetadataRepository.findById(book.id).let { + logger.debug { "Original metadata: $it" } + val patched = metadataApplier.apply(bPatch, it) + logger.debug { "Patched metadata: $patched" } - bookMetadataRepository.update(patched) + bookMetadataRepository.update(patched) + } + } } + } + } + } - bPatch.series?.let { sPatch -> - seriesMetadataRepository.findById(book.seriesId).let { - logger.debug { "Apply metadata for series: ${book.seriesId}" } + fun refreshMetadata(series: Series) { + logger.info { "Refresh metadata for series: $series" } + + val library = libraryRepository.findById(series.libraryId) + + seriesMetadataProviders.forEach { provider -> + when { + provider is ComicInfoProvider && !library.importComicInfoSeries -> logger.info { "Library is not set to import series metadata from ComicInfo, skipping" } + provider is EpubMetadataProvider && !library.importEpubSeries -> logger.info { "Library is not set to import series metadata from Epub, skipping" } + else -> { + logger.debug { "Provider: $provider" } + val patches = bookRepository.findBySeriesId(series.id) + .mapNotNull { provider.getSeriesMetadataFromBook(it, mediaRepository.findById(it.id)) } + + 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) + + seriesMetadataRepository.findById(series.id).let { + logger.debug { "Apply metadata for series: $series" } logger.debug { "Original metadata: $it" } - val patched = metadataApplier.apply(sPatch, it) + val patched = metadataApplier.apply(aggregatedPatch, it) logger.debug { "Patched metadata: $patched" } seriesMetadataRepository.update(patched) @@ -49,4 +94,13 @@ class MetadataLifecycle( } } + private fun Iterable.uniqueOrNull(transform: (T) -> R?): R? { + return this + .mapNotNull(transform) + .distinct() + .let { + if (it.size == 1) it.first() else null + } + } } + 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 b98aaa85f..fa5e352ff 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 @@ -23,6 +23,7 @@ import org.springframework.data.domain.PageImpl import org.springframework.data.domain.PageRequest import org.springframework.data.domain.Pageable import org.springframework.stereotype.Component +import toFilePath import java.net.URL @Component @@ -207,7 +208,7 @@ class BookDtoDao( seriesId = seriesId, libraryId = libraryId, name = name, - url = URL(url).toURI().path, + url = URL(url).toFilePath(), number = number, created = createdDate.toUTC(), lastModified = lastModifiedDate.toUTC(), diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/LibraryDao.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/LibraryDao.kt index a1bf38815..de1696b52 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/LibraryDao.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/LibraryDao.kt @@ -8,6 +8,7 @@ import org.gotson.komga.jooq.tables.records.LibraryRecord import org.jooq.DSLContext import org.springframework.stereotype.Component import java.net.URL +import java.time.LocalDateTime @Component class LibraryDao( @@ -18,10 +19,17 @@ class LibraryDao( private val ul = Tables.USER_LIBRARY_SHARING override fun findByIdOrNull(libraryId: Long): Library? = + findOne(libraryId) + ?.toDomain() + + override fun findById(libraryId: Long): Library = + findOne(libraryId) + .toDomain() + + private fun findOne(libraryId: Long) = dsl.selectFrom(l) .where(l.ID.eq(libraryId)) .fetchOneInto(l) - ?.toDomain() override fun findAll(): Collection = dsl.selectFrom(l) @@ -34,12 +42,6 @@ class LibraryDao( .fetchInto(l) .map { it.toDomain() } - override fun existsByName(name: String): Boolean = - dsl.fetchExists( - dsl.selectFrom(l) - .where(l.NAME.equalIgnoreCase(name)) - ) - override fun delete(libraryId: Long) { dsl.transaction { config -> with(config.dsl()) @@ -67,9 +69,28 @@ class LibraryDao( .set(l.ID, id) .set(l.NAME, library.name) .set(l.ROOT, library.root.toString()) + .set(l.IMPORT_COMICINFO_BOOK, library.importComicInfoBook) + .set(l.IMPORT_COMICINFO_SERIES, library.importComicInfoSeries) + .set(l.IMPORT_COMICINFO_COLLECTION, library.importComicInfoCollection) + .set(l.IMPORT_EPUB_BOOK, library.importEpubBook) + .set(l.IMPORT_EPUB_SERIES, library.importEpubSeries) .execute() - return findByIdOrNull(id)!! + return findById(id) + } + + override fun update(library: Library) { + dsl.update(l) + .set(l.NAME, library.name) + .set(l.ROOT, library.root.toString()) + .set(l.IMPORT_COMICINFO_BOOK, library.importComicInfoBook) + .set(l.IMPORT_COMICINFO_SERIES, library.importComicInfoSeries) + .set(l.IMPORT_COMICINFO_COLLECTION, library.importComicInfoCollection) + .set(l.IMPORT_EPUB_BOOK, library.importEpubBook) + .set(l.IMPORT_EPUB_SERIES, library.importEpubSeries) + .set(l.LAST_MODIFIED_DATE, LocalDateTime.now()) + .where(l.ID.eq(library.id)) + .execute() } override fun count(): Long = dsl.fetchCount(l).toLong() @@ -79,6 +100,11 @@ class LibraryDao( Library( name = name, root = URL(root), + importComicInfoBook = importComicinfoBook, + importComicInfoSeries = importComicinfoSeries, + importComicInfoCollection = importComicinfoCollection, + importEpubBook = importEpubBook, + importEpubSeries = importEpubSeries, id = id, createdDate = createdDate, lastModifiedDate = lastModifiedDate 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 7bade1092..8f230f11f 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 @@ -22,6 +22,7 @@ import org.springframework.data.domain.PageRequest import org.springframework.data.domain.Pageable import org.springframework.data.domain.Sort import org.springframework.stereotype.Component +import toFilePath import java.math.BigDecimal import java.net.URL @@ -188,7 +189,7 @@ class SeriesDtoDao( id = id, libraryId = libraryId, name = name, - url = URL(url).toURI().path, + url = URL(url).toFilePath(), created = createdDate.toUTC(), lastModified = lastModifiedDate.toUTC(), fileLastModified = fileLastModified.toUTC(), diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/SeriesMetadataProvider.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/SeriesMetadataProvider.kt new file mode 100644 index 000000000..4abbda685 --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/SeriesMetadataProvider.kt @@ -0,0 +1,9 @@ +package org.gotson.komga.infrastructure.metadata + +import org.gotson.komga.domain.model.Book +import org.gotson.komga.domain.model.Media +import org.gotson.komga.domain.model.SeriesMetadataPatch + +interface SeriesMetadataProvider { + fun getSeriesMetadataFromBook(book: Book, media: Media): SeriesMetadataPatch? +} 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 1403a050f..fb5fb5baa 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 @@ -10,6 +10,7 @@ import org.gotson.komga.domain.model.Media 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.springframework.beans.factory.annotation.Autowired @@ -24,7 +25,7 @@ private const val COMIC_INFO = "ComicInfo.xml" class ComicInfoProvider( @Autowired(required = false) private val mapper: XmlMapper = XmlMapper(), private val bookAnalyzer: BookAnalyzer -) : BookMetadataProvider { +) : BookMetadataProvider, SeriesMetadataProvider { override fun getBookMetadataFromBook(book: Book, media: Media): BookMetadataPatch? { getComicInfo(book, media)?.let { comicInfo -> @@ -56,12 +57,18 @@ class ComicInfoProvider( comicInfo.publisher, comicInfo.ageRating?.ageRating, releaseDate, - authors.ifEmpty { null }, - SeriesMetadataPatch( - comicInfo.series, - comicInfo.series, - null - ) + authors.ifEmpty { null } + ) + } + return null + } + + override fun getSeriesMetadataFromBook(book: Book, media: Media): SeriesMetadataPatch? { + getComicInfo(book, media)?.let { comicInfo -> + return SeriesMetadataPatch( + comicInfo.series, + comicInfo.series, + null ) } 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 a595b8438..0522f4e75 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 @@ -8,6 +8,7 @@ import org.gotson.komga.domain.model.Media 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.jsoup.Jsoup import org.springframework.stereotype.Service import java.time.LocalDate @@ -16,7 +17,7 @@ import java.time.format.DateTimeFormatter @Service class EpubMetadataProvider( private val epubExtractor: EpubExtractor -) : BookMetadataProvider { +) : BookMetadataProvider, SeriesMetadataProvider { private val relators = mapOf( "aut" to "writer", @@ -30,7 +31,7 @@ class EpubMetadataProvider( override fun getBookMetadataFromBook(book: Book, media: Media): BookMetadataPatch? { if (media.mediaType != "application/epub+zip") return null epubExtractor.getPackageFile(book.path())?.let { packageFile -> - val opf = Jsoup.parse(packageFile.toString()) + val opf = Jsoup.parse(packageFile) val title = opf.selectFirst("metadata > dc|title")?.text() val publisher = opf.selectFirst("metadata > dc|publisher")?.text() @@ -57,8 +58,6 @@ class EpubMetadataProvider( Author(name, relators[role] ?: "writer") } - val series = opf.selectFirst("metadata > meta[property=belongs-to-collection]")?.text() - return BookMetadataPatch( title = title, summary = description, @@ -68,13 +67,24 @@ class EpubMetadataProvider( publisher = publisher, ageRating = null, releaseDate = date, - authors = authors, - series = SeriesMetadataPatch(series, series, null) + authors = authors ) } return null } + override fun getSeriesMetadataFromBook(book: Book, media: Media): SeriesMetadataPatch? { + if (media.mediaType != "application/epub+zip") return null + epubExtractor.getPackageFile(book.path())?.let { packageFile -> + val opf = Jsoup.parse(packageFile) + + val series = opf.selectFirst("metadata > meta[property=belongs-to-collection]")?.text() + + return SeriesMetadataPatch(series, series, null) + } + return null + } + private fun parseDate(date: String): LocalDate? = try { LocalDate.parse(date, DateTimeFormatter.ISO_DATE) diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/web/Utils.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/web/Utils.kt new file mode 100644 index 000000000..95376b319 --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/web/Utils.kt @@ -0,0 +1,8 @@ +import java.net.URL +import java.nio.file.Paths + +fun URL.toFilePath(): String = + Paths.get(this.toURI()).toString() + +fun filePathToUrl(filePath: String): URL = + Paths.get(filePath).toUri().toURL() diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/LibraryController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/LibraryController.kt index 017d0fc6b..ee7b36657 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/LibraryController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/LibraryController.kt @@ -1,5 +1,6 @@ package org.gotson.komga.interfaces.rest +import filePathToUrl import mu.KotlinLogging import org.gotson.komga.application.tasks.TaskReceiver import org.gotson.komga.domain.model.DirectoryNotFoundException @@ -19,11 +20,13 @@ import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.PutMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController import org.springframework.web.server.ResponseStatusException +import toFilePath import java.io.FileNotFoundException import javax.validation.Valid import javax.validation.constraints.NotBlank @@ -66,7 +69,17 @@ class LibraryController( @Valid @RequestBody library: LibraryCreationDto ): LibraryDto = try { - libraryLifecycle.addLibrary(Library(library.name, library.root)).toDto(includeRoot = principal.user.roleAdmin) + libraryLifecycle.addLibrary( + Library( + name = library.name, + root = filePathToUrl(library.root), + importComicInfoBook = library.importComicInfoBook, + importComicInfoSeries = library.importComicInfoSeries, + importComicInfoCollection = library.importComicInfoCollection, + importEpubBook = library.importEpubBook, + importEpubSeries = library.importEpubSeries + ) + ).toDto(includeRoot = principal.user.roleAdmin) } catch (e: Exception) { when (e) { is FileNotFoundException, @@ -78,6 +91,28 @@ class LibraryController( } } + @PutMapping("/{id}") + @PreAuthorize("hasRole('$ROLE_ADMIN')") + @ResponseStatus(HttpStatus.NO_CONTENT) + fun updateOne( + @PathVariable id: Long, + @Valid @RequestBody library: LibraryUpdateDto + ) { + libraryRepository.findByIdOrNull(id)?.let { + val toUpdate = Library( + id = id, + name = library.name, + root = filePathToUrl(library.root), + importComicInfoBook = library.importComicInfoBook, + importComicInfoSeries = library.importComicInfoSeries, + importComicInfoCollection = library.importComicInfoCollection, + importEpubBook = library.importEpubBook, + importEpubSeries = library.importEpubSeries + ) + libraryLifecycle.updateLibrary(toUpdate) + } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + } + @DeleteMapping("/{id}") @PreAuthorize("hasRole('$ROLE_ADMIN')") @ResponseStatus(HttpStatus.NO_CONTENT) @@ -117,17 +152,42 @@ class LibraryController( data class LibraryCreationDto( @get:NotBlank val name: String, - @get:NotBlank val root: String + @get:NotBlank val root: String, + val importComicInfoBook: Boolean = true, + val importComicInfoSeries: Boolean = true, + val importComicInfoCollection: Boolean = true, + val importEpubBook: Boolean = true, + val importEpubSeries: Boolean = true ) data class LibraryDto( val id: Long, val name: String, - val root: String + val root: String, + val importComicInfoBook: Boolean, + val importComicInfoSeries: Boolean, + val importComicInfoCollection: Boolean, + val importEpubBook: Boolean, + val importEpubSeries: Boolean +) + +data class LibraryUpdateDto( + @get:NotBlank val name: String, + @get:NotBlank val root: String, + val importComicInfoBook: Boolean, + val importComicInfoSeries: Boolean, + val importComicInfoCollection: Boolean, + val importEpubBook: Boolean, + val importEpubSeries: Boolean ) fun Library.toDto(includeRoot: Boolean) = LibraryDto( id = id, name = name, - root = if (includeRoot) root.toURI().path else "" + root = if (includeRoot) this.root.toFilePath() else "", + importComicInfoBook = importComicInfoBook, + importComicInfoSeries = importComicInfoSeries, + importComicInfoCollection = importComicInfoCollection, + importEpubBook = importEpubBook, + importEpubSeries = importEpubSeries ) diff --git a/komga/src/test/kotlin/org/gotson/komga/application/tasks/TaskHandlerTest.kt b/komga/src/test/kotlin/org/gotson/komga/application/tasks/TaskHandlerTest.kt index 35d6e93a6..99ba78797 100644 --- a/komga/src/test/kotlin/org/gotson/komga/application/tasks/TaskHandlerTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/application/tasks/TaskHandlerTest.kt @@ -2,8 +2,12 @@ package org.gotson.komga.application.tasks import com.ninjasquad.springmockk.MockkBean import io.mockk.every +import io.mockk.just +import io.mockk.runs import io.mockk.verify import mu.KotlinLogging +import org.gotson.komga.domain.model.Book +import org.gotson.komga.domain.model.Series import org.gotson.komga.domain.model.makeBook import org.gotson.komga.domain.model.makeLibrary import org.gotson.komga.domain.model.makeSeries @@ -78,7 +82,8 @@ class TaskHandlerTest( seriesLifecycle.addBooks(it, listOf(book)) } - every { mockMetadataLifecycle.refreshMetadata(any()) } answers { Thread.sleep(1_000) } + every { mockMetadataLifecycle.refreshMetadata(any()) } answers { Thread.sleep(1_000) } + every { mockMetadataLifecycle.refreshMetadata(any()) } just runs val createdBook = bookRepository.findAll().first() @@ -88,6 +93,6 @@ class TaskHandlerTest( Thread.sleep(5_000) - verify(atLeast = 1, atMost = 3) { mockMetadataLifecycle.refreshMetadata(any()) } + verify(atLeast = 1, atMost = 3) { mockMetadataLifecycle.refreshMetadata(any()) } } } diff --git a/komga/src/test/kotlin/org/gotson/komga/domain/service/LibraryLifecycleTest.kt b/komga/src/test/kotlin/org/gotson/komga/domain/service/LibraryLifecycleTest.kt index 3438b8763..c33078252 100644 --- a/komga/src/test/kotlin/org/gotson/komga/domain/service/LibraryLifecycleTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/domain/service/LibraryLifecycleTest.kt @@ -8,6 +8,7 @@ import org.gotson.komga.domain.model.Library import org.gotson.komga.domain.model.PathContainedInPath import org.gotson.komga.domain.persistence.LibraryRepository import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.springframework.beans.factory.annotation.Autowired @@ -15,6 +16,7 @@ import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabas import org.springframework.boot.test.context.SpringBootTest import org.springframework.test.context.junit.jupiter.SpringExtension import java.io.FileNotFoundException +import java.net.URL import java.nio.file.Files @ExtendWith(SpringExtension::class) @@ -30,74 +32,213 @@ class LibraryLifecycleTest( libraryRepository.deleteAll() } - @Test - fun `when adding library with non-existent root folder then exception is thrown`() { - // when - val thrown = catchThrowable { libraryLifecycle.addLibrary(Library("test", "/non-existent")) } + @Nested + inner class Add { + @Test + fun `when adding library with non-existent root folder then exception is thrown`() { + // when + val thrown = catchThrowable { libraryLifecycle.addLibrary(Library("test", URL("file:/non-existent"))) } - // then - assertThat(thrown).isInstanceOf(FileNotFoundException::class.java) - } - - @Test - fun `when adding library with non-directory root folder then exception is thrown`() { - // when - val thrown = catchThrowable { - libraryLifecycle.addLibrary(Library("test", Files.createTempFile(null, null).toUri().toURL())) + // then + assertThat(thrown).isInstanceOf(FileNotFoundException::class.java) } - // then - assertThat(thrown).isInstanceOf(DirectoryNotFoundException::class.java) - } + @Test + fun `when adding library with non-directory root folder then exception is thrown`() { + // when + val thrown = catchThrowable { + libraryLifecycle.addLibrary(Library("test", Files.createTempFile(null, null).toUri().toURL())) + } - @Test - fun `given existing library when adding library with same name then exception is thrown`() { - // given - libraryLifecycle.addLibrary(Library("test", Files.createTempDirectory(null).toUri().toURL())) + // then + assertThat(thrown).isInstanceOf(DirectoryNotFoundException::class.java) + } - // when - val thrown = catchThrowable { + @Test + fun `given existing library when adding library with same name then exception is thrown`() { + // given libraryLifecycle.addLibrary(Library("test", Files.createTempDirectory(null).toUri().toURL())) + + // when + val thrown = catchThrowable { + libraryLifecycle.addLibrary(Library("test", Files.createTempDirectory(null).toUri().toURL())) + } + + // then + assertThat(thrown).isInstanceOf(DuplicateNameException::class.java) } - // then - assertThat(thrown).isInstanceOf(DuplicateNameException::class.java) - } - - @Test - fun `given existing library when adding library with root folder as child of existing library then exception is thrown`() { - // given - val parent = Files.createTempDirectory(null) - libraryLifecycle.addLibrary(Library("parent", parent.toUri().toURL())) - - // when - val child = Files.createTempDirectory(parent, "") - val thrown = catchThrowable { - libraryLifecycle.addLibrary(Library("child", child.toUri().toURL())) - } - - // then - assertThat(thrown) - .isInstanceOf(PathContainedInPath::class.java) - .hasMessageContaining("child") - } - - @Test - fun `given existing library when adding library with root folder as parent of existing library then exception is thrown`() { - // given - val parent = Files.createTempDirectory(null) - val child = Files.createTempDirectory(parent, null) - libraryLifecycle.addLibrary(Library("child", child.toUri().toURL())) - - // when - val thrown = catchThrowable { + @Test + fun `given existing library when adding library with root folder as child of existing library then exception is thrown`() { + // given + val parent = Files.createTempDirectory(null) libraryLifecycle.addLibrary(Library("parent", parent.toUri().toURL())) + + // when + val child = Files.createTempDirectory(parent, "") + val thrown = catchThrowable { + libraryLifecycle.addLibrary(Library("child", child.toUri().toURL())) + } + + // then + assertThat(thrown) + .isInstanceOf(PathContainedInPath::class.java) + .hasMessageContaining("child") } - // then - assertThat(thrown) - .isInstanceOf(PathContainedInPath::class.java) - .hasMessageContaining("parent") + @Test + fun `given existing library when adding library with root folder as parent of existing library then exception is thrown`() { + // given + val parent = Files.createTempDirectory(null) + val child = Files.createTempDirectory(parent, null) + libraryLifecycle.addLibrary(Library("child", child.toUri().toURL())) + + // when + val thrown = catchThrowable { + libraryLifecycle.addLibrary(Library("parent", parent.toUri().toURL())) + } + + // then + assertThat(thrown) + .isInstanceOf(PathContainedInPath::class.java) + .hasMessageContaining("parent") + } } + @Nested + inner class Update { + private val rootFolder = Files.createTempDirectory(null) + private val library = Library("Existing", rootFolder.toUri().toURL()) + + @Test + fun `given existing library when updating with non-existent root folder then exception is thrown`() { + // given + val existing = libraryLifecycle.addLibrary(library) + + // when + val toUpdate = existing.copy(name = "test", root = URL("file:/non-existent")) + val thrown = catchThrowable { libraryLifecycle.updateLibrary(toUpdate) } + + // then + assertThat(thrown).isInstanceOf(FileNotFoundException::class.java) + } + + @Test + fun `given existing library when updating with non-directory root folder then exception is thrown`() { + // given + val existing = libraryLifecycle.addLibrary(library) + + // when + val toUpdate = existing.copy(name = "test", root = Files.createTempFile(null, null).toUri().toURL()) + val thrown = catchThrowable { + libraryLifecycle.updateLibrary(toUpdate) + } + + // then + assertThat(thrown).isInstanceOf(DirectoryNotFoundException::class.java) + } + + @Test + fun `given single existing library when updating library with same name then it is updated`() { + // given + val existing = libraryLifecycle.addLibrary(library) + + // when + val thrown = catchThrowable { + libraryLifecycle.updateLibrary(existing) + } + + // then + assertThat(thrown).doesNotThrowAnyException() + } + + @Test + fun `given existing library when updating library with same name then exception is thrown`() { + // given + libraryLifecycle.addLibrary(Library("test", Files.createTempDirectory(null).toUri().toURL())) + val existing = libraryLifecycle.addLibrary(library) + + // when + val toUpdate = existing.copy(name = "test", root = Files.createTempDirectory(null).toUri().toURL()) + val thrown = catchThrowable { + libraryLifecycle.updateLibrary(toUpdate) + } + + // then + assertThat(thrown).isInstanceOf(DuplicateNameException::class.java) + } + + @Test + fun `given single existing library when updating library with root folder as child of existing library then no exception is thrown`() { + // given + val existing = libraryLifecycle.addLibrary(library) + + // when + val child = Files.createTempDirectory(rootFolder, "") + val toUpdate = existing.copy(root = child.toUri().toURL()) + val thrown = catchThrowable { + libraryLifecycle.updateLibrary(toUpdate) + } + + // then + assertThat(thrown).doesNotThrowAnyException() + } + + @Test + fun `given existing library when updating library with root folder as child of existing library then exception is thrown`() { + // given + val parent = Files.createTempDirectory(null) + libraryLifecycle.addLibrary(Library("parent", parent.toUri().toURL())) + val existing = libraryLifecycle.addLibrary(library) + + // when + val child = Files.createTempDirectory(parent, "") + val toUpdate = existing.copy(root = child.toUri().toURL()) + val thrown = catchThrowable { + libraryLifecycle.updateLibrary(toUpdate) + } + + // then + assertThat(thrown) + .isInstanceOf(PathContainedInPath::class.java) + .hasMessageContaining("child") + } + + @Test + fun `given single existing library when updating library with root folder as parent of existing library then no exception is thrown`() { + // given + val parent = Files.createTempDirectory(null) + val child = Files.createTempDirectory(parent, null) + val existing = libraryLifecycle.addLibrary(Library("child", child.toUri().toURL())) + + // when + val toUpdate = existing.copy(root = parent.toUri().toURL()) + val thrown = catchThrowable { + libraryLifecycle.updateLibrary(toUpdate) + } + + // then + assertThat(thrown).doesNotThrowAnyException() + } + + @Test + fun `given existing library when updating library with root folder as parent of existing library then exception is thrown`() { + // given + val parent = Files.createTempDirectory(null) + val child = Files.createTempDirectory(parent, null) + libraryLifecycle.addLibrary(Library("child", child.toUri().toURL())) + val existing = libraryLifecycle.addLibrary(library) + + // when + val toUpdate = existing.copy(root = parent.toUri().toURL()) + val thrown = catchThrowable { + libraryLifecycle.updateLibrary(toUpdate) + } + + // then + assertThat(thrown) + .isInstanceOf(PathContainedInPath::class.java) + .hasMessageContaining("parent") + } + } } diff --git a/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/LibraryDaoTest.kt b/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/LibraryDaoTest.kt index 35029b654..c5b7342b0 100644 --- a/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/LibraryDaoTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/LibraryDaoTest.kt @@ -45,6 +45,46 @@ class LibraryDaoTest( assertThat(created.root).isEqualTo(library.root) } + @Test + fun `given existing library when updating then it is persisted`() { + val library = Library( + name = "Library", + root = URL("file://library") + ) + val created = libraryDao.insert(library) + + Thread.sleep(5) + + val modificationDate = LocalDateTime.now() + + val updated = created.copy( + name = "LibraryUpdated", + root = URL("file://library2"), + importEpubSeries = false, + importEpubBook = false, + importComicInfoCollection = false, + importComicInfoSeries = false, + importComicInfoBook = false + ) + + libraryDao.update(updated) + val modified = libraryDao.findById(updated.id) + + assertThat(modified.id).isEqualTo(updated.id) + assertThat(modified.createdDate).isEqualTo(updated.createdDate) + assertThat(modified.lastModifiedDate) + .isAfterOrEqualTo(modificationDate) + .isNotEqualTo(updated.lastModifiedDate) + + assertThat(modified.name).isEqualTo(updated.name) + assertThat(modified.root).isEqualTo(updated.root) + assertThat(modified.importEpubSeries).isEqualTo(updated.importEpubSeries) + assertThat(modified.importEpubBook).isEqualTo(updated.importEpubBook) + assertThat(modified.importComicInfoCollection).isEqualTo(updated.importComicInfoCollection) + assertThat(modified.importComicInfoSeries).isEqualTo(updated.importComicInfoSeries) + assertThat(modified.importComicInfoBook).isEqualTo(updated.importComicInfoBook) + } + @Test fun `given a library when deleting then it is deleted`() { val library = Library( @@ -141,19 +181,4 @@ class LibraryDaoTest( assertThat(found).isNull() } - - @Test - fun `given libraries when checking if exists by name then returns true or false`() { - val library = Library( - name = "Library", - root = URL("file://library") - ) - libraryDao.insert(library) - - val exists = libraryDao.existsByName("LIBRARY") - val notExists = libraryDao.existsByName("LIBRARY2") - - assertThat(exists).isTrue() - assertThat(notExists).isFalse() - } } diff --git a/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/MediaDaoTest.kt b/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/MediaDaoTest.kt index 5d258ca2f..18dc56cd0 100644 --- a/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/MediaDaoTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/infrastructure/jooq/MediaDaoTest.kt @@ -129,19 +129,17 @@ class MediaDaoTest( val modificationDate = LocalDateTime.now() - val updated = with(created) { - copy( - status = Media.Status.ERROR, - mediaType = "application/rar", - thumbnail = Random.nextBytes(1), - pages = listOf(BookPage( - fileName = "2.png", - mediaType = "image/png" - )), - files = listOf("id.txt"), - comment = "comment2" - ) - } + val updated = created.copy( + status = Media.Status.ERROR, + mediaType = "application/rar", + thumbnail = Random.nextBytes(1), + pages = listOf(BookPage( + fileName = "2.png", + mediaType = "image/png" + )), + files = listOf("id.txt"), + comment = "comment2" + ) mediaDao.update(updated) val modified = mediaDao.findById(updated.bookId) 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 aee16c2b5..9cead5cc4 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 @@ -157,9 +157,9 @@ class ComicInfoProviderTest { every { mockMapper.readValue(any(), ComicInfo::class.java) } returns comicInfo - val patch = comicInfoProvider.getBookMetadataFromBook(book, media)!!.series + val patch = comicInfoProvider.getSeriesMetadataFromBook(book, media)!! - with(patch!!) { + with(patch) { assertThat(title).isEqualTo("series") assertThat(titleSort).isEqualTo("series") assertThat(status).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 fd9bfb2e7..91f44b763 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 @@ -22,6 +22,7 @@ import org.gotson.komga.domain.service.KomgaUserLifecycle import org.gotson.komga.domain.service.LibraryLifecycle import org.gotson.komga.domain.service.SeriesLifecycle import org.gotson.komga.infrastructure.security.KomgaPrincipal +import org.hamcrest.Matchers import org.hamcrest.core.IsNull import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.AfterEach @@ -451,10 +452,9 @@ class BookControllerTest( val book = bookRepository.findAll().first() - val url = "/1.cbr" val validation: MockMvcResultMatchersDsl.() -> Unit = { status { isOk } - jsonPath("$.content[0].url") { value(url) } + jsonPath("$.content[0].url") { value(Matchers.containsString("1.cbr")) } } mockMvc.get("/api/v1/books") @@ -469,7 +469,7 @@ class BookControllerTest( mockMvc.get("/api/v1/books/${book.id}") .andExpect { status { isOk } - jsonPath("$.url") { value(url) } + jsonPath("$.url") { value(Matchers.containsString("1.cbr")) } } } } diff --git a/komga/src/test/kotlin/org/gotson/komga/interfaces/rest/LibraryControllerTest.kt b/komga/src/test/kotlin/org/gotson/komga/interfaces/rest/LibraryControllerTest.kt index 6be276361..5f280eb64 100644 --- a/komga/src/test/kotlin/org/gotson/komga/interfaces/rest/LibraryControllerTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/interfaces/rest/LibraryControllerTest.kt @@ -4,6 +4,7 @@ import org.gotson.komga.domain.model.ROLE_ADMIN import org.gotson.komga.domain.model.ROLE_USER import org.gotson.komga.domain.model.makeLibrary import org.gotson.komga.domain.persistence.LibraryRepository +import org.hamcrest.Matchers import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Nested @@ -136,13 +137,13 @@ class LibraryControllerTest( mockMvc.get(route) .andExpect { status { isOk } - jsonPath("$[0].root") { value("/library1") } + jsonPath("$[0].root") { value(Matchers.containsString("library1")) } } mockMvc.get("${route}/${library.id}") .andExpect { status { isOk } - jsonPath("$.root") { value("/library1") } + jsonPath("$.root") { value(Matchers.containsString("library1")) } } } } 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 5663a9273..5df4ddbc4 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 @@ -326,10 +326,9 @@ class SeriesControllerTest( } } - val url = "/series" val validation: MockMvcResultMatchersDsl.() -> Unit = { status { isOk } - jsonPath("$.content[0].url") { value(url) } + jsonPath("$.content[0].url") { value(Matchers.containsString("series")) } } mockMvc.get("/api/v1/series") @@ -344,7 +343,7 @@ class SeriesControllerTest( mockMvc.get("/api/v1/series/${createdSeries.id}") .andExpect { status { isOk } - jsonPath("$.url") { value(url) } + jsonPath("$.url") { value(Matchers.containsString("series")) } } } }