feat: import ComicRack lists as read lists

Closes #464
This commit is contained in:
Gauthier Roebroeck 2021-03-16 10:10:51 +08:00
parent e5343d7ab4
commit c1e435762c
27 changed files with 787 additions and 24 deletions

View file

@ -11,3 +11,10 @@ ERR_1005 | Unknown error while analyzing book
ERR_1006 | Book does not contain any page ERR_1006 | Book does not contain any page
ERR_1007 | Some entries could not be analyzed ERR_1007 | Some entries could not be analyzed
ERR_1008 | Unknown error while getting book's entries ERR_1008 | Unknown error while getting book's entries
ERR_1009 | A read list with that name already exists
ERR_1010 | No books were matched within the read list request
ERR_1011 | No unique match for series
ERR_1012 | No match for series
ERR_1013 | No unique match for book number within series
ERR_1014 | No match for book number within series
ERR_1015 | Error while deserializing ComicRack ReadingList

View file

@ -0,0 +1,25 @@
package org.gotson.komga.domain.model
/**
* Represents a request to create a reading list.
*/
data class ReadListRequest(
val name: String,
val books: List<ReadListRequestBook>,
)
data class ReadListRequestBook(
val series: String,
val number: Int,
)
data class ReadListRequestResult(
val readList: ReadList?,
val unmatchedBooks: List<ReadListRequestResultBook> = emptyList(),
val errorCode: String = "",
)
data class ReadListRequestResultBook(
val book: ReadListRequestBook,
val errorCode: String = "",
)

View file

