mirror of
https://github.com/adityachandelgit/BookLore.git
synced 2026-02-18 03:07:40 +01:00
WIP: Metadata v2 refactor almost complete
This commit is contained in:
@@ -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<MetadataProvider, BookParser> parserMap(GoogleParser googleParser, AmazonBookParser amazonBookParser, GoodReadsParser goodReadsParser) {
|
||||
return Map.of(
|
||||
MetadataProvider.Amazon, amazonBookParser,
|
||||
MetadataProvider.GoodReads, goodReadsParser,
|
||||
MetadataProvider.Google, googleParser
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<List<FetchedBookMetadata>> 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<String> scheduleRefresh(@PathVariable Long libraryId, @RequestBody LibraryMetadataRefreshRequest request) {
|
||||
jobSchedulerService.scheduleMetadataRefresh(request);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@PutMapping(path = "/refreshV2")
|
||||
public ResponseEntity<String> scheduleRefreshV2(@RequestBody MetadataRefreshRequest request) {
|
||||
public ResponseEntity<String> scheduleRefreshV2(@Validated @RequestBody MetadataRefreshRequest request) {
|
||||
jobSchedulerService.scheduleMetadataRefreshV2(request);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@PutMapping(path = "/books/refresh")
|
||||
public ResponseEntity<String> scheduleBookMetadataRefresh(@RequestBody BooksMetadataRefreshRequest request) {
|
||||
jobSchedulerService.scheduleBookMetadataRefresh(request);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<Long> bookIds;
|
||||
|
||||
@NotNull(message = "Refresh options cannot be null")
|
||||
private MetadataRefreshOptions refreshOptions;
|
||||
|
||||
public enum RefreshType {
|
||||
|
||||
@@ -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 <T> void scheduleJob(T request, Class<? extends Job> jobClass, String name) {
|
||||
private <T> 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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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<MetadataProvider, BookParser> parserMap;
|
||||
|
||||
public List<FetchedBookMetadata> fetchMetadataList(long bookId, FetchMetadataRequest request) {
|
||||
|
||||
public List<FetchedBookMetadata> fetchMetadataForRequest(long bookId, FetchMetadataRequest request) {
|
||||
BookEntity bookEntity = bookRepository.findById(bookId).orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(bookId));
|
||||
Book book = bookMapper.toBook(bookEntity);
|
||||
|
||||
List<CompletableFuture<List<FetchedBookMetadata>>> futures = request.getProviders().stream()
|
||||
.map(provider -> CompletableFuture.supplyAsync(() -> fetchMetadataFromProvider(provider, book, request))
|
||||
List<List<FetchedBookMetadata>> 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<List<FetchedBookMetadata>> allMetadata = futures.stream().map(CompletableFuture::join).toList();
|
||||
.toList()
|
||||
.stream().map(CompletableFuture::join).toList();
|
||||
|
||||
List<FetchedBookMetadata> interleavedMetadata = new ArrayList<>();
|
||||
int maxSize = allMetadata.stream().mapToInt(List::size).max().orElse(0);
|
||||
@@ -76,73 +70,34 @@ public class BookMetadataService {
|
||||
return interleavedMetadata;
|
||||
}
|
||||
|
||||
private List<FetchedBookMetadata> 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<FetchedBookMetadata> 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<BookEntity> 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<BookEntity> 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<BookEntity> 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<MetadataProvider> providers = prepareProviders(request);
|
||||
List<BookEntity> books = getBookEntities(request);
|
||||
for (BookEntity bookEntity : books) {
|
||||
try {
|
||||
Map<MetadataProvider, FetchedBookMetadata> 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<MetadataProvider> nonDefaultProviders = getNonDefaultProviders(request);
|
||||
Set<MetadataProvider> allProviders = new HashSet<>(nonDefaultProviders);
|
||||
allProviders.add(defaultProvider);
|
||||
List<BookEntity> books = getBookEntities(request);
|
||||
|
||||
for (BookEntity bookEntity : books) {
|
||||
try {
|
||||
Map<MetadataProvider, FetchedBookMetadata> 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<MetadataProvider> prepareProviders(MetadataRefreshRequest request) {
|
||||
if (request.getRefreshOptions().getFieldOptions() == null) {
|
||||
return List.of(request.getRefreshOptions().getDefaultProvider());
|
||||
} else {
|
||||
Set<MetadataProvider> allProviders = new HashSet<>(getNonDefaultProviders(request));
|
||||
allProviders.add(request.getRefreshOptions().getDefaultProvider());
|
||||
return new ArrayList<>(allProviders);
|
||||
}
|
||||
log.info("Refresh Metadata V2 task completed!");
|
||||
}
|
||||
|
||||
@Transactional
|
||||
protected Map<MetadataProvider, FetchedBookMetadata> fetchMetadataForBook(List<MetadataProvider> 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<MetadataProvider, FetchedBookMetadata> metadataMap) {
|
||||
if (request.getRefreshOptions().getFieldOptions() == null) {
|
||||
return metadataMap.get(request.getRefreshOptions().getDefaultProvider());
|
||||
} else {
|
||||
MetadataProvider defaultProvider = request.getRefreshOptions().getDefaultProvider();
|
||||
Set<MetadataProvider> 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<MetadataProvider, FetchedBookMetadata> 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<String> resolveFieldAsList(Map<MetadataProvider, FetchedBookMetadata> 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<BookEntity> getBookEntities(MetadataRefreshRequest request) {
|
||||
MetadataRefreshRequest.RefreshType refreshType = request.getRefreshType();
|
||||
List<BookEntity> 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<BookEntity> 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;
|
||||
}
|
||||
}
|
||||
8
booklore-ui/package-lock.json
generated
8
booklore-ui/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<div class="layout-topbar">
|
||||
|
||||
<a class="layout-topbar-logo" routerLink="">
|
||||
<img src="assets/layout/images/{{'logo-dark'}}.svg"
|
||||
alt="logo">
|
||||
<span class="flex">
|
||||
<p>Book</p>
|
||||
<p class="half-title">lore</p>
|
||||
<p class="half-title text-3xl">lore</p>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {MetadataProvider} from '../provider.model';
|
||||
import {MetadataProvider} from '../../provider.model';
|
||||
|
||||
export interface BookAutoMetadataRefresh {
|
||||
bookIds: number[],
|
||||
@@ -1,4 +1,4 @@
|
||||
import {MetadataProvider} from '../provider.model';
|
||||
import {MetadataProvider} from '../../provider.model';
|
||||
|
||||
export interface LibraryAutoMetadataRefreshRequest {
|
||||
libraryId: number,
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<number>;
|
||||
refreshOptions: MetadataRefreshOptions;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum MetadataRefreshType {
|
||||
BOOKS = 'BOOKS',
|
||||
LIBRARY = 'LIBRARY'
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -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<void>(`${this.url}/refreshV2`, metadataRefreshRequest);
|
||||
}
|
||||
|
||||
autoRefreshLibraryBooksMetadata(libraryId: number, metadataProvider: MetadataProvider, replaceCover: boolean): Observable<void> {
|
||||
const requestPayload: LibraryAutoMetadataRefreshRequest = {
|
||||
libraryId: libraryId,
|
||||
metadataProvider: metadataProvider,
|
||||
replaceCover: replaceCover
|
||||
}
|
||||
return this.http.put<void>(`${this.url}/library/${libraryId}/refresh`, requestPayload);
|
||||
}
|
||||
|
||||
autoRefreshBooksMetadata(selectedBooks: Set<number>, metadataProvider: MetadataProvider, replaceCover: boolean): Observable<void> {
|
||||
const requestPayload: BookAutoMetadataRefresh = {
|
||||
bookIds: Array.from(selectedBooks),
|
||||
metadataProvider: metadataProvider,
|
||||
replaceCover
|
||||
};
|
||||
return this.http.put<void>(`${this.url}/books/refresh`, requestPayload);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
<div class="flex flex-col gap-6 min-w-full custom-table p-6">
|
||||
<!-- Provider Select -->
|
||||
<p-select
|
||||
[options]="providers"
|
||||
[(ngModel)]="selectedProvider"
|
||||
placeholder="Select Provider"
|
||||
class="w-full"
|
||||
></p-select>
|
||||
<div class="flex flex-col gap-6 p-6 min-w-96">
|
||||
|
||||
<p-select fluid [options]="providers" [(ngModel)]="selectedProvider" placeholder="Select Provider" class="flex-grow"></p-select>
|
||||
|
||||
<!-- Refresh Covers Section -->
|
||||
<div class="flex flex-row items-center gap-4">
|
||||
<p class="pl-2">Refresh covers: </p>
|
||||
<p-checkbox [(ngModel)]="refreshCovers" [binary]="true"></p-checkbox>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<p-button class="pt-4 justify-end" label="Submit" icon="pi pi-check"></p-button>
|
||||
<p-button class="flex pt-4 justify-end" label="Submit" [disabled]="!selectedProvider" (onClick)="submit()"></p-button>
|
||||
</div>
|
||||
|
||||
@@ -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<MetadataRefreshOptions> = new EventEmitter<MetadataRefreshOptions>();
|
||||
|
||||
submit() {
|
||||
const metadataRefreshOptions: MetadataRefreshOptions = {
|
||||
defaultProvider: this.selectedProvider,
|
||||
refreshCovers: this.refreshCovers,
|
||||
};
|
||||
this.metadataOptionsSubmitted.emit(metadataRefreshOptions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<div class="flex flex-col items-center space-y-6">
|
||||
<div class="flex flex-col space-y-6">
|
||||
<ng-container *ngIf="isBasicMode; else advancedOptions">
|
||||
<app-metadata-basic-fetch-options ></app-metadata-basic-fetch-options>
|
||||
<app-metadata-basic-fetch-options (metadataOptionsSubmitted)="onMetadataSubmit($event)"></app-metadata-basic-fetch-options>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #advancedOptions>
|
||||
<app-metadata-advanced-fetch-options (metadataOptionsSubmitted)="onAdvancedMetadataOptionSubmit($event)"></app-metadata-advanced-fetch-options>
|
||||
<app-metadata-advanced-fetch-options (metadataOptionsSubmitted)="onMetadataSubmit($event)"></app-metadata-advanced-fetch-options>
|
||||
</ng-template>
|
||||
|
||||
<p-divider></p-divider>
|
||||
|
||||
@@ -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<number>;
|
||||
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<number>;
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user