mirror of
https://github.com/booklore-app/booklore.git
synced 2026-02-18 00:17:53 +01:00
feat(reader): add PDF annotations, Range streaming, and optimized chunk loading (#2701)
This commit is contained in:
@@ -240,8 +240,8 @@ public class SecurityConfig {
|
||||
CorsConfiguration configuration = new CorsConfiguration();
|
||||
configuration.setAllowedOriginPatterns(List.of("*"));
|
||||
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"));
|
||||
configuration.setAllowedHeaders(List.of("Authorization", "Cache-Control", "Content-Type"));
|
||||
configuration.setExposedHeaders(List.of("Content-Disposition"));
|
||||
configuration.setAllowedHeaders(List.of("Authorization", "Cache-Control", "Content-Type", "Range"));
|
||||
configuration.setExposedHeaders(List.of("Content-Disposition", "Accept-Ranges", "Content-Range", "Content-Length"));
|
||||
configuration.setAllowCredentials(true);
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
|
||||
@@ -18,7 +18,7 @@ public class ImageCachingFilter extends OncePerRequestFilter {
|
||||
String uri = request.getRequestURI();
|
||||
if (uri.startsWith("/api/v1/media/book/") &&
|
||||
(uri.contains("/cover") || uri.contains("/thumbnail") || uri.contains("/backup-cover") ||
|
||||
uri.contains("/pdf/pages/") || uri.contains("/cbx/pages/"))) {
|
||||
uri.contains("/cbx/pages/"))) {
|
||||
response.setHeader(HttpHeaders.CACHE_CONTROL, "public, max-age=3600");
|
||||
response.setHeader(HttpHeaders.EXPIRES, String.valueOf(System.currentTimeMillis() + 3600_000));
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Max;
|
||||
@@ -119,14 +120,19 @@ public class BookController {
|
||||
return ResponseEntity.ok(bookMetadataService.getComicInfoMetadata(bookId));
|
||||
}
|
||||
|
||||
@Operation(summary = "Get book content", description = "Retrieve the binary content of a book for reading.")
|
||||
@ApiResponse(responseCode = "200", description = "Book content returned successfully")
|
||||
@Operation(summary = "Get book content", description = "Retrieve the binary content of a book for reading. Supports HTTP Range requests for partial content streaming.")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "Full book content returned"),
|
||||
@ApiResponse(responseCode = "206", description = "Partial content returned for Range request")
|
||||
})
|
||||
@GetMapping("/{bookId}/content")
|
||||
@CheckBookAccess(bookIdParam = "bookId")
|
||||
public ResponseEntity<Resource> getBookContent(
|
||||
public void getBookContent(
|
||||
@Parameter(description = "ID of the book") @PathVariable long bookId,
|
||||
@Parameter(description = "Optional book type for alternative format (e.g., EPUB, PDF, MOBI)") @RequestParam(required = false) String bookType) {
|
||||
return bookService.getBookContent(bookId, bookType);
|
||||
@Parameter(description = "Optional book type for alternative format (e.g., EPUB, PDF, MOBI)") @RequestParam(required = false) String bookType,
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response) throws java.io.IOException {
|
||||
bookService.streamBookContent(bookId, bookType, request, response);
|
||||
}
|
||||
|
||||
@Operation(summary = "Download book", description = "Download the book file. Requires download permission or admin.")
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.booklore.controller;
|
||||
import org.booklore.service.book.BookService;
|
||||
import org.booklore.service.bookdrop.BookDropService;
|
||||
import org.booklore.service.reader.CbxReaderService;
|
||||
import org.booklore.service.reader.PdfReaderService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
@@ -25,7 +24,6 @@ import java.io.IOException;
|
||||
public class BookMediaController {
|
||||
|
||||
private final BookService bookService;
|
||||
private final PdfReaderService pdfReaderService;
|
||||
private final CbxReaderService cbxReaderService;
|
||||
private final BookDropService bookDropService;
|
||||
|
||||
@@ -57,18 +55,6 @@ public class BookMediaController {
|
||||
return ResponseEntity.ok(bookService.getAudiobookCover(bookId));
|
||||
}
|
||||
|
||||
@Operation(summary = "Get PDF page as image", description = "Retrieve a specific page from a PDF book as an image.")
|
||||
@ApiResponse(responseCode = "200", description = "PDF page image returned successfully")
|
||||
@GetMapping("/book/{bookId}/pdf/pages/{pageNumber}")
|
||||
public void getPdfPage(
|
||||
@Parameter(description = "ID of the book") @PathVariable Long bookId,
|
||||
@Parameter(description = "Page number to retrieve") @PathVariable int pageNumber,
|
||||
@Parameter(description = "Optional book type for alternative format (e.g., PDF, CBX)") @RequestParam(required = false) String bookType,
|
||||
HttpServletResponse response) throws IOException {
|
||||
response.setContentType(MediaType.IMAGE_JPEG_VALUE);
|
||||
pdfReaderService.streamPageImage(bookId, bookType, pageNumber, response.getOutputStream());
|
||||
}
|
||||
|
||||
@Operation(summary = "Get CBX page as image", description = "Retrieve a specific page from a CBX book as an image.")
|
||||
@ApiResponse(responseCode = "200", description = "CBX page image returned successfully")
|
||||
@GetMapping("/book/{bookId}/cbx/pages/{pageNumber}")
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.booklore.controller;
|
||||
|
||||
import org.booklore.config.security.annotation.CheckBookAccess;
|
||||
import org.booklore.service.book.PdfAnnotationService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/api/v1/pdf-annotations")
|
||||
@Tag(name = "PDF Annotations", description = "Endpoints for managing PDF annotation data")
|
||||
public class PdfAnnotationController {
|
||||
|
||||
private final PdfAnnotationService pdfAnnotationService;
|
||||
|
||||
@Operation(summary = "Get PDF annotations for a book", description = "Retrieve serialized PDF annotations for a specific book.")
|
||||
@ApiResponse(responseCode = "200", description = "Annotations returned successfully")
|
||||
@ApiResponse(responseCode = "204", description = "No annotations found")
|
||||
@GetMapping("/book/{bookId}")
|
||||
@CheckBookAccess(bookIdParam = "bookId")
|
||||
public ResponseEntity<Map<String, String>> getAnnotations(
|
||||
@Parameter(description = "ID of the book") @PathVariable Long bookId) {
|
||||
return pdfAnnotationService.getAnnotations(bookId)
|
||||
.map(data -> ResponseEntity.ok(Map.of("data", data)))
|
||||
.orElse(ResponseEntity.noContent().build());
|
||||
}
|
||||
|
||||
@Operation(summary = "Save PDF annotations for a book", description = "Save or update serialized PDF annotations for a specific book.")
|
||||
@ApiResponse(responseCode = "204", description = "Annotations saved successfully")
|
||||
@PutMapping("/book/{bookId}")
|
||||
@CheckBookAccess(bookIdParam = "bookId")
|
||||
public ResponseEntity<Void> saveAnnotations(
|
||||
@Parameter(description = "ID of the book") @PathVariable Long bookId,
|
||||
@RequestBody Map<String, String> body) {
|
||||
pdfAnnotationService.saveAnnotations(bookId, body.get("data"));
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@Operation(summary = "Delete PDF annotations for a book", description = "Delete all PDF annotations for a specific book.")
|
||||
@ApiResponse(responseCode = "204", description = "Annotations deleted successfully")
|
||||
@DeleteMapping("/book/{bookId}")
|
||||
@CheckBookAccess(bookIdParam = "bookId")
|
||||
public ResponseEntity<Void> deleteAnnotations(
|
||||
@Parameter(description = "ID of the book") @PathVariable Long bookId) {
|
||||
pdfAnnotationService.deleteAnnotations(bookId);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.booklore.model.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.UpdateTimestamp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Table(name = "pdf_annotations")
|
||||
public class PdfAnnotationEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "user_id", nullable = false)
|
||||
private BookLoreUserEntity user;
|
||||
|
||||
@Column(name = "user_id", insertable = false, updatable = false)
|
||||
private Long userId;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "book_id", nullable = false)
|
||||
private BookEntity book;
|
||||
|
||||
@Column(name = "book_id", insertable = false, updatable = false)
|
||||
private Long bookId;
|
||||
|
||||
@Lob
|
||||
@Column(name = "data", nullable = false, columnDefinition = "LONGTEXT")
|
||||
private String data;
|
||||
|
||||
@jakarta.persistence.Version
|
||||
@Column(name = "version", nullable = false)
|
||||
private Long version;
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@UpdateTimestamp
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.booklore.repository;
|
||||
|
||||
import org.booklore.model.entity.PdfAnnotationEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface PdfAnnotationRepository extends JpaRepository<PdfAnnotationEntity, Long> {
|
||||
|
||||
Optional<PdfAnnotationEntity> findByBookIdAndUserId(Long bookId, Long userId);
|
||||
|
||||
void deleteByBookIdAndUserId(Long bookId, Long userId);
|
||||
}
|
||||
@@ -18,8 +18,10 @@ import org.booklore.repository.BookFileRepository;
|
||||
import org.booklore.service.metadata.sidecar.SidecarMetadataWriter;
|
||||
import org.booklore.service.monitoring.MonitoringRegistrationService;
|
||||
import org.booklore.service.progress.ReadingProgressService;
|
||||
import org.booklore.service.FileStreamingService;
|
||||
import org.booklore.util.FileService;
|
||||
import org.booklore.util.FileUtils;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -63,6 +65,7 @@ public class BookService {
|
||||
private final BookUpdateService bookUpdateService;
|
||||
private final EbookViewerPreferenceRepository ebookViewerPreferencesRepository;
|
||||
private final SidecarMetadataWriter sidecarMetadataWriter;
|
||||
private final FileStreamingService fileStreamingService;
|
||||
|
||||
|
||||
public List<Book> getBookDTOs(boolean includeDescription) {
|
||||
@@ -320,6 +323,36 @@ public class BookService {
|
||||
.body(resource);
|
||||
}
|
||||
|
||||
public void streamBookContent(long bookId, String bookType, HttpServletRequest request, HttpServletResponse response) throws IOException {
|
||||
BookEntity bookEntity = bookRepository.findById(bookId).orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(bookId));
|
||||
String filePath;
|
||||
if (bookType != null) {
|
||||
BookFileType requestedType = BookFileType.valueOf(bookType.toUpperCase());
|
||||
BookFileEntity bookFile = bookEntity.getBookFiles().stream()
|
||||
.filter(bf -> bf.getBookType() == requestedType)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> ApiError.FILE_NOT_FOUND.createException("No file of type " + bookType + " found for book"));
|
||||
filePath = bookFile.getFullFilePath().toString();
|
||||
} else {
|
||||
filePath = FileUtils.getBookFullPath(bookEntity);
|
||||
}
|
||||
|
||||
Path path = Paths.get(filePath);
|
||||
String fileName = path.getFileName().toString();
|
||||
String extension = fileName.contains(".") ? fileName.substring(fileName.lastIndexOf('.') + 1) : "";
|
||||
String contentType = switch (extension.toLowerCase()) {
|
||||
case "pdf" -> "application/pdf";
|
||||
case "epub" -> "application/epub+zip";
|
||||
case "mobi", "azw3" -> "application/x-mobipocket-ebook";
|
||||
case "cbz" -> "application/vnd.comicbook+zip";
|
||||
case "cbr" -> "application/vnd.comicbook-rar";
|
||||
case "fb2" -> "application/x-fictionbook+xml";
|
||||
default -> "application/octet-stream";
|
||||
};
|
||||
|
||||
fileStreamingService.streamWithRangeSupport(path, contentType, request, response);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ResponseEntity<BookDeletionResponse> deleteBooks(Set<Long> ids) {
|
||||
List<BookEntity> books = bookQueryService.findAllWithMetadataByIds(ids);
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package org.booklore.service.book;
|
||||
|
||||
import org.booklore.config.security.service.AuthenticationService;
|
||||
import org.booklore.model.entity.BookEntity;
|
||||
import org.booklore.model.entity.BookLoreUserEntity;
|
||||
import org.booklore.model.entity.PdfAnnotationEntity;
|
||||
import org.booklore.repository.BookRepository;
|
||||
import org.booklore.repository.PdfAnnotationRepository;
|
||||
import org.booklore.repository.UserRepository;
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class PdfAnnotationService {
|
||||
|
||||
private final PdfAnnotationRepository pdfAnnotationRepository;
|
||||
private final BookRepository bookRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final AuthenticationService authenticationService;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<String> getAnnotations(Long bookId) {
|
||||
Long userId = getCurrentUserId();
|
||||
return pdfAnnotationRepository.findByBookIdAndUserId(bookId, userId)
|
||||
.map(PdfAnnotationEntity::getData);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void saveAnnotations(Long bookId, String data) {
|
||||
Long userId = getCurrentUserId();
|
||||
Optional<PdfAnnotationEntity> existing = pdfAnnotationRepository.findByBookIdAndUserId(bookId, userId);
|
||||
|
||||
if (existing.isPresent()) {
|
||||
PdfAnnotationEntity entity = existing.get();
|
||||
entity.setData(data);
|
||||
pdfAnnotationRepository.save(entity);
|
||||
log.info("Updated PDF annotations for book {} by user {}", bookId, userId);
|
||||
} else {
|
||||
PdfAnnotationEntity entity = PdfAnnotationEntity.builder()
|
||||
.book(findBook(bookId))
|
||||
.user(findUser(userId))
|
||||
.data(data)
|
||||
.build();
|
||||
pdfAnnotationRepository.save(entity);
|
||||
log.info("Created PDF annotations for book {} by user {}", bookId, userId);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteAnnotations(Long bookId) {
|
||||
Long userId = getCurrentUserId();
|
||||
pdfAnnotationRepository.deleteByBookIdAndUserId(bookId, userId);
|
||||
log.info("Deleted PDF annotations for book {} by user {}", bookId, userId);
|
||||
}
|
||||
|
||||
private Long getCurrentUserId() {
|
||||
return authenticationService.getAuthenticatedUser().getId();
|
||||
}
|
||||
|
||||
private BookEntity findBook(Long bookId) {
|
||||
return bookRepository.findById(bookId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Book not found: " + bookId));
|
||||
}
|
||||
|
||||
private BookLoreUserEntity findUser(Long userId) {
|
||||
return userRepository.findById(userId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("User not found: " + userId));
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,5 @@
|
||||
package org.booklore.service.reader;
|
||||
|
||||
import org.booklore.exception.ApiError;
|
||||
import org.booklore.model.dto.response.PdfBookInfo;
|
||||
import org.booklore.model.dto.response.PdfOutlineItem;
|
||||
import org.booklore.model.entity.BookEntity;
|
||||
import org.booklore.model.entity.BookFileEntity;
|
||||
import org.booklore.model.enums.BookFileType;
|
||||
import org.booklore.repository.BookRepository;
|
||||
import org.booklore.util.FileUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.pdfbox.Loader;
|
||||
@@ -18,6 +10,14 @@ import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocume
|
||||
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem;
|
||||
import org.apache.pdfbox.rendering.ImageType;
|
||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import org.booklore.exception.ApiError;
|
||||
import org.booklore.model.dto.response.PdfBookInfo;
|
||||
import org.booklore.model.dto.response.PdfOutlineItem;
|
||||
import org.booklore.model.entity.BookEntity;
|
||||
import org.booklore.model.entity.BookFileEntity;
|
||||
import org.booklore.model.enums.BookFileType;
|
||||
import org.booklore.repository.BookRepository;
|
||||
import org.booklore.util.FileUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
@@ -27,7 +27,10 @@ import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
@@ -57,8 +60,8 @@ public class PdfReaderService {
|
||||
}
|
||||
}
|
||||
|
||||
public List<Integer> getAvailablePages(Long bookId) {
|
||||
return getAvailablePages(bookId, null);
|
||||
public void getAvailablePages(Long bookId) {
|
||||
getAvailablePages(bookId, null);
|
||||
}
|
||||
|
||||
public List<Integer> getAvailablePages(Long bookId, String bookType) {
|
||||
@@ -74,10 +77,6 @@ public class PdfReaderService {
|
||||
}
|
||||
}
|
||||
|
||||
public PdfBookInfo getBookInfo(Long bookId) {
|
||||
return getBookInfo(bookId, null);
|
||||
}
|
||||
|
||||
public PdfBookInfo getBookInfo(Long bookId, String bookType) {
|
||||
Path pdfPath = getBookPath(bookId, bookType);
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
CREATE TABLE IF NOT EXISTS pdf_annotations
|
||||
(
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL,
|
||||
book_id BIGINT NOT NULL,
|
||||
data LONGTEXT NOT NULL,
|
||||
version BIGINT NOT NULL DEFAULT 0,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
UNIQUE (user_id, book_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_pdf_annotations_user_id ON pdf_annotations (user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_pdf_annotations_book_id ON pdf_annotations (book_id);
|
||||
|
||||
ALTER TABLE pdf_annotations
|
||||
ADD CONSTRAINT fk_pdf_annotations_user
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE pdf_annotations
|
||||
ADD CONSTRAINT fk_pdf_annotations_book
|
||||
FOREIGN KEY (book_id) REFERENCES book (id) ON DELETE CASCADE;
|
||||
@@ -10,6 +10,7 @@ import org.booklore.service.book.BookUpdateService;
|
||||
import org.booklore.service.progress.ReadingProgressService;
|
||||
import org.booklore.service.metadata.sidecar.SidecarMetadataWriter;
|
||||
import org.booklore.service.monitoring.MonitoringRegistrationService;
|
||||
import org.booklore.service.FileStreamingService;
|
||||
import org.booklore.util.FileService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -53,6 +54,7 @@ class BookServiceDeleteTests {
|
||||
MonitoringRegistrationService monitoringRegistrationService = Mockito.mock(MonitoringRegistrationService.class);
|
||||
BookUpdateService bookUpdateService = Mockito.mock(BookUpdateService.class);
|
||||
SidecarMetadataWriter sidecarMetadataWriter = Mockito.mock(SidecarMetadataWriter.class);
|
||||
FileStreamingService fileStreamingService = Mockito.mock(FileStreamingService.class);
|
||||
|
||||
bookService = new BookService(
|
||||
bookRepository,
|
||||
@@ -70,7 +72,8 @@ class BookServiceDeleteTests {
|
||||
monitoringRegistrationService,
|
||||
bookUpdateService,
|
||||
ebookViewerPreferenceRepository,
|
||||
sidecarMetadataWriter
|
||||
sidecarMetadataWriter,
|
||||
fileStreamingService
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user