@ -11,6 +11,7 @@ interface SeriesRepository {
fun findByLibraryIdAndUrlNotIn(libraryId: String, urls: Collection<URL>): Collection<Series> fun findByLibraryIdAndUrlNotIn(libraryId: String, urls: Collection<URL>): Collection<Series>
fun findByLibraryIdAndUrl(libraryId: String, url: URL): Series? fun findByLibraryIdAndUrl(libraryId: String, url: URL): Series?
fun findAll(search: SeriesSearch): Collection<Series> fun findAll(search: SeriesSearch): Collection<Series>
fun findByTitle(title: String): Collection<Series>
fun getLibraryId(seriesId: String): String? fun getLibraryId(seriesId: String): String?

View file

@ -18,7 +18,7 @@ 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
import org.gotson.komga.infrastructure.metadata.barcode.IsbnBarcodeProvider import org.gotson.komga.infrastructure.metadata.barcode.IsbnBarcodeProvider
import org.gotson.komga.infrastructure.metadata.comicinfo.ComicInfoProvider import org.gotson.komga.infrastructure.metadata.comicrack.ComicInfoProvider
import org.gotson.komga.infrastructure.metadata.epub.EpubMetadataProvider import org.gotson.komga.infrastructure.metadata.epub.EpubMetadataProvider
import org.gotson.komga.infrastructure.metadata.localartwork.LocalArtworkProvider import org.gotson.komga.infrastructure.metadata.localartwork.LocalArtworkProvider
import org.springframework.stereotype.Service import org.springframework.stereotype.Service

View file

@ -3,8 +3,10 @@ package org.gotson.komga.domain.service
import mu.KotlinLogging import mu.KotlinLogging
import org.gotson.komga.domain.model.DuplicateNameException import org.gotson.komga.domain.model.DuplicateNameException
import org.gotson.komga.domain.model.ReadList import org.gotson.komga.domain.model.ReadList
import org.gotson.komga.domain.model.ReadListRequestResult
import org.gotson.komga.domain.persistence.ReadListRepository import org.gotson.komga.domain.persistence.ReadListRepository
import org.gotson.komga.infrastructure.image.MosaicGenerator import org.gotson.komga.infrastructure.image.MosaicGenerator
import org.gotson.komga.infrastructure.metadata.comicrack.ReadListProvider
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
@ -13,7 +15,9 @@ private val logger = KotlinLogging.logger {}
class ReadListLifecycle( class ReadListLifecycle(
private val readListRepository: ReadListRepository, private val readListRepository: ReadListRepository,
private val bookLifecycle: BookLifecycle, private val bookLifecycle: BookLifecycle,
private val mosaicGenerator: MosaicGenerator private val mosaicGenerator: MosaicGenerator,
private val readListMatcher: ReadListMatcher,
private val readListProvider: ReadListProvider,
) { ) {
@Throws( @Throws(
@ -55,4 +59,21 @@ class ReadListLifecycle(
val images = ids.mapNotNull { bookLifecycle.getThumbnailBytes(it) } val images = ids.mapNotNull { bookLifecycle.getThumbnailBytes(it) }
return mosaicGenerator.createMosaic(images) return mosaicGenerator.createMosaic(images)
} }
fun importReadList(fileContent: ByteArray): ReadListRequestResult {
val request = try {
readListProvider.importFromCbl(fileContent) ?: return ReadListRequestResult(null, emptyList(), "ERR_1015")
} catch (e: Exception) {
return ReadListRequestResult(null, emptyList(), "ERR_1015")
}
val result = readListMatcher.matchReadListRequest(request)
return when {
result.readList != null -> {
readListRepository.insert(result.readList)
result.copy(readList = readListRepository.findByIdOrNull(result.readList.id)!!)
}
else -> result
}
}
} }

View file

@ -0,0 +1,67 @@
package org.gotson.komga.domain.service
import mu.KotlinLogging
import org.gotson.komga.domain.model.ReadList
import org.gotson.komga.domain.model.ReadListRequest
import org.gotson.komga.domain.model.ReadListRequestResult
import org.gotson.komga.domain.model.ReadListRequestResultBook
import org.gotson.komga.domain.persistence.BookMetadataRepository
import org.gotson.komga.domain.persistence.BookRepository
import org.gotson.komga.domain.persistence.ReadListRepository
import org.gotson.komga.domain.persistence.SeriesRepository
import org.gotson.komga.infrastructure.language.toIndexedMap
import org.springframework.stereotype.Service
private val logger = KotlinLogging.logger {}
@Service
class ReadListMatcher(
private val readListRepository: ReadListRepository,
private val seriesRepository: SeriesRepository,
private val bookRepository: BookRepository,
private val bookMetadataRepository: BookMetadataRepository,
) {
fun matchReadListRequest(request: ReadListRequest): ReadListRequestResult {
logger.info { "Trying to match $request" }
if (readListRepository.existsByName(request.name)) {
return ReadListRequestResult(readList = null, unmatchedBooks = request.books.map { ReadListRequestResultBook(it) }, errorCode = "ERR_1009")
}
val bookIds = mutableListOf<String>()
val unmatchedBooks = mutableListOf<ReadListRequestResultBook>()
request.books.forEach { book ->
val seriesMatches = seriesRepository.findByTitle(book.series)
when {
seriesMatches.size > 1 -> unmatchedBooks += ReadListRequestResultBook(book, "ERR_1011")
seriesMatches.isEmpty() -> unmatchedBooks += ReadListRequestResultBook(book, "ERR_1012")
else -> {
val seriesId = seriesMatches.first().id
val seriesBooks = bookRepository.findBySeriesId(seriesId)
val bookMatches = bookMetadataRepository.findByIds(seriesBooks.map { it.id })
.filter { it.number.toIntOrNull()?.equals(book.number) ?: false }
.map { it.bookId }
when {
bookMatches.size > 1 -> unmatchedBooks += ReadListRequestResultBook(book, "ERR_1013")
bookMatches.isEmpty() -> unmatchedBooks += ReadListRequestResultBook(book, "ERR_1014")
else -> bookIds.add(bookMatches.first())
}
}
}
}
return if (bookIds.isNotEmpty())
ReadListRequestResult(
readList = ReadList(name = request.name, bookIds = bookIds.toIndexedMap()),
unmatchedBooks = unmatchedBooks
)
else {
ReadListRequestResult(
readList = null,
unmatchedBooks = unmatchedBooks,
errorCode = "ERR_1010"
)
}
}
}

View file

@ -51,6 +51,14 @@ class SeriesDao(
.fetchOneInto(s) .fetchOneInto(s)
?.toDomain() ?.toDomain()
override fun findByTitle(title: String): Collection<Series> =
dsl.selectDistinct(*s.fields())
.from(s)
.leftJoin(d).on(s.ID.eq(d.SERIES_ID))
.where(d.TITLE.equalIgnoreCase(title))
.fetchInto(s)
.map { it.toDomain() }
override fun getLibraryId(seriesId: String): String? = override fun getLibraryId(seriesId: String): String? =
dsl.select(s.LIBRARY_ID) dsl.select(s.LIBRARY_ID)
.from(s) .from(s)

View file

@ -1,4 +1,4 @@
package org.gotson.komga.infrastructure.metadata.comicinfo package org.gotson.komga.infrastructure.metadata.comicrack
import com.fasterxml.jackson.dataformat.xml.XmlMapper import com.fasterxml.jackson.dataformat.xml.XmlMapper
import mu.KotlinLogging import mu.KotlinLogging
@ -11,8 +11,8 @@ import org.gotson.komga.domain.model.SeriesMetadataPatch
import org.gotson.komga.domain.service.BookAnalyzer import org.gotson.komga.domain.service.BookAnalyzer
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
import org.gotson.komga.infrastructure.metadata.comicinfo.dto.ComicInfo import org.gotson.komga.infrastructure.metadata.comicrack.dto.ComicInfo
import org.gotson.komga.infrastructure.metadata.comicinfo.dto.Manga import org.gotson.komga.infrastructure.metadata.comicrack.dto.Manga
import org.gotson.komga.infrastructure.validation.BCP47TagValidator import org.gotson.komga.infrastructure.validation.BCP47TagValidator
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@ -45,7 +45,12 @@ class ComicInfoProvider(
val readLists = mutableListOf<BookMetadataPatch.ReadListEntry>() val readLists = mutableListOf<BookMetadataPatch.ReadListEntry>()
if (!comicInfo.alternateSeries.isNullOrBlank()) { if (!comicInfo.alternateSeries.isNullOrBlank()) {
readLists.add(BookMetadataPatch.ReadListEntry(comicInfo.alternateSeries!!, comicInfo.alternateNumber?.toIntOrNull())) readLists.add(
BookMetadataPatch.ReadListEntry(
comicInfo.alternateSeries!!,
comicInfo.alternateNumber?.toIntOrNull()
)
)
} }
comicInfo.storyArc?.let { value -> comicInfo.storyArc?.let { value ->
@ -75,9 +80,7 @@ class ComicInfoProvider(
} }
val genres = comicInfo.genre?.split(',')?.mapNotNull { it.trim().ifBlank { null } } val genres = comicInfo.genre?.split(',')?.mapNotNull { it.trim().ifBlank { null } }
val series = comicInfo.series?.ifBlank { null }?.let { series -> val series = computeSeriesFromSeriesAndVolume(comicInfo.series, comicInfo.volume)
series + (comicInfo.volume?.let { if (it != 1) " ($it)" else "" } ?: "")
}
return SeriesMetadataPatch( return SeriesMetadataPatch(
title = series, title = series,
@ -115,3 +118,8 @@ class ComicInfoProvider(
if (list.isNotEmpty()) list.map { Author(it, role) } else null if (list.isNotEmpty()) list.map { Author(it, role) } else null
} }
} }
fun computeSeriesFromSeriesAndVolume(series: String?, volume: Int?): String? =
series?.ifBlank { null }?.let { s ->
s + (volume?.let { if (it != 1) " ($it)" else "" } ?: "")
}

View file

@ -0,0 +1,48 @@
package org.gotson.komga.infrastructure.metadata.comicrack
import com.fasterxml.jackson.dataformat.xml.XmlMapper
import mu.KotlinLogging
import org.gotson.komga.domain.model.ReadListRequest
import org.gotson.komga.domain.model.ReadListRequestBook
import org.gotson.komga.infrastructure.metadata.comicrack.dto.ReadingList
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
private val logger = KotlinLogging.logger {}
@Service
class ReadListProvider(
@Autowired(required = false) private val mapper: XmlMapper = XmlMapper(),
) {
fun importFromCbl(cbl: ByteArray): ReadListRequest? {
try {
val readingList = mapper.readValue(cbl, ReadingList::class.java)
if (readingList.books.isNotEmpty()) {
logger.debug { "Trying to convert ComicRack ReadingList to ReadListRequest: $readingList" }
if (readingList.name.isNullOrBlank()) {
logger.warn { "ReadingList has no name, skipping" }
return null
}
val books = readingList.books.mapNotNull {
val series = computeSeriesFromSeriesAndVolume(it.series, it.volume)
if (!series.isNullOrBlank() && it.number != null)
ReadListRequestBook(series, it.number!!)
else {
logger.warn { "Book is missing series or number, skipping: $it" }
null
}
}
if (books.isNotEmpty())
return ReadListRequest(name = readingList.name!!, books = books)
.also { logger.debug { "Converted request: $it" } }
}
} catch (e: Exception) {
logger.error(e) { "Error while trying to parse ComicRack ReadingList" }
}
return null
}
}

View file

@ -1,4 +1,4 @@
package org.gotson.komga.infrastructure.metadata.comicinfo.dto package org.gotson.komga.infrastructure.metadata.comicrack.dto
import com.fasterxml.jackson.annotation.JsonCreator import com.fasterxml.jackson.annotation.JsonCreator

View file

@ -0,0 +1,26 @@
package org.gotson.komga.infrastructure.metadata.comicrack.dto
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty
@JsonIgnoreProperties(ignoreUnknown = true)
class Book {
@JsonProperty(value = "Series")
var series: String? = null
@JsonProperty(value = "Number")
var number: Int? = null
@JsonProperty(value = "Volume")
var volume: Int? = null
@JsonProperty(value = "Year")
var year: Int? = null
@JsonProperty(value = "FileName")
var fileName: String? = null
override fun toString(): String {
return "Book(series=$series, number=$number, volume=$volume, year=$year, fileName=$fileName)"
}
}

View file

@ -1,4 +1,4 @@
package org.gotson.komga.infrastructure.metadata.comicinfo.dto package org.gotson.komga.infrastructure.metadata.comicrack.dto
import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty

View file

@ -1,4 +1,4 @@
package org.gotson.komga.infrastructure.metadata.comicinfo.dto package org.gotson.komga.infrastructure.metadata.comicrack.dto
import com.fasterxml.jackson.annotation.JsonCreator import com.fasterxml.jackson.annotation.JsonCreator

View file

@ -0,0 +1,24 @@
package org.gotson.komga.infrastructure.metadata.comicrack.dto
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.annotation.JsonSetter
import com.fasterxml.jackson.annotation.Nulls
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty
@JsonIgnoreProperties(ignoreUnknown = true)
class ReadingList {
@JsonProperty(value = "Name")
var name: String? = null
@JacksonXmlElementWrapper(useWrapping = true)
@JacksonXmlProperty(localName = "Books")
@JsonSetter(nulls = Nulls.AS_EMPTY)
var books: List<Book> = emptyList()
override fun toString(): String {
return "ReadingList(name=$name, books=$books)"
}
}

View file

@ -1,4 +1,4 @@
package org.gotson.komga.infrastructure.metadata.comicinfo.dto package org.gotson.komga.infrastructure.metadata.comicrack.dto
import com.fasterxml.jackson.annotation.JsonCreator import com.fasterxml.jackson.annotation.JsonCreator

View file

@ -17,6 +17,7 @@ import org.gotson.komga.infrastructure.swagger.PageableWithoutSortAsQueryParam
import org.gotson.komga.interfaces.rest.dto.BookDto import org.gotson.komga.interfaces.rest.dto.BookDto
import org.gotson.komga.interfaces.rest.dto.ReadListCreationDto import org.gotson.komga.interfaces.rest.dto.ReadListCreationDto
import org.gotson.komga.interfaces.rest.dto.ReadListDto import org.gotson.komga.interfaces.rest.dto.ReadListDto
import org.gotson.komga.interfaces.rest.dto.ReadListRequestResultDto
import org.gotson.komga.interfaces.rest.dto.ReadListUpdateDto import org.gotson.komga.interfaces.rest.dto.ReadListUpdateDto
import org.gotson.komga.interfaces.rest.dto.restrictUrl import org.gotson.komga.interfaces.rest.dto.restrictUrl
import org.gotson.komga.interfaces.rest.dto.toDto import org.gotson.komga.interfaces.rest.dto.toDto
@ -41,6 +42,7 @@ import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import org.springframework.web.multipart.MultipartFile
import org.springframework.web.server.ResponseStatusException import org.springframework.web.server.ResponseStatusException
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.validation.Valid import javax.validation.Valid
@ -136,6 +138,13 @@ class ReadListController(
throw ResponseStatusException(HttpStatus.BAD_REQUEST, e.message) throw ResponseStatusException(HttpStatus.BAD_REQUEST, e.message)
} }
@PostMapping("/import")
@PreAuthorize("hasRole('$ROLE_ADMIN')")
fun importFromComicRackList(
@RequestParam("files") files: List<MultipartFile>,
): List<ReadListRequestResultDto> =
files.map { readListLifecycle.importReadList(it.bytes).toDto(it.originalFilename) }
@PatchMapping("{id}") @PatchMapping("{id}")
@PreAuthorize("hasRole('$ROLE_ADMIN')") @PreAuthorize("hasRole('$ROLE_ADMIN')")
@ResponseStatus(HttpStatus.NO_CONTENT) @ResponseStatus(HttpStatus.NO_CONTENT)

View file

@ -0,0 +1,42 @@
package org.gotson.komga.interfaces.rest.dto
import org.gotson.komga.domain.model.ReadListRequestBook
import org.gotson.komga.domain.model.ReadListRequestResult
import org.gotson.komga.domain.model.ReadListRequestResultBook
data class ReadListRequestBookDto(
val series: String,
val number: Int,
)
data class ReadListRequestResultDto(
val readList: ReadListDto?,
val unmatchedBooks: List<ReadListRequestResultBookDto> = emptyList(),
val errorCode: String = "",
val requestName: String,
)
data class ReadListRequestResultBookDto(
val book: ReadListRequestBookDto,
val errorCode: String = "",
)
fun ReadListRequestResult.toDto(requestName: String?) =
ReadListRequestResultDto(
readList = readList?.toDto(),
unmatchedBooks = unmatchedBooks.map { it.toDto() },
errorCode = errorCode,
requestName = requestName ?: "",
)
fun ReadListRequestResultBook.toDto() =
ReadListRequestResultBookDto(
book = book.toDto(),
errorCode = errorCode,
)
fun ReadListRequestBook.toDto() =
ReadListRequestBookDto(
series = series,
number = number,
)

View file

@ -0,0 +1,209 @@
package org.gotson.komga.domain.service
import org.assertj.core.api.Assertions.assertThat
import org.gotson.komga.domain.model.ReadList
import org.gotson.komga.domain.model.ReadListRequest
import org.gotson.komga.domain.model.ReadListRequestBook
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.BookMetadataRepository
import org.gotson.komga.domain.persistence.LibraryRepository
import org.gotson.komga.domain.persistence.ReadListRepository
import org.gotson.komga.domain.persistence.SeriesMetadataRepository
import org.gotson.komga.domain.persistence.SeriesRepository
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.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.junit.jupiter.SpringExtension
@ExtendWith(SpringExtension::class)
@SpringBootTest
class ReadListMatcherTest(
@Autowired private val seriesLifecycle: SeriesLifecycle,
@Autowired private val seriesRepository: SeriesRepository,
@Autowired private val seriesMetadataRepository: SeriesMetadataRepository,
@Autowired private val readListLifecycle: ReadListLifecycle,
@Autowired private val readListRepository: ReadListRepository,
@Autowired private val libraryRepository: LibraryRepository,
@Autowired private val bookMetadataRepository: BookMetadataRepository,
@Autowired private val readListMatcher: ReadListMatcher,
) {
private val library = makeLibrary()
@BeforeAll
fun `setup library`() {
libraryRepository.insert(library)
}
@AfterAll
fun `teardown library`() {
libraryRepository.deleteAll()
}
@AfterEach
fun `clear repository`() {
readListRepository.deleteAll()
seriesLifecycle.deleteMany(seriesRepository.findAll().map { it.id })
}
@Test
fun `given request with existing series and books when matching then result contains a read list with all books`() {
// given
val booksSeries1 = listOf(
makeBook("book1", libraryId = library.id),
makeBook("book5", libraryId = library.id),
)
makeSeries(name = "batman", libraryId = library.id).let { s ->
seriesLifecycle.createSeries(s)
seriesLifecycle.addBooks(s, booksSeries1)
seriesLifecycle.sortBooks(s)
seriesMetadataRepository.findById(s.id).let {
seriesMetadataRepository.update(it.copy(title = "Batman: White Knight"))
}
}
val booksSeries2 = listOf(
makeBook("book1", libraryId = library.id),
makeBook("book2", libraryId = library.id),
)
makeSeries(name = "joker", libraryId = library.id).let { s ->
seriesLifecycle.createSeries(s)
seriesLifecycle.addBooks(s, booksSeries2)
seriesLifecycle.sortBooks(s)
bookMetadataRepository.findById(booksSeries2[0].id).let {
bookMetadataRepository.update(it.copy(number = "025"))
}
}
val request = ReadListRequest(
name = "readlist",
books = listOf(
ReadListRequestBook(series = "Batman: White Knight", number = 1),
ReadListRequestBook(series = "joker", number = 2),
ReadListRequestBook(series = "Batman: White Knight", number = 2),
ReadListRequestBook(series = "joker", number = 25),
)
)
// when
val result = readListMatcher.matchReadListRequest(request)
// then
with(result) {
assertThat(readList).isNotNull
assertThat(unmatchedBooks).isEmpty()
assertThat(errorCode).isBlank()
with(readList!!) {
assertThat(name).isEqualTo(request.name)
assertThat(bookIds).hasSize(4)
assertThat(bookIds).containsExactlyEntriesOf(
mapOf(
0 to booksSeries1[0].id,
1 to booksSeries2[1].id,
2 to booksSeries1[1].id,
3 to booksSeries2[0].id,
)
)
}
}
}
@Test
fun `given request with existing read list when matching then result has no readlist and appropriate error code`() {
// given
readListLifecycle.addReadList(
ReadList(name = "my ReadList")
)
val request = ReadListRequest(
name = "my readlist",
books = listOf(
ReadListRequestBook(series = "batman: white knight", number = 1),
ReadListRequestBook(series = "joker", number = 2),
ReadListRequestBook(series = "BATMAN: WHITE KNIGHT", number = 2),
ReadListRequestBook(series = "joker", number = 25),
)
)
// when
val result = readListMatcher.matchReadListRequest(request)
// then
with(result) {
assertThat(readList).isNull()
assertThat(errorCode).isEqualTo("ERR_1009")
assertThat(unmatchedBooks.map { it.book }).containsExactlyElementsOf(request.books)
}
}
@Test
fun `given request and some matching series or books when matching then returns result with appropriate error codes`() {
// given
val booksSeries1 = listOf(
makeBook("book1", libraryId = library.id),
makeBook("book5", libraryId = library.id),
)
makeSeries(name = "batman", libraryId = library.id).let { s ->
seriesLifecycle.createSeries(s)
seriesLifecycle.addBooks(s, booksSeries1)
seriesLifecycle.sortBooks(s)
bookMetadataRepository.findById(booksSeries1[0].id).let {
bookMetadataRepository.update(it.copy(number = "2"))
}
}
val booksSeries2 = listOf(
makeBook("book1", libraryId = library.id),
makeBook("book2", libraryId = library.id),
)
makeSeries(name = "joker", libraryId = library.id).let { s ->
seriesLifecycle.createSeries(s)
seriesLifecycle.addBooks(s, booksSeries2)
seriesLifecycle.sortBooks(s)
}
makeSeries(name = "joker", libraryId = library.id).let { s ->
seriesLifecycle.createSeries(s)
}
val request = ReadListRequest(
name = "readlist",
books = listOf(
ReadListRequestBook(series = "tokyo ghost", number = 1),
ReadListRequestBook(series = "batman", number = 3),
ReadListRequestBook(series = "joker", number = 3),
ReadListRequestBook(series = "batman", number = 2),
)
)
// when
val result = readListMatcher.matchReadListRequest(request)
// then
with(result) {
assertThat(readList).isNull()
assertThat(errorCode).isEqualTo("ERR_1010")
assertThat(unmatchedBooks).hasSize(4)
assertThat(unmatchedBooks[0].book).isEqualTo(request.books[0])
assertThat(unmatchedBooks[0].errorCode).isEqualTo("ERR_1012")
assertThat(unmatchedBooks[1].book).isEqualTo(request.books[1])
assertThat(unmatchedBooks[1].errorCode).isEqualTo("ERR_1014")
assertThat(unmatchedBooks[2].book).isEqualTo(request.books[2])
assertThat(unmatchedBooks[2].errorCode).isEqualTo("ERR_1011")
assertThat(unmatchedBooks[3].book).isEqualTo(request.books[3])
assertThat(unmatchedBooks[3].errorCode).isEqualTo("ERR_1013")
}
}
}

View file

@ -1,4 +1,4 @@
package org.gotson.komga.infrastructure.metadata.comicinfo package org.gotson.komga.infrastructure.metadata.comicrack
import com.fasterxml.jackson.dataformat.xml.XmlMapper import com.fasterxml.jackson.dataformat.xml.XmlMapper
import io.mockk.every import io.mockk.every
@ -8,12 +8,16 @@ import org.gotson.komga.domain.model.Media
import org.gotson.komga.domain.model.SeriesMetadata import org.gotson.komga.domain.model.SeriesMetadata
import org.gotson.komga.domain.model.makeBook import org.gotson.komga.domain.model.makeBook
import org.gotson.komga.domain.service.BookAnalyzer import org.gotson.komga.domain.service.BookAnalyzer
import org.gotson.komga.infrastructure.metadata.comicinfo.dto.AgeRating import org.gotson.komga.infrastructure.metadata.comicrack.dto.AgeRating
import org.gotson.komga.infrastructure.metadata.comicinfo.dto.ComicInfo import org.gotson.komga.infrastructure.metadata.comicrack.dto.ComicInfo
import org.gotson.komga.infrastructure.metadata.comicinfo.dto.Manga import org.gotson.komga.infrastructure.metadata.comicrack.dto.Manga
import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.MethodSource
import java.time.LocalDate import java.time.LocalDate
import java.util.stream.Stream
class ComicInfoProviderTest { class ComicInfoProviderTest {
@ -285,4 +289,18 @@ class ComicInfoProviderTest {
} }
} }
} }
fun computeSeriesFromSeriesAndVolumeArguments() = Stream.of(
Arguments.of("", null, null),
Arguments.of(null, null, null),
Arguments.of("Series", null, "Series"),
Arguments.of("Series", 1, "Series"),
Arguments.of("Series", 10, "Series (10)"),
)
@ParameterizedTest
@MethodSource("computeSeriesFromSeriesAndVolumeArguments")
fun `given series and volume when computing series name then it is correct`(series: String?, volume: Int?, expected: String?) {
assertThat(computeSeriesFromSeriesAndVolume(series, volume)).isEqualTo(expected)
}
} }

