Implement library update

This commit is contained in:
aditya.chandel
2025-01-19 02:56:25 -07:00
parent 6beae77c35
commit 67d1b75831
23 changed files with 261 additions and 124 deletions

View File

@@ -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();
}
}*/
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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> {
}

View File

@@ -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())

View File

@@ -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);

View 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);
}

View File

@@ -86,7 +86,7 @@ public class FileUploadService {
LibraryFile libraryFile = LibraryFile.builder()
.libraryEntity(libraryEntity)
.bookFileType(fileType)
.filePath(storageFile.getAbsolutePath())
.fileName(storageFile.getAbsolutePath())
.build();
switch (fileType) {

View File

@@ -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;

View File

@@ -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));

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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)
);

View File

@@ -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));
});

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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) {

View File

@@ -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({

View File

@@ -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;
})
);

View File

@@ -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'"

View File

@@ -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[] {