diff --git a/ERRORCODES.md b/ERRORCODES.md index 5e0ba285..d6c8ceb1 100644 --- a/ERRORCODES.md +++ b/ERRORCODES.md @@ -1,37 +1,37 @@ # Error codes -| Code | Description | -|----------|---------------------------------------------------------| -| ERR_1000 | File could not be accessed during analysis | -| ERR_1001 | Media type is not supported during analysis | -| ERR_1002 | Encrypted RAR archives are not supported | -| ERR_1003 | Solid RAR archives are not supported | -| ERR_1004 | Multi-Volume RAR archives are not supported | -| 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 | -| ERR_1016 | Directory not accessible or not a directory | -| ERR_1017 | Cannot scan folder that is part of an existing library | -| ERR_1018 | File not found | -| ERR_1019 | Cannot import file that is part of an existing library | -| ERR_1020 | Book to upgrade does not belong to provided series | -| ERR_1021 | Destination file already exists | -| ERR_1022 | Newly imported book could not be scanned | -| ERR_1023 | Book already present in ReadingList | -| ERR_1024 | OAuth2 login error: no email attribute | -| ERR_1025 | OAuth2 login error: no local user exist with that email | -| ERR_1026 | OpenIDConnect login error: email not verified | -| ERR_1027 | OpenIDConnect login error: no email_verified attribute | -| ERR_1028 | OpenIDConnect login error: no email attribute | -| ERR_1029 | ComicRack CBL does not contain any Book element | -| ERR_1030 | ComicRack CBL has no Name element | -| ERR_1031 | ComicRack CBL Book is missing series or number | +| Code | Description | +|--------------|---------------------------------------------------------| +| ERR_1000 | File could not be accessed during analysis | +| ERR_1001 | Media type is not supported during analysis | +| ERR_1002 | Encrypted RAR archives are not supported | +| ERR_1003 | Solid RAR archives are not supported | +| ERR_1004 | Multi-Volume RAR archives are not supported | +| 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 | +| ERR_1016 | Directory not accessible or not a directory | +| ERR_1017 | Cannot scan folder that is part of an existing library | +| ERR_1018 | File not found | +| ERR_1019 | Cannot import file that is part of an existing library | +| ERR_1020 | Book to upgrade does not belong to provided series | +| ERR_1021 | Destination file already exists | +| ERR_1022 | Newly imported book could not be scanned | +| ERR_1023 | Book already present in ReadingList | +| ERR_1024 | OAuth2 login error: no email attribute | +| ERR_1025 | OAuth2 login error: no local user exist with that email | +| ERR_1026 | OpenIDConnect login error: email not verified | +| ERR_1027 | OpenIDConnect login error: no email_verified attribute | +| ERR_1028 | OpenIDConnect login error: no email attribute | +| ERR_1029 | ComicRack CBL does not contain any Book element | +| ERR_1030 | ComicRack CBL has no Name element | +| ERR_1031 | ComicRack CBL Book is missing series or number | diff --git a/komga/src/flyway/resources/db/migration/sqlite/V20230310121946__series_metadata_index.sql b/komga/src/flyway/resources/db/migration/sqlite/V20230310121946__series_metadata_index.sql new file mode 100644 index 00000000..4e869ce5 --- /dev/null +++ b/komga/src/flyway/resources/db/migration/sqlite/V20230310121946__series_metadata_index.sql @@ -0,0 +1,2 @@ +create index if not exists idx__series_metadata__title + on SERIES_METADATA (TITLE); 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 index 075fa333..a894ef92 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/model/ReadListRequest.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/ReadListRequest.kt @@ -13,20 +13,9 @@ data class ReadListRequestBook( val number: String, ) -data class ReadListRequestResult( - val readList: ReadList?, - val unmatchedBooks: List = emptyList(), - val errorCode: String = "", -) - -data class ReadListRequestResultBook( - val book: ReadListRequestBook, - val errorCode: String = "", -) - data class ReadListRequestMatch( val readListMatch: ReadListMatch, - val matches: List, + val requests: Collection, val errorCode: String = "", ) @@ -37,5 +26,16 @@ data class ReadListMatch( data class ReadListRequestBookMatches( val request: ReadListRequestBook, - val matches: Map>, + val matches: Map>, +) + +data class ReadListRequestBookMatchSeries( + val id: String, + val title: String, +) + +data class ReadListRequestBookMatchBook( + val id: String, + val number: String, + val title: String, ) diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/persistence/ReadListRequestRepository.kt b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/ReadListRequestRepository.kt new file mode 100644 index 00000000..f5de789c --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/domain/persistence/ReadListRequestRepository.kt @@ -0,0 +1,8 @@ +package org.gotson.komga.domain.persistence + +import org.gotson.komga.domain.model.ReadListRequestBook +import org.gotson.komga.domain.model.ReadListRequestBookMatches + +interface ReadListRequestRepository { + fun matchBookRequests(requests: Collection): Collection +} 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 9e661092..05e63fa9 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 @@ -6,7 +6,6 @@ import org.gotson.komga.domain.model.DomainEvent import org.gotson.komga.domain.model.DuplicateNameException import org.gotson.komga.domain.model.ReadList import org.gotson.komga.domain.model.ReadListRequestMatch -import org.gotson.komga.domain.model.ReadListRequestResult import org.gotson.komga.domain.model.ThumbnailReadList import org.gotson.komga.domain.persistence.ReadListRepository import org.gotson.komga.domain.persistence.ThumbnailReadListRepository @@ -122,24 +121,8 @@ class ReadListLifecycle( 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.matchAndCreateReadListRequest(request) - return when { - result.readList != null -> { - result.copy(readList = addReadList(result.readList)) - } - else -> result - } - } - fun matchComicRackList(fileContent: ByteArray): ReadListRequestMatch { - val request = readListProvider.importFromCblV2(fileContent) + val request = readListProvider.importFromCbl(fileContent) return readListMatcher.matchReadListRequest(request) } 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 index 713d0372..20c09c57 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/ReadListMatcher.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/ReadListMatcher.kt @@ -1,18 +1,11 @@ package org.gotson.komga.domain.service import mu.KotlinLogging -import org.gotson.komga.domain.model.ReadList import org.gotson.komga.domain.model.ReadListMatch import org.gotson.komga.domain.model.ReadListRequest -import org.gotson.komga.domain.model.ReadListRequestBookMatches import org.gotson.komga.domain.model.ReadListRequestMatch -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.language.toIndexedMap +import org.gotson.komga.domain.persistence.ReadListRequestRepository import org.springframework.stereotype.Service private val logger = KotlinLogging.logger {} @@ -20,55 +13,8 @@ private val logger = KotlinLogging.logger {} @Service class ReadListMatcher( private val readListRepository: ReadListRepository, - private val seriesRepository: SeriesRepository, - private val bookRepository: BookRepository, - private val bookMetadataRepository: BookMetadataRepository, + private val readListRequestRepository: ReadListRequestRepository, ) { - - fun matchAndCreateReadListRequest(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.findAllByTitle(book.series.first()) - 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.findAllBySeriesId(seriesId) - val bookMatches = bookMetadataRepository.findAllByIds(seriesBooks.map { it.id }) - .filter { (it.number.trimStart('0') == book.number.trimStart('0')) } - .map { it.bookId } - when { - bookMatches.size > 1 -> unmatchedBooks += ReadListRequestResultBook(book, "ERR_1013") - bookMatches.isEmpty() -> unmatchedBooks += ReadListRequestResultBook(book, "ERR_1014") - bookIds.contains(bookMatches.first()) -> unmatchedBooks += ReadListRequestResultBook(book, "ERR_1023") - 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", - ) - } - } - fun matchReadListRequest(request: ReadListRequest): ReadListRequestMatch { logger.info { "Trying to match $request" } @@ -76,13 +22,7 @@ class ReadListMatcher( if (readListRepository.existsByName(request.name)) ReadListMatch(request.name, "ERR_1009") else ReadListMatch(request.name) - val matches = request.books.map { book -> - val matches = book.series.flatMap { seriesRepository.findAllByTitle(it) }.associateWith { series -> - bookRepository.findAllBySeriesId(series.id) - .filter { (bookMetadataRepository.findById(it.id).number.trimStart('0') == book.number.trimStart('0')) } - } - ReadListRequestBookMatches(book, matches) - } + val matches = readListRequestRepository.matchBookRequests(request.books) return ReadListRequestMatch(readListMatch, matches) } diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ReadListRequestDao.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ReadListRequestDao.kt new file mode 100644 index 00000000..6276231a --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/jooq/ReadListRequestDao.kt @@ -0,0 +1,61 @@ +package org.gotson.komga.infrastructure.jooq + +import org.gotson.komga.domain.model.ReadListRequestBook +import org.gotson.komga.domain.model.ReadListRequestBookMatchBook +import org.gotson.komga.domain.model.ReadListRequestBookMatchSeries +import org.gotson.komga.domain.model.ReadListRequestBookMatches +import org.gotson.komga.domain.persistence.ReadListRequestRepository +import org.gotson.komga.jooq.Tables +import org.jooq.DSLContext +import org.jooq.impl.DSL.ltrim +import org.jooq.impl.DSL.row +import org.jooq.impl.DSL.value +import org.jooq.impl.DSL.values +import org.springframework.stereotype.Component + +@Component +class ReadListRequestDao( + private val dsl: DSLContext, +) : ReadListRequestRepository { + private val sd = Tables.SERIES_METADATA + private val b = Tables.BOOK + private val bd = Tables.BOOK_METADATA + + override fun matchBookRequests(requests: Collection): Collection { + // use a table expression to join the requests to their potential matches + val requestsAsRows = requests.flatMapIndexed { i, r -> r.series.map { row(i, it, r.number) } } + val seriesField = "series" + val indexField = "index" + val numberField = "number" + val requestsTable = values(*requestsAsRows.toTypedArray()).`as`("request", indexField, seriesField, numberField) + val matchedRequests = dsl.select( + requestsTable.field(indexField, Int::class.java), + sd.SERIES_ID, + sd.TITLE, + bd.BOOK_ID, + bd.NUMBER, + bd.TITLE, + ) + .from(requestsTable) + .innerJoin(sd).on(requestsTable.field(seriesField, String::class.java)?.eq(sd.TITLE.noCase())) + .innerJoin(b).on(sd.SERIES_ID.eq(b.SERIES_ID)) + .innerJoin(bd).on( + b.ID.eq(bd.BOOK_ID) + .and(ltrim(bd.NUMBER, value("0")).eq(ltrim(requestsTable.field(numberField, String::class.java), value("0")).noCase())), + ).fetchGroups(requestsTable.field(indexField, Int::class.java)) + .mapValues { (_, records) -> + // use the requests index to match results + records.groupBy( + { ReadListRequestBookMatchSeries(it.get(1, String::class.java), it.get(2, String::class.java)) }, + { ReadListRequestBookMatchBook(it.get(3, String::class.java), it.get(4, String::class.java), it.get(5, String::class.java)) }, + ) + } + + return requests.mapIndexed { i, request -> + ReadListRequestBookMatches( + request, + matchedRequests.getOrDefault(i, emptyMap()), + ) + } + } +} 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 index f47c2bcf..fee185b8 100644 --- 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 @@ -15,40 +15,8 @@ private val logger = KotlinLogging.logger {} 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(setOf(series), it.number!!.trim()) - 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 - } - @Throws(ComicRackListException::class) - fun importFromCblV2(cbl: ByteArray): ReadListRequest { + fun importFromCbl(cbl: ByteArray): ReadListRequest { val readingList = try { mapper.readValue(cbl, ReadingList::class.java) } catch (e: Exception) { diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ReadListController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ReadListController.kt index 274da1e5..9ff241ea 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ReadListController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ReadListController.kt @@ -39,7 +39,6 @@ import org.gotson.komga.interfaces.api.rest.dto.BookDto import org.gotson.komga.interfaces.api.rest.dto.ReadListCreationDto import org.gotson.komga.interfaces.api.rest.dto.ReadListDto import org.gotson.komga.interfaces.api.rest.dto.ReadListRequestMatchDto -import org.gotson.komga.interfaces.api.rest.dto.ReadListRequestResultDto import org.gotson.komga.interfaces.api.rest.dto.ReadListUpdateDto import org.gotson.komga.interfaces.api.rest.dto.TachiyomiReadProgressDto import org.gotson.komga.interfaces.api.rest.dto.TachiyomiReadProgressUpdateDto @@ -242,14 +241,6 @@ class ReadListController( throw ResponseStatusException(HttpStatus.BAD_REQUEST, e.message) } - @Deprecated("Deprecated since 0.162.0, use api/v1/readlists/match/comicrack instead") - @PostMapping("/import") - @PreAuthorize("hasRole('$ROLE_ADMIN')") - fun importFromComicRackList( - @RequestParam("files") files: List, - ): List = - files.map { readListLifecycle.importReadList(it.bytes).toDto(it.originalFilename) } - @PostMapping("match/comicrack") @PreAuthorize("hasRole('$ROLE_ADMIN')") fun matchFromComicRackList( diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/ReadListRequestMatchDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/ReadListRequestMatchDto.kt index 98954d22..d1c83e7c 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/ReadListRequestMatchDto.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/ReadListRequestMatchDto.kt @@ -6,21 +6,18 @@ import org.gotson.komga.domain.model.ReadListRequestMatch data class ReadListRequestMatchDto( val readListMatch: ReadListMatchDto, - val matches: List, + val requests: Collection, val errorCode: String = "", ) fun ReadListRequestMatch.toDto() = ReadListRequestMatchDto( readListMatch.toDto(), - matches.map { + requests.map { request -> ReadListRequestBookMatchesDto( - it.request.toDtoV2(), - it.matches.entries.map { (series, books) -> - ReadListRequestBookMatchDto( - series.id, - books.map { book -> book.id }, - ) + request.request.toDto(), + request.matches.entries.map { (series, books) -> + ReadListRequestBookMatchDto(ReadListRequestBookMatchSeriesDto(series.id, series.title), books.map { ReadListRequestBookMatchBookDto(it.id, it.number, it.title) }) }, ) }, @@ -31,25 +28,35 @@ data class ReadListMatchDto( val errorCode: String = "", ) +fun ReadListMatch.toDto() = ReadListMatchDto(name, errorCode) + data class ReadListRequestBookMatchesDto( - val request: ReadListRequestBookV2Dto, + val request: ReadListRequestBookDto, val matches: List, ) -data class ReadListRequestBookV2Dto( +data class ReadListRequestBookDto( val series: Set, val number: String, ) -fun ReadListRequestBook.toDtoV2() = - ReadListRequestBookV2Dto( +fun ReadListRequestBook.toDto() = + ReadListRequestBookDto( series = series, number = number, ) data class ReadListRequestBookMatchDto( - val seriesId: String, - val bookIds: List, + val series: ReadListRequestBookMatchSeriesDto, + val books: Collection, ) -fun ReadListMatch.toDto() = ReadListMatchDto(name, errorCode) +data class ReadListRequestBookMatchSeriesDto( + val seriesId: String, + val title: String, +) +data class ReadListRequestBookMatchBookDto( + val bookId: String, + val number: String, + val title: String, +) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/ReadListRequestResultDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/ReadListRequestResultDto.kt deleted file mode 100644 index e21fd75f..00000000 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/ReadListRequestResultDto.kt +++ /dev/null @@ -1,42 +0,0 @@ -package org.gotson.komga.interfaces.api.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: String, -) - -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.first(), - number = number, - ) diff --git a/komga/src/main/resources/application-localdb.yml b/komga/src/main/resources/application-localdb.yml index e8cf6c54..b90d83e7 100644 --- a/komga/src/main/resources/application-localdb.yml +++ b/komga/src/main/resources/application-localdb.yml @@ -1,5 +1,5 @@ komga: - workspace: localdb + workspace: diesel database: file: \${komga.config-dir}/\${komga.workspace}.sqlite lucene: 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 index 323c91be..90bbaf6e 100644 --- a/komga/src/test/kotlin/org/gotson/komga/domain/service/ReadListMatcherTest.kt +++ b/komga/src/test/kotlin/org/gotson/komga/domain/service/ReadListMatcherTest.kt @@ -68,212 +68,6 @@ class ReadListMatcherTest( seriesLifecycle.deleteMany(seriesRepository.findAll()) } - @Nested - inner class MatchAndCreate { - @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 = "0025")) - } - } - - val request = ReadListRequest( - name = "readlist", - books = listOf( - ReadListRequestBook(series = setOf("Batman: White Knight"), number = "1"), - ReadListRequestBook(series = setOf("joker"), number = "02"), - ReadListRequestBook(series = setOf("Batman: White Knight"), number = "2"), - ReadListRequestBook(series = setOf("joker"), number = "25"), - ), - ) - - // when - val result = readListMatcher.matchAndCreateReadListRequest(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 = setOf("batman: white knight"), number = "1"), - ReadListRequestBook(series = setOf("joker"), number = "2"), - ReadListRequestBook(series = setOf("BATMAN: WHITE KNIGHT"), number = "2"), - ReadListRequestBook(series = setOf("joker"), number = "25"), - ), - ) - - // when - val result = readListMatcher.matchAndCreateReadListRequest(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 = setOf("tokyo ghost"), number = "1"), - ReadListRequestBook(series = setOf("batman"), number = "3"), - ReadListRequestBook(series = setOf("joker"), number = "3"), - ReadListRequestBook(series = setOf("batman"), number = "2"), - ), - ) - - // when - val result = readListMatcher.matchAndCreateReadListRequest(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") - } - } - - @Test - fun `given request with duplicate books when matching then returns result with appropriate error codes`() { - // given - val booksSeries1 = listOf( - makeBook("book1", libraryId = library.id), - makeBook("book2", libraryId = library.id), - ) - makeSeries(name = "batman", libraryId = library.id).let { s -> - seriesLifecycle.createSeries(s) - seriesLifecycle.addBooks(s, booksSeries1) - seriesLifecycle.sortBooks(s) - } - - val request = ReadListRequest( - name = "readlist", - books = listOf( - ReadListRequestBook(series = setOf("batman"), number = "1"), - ReadListRequestBook(series = setOf("batman"), number = "2"), - ReadListRequestBook(series = setOf("batman"), number = "2"), - ), - ) - - // when - val result = readListMatcher.matchAndCreateReadListRequest(request) - - // then - with(result) { - assertThat(readList).isNotNull - with(readList!!) { - assertThat(name).isEqualTo(request.name) - assertThat(bookIds).hasSize(2) - assertThat(bookIds).containsExactlyEntriesOf( - mapOf( - 0 to booksSeries1[0].id, - 1 to booksSeries1[1].id, - ), - ) - } - - assertThat(errorCode).isBlank - - assertThat(unmatchedBooks).hasSize(1) - - assertThat(unmatchedBooks[0].book).isEqualTo(request.books[2]) - assertThat(unmatchedBooks[0].errorCode).isEqualTo("ERR_1023") - } - } - } - @Nested inner class Match { private fun Collection.mapIds() = map { @@ -313,7 +107,7 @@ class ReadListMatcherTest( } val request = ReadListRequest( - name = "readlist", + name = "readlist request", books = listOf( ReadListRequestBook(series = setOf("Batman: White Knight"), number = "1"), ReadListRequestBook(series = setOf("joker"), number = "02"), @@ -331,9 +125,9 @@ class ReadListMatcherTest( assertThat(name).isEqualTo(request.name) assertThat(errorCode).isBlank } - assertThat(matches).hasSize(4) - assertThat(matches.map { it.request }).containsExactlyElementsOf(request.books) - assertThat(matches.mapIds()).isEqualTo( + assertThat(requests).hasSize(4) + assertThat(requests.map { it.request }).containsExactlyElementsOf(request.books) + assertThat(requests.mapIds()).isEqualTo( listOf( mapOf(series1.id to listOf(booksSeries1[0].id)), mapOf(series2.id to listOf(booksSeries2[1].id)), @@ -397,9 +191,9 @@ class ReadListMatcherTest( assertThat(name).isEqualTo(request.name) assertThat(errorCode).isEqualTo("ERR_1009") } - assertThat(matches).hasSize(4) - assertThat(matches.map { it.request }).containsExactlyElementsOf(request.books) - assertThat(matches.mapIds()).isEqualTo( + assertThat(requests).hasSize(4) + assertThat(requests.map { it.request }).containsExactlyElementsOf(request.books) + assertThat(requests.mapIds()).isEqualTo( listOf( mapOf(series1.id to listOf(booksSeries1[0].id)), mapOf(series2.id to listOf(booksSeries2[1].id)), @@ -459,13 +253,13 @@ class ReadListMatcherTest( assertThat(name).isEqualTo(request.name) assertThat(errorCode).isBlank } - assertThat(matches).hasSize(4) - assertThat(matches.map { it.request }).containsExactlyElementsOf(request.books) - assertThat(matches.mapIds()).isEqualTo( + assertThat(requests).hasSize(4) + assertThat(requests.map { it.request }).containsExactlyElementsOf(request.books) + assertThat(requests.mapIds()).isEqualTo( listOf( emptyMap(), - mapOf(series1.id to emptyList()), - mapOf(series2.id to listOf(booksSeries2[1].id), series2dupe.id to emptyList()), + emptyMap(), + mapOf(series2.id to listOf(booksSeries2[1].id)), mapOf(series1.id to listOf(booksSeries1[0].id, booksSeries1[1].id)), ), ) 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 index 4131f41c..dc6e1f59 100644 --- 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 @@ -41,152 +41,6 @@ class ReadListProviderTest { // 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).containsExactlyInAnyOrder("series 1 (2005)") - assertThat(number).isEqualTo("4") - } - - with(books[1]) { - assertThat(series).containsExactlyInAnyOrder("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() - } - } - - @Nested - inner class ImportFromCblV2 { - @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.importFromCblV2(ByteArray(0)) - // then with(request) { assertThat(name).isEqualTo(cbl.name) @@ -229,7 +83,7 @@ class ReadListProviderTest { every { mockMapper.readValue(any(), ReadingList::class.java) } returns cbl // when - val thrown = catchThrowable { readListProvider.importFromCblV2(ByteArray(0)) } + val thrown = catchThrowable { readListProvider.importFromCbl(ByteArray(0)) } // then assertThat(thrown).isInstanceOf(ComicRackListException::class.java) @@ -247,7 +101,7 @@ class ReadListProviderTest { every { mockMapper.readValue(any(), ReadingList::class.java) } returns cbl // when - val thrown = catchThrowable { readListProvider.importFromCblV2(ByteArray(0)) } + val thrown = catchThrowable { readListProvider.importFromCbl(ByteArray(0)) } // then assertThat(thrown).isInstanceOf(ComicRackListException::class.java) @@ -275,7 +129,7 @@ class ReadListProviderTest { every { mockMapper.readValue(any(), ReadingList::class.java) } returns cbl // when - val thrown = catchThrowable { readListProvider.importFromCblV2(ByteArray(0)) } + val thrown = catchThrowable { readListProvider.importFromCbl(ByteArray(0)) } // then assertThat(thrown).isInstanceOf(ComicRackListException::class.java) @@ -303,7 +157,7 @@ class ReadListProviderTest { every { mockMapper.readValue(any(), ReadingList::class.java) } returns cbl // when - val thrown = catchThrowable { readListProvider.importFromCblV2(ByteArray(0)) } + val thrown = catchThrowable { readListProvider.importFromCbl(ByteArray(0)) } // then assertThat(thrown).isInstanceOf(ComicRackListException::class.java)