mirror of
https://github.com/gotson/komga.git
synced 2025-01-08 11:47:47 +08:00
perf(api): faster readlist matching for cbl
BREAKING-CHANGE: removed api/v1/readlists/import
This commit is contained in:
parent
baa39eb00f
commit
2461c835ad
@ -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 |
|
||||
|
@ -0,0 +1,2 @@
|
||||
create index if not exists idx__series_metadata__title
|
||||
on SERIES_METADATA (TITLE);
|
@ -13,20 +13,9 @@ data class ReadListRequestBook(
|
||||
val number: String,
|
||||
)
|
||||
|
||||
data class ReadListRequestResult(
|
||||
val readList: ReadList?,
|
||||
val unmatchedBooks: List<ReadListRequestResultBook> = emptyList(),
|
||||
val errorCode: String = "",
|
||||
)
|
||||
|
||||
data class ReadListRequestResultBook(
|
||||
val book: ReadListRequestBook,
|
||||
val errorCode: String = "",
|
||||
)
|
||||
|
||||
data class ReadListRequestMatch(
|
||||
val readListMatch: ReadListMatch,
|
||||
val matches: List<ReadListRequestBookMatches>,
|
||||
val requests: Collection<ReadListRequestBookMatches>,
|
||||
val errorCode: String = "",
|
||||
)
|
||||
|
||||
@ -37,5 +26,16 @@ data class ReadListMatch(
|
||||
|
||||
data class ReadListRequestBookMatches(
|
||||
val request: ReadListRequestBook,
|
||||
val matches: Map<Series, Collection<Book>>,
|
||||
val matches: Map<ReadListRequestBookMatchSeries, Collection<ReadListRequestBookMatchBook>>,
|
||||
)
|
||||
|
||||
data class ReadListRequestBookMatchSeries(
|
||||
val id: String,
|
||||
val title: String,
|
||||
)
|
||||
|
||||
data class ReadListRequestBookMatchBook(
|
||||
val id: String,
|
||||
val number: String,
|
||||
val title: String,
|
||||
)
|
||||
|
@ -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<ReadListRequestBook>): Collection<ReadListRequestBookMatches>
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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<String>()
|
||||
val unmatchedBooks = mutableListOf<ReadListRequestResultBook>()
|
||||
|
||||
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)
|
||||
}
|
||||
|
@ -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<ReadListRequestBook>): Collection<ReadListRequestBookMatches> {
|
||||
// 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()),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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<MultipartFile>,
|
||||
): List<ReadListRequestResultDto> =
|
||||
files.map { readListLifecycle.importReadList(it.bytes).toDto(it.originalFilename) }
|
||||
|
||||
@PostMapping("match/comicrack")
|
||||
@PreAuthorize("hasRole('$ROLE_ADMIN')")
|
||||
fun matchFromComicRackList(
|
||||
|
@ -6,21 +6,18 @@ import org.gotson.komga.domain.model.ReadListRequestMatch
|
||||
|
||||
data class ReadListRequestMatchDto(
|
||||
val readListMatch: ReadListMatchDto,
|
||||
val matches: List<ReadListRequestBookMatchesDto>,
|
||||
val requests: Collection<ReadListRequestBookMatchesDto>,
|
||||
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<ReadListRequestBookMatchDto>,
|
||||
)
|
||||
|
||||
data class ReadListRequestBookV2Dto(
|
||||
data class ReadListRequestBookDto(
|
||||
val series: Set<String>,
|
||||
val number: String,
|
||||
)
|
||||
|
||||
fun ReadListRequestBook.toDtoV2() =
|
||||
ReadListRequestBookV2Dto(
|
||||
fun ReadListRequestBook.toDto() =
|
||||
ReadListRequestBookDto(
|
||||
series = series,
|
||||
number = number,
|
||||
)
|
||||
|
||||
data class ReadListRequestBookMatchDto(
|
||||
val seriesId: String,
|
||||
val bookIds: List<String>,
|
||||
val series: ReadListRequestBookMatchSeriesDto,
|
||||
val books: Collection<ReadListRequestBookMatchBookDto>,
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
|
@ -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<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.first(),
|
||||
number = number,
|
||||
)
|
@ -1,5 +1,5 @@
|
||||
komga:
|
||||
workspace: localdb
|
||||
workspace: diesel
|
||||
database:
|
||||
file: \${komga.config-dir}/\${komga.workspace}.sqlite
|
||||
lucene:
|
||||
|
@ -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<ReadListRequestBookMatches>.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)),
|
||||
),
|
||||
)
|
||||
|
@ -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<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()
|
||||
}
|
||||
}
|
||||
|
||||
@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<ByteArray>(), 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<ByteArray>(), 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<ByteArray>(), 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<ByteArray>(), 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<ByteArray>(), 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)
|
||||
|
Loading…
Reference in New Issue
Block a user