View file

@ -0,0 +1,158 @@
package org.gotson.komga.infrastructure.metadata.comicrack
import com.fasterxml.jackson.dataformat.xml.XmlMapper
import io.mockk.every
import io.mockk.mockk
import org.assertj.core.api.Assertions.assertThat
import org.gotson.komga.infrastructure.metadata.comicrack.dto.Book
import org.gotson.komga.infrastructure.metadata.comicrack.dto.ReadingList
import org.junit.jupiter.api.Test
class ReadListProviderTest {
private val mockMapper = mockk<XmlMapper>()
private val readListProvider = ReadListProvider(mockMapper)
@Test
fun `given CBL list with books when getting ReadListRequest then it is valid`() {
// given
val cbl = ReadingList().apply {
name = "my read list"
books = listOf(
Book().apply {
series = "series 1"
number = 4
volume = 2005
},
Book().apply {
series = "series 2"
number = 1
},
)
}
every { mockMapper.readValue(any<ByteArray>(), ReadingList::class.java) } returns cbl
// when
val request = readListProvider.importFromCbl(ByteArray(0))
// then
assertThat(request).isNotNull
with(request!!) {
assertThat(name).isEqualTo(cbl.name)
assertThat(books).hasSize(2)
with(books[0]) {
assertThat(series).isEqualTo("series 1 (2005)")
assertThat(number).isEqualTo(4)
}
with(books[1]) {
assertThat(series).isEqualTo("series 2")
assertThat(number).isEqualTo(1)
}
}
}
@Test
fun `given CBL list with invalid books when getting ReadListRequest then it is null`() {
// given
val cbl = ReadingList().apply {
name = "my read list"
books = listOf(
Book().apply {
series = " "
number = 4
volume = 2005
},
Book().apply {
series = null
number = 1
},
Book().apply {
series = "Series"
number = null
},
)
}
every { mockMapper.readValue(any<ByteArray>(), ReadingList::class.java) } returns cbl
// when
val request = readListProvider.importFromCbl(ByteArray(0))
// then
assertThat(request).isNull()
}
@Test
fun `given CBL list without books when getting ReadListRequest then it is null`() {
// given
val cbl = ReadingList().apply {
name = "my read list"
books = emptyList()
}
every { mockMapper.readValue(any<ByteArray>(), ReadingList::class.java) } returns cbl
// when
val request = readListProvider.importFromCbl(ByteArray(0))
// then
assertThat(request).isNull()
}
@Test
fun `given CBL list without name when getting ReadListRequest then it is null`() {
// given
val cbl = ReadingList().apply {
name = null
books = listOf(
Book().apply {
series = "series 1"
number = 4
volume = 2005
},
Book().apply {
series = "series 2"
number = 1
},
)
}
every { mockMapper.readValue(any<ByteArray>(), ReadingList::class.java) } returns cbl
// when
val request = readListProvider.importFromCbl(ByteArray(0))
// then
assertThat(request).isNull()
}
@Test
fun `given CBL list with blank name when getting ReadListRequest then it is null`() {
// given
val cbl = ReadingList().apply {
name = " "
books = listOf(
Book().apply {
series = "series 1"
number = 4
volume = 2005
},
Book().apply {
series = "series 2"
number = 1
},
)
}
every { mockMapper.readValue(any<ByteArray>(), ReadingList::class.java) } returns cbl
// when
val request = readListProvider.importFromCbl(ByteArray(0))
// then
assertThat(request).isNull()
}
}

