feat: read ISBN from barcode

closes #380, closes #381
This commit is contained in:
Gauthier Roebroeck 2021-03-07 11:24:13 +08:00
parent 2987260ca6
commit 6431b1f000
23 changed files with 374 additions and 126 deletions

View File

@ -63,6 +63,7 @@ dependencies {
implementation("commons-io:commons-io:2.8.0")
implementation("org.apache.commons:commons-lang3:3.12.0")
implementation("commons-validator:commons-validator:1.7")
implementation("com.ibm.icu:icu4j:68.2")
@ -82,6 +83,9 @@ dependencies {
runtimeOnly("com.github.jai-imageio:jai-imageio-jpeg2000:1.4.0")
runtimeOnly("org.apache.pdfbox:jbig2-imageio:3.0.3")
// barcode scanning
implementation("com.google.zxing:core:3.4.1")
implementation("com.jakewharton.byteunits:byteunits:0.9.1")
implementation("com.github.f4b6a3:tsid-creator:3.0.1")

View File

@ -0,0 +1,7 @@
alter table library
add column IMPORT_BARCODE_ISBN boolean NOT NULL DEFAULT 1;
alter table book_metadata
add column ISBN varchar NOT NULL DEFAULT '';
alter table book_metadata
add column ISBN_LOCK boolean NOT NULL DEFAULT 0;

View File

@ -11,6 +11,7 @@ class BookMetadata(
val releaseDate: LocalDate? = null,
val authors: List<Author> = emptyList(),
tags: Set<String> = emptySet(),
val isbn: String = "",
val titleLock: Boolean = false,
val summaryLock: Boolean = false,
@ -19,6 +20,7 @@ class BookMetadata(
val releaseDateLock: Boolean = false,
val authorsLock: Boolean = false,
val tagsLock: Boolean = false,
val isbnLock: Boolean = false,
val bookId: String = "",
@ -39,6 +41,7 @@ class BookMetadata(
releaseDate: LocalDate? = this.releaseDate,
authors: List<Author> = this.authors.toList(),
tags: Set<String> = this.tags,
isbn: String = this.isbn,
titleLock: Boolean = this.titleLock,
summaryLock: Boolean = this.summaryLock,
numberLock: Boolean = this.numberLock,
@ -46,6 +49,7 @@ class BookMetadata(
releaseDateLock: Boolean = this.releaseDateLock,
authorsLock: Boolean = this.authorsLock,
tagsLock: Boolean = this.tagsLock,
isbnLock: Boolean = this.isbnLock,
bookId: String = this.bookId,
createdDate: LocalDateTime = this.createdDate,
lastModifiedDate: LocalDateTime = this.lastModifiedDate
@ -58,6 +62,7 @@ class BookMetadata(
releaseDate = releaseDate,
authors = authors,
tags = tags,
isbn = isbn,
titleLock = titleLock,
summaryLock = summaryLock,
numberLock = numberLock,
@ -65,11 +70,12 @@ class BookMetadata(
releaseDateLock = releaseDateLock,
authorsLock = authorsLock,
tagsLock = tagsLock,
isbnLock = isbnLock,
bookId = bookId,
createdDate = createdDate,
lastModifiedDate = lastModifiedDate
)
override fun toString(): String =
"BookMetadata(numberSort=$numberSort, releaseDate=$releaseDate, authors=$authors, titleLock=$titleLock, summaryLock=$summaryLock, numberLock=$numberLock, numberSortLock=$numberSortLock, releaseDateLock=$releaseDateLock, authorsLock=$authorsLock, bookId=$bookId, createdDate=$createdDate, lastModifiedDate=$lastModifiedDate, title='$title', summary='$summary', number='$number')"
"BookMetadata(numberSort=$numberSort, releaseDate=$releaseDate, authors=$authors, isbn='$isbn', titleLock=$titleLock, summaryLock=$summaryLock, numberLock=$numberLock, numberSortLock=$numberSortLock, releaseDateLock=$releaseDateLock, authorsLock=$authorsLock, tagsLock=$tagsLock, isbnLock=$isbnLock, bookId='$bookId', createdDate=$createdDate, lastModifiedDate=$lastModifiedDate, title='$title', summary='$summary', number='$number', tags=$tags)"
}

View File

@ -3,12 +3,13 @@ package org.gotson.komga.domain.model
import java.time.LocalDate
data class BookMetadataPatch(
val title: String?,
val summary: String?,
val number: String?,
val numberSort: Float?,
val releaseDate: LocalDate?,
val authors: List<Author>?,
val title: String? = null,
val summary: String? = null,
val number: String? = null,
val numberSort: Float? = null,
val releaseDate: LocalDate? = null,
val authors: List<Author>? = null,
val isbn: String? = null,
val readLists: List<ReadListEntry> = emptyList()
) {

View File

@ -16,6 +16,7 @@ data class Library(
val importEpubBook: Boolean = true,
val importEpubSeries: Boolean = true,
val importLocalArtwork: Boolean = true,
val importBarcodeIsbn: Boolean = true,
val scanForceModifiedTime: Boolean = false,
val scanDeep: Boolean = false,

View File

@ -24,7 +24,8 @@ class MetadataApplier {
number = getIfNotLocked(number, patch.number, numberLock),
numberSort = getIfNotLocked(numberSort, patch.numberSort, numberSortLock),
releaseDate = getIfNotLocked(releaseDate, patch.releaseDate, releaseDateLock),
authors = getIfNotLocked(authors, patch.authors, authorsLock)
authors = getIfNotLocked(authors, patch.authors, authorsLock),
isbn = getIfNotLocked(isbn, patch.isbn, isbnLock),
)
}

View File

@ -2,6 +2,7 @@ package org.gotson.komga.domain.service
import mu.KotlinLogging
import org.gotson.komga.domain.model.Book
import org.gotson.komga.domain.model.BookMetadataPatch
import org.gotson.komga.domain.model.ReadList
import org.gotson.komga.domain.model.Series
import org.gotson.komga.domain.model.SeriesCollection
@ -16,6 +17,7 @@ import org.gotson.komga.domain.persistence.SeriesCollectionRepository
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.epub.EpubMetadataProvider
import org.gotson.komga.infrastructure.metadata.localartwork.LocalArtworkProvider
@ -58,63 +60,80 @@ class MetadataLifecycle(
logger.debug { "Provider: $provider" }
val patch = provider.getBookMetadataFromBook(book, media)
// handle book metadata
if ((provider is ComicInfoProvider && library.importComicInfoBook) ||
(provider is EpubMetadataProvider && library.importEpubBook)
if (
(provider is ComicInfoProvider && library.importComicInfoBook) ||
(provider is EpubMetadataProvider && library.importEpubBook) ||
(provider is IsbnBarcodeProvider && library.importBarcodeIsbn)
) {
patch?.let { bPatch ->
bookMetadataRepository.findById(book.id).let {
logger.debug { "Original metadata: $it" }
val patched = metadataApplier.apply(bPatch, it)
logger.debug { "Patched metadata: $patched" }
bookMetadataRepository.update(patched)
}
}
handlePatchForBookMetadata(patch, book)
}
// handle read lists
if (provider is ComicInfoProvider && library.importComicInfoReadList) {
patch?.readLists?.forEach { readList ->
readListRepository.findByNameOrNull(readList.name).let { existing ->
if (existing != null) {
if (existing.bookIds.containsValue(book.id))
logger.debug { "Book is already in existing readlist '${existing.name}'" }
else {
val map = existing.bookIds.toSortedMap()
val key = if (readList.number != null && existing.bookIds.containsKey(readList.number)) {
logger.debug { "Existing readlist '${existing.name}' already contains a book at position ${readList.number}, adding book '${book.name}' at the end" }
existing.bookIds.lastKey() + 1
} else {
logger.debug { "Adding book '${book.name}' to existing readlist '${existing.name}'" }
readList.number ?: existing.bookIds.lastKey() + 1
}
map[key] = book.id
readListLifecycle.updateReadList(
existing.copy(bookIds = map)
)
}
} else {
logger.debug { "Adding book '${book.name}' to new readlist '$readList'" }
readListLifecycle.addReadList(
ReadList(
name = readList.name,
bookIds = mapOf((readList.number ?: 0) to book.id).toSortedMap()
)
)
}
}
}
handlePatchForReadLists(patch, book)
}
}
}
}
if (library.importLocalArtwork)
localArtworkProvider.getBookThumbnails(book).forEach {
bookLifecycle.addThumbnailForBook(it)
if (library.importLocalArtwork) refreshMetadataLocalArtwork(book)
}
private fun handlePatchForReadLists(
patch: BookMetadataPatch?,
book: Book
) {
patch?.readLists?.forEach { readList ->
readListRepository.findByNameOrNull(readList.name).let { existing ->
if (existing != null) {
if (existing.bookIds.containsValue(book.id))
logger.debug { "Book is already in existing readlist '${existing.name}'" }
else {
val map = existing.bookIds.toSortedMap()
val key = if (readList.number != null && existing.bookIds.containsKey(readList.number)) {
logger.debug { "Existing readlist '${existing.name}' already contains a book at position ${readList.number}, adding book '${book.name}' at the end" }
existing.bookIds.lastKey() + 1
} else {
logger.debug { "Adding book '${book.name}' to existing readlist '${existing.name}'" }
readList.number ?: existing.bookIds.lastKey() + 1
}
map[key] = book.id
readListLifecycle.updateReadList(
existing.copy(bookIds = map)
)
}
} else {
logger.debug { "Adding book '${book.name}' to new readlist '$readList'" }
readListLifecycle.addReadList(
ReadList(
name = readList.name,
bookIds = mapOf((readList.number ?: 0) to book.id).toSortedMap()
)
)
}
}
}
}
private fun handlePatchForBookMetadata(
patch: BookMetadataPatch?,
book: Book
) {
patch?.let { bPatch ->
bookMetadataRepository.findById(book.id).let {
logger.debug { "Original metadata: $it" }
val patched = metadataApplier.apply(bPatch, it)
logger.debug { "Patched metadata: $patched" }
bookMetadataRepository.update(patched)
}
}
}
private fun refreshMetadataLocalArtwork(book: Book) {
localArtworkProvider.getBookThumbnails(book).forEach {
bookLifecycle.addThumbnailForBook(it)
}
}
fun refreshMetadata(series: Series) {
@ -131,68 +150,83 @@ class MetadataLifecycle(
val patches = bookRepository.findBySeriesId(series.id)
.mapNotNull { provider.getSeriesMetadataFromBook(it, mediaRepository.findById(it.id)) }
// handle series metadata
if ((provider is ComicInfoProvider && library.importComicInfoSeries) ||
if (
(provider is ComicInfoProvider && library.importComicInfoSeries) ||
(provider is EpubMetadataProvider && library.importEpubSeries)
) {
val aggregatedPatch = SeriesMetadataPatch(
title = patches.mostFrequent { it.title },
titleSort = patches.mostFrequent { it.titleSort },
status = patches.mostFrequent { it.status },
genres = patches.mapNotNull { it.genres }.flatten().toSet().ifEmpty { null },
language = patches.mostFrequent { it.language },
summary = null,
readingDirection = patches.mostFrequent { it.readingDirection },
ageRating = patches.mapNotNull { it.ageRating }.maxOrNull(),
publisher = patches.mostFrequent { it.publisher },
collections = emptyList()
)
seriesMetadataRepository.findById(series.id).let {
logger.debug { "Apply metadata for series: $series" }
logger.debug { "Original metadata: $it" }
val patched = metadataApplier.apply(aggregatedPatch, it)
logger.debug { "Patched metadata: $patched" }
seriesMetadataRepository.update(patched)
}
handlePatchForSeriesMetadata(patches, series)
}
// add series to collections
if (provider is ComicInfoProvider && library.importComicInfoCollection) {
patches.flatMap { it.collections }.distinct().forEach { collection ->
collectionRepository.findByNameOrNull(collection).let { existing ->
if (existing != null) {
if (existing.seriesIds.contains(series.id))
logger.debug { "Series is already in existing collection '${existing.name}'" }
else {
logger.debug { "Adding series '${series.name}' to existing collection '${existing.name}'" }
collectionLifecycle.updateCollection(
existing.copy(seriesIds = existing.seriesIds + series.id)
)
}
} else {
logger.debug { "Adding series '${series.name}' to new collection '$collection'" }
collectionLifecycle.addCollection(
SeriesCollection(
name = collection,
seriesIds = listOf(series.id)
)
)
}
}
}
handlePatchForCollections(patches, series)
}
}
}
}
if (library.importLocalArtwork)
localArtworkProvider.getSeriesThumbnails(series).forEach {
seriesLifecycle.addThumbnailForSeries(it)
if (library.importLocalArtwork) refreshMetadataLocalArtwork(series)
}
private fun refreshMetadataLocalArtwork(series: Series) {
localArtworkProvider.getSeriesThumbnails(series).forEach {
seriesLifecycle.addThumbnailForSeries(it)
}
}
private fun handlePatchForCollections(
patches: List<SeriesMetadataPatch>,
series: Series
) {
patches.flatMap { it.collections }.distinct().forEach { collection ->
collectionRepository.findByNameOrNull(collection).let { existing ->
if (existing != null) {
if (existing.seriesIds.contains(series.id))
logger.debug { "Series is already in existing collection '${existing.name}'" }
else {
logger.debug { "Adding series '${series.name}' to existing collection '${existing.name}'" }
collectionLifecycle.updateCollection(
existing.copy(seriesIds = existing.seriesIds + series.id)
)
}
} else {
logger.debug { "Adding series '${series.name}' to new collection '$collection'" }
collectionLifecycle.addCollection(
SeriesCollection(
name = collection,
seriesIds = listOf(series.id)
)
)
}
}
}
}
private fun handlePatchForSeriesMetadata(
patches: List<SeriesMetadataPatch>,
series: Series
) {
val aggregatedPatch = SeriesMetadataPatch(
title = patches.mostFrequent { it.title },
titleSort = patches.mostFrequent { it.titleSort },
status = patches.mostFrequent { it.status },
genres = patches.mapNotNull { it.genres }.flatten().toSet().ifEmpty { null },
language = patches.mostFrequent { it.language },
summary = null,
readingDirection = patches.mostFrequent { it.readingDirection },
ageRating = patches.mapNotNull { it.ageRating }.maxOrNull(),
publisher = patches.mostFrequent { it.publisher },
collections = emptyList()
)
seriesMetadataRepository.findById(series.id).let {
logger.debug { "Apply metadata for series: $series" }
logger.debug { "Original metadata: $it" }
val patched = metadataApplier.apply(aggregatedPatch, it)
logger.debug { "Patched metadata: $patched" }
seriesMetadataRepository.update(patched)
}
}
fun aggregateMetadata(series: Series) {

View File

@ -346,6 +346,8 @@ class BookDtoDao(
authorsLock = authorsLock,
tags = tags,
tagsLock = tagsLock,
isbn = isbn,
isbnLock = isbnLock,
created = createdDate,
lastModified = lastModifiedDate
)

View File

@ -73,8 +73,10 @@ class BookMetadataDao(
d.RELEASE_DATE,
d.RELEASE_DATE_LOCK,
d.AUTHORS_LOCK,
d.TAGS_LOCK
).values(null as String?, null, null, null, null, null, null, null, null, null, null, null, null)
d.TAGS_LOCK,
d.ISBN,
d.ISBN_LOCK
).values(null as String?, null, null, null, null, null, null, null, null, null, null, null, null, null, null)
).also { step ->
metadatas.forEach {
step.bind(
@ -90,7 +92,9 @@ class BookMetadataDao(
it.releaseDate,
it.releaseDateLock,
it.authorsLock,
it.tagsLock
it.tagsLock,
it.isbn,
it.isbnLock
)
}
}.execute()
@ -129,6 +133,8 @@ class BookMetadataDao(
.set(d.RELEASE_DATE_LOCK, metadata.releaseDateLock)
.set(d.AUTHORS_LOCK, metadata.authorsLock)
.set(d.TAGS_LOCK, metadata.tagsLock)
.set(d.ISBN, metadata.isbn)
.set(d.ISBN_LOCK, metadata.isbnLock)
.set(d.LAST_MODIFIED_DATE, LocalDateTime.now(ZoneId.of("Z")))
.where(d.BOOK_ID.eq(metadata.bookId))
.execute()
@ -205,6 +211,7 @@ class BookMetadataDao(
releaseDate = releaseDate,
authors = authors,
tags = tags,
isbn = isbn,
bookId = bookId,
@ -217,7 +224,8 @@ class BookMetadataDao(
numberSortLock = numberSortLock,
releaseDateLock = releaseDateLock,
authorsLock = authorsLock,
tagsLock = tagsLock
tagsLock = tagsLock,
isbnLock = isbnLock,
)
private fun BookMetadataAuthorRecord.toDomain() =

View File

@ -72,6 +72,7 @@ class LibraryDao(
.set(l.IMPORT_EPUB_BOOK, library.importEpubBook)
.set(l.IMPORT_EPUB_SERIES, library.importEpubSeries)
.set(l.IMPORT_LOCAL_ARTWORK, library.importLocalArtwork)
.set(l.IMPORT_BARCODE_ISBN, library.importBarcodeIsbn)
.set(l.SCAN_FORCE_MODIFIED_TIME, library.scanForceModifiedTime)
.set(l.SCAN_DEEP, library.scanDeep)
.execute()
@ -88,6 +89,7 @@ class LibraryDao(
.set(l.IMPORT_EPUB_BOOK, library.importEpubBook)
.set(l.IMPORT_EPUB_SERIES, library.importEpubSeries)
.set(l.IMPORT_LOCAL_ARTWORK, library.importLocalArtwork)
.set(l.IMPORT_BARCODE_ISBN, library.importBarcodeIsbn)
.set(l.SCAN_FORCE_MODIFIED_TIME, library.scanForceModifiedTime)
.set(l.SCAN_DEEP, library.scanDeep)
.set(l.LAST_MODIFIED_DATE, LocalDateTime.now(ZoneId.of("Z")))
@ -108,6 +110,7 @@ class LibraryDao(
importEpubBook = importEpubBook,
importEpubSeries = importEpubSeries,
importLocalArtwork = importLocalArtwork,
importBarcodeIsbn = importBarcodeIsbn,
scanForceModifiedTime = scanForceModifiedTime,
scanDeep = scanDeep,
id = id,

View File

@ -0,0 +1,69 @@
package org.gotson.komga.infrastructure.metadata.barcode
import com.google.zxing.BarcodeFormat
import com.google.zxing.BinaryBitmap
import com.google.zxing.DecodeHintType
import com.google.zxing.MultiFormatReader
import com.google.zxing.RGBLuminanceSource
import com.google.zxing.common.HybridBinarizer
import mu.KotlinLogging
import org.apache.commons.validator.routines.ISBNValidator
import org.gotson.komga.domain.model.Book
import org.gotson.komga.domain.model.BookMetadataPatch
import org.gotson.komga.domain.model.Media
import org.gotson.komga.domain.service.BookAnalyzer
import org.gotson.komga.infrastructure.metadata.BookMetadataProvider
import org.springframework.stereotype.Service
import java.util.EnumSet
import javax.imageio.ImageIO
private val logger = KotlinLogging.logger {}
private const val PAGES_LAST = 3
private const val PAGES_FIRST = 3
@Service
class IsbnBarcodeProvider(
private val bookAnalyzer: BookAnalyzer,
private val validator: ISBNValidator
) : BookMetadataProvider {
private val hints = mapOf(
DecodeHintType.POSSIBLE_FORMATS to EnumSet.of(BarcodeFormat.EAN_13),
DecodeHintType.TRY_HARDER to true
)
override fun getBookMetadataFromBook(book: Book, media: Media): BookMetadataPatch? {
val pagesToTry = (1..media.pages.size).toList().let {
(it.takeLast(PAGES_LAST).reversed() + it.take(PAGES_FIRST)).distinct()
}
for (p in pagesToTry) {
val imageBytes = bookAnalyzer.getPageContent(book, p)
ImageIO.read(imageBytes.inputStream())?.let { image ->
val pixels = image.getRGB(0, 0, image.width, image.height, null, 0, image.width)
val source = RGBLuminanceSource(image.width, image.height, pixels)
val bitmap = BinaryBitmap(HybridBinarizer(source))
val result = try {
MultiFormatReader().decode(bitmap, hints)
} catch (e: Exception) {
null
}
if (result == null || result.text == null) {
logger.debug { "Book page $p does not contain a barcode: $book" }
} else {
if (validator.isValid(result.text)) {
logger.debug { "Book page $p contains barcode which is valid ISBN: '${result.text}'. $book" }
return BookMetadataPatch(isbn = validator.validate(result.text))
} else {
logger.debug { "Book page $p contains barcode which is invalid ISBN: '${result.text}'. $book" }
}
}
}
}
return null
}
}

View File

@ -0,0 +1,12 @@
package org.gotson.komga.infrastructure.metadata.barcode
import org.apache.commons.validator.routines.ISBNValidator
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class IsbnConfiguration {
@Bean
fun isbnValidator() = ISBNValidator(true)
}

View File

@ -56,8 +56,6 @@ class EpubMetadataProvider(
return BookMetadataPatch(
title = title,
summary = description,
number = null,
numberSort = null,
releaseDate = date,
authors = authors
)

View File

@ -443,7 +443,9 @@ class BookController(
tags = if (isSet("tags")) {
if (tags != null) tags!! else emptySet()
} else existing.tags,
tagsLock = tagsLock ?: existing.tagsLock
tagsLock = tagsLock ?: existing.tagsLock,
isbn = isbn?.filter { it.isDigit() } ?: existing.isbn,
isbnLock = isbnLock ?: existing.isbnLock
)
}
bookMetadataRepository.update(updated)

View File

@ -80,6 +80,7 @@ class LibraryController(
importEpubBook = library.importEpubBook,
importEpubSeries = library.importEpubSeries,
importLocalArtwork = library.importLocalArtwork,
importBarcodeIsbn = library.importBarcodeIsbn,
scanForceModifiedTime = library.scanForceModifiedTime,
scanDeep = library.scanDeep
)
@ -114,6 +115,7 @@ class LibraryController(
importEpubBook = library.importEpubBook,
importEpubSeries = library.importEpubSeries,
importLocalArtwork = library.importLocalArtwork,
importBarcodeIsbn = library.importBarcodeIsbn,
scanForceModifiedTime = library.scanForceModifiedTime,
scanDeep = library.scanDeep
)
@ -168,6 +170,7 @@ data class LibraryCreationDto(
val importEpubBook: Boolean = true,
val importEpubSeries: Boolean = true,
val importLocalArtwork: Boolean = true,
val importBarcodeIsbn: Boolean = true,
val scanForceModifiedTime: Boolean = false,
val scanDeep: Boolean = false
)
@ -183,6 +186,7 @@ data class LibraryDto(
val importEpubBook: Boolean,
val importEpubSeries: Boolean,
val importLocalArtwork: Boolean,
val importBarcodeIsbn: Boolean,
val scanForceModifiedTime: Boolean,
val scanDeep: Boolean
)
@ -197,6 +201,7 @@ data class LibraryUpdateDto(
val importEpubBook: Boolean,
val importEpubSeries: Boolean,
val importLocalArtwork: Boolean,
val importBarcodeIsbn: Boolean,
val scanForceModifiedTime: Boolean,
val scanDeep: Boolean
)
@ -212,6 +217,7 @@ fun Library.toDto(includeRoot: Boolean) = LibraryDto(
importEpubBook = importEpubBook,
importEpubSeries = importEpubSeries,
importLocalArtwork = importLocalArtwork,
importBarcodeIsbn = importBarcodeIsbn,
scanForceModifiedTime = scanForceModifiedTime,
scanDeep = scanDeep
)

View File

@ -52,6 +52,8 @@ data class BookMetadataDto(
val authorsLock: Boolean,
val tags: Set<String>,
val tagsLock: Boolean,
val isbn: String,
val isbnLock: Boolean,
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
val created: LocalDateTime,

View File

@ -1,6 +1,7 @@
package org.gotson.komga.interfaces.rest.dto
import org.gotson.komga.infrastructure.validation.NullOrNotBlank
import org.hibernate.validator.constraints.ISBN
import java.time.LocalDate
import javax.validation.Valid
import javax.validation.constraints.NotBlank
@ -49,6 +50,11 @@ class BookMetadataUpdateDto {
}
var tagsLock: Boolean? = null
@get:ISBN
var isbn: String? = null
var isbnLock: Boolean? = null
}
class AuthorUpdateDto {

View File

@ -67,6 +67,7 @@ class BookMetadataDaoTest(
releaseDate = LocalDate.now(),
authors = listOf(Author("author", "role")),
tags = setOf("tag", "another"),
isbn = "987654321",
bookId = book.id,
titleLock = true,
summaryLock = true,
@ -74,7 +75,8 @@ class BookMetadataDaoTest(
numberSortLock = true,
releaseDateLock = true,
authorsLock = true,
tagsLock = true
tagsLock = true,
isbnLock = true,
)
bookMetadataDao.insert(metadata)
@ -95,6 +97,7 @@ class BookMetadataDaoTest(
assertThat(role).isEqualTo(metadata.authors.first().role)
}
assertThat(created.tags).containsAll(metadata.tags)
assertThat(created.isbn).isEqualTo(metadata.isbn)
assertThat(created.titleLock).isEqualTo(metadata.titleLock)
assertThat(created.summaryLock).isEqualTo(metadata.summaryLock)
@ -103,6 +106,7 @@ class BookMetadataDaoTest(
assertThat(created.releaseDateLock).isEqualTo(metadata.releaseDateLock)
assertThat(created.authorsLock).isEqualTo(metadata.authorsLock)
assertThat(created.tagsLock).isEqualTo(metadata.tagsLock)
assertThat(created.isbnLock).isEqualTo(metadata.isbnLock)
}
@Test
@ -120,20 +124,22 @@ class BookMetadataDaoTest(
assertThat(created.bookId).isEqualTo(book.id)
assertThat(created.title).isEqualTo(metadata.title)
assertThat(created.summary).isBlank()
assertThat(created.summary).isBlank
assertThat(created.number).isEqualTo(metadata.number)
assertThat(created.numberSort).isEqualTo(metadata.numberSort)
assertThat(created.releaseDate).isNull()
assertThat(created.authors).isEmpty()
assertThat(created.tags).isEmpty()
assertThat(created.isbn).isBlank
assertThat(created.titleLock).isFalse()
assertThat(created.summaryLock).isFalse()
assertThat(created.numberLock).isFalse()
assertThat(created.numberSortLock).isFalse()
assertThat(created.releaseDateLock).isFalse()
assertThat(created.authorsLock).isFalse()
assertThat(created.tagsLock).isFalse()
assertThat(created.titleLock).isFalse
assertThat(created.summaryLock).isFalse
assertThat(created.numberLock).isFalse
assertThat(created.numberSortLock).isFalse
assertThat(created.releaseDateLock).isFalse
assertThat(created.authorsLock).isFalse
assertThat(created.tagsLock).isFalse
assertThat(created.isbnLock).isFalse
}
@Test
@ -160,13 +166,15 @@ class BookMetadataDaoTest(
releaseDate = LocalDate.now(),
authors = listOf(Author("author2", "role2")),
tags = setOf("another"),
isbn = "987654321",
titleLock = true,
summaryLock = true,
numberLock = true,
numberSortLock = true,
releaseDateLock = true,
authorsLock = true,
tagsLock = true
tagsLock = true,
isbnLock = true,
)
}
@ -183,6 +191,7 @@ class BookMetadataDaoTest(
assertThat(modified.summary).isEqualTo(updated.summary)
assertThat(modified.number).isEqualTo(updated.number)
assertThat(modified.numberSort).isEqualTo(updated.numberSort)
assertThat(modified.isbn).isEqualTo(updated.isbn)
assertThat(modified.titleLock).isEqualTo(updated.titleLock)
assertThat(modified.summaryLock).isEqualTo(updated.summaryLock)
@ -191,6 +200,7 @@ class BookMetadataDaoTest(
assertThat(modified.releaseDateLock).isEqualTo(updated.releaseDateLock)
assertThat(modified.authorsLock).isEqualTo(updated.authorsLock)
assertThat(modified.tagsLock).isEqualTo(updated.tagsLock)
assertThat(modified.isbnLock).isEqualTo(updated.isbnLock)
assertThat(modified.tags).containsAll(updated.tags)
assertThat(modified.authors.first().name).isEqualTo(updated.authors.first().name)

View File

@ -59,7 +59,10 @@ class LibraryDaoTest(
importEpubBook = false,
importComicInfoCollection = false,
importComicInfoSeries = false,
importComicInfoBook = false
importComicInfoBook = false,
importComicInfoReadList = false,
importBarcodeIsbn = false,
importLocalArtwork = false,
)
}
@ -79,6 +82,9 @@ class LibraryDaoTest(
assertThat(modified.importComicInfoCollection).isEqualTo(updated.importComicInfoCollection)
assertThat(modified.importComicInfoSeries).isEqualTo(updated.importComicInfoSeries)
assertThat(modified.importComicInfoBook).isEqualTo(updated.importComicInfoBook)
assertThat(modified.importComicInfoReadList).isEqualTo(updated.importComicInfoReadList)
assertThat(modified.importBarcodeIsbn).isEqualTo(updated.importBarcodeIsbn)
assertThat(modified.importLocalArtwork).isEqualTo(updated.importLocalArtwork)
}
@Test

View File

@ -0,0 +1,64 @@
package org.gotson.komga.infrastructure.metadata.barcode
import io.mockk.every
import io.mockk.mockk
import org.apache.commons.validator.routines.ISBNValidator
import org.assertj.core.api.Assertions.assertThat
import org.gotson.komga.domain.model.BookPage
import org.gotson.komga.domain.model.Media
import org.gotson.komga.domain.model.makeBook
import org.gotson.komga.domain.service.BookAnalyzer
import org.junit.jupiter.api.Test
import org.springframework.core.io.ClassPathResource
class IsbnBarcodeProviderTest {
private val mockAnalyzer = mockk<BookAnalyzer>()
private val isbnBarcodeProvider = IsbnBarcodeProvider(mockAnalyzer, ISBNValidator(true))
@Test
fun `given book page with barcode when getting book metadata then ISBN is returned`() {
// given
val file = ClassPathResource("barcode/page_384.jpg").file
every { mockAnalyzer.getPageContent(any(), any()) } returns file.readBytes()
val book = makeBook("Book1")
val media = Media(pages = listOf(BookPage("page", "image/jpeg")))
// when
val patch = isbnBarcodeProvider.getBookMetadataFromBook(book, media)
// then
assertThat(patch?.isbn).isEqualTo("9782811632397")
}
@Test
fun `given invalid image page when getting book metadata then patch is null`() {
// given
every { mockAnalyzer.getPageContent(any(), any()) } returns ByteArray(0)
val book = makeBook("Book1")
val media = Media(pages = listOf(BookPage("page", "image/jpeg")))
// when
val patch = isbnBarcodeProvider.getBookMetadataFromBook(book, media)
// then
assertThat(patch).isNull()
}
@Test
fun `given page without barcode when getting book metadata then patch is null`() {
// given
val file = ClassPathResource("barcode/komga.png").file
every { mockAnalyzer.getPageContent(any(), any()) } returns file.readBytes()
val book = makeBook("Book1")
val media = Media(pages = listOf(BookPage("page", "image/jpeg")))
// when
val patch = isbnBarcodeProvider.getBookMetadataFromBook(book, media)
// then
assertThat(patch).isNull()
}
}

View File

@ -583,7 +583,9 @@ class BookControllerTest(
strings = [
"""{"title":""}""",
"""{"number":""}""",
"""{"authors":"[{"name":""}]"}"""
"""{"authors":"[{"name":""}]"}""",
"""{"isbn":"1617290459"}""", // isbn 10
"""{"isbn":"978-123-456-789-6"}""", // invalid check digit
]
)
@WithMockCustomUser(roles = [ROLE_ADMIN])
@ -632,7 +634,9 @@ class BookControllerTest(
],
"authorsLock":true,
"tags":["tag"],
"tagsLock":true
"tagsLock":true,
"isbn":"978-161-729-045-9abc xxxoefj",
"isbnLock":true
}
""".trimIndent()
@ -658,6 +662,7 @@ class BookControllerTest(
tuple("newAuthor2", "newauthorrole2")
)
assertThat(tags).containsExactly("tag")
assertThat(isbn).isEqualTo("9781617290459")
assertThat(titleLock).isEqualTo(true)
assertThat(summaryLock).isEqualTo(true)
@ -666,6 +671,7 @@ class BookControllerTest(
assertThat(releaseDateLock).isEqualTo(true)
assertThat(authorsLock).isEqualTo(true)
assertThat(tagsLock).isEqualTo(true)
assertThat(isbnLock).isEqualTo(true)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB