Skip to content

Commit 74fa38b

Browse files
authored
Merge pull request #2435 from booklore-app/develop
Merge develop into master for release
2 parents 27d96f6 + e03cca1 commit 74fa38b

File tree

111 files changed

+3633
-1884
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

111 files changed

+3633
-1884
lines changed

booklore-api/src/main/java/com/adityachandel/booklore/service/library/LibraryProcessingService.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
import com.adityachandel.booklore.exception.ApiError;
44
import com.adityachandel.booklore.model.dto.settings.LibraryFile;
5-
import com.adityachandel.booklore.model.entity.BookFileEntity;
65
import com.adityachandel.booklore.model.entity.BookEntity;
6+
import com.adityachandel.booklore.model.entity.BookFileEntity;
77
import com.adityachandel.booklore.model.entity.LibraryEntity;
88
import com.adityachandel.booklore.model.websocket.LogNotification;
99
import com.adityachandel.booklore.model.websocket.Topic;
@@ -22,7 +22,8 @@
2222
import java.io.UncheckedIOException;
2323
import java.nio.file.Files;
2424
import java.nio.file.Path;
25-
import java.util.*;
25+
import java.util.List;
26+
import java.util.Set;
2627
import java.util.stream.Collectors;
2728

2829
@AllArgsConstructor
@@ -115,13 +116,19 @@ protected static List<Long> detectDeletedBookIds(List<LibraryFile> libraryFiles,
115116

116117
return libraryEntity.getBookEntities().stream()
117118
.filter(book -> (book.getDeleted() == null || !book.getDeleted()))
118-
.filter(book -> !currentFullPaths.contains(book.getFullFilePath()))
119+
.filter(book -> {
120+
if (book.getBookFiles() == null || book.getBookFiles().isEmpty()) {
121+
return true;
122+
}
123+
return !currentFullPaths.contains(book.getFullFilePath());
124+
})
119125
.map(BookEntity::getId)
120126
.collect(Collectors.toList());
121127
}
122128