View file

@ -1,6 +1,7 @@
package org.gotson.komga.infrastructure.metadata.comicinfo.dto package org.gotson.komga.infrastructure.metadata.comicrack.dto
import com.fasterxml.jackson.dataformat.xml.XmlMapper import com.fasterxml.jackson.dataformat.xml.XmlMapper
import com.fasterxml.jackson.module.kotlin.readValue
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.springframework.core.io.ClassPathResource import org.springframework.core.io.ClassPathResource
@ -9,9 +10,9 @@ class ComicInfoTest {
@Test @Test
fun `given valid xml file when deserializing then properties are available`() { fun `given valid xml file when deserializing then properties are available`() {
val file = ClassPathResource("comicinfo/ComicInfo.xml") val file = ClassPathResource("comicrack/ComicInfo.xml")
val mapper = XmlMapper() val mapper = XmlMapper()
val comicInfo = mapper.readValue(file.url, ComicInfo::class.java) val comicInfo = mapper.readValue<ComicInfo>(file.url)
with(comicInfo) { with(comicInfo) {
assertThat(title).isEqualTo("v01 - Preludes & Nocturnes - 30th Anniversary Edition") assertThat(title).isEqualTo("v01 - Preludes & Nocturnes - 30th Anniversary Edition")
@ -34,9 +35,9 @@ class ComicInfoTest {
@Test @Test
fun `given another valid xml file when deserializing then properties are available`() { fun `given another valid xml file when deserializing then properties are available`() {
val file = ClassPathResource("comicinfo/ComicInfo2.xml") val file = ClassPathResource("comicrack/ComicInfo2.xml")
val mapper = XmlMapper() val mapper = XmlMapper()
val comicInfo = mapper.readValue(file.url, ComicInfo::class.java) val comicInfo = mapper.readValue<ComicInfo>(file.url)
with(comicInfo) { with(comicInfo) {
assertThat(title).isEqualTo("v01 - Preludes & Nocturnes - 30th Anniversary Edition") assertThat(title).isEqualTo("v01 - Preludes & Nocturnes - 30th Anniversary Edition")
@ -62,9 +63,9 @@ class ComicInfoTest {
@Test @Test
fun `given incorrect enum values when deserializing then it is ignored`() { fun `given incorrect enum values when deserializing then it is ignored`() {
val file = ClassPathResource("comicinfo/InvalidEnumValues.xml") val file = ClassPathResource("comicrack/InvalidEnumValues.xml")
val mapper = XmlMapper() val mapper = XmlMapper()
val comicInfo = mapper.readValue(file.url, ComicInfo::class.java) val comicInfo = mapper.readValue<ComicInfo>(file.url)
with(comicInfo) { with(comicInfo) {
assertThat(ageRating).isNull() assertThat(ageRating).isNull()

View file

@ -0,0 +1,59 @@
package org.gotson.komga.infrastructure.metadata.comicrack.dto
import com.fasterxml.jackson.dataformat.xml.XmlMapper
import com.fasterxml.jackson.module.kotlin.readValue
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.core.io.ClassPathResource
class ReadingListTest {
private val mapper = XmlMapper()
@Test
fun `given valid xml file when deserializing then properties are available`() {
val file = ClassPathResource("comicrack/ReadingList.xml")
val readingList = mapper.readValue<ReadingList>(file.url)
with(readingList) {
assertThat(name).isEqualTo("Civil War")
assertThat(books).hasSize(3)
with(books[0]) {
assertThat(series).isEqualTo("Civil War")
assertThat(number).isEqualTo(1)
assertThat(volume).isEqualTo(2006)
assertThat(year).isEqualTo(2006)
assertThat(fileName).isEqualTo("Civil War Vol.2006 #01 (July, 2006)")
}
with(books[1]) {
assertThat(series).isEqualTo("Wolverine")
assertThat(number).isEqualTo(42)
assertThat(volume).isEqualTo(2003)
assertThat(year).isEqualTo(2006)
assertThat(fileName).isEqualTo("Wolverine Vol.2003 #42 (July, 2006)")
}
with(books[2]) {
assertThat(series).isEqualTo("X-Factor")
assertThat(number).isEqualTo(8)
assertThat(volume).isEqualTo(2006)
assertThat(year).isEqualTo(2006)
assertThat(fileName).isEqualTo("X-Factor Vol.2006 #08 (August, 2006)")
}
}
}
@Test
fun `given valid xml file for smart list when deserializing then properties are available`() {
val file = ClassPathResource("comicrack/ReadingList_SmartList.xml")
val mapper = XmlMapper()
val readingList = mapper.readValue<ReadingList>(file.url)
with(readingList) {
assertThat(name).isEqualTo("Golden Age")
assertThat(books).isEmpty()
}
}
}

View file

@ -0,0 +1,19 @@
<?xml version="1.0"?>
<ReadingList xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Name>Civil War</Name>
<Books>
<Book Series="Civil War" Number="1" Volume="2006" Year="2006">
<Id>1b21c8c4-e8d7-44f6-992d-97d24ce8123e</Id>
<FileName>Civil War Vol.2006 #01 (July, 2006)</FileName>
</Book>
<Book Series="Wolverine" Number="42" Volume="2003" Year="2006">
<Id>29a69cbf-af64-471d-889c-8fc0f0080f7c</Id>
<FileName>Wolverine Vol.2003 #42 (July, 2006)</FileName>
</Book>
<Book Series="X-Factor" Number="8" Volume="2006" Year="2006">
<Id>ec70e585-5a80-428c-a67e-fd22b668449b</Id>
<FileName>X-Factor Vol.2006 #08 (August, 2006)</FileName>
</Book>
</Books>
<Matchers />
</ReadingList>

View file

@ -0,0 +1,13 @@
<?xml version="1.0"?>
<ReadingList xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Name>Golden Age</Name>
<Books />
<Matchers>
<ComicBookMatcher xsi:type="ComicBookSeriesMatcher" MatchOperator="4">
<MatchValue>Marvel Masterworks</MatchValue>
</ComicBookMatcher>
<ComicBookMatcher xsi:type="ComicBookSeriesMatcher" MatchOperator="1">
<MatchValue>Golden Age</MatchValue>
</ComicBookMatcher>
</Matchers>
</ReadingList>