Skip to content

Commit 8a165a0

Browse files
authored
Merge pull request #1764 from booklore-app/develop
Merge develop into master for the release
2 parents 3f94d8a + 66220b8 commit 8a165a0

File tree

97 files changed

+7079
-1612
lines changed

Some content is hidden

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

97 files changed

+7079
-1612
lines changed

booklore-api/src/main/java/com/adityachandel/booklore/controller/MetadataController.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ public ResponseEntity<BookMetadata> updateMetadata(
7272
.updateThumbnail(true)
7373
.mergeCategories(mergeCategories)
7474
.replaceMode(MetadataReplaceMode.REPLACE_ALL)
75-
.mergeMoods(true)
76-
.mergeTags(true)
75+
.mergeMoods(false)
76+
.mergeTags(false)
7777
.build();
7878

7979
bookMetadataUpdater.setBookMetadata(context);

booklore-api/src/main/java/com/adityachandel/booklore/mapper/custom/BookLoreUserTransformer.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,17 @@ public BookLoreUser toDTO(BookLoreUserEntity userEntity) {
6161
case NEW_PDF_READER_SETTING -> userSettings.setNewPdfReaderSetting(objectMapper.readValue(value, BookLoreUser.UserSettings.NewPdfReaderSetting.class));
6262
case SIDEBAR_LIBRARY_SORTING -> userSettings.setSidebarLibrarySorting(objectMapper.readValue(value, SidebarSortOption.class));
6363
case SIDEBAR_SHELF_SORTING -> userSettings.setSidebarShelfSorting(objectMapper.readValue(value, SidebarSortOption.class));
64+
case SIDEBAR_MAGIC_SHELF_SORTING -> userSettings.setSidebarMagicShelfSorting(objectMapper.readValue(value, SidebarSortOption.class));
6465
case ENTITY_VIEW_PREFERENCES -> userSettings.setEntityViewPreferences(objectMapper.readValue(value, BookLoreUser.UserSettings.EntityViewPreferences.class));
6566
case TABLE_COLUMN_PREFERENCE -> userSettings.setTableColumnPreference(objectMapper.readValue(value, new TypeReference<>() {}));
6667
case DASHBOARD_CONFIG -> userSettings.setDashboardConfig(objectMapper.readValue(value, BookLoreUser.UserSettings.DashboardConfig.class));
6768
}
6869
} else {
6970
switch (settingKey) {
71+
case FILTER_MODE -> userSettings.setFilterMode(value);
7072
case FILTER_SORTING_MODE -> userSettings.setFilterSortingMode(value);
7173
case METADATA_CENTER_VIEW_MODE -> userSettings.setMetadataCenterViewMode(value);
74+
case ENABLE_SERIES_VIEW -> userSettings.setEnableSeriesView(Boolean.parseBoolean(value));
7275
}
7376
}
7477
} catch (IllegalArgumentException e) {

booklore-api/src/main/java/com/adityachandel/booklore/model/dto/BookLoreUser.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,14 @@ public static class UserSettings {
4747
public CbxReaderSetting cbxReaderSetting;
4848
public SidebarSortOption sidebarLibrarySorting;
4949
public SidebarSortOption sidebarShelfSorting;
50+
public SidebarSortOption sidebarMagicShelfSorting;
5051
public EntityViewPreferences entityViewPreferences;
5152
public List<TableColumnPreference> tableColumnPreference;
53+
public String filterMode;
5254
public String filterSortingMode;
5355
public String metadataCenterViewMode;
5456
public boolean koReaderEnabled;
57+
public boolean enableSeriesView;
5558
public DashboardConfig dashboardConfig;
5659

5760
@Data

booklore-api/src/main/java/com/adityachandel/booklore/model/dto/settings/UserSettingKey.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ public enum UserSettingKey {
1111
CBX_READER_SETTING("cbxReaderSetting", true),
1212
SIDEBAR_LIBRARY_SORTING("sidebarLibrarySorting", true),
1313
SIDEBAR_SHELF_SORTING("sidebarShelfSorting", true),
14+
SIDEBAR_MAGIC_SHELF_SORTING("sidebarMagicShelfSorting", true),
1415
ENTITY_VIEW_PREFERENCES("entityViewPreferences", true),
1516
TABLE_COLUMN_PREFERENCE("tableColumnPreference", true),
1617
DASHBOARD_CONFIG("dashboardConfig", true),
17-
18+
FILTER_MODE("filterMode", false),
1819
FILTER_SORTING_MODE("filterSortingMode", false),
19-
METADATA_CENTER_VIEW_MODE("metadataCenterViewMode", false);
20+
METADATA_CENTER_VIEW_MODE("metadataCenterViewMode", false),
21+
ENABLE_SERIES_VIEW("enableSeriesView", false);
2022

2123

2224
private final String dbKey;

booklore-api/src/main/java/com/adityachandel/booklore/repository/BookOpdsRepository.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public interface BookOpdsRepository extends JpaRepository<BookEntity, Long>, Jpa
2020
// ALL BOOKS - Two Query Pattern
2121
// ============================================
2222

23-
@Query("SELECT b.id FROM BookEntity b WHERE (b.deleted IS NULL OR b.deleted = false)")
23+
@Query("SELECT b.id FROM BookEntity b WHERE (b.deleted IS NULL OR b.deleted = false) ORDER BY b.addedOn DESC")
2424
Page<Long> findBookIds(Pageable pageable);
2525

2626
@EntityGraph(attributePaths = {"metadata", "additionalFiles", "shelves"})
@@ -40,7 +40,7 @@ public interface BookOpdsRepository extends JpaRepository<BookEntity, Long>, Jpa
4040
// BOOKS BY LIBRARY IDs - Two Query Pattern
4141
// ============================================
4242

43-
@Query("SELECT b.id FROM BookEntity b WHERE b.library.id IN :libraryIds AND (b.deleted IS NULL OR b.deleted = false)")
43+
@Query("SELECT b.id FROM BookEntity b WHERE b.library.id IN :libraryIds AND (b.deleted IS NULL OR b.deleted = false) ORDER BY b.addedOn DESC")
4444
Page<Long> findBookIdsByLibraryIds(@Param("libraryIds") Collection<Long> libraryIds, Pageable pageable);
4545

4646
@EntityGraph(attributePaths = {"metadata", "additionalFiles", "shelves"})
@@ -60,7 +60,7 @@ public interface BookOpdsRepository extends JpaRepository<BookEntity, Long>, Jpa
6060
// BOOKS BY SHELF ID - Two Query Pattern
6161
// ============================================
6262

63-
@Query("SELECT DISTINCT b.id FROM BookEntity b JOIN b.shelves s WHERE s.id = :shelfId AND (b.deleted IS NULL OR b.deleted = false)")
63+
@Query("SELECT DISTINCT b.id FROM BookEntity b JOIN b.shelves s WHERE s.id = :shelfId AND (b.deleted IS NULL OR b.deleted = false) ORDER BY b.addedOn DESC")
6464
Page<Long> findBookIdsByShelfId(@Param("shelfId") Long shelfId, Pageable pageable);
6565

6666
@EntityGraph(attributePaths = {"metadata", "additionalFiles", "shelves"})
@@ -81,6 +81,7 @@ OR LOWER(m.subtitle) LIKE LOWER(CONCAT('%', :text, '%'))
8181
OR LOWER(m.seriesName) LIKE LOWER(CONCAT('%', :text, '%'))
8282
OR LOWER(a.name) LIKE LOWER(CONCAT('%', :text, '%'))
8383
)
84+
ORDER BY b.addedOn DESC
8485
""")
8586
Page<Long> findBookIdsByMetadataSearch(@Param("text") String text, Pageable pageable);
8687

@@ -104,6 +105,7 @@ OR LOWER(m.subtitle) LIKE LOWER(CONCAT('%', :text, '%'))
104105
OR LOWER(m.seriesName) LIKE LOWER(CONCAT('%', :text, '%'))
105106
OR LOWER(a.name) LIKE LOWER(CONCAT('%', :text, '%'))
106107
)
108+
ORDER BY b.addedOn DESC
107109
""")
108110
Page<Long> findBookIdsByMetadataSearchAndLibraryIds(@Param("text") String text, @Param("libraryIds") Collection<Long> libraryIds, Pageable pageable);
109111

@@ -120,4 +122,4 @@ OR LOWER(a.name) LIKE LOWER(CONCAT('%', :text, '%'))
120122

121123
@Query(value = "SELECT b.id FROM BookEntity b WHERE b.library.id IN :libraryIds AND (b.deleted IS NULL OR b.deleted = false) ORDER BY function('RAND')", nativeQuery = false)
122124
List<Long> findRandomBookIdsByLibraryIds(@Param("libraryIds") Collection<Long> libraryIds);
123-
}
125+
}

