From d4121b241aeeccb0ed4128047104dc25e3995783 Mon Sep 17 00:00:00 2001 From: Muppetteer Date: Tue, 2 Dec 2025 11:27:50 +1100 Subject: [PATCH 001/110] Fix dev docker environment (#1713) --- dev.docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev.docker-compose.yml b/dev.docker-compose.yml index 631c49b9..9ce435b3 100644 --- a/dev.docker-compose.yml +++ b/dev.docker-compose.yml @@ -1,6 +1,6 @@ services: backend: - image: gradle:9-jdk25-alpine + image: gradle:8-jdk21-alpine command: sh -c "cd /booklore-api && ./gradlew bootRun" ports: - "${BACKEND_PORT:-8080}:8080" From b5460a5e90496f4202dde10c3233988f92150298 Mon Sep 17 00:00:00 2001 From: Aditya Chandel <8075870+adityachandelgit@users.noreply.github.com> Date: Mon, 1 Dec 2025 19:25:17 -0700 Subject: [PATCH 002/110] Fix: Date filter doesn't work very well (#1715) --- .../book-browser/book-filter/book-filter.component.ts | 4 ++-- .../components/book-browser/filter-label.helper.ts | 2 +- .../components/book-browser/filters/SidebarFilter.ts | 10 ++++------ .../metadata-viewer/metadata-viewer.component.ts | 2 +- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/booklore-ui/src/app/features/book/components/book-browser/book-filter/book-filter.component.ts b/booklore-ui/src/app/features/book/components/book-browser/book-filter/book-filter.component.ts index 581a024a..4ef9cffb 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/book-filter/book-filter.component.ts +++ b/booklore-ui/src/app/features/book/components/book-browser/book-filter/book-filter.component.ts @@ -89,11 +89,11 @@ function getRatingRangeFilters10(rating?: number): { id: string; name: string; s return idx ? [{id: idx.id, name: idx.label, sortIndex: idx.sortIndex}] : []; } -function extractPublishedYearFilter(book: Book): { id: number; name: string }[] { +function extractPublishedYearFilter(book: Book): { id: string; name: string }[] { const date = book.metadata?.publishedDate; if (!date) return []; const year = new Date(date).getFullYear(); - return [{id: year, name: year.toString()}]; + return [{id: year.toString(), name: year.toString()}]; } function getShelfStatusFilter(book: Book): { id: string; name: string }[] { diff --git a/booklore-ui/src/app/features/book/components/book-browser/filter-label.helper.ts b/booklore-ui/src/app/features/book/components/book-browser/filter-label.helper.ts index 5197b7eb..e0724ff4 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/filter-label.helper.ts +++ b/booklore-ui/src/app/features/book/components/book-browser/filter-label.helper.ts @@ -14,7 +14,7 @@ export class FilterLabelHelper { publisher: 'Publisher', readStatus: 'Read Status', personalRating: 'Personal Rating', - publishedYear: 'Year Published', + publishedDate: 'Year Published', matchScore: 'Metadata Match Score', language: 'Language', bookType: 'Book Type', diff --git a/booklore-ui/src/app/features/book/components/book-browser/filters/SidebarFilter.ts b/booklore-ui/src/app/features/book/components/book-browser/filters/SidebarFilter.ts index 78c870f7..6873bad4 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/filters/SidebarFilter.ts +++ b/booklore-ui/src/app/features/book/components/book-browser/filters/SidebarFilter.ts @@ -93,13 +93,11 @@ export class SideBarFilter implements BookFilter { return filterValues.some(range => isRatingInRange(book.metadata?.hardcoverRating, range)); case 'personalRating': return filterValues.some(range => isRatingInRange10(book.metadata?.personalRating, range)); - case 'publishedYear': - const bookYear = book.metadata?.publishedDate - ? new Date(book.metadata.publishedDate).getFullYear().toString() - : null; - return bookYear ? filterValues.includes(bookYear) : false; case 'publishedDate': - return filterValues.includes(new Date(book.metadata?.publishedDate || '').getFullYear()); + const bookYear = book.metadata?.publishedDate + ? new Date(book.metadata.publishedDate).getFullYear() + : null; + return bookYear ? filterValues.some(val => val == bookYear || val == bookYear.toString()) : false; case 'fileSize': return filterValues.some(range => isFileSizeInRange(book.fileSizeKb, range)); case 'shelfStatus': diff --git a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.ts b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.ts index 8d11c28c..20fef2b2 100644 --- a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.ts +++ b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.ts @@ -587,7 +587,7 @@ export class MetadataViewerComponent implements OnInit, OnChanges { goToPublishedYear(publishedDate: string): void { const year = this.extractYear(publishedDate); if (year) { - this.handleMetadataClick('publishedYear', year); + this.handleMetadataClick('publishedDate', year); } } From 4da832cd19665b36c2d5690fd8e3a7e06cb1c148 Mon Sep 17 00:00:00 2001 From: Aditya Chandel <8075870+adityachandelgit@users.noreply.github.com> Date: Mon, 1 Dec 2025 20:02:53 -0700 Subject: [PATCH 003/110] Fix: Missing close button from dialogue window (#1717) --- .../book-browser/book-card/book-card.component.scss | 4 ++++ .../book-browser/book-card/book-card.component.ts | 11 ++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/booklore-ui/src/app/features/book/components/book-browser/book-card/book-card.component.scss b/booklore-ui/src/app/features/book/components/book-browser/book-card/book-card.component.scss index 4cb437a4..665ffaac 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/book-card/book-card.component.scss +++ b/booklore-ui/src/app/features/book/components/book-browser/book-card/book-card.component.scss @@ -234,3 +234,7 @@ ::ng-deep .custom-button-padding .p-button { padding-block: 0.25rem !important; } + +::ng-deep .book-details-dialog .p-dialog-header { + padding: 1rem 1.25rem !important; +} diff --git a/booklore-ui/src/app/features/book/components/book-browser/book-card/book-card.component.ts b/booklore-ui/src/app/features/book/components/book-browser/book-card/book-card.component.ts index 5ea5831d..d6248a27 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/book-card/book-card.component.ts +++ b/booklore-ui/src/app/features/book/components/book-browser/book-card/book-card.component.ts @@ -495,11 +495,16 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { }); } else { this.dialogService.open(BookMetadataCenterComponent, { - width: '85%', + width: '90%', data: {bookId: book.id}, modal: true, - dismissableMask: true, - showHeader: false + dismissableMask: false, + showHeader: true, + closable: true, + closeOnEscape: true, + maximizable: true, + header: 'Book Details', + styleClass: 'book-details-dialog' }); } } From a179749c930b33925f51700ff22da1bc4cac086a Mon Sep 17 00:00:00 2001 From: beedaddy Date: Tue, 2 Dec 2025 16:07:09 +0100 Subject: [PATCH 004/110] Re-add "description" to epub metadata extraction (#1727) A former commit accidentially removed the description field from the metadata extraction. Fixes #1725 --- .../service/metadata/extractor/EpubMetadataExtractor.java | 1 + 1 file changed, 1 insertion(+) diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/extractor/EpubMetadataExtractor.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/extractor/EpubMetadataExtractor.java index 917e3750..4caef994 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/extractor/EpubMetadataExtractor.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/extractor/EpubMetadataExtractor.java @@ -169,6 +169,7 @@ public class EpubMetadataExtractor implements FileMetadataExtractor { } case "creator" -> authors.add(text); case "subject" -> categories.add(text); + case "description" -> builderMeta.description(text); case "publisher" -> builderMeta.publisher(text); case "language" -> builderMeta.language(text); case "identifier" -> { From d40a650df761014db8c8c383784a8caa36fede19 Mon Sep 17 00:00:00 2001 From: Sergio Visinoni Date: Tue, 2 Dec 2025 18:06:59 +0100 Subject: [PATCH 005/110] Fix race conditions between Bookdrop and Monitoring Service when importing multiple files (#1709) * fix(bookdrop): gate library re-registration during single-file moves - query MonitoringService to see if the library is actively watched before unregistering/re-registering it - lower log noise for these operations and monitoring registration - expose MonitoringService#isLibraryMonitored via MonitoringRegistrationService - add FileMoveServiceTest covering monitored vs unmonitored scenarios This fixes issues when importing multiple files via bookdrop. Refs: #1608 * fix(bookdrop): avoid unnecessary stacktrace * Use `deleteIfExists` to avoid trying to delete a missing file This happens regularly as the file has already been removed by other methods --- .../service/bookdrop/BookDropService.java | 2 +- .../booklore/service/file/FileMoveHelper.java | 1 + .../service/file/FileMoveService.java | 12 +- .../MonitoringRegistrationService.java | 5 + .../service/monitoring/MonitoringService.java | 4 + .../service/file/FileMoveServiceTest.java | 109 ++++++++++++++++++ 6 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 booklore-api/src/test/java/com/adityachandel/booklore/service/file/FileMoveServiceTest.java diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/bookdrop/BookDropService.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/bookdrop/BookDropService.java index 3b70a904..5c837db1 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/bookdrop/BookDropService.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/bookdrop/BookDropService.java @@ -507,7 +507,7 @@ public class BookDropService { private void cleanupTempFile(Path tempPath) { if (tempPath != null) { try { - Files.delete(tempPath); + Files.deleteIfExists(tempPath); } catch (Exception e) { log.warn("Failed to cleanup temp file: {}", tempPath, e); } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/file/FileMoveHelper.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/file/FileMoveHelper.java index 56b0dcaa..86846bf0 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/file/FileMoveHelper.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/file/FileMoveHelper.java @@ -74,6 +74,7 @@ public class FileMoveHelper { } public void registerLibraryPaths(Long libraryId, Path libraryRoot) { + log.debug("Registering library paths for library {} with root {}", libraryId, libraryRoot); monitoringRegistrationService.registerLibraryPaths(libraryId, libraryRoot); } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/file/FileMoveService.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/file/FileMoveService.java index 5160cd94..04f711d1 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/file/FileMoveService.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/file/FileMoveService.java @@ -122,8 +122,10 @@ public class FileMoveService { Long libraryId = bookEntity.getLibraryPath().getLibrary().getId(); Path libraryRoot = Paths.get(bookEntity.getLibraryPath().getPath()).toAbsolutePath().normalize(); + boolean isLibraryMonitoredWhenCalled = false; try { + isLibraryMonitoredWhenCalled = monitoringRegistrationService.isLibraryMonitored(libraryId); String pattern = fileMoveHelper.getFileNamingPattern(bookEntity.getLibraryPath().getLibrary()); Path currentFilePath = bookEntity.getFullFilePath(); Path expectedFilePath = fileMoveHelper.generateNewFilePath(bookEntity, bookEntity.getLibraryPath(), pattern); @@ -134,7 +136,10 @@ public class FileMoveService { log.info("File for book ID {} needs to be moved from {} to {} to match library pattern", bookEntity.getId(), currentFilePath, expectedFilePath); - fileMoveHelper.unregisterLibrary(libraryId); + if (isLibraryMonitoredWhenCalled) { + log.debug("Unregistering library {} before moving a single file", libraryId); + fileMoveHelper.unregisterLibrary(libraryId); + } fileMoveHelper.moveFile(currentFilePath, expectedFilePath); @@ -151,7 +156,10 @@ public class FileMoveService { } catch (Exception e) { log.error("Failed to move file for book ID {}: {}", bookEntity.getId(), e.getMessage(), e); } finally { - fileMoveHelper.registerLibraryPaths(libraryId, libraryRoot); + if (isLibraryMonitoredWhenCalled) { + log.debug("Registering library paths for library {} with root {}", libraryId, libraryRoot); + fileMoveHelper.registerLibraryPaths(libraryId, libraryRoot); + } } return FileMoveResult.builder().moved(false).build(); diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/monitoring/MonitoringRegistrationService.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/monitoring/MonitoringRegistrationService.java index 57c83e80..f4455ef0 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/monitoring/MonitoringRegistrationService.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/monitoring/MonitoringRegistrationService.java @@ -21,6 +21,10 @@ public class MonitoringRegistrationService { return monitoringService.isPathMonitored(path); } + public boolean isLibraryMonitored(Long libraryId) { + return monitoringService.isLibraryMonitored(libraryId); + } + public void unregisterSpecificPath(Path path) { monitoringService.unregisterPath(path); } @@ -42,6 +46,7 @@ public class MonitoringRegistrationService { return; } try { + log.debug("Registering library paths for libraryId {} at {}", libraryId, libraryRoot); monitoringService.registerPath(libraryRoot, libraryId); try (var stream = Files.walk(libraryRoot)) { stream.filter(Files::isDirectory) diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/monitoring/MonitoringService.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/monitoring/MonitoringService.java index 606e99bf..acde6d8f 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/monitoring/MonitoringService.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/monitoring/MonitoringService.java @@ -241,4 +241,8 @@ public class MonitoringService { public boolean isPathMonitored(Path path) { return monitoredPaths.contains(path.toAbsolutePath().normalize()); } + + public boolean isLibraryMonitored(Long libraryId) { + return libraryWatchStatusMap.getOrDefault(libraryId, false); + } } diff --git a/booklore-api/src/test/java/com/adityachandel/booklore/service/file/FileMoveServiceTest.java b/booklore-api/src/test/java/com/adityachandel/booklore/service/file/FileMoveServiceTest.java new file mode 100644 index 00000000..59a59300 --- /dev/null +++ b/booklore-api/src/test/java/com/adityachandel/booklore/service/file/FileMoveServiceTest.java @@ -0,0 +1,109 @@ +package com.adityachandel.booklore.service.file; + +import com.adityachandel.booklore.mapper.BookMapper; +import com.adityachandel.booklore.mapper.LibraryMapper; +import com.adityachandel.booklore.model.dto.FileMoveResult; +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.repository.BookRepository; +import com.adityachandel.booklore.repository.LibraryRepository; +import com.adityachandel.booklore.service.NotificationService; +import com.adityachandel.booklore.service.monitoring.MonitoringRegistrationService; +import jakarta.persistence.EntityManager; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class FileMoveServiceTest { + + @Mock + private BookRepository bookRepository; + @Mock + private LibraryRepository libraryRepository; + @Mock + private FileMoveHelper fileMoveHelper; + @Mock + private MonitoringRegistrationService monitoringRegistrationService; + @Mock + private LibraryMapper libraryMapper; + @Mock + private BookMapper bookMapper; + @Mock + private NotificationService notificationService; + @Mock + private EntityManager entityManager; + + @InjectMocks + private FileMoveService fileMoveService; + + private BookEntity bookEntity; + private Path expectedFilePath; + + @BeforeEach + void setUp() throws Exception { + LibraryEntity library = new LibraryEntity(); + library.setId(42L); + + LibraryPathEntity libraryPath = new LibraryPathEntity(); + libraryPath.setId(77L); + libraryPath.setPath("/library/root"); + libraryPath.setLibrary(library); + + bookEntity = new BookEntity(); + bookEntity.setId(999L); + bookEntity.setLibrary(library); + bookEntity.setLibraryPath(libraryPath); + bookEntity.setFileSubPath("SciFi"); + bookEntity.setFileName("Original.epub"); + + expectedFilePath = Paths.get(libraryPath.getPath(), bookEntity.getFileSubPath(), "Renamed.epub"); + + when(fileMoveHelper.getFileNamingPattern(library)).thenReturn("{title}"); + when(fileMoveHelper.generateNewFilePath(bookEntity, libraryPath, "{title}")).thenReturn(expectedFilePath); + when(fileMoveHelper.extractSubPath(expectedFilePath, libraryPath)).thenReturn(bookEntity.getFileSubPath()); + doNothing().when(fileMoveHelper).moveFile(any(Path.class), any(Path.class)); + doNothing().when(fileMoveHelper).deleteEmptyParentDirsUpToLibraryFolders(any(Path.class), anySet()); + } + + @Test + void moveSingleFile_whenLibraryMonitored_reRegistersLibraryPaths() throws Exception { + when(monitoringRegistrationService.isLibraryMonitored(42L)).thenReturn(true); + + FileMoveResult result = fileMoveService.moveSingleFile(bookEntity); + + assertTrue(result.isMoved()); + + verify(fileMoveHelper).unregisterLibrary(42L); + Path expectedRoot = Paths.get(bookEntity.getLibraryPath().getPath()).toAbsolutePath().normalize(); + verify(fileMoveHelper).registerLibraryPaths(42L, expectedRoot); + } + + @Test + void moveSingleFile_whenLibraryNotMonitored_skipsMonitoringCalls() throws Exception { + when(monitoringRegistrationService.isLibraryMonitored(42L)).thenReturn(false); + + FileMoveResult result = fileMoveService.moveSingleFile(bookEntity); + + assertTrue(result.isMoved()); + + verify(fileMoveHelper, never()).unregisterLibrary(anyLong()); + verify(fileMoveHelper, never()).registerLibraryPaths(anyLong(), any(Path.class)); + } +} From ff2654db1b68ec065b9ea54152d9926395df2a23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Sz=C3=BCcs?= <127139797+balazs-szucs@users.noreply.github.com> Date: Tue, 2 Dec 2025 18:13:55 +0100 Subject: [PATCH 006/110] feat(metadata): consider locked, but empty or null metadata valid (#1729) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Balázs Szücs --- .../metadata/MetadataMatchService.java | 56 ++++----- .../metadata/MetadataMatchServiceTest.java | 109 ++++++++++++++++++ 2 files changed, 138 insertions(+), 27 deletions(-) create mode 100644 booklore-api/src/test/java/com/adityachandel/booklore/service/metadata/MetadataMatchServiceTest.java diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/MetadataMatchService.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/MetadataMatchService.java index 5076c8d9..4174ce43 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/MetadataMatchService.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/MetadataMatchService.java @@ -41,39 +41,41 @@ public class MetadataMatchService { float score = 0f; - if (isPresent(metadata.getTitle())) score += weights.getTitle(); - if (isPresent(metadata.getSubtitle())) score += weights.getSubtitle(); - if (isPresent(metadata.getDescription())) score += weights.getDescription(); - if (hasContent(metadata.getAuthors())) score += weights.getAuthors(); - if (isPresent(metadata.getPublisher())) score += weights.getPublisher(); - if (metadata.getPublishedDate() != null) score += weights.getPublishedDate(); - if (isPresent(metadata.getSeriesName())) score += weights.getSeriesName(); - if (metadata.getSeriesNumber() != null && metadata.getSeriesNumber() > 0) score += weights.getSeriesNumber(); - if (metadata.getSeriesTotal() != null && metadata.getSeriesTotal() > 0) score += weights.getSeriesTotal(); - if (isPresent(metadata.getIsbn13())) score += weights.getIsbn13(); - if (isPresent(metadata.getIsbn10())) score += weights.getIsbn10(); - if (isPresent(metadata.getLanguage())) score += weights.getLanguage(); - if (metadata.getPageCount() != null && metadata.getPageCount() > 0) score += weights.getPageCount(); - if (hasContent(metadata.getCategories())) score += weights.getCategories(); - if (isPositive(metadata.getAmazonRating())) score += weights.getAmazonRating(); - if (isPositive(metadata.getAmazonReviewCount())) score += weights.getAmazonReviewCount(); - if (isPositive(metadata.getGoodreadsRating())) score += weights.getGoodreadsRating(); - if (isPositive(metadata.getGoodreadsReviewCount())) score += weights.getGoodreadsReviewCount(); - if (isPositive(metadata.getHardcoverRating())) score += weights.getHardcoverRating(); - if (isPositive(metadata.getHardcoverReviewCount())) score += weights.getHardcoverReviewCount(); + + + if (isPresent(metadata.getTitle(), metadata.getTitleLocked())) score += weights.getTitle(); + if (isPresent(metadata.getSubtitle(), metadata.getSubtitleLocked())) score += weights.getSubtitle(); + if (isPresent(metadata.getDescription(), metadata.getDescriptionLocked())) score += weights.getDescription(); + if (hasContent(metadata.getAuthors(), metadata.getAuthorsLocked())) score += weights.getAuthors(); + if (isPresent(metadata.getPublisher(), metadata.getPublisherLocked())) score += weights.getPublisher(); + if (metadata.getPublishedDate() != null || Boolean.TRUE.equals(metadata.getPublishedDateLocked())) score += weights.getPublishedDate(); + if (isPresent(metadata.getSeriesName(), metadata.getSeriesNameLocked())) score += weights.getSeriesName(); + if ((metadata.getSeriesNumber() != null && metadata.getSeriesNumber() > 0) || Boolean.TRUE.equals(metadata.getSeriesNumberLocked())) score += weights.getSeriesNumber(); + if ((metadata.getSeriesTotal() != null && metadata.getSeriesTotal() > 0) || Boolean.TRUE.equals(metadata.getSeriesTotalLocked())) score += weights.getSeriesTotal(); + if (isPresent(metadata.getIsbn13(), metadata.getIsbn13Locked())) score += weights.getIsbn13(); + if (isPresent(metadata.getIsbn10(), metadata.getIsbn10Locked())) score += weights.getIsbn10(); + if (isPresent(metadata.getLanguage(), metadata.getLanguageLocked())) score += weights.getLanguage(); + if ((metadata.getPageCount() != null && metadata.getPageCount() > 0) || Boolean.TRUE.equals(metadata.getPageCountLocked())) score += weights.getPageCount(); + if (hasContent(metadata.getCategories(), metadata.getCategoriesLocked())) score += weights.getCategories(); + if (isPositive(metadata.getAmazonRating(), metadata.getAmazonRatingLocked())) score += weights.getAmazonRating(); + if (isPositive(metadata.getAmazonReviewCount(), metadata.getAmazonReviewCountLocked())) score += weights.getAmazonReviewCount(); + if (isPositive(metadata.getGoodreadsRating(), metadata.getGoodreadsRatingLocked())) score += weights.getGoodreadsRating(); + if (isPositive(metadata.getGoodreadsReviewCount(), metadata.getGoodreadsReviewCountLocked())) score += weights.getGoodreadsReviewCount(); + if (isPositive(metadata.getHardcoverRating(), metadata.getHardcoverRatingLocked())) score += weights.getHardcoverRating(); + if (isPositive(metadata.getHardcoverReviewCount(), metadata.getHardcoverReviewCountLocked())) score += weights.getHardcoverReviewCount(); return (score / totalWeight) * 100f; } - private boolean isPresent(String value) { - return value != null && !value.isBlank(); + private boolean isPresent(String value, Boolean locked) { + return (value != null && !value.isBlank()) || Boolean.TRUE.equals(locked); } - private boolean hasContent(Iterable iterable) { - return iterable != null && iterable.iterator().hasNext(); + private boolean hasContent(Iterable iterable, Boolean locked) { + return (iterable != null && iterable.iterator().hasNext()) || Boolean.TRUE.equals(locked); } - private boolean isPositive(Number number) { - return number != null && number.doubleValue() > 0; + private boolean isPositive(Number number, Boolean locked) { + return (number != null && number.doubleValue() > 0) || Boolean.TRUE.equals(locked); } -} \ No newline at end of file +} diff --git a/booklore-api/src/test/java/com/adityachandel/booklore/service/metadata/MetadataMatchServiceTest.java b/booklore-api/src/test/java/com/adityachandel/booklore/service/metadata/MetadataMatchServiceTest.java new file mode 100644 index 00000000..246e484b --- /dev/null +++ b/booklore-api/src/test/java/com/adityachandel/booklore/service/metadata/MetadataMatchServiceTest.java @@ -0,0 +1,109 @@ + +package com.adityachandel.booklore.service.metadata; + +import com.adityachandel.booklore.model.dto.settings.AppSettings; +import com.adityachandel.booklore.model.dto.settings.MetadataMatchWeights; +import com.adityachandel.booklore.model.entity.BookEntity; +import com.adityachandel.booklore.model.entity.BookMetadataEntity; +import com.adityachandel.booklore.service.appsettings.AppSettingService; +import com.adityachandel.booklore.service.book.BookQueryService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class MetadataMatchServiceTest { + + @Mock + private AppSettingService appSettingsService; + + @Mock + private BookQueryService bookQueryService; + + @InjectMocks + private MetadataMatchService metadataMatchService; + + private MetadataMatchWeights weights; + + @BeforeEach + void setUp() { + weights = MetadataMatchWeights.builder() + .title(10) + .subtitle(5) + .description(5) + .authors(10) + .build(); // other fields default to 0 + + AppSettings appSettings = AppSettings.builder() + .metadataMatchWeights(weights) + .build(); + + when(appSettingsService.getAppSettings()).thenReturn(appSettings); + } + + @Test + void calculateMatchScore_shouldScoreOnlyPresentFields() { + BookMetadataEntity metadata = BookMetadataEntity.builder() + .title("Some Title") + .build(); + + BookEntity book = BookEntity.builder() + .metadata(metadata) + .build(); + + Float score = metadataMatchService.calculateMatchScore(book); + + assertEquals(10f / 30f * 100f, score, 0.01f); + } + + @Test + void calculateMatchScore_shouldScoreLockedEmptySubtitle() { + BookMetadataEntity metadata = BookMetadataEntity.builder() + .title("Some Title") + .subtitleLocked(true) // Empty but locked + .build(); + + BookEntity book = BookEntity.builder() + .metadata(metadata) + .build(); + + Float score = metadataMatchService.calculateMatchScore(book); + + assertEquals(50.0f, score, 0.01f); + } + + @Test + void calculateMatchScore_shouldScoreLockedNullSeriesNumber() { + weights = MetadataMatchWeights.builder() + .title(10) + .seriesNumber(5) + .build(); + + // Total 15 + + AppSettings appSettings = AppSettings.builder() + .metadataMatchWeights(weights) + .build(); + + when(appSettingsService.getAppSettings()).thenReturn(appSettings); + + BookMetadataEntity metadata = BookMetadataEntity.builder() + .title("Some Title") + .seriesNumberLocked(true) // Null but locked + .build(); + + BookEntity book = BookEntity.builder() + .metadata(metadata) + .build(); + + Float score = metadataMatchService.calculateMatchScore(book); + + assertEquals(100.0f, score, 0.01f); + } +} From 8e8d57af7312c968774af7a93445ed3d01f079a9 Mon Sep 17 00:00:00 2001 From: WorldTeacher <41587052+WorldTeacher@users.noreply.github.com> Date: Tue, 2 Dec 2025 19:20:40 +0100 Subject: [PATCH 007/110] opds sort by addedon Desc by default (#1691) Co-authored-by: WorldTeacher --- .../booklore/repository/BookOpdsRepository.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/repository/BookOpdsRepository.java b/booklore-api/src/main/java/com/adityachandel/booklore/repository/BookOpdsRepository.java index b680f334..fcfb2bca 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/repository/BookOpdsRepository.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/repository/BookOpdsRepository.java @@ -20,7 +20,7 @@ public interface BookOpdsRepository extends JpaRepository, Jpa // ALL BOOKS - Two Query Pattern // ============================================ - @Query("SELECT b.id FROM BookEntity b WHERE (b.deleted IS NULL OR b.deleted = false)") + @Query("SELECT b.id FROM BookEntity b WHERE (b.deleted IS NULL OR b.deleted = false) ORDER BY b.addedOn DESC") Page findBookIds(Pageable pageable); @EntityGraph(attributePaths = {"metadata", "additionalFiles", "shelves"}) @@ -40,7 +40,7 @@ public interface BookOpdsRepository extends JpaRepository, Jpa // BOOKS BY LIBRARY IDs - Two Query Pattern // ============================================ - @Query("SELECT b.id FROM BookEntity b WHERE b.library.id IN :libraryIds AND (b.deleted IS NULL OR b.deleted = false)") + @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") Page findBookIdsByLibraryIds(@Param("libraryIds") Collection libraryIds, Pageable pageable); @EntityGraph(attributePaths = {"metadata", "additionalFiles", "shelves"}) @@ -60,7 +60,7 @@ public interface BookOpdsRepository extends JpaRepository, Jpa // BOOKS BY SHELF ID - Two Query Pattern // ============================================ - @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)") + @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") Page findBookIdsByShelfId(@Param("shelfId") Long shelfId, Pageable pageable); @EntityGraph(attributePaths = {"metadata", "additionalFiles", "shelves"}) @@ -81,6 +81,7 @@ public interface BookOpdsRepository extends JpaRepository, Jpa OR LOWER(m.seriesName) LIKE LOWER(CONCAT('%', :text, '%')) OR LOWER(a.name) LIKE LOWER(CONCAT('%', :text, '%')) ) + ORDER BY b.addedOn DESC """) Page findBookIdsByMetadataSearch(@Param("text") String text, Pageable pageable); @@ -104,6 +105,7 @@ public interface BookOpdsRepository extends JpaRepository, Jpa OR LOWER(m.seriesName) LIKE LOWER(CONCAT('%', :text, '%')) OR LOWER(a.name) LIKE LOWER(CONCAT('%', :text, '%')) ) + ORDER BY b.addedOn DESC """) Page findBookIdsByMetadataSearchAndLibraryIds(@Param("text") String text, @Param("libraryIds") Collection libraryIds, Pageable pageable); @@ -120,4 +122,4 @@ public interface BookOpdsRepository extends JpaRepository, Jpa @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) List findRandomBookIdsByLibraryIds(@Param("libraryIds") Collection libraryIds); -} +} \ No newline at end of file From 6b0b07804451346f7d06e4e166628c12db093cc8 Mon Sep 17 00:00:00 2001 From: Aditya Chandel <8075870+adityachandelgit@users.noreply.github.com> Date: Tue, 2 Dec 2025 16:31:19 -0700 Subject: [PATCH 008/110] Align the layout to be consistent throughout all dialogs (#1737) --- .../book-card/book-card.component.ts | 4 +- .../book-searcher.component.scss | 1 - .../shelf-assigner.component.html | 8 + .../shelf-assigner.component.scss | 22 +- .../shelf-creator.component.html | 9 + .../shelf-creator.component.scss | 24 +- .../bookdrop-file-review.component.html | 90 ++- .../bookdrop-file-review.component.scss | 290 ++++++- .../library-creator.component.html | 273 ++++--- .../library-creator.component.scss | 720 +++++++++++------- .../library-creator.component.ts | 5 + .../component/magic-shelf-component.html | 134 ++-- .../component/magic-shelf-component.scss | 349 +++++++++ .../component/magic-shelf-component.ts | 8 +- .../metadata-viewer.component.ts | 2 + .../metadata-manager.component.scss | 2 +- .../kobo-sync-settings-component.html | 153 ++-- .../kobo-sync-settings-component.scss | 27 +- .../kobo-sync-settings-component.ts | 26 +- .../koreader-settings-component.html | 12 +- .../koreader-settings-component.ts | 15 +- .../settings/opds-settings/opds-settings.scss | 1 + .../settings/opds-settings/opds-settings.ts | 2 +- .../create-user-dialog.component.html | 364 ++++++--- .../create-user-dialog.component.scss | 331 ++++++++ .../create-user-dialog.component.ts | 4 + .../user-management.component.ts | 2 +- .../user-profile-dialog.component.html | 243 +++--- .../user-profile-dialog.component.scss | 250 ++++++ .../user-profile-dialog.component.ts | 44 +- .../book-uploader.component.html | 280 ++++--- .../book-uploader.component.scss | 435 +++++++++++ .../book-uploader/book-uploader.component.ts | 6 + .../directory-picker.component.html | 172 +++-- .../directory-picker.component.scss | 669 +++++++++------- .../directory-picker.component.ts | 2 - .../github-support-dialog.scss | 1 + .../services/dialog-launcher.service.ts | 11 +- 38 files changed, 3663 insertions(+), 1328 deletions(-) diff --git a/booklore-ui/src/app/features/book/components/book-browser/book-card/book-card.component.ts b/booklore-ui/src/app/features/book/components/book-browser/book-card/book-card.component.ts index d6248a27..81154853 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/book-card/book-card.component.ts +++ b/booklore-ui/src/app/features/book/components/book-browser/book-card/book-card.component.ts @@ -10,7 +10,6 @@ import {BookService} from '../../../service/book.service'; import {CheckboxChangeEvent, CheckboxModule} from 'primeng/checkbox'; import {FormsModule} from '@angular/forms'; import {MetadataRefreshType} from '../../../../metadata/model/request/metadata-refresh-type.enum'; -import {MetadataRefreshRequest} from '../../../../metadata/model/request/metadata-refresh-request.model'; import {UrlHelperService} from '../../../../../shared/service/url-helper.service'; import {NgClass} from '@angular/common'; import {UserService} from '../../../../settings/user-management/user.service'; @@ -27,7 +26,6 @@ import {ResetProgressTypes} from '../../../../../shared/constants/reset-progress import {ReadStatusHelper} from '../../../helpers/read-status.helper'; import {BookDialogHelperService} from '../BookDialogHelperService'; import {MetadataFetchOptionsComponent} from '../../../../metadata/component/metadata-options-dialog/metadata-fetch-options/metadata-fetch-options.component'; -import {TaskCreateRequest, TaskType} from '../../../../settings/task-management/task.service'; import {TaskHelperService} from '../../../../settings/task-management/task-helper.service'; @Component({ @@ -464,7 +462,9 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { private openShelfDialog(): void { this.dialogService.open(ShelfAssignerComponent, { header: `Update Book's Shelves`, + showHeader: false, modal: true, + dismissableMask: true, closable: true, contentStyle: {overflow: 'auto'}, baseZIndex: 10, diff --git a/booklore-ui/src/app/features/book/components/book-searcher/book-searcher.component.scss b/booklore-ui/src/app/features/book/components/book-searcher/book-searcher.component.scss index 143d78ad..152a79f6 100644 --- a/booklore-ui/src/app/features/book/components/book-searcher/book-searcher.component.scss +++ b/booklore-ui/src/app/features/book/components/book-searcher/book-searcher.component.scss @@ -217,7 +217,6 @@ gap: 0.375rem; font-size: 0.8rem; color: var(--primary-color); - background-color: rgba(var(--primary-color-rgb, 59, 130, 246), 0.1); padding: 0.075rem 0.125rem; border-radius: 4px; width: fit-content; diff --git a/booklore-ui/src/app/features/book/components/shelf-assigner/shelf-assigner.component.html b/booklore-ui/src/app/features/book/components/shelf-assigner/shelf-assigner.component.html index 6e4f783f..b28c6ba5 100644 --- a/booklore-ui/src/app/features/book/components/shelf-assigner/shelf-assigner.component.html +++ b/booklore-ui/src/app/features/book/components/shelf-assigner/shelf-assigner.component.html @@ -13,6 +13,14 @@ }

+ +
diff --git a/booklore-ui/src/app/features/book/components/shelf-assigner/shelf-assigner.component.scss b/booklore-ui/src/app/features/book/components/shelf-assigner/shelf-assigner.component.scss index bf9e1b4f..4c67134b 100644 --- a/booklore-ui/src/app/features/book/components/shelf-assigner/shelf-assigner.component.scss +++ b/booklore-ui/src/app/features/book/components/shelf-assigner/shelf-assigner.component.scss @@ -12,11 +12,12 @@ } .panel-header { + position: relative; display: flex; align-items: center; gap: 1rem; - padding: 1.25rem; - background: linear-gradient(135deg, rgba(var(--primary-color-rgb), 0.1) 0%, rgba(var(--primary-color-rgb), 0.05) 100%); + padding: 1.25rem 3.5rem 1.25rem 1.25rem; + border-radius: 10px 10px 0 0; border: 1px solid var(--border-color); border-bottom: none; @@ -30,7 +31,7 @@ height: 48px; background: var(--primary-color); border-radius: 10px; - box-shadow: 0 3px 8px rgba(var(--primary-color-rgb), 0.3); + .header-icon { font-size: 1.5rem; @@ -56,7 +57,7 @@ } @media (max-width: 480px) { - padding: 1rem; + padding: 1rem 3rem 1rem 1rem; .header-icon-wrapper { width: 40px; @@ -76,6 +77,16 @@ font-size: 0.8rem; } } + + ::ng-deep .close-button { + top: 0.5rem; + right: 0.5rem; + + .p-button { + width: 2rem; + height: 2rem; + } + } } } @@ -188,7 +199,6 @@ } &.selected { - background: rgba(var(--primary-color-rgb), 0.08); border-color: var(--primary-color); .shelf-icon-wrapper { @@ -332,7 +342,7 @@ justify-content: space-between; align-items: center; gap: 0.75rem; - padding: 1.25rem 1.5rem; + padding: 1rem 1.5rem; background: var(--card-background); border: 1px solid var(--border-color); border-top: none; diff --git a/booklore-ui/src/app/features/book/components/shelf-creator/shelf-creator.component.html b/booklore-ui/src/app/features/book/components/shelf-creator/shelf-creator.component.html index 7a58dc19..4b7e124f 100644 --- a/booklore-ui/src/app/features/book/components/shelf-creator/shelf-creator.component.html +++ b/booklore-ui/src/app/features/book/components/shelf-creator/shelf-creator.component.html @@ -7,6 +7,15 @@

Create New Shelf

Add a custom shelf to organize your books

+
diff --git a/booklore-ui/src/app/features/book/components/shelf-creator/shelf-creator.component.scss b/booklore-ui/src/app/features/book/components/shelf-creator/shelf-creator.component.scss index b27d5785..317c9dce 100644 --- a/booklore-ui/src/app/features/book/components/shelf-creator/shelf-creator.component.scss +++ b/booklore-ui/src/app/features/book/components/shelf-creator/shelf-creator.component.scss @@ -11,11 +11,11 @@ } .panel-header { + position: relative; display: flex; align-items: center; gap: 1rem; padding: 1.25rem; - background: linear-gradient(135deg, rgba(var(--primary-color-rgb), 0.1) 0%, rgba(var(--primary-color-rgb), 0.05) 100%); border-radius: 10px 10px 0 0; border: 1px solid var(--border-color); @@ -27,7 +27,7 @@ height: 48px; background: var(--primary-color); border-radius: 10px; - box-shadow: 0 3px 8px rgba(var(--primary-color-rgb), 0.3); + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.2); .header-icon { font-size: 1.5rem; @@ -52,6 +52,12 @@ } } + .close-button { + position: absolute; + top: 0.75rem; + right: 0.75rem; + } + @media (max-width: 480px) { padding: 1rem; @@ -73,6 +79,11 @@ font-size: 0.8rem; } } + + .close-button { + top: 0.5rem; + right: 0.5rem; + } } } @@ -176,7 +187,7 @@ border-color: var(--primary-color); color: var(--primary-color); transform: translateY(-1px); - box-shadow: 0 3px 8px rgba(var(--primary-color-rgb), 0.2); + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15); } i { @@ -237,7 +248,7 @@ height: 42px; background: var(--primary-color); border-radius: 8px; - box-shadow: 0 3px 8px rgba(var(--primary-color-rgb), 0.3); + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.2); i { font-size: 1.25rem; @@ -300,7 +311,7 @@ justify-content: space-between; align-items: center; gap: 1rem; - padding: 1.25rem 1.5rem; + padding: 1rem 1.5rem; background: var(--card-background); border: 1px solid var(--border-color); border-top: none; @@ -340,7 +351,7 @@ display: flex; align-items: center; gap: 0.375rem; - padding: 0.5rem 0.75rem; + padding: 0.25rem 0.5rem; font-weight: 500; border-radius: 6px; font-size: 0.875rem; @@ -381,4 +392,3 @@ } } } - diff --git a/booklore-ui/src/app/features/bookdrop/component/bookdrop-file-review/bookdrop-file-review.component.html b/booklore-ui/src/app/features/bookdrop/component/bookdrop-file-review/bookdrop-file-review.component.html index 261188a3..cfbe582f 100644 --- a/booklore-ui/src/app/features/bookdrop/component/bookdrop-file-review/bookdrop-file-review.component.html +++ b/booklore-ui/src/app/features/bookdrop/component/bookdrop-file-review/bookdrop-file-review.component.html @@ -1,25 +1,24 @@ -
-
-
-
-

