From 1fdbf376125f76dd52c6f5bf0d479323dc2c50fd Mon Sep 17 00:00:00 2001 From: bayang Date: Tue, 5 Nov 2024 21:02:28 +0100 Subject: [PATCH] feat: add inventaireio metadata plugin --- build.gradle.kts | 2 + src/jelu-ui/src/components/AddBook.vue | 36 ++ src/jelu-ui/src/components/BookDetail.vue | 61 ++- src/jelu-ui/src/components/MetadataDetail.vue | 24 + .../src/components/MetadataPluginsModal.vue | 11 +- src/jelu-ui/src/locales/en.json | 12 +- src/jelu-ui/src/model/Book.ts | 4 + src/jelu-ui/src/model/Metadata.ts | 6 +- .../github/bayang/jelu/config/GlobalConfig.kt | 6 + .../bayang/jelu/config/JeluProperties.kt | 1 + .../jelu/controllers/MetadataController.kt | 4 +- .../github/bayang/jelu/dao/BookRepository.kt | 16 + .../io/github/bayang/jelu/dao/BookTable.kt | 16 + .../io/github/bayang/jelu/dto/BookDto.kt | 16 + .../io/github/bayang/jelu/dto/MetadataDto.kt | 5 + .../github/bayang/jelu/search/LuceneEntity.kt | 4 + .../bayang/jelu/service/AppLifecycleAware.kt | 4 +- .../bayang/jelu/service/SearchIndexService.kt | 2 +- .../jelu/service/imports/CsvImportService.kt | 1 - .../service/metadata/FetchMetadataService.kt | 12 +- .../providers/CalibreMetadataProvider.kt | 12 +- .../providers/DebugMetadataProvider.kt | 6 +- .../providers/GoogleBooksIMetaDataProvider.kt | 14 +- .../metadata/providers/IMetaDataProvider.kt | 4 +- .../providers/InventaireIoMetadataProvider.kt | 440 ++++++++++++++++++ .../service/metadata/providers/Wikidata.kt | 29 ++ src/main/resources/application-dev.yml | 4 + src/main/resources/liquibase.xml | 12 + .../metadata/FetchMetadataServiceTest.kt | 21 +- .../GoogleBooksIMetaDataProviderTest.kt | 2 +- .../InventaireIoMetadataProviderTest.kt | 145 ++++++ 31 files changed, 886 insertions(+), 46 deletions(-) create mode 100644 src/main/kotlin/io/github/bayang/jelu/service/metadata/providers/InventaireIoMetadataProvider.kt create mode 100644 src/main/kotlin/io/github/bayang/jelu/service/metadata/providers/Wikidata.kt create mode 100644 src/test/kotlin/io/github/bayang/jelu/service/metadata/providers/InventaireIoMetadataProviderTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index bd0edbb7..f8e1c3d0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -50,6 +50,8 @@ dependencies { // implementation("com.unboundid:unboundid-ldapsdk:6.0.5") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation("org.apache.httpcomponents.client5:httpclient5") + implementation("com.github.ben-manes.caffeine:caffeine") implementation("com.fasterxml.staxmate:staxmate:2.4.1") diff --git a/src/jelu-ui/src/components/AddBook.vue b/src/jelu-ui/src/components/AddBook.vue index c6b09c95..86f1b504 100644 --- a/src/jelu-ui/src/components/AddBook.vue +++ b/src/jelu-ui/src/components/AddBook.vue @@ -52,6 +52,10 @@ const form = reactive({ amazonId: "", goodreadsId: "", librarythingId: "", + isfdbId: "", + openlibraryId: "", + noosfereId: "", + inventaireId: "", language: "" }); const eventType = ref(null); @@ -210,6 +214,10 @@ const fillBook = (formdata: any, publishedDate: Date | null): UserBook => { amazonId: formdata.amazonId, goodreadsId: formdata.goodreadsId, librarythingId: formdata.librarythingId, + isfdbId: formdata.isfdbId, + openlibraryId: formdata.openlibraryId, + noosfereId: formdata.noosfereId, + inventaireId: formdata.inventaireId, language: formdata.language, authors: [], translators: [], @@ -249,6 +257,10 @@ const clearForm = () => { form.googleId = "" form.goodreadsId = "" form.librarythingId = "" + form.isfdbId = "" + form.openlibraryId = "" + form.noosfereId = "" + form.inventaireId = "" form.percentRead = null form.currentPageNumber = null }; @@ -701,6 +713,30 @@ let displayDatepicker = computed(() => { :placeholder="t('book.librarything_id')" class="input focus:input-accent" /> + + + +
diff --git a/src/jelu-ui/src/components/BookDetail.vue b/src/jelu-ui/src/components/BookDetail.vue index 159290f0..773e9190 100644 --- a/src/jelu-ui/src/components/BookDetail.vue +++ b/src/jelu-ui/src/components/BookDetail.vue @@ -123,7 +123,11 @@ const sortedEvents = computed(() => { const hasExternalLink = computed(() => book.value?.book.amazonId != null || book.value?.book.goodreadsId != null || book.value?.book.googleId != null - || book.value?.book.librarythingId != null) + || book.value?.book.librarythingId != null + || book.value?.book.openlibraryId != null + || book.value?.book.isfdbId != null + || book.value?.book.noosfereId != null + || book.value?.book.inventaireId != null) function modalClosed() { console.log("modal closed") @@ -516,6 +520,16 @@ const formatSeries = async (series: Series) => { const timestamp = () => new Date().toISOString() let currentTimestamp = timestamp() +const getIsbn = (): string|null => { + if (book.value?.book.isbn13 && book.value.book.isbn13.length > 0) { + return book.value.book.isbn13.replaceAll("-", "") + } + if (book.value?.book.isbn10 && book.value.book.isbn10.length > 0) { + return book.value.book.isbn10.replaceAll("-", "") + } + return null +} + getBook() @@ -875,6 +889,51 @@ getBook() target="_blank" >librarything + + ISFDB + + + Openlibrary + + + Noosfere + + + inventaire + + + inventaire +
{{ t('book.amazon_id') }} : {{ metadata.amazonId }}

+

+ {{ t('book.isfdb_id') }} : {{ metadata.isfdbId }} +

+

+ {{ t('book.openlibrary_id') }} : {{ metadata.openlibraryId }} +

+

+ {{ t('book.noosfere_id') }} : {{ metadata.noosfereId }} +

+

+ {{ t('book.inventaire_id') }} : {{ metadata.inventaireId }} +

diff --git a/src/jelu-ui/src/components/MetadataPluginsModal.vue b/src/jelu-ui/src/components/MetadataPluginsModal.vue index a5d2a442..c4a6653a 100644 --- a/src/jelu-ui/src/components/MetadataPluginsModal.vue +++ b/src/jelu-ui/src/components/MetadataPluginsModal.vue @@ -75,7 +75,9 @@ function checkMove(evt: any){ > {{ t('metadata.reorder_plugins') }} -

{{ t('metadata.description') }}

+

+ {{ t('metadata.description') }} +

-
+
+

+ {{ t('metadata.note') }} +

diff --git a/src/jelu-ui/src/locales/en.json b/src/jelu-ui/src/locales/en.json index 9846061a..61d5699a 100644 --- a/src/jelu-ui/src/locales/en.json +++ b/src/jelu-ui/src/locales/en.json @@ -112,7 +112,8 @@ "avg_rating" : "average rating : {rating}", "add_quote" : "add book quote", "description" : "description", - "user_rating" : "my rating : {rating}" + "user_rating" : "my rating : {rating}", + "apply" : "apply" }, "settings" : { "pick_language" : "Pick your language", @@ -202,6 +203,10 @@ "goodreads_id" : "Goodreads Id", "amazon_id" : "Amazon Id", "librarything_id" : "Librarything Id", + "isfdb_id" : "ISFDB Id", + "openlibrary_id" : "Openlibrary Id", + "noosfere_id" : "Noosfere Id", + "inventaire_id" : "inventaire id", "publisher" : "publisher", "published_date" : "published date", "page_count" : "page count", @@ -337,8 +342,9 @@ }, "metadata" : { "reorder_plugins" : "reorder plugins", - "description" : "Plugins will be called from top to bottom, disabled plugins will not be used", - "default_priority" : "default priority" + "description" : "Plugins will be called from top to bottom, disabled plugins will not be used.", + "default_priority" : "default priority", + "note" : "This change is only temporary, if you want to permanently reorder the plugins you have to edit the config." }, "book_quotes" : { "edit_quote" : "Edit book quote", diff --git a/src/jelu-ui/src/model/Book.ts b/src/jelu-ui/src/model/Book.ts index 488a6e50..8c5c5254 100644 --- a/src/jelu-ui/src/model/Book.ts +++ b/src/jelu-ui/src/model/Book.ts @@ -23,6 +23,10 @@ export interface Book { amazonId?: string, goodreadsId?: string, librarythingId?: string, + isfdbId?: string, + openlibraryId?: string, + noosfereId?: string, + inventaireId?: string, language?: string, userBookId?: string, userbook?: UserBook, diff --git a/src/jelu-ui/src/model/Metadata.ts b/src/jelu-ui/src/model/Metadata.ts index 9157fded..e5f3ea06 100644 --- a/src/jelu-ui/src/model/Metadata.ts +++ b/src/jelu-ui/src/model/Metadata.ts @@ -15,4 +15,8 @@ export interface Metadata { googleId?: string, amazonId?: string, goodreadsId?: string, -} \ No newline at end of file + isfdbId?: string, + openlibraryId?: string, + noosfereId?: string, + inventaireId?: string, +} diff --git a/src/main/kotlin/io/github/bayang/jelu/config/GlobalConfig.kt b/src/main/kotlin/io/github/bayang/jelu/config/GlobalConfig.kt index 5d01fa76..a5d3ccf3 100644 --- a/src/main/kotlin/io/github/bayang/jelu/config/GlobalConfig.kt +++ b/src/main/kotlin/io/github/bayang/jelu/config/GlobalConfig.kt @@ -8,6 +8,7 @@ import org.springframework.http.client.reactive.ReactorClientHttpConnector import org.springframework.http.codec.ClientCodecConfigurer import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.web.client.RestClient import org.springframework.web.cors.CorsConfiguration import org.springframework.web.cors.UrlBasedCorsConfigurationSource import org.springframework.web.reactive.function.client.ExchangeStrategies @@ -31,6 +32,11 @@ class GlobalConfig { ).build() } + @Bean("springRestClient") + fun springRestClient(): RestClient { + return RestClient.create() + } + @Bean("passwordEncoder") fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder() diff --git a/src/main/kotlin/io/github/bayang/jelu/config/JeluProperties.kt b/src/main/kotlin/io/github/bayang/jelu/config/JeluProperties.kt index afdfd6d8..46764048 100644 --- a/src/main/kotlin/io/github/bayang/jelu/config/JeluProperties.kt +++ b/src/main/kotlin/io/github/bayang/jelu/config/JeluProperties.kt @@ -26,6 +26,7 @@ data class JeluProperties( var isEnabled: Boolean = false, var apiKey: String?, var order: Int = -1000, + var config: String? = null, ) data class Database( diff --git a/src/main/kotlin/io/github/bayang/jelu/controllers/MetadataController.kt b/src/main/kotlin/io/github/bayang/jelu/controllers/MetadataController.kt index 7d20d42d..4b1d05e7 100644 --- a/src/main/kotlin/io/github/bayang/jelu/controllers/MetadataController.kt +++ b/src/main/kotlin/io/github/bayang/jelu/controllers/MetadataController.kt @@ -41,7 +41,7 @@ class MetadataController( @RequestParam(name = "isbn", required = false) isbn: String?, @RequestParam(name = "title", required = false) title: String?, @RequestParam(name = "authors", required = false) authors: String?, - ): Mono = + ): MetadataDto = if (pluginInfoHolder.plugins().isEmpty()) { throw JeluException("Automatic fetching of metadata is disabled, install calibre or configure a metadata plugin") } else { @@ -71,7 +71,7 @@ class MetadataController( fun fetchMetadata( @RequestBody @Valid metadataRequestDto: MetadataRequestDto, - ): Mono { + ): MetadataDto { if (pluginInfoHolder.plugins().isEmpty()) { throw JeluException("Automatic fetching of metadata is disabled, install calibre or configure a metadata plugin") } diff --git a/src/main/kotlin/io/github/bayang/jelu/dao/BookRepository.kt b/src/main/kotlin/io/github/bayang/jelu/dao/BookRepository.kt index 53a1526f..7240446a 100644 --- a/src/main/kotlin/io/github/bayang/jelu/dao/BookRepository.kt +++ b/src/main/kotlin/io/github/bayang/jelu/dao/BookRepository.kt @@ -662,6 +662,18 @@ class BookRepository( book.librarythingId?.let { updated.librarythingId = book.librarythingId.trim() } + book.isfdbId?.let { + updated.isfdbId = book.isfdbId.trim() + } + book.openlibraryId?.let { + updated.openlibraryId = book.openlibraryId.trim() + } + book.noosfereId?.let { + updated.noosfereId = book.noosfereId + } + book.inventaireId?.let { + updated.inventaireId = book.inventaireId + } book.language?.let { updated.language = book.language.trim() } @@ -929,6 +941,10 @@ class BookRepository( this.goodreadsId = cleanString(book.goodreadsId) this.googleId = cleanString(book.googleId) this.librarythingId = cleanString(book.librarythingId) + this.isfdbId = cleanString(book.isfdbId) + this.openlibraryId = cleanString(book.openlibraryId) + this.noosfereId = cleanString(book.noosfereId) + this.inventaireId = cleanString(book.inventaireId) this.language = cleanString(book.language) } book.series?.forEach { diff --git a/src/main/kotlin/io/github/bayang/jelu/dao/BookTable.kt b/src/main/kotlin/io/github/bayang/jelu/dao/BookTable.kt index a6ada75d..ef75d5d2 100644 --- a/src/main/kotlin/io/github/bayang/jelu/dao/BookTable.kt +++ b/src/main/kotlin/io/github/bayang/jelu/dao/BookTable.kt @@ -27,6 +27,10 @@ object BookTable : UUIDTable("book") { val goodreadsId: Column = varchar("goodreads_id", 30).nullable() val amazonId: Column = varchar("amazon_id", 30).nullable() val librarythingId: Column = varchar("librarything_id", 30).nullable() + val isfdbId: Column = varchar("isfdb_id", length = 30).nullable() + val openlibraryId: Column = varchar("openlibrary_id", 30).nullable() + val noosfereId: Column = varchar("noosfere_id", 128).nullable() + val inventaireId: Column = varchar("inventaire_id", 128).nullable() val language: Column = varchar("language", 30).nullable() } class Book(id: EntityID) : UUIDEntity(id) { @@ -52,6 +56,10 @@ class Book(id: EntityID) : UUIDEntity(id) { var amazonId by BookTable.amazonId var goodreadsId by BookTable.goodreadsId var librarythingId by BookTable.librarythingId + var isfdbId by BookTable.isfdbId + var openlibraryId by BookTable.openlibraryId + var noosfereId by BookTable.noosfereId + var inventaireId by BookTable.inventaireId val userBooks by UserBook referrersOn UserBookTable.book var userBookId: UUID? = null var userBook: UserBook? = null @@ -75,6 +83,10 @@ class Book(id: EntityID) : UUIDEntity(id) { googleId = this.googleId, amazonId = this.amazonId, librarythingId = this.librarythingId, + isfdbId = this.isfdbId, + openlibraryId = this.openlibraryId, + noosfereId = this.noosfereId, + inventaireId = this.inventaireId, language = this.language, authors = this.authors.map { it.toAuthorDto() }, translators = this.translators.map { it.toAuthorDto() }, @@ -100,6 +112,10 @@ class Book(id: EntityID) : UUIDEntity(id) { googleId = this.googleId, amazonId = this.amazonId, librarythingId = this.librarythingId, + isfdbId = this.isfdbId, + openlibraryId = this.openlibraryId, + noosfereId = this.noosfereId, + inventaireId = this.inventaireId, language = this.language, authors = this.authors.map { it.toAuthorDto() }, translators = this.translators.map { it.toAuthorDto() }, diff --git a/src/main/kotlin/io/github/bayang/jelu/dto/BookDto.kt b/src/main/kotlin/io/github/bayang/jelu/dto/BookDto.kt index cbe4a94f..139ee95c 100644 --- a/src/main/kotlin/io/github/bayang/jelu/dto/BookDto.kt +++ b/src/main/kotlin/io/github/bayang/jelu/dto/BookDto.kt @@ -23,6 +23,10 @@ data class BookDto( val amazonId: String?, val goodreadsId: String?, val librarythingId: String?, + val isfdbId: String?, + val openlibraryId: String?, + val noosfereId: String?, + val inventaireId: String?, val language: String?, val userBookId: UUID?, val userbook: UserBookLightWithoutBookDto?, @@ -46,6 +50,10 @@ data class BookCreateDto( var amazonId: String? = null, var goodreadsId: String? = null, var librarythingId: String? = null, + var isfdbId: String? = null, + var openlibraryId: String? = null, + var noosfereId: String? = null, + var inventaireId: String? = null, var language: String? = null, ) @@ -65,6 +73,10 @@ data class BookUpdateDto( val amazonId: String?, val goodreadsId: String?, val librarythingId: String?, + val isfdbId: String?, + val openlibraryId: String?, + val noosfereId: String?, + val inventaireId: String?, val language: String?, var series: List?, ) @@ -129,6 +141,10 @@ fun fromBookCreateDto(dto: BookCreateDto): BookUpdateDto { amazonId = dto.amazonId, goodreadsId = dto.goodreadsId, librarythingId = dto.librarythingId, + isfdbId = dto.isfdbId, + openlibraryId = dto.openlibraryId, + noosfereId = dto.noosfereId, + inventaireId = dto.inventaireId, language = dto.language, series = dto.series, ) diff --git a/src/main/kotlin/io/github/bayang/jelu/dto/MetadataDto.kt b/src/main/kotlin/io/github/bayang/jelu/dto/MetadataDto.kt index cb965b6c..2cca0bdd 100644 --- a/src/main/kotlin/io/github/bayang/jelu/dto/MetadataDto.kt +++ b/src/main/kotlin/io/github/bayang/jelu/dto/MetadataDto.kt @@ -17,6 +17,11 @@ data class MetadataDto( var googleId: String? = null, var amazonId: String? = null, var goodreadsId: String? = null, + var librarythingId: String? = null, + var isfdbId: String? = null, + var openlibraryId: String? = null, + var inventaireId: String? = null, + var noosfereId: String? = null, ) data class MetadataRequestDto( val isbn: String? = null, diff --git a/src/main/kotlin/io/github/bayang/jelu/search/LuceneEntity.kt b/src/main/kotlin/io/github/bayang/jelu/search/LuceneEntity.kt index 1319923e..656245a0 100644 --- a/src/main/kotlin/io/github/bayang/jelu/search/LuceneEntity.kt +++ b/src/main/kotlin/io/github/bayang/jelu/search/LuceneEntity.kt @@ -42,6 +42,10 @@ fun Book.toDocument() = if (!goodreadsId.isNullOrBlank()) add(TextField("goodreadsId", goodreadsId, Field.Store.NO)) if (!amazonId.isNullOrBlank()) add(TextField("amazonId", amazonId, Field.Store.NO)) if (!librarythingId.isNullOrBlank()) add(TextField("librarythingId", librarythingId, Field.Store.NO)) + if (!noosfereId.isNullOrBlank()) add(TextField("noosfereId", noosfereId, Field.Store.NO)) + if (!isfdbId.isNullOrBlank()) add(TextField("isfdbId", isfdbId, Field.Store.NO)) + if (!inventaireId.isNullOrBlank()) add(TextField("inventaireId", inventaireId, Field.Store.NO)) + if (!openlibraryId.isNullOrBlank()) add(TextField("openlibraryId", openlibraryId, Field.Store.NO)) add(StringField(LuceneEntity.TYPE, LuceneEntity.Book.type, Field.Store.NO)) add(StringField(LuceneEntity.Book.id, id.value.toString(), Field.Store.YES)) diff --git a/src/main/kotlin/io/github/bayang/jelu/service/AppLifecycleAware.kt b/src/main/kotlin/io/github/bayang/jelu/service/AppLifecycleAware.kt index a0e02b20..5ccfccb9 100644 --- a/src/main/kotlin/io/github/bayang/jelu/service/AppLifecycleAware.kt +++ b/src/main/kotlin/io/github/bayang/jelu/service/AppLifecycleAware.kt @@ -51,8 +51,8 @@ class AppLifecycleAware( val nowString: String = OffsetDateTime.now(ZoneId.systemDefault()).format(DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss")) logger.info { "Series data not migrated, starting migration at at $nowString" } bookService.migrateSeries() - var end = System.currentTimeMillis() - var deltaInSec = (end - start) / 1000 + val end = System.currentTimeMillis() + val deltaInSec = (end - start) / 1000 logger.info { "Series data migration completed after : $deltaInSec seconds, check your data" } val seriesMigrated = lifeCycleService.setSeriesMigrated() logger.debug { "lifeCycled updated to ${seriesMigrated.seriesMigrated}" } diff --git a/src/main/kotlin/io/github/bayang/jelu/service/SearchIndexService.kt b/src/main/kotlin/io/github/bayang/jelu/service/SearchIndexService.kt index 27bab6ca..39b247e6 100644 --- a/src/main/kotlin/io/github/bayang/jelu/service/SearchIndexService.kt +++ b/src/main/kotlin/io/github/bayang/jelu/service/SearchIndexService.kt @@ -17,7 +17,7 @@ import java.util.UUID import kotlin.math.ceil import kotlin.time.measureTime -const val INDEX_VERSION = 1 +const val INDEX_VERSION = 2 private val logger = KotlinLogging.logger {} @Component diff --git a/src/main/kotlin/io/github/bayang/jelu/service/imports/CsvImportService.kt b/src/main/kotlin/io/github/bayang/jelu/service/imports/CsvImportService.kt index a39fcceb..7793fd9a 100644 --- a/src/main/kotlin/io/github/bayang/jelu/service/imports/CsvImportService.kt +++ b/src/main/kotlin/io/github/bayang/jelu/service/imports/CsvImportService.kt @@ -166,7 +166,6 @@ class CsvImportService( config[CalibreMetadataProvider.fetchCover] = importConfig.shouldFetchCovers.toString() metadata = fetchMetadataService .fetchMetadata(MetadataRequestDto(isbn), config) - .block()!! } else { logger.debug { "no isbn on entity ${importEntity.id}, not fetching metadata" } } diff --git a/src/main/kotlin/io/github/bayang/jelu/service/metadata/FetchMetadataService.kt b/src/main/kotlin/io/github/bayang/jelu/service/metadata/FetchMetadataService.kt index 6ec0c3da..37fdb5e5 100644 --- a/src/main/kotlin/io/github/bayang/jelu/service/metadata/FetchMetadataService.kt +++ b/src/main/kotlin/io/github/bayang/jelu/service/metadata/FetchMetadataService.kt @@ -6,7 +6,7 @@ import io.github.bayang.jelu.service.metadata.providers.IMetaDataProvider import io.github.bayang.jelu.utils.PluginInfoComparator import mu.KotlinLogging import org.springframework.stereotype.Service -import reactor.core.publisher.Mono +import java.util.Optional private val logger = KotlinLogging.logger {} @@ -19,7 +19,7 @@ class FetchMetadataService( fun fetchMetadata( metadataRequestDto: MetadataRequestDto, config: Map = mapOf(), - ): Mono { + ): MetadataDto { var pluginsToUse = if (metadataRequestDto.plugins.isNullOrEmpty()) pluginInfoHolder.plugins() else metadataRequestDto.plugins pluginsToUse = pluginsToUse.toMutableList() // pluginInfoHolder sorts plugins, but plugins received via metadataRequestDto @@ -30,14 +30,14 @@ class FetchMetadataService( logger.trace { "fetching provider for plugin ${plugin.name} with order ${plugin.order} " } val provider = providers.find { plugin.name.equals(it.name(), true) } if (provider != null) { - val res: Mono? = provider.fetchMetadata(metadataRequestDto, config) - if (res != null) { - return res + val res: Optional = provider.fetchMetadata(metadataRequestDto, config) + if (res.isPresent) { + return res.get() } } else { logger.warn { "could not find provider for plugin info ${plugin.name}" } } } - return Mono.just(MetadataDto()) + return MetadataDto() } } diff --git a/src/main/kotlin/io/github/bayang/jelu/service/metadata/providers/CalibreMetadataProvider.kt b/src/main/kotlin/io/github/bayang/jelu/service/metadata/providers/CalibreMetadataProvider.kt index ad0e1479..9b6115ba 100644 --- a/src/main/kotlin/io/github/bayang/jelu/service/metadata/providers/CalibreMetadataProvider.kt +++ b/src/main/kotlin/io/github/bayang/jelu/service/metadata/providers/CalibreMetadataProvider.kt @@ -11,9 +11,9 @@ import mu.KotlinLogging import org.apache.commons.validator.routines.ISBNValidator import org.codehaus.staxmate.SMInputFactory import org.springframework.stereotype.Service -import reactor.core.publisher.Mono import java.io.BufferedReader import java.io.File +import java.util.Optional private val logger = KotlinLogging.logger {} @@ -35,12 +35,12 @@ class CalibreMetadataProvider( override fun fetchMetadata( metadataRequestDto: MetadataRequestDto, config: Map, - ): Mono? { + ): Optional { if (metadataRequestDto.isbn.isNullOrBlank() && metadataRequestDto.title.isNullOrBlank() && metadataRequestDto.authors.isNullOrBlank() ) { logger.error { "At least one of isbn, authors or title is required to fetch metadata" } - return null + return Optional.empty() } val onlyUseCorePlugins: Boolean = if (config.containsKey(CalibreMetadataProvider.onlyUseCorePlugins)) { config[CalibreMetadataProvider.onlyUseCorePlugins].toBoolean() @@ -134,14 +134,14 @@ class CalibreMetadataProvider( parseOpf.image = targetCover.name logger.trace { "fetch metadata image ${targetCover.name}" } } - return Mono.just(parseOpf) + return Optional.of(parseOpf) } else { logger.error { "fetch ebookmetadata process exited abnormally with code $exitVal" } - return null + return Optional.empty() } } catch (e: Exception) { logger.error(e) { "failure while calling fetch-ebook-metadata process" } - return null + return Optional.empty() } } diff --git a/src/main/kotlin/io/github/bayang/jelu/service/metadata/providers/DebugMetadataProvider.kt b/src/main/kotlin/io/github/bayang/jelu/service/metadata/providers/DebugMetadataProvider.kt index 3e4b98dd..56e082a4 100644 --- a/src/main/kotlin/io/github/bayang/jelu/service/metadata/providers/DebugMetadataProvider.kt +++ b/src/main/kotlin/io/github/bayang/jelu/service/metadata/providers/DebugMetadataProvider.kt @@ -5,7 +5,7 @@ import io.github.bayang.jelu.dto.MetadataRequestDto import io.github.bayang.jelu.service.metadata.PluginInfoHolder import mu.KotlinLogging import org.springframework.stereotype.Service -import reactor.core.publisher.Mono +import java.util.Optional private val logger = KotlinLogging.logger {} @@ -15,12 +15,12 @@ class DebugMetadataProvider : IMetaDataProvider { override fun fetchMetadata( metadataRequestDto: MetadataRequestDto, config: Map, - ): Mono? { + ): Optional { logger.debug { "debug plugin called with isbn ${metadataRequestDto.isbn}, title ${metadataRequestDto.title}, " + "authors ${metadataRequestDto.authors}, config $config, plugins ${metadataRequestDto.plugins}" } - return null + return Optional.empty() } override fun name(): String = PluginInfoHolder.jelu_debug diff --git a/src/main/kotlin/io/github/bayang/jelu/service/metadata/providers/GoogleBooksIMetaDataProvider.kt b/src/main/kotlin/io/github/bayang/jelu/service/metadata/providers/GoogleBooksIMetaDataProvider.kt index 232a2db5..bfb941f2 100644 --- a/src/main/kotlin/io/github/bayang/jelu/service/metadata/providers/GoogleBooksIMetaDataProvider.kt +++ b/src/main/kotlin/io/github/bayang/jelu/service/metadata/providers/GoogleBooksIMetaDataProvider.kt @@ -11,7 +11,8 @@ import org.springframework.http.HttpStatus import org.springframework.stereotype.Service import org.springframework.web.reactive.function.client.WebClient import org.springframework.web.util.UriBuilder -import reactor.core.publisher.Mono +import java.time.Duration +import java.util.Optional private val logger = KotlinLogging.logger {} @@ -26,12 +27,12 @@ class GoogleBooksIMetaDataProvider( override fun fetchMetadata( metadataRequestDto: MetadataRequestDto, config: Map, - ): Mono? { + ): Optional { val googleProviderApiKey = getGoogleProviderApiKey() if (googleProviderApiKey.isNullOrBlank() || metadataRequestDto.isbn.isNullOrBlank()) { - return null + return Optional.empty() } - return restClient.get() + val res = restClient.get() .uri { uriBuilder: UriBuilder -> uriBuilder .scheme("https") @@ -55,6 +56,11 @@ class GoogleBooksIMetaDataProvider( null } } + .block(Duration.ofSeconds(60)) + if (res == null) { + return Optional.empty() + } + return Optional.of(res) } override fun name(): String { diff --git a/src/main/kotlin/io/github/bayang/jelu/service/metadata/providers/IMetaDataProvider.kt b/src/main/kotlin/io/github/bayang/jelu/service/metadata/providers/IMetaDataProvider.kt index 10854b7e..13936631 100644 --- a/src/main/kotlin/io/github/bayang/jelu/service/metadata/providers/IMetaDataProvider.kt +++ b/src/main/kotlin/io/github/bayang/jelu/service/metadata/providers/IMetaDataProvider.kt @@ -2,13 +2,13 @@ package io.github.bayang.jelu.service.metadata.providers import io.github.bayang.jelu.dto.MetadataDto import io.github.bayang.jelu.dto.MetadataRequestDto -import reactor.core.publisher.Mono +import java.util.Optional interface IMetaDataProvider { fun fetchMetadata( metadataRequestDto: MetadataRequestDto, config: Map = mapOf(), - ): Mono? + ): Optional fun name(): String } diff --git a/src/main/kotlin/io/github/bayang/jelu/service/metadata/providers/InventaireIoMetadataProvider.kt b/src/main/kotlin/io/github/bayang/jelu/service/metadata/providers/InventaireIoMetadataProvider.kt new file mode 100644 index 00000000..5ad3fb18 --- /dev/null +++ b/src/main/kotlin/io/github/bayang/jelu/service/metadata/providers/InventaireIoMetadataProvider.kt @@ -0,0 +1,440 @@ +package io.github.bayang.jelu.service.metadata.providers + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import io.github.bayang.jelu.config.JeluProperties +import io.github.bayang.jelu.dto.MetadataDto +import io.github.bayang.jelu.dto.MetadataRequestDto +import jakarta.annotation.Resource +import mu.KotlinLogging +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Service +import org.springframework.web.client.RestClient +import java.util.Optional + +private val logger = KotlinLogging.logger {} + +@Service +class InventaireIoMetadataProvider( + @Resource(name = "springRestClient") private val restClient: RestClient, + private val properties: JeluProperties, + private val objectMapper: ObjectMapper, +) : IMetaDataProvider { + + private val _name = "inventaireio" + + private val inventaireHost = "https://inventaire.io" + private val inventaireApi = "$inventaireHost/api/" + + private val defaultLanguageCode: String = "en" + + override fun fetchMetadata(metadataRequestDto: MetadataRequestDto, config: Map): Optional { + if (!metadataRequestDto.isbn.isNullOrBlank()) { + val isbn = metadataRequestDto.isbn.replace("-", "", true) + return restClient.get() + .uri(inventaireApi) { + uriBuilder -> + uriBuilder + .path("entities") + .queryParam("action", "by-uris") + .queryParam("uris", "isbn:$isbn") + .build() + } + .exchange { clientRequest, clientResponse -> + if (clientResponse.statusCode == HttpStatus.OK) { + val bodyString = clientResponse.bodyTo(String::class.java) + val node = objectMapper.readTree(bodyString).get("entities") + var p = parseIsbnResult(node, isbn) + p = enrichWithEditionResult(p) + p = enrichWithAuhors(p) + p = enrichWithGenres(p) + p = enrichWithSeries(p) + Optional.of(toMetadataDto(p)) + } else { + val b = clientResponse.bodyTo(String::class.java).orEmpty() + logger.error { "error fetching metadata from inventaire.io : ${clientResponse.statusCode}; $b " } + Optional.empty() + } + } + } else if (!metadataRequestDto.title.isNullOrBlank()) { + var p = searchByTitle(metadataRequestDto.title) + p = enrichWithEditionResult(p) + p = enrichWithAuhors(p) + p = enrichWithSeries(p) + p = enrichWithGenres(p) + return Optional.of(toMetadataDto(p)) + } else if (!metadataRequestDto.authors.isNullOrBlank()) { + return searchAuthorsTitles(metadataRequestDto.authors) + } + return Optional.empty() + } + + private fun searchAuthorsTitles(author: String): Optional { + return restClient.get() + .uri(inventaireApi) { + uriBuilder -> + uriBuilder + .path("search") + .queryParam("types", "humans") + .queryParam("search", author) + .build() + } + .exchange { clientRequest, clientResponse -> + if (clientResponse.statusCode == HttpStatus.OK) { + val bodyString = clientResponse.bodyTo(String::class.java) + val node = objectMapper.readTree(bodyString).get("results") + val authorUri = parseSearchAuthorsResults(node) + var p = getAuthorBooks(authorUri) + p = enrichWithEditionResult(p) + p = enrichWithAuhors(p) + p = enrichWithSeries(p) + p = enrichWithGenres(p) + Optional.of(toMetadataDto(p)) + } else { + logger.error { "error fetching metadata from inventaire.io : ${clientResponse.statusCode} " } + Optional.empty() + } + } + } + + private fun searchByTitle(title: String): ParsingDto { + return restClient.get() + .uri(inventaireApi) { + uriBuilder -> + uriBuilder + .path("search") + .queryParam("types", "works") + .queryParam("search", title) + .build() + } + .exchange { clientRequest, clientResponse -> + if (clientResponse.statusCode == HttpStatus.OK) { + val bodyString = clientResponse.bodyTo(String::class.java) + val node = objectMapper.readTree(bodyString).get("results") + parseSearchResults(node) + } else { + logger.error { "error fetching metadata from inventaire.io : ${clientResponse.statusCode} " } + ParsingDto(MetadataDto(), "") + } + } + } + + private fun parseSearchResults(node: JsonNode): ParsingDto { + val firstResult = node.asIterable().first() + val dto = MetadataDto() + if (firstResult.has("label")) { + dto.title = firstResult.get("label").asText() + } + if (firstResult.has("description")) { + dto.summary = firstResult.get("description").asText() + } + val res = ParsingDto(dto, "") + if (firstResult.has("uri")) { + res.editionClaim = firstResult.get("uri").asText() + } + if (firstResult.has("image")) { + val imgParent = firstResult.get("image") + if (!imgParent.isEmpty) { + val img = imgParent[0].asText() + dto.image = imagePath(img) + } + } + return res + } + + private fun parseSearchAuthorsResults(node: JsonNode): String { + val firstResult = node.asIterable().first() + if (firstResult.has("uri")) { + return firstResult.get("uri").asText() + } + return "" + } + + private fun parseIsbnResult(node: JsonNode, isbn: String): ParsingDto { + val dto = MetadataDto() + val parsingDto = ParsingDto(dto, "") + if (node.has("isbn:$isbn")) { + val data = node.get("isbn:$isbn") + if (data.has("claims")) { + val claims = data["claims"] + parseClaims(claims, parsingDto) + } + if (data.has("originalLang")) { + dto.language = data["originalLang"].asText() + } + if (data.has("image") && data.get("image").get("url") != null) { + dto.image = imagePath(data["image"]["url"].asText()) + } + if (data.has("invId")) { + dto.inventaireId = data["invId"].asText() + } + } + return parsingDto + } + + /** + * a local image path + * (ex: /img/entities/57883743aa7c6ad25885a63e6e94349ec4f71562) + * that you are can then request resized + * (ex: https://inventaire.io/img/entities/300x300/57883743aa7c6ad25885a63e6e94349ec4f71562) + * a Wikimedia Commons file name + * (ex: Les Deux Nigauds Comtesse de Ségur.jpg) which can then also be requested resized + * (ex: https://commons.wikimedia.org/wiki/Special:FilePath/Les%20Deux%20Nigauds%20Comtesse%20de%20S%C3%A9gur.jpg?width=300, + * be sure to URL-encode the filename) + */ + fun imagePath(url: String): String { + return if (url.startsWith("/img/")) { + inventaireHost + url + } else { + "https://commons.wikimedia.org/wiki/Special:FilePath/$url" + } + } + + fun parseClaims(node: JsonNode, dto: ParsingDto) { + parseClaims(node, dto.metadataDto) + if (node.has(Wikidata.AUTHOR)) { + val authors = node[Wikidata.AUTHOR].asIterable() + authors.forEach { dto.authorsClaims.add(it.asText()) } + } + if (dto.editionClaim.isBlank() && node.has(Wikidata.EDITION_OR_TRANSLATION)) { + dto.editionClaim = getFieldOrNull(Wikidata.EDITION_OR_TRANSLATION, node).orEmpty() + } + if (node.has(Wikidata.SERIES)) { + val series = node[Wikidata.SERIES].asIterable() + series.forEach { dto.seriesClaims.add(it.asText()) } + } + if (node.has(Wikidata.GENRE)) { + val genres = node[Wikidata.GENRE].asIterable() + genres.forEach { dto.genresClaims.add(it.asText()) } + } + } + + private fun parseClaims(node: JsonNode, dto: MetadataDto) { + if (dto.title == null) { + dto.title = getFieldOrNull(Wikidata.TITLE, node) + } + if (dto.isbn10 == null) { + dto.isbn10 = getFieldOrNull(Wikidata.ISBN10, node) + } + if (dto.isbn13 == null) { + dto.isbn13 = getFieldOrNull(Wikidata.ISBN13, node) + } + if (dto.pageCount == null && node.has(Wikidata.NB_PAGES)) { + dto.pageCount = node[Wikidata.NB_PAGES][0].asInt() + } + if (dto.goodreadsId == null) { + dto.goodreadsId = getFieldOrNull(Wikidata.GOODREADS_ID, node) + } + if (dto.publishedDate == null) { + dto.publishedDate = getFieldOrNull(Wikidata.PUBLICATION_DATE, node) + } + if (dto.librarythingId == null) { + dto.librarythingId = getFieldOrNull(Wikidata.LIBRARYTHING_WORK_ID, node) + } + if (dto.isfdbId == null) { + dto.isfdbId = getFieldOrNull(Wikidata.ISFDB_TITLE_ID, node) + } + if (dto.openlibraryId == null) { + dto.openlibraryId = getFieldOrNull(Wikidata.OPEN_LIBRARY_ID, node) + } + if (dto.noosfereId == null) { + dto.noosfereId = getFieldOrNull(Wikidata.NOOSFERE_BOOK_ID, node) + } + if (dto.numberInSeries == null && node.has(Wikidata.SERIES_ORDINAL)) { + dto.numberInSeries = node[Wikidata.SERIES_ORDINAL][0].asDouble() + } + } + + private fun getFieldOrNull(fieldName: String, node: JsonNode): String? { + if (node.has(fieldName)) { + return node[fieldName][0].asText() + } + return null + } + + private fun getAuthorBooks(authorUri: String): ParsingDto { + if (authorUri.isNotBlank()) { + return restClient.get() + .uri(inventaireApi) { + uriBuilder -> + uriBuilder + .path("entities") + .queryParam("action", "author-works") + .queryParam("uri", authorUri) + .build() + } + .exchange { clientRequest, clientResponse -> + if (clientResponse.statusCode == HttpStatus.OK) { + val bodyString = clientResponse.bodyTo(String::class.java) + val node = objectMapper.readTree(bodyString).get("works") + parseAuthorWorks(node) + } else { + logger.error { "error fetching metadata from edition from inventaire.io : ${clientResponse.statusCode} " } + ParsingDto(MetadataDto(), "") + } + } + } + return ParsingDto(MetadataDto(), "") + } + + private fun parseAuthorWorks(node: JsonNode): ParsingDto { + val parsingDto = ParsingDto(MetadataDto(), "") + val first = node.asIterable().first() + if (first.has("uri")) { + parsingDto.editionClaim = first["uri"].asText() + } + if (first.has("serie")) { + parsingDto.seriesClaims.add(first["serie"].asText()) + } + return parsingDto + } + + private fun enrichWithEditionResult(dto: ParsingDto): ParsingDto { + if (dto.editionClaim.isNotBlank()) { + return restClient.get() + .uri(inventaireApi) { + uriBuilder -> + uriBuilder + .path("entities") + .queryParam("action", "by-uris") + .queryParam("uris", dto.editionClaim) + .build() + } + .exchange { clientRequest, clientResponse -> + if (clientResponse.statusCode == HttpStatus.OK) { + val bodyString = clientResponse.bodyTo(String::class.java) + val node = objectMapper.readTree(bodyString).get("entities") + parseEditionResult(node, dto) + } else { + logger.error { "error fetching metadata from edition from inventaire.io : ${clientResponse.statusCode} " } + dto + } + } + } + return dto + } + + fun enrichWithAuhors(dto: ParsingDto): ParsingDto { + if (dto.authorsClaims.isNotEmpty()) { + dto.authorsClaims.stream() + .forEach { author -> + val res = fetchDataPage(author) + dto.metadataDto.authors.add(res) + } + } + return dto + } + + fun enrichWithSeries(dto: ParsingDto): ParsingDto { + if (dto.seriesClaims.isNotEmpty()) { + dto.seriesClaims.stream() + .forEach { series -> + val res = fetchDataPage(series) + dto.metadataDto.series = res + } + } + return dto + } + + fun enrichWithGenres(dto: ParsingDto): ParsingDto { + if (dto.genresClaims.isNotEmpty()) { + dto.genresClaims.stream() + .forEach { genre -> + val res = fetchDataPage(genre) + dto.metadataDto.tags.add(res) + } + } + return dto + } + + fun fetchDataPage(dataId: String): String { + return restClient.get() + .uri(inventaireApi) { + uriBuilder -> + uriBuilder + .path("entities") + .queryParam("action", "by-uris") + .queryParam("uris", dataId) + .build() + } + .exchange { clientRequest, clientResponse -> + if (clientResponse.statusCode == HttpStatus.OK) { + val bodyString = clientResponse.bodyTo(String::class.java) + val node = objectMapper.readTree(bodyString).get("entities") + parseDataBody(node, dataId) + } else { + logger.error { "error fetching metadata from data : $dataId from inventaire.io : ${clientResponse.statusCode} " } + "" + } + } + } + + /** + * author page, genre page, series page + */ + fun parseDataBody(node: JsonNode, dataId: String): String { + var res = "" + if (node.has(dataId)) { + val data = node.get(dataId) + if (data.has("labels")) { + val dataLabels = data.get("labels") + if (!getPreferredLanguage().isNullOrBlank() && dataLabels.has(getPreferredLanguage())) { + res = dataLabels.get(getPreferredLanguage()).asText() + } else if (dataLabels.has(defaultLanguageCode)) { + res = dataLabels.get(defaultLanguageCode).asText() + } else if (dataLabels.size() > 0) { + res = dataLabels.first().asText() + } + } + } + return res + } + + private fun parseEditionResult(node: JsonNode, dto: ParsingDto): ParsingDto { + if (node.has(dto.editionClaim)) { + val data = node.get(dto.editionClaim) + if (data.has("descriptions")) { + val descs = data.get("descriptions") + if (!getPreferredLanguage().isNullOrBlank() && descs.has(getPreferredLanguage())) { + dto.metadataDto.summary = descs.get(getPreferredLanguage()).asText() + } else if (descs.has(defaultLanguageCode)) { + dto.metadataDto.summary = descs.get(defaultLanguageCode).asText() + } else { + dto.metadataDto.summary = descs.first().asText() + } + } + if (data.has("claims")) { + val claims = data.get("claims") + parseClaims(claims, dto) + } + if (dto.metadataDto.title == null) { + dto.metadataDto.title = parseDataBody(node, dto.editionClaim) + } + if (data.has("invId") && dto.metadataDto.inventaireId == null) { + dto.metadataDto.inventaireId = data["invId"].asText() + } + } + return dto + } + + private fun toMetadataDto(dto: ParsingDto): MetadataDto { + return dto.metadataDto + } + + override fun name(): String { + return _name + } + + private fun getPreferredLanguage(): String? = properties + .metadataProviders + ?.find { it.isEnabled && it.name == _name } + ?.config +} + +data class ParsingDto( + val metadataDto: MetadataDto, + var editionClaim: String, + val authorsClaims: MutableSet = mutableSetOf(), + val seriesClaims: MutableSet = mutableSetOf(), + val genresClaims: MutableSet = mutableSetOf(), +) diff --git a/src/main/kotlin/io/github/bayang/jelu/service/metadata/providers/Wikidata.kt b/src/main/kotlin/io/github/bayang/jelu/service/metadata/providers/Wikidata.kt new file mode 100644 index 00000000..9856073f --- /dev/null +++ b/src/main/kotlin/io/github/bayang/jelu/service/metadata/providers/Wikidata.kt @@ -0,0 +1,29 @@ +package io.github.bayang.jelu.service.metadata.providers + +/** + * see https://www.wikidata.org/wiki/Property:P212 + */ +class Wikidata { + companion object { + const val PREFIX = "wdt:" + const val TITLE = "${PREFIX}P1476" + const val ISBN13 = "${PREFIX}P212" + const val ISBN10 = "${PREFIX}P957" + const val EDITION_OR_TRANSLATION = "${PREFIX}P629" + const val PUBLISHER = "${PREFIX}P123" + const val PUBLICATION_DATE = "${PREFIX}P577" + const val NB_PAGES = "${PREFIX}P1104" + const val GOODREADS_ID = "${PREFIX}P2969" + const val LANGUAGE_OF_WORK_OR_NAME = "${PREFIX}P407" + const val AUTHOR = "${PREFIX}P50" + const val GENRE = "${PREFIX}P136" + const val SERIES = "${PREFIX}P179" + const val OPEN_LIBRARY_ID = "${PREFIX}P648" + const val MAIN_SUBJECT = "${PREFIX}P921" + const val LIBRARYTHING_WORK_ID = "${PREFIX}P1085" + const val ISFDB_TITLE_ID = "${PREFIX}P1274" + const val GOODREADS_WORK_ID = "${PREFIX}P8383" + const val NOOSFERE_BOOK_ID = "${PREFIX}P5571" + const val SERIES_ORDINAL = "${PREFIX}P1545" + } +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 72dd8c84..3e713a29 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -11,6 +11,10 @@ jelu: apiKey: "" order: -100000 name: "jelu-debug" + - name: "inventaireio" + is-enabled: false + order: 200000 + config: "fr" metadata: calibre: path: /usr/bin/fetch-ebook-metadata diff --git a/src/main/resources/liquibase.xml b/src/main/resources/liquibase.xml index c79d394f..25e29fdf 100644 --- a/src/main/resources/liquibase.xml +++ b/src/main/resources/liquibase.xml @@ -606,4 +606,16 @@ ); + + + ALTER TABLE book ADD isfdb_id varchar(30); + ALTER TABLE book ADD openlibrary_id varchar(30); + + + + + ALTER TABLE book ADD noosfere_id varchar(128); + ALTER TABLE book ADD inventaire_id varchar(128); + + diff --git a/src/test/kotlin/io/github/bayang/jelu/service/metadata/FetchMetadataServiceTest.kt b/src/test/kotlin/io/github/bayang/jelu/service/metadata/FetchMetadataServiceTest.kt index 8f786bc5..177ccbff 100644 --- a/src/test/kotlin/io/github/bayang/jelu/service/metadata/FetchMetadataServiceTest.kt +++ b/src/test/kotlin/io/github/bayang/jelu/service/metadata/FetchMetadataServiceTest.kt @@ -10,6 +10,7 @@ import io.mockk.every import io.mockk.mockk import io.mockk.verify import org.junit.jupiter.api.Test +import java.util.Optional class FetchMetadataServiceTest { @@ -27,10 +28,10 @@ class FetchMetadataServiceTest { val providers = mutableListOf() val jeluDebug = mockk() every { jeluDebug.name() } returns PluginInfoHolder.jelu_debug - every { jeluDebug.fetchMetadata(any(), any()) } returns null + every { jeluDebug.fetchMetadata(any(), any()) } returns Optional.empty() val calibre = mockk() every { calibre.name() } returns PluginInfoHolder.calibre - every { calibre.fetchMetadata(any(), any()) } returns null + every { calibre.fetchMetadata(any(), any()) } returns Optional.empty() providers.add(jeluDebug) providers.add(calibre) val info = PluginInfoHolder(jeluProperties) @@ -54,10 +55,10 @@ class FetchMetadataServiceTest { val providers = mutableListOf() val jeluDebug = mockk() every { jeluDebug.name() } returns PluginInfoHolder.jelu_debug - every { jeluDebug.fetchMetadata(any(), any()) } returns null + every { jeluDebug.fetchMetadata(any(), any()) } returns Optional.empty() val calibre = mockk() every { calibre.name() } returns PluginInfoHolder.calibre - every { calibre.fetchMetadata(any(), any()) } returns null + every { calibre.fetchMetadata(any(), any()) } returns Optional.empty() providers.add(jeluDebug) providers.add(calibre) val info = PluginInfoHolder(jeluProperties) @@ -91,10 +92,10 @@ class FetchMetadataServiceTest { val providers = mutableListOf() val jeluDebug = mockk() every { jeluDebug.name() } returns PluginInfoHolder.jelu_debug - every { jeluDebug.fetchMetadata(any(), any()) } returns null + every { jeluDebug.fetchMetadata(any(), any()) } returns Optional.empty() val calibre = mockk() every { calibre.name() } returns PluginInfoHolder.calibre - every { calibre.fetchMetadata(any(), any()) } returns null + every { calibre.fetchMetadata(any(), any()) } returns Optional.empty() providers.add(jeluDebug) providers.add(calibre) val info = PluginInfoHolder(jeluProperties) @@ -118,10 +119,10 @@ class FetchMetadataServiceTest { val providers = mutableListOf() val jeluDebug = mockk() every { jeluDebug.name() } returns PluginInfoHolder.jelu_debug - every { jeluDebug.fetchMetadata(any(), any()) } returns null + every { jeluDebug.fetchMetadata(any(), any()) } returns Optional.empty() val calibre = mockk() every { calibre.name() } returns PluginInfoHolder.calibre - every { calibre.fetchMetadata(any(), any()) } returns null + every { calibre.fetchMetadata(any(), any()) } returns Optional.empty() providers.add(jeluDebug) providers.add(calibre) val info = PluginInfoHolder(jeluProperties) @@ -159,10 +160,10 @@ class FetchMetadataServiceTest { val providers = mutableListOf() val jeluDebug = mockk() every { jeluDebug.name() } returns PluginInfoHolder.jelu_debug - every { jeluDebug.fetchMetadata(any(), any()) } returns null + every { jeluDebug.fetchMetadata(any(), any()) } returns Optional.empty() val calibre = mockk() every { calibre.name() } returns PluginInfoHolder.calibre - every { calibre.fetchMetadata(any(), any()) } returns null + every { calibre.fetchMetadata(any(), any()) } returns Optional.empty() providers.add(jeluDebug) providers.add(calibre) val info = PluginInfoHolder(jeluProperties) diff --git a/src/test/kotlin/io/github/bayang/jelu/service/metadata/providers/GoogleBooksIMetaDataProviderTest.kt b/src/test/kotlin/io/github/bayang/jelu/service/metadata/providers/GoogleBooksIMetaDataProviderTest.kt index 75c0b012..7ff59f08 100644 --- a/src/test/kotlin/io/github/bayang/jelu/service/metadata/providers/GoogleBooksIMetaDataProviderTest.kt +++ b/src/test/kotlin/io/github/bayang/jelu/service/metadata/providers/GoogleBooksIMetaDataProviderTest.kt @@ -116,7 +116,7 @@ class GoogleBooksIMetaDataProviderTest { val result: MetadataDto = service.fetchMetadata( MetadataRequestDto("9781785650406"), mapOf(), - )?.block()!! + ).get() // Then Assertions.assertNotNull(result) diff --git a/src/test/kotlin/io/github/bayang/jelu/service/metadata/providers/InventaireIoMetadataProviderTest.kt b/src/test/kotlin/io/github/bayang/jelu/service/metadata/providers/InventaireIoMetadataProviderTest.kt new file mode 100644 index 00000000..4f80c4f5 --- /dev/null +++ b/src/test/kotlin/io/github/bayang/jelu/service/metadata/providers/InventaireIoMetadataProviderTest.kt @@ -0,0 +1,145 @@ +package io.github.bayang.jelu.service.metadata.providers + +import com.fasterxml.jackson.databind.ObjectMapper +import io.github.bayang.jelu.config.JeluProperties +import io.github.bayang.jelu.dto.MetadataDto +import io.github.bayang.jelu.dto.MetadataRequestDto +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.web.client.MockRestServiceServer +import org.springframework.test.web.client.match.MockRestRequestMatchers +import org.springframework.test.web.client.response.MockRestResponseCreators +import org.springframework.web.client.RestClient + +@SpringBootTest +class InventaireIoMetadataProviderTest( + @Autowired private val springRestClient: RestClient, +) { + + @Test + fun fetchMetadata_fromCorrectIsbn_returnsBookMetaDataDto() { + val builder = springRestClient.mutate() + val serv = MockRestServiceServer.bindTo(builder).build() + val isbn = MockRestResponseCreators.withSuccess().body( + """ + {"entities":{"isbn:9782290349229":{"_id":"d59e3e64f92c6340fbb10c5dcf7c0abf","_rev":"4-2db713fd44c6ade760f623367ec7c33b","type":"edition","labels":{"fromclaims":"L'homme aux cercles bleus"},"claims":{"wdt:P31":["wd:Q3331189"],"wdt:P212":["978-2-290-34922-9"],"wdt:P957":["2-290-34922-4"],"wdt:P407":["wd:Q150"],"wdt:P1476":["L'homme aux cercles bleus"],"wdt:P629":["wd:Q3203603"],"wdt:P123":["wd:Q3156592"],"invp:P2":["57883743aa7c6ad25885a63e6e94349ec4f71562"],"wdt:P577":["2005-05-01"],"wdt:P1104":[220],"wdt:P2969":["1508217"]},"created":1485023383338,"updated":1668681738527,"version":6,"uri":"isbn:9782290349229","originalLang":"fr","image":{"url":"/img/entities/57883743aa7c6ad25885a63e6e94349ec4f71562"},"invId":"d59e3e64f92c6340fbb10c5dcf7c0abf"}},"redirects":{}} + """, + ) + val edition = MockRestResponseCreators.withSuccess().body( + """ + {"entities":{"wd:Q3203603":{"uri":"wd:Q3203603","wdId":"Q3203603","type":"work","labels":{"fr":"L'Homme aux cercles bleus","en":"The Chalk Circle Man","it":"L'uomo dei cerchi azzurri","ja":"青チョークの男","nl":"The Chalk Circle Man","de":"Es geht noch ein Zug von der Gare du Nord","ko":"파란 동그라미의 사나이","ga":"L'Homme aux cercles bleus"},"aliases":{"fr":["L'homme aux cercles bleus","L'Homme Aux Cercles Bleus"]},"descriptions":{"it":"romanzo scritto da Fred Vargas","en":"1991 mystery novel by Fred Vargas, 1st of her Detective Adamsberg series","de":"Mystery-Kriminalroman von Fred Vargas (1991), erster Roman der Adamsberg-Reihe","fr":"roman de Fred Vargas","nl":"boek van Fred Vargas","he":"ספר","es":"libro de Fred Vargas","br":"romant","ko":"프레드 바르가스의 소설","ar":"عمل مكتوب"},"sitelinks":{"enwiki":{"title":"The Chalk Circle Man","badges":[]},"frwiki":{"title":"L'Homme aux cercles bleus","badges":[]},"itwiki":{"title":"L'uomo dei cerchi azzurri","badges":[]},"jawiki":{"title":"青チョークの男","badges":[]}},"claims":{"wdt:P31":["wd:Q47461344","wd:Q7725634"],"wdt:P136":["wd:Q182015","wd:Q5937792"],"wdt:P179":["wd:Q27536277"],"wdt:P407":["wd:Q150"],"wdt:P577":["1991"],"wdt:P921":["wd:Q484188"],"wdt:P7937":["wd:Q8261"],"wdt:P50":["wd:Q237087"],"wdt:P674":["wd:Q1684606"],"wdt:P840":["wd:Q90"],"wdt:P1476":["L'Homme aux cercles bleus"],"wdt:P156":["wd:Q3203802"],"wdt:P166":["wd:Q2425126"],"wdt:P1545":["1"]},"originalLang":"fr","lastrevid":2193779480}},"redirects":{}} + """, + ) + val author = MockRestResponseCreators.withSuccess().body( + """ + {"entities":{"wd:Q237087":{"uri":"wd:Q237087","wdId":"Q237087","type":"human","labels":{"pt":"Fred Vargas","fr":"Fred Vargas","en":"Fred Vargas","es":"Fred Vargas","ca":"Fred Vargas","it":"Fred Vargas","et":"Fred Vargas","de":"Fred Vargas","nn":"Fred Vargas","ja":"フレッド・ヴァルガス","cs":"Fred Vargas","fa":"فرد وارگا","bg":"Фред Варгас","mzn":"فرد وارگا","nl":"Fred Vargas","da":"Fred Vargas","sv":"Fred Vargas","zh":"弗雷德·瓦格斯","ckb":"فرێد ڤارگاس","nb":"Fred Vargas","sl":"Fred Vargas","fi":"Fred Vargas","hu":"Fred Vargas","pl":"Fred Vargas","azb":"فرد وارقا","eu":"Fred Vargas","br":"Fred Vargas","gl":"Fred Vargas","uk":"Фред Варґас","ast":"Fred Vargas","ru":"Фред Варгас","ar":"فرد فارغاس","pt-br":"Fred Vargas","cy":"Fred Vargas","sq":"Fred Vargas","arz":"فرد فارجاس","pap":"Fred Vargas","ka":"ფრედ ვარგასი","tr":"Fred Vargas"},"aliases":{"fr":["Frédérique Audoin-Rouzeau"],"es":["Frederique Audoin-Rouzeau","Frédérique Audoin-Rouzeau","Fredérique Audoin-Rouzeau","Frédérique Audoin Rouzeau","Frederique Audoin Rouzeau","Fredérique Audoin Rouzeau"],"et":["Frédérique Audoin-Rouzeau"],"cs":["Frédérique Audouin-Rouzeau","Fredérique Vargas"],"sv":["Vargas"],"nb":["Frédérique Audouin-Rouzeau","Frederique Audouin-Rouzeau"],"en":["Frédérique Audoin-Rouzeau"],"it":["Frédérique Audoin-Rouzeau"],"uk":["Фредерік Одуан-Рузо"],"de":["Frédérique Audoin-Rouzeau"],"pl":["Frédérique Audoin-Rouzeau"],"zh":["弗蕾德·瓦尔加斯"],"ru":["Варгас, Фред"]},"descriptions":{"it":"scrittrice francese di romanzi gialli e archeologa","fr":"archéozoologue et romancière française","de":"französische Krimi-Autorin und Archäologin","nl":"Frans schrijfster","en":"French writer, archaeologist and historian","fa":"نویسنده و دیرین‌شناس فرانسوی","br":"skrivagnerez gall","he":"סופרת צרפתייה","gl":"escritora francesa","es":"escritora francesa","ca":"escriptora francesa","ar":"كاتبة فرنسية","fi":"ranskalainen kirjailija","bn":"ফরাসি লেখিকা","pl":"francuska pisarka i archeolog","ro":"scriitoare franceză","et":"Prantsusmaa kirjanik","en-gb":"French writer","sq":"shkrimtare franceze","en-ca":"French writer","pt":"Escritora francesa","ga":"scríbhneoir Francach","hu":"francia történész, régész és regényírónő","dv":"ލިޔުންތެރިއެއް","cs":"francouzská spisovatelka","ast":"escritora francesa","eu":"idazle frantziarra","nn":"fransk skribent","sv":"fransk författare","da":"fransk skribent","uk":"французька письменниця","zh":"法国历史学家、考古学家、作家","ja":"フランスの小説家 (1957 - )","tr":"Fransız yazar (d. 1957)"},"sitelinks":{"arwiki":{"title":"فرد فارغاس","badges":[]},"arzwiki":{"title":"فرد فارجاس","badges":[]},"astwiki":{"title":"Fred Vargas","badges":[]},"azbwiki":{"title":"فرد وارقا","badges":[]},"bgwiki":{"title":"Фред Варгас","badges":[]},"cawiki":{"title":"Fred Vargas","badges":[]},"ckbwiki":{"title":"فرێد ڤارگاس","badges":[]},"commonswiki":{"title":"Category:Fred Vargas","badges":[]},"cswiki":{"title":"Fred Vargas","badges":[]},"cswikiquote":{"title":"Fred Vargas","badges":[]},"dawiki":{"title":"Fred Vargas","badges":[]},"dewiki":{"title":"Fred Vargas","badges":[]},"enwiki":{"title":"Fred Vargas","badges":[]},"eswiki":{"title":"Fred Vargas","badges":[]},"eswikiquote":{"title":"Fred Vargas","badges":[]},"etwiki":{"title":"Fred Vargas","badges":[]},"etwikiquote":{"title":"Fred Vargas","badges":[]},"euwiki":{"title":"Fred Vargas","badges":[]},"fawiki":{"title":"فرد وارگا","badges":[]},"fiwiki":{"title":"Fred Vargas","badges":[]},"frwiki":{"title":"Fred Vargas","badges":[]},"frwikiquote":{"title":"Fred Vargas","badges":[]},"glwiki":{"title":"Fred Vargas","badges":[]},"huwiki":{"title":"Fred Vargas","badges":[]},"itwiki":{"title":"Fred Vargas","badges":[]},"itwikiquote":{"title":"Fred Vargas","badges":[]},"jawiki":{"title":"フレッド・ヴァルガス","badges":[]},"kawiki":{"title":"ფრედ ვარგასი","badges":[]},"mznwiki":{"title":"فرد وارگا","badges":[]},"nlwiki":{"title":"Fred Vargas","badges":[]},"nnwiki":{"title":"Fred Vargas","badges":[]},"nowiki":{"title":"Fred Vargas","badges":[]},"plwiki":{"title":"Fred Vargas","badges":[]},"ptwiki":{"title":"Fred Vargas","badges":[]},"ruwiki":{"title":"Варгас, Фред","badges":[]},"slwiki":{"title":"Fred Vargas","badges":[]},"svwiki":{"title":"Fred Vargas","badges":[]},"ukwiki":{"title":"Фред Варґас","badges":[]},"zhwiki":{"title":"弗雷德·瓦格斯","badges":[]}},"claims":{"wdt:P31":["wd:Q5"],"wdt:P135":["wd:Q3326717"],"wdt:P136":["wd:Q208505"],"wdt:P213":["000000036862981X","0000000120300340"],"wdt:P214":["86983699","166612748"],"wdt:P227":["121145344","1089332017"],"wdt:P244":["n95096250"],"wdt:P268":["12058359w"],"wdt:P269":["028828232"],"wdt:P349":["00884611"],"wdt:P569":["1957-06-07"],"wdt:P648":["OL6926243A"],"wdt:P950":["XX1217760"],"wdt:P1006":["071874097"],"wdt:P1412":["wd:Q150"],"wdt:P2607":["5624dae0-2980-496a-9d14-bc7320800e3e"],"wdt:P2963":["68906"],"wdt:P3630":["3212"],"wdt:P5491":["2874"],"wdt:P7400":["vargasfred"],"wdt:P27":["wd:Q142"],"wdt:P69":["wd:Q999763"],"wdt:P103":["wd:Q150"],"wdt:P106":["wd:Q6625963","wd:Q3621491","wd:Q201788","wd:Q482980"],"wdt:P166":["wd:Q1320436","wd:Q2425126","wd:Q924392","wd:Q3332454"],"wdt:P22":["wd:Q3379257"],"wdt:P3373":["wd:Q2360290","wd:Q914690"],"wdt:P18":["Fred Vargas 2009.jpg"]},"originalLang":"fr","lastrevid":2234315588,"image":{"url":"https://commons.wikimedia.org/wiki/Special:FilePath/Fred%20Vargas%202009.jpg?width=1000","file":"Fred Vargas 2009.jpg","credits":{"text":"Wikimedia Commons","url":"https://commons.wikimedia.org/wiki/File:Fred Vargas 2009.jpg"}}}},"redirects":{}} + """, + ) + val genre1 = MockRestResponseCreators.withSuccess().body( + """ + {"entities":{"wd:Q182015":{"uri":"wd:Q182015","wdId":"Q182015","labels":{"zh-hans":"惊悚","zh-hant":"驚悚","zh-hk":"驚慄","zh-cn":"惊悚","zh-sg":"惊悚","zh-tw":"驚悚","ky":"триллер","eu":"thriller","pl":"dreszczowiec","be":"трылер","he":"מותחן","fr":"thriller","ko":"스릴러","es":"suspenso","af":"thriller","hu":"thriller","it":"thriller","gl":"suspense","et":"triller","id":"cerita seru","de":"Thriller","ja":"スリラー","nl":"thriller","ar":"إثارة","sv":"thriller","pt":"thriller","eo":"trilero","sk":"triler","ru":"триллер","hy":"թրիլեր","en":"thriller","tr":"gerilim","ro":"thriller","ca":"thriller","fi":"trilleri","uk":"трилер","la":"Thriller","be-tarask":"трылер","fo":"Nøtrisøga","cs":"thriller","fa":"دلهره‌آور","bg":"трилър","az":"triller","hr":"triler","lt":"trileris","da":"thriller","an":"thriller","zh":"驚悚","sr":"трилер","sr-ec":"трилер","sr-el":"triler","nb":"thriller","mk":"трилер","sco":"thriller","lv":"trilleris","tg":"триллер","fy":"Skriller","ka":"თრილერი","sh":"triler","el":"θρίλερ","ckb":"چەشنی ھەستبزوێن","bn":"রোমাঞ্চকর সৃষ্টিকর্ম","lb":"Thriller","ur":"سنسنی خیز","cy":"cyffro","tt":"триллер","tt-cyrl":"триллер","bho":"थ्रिलर","vi":"kịch tính","th":"แนวระทึกขวัญ","ms":"genre debaran","sq":"trillim","ga":"scéinséir","ast":"suspense","myv":"триллер","hyw":"Զգայացունց տրամա","wuu":"惊悚","uz":"triller","yue":"驚慄","xmf":"თრილერი","pt-br":"suspense","nn":"thriller","en-gb":"thriller","hi":"थ्रिलर (शैली)","sl":"triler","km":"តក់ស្លុត","ce":"триллер","sd":"سنسني خيز","mzn":"تریلر"},"aliases":{"zh":["驚悚作品"],"he":["סרט מתח","מותחן פסיכולוגי","מתח","מותחן פשע"],"fr":["Thriller (littérature)"],"ko":["드릴러","스릴러 영화"],"es":["thriller","suspense","intriga"],"id":["tegang","getaran"],"nl":["literaire thriller"],"ar":["إثاره"],"sv":["triller","thrillern","rysare"],"pt":["suspense"],"eo":["thriller","suspensfilmo","suspensa filmo"],"ru":["триллеры"],"ca":["suspens"],"uk":["Трілер","Триллер","трилери"],"be-tarask":["трымценьнік"],"cs":["Triller","Psychothriller"],"fa":["فیلم دلهره‌آور","دلهره‌اور","فیلم هیجانی","دلهره اور","دلهرهٔ آور","دلهره آور"],"hr":["Thriller"],"pl":["thriller"],"en":["suspense","suspense fiction","thriller fiction","suspense thriller"],"th":["แนวตื่นเต้น","แนวเขย่าขวัญ"],"tr":["endişe"],"ms":["Filem seram ngeri"],"mk":["трилер (жанр)"],"uz":["triller (janr)"],"bn":["থ্রিলার"],"pt-br":["thriller"],"en-gb":["suspense","thriller fiction","suspense fiction"],"nb":["thrillersjangeren"],"km":["ភាពយន្តតក់ស្លុត","ព្រឺព្រួច","អាថ៌កំបាំងបែបរន្ធត់","ស៊ើបអង្កេតបែបរន្ធត់","រន្ធត់","ភិតភ័យ"],"mzn":["دل‌توآر","دل تو آر","دلتویار","زهله یار","زهله آر"]},"descriptions":{"fr":"genre artistique","en":"genre of fiction","nl":"genre","de":"Literatur- und Filmgenre","it":"genere letterario, cinematografico e televisivo","ru":"жанр художественного произведения","pl":"gatunek filmu","pt":"gênero artístico","es":"género literario, cinematográfico y televisivo","sv":"genre i litteratur, film och TV-serier","ro":"gen de literatură, film, televiziune","cs":"žánr filmového, televizního nebo literárního díla","ca":"gènere literari, cinematogràfic i televisiu","bn":"সাহিত্য, চলচ্চিত্র ও টেলিভিশন অনুষ্ঠানের প্রকার","uk":"особливий жанр кіно та літератури, в яких специфічні засоби повинні викликати у глядачів або читачів почуття тривожного очікування, невизначеності, хвилювання чи страху","sk":"žáner literárneho, filmového alebo televízneho diela","nb":"sjangerbetegnelse innen litteratur, drama, film og TV","zh":"文學,電影和電視節目類型","lv":"literatūras, kino un televīzijas žanrs","sq":"zhanër i letërsisë, filmave dhe programeve televizive","fi":"kirjallisuuden tai elokuvataiteen tyylilaji","id":"genre sastra, film, dan program televisi","tr":"edebiyat, film ve televizyon programcılığında bir tür","el":"είδος έργου λογοτεχνικού, κινηματογράφικού και τηλεοπτικού","ckb":"ژانری ئەدەبی و هونەری","he":"סוגה המיושמת בספרות, בקולנוע ובטלוויזיה","mk":"жанр","uz":"janr","cy":"genre mewn ffuglen","sr":"филмски, телевизијски и књижевни жанр","pt-br":"gênero de ficção","en-gb":"genre of fiction","eo":"ĝenro de literaturo, komputilaj ludoj kaj filmoj, kiu uzas suspenson, tension kaj psikan eksciton kiel stilaj elementoj","mzn":"اتی ژانر","da":"genre indenfor film, litteratur m.v.","be-tarask":"мастацкі жанр"},"sitelinks":{"afwiki":{"title":"Riller","badges":[]},"arwiki":{"title":"إثارة (نوع فني)","badges":[]},"azwiki":{"title":"Triller (janr)","badges":[]},"be_x_oldwiki":{"title":"Трылер","badges":[]},"bewiki":{"title":"Трылер","badges":[]},"bgwiki":{"title":"Трилър","badges":[]},"bhwiki":{"title":"थ्रिलर","badges":[]},"bnwiki":{"title":"রোমাঞ্চকর সৃষ্টিকর্ম","badges":[]},"cawiki":{"title":"Thriller","badges":[]},"cewiki":{"title":"Триллер","badges":[]},"ckbwiki":{"title":"چەشنی ھەستبزوێن","badges":[]},"commonswiki":{"title":"Category:Thriller","badges":[]},"cswiki":{"title":"Thriller","badges":[]},"dawiki":{"title":"Thriller","badges":[]},"dewiki":{"title":"Thriller","badges":[]},"enwiki":{"title":"Thriller (genre)","badges":[]},"eowiki":{"title":"Trilero","badges":[]},"eswiki":{"title":"Suspenso","badges":[]},"etwiki":{"title":"Triller (žanr)","badges":[]},"euwiki":{"title":"Thriller","badges":[]},"fawiki":{"title":"دلهره‌آور (ژانر)","badges":[]},"fiwiki":{"title":"Trilleri","badges":[]},"fowiki":{"title":"Nøtrisøga","badges":[]},"frwiki":{"title":"Thriller (genre)","badges":[]},"fywiki":{"title":"Skriller","badges":[]},"glwiki":{"title":"Suspense","badges":[]},"hewiki":{"title":"מותחן","badges":[]},"hiwiki":{"title":"थ्रिलर (शैली)","badges":[]},"hrwiki":{"title":"Triler","badges":[]},"huwiki":{"title":"Thriller (műfaj)","badges":[]},"hywiki":{"title":"Թրիլլեր","badges":[]},"idwiki":{"title":"Cerita seru","badges":[]},"itwiki":{"title":"Thriller","badges":[]},"jawiki":{"title":"スリラー","badges":[]},"kawiki":{"title":"თრილერი","badges":[]},"kowiki":{"title":"스릴러","badges":[]},"kywiki":{"title":"Триллер","badges":[]},"lawiki":{"title":"Fabula formidulosa","badges":[]},"lbwiki":{"title":"Thriller","badges":[]},"ltwiki":{"title":"Trileris","badges":[]},"lvwiki":{"title":"Trilleris","badges":[]},"mkwiki":{"title":"Трилер (жанр)","badges":[]},"mswiki":{"title":"Genre debaran","badges":[]},"myvwiki":{"title":"Триллер","badges":[]},"nlwiki":{"title":"Thriller (genre)","badges":[]},"nowiki":{"title":"Thriller","badges":[]},"plwiki":{"title":"Dreszczowiec","badges":[]},"ptwiki":{"title":"Thriller (gênero)","badges":[]},"rowiki":{"title":"Thriller (gen)","badges":[]},"ruwiki":{"title":"Триллер","badges":[]},"ruwikinews":{"title":"Категория:Триллер","badges":[]},"sdwiki":{"title":"سنسني خيز","badges":[]},"shwiki":{"title":"Triler (žanr)","badges":[]},"simplewiki":{"title":"Thriller (genre)","badges":[]},"skwiki":{"title":"Triler","badges":[]},"srwiki":{"title":"Triler (žanr)","badges":[]},"svwiki":{"title":"Thriller","badges":[]},"tgwiki":{"title":"Триллер","badges":[]},"thwiki":{"title":"แนวระทึกขวัญ","badges":[]},"trwiki":{"title":"Gerilim (tür)","badges":[]},"ukwiki":{"title":"Трилер","badges":[]},"ukwikiquote":{"title":"Трилер","badges":[]},"urwiki":{"title":"سنسنی خیز (صنف)","badges":[]},"uzwiki":{"title":"Triller (janr)","badges":[]},"viwiki":{"title":"Giật gân (thể loại)","badges":[]},"wuuwiki":{"title":"惊悚","badges":[]},"xmfwiki":{"title":"თრილერი","badges":[]},"zh_yuewiki":{"title":"驚慄","badges":[]},"zhwiki":{"title":"驚悚","badges":[]}},"claims":{"wdt:P31":["wd:Q108465955"],"wdt:P227":["4185351-9"],"wdt:P18":["North by Northwest movie trailer screenshot (37).jpg"]},"lastrevid":2241285782,"image":{"url":"https://commons.wikimedia.org/wiki/Special:FilePath/North%20by%20Northwest%20movie%20trailer%20screenshot%20%2837%29.jpg?width=1000","file":"North by Northwest movie trailer screenshot (37).jpg","credits":{"text":"Wikimedia Commons","url":"https://commons.wikimedia.org/wiki/File:North by Northwest movie trailer screenshot (37).jpg"}}}},"redirects":{}} + """, + ) + val genre2 = MockRestResponseCreators.withSuccess().body( + """ + {"entities":{"wd:Q5937792":{"uri":"wd:Q5937792","wdId":"Q5937792","labels":{"fa":"داستان‌های جنایی","nl":"krimi","de":"Krimi","es":"género policíaco","en":"crime fiction","eo":"krimfikcio","hr":"Žanr kriminalistike","ca":"gènere policíac","ar":"أدب الجريمة","bs":"Kriminalistički žanr","ru":"криминальный жанр","fr":"genre policier","de-ch":"Krimi","en-ca":"Crime fiction","en-gb":"Crime fiction","sh":"Kriminalistički žanr","ta":"குற்றப்புனைவு","tr":"Polisiye","zh":"犯罪作品","af":"misdaadfiksie","it":"giallo","sv":"deckare","ro":"ficțiune polițistă","hu":"krimi","mk":"криминалистички жанр","fi":"rikoskirjallisuus","el":"αστυνομική μυθοπλασία","ko":"범죄물","cs":"kriminální fikce","ja":"犯罪フィクション","sr":"kriminalistička fantastika","da":"krimi","bn":"অপরাধ কল্পকাহিনী","hy":"քրեական","nb":"krim","he":"ספרות פשע","ms":"Jenayah","id":"fiksi kriminal","vi":"truyện trinh thám","be-tarask":"крымінальны жанр","fy":"Misdiefiksje","yue":"犯罪故仔","sd":"ڪرائيم فڪشن","az":"cinayət janrı","th":"นิยายสืบสวนสอบสวน","uk":"кримінальний жанр","nn":"krim","lv":"Kriminālliteratūra","bg":"Криминална литература","is":"Glæpasaga","zh-hans":"犯罪作品","zh-hant":"虛構犯罪","cy":"ffuglen drosedd","sms":"kriminaalǩeerjlažvuõtt","ast":"xéneru policiacu","pl":"fikcja kryminalna","pt":"Ficção policial","ga":"ficsean coiriúlachta","ur":"جرم فکشن","ckb":"وێژەی تاوانباری","se":"rihkusgirjjálašvuohta","smn":"rikoskirjálâšvuotâ","sl":"kriminalna fikcija","os":"криминалон жанр","lb":"Krimi","lmo":"sgiald"},"aliases":{"fa":["ادبیات داستانی جنایی"],"es":["género policial","policial","policiaco","policíaco","ficción criminal","ficción policíaca","genero policiaco","genero policial","ficcion criminal","ficcion policiaca"],"hu":["bűnügyi történet"],"ko":["크라임 픽션"],"cs":["detektivní fikce"],"ja":["クライムフィクション","クライムスリラー","犯罪小説","殺人ミステリー","ディテクティブ・ストーリー","探偵物語","ディテクティブストーリー"],"sr":["kriminalistička drama"],"zh-hans":["犯罪小说"],"ru":["криминальный вымысел","полицейское произведение","криминал"],"zh":["終極警網","兇殺懸疑"],"sv":["kriminalfiktion"],"cy":["stori dditectif","dirgelwch llofruddiaeth"],"nn":["kriminallitteratur","krimlitteratur"],"fi":["rikosfiktio"],"ast":["policiacu"],"ca":["gènere de ficció criminal"],"ur":["کرائم فکشن"],"el":["αστυνομικό δράμα"],"sl":["kriminalno leposlovje","kriminalka"]},"descriptions":{"en":"genre of fiction focusing on crime, encompassing literature, film and theatre","es":"género de obras creativas con un enfoque en crímenes","ru":"художественный жанр","it":"genere letterario, televisivo e cinematografico","ko":"범죄에 초점이 맞춰져 있는 장르의 한 종류","cs":"žánr fikce zaměřený na zločin","nb":"sjanger innen fiksjon med fokus på kriminalitet","de":"Genre","eo":"ĝenro de fikcio kies ĉefa temo estas krimo kaj investigado, inkluzivigas literaturon, filmon kaj teatron","fr":"catégorie de fictions sur le thème de la criminalité","ca":"gènere de ficció centrat en el crim","sr":"žanr fikcije koji se fokusira na zločin","be-tarask":"мастацкі жанр","sv":"genre inom fiktion vars huvudtema är kriminalitet och brottsutredning, omfattar literatur, film och teater","da":"fiktionsgenre","tr":"Suç ve suçlularla ilgili kurgu","uk":"художній жанр","zh-hans":"文类","zh":"文类","fi":"kirjallisuuden lajityyppi","id":"genre fiksi yang berfokus pada tindakan kriminal","nl":"Duitstalige benaming van het misdaadgenre","cy":"ffuglen sy'n canolbwyntio ar drosedd","ja":"探偵フィクションを含むフィクションのジャンル","vi":"thể loại giả tưởng tập trung vào tội phạm, bao gồm văn học, phim ảnh và sân khấu","lmo":"sgender literari"},"sitelinks":{"arwiki":{"title":"أدب الجريمة","badges":[]},"bnwiki":{"title":"অপরাধ কল্পকাহিনী","badges":[]},"bswiki":{"title":"Kriminalistički žanr","badges":[]},"cawiki":{"title":"Gènere policíac","badges":[]},"ckbwiki":{"title":"وێژەی تاوانباری","badges":[]},"dewiki":{"title":"Krimi","badges":[]},"elwiki":{"title":"Αστυνομικό μυθιστόρημα","badges":[]},"enwiki":{"title":"Crime fiction","badges":[]},"eowiki":{"title":"Krimfikcio","badges":[]},"eswiki":{"title":"Ficción criminal","badges":[]},"fawiki":{"title":"داستان جنایی","badges":[]},"fiwiki":{"title":"Rikoskirjallisuus","badges":[]},"frwiki":{"title":"Genre policier","badges":[]},"fywiki":{"title":"Misdiefiksje","badges":[]},"hrwiki":{"title":"Žanr kriminalistike","badges":[]},"hywiki":{"title":"Քրեական գեղարվեստական գրականություն","badges":[]},"idwiki":{"title":"Fiksi kejahatan","badges":[]},"iswiki":{"title":"Glæpasaga","badges":[]},"itwiki":{"title":"Giallo (genere)","badges":[]},"itwikiquote":{"title":"Letteratura gialla","badges":[]},"jawiki":{"title":"犯罪小説","badges":[]},"kowiki":{"title":"범죄물","badges":[]},"lmowiki":{"title":"Romanz sgiald","badges":[]},"mswiki":{"title":"Cereka jenayah","badges":[]},"nlwiki":{"title":"Krimi","badges":[]},"ptwiki":{"title":"Ficção policial","badges":[]},"sdwiki":{"title":"ڪرائيم فڪشن","badges":[]},"shwiki":{"title":"Kriminalistički žanr","badges":[]},"simplewiki":{"title":"Crime fiction","badges":[]},"srwiki":{"title":"Kriminalistička fantastika","badges":[]},"svwiki":{"title":"Deckare","badges":["Q17559452"]},"tawiki":{"title":"குற்றப்புனைவு","badges":[]},"trwiki":{"title":"Polisiye","badges":[]},"zh_yuewiki":{"title":"犯罪故仔","badges":[]},"zhwiki":{"title":"犯罪小說","badges":[]}},"claims":{"wdt:P31":["wd:Q108465955"],"wdt:P227":["4165727-5"],"wdt:P244":["sh85033995"],"wdt:P268":["12453012w"],"wdt:P921":["wd:Q83267"],"wdt:P1889":["wd:Q20664530","wd:Q208505","wd:Q1438652"]},"lastrevid":2241164757,"image":{}}},"redirects":{}} + """, + ) + val series = MockRestResponseCreators.withSuccess().body( + """ + {"entities":{"wd:Q27536277":{"uri":"wd:Q27536277","wdId":"Q27536277","type":"serie","labels":{"fr":"Commissaire Jean-Baptiste Adamsberg","en":"Commissioner Jean-Baptiste Adamsberg","de":"Kommissar Jean-Baptiste Adamsberg","it":"Commissario Jean-Baptiste Adamsberg","ga":"Commissaire Jean-Baptiste Adamsberg"},"aliases":{},"descriptions":{"fr":"série de livre de Fred Vargas","en":"book serie by Fred Vargas","de":"Buchserie von Fred Vargas mit diesem Protagonisten","it":"serie di libri di Fred Vargas"},"sitelinks":{},"claims":{"wdt:P31":["wd:Q277759"],"wdt:P50":["wd:Q237087"],"wdt:P136":["wd:Q208505"],"wdt:P407":["wd:Q150"],"wdt:P1476":["Commissaire Jean-Baptiste Adamsberg"],"wdt:P674":["wd:Q1684606"]},"originalLang":"fr","lastrevid":1716997222,"image":{}}},"redirects":{}} + """, + ) + + serv.expect(MockRestRequestMatchers.requestTo("https://inventaire.io/api/entities?action=by-uris&uris=isbn:9782290349229")).andRespond(isbn) + serv.expect(MockRestRequestMatchers.requestTo("https://inventaire.io/api/entities?action=by-uris&uris=wd:Q3203603")).andRespond(edition) + serv.expect(MockRestRequestMatchers.requestTo("https://inventaire.io/api/entities?action=by-uris&uris=wd:Q237087")).andRespond(author) + serv.expect(MockRestRequestMatchers.requestTo("https://inventaire.io/api/entities?action=by-uris&uris=wd:Q182015")).andRespond(genre1) + serv.expect(MockRestRequestMatchers.requestTo("https://inventaire.io/api/entities?action=by-uris&uris=wd:Q5937792")).andRespond(genre2) + serv.expect(MockRestRequestMatchers.requestTo("https://inventaire.io/api/entities?action=by-uris&uris=wd:Q27536277")).andRespond(series) + val jeluProperties = JeluProperties( + JeluProperties.Database(""), + JeluProperties.Files("", "", true), + JeluProperties.Session(1), + JeluProperties.Cors(), + JeluProperties.Metadata(JeluProperties.Calibre("")), + JeluProperties.Auth(JeluProperties.Ldap(), JeluProperties.Proxy()), + listOf( + JeluProperties.MetaDataProvider( + "inventaireio", + true, + apiKey = null, + ), + ), + ) + val service = InventaireIoMetadataProvider(builder.build(), jeluProperties, ObjectMapper()) + + // When + val result: MetadataDto = service.fetchMetadata( + MetadataRequestDto("9782290349229"), + mapOf(), + ).get() + + // Then + Assertions.assertNotNull(result) + Assertions.assertEquals("L'homme aux cercles bleus", result.title) + Assertions.assertEquals("2-290-34922-4", result.isbn10) + Assertions.assertEquals("978-2-290-34922-9", result.isbn13) + Assertions.assertEquals(mutableSetOf("Fred Vargas"), result.authors) + Assertions.assertEquals( + "https://inventaire.io/img/entities/57883743aa7c6ad25885a63e6e94349ec4f71562", + result.image, + ) + Assertions.assertEquals("fr", result.language) + Assertions.assertEquals("2005-05-01", result.publishedDate) + Assertions.assertEquals("1991 mystery novel by Fred Vargas, 1st of her Detective Adamsberg series", result.summary) + } + + @Test + fun testParseClaims() { + val jeluProperties = JeluProperties( + JeluProperties.Database(""), + JeluProperties.Files("", "", true), + JeluProperties.Session(1), + JeluProperties.Cors(), + JeluProperties.Metadata(JeluProperties.Calibre("")), + JeluProperties.Auth(JeluProperties.Ldap(), JeluProperties.Proxy()), + listOf( + JeluProperties.MetaDataProvider( + "inventaireio", + true, + apiKey = null, + ), + ), + ) + val objectMapper = ObjectMapper() + val service = InventaireIoMetadataProvider(springRestClient, jeluProperties, objectMapper) + val res = """ + { + "wdt:P31" : [ "wd:Q3331189" ], + "wdt:P212" : [ "978-2-290-34922-9" ], + "wdt:P957" : [ "2-290-34922-4" ], + "wdt:P407" : [ "wd:Q150" ], + "wdt:P1476" : [ "L'homme aux cercles bleus" ], + "wdt:P629" : [ "wd:Q3203603" ], + "wdt:P123" : [ "wd:Q3156592" ], + "invp:P2" : [ "57883743aa7c6ad25885a63e6e94349ec4f71562" ], + "wdt:P577" : [ "2005-05-01" ], + "wdt:P1104" : [ 220 ], + "wdt:P2969" : [ "1508217" ] + } + """ + val node = objectMapper.readTree(res) + val dto = ParsingDto(MetadataDto(), "") + service.parseClaims(node, dto) + Assertions.assertEquals("978-2-290-34922-9", dto.metadataDto.isbn13) + Assertions.assertEquals("2-290-34922-4", dto.metadataDto.isbn10) + Assertions.assertEquals("2005-05-01", dto.metadataDto.publishedDate) + Assertions.assertEquals(220, dto.metadataDto.pageCount) + Assertions.assertEquals("1508217", dto.metadataDto.goodreadsId) + Assertions.assertEquals("L'homme aux cercles bleus", dto.metadataDto.title) + Assertions.assertEquals("wd:Q3203603", dto.editionClaim) + } +}