From 8b6d23e930af97b1b51b21ccd8737d987e6ce61a Mon Sep 17 00:00:00 2001 From: Gauthier Roebroeck Date: Tue, 25 Feb 2025 11:44:30 +0800 Subject: [PATCH] fix(api): better matching of series name when importing books with metadata --- .../domain/service/TransientBookLifecycle.kt | 17 ++++- .../service/TransientBookLifecycleTest.kt | 71 +++++++++++++++++++ 2 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 komga/src/test/kotlin/org/gotson/komga/domain/service/TransientBookLifecycleTest.kt diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/TransientBookLifecycle.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/TransientBookLifecycle.kt index 5743942be..9f76dc0ed 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/TransientBookLifecycle.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/TransientBookLifecycle.kt @@ -5,6 +5,9 @@ import org.gotson.komga.domain.model.Media import org.gotson.komga.domain.model.MediaNotReadyException import org.gotson.komga.domain.model.MediaProfile import org.gotson.komga.domain.model.PathContainedInPath +import org.gotson.komga.domain.model.SearchCondition +import org.gotson.komga.domain.model.SearchContext +import org.gotson.komga.domain.model.SearchOperator import org.gotson.komga.domain.model.TransientBook import org.gotson.komga.domain.model.TypedBytes import org.gotson.komga.domain.model.toBookWithMedia @@ -15,6 +18,7 @@ import org.gotson.komga.infrastructure.image.ImageType import org.gotson.komga.infrastructure.metadata.BookMetadataProvider import org.gotson.komga.infrastructure.metadata.SeriesMetadataFromBookProvider import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.data.domain.Pageable import org.springframework.stereotype.Service import java.nio.file.Paths @@ -64,7 +68,7 @@ class TransientBookLifecycle( fun getMetadata(transientBook: TransientBook): Pair { val bookWithMedia = transientBook.toBookWithMedia() val number = bookMetadataProviders.firstNotNullOfOrNull { it.getBookMetadataFromBook(bookWithMedia)?.numberSort } - val series = + val seriesNamesFromMetadata = seriesMetadataProviders .flatMap { buildList { @@ -72,7 +76,16 @@ class TransientBookLifecycle( add(it.getSeriesMetadataFromBook(bookWithMedia, false)?.title) } }.filterNotNull() - .firstNotNullOfOrNull { seriesRepository.findAllByTitleContaining(it).firstOrNull() } + + val exactSearch = SearchCondition.AnyOfSeries(seriesNamesFromMetadata.map { SearchCondition.Title(SearchOperator.Is(it)) }) + val exactMatches = seriesRepository.findAll(exactSearch, SearchContext.ofAnonymousUser(), Pageable.unpaged()) + val series = + if (!exactMatches.isEmpty) { + exactMatches.content.first() + } else { + val containsSearch = SearchCondition.AnyOfSeries(seriesNamesFromMetadata.map { SearchCondition.Title(SearchOperator.Contains(it)) }) + seriesRepository.findAll(containsSearch, SearchContext.ofAnonymousUser(), Pageable.unpaged()).content.firstOrNull() + } return series?.id to number } diff --git a/komga/src/test/kotlin/org/gotson/komga/domain/service/TransientBookLifecycleTest.kt b/komga/src/test/kotlin/org/gotson/komga/domain/service/TransientBookLifecycleTest.kt new file mode 100644 index 000000000..a80ed0539 --- /dev/null +++ b/komga/src/test/kotlin/org/gotson/komga/domain/service/TransientBookLifecycleTest.kt @@ -0,0 +1,71 @@ +package org.gotson.komga.domain.service + +import com.ninjasquad.springmockk.SpykBean +import io.mockk.every +import org.assertj.core.api.Assertions.assertThat +import org.gotson.komga.domain.model.BookMetadataPatch +import org.gotson.komga.domain.model.Media +import org.gotson.komga.domain.model.SeriesMetadataPatch +import org.gotson.komga.domain.model.TransientBook +import org.gotson.komga.domain.model.makeBook +import org.gotson.komga.domain.model.makeLibrary +import org.gotson.komga.domain.model.makeSeries +import org.gotson.komga.domain.persistence.LibraryRepository +import org.gotson.komga.domain.persistence.SeriesRepository +import org.gotson.komga.infrastructure.metadata.comicrack.ComicInfoProvider +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest + +@SpringBootTest +class TransientBookLifecycleTest( + @Autowired private val seriesLifecycle: SeriesLifecycle, + @Autowired private val seriesRepository: SeriesRepository, + @Autowired private val libraryRepository: LibraryRepository, + @Autowired private val transientBookLifecycle: TransientBookLifecycle, +) { + private val library = makeLibrary() + + @SpykBean + private lateinit var mockProvider: ComicInfoProvider + + @BeforeAll + fun `setup library`() { + libraryRepository.insert(library) + } + + @AfterAll + fun teardown() { + libraryRepository.deleteAll() + } + + @AfterEach + fun cleanup() { + seriesLifecycle.deleteMany(seriesRepository.findAll()) + } + + @Test + fun `when getting metadata for transient book then the most specific series name is matched first`() { + seriesLifecycle.createSeries(makeSeries("Batman and Robin", libraryId = library.id)) + val seriesExact = makeSeries("Batman", libraryId = library.id) + seriesLifecycle.createSeries(seriesExact) + seriesLifecycle.createSeries(makeSeries("Batman and Robin (2022)", libraryId = library.id)) + + val book = + TransientBook( + makeBook("whatever"), + Media(), + ) + + every { mockProvider.getBookMetadataFromBook(any()) } returns BookMetadataPatch(null, null, null, 15F, null, null, null, null, null, emptyList()) + every { mockProvider.getSeriesMetadataFromBook(any(), any()) } returns SeriesMetadataPatch("BATMAN", null, null, null, null, null, null, null, null, null, emptySet()) + + val (seriesId, number) = transientBookLifecycle.getMetadata(book) + + assertThat(seriesId).isEqualTo(seriesExact.id) + assertThat(number).isEqualTo(15F) + } +}