From df001a0020d5b7b17e90225a98aa64aedc9822f7 Mon Sep 17 00:00:00 2001 From: "aditya.chandel" <> Date: Wed, 1 Jan 2025 15:28:55 -0700 Subject: [PATCH] WIP: Book parsing --- .../booklore/controller/BookController.java | 47 ++---- .../controller/LibraryController.java | 7 +- .../booklore/exception/ApiError.java | 3 +- .../booklore/model/dto/BookMetadataDTO.java | 10 +- .../booklore/model/entity/BookMetadata.java | 9 +- .../booklore/service/BooksService.java | 156 ++++-------------- .../service/metadata/BookMetadataService.java | 92 +++++++++++ .../BookFetchQuery.java} | 4 +- .../metadata/model/BookMetadataSource.java | 5 + .../metadata/model/FetchedBookMetadata.java | 29 ++++ ...mazonParser.java => AmazonBookParser.java} | 144 ++++++++++------ .../service/metadata/parser/BookParser.java | 8 + .../transformer/BookMetadataTransformer.java | 2 +- .../V1__Create_Library_and_Book_Tables.sql | 2 + .../book-metadata.component.html | 2 +- .../book-metadata/book-metadata.component.ts | 36 +--- booklore-ui/src/app/book/model/book.model.ts | 62 ++----- .../src/app/book/service/book.service.ts | 31 +--- .../metadata-searcher.component.html | 4 +- .../metadata-searcher.component.ts | 63 ++++--- 20 files changed, 364 insertions(+), 352 deletions(-) create mode 100644 booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/BookMetadataService.java rename booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/{parser/model/QueryData.java => model/BookFetchQuery.java} (61%) create mode 100644 booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/model/BookMetadataSource.java create mode 100644 booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/model/FetchedBookMetadata.java rename booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/parser/{AmazonParser.java => AmazonBookParser.java} (71%) create mode 100644 booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/parser/BookParser.java diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/controller/BookController.java b/booklore-api/src/main/java/com/adityachandel/booklore/controller/BookController.java index a04542c59..36ae1d009 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/controller/BookController.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/controller/BookController.java @@ -3,15 +3,14 @@ package com.adityachandel.booklore.controller; import com.adityachandel.booklore.model.dto.BookDTO; import com.adityachandel.booklore.model.dto.BookMetadataDTO; import com.adityachandel.booklore.model.dto.BookViewerSettingDTO; -import com.adityachandel.booklore.model.dto.request.SetMetadataRequest; import com.adityachandel.booklore.model.dto.request.ShelvesAssignmentRequest; -import com.adityachandel.booklore.model.dto.response.GoogleBooksMetadata; import com.adityachandel.booklore.service.BooksService; -import com.adityachandel.booklore.service.metadata.parser.AmazonParser; -import com.adityachandel.booklore.service.metadata.parser.model.QueryData; +import com.adityachandel.booklore.service.metadata.BookMetadataService; +import com.adityachandel.booklore.service.metadata.model.BookFetchQuery; +import com.adityachandel.booklore.service.metadata.model.BookMetadataSource; +import com.adityachandel.booklore.service.metadata.model.FetchedBookMetadata; import jakarta.validation.Valid; import lombok.AllArgsConstructor; -import lombok.extern.java.Log; import org.springframework.core.io.Resource; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -25,16 +24,16 @@ import java.util.List; public class BookController { private BooksService booksService; - private AmazonParser amazonParser; + private BookMetadataService bookMetadataService; @GetMapping("/{bookId}") - public ResponseEntity getBook(@PathVariable long bookId) { - return ResponseEntity.ok(booksService.getBook(bookId)); + public ResponseEntity getBook(@PathVariable long bookId, @RequestParam(required = false, defaultValue = "false") boolean withDescription) { + return ResponseEntity.ok(booksService.getBook(bookId, withDescription)); } @GetMapping - public ResponseEntity> getBooks() { - return ResponseEntity.ok(booksService.getBooks()); + public ResponseEntity> getBooks(@RequestParam(required = false, defaultValue = "false") boolean withDescription) { + return ResponseEntity.ok(booksService.getBooks(withDescription)); } @GetMapping("/search") @@ -69,34 +68,18 @@ public class BookController { return ResponseEntity.ok(booksService.updateLastReadTime(bookId)); } - @GetMapping("/{bookId}/fetch-metadata") - public ResponseEntity> getBookFetchMetadata(@PathVariable long bookId) { - return ResponseEntity.ok(booksService.fetchProspectiveMetadataListByBookId(bookId)); + @PostMapping("/{bookId}/source/{source}/metadata") + public ResponseEntity getBookMetadata(@RequestBody(required = false) BookFetchQuery bookFetchQuery, @PathVariable Long bookId, @PathVariable BookMetadataSource source) { + return ResponseEntity.ok(bookMetadataService.fetchBookMetadata(bookId, source, bookFetchQuery)); } - @PostMapping("/{bookId}/query-for-books") - public ResponseEntity getBookMetadata(@RequestBody(required = false) QueryData queryData, @PathVariable Long bookId) { - return ResponseEntity.ok(amazonParser.queryForBookMetadata(bookId, queryData)); - } - - @GetMapping("/fetch-metadata") - public ResponseEntity> fetchMedataByTerm(@RequestParam String term) { - return ResponseEntity.ok(booksService.fetchProspectiveMetadataListBySearchTerm(term)); - } - - @PutMapping("/{bookId}/set-metadata") - public ResponseEntity setBookMetadata(@RequestBody SetMetadataRequest setMetadataRequest, @PathVariable long bookId) { - return ResponseEntity.ok(booksService.setMetadata(setMetadataRequest, bookId)); + @PutMapping("/{bookId}/source/{source}/metadata") + public ResponseEntity setBookMetadata(@RequestBody FetchedBookMetadata setMetadataRequest, @PathVariable long bookId, @PathVariable BookMetadataSource source) { + return ResponseEntity.ok(booksService.setBookMetadata(bookId, source, setMetadataRequest)); } @PostMapping("/assign-shelves") public ResponseEntity> addBookToShelf(@RequestBody @Valid ShelvesAssignmentRequest request) { return ResponseEntity.ok(booksService.assignShelvesToBooks(request.getBookIds(), request.getShelvesToAssign(), request.getShelvesToUnassign())); } - - @PutMapping("/{bookId}/metadata") - public ResponseEntity updateBookMetadata(@RequestBody BookMetadataDTO metadataDTO, @PathVariable long bookId) throws IOException { - return ResponseEntity.ok(booksService.setMetadata(bookId, metadataDTO)); - } - } \ No newline at end of file diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/controller/LibraryController.java b/booklore-api/src/main/java/com/adityachandel/booklore/controller/LibraryController.java index 06acf7384..3f451a495 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/controller/LibraryController.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/controller/LibraryController.java @@ -1,20 +1,17 @@ package com.adityachandel.booklore.controller; import com.adityachandel.booklore.model.dto.BookDTO; -import com.adityachandel.booklore.model.dto.BookMetadataDTO; import com.adityachandel.booklore.model.dto.BookWithNeighborsDTO; import com.adityachandel.booklore.model.dto.LibraryDTO; import com.adityachandel.booklore.model.dto.request.CreateLibraryRequest; import com.adityachandel.booklore.model.entity.Sort; import com.adityachandel.booklore.service.BooksService; import com.adityachandel.booklore.service.LibraryService; -import com.adityachandel.booklore.service.metadata.parser.AmazonParser; -import com.adityachandel.booklore.service.metadata.parser.model.QueryData; +import com.adityachandel.booklore.service.metadata.parser.AmazonBookParser; import lombok.AllArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.io.IOException; import java.util.List; @@ -25,7 +22,7 @@ public class LibraryController { private LibraryService libraryService; private BooksService booksService; - private AmazonParser amazonParser; + private AmazonBookParser amazonBookParser; @GetMapping("/{libraryId}") public ResponseEntity getLibrary(@PathVariable long libraryId) { 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 fe4c34bb1..e83635fdc 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 @@ -19,7 +19,8 @@ public enum ApiError { FILE_ALREADY_EXISTS(HttpStatus.CONFLICT, "File already exists"), INVALID_QUERY_PARAMETERS(HttpStatus.BAD_REQUEST, "Query parameters are required for the search."), SHELF_ALREADY_EXISTS(HttpStatus.CONFLICT, "Shelf already exists: %s"), - SHELF_NOT_FOUND(HttpStatus.NOT_FOUND, "Shelf not found with ID: %d"); + SHELF_NOT_FOUND(HttpStatus.NOT_FOUND, "Shelf not found with ID: %d"), + METADATA_SOURCE_NOT_IMPLEMENT_OR_DOES_NOT_EXIST(HttpStatus.BAD_REQUEST, "Metadata source not implement or does not exist" ),; private final HttpStatus status; private final String message; diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/BookMetadataDTO.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/BookMetadataDTO.java index cf5176c33..6d3d82cb7 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/BookMetadataDTO.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/BookMetadataDTO.java @@ -3,6 +3,7 @@ package com.adityachandel.booklore.model.dto; import lombok.Builder; import lombok.Data; +import java.time.LocalDate; import java.util.List; @Data @@ -14,16 +15,15 @@ public class BookMetadataDTO { private String title; private String subtitle; private String publisher; - private String publishedDate; + private LocalDate publishedDate; private String description; private String isbn13; private String isbn10; private Integer pageCount; - private String thumbnail; private String language; + private Float rating; + private Integer reviewCount; private List authors; private List categories; - private String rating; - private String reviewCount; - private String printLength; + } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/BookMetadata.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/BookMetadata.java index 2930f8175..952fcfad8 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/BookMetadata.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/BookMetadata.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; import lombok.*; +import java.time.LocalDate; import java.util.List; @Entity @@ -29,7 +30,7 @@ public class BookMetadata { private String publisher; @Column(name = "published_date") - private String publishedDate; + private LocalDate publishedDate; @Column(name = "description", columnDefinition = "TEXT") private String description; @@ -49,6 +50,12 @@ public class BookMetadata { @Column(name = "language", length = 10) private String language; + @Column(name = "rating") + private Float rating; + + @Column(name = "review_count") + private Integer reviewCount; + @OneToOne(fetch = FetchType.LAZY) @MapsId @JoinColumn(name = "book_id") diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/BooksService.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/BooksService.java index 80a661bde..6dcb0319d 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/BooksService.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/BooksService.java @@ -1,16 +1,22 @@ package com.adityachandel.booklore.service; import com.adityachandel.booklore.exception.ApiError; -import com.adityachandel.booklore.model.dto.*; -import com.adityachandel.booklore.model.dto.request.SetMetadataRequest; +import com.adityachandel.booklore.model.dto.BookDTO; +import com.adityachandel.booklore.model.dto.BookMetadataDTO; +import com.adityachandel.booklore.model.dto.BookViewerSettingDTO; +import com.adityachandel.booklore.model.dto.BookWithNeighborsDTO; import com.adityachandel.booklore.model.dto.response.GoogleBooksMetadata; -import com.adityachandel.booklore.model.entity.*; +import com.adityachandel.booklore.model.entity.Author; +import com.adityachandel.booklore.model.entity.Book; +import com.adityachandel.booklore.model.entity.BookViewerSetting; +import com.adityachandel.booklore.model.entity.Shelf; import com.adityachandel.booklore.repository.*; -import com.adityachandel.booklore.transformer.BookMetadataTransformer; +import com.adityachandel.booklore.service.metadata.BookMetadataService; +import com.adityachandel.booklore.service.metadata.model.BookMetadataSource; +import com.adityachandel.booklore.service.metadata.model.FetchedBookMetadata; import com.adityachandel.booklore.transformer.BookSettingTransformer; import com.adityachandel.booklore.transformer.BookTransformer; import com.adityachandel.booklore.util.BookUtils; -import com.adityachandel.booklore.util.DateUtils; import com.adityachandel.booklore.util.FileService; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -24,10 +30,6 @@ import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.time.Instant; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; -import java.util.Arrays; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -40,23 +42,29 @@ public class BooksService { private final BookRepository bookRepository; private final BookViewerSettingRepository bookViewerSettingRepository; private final GoogleBookMetadataService googleBookMetadataService; - private final BookMetadataRepository metadataRepository; - private final AuthorRepository authorRepository; - private final CategoryRepository categoryRepository; private final LibraryRepository libraryRepository; - private final NotificationService notificationService; private final ShelfRepository shelfRepository; private final FileService fileService; + private final BookMetadataService bookMetadataService; - public BookDTO getBook(long bookId) { + public BookDTO getBook(long bookId, boolean withDescription) { Book book = bookRepository.findById(bookId).orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(bookId)); - return BookTransformer.convertToBookDTO(book); + BookDTO bookDTO = BookTransformer.convertToBookDTO(book); + if (!withDescription) { + bookDTO.getMetadata().setDescription(null); + } + return bookDTO; } - public List getBooks() { + public List getBooks(boolean withDescription) { return bookRepository.findAll().stream() .map(BookTransformer::convertToBookDTO) + .peek(bookDTO -> { + if (!withDescription) { + bookDTO.getMetadata().setDescription(null); + } + }) .collect(Collectors.toList()); } @@ -117,118 +125,6 @@ public class BooksService { return googleBookMetadataService.queryByTerm(searchTerm); } - public BookMetadataDTO setMetadata(long bookId, BookMetadataDTO newMetadata) throws IOException { - Book book = bookRepository.findById(bookId).orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(bookId)); - BookMetadata metadata = book.getMetadata(); - metadata.setTitle(newMetadata.getTitle()); - metadata.setSubtitle(newMetadata.getSubtitle()); - metadata.setPublisher(newMetadata.getPublisher()); - metadata.setPublishedDate(DateUtils.parseDateToInstant(newMetadata.getPublishedDate())); - metadata.setLanguage(newMetadata.getLanguage()); - metadata.setIsbn10(newMetadata.getIsbn10()); - metadata.setIsbn13(newMetadata.getIsbn13()); - metadata.setDescription(newMetadata.getDescription()); - metadata.setPageCount(newMetadata.getPageCount()); - if (newMetadata.getAuthors() != null && !newMetadata.getAuthors().isEmpty()) { - List authors = newMetadata.getAuthors().stream() - .map(authorDTO -> authorRepository.findByName(authorDTO.getName()) - .orElseGet(() -> authorRepository.save(Author.builder().name(authorDTO.getName()).build()))) - .collect(Collectors.toList()); - metadata.setAuthors(authors); - } - if (newMetadata.getCategories() != null && !newMetadata.getCategories().isEmpty()) { - List categories = newMetadata - .getCategories() - .stream() - .map(CategoryDTO::getName) - .collect(Collectors.toSet()) - .stream() - .map(categoryName -> categoryRepository.findByName(categoryName) - .orElseGet(() -> categoryRepository.save(Category.builder().name(categoryName).build()))) - .collect(Collectors.toList()); - metadata.setCategories(categories); - } - - if(newMetadata.getThumbnail() != null && !newMetadata.getThumbnail().isEmpty()) { - String thumbnailPath = fileService.createThumbnail(bookId, newMetadata.getThumbnail(), "amz"); - metadata.setThumbnail(thumbnailPath); - } - - authorRepository.saveAll(metadata.getAuthors()); - categoryRepository.saveAll(metadata.getCategories()); - metadataRepository.save(metadata); - return BookMetadataTransformer.convertToBookDTO(metadata); - } - - public BookDTO setMetadata(SetMetadataRequest setMetadataRequest, long bookId) { - Book book = bookRepository.findById(bookId).orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(bookId)); - GoogleBooksMetadata gMetadata = googleBookMetadataService.getByGoogleBookId(setMetadataRequest.getGoogleBookId()); - BookMetadata metadata = book.getMetadata(); - metadata.setDescription(gMetadata.getDescription()); - metadata.setTitle(gMetadata.getTitle()); - metadata.setLanguage(gMetadata.getLanguage()); - metadata.setPublisher(gMetadata.getPublisher()); - String publishedDate = gMetadata.getPublishedDate(); - if (publishedDate != null && !publishedDate.isEmpty()) { - String normalizeDate = normalizeDate(publishedDate); - metadata.setPublishedDate(normalizeDate); - } - metadata.setSubtitle(gMetadata.getSubtitle()); - metadata.setPageCount(gMetadata.getPageCount()); - metadata.setThumbnail(gMetadata.getThumbnail()); - if (gMetadata.getAuthors() != null && !gMetadata.getAuthors().isEmpty()) { - List authors = gMetadata.getAuthors().stream() - .map(authorName -> authorRepository.findByName(authorName) - .orElseGet(() -> authorRepository.save(Author.builder().name(authorName).build()))) - .collect(Collectors.toList()); - metadata.setAuthors(authors); - } - if (gMetadata.getCategories() != null && !gMetadata.getCategories().isEmpty()) { - List categories = gMetadata - .getCategories() - .stream() - .map(c -> Arrays.stream(c.split("/")) - .map(String::trim) - .filter(s -> !s.isEmpty() && !s.equalsIgnoreCase("General")) - .toList()) - .flatMap(List::stream) - .collect(Collectors.toSet()) - .stream() - .map(categoryName -> categoryRepository.findByName(categoryName) - .orElseGet(() -> categoryRepository.save(Category.builder().name(categoryName).build()))) - .collect(Collectors.toList()); - metadata.setCategories(categories); - } - metadata.setIsbn10(gMetadata.getIsbn10()); - metadata.setIsbn13(gMetadata.getIsbn13()); - authorRepository.saveAll(metadata.getAuthors()); - categoryRepository.saveAll(metadata.getCategories()); - metadataRepository.save(metadata); - - return BookTransformer.convertToBookDTO(book); - } - - public String normalizeDate(String input) { - DateTimeFormatter fullDateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); - if (input.matches("\\d{4}")) { - return input + "-01-01"; - } - try { - LocalDate.parse(input, fullDateFormatter); - return input; - } catch (DateTimeParseException e) { - throw new IllegalArgumentException("Invalid date format: " + input); - } - } - - public String getFileNameWithoutExtension(String fileName) { - int dotIndex = fileName.lastIndexOf('.'); - if (dotIndex == -1) { - return fileName; - } else { - return fileName.substring(0, dotIndex); - } - } public BookWithNeighborsDTO getBookWithNeighbours(long libraryId, long bookId) { libraryRepository.findById(libraryId).orElseThrow(() -> ApiError.LIBRARY_NOT_FOUND.createException(libraryId)); @@ -268,4 +164,8 @@ public class BooksService { Book book = bookRepository.findById(bookId).orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(bookId)); return fileService.getBookCover(book.getMetadata().getThumbnail()); } + + public BookMetadataDTO setBookMetadata(long bookId, BookMetadataSource source, FetchedBookMetadata setMetadataRequest) { + return bookMetadataService.setBookMetadata(bookId, setMetadataRequest, source); + } } \ No newline at end of file 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 new file mode 100644 index 000000000..7802fbc4a --- /dev/null +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/BookMetadataService.java @@ -0,0 +1,92 @@ +package com.adityachandel.booklore.service.metadata; + +import com.adityachandel.booklore.exception.ApiError; +import com.adityachandel.booklore.model.dto.BookMetadataDTO; +import com.adityachandel.booklore.model.dto.CategoryDTO; +import com.adityachandel.booklore.model.entity.Author; +import com.adityachandel.booklore.model.entity.Book; +import com.adityachandel.booklore.model.entity.BookMetadata; +import com.adityachandel.booklore.model.entity.Category; +import com.adityachandel.booklore.repository.AuthorRepository; +import com.adityachandel.booklore.repository.BookMetadataRepository; +import com.adityachandel.booklore.repository.BookRepository; +import com.adityachandel.booklore.repository.CategoryRepository; +import com.adityachandel.booklore.service.metadata.model.BookFetchQuery; +import com.adityachandel.booklore.service.metadata.model.BookMetadataSource; +import com.adityachandel.booklore.service.metadata.model.FetchedBookMetadata; +import com.adityachandel.booklore.service.metadata.parser.AmazonBookParser; +import com.adityachandel.booklore.transformer.BookMetadataTransformer; +import com.adityachandel.booklore.util.FileService; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@AllArgsConstructor +public class BookMetadataService { + + private AmazonBookParser amazonBookParser; + private BookRepository bookRepository; + private AuthorRepository authorRepository; + private BookMetadataRepository bookMetadataRepository; + private CategoryRepository categoryRepository; + private FileService fileService; + + public FetchedBookMetadata fetchBookMetadata(long bookId, BookMetadataSource source, BookFetchQuery bookFetchQuery) { + if (source == BookMetadataSource.AMAZON) { + return amazonBookParser.fetchMetadata(bookId, bookFetchQuery); + } else { + throw ApiError.METADATA_SOURCE_NOT_IMPLEMENT_OR_DOES_NOT_EXIST.createException(); + } + } + + public BookMetadataDTO setBookMetadata(long bookId, FetchedBookMetadata newMetadata, BookMetadataSource source) { + Book book = bookRepository.findById(bookId).orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(bookId)); + BookMetadata metadata = book.getMetadata(); + metadata.setTitle(newMetadata.getTitle()); + metadata.setSubtitle(newMetadata.getSubtitle()); + metadata.setPublisher(newMetadata.getPublisher()); + metadata.setPublishedDate(newMetadata.getPublishedDate()); + metadata.setLanguage(newMetadata.getLanguage()); + metadata.setIsbn10(newMetadata.getIsbn10()); + metadata.setIsbn13(newMetadata.getIsbn13()); + metadata.setDescription(newMetadata.getDescription()); + metadata.setPageCount(newMetadata.getPageCount()); + if (newMetadata.getAuthors() != null && !newMetadata.getAuthors().isEmpty()) { + List authors = newMetadata.getAuthors().stream() + .map(authorName -> authorRepository.findByName(authorName) + .orElseGet(() -> authorRepository.save(Author.builder().name(authorName).build()))) + .collect(Collectors.toList()); + metadata.setAuthors(authors); + } + if (newMetadata.getCategories() != null && !newMetadata.getCategories().isEmpty()) { + List categories = new HashSet<>(newMetadata + .getCategories()) + .stream() + .map(categoryName -> categoryRepository.findByName(categoryName) + .orElseGet(() -> categoryRepository.save(Category.builder().name(categoryName).build()))) + .collect(Collectors.toList()); + metadata.setCategories(categories); + } + + if (newMetadata.getThumbnailUrl() != null && !newMetadata.getThumbnailUrl().isEmpty()) { + String thumbnailPath = null; + try { + thumbnailPath = fileService.createThumbnail(bookId, newMetadata.getThumbnailUrl(), source.name()); + } catch (IOException e) { + throw new RuntimeException(e); + } + metadata.setThumbnail(thumbnailPath); + } + + authorRepository.saveAll(metadata.getAuthors()); + categoryRepository.saveAll(metadata.getCategories()); + bookMetadataRepository.save(metadata); + return BookMetadataTransformer.convertToBookDTO(metadata); + } + +} \ No newline at end of file diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/parser/model/QueryData.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/model/BookFetchQuery.java similarity index 61% rename from booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/parser/model/QueryData.java rename to booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/model/BookFetchQuery.java index fe0f53d96..f57dbed3f 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/parser/model/QueryData.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/model/BookFetchQuery.java @@ -1,11 +1,11 @@ -package com.adityachandel.booklore.service.metadata.parser.model; +package com.adityachandel.booklore.service.metadata.model; import lombok.Builder; import lombok.Data; @Builder @Data -public class QueryData { +public class BookFetchQuery { private String isbn; private String bookTitle; private String author; diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/model/BookMetadataSource.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/model/BookMetadataSource.java new file mode 100644 index 000000000..1ab18c502 --- /dev/null +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/model/BookMetadataSource.java @@ -0,0 +1,5 @@ +package com.adityachandel.booklore.service.metadata.model; + +public enum BookMetadataSource { + AMAZON, GOOGLE, GOOD_READS +} diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/model/FetchedBookMetadata.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/model/FetchedBookMetadata.java new file mode 100644 index 000000000..70f8fde9e --- /dev/null +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/model/FetchedBookMetadata.java @@ -0,0 +1,29 @@ +package com.adityachandel.booklore.service.metadata.model; + +import lombok.Builder; +import lombok.Data; + +import java.time.LocalDate; +import java.util.List; + +@Data +@Builder +public class FetchedBookMetadata { + private Long bookId; + private String googleBookId; + private String amazonBookId; + private String title; + private String subtitle; + private String publisher; + private LocalDate publishedDate; + private String description; + private String isbn13; + private String isbn10; + private Integer pageCount; + private String thumbnailUrl; + private String language; + private Float rating; + private Integer reviewCount; + private List authors; + private List categories; +} diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/parser/AmazonParser.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/parser/AmazonBookParser.java similarity index 71% rename from booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/parser/AmazonParser.java rename to booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/parser/AmazonBookParser.java index 90c83c7a3..6628f3013 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/parser/AmazonParser.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/parser/AmazonBookParser.java @@ -1,14 +1,10 @@ package com.adityachandel.booklore.service.metadata.parser; import com.adityachandel.booklore.exception.ApiError; -import com.adityachandel.booklore.model.dto.AuthorDTO; -import com.adityachandel.booklore.model.dto.BookMetadataDTO; -import com.adityachandel.booklore.model.dto.CategoryDTO; import com.adityachandel.booklore.model.entity.Book; -import com.adityachandel.booklore.model.entity.Library; import com.adityachandel.booklore.repository.BookRepository; -import com.adityachandel.booklore.repository.LibraryRepository; -import com.adityachandel.booklore.service.metadata.parser.model.QueryData; +import com.adityachandel.booklore.service.metadata.model.FetchedBookMetadata; +import com.adityachandel.booklore.service.metadata.model.BookFetchQuery; import com.adityachandel.booklore.util.BookUtils; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -20,6 +16,10 @@ import org.jsoup.select.Elements; import org.springframework.stereotype.Service; import java.io.IOException; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -27,59 +27,99 @@ import java.util.stream.Collectors; @Slf4j @Service @AllArgsConstructor -public class AmazonParser { +public class AmazonBookParser implements BookParser { private BookRepository bookRepository; - public BookMetadataDTO queryForBookMetadata(Long bookId, QueryData queryData) { + public FetchedBookMetadata fetchMetadata(Long bookId, BookFetchQuery bookFetchQuery) { Book book = bookRepository.findById(bookId).orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(bookId)); - if (queryData == null || (queryData.getBookTitle() == null && queryData.getAuthor() == null && queryData.getIsbn() == null)) { + if (bookFetchQuery == null || (bookFetchQuery.getBookTitle() == null && bookFetchQuery.getAuthor() == null && bookFetchQuery.getIsbn() == null)) { String title = book.getMetadata().getTitle(); if (title == null || title.isEmpty()) { String cleanFileName = BookUtils.cleanFileName(book.getFileName()); - queryData = QueryData.builder().bookTitle(cleanFileName).build(); + bookFetchQuery = BookFetchQuery.builder().bookTitle(cleanFileName).build(); } else { - queryData = QueryData.builder().bookTitle(title).build(); + bookFetchQuery = BookFetchQuery.builder().bookTitle(title).build(); } } - String amazonBookId = getAmazonBookId(queryData); + String amazonBookId = getAmazonBookId(bookFetchQuery); if (amazonBookId == null) { return null; } return getBookMetadata(amazonBookId); } - public String getAmazonBookId(QueryData queryData) { - String queryUrl = buildQueryUrl(queryData); + private String getAmazonBookId(BookFetchQuery bookFetchQuery) { + String queryUrl = buildQueryUrl(bookFetchQuery); if (queryUrl == null) { + log.error("Query URL is null, cannot proceed."); return null; } try { Document doc = fetchDoc(queryUrl); Element searchResults = doc.select("span[data-component-type=s-search-results]").first(); + if (searchResults == null) { + log.error("No search results found for query: {}", queryUrl); + return null; + } + Element item = searchResults.select("div[role=listitem][data-index=2]").first(); - return item.attr("data-asin"); + if (item == null) { + log.error("No item found in the search results."); + return null; + } + + String bookLink = null; + + // Try to get 'Paperback' and 'Hardcover' links + for (String type : new String[]{"Paperback", "Hardcover"}) { + Element link = item.select("a:containsOwn(" + type + ")").first(); + if (link != null) { + bookLink = link.attr("href"); + log.info("{} link found: {}", type, bookLink); + break; // Take the first found link, whether Paperback or Hardcover + } else { + log.info("No link containing '{}' found.", type); + } + } + + if (bookLink != null) { + String asin = extractAsinFromUrl(bookLink); + log.info("Book ASIN extracted: {}", asin); + return asin; + } else { + String asin = item.attr("data-asin"); + log.info("No book link found, returning ASIN: {}", asin); + return asin; + } } catch (Exception e) { - log.error("Failed to get asin: {}", e.getMessage()); + log.error("Failed to get asin: {}", e.getMessage(), e); return null; } } - public BookMetadataDTO getBookMetadata(String amazonBookId) { + private String extractAsinFromUrl(String url) { + // Extract the ASIN (book ID) from the URL, which will be the part after "/dp/" + String[] parts = url.split("/dp/"); + if (parts.length > 1) { + String[] asinParts = parts[1].split("/"); + return asinParts[0]; + } + return null; + } + + private FetchedBookMetadata getBookMetadata(String amazonBookId) { + log.info("Fetching book metadata for amazon book {}", amazonBookId); Document doc = fetchDoc("https://www.amazon.com/dp/" + amazonBookId); - return BookMetadataDTO.builder() + return FetchedBookMetadata.builder() .amazonBookId(amazonBookId) .title(getTitle(doc)) .subtitle(getSubtitle(doc)) - .authors(getAuthors(doc).stream() - .map(name -> AuthorDTO.builder().name(name).build()) - .collect(Collectors.toList())) - .categories(getBestSellerCategories(doc).stream() - .map(category -> CategoryDTO.builder().name(category).build()) - .collect(Collectors.toList())) + .authors(getAuthors(doc).stream().toList()) + .categories(getBestSellerCategories(doc).stream().toList()) .description(getDescription(doc)) .isbn13(getIsbn13(doc)) .isbn10(getIsbn10(doc)) @@ -87,32 +127,32 @@ public class AmazonParser { .publishedDate(getPublicationDate(doc)) .language(getLanguage(doc)) .pageCount(getPageCount(doc)) - .thumbnail(getThumbnail(doc)) + .thumbnailUrl(getThumbnail(doc)) .rating(getRating(doc)) .reviewCount(getReviewCount(doc)) - .printLength(getPrintLength(doc)) .build(); } - private String buildQueryUrl(QueryData queryData) { + private String buildQueryUrl(BookFetchQuery bookFetchQuery) { StringBuilder queryBuilder = new StringBuilder("https://www.amazon.com/s/?search-alias=stripbooks&unfiltered=1&sort=relevanceexprank"); - if (queryData.getIsbn() != null && !queryData.getIsbn().isEmpty()) { - queryBuilder.append("&field-isbn=").append(queryData.getIsbn()); + if (bookFetchQuery.getIsbn() != null && !bookFetchQuery.getIsbn().isEmpty()) { + queryBuilder.append("&field-isbn=").append(bookFetchQuery.getIsbn()); } - if (queryData.getBookTitle() != null && !queryData.getBookTitle().isEmpty()) { - queryBuilder.append("&field-title=").append(queryData.getBookTitle().replace(" ", "%20")); + if (bookFetchQuery.getBookTitle() != null && !bookFetchQuery.getBookTitle().isEmpty()) { + queryBuilder.append("&field-title=").append(bookFetchQuery.getBookTitle().replace(" ", "%20")); } - if (queryData.getAuthor() != null && !queryData.getAuthor().isEmpty()) { - queryBuilder.append("&field-author=").append(queryData.getAuthor().replace(" ", "%20")); + if (bookFetchQuery.getAuthor() != null && !bookFetchQuery.getAuthor().isEmpty()) { + queryBuilder.append("&field-author=").append(bookFetchQuery.getAuthor().replace(" ", "%20")); } - if (queryData.getIsbn() == null && queryData.getBookTitle() == null && queryData.getAuthor() != null) { + if (bookFetchQuery.getIsbn() == null && bookFetchQuery.getBookTitle() == null && bookFetchQuery.getAuthor() != null) { return null; } + log.info("Query URL: {}", queryBuilder.toString()); return queryBuilder.toString(); } @@ -205,11 +245,11 @@ public class AmazonParser { return null; } - private String getPublicationDate(Document doc) { + private LocalDate getPublicationDate(Document doc) { try { Element publicationDateElement = doc.select("#rpi-attribute-book_details-publication_date .rpi-attribute-value span").first(); if (publicationDateElement != null) { - return publicationDateElement.text(); + return parseAmazonDate(publicationDateElement.text()); } log.error("Error fetching publication date: Element not found."); } catch (Exception e) { @@ -231,19 +271,6 @@ public class AmazonParser { return null; } - private String getPrintLength(Document doc) { - try { - Element printLengthElement = doc.select("#rpi-attribute-book_details-fiona_pages .rpi-attribute-value span").first(); - if (printLengthElement != null) { - return printLengthElement.text(); - } - log.error("Error fetching print length: Element not found."); - } catch (Exception e) { - log.error("Error fetching print length: {}", e.getMessage()); - } - return null; - } - private Set getBestSellerCategories(Document doc) { try { Element bestSellerCategoriesElement = doc.select("#detailBullets_feature_div").first(); @@ -262,13 +289,16 @@ public class AmazonParser { return Set.of(); } - private String getRating(Document doc) { + private Float getRating(Document doc) { try { Element reviewDiv = doc.select("div#averageCustomerReviews_feature_div").first(); if (reviewDiv != null) { Elements ratingElements = reviewDiv.select("span#acrPopover span.a-size-base.a-color-base"); if (!ratingElements.isEmpty()) { - return Objects.requireNonNull(ratingElements.first()).text(); + String text = Objects.requireNonNull(ratingElements.first()).text(); + if (!text.isEmpty()) { + return Float.parseFloat(text); + } } } } catch (Exception e) { @@ -277,13 +307,16 @@ public class AmazonParser { return null; } - private String getReviewCount(Document doc) { + private Integer getReviewCount(Document doc) { try { Element reviewDiv = doc.select("div#averageCustomerReviews_feature_div").first(); if (reviewDiv != null) { Element reviewCountElement = reviewDiv.getElementById("acrCustomerReviewText"); if (reviewCountElement != null) { - return Objects.requireNonNull(reviewCountElement).text().split(" ")[0]; + String reviewCount = Objects.requireNonNull(reviewCountElement).text().split(" ")[0]; + if (!reviewCount.isEmpty()) { + return Integer.parseInt(reviewCount); + } } } } catch (Exception e) { @@ -355,4 +388,9 @@ public class AmazonParser { throw new RuntimeException(e); } } + + private LocalDate parseAmazonDate(String dateString) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMMM d, yyyy"); + return LocalDate.parse(dateString, formatter); + } } \ No newline at end of file diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/parser/BookParser.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/parser/BookParser.java new file mode 100644 index 000000000..f046d4842 --- /dev/null +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/parser/BookParser.java @@ -0,0 +1,8 @@ +package com.adityachandel.booklore.service.metadata.parser; + +import com.adityachandel.booklore.service.metadata.model.FetchedBookMetadata; +import com.adityachandel.booklore.service.metadata.model.BookFetchQuery; + +public interface BookParser { + FetchedBookMetadata fetchMetadata(Long bookId, BookFetchQuery bookFetchQuery); +} diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/transformer/BookMetadataTransformer.java b/booklore-api/src/main/java/com/adityachandel/booklore/transformer/BookMetadataTransformer.java index a145b5dbf..d2fbf9744 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/transformer/BookMetadataTransformer.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/transformer/BookMetadataTransformer.java @@ -19,10 +19,10 @@ public class BookMetadataTransformer { return BookMetadataDTO.builder() .bookId(bookMetadata.getBookId()) .title(bookMetadata.getTitle()) - .description(bookMetadata.getDescription()) .isbn10(bookMetadata.getIsbn10()) .isbn13(bookMetadata.getIsbn13()) .publisher(bookMetadata.getPublisher()) + .description(bookMetadata.getDescription()) .subtitle(bookMetadata.getSubtitle()) .language(bookMetadata.getLanguage()) .pageCount(bookMetadata.getPageCount()) diff --git a/booklore-api/src/main/resources/db/migration/V1__Create_Library_and_Book_Tables.sql b/booklore-api/src/main/resources/db/migration/V1__Create_Library_and_Book_Tables.sql index 5e612af1a..94fb57a7f 100644 --- a/booklore-api/src/main/resources/db/migration/V1__Create_Library_and_Book_Tables.sql +++ b/booklore-api/src/main/resources/db/migration/V1__Create_Library_and_Book_Tables.sql @@ -34,6 +34,8 @@ CREATE TABLE IF NOT EXISTS book_metadata page_count INT, thumbnail VARCHAR(1000), language VARCHAR(10), + rating FLOAT, + review_count INT, CONSTRAINT fk_book_metadata FOREIGN KEY (book_id) REFERENCES book (id) ON DELETE CASCADE ); diff --git a/booklore-ui/src/app/book/component/book-metadata/book-metadata.component.html b/booklore-ui/src/app/book/component/book-metadata/book-metadata.component.html index 6f8b9ec19..e26b00479 100644 --- a/booklore-ui/src/app/book/component/book-metadata/book-metadata.component.html +++ b/booklore-ui/src/app/book/component/book-metadata/book-metadata.component.html @@ -37,7 +37,7 @@ {{ book?.metadata?.title }}: {{ book?.metadata?.subtitle }}
- +
diff --git a/booklore-ui/src/app/book/component/book-metadata/book-metadata.component.ts b/booklore-ui/src/app/book/component/book-metadata/book-metadata.component.ts index 5a67d94f6..a0fb5aca3 100644 --- a/booklore-ui/src/app/book/component/book-metadata/book-metadata.component.ts +++ b/booklore-ui/src/app/book/component/book-metadata/book-metadata.component.ts @@ -3,14 +3,12 @@ import {ActivatedRoute, Router} from '@angular/router'; import {Book} from '../../model/book.model'; import {Button} from 'primeng/button'; import {NgForOf, NgIf} from '@angular/common'; -import {BookMetadataDialogComponent} from '../book-metadata-dialog/book-metadata-dialog.component'; import {DialogService, DynamicDialogRef} from 'primeng/dynamicdialog'; -import {Subscription} from 'rxjs'; +import {forkJoin, Subscription} from 'rxjs'; import {TagModule} from 'primeng/tag'; import {BookService} from '../../service/book.service'; import {LibraryService} from '../../service/library.service'; import {MetadataSearcherComponent} from '../../../metadata-searcher/metadata-searcher.component'; -import {ProgressSpinner} from 'primeng/progressspinner'; import {LoadingService} from '../../../loading.service'; @Component({ @@ -78,10 +76,13 @@ export class BookMetadataComponent implements OnInit, OnDestroy { return 'No authors available'; } - openEditDialog(bookId: number, libraryId: number) { + openEditDialog(bookId: number) { this.loadingService.show(); - this.bookService.getFetchBookMetadata(bookId).subscribe({ - next: (fetchedMetadata) => { + forkJoin({ + book: this.bookService.getBookByIdFromAPI(bookId, true), + fetchedMetadata: this.bookService.fetchMetadataFromSource(bookId) + }).subscribe({ + next: ({ book, fetchedMetadata }) => { this.loadingService.hide(); this.dialogRef = this.dialogService.open(MetadataSearcherComponent, { header: 'Update Book Metadata', @@ -95,7 +96,7 @@ export class BookMetadataComponent implements OnInit, OnDestroy { 'padding': '1.25rem 1.25rem 0', }, data: { - currentMetadata: this.book?.metadata, + currentMetadata: book.metadata, fetchedMetadata: fetchedMetadata, book: this.book } @@ -118,27 +119,6 @@ export class BookMetadataComponent implements OnInit, OnDestroy { }); } - /*openEditDialog1(bookId: number | undefined, libraryId: number | undefined) { - this.dialogRef = this.dialogService.open(BookMetadataDialogComponent, { - header: 'Metadata: Google Books', - modal: true, - closable: true, - width: '65%', - height: '85%', - data: { - bookId: bookId, - libraryId: libraryId, - bookTitle: this.book?.metadata?.title, - }, - }); - - this.dialogSubscription = this.dialogRef.onClose.subscribe(() => { - if (this.book?.id && this.book.libraryId) { - this.loadBookWithNeighbors(this.book.id, this.book.libraryId); - } - }); - }*/ - coverImageSrc(bookId: number | undefined): string { if (bookId === null) { return 'assets/book-cover-metadata.png'; diff --git a/booklore-ui/src/app/book/model/book.model.ts b/booklore-ui/src/app/book/model/book.model.ts index 97d4016c6..d52c5d37c 100644 --- a/booklore-ui/src/app/book/model/book.model.ts +++ b/booklore-ui/src/app/book/model/book.model.ts @@ -17,7 +17,7 @@ export interface BookMetadata { publisher: string; publishedDate: string; isbn10: string; - description: string; + description?: string; pageCount: number; language: string; googleBookId: string; @@ -25,34 +25,23 @@ export interface BookMetadata { } export interface FetchedMetadata { - title: string; - subtitle?: string; - authors: Author[]; - categories: Category[]; - publisher: string; - publishedDate: string; - isbn10: string; - description: string; - pageCount: number; - language: string; - googleBookId: string; - thumbnail: string; - [key: string]: any; -} - -export interface UpdateMedata { - title?: string; - subtitle?: string; - authors?: Author[]; - categories?: Category[]; - publisher?: string; - publishedDate?: string; - isbn10?: string; - description?: string; - pageCount?: number; - language?: string; - googleBookId?: string; - thumbnail?: string; + bookId: number | null; + googleBookId: string | null; + amazonBookId: string | null; + title: string | null; + subtitle?: string | null; + publisher: string | null; + publishedDate: string | null; + description: string | null; + isbn13: string | null; + isbn10: string | null; + pageCount: number | null; + thumbnailUrl: string | null; + language: string | null; + rating: number | null; + reviewCount: number | null; + authors: string[]; + categories: string[]; [key: string]: any; } @@ -66,30 +55,15 @@ export interface Category { name: string; } - export interface BookWithNeighborsDTO { currentBook: Book; previousBookId: number | null; nextBookId: number | null; } -export interface BookUpdateEvent { - libraryId: number; - filename: string; - book: { - id: number; - libraryId: number; - fileName: string; - addedOn: string; - metadata: BookMetadata; - }; - parsingStatus: string; -} - export interface BookSetting { pageNumber: number; zoom: number | string; sidebar_visible: boolean; spread: 'off' | 'even' | 'odd'; } - diff --git a/booklore-ui/src/app/book/service/book.service.ts b/booklore-ui/src/app/book/service/book.service.ts index db88526b9..cfd1f2028 100644 --- a/booklore-ui/src/app/book/service/book.service.ts +++ b/booklore-ui/src/app/book/service/book.service.ts @@ -2,7 +2,7 @@ import {Injectable} from '@angular/core'; import {BehaviorSubject, Observable, of} from 'rxjs'; import {HttpClient} from '@angular/common/http'; import {catchError, map, switchMap, tap} from 'rxjs/operators'; -import {Book, BookMetadata, BookSetting, FetchedMetadata, UpdateMedata} from '../model/book.model'; +import {Book, BookMetadata, BookSetting, FetchedMetadata} from '../model/book.model'; import {BookState} from '../model/state/book-state.model'; @Injectable({ @@ -187,28 +187,15 @@ export class BookService { this.bookStateSubject.next({...currentState, books: filteredBooks}); } - getFetchBookMetadata(bookId: number): Observable { - return this.http.post(`${this.url}/${bookId}/query-for-books`, null); + getBookByIdFromAPI(bookId: number, withDescription: boolean) { + return this.http.get(`${this.url}/${bookId}`); } - updateMetadata(bookId: number, updateMedata: UpdateMedata): Observable { - return this.http.put(`${this.url}/${bookId}/metadata`, updateMedata).pipe( - map((updatedMetadata) => { - const currentState = this.bookStateSubject.value; - const updatedBooks = currentState.books?.map((book) => { - if (book.id === bookId) { - return { ...book, metadata: updatedMetadata }; - } - return book; - }) || []; - this.bookStateSubject.next({ ...currentState, books: updatedBooks }); - return updatedMetadata; - }), - catchError((error) => { - const currentState = this.bookStateSubject.value; - this.bookStateSubject.next({ ...currentState, error: error.message }); - throw error; - }) - ); + fetchMetadataFromSource(bookId: number): Observable { + return this.http.post(`${this.url}/${bookId}/source/AMAZON/metadata`, null); + } + + updateMetadata(bookId: number, updateMedata: FetchedMetadata): Observable { + return this.http.put(`${this.url}/${bookId}/source/AMAZON/metadata`, updateMedata); } } diff --git a/booklore-ui/src/app/metadata-searcher/metadata-searcher.component.html b/booklore-ui/src/app/metadata-searcher/metadata-searcher.component.html index 04832a7ec..58d1d8b36 100644 --- a/booklore-ui/src/app/metadata-searcher/metadata-searcher.component.html +++ b/booklore-ui/src/app/metadata-searcher/metadata-searcher.component.html @@ -83,9 +83,9 @@
Book Thumbnail - Fetched Thumbnail + Fetched Thumbnail - Fetched Thumbnail + Fetched Thumbnail
diff --git a/booklore-ui/src/app/metadata-searcher/metadata-searcher.component.ts b/booklore-ui/src/app/metadata-searcher/metadata-searcher.component.ts index 91ee5d80e..308a98c04 100644 --- a/booklore-ui/src/app/metadata-searcher/metadata-searcher.component.ts +++ b/booklore-ui/src/app/metadata-searcher/metadata-searcher.component.ts @@ -1,5 +1,5 @@ import {Component} from '@angular/core'; -import {Book, FetchedMetadata, UpdateMedata} from '../book/model/book.model'; +import {Book, BookMetadata, FetchedMetadata} from '../book/model/book.model'; import {DynamicDialogConfig, DynamicDialogRef} from 'primeng/dynamicdialog'; import {BookService} from '../book/service/book.service'; import {MessageService} from 'primeng/api'; @@ -13,7 +13,7 @@ import {MessageService} from 'primeng/api'; export class MetadataSearcherComponent { fetchedMetadata: FetchedMetadata; - toUpdateMetadata: UpdateMedata; + toUpdateMetadata: FetchedMetadata; book: Book; loading: boolean = false; updateThumbnail: boolean = false; @@ -21,60 +21,47 @@ export class MetadataSearcherComponent { constructor(public dynamicDialogConfig: DynamicDialogConfig, private dynamicDialogRef: DynamicDialogRef, private bookService: BookService, private messageService: MessageService) { - this.fetchedMetadata = this.dynamicDialogConfig.data.fetchedMetadata; - this.toUpdateMetadata = JSON.parse(JSON.stringify(this.dynamicDialogConfig.data.currentMetadata)); + this.toUpdateMetadata = this.convertBookMetadataToFetchedMetadata(this.dynamicDialogConfig.data.currentMetadata); this.book = this.dynamicDialogConfig.data.book; } - copyFetchedToCurrent(field: keyof UpdateMedata) { + copyFetchedToCurrent(field: keyof FetchedMetadata) { if (this.fetchedMetadata && this.toUpdateMetadata) { this.toUpdateMetadata[field] = this.fetchedMetadata[field]; } } get currentAuthorsString(): string { - return this.toUpdateMetadata.authors ? this.toUpdateMetadata.authors.map(author => author.name).join(', ') : ''; + return this.toUpdateMetadata.authors ? this.toUpdateMetadata.authors.map(author => author).join(', ') : ''; } set currentAuthorsString(value: string) { - this.toUpdateMetadata.authors = value.split(',').map(name => ({ - id: this.toUpdateMetadata.authors?.find(author => author.name.trim() === name.trim())?.id || 0, - name: name.trim() - })); + this.toUpdateMetadata.authors = value.split(','); } get fetchedAuthorsString(): string { - return this.fetchedMetadata.authors ? this.fetchedMetadata.authors.map(author => author.name).join(', ') : ''; + return this.fetchedMetadata.authors ? this.fetchedMetadata.authors.map(author => author).join(', ') : ''; } set fetchedAuthorsString(value: string) { - this.fetchedMetadata.authors = value.split(',').map(name => ({ - id: this.fetchedMetadata.authors?.find(author => author.name.trim() === name.trim())?.id || 0, - name: name.trim() - })); + this.fetchedMetadata.authors = value.split(','); } get currentCategoriesString(): string { - return this.toUpdateMetadata.categories ? this.toUpdateMetadata.categories.map(category => category.name).join(', ') : ''; + return this.toUpdateMetadata.categories ? this.toUpdateMetadata.categories.map(category => category).join(', ') : ''; } set currentCategoriesString(value: string) { - this.toUpdateMetadata.categories = value.split(',').map(name => ({ - id: this.toUpdateMetadata.categories?.find(category => category.name.trim() === name.trim())?.id || 0, - name: name.trim() - })); + this.toUpdateMetadata.categories = value.split(','); } get fetchedCategoriesString(): string { - return this.fetchedMetadata.categories ? this.fetchedMetadata.categories.map(category => category.name).join(', ') : ''; + return this.fetchedMetadata.categories ? this.fetchedMetadata.categories.map(category => category).join(', ') : ''; } set fetchedCategoriesString(value: string) { - this.fetchedMetadata.categories = value.split(',').map(name => ({ - id: this.fetchedMetadata.categories?.find(category => category.name.trim() === name.trim())?.id || 0, - name: name.trim() - })); + this.fetchedMetadata.categories = value.split(','); } closeDialog() { @@ -142,12 +129,34 @@ export class MetadataSearcherComponent { this.toUpdateMetadata.categories = this.fetchedMetadata.categories; } - if(this.updateThumbnail) { - this.toUpdateMetadata.thumbnail = this.fetchedMetadata.thumbnail; + if (this.updateThumbnail) { + this.toUpdateMetadata.thumbnailUrl = this.fetchedMetadata.thumbnailUrl; } } shouldUpdateThumbnail() { this.updateThumbnail = true; } + + convertBookMetadataToFetchedMetadata(bookMetadata: BookMetadata): FetchedMetadata { + return { + bookId: null, + googleBookId: bookMetadata.googleBookId || null, + amazonBookId: '', + title: bookMetadata.title || null, + subtitle: bookMetadata.subtitle || null, + publisher: bookMetadata.publisher || null, + publishedDate: bookMetadata.publishedDate || null, + description: bookMetadata.description || null, + isbn13: '', + isbn10: bookMetadata.isbn10 || null, + pageCount: bookMetadata.pageCount || null, + thumbnailUrl: '', + language: bookMetadata.language || null, + rating: null, + reviewCount: null, + authors: bookMetadata.authors.map(author => author.name), + categories: bookMetadata.categories.map(category => category.name), + }; + } }