feat: generate collections from ComicInfo SeriesGroup

optional behavior that can be set per library

closes #210
This commit is contained in:
Gauthier Roebroeck 2020-07-03 16:45:51 +08:00
parent 0d20c2a464
commit 277cdcd4e3
10 changed files with 87 additions and 31 deletions

View file

@ -87,11 +87,11 @@
label="Series title" label="Series title"
hide-details hide-details
/> />
<!-- <v-checkbox--> <v-checkbox
<!-- v-model="form.importComicInfoCollection"--> v-model="form.importComicInfoCollection"
<!-- label="Collections"--> label="Collections"
<!-- hide-details--> hide-details
<!-- />--> />
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row>

View file

@ -3,5 +3,7 @@ package org.gotson.komga.domain.model
data class SeriesMetadataPatch( data class SeriesMetadataPatch(
val title: String?, val title: String?,
val titleSort: String?, val titleSort: String?,
val status: SeriesMetadata.Status? val status: SeriesMetadata.Status?,
val collections: List<String> = emptyList()
) )

View file

@ -26,6 +26,8 @@ interface SeriesCollectionRepository {
*/ */
fun findAllBySeries(containsSeriesId: Long, filterOnLibraryIds: Collection<Long>?): Collection<SeriesCollection> fun findAllBySeries(containsSeriesId: Long, filterOnLibraryIds: Collection<Long>?): Collection<SeriesCollection>
fun findByNameOrNull(name: String): SeriesCollection?
fun insert(collection: SeriesCollection): SeriesCollection fun insert(collection: SeriesCollection): SeriesCollection
fun update(collection: SeriesCollection) fun update(collection: SeriesCollection)

View file

@ -3,11 +3,13 @@ package org.gotson.komga.domain.service
import mu.KotlinLogging import mu.KotlinLogging
import org.gotson.komga.domain.model.Book import org.gotson.komga.domain.model.Book
import org.gotson.komga.domain.model.Series 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.model.SeriesMetadataPatch
import org.gotson.komga.domain.persistence.BookMetadataRepository import org.gotson.komga.domain.persistence.BookMetadataRepository
import org.gotson.komga.domain.persistence.BookRepository import org.gotson.komga.domain.persistence.BookRepository
import org.gotson.komga.domain.persistence.LibraryRepository import org.gotson.komga.domain.persistence.LibraryRepository
import org.gotson.komga.domain.persistence.MediaRepository 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.domain.persistence.SeriesMetadataRepository
import org.gotson.komga.infrastructure.metadata.BookMetadataProvider import org.gotson.komga.infrastructure.metadata.BookMetadataProvider
import org.gotson.komga.infrastructure.metadata.SeriesMetadataProvider import org.gotson.komga.infrastructure.metadata.SeriesMetadataProvider
@ -26,7 +28,9 @@ class MetadataLifecycle(
private val bookMetadataRepository: BookMetadataRepository, private val bookMetadataRepository: BookMetadataRepository,
private val seriesMetadataRepository: SeriesMetadataRepository, private val seriesMetadataRepository: SeriesMetadataRepository,
private val libraryRepository: LibraryRepository, private val libraryRepository: LibraryRepository,
private val bookRepository: BookRepository private val bookRepository: BookRepository,
private val collectionRepository: SeriesCollectionRepository,
private val collectionLifecycle: SeriesCollectionLifecycle
) { ) {
fun refreshMetadata(book: Book) { fun refreshMetadata(book: Book) {
@ -63,31 +67,59 @@ class MetadataLifecycle(
seriesMetadataProviders.forEach { provider -> seriesMetadataProviders.forEach { provider ->
when { 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" } provider is EpubMetadataProvider && !library.importEpubSeries -> logger.info { "Library is not set to import series metadata from Epub, skipping" }
else -> { else -> {
logger.debug { "Provider: $provider" } logger.debug { "Provider: $provider" }
val patches = bookRepository.findBySeriesId(series.id) val patches = bookRepository.findBySeriesId(series.id)
.mapNotNull { provider.getSeriesMetadataFromBook(it, mediaRepository.findById(it.id)) } .mapNotNull { provider.getSeriesMetadataFromBook(it, mediaRepository.findById(it.id)) }
val title = patches.uniqueOrNull { it.title } // handle series metadata
val titleSort = patches.uniqueOrNull { it.titleSort } if ((provider is ComicInfoProvider && library.importComicInfoSeries) ||
val status = patches.uniqueOrNull { it.status } (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 (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 (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 (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 { seriesMetadataRepository.findById(series.id).let {
logger.debug { "Apply metadata for series: $series" } logger.debug { "Apply metadata for series: $series" }
logger.debug { "Original metadata: $it" } logger.debug { "Original metadata: $it" }
val patched = metadataApplier.apply(aggregatedPatch, it) val patched = metadataApplier.apply(aggregatedPatch, it)
logger.debug { "Patched metadata: $patched" } 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)
))
}
}
}
} }
} }
} }

View file

@ -113,6 +113,12 @@ class SeriesCollectionDao(
.fetchAndMap(filterOnLibraryIds) .fetchAndMap(filterOnLibraryIds)
} }
override fun findByNameOrNull(name: String): SeriesCollection? =
selectBase()
.where(c.NAME.equalIgnoreCase(name))
.fetchAndMap(null)
.firstOrNull()
private fun selectBase() = private fun selectBase() =
dsl.selectDistinct(*c.fields()) dsl.selectDistinct(*c.fields())
.from(c) .from(c)

View file

@ -68,7 +68,8 @@ class ComicInfoProvider(
return SeriesMetadataPatch( return SeriesMetadataPatch(
comicInfo.series, comicInfo.series,
comicInfo.series, comicInfo.series,
null null,
listOfNotNull(comicInfo.seriesGroup)
) )
} }
return null return null

View file

@ -24,7 +24,7 @@ logging:
org.apache.activemq.audit.message: WARN org.apache.activemq.audit.message: WARN
# org.jooq: DEBUG # org.jooq: DEBUG
# web: DEBUG # web: DEBUG
# org.gotson.komga: DEBUG org.gotson.komga: DEBUG
# org.springframework.jms: DEBUG # org.springframework.jms: DEBUG
# org.springframework.security.web.FilterChainProxy: DEBUG # org.springframework.security.web.FilterChainProxy: DEBUG

View file

@ -153,6 +153,7 @@ class ComicInfoProviderTest {
fun `given comicInfo when getting series metadata then metadata patch is valid`() { fun `given comicInfo when getting series metadata then metadata patch is valid`() {
val comicInfo = ComicInfo().apply { val comicInfo = ComicInfo().apply {
series = "series" series = "series"
seriesGroup = "collection"
} }
every { mockMapper.readValue(any<ByteArray>(), ComicInfo::class.java) } returns comicInfo every { mockMapper.readValue(any<ByteArray>(), ComicInfo::class.java) } returns comicInfo
@ -163,7 +164,9 @@ class ComicInfoProviderTest {
assertThat(title).isEqualTo("series") assertThat(title).isEqualTo("series")
assertThat(titleSort).isEqualTo("series") assertThat(titleSort).isEqualTo("series")
assertThat(status).isNull() assertThat(status).isNull()
assertThat(collections).containsExactly("collection")
} }
} }
} }
} }

