mirror of
https://github.com/booklore-app/booklore.git
synced 2025-12-23 22:28:11 -05:00
Implement library update
This commit is contained in:
@@ -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<Library> 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();
|
||||
}
|
||||
}*/
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<BookEntity> books;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String path;
|
||||
}
|
||||
|
||||
@@ -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<BookEntity, Long>, JpaSpec
|
||||
|
||||
List<BookEntity> findBooksByLibraryId(Long libraryId);
|
||||
|
||||
@Query("SELECT b.id FROM BookEntity b WHERE b.libraryPath.id IN :libraryPathIds")
|
||||
List<Long> findAllBookIdsByLibraryPathIdIn(@Param("libraryPathIds") Collection<Long> libraryPathIds);
|
||||
|
||||
Optional<BookEntity> findBookByIdAndLibraryId(long id, long libraryId);
|
||||
|
||||
Optional<BookEntity> findBookByFileNameAndLibraryId(String fileName, long libraryId);
|
||||
@@ -28,10 +32,6 @@ public interface BookRepository extends JpaRepository<BookEntity, Long>, JpaSpec
|
||||
@Query("SELECT b FROM BookEntity b JOIN b.metadata bm WHERE LOWER(bm.title) LIKE LOWER(CONCAT('%', :title, '%'))")
|
||||
List<BookEntity> findByTitleContainingIgnoreCase(@Param("title") String title);
|
||||
|
||||
Optional<BookEntity> findFirstByLibraryIdAndIdLessThanOrderByIdDesc(Long libraryId, Long currentBookId);
|
||||
|
||||
Optional<BookEntity> findFirstByLibraryIdAndIdGreaterThanOrderByIdAsc(Long libraryId, Long currentBookId);
|
||||
|
||||
@Query("SELECT b FROM BookEntity b JOIN b.shelves s WHERE s.id = :shelfId")
|
||||
List<BookEntity> findByShelfId(@Param("shelfId") Long shelfId);
|
||||
|
||||
|
||||
@@ -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<LibraryPathEntity, Long> {
|
||||
|
||||
}
|
||||
@@ -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())
|
||||
|
||||
@@ -99,7 +99,7 @@ public class BooksService {
|
||||
|
||||
public ResponseEntity<byte[]> 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<Resource> 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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ public class FileUploadService {
|
||||
LibraryFile libraryFile = LibraryFile.builder()
|
||||
.libraryEntity(libraryEntity)
|
||||
.bookFileType(fileType)
|
||||
.filePath(storageFile.getAbsolutePath())
|
||||
.fileName(storageFile.getAbsolutePath())
|
||||
.build();
|
||||
|
||||
switch (fileType) {
|
||||
|
||||
@@ -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<BookEntity> removedBookEntities) {
|
||||
@@ -70,12 +70,12 @@ public class LibraryProcessingService {
|
||||
@Transactional
|
||||
protected void processLibraryFiles(List<LibraryFile> 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<BookEntity> getRemovedBooks(LibraryEntity libraryEntity) throws IOException {
|
||||
List<LibraryFile> libraryFiles = getLibraryFiles(libraryEntity);
|
||||
List<BookEntity> bookEntities = libraryEntity.getBookEntities();
|
||||
Set<String> 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<LibraryFile> getUnProcessedFiles(LibraryEntity libraryEntity) throws IOException {
|
||||
List<LibraryFile> libraryFiles = getLibraryFiles(libraryEntity);
|
||||
List<BookEntity> 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<LibraryFile> getLibraryFiles(LibraryEntity libraryEntity) throws IOException {
|
||||
List<LibraryFile> 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<LibraryFile> findLibraryFiles(String directoryPath, LibraryEntity libraryEntity) throws IOException {
|
||||
private List<LibraryFile> findLibraryFiles(LibraryPathEntity libraryPathEntity, LibraryEntity libraryEntity) throws IOException {
|
||||
List<LibraryFile> 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;
|
||||
|
||||
@@ -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<String> currentPaths = library.getLibraryPaths().stream().map(LibraryPathEntity::getPath).collect(Collectors.toSet());
|
||||
Set<String> updatedPaths = request.getPaths().stream().map(LibraryPath::getPath).collect(Collectors.toSet());
|
||||
|
||||
Set<String> deletedPaths = currentPaths.stream().filter(path -> !updatedPaths.contains(path)).collect(Collectors.toSet());
|
||||
Set<String> newPaths = updatedPaths.stream().filter(path -> !currentPaths.contains(path)).collect(Collectors.toSet());
|
||||
|
||||
if (!deletedPaths.isEmpty()) {
|
||||
Set<LibraryPathEntity> pathsToRemove = library.getLibraryPaths().stream()
|
||||
.filter(pathEntity -> deletedPaths.contains(pathEntity.getPath()))
|
||||
.collect(Collectors.toSet());
|
||||
library.getLibraryPaths().removeAll(pathsToRemove);
|
||||
|
||||
List<Long> 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<LibraryPathEntity> 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));
|
||||
|
||||
@@ -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<BookEntity> 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);
|
||||
}
|
||||
|
||||
@@ -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<BookEntity> 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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<div class="library-name-icon-parent">
|
||||
<div class="library-name-div">
|
||||
<p>Library Name: </p>
|
||||
<input type="text" pInputText [(ngModel)]="libraryName" placeholder="Enter library name..."/>
|
||||
<input type="text" pInputText [(ngModel)]="chosenLibraryName" placeholder="Enter library name..."/>
|
||||
</div>
|
||||
<div class="library-icon-div">
|
||||
<p>Library Icon:</p>
|
||||
@@ -64,7 +64,7 @@
|
||||
</div>
|
||||
<div class="flex pt-6 justify-between">
|
||||
<p-button label="Back" icon="pi pi-arrow-left" iconPos="right" (onClick)="activateCallback(1)" />
|
||||
<p-button severity="success" label="Save" icon="pi pi-save" [disabled]="!isDirectorySelectionValid()" (onClick)="addLibrary()"></p-button>
|
||||
<p-button severity="success" label="Save" icon="pi pi-save" [disabled]="!isDirectorySelectionValid()" (onClick)="createOrUpdateLibrary()"></p-button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -182,10 +182,10 @@ export class BookService {
|
||||
this.bookStateSubject.next({...currentState, books: updatedBooks});
|
||||
}
|
||||
|
||||
handleRemovedBookIds(removedBookIds: Set<number>): 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) {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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<Library> {
|
||||
return this.http.post<Library>(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<Library> {
|
||||
return this.http.put<Library>(`${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;
|
||||
})
|
||||
);
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
<div class="form-row">
|
||||
<label class="label"></label>
|
||||
<div class="input-container">
|
||||
<img *ngIf="!updateThumbnailUrl" [src]="urlHelper.getCoverUrl(metadata.bookId, metadata?.coverUpdatedOn)" alt="Book Thumbnail" class="thumbnail"/>
|
||||
<img *ngIf="!updateThumbnailUrl" [src]="urlHelper.getCoverUrl(metadata.bookId, metadata.coverUpdatedOn)" alt="Book Thumbnail" class="thumbnail"/>
|
||||
<img *ngIf="updateThumbnailUrl" [src]="fetchedMetadata.thumbnailUrl" alt="Fetched Thumbnail" class="thumbnail"/>
|
||||
<p-button
|
||||
[icon]="thumbnailSaved ? 'pi pi-check' : 'pi pi-arrow-left'"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Component, EventEmitter, Output} from '@angular/core';
|
||||
import {Component, EventEmitter, Input, Output} from '@angular/core';
|
||||
import {DialogModule} from 'primeng/dialog';
|
||||
import {NgForOf} from '@angular/common';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
@@ -14,9 +14,6 @@ import {FormsModule} from '@angular/forms';
|
||||
],
|
||||
})
|
||||
export class IconPickerComponent {
|
||||
iconDialogVisible: boolean = false;
|
||||
selectedIcon: string | null = null;
|
||||
searchText: string = '';
|
||||
iconCategories: string[] = [
|
||||
"address-book", "align-center", "align-justify", "align-left", "align-right", "android",
|
||||
"angle-double-down", "angle-double-left", "angle-double-right", "angle-double-up", "angle-down", "angle-left",
|
||||
@@ -51,7 +48,10 @@ export class IconPickerComponent {
|
||||
];
|
||||
|
||||
icons: string[] = this.createIconList(this.iconCategories);
|
||||
iconDialogVisible: boolean = false;
|
||||
searchText: string = '';
|
||||
|
||||
selectedIcon: string | null = null;
|
||||
@Output() iconSelected = new EventEmitter<string>();
|
||||
|
||||
createIconList(categories: string[]): string[] {
|
||||
|
||||
Reference in New Issue
Block a user