Fix: bulk metadata editor not working (#1415)

This commit is contained in:
Aditya Chandel
2025-10-22 16:28:13 -06:00
committed by GitHub
parent b226c43343
commit 4e268ab199
9 changed files with 184 additions and 181 deletions

View File

@@ -66,8 +66,12 @@ public class MetadataController {
@PutMapping("/bulk-edit-metadata")
@PreAuthorize("@securityUtil.canEditMetadata() or @securityUtil.isAdmin()")
public ResponseEntity<List<BookMetadata>> bulkEditMetadata(@RequestBody BulkMetadataUpdateRequest bulkMetadataUpdateRequest, @RequestParam boolean mergeCategories) {
return ResponseEntity.ok(bookMetadataService.bulkUpdateMetadata(bulkMetadataUpdateRequest, mergeCategories));
public ResponseEntity<Void> bulkEditMetadata(@RequestBody BulkMetadataUpdateRequest bulkMetadataUpdateRequest) {
boolean mergeCategories = bulkMetadataUpdateRequest.isMergeCategories();
boolean mergeMoods = bulkMetadataUpdateRequest.isMergeMoods();
boolean mergeTags = bulkMetadataUpdateRequest.isMergeTags();
bookMetadataService.bulkUpdateMetadata(bulkMetadataUpdateRequest, mergeCategories, mergeMoods, mergeTags);
return ResponseEntity.noContent().build();
}
@PostMapping("/{bookId}/metadata/cover/upload")

View File

@@ -12,5 +12,7 @@ public class MetadataUpdateContext {
private MetadataUpdateWrapper metadataUpdateWrapper;
private boolean updateThumbnail;
private boolean mergeCategories;
private boolean mergeMoods;
private boolean mergeTags;
private MetadataReplaceMode replaceMode;
}

View File

@@ -35,4 +35,8 @@ public class BulkMetadataUpdateRequest {
private Set<String> tags;
private boolean clearTags;
private boolean mergeCategories;
private boolean mergeMoods;
private boolean mergeTags;
}

View File

@@ -235,7 +235,7 @@ public class BookMetadataService {
}
@Transactional
public List<BookMetadata> bulkUpdateMetadata(BulkMetadataUpdateRequest request, boolean mergeCategories) {
public void bulkUpdateMetadata(BulkMetadataUpdateRequest request, boolean mergeCategories, boolean mergeMoods, boolean mergeTags) {
List<BookEntity> books = bookRepository.findAllWithMetadataByIds(request.getBookIds());
MetadataClearFlags clearFlags = metadataClearFlagsMapper.toClearFlags(request);
@@ -261,14 +261,12 @@ public class BookMetadataService {
.build())
.updateThumbnail(false)
.mergeCategories(mergeCategories)
.mergeMoods(mergeMoods)
.mergeTags(mergeTags)
.build();
bookMetadataUpdater.setBookMetadata(context);
notificationService.sendMessage(Topic.BOOK_UPDATE, bookMapper.toBook(book));
}
return books.stream()
.map(BookEntity::getMetadata)
.map(m -> bookMetadataMapper.toBookMetadata(m, false))
.toList();
}
}

View File

@@ -31,9 +31,7 @@ import java.io.File;
import java.net.InetAddress;
import java.net.URL;
import java.time.Instant;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@@ -60,6 +58,8 @@ public class BookMetadataUpdater {
BookEntity bookEntity = context.getBookEntity();
MetadataUpdateWrapper wrapper = context.getMetadataUpdateWrapper();
boolean mergeCategories = context.isMergeCategories();
boolean mergeMoods = context.isMergeMoods();
boolean mergeTags = context.isMergeTags();
boolean updateThumbnail = context.isUpdateThumbnail();
MetadataReplaceMode replaceMode = context.getReplaceMode();
@@ -97,8 +97,8 @@ public class BookMetadataUpdater {
updateBasicFields(newMetadata, metadata, clearFlags, replaceMode);
updateAuthorsIfNeeded(newMetadata, metadata, clearFlags, mergeCategories, replaceMode);
updateCategoriesIfNeeded(newMetadata, metadata, clearFlags, mergeCategories, replaceMode);
updateMoodsIfNeeded(newMetadata, metadata, clearFlags, mergeCategories, replaceMode);
updateTagsIfNeeded(newMetadata, metadata, clearFlags, mergeCategories, replaceMode);
updateMoodsIfNeeded(newMetadata, metadata, clearFlags, mergeMoods, replaceMode);
updateTagsIfNeeded(newMetadata, metadata, clearFlags, mergeTags, replaceMode);
bookReviewUpdateService.updateBookReviews(newMetadata, metadata, clearFlags, mergeCategories);
updateThumbnailIfNeeded(bookId, newMetadata, metadata, updateThumbnail);
@@ -111,26 +111,23 @@ public class BookMetadataUpdater {
log.warn("Failed to calculate metadata match score for book ID {}: {}", bookId, e.getMessage());
}
if ((writeToFile && hasValueChanges) || thumbnailRequiresUpdate) {
if (bookType == BookFileType.CBX && !convertCbrCb7ToCbz) {
log.info("CBX metadata writing disabled for book ID {}", bookId);
} else {
metadataWriterFactory.getWriter(bookType).ifPresent(writer -> {
try {
String thumbnailUrl = updateThumbnail ? newMetadata.getThumbnailUrl() : null;
if ((StringUtils.hasText(thumbnailUrl) && isLocalOrPrivateUrl(thumbnailUrl) || Boolean.TRUE.equals(metadata.getCoverLocked()))) {
log.debug("Blocked local/private thumbnail URL: {}", thumbnailUrl);
thumbnailUrl = null;
}
File file = new File(bookEntity.getFullFilePath().toUri());
writer.writeMetadataToFile(file, metadata, thumbnailUrl, clearFlags);
String newHash = FileFingerprint.generateHash(bookEntity.getFullFilePath());
bookEntity.setCurrentHash(newHash);
} catch (Exception e) {
log.warn("Failed to write metadata for book ID {}: {}", bookId, e.getMessage());
boolean hasValueChangesForFileWrite = MetadataChangeDetector.hasValueChangesForFileWrite(newMetadata, metadata, clearFlags);
if ((writeToFile && hasValueChangesForFileWrite) || thumbnailRequiresUpdate) {
metadataWriterFactory.getWriter(bookType).ifPresent(writer -> {
try {
String thumbnailUrl = updateThumbnail ? newMetadata.getThumbnailUrl() : null;
if ((StringUtils.hasText(thumbnailUrl) && isLocalOrPrivateUrl(thumbnailUrl) || Boolean.TRUE.equals(metadata.getCoverLocked()))) {
log.debug("Blocked local/private thumbnail URL: {}", thumbnailUrl);
thumbnailUrl = null;
}
});
}
File file = new File(bookEntity.getFullFilePath().toUri());
writer.writeMetadataToFile(file, metadata, thumbnailUrl, clearFlags);
String newHash = FileFingerprint.generateHash(bookEntity.getFullFilePath());
bookEntity.setCurrentHash(newHash);
} catch (Exception e) {
log.warn("Failed to write metadata for book ID {}: {}", bookId, e.getMessage());
}
});
}
boolean moveFilesToLibraryPattern = settings.isMoveFilesToLibraryPattern();
@@ -175,18 +172,20 @@ public class BookMetadataUpdater {
handleFieldUpdate(e.getHardcoverReviewCountLocked(), clear.isHardcoverReviewCount(), m.getHardcoverReviewCount(), e::setHardcoverReviewCount, e::getHardcoverReviewCount, replaceMode);
}
private <T> void handleFieldUpdate(Boolean locked, boolean shouldClear, T newValue, Consumer<T> setter, Supplier<T> getter, MetadataReplaceMode replaceMode) {
private <T> void handleFieldUpdate(Boolean locked, boolean shouldClear, T newValue, Consumer<T> setter, Supplier<T> getter, MetadataReplaceMode mode) {
if (Boolean.TRUE.equals(locked)) return;
if (shouldClear) {
setter.accept(null);
return;
} else if (replaceMode == MetadataReplaceMode.REPLACE_ALL) {
}
if (mode == null) {
if (newValue != null) setter.accept(newValue);
return;
}
if (mode == MetadataReplaceMode.REPLACE_ALL) {
setter.accept(newValue);
} else if (mode == MetadataReplaceMode.REPLACE_MISSING && isValueMissing(getter.get())) {
setter.accept(newValue);
} else if (replaceMode == MetadataReplaceMode.REPLACE_MISSING) {
T currentValue = getter.get();
if (isValueMissing(currentValue)) {
setter.accept(newValue);
}
}
}
@@ -197,153 +196,154 @@ public class BookMetadataUpdater {
}
private void updateAuthorsIfNeeded(BookMetadata m, BookMetadataEntity e, MetadataClearFlags clear, boolean merge, MetadataReplaceMode replaceMode) {
if (Boolean.TRUE.equals(e.getAuthorsLocked())) {
return;
}
if (e.getAuthors() == null) {
e.setAuthors(new HashSet<>());
}
if (Boolean.TRUE.equals(e.getAuthorsLocked())) return;
e.setAuthors(Optional.ofNullable(e.getAuthors()).orElseGet(HashSet::new));
if (clear.isAuthors()) {
e.getAuthors().clear();
return;
}
if (m.getAuthors() == null) {
if (replaceMode == MetadataReplaceMode.REPLACE_ALL) {
e.getAuthors().clear();
}
Set<String> authorNames = Optional.ofNullable(m.getAuthors()).orElse(Collections.emptySet());
if (authorNames.isEmpty()) {
if (replaceMode == MetadataReplaceMode.REPLACE_ALL) e.getAuthors().clear();
return;
}
Set<AuthorEntity> newAuthors = m.getAuthors().stream()
.filter(a -> a != null && !a.isBlank())
Set<AuthorEntity> newAuthors = authorNames.stream()
.filter(name -> name != null && !name.isBlank())
.map(name -> authorRepository.findByName(name)
.orElseGet(() -> authorRepository.save(AuthorEntity.builder().name(name).build())))
.collect(Collectors.toSet());
if (replaceMode == MetadataReplaceMode.REPLACE_ALL) {
if (merge) {
e.getAuthors().addAll(newAuthors);
} else {
e.getAuthors().clear();
e.getAuthors().addAll(newAuthors);
}
} else if (replaceMode == MetadataReplaceMode.REPLACE_MISSING) {
if (e.getAuthors().isEmpty()) {
e.getAuthors().addAll(newAuthors);
}
if (newAuthors.isEmpty()) return;
boolean replaceAll = replaceMode == MetadataReplaceMode.REPLACE_ALL;
boolean replaceMissing = replaceMode == MetadataReplaceMode.REPLACE_MISSING;
if (replaceAll) {
if (!merge) e.getAuthors().clear();
e.getAuthors().addAll(newAuthors);
} else if (replaceMissing && e.getAuthors().isEmpty()) {
e.getAuthors().addAll(newAuthors);
} else if (replaceMode == null) {
if (!merge) e.getAuthors().clear();
e.getAuthors().addAll(newAuthors);
}
}
private void updateCategoriesIfNeeded(BookMetadata m, BookMetadataEntity e, MetadataClearFlags clear, boolean merge, MetadataReplaceMode replaceMode) {
if (Boolean.TRUE.equals(e.getCategoriesLocked())) {
return;
}
if (e.getCategories() == null) {
e.setCategories(new HashSet<>());
}
if (Boolean.TRUE.equals(e.getCategoriesLocked())) return;
e.setCategories(Optional.ofNullable(e.getCategories()).orElseGet(HashSet::new));
if (clear.isCategories()) {
e.getCategories().clear();
return;
}
if (m.getCategories() == null) {
if (replaceMode == MetadataReplaceMode.REPLACE_ALL) {
e.getCategories().clear();
}
Set<String> categoryNames = Optional.ofNullable(m.getCategories()).orElse(Collections.emptySet());
if (categoryNames.isEmpty()) {
if (replaceMode == MetadataReplaceMode.REPLACE_ALL) e.getCategories().clear();
return;
}
Set<CategoryEntity> newCategories = m.getCategories().stream()
.filter(n -> n != null && !n.isBlank())
Set<CategoryEntity> newCategories = categoryNames.stream()
.filter(name -> name != null && !name.isBlank())
.map(name -> categoryRepository.findByName(name)
.orElseGet(() -> categoryRepository.save(CategoryEntity.builder().name(name).build())))
.collect(Collectors.toSet());
if (replaceMode == MetadataReplaceMode.REPLACE_ALL) {
if (merge) {
e.getCategories().addAll(newCategories);
} else {
e.getCategories().clear();
e.getCategories().addAll(newCategories);
}
} else if (replaceMode == MetadataReplaceMode.REPLACE_MISSING) {
if (e.getCategories().isEmpty()) {
e.getCategories().addAll(newCategories);
}
if (newCategories.isEmpty()) return;
boolean replaceAll = replaceMode == MetadataReplaceMode.REPLACE_ALL;
boolean replaceMissing = replaceMode == MetadataReplaceMode.REPLACE_MISSING;
if (replaceAll) {
if (!merge) e.getCategories().clear();
e.getCategories().addAll(newCategories);
} else if (replaceMissing && e.getCategories().isEmpty()) {
e.getCategories().addAll(newCategories);
} else if (replaceMode == null) {
if (!merge) e.getCategories().clear();
e.getCategories().addAll(newCategories);
}
}
private void updateMoodsIfNeeded(BookMetadata m, BookMetadataEntity e, MetadataClearFlags clear, boolean merge, MetadataReplaceMode replaceMode) {
if (Boolean.TRUE.equals(e.getMoodsLocked())) {
return;
}
if (e.getMoods() == null) {
e.setMoods(new HashSet<>());
}
if (Boolean.TRUE.equals(e.getMoodsLocked())) return;
e.setMoods(Optional.ofNullable(e.getMoods()).orElseGet(HashSet::new));
if (clear.isMoods()) {
e.getMoods().clear();
return;
}
if (m.getMoods() == null) {
if (replaceMode == MetadataReplaceMode.REPLACE_ALL) {
e.getMoods().clear();
}
Set<String> moodNames = Optional.ofNullable(m.getMoods()).orElse(Collections.emptySet());
if (moodNames.isEmpty()) {
if (replaceMode == MetadataReplaceMode.REPLACE_ALL) e.getMoods().clear();
return;
}
Set<MoodEntity> newMoods = m.getMoods().stream()
.filter(n -> n != null && !n.isBlank())
Set<MoodEntity> newMoods = moodNames.stream()
.filter(name -> name != null && !name.isBlank())
.map(name -> moodRepository.findByName(name)
.orElseGet(() -> moodRepository.save(MoodEntity.builder().name(name).build())))
.collect(Collectors.toSet());
if (replaceMode == MetadataReplaceMode.REPLACE_ALL) {
if (merge) {
e.getMoods().addAll(newMoods);
} else {
e.getMoods().clear();
e.getMoods().addAll(newMoods);
}
} else if (replaceMode == MetadataReplaceMode.REPLACE_MISSING) {
if (e.getMoods().isEmpty()) {
e.getMoods().addAll(newMoods);
}
if (newMoods.isEmpty()) return;
boolean replaceAll = replaceMode == MetadataReplaceMode.REPLACE_ALL;
boolean replaceMissing = replaceMode == MetadataReplaceMode.REPLACE_MISSING;
if (replaceAll) {
if (!merge) e.getMoods().clear();
e.getMoods().addAll(newMoods);
} else if (replaceMissing && e.getMoods().isEmpty()) {
e.getMoods().addAll(newMoods);
} else if (replaceMode == null) {
if (!merge) e.getMoods().clear();
e.getMoods().addAll(newMoods);
}
}
private void updateTagsIfNeeded(BookMetadata m, BookMetadataEntity e, MetadataClearFlags clear, boolean merge, MetadataReplaceMode replaceMode) {
if (Boolean.TRUE.equals(e.getTagsLocked())) {
return;
}
if (e.getTags() == null) {
e.setTags(new HashSet<>());
}
if (Boolean.TRUE.equals(e.getTagsLocked())) return;
e.setTags(Optional.ofNullable(e.getTags()).orElseGet(HashSet::new));
if (clear.isTags()) {
e.getTags().clear();
return;
}
if (m.getTags() == null) {
if (replaceMode == MetadataReplaceMode.REPLACE_ALL) {
e.getTags().clear();
}
Set<String> tagNames = Optional.ofNullable(m.getTags()).orElse(Collections.emptySet());
if (tagNames.isEmpty()) {
if (replaceMode == MetadataReplaceMode.REPLACE_ALL) e.getTags().clear();
return;
}
Set<TagEntity> newTags = m.getTags().stream()
.filter(n -> n != null && !n.isBlank())
Set<TagEntity> newTags = tagNames.stream()
.filter(name -> name != null && !name.isBlank())
.map(name -> tagRepository.findByName(name)
.orElseGet(() -> tagRepository.save(TagEntity.builder().name(name).build())))
.collect(Collectors.toSet());
if (replaceMode == MetadataReplaceMode.REPLACE_ALL) {
if (merge) {
e.getTags().addAll(newTags);
} else {
e.getTags().clear();
e.getTags().addAll(newTags);
}
} else if (replaceMode == MetadataReplaceMode.REPLACE_MISSING) {
if (e.getTags().isEmpty()) {
e.getTags().addAll(newTags);
}
if (newTags.isEmpty()) return;
boolean replaceAll = replaceMode == MetadataReplaceMode.REPLACE_ALL;
boolean replaceMissing = replaceMode == MetadataReplaceMode.REPLACE_MISSING;
if (replaceAll) {
if (!merge) e.getTags().clear();
e.getTags().addAll(newTags);
} else if (replaceMissing && e.getTags().isEmpty()) {
e.getTags().addAll(newTags);
} else if (replaceMode == null) {
if (!merge) e.getTags().clear();
e.getTags().addAll(newTags);
}
}

View File

@@ -2,9 +2,7 @@ package com.adityachandel.booklore.util;
import com.adityachandel.booklore.model.MetadataClearFlags;
import com.adityachandel.booklore.model.dto.BookMetadata;
import com.adityachandel.booklore.model.entity.AuthorEntity;
import com.adityachandel.booklore.model.entity.BookMetadataEntity;
import com.adityachandel.booklore.model.entity.CategoryEntity;
import com.adityachandel.booklore.model.entity.*;
import lombok.extern.slf4j.Slf4j;
import java.util.*;
@@ -97,38 +95,28 @@ public class MetadataChangeDetector {
return !diffs.isEmpty();
}
public static boolean hasLockChanges(BookMetadata newMeta, BookMetadataEntity existingMeta) {
if (differsLock(newMeta.getTitleLocked(), existingMeta.getTitleLocked())) return true;
if (differsLock(newMeta.getSubtitleLocked(), existingMeta.getSubtitleLocked())) return true;
if (differsLock(newMeta.getPublisherLocked(), existingMeta.getPublisherLocked())) return true;
if (differsLock(newMeta.getPublishedDateLocked(), existingMeta.getPublishedDateLocked())) return true;
if (differsLock(newMeta.getDescriptionLocked(), existingMeta.getDescriptionLocked())) return true;
if (differsLock(newMeta.getSeriesNameLocked(), existingMeta.getSeriesNameLocked())) return true;
if (differsLock(newMeta.getSeriesNumberLocked(), existingMeta.getSeriesNumberLocked())) return true;
if (differsLock(newMeta.getSeriesTotalLocked(), existingMeta.getSeriesTotalLocked())) return true;
if (differsLock(newMeta.getIsbn13Locked(), existingMeta.getIsbn13Locked())) return true;
if (differsLock(newMeta.getIsbn10Locked(), existingMeta.getIsbn10Locked())) return true;
if (differsLock(newMeta.getAsinLocked(), existingMeta.getAsinLocked())) return true;
if (differsLock(newMeta.getGoodreadsIdLocked(), existingMeta.getGoodreadsIdLocked())) return true;
if (differsLock(newMeta.getComicvineIdLocked(), existingMeta.getComicvineIdLocked())) return true;
if (differsLock(newMeta.getHardcoverIdLocked(), existingMeta.getHardcoverIdLocked())) return true;
if (differsLock(newMeta.getGoogleIdLocked(), existingMeta.getGoogleIdLocked())) return true;
if (differsLock(newMeta.getPageCountLocked(), existingMeta.getPageCountLocked())) return true;
if (differsLock(newMeta.getLanguageLocked(), existingMeta.getLanguageLocked())) return true;
if (differsLock(newMeta.getPersonalRatingLocked(), existingMeta.getPersonalRatingLocked())) return true;
if (differsLock(newMeta.getAmazonRatingLocked(), existingMeta.getAmazonRatingLocked())) return true;
if (differsLock(newMeta.getAmazonReviewCountLocked(), existingMeta.getAmazonReviewCountLocked())) return true;
if (differsLock(newMeta.getGoodreadsRatingLocked(), existingMeta.getGoodreadsRatingLocked())) return true;
if (differsLock(newMeta.getGoodreadsReviewCountLocked(), existingMeta.getGoodreadsReviewCountLocked())) return true;
if (differsLock(newMeta.getHardcoverRatingLocked(), existingMeta.getHardcoverRatingLocked())) return true;
if (differsLock(newMeta.getHardcoverReviewCountLocked(), existingMeta.getHardcoverReviewCountLocked())) return true;
if (differsLock(newMeta.getCoverLocked(), existingMeta.getCoverLocked())) return true;
if (differsLock(newMeta.getAuthorsLocked(), existingMeta.getAuthorsLocked())) return true;
if (differsLock(newMeta.getCategoriesLocked(), existingMeta.getCategoriesLocked())) return true;
if (differsLock(newMeta.getMoodsLocked(), existingMeta.getMoodsLocked())) return true;
if (differsLock(newMeta.getTagsLocked(), existingMeta.getTagsLocked())) return true;
if (differsLock(newMeta.getReviewsLocked(), existingMeta.getReviewsLocked())) return true;
return false;
public static boolean hasValueChangesForFileWrite(BookMetadata newMeta, BookMetadataEntity existingMeta, MetadataClearFlags clear) {
List<String> diffs = new ArrayList<>();
compareValue(diffs, "title", clear.isTitle(), newMeta.getTitle(), existingMeta.getTitle(), () -> !isTrue(existingMeta.getTitleLocked()));
compareValue(diffs, "subtitle", clear.isSubtitle(), newMeta.getSubtitle(), existingMeta.getSubtitle(), () -> !isTrue(existingMeta.getSubtitleLocked()));
compareValue(diffs, "publisher", clear.isPublisher(), newMeta.getPublisher(), existingMeta.getPublisher(), () -> !isTrue(existingMeta.getPublisherLocked()));
compareValue(diffs, "publishedDate", clear.isPublishedDate(), newMeta.getPublishedDate(), existingMeta.getPublishedDate(), () -> !isTrue(existingMeta.getPublishedDateLocked()));
compareValue(diffs, "description", clear.isDescription(), newMeta.getDescription(), existingMeta.getDescription(), () -> !isTrue(existingMeta.getDescriptionLocked()));
compareValue(diffs, "seriesName", clear.isSeriesName(), newMeta.getSeriesName(), existingMeta.getSeriesName(), () -> !isTrue(existingMeta.getSeriesNameLocked()));
compareValue(diffs, "seriesNumber", clear.isSeriesNumber(), newMeta.getSeriesNumber(), existingMeta.getSeriesNumber(), () -> !isTrue(existingMeta.getSeriesNumberLocked()));
compareValue(diffs, "seriesTotal", clear.isSeriesTotal(), newMeta.getSeriesTotal(), existingMeta.getSeriesTotal(), () -> !isTrue(existingMeta.getSeriesTotalLocked()));
compareValue(diffs, "isbn13", clear.isIsbn13(), newMeta.getIsbn13(), existingMeta.getIsbn13(), () -> !isTrue(existingMeta.getIsbn13Locked()));
compareValue(diffs, "isbn10", clear.isIsbn10(), newMeta.getIsbn10(), existingMeta.getIsbn10(), () -> !isTrue(existingMeta.getIsbn10Locked()));
compareValue(diffs, "asin", clear.isAsin(), newMeta.getAsin(), existingMeta.getAsin(), () -> !isTrue(existingMeta.getAsinLocked()));
compareValue(diffs, "goodreadsId", clear.isGoodreadsId(), newMeta.getGoodreadsId(), existingMeta.getGoodreadsId(), () -> !isTrue(existingMeta.getGoodreadsIdLocked()));
compareValue(diffs, "comicvineId", clear.isComicvineId(), newMeta.getComicvineId(), existingMeta.getComicvineId(), () -> !isTrue(existingMeta.getComicvineIdLocked()));
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();
}
private static void compare(List<String> diffs, String field, boolean shouldClear, Object newVal, Object oldVal, Supplier<Boolean> isUnlocked, Boolean newLock, Boolean oldLock) {
@@ -202,6 +190,12 @@ public class MetadataChangeDetector {
if (e instanceof CategoryEntity category) {
return category.getName();
}
if (e instanceof MoodEntity mood) {
return mood.getName();
}
if (e instanceof TagEntity tag) {
return tag.getName();
}
return e.toString();
})
.collect(Collectors.toSet());

View File

@@ -229,6 +229,9 @@ export interface BulkMetadataUpdateRequest {
clearMoods?: boolean;
tags?: string[];
clearTags?: boolean;
mergeCategories?: boolean;
mergeMoods?: boolean;
mergeTags?: boolean;
}
export interface BookDeletionResponse {

View File

@@ -448,15 +448,9 @@ export class BookService {
);
}
updateBooksMetadata(request: BulkMetadataUpdateRequest, mergeCategories: boolean): Observable<BookMetadata[]> {
const params = new HttpParams().set('mergeCategories', mergeCategories.toString());
return this.http.put<BookMetadata[]>(`${this.url}/bulk-edit-metadata`, request, {params}).pipe(
map(updatedMetadataList => {
request.bookIds.forEach((id, index) => {
this.handleBookMetadataUpdate(id, updatedMetadataList[index]);
});
return updatedMetadataList;
})
updateBooksMetadata(request: BulkMetadataUpdateRequest): Observable<void> {
return this.http.put(`${this.url}/bulk-edit-metadata`, request).pipe(
map(() => void 0)
);
}

View File

@@ -95,7 +95,7 @@ export class BulkMetadataUpdateComponent implements OnInit {
if (inputValue) {
const currentValue = this.metadataForm.get(fieldName)?.value || [];
const values = Array.isArray(currentValue) ? currentValue :
typeof currentValue === 'string' && currentValue ? currentValue.split(',').map((v: string) => v.trim()) : [];
typeof currentValue === 'string' && currentValue ? currentValue.split(',').map((v: string) => v.trim()) : [];
// Add the new value if it's not already in the array
if (!values.includes(inputValue)) {
@@ -110,7 +110,7 @@ export class BulkMetadataUpdateComponent implements OnInit {
onFormKeydown(event: KeyboardEvent): void {
if (event.key === 'Enter') {
if ((event.target as HTMLElement)?.tagName === 'BUTTON' &&
(event.target as HTMLButtonElement)?.type === 'submit') {
(event.target as HTMLButtonElement)?.type === 'submit') {
return;
}
event.preventDefault();
@@ -152,17 +152,21 @@ export class BulkMetadataUpdateComponent implements OnInit {
clearMoods: this.clearFields.moods,
tags: this.clearFields.tags ? [] : (formValue.tags?.length ? formValue.tags : undefined),
clearTags: this.clearFields.tags
clearTags: this.clearFields.tags,
mergeCategories: this.mergeCategories,
mergeMoods: this.mergeMoods,
mergeTags: this.mergeTags
};
this.loading = true;
this.bookService.updateBooksMetadata(payload, this.mergeCategories).subscribe({
next: updatedBooks => {
this.bookService.updateBooksMetadata(payload).subscribe({
next: () => {
this.loading = false;
this.messageService.add({
severity: 'success',
summary: 'Metadata Updated',
detail: `${updatedBooks.length} book${updatedBooks.length > 1 ? 's' : ''} updated successfully`
detail: 'Books updated successfully'
});
this.ref.close(true);
},