From c1e435762c732d38a4e074f4927fa17445d89709 Mon Sep 17 00:00:00 2001 From: Gauthier Roebroeck Date: Tue, 16 Mar 2021 10:10:51 +0800 Subject: [PATCH] feat: import ComicRack lists as read lists Closes #464 --- ERRORCODES.md | 7 + .../komga/domain/model/ReadListRequest.kt | 25 +++ .../domain/persistence/SeriesRepository.kt | 1 + .../komga/domain/service/MetadataLifecycle.kt | 2 +- .../komga/domain/service/ReadListLifecycle.kt | 23 +- .../komga/domain/service/ReadListMatcher.kt | 67 ++++++ .../komga/infrastructure/jooq/SeriesDao.kt | 8 + .../ComicInfoProvider.kt | 22 +- .../metadata/comicrack/ReadListProvider.kt | 48 ++++ .../{comicinfo => comicrack}/dto/AgeRating.kt | 2 +- .../metadata/comicrack/dto/Book.kt | 26 +++ .../{comicinfo => comicrack}/dto/ComicInfo.kt | 2 +- .../{comicinfo => comicrack}/dto/Manga.kt | 2 +- .../metadata/comicrack/dto/ReadingList.kt | 24 ++ .../{comicinfo => comicrack}/dto/YesNo.kt | 2 +- .../interfaces/rest/ReadListController.kt | 9 + .../rest/dto/ReadListRequestResultDto.kt | 42 ++++ .../domain/service/ReadListMatcherTest.kt | 209 ++++++++++++++++++ .../ComicInfoProviderTest.kt | 26 ++- .../comicrack/ReadListProviderTest.kt | 158 +++++++++++++ .../dto/ComicInfoTest.kt | 15 +- .../metadata/comicrack/dto/ReadingListTest.kt | 59 +++++ .../{comicinfo => comicrack}/ComicInfo.xml | 0 .../{comicinfo => comicrack}/ComicInfo2.xml | 0 .../InvalidEnumValues.xml | 0 .../test/resources/comicrack/ReadingList.xml | 19 ++ .../comicrack/ReadingList_SmartList.xml | 13 ++ 27 files changed, 787 insertions(+), 24 deletions(-) create mode 100644 komga/src/main/kotlin/org/gotson/komga/domain/model/ReadListRequest.kt create mode 100644 komga/src/main/kotlin/org/gotson/komga/domain/service/ReadListMatcher.kt rename komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/{comicinfo => comicrack}/ComicInfoProvider.kt (87%) create mode 100644 komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/ReadListProvider.kt rename komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/{comicinfo => comicrack}/dto/AgeRating.kt (92%) create mode 100644 komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/dto/Book.kt rename komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/{comicinfo => comicrack}/dto/ComicInfo.kt (97%) rename komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/{comicinfo => comicrack}/dto/Manga.kt (85%) create mode 100644 komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/dto/ReadingList.kt rename komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/{comicinfo => comicrack}/dto/YesNo.kt (82%) create mode 100644 komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/ReadListRequestResultDto.kt create mode 100644 komga/src/test/kotlin/org/gotson/komga/domain/service/ReadListMatcherTest.kt rename komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/{comicinfo => comicrack}/ComicInfoProviderTest.kt (90%) create mode 100644 komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/ReadListProviderTest.kt rename komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/{comicinfo => comicrack}/dto/ComicInfoTest.kt (85%) create mode 100644 komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/dto/ReadingListTest.kt rename komga/src/test/resources/{comicinfo => comicrack}/ComicInfo.xml (100%) rename komga/src/test/resources/{comicinfo => comicrack}/ComicInfo2.xml (100%) rename komga/src/test/resources/{comicinfo => comicrack}/InvalidEnumValues.xml (100%) create mode 100644 komga/src/test/resources/comicrack/ReadingList.xml create mode 100644 komga/src/test/resources/comicrack/ReadingList_SmartList.xml diff --git a/ERRORCODES.md b/ERRORCODES.md index 250737216..a8906cccb 100644 --- a/ERRORCODES.md +++ b/ERRORCODES.md @@ -11,3 +11,10 @@ ERR_1005 | Unknown error while analyzing book ERR_1006 | Book does not contain any page ERR_1007 | Some entries could not be analyzed 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 diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/ReadListRequest.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/ReadListRequest.kt new file mode 100644 index 000000000..fa8bbcef1 --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/ReadListRequest.kt @@ -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, +) + +data class ReadListRequestBook( + val series: String, + val number: Int, +) + +data class ReadListRequestResult( + val readList: ReadList?, + val unmatchedBooks: List = emptyList(), + val errorCode: String = "", +) + +data class ReadListRequestResultBook( + val book: ReadListRequestBook, + val errorCode: String = "", +) 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 8af1cbe54..42be14584 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,6 +11,7 @@ interface SeriesRepository { fun findByLibraryIdAndUrlNotIn(libraryId: String, urls: Collection): Collection fun findByLibraryIdAndUrl(libraryId: String, url: URL): Series? fun findAll(search: SeriesSearch): Collection + fun findByTitle(title: String): Collection fun getLibraryId(seriesId: String): String? 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 77506b3fb..f2e5ed6e4 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 @@ -18,7 +18,7 @@ import org.gotson.komga.domain.persistence.SeriesMetadataRepository import org.gotson.komga.infrastructure.metadata.BookMetadataProvider import org.gotson.komga.infrastructure.metadata.SeriesMetadataProvider 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.localartwork.LocalArtworkProvider import org.springframework.stereotype.Service diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/ReadListLifecycle.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/ReadListLifecycle.kt index 8b3c61fe5..089e89746 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/ReadListLifecycle.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/ReadListLifecycle.kt @@ -3,8 +3,10 @@ package org.gotson.komga.domain.service import mu.KotlinLogging import org.gotson.komga.domain.model.DuplicateNameException 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.infrastructure.image.MosaicGenerator +import org.gotson.komga.infrastructure.metadata.comicrack.ReadListProvider import org.springframework.stereotype.Service private val logger = KotlinLogging.logger {} @@ -13,7 +15,9 @@ private val logger = KotlinLogging.logger {} class ReadListLifecycle( private val readListRepository: ReadListRepository, private val bookLifecycle: BookLifecycle, - private val mosaicGenerator: MosaicGenerator + private val mosaicGenerator: MosaicGenerator, + private val readListMatcher: ReadListMatcher, + private val readListProvider: ReadListProvider, ) { @Throws( @@ -55,4 +59,21 @@ class ReadListLifecycle( val images = ids.mapNotNull { bookLifecycle.getThumbnailBytes(it) } 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 + } + } } diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/ReadListMatcher.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/ReadListMatcher.kt new file mode 100644 index 000000000..3285ed524 --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/ReadListMatcher.kt @@ -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() + val unmatchedBooks = mutableListOf() + + 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" + ) + } + } +} diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesDao.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesDao.kt index 1e726b9f9..112dc7662 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesDao.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/SeriesDao.kt @@ -51,6 +51,14 @@ class SeriesDao( .fetchOneInto(s) ?.toDomain() + override fun findByTitle(title: String): Collection = + 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? = dsl.select(s.LIBRARY_ID) .from(s) 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/comicrack/ComicInfoProvider.kt similarity index 87% rename from komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicinfo/ComicInfoProvider.kt rename to komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/ComicInfoProvider.kt index 7c3a775cd..8a3867866 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicinfo/ComicInfoProvider.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/ComicInfoProvider.kt @@ -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 mu.KotlinLogging @@ -11,8 +11,8 @@ import org.gotson.komga.domain.model.SeriesMetadataPatch import org.gotson.komga.domain.service.BookAnalyzer import org.gotson.komga.infrastructure.metadata.BookMetadataProvider import org.gotson.komga.infrastructure.metadata.SeriesMetadataProvider -import org.gotson.komga.infrastructure.metadata.comicinfo.dto.ComicInfo -import org.gotson.komga.infrastructure.metadata.comicinfo.dto.Manga +import org.gotson.komga.infrastructure.metadata.comicrack.dto.ComicInfo +import org.gotson.komga.infrastructure.metadata.comicrack.dto.Manga import org.gotson.komga.infrastructure.validation.BCP47TagValidator import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Service @@ -45,7 +45,12 @@ class ComicInfoProvider( val readLists = mutableListOf() 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 -> @@ -75,9 +80,7 @@ class ComicInfoProvider( } val genres = comicInfo.genre?.split(',')?.mapNotNull { it.trim().ifBlank { null } } - val series = comicInfo.series?.ifBlank { null }?.let { series -> - series + (comicInfo.volume?.let { if (it != 1) " ($it)" else "" } ?: "") - } + val series = computeSeriesFromSeriesAndVolume(comicInfo.series, comicInfo.volume) return SeriesMetadataPatch( title = series, @@ -115,3 +118,8 @@ class ComicInfoProvider( 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 "" } ?: "") + } diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/ReadListProvider.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/ReadListProvider.kt new file mode 100644 index 000000000..b4a13018a --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/ReadListProvider.kt @@ -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 + } +} diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicinfo/dto/AgeRating.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/dto/AgeRating.kt similarity index 92% rename from komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicinfo/dto/AgeRating.kt rename to komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/dto/AgeRating.kt index bec1a0989..5fad8d4e2 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicinfo/dto/AgeRating.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/dto/AgeRating.kt @@ -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 diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/dto/Book.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/dto/Book.kt new file mode 100644 index 000000000..26f4eb0dd --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/dto/Book.kt @@ -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)" + } +} diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicinfo/dto/ComicInfo.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/dto/ComicInfo.kt similarity index 97% rename from komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicinfo/dto/ComicInfo.kt rename to komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/dto/ComicInfo.kt index cab199737..eec217cc0 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicinfo/dto/ComicInfo.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/dto/ComicInfo.kt @@ -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.JsonProperty diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicinfo/dto/Manga.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/dto/Manga.kt similarity index 85% rename from komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicinfo/dto/Manga.kt rename to komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/dto/Manga.kt index 6d38c45e8..b4a5e35cb 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicinfo/dto/Manga.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/dto/Manga.kt @@ -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 diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/dto/ReadingList.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/dto/ReadingList.kt new file mode 100644 index 000000000..7b917a747 --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/dto/ReadingList.kt @@ -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 = emptyList() + + override fun toString(): String { + return "ReadingList(name=$name, books=$books)" + } +} diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicinfo/dto/YesNo.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/dto/YesNo.kt similarity index 82% rename from komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicinfo/dto/YesNo.kt rename to komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/dto/YesNo.kt index f1cd4ffad..48ca3412e 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicinfo/dto/YesNo.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/dto/YesNo.kt @@ -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 diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/ReadListController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/ReadListController.kt index 60b8578f0..ca70646aa 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/ReadListController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/ReadListController.kt @@ -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.ReadListCreationDto 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.restrictUrl 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.ResponseStatus import org.springframework.web.bind.annotation.RestController +import org.springframework.web.multipart.MultipartFile import org.springframework.web.server.ResponseStatusException import java.util.concurrent.TimeUnit import javax.validation.Valid @@ -136,6 +138,13 @@ class ReadListController( throw ResponseStatusException(HttpStatus.BAD_REQUEST, e.message) } + @PostMapping("/import") + @PreAuthorize("hasRole('$ROLE_ADMIN')") + fun importFromComicRackList( + @RequestParam("files") files: List, + ): List = + files.map { readListLifecycle.importReadList(it.bytes).toDto(it.originalFilename) } + @PatchMapping("{id}") @PreAuthorize("hasRole('$ROLE_ADMIN')") @ResponseStatus(HttpStatus.NO_CONTENT) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/ReadListRequestResultDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/ReadListRequestResultDto.kt new file mode 100644 index 000000000..d2aff6733 --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/dto/ReadListRequestResultDto.kt @@ -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 = 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, + ) diff --git a/komga/src/test/kotlin/org/gotson/komga/domain/service/ReadListMatcherTest.kt b/komga/src/test/kotlin/org/gotson/komga/domain/service/ReadListMatcherTest.kt new file mode 100644 index 000000000..21d93542a --- /dev/null +++ b/komga/src/test/kotlin/org/gotson/komga/domain/service/ReadListMatcherTest.kt @@ -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") + } + } +} 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/comicrack/ComicInfoProviderTest.kt similarity index 90% rename from komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/comicinfo/ComicInfoProviderTest.kt rename to komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/ComicInfoProviderTest.kt index 3ef0b3585..7fa7c742f 100644 --- a/komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/comicinfo/ComicInfoProviderTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/ComicInfoProviderTest.kt @@ -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 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.makeBook import org.gotson.komga.domain.service.BookAnalyzer -import org.gotson.komga.infrastructure.metadata.comicinfo.dto.AgeRating -import org.gotson.komga.infrastructure.metadata.comicinfo.dto.ComicInfo -import org.gotson.komga.infrastructure.metadata.comicinfo.dto.Manga +import org.gotson.komga.infrastructure.metadata.comicrack.dto.AgeRating +import org.gotson.komga.infrastructure.metadata.comicrack.dto.ComicInfo +import org.gotson.komga.infrastructure.metadata.comicrack.dto.Manga import org.junit.jupiter.api.Nested 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.util.stream.Stream 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) + } } diff --git a/komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/ReadListProviderTest.kt b/komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/ReadListProviderTest.kt new file mode 100644 index 000000000..a3f6b90cc --- /dev/null +++ b/komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/ReadListProviderTest.kt @@ -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() + 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(), 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(), 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(), 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(), 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(), ReadingList::class.java) } returns cbl + + // when + val request = readListProvider.importFromCbl(ByteArray(0)) + + // then + assertThat(request).isNull() + } +} 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/comicrack/dto/ComicInfoTest.kt similarity index 85% rename from komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/comicinfo/dto/ComicInfoTest.kt rename to komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/dto/ComicInfoTest.kt index c0cf50170..f71c439fe 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/comicrack/dto/ComicInfoTest.kt @@ -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.module.kotlin.readValue import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.springframework.core.io.ClassPathResource @@ -9,9 +10,9 @@ class ComicInfoTest { @Test 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 comicInfo = mapper.readValue(file.url, ComicInfo::class.java) + val comicInfo = mapper.readValue(file.url) with(comicInfo) { assertThat(title).isEqualTo("v01 - Preludes & Nocturnes - 30th Anniversary Edition") @@ -34,9 +35,9 @@ class ComicInfoTest { @Test 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 comicInfo = mapper.readValue(file.url, ComicInfo::class.java) + val comicInfo = mapper.readValue(file.url) with(comicInfo) { assertThat(title).isEqualTo("v01 - Preludes & Nocturnes - 30th Anniversary Edition") @@ -62,9 +63,9 @@ class ComicInfoTest { @Test 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 comicInfo = mapper.readValue(file.url, ComicInfo::class.java) + val comicInfo = mapper.readValue(file.url) with(comicInfo) { assertThat(ageRating).isNull() diff --git a/komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/dto/ReadingListTest.kt b/komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/dto/ReadingListTest.kt new file mode 100644 index 000000000..375140ee6 --- /dev/null +++ b/komga/src/test/kotlin/org/gotson/komga/infrastructure/metadata/comicrack/dto/ReadingListTest.kt @@ -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(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(file.url) + + with(readingList) { + assertThat(name).isEqualTo("Golden Age") + assertThat(books).isEmpty() + } + } +} diff --git a/komga/src/test/resources/comicinfo/ComicInfo.xml b/komga/src/test/resources/comicrack/ComicInfo.xml similarity index 100% rename from komga/src/test/resources/comicinfo/ComicInfo.xml rename to komga/src/test/resources/comicrack/ComicInfo.xml diff --git a/komga/src/test/resources/comicinfo/ComicInfo2.xml b/komga/src/test/resources/comicrack/ComicInfo2.xml similarity index 100% rename from komga/src/test/resources/comicinfo/ComicInfo2.xml rename to komga/src/test/resources/comicrack/ComicInfo2.xml diff --git a/komga/src/test/resources/comicinfo/InvalidEnumValues.xml b/komga/src/test/resources/comicrack/InvalidEnumValues.xml similarity index 100% rename from komga/src/test/resources/comicinfo/InvalidEnumValues.xml rename to komga/src/test/resources/comicrack/InvalidEnumValues.xml diff --git a/komga/src/test/resources/comicrack/ReadingList.xml b/komga/src/test/resources/comicrack/ReadingList.xml new file mode 100644 index 000000000..d24d2b0ee --- /dev/null +++ b/komga/src/test/resources/comicrack/ReadingList.xml @@ -0,0 +1,19 @@ + + + Civil War + + + 1b21c8c4-e8d7-44f6-992d-97d24ce8123e + Civil War Vol.2006 #01 (July, 2006) + + + 29a69cbf-af64-471d-889c-8fc0f0080f7c + Wolverine Vol.2003 #42 (July, 2006) + + + ec70e585-5a80-428c-a67e-fd22b668449b + X-Factor Vol.2006 #08 (August, 2006) + + + + diff --git a/komga/src/test/resources/comicrack/ReadingList_SmartList.xml b/komga/src/test/resources/comicrack/ReadingList_SmartList.xml new file mode 100644 index 000000000..0efdf3383 --- /dev/null +++ b/komga/src/test/resources/comicrack/ReadingList_SmartList.xml @@ -0,0 +1,13 @@ + + + Golden Age + + + + Marvel Masterworks + + + Golden Age + + + \ No newline at end of file