booklore-api/src/main/java/com/adityachandel/booklore/service/bookdrop/BookDropService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,7 @@ private void cleanupFailedMove(Path target) {
507507
private void cleanupTempFile(Path tempPath) {
508508
if (tempPath != null) {
509509
try {
510-
Files.delete(tempPath);
510+
Files.deleteIfExists(tempPath);
511511
} catch (Exception e) {
512512
log.warn("Failed to cleanup temp file: {}", tempPath, e);
513513
}

booklore-api/src/main/java/com/adityachandel/booklore/service/file/FileMoveHelper.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ public void unregisterLibrary(Long libraryId) {
7474
}
7575

7676
public void registerLibraryPaths(Long libraryId, Path libraryRoot) {
77+
log.debug("Registering library paths for library {} with root {}", libraryId, libraryRoot);
7778
monitoringRegistrationService.registerLibraryPaths(libraryId, libraryRoot);
7879
}
7980

booklore-api/src/main/java/com/adityachandel/booklore/service/file/FileMoveService.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,10 @@ public FileMoveResult moveSingleFile(BookEntity bookEntity) {
122122

123123
Long libraryId = bookEntity.getLibraryPath().getLibrary().getId();
124124
Path libraryRoot = Paths.get(bookEntity.getLibraryPath().getPath()).toAbsolutePath().normalize();
125+
boolean isLibraryMonitoredWhenCalled = false;
125126

126127
try {
128+
isLibraryMonitoredWhenCalled = monitoringRegistrationService.isLibraryMonitored(libraryId);
127129
String pattern = fileMoveHelper.getFileNamingPattern(bookEntity.getLibraryPath().getLibrary());
128130
Path currentFilePath = bookEntity.getFullFilePath();
129131
Path expectedFilePath = fileMoveHelper.generateNewFilePath(bookEntity, bookEntity.getLibraryPath(), pattern);
@@ -134,7 +136,10 @@ public FileMoveResult moveSingleFile(BookEntity bookEntity) {
134136

135137
log.info("File for book ID {} needs to be moved from {} to {} to match library pattern", bookEntity.getId(), currentFilePath, expectedFilePath);
136138

137-
fileMoveHelper.unregisterLibrary(libraryId);
139+
if (isLibraryMonitoredWhenCalled) {
140+
log.debug("Unregistering library {} before moving a single file", libraryId);
141+
fileMoveHelper.unregisterLibrary(libraryId);
142+
}
138143

139144
fileMoveHelper.moveFile(currentFilePath, expectedFilePath);
140145

@@ -151,7 +156,10 @@ public FileMoveResult moveSingleFile(BookEntity bookEntity) {
151156
} catch (Exception e) {
152157
log.error("Failed to move file for book ID {}: {}", bookEntity.getId(), e.getMessage(), e);
153158
} finally {
154-
fileMoveHelper.registerLibraryPaths(libraryId, libraryRoot);
159+
if (isLibraryMonitoredWhenCalled) {
160+
log.debug("Registering library paths for library {} with root {}", libraryId, libraryRoot);
161+
fileMoveHelper.registerLibraryPaths(libraryId, libraryRoot);
162+
}
155163
}
156164

157165
return FileMoveResult.builder().moved(false).build();

booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/MetadataMatchService.java

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -41,39 +41,41 @@ public Float calculateMatchScore(BookEntity book) {
4141

4242
float score = 0f;
4343

44-
if (isPresent(metadata.getTitle())) score += weights.getTitle();
45-
if (isPresent(metadata.getSubtitle())) score += weights.getSubtitle();
46-
if (isPresent(metadata.getDescription())) score += weights.getDescription();
47-
if (hasContent(metadata.getAuthors())) score += weights.getAuthors();
48-
if (isPresent(metadata.getPublisher())) score += weights.getPublisher();
49-
if (metadata.getPublishedDate() != null) score += weights.getPublishedDate();
50-
if (isPresent(metadata.getSeriesName())) score += weights.getSeriesName();
51-
if (metadata.getSeriesNumber() != null && metadata.getSeriesNumber() > 0) score += weights.getSeriesNumber();
52-
if (metadata.getSeriesTotal() != null && metadata.getSeriesTotal() > 0) score += weights.getSeriesTotal();
53-
if (isPresent(metadata.getIsbn13())) score += weights.getIsbn13();
54-
if (isPresent(metadata.getIsbn10())) score += weights.getIsbn10();
55-
if (isPresent(metadata.getLanguage())) score += weights.getLanguage();
56-
if (metadata.getPageCount() != null && metadata.getPageCount() > 0) score += weights.getPageCount();
57-
if (hasContent(metadata.getCategories())) score += weights.getCategories();
58-
if (isPositive(metadata.getAmazonRating())) score += weights.getAmazonRating();
59-
if (isPositive(metadata.getAmazonReviewCount())) score += weights.getAmazonReviewCount();
60-
if (isPositive(metadata.getGoodreadsRating())) score += weights.getGoodreadsRating();
61-
if (isPositive(metadata.getGoodreadsReviewCount())) score += weights.getGoodreadsReviewCount();
62-
if (isPositive(metadata.getHardcoverRating())) score += weights.getHardcoverRating();
63-
if (isPositive(metadata.getHardcoverReviewCount())) score += weights.getHardcoverReviewCount();
44+
45+
46+
if (isPresent(metadata.getTitle(), metadata.getTitleLocked())) score += weights.getTitle();
47+
if (isPresent(metadata.getSubtitle(), metadata.getSubtitleLocked())) score += weights.getSubtitle();
48+
if (isPresent(metadata.getDescription(), metadata.getDescriptionLocked())) score += weights.getDescription();
49+
if (hasContent(metadata.getAuthors(), metadata.getAuthorsLocked())) score += weights.getAuthors();
50+
if (isPresent(metadata.getPublisher(), metadata.getPublisherLocked())) score += weights.getPublisher();
51+
if (metadata.getPublishedDate() != null || Boolean.TRUE.equals(metadata.getPublishedDateLocked())) score += weights.getPublishedDate();
52+
if (isPresent(metadata.getSeriesName(), metadata.getSeriesNameLocked())) score += weights.getSeriesName();
53+
if ((metadata.getSeriesNumber() != null && metadata.getSeriesNumber() > 0) || Boolean.TRUE.equals(metadata.getSeriesNumberLocked())) score += weights.getSeriesNumber();
54+
if ((metadata.getSeriesTotal() != null && metadata.getSeriesTotal() > 0) || Boolean.TRUE.equals(metadata.getSeriesTotalLocked())) score += weights.getSeriesTotal();
55+
if (isPresent(metadata.getIsbn13(), metadata.getIsbn13Locked())) score += weights.getIsbn13();
56+
if (isPresent(metadata.getIsbn10(), metadata.getIsbn10Locked())) score += weights.getIsbn10();
57+
if (isPresent(metadata.getLanguage(), metadata.getLanguageLocked())) score += weights.getLanguage();
58+
if ((metadata.getPageCount() != null && metadata.getPageCount() > 0) || Boolean.TRUE.equals(metadata.getPageCountLocked())) score += weights.getPageCount();
59+
if (hasContent(metadata.getCategories(), metadata.getCategoriesLocked())) score += weights.getCategories();
60+
if (isPositive(metadata.getAmazonRating(), metadata.getAmazonRatingLocked())) score += weights.getAmazonRating();
61+
if (isPositive(metadata.getAmazonReviewCount(), metadata.getAmazonReviewCountLocked())) score += weights.getAmazonReviewCount();
62+
if (isPositive(metadata.getGoodreadsRating(), metadata.getGoodreadsRatingLocked())) score += weights.getGoodreadsRating();
63+
if (isPositive(metadata.getGoodreadsReviewCount(), metadata.getGoodreadsReviewCountLocked())) score += weights.getGoodreadsReviewCount();
64+
if (isPositive(metadata.getHardcoverRating(), metadata.getHardcoverRatingLocked())) score += weights.getHardcoverRating();
65+
if (isPositive(metadata.getHardcoverReviewCount(), metadata.getHardcoverReviewCountLocked())) score += weights.getHardcoverReviewCount();
6466

6567
return (score / totalWeight) * 100f;
6668
}
6769

68-
private boolean isPresent(String value) {
69-
return value != null && !value.isBlank();
70+
private boolean isPresent(String value, Boolean locked) {
71+
return (value != null && !value.isBlank()) || Boolean.TRUE.equals(locked);
7072
}
7173

72-
private boolean hasContent(Iterable<?> iterable) {
73-
return iterable != null && iterable.iterator().hasNext();
74+
private boolean hasContent(Iterable<?> iterable, Boolean locked) {
75+
return (iterable != null && iterable.iterator().hasNext()) || Boolean.TRUE.equals(locked);
7476
}
7577

76-
private boolean isPositive(Number number) {
77-
return number != null && number.doubleValue() > 0;
78+
private boolean isPositive(Number number, Boolean locked) {
79+
return (number != null && number.doubleValue() > 0) || Boolean.TRUE.equals(locked);
7880
}
79-
}
81+
}

booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/extractor/EpubMetadataExtractor.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,14 @@
2727
import java.util.HashSet;
2828
import java.util.Map;
2929
import java.util.Set;
30+
import java.util.regex.Pattern;
3031

3132
@Slf4j
3233
@Component
3334
public class EpubMetadataExtractor implements FileMetadataExtractor {
3435

36+
private static final Pattern YEAR_ONLY_PATTERN = Pattern.compile("^\\d{4}$");
37+
3538
@Override
3639
public byte[] extractCover(File epubFile) {
3740
try (FileInputStream fis = new FileInputStream(epubFile)) {
@@ -169,6 +172,7 @@ public BookMetadata extractMetadata(File epubFile) {
169172
}
170173
case "creator" -> authors.add(text);
171174
case "subject" -> categories.add(text);
175+
case "description" -> builderMeta.description(text);
172176
case "publisher" -> builderMeta.publisher(text);
173177
case "language" -> builderMeta.language(text);
174178
case "identifier" -> {
@@ -262,6 +266,16 @@ private void safeParseDouble(String value, java.util.function.DoubleConsumer set
262266
private LocalDate parseDate(String value) {
263267
if (StringUtils.isBlank(value)) return null;
264268

269+
value = value.trim();
270+
271+
// Check for year-only format first (e.g., "2024") - common in EPUB metadata
272+
if (YEAR_ONLY_PATTERN.matcher(value).matches()) {
273+
int year = Integer.parseInt(value);
274+
if (year >= 1 && year <= 9999) {
275+
return LocalDate.of(year, 1, 1);
276+
}
277+
}
278+
265279
try {
266280
return LocalDate.parse(value);
267281
} catch (Exception ignored) {
@@ -272,9 +286,12 @@ private LocalDate parseDate(String value) {
272286
} catch (Exception ignored) {
273287
}
274288

275-
try {
276-
return LocalDate.parse(value.substring(0, 10));
277-
} catch (Exception ignored) {
289+
// Try parsing first 10 characters for ISO date format with extra content
290+
if (value.length() >= 10) {
291+
try {
292+
return LocalDate.parse(value.substring(0, 10));
293+
} catch (Exception ignored) {
294+
}
278295
}
279296

280297
log.warn("Failed to parse date from string: {}", value);

0 commit comments

Comments
 (0)