mirror of
https://github.com/booklore-app/booklore.git
synced 2025-12-23 22:28:11 -05:00
WIP: New metadata engine
This commit is contained in:
@@ -43,7 +43,7 @@ dependencies {
|
||||
implementation 'org.apache.pdfbox:jbig2-imageio:3.0.3'
|
||||
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-websocket:3.4.0'
|
||||
|
||||
implementation 'org.jsoup:jsoup:1.18.3'
|
||||
}
|
||||
|
||||
hibernate {
|
||||
|
||||
@@ -1,13 +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.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 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.*;
|
||||
@@ -21,6 +25,7 @@ import java.util.List;
|
||||
public class BookController {
|
||||
|
||||
private BooksService booksService;
|
||||
private AmazonParser amazonParser;
|
||||
|
||||
@GetMapping("/{bookId}")
|
||||
public ResponseEntity<BookDTO> getBook(@PathVariable long bookId) {
|
||||
@@ -69,6 +74,11 @@ public class BookController {
|
||||
return ResponseEntity.ok(booksService.fetchProspectiveMetadataListByBookId(bookId));
|
||||
}
|
||||
|
||||
@PostMapping("/{bookId}/query-for-books")
|
||||
public ResponseEntity<BookMetadataDTO> getBookMetadata(@RequestBody(required = false) QueryData queryData, @PathVariable Long bookId) {
|
||||
return ResponseEntity.ok(amazonParser.queryForBookMetadata(bookId, queryData));
|
||||
}
|
||||
|
||||
@GetMapping("/fetch-metadata")
|
||||
public ResponseEntity<List<GoogleBooksMetadata>> fetchMedataByTerm(@RequestParam String term) {
|
||||
return ResponseEntity.ok(booksService.fetchProspectiveMetadataListBySearchTerm(term));
|
||||
@@ -83,4 +93,10 @@ public class BookController {
|
||||
public ResponseEntity<List<BookDTO>> addBookToShelf(@RequestBody @Valid ShelvesAssignmentRequest request) {
|
||||
return ResponseEntity.ok(booksService.assignShelvesToBooks(request.getBookIds(), request.getShelvesToAssign(), request.getShelvesToUnassign()));
|
||||
}
|
||||
|
||||
@PutMapping("/{bookId}/metadata")
|
||||
public ResponseEntity<BookMetadataDTO> updateBookMetadata(@RequestBody BookMetadataDTO metadataDTO, @PathVariable long bookId) throws IOException {
|
||||
return ResponseEntity.ok(booksService.setMetadata(bookId, metadataDTO));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,16 +1,20 @@
|
||||
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 lombok.AllArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -21,6 +25,7 @@ public class LibraryController {
|
||||
|
||||
private LibraryService libraryService;
|
||||
private BooksService booksService;
|
||||
private AmazonParser amazonParser;
|
||||
|
||||
@GetMapping("/{libraryId}")
|
||||
public ResponseEntity<LibraryDTO> getLibrary(@PathVariable long libraryId) {
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
package com.adityachandel.booklore.model.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Builder
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@Builder
|
||||
public class BookMetadataDTO {
|
||||
private Long bookId;
|
||||
private String googleBookId;
|
||||
private String amazonBookId;
|
||||
private String title;
|
||||
private String subtitle;
|
||||
private String publisher;
|
||||
@@ -24,4 +23,7 @@ public class BookMetadataDTO {
|
||||
private String language;
|
||||
private List<AuthorDTO> authors;
|
||||
private List<CategoryDTO> categories;
|
||||
private String rating;
|
||||
private String reviewCount;
|
||||
private String printLength;
|
||||
}
|
||||
|
||||
@@ -19,9 +19,6 @@ public class BookMetadata {
|
||||
@Column(name = "book_id")
|
||||
private Long bookId;
|
||||
|
||||
@Column(name = "google_book_id", nullable = false, unique = true)
|
||||
private String googleBookId;
|
||||
|
||||
@Column(name = "title", nullable = false)
|
||||
private String title;
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
package com.adityachandel.booklore.service;
|
||||
|
||||
import com.adityachandel.booklore.config.AppProperties;
|
||||
import com.adityachandel.booklore.model.dto.BookDTO;
|
||||
import com.adityachandel.booklore.model.dto.BookWithNeighborsDTO;
|
||||
import com.adityachandel.booklore.model.dto.response.GoogleBooksMetadata;
|
||||
import com.adityachandel.booklore.model.dto.BookViewerSettingDTO;
|
||||
import com.adityachandel.booklore.model.dto.request.SetMetadataRequest;
|
||||
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.response.GoogleBooksMetadata;
|
||||
import com.adityachandel.booklore.model.entity.*;
|
||||
import com.adityachandel.booklore.repository.*;
|
||||
import com.adityachandel.booklore.transformer.BookMetadataTransformer;
|
||||
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;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.UrlResource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -23,13 +23,13 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@@ -37,7 +37,6 @@ import java.util.stream.Collectors;
|
||||
@Service
|
||||
public class BooksService {
|
||||
|
||||
private final AppProperties appProperties;
|
||||
private final BookRepository bookRepository;
|
||||
private final BookViewerSettingRepository bookViewerSettingRepository;
|
||||
private final GoogleBookMetadataService googleBookMetadataService;
|
||||
@@ -47,6 +46,7 @@ public class BooksService {
|
||||
private final LibraryRepository libraryRepository;
|
||||
private final NotificationService notificationService;
|
||||
private final ShelfRepository shelfRepository;
|
||||
private final FileService fileService;
|
||||
|
||||
|
||||
public BookDTO getBook(long bookId) {
|
||||
@@ -69,22 +69,6 @@ public class BooksService {
|
||||
bookViewerSettingRepository.save(bookViewerSetting);
|
||||
}
|
||||
|
||||
public Resource getBookCover(long bookId) {
|
||||
Book book = bookRepository.findById(bookId).orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(bookId));
|
||||
String thumbPath = appProperties.getPathConfig() + "/thumbs/" + getFileNameWithoutExtension(book.getFileName()) + ".jpg";
|
||||
Path filePath = Paths.get(thumbPath);
|
||||
try {
|
||||
Resource resource = new UrlResource(filePath.toUri());
|
||||
if (resource.exists() && resource.isReadable()) {
|
||||
return resource;
|
||||
} else {
|
||||
throw ApiError.IMAGE_NOT_FOUND.createException(thumbPath);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw ApiError.IMAGE_NOT_FOUND.createException(thumbPath);
|
||||
}
|
||||
}
|
||||
|
||||
public ResponseEntity<byte[]> getBookData(long bookId) throws IOException {
|
||||
Book book = bookRepository.findById(bookId).orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(bookId));
|
||||
byte[] pdfBytes = Files.readAllBytes(new File(book.getPath()).toPath());
|
||||
@@ -116,7 +100,7 @@ public class BooksService {
|
||||
searchString.append(book.getMetadata().getTitle());
|
||||
}
|
||||
if (searchString.isEmpty()) {
|
||||
searchString.append(cleanFileName(book.getFileName()));
|
||||
searchString.append(BookUtils.cleanFileName(book.getFileName()));
|
||||
}
|
||||
if (book.getMetadata().getAuthors() != null && !book.getMetadata().getAuthors().isEmpty()) {
|
||||
if (!searchString.isEmpty()) {
|
||||
@@ -129,23 +113,57 @@ public class BooksService {
|
||||
return googleBookMetadataService.queryByTerm(searchString.toString());
|
||||
}
|
||||
|
||||
private char[] cleanFileName(String fileName) {
|
||||
if (fileName == null) {
|
||||
return null;
|
||||
}
|
||||
String cleanedFileName = fileName.replace("(Z-Library)", "").trim();
|
||||
return cleanedFileName.toCharArray();
|
||||
}
|
||||
|
||||
public List<GoogleBooksMetadata> fetchProspectiveMetadataListBySearchTerm(String searchTerm) {
|
||||
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<Author> 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<Category> 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.setGoogleBookId(gMetadata.getGoogleBookId());
|
||||
metadata.setDescription(gMetadata.getDescription());
|
||||
metadata.setTitle(gMetadata.getTitle());
|
||||
metadata.setLanguage(gMetadata.getLanguage());
|
||||
@@ -229,11 +247,9 @@ public class BooksService {
|
||||
List<Book> books = bookRepository.findAllById(bookIds);
|
||||
List<Shelf> shelvesToAssign = shelfRepository.findAllById(shelfIdsToAssign);
|
||||
List<Shelf> shelvesToUnassign = shelfRepository.findAllById(shelfIdsToUnassign);
|
||||
|
||||
for (Book book : books) {
|
||||
book.getShelves().removeIf(shelf -> shelfIdsToUnassign.contains(shelf.getId()));
|
||||
shelvesToUnassign.forEach(shelf -> shelf.getBooks().remove(book));
|
||||
|
||||
shelvesToAssign.forEach(shelf -> {
|
||||
if (!book.getShelves().contains(shelf)) {
|
||||
book.getShelves().add(shelf);
|
||||
@@ -242,12 +258,14 @@ public class BooksService {
|
||||
shelf.getBooks().add(book);
|
||||
}
|
||||
});
|
||||
|
||||
bookRepository.save(book);
|
||||
shelfRepository.saveAll(shelvesToAssign);
|
||||
}
|
||||
|
||||
return books.stream().map(BookTransformer::convertToBookDTO).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
}
|
||||
public Resource getBookCover(long bookId) {
|
||||
Book book = bookRepository.findById(bookId).orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(bookId));
|
||||
return fileService.getBookCover(book.getMetadata().getThumbnail());
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import com.adityachandel.booklore.model.entity.Book;
|
||||
import com.adityachandel.booklore.model.entity.BookMetadata;
|
||||
import com.adityachandel.booklore.repository.BookRepository;
|
||||
import com.adityachandel.booklore.transformer.BookTransformer;
|
||||
import com.adityachandel.booklore.util.FileService;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.PersistenceContext;
|
||||
import lombok.AllArgsConstructor;
|
||||
@@ -69,13 +70,16 @@ public class PdfFileProcessor implements FileProcessor {
|
||||
bookCreatorService.addAuthorsToBook(authors, book);
|
||||
}
|
||||
}
|
||||
generateCoverImage(bookFile, new File(appProperties.getPathConfig() + "/thumbs"), document);
|
||||
bookCreatorService.saveConnections(book);
|
||||
Book saved = bookRepository.save(book);
|
||||
boolean success = generateCoverImage(saved.getId(), new File(appProperties.getPathConfig() + "/thumbs"), document);
|
||||
if (success) {
|
||||
bookMetadata.setThumbnail(appProperties.getPathConfig() + "/thumbs/" + book.getId() + "/f.jpg");
|
||||
}
|
||||
bookRepository.flush();
|
||||
} catch (Exception e) {
|
||||
log.error("Error while processing file {}, error: {}", libraryFile.getFilePath(), e.getMessage());
|
||||
}
|
||||
bookCreatorService.saveConnections(book);
|
||||
bookRepository.save(book);
|
||||
bookRepository.flush();
|
||||
return BookTransformer.convertToBookDTO(book);
|
||||
}
|
||||
|
||||
@@ -92,13 +96,19 @@ public class PdfFileProcessor implements FileProcessor {
|
||||
return authorNames.stream().map(String::trim).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private void generateCoverImage(File bookFile, File coverDirectory, PDDocument document) throws IOException {
|
||||
private boolean generateCoverImage(Long bookId, File coverDirectory, PDDocument document) throws IOException {
|
||||
PDFRenderer renderer = new PDFRenderer(document);
|
||||
BufferedImage coverImage = renderer.renderImageWithDPI(0, 300, ImageType.RGB);
|
||||
BufferedImage resizedImage = resizeImage(coverImage, 250, 350);
|
||||
String coverImageName = getFileNameWithoutExtension(bookFile.getName()) + ".jpg";
|
||||
File coverImageFile = new File(coverDirectory, coverImageName);
|
||||
ImageIO.write(resizedImage, "JPEG", coverImageFile);
|
||||
File bookDirectory = new File(coverDirectory, bookId.toString());
|
||||
if (!bookDirectory.exists()) {
|
||||
if (!bookDirectory.mkdirs()) {
|
||||
throw new IOException("Failed to create directory: " + bookDirectory.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
String coverImageName = "f.jpg";
|
||||
File coverImageFile = new File(bookDirectory, coverImageName);
|
||||
return ImageIO.write(resizedImage, "JPEG", coverImageFile);
|
||||
}
|
||||
|
||||
public static BufferedImage resizeImage(BufferedImage originalImage, int width, int height) {
|
||||
@@ -109,13 +119,4 @@ public class PdfFileProcessor implements FileProcessor {
|
||||
g2d.dispose();
|
||||
return resizedImage;
|
||||
}
|
||||
|
||||
public static String getFileNameWithoutExtension(String fileName) {
|
||||
int dotIndex = fileName.lastIndexOf('.');
|
||||
if (dotIndex == -1) {
|
||||
return fileName;
|
||||
} else {
|
||||
return fileName.substring(0, dotIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
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.util.BookUtils;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jsoup.Connection;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.jsoup.select.Elements;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
public class AmazonParser {
|
||||
|
||||
private BookRepository bookRepository;
|
||||
|
||||
public BookMetadataDTO queryForBookMetadata(Long bookId, QueryData queryData) {
|
||||
Book book = bookRepository.findById(bookId).orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(bookId));
|
||||
if (queryData == null || (queryData.getBookTitle() == null && queryData.getAuthor() == null && queryData.getIsbn() == null)) {
|
||||
String title = book.getMetadata().getTitle();
|
||||
if (title == null || title.isEmpty()) {
|
||||
String cleanFileName = BookUtils.cleanFileName(book.getFileName());
|
||||
queryData = QueryData.builder().bookTitle(cleanFileName).build();
|
||||
} else {
|
||||
queryData = QueryData.builder().bookTitle(title).build();
|
||||
}
|
||||
}
|
||||
String amazonBookId = getAmazonBookId(queryData);
|
||||
if (amazonBookId == null) {
|
||||
return null;
|
||||
}
|
||||
return getBookMetadata(amazonBookId);
|
||||
}
|
||||
|
||||
public String getAmazonBookId(QueryData queryData) {
|
||||
String queryUrl = buildQueryUrl(queryData);
|
||||
if (queryUrl == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
Document doc = fetchDoc(queryUrl);
|
||||
Element searchResults = doc.select("span[data-component-type=s-search-results]").first();
|
||||
Element item = searchResults.select("div[role=listitem][data-index=2]").first();
|
||||
return item.attr("data-asin");
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to get asin: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public BookMetadataDTO getBookMetadata(String amazonBookId) {
|
||||
|
||||
Document doc = fetchDoc("https://www.amazon.com/dp/" + amazonBookId);
|
||||
|
||||
return BookMetadataDTO.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()))
|
||||
.description(getDescription(doc))
|
||||
.isbn13(getIsbn13(doc))
|
||||
.isbn10(getIsbn10(doc))
|
||||
.publisher(getPublisher(doc))
|
||||
.publishedDate(getPublicationDate(doc))
|
||||
.language(getLanguage(doc))
|
||||
.pageCount(getPageCount(doc))
|
||||
.thumbnail(getThumbnail(doc))
|
||||
.rating(getRating(doc))
|
||||
.reviewCount(getReviewCount(doc))
|
||||
.printLength(getPrintLength(doc))
|
||||
.build();
|
||||
}
|
||||
|
||||
private String buildQueryUrl(QueryData queryData) {
|
||||
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 (queryData.getBookTitle() != null && !queryData.getBookTitle().isEmpty()) {
|
||||
queryBuilder.append("&field-title=").append(queryData.getBookTitle().replace(" ", "%20"));
|
||||
}
|
||||
|
||||
if (queryData.getAuthor() != null && !queryData.getAuthor().isEmpty()) {
|
||||
queryBuilder.append("&field-author=").append(queryData.getAuthor().replace(" ", "%20"));
|
||||
}
|
||||
|
||||
if (queryData.getIsbn() == null && queryData.getBookTitle() == null && queryData.getAuthor() != null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return queryBuilder.toString();
|
||||
}
|
||||
|
||||
private String getTitle(Document doc) {
|
||||
Element titleElement = doc.getElementById("productTitle");
|
||||
if (titleElement != null) {
|
||||
return titleElement.text();
|
||||
}
|
||||
log.error("Error fetching title: Element not found.");
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getSubtitle(Document doc) {
|
||||
Element subtitleElement = doc.getElementById("productSubtitle");
|
||||
if (subtitleElement != null) {
|
||||
return subtitleElement.text();
|
||||
}
|
||||
log.error("Error fetching subtitle: Element not found.");
|
||||
return null;
|
||||
}
|
||||
|
||||
private Set<String> getAuthors(Document doc) {
|
||||
try {
|
||||
Element bylineDiv = doc.select("#bylineInfo_feature_div").first();
|
||||
if (bylineDiv != null) {
|
||||
return bylineDiv
|
||||
.select(".author a")
|
||||
.stream()
|
||||
.map(Element::text)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
log.error("Error fetching authors: Byline element not found.");
|
||||
} catch (Exception e) {
|
||||
log.error("Error fetching authors: {}", e.getMessage());
|
||||
}
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
private String getDescription(Document doc) {
|
||||
try {
|
||||
Elements descriptionElements = doc.select("[data-a-expander-name=book_description_expander] .a-expander-content");
|
||||
if (!descriptionElements.isEmpty()) {
|
||||
String html = descriptionElements.getFirst().html();
|
||||
html = html.replace("\n", "<br>");
|
||||
return html;
|
||||
}
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
log.error("Error extracting description from the document", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getIsbn10(Document doc) {
|
||||
try {
|
||||
Element isbn10Element = doc.select("#rpi-attribute-book_details-isbn10 .rpi-attribute-value span").first();
|
||||
if (isbn10Element != null) {
|
||||
return isbn10Element.text();
|
||||
}
|
||||
log.error("Error fetching ISBN-10: Element not found.");
|
||||
} catch (Exception e) {
|
||||
log.error("Error fetching ISBN-10: {}", e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getIsbn13(Document doc) {
|
||||
try {
|
||||
Element isbn13Element = doc.select("#rpi-attribute-book_details-isbn13 .rpi-attribute-value span").first();
|
||||
if (isbn13Element != null) {
|
||||
return isbn13Element.text();
|
||||
}
|
||||
log.error("Error fetching ISBN-13: Element not found.");
|
||||
} catch (Exception e) {
|
||||
log.error("Error fetching ISBN-13: {}", e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getPublisher(Document doc) {
|
||||
try {
|
||||
Element publisherElement = doc.select("#rpi-attribute-book_details-publisher .rpi-attribute-value span").first();
|
||||
if (publisherElement != null) {
|
||||
return publisherElement.text();
|
||||
}
|
||||
log.error("Error fetching publisher: Element not found.");
|
||||
} catch (Exception e) {
|
||||
log.error("Error fetching publisher: {}", e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String 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();
|
||||
}
|
||||
log.error("Error fetching publication date: Element not found.");
|
||||
} catch (Exception e) {
|
||||
log.error("Error fetching publication date: {}", e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getLanguage(Document doc) {
|
||||
try {
|
||||
Element languageElement = doc.select("#rpi-attribute-language .rpi-attribute-value span").first();
|
||||
if (languageElement != null) {
|
||||
return languageElement.text();
|
||||
}
|
||||
log.error("Error fetching language: Element not found.");
|
||||
} catch (Exception e) {
|
||||
log.error("Error fetching language: {}", e.getMessage());
|
||||
}
|
||||
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<String> getBestSellerCategories(Document doc) {
|
||||
try {
|
||||
Element bestSellerCategoriesElement = doc.select("#detailBullets_feature_div").first();
|
||||
if (bestSellerCategoriesElement != null) {
|
||||
return bestSellerCategoriesElement
|
||||
.select(".zg_hrsr .a-list-item a")
|
||||
.stream()
|
||||
.map(Element::text)
|
||||
.map(c -> c.replace("(Books)", "").trim())
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
log.error("Error fetching best seller categories: Element not found.");
|
||||
} catch (Exception e) {
|
||||
log.error("Error fetching best seller categories: {}", e.getMessage());
|
||||
}
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
private String 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();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error fetching rating", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String 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];
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error fetching review count", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getThumbnail(Document doc) {
|
||||
try {
|
||||
Element imageElement = doc.select("#landingImage").first();
|
||||
if (imageElement != null) {
|
||||
return imageElement.attr("src");
|
||||
}
|
||||
log.error("Error fetching image URL: Image element not found.");
|
||||
} catch (Exception e) {
|
||||
log.error("Error fetching image URL: {}", e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Integer getPageCount(Document doc) {
|
||||
Elements pageCountElements = doc.select("#rpi-attribute-book_details-fiona_pages .rpi-attribute-value span");
|
||||
if (!pageCountElements.isEmpty()) {
|
||||
String pageCountText = pageCountElements.first().text();
|
||||
if (!pageCountText.isEmpty()) {
|
||||
try {
|
||||
String cleanedPageCount = pageCountText.replaceAll("[^\\d]", "");
|
||||
return Integer.parseInt(cleanedPageCount);
|
||||
} catch (NumberFormatException e) {
|
||||
log.error("Error parsing page count: {}", pageCountText, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Document fetchDoc(String url) {
|
||||
try {
|
||||
Connection.Response response = Jsoup.connect(url)
|
||||
.header("accept", "text/html, application/json")
|
||||
.header("accept-language", "en-US,en;q=0.9")
|
||||
.header("content-type", "application/json")
|
||||
.header("device-memory", "8")
|
||||
.header("downlink", "10")
|
||||
.header("dpr", "2")
|
||||
.header("ect", "4g")
|
||||
.header("origin", "https://www.amazon.com")
|
||||
.header("priority", "u=1, i")
|
||||
.header("rtt", "50")
|
||||
.header("sec-ch-device-memory", "8")
|
||||
.header("sec-ch-dpr", "2")
|
||||
.header("sec-ch-ua", "\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"")
|
||||
.header("sec-ch-ua-mobile", "?0")
|
||||
.header("sec-ch-ua-platform", "\"macOS\"")
|
||||
.header("sec-ch-viewport-width", "1170")
|
||||
.header("sec-fetch-dest", "empty")
|
||||
.header("sec-fetch-mode", "cors")
|
||||
.header("sec-fetch-site", "same-origin")
|
||||
.header("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36")
|
||||
.header("viewport-width", "1170")
|
||||
.header("x-amz-amabot-click-attributes", "disable")
|
||||
.header("x-requested-with", "XMLHttpRequest")
|
||||
.method(Connection.Method.GET)
|
||||
.execute();
|
||||
return response.parse();
|
||||
} catch (IOException e) {
|
||||
log.error("Error parsing url: {}", url, e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.adityachandel.booklore.service.metadata.parser.model;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Builder
|
||||
@Data
|
||||
public class QueryData {
|
||||
private String isbn;
|
||||
private String bookTitle;
|
||||
private String author;
|
||||
}
|
||||
@@ -1,8 +1,16 @@
|
||||
package com.adityachandel.booklore.transformer;
|
||||
|
||||
import com.adityachandel.booklore.model.dto.BookMetadataDTO;
|
||||
import com.adityachandel.booklore.model.entity.Author;
|
||||
import com.adityachandel.booklore.model.entity.BookMetadata;
|
||||
import com.adityachandel.booklore.model.entity.Category;
|
||||
import com.adityachandel.booklore.util.FileService;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.hibernate.annotations.Comment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class BookMetadataTransformer {
|
||||
@@ -10,7 +18,6 @@ public class BookMetadataTransformer {
|
||||
public static BookMetadataDTO convertToBookDTO(BookMetadata bookMetadata) {
|
||||
return BookMetadataDTO.builder()
|
||||
.bookId(bookMetadata.getBookId())
|
||||
.googleBookId(bookMetadata.getGoogleBookId())
|
||||
.title(bookMetadata.getTitle())
|
||||
.description(bookMetadata.getDescription())
|
||||
.isbn10(bookMetadata.getIsbn10())
|
||||
@@ -18,7 +25,6 @@ public class BookMetadataTransformer {
|
||||
.publisher(bookMetadata.getPublisher())
|
||||
.subtitle(bookMetadata.getSubtitle())
|
||||
.language(bookMetadata.getLanguage())
|
||||
.thumbnail(bookMetadata.getThumbnail())
|
||||
.pageCount(bookMetadata.getPageCount())
|
||||
.publishedDate(bookMetadata.getPublishedDate())
|
||||
.authors(bookMetadata.getAuthors() == null ? null : bookMetadata.getAuthors().stream().map(AuthorTransformer::toAuthorDTO).collect(Collectors.toList()))
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.adityachandel.booklore.util;
|
||||
|
||||
public class BookUtils {
|
||||
|
||||
public static String cleanFileName(String fileName) {
|
||||
if (fileName == null) {
|
||||
return null;
|
||||
}
|
||||
fileName = fileName.replace("(Z-Library)", "").trim();
|
||||
|
||||
// Remove the author name inside parentheses (e.g. (Jon Yablonski))
|
||||
fileName = fileName.replaceAll("\\s?\\(.*?\\)", "").trim();
|
||||
|
||||
// Remove the file extension (e.g., .pdf, .docx)
|
||||
int dotIndex = fileName.lastIndexOf('.');
|
||||
if (dotIndex > 0) {
|
||||
fileName = fileName.substring(0, dotIndex).trim();
|
||||
}
|
||||
|
||||
return fileName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.adityachandel.booklore.util;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class DateUtils {
|
||||
|
||||
private static final String DATE_FORMAT_REGEX = "^[A-Za-z]+\\s[0-9]{1,2},\\s[0-9]{4}$";
|
||||
|
||||
public static String parseDateToInstant(String dateString) {
|
||||
if (dateString == null || dateString.trim().isEmpty() || !Pattern.matches(DATE_FORMAT_REGEX, dateString)) {
|
||||
return dateString;
|
||||
}
|
||||
try {
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMMM d, yyyy", Locale.ENGLISH);
|
||||
LocalDate localDate = LocalDate.parse(dateString, formatter);
|
||||
Instant instant = localDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
LocalDate finalDate = instant.atZone(ZoneId.systemDefault()).toLocalDate();
|
||||
return finalDate.toString();
|
||||
} catch (DateTimeParseException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.adityachandel.booklore.util;
|
||||
|
||||
import com.adityachandel.booklore.config.AppProperties;
|
||||
import com.adityachandel.booklore.exception.ApiError;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.UrlResource;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class FileService {
|
||||
|
||||
private final AppProperties appProperties;
|
||||
|
||||
public Resource getBookCover(String thumbnailPath) {
|
||||
Path filePath = Paths.get(thumbnailPath);
|
||||
try {
|
||||
Resource resource = new UrlResource(filePath.toUri());
|
||||
if (resource.exists() && resource.isReadable()) {
|
||||
return resource;
|
||||
} else {
|
||||
throw ApiError.IMAGE_NOT_FOUND.createException(filePath);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw ApiError.IMAGE_NOT_FOUND.createException(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
/*private String getCurrentThumbnail(String bookThumbFolder) {
|
||||
File directory = new File(bookThumbFolder);
|
||||
if (!directory.isDirectory()) {
|
||||
log.error("Invalid directory path: {}", bookThumbFolder);
|
||||
return null;
|
||||
}
|
||||
File[] files = directory.listFiles((dir, name) -> name.endsWith("-current.jpg"));
|
||||
if (files == null || files.length == 0) {
|
||||
log.info("No file ending with '-current.jpg' found in directory: {}", bookThumbFolder);
|
||||
return null;
|
||||
}
|
||||
return files[0].getPath();
|
||||
}*/
|
||||
|
||||
public String createThumbnail(long bookId, String thumbnailUrl, String suffix) throws IOException {
|
||||
String newFilename = suffix + ".jpg";
|
||||
resizeAndSaveImage(thumbnailUrl, new File(getThumbnailPath(bookId)), newFilename);
|
||||
return getThumbnailPath(bookId) + newFilename;
|
||||
//removeOldCurrent(bookId, prefix);
|
||||
}
|
||||
|
||||
/*public void removeOldCurrent(long bookId, String except) {
|
||||
String thumbnailPath = getThumbnailPath(bookId);
|
||||
File directory = new File(thumbnailPath);
|
||||
if (!directory.isDirectory()) {
|
||||
log.error("Invalid directory path: {}", thumbnailPath);
|
||||
return;
|
||||
}
|
||||
File[] files = directory.listFiles((dir, name) ->
|
||||
name.endsWith(".jpg") && !name.equals(except + "-current.jpg") && name.contains("-current"));
|
||||
if (files == null || files.length == 0) {
|
||||
log.info("No matching files to process in directory: {}", thumbnailPath);
|
||||
return;
|
||||
}
|
||||
for (File file : files) {
|
||||
File newFile = new File(file.getParent(), file.getName().replace("-current", ""));
|
||||
if (file.renameTo(newFile)) {
|
||||
log.info("Renamed: {} -> {}", file.getName(), newFile.getName());
|
||||
} else {
|
||||
log.error("Failed to rename: {}", file.getName());
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
private void resizeAndSaveImage(String imageUrl, File outputFolder, String outputFileName) throws IOException {
|
||||
BufferedImage originalImage;
|
||||
try (InputStream inputStream = new URL(imageUrl).openStream()) {
|
||||
originalImage = ImageIO.read(inputStream);
|
||||
}
|
||||
if (originalImage == null) {
|
||||
throw new IOException("Failed to read image from URL: " + imageUrl);
|
||||
}
|
||||
BufferedImage resizedImage = resizeImage(originalImage);
|
||||
if (!outputFolder.exists() && !outputFolder.mkdirs()) {
|
||||
throw new IOException("Failed to create output directory: " + outputFolder.getAbsolutePath());
|
||||
}
|
||||
File outputFile = new File(outputFolder, outputFileName);
|
||||
ImageIO.write(resizedImage, "JPEG", outputFile);
|
||||
log.info("Image saved to: {}", outputFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
private BufferedImage resizeImage(BufferedImage originalImage) {
|
||||
BufferedImage resizedImage = new BufferedImage(250, 350, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g2d = resizedImage.createGraphics();
|
||||
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||
g2d.drawImage(originalImage, 0, 0, 250, 350, null);
|
||||
g2d.dispose();
|
||||
return resizedImage;
|
||||
}
|
||||
|
||||
public String getThumbnailPath(long bookId) {
|
||||
return appProperties.getPathConfig() + "/thumbs/" + bookId + "/";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -24,14 +24,13 @@ CREATE TABLE IF NOT EXISTS book
|
||||
CREATE TABLE IF NOT EXISTS book_metadata
|
||||
(
|
||||
book_id BIGINT NOT NULL PRIMARY KEY,
|
||||
google_book_id VARCHAR(255) UNIQUE,
|
||||
title VARCHAR(255),
|
||||
subtitle VARCHAR(255),
|
||||
publisher VARCHAR(255),
|
||||
published_date DATE,
|
||||
description TEXT,
|
||||
isbn_13 VARCHAR(13),
|
||||
isbn_10 VARCHAR(10),
|
||||
isbn_13 VARCHAR(20),
|
||||
isbn_10 VARCHAR(20),
|
||||
page_count INT,
|
||||
thumbnail VARCHAR(1000),
|
||||
language VARCHAR(10),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<p-confirmDialog />
|
||||
<p-toast></p-toast>
|
||||
<app-loading-overlay></app-loading-overlay>
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
@@ -41,6 +41,9 @@ import {ProgressSpinnerModule} from 'primeng/progressspinner';
|
||||
import {provideAnimationsAsync} from '@angular/platform-browser/animations/async';
|
||||
import {providePrimeNG} from 'primeng/config';
|
||||
import Aura from '@primeng/themes/aura';
|
||||
import { MetadataSearcherComponent } from './metadata-searcher/metadata-searcher.component';
|
||||
import {IftaLabel} from 'primeng/iftalabel';
|
||||
import { LoadingOverlayComponent } from './loading-overlay/loading-overlay.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -48,7 +51,8 @@ import Aura from '@primeng/themes/aura';
|
||||
DirectoryPickerComponent,
|
||||
LibraryCreatorComponent,
|
||||
ShelfAssignerComponent,
|
||||
BookBrowserComponent
|
||||
BookBrowserComponent,
|
||||
MetadataSearcherComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
@@ -78,6 +82,8 @@ import Aura from '@primeng/themes/aura';
|
||||
MenuModule,
|
||||
IconPickerComponent,
|
||||
ProgressSpinnerModule,
|
||||
IftaLabel,
|
||||
LoadingOverlayComponent,
|
||||
],
|
||||
providers: [
|
||||
DialogService,
|
||||
|
||||
@@ -74,7 +74,9 @@ export class BookCardComponent implements OnInit {
|
||||
{
|
||||
label: 'View metadata',
|
||||
icon: 'pi pi-info-circle',
|
||||
command: () => this.openBookInfo(this.book),
|
||||
command: () => {
|
||||
this.openBookInfo(this.book)
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<div *ngFor="let metadata of bookMetadataList" class="book-metadata">
|
||||
<div class="metadata-header">
|
||||
<div class="book-image-container">
|
||||
<img class="book-image" [src]="metadata.thumbnail || 'assets/book-cover-metadata.png'" alt="Book Cover"/>
|
||||
<!--<img class="book-image" [src]="metadata.thumbnail || 'assets/book-cover-metadata.png'" alt="Book Cover"/>-->
|
||||
<p-button class="select-button" (click)="selectMetadata(metadata)">Select</p-button>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
{{ book?.metadata?.title }}<span *ngIf="book?.metadata?.subtitle">: {{ book?.metadata?.subtitle }}</span>
|
||||
</h1>
|
||||
<div class="button-group">
|
||||
<p-button [rounded]="true" [raised]="true" [outlined]="true" (onClick)="openEditDialog(book?.id, book?.libraryId)" icon="pi pi-pencil"></p-button>
|
||||
<p-button [rounded]="true" [raised]="true" [outlined]="true" (onClick)="openEditDialog(book?.id!, book?.libraryId!)" icon="pi pi-pencil"></p-button>
|
||||
<p-button [rounded]="true" [raised]="true" [outlined]="true" (onClick)="readBook(book!)" icon="pi pi-eye"></p-button>
|
||||
<p-button [rounded]="true" [raised]="true" [outlined]="true" icon="pi pi-download"></p-button>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,9 @@ import {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({
|
||||
selector: 'app-book-metadata',
|
||||
@@ -27,18 +30,18 @@ export class BookMetadataComponent implements OnInit, OnDestroy {
|
||||
previousBookId: number | null = null;
|
||||
|
||||
private routeSubscription!: Subscription;
|
||||
private dialogRef: DynamicDialogRef | undefined;
|
||||
private dialogRef!: DynamicDialogRef;
|
||||
private dialogSubscription?: Subscription;
|
||||
|
||||
constructor(
|
||||
private bookService: BookService, private activatedRoute: ActivatedRoute,
|
||||
private dialogService: DialogService, private router: Router,
|
||||
private libraryService: LibraryService) {
|
||||
private libraryService: LibraryService, private loadingService: LoadingService,) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.routeSubscription = this.activatedRoute.paramMap.subscribe((paramMap) => {
|
||||
const bookId = +paramMap.get('bookId')!;
|
||||
const bookId = +paramMap.get('bookId')!;
|
||||
const libraryId = +paramMap.get('libraryId')!;
|
||||
if (bookId && libraryId) {
|
||||
this.loadBookWithNeighbors(bookId, libraryId);
|
||||
@@ -75,7 +78,47 @@ export class BookMetadataComponent implements OnInit, OnDestroy {
|
||||
return 'No authors available';
|
||||
}
|
||||
|
||||
openEditDialog(bookId: number | undefined, libraryId: number | undefined) {
|
||||
openEditDialog(bookId: number, libraryId: number) {
|
||||
this.loadingService.show();
|
||||
this.bookService.getFetchBookMetadata(bookId).subscribe({
|
||||
next: (fetchedMetadata) => {
|
||||
this.loadingService.hide();
|
||||
this.dialogRef = this.dialogService.open(MetadataSearcherComponent, {
|
||||
header: 'Update Book Metadata',
|
||||
modal: true,
|
||||
closable: true,
|
||||
width: '1350px',
|
||||
height: '1000px',
|
||||
contentStyle: {
|
||||
'overflow-y': 'auto',
|
||||
'max-height': 'calc(100vh - 150px)',
|
||||
'padding': '1.25rem 1.25rem 0',
|
||||
},
|
||||
data: {
|
||||
currentMetadata: this.book?.metadata,
|
||||
fetchedMetadata: fetchedMetadata,
|
||||
book: this.book
|
||||
}
|
||||
});
|
||||
|
||||
if (this.dialogRef) {
|
||||
this.dialogSubscription = this.dialogRef.onClose.subscribe(() => {
|
||||
if (this.book?.id && this.book?.libraryId) {
|
||||
this.loadBookWithNeighbors(this.book.id, this.book.libraryId);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.error('DialogRef is undefined or null');
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
this.loadingService.hide();
|
||||
console.error('Error fetching metadata:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/*openEditDialog1(bookId: number | undefined, libraryId: number | undefined) {
|
||||
this.dialogRef = this.dialogService.open(BookMetadataDialogComponent, {
|
||||
header: 'Metadata: Google Books',
|
||||
modal: true,
|
||||
@@ -94,7 +137,7 @@ export class BookMetadataComponent implements OnInit, OnDestroy {
|
||||
this.loadBookWithNeighbors(this.book.id, this.book.libraryId);
|
||||
}
|
||||
});
|
||||
}
|
||||
}*/
|
||||
|
||||
coverImageSrc(bookId: number | undefined): string {
|
||||
if (bookId === null) {
|
||||
|
||||
@@ -10,7 +10,6 @@ export interface Book {
|
||||
}
|
||||
|
||||
export interface BookMetadata {
|
||||
thumbnail: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
authors: Author[];
|
||||
@@ -22,6 +21,39 @@ export interface BookMetadata {
|
||||
pageCount: number;
|
||||
language: string;
|
||||
googleBookId: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
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;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface Author {
|
||||
|
||||
@@ -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} from '../model/book.model';
|
||||
import {Book, BookMetadata, BookSetting, FetchedMetadata, UpdateMedata} from '../model/book.model';
|
||||
import {BookState} from '../model/state/book-state.model';
|
||||
|
||||
@Injectable({
|
||||
@@ -186,4 +186,29 @@ export class BookService {
|
||||
const filteredBooks = (currentState.books || []).filter(book => !removedBookIds.has(book.id));
|
||||
this.bookStateSubject.next({...currentState, books: filteredBooks});
|
||||
}
|
||||
|
||||
getFetchBookMetadata(bookId: number): Observable<FetchedMetadata> {
|
||||
return this.http.post<FetchedMetadata>(`${this.url}/${bookId}/query-for-books`, null);
|
||||
}
|
||||
|
||||
updateMetadata(bookId: number, updateMedata: UpdateMedata): Observable<BookMetadata> {
|
||||
return this.http.put<BookMetadata>(`${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;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { HttpClient } from '@angular/common/http';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
import { Library } from '../model/library.model';
|
||||
import { BookService } from './book.service';
|
||||
import { BookWithNeighborsDTO } from '../model/book.model';
|
||||
import {BookMetadata, BookWithNeighborsDTO} from '../model/book.model';
|
||||
import { SortOption } from '../model/sort.model';
|
||||
import { LibraryState } from '../model/state/library-state.model';
|
||||
|
||||
@@ -113,4 +113,5 @@ export class LibraryService {
|
||||
map(state => (state.books || []).filter(book => book.libraryId === libraryId).length)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<div *ngIf="loading" class="loading-overlay">
|
||||
<p-progressSpinner></p-progressSpinner>
|
||||
</div>
|
||||
@@ -0,0 +1,12 @@
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5); /* Dimmed background */
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9999; /* Ensure it stays on top of all content */
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import {LoadingService} from '../loading.service';
|
||||
import {ProgressSpinner} from 'primeng/progressspinner';
|
||||
import {NgIf} from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-loading-overlay',
|
||||
templateUrl: './loading-overlay.component.html',
|
||||
imports: [
|
||||
ProgressSpinner,
|
||||
NgIf
|
||||
],
|
||||
styleUrls: ['./loading-overlay.component.scss']
|
||||
})
|
||||
export class LoadingOverlayComponent implements OnInit, OnDestroy {
|
||||
loading: boolean = false;
|
||||
private loadingSubscription: Subscription | undefined;
|
||||
|
||||
constructor(private loadingService: LoadingService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadingSubscription = this.loadingService.loading$.subscribe(
|
||||
(loading) => {
|
||||
this.loading = loading;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.loadingSubscription?.unsubscribe();
|
||||
}
|
||||
}
|
||||
19
booklore-ui/src/app/loading.service.ts
Normal file
19
booklore-ui/src/app/loading.service.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// loading.service.ts
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class LoadingService {
|
||||
private loadingSubject = new BehaviorSubject<boolean>(false);
|
||||
loading$ = this.loadingSubject.asObservable();
|
||||
|
||||
show(): void {
|
||||
this.loadingSubject.next(true);
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
this.loadingSubject.next(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<div class="form-row">
|
||||
<label for="title" class="label">Title</label>
|
||||
<div class="input-container">
|
||||
<input pInputText id="title" [(ngModel)]="toUpdateMetadata.title" class="input"/>
|
||||
<p-button icon="pi pi-arrow-left" [outlined]="true" class="arrow-button" (click)="copyFetchedToCurrent('title')"/>
|
||||
<input pInputText id="titleFetched" [(ngModel)]="fetchedMetadata.title" class="input"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="authors" class="label">Authors</label>
|
||||
<div class="input-container">
|
||||
<input pInputText id="authors" [(ngModel)]="currentAuthorsString" class="input"/>
|
||||
<p-button icon="pi pi-arrow-left" [outlined]="true" class="arrow-button" (click)="copyFetchedToCurrent('authors')"/>
|
||||
<input pInputText id="authorsFetched" [(ngModel)]="fetchedAuthorsString" class="input"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="categories" class="label">Categories</label>
|
||||
<div class="input-container">
|
||||
<input pInputText id="categories" [(ngModel)]="currentCategoriesString" class="input"/>
|
||||
<p-button icon="pi pi-arrow-left" [outlined]="true" class="arrow-button" (click)="copyFetchedToCurrent('categories')"/>
|
||||
<input pInputText id="categoriesFetched" [(ngModel)]="fetchedCategoriesString" class="input"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="publisher" class="label">Publisher</label>
|
||||
<div class="input-container">
|
||||
<input pInputText id="publisher" [(ngModel)]="toUpdateMetadata.publisher" class="input"/>
|
||||
<p-button icon="pi pi-arrow-left" [outlined]="true" class="arrow-button" (click)="copyFetchedToCurrent('publisher')"/>
|
||||
<input pInputText id="publisherFetched" [(ngModel)]="fetchedMetadata.publisher" class="input"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="publishedDate" class="label">Published Date</label>
|
||||
<div class="input-container">
|
||||
<input pInputText id="publishedDate" [(ngModel)]="toUpdateMetadata.publishedDate" class="input"/>
|
||||
<p-button icon="pi pi-arrow-left" [outlined]="true" class="arrow-button" (click)="copyFetchedToCurrent('publishedDate')"/>
|
||||
<input pInputText id="publishedDateFetched" [(ngModel)]="fetchedMetadata.publishedDate" class="input"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="isbn10" class="label">ISBN-10</label>
|
||||
<div class="input-container">
|
||||
<input pInputText id="isbn10" [(ngModel)]="toUpdateMetadata.isbn10" class="input"/>
|
||||
<p-button icon="pi pi-arrow-left" [outlined]="true" class="arrow-button" (click)="copyFetchedToCurrent('isbn10')"/>
|
||||
<input pInputText id="isbn10Fetched" [(ngModel)]="fetchedMetadata.isbn10" class="input"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="language" class="label">Language</label>
|
||||
<div class="input-container">
|
||||
<input pInputText id="language" [(ngModel)]="toUpdateMetadata.language" class="input"/>
|
||||
<p-button icon="pi pi-arrow-left" [outlined]="true" class="arrow-button" (click)="copyFetchedToCurrent('language')"/>
|
||||
<input pInputText id="languageFetched" [(ngModel)]="fetchedMetadata.language" class="input"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="language" class="label">Page Count</label>
|
||||
<div class="input-container">
|
||||
<input pInputText id="pageCount" [(ngModel)]="toUpdateMetadata.pageCount" class="input"/>
|
||||
<p-button icon="pi pi-arrow-left" [outlined]="true" class="arrow-button" (click)="copyFetchedToCurrent('pageCount')"/>
|
||||
<input pInputText id="pageCountFetched" [(ngModel)]="fetchedMetadata.pageCount" class="input"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="description" class="label">Description</label>
|
||||
<div class="input-container">
|
||||
<textarea id="description" [(ngModel)]="toUpdateMetadata.description" class="input"></textarea>
|
||||
<p-button icon="pi pi-arrow-left" [outlined]="true" class="arrow-button" (click)="copyFetchedToCurrent('description')"/>
|
||||
<textarea id="descriptionFetched" [(ngModel)]="fetchedMetadata.description" class="input"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label class="label"></label>
|
||||
<div class="input-container">
|
||||
<img *ngIf="!updateThumbnail" [src]="coverImageSrc(book)" alt="Book Thumbnail" class="thumbnail"/>
|
||||
<img *ngIf="updateThumbnail" [src]="fetchedMetadata.thumbnail" alt="Fetched Thumbnail" class="thumbnail"/>
|
||||
<p-button icon="pi pi-arrow-left" [outlined]="true" class="arrow-button" (click)="shouldUpdateThumbnail()"/>
|
||||
<img [src]="fetchedMetadata.thumbnail" alt="Fetched Thumbnail" class="thumbnail"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<p-button label="Cancel" icon="pi pi-times" severity="secondary" class="cancel-button" (onClick)="closeDialog()"></p-button>
|
||||
<p-button label="Save" icon="pi pi-check" severity="primary" class="save-button" [disabled]="loading" (onClick)="saveMetadata()"></p-button>
|
||||
</div>
|
||||
@@ -0,0 +1,56 @@
|
||||
.button-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
padding: 1.25rem;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.label {
|
||||
width: 8rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 50%;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
width: 15.625rem;
|
||||
height: 21.875rem;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
margin-left: 2.5rem;
|
||||
margin-right: 2.5rem;
|
||||
}
|
||||
|
||||
.arrow-button {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {Book, FetchedMetadata, UpdateMedata} from '../book/model/book.model';
|
||||
import {DynamicDialogConfig, DynamicDialogRef} from 'primeng/dynamicdialog';
|
||||
import {BookService} from '../book/service/book.service';
|
||||
import {MessageService} from 'primeng/api';
|
||||
|
||||
@Component({
|
||||
selector: 'app-metadata-searcher',
|
||||
standalone: false,
|
||||
templateUrl: './metadata-searcher.component.html',
|
||||
styleUrls: ['./metadata-searcher.component.scss']
|
||||
})
|
||||
export class MetadataSearcherComponent {
|
||||
|
||||
fetchedMetadata: FetchedMetadata;
|
||||
toUpdateMetadata: UpdateMedata;
|
||||
book: Book;
|
||||
loading: boolean = false;
|
||||
updateThumbnail: boolean = false;
|
||||
|
||||
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.book = this.dynamicDialogConfig.data.book;
|
||||
}
|
||||
|
||||
copyFetchedToCurrent(field: keyof UpdateMedata) {
|
||||
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(', ') : '';
|
||||
}
|
||||
|
||||
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()
|
||||
}));
|
||||
}
|
||||
|
||||
get fetchedAuthorsString(): string {
|
||||
return this.fetchedMetadata.authors ? this.fetchedMetadata.authors.map(author => author.name).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()
|
||||
}));
|
||||
}
|
||||
|
||||
get currentCategoriesString(): string {
|
||||
return this.toUpdateMetadata.categories ? this.toUpdateMetadata.categories.map(category => category.name).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()
|
||||
}));
|
||||
}
|
||||
|
||||
get fetchedCategoriesString(): string {
|
||||
return this.fetchedMetadata.categories ? this.fetchedMetadata.categories.map(category => category.name).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()
|
||||
}));
|
||||
}
|
||||
|
||||
closeDialog() {
|
||||
this.dynamicDialogRef.close();
|
||||
}
|
||||
|
||||
coverImageSrc(book: Book): string {
|
||||
return this.bookService.getBookCoverUrl(book.id);
|
||||
}
|
||||
|
||||
saveMetadata() {
|
||||
this.loading = true;
|
||||
this.updateFields();
|
||||
this.bookService.updateMetadata(this.book.id, this.toUpdateMetadata).subscribe({
|
||||
next: () => {
|
||||
this.messageService.add({severity: 'info', summary: 'Success', detail: 'Book metadata updated'});
|
||||
this.loading = false;
|
||||
this.dynamicDialogRef.close();
|
||||
},
|
||||
error: (error) => {
|
||||
this.loading = false;
|
||||
this.messageService.add({severity: 'error', summary: 'Error', detail: 'Failed to update book metadata'});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateFields() {
|
||||
if (!this.toUpdateMetadata.title && this.fetchedMetadata.title) {
|
||||
this.toUpdateMetadata.title = this.fetchedMetadata.title;
|
||||
}
|
||||
|
||||
if (!this.toUpdateMetadata.subtitle && this.fetchedMetadata.subtitle) {
|
||||
this.toUpdateMetadata.subtitle = this.fetchedMetadata.subtitle;
|
||||
}
|
||||
|
||||
if (!this.toUpdateMetadata.publisher && this.fetchedMetadata.publisher) {
|
||||
this.toUpdateMetadata.publisher = this.fetchedMetadata.publisher;
|
||||
}
|
||||
|
||||
if (!this.toUpdateMetadata.publishedDate && this.fetchedMetadata.publishedDate) {
|
||||
this.toUpdateMetadata.publishedDate = this.fetchedMetadata.publishedDate;
|
||||
}
|
||||
|
||||
if (!this.toUpdateMetadata.description && this.fetchedMetadata.description) {
|
||||
this.toUpdateMetadata.description = this.fetchedMetadata.description;
|
||||
}
|
||||
|
||||
if (!this.toUpdateMetadata.isbn10 && this.fetchedMetadata.isbn10) {
|
||||
this.toUpdateMetadata.isbn10 = this.fetchedMetadata.isbn10;
|
||||
}
|
||||
|
||||
if (!this.toUpdateMetadata.language && this.fetchedMetadata.language) {
|
||||
this.toUpdateMetadata.language = this.fetchedMetadata.language;
|
||||
}
|
||||
|
||||
if (!this.toUpdateMetadata.pageCount && this.fetchedMetadata.pageCount) {
|
||||
this.toUpdateMetadata.pageCount = this.fetchedMetadata.pageCount;
|
||||
}
|
||||
|
||||
if (!this.toUpdateMetadata.authors || this.toUpdateMetadata.authors.length === 0) {
|
||||
this.toUpdateMetadata.authors = this.fetchedMetadata.authors;
|
||||
}
|
||||
|
||||
if (!this.toUpdateMetadata.categories || this.toUpdateMetadata.categories.length === 0) {
|
||||
this.toUpdateMetadata.categories = this.fetchedMetadata.categories;
|
||||
}
|
||||
|
||||
if(this.updateThumbnail) {
|
||||
this.toUpdateMetadata.thumbnail = this.fetchedMetadata.thumbnail;
|
||||
}
|
||||
}
|
||||
|
||||
shouldUpdateThumbnail() {
|
||||
this.updateThumbnail = true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user