View file

@ -28,6 +28,7 @@ class ComicInfoTest {
assertThat(ageRating).isEqualTo(AgeRating.MATURE_17) assertThat(ageRating).isEqualTo(AgeRating.MATURE_17)
assertThat(blackAndWhite).isEqualTo(YesNo.NO) assertThat(blackAndWhite).isEqualTo(YesNo.NO)
assertThat(manga).isEqualTo(Manga.NO) assertThat(manga).isEqualTo(Manga.NO)
assertThat(seriesGroup).isEqualTo("Sandman")
} }
} }

View file

@ -1,15 +1,23 @@
<?xml version="1.0"?> <?xml version="1.0"?>
<ComicInfo xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <ComicInfo>
<Title>v01 - Preludes &amp; Nocturnes - 30th Anniversary Edition</Title> <Title>v01 - Preludes &amp; Nocturnes - 30th Anniversary Edition</Title>
<Series>Sandman</Series> <Series>Sandman</Series>
<Web>https://www.comixology.com/Sandman/digital-comic/727888</Web> <Web>https://www.comixology.com/Sandman/digital-comic/727888</Web>
<Summary>Neil Gaiman's seminal series, THE SANDMAN, celebrates its 30th anniversary with an all-new edition of THE SANDMAN VOL. 1: PRELUDES &amp; NOCTURNES! <Summary>Neil Gaiman's seminal series, THE SANDMAN, celebrates its 30th anniversary with an all-new edition of THE
SANDMAN VOL. 1: PRELUDES &amp; 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 &amp; 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 &amp; 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.</Summary>  Collects THE SANDMAN #1-8.</Summary>
<Notes>Scraped metadata from Comixology [CMXDB727888], [RELDATE:2018-10-30]</Notes> <Notes>Scraped metadata from Comixology [CMXDB727888], [RELDATE:2018-10-30]</Notes>
@ -21,5 +29,6 @@
<AgeRating>Mature 17+</AgeRating> <AgeRating>Mature 17+</AgeRating>
<BlackAndWhite>No</BlackAndWhite> <BlackAndWhite>No</BlackAndWhite>
<Manga>No</Manga> <Manga>No</Manga>
<SeriesGroup>Sandman</SeriesGroup>
<ScanInformation></ScanInformation> <ScanInformation></ScanInformation>
</ComicInfo> </ComicInfo>