123129
protected List<LibraryFile> detectNewBookPaths(List<LibraryFile> libraryFiles, LibraryEntity libraryEntity) {
124130
Set<String> existingKeys = libraryEntity.getBookEntities().stream()
131+
.filter(book -> book.getBookFiles() != null && !book.getBookFiles().isEmpty())
125132
.map(this::generateUniqueKey)
126133
.collect(Collectors.toSet());
127134

booklore-api/src/main/java/com/adityachandel/booklore/service/opds/OpdsFeedService.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,13 @@ private String now() {
604604
return DateTimeFormatter.ISO_INSTANT.format(java.time.Instant.now());
605605
}
606606

607+
private boolean hasValidFilePath(Book book) {
608+
return book.getFileName() != null
609+
&& book.getLibraryPath() != null
610+
&& book.getLibraryPath().getPath() != null
611+
&& book.getFileSubPath() != null;
612+
}
613+
607614
private String fileMimeType(Book book) {
608615
if (book == null || book.getBookType() == null) {
609616
return "application/octet-stream";
@@ -612,7 +619,7 @@ private String fileMimeType(Book book) {
612619
case PDF -> "application/pdf";
613620
case EPUB -> "application/epub+zip";
614621
case FB2 -> {
615-
if (book.getFileName() != null) {
622+
if (hasValidFilePath(book)) {
616623
ArchiveUtils.ArchiveType type = ArchiveUtils.detectArchiveType(new File(FileUtils.getBookFullPath(book)));
617624
if (type == ArchiveUtils.ArchiveType.ZIP) {
618625
yield "application/zip";
@@ -632,7 +639,7 @@ yield switch (book.getArchiveType()) {
632639
};
633640
}
634641

635-
if (book.getFileName() != null) {
642+
if (hasValidFilePath(book)) {
636643
ArchiveUtils.ArchiveType type = ArchiveUtils.detectArchiveType(new File(FileUtils.getBookFullPath(book)));
637644
yield switch (type) {
638645
case RAR -> "application/vnd.comicbook-rar";
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package com.adityachandel.booklore.service.library;
2+
3+
import com.adityachandel.booklore.model.entity.BookEntity;
4+
import com.adityachandel.booklore.model.entity.LibraryEntity;
5+
import com.adityachandel.booklore.model.entity.LibraryPathEntity;
6+
import com.adityachandel.booklore.model.enums.LibraryScanMode;
7+
import com.adityachandel.booklore.repository.BookAdditionalFileRepository;
8+
import com.adityachandel.booklore.repository.LibraryRepository;
9+
import com.adityachandel.booklore.service.NotificationService;
10+
import com.adityachandel.booklore.task.options.RescanLibraryContext;
11+
import jakarta.persistence.EntityManager;
12+
import org.junit.jupiter.api.BeforeEach;
13+
import org.junit.jupiter.api.Test;
14+
import org.junit.jupiter.api.extension.ExtendWith;
15+
import org.junit.jupiter.api.io.TempDir;
16+
import org.mockito.Mock;
17+
import org.mockito.junit.jupiter.MockitoExtension;
18+
19+
import java.io.IOException;
20+
import java.nio.file.Files;
21+
import java.nio.file.Path;
22+
import java.util.Collections;
23+
import java.util.List;
24+
import java.util.Optional;
25+
26+
import static org.mockito.ArgumentMatchers.any;
27+
import static org.mockito.Mockito.*;
28+
29+
@ExtendWith(MockitoExtension.class)
30+
class LibraryProcessingServiceRegressionTest {
31+
32+
@Mock
33+
private LibraryRepository libraryRepository;
34+
@Mock
35+
private NotificationService notificationService;
36+
@Mock
37+
private BookAdditionalFileRepository bookAdditionalFileRepository;
38+
@Mock
39+
private LibraryFileProcessorRegistry fileProcessorRegistry;
40+
@Mock
41+
private BookRestorationService bookRestorationService;
42+
@Mock
43+
private BookDeletionService bookDeletionService;
44+
@Mock
45+
private LibraryFileHelper libraryFileHelper;
46+
@Mock
47+
private EntityManager entityManager;
48+
@Mock
49+
private LibraryFileProcessor libraryFileProcessor;
50+
51+
private LibraryProcessingService libraryProcessingService;
52+
53+
@BeforeEach
54+
void setUp() {
55+
libraryProcessingService = new LibraryProcessingService(
56+
libraryRepository,
57+
notificationService,
58+
bookAdditionalFileRepository,
59+
fileProcessorRegistry,
60+
bookRestorationService,
61+
bookDeletionService,
62+
libraryFileHelper,
63+
entityManager
64+
);
65+
}
66+
67+
@Test
68+
void rescanLibrary_shouldThrowException_whenBookHasNoFiles(@TempDir Path tempDir) throws IOException {
69+
long libraryId = 1L;
70+
Path accessiblePath = tempDir.resolve("accessible");
71+
Files.createDirectory(accessiblePath);
72+
73+
LibraryEntity libraryEntity = new LibraryEntity();
74+
libraryEntity.setId(libraryId);
75+
libraryEntity.setName("Test Library");
76+
libraryEntity.setScanMode(LibraryScanMode.FILE_AS_BOOK);
77+
78+
LibraryPathEntity pathEntity = new LibraryPathEntity();
79+
pathEntity.setId(10L);
80+
pathEntity.setPath(accessiblePath.toString());
81+
libraryEntity.setLibraryPaths(List.of(pathEntity));
82+
83+
BookEntity bookWithNoFiles = new BookEntity();
84+
bookWithNoFiles.setId(1L);
85+
bookWithNoFiles.setLibraryPath(pathEntity);
86+
bookWithNoFiles.setBookFiles(Collections.emptyList()); // Empty files list
87+
88+
libraryEntity.setBookEntities(List.of(bookWithNoFiles));
89+
90+
when(libraryRepository.findById(libraryId)).thenReturn(Optional.of(libraryEntity));
91+
when(fileProcessorRegistry.getProcessor(libraryEntity)).thenReturn(libraryFileProcessor);
92+
// We need at least one file so it doesn't think the library is offline
93+
when(libraryFileHelper.getLibraryFiles(libraryEntity, libraryFileProcessor)).thenReturn(List.of(
94+
com.adityachandel.booklore.model.dto.settings.LibraryFile.builder()
95+
.libraryPathEntity(pathEntity)
96+
.fileName("other.epub")
97+
.fileSubPath("")
98+
.build()
99+
));
100+
101+
RescanLibraryContext context = RescanLibraryContext.builder().libraryId(libraryId).build();
102+
103+
// Should not throw exception anymore
104+
libraryProcessingService.rescanLibrary(context);
105+
106+
// Verify that the book with no files (ID 1) was detected as deleted
107+
verify(bookDeletionService).processDeletedLibraryFiles(
108+
argThat(list -> list.contains(1L)),
109+
any()
110+
);
111+
}
112+
}

0 commit comments

Comments
 (0)