+
+
+
+
+

Review Bookdrop Files

-

+

These files were uploaded to the - Bookdrop Folder. + Bookdrop Folder. Review their fetched metadata, assign a library and subpath, and finalize where they belong in your collection.

-
+
-
- - Loading Bookdrop files. Please wait... +
+
+ + Loading Bookdrop files. Please wait...
} @else { -
+
@if (saving) { -
- -
- +
+ +
+ Organizing and moving files to their designated libraries. Please wait...
@@ -56,28 +55,26 @@ } @if (bookdropFileUis.length !== 0) { -
+
-
- Library for All Files: +
+ Library for All Files: - Subpath for All Files: + Subpath for All Files: @@ -92,7 +89,7 @@
-
+
-
+
@if (bookdropFileUis.length === 0) { -
+
No bookdrop files to review.
} @else { @for (file of bookdropFileUis; track file) { -
-
+
+
@@ -160,7 +156,7 @@ [src]="file.metadataForm.get('thumbnailUrl')?.value" alt="Cover" title="Original cover" - class="w-6 h-8 rounded-sm object-cover cursor-pointer hover:scale-105 hover:shadow-md transition-transform duration-200" + class="cover-image" (click)="file.showDetails = !file.showDetails"/> } @@ -169,20 +165,20 @@ [src]="file.file.fetchedMetadata?.thumbnailUrl" alt="Fetched Cover" title="Fetched cover" - class="w-6 h-8 rounded-sm object-cover cursor-pointer hover:scale-105 hover:shadow-md transition-transform duration-200" + class="cover-image" (click)="file.showDetails = !file.showDetails"/> } -
+
{{ file.file.fileName }}
@@ -209,7 +205,7 @@ optionLabel="name" optionValue="id" placeholder="Select Subpath" - class="min-w-[8rem] max-w-[16rem]" + class="path-select" appendTo="body" [(ngModel)]="file.selectedPathId"> @@ -224,7 +220,7 @@ @if (file.showDetails) { -
+

- Configure sorting options for your library and shelves shown in the sidebar. + Configure sorting options for your library and shelves names shown in the left menu sidebar.

@@ -17,11 +17,6 @@

- Choose how books are sorted and displayed in the library sidebar. + Choose how library names are sorted and displayed in the sidebar.

@@ -43,11 +38,6 @@

- Choose how books are sorted and displayed in the shelf sidebar. + Choose how shelf names are sorted and displayed in the sidebar. +

+
+
+ +
+
+
+ + + +
+

+ Choose how magic shelf names are sorted and displayed in the sidebar.

diff --git a/booklore-ui/src/app/features/settings/view-preferences-parent/sidebar-sorting-preferences/sidebar-sorting-preferences.component.ts b/booklore-ui/src/app/features/settings/view-preferences-parent/sidebar-sorting-preferences/sidebar-sorting-preferences.component.ts index 189e91db..eae4952f 100644 --- a/booklore-ui/src/app/features/settings/view-preferences-parent/sidebar-sorting-preferences/sidebar-sorting-preferences.component.ts +++ b/booklore-ui/src/app/features/settings/view-preferences-parent/sidebar-sorting-preferences/sidebar-sorting-preferences.component.ts @@ -1,7 +1,6 @@ import {Component, inject, OnDestroy, OnInit} from '@angular/core'; import {Select} from 'primeng/select'; -import {Tooltip} from 'primeng/tooltip'; -import {SidebarLibrarySorting, SidebarShelfSorting, User, UserService, UserSettings, UserState} from '../../user-management/user.service'; +import {SidebarLibrarySorting, SidebarMagicShelfSorting, SidebarShelfSorting, User, UserService, UserSettings, UserState} from '../../user-management/user.service'; import {MessageService} from 'primeng/api'; import {Observable, Subject} from 'rxjs'; import {FormsModule} from '@angular/forms'; @@ -11,7 +10,6 @@ import {filter, takeUntil} from 'rxjs/operators'; selector: 'app-sidebar-sorting-preferences', imports: [ Select, - Tooltip, FormsModule ], templateUrl: './sidebar-sorting-preferences.component.html', @@ -28,6 +26,7 @@ export class SidebarSortingPreferencesComponent implements OnInit, OnDestroy { selectedLibrarySorting: SidebarLibrarySorting = {field: 'id', order: 'asc'}; selectedShelfSorting: SidebarShelfSorting = {field: 'id', order: 'asc'}; + selectedMagicShelfSorting: SidebarMagicShelfSorting = {field: 'id', order: 'asc'}; private readonly userService = inject(UserService); private readonly messageService = inject(MessageService); @@ -54,6 +53,7 @@ export class SidebarSortingPreferencesComponent implements OnInit, OnDestroy { private loadPreferences(settings: UserSettings): void { this.selectedLibrarySorting = settings.sidebarLibrarySorting; this.selectedShelfSorting = settings.sidebarShelfSorting; + this.selectedMagicShelfSorting = settings.sidebarMagicShelfSorting; } private updatePreference(path: string[], value: any): void { @@ -82,4 +82,8 @@ export class SidebarSortingPreferencesComponent implements OnInit, OnDestroy { onShelfSortingChange() { this.updatePreference(['sidebarShelfSorting'], this.selectedShelfSorting); } + + onMagicShelfSortingChange() { + this.updatePreference(['sidebarMagicShelfSorting'], this.selectedMagicShelfSorting); + } } diff --git a/booklore-ui/src/app/shared/layout/component/layout-menu/app.menu.component.ts b/booklore-ui/src/app/shared/layout/component/layout-menu/app.menu.component.ts index 0de40059..9162be03 100644 --- a/booklore-ui/src/app/shared/layout/component/layout-menu/app.menu.component.ts +++ b/booklore-ui/src/app/shared/layout/component/layout-menu/app.menu.component.ts @@ -42,6 +42,8 @@ export class AppMenuComponent implements OnInit { librarySortOrder: 'asc' | 'desc' = 'desc'; shelfSortField: 'name' | 'id' = 'name'; shelfSortOrder: 'asc' | 'desc' = 'asc'; + magicShelfSortField: 'name' | 'id' = 'name'; + magicShelfSortOrder: 'asc' | 'desc' = 'asc'; ngOnInit(): void { @@ -60,6 +62,10 @@ export class AppMenuComponent implements OnInit { this.shelfSortField = this.validateSortField(userState.user.userSettings.sidebarShelfSorting.field); this.shelfSortOrder = this.validateSortOrder(userState.user.userSettings.sidebarShelfSorting.order); } + if (userState.user?.userSettings.sidebarMagicShelfSorting) { + this.magicShelfSortField = this.validateSortField(userState.user.userSettings.sidebarMagicShelfSorting.field); + this.magicShelfSortOrder = this.validateSortOrder(userState.user.userSettings.sidebarMagicShelfSorting.order); + } this.initMenus(); }); @@ -113,7 +119,7 @@ export class AppMenuComponent implements OnInit { this.magicShelfMenu$ = this.magicShelfService.shelvesState$.pipe( map((state: MagicShelfState) => { const shelves = state.shelves ?? []; - const sortedShelves = this.sortArray(shelves, 'name', 'asc'); + const sortedShelves = this.sortArray(shelves, this.magicShelfSortField, this.magicShelfSortOrder); return [ { label: 'Magic Shelves', From 147c374d5177530d54318a44ea0bfeed6cca26be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Sz=C3=BCcs?= <127139797+balazs-szucs@users.noreply.github.com> Date: Fri, 5 Dec 2025 16:41:28 +0100 Subject: [PATCH 021/110] hotfix(auth): add missing EC algo for Authentik, manually increase timeout limit (#1747) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * hotfix(auth) enhance OIDC JWT validation error messaging and improve JWK source configuration Signed-off-by: Balázs Szücs * fix(auth): add RSA algorithm to JWS algorithm set for JWT processing Signed-off-by: Balázs Szücs --------- Signed-off-by: Balázs Szücs --- .../filter/DualJwtAuthenticationFilter.java | 2 +- .../service/DynamicOidcJwtProcessor.java | 35 ++++++++++++++----- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/config/security/filter/DualJwtAuthenticationFilter.java b/booklore-api/src/main/java/com/adityachandel/booklore/config/security/filter/DualJwtAuthenticationFilter.java index 13530ee1..ae6ddfd6 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/config/security/filter/DualJwtAuthenticationFilter.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/config/security/filter/DualJwtAuthenticationFilter.java @@ -141,7 +141,7 @@ public class DualJwtAuthenticationFilter extends OncePerRequestFilter { } catch (Exception e) { log.error("OIDC authentication failed", e); - throw ApiError.GENERIC_UNAUTHORIZED.createException("OIDC JWT validation failed"); + throw ApiError.GENERIC_UNAUTHORIZED.createException("OIDC JWT validation failed: " + e.getMessage()); } } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/config/security/service/DynamicOidcJwtProcessor.java b/booklore-api/src/main/java/com/adityachandel/booklore/config/security/service/DynamicOidcJwtProcessor.java index 3a21df71..694fdb7c 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/config/security/service/DynamicOidcJwtProcessor.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/config/security/service/DynamicOidcJwtProcessor.java @@ -3,19 +3,25 @@ package com.adityachandel.booklore.config.security.service; import com.adityachandel.booklore.model.dto.settings.OidcProviderDetails; import com.adityachandel.booklore.service.appsettings.AppSettingService; import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.jwk.source.DefaultJWKSetCache; import com.nimbusds.jose.jwk.source.JWKSource; -import com.nimbusds.jose.jwk.source.JWKSourceBuilder; +import com.nimbusds.jose.jwk.source.RemoteJWKSet; import com.nimbusds.jose.proc.JWSKeySelector; import com.nimbusds.jose.proc.JWSVerificationKeySelector; import com.nimbusds.jose.proc.SecurityContext; +import com.nimbusds.jose.util.DefaultResourceRetriever; import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; import com.nimbusds.jwt.proc.DefaultJWTProcessor; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.stereotype.Component; import java.net.URI; import java.time.Duration; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.TimeUnit; @Slf4j @Component @@ -42,19 +48,25 @@ public class DynamicOidcJwtProcessor { throw new IllegalStateException("OIDC issuer URI is not configured in app settings."); } - String discoveryUri = providerDetails.getIssuerUri() + "/.well-known/openid-configuration"; + String discoveryUri = providerDetails.getIssuerUri().replaceAll("/$", "") + "/.well-known/openid-configuration"; log.info("Fetching OIDC discovery document from {}", discoveryUri); URI jwksUri = fetchJwksUri(discoveryUri); Duration ttl = Duration.ofHours(6); Duration refresh = Duration.ofHours(1); - - JWKSource jwkSource = JWKSourceBuilder.create(jwksUri.toURL()) - .cache(ttl.toMillis(), refresh.toMillis()) - .build(); - JWSKeySelector keySelector = new JWSVerificationKeySelector<>(JWSAlgorithm.RS256, jwkSource); + DefaultResourceRetriever resourceRetriever = new DefaultResourceRetriever(10000, 10000); + DefaultJWKSetCache jwkSetCache = new DefaultJWKSetCache(ttl.toMillis(), refresh.toMillis(), TimeUnit.MILLISECONDS); + + JWKSource jwkSource = new RemoteJWKSet<>(jwksUri.toURL(), resourceRetriever, jwkSetCache); + + Set jwsAlgs = new HashSet<>(); + jwsAlgs.addAll(JWSAlgorithm.Family.RSA); + jwsAlgs.addAll(JWSAlgorithm.Family.EC); + jwsAlgs.addAll(JWSAlgorithm.Family.RSA); + + JWSKeySelector keySelector = new JWSVerificationKeySelector<>(jwsAlgs, jwkSource); ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor<>(); jwtProcessor.setJWSKeySelector(keySelector); @@ -62,7 +74,14 @@ public class DynamicOidcJwtProcessor { } private URI fetchJwksUri(String discoveryUri) throws Exception { - var restClient = org.springframework.web.client.RestClient.create(); + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setConnectTimeout(10000); + factory.setReadTimeout(10000); + + var restClient = org.springframework.web.client.RestClient.builder() + .requestFactory(factory) + .build(); + var discoveryDoc = restClient.get() .uri(discoveryUri) .retrieve() From 16fe1b632bad2e022454e0af1a658f6ff57f634b Mon Sep 17 00:00:00 2001 From: Muppetteer Date: Sat, 6 Dec 2025 03:44:04 +1100 Subject: [PATCH 022/110] Fix script error on clearing multi-select fields (#1756) --- booklore-ui/package-lock.json | 16 ++++++++-------- booklore-ui/package.json | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/booklore-ui/package-lock.json b/booklore-ui/package-lock.json index 568954c2..a09c0c33 100644 --- a/booklore-ui/package-lock.json +++ b/booklore-ui/package-lock.json @@ -18,7 +18,7 @@ "@angular/platform-browser-dynamic": "^20.3.5", "@angular/router": "^20.3.5", "@iharbeck/ngx-virtual-scroller": "^19.0.1", - "@primeng/themes": "^20.3.0", + "@primeng/themes": "^20.4.0", "@stomp/rx-stomp": "^2.3.0", "@stomp/stompjs": "^7.2.1", "@tweenjs/tween.js": "^25.0.0", @@ -33,7 +33,7 @@ "ngx-extended-pdf-viewer": "^25.6.1", "ngx-infinite-scroll": "^20.0.0", "primeicons": "^7.0.0", - "primeng": "^20.3.0", + "primeng": "^20.4.0", "quill": "^2.0.3", "rxjs": "^7.8.2", "showdown": "^2.1.0", @@ -5768,9 +5768,9 @@ } }, "node_modules/@primeng/themes": { - "version": "20.3.0", - "resolved": "https://registry.npmjs.org/@primeng/themes/-/themes-20.3.0.tgz", - "integrity": "sha512-mQhUajhT4sJQSMLa21ZlK4p8XhZcFhWUiSQN5v2HA7oErNMupdr4RDlqSejBOCRBc6Y5wBBcYCGuWNlX6VHvqQ==", + "version": "20.4.0", + "resolved": "https://registry.npmjs.org/@primeng/themes/-/themes-20.4.0.tgz", + "integrity": "sha512-bh1yIRbCDAo+OLhQ+bm8sgwlZFRphwlR3/GXOdshJVurm5/Up+CWzoRqsZw/Q2RSrq0x3rDNA2pOTIYpcwgXbA==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "@primeuix/styled": "^0.7.4", @@ -14147,9 +14147,9 @@ "license": "MIT" }, "node_modules/primeng": { - "version": "20.3.0", - "resolved": "https://registry.npmjs.org/primeng/-/primeng-20.3.0.tgz", - "integrity": "sha512-ljRRundjAfhR6PC75T3uDpg+v3IpBdbAyAlM0kMCHP5H0gtWUang68dd0ibO67/6CAKEcXVxwUnbqy2D6et4jA==", + "version": "20.4.0", + "resolved": "https://registry.npmjs.org/primeng/-/primeng-20.4.0.tgz", + "integrity": "sha512-vXUD1G4/uet4rDkPW8xx7yZWj7RmsmexEJ3+GhpQgsNaLtPFsTCVfQq8v4FQ4tIs7shoD0hz76d3jtjGWZ49QQ==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "@primeuix/styled": "^0.7.4", diff --git a/booklore-ui/package.json b/booklore-ui/package.json index d38a9230..420a6810 100644 --- a/booklore-ui/package.json +++ b/booklore-ui/package.json @@ -22,7 +22,7 @@ "@angular/platform-browser-dynamic": "^20.3.5", "@angular/router": "^20.3.5", "@iharbeck/ngx-virtual-scroller": "^19.0.1", - "@primeng/themes": "^20.3.0", + "@primeng/themes": "^20.4.0", "@stomp/rx-stomp": "^2.3.0", "@stomp/stompjs": "^7.2.1", "@tweenjs/tween.js": "^25.0.0", @@ -37,7 +37,7 @@ "ngx-extended-pdf-viewer": "^25.6.1", "ngx-infinite-scroll": "^20.0.0", "primeicons": "^7.0.0", - "primeng": "^20.3.0", + "primeng": "^20.4.0", "quill": "^2.0.3", "rxjs": "^7.8.2", "showdown": "^2.1.0", From 474e95c4f2b5fca3347e73c135747d88019cf435 Mon Sep 17 00:00:00 2001 From: Muppetteer Date: Sat, 6 Dec 2025 03:45:10 +1100 Subject: [PATCH 023/110] Hide empty filters from sidebar (#1759) --- .../book-filter/book-filter.component.html | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/booklore-ui/src/app/features/book/components/book-browser/book-filter/book-filter.component.html b/booklore-ui/src/app/features/book/components/book-browser/book-filter/book-filter.component.html index d21894d3..7d12ec30 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/book-filter/book-filter.component.html +++ b/booklore-ui/src/app/features/book/components/book-browser/book-filter/book-filter.component.html @@ -15,6 +15,8 @@
@for (filterType of filterTypes; track trackByFilterType(i, filterType); let i = $index) { + @if (filterStreams[filterType] | async; as filters) { + @if (filters.length > 0) { @@ -28,28 +30,28 @@ - @if (filterStreams[filterType] | async; as filters) { -
- @for (filter of filters; track trackByFilter(j, filter); let j = $index) { -
- {{ filter.value.name || filter.value }} - -
- } - @if (truncatedFilters[filterType]) { -
- Showing first 250 items -
- } -
- } +
+ @for (filter of filters; track trackByFilter(j, filter); let j = $index) { +
+ {{ filter.value.name || filter.value }} + +
+ } + @if (truncatedFilters[filterType]) { +
+ Showing first 500 items +
+ } +
+ } + } }
From 51fa54bed32d34b47c8936b2af9a1d003ec08110 Mon Sep 17 00:00:00 2001 From: Muppetteer Date: Sun, 7 Dec 2025 03:16:42 +1100 Subject: [PATCH 024/110] Improve bookdrop UI (#1768) * Improve bookdrop UI * Rename resetAll as it doesn't apply to all now * Fix display of copied metadata fields --- ...okdrop-file-metadata-picker.component.html | 238 ++++++++++-------- ...okdrop-file-metadata-picker.component.scss | 43 +++- ...bookdrop-file-metadata-picker.component.ts | 17 +- .../bookdrop-file-review.component.html | 115 ++++----- .../bookdrop-file-review.component.scss | 30 +-- .../bookdrop-file-review.component.ts | 43 +++- 6 files changed, 298 insertions(+), 188 deletions(-) diff --git a/booklore-ui/src/app/features/bookdrop/component/bookdrop-file-metadata-picker/bookdrop-file-metadata-picker.component.html b/booklore-ui/src/app/features/bookdrop/component/bookdrop-file-metadata-picker/bookdrop-file-metadata-picker.component.html index d5d49c1c..dc5bb36b 100644 --- a/booklore-ui/src/app/features/bookdrop/component/bookdrop-file-metadata-picker/bookdrop-file-metadata-picker.component.html +++ b/booklore-ui/src/app/features/bookdrop/component/bookdrop-file-metadata-picker/bookdrop-file-metadata-picker.component.html @@ -1,11 +1,13 @@ -@if (fetchedMetadata) { -
+@if (fetchedMetadata && fetchedMetadata.title) { +
-
-
-

Current Metadata

-
+
+ +
+

Current Metadata

+
-

Fetched Metadata

+

Fetched Metadata

-
+
-
+
- +
- +
- +
-
- -
+ - -
- - -
+
} @for (field of metadataDescription; track field) {
- +
- + - +
} @for (field of metadataFieldsBottom; track field) {
- +
-
-

Current Metadata:

- - @for (field of metadataFieldsTop; track field) { -
- -
- -
-
- } - - @for (field of metadataChips; track field) { -
- -
- -
-
- } - - @for (field of metadataDescription; track field) { -
- -
- -
-
- } - - @for (field of metadataFieldsBottom; track field) { -
- -
- -
-
- } -
- -
-
- Book Thumbnail - + +
+
+

+ + Unable to fetch new metadata for this file +

+
+
+
+
+ Book Thumbnail + +
+
+ +
+ @for (field of metadataFieldsTop; track field) { +
+ +
+ +
+
+ } + + @for (field of metadataChips; track field) { +
+ +
+ +
+
+ } + + @for (field of metadataDescription; track field) { +
+ +
+ +
+
+ } + + @for (field of metadataFieldsBottom; track field) { +
+ +
+ +
+
+ } +
+
} diff --git a/booklore-ui/src/app/features/bookdrop/component/bookdrop-file-metadata-picker/bookdrop-file-metadata-picker.component.scss b/booklore-ui/src/app/features/bookdrop/component/bookdrop-file-metadata-picker/bookdrop-file-metadata-picker.component.scss index 6ea5335c..accae36f 100644 --- a/booklore-ui/src/app/features/bookdrop/component/bookdrop-file-metadata-picker/bookdrop-file-metadata-picker.component.scss +++ b/booklore-ui/src/app/features/bookdrop/component/bookdrop-file-metadata-picker/bookdrop-file-metadata-picker.component.scss @@ -22,10 +22,49 @@ --p-button-outlined-primary-color: #E32636; } -.outlined-input-green { - border: 0.75px solid forestgreen !important; +input.outlined-input-green, +textarea.outlined-input-green, +::ng-deep p-autocomplete.outlined-input-green ul.p-autocomplete-input-multiple { + border: 1px solid forestgreen !important; } ::ng-deep .p-inputchips { width: 100% !important; } + +::ng-deep .p-autocomplete .p-chip .p-chip-label { + font-size: 12px; +} + +.metapicker { + box-sizing: border-box; +} + +.metaheader { + border-bottom: 1px solid var(--border-color); + padding: 1rem; + margin-bottom: 1rem; + justify-content: space-between; +} + +.metaheader .midbuttons { + white-space: nowrap; +} + +.metacontent { + padding: 0 1rem 1rem; +} + +.metadata-status { + &.copied { + color: rgb(34, 197, 94); + } + + &.no-metadata { + color: rgb(59, 130, 246); + } + + &.not-applied { + color: rgb(239, 68, 68); + } +} \ No newline at end of file diff --git a/booklore-ui/src/app/features/bookdrop/component/bookdrop-file-metadata-picker/bookdrop-file-metadata-picker.component.ts b/booklore-ui/src/app/features/bookdrop/component/bookdrop-file-metadata-picker/bookdrop-file-metadata-picker.component.ts index f9fc1a4c..84d2e02f 100644 --- a/booklore-ui/src/app/features/bookdrop/component/bookdrop-file-metadata-picker/bookdrop-file-metadata-picker.component.ts +++ b/booklore-ui/src/app/features/bookdrop/component/bookdrop-file-metadata-picker/bookdrop-file-metadata-picker.component.ts @@ -10,6 +10,7 @@ import {Textarea} from 'primeng/textarea'; import {AutoComplete} from 'primeng/autocomplete'; import {Image} from 'primeng/image'; import {LazyLoadImageModule} from 'ng-lazyload-image'; +import {ConfirmationService} from 'primeng/api'; @Component({ selector: 'app-bookdrop-file-metadata-picker-component', @@ -30,6 +31,8 @@ import {LazyLoadImageModule} from 'ng-lazyload-image'; }) export class BookdropFileMetadataPickerComponent { + private readonly confirmationService = inject(ConfirmationService); + @Input() fetchedMetadata!: BookMetadata; @Input() originalMetadata?: BookMetadata; @Input() metadataForm!: FormGroup; @@ -97,10 +100,10 @@ export class BookdropFileMetadataPickerComponent { }); } - copyAll() { + copyAll(includeCover: boolean = true): void { if (this.fetchedMetadata) { Object.keys(this.fetchedMetadata).forEach((field) => { - if (this.fetchedMetadata[field] && field !== 'thumbnailUrl') { + if (this.fetchedMetadata[field] && (includeCover || field !== 'thumbnailUrl')) { this.copyFetchedToCurrent(field); } }); @@ -164,6 +167,16 @@ export class BookdropFileMetadataPickerComponent { } } + confirmReset(): void { + this.confirmationService.confirm({ + message: 'Are you sure you want to reset all metadata changes made to this file?', + header: 'Reset Metadata Changes?', + icon: 'pi pi-exclamation-triangle', + acceptButtonStyleClass: 'p-button-danger', + accept: () => this.resetAll() + }); + } + resetAll() { if (this.originalMetadata) { this.metadataForm.patchValue({ diff --git a/booklore-ui/src/app/features/bookdrop/component/bookdrop-file-review/bookdrop-file-review.component.html b/booklore-ui/src/app/features/bookdrop/component/bookdrop-file-review/bookdrop-file-review.component.html index cfbe582f..882abe81 100644 --- a/booklore-ui/src/app/features/bookdrop/component/bookdrop-file-review/bookdrop-file-review.component.html +++ b/booklore-ui/src/app/features/bookdrop/component/bookdrop-file-review/bookdrop-file-review.component.html @@ -20,7 +20,7 @@
- -
- Library for All Files: - - - - Subpath for All Files: - - - - - -
-
+ + + +
+ +
+ + + + + - - -
} @@ -182,7 +163,7 @@ }" [pTooltip]="copiedFlags[file.file.id] ? 'Fetched metadata has been applied.' - : !file.file.fetchedMetadata + : (!file.file.fetchedMetadata || !file.file.fetchedMetadata.title) ? 'No fetched metadata available. Original metadata will be used.' : 'Fetched metadata hasn’t been applied yet. Open metadata picker to review.'" tooltipPosition="top"> @@ -242,7 +223,7 @@