From 7728cebcf92b99c78ff2802980f6895e7d3eb5c6 Mon Sep 17 00:00:00 2001 From: "aditya.chandel" <> Date: Sun, 12 Jan 2025 13:04:03 -0700 Subject: [PATCH] WIP: Metadata v2 refactor almost complete --- .../booklore/config/BookParserConfig.java | 24 ++ .../controller/MetadataController.java | 18 +- .../booklore/exception/ApiError.java | 3 +- .../dto/request/MetadataRefreshOptions.java | 4 +- .../dto/request/MetadataRefreshRequest.java | 5 + .../booklore/quartz/JobSchedulerService.java | 20 +- .../booklore/quartz/QuartzConfig.java | 8 - .../quartz/RefreshBooksMetadataJob.java | 34 --- .../quartz/RefreshLibraryMetadataJob.java | 31 --- ...dataJobV2.java => RefreshMetadataJob.java} | 5 +- .../service/metadata/BookMetadataService.java | 245 ++++++++---------- booklore-ui/package-lock.json | 8 +- booklore-ui/package.json | 2 +- .../metadata-searcher.component.ts | 2 +- .../book-browser/book-browser.component.ts | 42 +-- .../layout/app.topbar.component.html | 4 +- .../book-auto-metadata-refresh.model.ts | 2 +- .../fetch-metadata-request.model.ts | 0 .../library-auto-metadata-refresh.model.ts | 2 +- .../metadata-refresh-options.model.ts | 19 ++ .../metadata-refresh-request.model.ts | 9 + .../metadata/metadata-refresh-type.enum.ts | 4 + .../service/library-shelf-menu.service.ts | 4 +- .../src/app/book/service/metadata.service.ts | 28 +- ...tadata-advanced-fetch-options.component.ts | 24 +- ...etadata-basic-fetch-options.component.html | 15 +- .../metadata-basic-fetch-options.component.ts | 22 +- .../metadata-fetch-options.component.html | 6 +- .../metadata-fetch-options.component.ts | 67 +++-- .../assets/layout/styles/layout/_topbar.scss | 2 +- 30 files changed, 268 insertions(+), 391 deletions(-) create mode 100644 booklore-api/src/main/java/com/adityachandel/booklore/config/BookParserConfig.java delete mode 100644 booklore-api/src/main/java/com/adityachandel/booklore/quartz/RefreshBooksMetadataJob.java delete mode 100644 booklore-api/src/main/java/com/adityachandel/booklore/quartz/RefreshLibraryMetadataJob.java rename booklore-api/src/main/java/com/adityachandel/booklore/quartz/{RefreshMetadataJobV2.java => RefreshMetadataJob.java} (83%) rename booklore-ui/src/app/book/model/request/{ => metadata}/book-auto-metadata-refresh.model.ts (70%) rename booklore-ui/src/app/book/model/request/{ => metadata}/fetch-metadata-request.model.ts (100%) rename booklore-ui/src/app/book/model/request/{ => metadata}/library-auto-metadata-refresh.model.ts (71%) create mode 100644 booklore-ui/src/app/book/model/request/metadata/metadata-refresh-options.model.ts create mode 100644 booklore-ui/src/app/book/model/request/metadata/metadata-refresh-request.model.ts create mode 100644 booklore-ui/src/app/book/model/request/metadata/metadata-refresh-type.enum.ts diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/config/BookParserConfig.java b/booklore-api/src/main/java/com/adityachandel/booklore/config/BookParserConfig.java new file mode 100644 index 000000000..80551ffe0 --- /dev/null +++ b/booklore-api/src/main/java/com/adityachandel/booklore/config/BookParserConfig.java @@ -0,0 +1,24 @@ +package com.adityachandel.booklore.config; + +import com.adityachandel.booklore.service.metadata.model.MetadataProvider; +import com.adityachandel.booklore.service.metadata.parser.AmazonBookParser; +import com.adityachandel.booklore.service.metadata.parser.BookParser; +import com.adityachandel.booklore.service.metadata.parser.GoodReadsParser; +import com.adityachandel.booklore.service.metadata.parser.GoogleParser; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.Map; + +@Configuration +public class BookParserConfig { + + @Bean + public Map parserMap(GoogleParser googleParser, AmazonBookParser amazonBookParser, GoodReadsParser goodReadsParser) { + return Map.of( + MetadataProvider.Amazon, amazonBookParser, + MetadataProvider.GoodReads, goodReadsParser, + MetadataProvider.Google, googleParser + ); + } +} diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/controller/MetadataController.java b/booklore-api/src/main/java/com/adityachandel/booklore/controller/MetadataController.java index 4d24a6ca7..1ff4b7c5d 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/controller/MetadataController.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/controller/MetadataController.java @@ -13,6 +13,7 @@ import com.adityachandel.booklore.service.metadata.model.FetchedBookMetadata; import com.adityachandel.booklore.service.metadata.model.MetadataProvider; import lombok.AllArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -29,7 +30,7 @@ public class MetadataController { @PostMapping("/{bookId}") public ResponseEntity> getBookMetadata(@RequestBody(required = false) FetchMetadataRequest fetchMetadataRequest, @PathVariable Long bookId) { - return ResponseEntity.ok(bookMetadataService.fetchMetadataList(bookId, fetchMetadataRequest)); + return ResponseEntity.ok(bookMetadataService.fetchMetadataForRequest(bookId, fetchMetadataRequest)); } @PutMapping("/{bookId}") @@ -44,22 +45,9 @@ public class MetadataController { return ResponseEntity.ok(bookMetadata); } - @PutMapping(path = "/library/{libraryId}/refresh") - public ResponseEntity scheduleRefresh(@PathVariable Long libraryId, @RequestBody LibraryMetadataRefreshRequest request) { - jobSchedulerService.scheduleMetadataRefresh(request); - return ResponseEntity.noContent().build(); - } - @PutMapping(path = "/refreshV2") - public ResponseEntity scheduleRefreshV2(@RequestBody MetadataRefreshRequest request) { + public ResponseEntity scheduleRefreshV2(@Validated @RequestBody MetadataRefreshRequest request) { jobSchedulerService.scheduleMetadataRefreshV2(request); return ResponseEntity.noContent().build(); } - - @PutMapping(path = "/books/refresh") - public ResponseEntity scheduleBookMetadataRefresh(@RequestBody BooksMetadataRefreshRequest request) { - jobSchedulerService.scheduleBookMetadataRefresh(request); - return ResponseEntity.noContent().build(); - } - } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/exception/ApiError.java b/booklore-api/src/main/java/com/adityachandel/booklore/exception/ApiError.java index 12cda1b86..afb641614 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/exception/ApiError.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/exception/ApiError.java @@ -21,7 +21,8 @@ public enum ApiError { SHELF_NOT_FOUND(HttpStatus.NOT_FOUND, "Shelf not found with ID: %d"), SCHEDULE_REFRESH_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to schedule metadata refresh job. Error: %s"), ANOTHER_METADATA_JOB_RUNNING(HttpStatus.CONFLICT, "A metadata refresh job is currently running. Please wait for it to complete before initiating a new one."), - METADATA_SOURCE_NOT_IMPLEMENT_OR_DOES_NOT_EXIST(HttpStatus.BAD_REQUEST, "Metadata source not implement or does not exist"); + METADATA_SOURCE_NOT_IMPLEMENT_OR_DOES_NOT_EXIST(HttpStatus.BAD_REQUEST, "Metadata source not implement or does not exist"), + INVALID_REFRESH_TYPE(HttpStatus.BAD_REQUEST, "The refresh type is invalid"),; private final HttpStatus status; private final String message; diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/request/MetadataRefreshOptions.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/request/MetadataRefreshOptions.java index 86230f3a7..97154acb8 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/request/MetadataRefreshOptions.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/request/MetadataRefreshOptions.java @@ -2,6 +2,7 @@ package com.adityachandel.booklore.model.dto.request; import com.adityachandel.booklore.service.metadata.model.MetadataProvider; import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.Setter; import lombok.NoArgsConstructor; @@ -12,8 +13,9 @@ import lombok.AllArgsConstructor; @NoArgsConstructor @AllArgsConstructor public class MetadataRefreshOptions { - + @NotNull(message = "Default Provider cannot be null") private MetadataProvider defaultProvider; + private boolean refreshCovers; private FieldOptions fieldOptions; diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/request/MetadataRefreshRequest.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/request/MetadataRefreshRequest.java index ca87c9f4f..eac63b886 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/request/MetadataRefreshRequest.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/request/MetadataRefreshRequest.java @@ -1,14 +1,19 @@ package com.adityachandel.booklore.model.dto.request; +import jakarta.validation.constraints.NotNull; import lombok.Data; import java.util.Set; @Data public class MetadataRefreshRequest { + @NotNull(message = "Refresh type cannot be null") private RefreshType refreshType; + private Long libraryId; private Set bookIds; + + @NotNull(message = "Refresh options cannot be null") private MetadataRefreshOptions refreshOptions; public enum RefreshType { diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/quartz/JobSchedulerService.java b/booklore-api/src/main/java/com/adityachandel/booklore/quartz/JobSchedulerService.java index 822cc8e06..24aeb1f98 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/quartz/JobSchedulerService.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/quartz/JobSchedulerService.java @@ -1,8 +1,6 @@ package com.adityachandel.booklore.quartz; import com.adityachandel.booklore.exception.ApiError; -import com.adityachandel.booklore.model.dto.request.BooksMetadataRefreshRequest; -import com.adityachandel.booklore.model.dto.request.LibraryMetadataRefreshRequest; import com.adityachandel.booklore.model.dto.request.MetadataRefreshRequest; import lombok.AllArgsConstructor; import org.quartz.*; @@ -14,31 +12,23 @@ public class JobSchedulerService { private final Scheduler scheduler; - public void scheduleMetadataRefresh(LibraryMetadataRefreshRequest request) { - scheduleJob(request, RefreshLibraryMetadataJob.class, "libraryMetadataJob"); - } - public void scheduleMetadataRefreshV2(MetadataRefreshRequest request) { - scheduleJob(request, RefreshMetadataJobV2.class, "libraryMetadataJobV2"); + scheduleJob(request); } - public void scheduleBookMetadataRefresh(BooksMetadataRefreshRequest request) { - scheduleJob(request, RefreshBooksMetadataJob.class, "booksMetadataJob"); - } - - private void scheduleJob(T request, Class jobClass, String name) { + private void scheduleJob(T request) { try { JobDataMap jobDataMap = new JobDataMap(); jobDataMap.put("request", request); - JobDetail jobDetail = JobBuilder.newJob(jobClass) - .withIdentity(name, "Metadata") + JobDetail jobDetail = JobBuilder.newJob(RefreshMetadataJob.class) + .withIdentity("metadataRefreshJobV2", "metadataRefreshJobV2") .usingJobData(jobDataMap) .build(); Trigger trigger = TriggerBuilder.newTrigger() .forJob(jobDetail) - .withIdentity(name, "Metadata") + .withIdentity("metadataRefreshJobV2", "metadataRefreshJobV2") .startNow() .build(); diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/quartz/QuartzConfig.java b/booklore-api/src/main/java/com/adityachandel/booklore/quartz/QuartzConfig.java index 1f2409259..74ffea91d 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/quartz/QuartzConfig.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/quartz/QuartzConfig.java @@ -20,12 +20,4 @@ public class QuartzConfig { jobFactory.setApplicationContext(applicationContext); return jobFactory; } - - @Bean - public JobDetail refreshMetadataJobDetail() { - return JobBuilder.newJob(RefreshLibraryMetadataJob.class) - .withIdentity("refreshMetadataJob") - .storeDurably() - .build(); - } } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/quartz/RefreshBooksMetadataJob.java b/booklore-api/src/main/java/com/adityachandel/booklore/quartz/RefreshBooksMetadataJob.java deleted file mode 100644 index af9151349..000000000 --- a/booklore-api/src/main/java/com/adityachandel/booklore/quartz/RefreshBooksMetadataJob.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.adityachandel.booklore.quartz; - -import com.adityachandel.booklore.model.dto.request.BooksMetadataRefreshRequest; -import com.adityachandel.booklore.model.dto.request.LibraryMetadataRefreshRequest; -import com.adityachandel.booklore.service.metadata.BookMetadataService; -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.quartz.DisallowConcurrentExecution; -import org.quartz.Job; -import org.quartz.JobExecutionContext; -import org.quartz.JobExecutionException; -import org.springframework.stereotype.Component; - -import java.util.stream.Collectors; - -@Slf4j -@Component -@AllArgsConstructor -@DisallowConcurrentExecution -public class RefreshBooksMetadataJob implements Job { - - private BookMetadataService bookMetadataService; - - @Override - public void execute(JobExecutionContext context) throws JobExecutionException { - try { - BooksMetadataRefreshRequest request = (BooksMetadataRefreshRequest) context.getMergedJobDataMap().get("request"); - log.info("Refreshing metadata for Books: {}", request.getBookIds().stream().map(String::valueOf).collect(Collectors.joining(", "))); - bookMetadataService.refreshBooksMetadata(request); - } catch (Exception e) { - throw new JobExecutionException("Error occurred while executing metadata refresh job", e); - } - } -} diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/quartz/RefreshLibraryMetadataJob.java b/booklore-api/src/main/java/com/adityachandel/booklore/quartz/RefreshLibraryMetadataJob.java deleted file mode 100644 index 9b96ac1f9..000000000 --- a/booklore-api/src/main/java/com/adityachandel/booklore/quartz/RefreshLibraryMetadataJob.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.adityachandel.booklore.quartz; - -import com.adityachandel.booklore.model.dto.request.LibraryMetadataRefreshRequest; -import com.adityachandel.booklore.service.metadata.BookMetadataService; -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.quartz.DisallowConcurrentExecution; -import org.quartz.Job; -import org.quartz.JobExecutionContext; -import org.quartz.JobExecutionException; -import org.springframework.stereotype.Component; - -@Slf4j -@Component -@AllArgsConstructor -@DisallowConcurrentExecution -public class RefreshLibraryMetadataJob implements Job { - - private BookMetadataService bookMetadataService; - - @Override - public void execute(JobExecutionContext context) throws JobExecutionException { - try { - LibraryMetadataRefreshRequest request = (LibraryMetadataRefreshRequest) context.getMergedJobDataMap().get("request"); - log.info("Refreshing metadata for library ID: {}", request.getLibraryId()); - bookMetadataService.refreshLibraryMetadata(request); - } catch (Exception e) { - throw new JobExecutionException("Error occurred while executing metadata refresh job", e); - } - } -} diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/quartz/RefreshMetadataJobV2.java b/booklore-api/src/main/java/com/adityachandel/booklore/quartz/RefreshMetadataJob.java similarity index 83% rename from booklore-api/src/main/java/com/adityachandel/booklore/quartz/RefreshMetadataJobV2.java rename to booklore-api/src/main/java/com/adityachandel/booklore/quartz/RefreshMetadataJob.java index b66a12ca2..c7f8537b8 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/quartz/RefreshMetadataJobV2.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/quartz/RefreshMetadataJob.java @@ -1,6 +1,5 @@ package com.adityachandel.booklore.quartz; -import com.adityachandel.booklore.model.dto.request.MetadataRefreshOptions; import com.adityachandel.booklore.model.dto.request.MetadataRefreshRequest; import com.adityachandel.booklore.service.metadata.BookMetadataService; import lombok.AllArgsConstructor; @@ -15,7 +14,7 @@ import org.springframework.stereotype.Component; @Component @AllArgsConstructor @DisallowConcurrentExecution -public class RefreshMetadataJobV2 implements Job { +public class RefreshMetadataJob implements Job { private BookMetadataService bookMetadataService; @@ -23,7 +22,7 @@ public class RefreshMetadataJobV2 implements Job { public void execute(JobExecutionContext context) throws JobExecutionException { try { MetadataRefreshRequest request = (MetadataRefreshRequest) context.getMergedJobDataMap().get("request"); - bookMetadataService.refreshMetadataV2(request); + bookMetadataService.refreshMetadata(request); } catch (Exception e) { throw new JobExecutionException("Error occurred while executing metadata refresh job", e); } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/BookMetadataService.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/BookMetadataService.java index 29a735fb0..b5263fe3c 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/BookMetadataService.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/BookMetadataService.java @@ -4,8 +4,6 @@ import com.adityachandel.booklore.exception.ApiError; import com.adityachandel.booklore.mapper.BookMapper; import com.adityachandel.booklore.model.dto.Author; import com.adityachandel.booklore.model.dto.Book; -import com.adityachandel.booklore.model.dto.request.BooksMetadataRefreshRequest; -import com.adityachandel.booklore.model.dto.request.LibraryMetadataRefreshRequest; import com.adityachandel.booklore.model.dto.request.MetadataRefreshOptions; import com.adityachandel.booklore.model.dto.request.MetadataRefreshRequest; import com.adityachandel.booklore.model.entity.BookEntity; @@ -18,9 +16,7 @@ import com.adityachandel.booklore.service.NotificationService; import com.adityachandel.booklore.service.metadata.model.FetchMetadataRequest; import com.adityachandel.booklore.service.metadata.model.FetchedBookMetadata; import com.adityachandel.booklore.service.metadata.model.MetadataProvider; -import com.adityachandel.booklore.service.metadata.parser.AmazonBookParser; -import com.adityachandel.booklore.service.metadata.parser.GoodReadsParser; -import com.adityachandel.booklore.service.metadata.parser.GoogleParser; +import com.adityachandel.booklore.service.metadata.parser.BookParser; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -33,34 +29,32 @@ import java.util.concurrent.ThreadLocalRandom; import java.util.stream.Collectors; import static com.adityachandel.booklore.model.stomp.LogNotification.createLogNotification; +import static com.adityachandel.booklore.service.metadata.model.MetadataProvider.*; @Slf4j @Service @AllArgsConstructor public class BookMetadataService { - private final GoogleParser googleParser; - private final AmazonBookParser amazonBookParser; - private final GoodReadsParser goodReadsParser; private final BookRepository bookRepository; private final LibraryRepository libraryRepository; private final BookMapper bookMapper; private final BookMetadataUpdater bookMetadataUpdater; private final NotificationService notificationService; + private final Map parserMap; - public List fetchMetadataList(long bookId, FetchMetadataRequest request) { + + public List fetchMetadataForRequest(long bookId, FetchMetadataRequest request) { BookEntity bookEntity = bookRepository.findById(bookId).orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(bookId)); - Book book = bookMapper.toBook(bookEntity); - List>> futures = request.getProviders().stream() - .map(provider -> CompletableFuture.supplyAsync(() -> fetchMetadataFromProvider(provider, book, request)) + List> allMetadata = request.getProviders().stream() + .map(provider -> CompletableFuture.supplyAsync(() -> fetchMetadataListFromAProvider(provider, bookMapper.toBook(bookEntity), request)) .exceptionally(e -> { log.error("Error fetching metadata from provider: {}", provider, e); return List.of(); })) - .toList(); - - List> allMetadata = futures.stream().map(CompletableFuture::join).toList(); + .toList() + .stream().map(CompletableFuture::join).toList(); List interleavedMetadata = new ArrayList<>(); int maxSize = allMetadata.stream().mapToInt(List::size).max().orElse(0); @@ -76,73 +70,34 @@ public class BookMetadataService { return interleavedMetadata; } - private List fetchMetadataFromProvider(MetadataProvider provider, Book book, FetchMetadataRequest request) { - if (provider == MetadataProvider.Amazon) { - return amazonBookParser.fetchMetadata(book, request); - } else if (provider == MetadataProvider.GoodReads) { - return goodReadsParser.fetchMetadata(book, request); - } else if (provider == MetadataProvider.Google) { - return googleParser.fetchMetadata(book, request); - } else { - throw ApiError.METADATA_SOURCE_NOT_IMPLEMENT_OR_DOES_NOT_EXIST.createException(); - } + public List fetchMetadataListFromAProvider(MetadataProvider provider, Book book, FetchMetadataRequest request) { + return getParser(provider).fetchMetadata(book, request); } - public FetchedBookMetadata fetchTopMetadata(MetadataProvider provider, Book book) { - FetchMetadataRequest fetchMetadataRequest = FetchMetadataRequest.builder() - .isbn(book.getMetadata().getIsbn10()) - .author(book.getMetadata().getAuthors().stream().map(Author::getName).collect(Collectors.joining(", "))) - .title(book.getMetadata().getTitle()) - .bookId(book.getId()) - .build(); - if (provider == MetadataProvider.Amazon) { - return amazonBookParser.fetchTopMetadata(book, fetchMetadataRequest); - } else if (provider == MetadataProvider.GoodReads) { - return goodReadsParser.fetchTopMetadata(book, fetchMetadataRequest); - } else if (provider == MetadataProvider.Google) { - return goodReadsParser.fetchTopMetadata(book, fetchMetadataRequest); - } else { - throw ApiError.METADATA_SOURCE_NOT_IMPLEMENT_OR_DOES_NOT_EXIST.createException(); - } + public FetchedBookMetadata fetchTopMetadataFromAProvider(MetadataProvider provider, Book book) { + return getParser(provider).fetchTopMetadata(book, buildFetchMetadataRequestFromBook(book)); } @Transactional - public void refreshLibraryMetadata(LibraryMetadataRefreshRequest request) { - LibraryEntity libraryEntity = libraryRepository.findById(request.getLibraryId()).orElseThrow(() -> ApiError.LIBRARY_NOT_FOUND.createException(request.getLibraryId())); - List books = libraryEntity.getBookEntities().stream() - .sorted(Comparator.comparing(BookEntity::getFileName, Comparator.nullsLast(String::compareTo))) - .toList(); - refreshBooksMetadata(books, request.getMetadataProvider(), request.isReplaceCover()); - log.info("Library Refresh Metadata task completed!"); - } - - @Transactional - public void refreshBooksMetadata(BooksMetadataRefreshRequest request) { - List books = bookRepository.findAllByIdIn(request.getBookIds()).stream() - .sorted(Comparator.comparing(BookEntity::getFileName, Comparator.nullsLast(String::compareTo))) - .toList(); - refreshBooksMetadata(books, request.getMetadataProvider(), request.isReplaceCover()); - log.info("Books Refresh Metadata task completed!"); - } - - @Transactional - public void refreshBooksMetadata(List books, MetadataProvider metadataProvider, boolean replaceCover) { - try { - for (BookEntity bookEntity : books) { - FetchedBookMetadata metadata = fetchTopMetadata(metadataProvider, bookMapper.toBook(bookEntity)); - if (metadata != null) { - updateBookMetadata(bookEntity, metadata, replaceCover); - if (metadataProvider == MetadataProvider.GoodReads) { - Thread.sleep(Duration.ofSeconds(ThreadLocalRandom.current().nextInt(3, 10)).toMillis()); - } + public void refreshMetadata(MetadataRefreshRequest request) { + log.info("Refresh Metadata task started!"); + List providers = prepareProviders(request); + List books = getBookEntities(request); + for (BookEntity bookEntity : books) { + try { + Map metadataMap = fetchMetadataForBook(providers, bookEntity); + if (providers.contains(GoodReads)) { + Thread.sleep(Duration.ofSeconds(ThreadLocalRandom.current().nextInt(2, 6)).toMillis()); } + FetchedBookMetadata fetchedBookMetadata = getRawOrCombinedMetadata(request, metadataMap); + updateBookMetadata(bookEntity, fetchedBookMetadata, shouldRefreshCovers(request)); + } catch (Exception e) { + log.error("Error while updating book metadata, book: {}", bookEntity.getFileName(), e); } - } catch (Exception e) { - log.error("Error while parsing library books", e); } + log.info("Refresh Metadata task completed!"); } - @Transactional protected void updateBookMetadata(BookEntity bookEntity, FetchedBookMetadata metadata, boolean replaceCover) { if (metadata != null) { @@ -156,43 +111,52 @@ public class BookMetadataService { } @Transactional - public void refreshMetadataV2(MetadataRefreshRequest request) { - log.info("Refresh Metadata V2 task started!"); - - MetadataProvider defaultProvider = request.getRefreshOptions().getDefaultProvider(); - Set nonDefaultProviders = getNonDefaultProviders(request); - Set allProviders = new HashSet<>(nonDefaultProviders); - allProviders.add(defaultProvider); - List books = getBookEntities(request); - - for (BookEntity bookEntity : books) { - try { - Map metadataMap = allProviders.stream() - .map(provider -> CompletableFuture.supplyAsync(() -> fetchTopMetadata(provider, bookMapper.toBook(bookEntity))) - .exceptionally(e -> { - log.error("Error fetching metadata from provider: {}", provider, e); - return null; - })) - .map(CompletableFuture::join) - .filter(Objects::nonNull) - .collect(Collectors.toMap( - FetchedBookMetadata::getProvider, - metadata -> metadata, - (existing, replacement) -> existing - )); - - FetchedBookMetadata combinedMetadata = setSpecificMetadata(request, metadataMap); - nonDefaultProviders.forEach(provider -> setUnspecificMetadata(metadataMap, combinedMetadata, provider)); - setUnspecificMetadata(metadataMap, combinedMetadata, defaultProvider); - if (allProviders.contains(MetadataProvider.GoodReads)) { - Thread.sleep(Duration.ofSeconds(ThreadLocalRandom.current().nextInt(2, 6)).toMillis()); - } - updateBookMetadata(bookEntity, combinedMetadata, false); - } catch (Exception e) { - log.error("Error while updating book metadata, book: {}", bookEntity.getFileName(), e); - } + protected List prepareProviders(MetadataRefreshRequest request) { + if (request.getRefreshOptions().getFieldOptions() == null) { + return List.of(request.getRefreshOptions().getDefaultProvider()); + } else { + Set allProviders = new HashSet<>(getNonDefaultProviders(request)); + allProviders.add(request.getRefreshOptions().getDefaultProvider()); + return new ArrayList<>(allProviders); } - log.info("Refresh Metadata V2 task completed!"); + } + + @Transactional + protected Map fetchMetadataForBook(List providers, BookEntity bookEntity) { + return providers.stream() + .map(provider -> CompletableFuture.supplyAsync(() -> fetchTopMetadataFromAProvider(provider, bookMapper.toBook(bookEntity))) + .exceptionally(e -> { + log.error("Error fetching metadata from provider: {}", provider, e); + return null; + })) + .map(CompletableFuture::join) + .filter(Objects::nonNull) + .collect(Collectors.toMap( + FetchedBookMetadata::getProvider, + metadata -> metadata, + (existing, replacement) -> existing + )); + } + + @Transactional + protected FetchedBookMetadata getRawOrCombinedMetadata(MetadataRefreshRequest request, Map metadataMap) { + if (request.getRefreshOptions().getFieldOptions() == null) { + return metadataMap.get(request.getRefreshOptions().getDefaultProvider()); + } else { + MetadataProvider defaultProvider = request.getRefreshOptions().getDefaultProvider(); + Set nonDefaultProviders = getNonDefaultProviders(request); + FetchedBookMetadata combinedMetadata = setSpecificMetadata(request, metadataMap); + + nonDefaultProviders.forEach(provider -> setUnspecificMetadata(metadataMap, combinedMetadata, provider)); + setUnspecificMetadata(metadataMap, combinedMetadata, defaultProvider); + + return combinedMetadata; + } + } + + @Transactional + protected boolean shouldRefreshCovers(MetadataRefreshRequest request) { + return request.getRefreshOptions().isRefreshCovers(); } @Transactional @@ -255,46 +219,20 @@ public class BookMetadataService { return values; } - /*@Transactional - protected String resolveFieldAsString(Map metadataMap, MetadataRefreshOptions.FieldProvider fieldProvider) { - if (fieldProvider.getDefaultProvider() != null && metadataMap.containsKey(fieldProvider.getDefaultProvider())) { - return metadataMap.get(fieldProvider.getDefaultProvider()).getTitle(); - } - if (fieldProvider.getP2() != null && metadataMap.containsKey(fieldProvider.getP2())) { - return metadataMap.get(fieldProvider.getP2()).getTitle(); - } - if (fieldProvider.getP1() != null && metadataMap.containsKey(fieldProvider.getP1())) { - return metadataMap.get(fieldProvider.getP1()).getTitle(); - } - return null; - } - - @Transactional - protected List resolveFieldAsList(Map metadataMap, MetadataRefreshOptions.FieldProvider fieldProvider) { - if (fieldProvider.getDefaultProvider() != null && metadataMap.containsKey(fieldProvider.getDefaultProvider())) { - return metadataMap.get(fieldProvider.getDefaultProvider()).getAuthors(); - } - if (fieldProvider.getP2() != null && metadataMap.containsKey(fieldProvider.getP2())) { - return metadataMap.get(fieldProvider.getP2()).getAuthors(); - } - if (fieldProvider.getP1() != null && metadataMap.containsKey(fieldProvider.getP1())) { - return metadataMap.get(fieldProvider.getP1()).getAuthors(); - } - return Collections.emptyList(); - }*/ - @Transactional protected List getBookEntities(MetadataRefreshRequest request) { MetadataRefreshRequest.RefreshType refreshType = request.getRefreshType(); - List books = new ArrayList<>(); - if (refreshType == MetadataRefreshRequest.RefreshType.LIBRARY) { - LibraryEntity libraryEntity = libraryRepository.findById(request.getLibraryId()).orElseThrow(() -> ApiError.LIBRARY_NOT_FOUND.createException(request.getLibraryId())); - books = libraryEntity.getBookEntities().stream() - .sorted(Comparator.comparing(BookEntity::getFileName, Comparator.nullsLast(String::compareTo))) - .toList(); - } else if (refreshType == MetadataRefreshRequest.RefreshType.BOOKS) { - //TODO + if (refreshType != MetadataRefreshRequest.RefreshType.LIBRARY && refreshType != MetadataRefreshRequest.RefreshType.BOOKS) { + throw ApiError.INVALID_REFRESH_TYPE.createException(); } + List books = switch (refreshType) { + case LIBRARY -> { + LibraryEntity libraryEntity = libraryRepository.findById(request.getLibraryId()).orElseThrow(() -> ApiError.LIBRARY_NOT_FOUND.createException(request.getLibraryId())); + yield libraryEntity.getBookEntities(); + } + case BOOKS -> bookRepository.findAllByIdIn(request.getBookIds()); + }; + books.sort(Comparator.comparing(BookEntity::getFileName, Comparator.nullsLast(String::compareTo))); return books; } @@ -328,4 +266,23 @@ public class BookMetadataService { } } } + + private FetchMetadataRequest buildFetchMetadataRequestFromBook(Book book) { + return FetchMetadataRequest.builder() + .isbn(book.getMetadata().getIsbn10()) + .author(book.getMetadata().getAuthors().stream() + .map(Author::getName) + .collect(Collectors.joining(", "))) + .title(book.getMetadata().getTitle()) + .bookId(book.getId()) + .build(); + } + + private BookParser getParser(MetadataProvider provider) { + BookParser parser = parserMap.get(provider); + if (parser == null) { + throw ApiError.METADATA_SOURCE_NOT_IMPLEMENT_OR_DOES_NOT_EXIST.createException(); + } + return parser; + } } \ No newline at end of file diff --git a/booklore-ui/package-lock.json b/booklore-ui/package-lock.json index 4e0716feb..eaf036d59 100644 --- a/booklore-ui/package-lock.json +++ b/booklore-ui/package-lock.json @@ -24,7 +24,7 @@ "ngx-extended-pdf-viewer": "^22.0.0", "ngx-infinite-scroll": "^19.0.0", "primeicons": "^7.0.0", - "primeng": "19.0.3", + "primeng": "19.0.5", "quill": "^2.0.3", "rxjs": "~7.8.0", "tailwindcss-primeui": "^0.3.4", @@ -12037,9 +12037,9 @@ "license": "MIT" }, "node_modules/primeng": { - "version": "19.0.3", - "resolved": "https://registry.npmjs.org/primeng/-/primeng-19.0.3.tgz", - "integrity": "sha512-cv15SG37WcMFlGZY9cyMULM8l91PTYRONdvMoDECNnH7LV0EkG1jvrsAezn8c4IsoJheofnoZez9D6Z9TpS4PQ==", + "version": "19.0.5", + "resolved": "https://registry.npmjs.org/primeng/-/primeng-19.0.5.tgz", + "integrity": "sha512-3IMWTUykIyZpT7d+pD7KzB+68GcY8/xOV10V1Tf09cPkPuwXlFP+NAVGIqAOXBzkw+RmJksSBoa13bpS/fMo1g==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "@primeuix/styled": "^0.3.2", diff --git a/booklore-ui/package.json b/booklore-ui/package.json index 39e90522c..b8072a9b6 100644 --- a/booklore-ui/package.json +++ b/booklore-ui/package.json @@ -26,7 +26,7 @@ "ngx-extended-pdf-viewer": "^22.0.0", "ngx-infinite-scroll": "^19.0.0", "primeicons": "^7.0.0", - "primeng": "19.0.3", + "primeng": "19.0.5", "quill": "^2.0.3", "rxjs": "~7.8.0", "tailwindcss-primeui": "^0.3.4", diff --git a/booklore-ui/src/app/book-metadata-center/metadata-searcher/metadata-searcher.component.ts b/booklore-ui/src/app/book-metadata-center/metadata-searcher/metadata-searcher.component.ts index e8c9ca1bd..f5d229199 100644 --- a/booklore-ui/src/app/book-metadata-center/metadata-searcher/metadata-searcher.component.ts +++ b/booklore-ui/src/app/book-metadata-center/metadata-searcher/metadata-searcher.component.ts @@ -6,7 +6,7 @@ import {Divider} from 'primeng/divider'; import {NgForOf, NgIf} from '@angular/common'; import {MetadataProvider} from '../../book/model/provider.model'; import {BookService} from '../../book/service/book.service'; -import {FetchMetadataRequest} from '../../book/model/request/fetch-metadata-request.model'; +import {FetchMetadataRequest} from '../../book/model/request/metadata/fetch-metadata-request.model'; import {FetchedMetadata} from '../../book/model/book.model'; import {ProgressSpinner} from 'primeng/progressspinner'; import {MetadataPickerComponent} from '../metadata-picker/metadata-picker.component'; diff --git a/booklore-ui/src/app/book/component/book-browser/book-browser.component.ts b/booklore-ui/src/app/book/component/book-browser/book-browser.component.ts index e80784261..08e398584 100644 --- a/booklore-ui/src/app/book/component/book-browser/book-browser.component.ts +++ b/booklore-ui/src/app/book/component/book-browser/book-browser.component.ts @@ -18,8 +18,8 @@ import {LibraryShelfMenuService} from '../../service/library-shelf-menu.service' import {SelectButtonChangeEvent} from 'primeng/selectbutton'; import {BookTableComponent} from '../book-table/book-table.component'; import {animate, state, style, transition, trigger} from '@angular/animations'; -import {MetadataProvider} from '../../model/provider.model'; -import {MetadataService} from '../../service/metadata.service'; +import {MetadataFetchOptionsComponent} from '../../../metadata-fetch-options/metadata-fetch-options.component'; +import {MetadataRefreshType} from '../../model/request/metadata/metadata-refresh-type.enum'; export enum EntityType { LIBRARY = 'Library', @@ -79,7 +79,6 @@ export class BookBrowserComponent implements OnInit { private shelfService: ShelfService, private dialogService: DialogService, private sortService: SortService, - private metadataService: MetadataService, private libraryShelfMenuService: LibraryShelfMenuService) { } @@ -300,10 +299,6 @@ export class BookBrowserComponent implements OnInit { this.bookTitle$.next(newTitle); } - openMetadataOptionsDialog() { - - } - openShelfAssigner() { this.dynamicDialogRef = this.dialogService.open(ShelfAssignerComponent, { header: `Update Books Shelves`, @@ -328,32 +323,15 @@ export class BookBrowserComponent implements OnInit { } updateMetadata() { - this.metadataService.autoRefreshBooksMetadata(this.selectedBooks, MetadataProvider.Amazon, false).subscribe({ - next: () => { - this.messageService.add({ - severity: 'success', - summary: 'Metadata Update Scheduled', - detail: 'The metadata update for the selected books has been successfully scheduled.' - }); - }, - error: (e) => { - if (e.status === 409) { - this.messageService.add({ - severity: 'error', - summary: 'Task Already Running', - life: 5000, - detail: 'A metadata refresh task is already in progress. Please wait for it to complete before starting another one.' - }); - } else { - this.messageService.add({ - severity: 'error', - summary: 'Metadata Update Failed', - life: 5000, - detail: 'An unexpected error occurred while scheduling the metadata update. Please try again later or contact support if the issue persists.' - }); - } + this.dialogService.open(MetadataFetchOptionsComponent, { + header: 'Metadata Refresh Options', + modal: true, + closable: true, + data: { + bookIds: this.selectedBooks, + metadataRefreshType: MetadataRefreshType.BOOKS } - }); + }) } } diff --git a/booklore-ui/src/app/book/component/layout/app.topbar.component.html b/booklore-ui/src/app/book/component/layout/app.topbar.component.html index 151a6e684..7753811b3 100644 --- a/booklore-ui/src/app/book/component/layout/app.topbar.component.html +++ b/booklore-ui/src/app/book/component/layout/app.topbar.component.html @@ -1,11 +1,9 @@
diff --git a/booklore-ui/src/app/book/model/request/book-auto-metadata-refresh.model.ts b/booklore-ui/src/app/book/model/request/metadata/book-auto-metadata-refresh.model.ts similarity index 70% rename from booklore-ui/src/app/book/model/request/book-auto-metadata-refresh.model.ts rename to booklore-ui/src/app/book/model/request/metadata/book-auto-metadata-refresh.model.ts index 96428249c..1af666a3b 100644 --- a/booklore-ui/src/app/book/model/request/book-auto-metadata-refresh.model.ts +++ b/booklore-ui/src/app/book/model/request/metadata/book-auto-metadata-refresh.model.ts @@ -1,4 +1,4 @@ -import {MetadataProvider} from '../provider.model'; +import {MetadataProvider} from '../../provider.model'; export interface BookAutoMetadataRefresh { bookIds: number[], diff --git a/booklore-ui/src/app/book/model/request/fetch-metadata-request.model.ts b/booklore-ui/src/app/book/model/request/metadata/fetch-metadata-request.model.ts similarity index 100% rename from booklore-ui/src/app/book/model/request/fetch-metadata-request.model.ts rename to booklore-ui/src/app/book/model/request/metadata/fetch-metadata-request.model.ts diff --git a/booklore-ui/src/app/book/model/request/library-auto-metadata-refresh.model.ts b/booklore-ui/src/app/book/model/request/metadata/library-auto-metadata-refresh.model.ts similarity index 71% rename from booklore-ui/src/app/book/model/request/library-auto-metadata-refresh.model.ts rename to booklore-ui/src/app/book/model/request/metadata/library-auto-metadata-refresh.model.ts index 162d5fc52..7d78a5286 100644 --- a/booklore-ui/src/app/book/model/request/library-auto-metadata-refresh.model.ts +++ b/booklore-ui/src/app/book/model/request/metadata/library-auto-metadata-refresh.model.ts @@ -1,4 +1,4 @@ -import {MetadataProvider} from '../provider.model'; +import {MetadataProvider} from '../../provider.model'; export interface LibraryAutoMetadataRefreshRequest { libraryId: number, diff --git a/booklore-ui/src/app/book/model/request/metadata/metadata-refresh-options.model.ts b/booklore-ui/src/app/book/model/request/metadata/metadata-refresh-options.model.ts new file mode 100644 index 000000000..0d7854189 --- /dev/null +++ b/booklore-ui/src/app/book/model/request/metadata/metadata-refresh-options.model.ts @@ -0,0 +1,19 @@ +export interface MetadataRefreshOptions { + defaultProvider: string; + refreshCovers: boolean; + fieldOptions?: FieldOptions; +} + +export interface FieldProvider { + default: string | null; + p2: string | null; + p1: string | null; +} + +export interface FieldOptions { + title: FieldProvider; + description: FieldProvider; + authors: FieldProvider; + categories: FieldProvider; + cover: FieldProvider; +} diff --git a/booklore-ui/src/app/book/model/request/metadata/metadata-refresh-request.model.ts b/booklore-ui/src/app/book/model/request/metadata/metadata-refresh-request.model.ts new file mode 100644 index 000000000..31beb1014 --- /dev/null +++ b/booklore-ui/src/app/book/model/request/metadata/metadata-refresh-request.model.ts @@ -0,0 +1,9 @@ +import {MetadataRefreshType} from './metadata-refresh-type.enum'; +import {MetadataRefreshOptions} from './metadata-refresh-options.model'; + +export interface MetadataRefreshRequest { + refreshType: MetadataRefreshType; + libraryId?: number; + bookIds?: Set; + refreshOptions: MetadataRefreshOptions; +} diff --git a/booklore-ui/src/app/book/model/request/metadata/metadata-refresh-type.enum.ts b/booklore-ui/src/app/book/model/request/metadata/metadata-refresh-type.enum.ts new file mode 100644 index 000000000..2d8a41185 --- /dev/null +++ b/booklore-ui/src/app/book/model/request/metadata/metadata-refresh-type.enum.ts @@ -0,0 +1,4 @@ +export enum MetadataRefreshType { + BOOKS = 'BOOKS', + LIBRARY = 'LIBRARY' +} diff --git a/booklore-ui/src/app/book/service/library-shelf-menu.service.ts b/booklore-ui/src/app/book/service/library-shelf-menu.service.ts index fbdf4da86..ff5f7b9d2 100644 --- a/booklore-ui/src/app/book/service/library-shelf-menu.service.ts +++ b/booklore-ui/src/app/book/service/library-shelf-menu.service.ts @@ -9,6 +9,7 @@ import {MetadataService} from './metadata.service'; import {MetadataProvider} from '../model/provider.model'; import {DialogService} from 'primeng/dynamicdialog'; import {MetadataFetchOptionsComponent} from '../../metadata-fetch-options/metadata-fetch-options.component'; +import {MetadataRefreshType} from '../model/request/metadata/metadata-refresh-type.enum'; @Injectable({ providedIn: 'root', @@ -94,7 +95,8 @@ export class LibraryShelfMenuService { modal: true, closable: true, data: { - libraryId: entity?.id + libraryId: entity?.id, + metadataRefreshType: MetadataRefreshType.LIBRARY } }) /*this.confirmationService.confirm({ diff --git a/booklore-ui/src/app/book/service/metadata.service.ts b/booklore-ui/src/app/book/service/metadata.service.ts index 9a0169574..79ad3a5aa 100644 --- a/booklore-ui/src/app/book/service/metadata.service.ts +++ b/booklore-ui/src/app/book/service/metadata.service.ts @@ -1,16 +1,12 @@ import {Injectable} from '@angular/core'; import {HttpClient} from '@angular/common/http'; -import {MetadataProvider} from "../model/provider.model"; import {Observable} from "rxjs"; -import {BookAutoMetadataRefresh} from "../model/request/book-auto-metadata-refresh.model"; -import {LibraryAutoMetadataRefreshRequest} from "../model/request/library-auto-metadata-refresh.model"; -import {FetchMetadataRequest} from "../model/request/fetch-metadata-request.model"; +import {FetchMetadataRequest} from "../model/request/metadata/fetch-metadata-request.model"; import {BookMetadata, FetchedMetadata} from "../model/book.model"; import {BookMetadataBI} from "../model/book-metadata-for-book-info.model"; import {tap} from "rxjs/operators"; import {BookService} from "./book.service"; -import {MetadataRefreshOptions} from '../../metadata-advanced-fetch-options/metadata-advanced-fetch-options.component'; -import {MetadataRefreshRequest} from '../../metadata-fetch-options/metadata-fetch-options.component'; +import {MetadataRefreshRequest} from '../model/request/metadata/metadata-refresh-request.model'; @Injectable({ providedIn: 'root' @@ -34,25 +30,7 @@ export class MetadataService { ); } - autoRefreshLibraryBooksMetadataV2(metadataRefreshRequest: MetadataRefreshRequest) { + autoRefreshMetadata(metadataRefreshRequest: MetadataRefreshRequest) { return this.http.put(`${this.url}/refreshV2`, metadataRefreshRequest); } - - autoRefreshLibraryBooksMetadata(libraryId: number, metadataProvider: MetadataProvider, replaceCover: boolean): Observable { - const requestPayload: LibraryAutoMetadataRefreshRequest = { - libraryId: libraryId, - metadataProvider: metadataProvider, - replaceCover: replaceCover - } - return this.http.put(`${this.url}/library/${libraryId}/refresh`, requestPayload); - } - - autoRefreshBooksMetadata(selectedBooks: Set, metadataProvider: MetadataProvider, replaceCover: boolean): Observable { - const requestPayload: BookAutoMetadataRefresh = { - bookIds: Array.from(selectedBooks), - metadataProvider: metadataProvider, - replaceCover - }; - return this.http.put(`${this.url}/books/refresh`, requestPayload); - } } diff --git a/booklore-ui/src/app/metadata-advanced-fetch-options/metadata-advanced-fetch-options.component.ts b/booklore-ui/src/app/metadata-advanced-fetch-options/metadata-advanced-fetch-options.component.ts index 20afd82b4..b4ffc670a 100644 --- a/booklore-ui/src/app/metadata-advanced-fetch-options/metadata-advanced-fetch-options.component.ts +++ b/booklore-ui/src/app/metadata-advanced-fetch-options/metadata-advanced-fetch-options.component.ts @@ -5,28 +5,7 @@ import {NgForOf, TitleCasePipe} from '@angular/common'; import {Checkbox} from 'primeng/checkbox'; import {Button} from 'primeng/button'; import {MessageService} from 'primeng/api'; - - -export interface MetadataRefreshOptions { - refreshType: string; - defaultProvider: string; - refreshCovers: boolean; - fieldOptions: FieldOptions; -} - -export interface FieldProvider { - default: string | null; - p2: string | null; - p1: string | null; -} - -export interface FieldOptions { - title: FieldProvider; - description: FieldProvider; - authors: FieldProvider; - categories: FieldProvider; - cover: FieldProvider; -} +import {FieldOptions, FieldProvider, MetadataRefreshOptions} from '../book/model/request/metadata/metadata-refresh-options.model'; @Component({ selector: 'app-metadata-advanced-fetch-options', @@ -69,7 +48,6 @@ export class MetadataAdvancedFetchOptionsComponent { }); if (allProvidersSelected) { const metadataRefreshOptions: MetadataRefreshOptions = { - refreshType: 'LIBRARY', defaultProvider: this.allDefault.value, refreshCovers: this.refreshCovers, fieldOptions: this.fieldOptions diff --git a/booklore-ui/src/app/metadata-basic-fetch-options/metadata-basic-fetch-options.component.html b/booklore-ui/src/app/metadata-basic-fetch-options/metadata-basic-fetch-options.component.html index 0826adbb6..23f56684f 100644 --- a/booklore-ui/src/app/metadata-basic-fetch-options/metadata-basic-fetch-options.component.html +++ b/booklore-ui/src/app/metadata-basic-fetch-options/metadata-basic-fetch-options.component.html @@ -1,18 +1,11 @@ -
- - +
+ + -

Refresh covers:

- - +
diff --git a/booklore-ui/src/app/metadata-basic-fetch-options/metadata-basic-fetch-options.component.ts b/booklore-ui/src/app/metadata-basic-fetch-options/metadata-basic-fetch-options.component.ts index fffaed6d7..be5f942dd 100644 --- a/booklore-ui/src/app/metadata-basic-fetch-options/metadata-basic-fetch-options.component.ts +++ b/booklore-ui/src/app/metadata-basic-fetch-options/metadata-basic-fetch-options.component.ts @@ -1,23 +1,29 @@ -import { Component } from '@angular/core'; +import {Component, EventEmitter, Output} from '@angular/core'; import {Button} from 'primeng/button'; import {Checkbox} from 'primeng/checkbox'; import {Select} from 'primeng/select'; import {FormsModule} from '@angular/forms'; +import {MetadataRefreshOptions} from '../book/model/request/metadata/metadata-refresh-options.model'; @Component({ selector: 'app-metadata-basic-fetch-options', standalone: true, templateUrl: './metadata-basic-fetch-options.component.html', - imports: [ - Button, - Checkbox, - Select, - FormsModule - ], - styleUrl: './metadata-basic-fetch-options.component.scss' + styleUrl: './metadata-basic-fetch-options.component.scss', + imports: [Button, Checkbox, Select, FormsModule] }) export class MetadataBasicFetchOptionsComponent { providers: string[] = ['Amazon', 'Google', 'GoodReads']; selectedProvider!: string; refreshCovers: boolean = false; + + @Output() metadataOptionsSubmitted: EventEmitter = new EventEmitter(); + + submit() { + const metadataRefreshOptions: MetadataRefreshOptions = { + defaultProvider: this.selectedProvider, + refreshCovers: this.refreshCovers, + }; + this.metadataOptionsSubmitted.emit(metadataRefreshOptions); + } } diff --git a/booklore-ui/src/app/metadata-fetch-options/metadata-fetch-options.component.html b/booklore-ui/src/app/metadata-fetch-options/metadata-fetch-options.component.html index 3dd0225ac..b67d0c7c0 100644 --- a/booklore-ui/src/app/metadata-fetch-options/metadata-fetch-options.component.html +++ b/booklore-ui/src/app/metadata-fetch-options/metadata-fetch-options.component.html @@ -1,10 +1,10 @@ -
+
- + - + diff --git a/booklore-ui/src/app/metadata-fetch-options/metadata-fetch-options.component.ts b/booklore-ui/src/app/metadata-fetch-options/metadata-fetch-options.component.ts index ef64dc617..4605ebbdf 100644 --- a/booklore-ui/src/app/metadata-fetch-options/metadata-fetch-options.component.ts +++ b/booklore-ui/src/app/metadata-fetch-options/metadata-fetch-options.component.ts @@ -1,23 +1,15 @@ import {Component} from '@angular/core'; -import {DynamicDialogConfig} from 'primeng/dynamicdialog'; -import {MetadataAdvancedFetchOptionsComponent, MetadataRefreshOptions} from '../metadata-advanced-fetch-options/metadata-advanced-fetch-options.component'; +import {DynamicDialogConfig, DynamicDialogRef} from 'primeng/dynamicdialog'; +import {MetadataAdvancedFetchOptionsComponent} from '../metadata-advanced-fetch-options/metadata-advanced-fetch-options.component'; import {Divider} from 'primeng/divider'; import {Button} from 'primeng/button'; import {MetadataBasicFetchOptionsComponent} from '../metadata-basic-fetch-options/metadata-basic-fetch-options.component'; import {NgIf} from '@angular/common'; import {MetadataService} from '../book/service/metadata.service'; - -export interface MetadataRefreshRequest { - refreshType: RefreshType; - libraryId?: number; - bookIds?: Set; - refreshOptions: MetadataRefreshOptions; -} - -enum RefreshType { - BOOKS = 'BOOKS', - LIBRARY = 'LIBRARY' -} +import {MetadataRefreshRequest} from '../book/model/request/metadata/metadata-refresh-request.model'; +import {MetadataRefreshType} from '../book/model/request/metadata/metadata-refresh-type.enum'; +import {MessageService} from 'primeng/api'; +import {MetadataRefreshOptions} from '../book/model/request/metadata/metadata-refresh-options.model'; @Component({ selector: 'app-metadata-fetch-options', @@ -33,30 +25,57 @@ enum RefreshType { styleUrl: './metadata-fetch-options.component.scss' }) export class MetadataFetchOptionsComponent { - isBasicMode: boolean = false; + isBasicMode: boolean = true; libraryId!: number; + bookIds!: Set; + metadataRefreshType!: MetadataRefreshType; - constructor(private dynamicDialogConfig: DynamicDialogConfig, private metadataService: MetadataService) { + constructor(private dynamicDialogConfig: DynamicDialogConfig, + private dynamicDialogRef: DynamicDialogRef, + private metadataService: MetadataService, + private messageService: MessageService) { this.libraryId = dynamicDialogConfig.data.libraryId; + this.bookIds = dynamicDialogConfig.data.bookIds; + this.metadataRefreshType = dynamicDialogConfig.data.metadataRefreshType; } toggleMode() { this.isBasicMode = !this.isBasicMode; } - onAdvancedMetadataOptionSubmit(metadataRefreshOptions: MetadataRefreshOptions) { + onMetadataSubmit(metadataRefreshOptions: MetadataRefreshOptions) { const metadataRefreshRequest: MetadataRefreshRequest = { - refreshType: RefreshType.LIBRARY, - libraryId: this.libraryId, - refreshOptions: metadataRefreshOptions + refreshType: this.metadataRefreshType, + refreshOptions: metadataRefreshOptions, + ...(this.metadataRefreshType === 'BOOKS' && this.bookIds != null && {bookIds: this.bookIds}), + ...(this.metadataRefreshType === 'LIBRARY' && this.libraryId != null && {libraryId: this.libraryId}) }; - this.metadataService.autoRefreshLibraryBooksMetadataV2(metadataRefreshRequest).subscribe({ + this.metadataService.autoRefreshMetadata(metadataRefreshRequest).subscribe({ next: () => { - // Handle success + this.messageService.add({ + severity: 'success', + summary: 'Metadata Update Scheduled', + detail: 'The metadata update for the selected books has been successfully scheduled.' + }); }, - error: (error) => { - // Handle error + error: (e) => { + if (e.status === 409) { + this.messageService.add({ + severity: 'error', + summary: 'Task Already Running', + life: 5000, + detail: 'A metadata refresh task is already in progress. Please wait for it to complete before starting another one.' + }); + } else { + this.messageService.add({ + severity: 'error', + summary: 'Metadata Update Failed', + life: 5000, + detail: 'An unexpected error occurred while scheduling the metadata update. Please try again later or contact support if the issue persists.' + }); + } } }); + this.dynamicDialogRef.close(); } } diff --git a/booklore-ui/src/assets/layout/styles/layout/_topbar.scss b/booklore-ui/src/assets/layout/styles/layout/_topbar.scss index 0e8759c88..c692fb4c1 100644 --- a/booklore-ui/src/assets/layout/styles/layout/_topbar.scss +++ b/booklore-ui/src/assets/layout/styles/layout/_topbar.scss @@ -23,7 +23,7 @@ display: flex; align-items: center; color: var(--surface-900); - font-size: 1.5rem; + font-size: 1.7rem; font-weight: 500; width: 200px; border-radius: 12px;