mirror of
https://github.com/adityachandelgit/BookLore.git
synced 2026-03-16 16:42:08 -05:00
Fix issue where metadata updates to file caused books to be removed from booklore
This commit is contained in:
committed by
Aditya Chandel
parent
47e24e2241
commit
63dc2bcbc7
-1
@@ -39,7 +39,6 @@ public class AuthenticationService {
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final JwtUtils jwtUtils;
|
||||
private final DefaultSettingInitializer defaultSettingInitializer;
|
||||
private final OpdsUserRepository opdsUserRepository;
|
||||
|
||||
public BookLoreUser getAuthenticatedUser() {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
+13
-4
@@ -21,6 +21,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/books")
|
||||
@@ -68,12 +69,20 @@ public class MetadataController {
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@PostMapping("/{bookId}/metadata/cover")
|
||||
@PostMapping("/{bookId}/metadata/cover/upload")
|
||||
@PreAuthorize("@securityUtil.canEditMetadata() or @securityUtil.isAdmin()")
|
||||
@CheckBookAccess(bookIdParam = "bookId")
|
||||
public ResponseEntity<BookMetadata> uploadCover(@PathVariable Long bookId, @RequestParam("file") MultipartFile file) {
|
||||
BookMetadata updatedMetadata = bookMetadataService.handleCoverUpload(bookId, file);
|
||||
return ResponseEntity.ok(updatedMetadata);
|
||||
public ResponseEntity<BookMetadata> uploadCoverFromFile(@PathVariable Long bookId, @RequestParam("file") MultipartFile file) {
|
||||
BookMetadata updated = bookMetadataService.updateCoverImageFromFile(bookId, file);
|
||||
return ResponseEntity.ok(updated);
|
||||
}
|
||||
|
||||
@PostMapping("/{bookId}/metadata/cover/from-url")
|
||||
@PreAuthorize("@securityUtil.canEditMetadata() or @securityUtil.isAdmin()")
|
||||
@CheckBookAccess(bookIdParam = "bookId")
|
||||
public ResponseEntity<BookMetadata> uploadCoverFromUrl(@PathVariable Long bookId, @RequestBody Map<String, String> body) {
|
||||
BookMetadata updated = bookMetadataService.updateCoverImageFromUrl(bookId, body.get("url"));
|
||||
return ResponseEntity.ok(updated);
|
||||
}
|
||||
|
||||
@PutMapping("/metadata/toggle-all-lock")
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.adityachandel.booklore.model.entity.BookLoreUserEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
@@ -12,4 +13,6 @@ public interface UserRepository extends JpaRepository<BookLoreUserEntity, Long>
|
||||
Optional<BookLoreUserEntity> findByUsername(String username);
|
||||
|
||||
Optional<BookLoreUserEntity> findById(Long id);
|
||||
|
||||
List<BookLoreUserEntity> findAllByLibraries_Id(Long libraryId);
|
||||
}
|
||||
|
||||
+15
-15
@@ -95,33 +95,33 @@ public class SettingPersistenceHelper {
|
||||
|
||||
MetadataRefreshOptions getDefaultMetadataRefreshOptions() {
|
||||
MetadataRefreshOptions.FieldProvider titleProviders =
|
||||
new MetadataRefreshOptions.FieldProvider(MetadataProvider.Douban, MetadataProvider.Google, MetadataProvider.Amazon, MetadataProvider.GoodReads);
|
||||
new MetadataRefreshOptions.FieldProvider(null, MetadataProvider.Google, MetadataProvider.Amazon, MetadataProvider.GoodReads);
|
||||
MetadataRefreshOptions.FieldProvider subtitleProviders =
|
||||
new MetadataRefreshOptions.FieldProvider(MetadataProvider.Douban, MetadataProvider.Google, MetadataProvider.Amazon, MetadataProvider.GoodReads);
|
||||
new MetadataRefreshOptions.FieldProvider(null, MetadataProvider.Google, MetadataProvider.Amazon, MetadataProvider.GoodReads);
|
||||
MetadataRefreshOptions.FieldProvider descriptionProviders =
|
||||
new MetadataRefreshOptions.FieldProvider(MetadataProvider.Douban, MetadataProvider.Google, MetadataProvider.Amazon, MetadataProvider.GoodReads);
|
||||
new MetadataRefreshOptions.FieldProvider(null, MetadataProvider.Google, MetadataProvider.Amazon, MetadataProvider.GoodReads);
|
||||
MetadataRefreshOptions.FieldProvider authorsProviders =
|
||||
new MetadataRefreshOptions.FieldProvider(MetadataProvider.Douban, MetadataProvider.Google, MetadataProvider.Amazon, MetadataProvider.GoodReads);
|
||||
new MetadataRefreshOptions.FieldProvider(null, MetadataProvider.Google, MetadataProvider.Amazon, MetadataProvider.GoodReads);
|
||||
MetadataRefreshOptions.FieldProvider publisherProviders =
|
||||
new MetadataRefreshOptions.FieldProvider(MetadataProvider.Douban, MetadataProvider.Google, MetadataProvider.Amazon, MetadataProvider.GoodReads);
|
||||
new MetadataRefreshOptions.FieldProvider(null, MetadataProvider.Google, MetadataProvider.Amazon, MetadataProvider.GoodReads);
|
||||
MetadataRefreshOptions.FieldProvider publishedDateProviders =
|
||||
new MetadataRefreshOptions.FieldProvider(MetadataProvider.Douban, MetadataProvider.Google, MetadataProvider.Amazon, MetadataProvider.GoodReads);
|
||||
new MetadataRefreshOptions.FieldProvider(null, MetadataProvider.Google, MetadataProvider.Amazon, MetadataProvider.GoodReads);
|
||||
MetadataRefreshOptions.FieldProvider seriesNameProviders =
|
||||
new MetadataRefreshOptions.FieldProvider(MetadataProvider.Douban, MetadataProvider.Google, MetadataProvider.Amazon, MetadataProvider.GoodReads);
|
||||
new MetadataRefreshOptions.FieldProvider(null, MetadataProvider.Google, MetadataProvider.Amazon, MetadataProvider.GoodReads);
|
||||
MetadataRefreshOptions.FieldProvider seriesNumberProviders =
|
||||
new MetadataRefreshOptions.FieldProvider(MetadataProvider.Douban, MetadataProvider.Google, MetadataProvider.Amazon, MetadataProvider.GoodReads);
|
||||
new MetadataRefreshOptions.FieldProvider(null, MetadataProvider.Google, MetadataProvider.Amazon, MetadataProvider.GoodReads);
|
||||
MetadataRefreshOptions.FieldProvider seriesTotalProviders =
|
||||
new MetadataRefreshOptions.FieldProvider(MetadataProvider.Douban, MetadataProvider.Google, MetadataProvider.Amazon, MetadataProvider.GoodReads);
|
||||
new MetadataRefreshOptions.FieldProvider(null, MetadataProvider.Google, MetadataProvider.Amazon, MetadataProvider.GoodReads);
|
||||
MetadataRefreshOptions.FieldProvider isbn13Providers =
|
||||
new MetadataRefreshOptions.FieldProvider(MetadataProvider.Douban, MetadataProvider.Google, MetadataProvider.Amazon, MetadataProvider.GoodReads);
|
||||
new MetadataRefreshOptions.FieldProvider(null, MetadataProvider.Google, MetadataProvider.Amazon, MetadataProvider.GoodReads);
|
||||
MetadataRefreshOptions.FieldProvider isbn10Providers =
|
||||
new MetadataRefreshOptions.FieldProvider(MetadataProvider.Douban, MetadataProvider.Google, MetadataProvider.Amazon, MetadataProvider.GoodReads);
|
||||
new MetadataRefreshOptions.FieldProvider(null, MetadataProvider.Google, MetadataProvider.Amazon, MetadataProvider.GoodReads);
|
||||
MetadataRefreshOptions.FieldProvider languageProviders =
|
||||
new MetadataRefreshOptions.FieldProvider(MetadataProvider.Douban, MetadataProvider.Google, MetadataProvider.Amazon, MetadataProvider.GoodReads);
|
||||
new MetadataRefreshOptions.FieldProvider(null, MetadataProvider.Google, MetadataProvider.Amazon, MetadataProvider.GoodReads);
|
||||
MetadataRefreshOptions.FieldProvider categoriesProviders =
|
||||
new MetadataRefreshOptions.FieldProvider(MetadataProvider.Douban, MetadataProvider.Google, MetadataProvider.Amazon, MetadataProvider.GoodReads);
|
||||
new MetadataRefreshOptions.FieldProvider(null, MetadataProvider.Google, MetadataProvider.Amazon, MetadataProvider.GoodReads);
|
||||
MetadataRefreshOptions.FieldProvider coverProviders =
|
||||
new MetadataRefreshOptions.FieldProvider(MetadataProvider.Douban, MetadataProvider.Google, MetadataProvider.Amazon, MetadataProvider.GoodReads);
|
||||
new MetadataRefreshOptions.FieldProvider(null, MetadataProvider.Google, MetadataProvider.Amazon, MetadataProvider.GoodReads);
|
||||
|
||||
MetadataRefreshOptions.FieldOptions fieldOptions = new MetadataRefreshOptions.FieldOptions(
|
||||
titleProviders,
|
||||
@@ -144,7 +144,7 @@ public class SettingPersistenceHelper {
|
||||
MetadataProvider.GoodReads,
|
||||
MetadataProvider.Amazon,
|
||||
MetadataProvider.Google,
|
||||
MetadataProvider.Douban,
|
||||
null,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.adityachandel.booklore.service.event;
|
||||
|
||||
import com.adityachandel.booklore.model.dto.BookLoreUser;
|
||||
import com.adityachandel.booklore.model.websocket.Topic;
|
||||
import com.adityachandel.booklore.service.user.UserService;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.adityachandel.booklore.model.websocket.LogNotification.createLogNotification;
|
||||
|
||||
@AllArgsConstructor
|
||||
@Service
|
||||
public class AdminEventBroadcaster {
|
||||
|
||||
private final SimpMessagingTemplate messagingTemplate;
|
||||
private final UserService userService;
|
||||
|
||||
public void broadcastAdminEvent(String message) {
|
||||
List<BookLoreUser> admins = userService.getBookLoreUsers().stream()
|
||||
.filter(u -> u.getPermissions().isAdmin())
|
||||
.toList();
|
||||
for (BookLoreUser admin : admins) {
|
||||
messagingTemplate.convertAndSendToUser(admin.getUsername(), Topic.LOG.getPath(), createLogNotification(message));
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package com.adityachandel.booklore.service.event;
|
||||
|
||||
import com.adityachandel.booklore.model.dto.Book;
|
||||
import com.adityachandel.booklore.model.websocket.Topic;
|
||||
import com.adityachandel.booklore.service.user.UserService;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import static com.adityachandel.booklore.model.websocket.LogNotification.createLogNotification;
|
||||
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
@Service
|
||||
public class BookEventBroadcaster {
|
||||
|
||||
private final SimpMessagingTemplate messagingTemplate;
|
||||
private final UserService userService;
|
||||
|
||||
public void broadcastBookAddEvent(Book book) {
|
||||
Long libraryId = book.getLibraryId();
|
||||
userService.getBookLoreUsers().stream()
|
||||
.filter(u -> u.getPermissions().isAdmin() || u.getAssignedLibraries().stream()
|
||||
.anyMatch(lib -> lib.getId().equals(libraryId)))
|
||||
.forEach(u -> {
|
||||
String username = u.getUsername();
|
||||
messagingTemplate.convertAndSendToUser(username, Topic.BOOK_ADD.getPath(), book);
|
||||
messagingTemplate.convertAndSendToUser(username, Topic.LOG.getPath(), createLogNotification("Book added: " + book.getFileName()));
|
||||
log.debug("Sent BOOK_ADD and LOG notifications for '{}' to user '{}'", book.getFileName(), username);
|
||||
});
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -7,6 +7,7 @@ import com.adityachandel.booklore.model.enums.BookFileExtension;
|
||||
import com.adityachandel.booklore.model.enums.BookFileType;
|
||||
import com.adityachandel.booklore.model.websocket.Topic;
|
||||
import com.adityachandel.booklore.service.NotificationService;
|
||||
import com.adityachandel.booklore.service.event.BookEventBroadcaster;
|
||||
import com.adityachandel.booklore.service.fileprocessor.BookFileProcessor;
|
||||
import com.adityachandel.booklore.service.fileprocessor.BookFileProcessorRegistry;
|
||||
import lombok.AllArgsConstructor;
|
||||
@@ -25,7 +26,7 @@ import com.adityachandel.booklore.model.enums.LibraryScanMode;
|
||||
@Slf4j
|
||||
public class FileAsBookProcessor implements LibraryFileProcessor {
|
||||
|
||||
private final NotificationService notificationService;
|
||||
private final BookEventBroadcaster bookEventBroadcaster;
|
||||
private final BookFileProcessorRegistry processorRegistry;
|
||||
|
||||
@Override
|
||||
@@ -40,8 +41,7 @@ public class FileAsBookProcessor implements LibraryFileProcessor {
|
||||
log.info("Processing file: {}", libraryFile.getFileName());
|
||||
Book book = processLibraryFile(libraryFile);
|
||||
if (book != null) {
|
||||
notificationService.sendMessage(Topic.BOOK_ADD, book);
|
||||
notificationService.sendMessage(Topic.LOG, createLogNotification("Book added: " + book.getFileName()));
|
||||
bookEventBroadcaster.broadcastBookAddEvent(book);
|
||||
log.info("Processed file: {}", libraryFile.getFileName());
|
||||
}
|
||||
}
|
||||
|
||||
+14
-34
@@ -13,7 +13,8 @@ import com.adityachandel.booklore.model.enums.LibraryScanMode;
|
||||
import com.adityachandel.booklore.repository.BookAdditionalFileRepository;
|
||||
import com.adityachandel.booklore.repository.BookRepository;
|
||||
import com.adityachandel.booklore.service.FileFingerprint;
|
||||
import com.adityachandel.booklore.service.NotificationService;
|
||||
import com.adityachandel.booklore.service.event.AdminEventBroadcaster;
|
||||
import com.adityachandel.booklore.service.event.BookEventBroadcaster;
|
||||
import com.adityachandel.booklore.service.fileprocessor.BookFileProcessor;
|
||||
import com.adityachandel.booklore.service.fileprocessor.BookFileProcessorRegistry;
|
||||
import com.adityachandel.booklore.util.FileUtils;
|
||||
@@ -35,7 +36,8 @@ public class FolderAsBookFileProcessor implements LibraryFileProcessor {
|
||||
|
||||
private final BookRepository bookRepository;
|
||||
private final BookAdditionalFileRepository bookAdditionalFileRepository;
|
||||
private final NotificationService notificationService;
|
||||
private final BookEventBroadcaster bookEventBroadcaster;
|
||||
private final AdminEventBroadcaster adminEventBroadcaster;
|
||||
private final BookFileProcessorRegistry bookFileProcessorRegistry;
|
||||
|
||||
@Override
|
||||
@@ -142,9 +144,9 @@ public class FolderAsBookFileProcessor implements LibraryFileProcessor {
|
||||
|
||||
Optional<BookEntity> parentBook =
|
||||
bookRepository.findAllByLibraryPathIdAndFileSubPathStartingWith(
|
||||
directoryLibraryPathEntity.getId(), parentPath).stream()
|
||||
.filter(book -> book.getFileSubPath().equals(parentPath))
|
||||
.findFirst();
|
||||
directoryLibraryPathEntity.getId(), parentPath).stream()
|
||||
.filter(book -> book.getFileSubPath().equals(parentPath))
|
||||
.findFirst();
|
||||
if (parentBook.isPresent()) {
|
||||
return parentBook;
|
||||
}
|
||||
@@ -173,17 +175,7 @@ public class FolderAsBookFileProcessor implements LibraryFileProcessor {
|
||||
Book book = processor.processFile(bookFile);
|
||||
|
||||
if (book != null) {
|
||||
// Send notifications
|
||||
notificationService.sendMessage(
|
||||
com.adityachandel.booklore.model.websocket.Topic.BOOK_ADD,
|
||||
book
|
||||
);
|
||||
notificationService.sendMessage(
|
||||
com.adityachandel.booklore.model.websocket.Topic.LOG,
|
||||
com.adityachandel.booklore.model.websocket.LogNotification.createLogNotification(
|
||||
"Book added: " + book.getFileName()
|
||||
)
|
||||
);
|
||||
bookEventBroadcaster.broadcastBookAddEvent(book);
|
||||
|
||||
// Find the created book entity
|
||||
BookEntity bookEntity = bookRepository.getReferenceById(book.getId());
|
||||
@@ -197,26 +189,12 @@ public class FolderAsBookFileProcessor implements LibraryFileProcessor {
|
||||
return Optional.of(new CreateBookResult(bookEntity, bookFile));
|
||||
} else {
|
||||
log.warn("Book processor returned null for file: {}", bookFile.getFileName());
|
||||
|
||||
notificationService.sendMessage(
|
||||
com.adityachandel.booklore.model.websocket.Topic.LOG,
|
||||
com.adityachandel.booklore.model.websocket.LogNotification.createLogNotification(
|
||||
"Failed to create book from file: " + bookFile.getFileName()
|
||||
)
|
||||
);
|
||||
|
||||
adminEventBroadcaster.broadcastAdminEvent("Failed to create book from file: " + bookFile.getFileName());
|
||||
return Optional.empty();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error processing book file {}: {}", bookFile.getFileName(), e.getMessage(), e);
|
||||
|
||||
notificationService.sendMessage(
|
||||
com.adityachandel.booklore.model.websocket.Topic.LOG,
|
||||
com.adityachandel.booklore.model.websocket.LogNotification.createLogNotification(
|
||||
"Error processing book file: " + bookFile.getFileName() + " - " + e.getMessage()
|
||||
)
|
||||
);
|
||||
|
||||
adminEventBroadcaster.broadcastAdminEvent("Error processing book file: " + bookFile.getFileName() + " - " + e.getMessage());
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
@@ -280,8 +258,10 @@ public class FolderAsBookFileProcessor implements LibraryFileProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
public record GetOrCreateBookResult(Optional<BookEntity> bookEntity, List<LibraryFile> remainingFiles) {}
|
||||
public record GetOrCreateBookResult(Optional<BookEntity> bookEntity, List<LibraryFile> remainingFiles) {
|
||||
}
|
||||
|
||||
public record CreateBookResult(BookEntity bookEntity, LibraryFile libraryFile) {}
|
||||
public record CreateBookResult(BookEntity bookEntity, LibraryFile libraryFile) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+17
-5
@@ -20,6 +20,7 @@ import com.adityachandel.booklore.repository.BookMetadataRepository;
|
||||
import com.adityachandel.booklore.repository.BookRepository;
|
||||
import com.adityachandel.booklore.service.BookQueryService;
|
||||
import com.adityachandel.booklore.service.NotificationService;
|
||||
import com.adityachandel.booklore.service.metadata.writer.MetadataWriter;
|
||||
import com.adityachandel.booklore.util.SecurityContextVirtualThread;
|
||||
import com.adityachandel.booklore.service.appsettings.AppSettingService;
|
||||
import com.adityachandel.booklore.service.fileprocessor.BookFileProcessor;
|
||||
@@ -44,6 +45,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.adityachandel.booklore.model.websocket.LogNotification.createLogNotification;
|
||||
@@ -148,14 +150,23 @@ public class BookMetadataService {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public BookMetadata handleCoverUpload(Long bookId, MultipartFile file) {
|
||||
public BookMetadata updateCoverImageFromFile(Long bookId, MultipartFile file) {
|
||||
fileService.createThumbnailFromFile(bookId, file);
|
||||
return updateCover(bookId, (writer, book) -> writer.replaceCoverImageFromUpload(book, file));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public BookMetadata updateCoverImageFromUrl(Long bookId, String url) {
|
||||
fileService.createThumbnailFromUrl(bookId, url);
|
||||
return updateCover(bookId, (writer, book) -> writer.replaceCoverImageFromUrl(book, url));
|
||||
}
|
||||
|
||||
private BookMetadata updateCover(Long bookId, BiConsumer<MetadataWriter, BookEntity> writerAction) {
|
||||
BookEntity bookEntity = bookRepository.findById(bookId).orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(bookId));
|
||||
bookEntity.getMetadata().setCoverUpdatedOn(Instant.now());
|
||||
boolean saveToOriginalFile = appSettingService.getAppSettings().getMetadataPersistenceSettings().isSaveToOriginalFile();
|
||||
if (saveToOriginalFile) {
|
||||
if (appSettingService.getAppSettings().getMetadataPersistenceSettings().isSaveToOriginalFile()) {
|
||||
metadataWriterFactory.getWriter(bookEntity.getBookType())
|
||||
.ifPresent(writer -> writer.replaceCoverImageFromUpload(bookEntity, file));
|
||||
.ifPresent(writer -> writerAction.accept(writer, bookEntity));
|
||||
}
|
||||
return bookMetadataMapper.toBookMetadata(bookEntity.getMetadata(), true);
|
||||
}
|
||||
@@ -261,4 +272,5 @@ public class BookMetadataService {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-5
@@ -59,6 +59,7 @@ public class BookMetadataUpdater {
|
||||
|
||||
boolean thumbnailRequiresUpdate = StringUtils.hasText(newMetadata.getThumbnailUrl());
|
||||
boolean hasMetadataChanges = MetadataChangeDetector.isDifferent(newMetadata, metadata, clearFlags);
|
||||
boolean hasValueChanges = MetadataChangeDetector.hasValueChanges(newMetadata, metadata, clearFlags);
|
||||
if (!thumbnailRequiresUpdate && !hasMetadataChanges) {
|
||||
log.info("No changes in metadata for book ID {}. Skipping update.", bookId);
|
||||
return;
|
||||
@@ -102,13 +103,13 @@ public class BookMetadataUpdater {
|
||||
log.warn("Failed to calculate metadata match score for book ID {}: {}", bookId, e.getMessage());
|
||||
}
|
||||
|
||||
if (writeToFile) {
|
||||
if ((writeToFile && hasValueChanges) || thumbnailRequiresUpdate) {
|
||||
metadataWriterFactory.getWriter(bookType).ifPresent(writer -> {
|
||||
try {
|
||||
String thumbnailUrl = setThumbnail ? newMetadata.getThumbnailUrl() : null;
|
||||
|
||||
if (StringUtils.hasText(thumbnailUrl) && isLocalOrPrivateUrl(thumbnailUrl)) {
|
||||
log.warn("Blocked local/private thumbnail URL: {}", thumbnailUrl);
|
||||
if ((StringUtils.hasText(thumbnailUrl) && isLocalOrPrivateUrl(thumbnailUrl) || Boolean.TRUE.equals(metadata.getCoverLocked()))) {
|
||||
log.debug("Blocked local/private thumbnail URL: {}", thumbnailUrl);
|
||||
thumbnailUrl = null;
|
||||
}
|
||||
|
||||
@@ -116,8 +117,6 @@ public class BookMetadataUpdater {
|
||||
writer.writeMetadataToFile(file, metadata, thumbnailUrl, false, clearFlags);
|
||||
String newHash = FileFingerprint.generateHash(bookEntity.getFullFilePath());
|
||||
bookEntity.setCurrentHash(newHash);
|
||||
log.info("Metadata written for book ID {}", bookId);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to write metadata for book ID {}: {}", bookId, e.getMessage());
|
||||
}
|
||||
|
||||
+1
-1
@@ -88,7 +88,7 @@ public class GoogleParser implements BookParser {
|
||||
.build()
|
||||
.toUri();
|
||||
|
||||
log.info("Google Books API URLx: {}", uri);
|
||||
log.info("Google Books API URL: {}", uri);
|
||||
|
||||
HttpClient client = HttpClient.newHttpClient();
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
|
||||
+74
-4
@@ -31,7 +31,7 @@ import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
@@ -44,6 +44,13 @@ public class EpubMetadataWriter implements MetadataWriter {
|
||||
|
||||
@Override
|
||||
public void writeMetadataToFile(File epubFile, BookMetadataEntity metadata, String thumbnailUrl, boolean restoreMode, MetadataClearFlags clear) {
|
||||
File backupFile = new File(epubFile.getParentFile(), epubFile.getName() + ".bak");
|
||||
try {
|
||||
Files.copy(epubFile.toPath(), backupFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (IOException ex) {
|
||||
log.warn("Failed to create backup of EPUB {}: {}", epubFile.getName(), ex.getMessage());
|
||||
return;
|
||||
}
|
||||
Path tempDir;
|
||||
try {
|
||||
tempDir = Files.createTempDirectory("epub_edit_" + UUID.randomUUID());
|
||||
@@ -155,7 +162,7 @@ public class EpubMetadataWriter implements MetadataWriter {
|
||||
hasChanges[0] = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (hasChanges[0]) {
|
||||
Transformer transformer = TransformerFactory.newInstance().newTransformer();
|
||||
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
|
||||
@@ -172,9 +179,24 @@ public class EpubMetadataWriter implements MetadataWriter {
|
||||
} else {
|
||||
log.info("No changes detected. Skipping EPUB write for: {}", epubFile.getName());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to write metadata to EPUB file {}: {}", epubFile.getName(), e.getMessage(), e);
|
||||
if (backupFile.exists()) {
|
||||
try {
|
||||
Files.copy(backupFile.toPath(), epubFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
log.info("Restored EPUB from backup: {}", epubFile.getName());
|
||||
} catch (IOException io) {
|
||||
log.error("Failed to restore EPUB from backup for {}: {}", epubFile.getName(), io.getMessage(), io);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (backupFile.exists()) {
|
||||
try {
|
||||
Files.delete(backupFile.toPath());
|
||||
} catch (IOException ex) {
|
||||
log.warn("Failed to delete backup for {}: {}", epubFile.getName(), ex.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,6 +289,53 @@ public class EpubMetadataWriter implements MetadataWriter {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void replaceCoverImageFromUrl(BookEntity bookEntity, String url) {
|
||||
if (url == null || url.isBlank()) {
|
||||
log.warn("Cover update via URL failed: empty or null URL.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
File epubFile = new File(bookEntity.getFullFilePath().toUri());
|
||||
Path tempDir = Files.createTempDirectory("epub_cover_url_" + UUID.randomUUID());
|
||||
new ZipFile(epubFile).extractAll(tempDir.toString());
|
||||
|
||||
File opfFile = findOpfFile(tempDir.toFile());
|
||||
if (opfFile == null) {
|
||||
log.warn("OPF file not found in EPUB: {}", epubFile.getName());
|
||||
return;
|
||||
}
|
||||
|
||||
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
|
||||
dbf.setNamespaceAware(true);
|
||||
DocumentBuilder builder = dbf.newDocumentBuilder();
|
||||
Document opfDoc = builder.parse(opfFile);
|
||||
|
||||
byte[] coverData = loadImage(url);
|
||||
if (coverData == null) {
|
||||
log.warn("Failed to load image from URL: {}", url);
|
||||
return;
|
||||
}
|
||||
|
||||
applyCoverImageToEpub(tempDir, opfDoc, coverData);
|
||||
|
||||
Transformer transformer = TransformerFactory.newInstance().newTransformer();
|
||||
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
|
||||
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
|
||||
transformer.transform(new DOMSource(opfDoc), new StreamResult(opfFile));
|
||||
|
||||
File tempEpub = new File(epubFile.getParentFile(), epubFile.getName() + ".tmp");
|
||||
addFolderContentsToZip(new ZipFile(tempEpub), tempDir.toFile(), tempDir.toFile());
|
||||
|
||||
if (!epubFile.delete()) throw new IOException("Could not delete original EPUB");
|
||||
if (!tempEpub.renameTo(epubFile)) throw new IOException("Could not rename temp EPUB");
|
||||
|
||||
log.info("Cover image updated in EPUB via URL: {}", epubFile.getName());
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to update EPUB with cover from URL: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public BookFileType getSupportedBookType() {
|
||||
return BookFileType.EPUB;
|
||||
@@ -466,4 +535,5 @@ public class EpubMetadataWriter implements MetadataWriter {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -15,5 +15,8 @@ public interface MetadataWriter {
|
||||
default void replaceCoverImageFromUpload(BookEntity bookEntity, MultipartFile file) {
|
||||
}
|
||||
|
||||
default void replaceCoverImageFromUrl(BookEntity bookEntity, String url) {
|
||||
}
|
||||
|
||||
BookFileType getSupportedBookType();
|
||||
}
|
||||
|
||||
+32
-3
@@ -28,6 +28,7 @@ import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
@@ -35,6 +36,7 @@ import java.util.Calendar;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@@ -47,21 +49,48 @@ public class PdfMetadataWriter implements MetadataWriter {
|
||||
return;
|
||||
}
|
||||
|
||||
Path filePath = file.toPath();
|
||||
Path backupPath = null;
|
||||
boolean backupCreated = false;
|
||||
File tempFile = null;
|
||||
|
||||
try {
|
||||
String prefix = "pdfBackup-" + UUID.randomUUID() + "-";
|
||||
backupPath = Files.createTempFile(prefix, ".pdf");
|
||||
Files.copy(filePath, backupPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
backupCreated = true;
|
||||
} catch (IOException e) {
|
||||
log.warn("Could not create PDF temp backup for {}: {}", file.getName(), e.getMessage());
|
||||
}
|
||||
|
||||
try (PDDocument pdf = Loader.loadPDF(file)) {
|
||||
pdf.setAllSecurityToBeRemoved(true);
|
||||
applyMetadataToDocument(pdf, metadataEntity, restoreMode, clear);
|
||||
tempFile = File.createTempFile("pdfmeta-", ".pdf");
|
||||
pdf.save(tempFile);
|
||||
Files.move(tempFile.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
Files.move(tempFile.toPath(), filePath, StandardCopyOption.REPLACE_EXISTING);
|
||||
log.info("Successfully embedded metadata into PDF: {}", file.getName());
|
||||
} catch (IOException e) {
|
||||
log.warn("Failed to write metadata to PDF {}", file.getName(), e);
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to write metadata to PDF {}: {}", file.getName(), e.getMessage(), e);
|
||||
if (backupCreated) {
|
||||
try {
|
||||
Files.copy(backupPath, filePath, StandardCopyOption.REPLACE_EXISTING);
|
||||
log.info("Restored PDF {} from temp backup after failure", file.getName());
|
||||
} catch (IOException ex) {
|
||||
log.error("Failed to restore PDF temp backup for {}: {}", file.getName(), ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (tempFile != null && tempFile.exists()) {
|
||||
tempFile.delete();
|
||||
}
|
||||
if (backupCreated) {
|
||||
try {
|
||||
Files.deleteIfExists(backupPath);
|
||||
} catch (IOException e) {
|
||||
log.warn("Could not delete PDF temp backup for {}: {}", file.getName(), e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -164,6 +164,13 @@ public class UserService {
|
||||
userRepository.save(user);
|
||||
}
|
||||
|
||||
public List<BookLoreUser> getUsersWithLibraryAccess(Long libraryId) {
|
||||
return userRepository.findAllByLibraries_Id(libraryId)
|
||||
.stream()
|
||||
.map(bookLoreUserTransformer::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private boolean meetsMinimumPasswordRequirements(String password) {
|
||||
return password != null && password.length() >= 6;
|
||||
}
|
||||
|
||||
+3
-3
@@ -43,12 +43,12 @@ public class BookFilePersistenceService {
|
||||
|
||||
boolean pathChanged = !Objects.equals(newSubPath, book.getFileSubPath()) || !Objects.equals(newLibraryPath.getId(), book.getLibraryPath().getId());
|
||||
|
||||
if (pathChanged) {
|
||||
if (pathChanged || Boolean.TRUE.equals(book.getDeleted())) {
|
||||
book.setLibraryPath(newLibraryPath);
|
||||
book.setFileSubPath(newSubPath);
|
||||
book.setDeleted(false);
|
||||
book.setDeleted(Boolean.FALSE);
|
||||
bookRepository.save(book);
|
||||
log.info("[FILE_CREATE] Updated path for existing book with hash '{}': '{}'", currentHash, path);
|
||||
log.info("[FILE_CREATE] Updated path / undeleted existing book with hash '{}': '{}'", currentHash, path);
|
||||
} else {
|
||||
log.info("[FILE_CREATE] Book with hash '{}' already exists at same path. Skipping update.", currentHash);
|
||||
}
|
||||
|
||||
+39
-11
@@ -1,7 +1,6 @@
|
||||
package com.adityachandel.booklore.service.watcher;
|
||||
|
||||
import com.adityachandel.booklore.exception.ApiError;
|
||||
import com.adityachandel.booklore.model.entity.BookEntity;
|
||||
import com.adityachandel.booklore.model.entity.LibraryEntity;
|
||||
import com.adityachandel.booklore.model.entity.LibraryPathEntity;
|
||||
import com.adityachandel.booklore.model.enums.BookFileExtension;
|
||||
@@ -19,7 +18,7 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.*;
|
||||
import java.util.List;
|
||||
import java.time.Duration;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.*;
|
||||
@@ -29,13 +28,17 @@ import java.util.concurrent.*;
|
||||
@AllArgsConstructor
|
||||
public class LibraryFileEventProcessor {
|
||||
|
||||
private static final long DEBOUNCE_MS = 500L;
|
||||
|
||||
private final BlockingQueue<FileEvent> eventQueue = new LinkedBlockingQueue<>();
|
||||
private final ExecutorService worker = Executors.newSingleThreadExecutor();
|
||||
private final LibraryRepository libraryRepository;
|
||||
private final BookFileTransactionalHandler bookFileTransactionalHandler;
|
||||
private final BookFilePersistenceService bookFilePersistenceService;
|
||||
private final NotificationService notificationService;
|
||||
|
||||
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
|
||||
private final ConcurrentMap<Path, ScheduledFuture<?>> pendingDeletes = new ConcurrentHashMap<>();
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
Thread.ofVirtual().start(() -> {
|
||||
@@ -54,17 +57,41 @@ public class LibraryFileEventProcessor {
|
||||
}
|
||||
|
||||
public void processFile(WatchEvent.Kind<?> eventKind, long libraryId, String libraryPath, String filePath) {
|
||||
eventQueue.offer(new FileEvent(eventKind, libraryId, libraryPath, filePath));
|
||||
Path path = Paths.get(filePath).toAbsolutePath().normalize();
|
||||
|
||||
if (eventKind == StandardWatchEventKinds.ENTRY_DELETE) {
|
||||
// Schedule DELETE after debounce
|
||||
ScheduledFuture<?> existing = pendingDeletes.put(path, scheduler.schedule(() -> {
|
||||
eventQueue.offer(new FileEvent(eventKind, libraryId, libraryPath, filePath));
|
||||
pendingDeletes.remove(path);
|
||||
}, DEBOUNCE_MS, TimeUnit.MILLISECONDS));
|
||||
|
||||
if (existing != null) existing.cancel(false);
|
||||
} else if (eventKind == StandardWatchEventKinds.ENTRY_CREATE) {
|
||||
// If a DELETE is pending for this path, cancel both DELETE and CREATE
|
||||
ScheduledFuture<?> pendingDelete = pendingDeletes.remove(path);
|
||||
if (pendingDelete != null) {
|
||||
pendingDelete.cancel(false);
|
||||
log.info("[DEBOUNCE] CREATE ignored because pending DELETE exists for '{}'", path);
|
||||
return;
|
||||
}
|
||||
// Otherwise process CREATE immediately
|
||||
eventQueue.offer(new FileEvent(eventKind, libraryId, libraryPath, filePath));
|
||||
} else {
|
||||
// Other events
|
||||
eventQueue.offer(new FileEvent(eventKind, libraryId, libraryPath, filePath));
|
||||
}
|
||||
}
|
||||
|
||||
private void handleEvent(FileEvent event) {
|
||||
Path path = Paths.get(event.filePath());
|
||||
Path path = Paths.get(event.filePath()).toAbsolutePath().normalize();
|
||||
String fileName = path.getFileName().toString();
|
||||
log.info("[PROCESS] '{}' event for '{}'", event.eventKind().name(), fileName);
|
||||
|
||||
LibraryEntity library = libraryRepository.findById(event.libraryId()).orElseThrow(() -> ApiError.LIBRARY_NOT_FOUND.createException(event.libraryId()));
|
||||
LibraryEntity library = libraryRepository.findById(event.libraryId())
|
||||
.orElseThrow(() -> ApiError.LIBRARY_NOT_FOUND.createException(event.libraryId()));
|
||||
|
||||
if (library.getLibraryPaths().stream().noneMatch(lp -> path.toString().startsWith(lp.getPath()))) {
|
||||
if (library.getLibraryPaths().stream().noneMatch(lp -> path.startsWith(lp.getPath()))) {
|
||||
log.warn("[SKIP] Path outside of library: '{}'", path);
|
||||
return;
|
||||
}
|
||||
@@ -102,7 +129,7 @@ public class LibraryFileEventProcessor {
|
||||
String libPath = bookFilePersistenceService.findMatchingLibraryPath(library, path);
|
||||
LibraryPathEntity libPathEntity = bookFilePersistenceService.getLibraryPathEntityForFile(library, libPath);
|
||||
|
||||
Path relPath = Paths.get(libPathEntity.getPath()).relativize(path.toAbsolutePath().normalize());
|
||||
Path relPath = Paths.get(libPathEntity.getPath()).relativize(path);
|
||||
String fileName = relPath.getFileName().toString();
|
||||
String fileSubPath = Optional.ofNullable(relPath.getParent()).map(Path::toString).orElse("");
|
||||
|
||||
@@ -110,7 +137,8 @@ public class LibraryFileEventProcessor {
|
||||
.ifPresentOrElse(book -> {
|
||||
book.setDeleted(true);
|
||||
bookFilePersistenceService.save(book);
|
||||
notificationService.sendMessageToPermissions(Topic.BOOKS_REMOVE, Set.of(book.getId()), Set.of(PermissionType.ADMIN, PermissionType.MANIPULATE_LIBRARY));
|
||||
notificationService.sendMessageToPermissions(Topic.BOOKS_REMOVE, Set.of(book.getId()),
|
||||
Set.of(PermissionType.ADMIN, PermissionType.MANIPULATE_LIBRARY));
|
||||
log.info("[MARKED_DELETED] Book '{}' marked as deleted", fileName);
|
||||
}, () -> log.warn("[NOT_FOUND] Book for deleted path '{}' not found", path));
|
||||
|
||||
@@ -161,8 +189,8 @@ public class LibraryFileEventProcessor {
|
||||
|
||||
@PreDestroy
|
||||
public void shutdown() {
|
||||
worker.shutdownNow();
|
||||
log.info("LibraryFileEventProcessor worker shutdown.");
|
||||
scheduler.shutdownNow();
|
||||
log.info("Shutting down LibraryFileEventProcessor...");
|
||||
}
|
||||
|
||||
public record FileEvent(WatchEvent.Kind<?> eventKind, long libraryId, String libraryPath, String filePath) {
|
||||
|
||||
+92
-18
@@ -2,15 +2,17 @@ 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 lombok.extern.slf4j.Slf4j;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.apache.commons.lang3.BooleanUtils.isTrue;
|
||||
|
||||
@Slf4j
|
||||
public class MetadataChangeDetector {
|
||||
|
||||
@@ -60,6 +62,69 @@ public class MetadataChangeDetector {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean hasValueChanges(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, "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()));
|
||||
compareValue(diffs, "goodreadsReviewCount", clear.isGoodreadsReviewCount(), newMeta.getGoodreadsReviewCount(), existingMeta.getGoodreadsReviewCount(), () -> !isTrue(existingMeta.getGoodreadsReviewCountLocked()));
|
||||
compareValue(diffs, "hardcoverRating", clear.isHardcoverRating(), newMeta.getHardcoverRating(), existingMeta.getHardcoverRating(), () -> !isTrue(existingMeta.getHardcoverRatingLocked()));
|
||||
compareValue(diffs, "hardcoverReviewCount", clear.isHardcoverReviewCount(), newMeta.getHardcoverReviewCount(), existingMeta.getHardcoverReviewCount(), () -> !isTrue(existingMeta.getHardcoverReviewCountLocked()));
|
||||
compareValue(diffs, "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();
|
||||
}
|
||||
|
||||
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.getReviewsLocked(), existingMeta.getReviewsLocked())) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void compare(List<String> diffs, String field, boolean shouldClear, Object newVal, Object oldVal, Supplier<Boolean> isUnlocked, Boolean newLock, Boolean oldLock) {
|
||||
boolean valueChanged = differs(shouldClear, newVal, oldVal, isUnlocked);
|
||||
boolean lockChanged = differsLock(newLock, oldLock);
|
||||
@@ -73,6 +138,17 @@ public class MetadataChangeDetector {
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> void compareValue(List<String> diffs,
|
||||
String field,
|
||||
boolean shouldClear,
|
||||
T newVal,
|
||||
T oldVal,
|
||||
Supplier<Boolean> isUnlocked) {
|
||||
if (differs(shouldClear, newVal, oldVal, isUnlocked)) {
|
||||
diffs.add(field + " changed");
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean differs(boolean shouldClear, Object newVal, Object oldVal, Supplier<Boolean> isUnlocked) {
|
||||
if (!isUnlocked.get()) return false;
|
||||
|
||||
@@ -109,21 +185,19 @@ public class MetadataChangeDetector {
|
||||
}
|
||||
|
||||
private static Set<String> toNameSet(Set<?> entities) {
|
||||
if (entities == null) return null;
|
||||
if (entities == null) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
return entities.stream()
|
||||
.map(e -> {
|
||||
try {
|
||||
return (String) e.getClass().getMethod("getName").invoke(e);
|
||||
} catch (Exception ex) {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.map(String::strip)
|
||||
.collect(Collectors.toSet());
|
||||
.map(e -> {
|
||||
if (e instanceof AuthorEntity author) {
|
||||
return author.getName();
|
||||
}
|
||||
if (e instanceof CategoryEntity category) {
|
||||
return category.getName();
|
||||
}
|
||||
return e.toString();
|
||||
})
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private static boolean isTrue(Boolean value) {
|
||||
return Boolean.TRUE.equals(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-15
@@ -5,9 +5,7 @@ import com.adityachandel.booklore.model.dto.settings.LibraryFile;
|
||||
import com.adityachandel.booklore.model.entity.LibraryEntity;
|
||||
import com.adityachandel.booklore.model.entity.LibraryPathEntity;
|
||||
import com.adityachandel.booklore.model.enums.BookFileType;
|
||||
import com.adityachandel.booklore.model.websocket.LogNotification;
|
||||
import com.adityachandel.booklore.model.websocket.Topic;
|
||||
import com.adityachandel.booklore.service.NotificationService;
|
||||
import com.adityachandel.booklore.service.event.BookEventBroadcaster;
|
||||
import com.adityachandel.booklore.service.fileprocessor.BookFileProcessor;
|
||||
import com.adityachandel.booklore.service.fileprocessor.BookFileProcessorRegistry;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
@@ -19,14 +17,13 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class FileAsBookProcessorTest {
|
||||
|
||||
@Mock
|
||||
private NotificationService notificationService;
|
||||
private BookEventBroadcaster bookEventBroadcaster;
|
||||
|
||||
@Mock
|
||||
private BookFileProcessorRegistry processorRegistry;
|
||||
@@ -102,9 +99,8 @@ class FileAsBookProcessorTest {
|
||||
fileAsBookProcessor.processLibraryFiles(libraryFiles, libraryEntity);
|
||||
|
||||
// Then
|
||||
verify(notificationService, times(2)).sendMessage(eq(Topic.BOOK_ADD), bookCaptor.capture());
|
||||
verify(notificationService, times(2)).sendMessage(eq(Topic.LOG), any(LogNotification.class));
|
||||
|
||||
verify(bookEventBroadcaster, times(2)).broadcastBookAddEvent(bookCaptor.capture());
|
||||
|
||||
List<Book> capturedBooks = bookCaptor.getAllValues();
|
||||
assertThat(capturedBooks).hasSize(2);
|
||||
assertThat(capturedBooks).containsExactly(book1, book2);
|
||||
@@ -150,8 +146,7 @@ class FileAsBookProcessorTest {
|
||||
fileAsBookProcessor.processLibraryFiles(libraryFiles, libraryEntity);
|
||||
|
||||
// Then
|
||||
verify(notificationService, times(1)).sendMessage(eq(Topic.BOOK_ADD), eq(book));
|
||||
verify(notificationService, times(1)).sendMessage(eq(Topic.LOG), any(LogNotification.class));
|
||||
verify(bookEventBroadcaster, times(1)).broadcastBookAddEvent(book);
|
||||
verify(processorRegistry, times(1)).getProcessorOrThrow(any());
|
||||
}
|
||||
|
||||
@@ -165,7 +160,7 @@ class FileAsBookProcessorTest {
|
||||
fileAsBookProcessor.processLibraryFiles(libraryFiles, libraryEntity);
|
||||
|
||||
// Then
|
||||
verify(notificationService, never()).sendMessage(any(Topic.class), any());
|
||||
verify(bookEventBroadcaster, never()).broadcastBookAddEvent(any());
|
||||
verify(processorRegistry, never()).getProcessorOrThrow(any());
|
||||
}
|
||||
|
||||
@@ -275,7 +270,7 @@ class FileAsBookProcessorTest {
|
||||
fileAsBookProcessor.processLibraryFiles(libraryFiles, libraryEntity);
|
||||
|
||||
// Then
|
||||
verify(notificationService, never()).sendMessage(any(Topic.class), any());
|
||||
verify(bookEventBroadcaster, never()).broadcastBookAddEvent(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -356,7 +351,6 @@ class FileAsBookProcessorTest {
|
||||
fileAsBookProcessor.processLibraryFiles(libraryFiles, libraryEntity);
|
||||
|
||||
// Then
|
||||
verify(notificationService, times(4)).sendMessage(eq(Topic.BOOK_ADD), any(Book.class));
|
||||
verify(notificationService, times(4)).sendMessage(eq(Topic.LOG), any(LogNotification.class));
|
||||
verify(bookEventBroadcaster, times(4)).broadcastBookAddEvent(any(Book.class));
|
||||
}
|
||||
}
|
||||
+6
-2
@@ -5,7 +5,8 @@ import com.adityachandel.booklore.model.enums.BookFileType;
|
||||
import com.adityachandel.booklore.repository.BookAdditionalFileRepository;
|
||||
import com.adityachandel.booklore.repository.BookRepository;
|
||||
import com.adityachandel.booklore.service.FileFingerprint;
|
||||
import com.adityachandel.booklore.service.NotificationService;
|
||||
import com.adityachandel.booklore.service.event.AdminEventBroadcaster;
|
||||
import com.adityachandel.booklore.service.event.BookEventBroadcaster;
|
||||
import com.adityachandel.booklore.service.fileprocessor.BookFileProcessor;
|
||||
import com.adityachandel.booklore.service.fileprocessor.BookFileProcessorRegistry;
|
||||
import com.adityachandel.booklore.util.FileUtils;
|
||||
@@ -30,7 +31,10 @@ class FolderAsBookFileProcessorExampleTest {
|
||||
private BookAdditionalFileRepository bookAdditionalFileRepository;
|
||||
|
||||
@Mock
|
||||
private NotificationService notificationService;
|
||||
private BookEventBroadcaster bookEventBroadcaster;
|
||||
|
||||
@Mock
|
||||
private AdminEventBroadcaster adminEventBroadcaster;
|
||||
|
||||
@Mock
|
||||
private BookFileProcessorRegistry bookFileProcessorRegistry;
|
||||
|
||||
+8
-4
@@ -4,6 +4,8 @@ import com.adityachandel.booklore.model.enums.LibraryScanMode;
|
||||
import com.adityachandel.booklore.repository.BookAdditionalFileRepository;
|
||||
import com.adityachandel.booklore.repository.BookRepository;
|
||||
import com.adityachandel.booklore.service.NotificationService;
|
||||
import com.adityachandel.booklore.service.event.AdminEventBroadcaster;
|
||||
import com.adityachandel.booklore.service.event.BookEventBroadcaster;
|
||||
import com.adityachandel.booklore.service.fileprocessor.BookFileProcessorRegistry;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -16,11 +18,12 @@ class FolderAsBookFileProcessorSimpleTest {
|
||||
void getScanMode_shouldReturnFolderAsBook() {
|
||||
BookRepository bookRepository = mock(BookRepository.class);
|
||||
BookAdditionalFileRepository bookAdditionalFileRepository = mock(BookAdditionalFileRepository.class);
|
||||
NotificationService notificationService = mock(NotificationService.class);
|
||||
AdminEventBroadcaster adminEventBroadcaster = mock(AdminEventBroadcaster.class);
|
||||
BookEventBroadcaster bookEventBroadcaster = mock(BookEventBroadcaster.class);
|
||||
BookFileProcessorRegistry bookFileProcessorRegistry = mock(BookFileProcessorRegistry.class);
|
||||
|
||||
FolderAsBookFileProcessor processor = new FolderAsBookFileProcessor(
|
||||
bookRepository, bookAdditionalFileRepository, notificationService, bookFileProcessorRegistry);
|
||||
bookRepository, bookAdditionalFileRepository, bookEventBroadcaster, adminEventBroadcaster, bookFileProcessorRegistry);
|
||||
|
||||
assertThat(processor.getScanMode()).isEqualTo(LibraryScanMode.FOLDER_AS_BOOK);
|
||||
}
|
||||
@@ -29,11 +32,12 @@ class FolderAsBookFileProcessorSimpleTest {
|
||||
void supportsSupplementaryFiles_shouldReturnTrue() {
|
||||
BookRepository bookRepository = mock(BookRepository.class);
|
||||
BookAdditionalFileRepository bookAdditionalFileRepository = mock(BookAdditionalFileRepository.class);
|
||||
NotificationService notificationService = mock(NotificationService.class);
|
||||
AdminEventBroadcaster adminEventBroadcaster = mock(AdminEventBroadcaster.class);
|
||||
BookEventBroadcaster bookEventBroadcaster = mock(BookEventBroadcaster.class);
|
||||
BookFileProcessorRegistry bookFileProcessorRegistry = mock(BookFileProcessorRegistry.class);
|
||||
|
||||
FolderAsBookFileProcessor processor = new FolderAsBookFileProcessor(
|
||||
bookRepository, bookAdditionalFileRepository, notificationService, bookFileProcessorRegistry);
|
||||
bookRepository, bookAdditionalFileRepository, bookEventBroadcaster, adminEventBroadcaster, bookFileProcessorRegistry);
|
||||
|
||||
assertThat(processor.supportsSupplementaryFiles()).isTrue();
|
||||
}
|
||||
|
||||
+22
-9
@@ -6,12 +6,11 @@ import com.adityachandel.booklore.model.entity.*;
|
||||
import com.adityachandel.booklore.model.enums.AdditionalFileType;
|
||||
import com.adityachandel.booklore.model.enums.BookFileType;
|
||||
import com.adityachandel.booklore.model.enums.LibraryScanMode;
|
||||
import com.adityachandel.booklore.model.websocket.LogNotification;
|
||||
import com.adityachandel.booklore.model.websocket.Topic;
|
||||
import com.adityachandel.booklore.service.event.BookEventBroadcaster;
|
||||
import com.adityachandel.booklore.service.event.AdminEventBroadcaster;
|
||||
import com.adityachandel.booklore.repository.BookAdditionalFileRepository;
|
||||
import com.adityachandel.booklore.repository.BookRepository;
|
||||
import com.adityachandel.booklore.service.FileFingerprint;
|
||||
import com.adityachandel.booklore.service.NotificationService;
|
||||
import com.adityachandel.booklore.service.fileprocessor.BookFileProcessor;
|
||||
import com.adityachandel.booklore.service.fileprocessor.BookFileProcessorRegistry;
|
||||
import com.adityachandel.booklore.util.FileUtils;
|
||||
@@ -43,7 +42,10 @@ class FolderAsBookFileProcessorTest {
|
||||
private BookAdditionalFileRepository bookAdditionalFileRepository;
|
||||
|
||||
@Mock
|
||||
private NotificationService notificationService;
|
||||
private BookEventBroadcaster bookEventBroadcaster;
|
||||
|
||||
@Mock
|
||||
private AdminEventBroadcaster adminEventBroadcaster;
|
||||
|
||||
@Mock
|
||||
private BookFileProcessorRegistry bookFileProcessorRegistry;
|
||||
@@ -127,8 +129,8 @@ class FolderAsBookFileProcessorTest {
|
||||
|
||||
// Then
|
||||
verify(mockBookFileProcessor).processFile(any(LibraryFile.class));
|
||||
verify(notificationService).sendMessage(Topic.BOOK_ADD, createdBook);
|
||||
verify(notificationService).sendMessage(eq(Topic.LOG), any(LogNotification.class));
|
||||
verify(bookEventBroadcaster).broadcastBookAddEvent(createdBook);
|
||||
verify(adminEventBroadcaster, never()).broadcastAdminEvent(anyString());
|
||||
verify(bookAdditionalFileRepository, times(2)).save(additionalFileCaptor.capture());
|
||||
|
||||
List<BookAdditionalFileEntity> capturedFiles = additionalFileCaptor.getAllValues();
|
||||
@@ -160,7 +162,8 @@ class FolderAsBookFileProcessorTest {
|
||||
|
||||
// Then
|
||||
verify(mockBookFileProcessor, never()).processFile(any());
|
||||
verify(notificationService, never()).sendMessage(eq(Topic.BOOK_ADD), any());
|
||||
verify(bookEventBroadcaster, never()).broadcastBookAddEvent(any());
|
||||
verify(adminEventBroadcaster, never()).broadcastAdminEvent(anyString());
|
||||
verify(bookAdditionalFileRepository, times(2)).save(additionalFileCaptor.capture());
|
||||
|
||||
List<BookAdditionalFileEntity> capturedFiles = additionalFileCaptor.getAllValues();
|
||||
@@ -192,6 +195,8 @@ class FolderAsBookFileProcessorTest {
|
||||
|
||||
// Then
|
||||
verify(mockBookFileProcessor, never()).processFile(any());
|
||||
verify(bookEventBroadcaster, never()).broadcastBookAddEvent(any());
|
||||
verify(adminEventBroadcaster, never()).broadcastAdminEvent(anyString());
|
||||
verify(bookAdditionalFileRepository, times(2)).save(additionalFileCaptor.capture());
|
||||
|
||||
List<BookAdditionalFileEntity> capturedFiles = additionalFileCaptor.getAllValues();
|
||||
@@ -236,6 +241,8 @@ class FolderAsBookFileProcessorTest {
|
||||
|
||||
// Then
|
||||
verify(mockBookFileProcessor).processFile(argThat(file -> file.getFileName().equals("book.epub")));
|
||||
verify(bookEventBroadcaster).broadcastBookAddEvent(createdBook);
|
||||
verify(adminEventBroadcaster, never()).broadcastAdminEvent(anyString());
|
||||
verify(bookAdditionalFileRepository, times(2)).save(additionalFileCaptor.capture());
|
||||
|
||||
List<BookAdditionalFileEntity> capturedFiles = additionalFileCaptor.getAllValues();
|
||||
@@ -279,6 +286,8 @@ class FolderAsBookFileProcessorTest {
|
||||
|
||||
// Then
|
||||
verify(mockBookFileProcessor).processFile(argThat(file -> file.getFileName().equals("book.pdf")));
|
||||
verify(bookEventBroadcaster).broadcastBookAddEvent(createdBook);
|
||||
verify(adminEventBroadcaster, never()).broadcastAdminEvent(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -310,6 +319,8 @@ class FolderAsBookFileProcessorTest {
|
||||
processor.processLibraryFiles(libraryFiles, libraryEntity);
|
||||
|
||||
// Then
|
||||
verify(bookEventBroadcaster, never()).broadcastBookAddEvent(any());
|
||||
verify(adminEventBroadcaster, never()).broadcastAdminEvent(anyString());
|
||||
verify(bookAdditionalFileRepository, times(1)).save(additionalFileCaptor.capture());
|
||||
|
||||
BookAdditionalFileEntity capturedFile = additionalFileCaptor.getValue();
|
||||
@@ -333,7 +344,8 @@ class FolderAsBookFileProcessorTest {
|
||||
|
||||
// Then
|
||||
verify(mockBookFileProcessor, never()).processFile(any());
|
||||
verify(notificationService, never()).sendMessage(eq(Topic.BOOK_ADD), any());
|
||||
verify(bookEventBroadcaster, never()).broadcastBookAddEvent(any());
|
||||
verify(adminEventBroadcaster, never()).broadcastAdminEvent(anyString());
|
||||
verify(bookAdditionalFileRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@@ -356,8 +368,9 @@ class FolderAsBookFileProcessorTest {
|
||||
processor.processLibraryFiles(libraryFiles, libraryEntity);
|
||||
|
||||
// Then
|
||||
verify(notificationService, never()).sendMessage(eq(Topic.BOOK_ADD), any());
|
||||
verify(bookEventBroadcaster, never()).broadcastBookAddEvent(any());
|
||||
verify(bookAdditionalFileRepository, never()).save(any());
|
||||
verify(adminEventBroadcaster).broadcastAdminEvent(anyString());
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
|
||||
@@ -316,7 +316,7 @@ export class BookService {
|
||||
const currentState = this.bookStateSubject.value;
|
||||
const updatedBooks = (currentState.books || []).map(book => {
|
||||
if (book.id === bookId) {
|
||||
const updatedBook = { ...book };
|
||||
const updatedBook = {...book};
|
||||
if (fileType === AdditionalFileType.ALTERNATIVE_FORMAT) {
|
||||
updatedBook.alternativeFormats = [...(book.alternativeFormats || []), newFile];
|
||||
} else {
|
||||
@@ -538,7 +538,17 @@ export class BookService {
|
||||
}
|
||||
|
||||
getUploadCoverUrl(bookId: number): string {
|
||||
return this.url + '/' + bookId + "/metadata/cover"
|
||||
return this.url + '/' + bookId + "/metadata/cover/upload"
|
||||
}
|
||||
|
||||
uploadCoverFromUrl(bookId: number, url: string): Observable<BookMetadata> {
|
||||
return this.http
|
||||
.post<BookMetadata>(`${this.url}/${bookId}/metadata/cover/from-url`, {url})
|
||||
.pipe(
|
||||
tap(updatedMetadata =>
|
||||
this.handleBookMetadataUpdate(bookId, updatedMetadata)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
getBookRecommendations(bookId: number, limit: number = 20): Observable<BookRecommendation[]> {
|
||||
@@ -688,4 +698,5 @@ export class BookService {
|
||||
getBackupMetadata(bookId: number) {
|
||||
return this.http.get<any>(`${this.url}/${bookId}/metadata/restore`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-7
@@ -605,12 +605,5 @@ export class MetadataEditorComponent implements OnInit {
|
||||
position: 'absolute'
|
||||
},
|
||||
});
|
||||
|
||||
ref.onClose.subscribe(result => {
|
||||
if (result) {
|
||||
this.metadataForm.get('thumbnailUrl')?.setValue(result);
|
||||
this.onSave();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,3 +97,4 @@
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import {Component, inject, Input, OnInit} from '@angular/core';
|
||||
import {Toast} from 'primeng/toast';
|
||||
import {MessageService} from 'primeng/api';
|
||||
import {FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators} from '@angular/forms';
|
||||
import {BookCoverService, CoverFetchRequest, CoverImage} from '../../shared/services/book-cover.service';
|
||||
import {finalize} from 'rxjs/operators';
|
||||
@@ -28,14 +30,13 @@ export class CoverSearchComponent implements OnInit {
|
||||
coverImages: CoverImage[] = [];
|
||||
loading = false;
|
||||
hasSearched = false;
|
||||
showPreview = false;
|
||||
previewImageUrl = '';
|
||||
|
||||
private fb = inject(FormBuilder);
|
||||
private bookCoverService = inject(BookCoverService);
|
||||
private dynamicDialogConfig = inject(DynamicDialogConfig);
|
||||
protected dynamicDialogRef = inject(DynamicDialogRef);
|
||||
protected bookService = inject(BookService);
|
||||
private messageService = inject(MessageService);
|
||||
|
||||
constructor() {
|
||||
this.searchForm = this.fb.group({
|
||||
@@ -87,26 +88,24 @@ export class CoverSearchComponent implements OnInit {
|
||||
}
|
||||
|
||||
selectAndSave(image: CoverImage) {
|
||||
this.dynamicDialogRef.close(image.url);
|
||||
}
|
||||
|
||||
previewImage(imageUrl: string) {
|
||||
this.previewImageUrl = imageUrl;
|
||||
this.showPreview = true;
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
closePreview() {
|
||||
this.showPreview = false;
|
||||
this.previewImageUrl = '';
|
||||
document.body.style.overflow = 'auto';
|
||||
}
|
||||
|
||||
onImageError(event: Event) {
|
||||
const target = event.target as HTMLImageElement;
|
||||
if (target) {
|
||||
target.style.display = 'none';
|
||||
}
|
||||
this.bookService.uploadCoverFromUrl(this.bookId, image.url)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.messageService.add({
|
||||
severity: 'success',
|
||||
summary: 'Cover Updated',
|
||||
detail: 'Cover image updated successfully.'
|
||||
});
|
||||
this.dynamicDialogRef.close();
|
||||
},
|
||||
error: err => {
|
||||
this.messageService.add({
|
||||
severity: 'error',
|
||||
summary: 'Cover Update Failed',
|
||||
detail: err?.message || 'Failed to update cover image.'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onClear() {
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ export class MetadataAdvancedFetchOptionsComponent implements OnChanges {
|
||||
'seriesName', 'seriesNumber', 'seriesTotal', 'isbn13', 'isbn10',
|
||||
'language', 'categories', 'cover'
|
||||
];
|
||||
providers: string[] = ['Amazon', 'Google', 'GoodReads', 'Hardcover', 'Comicvine'];
|
||||
providers: string[] = ['Amazon', 'Google', 'GoodReads', 'Hardcover', 'Comicvine', 'Douban'];
|
||||
|
||||
refreshCovers: boolean = false;
|
||||
mergeCategories: boolean = false;
|
||||
|
||||
Reference in New Issue
Block a user