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 ad636ec8e..8bd52adae 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.Book; -import com.adityachandel.booklore.model.dto.BookWithNeighbors; import com.adityachandel.booklore.model.dto.Library; -import com.adityachandel.booklore.model.dto.request.CreateLibraryRequest; import com.adityachandel.booklore.model.dto.Sort; +import com.adityachandel.booklore.model.dto.request.CreateLibraryRequest; import com.adityachandel.booklore.service.BooksService; import com.adityachandel.booklore.service.LibraryService; -import com.adityachandel.booklore.service.metadata.parser.AmazonBookParser; import lombok.AllArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; - @RestController @RequestMapping("/v1/library") @AllArgsConstructor @@ -38,6 +35,11 @@ public class LibraryController { return ResponseEntity.ok(libraryService.createLibrary(request)); } + @PutMapping("/{libraryId}") + public ResponseEntity updateLibrary(@RequestBody CreateLibraryRequest request, @PathVariable Long libraryId) { + return ResponseEntity.ok(libraryService.updateLibrary(request, libraryId)); + } + @DeleteMapping("/{libraryId}") public ResponseEntity deleteLibrary(@PathVariable long libraryId) { libraryService.deleteLibrary(libraryId); @@ -60,9 +62,9 @@ public class LibraryController { return ResponseEntity.ok(libraryService.updateSort(libraryId, sort)); } - @PutMapping("/{libraryId}/refresh") + /*@PutMapping("/{libraryId}/refresh") public ResponseEntity refreshLibrary(@PathVariable long libraryId) { libraryService.refreshLibrary(libraryId); return ResponseEntity.noContent().build(); - } + }*/ } \ No newline at end of file diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/LibraryFile.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/LibraryFile.java index 8e6470666..f8093d092 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/LibraryFile.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/LibraryFile.java @@ -1,6 +1,7 @@ package com.adityachandel.booklore.model; import com.adityachandel.booklore.model.entity.LibraryEntity; +import com.adityachandel.booklore.model.entity.LibraryPathEntity; import com.adityachandel.booklore.model.enums.BookFileType; import lombok.AllArgsConstructor; import lombok.Builder; @@ -11,6 +12,7 @@ import lombok.Data; @AllArgsConstructor public class LibraryFile { private LibraryEntity libraryEntity; - private String filePath; + private LibraryPathEntity libraryPathEntity; + private String fileName; private BookFileType bookFileType; } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/BookEntity.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/BookEntity.java index 2a84ef2e8..7573de710 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/BookEntity.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/BookEntity.java @@ -23,9 +23,6 @@ public class BookEntity { @Column(name = "file_name", length = 1000) private String fileName; - @Column(name = "path") - private String path; - @Column(name = "book_type") private BookFileType bookType; @@ -36,6 +33,10 @@ public class BookEntity { @JoinColumn(name = "library_id", nullable = false) private LibraryEntity library; + @ManyToOne + @JoinColumn(name = "library_path_id", nullable = false) + private LibraryPathEntity libraryPath; + @OneToOne(mappedBy = "book", cascade = CascadeType.ALL, orphanRemoval = true) private PdfViewerPreferencesEntity pdfViewerPrefs; diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/LibraryPathEntity.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/LibraryPathEntity.java index 7fa013289..17e4618f2 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/LibraryPathEntity.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/LibraryPathEntity.java @@ -3,6 +3,8 @@ package com.adityachandel.booklore.model.entity; import jakarta.persistence.*; import lombok.*; +import java.util.List; + @Entity @Getter @Setter @@ -20,6 +22,9 @@ public class LibraryPathEntity { @JoinColumn(name = "library_id", nullable = false) private LibraryEntity library; + @OneToMany(mappedBy = "libraryPath", fetch = FetchType.LAZY) + private List books; + @Column(nullable = false) private String path; } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/repository/BookRepository.java b/booklore-api/src/main/java/com/adityachandel/booklore/repository/BookRepository.java index ccff644eb..871e1006d 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/repository/BookRepository.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/repository/BookRepository.java @@ -1,6 +1,7 @@ package com.adityachandel.booklore.repository; import com.adityachandel.booklore.model.entity.BookEntity; +import com.adityachandel.booklore.model.entity.LibraryPathEntity; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; @@ -21,6 +22,9 @@ public interface BookRepository extends JpaRepository, JpaSpec List findBooksByLibraryId(Long libraryId); + @Query("SELECT b.id FROM BookEntity b WHERE b.libraryPath.id IN :libraryPathIds") + List findAllBookIdsByLibraryPathIdIn(@Param("libraryPathIds") Collection libraryPathIds); + Optional findBookByIdAndLibraryId(long id, long libraryId); Optional findBookByFileNameAndLibraryId(String fileName, long libraryId); @@ -28,10 +32,6 @@ public interface BookRepository extends JpaRepository, JpaSpec @Query("SELECT b FROM BookEntity b JOIN b.metadata bm WHERE LOWER(bm.title) LIKE LOWER(CONCAT('%', :title, '%'))") List findByTitleContainingIgnoreCase(@Param("title") String title); - Optional findFirstByLibraryIdAndIdLessThanOrderByIdDesc(Long libraryId, Long currentBookId); - - Optional findFirstByLibraryIdAndIdGreaterThanOrderByIdAsc(Long libraryId, Long currentBookId); - @Query("SELECT b FROM BookEntity b JOIN b.shelves s WHERE s.id = :shelfId") List findByShelfId(@Param("shelfId") Long shelfId); diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/repository/LibraryPathRepository.java b/booklore-api/src/main/java/com/adityachandel/booklore/repository/LibraryPathRepository.java new file mode 100644 index 000000000..c210f9a55 --- /dev/null +++ b/booklore-api/src/main/java/com/adityachandel/booklore/repository/LibraryPathRepository.java @@ -0,0 +1,10 @@ +package com.adityachandel.booklore.repository; + +import com.adityachandel.booklore.model.entity.LibraryPathEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface LibraryPathRepository extends JpaRepository { + +} diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/BookCreatorService.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/BookCreatorService.java index 49402ee52..5683709d6 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/BookCreatorService.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/BookCreatorService.java @@ -24,10 +24,10 @@ public class BookCreatorService { private EpubViewerPreferencesRepository epubViewerPreferencesRepository; public BookEntity createShellBook(LibraryFile libraryFile, BookFileType bookFileType) { - File bookFile = new File(libraryFile.getFilePath()); + File bookFile = new File(libraryFile.getLibraryPathEntity().getPath() + "/" + libraryFile.getFileName()); BookEntity bookEntity = BookEntity.builder() .library(libraryFile.getLibraryEntity()) - .path(bookFile.getPath()) + .libraryPath(libraryFile.getLibraryPathEntity()) .fileName(bookFile.getName()) .bookType(bookFileType) .addedOn(Instant.now()) 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 a0f186529..59d6d99bf 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 @@ -99,7 +99,7 @@ public class BooksService { public ResponseEntity getBookData(long bookId) throws IOException { BookEntity bookEntity = bookRepository.findById(bookId).orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(bookId)); - byte[] pdfBytes = Files.readAllBytes(new File(bookEntity.getPath()).toPath()); + byte[] pdfBytes = Files.readAllBytes(new File(bookEntity.getLibraryPath().getPath() + "/" + bookEntity.getFileName()).toPath()); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_TYPE, "application/pdf") .body(pdfBytes); @@ -158,7 +158,7 @@ public class BooksService { public ResponseEntity prepareFileForDownload(Long bookId) { try { BookEntity bookEntity = bookRepository.findById(bookId).orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(bookId)); - String filePath = bookEntity.getPath(); + String filePath = bookEntity.getLibraryPath().getPath() + "/" + bookEntity.getFileName(); Path file = Paths.get(filePath).toAbsolutePath().normalize(); Resource resource = new UrlResource(file.toUri()); String contentType = Files.probeContentType(file); diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/EpubService.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/EpubService.java index b5c0618a0..70818eb8b 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/EpubService.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/EpubService.java @@ -18,7 +18,7 @@ public class EpubService { public ByteArrayResource getEpubFile(Long bookId) throws IOException { BookEntity bookEntity = bookRepository.findById(bookId).orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(bookId)); - try (FileInputStream inputStream = new FileInputStream(bookEntity.getPath())) { + try (FileInputStream inputStream = new FileInputStream(bookEntity.getLibraryPath().getPath() + "/" + bookEntity.getFileName())) { byte[] fileContent = inputStream.readAllBytes(); return new ByteArrayResource(fileContent); } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/FileUploadService.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/FileUploadService.java index 2f8fc47a8..0d659570f 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/FileUploadService.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/FileUploadService.java @@ -86,7 +86,7 @@ public class FileUploadService { LibraryFile libraryFile = LibraryFile.builder() .libraryEntity(libraryEntity) .bookFileType(fileType) - .filePath(storageFile.getAbsolutePath()) + .fileName(storageFile.getAbsolutePath()) .build(); switch (fileType) { diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/LibraryProcessingService.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/LibraryProcessingService.java index e1b4033ae..d6bb2c96b 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/LibraryProcessingService.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/LibraryProcessingService.java @@ -48,14 +48,14 @@ public class LibraryProcessingService { notificationService.sendMessage(Topic.LOG, createLogNotification("Finished processing library: " + libraryEntity.getName())); } - @Transactional + /*@Transactional public void refreshLibrary(long libraryId) throws IOException { LibraryEntity libraryEntity = libraryRepository.findById(libraryId).orElseThrow(() -> ApiError.LIBRARY_NOT_FOUND.createException(libraryId)); notificationService.sendMessage(Topic.LOG, createLogNotification("Started refreshing library: " + libraryEntity.getName())); processLibraryFiles(getUnProcessedFiles(libraryEntity)); deleteRemovedBooks(getRemovedBooks(libraryEntity)); notificationService.sendMessage(Topic.LOG, createLogNotification("Finished refreshing library: " + libraryEntity.getName())); - } + }*/ @Transactional protected void deleteRemovedBooks(List removedBookEntities) { @@ -70,12 +70,12 @@ public class LibraryProcessingService { @Transactional protected void processLibraryFiles(List libraryFiles) { for (LibraryFile libraryFile : libraryFiles) { - log.info("Processing file: {}", libraryFile.getFilePath()); + log.info("Processing file: {}", libraryFile.getFileName()); Book book = processLibraryFile(libraryFile); if (book != null) { notificationService.sendMessage(Topic.BOOK_ADD, book); notificationService.sendMessage(Topic.LOG, createLogNotification("Book added: " + book.getFileName())); - log.info("Processed file: {}", libraryFile.getFilePath()); + log.info("Processed file: {}", libraryFile.getFileName()); } } } @@ -90,19 +90,19 @@ public class LibraryProcessingService { return null; } - @Transactional + /*@Transactional protected List getRemovedBooks(LibraryEntity libraryEntity) throws IOException { List libraryFiles = getLibraryFiles(libraryEntity); List bookEntities = libraryEntity.getBookEntities(); Set libraryFilePaths = libraryFiles.stream() - .map(LibraryFile::getFilePath) + .map(LibraryFile::getFileName) .collect(Collectors.toSet()); return bookEntities.stream() .filter(book -> !libraryFilePaths.contains(book.getPath())) .collect(Collectors.toList()); - } + }*/ - @Transactional + /*@Transactional protected List getUnProcessedFiles(LibraryEntity libraryEntity) throws IOException { List libraryFiles = getLibraryFiles(libraryEntity); List bookEntities = libraryEntity.getBookEntities(); @@ -110,21 +110,21 @@ public class LibraryProcessingService { .map(BookEntity::getPath) .collect(Collectors.toSet()); return libraryFiles.stream() - .filter(libraryFile -> !processedPaths.contains(libraryFile.getFilePath())) + .filter(libraryFile -> !processedPaths.contains(libraryFile.getFileName())) .collect(Collectors.toList()); - } + }*/ private List getLibraryFiles(LibraryEntity libraryEntity) throws IOException { List libraryFiles = new ArrayList<>(); - for (LibraryPathEntity libraryPath : libraryEntity.getLibraryPaths()) { - libraryFiles.addAll(findLibraryFiles(libraryPath.getPath(), libraryEntity)); + for (LibraryPathEntity libraryPathEntity : libraryEntity.getLibraryPaths()) { + libraryFiles.addAll(findLibraryFiles(libraryPathEntity, libraryEntity)); } return libraryFiles; } - private List findLibraryFiles(String directoryPath, LibraryEntity libraryEntity) throws IOException { + private List findLibraryFiles(LibraryPathEntity libraryPathEntity, LibraryEntity libraryEntity) throws IOException { List libraryFiles = new ArrayList<>(); - try (var stream = Files.walk(Path.of(directoryPath))) { + try (var stream = Files.walk(Path.of(libraryPathEntity.getPath()))) { stream.filter(Files::isRegularFile) .filter(file -> { String fileName = file.getFileName().toString().toLowerCase(); @@ -132,7 +132,7 @@ public class LibraryProcessingService { }) .forEach(file -> { BookFileType fileType = file.getFileName().toString().toLowerCase().endsWith(".pdf") ? BookFileType.PDF : BookFileType.EPUB; - libraryFiles.add(new LibraryFile(libraryEntity, file.toAbsolutePath().toString(), fileType)); + libraryFiles.add(new LibraryFile(libraryEntity, libraryPathEntity, file.toFile().getName(), fileType)); }); } return libraryFiles; diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/LibraryService.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/LibraryService.java index b922edc27..27f36f17a 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/LibraryService.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/LibraryService.java @@ -5,12 +5,15 @@ import com.adityachandel.booklore.mapper.BookMapper; import com.adityachandel.booklore.mapper.LibraryMapper; import com.adityachandel.booklore.model.dto.Book; import com.adityachandel.booklore.model.dto.Library; +import com.adityachandel.booklore.model.dto.LibraryPath; import com.adityachandel.booklore.model.dto.Sort; import com.adityachandel.booklore.model.dto.request.CreateLibraryRequest; import com.adityachandel.booklore.model.entity.BookEntity; import com.adityachandel.booklore.model.entity.LibraryEntity; import com.adityachandel.booklore.model.entity.LibraryPathEntity; +import com.adityachandel.booklore.model.websocket.Topic; import com.adityachandel.booklore.repository.BookRepository; +import com.adityachandel.booklore.repository.LibraryPathRepository; import com.adityachandel.booklore.repository.LibraryRepository; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -20,6 +23,7 @@ import org.springframework.stereotype.Service; import java.io.IOException; import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; @Slf4j @@ -28,10 +32,62 @@ import java.util.stream.Collectors; public class LibraryService { private final LibraryRepository libraryRepository; + private final LibraryPathRepository libraryPathRepository; private final BookRepository bookRepository; private final LibraryProcessingService libraryProcessingService; private final BookMapper bookMapper; private final LibraryMapper libraryMapper; + private final NotificationService notificationService; + + public Library updateLibrary(CreateLibraryRequest request, Long libraryId) { + LibraryEntity library = libraryRepository.findById(libraryId).orElseThrow(() -> ApiError.LIBRARY_NOT_FOUND.createException(libraryId)); + library.setName(request.getName()); + library.setIcon(request.getIcon()); + + Set currentPaths = library.getLibraryPaths().stream().map(LibraryPathEntity::getPath).collect(Collectors.toSet()); + Set updatedPaths = request.getPaths().stream().map(LibraryPath::getPath).collect(Collectors.toSet()); + + Set deletedPaths = currentPaths.stream().filter(path -> !updatedPaths.contains(path)).collect(Collectors.toSet()); + Set newPaths = updatedPaths.stream().filter(path -> !currentPaths.contains(path)).collect(Collectors.toSet()); + + if (!deletedPaths.isEmpty()) { + Set pathsToRemove = library.getLibraryPaths().stream() + .filter(pathEntity -> deletedPaths.contains(pathEntity.getPath())) + .collect(Collectors.toSet()); + library.getLibraryPaths().removeAll(pathsToRemove); + + List books = bookRepository.findAllBookIdsByLibraryPathIdIn(pathsToRemove.stream().map(LibraryPathEntity::getId).collect(Collectors.toSet())); + if (!books.isEmpty()) { + notificationService.sendMessage(Topic.BOOKS_REMOVE, books); + } + + libraryPathRepository.deleteAll(pathsToRemove); + libraryPathRepository.saveAll(library.getLibraryPaths()); + libraryRepository.save(library); + } + + if (!newPaths.isEmpty()) { + Set newPathEntities = newPaths.stream() + .map(path -> LibraryPathEntity.builder().path(path).library(library).build()) + .collect(Collectors.toSet()); + library.getLibraryPaths().addAll(newPathEntities); + libraryPathRepository.saveAll(library.getLibraryPaths()); + libraryRepository.save(library); + + Thread.startVirtualThread(() -> { + try { + libraryProcessingService.processLibrary(libraryId); + } catch (InvalidDataAccessApiUsageException e) { + log.warn("InvalidDataAccessApiUsageException - Library id: {}", libraryId); + } catch (IOException e) { + log.error("Error while parsing library books", e); + } + log.info("Parsing task completed!"); + }); + } + + return libraryMapper.toLibrary(library); + } public Library createLibrary(CreateLibraryRequest request) { LibraryEntity libraryEntity = LibraryEntity.builder() @@ -60,7 +116,7 @@ public class LibraryService { return libraryMapper.toLibrary(libraryEntity); } - public void refreshLibrary(long libraryId) { + /*public void refreshLibrary(long libraryId) { libraryRepository.findById(libraryId).orElseThrow(() -> ApiError.LIBRARY_NOT_FOUND.createException(libraryId)); Thread.startVirtualThread(() -> { try { @@ -72,7 +128,7 @@ public class LibraryService { } log.info("Parsing task completed!"); }); - } + }*/ public Library getLibrary(long libraryId) { LibraryEntity libraryEntity = libraryRepository.findById(libraryId).orElseThrow(() -> ApiError.LIBRARY_NOT_FOUND.createException(libraryId)); diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/fileprocessor/EpubProcessor.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/fileprocessor/EpubProcessor.java index d35f749af..3ae76f053 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/fileprocessor/EpubProcessor.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/fileprocessor/EpubProcessor.java @@ -24,7 +24,6 @@ import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; -import java.time.Instant; import java.time.LocalDate; import java.time.OffsetDateTime; import java.time.format.DateTimeParseException; @@ -46,7 +45,7 @@ public class EpubProcessor implements FileProcessor { @Transactional(propagation = Propagation.REQUIRES_NEW) @Override public Book processFile(LibraryFile libraryFile, boolean forceProcess) { - File bookFile = new File(libraryFile.getFilePath()); + File bookFile = new File(libraryFile.getFileName()); String fileName = bookFile.getName(); if (!forceProcess) { Optional bookOptional = bookRepository.findBookByFileNameAndLibraryId(fileName, libraryFile.getLibraryEntity().getId()); @@ -62,7 +61,7 @@ public class EpubProcessor implements FileProcessor { protected Book processNewFile(LibraryFile libraryFile) { BookEntity bookEntity = bookCreatorService.createShellBook(libraryFile, BookFileType.EPUB); try { - io.documentnode.epub4j.domain.Book epub = new EpubReader().readEpub(new FileInputStream(libraryFile.getFilePath())); + io.documentnode.epub4j.domain.Book epub = new EpubReader().readEpub(new FileInputStream(libraryFile.getLibraryPathEntity().getPath() + "/" + libraryFile.getFileName())); setBookMetadata(epub, bookEntity); processCover(epub, bookEntity); @@ -72,7 +71,7 @@ public class EpubProcessor implements FileProcessor { bookRepository.flush(); } catch (Exception e) { - log.error("Error while processing file {}, error: {}", libraryFile.getFilePath(), e.getMessage()); + log.error("Error while processing file {}, error: {}", libraryFile.getFileName(), e.getMessage()); } return bookMapper.toBook(bookEntity); } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/fileprocessor/PdfProcessor.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/fileprocessor/PdfProcessor.java index 5ae529987..89d5cdf86 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/fileprocessor/PdfProcessor.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/fileprocessor/PdfProcessor.java @@ -20,7 +20,6 @@ import org.springframework.transaction.annotation.Transactional; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; -import java.time.Instant; import java.util.Arrays; import java.util.HashSet; import java.util.Optional; @@ -40,7 +39,7 @@ public class PdfProcessor implements FileProcessor { @Transactional(propagation = Propagation.REQUIRES_NEW) @Override public Book processFile(LibraryFile libraryFile, boolean forceProcess) { - File bookFile = new File(libraryFile.getFilePath()); + File bookFile = new File(libraryFile.getFileName()); String fileName = bookFile.getName(); if (!forceProcess) { Optional bookOptional = bookRepository.findBookByFileNameAndLibraryId(fileName, libraryFile.getLibraryEntity().getId()); @@ -55,7 +54,7 @@ public class PdfProcessor implements FileProcessor { @Transactional(propagation = Propagation.REQUIRES_NEW) protected Book processNewFile(LibraryFile libraryFile) { BookEntity bookEntity = bookCreatorService.createShellBook(libraryFile, BookFileType.PDF); - try (PDDocument pdf = Loader.loadPDF(new File(libraryFile.getFilePath()))) { + try (PDDocument pdf = Loader.loadPDF(new File(libraryFile.getLibraryPathEntity().getPath() + "/" + libraryFile.getFileName()))) { setMetadata(pdf, bookEntity); processCover(pdf, bookEntity); @@ -64,7 +63,7 @@ public class PdfProcessor implements FileProcessor { bookEntity = bookRepository.save(bookEntity); bookRepository.flush(); } catch (Exception e) { - log.error("Error while processing file {}, error: {}", libraryFile.getFilePath(), e.getMessage()); + log.error("Error while processing file {}, error: {}", libraryFile.getFileName(), e.getMessage()); } return bookMapper.toBook(bookEntity); } 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 3e3e8123d..1efb24644 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 @@ -16,17 +16,18 @@ CREATE TABLE IF NOT EXISTS library_path CREATE TABLE IF NOT EXISTS book ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - file_name VARCHAR(255) NOT NULL, - book_type VARCHAR(6) NOT NULL, - library_id BIGINT NOT NULL, - path VARCHAR(1000) NOT NULL, - added_on TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - last_read_time TIMESTAMP NULL, - pdf_progress INT NULL, - epub_progress VARCHAR(1000) NULL, + id BIGINT AUTO_INCREMENT PRIMARY KEY, + file_name VARCHAR(255) NOT NULL, + book_type VARCHAR(6) NOT NULL, + library_id BIGINT NOT NULL, + library_path_id BIGINT NOT NULL, + added_on TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_read_time TIMESTAMP NULL, + pdf_progress INT NULL, + epub_progress VARCHAR(1000) NULL, CONSTRAINT fk_library FOREIGN KEY (library_id) REFERENCES library (id) ON DELETE CASCADE, + CONSTRAINT fk_library_path_id FOREIGN KEY (library_path_id) REFERENCES library_path (id) ON DELETE CASCADE, CONSTRAINT unique_file_library UNIQUE (file_name, library_id) ); diff --git a/booklore-ui/src/app/app.component.ts b/booklore-ui/src/app/app.component.ts index 43b8740af..5ff304c3f 100644 --- a/booklore-ui/src/app/app.component.ts +++ b/booklore-ui/src/app/app.component.ts @@ -27,7 +27,7 @@ export class AppComponent implements OnInit { this.bookService.handleNewlyCreatedBook(JSON.parse(message.body)); }); - this.rxStompService.watch('/topic/books-removed').subscribe((message: Message) => { + this.rxStompService.watch('/topic/books-remove').subscribe((message: Message) => { this.bookService.handleRemovedBookIds(JSON.parse(message.body)); }); diff --git a/booklore-ui/src/app/book/components/library-creator/library-creator.component.html b/booklore-ui/src/app/book/components/library-creator/library-creator.component.html index 86629b781..0d916c4f8 100644 --- a/booklore-ui/src/app/book/components/library-creator/library-creator.component.html +++ b/booklore-ui/src/app/book/components/library-creator/library-creator.component.html @@ -11,7 +11,7 @@

Library Name:

- +

Library Icon:

@@ -64,7 +64,7 @@
- +
diff --git a/booklore-ui/src/app/book/components/library-creator/library-creator.component.ts b/booklore-ui/src/app/book/components/library-creator/library-creator.component.ts index de32cf6aa..f32abb082 100644 --- a/booklore-ui/src/app/book/components/library-creator/library-creator.component.ts +++ b/booklore-ui/src/app/book/components/library-creator/library-creator.component.ts @@ -1,57 +1,60 @@ -import {Component, inject, ViewChild} from '@angular/core'; -import {DialogService, DynamicDialogRef} from 'primeng/dynamicdialog'; +import {Component, inject, OnInit, ViewChild} from '@angular/core'; +import {DialogService, DynamicDialogConfig, DynamicDialogRef} from 'primeng/dynamicdialog'; import {DirectoryPickerComponent} from '../../../utilities/component/directory-picker/directory-picker.component'; import {MessageService} from 'primeng/api'; import {Router} from '@angular/router'; import {LibraryService} from '../../service/library.service'; import {IconPickerComponent} from '../../../utilities/component/icon-picker/icon-picker.component'; -import {take} from 'rxjs'; import {Button} from 'primeng/button'; import {TableModule} from 'primeng/table'; import {Step, StepList, StepPanel, StepPanels, Stepper} from 'primeng/stepper'; import {NgIf} from '@angular/common'; import {FormsModule} from '@angular/forms'; import {InputText} from 'primeng/inputtext'; -import {BookService} from '../../service/book.service'; -import {Library, LibraryPath} from '../../model/library.model'; +import {Library} from '../../model/library.model'; @Component({ selector: 'app-library-creator', standalone: true, templateUrl: './library-creator.component.html', - imports: [ - Button, - TableModule, - StepPanel, - IconPickerComponent, - NgIf, - FormsModule, - InputText, - Stepper, - StepList, - Step, - StepPanels - ], + imports: [Button, TableModule, StepPanel, IconPickerComponent, NgIf, FormsModule, InputText, Stepper, StepList, Step, StepPanels], styleUrl: './library-creator.component.scss' }) -export class LibraryCreatorComponent { +export class LibraryCreatorComponent implements OnInit { @ViewChild(IconPickerComponent) iconPicker: IconPickerComponent | undefined; - libraryName: string = ''; + chosenLibraryName: string = ''; folders: string[] = []; - ref: DynamicDialogRef | undefined; selectedIcon: string | null = null; + mode!: string; + library!: Library | undefined; + editModeLibraryName: string = ''; + private dialogService = inject(DialogService); private dynamicDialogRef = inject(DynamicDialogRef); + private dynamicDialogConfig = inject(DynamicDialogConfig); private libraryService = inject(LibraryService); private messageService = inject(MessageService); private router = inject(Router); + ngOnInit(): void { + this.mode = this.dynamicDialogConfig.data.mode; + if (this.mode === 'edit') { + this.library = this.libraryService.findLibraryById(this.dynamicDialogConfig.data.libraryId); + if (this.library) { + this.chosenLibraryName = this.library.name; + this.editModeLibraryName = this.library.name; + this.selectedIcon = 'pi pi-' + this.library.icon; + this.folders = this.library.paths.map(path => path.path); + } + } + } + show() { - this.ref = this.dialogService.open(DirectoryPickerComponent, { + this.dynamicDialogRef = this.dialogService.open(DirectoryPickerComponent, { header: 'Select Media Directory', modal: true, width: '50%', @@ -60,7 +63,7 @@ export class LibraryCreatorComponent { baseZIndex: 10 }); - this.ref.onClose.subscribe((selectedFolder: string) => { + this.dynamicDialogRef.onClose.subscribe((selectedFolder: string) => { if (selectedFolder) { this.addFolder(selectedFolder); } @@ -90,49 +93,65 @@ export class LibraryCreatorComponent { } isLibraryDetailsValid(): boolean { - return !!this.libraryName.trim() && !!this.selectedIcon; + return !!this.chosenLibraryName.trim() && !!this.selectedIcon; } isDirectorySelectionValid(): boolean { return this.folders.length > 0; } - addLibrary() { - const library: Library = { - name: this.libraryName, - icon: this.selectedIcon?.replace('pi pi-', '') || 'heart', - paths: this.folders.map(folder => ({ path: folder })) - }; - - this.libraryService.createLibrary(library).subscribe({ - next: (createdLibrary) => { - this.router.navigate(['/library', createdLibrary.id, 'books']); - }, - error: (err) => { - console.error('Failed to create library:', err); - } - }); - - this.dynamicDialogRef.close(); + createOrUpdateLibrary() { + if (this.mode === 'edit') { + const library: Library = { + name: this.chosenLibraryName, + icon: this.selectedIcon?.replace('pi pi-', '') || 'heart', + paths: this.folders.map(folder => ({path: folder})) + }; + this.libraryService.updateLibrary(library, this.library?.id).subscribe({ + next: () => { + this.messageService.add({severity: 'success', summary: 'Library Updated', detail: 'The library was updated successfully.'}); + this.dynamicDialogRef.close(); + }, + error: (e) => { + this.messageService.add({severity: 'error', summary: 'Update Failed', detail: 'An error occurred while updating the library. Please try again.'}); + console.error(e); + } + }); + } else { + const library: Library = { + name: this.chosenLibraryName, + icon: this.selectedIcon?.replace('pi pi-', '') || 'heart', + paths: this.folders.map(folder => ({path: folder})) + }; + this.libraryService.createLibrary(library).subscribe({ + next: (createdLibrary) => { + this.router.navigate(['/library', createdLibrary.id, 'books']); + this.messageService.add({severity: 'success', summary: 'Library Created', detail: 'The library was created successfully.'}); + this.dynamicDialogRef.close(); + }, + error: (e) => { + this.messageService.add({severity: 'error', summary: 'Creation Failed', detail: 'An error occurred while creating the library. Please try again.'}); + console.error(e); + } + }); + } } validateLibraryNameAndProceed(activateCallback: Function) { - if (this.libraryName.trim()) { - const libraryName = this.libraryName.trim(); - this.libraryService.libraryState$ - .pipe(take(1)) - .subscribe(libraryState => { - const library = libraryState.libraries?.find(library => library.name === libraryName); - if (library) { - this.messageService.add({ - severity: 'error', - summary: 'Library Name Exists', - detail: 'This library name is already taken.', - }); - } else { - activateCallback(2); - } + let trimmedLibraryName = this.chosenLibraryName.trim(); + if (trimmedLibraryName && trimmedLibraryName != this.editModeLibraryName) { + let exists = this.libraryService.doesLibraryExistByName(trimmedLibraryName); + if (exists) { + this.messageService.add({ + severity: 'error', + summary: 'Library Name Exists', + detail: 'This library name is already taken.', }); + } else { + activateCallback(2); + } + } else { + activateCallback(2); } } diff --git a/booklore-ui/src/app/book/service/book.service.ts b/booklore-ui/src/app/book/service/book.service.ts index e3f6cbcfa..75f18c371 100644 --- a/booklore-ui/src/app/book/service/book.service.ts +++ b/booklore-ui/src/app/book/service/book.service.ts @@ -182,10 +182,10 @@ export class BookService { this.bookStateSubject.next({...currentState, books: updatedBooks}); } - handleRemovedBookIds(removedBookIds: Set): void { + handleRemovedBookIds(removedBookIds: number[]): void { const currentState = this.bookStateSubject.value; - const filteredBooks = (currentState.books || []).filter(book => !removedBookIds.has(book.id)); - this.bookStateSubject.next({...currentState, books: filteredBooks}); + const filteredBooks = (currentState.books || []).filter(book => !removedBookIds.includes(book.id)); // Check using includes() method + this.bookStateSubject.next({ ...currentState, books: filteredBooks }); } handleBookUpdate(updatedBook: Book) { diff --git a/booklore-ui/src/app/book/service/library-shelf-menu.service.ts b/booklore-ui/src/app/book/service/library-shelf-menu.service.ts index cf22e8322..08cc9cc0b 100644 --- a/booklore-ui/src/app/book/service/library-shelf-menu.service.ts +++ b/booklore-ui/src/app/book/service/library-shelf-menu.service.ts @@ -8,6 +8,7 @@ import {Shelf} from '../model/shelf.model'; import {DialogService} from 'primeng/dynamicdialog'; import {MetadataFetchOptionsComponent} from '../../metadata/metadata-options-dialog/metadata-fetch-options/metadata-fetch-options.component'; import {MetadataRefreshType} from '../../metadata/model/request/metadata-refresh-type.enum'; +import {LibraryCreatorComponent} from '../components/library-creator/library-creator.component'; @Injectable({ providedIn: 'root', @@ -21,12 +22,32 @@ export class LibraryShelfMenuService { private router = inject(Router); private dialogService = inject(DialogService); - initializeLibraryMenuItems(entity: Library | Shelf | null): MenuItem[] { return [ { label: 'Options', items: [ + { + label: 'Edit Library', + icon: 'pi pi-pen-to-square', + command: () => { + this.dialogService.open(LibraryCreatorComponent, { + header: 'Edit Library', + modal: true, + closable: true, + width: '675px', + height: '480px', + style: { + position: 'absolute', + top: '15%', + }, + data: { + mode: 'edit', + libraryId: entity?.id + } + }); + } + }, { label: 'Delete Library', icon: 'pi pi-trash', @@ -56,7 +77,7 @@ export class LibraryShelfMenuService { } }, { - label: 'Refresh Library', + label: 'Re-scan Library', icon: 'pi pi-refresh', command: () => { this.confirmationService.confirm({ diff --git a/booklore-ui/src/app/book/service/library.service.ts b/booklore-ui/src/app/book/service/library.service.ts index e94caf330..7a9d01887 100644 --- a/booklore-ui/src/app/book/service/library.service.ts +++ b/booklore-ui/src/app/book/service/library.service.ts @@ -46,6 +46,15 @@ export class LibraryService { }); } + doesLibraryExistByName(name: string): boolean { + const libraries = this.libraryStateSubject.value.libraries || []; + return libraries.some(library => library.name === name); + } + + findLibraryById(id: number): Library | undefined { + return this.libraryStateSubject.value.libraries?.find(library => library.id === id); + } + createLibrary(newLibrary: Library): Observable { return this.http.post(this.url, newLibrary).pipe( map(createdLibrary => { @@ -55,8 +64,21 @@ export class LibraryService { return createdLibrary; }), catchError(error => { + throw error; + }) + ); + } + + updateLibrary(library: Library, libraryId: number | undefined): Observable { + return this.http.put(`${this.url}/${libraryId}`, library).pipe( + map(updatedLibrary => { const currentState = this.libraryStateSubject.value; - this.libraryStateSubject.next({...currentState, error: error.message}); + const updatedLibraries = currentState.libraries ? currentState.libraries.map(existingLibrary => + existingLibrary.id === updatedLibrary.id ? updatedLibrary : existingLibrary) : [updatedLibrary]; + this.libraryStateSubject.next({...currentState, libraries: updatedLibraries,}); + return updatedLibrary; + }), + catchError(error => { throw error; }) ); diff --git a/booklore-ui/src/app/metadata/book-metadata-center/metadata-picker/metadata-picker.component.html b/booklore-ui/src/app/metadata/book-metadata-center/metadata-picker/metadata-picker.component.html index 2fe7ae326..8fed3fc8a 100644 --- a/booklore-ui/src/app/metadata/book-metadata-center/metadata-picker/metadata-picker.component.html +++ b/booklore-ui/src/app/metadata/book-metadata-center/metadata-picker/metadata-picker.component.html @@ -128,7 +128,7 @@
- Book Thumbnail + Book Thumbnail Fetched Thumbnail (); createIconList(categories: string[]): string[] {