From 8ee53ff7ba3c5128526c7e730d79a835d820c75a Mon Sep 17 00:00:00 2001 From: Muppetteer Date: Fri, 12 Dec 2025 12:37:21 +1100 Subject: [PATCH] Per user personal ratings (#1820) * Change personal ratings to be saved per user * Only copy ratings to users with existing progress records --- .../booklore/controller/BookController.java | 25 +++++++ .../booklore/model/MetadataClearFlags.java | 1 - .../booklore/model/dto/Book.java | 1 + .../booklore/model/dto/BookMetadata.java | 3 - .../booklore/model/dto/EpubMetadata.java | 1 - .../request/PersonalRatingUpdateRequest.java | 6 ++ .../model/entity/BookMetadataEntity.java | 9 --- .../model/entity/UserBookProgressEntity.java | 3 + .../service/BookRuleEvaluatorService.java | 2 +- .../booklore/service/book/BookService.java | 69 +++++++++++++++++-- .../service/fileprocessor/EpubProcessor.java | 1 - .../service/fileprocessor/PdfProcessor.java | 3 - .../service/metadata/BookMetadataUpdater.java | 2 - .../extractor/EpubMetadataExtractor.java | 4 -- .../extractor/PdfMetadataExtractor.java | 5 -- .../metadata/writer/EpubMetadataWriter.java | 5 -- .../metadata/writer/MetadataCopyHelper.java | 7 -- .../metadata/writer/PdfMetadataWriter.java | 5 -- .../booklore/util/MetadataChangeDetector.java | 3 - .../migration/V70__Add_user_rating_column.sql | 11 +++ .../util/MetadataChangeDetectorTest.java | 5 -- .../book-filter/book-filter.component.ts | 2 +- .../book-browser/filters/SidebarFilter.ts | 2 +- .../book-browser/sorting/BookSorter.ts | 1 + .../src/app/features/book/model/book.model.ts | 4 +- .../app/features/book/service/book.service.ts | 16 +++++ .../app/features/book/service/sort.service.ts | 2 +- .../service/book-rule-evaluator.service.ts | 2 +- .../metadata-editor.component.ts | 8 --- .../metadata-picker.component.ts | 6 -- .../metadata-viewer.component.html | 4 +- .../metadata-viewer.component.ts | 14 +--- .../author-popularity-chart.service.ts | 2 +- .../service/personal-rating-chart.service.ts | 2 +- .../service/reading-dna-chart.service.ts | 4 +- .../service/reading-habits-chart.service.ts | 2 +- .../service/reading-velocity-chart.service.ts | 6 +- ...reading-velocity-timeline-chart.service.ts | 4 +- ...eries-completion-progress-chart.service.ts | 4 +- 39 files changed, 147 insertions(+), 109 deletions(-) create mode 100644 booklore-api/src/main/java/com/adityachandel/booklore/model/dto/request/PersonalRatingUpdateRequest.java create mode 100644 booklore-api/src/main/resources/db/migration/V70__Add_user_rating_column.sql diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/controller/BookController.java b/booklore-api/src/main/java/com/adityachandel/booklore/controller/BookController.java index 326adcde..c8d84e50 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/controller/BookController.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/controller/BookController.java @@ -5,6 +5,7 @@ import com.adityachandel.booklore.exception.ApiError; import com.adityachandel.booklore.model.dto.Book; import com.adityachandel.booklore.model.dto.BookRecommendation; import com.adityachandel.booklore.model.dto.BookViewerSettings; +import com.adityachandel.booklore.model.dto.request.PersonalRatingUpdateRequest; import com.adityachandel.booklore.model.dto.request.ReadProgressRequest; import com.adityachandel.booklore.model.dto.request.ReadStatusUpdateRequest; import com.adityachandel.booklore.model.dto.request.ShelvesAssignmentRequest; @@ -186,4 +187,28 @@ public class BookController { List updatedBooks = bookService.resetProgress(bookIds, type); return ResponseEntity.ok(updatedBooks); } + + @Operation(summary = "Update personal rating", description = "Update the personal rating for one or more books.") + @ApiResponse(responseCode = "200", description = "Personal rating updated successfully") + @PutMapping("/personal-rating") + public ResponseEntity> updatePersonalRating( + @Parameter(description = "Personal rating update request") @RequestBody @Valid PersonalRatingUpdateRequest request) { + List updatedBooks = bookService.updatePersonalRating(request.ids(), request.rating()); + return ResponseEntity.ok(updatedBooks); + } + + @Operation(summary = "Reset personal rating", description = "Reset the personal rating for one or more books.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Personal rating reset successfully"), + @ApiResponse(responseCode = "400", description = "No book IDs provided") + }) + @PostMapping("/reset-personal-rating") + public ResponseEntity> resetPersonalRating( + @Parameter(description = "List of book IDs to reset personal rating for") @RequestBody List bookIds) { + if (bookIds == null || bookIds.isEmpty()) { + throw ApiError.GENERIC_BAD_REQUEST.createException("No book IDs provided"); + } + List updatedBooks = bookService.resetPersonalRating(bookIds); + return ResponseEntity.ok(updatedBooks); + } } 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 aabc8209..d15ce196 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 @@ -28,7 +28,6 @@ public class MetadataClearFlags { private boolean goodreadsReviewCount; private boolean hardcoverRating; private boolean hardcoverReviewCount; - private boolean personalRating; private boolean authors; private boolean categories; private boolean moods; diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/Book.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/Book.java index e19f8cb9..8526cd65 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/Book.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/Book.java @@ -32,6 +32,7 @@ public class Book { private CbxProgress cbxProgress; private KoProgress koreaderProgress; private KoboProgress koboProgress; + private Integer personalRating; private Set shelves; private String readStatus; private Instant dateFinished; 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 3b2986ed..2cb7d65d 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 @@ -29,7 +29,6 @@ public class BookMetadata { private String isbn10; private Integer pageCount; private String language; - private Double rating; private String asin; private Double amazonRating; private Integer amazonReviewCount; @@ -43,7 +42,6 @@ public class BookMetadata { private String doubanId; private Double doubanRating; private Integer doubanReviewCount; - private Double personalRating; private String googleId; private Instant coverUpdatedOn; private Set authors; @@ -72,7 +70,6 @@ public class BookMetadata { private Boolean googleIdLocked; private Boolean pageCountLocked; private Boolean languageLocked; - private Boolean personalRatingLocked; private Boolean amazonRatingLocked; private Boolean amazonReviewCountLocked; private Boolean goodreadsRatingLocked; diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/EpubMetadata.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/EpubMetadata.java index 73d1feaa..4e08b828 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/EpubMetadata.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/EpubMetadata.java @@ -31,7 +31,6 @@ public class EpubMetadata { private Integer pageCount; private String language; private String asin; - private Double personalRating; private Double amazonRating; private Integer amazonReviewCount; private String goodreadsId; diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/request/PersonalRatingUpdateRequest.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/request/PersonalRatingUpdateRequest.java new file mode 100644 index 00000000..fe03ef85 --- /dev/null +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/request/PersonalRatingUpdateRequest.java @@ -0,0 +1,6 @@ +package com.adityachandel.booklore.model.dto.request; + +import java.util.List; + +public record PersonalRatingUpdateRequest(List ids, Integer rating) { +} 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 9bf59a91..5ad6dc6a 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 @@ -88,9 +88,6 @@ public class BookMetadataEntity { @Column(name = "hardcover_review_count") private Integer hardcoverReviewCount; - @Column(name = "personal_rating") - private Double personalRating; - @Column(name = "asin", length = 10) private String asin; @@ -171,10 +168,6 @@ public class BookMetadataEntity { @Builder.Default private Boolean hardcoverReviewCountLocked = Boolean.FALSE; - @Column(name = "personal_rating_locked") - @Builder.Default - private Boolean personalRatingLocked = Boolean.FALSE; - @Column(name = "cover_locked") @Builder.Default private Boolean coverLocked = Boolean.FALSE; @@ -314,7 +307,6 @@ public class BookMetadataEntity { this.hardcoverRatingLocked = lock; this.hardcoverReviewCountLocked = lock; this.comicvineIdLocked = lock; - this.personalRatingLocked = lock; this.goodreadsIdLocked = lock; this.hardcoverIdLocked = lock; this.googleIdLocked = lock; @@ -346,7 +338,6 @@ public class BookMetadataEntity { && Boolean.TRUE.equals(this.goodreadsReviewCountLocked) && Boolean.TRUE.equals(this.hardcoverRatingLocked) && Boolean.TRUE.equals(this.hardcoverReviewCountLocked) - && Boolean.TRUE.equals(this.personalRatingLocked) && Boolean.TRUE.equals(this.goodreadsIdLocked) && Boolean.TRUE.equals(this.comicvineIdLocked) && Boolean.TRUE.equals(this.hardcoverIdLocked) diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/UserBookProgressEntity.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/UserBookProgressEntity.java index 018464ef..4f6d1ec4 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/UserBookProgressEntity.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/UserBookProgressEntity.java @@ -93,4 +93,7 @@ public class UserBookProgressEntity { @Column(name = "read_status_modified_time") private Instant readStatusModifiedTime; + + @Column(name = "personal_rating") + private Integer personalRating; } \ No newline at end of file diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/BookRuleEvaluatorService.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/BookRuleEvaluatorService.java index f58a3971..a402387d 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/BookRuleEvaluatorService.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/BookRuleEvaluatorService.java @@ -312,13 +312,13 @@ public class BookRuleEvaluatorService { case READ_STATUS -> progressJoin.get("readStatus"); case DATE_FINISHED -> progressJoin.get("dateFinished"); case LAST_READ_TIME -> progressJoin.get("lastReadTime"); + case PERSONAL_RATING -> progressJoin.get("personalRating"); case FILE_SIZE -> root.get("fileSizeKb"); case METADATA_SCORE -> root.get("metadataMatchScore"); case TITLE -> root.get("metadata").get("title"); case SUBTITLE -> root.get("metadata").get("subtitle"); case PUBLISHER -> root.get("metadata").get("publisher"); case PUBLISHED_DATE -> root.get("metadata").get("publishedDate"); - case PERSONAL_RATING -> root.get("metadata").get("personalRating"); case PAGE_COUNT -> root.get("metadata").get("pageCount"); case LANGUAGE -> root.get("metadata").get("language"); case SERIES_NAME -> root.get("metadata").get("seriesName"); diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/book/BookService.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/book/BookService.java index f05c66ba..92f1ea79 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/book/BookService.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/book/BookService.java @@ -100,6 +100,7 @@ public class BookService { book.setLastReadTime(progress.getLastReadTime()); book.setReadStatus(progress.getReadStatus() == null ? String.valueOf(ReadStatus.UNSET) : String.valueOf(progress.getReadStatus())); book.setDateFinished(progress.getDateFinished()); + book.setPersonalRating(progress.getPersonalRating()); } } @@ -191,6 +192,7 @@ public class BookService { book.setFilePath(FileUtils.getBookFullPath(bookEntity)); book.setReadStatus(userProgress.getReadStatus() == null ? String.valueOf(ReadStatus.UNSET) : String.valueOf(userProgress.getReadStatus())); book.setDateFinished(userProgress.getDateFinished()); + book.setPersonalRating(userProgress.getPersonalRating()); if (!withDescription) { book.getMetadata().setDescription(null); @@ -429,13 +431,7 @@ public class BookService { .findByUserIdAndBookId(user.getId(), bookEntity.getId()) .orElse(null); - if (progress != null) { - setBookProgress(book, progress); - book.setLastReadTime(progress.getLastReadTime()); - book.setReadStatus(progress.getReadStatus() == null ? String.valueOf(ReadStatus.UNSET) : String.valueOf(progress.getReadStatus())); - book.setDateFinished(progress.getDateFinished()); - } - + this.enrichBookWithProgress(book, progress); return book; }) .collect(Collectors.toList()); @@ -491,6 +487,65 @@ public class BookService { return updatedBooks; } + @Transactional + public List updatePersonalRating(List bookIds, Integer rating) { + BookLoreUser user = authenticationService.getAuthenticatedUser(); + + List books = bookRepository.findAllById(bookIds); + if (books.size() != bookIds.size()) { + throw ApiError.BOOK_NOT_FOUND.createException("One or more books not found"); + } + + BookLoreUserEntity userEntity = userRepository.findById(user.getId()).orElseThrow(() -> new UsernameNotFoundException("User not found")); + for (BookEntity book : books) { + UserBookProgressEntity progress = userBookProgressRepository + .findByUserIdAndBookId(user.getId(), book.getId()) + .orElse(new UserBookProgressEntity()); + + progress.setUser(userEntity); + progress.setBook(book); + progress.setPersonalRating(rating); + userBookProgressRepository.save(progress); + } + + return books.stream() + .map(bookEntity -> { + Book book = bookMapper.toBook(bookEntity); + book.setFilePath(FileUtils.getBookFullPath(bookEntity)); + + UserBookProgressEntity progress = userBookProgressRepository + .findByUserIdAndBookId(user.getId(), bookEntity.getId()) + .orElse(null); + + this.enrichBookWithProgress(book, progress); + return book; + }) + .collect(Collectors.toList()); + } + + public List resetPersonalRating(List bookIds) { + BookLoreUser user = authenticationService.getAuthenticatedUser(); + List updatedBooks = new ArrayList<>(); + Optional userEntity = userRepository.findById(user.getId()); + + for (Long bookId : bookIds) { + BookEntity bookEntity = bookRepository.findById(bookId).orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(bookId)); + + UserBookProgressEntity progress = userBookProgressRepository + .findByUserIdAndBookId(user.getId(), bookId) + .orElse(new UserBookProgressEntity()); + + progress.setBook(bookEntity); + progress.setUser(userEntity.orElseThrow()); + progress.setPersonalRating(null); + + userBookProgressRepository.save(progress); + updatedBooks.add(bookMapper.toBook(bookEntity)); + } + + return updatedBooks; + } + @Transactional public List assignShelvesToBooks(Set bookIds, Set shelfIdsToAssign, Set shelfIdsToUnassign) { BookLoreUser user = authenticationService.getAuthenticatedUser(); diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/fileprocessor/EpubProcessor.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/fileprocessor/EpubProcessor.java index 442451be..e710bd2f 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/fileprocessor/EpubProcessor.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/fileprocessor/EpubProcessor.java @@ -120,7 +120,6 @@ public class EpubProcessor extends AbstractFileProcessor implements BookFileProc metadata.setLanguage(truncate((lang == null || "UND".equalsIgnoreCase(lang)) ? "en" : lang, 1000)); metadata.setAsin(truncate(epubMetadata.getAsin(), 20)); - metadata.setPersonalRating(epubMetadata.getPersonalRating()); metadata.setAmazonRating(epubMetadata.getAmazonRating()); metadata.setAmazonReviewCount(epubMetadata.getAmazonReviewCount()); metadata.setGoodreadsId(truncate(epubMetadata.getGoodreadsId(), 100)); diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/fileprocessor/PdfProcessor.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/fileprocessor/PdfProcessor.java index c8e82236..f747cedb 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/fileprocessor/PdfProcessor.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/fileprocessor/PdfProcessor.java @@ -139,9 +139,6 @@ public class PdfProcessor extends AbstractFileProcessor implements BookFileProce if (StringUtils.isNotBlank(extracted.getIsbn13())) { bookEntity.getMetadata().setIsbn13(extracted.getIsbn13()); } - if (extracted.getPersonalRating() != null) { - bookEntity.getMetadata().setPersonalRating(extracted.getPersonalRating()); - } if (extracted.getCategories() != null) { bookCreatorService.addCategoriesToBook(extracted.getCategories(), bookEntity); } 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 8ed686c2..a5d022bb 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 @@ -163,7 +163,6 @@ public class BookMetadataUpdater { handleFieldUpdate(e.getGoogleIdLocked(), clear.isGoogleId(), m.getGoogleId(), v -> e.setGoogleId(nullIfBlank(v)), e::getGoogleId, replaceMode); handleFieldUpdate(e.getPageCountLocked(), clear.isPageCount(), m.getPageCount(), e::setPageCount, e::getPageCount, replaceMode); handleFieldUpdate(e.getLanguageLocked(), clear.isLanguage(), m.getLanguage(), v -> e.setLanguage(nullIfBlank(v)), e::getLanguage, replaceMode); - handleFieldUpdate(e.getPersonalRatingLocked(), clear.isPersonalRating(), m.getPersonalRating(), e::setPersonalRating, e::getPersonalRating, replaceMode); handleFieldUpdate(e.getAmazonRatingLocked(), clear.isAmazonRating(), m.getAmazonRating(), e::setAmazonRating, e::getAmazonRating, replaceMode); handleFieldUpdate(e.getAmazonReviewCountLocked(), clear.isAmazonReviewCount(), m.getAmazonReviewCount(), e::setAmazonReviewCount, e::getAmazonReviewCount, replaceMode); handleFieldUpdate(e.getGoodreadsRatingLocked(), clear.isGoodreadsRating(), m.getGoodreadsRating(), e::setGoodreadsRating, e::getGoodreadsRating, replaceMode); @@ -379,7 +378,6 @@ public class BookMetadataUpdater { Pair.of(m.getGoogleIdLocked(), e::setGoogleIdLocked), Pair.of(m.getPageCountLocked(), e::setPageCountLocked), Pair.of(m.getLanguageLocked(), e::setLanguageLocked), - Pair.of(m.getPersonalRatingLocked(), e::setPersonalRatingLocked), Pair.of(m.getAmazonRatingLocked(), e::setAmazonRatingLocked), Pair.of(m.getAmazonReviewCountLocked(), e::setAmazonReviewCountLocked), Pair.of(m.getGoodreadsRatingLocked(), e::setGoodreadsRatingLocked), diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/extractor/EpubMetadataExtractor.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/extractor/EpubMetadataExtractor.java index f06bd12a..01a7da24 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/extractor/EpubMetadataExtractor.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/extractor/EpubMetadataExtractor.java @@ -165,10 +165,6 @@ public class EpubMetadataExtractor implements FileMetadataExtractor { } } - if ("calibre:rating".equals(name) || "booklore:personal_rating".equals(prop)) { - safeParseDouble(content, builderMeta::personalRating); - } - switch (prop) { case "booklore:asin" -> builderMeta.asin(content); case "booklore:goodreads_id" -> builderMeta.goodreadsId(content); diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/extractor/PdfMetadataExtractor.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/extractor/PdfMetadataExtractor.java index 82f3d5e5..0351922c 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/extractor/PdfMetadataExtractor.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/extractor/PdfMetadataExtractor.java @@ -234,11 +234,6 @@ public class PdfMetadataExtractor implements FileMetadataExtractor { private void extractCalibreMetadata(XPath xpath, Document doc, BookMetadata.BookMetadataBuilder builder) { try { - String rating = xpath.evaluate("//calibre:rating/text()", doc); - if (StringUtils.isNotBlank(rating)) { - builder.personalRating(Double.valueOf(rating)); - } - String series = xpath.evaluate("//calibre:series/rdf:value/text()", doc); if (StringUtils.isNotBlank(series)) { builder.seriesName(series); diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/writer/EpubMetadataWriter.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/writer/EpubMetadataWriter.java index 6ea7e134..ef5995c7 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/writer/EpubMetadataWriter.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/writer/EpubMetadataWriter.java @@ -114,11 +114,6 @@ public class EpubMetadataWriter implements MetadataWriter { replaceMetaElement(metadataElement, opfDoc, "calibre:series_index", formatted, hasChanges); }); - helper.copyPersonalRating(clear != null && clear.isPersonalRating(), val -> { - String formatted = val != null ? String.format("%.1f", val) : null; - replaceMetaElement(metadataElement, opfDoc, "calibre:rating", formatted, hasChanges); - }); - List schemes = List.of("AMAZON", "GOOGLE", "GOODREADS", "HARDCOVER", "ISBN"); for (String scheme : schemes) { diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/writer/MetadataCopyHelper.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/writer/MetadataCopyHelper.java index 468861f9..f896e8d9 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/writer/MetadataCopyHelper.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/writer/MetadataCopyHelper.java @@ -112,13 +112,6 @@ public class MetadataCopyHelper { } } - public void copyPersonalRating(boolean clear, Consumer consumer) { - if (!isLocked(metadata.getPersonalRatingLocked())) { - if (clear) consumer.accept(null); - else if (metadata.getPersonalRating() != null) consumer.accept(metadata.getPersonalRating()); - } - } - public void copyGoodreadsId(boolean clear, Consumer consumer) { if (!isLocked(metadata.getGoodreadsIdLocked())) { if (clear) consumer.accept(null); diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/writer/PdfMetadataWriter.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/writer/PdfMetadataWriter.java index f415bbd2..fc01e221 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/writer/PdfMetadataWriter.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/writer/PdfMetadataWriter.java @@ -200,11 +200,6 @@ public class PdfMetadataWriter implements MetadataWriter { calibreDescription.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:calibreSI", "http://calibre-ebook.com/xmp-namespace-series-index"); calibreDescription.setAttributeNS("http://www.w3.org/1999/02/22-rdf-syntax-ns#", "rdf:about", ""); - helper.copyPersonalRating(clear != null && clear.isPersonalRating(), rating -> { - String value = (rating != null) ? String.valueOf((int) Math.round(rating)) : ""; - calibreDescription.appendChild(createSimpleElement(doc, "calibre:rating", value)); - }); - helper.copySeriesName(clear != null && clear.isSeriesName(), series -> { Element seriesElem = doc.createElementNS("http://calibre-ebook.com/xmp-namespace", "calibre:series"); seriesElem.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:calibreSI", "http://calibre-ebook.com/xmp-namespace-series-index"); 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 be75dd67..870cf5ce 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 @@ -38,7 +38,6 @@ public class MetadataChangeDetector { compare(changes, "googleId", clear.isGoogleId(), newMeta.getGoogleId(), existingMeta.getGoogleId(), () -> !isTrue(existingMeta.getGoogleIdLocked()), newMeta.getGoogleIdLocked(), existingMeta.getGoogleIdLocked()); compare(changes, "pageCount", clear.isPageCount(), newMeta.getPageCount(), existingMeta.getPageCount(), () -> !isTrue(existingMeta.getPageCountLocked()), newMeta.getPageCountLocked(), existingMeta.getPageCountLocked()); compare(changes, "language", clear.isLanguage(), newMeta.getLanguage(), existingMeta.getLanguage(), () -> !isTrue(existingMeta.getLanguageLocked()), newMeta.getLanguageLocked(), existingMeta.getLanguageLocked()); - compare(changes, "personalRating", clear.isPersonalRating(), newMeta.getPersonalRating(), existingMeta.getPersonalRating(), () -> !isTrue(existingMeta.getPersonalRatingLocked()), newMeta.getPersonalRatingLocked(), existingMeta.getPersonalRatingLocked()); compare(changes, "amazonRating", clear.isAmazonRating(), newMeta.getAmazonRating(), existingMeta.getAmazonRating(), () -> !isTrue(existingMeta.getAmazonRatingLocked()), newMeta.getAmazonRatingLocked(), existingMeta.getAmazonRatingLocked()); compare(changes, "amazonReviewCount", clear.isAmazonReviewCount(), newMeta.getAmazonReviewCount(), existingMeta.getAmazonReviewCount(), () -> !isTrue(existingMeta.getAmazonReviewCountLocked()), newMeta.getAmazonReviewCountLocked(), existingMeta.getAmazonReviewCountLocked()); compare(changes, "goodreadsRating", clear.isGoodreadsRating(), newMeta.getGoodreadsRating(), existingMeta.getGoodreadsRating(), () -> !isTrue(existingMeta.getGoodreadsRatingLocked()), newMeta.getGoodreadsRatingLocked(), existingMeta.getGoodreadsRatingLocked()); @@ -79,7 +78,6 @@ public class MetadataChangeDetector { compareValue(diffs, "googleId", clear.isGoogleId(), newMeta.getGoogleId(), existingMeta.getGoogleId(), () -> !isTrue(existingMeta.getGoogleIdLocked())); compareValue(diffs, "pageCount", clear.isPageCount(), newMeta.getPageCount(), existingMeta.getPageCount(), () -> !isTrue(existingMeta.getPageCountLocked())); compareValue(diffs, "language", clear.isLanguage(), newMeta.getLanguage(), existingMeta.getLanguage(), () -> !isTrue(existingMeta.getLanguageLocked())); - compareValue(diffs, "personalRating", clear.isPersonalRating(), newMeta.getPersonalRating(), existingMeta.getPersonalRating(), () -> !isTrue(existingMeta.getPersonalRatingLocked())); compareValue(diffs, "amazonRating", clear.isAmazonRating(), newMeta.getAmazonRating(), existingMeta.getAmazonRating(), () -> !isTrue(existingMeta.getAmazonRatingLocked())); compareValue(diffs, "amazonReviewCount", clear.isAmazonReviewCount(), newMeta.getAmazonReviewCount(), existingMeta.getAmazonReviewCount(), () -> !isTrue(existingMeta.getAmazonReviewCountLocked())); compareValue(diffs, "goodreadsRating", clear.isGoodreadsRating(), newMeta.getGoodreadsRating(), existingMeta.getGoodreadsRating(), () -> !isTrue(existingMeta.getGoodreadsRatingLocked())); @@ -111,7 +109,6 @@ public class MetadataChangeDetector { compareValue(diffs, "hardcoverId", clear.isHardcoverId(), newMeta.getHardcoverId(), existingMeta.getHardcoverId(), () -> !isTrue(existingMeta.getHardcoverIdLocked())); compareValue(diffs, "googleId", clear.isGoogleId(), newMeta.getGoogleId(), existingMeta.getGoogleId(), () -> !isTrue(existingMeta.getGoogleIdLocked())); compareValue(diffs, "language", clear.isLanguage(), newMeta.getLanguage(), existingMeta.getLanguage(), () -> !isTrue(existingMeta.getLanguageLocked())); - compareValue(diffs, "personalRating", clear.isPersonalRating(), newMeta.getPersonalRating(), existingMeta.getPersonalRating(), () -> !isTrue(existingMeta.getPersonalRatingLocked())); 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())); return !diffs.isEmpty(); diff --git a/booklore-api/src/main/resources/db/migration/V70__Add_user_rating_column.sql b/booklore-api/src/main/resources/db/migration/V70__Add_user_rating_column.sql new file mode 100644 index 00000000..3791f226 --- /dev/null +++ b/booklore-api/src/main/resources/db/migration/V70__Add_user_rating_column.sql @@ -0,0 +1,11 @@ +ALTER TABLE user_book_progress ADD COLUMN IF NOT EXISTS personal_rating TINYINT; + +-- Copies existing personal ratings to all users with progress records for matching books +UPDATE user_book_progress ubp +JOIN book_metadata bm ON ubp.book_id = bm.book_id +SET ubp.personal_rating = bm.personal_rating +WHERE bm.personal_rating IS NOT NULL; + +-- Drops obsolete columns +ALTER TABLE book_metadata DROP COLUMN personal_rating; +ALTER TABLE book_metadata DROP COLUMN personal_rating_locked; \ No newline at end of file diff --git a/booklore-api/src/test/java/com/adityachandel/booklore/util/MetadataChangeDetectorTest.java b/booklore-api/src/test/java/com/adityachandel/booklore/util/MetadataChangeDetectorTest.java index 8c337c3b..d699aba9 100644 --- a/booklore-api/src/test/java/com/adityachandel/booklore/util/MetadataChangeDetectorTest.java +++ b/booklore-api/src/test/java/com/adityachandel/booklore/util/MetadataChangeDetectorTest.java @@ -48,7 +48,6 @@ public class MetadataChangeDetectorTest { .googleId("google123") .pageCount(300) .language("en") - .personalRating(4.5) .amazonRating(4.2) .amazonReviewCount(1500) .goodreadsRating(4.1) @@ -72,7 +71,6 @@ public class MetadataChangeDetectorTest { .googleIdLocked(false) .pageCountLocked(false) .languageLocked(false) - .personalRatingLocked(false) .amazonRatingLocked(false) .amazonReviewCountLocked(false) .goodreadsRatingLocked(false) @@ -121,7 +119,6 @@ public class MetadataChangeDetectorTest { .googleId("google123") .pageCount(300) .language("en") - .personalRating(4.5) .amazonRating(4.2) .amazonReviewCount(1500) .goodreadsRating(4.1) @@ -145,7 +142,6 @@ public class MetadataChangeDetectorTest { .googleIdLocked(false) .pageCountLocked(false) .languageLocked(false) - .personalRatingLocked(false) .amazonRatingLocked(false) .amazonReviewCountLocked(false) .goodreadsRatingLocked(false) @@ -200,7 +196,6 @@ public class MetadataChangeDetectorTest { Arguments.of("googleId", (Consumer) m -> m.setGoogleId("google456")), Arguments.of("pageCount", (Consumer) m -> m.setPageCount(350)), Arguments.of("language", (Consumer) m -> m.setLanguage("fr")), - Arguments.of("personalRating", (Consumer) m -> m.setPersonalRating(4.8)), Arguments.of("amazonRating", (Consumer) m -> m.setAmazonRating(4.5)), Arguments.of("amazonReviewCount", (Consumer) m -> m.setAmazonReviewCount(2000)), Arguments.of("goodreadsRating", (Consumer) m -> m.setGoodreadsRating(4.3)), diff --git a/booklore-ui/src/app/features/book/components/book-browser/book-filter/book-filter.component.ts b/booklore-ui/src/app/features/book/components/book-browser/book-filter/book-filter.component.ts index 65743fe4..11185363 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/book-filter/book-filter.component.ts +++ b/booklore-ui/src/app/features/book/components/book-browser/book-filter/book-filter.component.ts @@ -239,7 +239,7 @@ export class BookFilterComponent implements OnInit, OnDestroy { } return [{id: status, name: getReadStatusName(status)}]; }, 'id', 'name'), - personalRating: this.getFilterStream((book: Book) => getRatingRangeFilters10(book.metadata?.personalRating!), 'id', 'name', 'sortIndex'), + personalRating: this.getFilterStream((book: Book) => getRatingRangeFilters10(book.personalRating!), 'id', 'name', 'sortIndex'), publishedDate: this.getFilterStream(extractPublishedYearFilter, 'id', 'name'), matchScore: this.getFilterStream((book: Book) => getMatchScoreRangeFilters(book.metadataMatchScore), 'id', 'name', 'sortIndex'), mood: this.getFilterStream( diff --git a/booklore-ui/src/app/features/book/components/book-browser/filters/SidebarFilter.ts b/booklore-ui/src/app/features/book/components/book-browser/filters/SidebarFilter.ts index f2288a7e..df4706e3 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/filters/SidebarFilter.ts +++ b/booklore-ui/src/app/features/book/components/book-browser/filters/SidebarFilter.ts @@ -93,7 +93,7 @@ export class SideBarFilter implements BookFilter { case 'hardcoverRating': return filterValues.some(range => isRatingInRange(book.metadata?.hardcoverRating, range)); case 'personalRating': - return filterValues.some(range => isRatingInRange10(book.metadata?.personalRating, range)); + return filterValues.some(range => isRatingInRange10(book.personalRating, range)); case 'publishedDate': const bookYear = book.metadata?.publishedDate ? new Date(book.metadata.publishedDate).getFullYear() diff --git a/booklore-ui/src/app/features/book/components/book-browser/sorting/BookSorter.ts b/booklore-ui/src/app/features/book/components/book-browser/sorting/BookSorter.ts index b0f71130..993de739 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/sorting/BookSorter.ts +++ b/booklore-ui/src/app/features/book/components/book-browser/sorting/BookSorter.ts @@ -10,6 +10,7 @@ export class BookSorter { { label: 'Author', field: 'author', direction: SortDirection.ASCENDING }, { label: 'Author + Series', field: 'authorSeries', direction: SortDirection.ASCENDING }, { label: 'Last Read', field: 'lastReadTime', direction: SortDirection.ASCENDING }, + { label: 'Personal Rating', field: 'personalRating', direction: SortDirection.ASCENDING }, { label: 'Added On', field: 'addedOn', direction: SortDirection.ASCENDING }, { label: 'File Size', field: 'fileSizeKb', direction: SortDirection.ASCENDING }, { label: 'Locked', field: 'locked', direction: SortDirection.ASCENDING }, 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 4d425e56..0c02eea4 100644 --- a/booklore-ui/src/app/features/book/model/book.model.ts +++ b/booklore-ui/src/app/features/book/model/book.model.ts @@ -41,6 +41,7 @@ export interface Book extends FileInfo { koboProgress?: KoboProgress; seriesCount?: number | null; metadataMatchScore?: number | null; + personalRating?: number | null; readStatus?: ReadStatus; dateFinished?: string; libraryPath?: { id: number }; @@ -98,7 +99,6 @@ export interface BookMetadata { goodreadsReviewCount?: number | null; hardcoverRating?: number | null; hardcoverReviewCount?: number | null; - personalRating?: number | null; coverUpdatedOn?: string; authors?: string[]; categories?: string[]; @@ -125,7 +125,6 @@ export interface BookMetadata { googleIdLocked?: boolean; pageCountLocked?: boolean; languageLocked?: boolean; - personalRatingLocked?: boolean; amazonRatingLocked?: boolean; amazonReviewCountLocked?: boolean; goodreadsRatingLocked?: boolean; @@ -166,7 +165,6 @@ export interface MetadataClearFlags { goodreadsReviewCount?: boolean; hardcoverRating?: boolean; hardcoverReviewCount?: boolean; - personalRating?: boolean; authors?: boolean; categories?: boolean; moods?: boolean; diff --git a/booklore-ui/src/app/features/book/service/book.service.ts b/booklore-ui/src/app/features/book/service/book.service.ts index 844074f9..bd4c4b70 100644 --- a/booklore-ui/src/app/features/book/service/book.service.ts +++ b/booklore-ui/src/app/features/book/service/book.service.ts @@ -531,6 +531,22 @@ export class BookService { ); } + resetPersonalRating(bookIds: number | number[]): Observable { + const ids = Array.isArray(bookIds) ? bookIds : [bookIds]; + return this.http.post(`${this.url}/reset-personal-rating`, ids).pipe( + tap(updatedBooks => updatedBooks.forEach(book => this.handleBookUpdate(book))) + ); + } + + updatePersonalRating(bookIds: number | number[], rating: number): Observable { + const ids = Array.isArray(bookIds) ? bookIds : [bookIds]; + return this.http.put(`${this.url}/personal-rating`, {ids, rating}).pipe( + tap(updatedBooks => { + updatedBooks.forEach(updatedBook => this.handleBookUpdate(updatedBook)); + }) + ); + } + consolidateMetadata(metadataType: 'authors' | 'categories' | 'moods' | 'tags' | 'series' | 'publishers' | 'languages', targetValues: string[], valuesToMerge: string[]): Observable { const payload = {metadataType, targetValues, valuesToMerge}; return this.http.post(`${this.url}/metadata/manage/consolidate`, payload).pipe( diff --git a/booklore-ui/src/app/features/book/service/sort.service.ts b/booklore-ui/src/app/features/book/service/sort.service.ts index 142de754..f2e9bce7 100644 --- a/booklore-ui/src/app/features/book/service/sort.service.ts +++ b/booklore-ui/src/app/features/book/service/sort.service.ts @@ -79,7 +79,7 @@ export class SortService { publisher: (book) => book.metadata?.publisher || null, pageCount: (book) => book.metadata?.pageCount || null, rating: (book) => book.metadata?.rating || null, - personalRating: (book) => book.metadata?.personalRating || null, + personalRating: (book) => book.personalRating || null, reviewCount: (book) => book.metadata?.reviewCount || null, amazonRating: (book) => book.metadata?.amazonRating || null, amazonReviewCount: (book) => book.metadata?.amazonReviewCount || null, diff --git a/booklore-ui/src/app/features/magic-shelf/service/book-rule-evaluator.service.ts b/booklore-ui/src/app/features/magic-shelf/service/book-rule-evaluator.service.ts index d5588266..6e0fd3ab 100644 --- a/booklore-ui/src/app/features/magic-shelf/service/book-rule-evaluator.service.ts +++ b/booklore-ui/src/app/features/magic-shelf/service/book-rule-evaluator.service.ts @@ -201,7 +201,7 @@ export class BookRuleEvaluatorService { case 'metadataScore': return book.metadataMatchScore; case 'personalRating': - return book.metadata?.personalRating; + return book.personalRating; case 'title': return book.metadata?.title?.toLowerCase() ?? null; case 'subtitle': 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 786d86b2..6c806316 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 @@ -146,7 +146,6 @@ export class MetadataEditorComponent implements OnInit { pageCount: new FormControl(""), language: new FormControl(""), asin: new FormControl(""), - personalRating: new FormControl(""), amazonRating: new FormControl(""), amazonReviewCount: new FormControl(""), goodreadsId: new FormControl(""), @@ -176,7 +175,6 @@ export class MetadataEditorComponent implements OnInit { pageCountLocked: new FormControl(false), languageLocked: new FormControl(false), asinLocked: new FormControl(false), - personalRatingLocked: new FormControl(false), amazonRatingLocked: new FormControl(false), amazonReviewCountLocked: new FormControl(false), goodreadsIdLocked: new FormControl(""), @@ -267,7 +265,6 @@ export class MetadataEditorComponent implements OnInit { rating: metadata.rating ?? null, reviewCount: metadata.reviewCount ?? null, asin: metadata.asin ?? null, - personalRating: metadata.personalRating ?? null, amazonRating: metadata.amazonRating ?? null, amazonReviewCount: metadata.amazonReviewCount ?? null, goodreadsId: metadata.goodreadsId ?? null, @@ -295,7 +292,6 @@ export class MetadataEditorComponent implements OnInit { pageCountLocked: metadata.pageCountLocked ?? false, languageLocked: metadata.languageLocked ?? false, asinLocked: metadata.asinLocked ?? false, - personalRatingLocked: metadata.personalRatingLocked ?? false, amazonRatingLocked: metadata.amazonRatingLocked ?? false, amazonReviewCountLocked: metadata.amazonReviewCountLocked ?? false, goodreadsIdLocked: metadata.goodreadsIdLocked ?? false, @@ -328,7 +324,6 @@ export class MetadataEditorComponent implements OnInit { {key: "asinLocked", control: "asin"}, {key: "amazonReviewCountLocked", control: "amazonReviewCount"}, {key: "amazonRatingLocked", control: "amazonRating"}, - {key: "personalRatingLocked", control: "personalRating"}, {key: "goodreadsIdLocked", control: "goodreadsId"}, {key: "comicvineIdLocked", control: "comicvineId"}, {key: "goodreadsReviewCountLocked", control: "goodreadsReviewCount"}, @@ -465,7 +460,6 @@ export class MetadataEditorComponent implements OnInit { rating: form.get("rating")?.value, reviewCount: form.get("reviewCount")?.value, asin: form.get("asin")?.value, - personalRating: form.get("personalRating")?.value, amazonRating: form.get("amazonRating")?.value, amazonReviewCount: form.get("amazonReviewCount")?.value, goodreadsId: form.get("goodreadsId")?.value, @@ -498,7 +492,6 @@ export class MetadataEditorComponent implements OnInit { languageLocked: form.get("languageLocked")?.value, asinLocked: form.get("asinLocked")?.value, amazonRatingLocked: form.get("amazonRatingLocked")?.value, - personalRatingLocked: form.get("personalRatingLocked")?.value, amazonReviewCountLocked: form.get("amazonReviewCountLocked")?.value, goodreadsIdLocked: form.get("goodreadsIdLocked")?.value, comicvineIdLocked: form.get("comicvineIdLocked")?.value, @@ -546,7 +539,6 @@ export class MetadataEditorComponent implements OnInit { pageCount: wasCleared("pageCount"), language: wasCleared("language"), asin: wasCleared("asin"), - personalRating: wasCleared("personalRating"), amazonRating: wasCleared("amazonRating"), amazonReviewCount: wasCleared("amazonReviewCount"), goodreadsId: wasCleared("goodreadsId"), 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 d1b64b1c..509f56c0 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 @@ -171,7 +171,6 @@ export class MetadataPickerComponent implements OnInit { pageCountLocked: new FormControl(false), languageLocked: new FormControl(false), asinLocked: new FormControl(false), - personalRatingLocked: new FormControl(false), amazonRatingLocked: new FormControl(false), amazonReviewCountLocked: new FormControl(false), goodreadsIdLocked: new FormControl(false), @@ -277,7 +276,6 @@ export class MetadataPickerComponent implements OnInit { pageCountLocked: metadata.pageCountLocked || false, languageLocked: metadata.languageLocked || false, asinLocked: metadata.asinLocked || false, - personalRatingLocked: metadata.personalRatingLocked || false, amazonRatingLocked: metadata.amazonRatingLocked || false, amazonReviewCountLocked: metadata.amazonReviewCountLocked || false, goodreadsIdLocked: metadata.goodreadsIdLocked || false, @@ -313,7 +311,6 @@ export class MetadataPickerComponent implements OnInit { if (metadata.isbn10Locked) this.metadataForm.get('isbn10')?.disable({emitEvent: false}); if (metadata.isbn13Locked) this.metadataForm.get('isbn13')?.disable({emitEvent: false}); if (metadata.asinLocked) this.metadataForm.get('asin')?.disable({emitEvent: false}); - if (metadata.personalRatingLocked) this.metadataForm.get('personalRating')?.disable({emitEvent: false}); if (metadata.amazonReviewCountLocked) this.metadataForm.get('amazonReviewCount')?.disable({emitEvent: false}); if (metadata.amazonRatingLocked) this.metadataForm.get('amazonRating')?.disable({emitEvent: false}); if (metadata.googleIdLocked) this.metadataForm.get('googleIdCount')?.disable({emitEvent: false}); @@ -393,7 +390,6 @@ export class MetadataPickerComponent implements OnInit { pageCount: this.metadataForm.get('pageCount')?.value || this.copiedFields['pageCount'] ? this.getPageCountOrCopied() : null, language: this.metadataForm.get('language')?.value || this.copiedFields['language'] ? this.getValueOrCopied('language') : '', asin: this.metadataForm.get('asin')?.value || this.copiedFields['asin'] ? this.getValueOrCopied('asin') : '', - personalRating: this.metadataForm.get('personalRating')?.value || this.copiedFields['personalRating'] ? this.getNumberOrCopied('personalRating') : null, amazonRating: this.metadataForm.get('amazonRating')?.value || this.copiedFields['amazonRating'] ? this.getNumberOrCopied('amazonRating') : null, amazonReviewCount: this.metadataForm.get('amazonReviewCount')?.value || this.copiedFields['amazonReviewCount'] ? this.getNumberOrCopied('amazonReviewCount') : null, goodreadsId: this.metadataForm.get('goodreadsId')?.value || this.copiedFields['goodreadsId'] ? this.getValueOrCopied('goodreadsId') : '', @@ -423,7 +419,6 @@ export class MetadataPickerComponent implements OnInit { pageCountLocked: this.metadataForm.get('pageCountLocked')?.value, languageLocked: this.metadataForm.get('languageLocked')?.value, asinLocked: this.metadataForm.get('asinLocked')?.value, - personalRatingLocked: this.metadataForm.get('personalRatingLocked')?.value, amazonRatingLocked: this.metadataForm.get('amazonRatingLocked')?.value, amazonReviewCountLocked: this.metadataForm.get('amazonReviewCountLocked')?.value, goodreadsIdLocked: this.metadataForm.get('goodreadsIdLocked')?.value, @@ -480,7 +475,6 @@ export class MetadataPickerComponent implements OnInit { seriesNumber: current.seriesNumber === null && original.seriesNumber !== null, seriesTotal: current.seriesTotal === null && original.seriesTotal !== null, cover: !current.thumbnailUrl && !!original.thumbnailUrl, - personalRating: current.personalRating === null && original.personalRating !== null, }; } 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 693432da..4c9907fc 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 @@ -93,10 +93,10 @@
+ [style.--p-rating-icon-active-color]="getStarColorScaled(book?.personalRating, 10)">
{ this.messageService.add({ severity: 'success', @@ -527,12 +522,7 @@ export class MetadataViewerComponent implements OnInit, OnChanges { } resetPersonalRating(book: Book): void { - if (!book?.metadata) return; - const updatedMetadata = {...book.metadata, personalRating: null}; - this.bookService.updateBookMetadata(book.id, { - metadata: updatedMetadata, - clearFlags: {personalRating: true} - }, false).subscribe({ + this.bookService.resetPersonalRating(book.id).subscribe({ next: () => { this.messageService.add({ severity: 'info', diff --git a/booklore-ui/src/app/features/stats/service/author-popularity-chart.service.ts b/booklore-ui/src/app/features/stats/service/author-popularity-chart.service.ts index eb44fbce..51d6c00d 100644 --- a/booklore-ui/src/app/features/stats/service/author-popularity-chart.service.ts +++ b/booklore-ui/src/app/features/stats/service/author-popularity-chart.service.ts @@ -271,7 +271,7 @@ export class AuthorPopularityChartService implements OnDestroy { if (book.metadata?.goodreadsRating) ratings.push(book.metadata.goodreadsRating); if (book.metadata?.amazonRating) ratings.push(book.metadata.amazonRating); if (book.metadata?.hardcoverRating) ratings.push(book.metadata.hardcoverRating); - if (book.metadata?.personalRating) ratings.push(book.metadata.personalRating); + if (book.personalRating) ratings.push(book.personalRating); if (ratings.length > 0) { const avgRating = ratings.reduce((sum, rating) => sum + rating, 0) / ratings.length; diff --git a/booklore-ui/src/app/features/stats/service/personal-rating-chart.service.ts b/booklore-ui/src/app/features/stats/service/personal-rating-chart.service.ts index 47a93ed1..8ab03364 100644 --- a/booklore-ui/src/app/features/stats/service/personal-rating-chart.service.ts +++ b/booklore-ui/src/app/features/stats/service/personal-rating-chart.service.ts @@ -208,7 +208,7 @@ export class PersonalRatingChartService implements OnDestroy { RATING_RANGES.forEach(range => rangeCounts.set(range.range, {count: 0, totalRating: 0})); books.forEach(book => { - const personalRating = book.metadata?.personalRating; + const personalRating = book.personalRating; if (!personalRating || personalRating === 0) { const noRatingData = rangeCounts.get('No Rating')!; diff --git a/booklore-ui/src/app/features/stats/service/reading-dna-chart.service.ts b/booklore-ui/src/app/features/stats/service/reading-dna-chart.service.ts index 65bf9924..3bb51bf4 100644 --- a/booklore-ui/src/app/features/stats/service/reading-dna-chart.service.ts +++ b/booklore-ui/src/app/features/stats/service/reading-dna-chart.service.ts @@ -296,7 +296,7 @@ export class ReadingDNAChartService implements OnDestroy { if (!metadata) return false; return (metadata.goodreadsRating && metadata.goodreadsRating >= 4.0) || (metadata.amazonRating && metadata.amazonRating >= 4.0) || - (metadata.personalRating && metadata.personalRating >= 4); + (book.personalRating && book.personalRating >= 4); }); const qualityRate = qualityBooks.length / books.length; @@ -366,7 +366,7 @@ export class ReadingDNAChartService implements OnDestroy { }); // Personal rating engagement shows emotional connection - const personallyRatedBooks = books.filter(book => book.metadata?.personalRating); + const personallyRatedBooks = books.filter(book => book.personalRating); const emotionalRate = emotionalBooks.length / books.length; const ratingEngagement = personallyRatedBooks.length / books.length; diff --git a/booklore-ui/src/app/features/stats/service/reading-habits-chart.service.ts b/booklore-ui/src/app/features/stats/service/reading-habits-chart.service.ts index f451ea7e..fcc98080 100644 --- a/booklore-ui/src/app/features/stats/service/reading-habits-chart.service.ts +++ b/booklore-ui/src/app/features/stats/service/reading-habits-chart.service.ts @@ -394,7 +394,7 @@ export class ReadingHabitsChartService implements OnDestroy { const metadataScore = (wellOrganizedBooks.length / books.length) * 35; // Max 35 // Personal ratings suggest systematic tracking - const ratedBooks = books.filter(book => book.metadata?.personalRating); + const ratedBooks = books.filter(book => book.personalRating); const ratingScore = (ratedBooks.length / books.length) * 25; // Max 25 return Math.min(100, seriesScore + metadataScore + ratingScore); diff --git a/booklore-ui/src/app/features/stats/service/reading-velocity-chart.service.ts b/booklore-ui/src/app/features/stats/service/reading-velocity-chart.service.ts index 49ceb7bb..09f842a7 100644 --- a/booklore-ui/src/app/features/stats/service/reading-velocity-chart.service.ts +++ b/booklore-ui/src/app/features/stats/service/reading-velocity-chart.service.ts @@ -271,7 +271,7 @@ export class ReadingVelocityChartService implements OnDestroy { const goodreadsRating = metadata.goodreadsRating || 0; const amazonRating = metadata.amazonRating || 0; - const personalRating = metadata.personalRating || 0; + const personalRating = book.personalRating || 0; return goodreadsRating >= 4.0 || amazonRating >= 4.0 || personalRating >= 4; } @@ -290,14 +290,14 @@ export class ReadingVelocityChartService implements OnDestroy { private calculateAverageRating(books: Book[]): number { const ratingsBooks = books.filter(book => { const metadata = book.metadata; - return metadata && (metadata.goodreadsRating || metadata.amazonRating || metadata.personalRating); + return metadata && (metadata.goodreadsRating || metadata.amazonRating || book.personalRating); }); if (ratingsBooks.length === 0) return 0; const totalRating = ratingsBooks.reduce((sum, book) => { const metadata = book.metadata!; - const rating = metadata.goodreadsRating || metadata.amazonRating || metadata.personalRating || 0; + const rating = metadata.goodreadsRating || metadata.amazonRating || book.personalRating || 0; return sum + rating; }, 0); diff --git a/booklore-ui/src/app/features/stats/service/reading-velocity-timeline-chart.service.ts b/booklore-ui/src/app/features/stats/service/reading-velocity-timeline-chart.service.ts index 2db476d6..681ea486 100644 --- a/booklore-ui/src/app/features/stats/service/reading-velocity-timeline-chart.service.ts +++ b/booklore-ui/src/app/features/stats/service/reading-velocity-timeline-chart.service.ts @@ -318,9 +318,9 @@ export class ReadingVelocityTimelineChartService implements OnDestroy { const averagePages = books.length > 0 ? Math.round(totalPages / books.length) : 0; // Calculate average rating - const ratedBooks = books.filter(book => book.metadata?.personalRating || book.metadata?.goodreadsRating); + const ratedBooks = books.filter(book => book.personalRating || book.metadata?.goodreadsRating); const totalRating = ratedBooks.reduce((sum, book) => { - const rating = book.metadata?.personalRating || book.metadata?.goodreadsRating || 0; + const rating = book.personalRating || book.metadata?.goodreadsRating || 0; return sum + rating; }, 0); const averageRating = ratedBooks.length > 0 ? Number((totalRating / ratedBooks.length).toFixed(1)) : 0; diff --git a/booklore-ui/src/app/features/stats/service/series-completion-progress-chart.service.ts b/booklore-ui/src/app/features/stats/service/series-completion-progress-chart.service.ts index 1aa8dff6..dd4e5279 100644 --- a/booklore-ui/src/app/features/stats/service/series-completion-progress-chart.service.ts +++ b/booklore-ui/src/app/features/stats/service/series-completion-progress-chart.service.ts @@ -275,13 +275,13 @@ export class SeriesCompletionProgressChartService implements OnDestroy { // Calculate average rating const ratedBooks = books.filter(book => { - const rating = book.metadata?.personalRating || book.metadata?.goodreadsRating; + const rating = book.personalRating || book.metadata?.goodreadsRating; return rating && rating > 0; }); const averageRating = ratedBooks.length > 0 ? Number((ratedBooks.reduce((sum, book) => { - const rating = book.metadata?.personalRating || book.metadata?.goodreadsRating || 0; + const rating = book.personalRating || book.metadata?.goodreadsRating || 0; return sum + rating; }, 0) / ratedBooks.length).toFixed(1)) : 0;