diff --git a/komga-webui/src/components/dialogs/LibraryEditDialog.vue b/komga-webui/src/components/dialogs/LibraryEditDialog.vue
index d14292bd0..551df3993 100644
--- a/komga-webui/src/components/dialogs/LibraryEditDialog.vue
+++ b/komga-webui/src/components/dialogs/LibraryEditDialog.vue
@@ -87,11 +87,11 @@
label="Series title"
hide-details
/>
-
-
-
-
-
+
diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/SeriesMetadataPatch.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/SeriesMetadataPatch.kt
index 6dbf08c3a..a86fed28f 100644
--- a/komga/src/main/kotlin/org/gotson/komga/domain/model/SeriesMetadataPatch.kt
+++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/SeriesMetadataPatch.kt
@@ -3,5 +3,7 @@ package org.gotson.komga.domain.model
data class SeriesMetadataPatch(
val title: String?,
val titleSort: String?,
- val status: SeriesMetadata.Status?
+ val status: SeriesMetadata.Status?,
+
+ val collections: List = emptyList()
)
diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/persistence/SeriesCollectionRepository.kt b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/SeriesCollectionRepository.kt
index db0ae516d..1d884a454 100644
--- a/komga/src/main/kotlin/org/gotson/komga/domain/persistence/SeriesCollectionRepository.kt
+++ b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/SeriesCollectionRepository.kt
@@ -26,6 +26,8 @@ interface SeriesCollectionRepository {
*/
fun findAllBySeries(containsSeriesId: Long, filterOnLibraryIds: Collection?): Collection
+ fun findByNameOrNull(name: String): SeriesCollection?
+
fun insert(collection: SeriesCollection): SeriesCollection
fun update(collection: SeriesCollection)
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 3442de955..b9a03f8ea 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
@@ -3,11 +3,13 @@ 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.SeriesCollection
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.SeriesCollectionRepository
import org.gotson.komga.domain.persistence.SeriesMetadataRepository
import org.gotson.komga.infrastructure.metadata.BookMetadataProvider
import org.gotson.komga.infrastructure.metadata.SeriesMetadataProvider
@@ -26,7 +28,9 @@ class MetadataLifecycle(
private val bookMetadataRepository: BookMetadataRepository,
private val seriesMetadataRepository: SeriesMetadataRepository,
private val libraryRepository: LibraryRepository,
- private val bookRepository: BookRepository
+ private val bookRepository: BookRepository,
+ private val collectionRepository: SeriesCollectionRepository,
+ private val collectionLifecycle: SeriesCollectionLifecycle
) {
fun refreshMetadata(book: Book) {
@@ -63,31 +67,59 @@ class MetadataLifecycle(
seriesMetadataProviders.forEach { provider ->
when {
- provider is ComicInfoProvider && !library.importComicInfoSeries -> logger.info { "Library is not set to import series metadata from ComicInfo, skipping" }
+ provider is ComicInfoProvider && !library.importComicInfoSeries && !library.importComicInfoCollection -> logger.info { "Library is not set to import series and collection 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 }
+ // handle series metadata
+ if ((provider is ComicInfoProvider && library.importComicInfoSeries) ||
+ (provider is EpubMetadataProvider && !library.importEpubSeries)) {
+ 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" }
+ 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)
+ val aggregatedPatch = SeriesMetadataPatch(title, titleSort, status)
- seriesMetadataRepository.findById(series.id).let {
- logger.debug { "Apply metadata for series: $series" }
+ seriesMetadataRepository.findById(series.id).let {
+ logger.debug { "Apply metadata for series: $series" }
- logger.debug { "Original metadata: $it" }
- val patched = metadataApplier.apply(aggregatedPatch, it)
- logger.debug { "Patched metadata: $patched" }
+ logger.debug { "Original metadata: $it" }
+ val patched = metadataApplier.apply(aggregatedPatch, it)
+ logger.debug { "Patched metadata: $patched" }
- seriesMetadataRepository.update(patched)
+ seriesMetadataRepository.update(patched)
+ }
+ }
+
+ // add series to collections
+ if (provider is ComicInfoProvider && library.importComicInfoCollection) {
+ patches.flatMap { it.collections }.distinct().forEach { collection ->
+ collectionRepository.findByNameOrNull(collection).let { existing ->
+ if (existing != null) {
+ if (existing.seriesIds.contains(series.id))
+ logger.debug { "Series is already in existing collection ${existing.name}" }
+ else {
+ logger.debug { "Adding series ${series.name} to existing collection ${existing.name}" }
+ collectionLifecycle.updateCollection(
+ existing.copy(seriesIds = existing.seriesIds + series.id)
+ )
+ }
+ } else {
+ logger.debug { "Adding series ${series.name} to new collection $collection" }
+ collectionLifecycle.addCollection(SeriesCollection(
+ name = collection,
+ seriesIds = listOf(series.id)
+ ))
+ }
+ }
+ }
}
}
}
diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesCollectionDao.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesCollectionDao.kt
index 9815428ff..17155360c 100644
--- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesCollectionDao.kt
+++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesCollectionDao.kt
@@ -113,6 +113,12 @@ class SeriesCollectionDao(
.fetchAndMap(filterOnLibraryIds)
}
+ override fun findByNameOrNull(name: String): SeriesCollection? =
+ selectBase()
+ .where(c.NAME.equalIgnoreCase(name))
+ .fetchAndMap(null)
+ .firstOrNull()
+
private fun selectBase() =
dsl.selectDistinct(*c.fields())
.from(c)
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 fb5fb5baa..2ac7fd44f 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
@@ -68,7 +68,8 @@ class ComicInfoProvider(
return SeriesMetadataPatch(
comicInfo.series,
comicInfo.series,
- null
+ null,
+ listOfNotNull(comicInfo.seriesGroup)
)
}
return null
diff --git a/komga/src/main/resources/application-dev.yml b/komga/src/main/resources/application-dev.yml
index a6dbb30b7..06188496b 100644
--- a/komga/src/main/resources/application-dev.yml
+++ b/komga/src/main/resources/application-dev.yml
@@ -24,7 +24,7 @@ logging:
org.apache.activemq.audit.message: WARN
# org.jooq: DEBUG
# web: DEBUG
-# org.gotson.komga: DEBUG
+ org.gotson.komga: DEBUG
# org.springframework.jms: DEBUG
# org.springframework.security.web.FilterChainProxy: DEBUG
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 9cead5cc4..4b28fd8b4 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
@@ -153,6 +153,7 @@ class ComicInfoProviderTest {
fun `given comicInfo when getting series metadata then metadata patch is valid`() {
val comicInfo = ComicInfo().apply {
series = "series"
+ seriesGroup = "collection"
}
every { mockMapper.readValue(any(), ComicInfo::class.java) } returns comicInfo
@@ -163,7 +164,9 @@ class ComicInfoProviderTest {
assertThat(title).isEqualTo("series")
assertThat(titleSort).isEqualTo("series")
assertThat(status).isNull()
+ assertThat(collections).containsExactly("collection")
}
}
+
}
}
diff --git a/komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/comicinfo/dto/ComicInfoTest.kt b/komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/comicinfo/dto/ComicInfoTest.kt
index 6ffe75cfd..5a232fcc7 100644
--- a/komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/comicinfo/dto/ComicInfoTest.kt
+++ b/komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/comicinfo/dto/ComicInfoTest.kt
@@ -28,6 +28,7 @@ class ComicInfoTest {
assertThat(ageRating).isEqualTo(AgeRating.MATURE_17)
assertThat(blackAndWhite).isEqualTo(YesNo.NO)
assertThat(manga).isEqualTo(Manga.NO)
+ assertThat(seriesGroup).isEqualTo("Sandman")
}
}
diff --git a/komga/src/test/resources/comicinfo/ComicInfo.xml b/komga/src/test/resources/comicinfo/ComicInfo.xml
index 3124538e1..8f86275fd 100644
--- a/komga/src/test/resources/comicinfo/ComicInfo.xml
+++ b/komga/src/test/resources/comicinfo/ComicInfo.xml
@@ -1,15 +1,23 @@
-
- v01 - Preludes & Nocturnes - 30th Anniversary Edition
- Sandman
- https://www.comixology.com/Sandman/digital-comic/727888
- Neil Gaiman's seminal series, THE SANDMAN, celebrates its 30th anniversary with an all-new edition of THE SANDMAN VOL. 1: PRELUDES & NOCTURNES!
+
+ v01 - Preludes & Nocturnes - 30th Anniversary Edition
+ Sandman
+ https://www.comixology.com/Sandman/digital-comic/727888
+ Neil Gaiman's seminal series, THE SANDMAN, celebrates its 30th anniversary with an all-new edition of THE
+ SANDMAN VOL. 1: PRELUDES & NOCTURNES!
- New York Times best-selling author Neil Gaiman's transcendent series THE SANDMAN is often hailed as the definitive Vertigo title and one of the finest achievements in graphic storytelling. Gaiman created an unforgettable tale of the forces that exist beyond life and death by weaving ancient mythology, folklore and fairy tales with his own distinct narrative vision.
+ New York Times best-selling author Neil Gaiman's transcendent series THE SANDMAN is often hailed as the
+ definitive Vertigo title and one of the finest achievements in graphic storytelling. Gaiman created an
+ unforgettable tale of the forces that exist beyond life and death by weaving ancient mythology, folklore and
+ fairy tales with his own distinct narrative vision.
- In PRELUDES & NOCTURNES, an occultist attempting to capture Death to bargain for eternal life traps her younger brother Dream instead. After his 70 year imprisonment and eventual escape, Dream, also known as Morpheus, goes on a quest for his lost objects of power. On his arduous journey Morpheus encounters Lucifer, John Constantine, and an all-powerful madman.
+ In PRELUDES & NOCTURNES, an occultist attempting to capture Death to bargain for eternal life traps her
+ younger brother Dream instead. After his 70 year imprisonment and eventual escape, Dream, also known as
+ Morpheus, goes on a quest for his lost objects of power. On his arduous journey Morpheus encounters Lucifer,
+ John Constantine, and an all-powerful madman.
- This book also includes the story "The Sound of Her Wings," which introduces us to the pragmatic and perky goth girl Death.
+ This book also includes the story "The Sound of Her Wings," which introduces us to the pragmatic and perky goth
+ girl Death.
Collects THE SANDMAN #1-8.
Scraped metadata from Comixology [CMXDB727888], [RELDATE:2018-10-30]
@@ -21,5 +29,6 @@
Mature 17+
No
No
+ Sandman