diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/TransientBook.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/TransientBook.kt new file mode 100644 index 000000000..b38587570 --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/TransientBook.kt @@ -0,0 +1,14 @@ +package org.gotson.komga.domain.model + +data class TransientBook( + val book: Book, + val media: Media, + val metadata: Metadata = Metadata(), +) { + data class Metadata( + val number: Float? = null, + val seriesId: String? = null, + ) +} + +fun TransientBook.toBookWithMedia() = BookWithMedia(book, media) diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/persistence/SeriesRepository.kt b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/SeriesRepository.kt index d5bde0557..e63636030 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/persistence/SeriesRepository.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/SeriesRepository.kt @@ -11,7 +11,7 @@ interface SeriesRepository { fun findAll(): Collection fun findAllByLibraryId(libraryId: String): Collection fun findAllNotDeletedByLibraryIdAndUrlNotIn(libraryId: String, urls: Collection): Collection - fun findAllByTitle(title: String): Collection + fun findAllByTitleContaining(title: String): Collection fun findAll(search: SeriesSearch): Collection fun getLibraryId(seriesId: String): String? diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/persistence/TransientBookRepository.kt b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/TransientBookRepository.kt index 5907e86ed..1543ff86b 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/persistence/TransientBookRepository.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/TransientBookRepository.kt @@ -1,9 +1,9 @@ package org.gotson.komga.domain.persistence -import org.gotson.komga.domain.model.BookWithMedia +import org.gotson.komga.domain.model.TransientBook interface TransientBookRepository { - fun findByIdOrNull(transientBookId: String): BookWithMedia? - fun save(transientBook: BookWithMedia) - fun save(transientBooks: Collection) + fun findByIdOrNull(transientBookId: String): TransientBook? + fun save(transientBook: TransientBook) + fun save(transientBooks: Collection) } diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/SeriesMetadataLifecycle.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/SeriesMetadataLifecycle.kt index 9b9d1a0e1..19b5e09da 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/SeriesMetadataLifecycle.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/SeriesMetadataLifecycle.kt @@ -52,7 +52,7 @@ class SeriesMetadataLifecycle( val patches = bookRepository.findAllBySeriesId(series.id) .mapNotNull { book -> try { - provider.getSeriesMetadataFromBook(BookWithMedia(book, mediaRepository.findById(book.id)), library) + provider.getSeriesMetadataFromBook(BookWithMedia(book, mediaRepository.findById(book.id)), library.importComicInfoSeriesAppendVolume) } catch (e: Exception) { logger.error(e) { "Error while getting metadata from ${provider.javaClass.simpleName} for book: $book" } null 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 c5bcaa7a5..52c5860e4 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 @@ -1,14 +1,19 @@ package org.gotson.komga.domain.service -import org.gotson.komga.domain.model.BookWithMedia +import org.gotson.komga.domain.model.BookMetadataPatchCapability 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.TransientBook import org.gotson.komga.domain.model.TypedBytes +import org.gotson.komga.domain.model.toBookWithMedia import org.gotson.komga.domain.persistence.LibraryRepository +import org.gotson.komga.domain.persistence.SeriesRepository import org.gotson.komga.domain.persistence.TransientBookRepository 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.stereotype.Service import java.nio.file.Paths @@ -21,37 +26,59 @@ class TransientBookLifecycle( private val libraryRepository: LibraryRepository, @Qualifier("pdfImageType") private val pdfImageType: ImageType, + private val seriesRepository: SeriesRepository, + private val seriesMetadataProviders: List, + bookMetadataProviders: List, ) { - fun scanAndPersist(filePath: String): List { + val bookMetadataProviders = bookMetadataProviders.filter { it.getCapabilities().contains(BookMetadataPatchCapability.NUMBER_SORT) } + + fun scanAndPersist(filePath: String): List { val folderToScan = Paths.get(filePath) libraryRepository.findAll().forEach { library -> if (folderToScan.startsWith(library.path)) throw PathContainedInPath("Cannot scan folder that is part of an existing library", "ERR_1017") } - val books = fileSystemScanner.scanRootFolder(folderToScan).series.values.flatten().map { BookWithMedia(it, Media()) } + val books = fileSystemScanner.scanRootFolder(folderToScan).series.values.flatten().map { TransientBook(it, Media()) } transientBookRepository.save(books) return books } - fun analyzeAndPersist(transientBook: BookWithMedia): BookWithMedia { + fun analyzeAndPersist(transientBook: TransientBook): TransientBook { val media = bookAnalyzer.analyze(transientBook.book, true) + val (seriesId, number) = getMetadata(transientBook.copy(media = media)) - val updated = transientBook.copy(media = media) + val updated = transientBook.copy(media = media, metadata = TransientBook.Metadata(number, seriesId)) transientBookRepository.save(updated) return updated } + fun getMetadata(transientBook: TransientBook): Pair { + val bookWithMedia = transientBook.toBookWithMedia() + val number = bookMetadataProviders.firstNotNullOfOrNull { it.getBookMetadataFromBook(bookWithMedia)?.numberSort } + val series = seriesMetadataProviders + .flatMap { + buildList { + if (it.supportsAppendVolume) add(it.getSeriesMetadataFromBook(bookWithMedia, true)?.title) + add(it.getSeriesMetadataFromBook(bookWithMedia, false)?.title) + } + } + .filterNotNull() + .firstNotNullOfOrNull { seriesRepository.findAllByTitleContaining(it).firstOrNull() } + + return series?.id to number + } + @Throws( MediaNotReadyException::class, IndexOutOfBoundsException::class, ) - fun getBookPage(transientBook: BookWithMedia, number: Int): TypedBytes { - val pageContent = bookAnalyzer.getPageContent(transientBook, number) + fun getBookPage(transientBook: TransientBook, number: Int): TypedBytes { + val pageContent = bookAnalyzer.getPageContent(transientBook.toBookWithMedia(), number) val pageMediaType = if (transientBook.media.profile == MediaProfile.PDF) pdfImageType.mediaType else transientBook.media.pages[number - 1].mediaType diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/cache/TransientBookCache.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/cache/TransientBookCache.kt index de2808018..4c1034050 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/cache/TransientBookCache.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/cache/TransientBookCache.kt @@ -2,7 +2,7 @@ package org.gotson.komga.infrastructure.cache import com.github.benmanes.caffeine.cache.Caffeine import mu.KotlinLogging -import org.gotson.komga.domain.model.BookWithMedia +import org.gotson.komga.domain.model.TransientBook import org.gotson.komga.domain.persistence.TransientBookRepository import org.springframework.stereotype.Service import java.util.concurrent.TimeUnit @@ -13,15 +13,15 @@ private val logger = KotlinLogging.logger {} class TransientBookCache : TransientBookRepository { private val cache = Caffeine.newBuilder() .expireAfterAccess(1, TimeUnit.HOURS) - .build() + .build() - override fun findByIdOrNull(transientBookId: String): BookWithMedia? = cache.getIfPresent(transientBookId) + override fun findByIdOrNull(transientBookId: String): TransientBook? = cache.getIfPresent(transientBookId) - override fun save(transientBook: BookWithMedia) { + override fun save(transientBook: TransientBook) { cache.put(transientBook.book.id, transientBook) } - override fun save(transientBooks: Collection) { + override fun save(transientBooks: Collection) { cache.putAll(transientBooks.associateBy { it.book.id }) } } diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/main/SeriesDao.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/main/SeriesDao.kt index 5f0eb3983..32ecb7668 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/main/SeriesDao.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/main/SeriesDao.kt @@ -67,11 +67,11 @@ class SeriesDao( .firstOrNull() ?.toDomain() - override fun findAllByTitle(title: String): Collection = + override fun findAllByTitleContaining(title: String): Collection = dsl.selectDistinct(*s.fields()) .from(s) .leftJoin(d).on(s.ID.eq(d.SERIES_ID)) - .where(d.TITLE.equalIgnoreCase(title)) + .where(d.TITLE.containsIgnoreCase(title)) .fetchInto(s) .map { it.toDomain() } diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/SeriesMetadataFromBookProvider.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/SeriesMetadataFromBookProvider.kt index b06659ac2..8e8ac482a 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/SeriesMetadataFromBookProvider.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/SeriesMetadataFromBookProvider.kt @@ -1,9 +1,9 @@ package org.gotson.komga.infrastructure.metadata import org.gotson.komga.domain.model.BookWithMedia -import org.gotson.komga.domain.model.Library import org.gotson.komga.domain.model.SeriesMetadataPatch interface SeriesMetadataFromBookProvider : MetadataProvider { - fun getSeriesMetadataFromBook(book: BookWithMedia, library: Library): SeriesMetadataPatch? + val supportsAppendVolume: Boolean + fun getSeriesMetadataFromBook(book: BookWithMedia, appendVolumeToTitle: Boolean): SeriesMetadataPatch? } diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/ComicInfoProvider.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/ComicInfoProvider.kt index ad882b95d..e140c6e76 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/ComicInfoProvider.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/ComicInfoProvider.kt @@ -119,7 +119,9 @@ class ComicInfoProvider( return null } - override fun getSeriesMetadataFromBook(book: BookWithMedia, library: Library): SeriesMetadataPatch? { + override val supportsAppendVolume = true + + override fun getSeriesMetadataFromBook(book: BookWithMedia, appendVolumeToTitle: Boolean): SeriesMetadataPatch? { getComicInfo(book)?.let { comicInfo -> val readingDirection = when (comicInfo.manga) { Manga.NO -> SeriesMetadata.ReadingDirection.LEFT_TO_RIGHT @@ -128,7 +130,7 @@ class ComicInfoProvider( } val genres = comicInfo.genre?.split(',')?.mapNotNull { it.trim().ifBlank { null } } - val series = if (library.importComicInfoSeriesAppendVolume) computeSeriesFromSeriesAndVolume(comicInfo.series, comicInfo.volume) else comicInfo.series + val series = if (appendVolumeToTitle) computeSeriesFromSeriesAndVolume(comicInfo.series, comicInfo.volume) else comicInfo.series return SeriesMetadataPatch( title = series, 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 bd285a069..3cf55b46a 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 @@ -84,7 +84,9 @@ class EpubMetadataProvider( return null } - override fun getSeriesMetadataFromBook(book: BookWithMedia, library: Library): SeriesMetadataPatch? { + override val supportsAppendVolume = false + + override fun getSeriesMetadataFromBook(book: BookWithMedia, appendVolumeToTitle: Boolean): SeriesMetadataPatch? { if (book.media.mediaType != MediaType.EPUB.type) return null getPackageFile(book.book.path)?.let { packageFile -> val opf = Jsoup.parse(packageFile, "", Parser.xmlParser()) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/TransientBooksController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/TransientBooksController.kt index af6674497..d66129811 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/TransientBooksController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/TransientBooksController.kt @@ -2,11 +2,11 @@ package org.gotson.komga.interfaces.api.rest import com.jakewharton.byteunits.BinaryByteUnit import mu.KotlinLogging -import org.gotson.komga.domain.model.BookWithMedia import org.gotson.komga.domain.model.CodedException import org.gotson.komga.domain.model.MediaNotReadyException import org.gotson.komga.domain.model.MediaProfile import org.gotson.komga.domain.model.ROLE_ADMIN +import org.gotson.komga.domain.model.TransientBook import org.gotson.komga.domain.persistence.TransientBookRepository import org.gotson.komga.domain.service.BookAnalyzer import org.gotson.komga.domain.service.TransientBookLifecycle @@ -81,8 +81,7 @@ class TransientBooksController( throw ResponseStatusException(HttpStatus.NOT_FOUND, "File not found, it may have moved") } } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) - - private fun BookWithMedia.toDto(): TransientBookDto { + private fun TransientBook.toDto(): TransientBookDto { val pages = if (media.profile == MediaProfile.PDF) bookAnalyzer.getPdfPagesDynamic(media) else media.pages return TransientBookDto( id = book.id, @@ -104,6 +103,8 @@ class TransientBooksController( }, files = media.files.map { it.fileName }, comment = media.comment ?: "", + number = metadata.number, + seriesId = metadata.seriesId, ) } } @@ -124,4 +125,6 @@ data class TransientBookDto( val pages: List, val files: List, val comment: String, + val number: Float?, + val seriesId: String?, ) diff --git a/komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/ComicInfoProviderTest.kt b/komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/ComicInfoProviderTest.kt index 64004e81f..c7092183d 100644 --- a/komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/ComicInfoProviderTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/ComicInfoProviderTest.kt @@ -12,7 +12,6 @@ import org.gotson.komga.domain.model.MediaFile import org.gotson.komga.domain.model.SeriesMetadata import org.gotson.komga.domain.model.WebLink import org.gotson.komga.domain.model.makeBook -import org.gotson.komga.domain.model.makeLibrary import org.gotson.komga.domain.service.BookAnalyzer import org.gotson.komga.infrastructure.metadata.comicrack.dto.AgeRating import org.gotson.komga.infrastructure.metadata.comicrack.dto.ComicInfo @@ -338,9 +337,6 @@ class ComicInfoProviderTest { @Nested inner class Series { - private val library = makeLibrary() - private val libraryNoAppend = library.copy(importComicInfoSeriesAppendVolume = false) - @Test fun `given comicInfo when getting series metadata then metadata patch is valid`() { val comicInfo = ComicInfo().apply { @@ -356,7 +352,7 @@ class ComicInfoProviderTest { every { mockMapper.readValue(any(), ComicInfo::class.java) } returns comicInfo - val patch = comicInfoProvider.getSeriesMetadataFromBook(BookWithMedia(book, media), library)!! + val patch = comicInfoProvider.getSeriesMetadataFromBook(BookWithMedia(book, media), true)!! with(patch) { assertThat(title).isEqualTo("séries") @@ -382,13 +378,13 @@ class ComicInfoProviderTest { every { mockMapper.readValue(any(), ComicInfo::class.java) } returns comicInfo - val patch = comicInfoProvider.getSeriesMetadataFromBook(BookWithMedia(book, media), library)!! + val patch = comicInfoProvider.getSeriesMetadataFromBook(BookWithMedia(book, media), true)!! with(patch) { assertThat(title).isEqualTo("series (2020)") } - val patchNoAppend = comicInfoProvider.getSeriesMetadataFromBook(BookWithMedia(book, media), libraryNoAppend)!! + val patchNoAppend = comicInfoProvider.getSeriesMetadataFromBook(BookWithMedia(book, media), false)!! with(patchNoAppend) { assertThat(title).isEqualTo("series") @@ -404,13 +400,13 @@ class ComicInfoProviderTest { every { mockMapper.readValue(any(), ComicInfo::class.java) } returns comicInfo - val patch = comicInfoProvider.getSeriesMetadataFromBook(BookWithMedia(book, media), library)!! + val patch = comicInfoProvider.getSeriesMetadataFromBook(BookWithMedia(book, media), true)!! with(patch) { assertThat(title).isEqualTo("series") } - val patchNoAppend = comicInfoProvider.getSeriesMetadataFromBook(BookWithMedia(book, media), libraryNoAppend)!! + val patchNoAppend = comicInfoProvider.getSeriesMetadataFromBook(BookWithMedia(book, media), false)!! with(patchNoAppend) { assertThat(title).isEqualTo("series") @@ -425,7 +421,7 @@ class ComicInfoProviderTest { every { mockMapper.readValue(any(), ComicInfo::class.java) } returns comicInfo - val patch = comicInfoProvider.getSeriesMetadataFromBook(BookWithMedia(book, media), library)!! + val patch = comicInfoProvider.getSeriesMetadataFromBook(BookWithMedia(book, media), true)!! with(patch) { assertThat(language).isNull() @@ -441,7 +437,7 @@ class ComicInfoProviderTest { every { mockMapper.readValue(any(), ComicInfo::class.java) } returns comicInfo - val patch = comicInfoProvider.getSeriesMetadataFromBook(BookWithMedia(book, media), library)!! + val patch = comicInfoProvider.getSeriesMetadataFromBook(BookWithMedia(book, media), true)!! with(patch) { assertThat(language).isEqualTo(expected) @@ -469,7 +465,7 @@ class ComicInfoProviderTest { every { mockMapper.readValue(any(), ComicInfo::class.java) } returns comicInfo - val patch = comicInfoProvider.getSeriesMetadataFromBook(BookWithMedia(book, media), library)!! + val patch = comicInfoProvider.getSeriesMetadataFromBook(BookWithMedia(book, media), true)!! with(patch) { assertThat(title).isNull() diff --git a/komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/epub/EpubMetadataProviderTest.kt b/komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/epub/EpubMetadataProviderTest.kt index 6562a4ec0..dd6a0c69c 100644 --- a/komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/epub/EpubMetadataProviderTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/epub/EpubMetadataProviderTest.kt @@ -10,7 +10,6 @@ import org.gotson.komga.domain.model.BookWithMedia import org.gotson.komga.domain.model.Media import org.gotson.komga.domain.model.SeriesMetadata import org.gotson.komga.domain.model.makeBook -import org.gotson.komga.domain.model.makeLibrary import org.gotson.komga.infrastructure.mediacontainer.epub.getPackageFile import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Nested @@ -122,15 +121,13 @@ class EpubMetadataProviderTest { @Nested inner class Series { - private val library = makeLibrary() - @Test fun `given epub 3 opf when getting series metadata then metadata patch is valid`() { val opf = ClassPathResource("epub/Panik im Paradies.opf") mockkStatic(::getPackageFile) every { getPackageFile(any()) } returns opf.file.readText() - val patch = epubMetadataProvider.getSeriesMetadataFromBook(BookWithMedia(book, media), library) + val patch = epubMetadataProvider.getSeriesMetadataFromBook(BookWithMedia(book, media), true) with(patch!!) { assertThat(title).isEqualTo("Die drei ??? Kids") @@ -148,7 +145,7 @@ class EpubMetadataProviderTest { mockkStatic(::getPackageFile) every { getPackageFile(any()) } returns opf.file.readText() - val patch = epubMetadataProvider.getSeriesMetadataFromBook(BookWithMedia(book, media), library) + val patch = epubMetadataProvider.getSeriesMetadataFromBook(BookWithMedia(book, media), true) with(patch!!) { assertThat(title).isEqualTo("Die drei ??? Kids") @@ -166,7 +163,7 @@ class EpubMetadataProviderTest { mockkStatic(::getPackageFile) every { getPackageFile(any()) } returns opf.file.readText() - val patch = epubMetadataProvider.getSeriesMetadataFromBook(BookWithMedia(book, media), library) + val patch = epubMetadataProvider.getSeriesMetadataFromBook(BookWithMedia(book, media), true) with(patch!!) { assertThat(title).isNull()