feat(api): metadata import settings per library

ability to edit a library
fix filepath returned by API for Windows paths
series metadata import is now looking at all the files from all books, instead of being imported for each book separately

related to #199
This commit is contained in:
Gauthier Roebroeck 2020-07-03 15:01:21 +08:00
parent 5760a06b7a
commit 6824212514
25 changed files with 537 additions and 148 deletions

View file

@ -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;

View file

@ -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"

View file

@ -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()
}

View file

@ -60,6 +60,10 @@ class TaskReceiver(
submitTask(Task.RefreshBookMetadata(book.id))
}
fun refreshSeriesMetadata(seriesId: Long) {
submitTask(Task.RefreshSeriesMetadata(seriesId))
}
fun databaseBackup() {
submitTask(Task.BackupDatabase)
}

View file

@ -11,6 +11,5 @@ data class BookMetadataPatch(
val publisher: String?,
val ageRating: Int?,
val releaseDate: LocalDate?,
val authors: List<Author>?,
val series: SeriesMetadataPatch?
val authors: List<Author>?
)

View file

@ -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())
}

View file

@ -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<Library>
fun findAllById(libraryIds: Collection<Long>): Collection<Library>
fun existsByName(name: String): Boolean
fun delete(libraryId: Long)
fun deleteAll()
fun insert(library: Library): Library
fun update(library: Library)
fun count(): Long
}

View file

@ -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<Library>) {
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) {

View file

@ -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<BookMetadataProvider>,
private val seriesMetadataProviders: List<SeriesMetadataProvider>,
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 <T, R : Any> Iterable<T>.uniqueOrNull(transform: (T) -> R?): R? {
return this
.mapNotNull(transform)
.distinct()
.let {
if (it.size == 1) it.first() else null
}
}
}

View file

@ -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(),

View file

@ -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<Library> =
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

View file

@ -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(),

View file

@ -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?
}

View file

@ -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

View file

@ -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)

View file

@ -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()

View file

@ -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
)

View file

@ -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<Book>()) } answers { Thread.sleep(1_000) }
every { mockMetadataLifecycle.refreshMetadata(any<Series>()) } 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<Book>()) }
}
}

View file

@ -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")
}
}
}

View file

@ -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()
}
}

View file

@ -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)

View file

@ -157,9 +157,9 @@ class ComicInfoProviderTest {
every { mockMapper.readValue(any<ByteArray>(), 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()

View file

@ -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")) }
}
}
}

View file

@ -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")) }
}
}
}

View file

@ -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")) }
}
}
}