From dfcd9db368778cf82bcb868680fd1252dad300eb Mon Sep 17 00:00:00 2001 From: Marcin Gajewski Date: Mon, 29 Dec 2025 03:50:53 +0100 Subject: [PATCH] Feat/lubimyczytac metadata provider (#2019) * feat: add LubimyCzytac metadata provider - Add LubimyCzytac parser with web scraping for lubimyczytac.pl - Extract book metadata including title, authors, description, ratings - Parse JSON-LD structured data for reliable metadata extraction - Add database migration with columns and JSON property name updates - Add comprehensive test coverage for parser * feat: add LubimyCzytac UI integration - Add LC ID and LC Rating fields to metadata picker - Update settings page with Lubimyczytac provider toggle - Add LubimyCzytac to metadata searcher provider list - Display Lubimyczytac in all metadata viewer sections - Add Lubimyczytac to advanced fetch options - Update TypeScript models to match backend serialization --- .../booklore/config/BookParserConfig.java | 5 +- .../booklore/model/MetadataClearFlags.java | 2 + .../booklore/model/dto/BookMetadata.java | 4 + .../settings/MetadataProviderSettings.java | 8 + .../model/entity/BookMetadataEntity.java | 17 + .../model/enums/MetadataProvider.java | 2 +- .../service/metadata/BookMetadataUpdater.java | 2 + .../metadata/parser/LubimyCzytacParser.java | 441 ++++++++++++++++++ .../booklore/util/MetadataChangeDetector.java | 4 + .../V80__Add_lubimyczytac_provider.sql | 6 + .../parser/LubimyCzytacParserTest.java | 156 +++++++ .../src/app/features/book/model/book.model.ts | 6 + .../metadata-editor.component.html | 24 + .../metadata-editor.component.ts | 8 + .../metadata-picker.component.ts | 18 + .../metadata-searcher.component.ts | 2 + .../metadata-viewer.component.html | 16 +- .../metadata-viewer.component.ts | 6 +- ...tadata-advanced-fetch-options.component.ts | 21 +- .../request/metadata-refresh-options.model.ts | 2 + .../metadata-provider-settings.component.html | 9 + .../metadata-provider-settings.component.ts | 5 +- .../library-metadata-settings.component.ts | 2 + .../app/shared/model/app-settings.model.ts | 6 + 24 files changed, 758 insertions(+), 14 deletions(-) create mode 100644 booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/parser/LubimyCzytacParser.java create mode 100644 booklore-api/src/main/resources/db/migration/V80__Add_lubimyczytac_provider.sql create mode 100644 booklore-api/src/test/java/com/adityachandel/booklore/service/metadata/parser/LubimyCzytacParserTest.java diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/config/BookParserConfig.java b/booklore-api/src/main/java/com/adityachandel/booklore/config/BookParserConfig.java index ee58fe829..5a735b1dc 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/config/BookParserConfig.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/config/BookParserConfig.java @@ -12,14 +12,15 @@ public class BookParserConfig { @Bean public Map parserMap(GoogleParser googleParser, AmazonBookParser amazonBookParser, - GoodReadsParser goodReadsParser, HardcoverParser hardcoverParser, ComicvineBookParser comicvineBookParser, DoubanBookParser doubanBookParser) { + GoodReadsParser goodReadsParser, HardcoverParser hardcoverParser, ComicvineBookParser comicvineBookParser, DoubanBookParser doubanBookParser, LubimyCzytacParser lubimyczytacParser) { return Map.of( MetadataProvider.Amazon, amazonBookParser, MetadataProvider.GoodReads, goodReadsParser, MetadataProvider.Google, googleParser, MetadataProvider.Hardcover, hardcoverParser, MetadataProvider.Comicvine, comicvineBookParser, - MetadataProvider.Douban, doubanBookParser + MetadataProvider.Douban, doubanBookParser, + MetadataProvider.Lubimyczytac, lubimyczytacParser ); } } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/MetadataClearFlags.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/MetadataClearFlags.java index 4a1548fe3..5e957fa46 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/MetadataClearFlags.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/MetadataClearFlags.java @@ -29,6 +29,8 @@ public class MetadataClearFlags { private boolean goodreadsReviewCount; private boolean hardcoverRating; private boolean hardcoverReviewCount; + private boolean lubimyczytacId; + private boolean lubimyczytacRating; private boolean authors; private boolean categories; private boolean moods; diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/BookMetadata.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/BookMetadata.java index 312648eb6..d6dfc0778 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/BookMetadata.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/BookMetadata.java @@ -43,7 +43,9 @@ public class BookMetadata { private String doubanId; private Double doubanRating; private Integer doubanReviewCount; + private Double lubimyczytacRating; private String googleId; + private String lubimyczytacId; private Instant coverUpdatedOn; private Set authors; private Set categories; @@ -80,6 +82,8 @@ public class BookMetadata { private Boolean hardcoverReviewCountLocked; private Boolean doubanRatingLocked; private Boolean doubanReviewCountLocked; + private Boolean lubimyczytacIdLocked; + private Boolean lubimyczytacRatingLocked; private Boolean coverLocked; private Boolean authorsLocked; private Boolean categoriesLocked; diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/settings/MetadataProviderSettings.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/settings/MetadataProviderSettings.java index 17d53f27f..d44a30477 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/settings/MetadataProviderSettings.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/settings/MetadataProviderSettings.java @@ -1,5 +1,6 @@ package com.adityachandel.booklore.model.dto.settings; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; @Data @@ -10,6 +11,8 @@ public class MetadataProviderSettings { private Hardcover hardcover; private Comicvine comicvine; private Douban douban; + @JsonProperty("lubimyczytac") + private Lubimyczytac lubimyczytac; @Data public static class Amazon { @@ -45,4 +48,9 @@ public class MetadataProviderSettings { public static class Douban { private boolean enabled; } + + @Data + public static class Lubimyczytac { + private boolean enabled; + } } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/BookMetadataEntity.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/BookMetadataEntity.java index 19b3ab99d..428cbb001 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/BookMetadataEntity.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/BookMetadataEntity.java @@ -106,6 +106,11 @@ public class BookMetadataEntity { @Column(name = "comicvine_id", length = 100) private String comicvineId; + @Column(name = "lubimyczytac_id", length = 100) + private String lubimyczytacId; + + @Column(name = "lubimyczytac_rating") + private Double lubimyczytacRating; @Column(name = "title_locked") @Builder.Default @@ -223,6 +228,14 @@ public class BookMetadataEntity { @Builder.Default private Boolean comicvineIdLocked = Boolean.FALSE; + @Column(name = "lubimyczytac_id_locked") + @Builder.Default + private Boolean lubimyczytacIdLocked = Boolean.FALSE; + + @Column(name = "lubimyczytac_rating_locked") + @Builder.Default + private Boolean lubimyczytacRatingLocked = Boolean.FALSE; + @Column(name = "reviews_locked") @Builder.Default private Boolean reviewsLocked = Boolean.FALSE; @@ -313,11 +326,13 @@ public class BookMetadataEntity { this.goodreadsReviewCountLocked = lock; this.hardcoverRatingLocked = lock; this.hardcoverReviewCountLocked = lock; + this.lubimyczytacRatingLocked = lock; this.comicvineIdLocked = lock; this.goodreadsIdLocked = lock; this.hardcoverIdLocked = lock; this.hardcoverBookIdLocked = lock; this.googleIdLocked = lock; + this.lubimyczytacIdLocked = lock; this.reviewsLocked = lock; } @@ -346,11 +361,13 @@ public class BookMetadataEntity { && Boolean.TRUE.equals(this.goodreadsReviewCountLocked) && Boolean.TRUE.equals(this.hardcoverRatingLocked) && Boolean.TRUE.equals(this.hardcoverReviewCountLocked) + && Boolean.TRUE.equals(this.lubimyczytacRatingLocked) && Boolean.TRUE.equals(this.goodreadsIdLocked) && Boolean.TRUE.equals(this.comicvineIdLocked) && Boolean.TRUE.equals(this.hardcoverIdLocked) && Boolean.TRUE.equals(this.hardcoverBookIdLocked) && Boolean.TRUE.equals(this.googleIdLocked) + && Boolean.TRUE.equals(this.lubimyczytacIdLocked) && Boolean.TRUE.equals(this.reviewsLocked) ; } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/enums/MetadataProvider.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/enums/MetadataProvider.java index e1a83e165..3c691a922 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/enums/MetadataProvider.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/enums/MetadataProvider.java @@ -1,5 +1,5 @@ package com.adityachandel.booklore.model.enums; public enum MetadataProvider { - Amazon, GoodReads, Google, Hardcover, Comicvine, Douban + Amazon, GoodReads, Google, Hardcover, Comicvine, Douban, Lubimyczytac } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/BookMetadataUpdater.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/BookMetadataUpdater.java index 90a009bff..507ed6e12 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/BookMetadataUpdater.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/BookMetadataUpdater.java @@ -170,6 +170,8 @@ public class BookMetadataUpdater { handleFieldUpdate(e.getGoodreadsReviewCountLocked(), clear.isGoodreadsReviewCount(), m.getGoodreadsReviewCount(), e::setGoodreadsReviewCount, e::getGoodreadsReviewCount, replaceMode); handleFieldUpdate(e.getHardcoverRatingLocked(), clear.isHardcoverRating(), m.getHardcoverRating(), e::setHardcoverRating, e::getHardcoverRating, replaceMode); handleFieldUpdate(e.getHardcoverReviewCountLocked(), clear.isHardcoverReviewCount(), m.getHardcoverReviewCount(), e::setHardcoverReviewCount, e::getHardcoverReviewCount, replaceMode); + handleFieldUpdate(e.getLubimyczytacIdLocked(), clear.isLubimyczytacId(), m.getLubimyczytacId(), v -> e.setLubimyczytacId(nullIfBlank(v)), e::getLubimyczytacId, replaceMode); + handleFieldUpdate(e.getLubimyczytacRatingLocked(), clear.isLubimyczytacRating(), m.getLubimyczytacRating(), e::setLubimyczytacRating, e::getLubimyczytacRating, replaceMode); } private void handleFieldUpdate(Boolean locked, boolean shouldClear, T newValue, Consumer setter, Supplier getter, MetadataReplaceMode mode) { diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/parser/LubimyCzytacParser.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/parser/LubimyCzytacParser.java new file mode 100644 index 000000000..876c93085 --- /dev/null +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/parser/LubimyCzytacParser.java @@ -0,0 +1,441 @@ +package com.adityachandel.booklore.service.metadata.parser; + +import com.adityachandel.booklore.model.dto.Book; +import com.adityachandel.booklore.model.dto.BookMetadata; +import com.adityachandel.booklore.model.dto.request.FetchMetadataRequest; +import com.adityachandel.booklore.model.enums.MetadataProvider; +import com.adityachandel.booklore.service.appsettings.AppSettingService; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Service +@Slf4j +@RequiredArgsConstructor +public class LubimyCzytacParser implements BookParser { + + private static final String BASE_URL = "https://lubimyczytac.pl"; + private static final String SEARCH_URL = BASE_URL + "/szukaj/ksiazki"; + private static final String USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"; + private static final int CONNECTION_TIMEOUT_MS = 10000; + private static final int MAX_RESULTS = 10; + private static final double RATING_SCALE_DIVISOR = 2.0; // Convert 10-point scale to 5-point scale + private static final Pattern SERIES_NUMBER_PATTERN = Pattern.compile("\\(tom\\s+(\\d+)\\)"); + private static final Pattern BOOK_ID_PATTERN = Pattern.compile("/ksiazka/(\\d+)"); + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + private final AppSettingService appSettingService; + + @Override + public List fetchMetadata(Book book, FetchMetadataRequest fetchMetadataRequest) { + log.info("Fetching LubimyCzytac metadata for book: {}", book.getTitle()); + + // Check if provider is enabled + var settings = appSettingService.getAppSettings().getMetadataProviderSettings(); + if (settings == null || settings.getLubimyczytac() == null || !settings.getLubimyczytac().isEnabled()) { + log.info("LubimyCzytac provider is disabled"); + return new ArrayList<>(); + } + + try { + // Build search query + String query = buildSearchQuery(fetchMetadataRequest); + if (query.isEmpty()) { + log.warn("Empty search query for book: {}", book.getTitle()); + return new ArrayList<>(); + } + + String author = fetchMetadataRequest.getAuthor(); + String searchIsbn = fetchMetadataRequest.getIsbn(); + boolean searchingByIsbn = searchIsbn != null && !searchIsbn.isEmpty(); + + // Search for books + List bookUrls = searchBooks(query, author); + if (bookUrls.isEmpty()) { + log.info("No results found for query: {}", query); + return new ArrayList<>(); + } + + // Parse details for each book + List results = new ArrayList<>(); + for (String url : bookUrls) { + BookMetadata metadata = parseBookDetails(url); + if (metadata != null) { + // If searching by ISBN, validate that the book's ISBN matches + if (searchingByIsbn) { + if (isbnMatches(metadata, searchIsbn)) { + results.add(metadata); + } else { + log.debug("Skipping book {} - ISBN doesn't match search ISBN {}", + metadata.getTitle(), searchIsbn); + } + } else { + results.add(metadata); + } + } + + // Limit results to avoid excessive scraping + if (results.size() >= MAX_RESULTS) { + break; + } + } + + log.info("Found {} LubimyCzytac results for query: {}", results.size(), query); + return results; + + } catch (Exception e) { + log.error("Error fetching LubimyCzytac metadata", e); + return new ArrayList<>(); + } + } + + @Override + public BookMetadata fetchTopMetadata(Book book, FetchMetadataRequest fetchMetadataRequest) { + List results = fetchMetadata(book, fetchMetadataRequest); + return results.isEmpty() ? null : results.get(0); + } + + private String buildSearchQuery(FetchMetadataRequest request) { + String isbn = request.getIsbn(); + if (isbn != null && !isbn.isEmpty()) { + return isbn; + } + + String title = request.getTitle(); + if (title != null && !title.isEmpty()) { + return title.trim(); + } + + return ""; + } + + private String buildSearchUrl(String query, String author) { + String encodedQuery = URLEncoder.encode(query, StandardCharsets.UTF_8); + String url = SEARCH_URL + "?phrase=" + encodedQuery; + + if (author != null && !author.isEmpty()) { + String encodedAuthor = URLEncoder.encode(author, StandardCharsets.UTF_8); + url += "&author=" + encodedAuthor; + } + + return url; + } + + private List searchBooks(String query, String author) { + List bookUrls = new ArrayList<>(); + + try { + String searchUrl = buildSearchUrl(query, author); + log.info("Searching LubimyCzytac: {}", searchUrl); + + Document doc = Jsoup.connect(searchUrl) + .userAgent(USER_AGENT) + .timeout(CONNECTION_TIMEOUT_MS) + .get(); + + Elements results = doc.select(".authorAllBooks__single"); + log.info("Found {} search results", results.size()); + + for (Element result : results) { + Element titleLink = result.selectFirst(".authorAllBooks__singleTextTitle"); + if (titleLink != null) { + String href = titleLink.attr("href"); + if (href != null && !href.isEmpty()) { + String fullUrl = href.startsWith("http") ? href : BASE_URL + href; + bookUrls.add(fullUrl); + } + } + } + + } catch (IOException e) { + log.error("Error searching LubimyCzytac", e); + } + + return bookUrls; + } + + private BookMetadata parseBookDetails(String url) { + try { + log.info("Parsing book details from: {}", url); + + Document doc = Jsoup.connect(url) + .userAgent(USER_AGENT) + .timeout(CONNECTION_TIMEOUT_MS) + .get(); + + BookMetadata metadata = new BookMetadata(); + metadata.setProvider(MetadataProvider.Lubimyczytac); + + // Extract LubimyCzytac ID from URL (e.g., /ksiazka/123456/title -> 123456) + String id = extractIdFromUrl(url); + metadata.setLubimyczytacId(id); + + Element titleElement = doc.selectFirst("h1.book__title"); + if (titleElement != null) { + metadata.setTitle(titleElement.text().trim()); + } + + Element coverElement = doc.selectFirst(".book-cover img"); + if (coverElement != null) { + String coverUrl = coverElement.attr("src"); + if (coverUrl != null && !coverUrl.isEmpty()) { + metadata.setThumbnailUrl(coverUrl.startsWith("http") ? coverUrl : BASE_URL + coverUrl); + } + } + + Element publisherElement = doc.selectFirst("a[href*=/wydawnictwo/]"); + if (publisherElement != null) { + metadata.setPublisher(publisherElement.text().trim()); + } + + Elements languageElements = doc.select("dt:contains(Język:) + dd"); + if (!languageElements.isEmpty()) { + String language = languageElements.first().text().trim().toLowerCase(); + metadata.setLanguage(mapLanguage(language)); + } + + Element descElement = doc.selectFirst(".collapse-content"); + if (descElement != null) { + String description = descElement.text(); + if (description != null && !description.isBlank()) { + metadata.setDescription(description); + } + } + + Element isbnMeta = doc.selectFirst("meta[property=books:isbn]"); + if (isbnMeta != null) { + String isbn = isbnMeta.attr("content").trim(); + if (isbn.length() == 13) { + metadata.setIsbn13(isbn); + } else if (isbn.length() == 10) { + metadata.setIsbn10(isbn); + } + } + + // Convert from 10-point to 5-point scale + Element ratingMeta = doc.selectFirst("meta[property=books:rating:value]"); + if (ratingMeta != null) { + try { + String ratingStr = ratingMeta.attr("content").trim(); + if (!ratingStr.isEmpty()) { + double rating = Double.parseDouble(ratingStr); + metadata.setLubimyczytacRating(rating / RATING_SCALE_DIVISOR); + } + } catch (NumberFormatException e) { + log.warn("Failed to parse rating for book: {}", url, e); + } + } + + Set tags = new HashSet<>(); + Elements tagElements = doc.select("a[href*=/ksiazki/t/]"); + for (Element tagElement : tagElements) { + String tag = tagElement.text().trim(); + if (!tag.isEmpty()) { + tags.add(tag); + } + } + if (!tags.isEmpty()) { + metadata.setTags(tags); + } + + // Series format: "Cykl: Series Name (tom 3)" or "Cykl: Series Name" + Elements seriesElements = doc.select("span.d-none.d-sm-block.mt-1:contains(Cykl:)"); + if (!seriesElements.isEmpty()) { + String seriesText = seriesElements.first().text().trim(); + parseSeriesInfo(seriesText, metadata); + } + + // Extract authors, categories, pages, and publish date from JSON-LD structured data + Elements jsonLdElements = doc.select("script[type=application/ld+json]"); + for (Element jsonLdElement : jsonLdElements) { + try { + String jsonLd = jsonLdElement.html(); + parseJsonLd(jsonLd, metadata); + } catch (Exception e) { + log.warn("Failed to parse JSON-LD", e); + } + } + + return metadata; + + } catch (IOException e) { + log.error("Error parsing book details from: {}", url, e); + return null; + } + } + + private String extractIdFromUrl(String url) { + // Extract ID from URL like https://lubimyczytac.pl/ksiazka/123456/title + try { + Matcher matcher = BOOK_ID_PATTERN.matcher(url); + if (matcher.find()) { + return matcher.group(1); + } + } catch (Exception e) { + log.warn("Could not extract ID from URL: {}", url); + } + return null; + } + + private boolean isbnMatches(BookMetadata metadata, String searchIsbn) { + // Normalize ISBN by removing hyphens and spaces for comparison + String normalizedSearch = searchIsbn.replaceAll("[\\s-]", ""); + + // Check ISBN-13 + if (metadata.getIsbn13() != null) { + String normalized13 = metadata.getIsbn13().replaceAll("[\\s-]", ""); + if (normalized13.equals(normalizedSearch)) { + return true; + } + } + + // Check ISBN-10 + if (metadata.getIsbn10() != null) { + String normalized10 = metadata.getIsbn10().replaceAll("[\\s-]", ""); + if (normalized10.equals(normalizedSearch)) { + return true; + } + } + + return false; + } + + private String mapLanguage(String polishLanguage) { + switch (polishLanguage) { + case "polski": + return "pl"; + case "angielski": + return "en"; + case "niemiecki": + return "de"; + case "francuski": + return "fr"; + case "hiszpański": + return "es"; + case "włoski": + return "it"; + default: + return polishLanguage; + } + } + + private void parseSeriesInfo(String seriesText, BookMetadata metadata) { + // Format: "Cykl: Series Name (tom 3)" or "Cykl: Series Name" + if (seriesText.startsWith("Cykl:")) { + seriesText = seriesText.substring(5).trim(); + } + + // Extract series number if present + var matcher = SERIES_NUMBER_PATTERN.matcher(seriesText); + + if (matcher.find()) { + try { + float seriesNumber = Float.parseFloat(matcher.group(1)); + metadata.setSeriesNumber(seriesNumber); + + // Remove the "(tom X)" part to get series name + String seriesName = SERIES_NUMBER_PATTERN.matcher(seriesText).replaceAll("").trim(); + metadata.setSeriesName(seriesName); + } catch (NumberFormatException e) { + log.warn("Failed to parse series number from: {}", seriesText); + metadata.setSeriesName(seriesText); + } + } else { + metadata.setSeriesName(seriesText); + } + } + + private void parseJsonLd(String jsonLd, BookMetadata metadata) { + try { + JsonNode root = OBJECT_MAPPER.readTree(jsonLd); + + // Pages + if (root.has("numberOfPages")) { + try { + int pages = root.get("numberOfPages").asInt(); + metadata.setPageCount(pages); + } catch (Exception e) { + log.warn("Failed to parse numberOfPages from JSON-LD"); + } + } + + // Publish date + if (root.has("datePublished")) { + try { + String dateStr = root.get("datePublished").asText(); + LocalDate date = LocalDate.parse(dateStr, DateTimeFormatter.ISO_DATE); + metadata.setPublishedDate(date); + } catch (Exception e) { + log.warn("Failed to parse datePublished from JSON-LD: {}", e.getMessage()); + } + } + + // Author(s) - can be single Person or array of Person objects + if (root.has("author")) { + try { + Set authors = new HashSet<>(); + JsonNode authorNode = root.get("author"); + + if (authorNode.isArray()) { + // Multiple authors + for (JsonNode author : authorNode) { + if (author.has("name")) { + authors.add(author.get("name").asText()); + } + } + } else if (authorNode.isObject() && authorNode.has("name")) { + // Single author + authors.add(authorNode.get("name").asText()); + } + + if (!authors.isEmpty()) { + metadata.setAuthors(authors); + } + } catch (Exception e) { + log.warn("Failed to parse author from JSON-LD: {}", e.getMessage()); + } + } + + // Genre/Category - extracted from genre URL + if (root.has("genre")) { + try { + String genreUrl = root.get("genre").asText(); + // Extract category name from URL like "https://lubimyczytac.pl/ksiazki/k/69/poradniki" + // Get the last segment after the final slash + String[] urlParts = genreUrl.split("/"); + if (urlParts.length > 0) { + String categoryName = urlParts[urlParts.length - 1]; + if (!categoryName.isEmpty()) { + Set categories = new HashSet<>(); + categories.add(categoryName); + metadata.setCategories(categories); + } + } + } catch (Exception e) { + log.warn("Failed to parse genre from JSON-LD: {}", e.getMessage()); + } + } + + } catch (Exception e) { + log.warn("Failed to parse JSON-LD structure", e); + } + } +} diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/util/MetadataChangeDetector.java b/booklore-api/src/main/java/com/adityachandel/booklore/util/MetadataChangeDetector.java index b4f65b106..e6cac161a 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/util/MetadataChangeDetector.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/util/MetadataChangeDetector.java @@ -45,6 +45,8 @@ public class MetadataChangeDetector { compare(changes, "goodreadsReviewCount", clear.isGoodreadsReviewCount(), newMeta.getGoodreadsReviewCount(), existingMeta.getGoodreadsReviewCount(), () -> !isTrue(existingMeta.getGoodreadsReviewCountLocked()), newMeta.getGoodreadsReviewCountLocked(), existingMeta.getGoodreadsReviewCountLocked()); compare(changes, "hardcoverRating", clear.isHardcoverRating(), newMeta.getHardcoverRating(), existingMeta.getHardcoverRating(), () -> !isTrue(existingMeta.getHardcoverRatingLocked()), newMeta.getHardcoverRatingLocked(), existingMeta.getHardcoverRatingLocked()); compare(changes, "hardcoverReviewCount", clear.isHardcoverReviewCount(), newMeta.getHardcoverReviewCount(), existingMeta.getHardcoverReviewCount(), () -> !isTrue(existingMeta.getHardcoverReviewCountLocked()), newMeta.getHardcoverReviewCountLocked(), existingMeta.getHardcoverReviewCountLocked()); + compare(changes, "lubimyczytacId", clear.isLubimyczytacId(), newMeta.getLubimyczytacId(), existingMeta.getLubimyczytacId(), () -> !isTrue(existingMeta.getLubimyczytacIdLocked()), newMeta.getLubimyczytacIdLocked(), existingMeta.getLubimyczytacIdLocked()); + compare(changes, "lubimyczytacRating", clear.isLubimyczytacRating(), newMeta.getLubimyczytacRating(), existingMeta.getLubimyczytacRating(), () -> !isTrue(existingMeta.getLubimyczytacRatingLocked()), newMeta.getLubimyczytacRatingLocked(), existingMeta.getLubimyczytacRatingLocked()); compare(changes, "authors", clear.isAuthors(), newMeta.getAuthors(), toNameSet(existingMeta.getAuthors()), () -> !isTrue(existingMeta.getAuthorsLocked()), newMeta.getAuthorsLocked(), existingMeta.getAuthorsLocked()); compare(changes, "categories", clear.isCategories(), newMeta.getCategories(), toNameSet(existingMeta.getCategories()), () -> !isTrue(existingMeta.getCategoriesLocked()), newMeta.getCategoriesLocked(), existingMeta.getCategoriesLocked()); compare(changes, "moods", clear.isMoods(), newMeta.getMoods(), toNameSet(existingMeta.getMoods()), () -> !isTrue(existingMeta.getMoodsLocked()), newMeta.getMoodsLocked(), existingMeta.getMoodsLocked()); @@ -86,6 +88,8 @@ public class MetadataChangeDetector { compareValue(diffs, "goodreadsReviewCount", clear.isGoodreadsReviewCount(), newMeta.getGoodreadsReviewCount(), existingMeta.getGoodreadsReviewCount(), () -> !isTrue(existingMeta.getGoodreadsReviewCountLocked())); compareValue(diffs, "hardcoverRating", clear.isHardcoverRating(), newMeta.getHardcoverRating(), existingMeta.getHardcoverRating(), () -> !isTrue(existingMeta.getHardcoverRatingLocked())); compareValue(diffs, "hardcoverReviewCount", clear.isHardcoverReviewCount(), newMeta.getHardcoverReviewCount(), existingMeta.getHardcoverReviewCount(), () -> !isTrue(existingMeta.getHardcoverReviewCountLocked())); + compareValue(diffs, "lubimyczytacId", clear.isLubimyczytacId(), newMeta.getLubimyczytacId(), existingMeta.getLubimyczytacId(), () -> !isTrue(existingMeta.getLubimyczytacIdLocked())); + compareValue(diffs, "lubimyczytacRating", clear.isLubimyczytacRating(), newMeta.getLubimyczytacRating(), existingMeta.getLubimyczytacRating(), () -> !isTrue(existingMeta.getLubimyczytacRatingLocked())); compareValue(diffs, "authors", clear.isAuthors(), newMeta.getAuthors(), toNameSet(existingMeta.getAuthors()), () -> !isTrue(existingMeta.getAuthorsLocked())); compareValue(diffs, "categories", clear.isCategories(), newMeta.getCategories(), toNameSet(existingMeta.getCategories()), () -> !isTrue(existingMeta.getCategoriesLocked())); compareValue(diffs, "moods", clear.isMoods(), newMeta.getMoods(), toNameSet(existingMeta.getMoods()), () -> !isTrue(existingMeta.getMoodsLocked())); diff --git a/booklore-api/src/main/resources/db/migration/V80__Add_lubimyczytac_provider.sql b/booklore-api/src/main/resources/db/migration/V80__Add_lubimyczytac_provider.sql new file mode 100644 index 000000000..0468ed443 --- /dev/null +++ b/booklore-api/src/main/resources/db/migration/V80__Add_lubimyczytac_provider.sql @@ -0,0 +1,6 @@ +-- Add LubimyCzytac metadata columns +ALTER TABLE book_metadata + ADD COLUMN IF NOT EXISTS lubimyczytac_id VARCHAR(100), + ADD COLUMN IF NOT EXISTS lubimyczytac_rating FLOAT, + ADD COLUMN IF NOT EXISTS lubimyczytac_id_locked BOOLEAN DEFAULT FALSE, + ADD COLUMN IF NOT EXISTS lubimyczytac_rating_locked BOOLEAN DEFAULT FALSE; diff --git a/booklore-api/src/test/java/com/adityachandel/booklore/service/metadata/parser/LubimyCzytacParserTest.java b/booklore-api/src/test/java/com/adityachandel/booklore/service/metadata/parser/LubimyCzytacParserTest.java new file mode 100644 index 000000000..375f38d53 --- /dev/null +++ b/booklore-api/src/test/java/com/adityachandel/booklore/service/metadata/parser/LubimyCzytacParserTest.java @@ -0,0 +1,156 @@ +package com.adityachandel.booklore.service.metadata.parser; + +import com.adityachandel.booklore.model.dto.Book; +import com.adityachandel.booklore.model.dto.BookMetadata; +import com.adityachandel.booklore.model.dto.request.FetchMetadataRequest; +import com.adityachandel.booklore.model.dto.settings.AppSettings; +import com.adityachandel.booklore.model.dto.settings.MetadataProviderSettings; +import com.adityachandel.booklore.service.appsettings.AppSettingService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class LubimyCzytacParserTest { + + @Mock + private AppSettingService appSettingService; + + @InjectMocks + private LubimyCzytacParser parser; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void testFetchMetadata_ProviderDisabled() { + // Given + Book book = Book.builder() + .title("Test Book") + .build(); + + FetchMetadataRequest request = FetchMetadataRequest.builder() + .title("Test Book") + .build(); + + // Mock disabled provider + AppSettings appSettings = new AppSettings(); + MetadataProviderSettings providerSettings = new MetadataProviderSettings(); + MetadataProviderSettings.Lubimyczytac lubimyCzytac = new MetadataProviderSettings.Lubimyczytac(); + lubimyCzytac.setEnabled(false); + providerSettings.setLubimyczytac(lubimyCzytac); + appSettings.setMetadataProviderSettings(providerSettings); + + when(appSettingService.getAppSettings()).thenReturn(appSettings); + + // When + List results = parser.fetchMetadata(book, request); + + // Then + assertNotNull(results); + assertTrue(results.isEmpty(), "Should return empty list when provider is disabled"); + verify(appSettingService).getAppSettings(); + } + + @Test + void testFetchMetadata_ProviderSettingsNull() { + // Given + Book book = Book.builder() + .title("Test Book") + .build(); + + FetchMetadataRequest request = FetchMetadataRequest.builder() + .title("Test Book") + .build(); + + // Mock null settings + AppSettings appSettings = new AppSettings(); + appSettings.setMetadataProviderSettings(null); + + when(appSettingService.getAppSettings()).thenReturn(appSettings); + + // When + List results = parser.fetchMetadata(book, request); + + // Then + assertNotNull(results); + assertTrue(results.isEmpty(), "Should return empty list when settings are null"); + verify(appSettingService).getAppSettings(); + } + + @Test + void testFetchMetadata_EmptyQuery() { + // Given + Book book = Book.builder() + .title("Test Book") + .build(); + + FetchMetadataRequest request = FetchMetadataRequest.builder() + .build(); + // Empty query - no title or ISBN + + // Mock enabled provider + AppSettings appSettings = new AppSettings(); + MetadataProviderSettings providerSettings = new MetadataProviderSettings(); + MetadataProviderSettings.Lubimyczytac lubimyCzytac = new MetadataProviderSettings.Lubimyczytac(); + lubimyCzytac.setEnabled(true); + providerSettings.setLubimyczytac(lubimyCzytac); + appSettings.setMetadataProviderSettings(providerSettings); + + when(appSettingService.getAppSettings()).thenReturn(appSettings); + + // When + List results = parser.fetchMetadata(book, request); + + // Then + assertNotNull(results); + assertTrue(results.isEmpty(), "Should return empty list when query is empty"); + verify(appSettingService).getAppSettings(); + } + + @Test + @Disabled("Integration test - requires network access to LubimyCzytac.pl") + void testFetchMetadata_Integration_RealBook() { + // Given + Book book = Book.builder() + .title("Wiedźmin") + .build(); + + FetchMetadataRequest request = FetchMetadataRequest.builder() + .title("Wiedźmin") + .author("Andrzej Sapkowski") + .build(); + + // Mock enabled provider + AppSettings appSettings = new AppSettings(); + MetadataProviderSettings providerSettings = new MetadataProviderSettings(); + MetadataProviderSettings.Lubimyczytac lubimyCzytac = new MetadataProviderSettings.Lubimyczytac(); + lubimyCzytac.setEnabled(true); + providerSettings.setLubimyczytac(lubimyCzytac); + appSettings.setMetadataProviderSettings(providerSettings); + + when(appSettingService.getAppSettings()).thenReturn(appSettings); + + // When + List results = parser.fetchMetadata(book, request); + + // Then + assertNotNull(results); + assertFalse(results.isEmpty(), "Should return results for real book"); + + BookMetadata firstResult = results.get(0); + assertNotNull(firstResult.getTitle(), "Title should be present"); + assertNotNull(firstResult.getLubimyczytacId(), "LubimyCzytac ID should be present"); + assertTrue(firstResult.getAuthors() != null && !firstResult.getAuthors().isEmpty(), + "Authors should be present"); + } +} diff --git a/booklore-ui/src/app/features/book/model/book.model.ts b/booklore-ui/src/app/features/book/model/book.model.ts index e536596fb..6111e6a2e 100644 --- a/booklore-ui/src/app/features/book/model/book.model.ts +++ b/booklore-ui/src/app/features/book/model/book.model.ts @@ -101,6 +101,8 @@ export interface BookMetadata { goodreadsReviewCount?: number | null; hardcoverRating?: number | null; hardcoverReviewCount?: number | null; + lubimyczytacId?: string; + lubimyczytacRating?: number | null; coverUpdatedOn?: string; authors?: string[]; categories?: string[]; @@ -134,6 +136,8 @@ export interface BookMetadata { goodreadsReviewCountLocked?: boolean; hardcoverRatingLocked?: boolean; hardcoverReviewCountLocked?: boolean; + lubimyczytacIdLocked?: boolean; + lubimyczytacRatingLocked?: boolean; coverUpdatedOnLocked?: boolean; authorsLocked?: boolean; categoriesLocked?: boolean; @@ -169,6 +173,8 @@ export interface MetadataClearFlags { goodreadsReviewCount?: boolean; hardcoverRating?: boolean; hardcoverReviewCount?: boolean; + lubimyczytacId?: boolean; + lubimyczytacRating?: boolean; authors?: boolean; categories?: boolean; moods?: boolean; diff --git a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.html b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.html index 3a4aef33b..50cfe186b 100644 --- a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.html +++ b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.html @@ -507,6 +507,30 @@ } +
+ +
+ + @if (!book.metadata!['lubimyczytacIdLocked']) { + + } + @if (book.metadata!['lubimyczytacIdLocked']) { + + } +
+
+
+ +
+ + @if (!book.metadata!['lubimyczytacRatingLocked']) { + + } + @if (book.metadata!['lubimyczytacRatingLocked']) { + + } +
+
diff --git a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.ts b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.ts index 1fa4a3aea..05fc54d8d 100644 --- a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.ts +++ b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.ts @@ -166,6 +166,8 @@ export class MetadataEditorComponent implements OnInit { hardcoverBookId: new FormControl(""), hardcoverRating: new FormControl(""), hardcoverReviewCount: new FormControl(""), + lubimyczytacId: new FormControl(""), + lubimyczytacRating: new FormControl(""), googleId: new FormControl(""), seriesName: new FormControl(""), seriesNumber: new FormControl(""), @@ -196,6 +198,8 @@ export class MetadataEditorComponent implements OnInit { hardcoverBookIdLocked: new FormControl(false), hardcoverRatingLocked: new FormControl(false), hardcoverReviewCountLocked: new FormControl(false), + lubimyczytacIdLocked: new FormControl(false), + lubimyczytacRatingLocked: new FormControl(false), googleIdLocked: new FormControl(false), seriesNameLocked: new FormControl(false), seriesNumberLocked: new FormControl(false), @@ -296,6 +300,8 @@ export class MetadataEditorComponent implements OnInit { hardcoverBookId: metadata.hardcoverBookId ?? null, hardcoverRating: metadata.hardcoverRating ?? null, hardcoverReviewCount: metadata.hardcoverReviewCount ?? null, + lubimyczytacId: metadata.lubimyczytacId ?? null, + lubimyczytacRating: metadata.lubimyczytacRating ?? null, googleId: metadata.googleId ?? null, seriesName: metadata.seriesName ?? null, seriesNumber: metadata.seriesNumber ?? null, @@ -324,6 +330,8 @@ export class MetadataEditorComponent implements OnInit { hardcoverBookIdLocked: metadata.hardcoverBookIdLocked ?? false, hardcoverRatingLocked: metadata.hardcoverRatingLocked ?? false, hardcoverReviewCountLocked: metadata.hardcoverReviewCountLocked ?? false, + lubimyczytacIdLocked: metadata.lubimyczytacIdLocked ?? false, + lubimyczytacRatingLocked: metadata.lubimyczytacRatingLocked ?? false, googleIdLocked: metadata.googleIdLocked ?? false, seriesNameLocked: metadata.seriesNameLocked ?? false, seriesNumberLocked: metadata.seriesNumberLocked ?? false, diff --git a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-picker/metadata-picker.component.ts b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-picker/metadata-picker.component.ts index ba1a88f27..37be51bd9 100644 --- a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-picker/metadata-picker.component.ts +++ b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-picker/metadata-picker.component.ts @@ -76,6 +76,8 @@ export class MetadataPickerComponent implements OnInit { {label: 'Hardcover Book ID', controlName: 'hardcoverBookId', lockedKey: 'hardcoverBookIdLocked', fetchedKey: 'hardcoverBookId'}, {label: 'HC Reviews', controlName: 'hardcoverReviewCount', lockedKey: 'hardcoverReviewCountLocked', fetchedKey: 'hardcoverReviewCount'}, {label: 'HC Rating', controlName: 'hardcoverRating', lockedKey: 'hardcoverRatingLocked', fetchedKey: 'hardcoverRating'}, + {label: 'LC ID', controlName: 'lubimyczytacId', lockedKey: 'lubimyczytacIdLocked', fetchedKey: 'lubimyczytacId'}, + {label: 'LC Rating', controlName: 'lubimyczytacRating', lockedKey: 'lubimyczytacRatingLocked', fetchedKey: 'lubimyczytacRating'}, {label: 'Google ID', controlName: 'googleId', lockedKey: 'googleIdLocked', fetchedKey: 'googleId'}, {label: 'Pages', controlName: 'pageCount', lockedKey: 'pageCountLocked', fetchedKey: 'pageCount'} ]; @@ -153,6 +155,8 @@ export class MetadataPickerComponent implements OnInit { hardcoverBookId: new FormControl(''), hardcoverRating: new FormControl(''), hardcoverReviewCount: new FormControl(''), + lubimyczytacId: new FormControl(''), + lubimyczytacRating: new FormControl(''), googleId: new FormControl(''), seriesName: new FormControl(''), seriesNumber: new FormControl(''), @@ -183,6 +187,8 @@ export class MetadataPickerComponent implements OnInit { hardcoverBookIdLocked: new FormControl(false), hardcoverRatingLocked: new FormControl(false), hardcoverReviewCountLocked: new FormControl(false), + lubimyczytacIdLocked: new FormControl(false), + lubimyczytacRatingLocked: new FormControl(false), googleIdLocked: new FormControl(false), seriesNameLocked: new FormControl(false), seriesNumberLocked: new FormControl(false), @@ -260,6 +266,8 @@ export class MetadataPickerComponent implements OnInit { hardcoverBookId: metadata.hardcoverBookId || null, hardcoverRating: metadata.hardcoverRating || null, hardcoverReviewCount: metadata.hardcoverReviewCount || null, + lubimyczytacId: metadata.lubimyczytacId || null, + lubimyczytacRating: metadata.lubimyczytacRating || null, googleId: metadata.googleId || null, seriesName: metadata.seriesName || null, seriesNumber: metadata.seriesNumber || null, @@ -290,6 +298,8 @@ export class MetadataPickerComponent implements OnInit { hardcoverBookIdLocked: metadata.hardcoverBookIdLocked || false, hardcoverRatingLocked: metadata.hardcoverRatingLocked || false, hardcoverReviewCountLocked: metadata.hardcoverReviewCountLocked || false, + lubimyczytacIdLocked: metadata.lubimyczytacIdLocked || false, + lubimyczytacRatingLocked: metadata.lubimyczytacRatingLocked || false, googleIdLocked: metadata.googleIdLocked || false, seriesNameLocked: metadata.seriesNameLocked || false, seriesNumberLocked: metadata.seriesNumberLocked || false, @@ -327,6 +337,8 @@ export class MetadataPickerComponent implements OnInit { if (metadata.hardcoverBookIdLocked) this.metadataForm.get('hardcoverBookId')?.disable({emitEvent: false}); if (metadata.hardcoverReviewCountLocked) this.metadataForm.get('hardcoverReviewCount')?.disable({emitEvent: false}); if (metadata.hardcoverRatingLocked) this.metadataForm.get('hardcoverRating')?.disable({emitEvent: false}); + if (metadata.lubimyczytacIdLocked) this.metadataForm.get('lubimyczytacId')?.disable({emitEvent: false}); + if (metadata.lubimyczytacRatingLocked) this.metadataForm.get('lubimyczytacRating')?.disable({emitEvent: false}); if (metadata.googleIdLocked) this.metadataForm.get('googleId')?.disable({emitEvent: false}); if (metadata.pageCountLocked) this.metadataForm.get('pageCount')?.disable({emitEvent: false}); if (metadata.descriptionLocked) this.metadataForm.get('description')?.disable({emitEvent: false}); @@ -406,6 +418,8 @@ export class MetadataPickerComponent implements OnInit { hardcoverBookId: this.metadataForm.get('hardcoverBookId')?.value || this.copiedFields['hardcoverBookId'] ? (this.getNumberOrCopied('hardcoverBookId') ?? null) : null, hardcoverRating: this.metadataForm.get('hardcoverRating')?.value || this.copiedFields['hardcoverRating'] ? this.getNumberOrCopied('hardcoverRating') : null, hardcoverReviewCount: this.metadataForm.get('hardcoverReviewCount')?.value || this.copiedFields['hardcoverReviewCount'] ? this.getNumberOrCopied('hardcoverReviewCount') : null, + lubimyczytacId: this.metadataForm.get('lubimyczytacId')?.value || this.copiedFields['lubimyczytacId'] ? this.getValueOrCopied('lubimyczytacId') : '', + lubimyczytacRating: this.metadataForm.get('lubimyczytacRating')?.value || this.copiedFields['lubimyczytacRating'] ? this.getNumberOrCopied('lubimyczytacRating') : null, googleId: this.metadataForm.get('googleId')?.value || this.copiedFields['googleId'] ? this.getValueOrCopied('googleId') : '', seriesName: this.metadataForm.get('seriesName')?.value || this.copiedFields['seriesName'] ? this.getValueOrCopied('seriesName') : '', seriesNumber: this.metadataForm.get('seriesNumber')?.value || this.copiedFields['seriesNumber'] ? this.getNumberOrCopied('seriesNumber') : null, @@ -436,6 +450,8 @@ export class MetadataPickerComponent implements OnInit { hardcoverBookIdLocked: this.metadataForm.get('hardcoverBookIdLocked')?.value, hardcoverRatingLocked: this.metadataForm.get('hardcoverRatingLocked')?.value, hardcoverReviewCountLocked: this.metadataForm.get('hardcoverReviewCountLocked')?.value, + lubimyczytacIdLocked: this.metadataForm.get('lubimyczytacIdLocked')?.value, + lubimyczytacRatingLocked: this.metadataForm.get('lubimyczytacRatingLocked')?.value, googleIdLocked: this.metadataForm.get('googleIdLocked')?.value, seriesNameLocked: this.metadataForm.get('seriesNameLocked')?.value, seriesNumberLocked: this.metadataForm.get('seriesNumberLocked')?.value, @@ -479,6 +495,8 @@ export class MetadataPickerComponent implements OnInit { hardcoverBookId: current.hardcoverBookId === null && original.hardcoverBookId !== null, hardcoverRating: current.hardcoverRating === null && original.hardcoverRating !== null, hardcoverReviewCount: current.hardcoverReviewCount === null && original.hardcoverReviewCount !== null, + lubimyczytacId: !current.lubimyczytacId && !!original.lubimyczytacId, + lubimyczytacRating: current.lubimyczytacRating === null && original.lubimyczytacRating !== null, googleId: !current.googleId && !!original.googleId, seriesName: !current.seriesName && !!original.seriesName, seriesNumber: current.seriesNumber === null && original.seriesNumber !== null, diff --git a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-searcher/metadata-searcher.component.ts b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-searcher/metadata-searcher.component.ts index 45f276ebe..6f5c1a683 100644 --- a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-searcher/metadata-searcher.component.ts +++ b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-searcher/metadata-searcher.component.ts @@ -189,6 +189,8 @@ export class MetadataSearcherComponent implements OnInit, OnDestroy { return `Hardcover`; } else if (metadata['doubanId']) { return `Douban`; + } else if (metadata['lubimyczytacId']) { + return `Lubimyczytac`; } else if (metadata.comicvineId) { if (metadata.comicvineId.startsWith('4000')) { const name = metadata.seriesName ? metadata.seriesName.replace(/ /g, '-').toLowerCase() + "-" + metadata.seriesNumber : ''; diff --git a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.html b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.html index fc921e4fe..6b55424a6 100644 --- a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.html +++ b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.html @@ -154,7 +154,7 @@
- @if (book?.metadata?.amazonRating || book?.metadata?.goodreadsRating || book?.metadata?.hardcoverRating || book?.metadata?.googleId) { + @if (book?.metadata?.amazonRating || book?.metadata?.goodreadsRating || book?.metadata?.hardcoverRating || book?.metadata?.lubimyczytacRating || book?.metadata?.googleId) {
} @@ -201,6 +201,20 @@ } + @if (book?.metadata?.lubimyczytacRating) { + + Lubimyczytac + {{ getRatingPercent(book.metadata!.lubimyczytacRating) }}% + + } + @if (book?.metadata?.googleId) {
+ +
+
+
+ + +
+
+
diff --git a/booklore-ui/src/app/features/settings/global-preferences/metadata-provider-settings/metadata-provider-settings.component.ts b/booklore-ui/src/app/features/settings/global-preferences/metadata-provider-settings/metadata-provider-settings.component.ts index 8a1ae37ad..cfe37f6f3 100644 --- a/booklore-ui/src/app/features/settings/global-preferences/metadata-provider-settings/metadata-provider-settings.component.ts +++ b/booklore-ui/src/app/features/settings/global-preferences/metadata-provider-settings/metadata-provider-settings.component.ts @@ -76,6 +76,7 @@ export class MetadataProviderSettingsComponent implements OnInit { comicvineEnabled: boolean = false; comicvineToken: string = ''; doubanEnabled: boolean = false; + lubimyCzytacEnabled: boolean = false; private appSettingsService = inject(AppSettingsService); private messageService = inject(MessageService); @@ -101,6 +102,7 @@ export class MetadataProviderSettingsComponent implements OnInit { this.comicvineEnabled = metadataProviderSettings?.comicvine?.enabled ?? false; this.comicvineToken = metadataProviderSettings?.comicvine?.apiKey ?? ''; this.doubanEnabled = metadataProviderSettings?.douban?.enabled ?? false; + this.lubimyCzytacEnabled = metadataProviderSettings?.lubimyczytac?.enabled ?? false; }); } @@ -143,7 +145,8 @@ export class MetadataProviderSettingsComponent implements OnInit { enabled: this.hardcoverEnabled, apiKey: this.hardcoverToken.trim() }, - douban: {enabled: this.doubanEnabled} + douban: {enabled: this.doubanEnabled}, + lubimyczytac: {enabled: this.lubimyCzytacEnabled} } } ]; diff --git a/booklore-ui/src/app/features/settings/library-metadata-settings/library-metadata-settings.component.ts b/booklore-ui/src/app/features/settings/library-metadata-settings/library-metadata-settings.component.ts index 022f416ce..fb37d1037 100644 --- a/booklore-ui/src/app/features/settings/library-metadata-settings/library-metadata-settings.component.ts +++ b/booklore-ui/src/app/features/settings/library-metadata-settings/library-metadata-settings.component.ts @@ -215,6 +215,8 @@ export class LibraryMetadataSettingsComponent implements OnInit { goodreadsReviewCount: {p1: null, p2: null, p3: null, p4: null}, hardcoverRating: {p1: null, p2: null, p3: null, p4: null}, hardcoverReviewCount: {p1: null, p2: null, p3: null, p4: null}, + lubimyczytacId: {p1: null, p2: null, p3: null, p4: null}, + lubimyczytacRating: {p1: null, p2: null, p3: null, p4: null}, moods: {p1: null, p2: null, p3: null, p4: null}, tags: {p1: null, p2: null, p3: null, p4: null} } diff --git a/booklore-ui/src/app/shared/model/app-settings.model.ts b/booklore-ui/src/app/shared/model/app-settings.model.ts index 05ffd65a1..bf4569e09 100644 --- a/booklore-ui/src/app/shared/model/app-settings.model.ts +++ b/booklore-ui/src/app/shared/model/app-settings.model.ts @@ -23,6 +23,7 @@ export interface MetadataMatchWeights { hardcoverReviewCount: number; doubanRating: number; doubanReviewCount: number; + lubimyczytacRating: number; coverImage: number; } @@ -50,6 +51,7 @@ export interface MetadataProviderSettings { hardcover: Hardcover; comicvine: Comicvine; douban: Douban; + lubimyczytac: Lubimyczytac; } export interface Amazon { @@ -81,6 +83,10 @@ export interface Douban { enabled: boolean; } +export interface Lubimyczytac { + enabled: boolean; +} + export interface MetadataPersistenceSettings { moveFilesToLibraryPattern: boolean; saveToOriginalFile: boolean;