mirror of
https://github.com/booklore-app/booklore.git
synced 2025-12-23 22:28:11 -05:00
Per user personal ratings (#1820)
* Change personal ratings to be saved per user * Only copy ratings to users with existing progress records
This commit is contained in:
@@ -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<Book> 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<List<Book>> updatePersonalRating(
|
||||
@Parameter(description = "Personal rating update request") @RequestBody @Valid PersonalRatingUpdateRequest request) {
|
||||
List<Book> 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<List<Book>> resetPersonalRating(
|
||||
@Parameter(description = "List of book IDs to reset personal rating for") @RequestBody List<Long> bookIds) {
|
||||
if (bookIds == null || bookIds.isEmpty()) {
|
||||
throw ApiError.GENERIC_BAD_REQUEST.createException("No book IDs provided");
|
||||
}
|
||||
List<Book> updatedBooks = bookService.resetPersonalRating(bookIds);
|
||||
return ResponseEntity.ok(updatedBooks);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -32,6 +32,7 @@ public class Book {
|
||||
private CbxProgress cbxProgress;
|
||||
private KoProgress koreaderProgress;
|
||||
private KoboProgress koboProgress;
|
||||
private Integer personalRating;
|
||||
private Set<Shelf> shelves;
|
||||
private String readStatus;
|
||||
private Instant dateFinished;
|
||||
|
||||
@@ -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<String> 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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.adityachandel.booklore.model.dto.request;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record PersonalRatingUpdateRequest(List<Long> ids, Integer rating) {
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -93,4 +93,7 @@ public class UserBookProgressEntity {
|
||||
|
||||
@Column(name = "read_status_modified_time")
|
||||
private Instant readStatusModifiedTime;
|
||||
|
||||
@Column(name = "personal_rating")
|
||||
private Integer personalRating;
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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<Book> updatePersonalRating(List<Long> bookIds, Integer rating) {
|
||||
BookLoreUser user = authenticationService.getAuthenticatedUser();
|
||||
|
||||
List<BookEntity> 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<Book> resetPersonalRating(List<Long> bookIds) {
|
||||
BookLoreUser user = authenticationService.getAuthenticatedUser();
|
||||
List<Book> updatedBooks = new ArrayList<>();
|
||||
Optional<BookLoreUserEntity> 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<Book> assignShelvesToBooks(Set<Long> bookIds, Set<Long> shelfIdsToAssign, Set<Long> shelfIdsToUnassign) {
|
||||
BookLoreUser user = authenticationService.getAuthenticatedUser();
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<String> schemes = List.of("AMAZON", "GOOGLE", "GOODREADS", "HARDCOVER", "ISBN");
|
||||
|
||||
for (String scheme : schemes) {
|
||||
|
||||
@@ -112,13 +112,6 @@ public class MetadataCopyHelper {
|
||||
}
|
||||
}
|
||||
|
||||
public void copyPersonalRating(boolean clear, Consumer<Double> 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<String> consumer) {
|
||||
if (!isLocked(metadata.getGoodreadsIdLocked())) {
|
||||
if (clear) consumer.accept(null);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
@@ -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<BookMetadata>) m -> m.setGoogleId("google456")),
|
||||
Arguments.of("pageCount", (Consumer<BookMetadata>) m -> m.setPageCount(350)),
|
||||
Arguments.of("language", (Consumer<BookMetadata>) m -> m.setLanguage("fr")),
|
||||
Arguments.of("personalRating", (Consumer<BookMetadata>) m -> m.setPersonalRating(4.8)),
|
||||
Arguments.of("amazonRating", (Consumer<BookMetadata>) m -> m.setAmazonRating(4.5)),
|
||||
Arguments.of("amazonReviewCount", (Consumer<BookMetadata>) m -> m.setAmazonReviewCount(2000)),
|
||||
Arguments.of("goodreadsRating", (Consumer<BookMetadata>) m -> m.setGoodreadsRating(4.3)),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -531,6 +531,22 @@ export class BookService {
|
||||
);
|
||||
}
|
||||
|
||||
resetPersonalRating(bookIds: number | number[]): Observable<Book[]> {
|
||||
const ids = Array.isArray(bookIds) ? bookIds : [bookIds];
|
||||
return this.http.post<Book[]>(`${this.url}/reset-personal-rating`, ids).pipe(
|
||||
tap(updatedBooks => updatedBooks.forEach(book => this.handleBookUpdate(book)))
|
||||
);
|
||||
}
|
||||
|
||||
updatePersonalRating(bookIds: number | number[], rating: number): Observable<Book[]> {
|
||||
const ids = Array.isArray(bookIds) ? bookIds : [bookIds];
|
||||
return this.http.put<Book[]>(`${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<any> {
|
||||
const payload = {metadataType, targetValues, valuesToMerge};
|
||||
return this.http.post(`${this.url}/metadata/manage/consolidate`, payload).pipe(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -93,10 +93,10 @@
|
||||
<div class="rating-group">
|
||||
<i class="pi pi-thumbs-up-fill rating-icon" pTooltip="Personal Rating" tooltipPosition="top"></i>
|
||||
<p-rating
|
||||
[ngModel]="book?.metadata!.personalRating"
|
||||
[ngModel]="book?.personalRating"
|
||||
(onRate)="onPersonalRatingChange(book, $event)"
|
||||
stars="10"
|
||||
[style.--p-rating-icon-active-color]="getStarColorScaled(book?.metadata!.personalRating, 10)">
|
||||
[style.--p-rating-icon-active-color]="getStarColorScaled(book?.personalRating, 10)">
|
||||
</p-rating>
|
||||
</div>
|
||||
<p-button
|
||||
|
||||
@@ -502,12 +502,7 @@ export class MetadataViewerComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
onPersonalRatingChange(book: Book, {value: personalRating}: RatingRateEvent): void {
|
||||
if (!book?.metadata) return;
|
||||
const updatedMetadata = {...book.metadata, personalRating};
|
||||
this.bookService.updateBookMetadata(book.id, {
|
||||
metadata: updatedMetadata,
|
||||
clearFlags: {personalRating: false}
|
||||
}, false).subscribe({
|
||||
this.bookService.updatePersonalRating(book.id, personalRating).subscribe({
|
||||
next: () => {
|
||||
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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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')!;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user