fix(api): better matching of series name when importing books with metadata

This commit is contained in:
Gauthier Roebroeck 2025-02-25 11:44:30 +08:00
parent ca93ed823e
commit 8b6d23e930
2 changed files with 86 additions and 2 deletions

View file

@ -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<String?, Float?> {
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
}

View file

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