mirror of
https://github.com/adityachandelgit/BookLore.git
synced 2026-01-08 04:09:50 -06:00
Merge pull request #1207 from booklore-app/develop
Merge develop into master for the release
This commit is contained in:
@@ -4,7 +4,11 @@ FROM node:22-alpine AS angular-build
|
||||
WORKDIR /angular-app
|
||||
|
||||
COPY ./booklore-ui/package.json ./booklore-ui/package-lock.json ./
|
||||
RUN npm install --force
|
||||
RUN npm config set registry http://registry.npmjs.org/ \
|
||||
&& npm config set fetch-retries 5 \
|
||||
&& npm config set fetch-retry-mintimeout 20000 \
|
||||
&& npm config set fetch-retry-maxtimeout 120000 \
|
||||
&& npm install --force
|
||||
COPY ./booklore-ui /angular-app/
|
||||
|
||||
RUN npm run build --configuration=production
|
||||
|
||||
@@ -3,9 +3,11 @@ package com.adityachandel.booklore.mapper;
|
||||
import com.adityachandel.booklore.model.dto.Shelf;
|
||||
import com.adityachandel.booklore.model.entity.ShelfEntity;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
|
||||
@Mapper(componentModel = "spring")
|
||||
public interface ShelfMapper {
|
||||
|
||||
@Mapping(source = "user.id", target = "userId")
|
||||
Shelf toShelf(ShelfEntity shelfEntity);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.adityachandel.booklore.mapper.v2;
|
||||
|
||||
import com.adityachandel.booklore.mapper.ShelfMapper;
|
||||
import com.adityachandel.booklore.model.dto.Book;
|
||||
import com.adityachandel.booklore.model.dto.BookMetadata;
|
||||
import com.adityachandel.booklore.model.dto.LibraryPath;
|
||||
@@ -11,7 +12,7 @@ import org.mapstruct.Named;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Mapper(componentModel = "spring")
|
||||
@Mapper(componentModel = "spring", uses = ShelfMapper.class)
|
||||
public interface BookMapperV2 {
|
||||
|
||||
@Mapping(source = "library.id", target = "libraryId")
|
||||
|
||||
@@ -12,7 +12,5 @@ import lombok.NoArgsConstructor;
|
||||
public class MetadataPersistenceSettings {
|
||||
private boolean saveToOriginalFile;
|
||||
private boolean convertCbrCb7ToCbz;
|
||||
private boolean backupMetadata;
|
||||
private boolean backupCover;
|
||||
private boolean moveFilesToLibraryPattern;
|
||||
}
|
||||
|
||||
@@ -142,6 +142,7 @@ public class BookService {
|
||||
UserBookProgressEntity userProgress = userBookProgressRepository.findByUserIdAndBookId(user.getId(), bookId).orElse(new UserBookProgressEntity());
|
||||
|
||||
Book book = bookMapper.toBook(bookEntity);
|
||||
book.setShelves(filterShelvesByUserId(book.getShelves(), user.getId()));
|
||||
book.setLastReadTime(userProgress.getLastReadTime());
|
||||
|
||||
if (bookEntity.getBookType() == BookFileType.PDF) {
|
||||
@@ -482,6 +483,7 @@ public class BookService {
|
||||
|
||||
return bookEntities.stream().map(bookEntity -> {
|
||||
Book book = bookMapper.toBook(bookEntity);
|
||||
book.setShelves(filterShelvesByUserId(book.getShelves(), user.getId()));
|
||||
book.setFilePath(FileUtils.getBookFullPath(bookEntity));
|
||||
enrichBookWithProgress(book, progressMap.get(bookEntity.getId()));
|
||||
return book;
|
||||
@@ -637,4 +639,11 @@ public class BookService {
|
||||
}
|
||||
}
|
||||
|
||||
private Set<Shelf> filterShelvesByUserId(Set<Shelf> shelves, Long userId) {
|
||||
if (shelves == null) return Collections.emptySet();
|
||||
return shelves.stream()
|
||||
.filter(shelf -> userId.equals(shelf.getUserId()))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -184,8 +184,6 @@ public class SettingPersistenceHelper {
|
||||
return MetadataPersistenceSettings.builder()
|
||||
.saveToOriginalFile(false)
|
||||
.convertCbrCb7ToCbz(false)
|
||||
.backupMetadata(false)
|
||||
.backupCover(false)
|
||||
.moveFilesToLibraryPattern(false)
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
package com.adityachandel.booklore.service.kobo;
|
||||
|
||||
import com.adityachandel.booklore.util.FileService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -17,13 +16,22 @@ import java.util.stream.Collectors;
|
||||
@Service
|
||||
public class KepubConversionService {
|
||||
|
||||
private static final String KEPUBIFY_BINARY_MACOS_ARM64 = "/bin/kepubify-darwin-arm64";
|
||||
private static final String KEPUBIFY_BINARY_LINUX_X64 = "/bin/kepubify-linux-64bit";
|
||||
@Autowired
|
||||
private FileService fileService;
|
||||
|
||||
private static final String KEPUBIFY_GITHUB_BASE_URL = "https://github.com/booklore-app/booklore-tools/raw/main/kepubify/";
|
||||
|
||||
private static final String BIN_DARWIN_ARM64 = "kepubify-darwin-arm64";
|
||||
private static final String BIN_DARWIN_X64 = "kepubify-darwin-64bit";
|
||||
private static final String BIN_LINUX_X64 = "kepubify-linux-64bit";
|
||||
private static final String BIN_LINUX_X86 = "kepubify-linux-32bit";
|
||||
private static final String BIN_LINUX_ARM = "kepubify-linux-arm";
|
||||
private static final String BIN_LINUX_ARM64 = "kepubify-linux-arm64";
|
||||
|
||||
public File convertEpubToKepub(File epubFile, File tempDir) throws IOException, InterruptedException {
|
||||
validateInputs(epubFile);
|
||||
|
||||
Path kepubifyBinary = setupKepubifyBinary(tempDir);
|
||||
Path kepubifyBinary = setupKepubifyBinary();
|
||||
File outputFile = executeKepubifyConversion(epubFile, tempDir, kepubifyBinary);
|
||||
|
||||
log.info("Successfully converted {} to {} (size: {} bytes)", epubFile.getName(), outputFile.getName(), outputFile.length());
|
||||
@@ -36,33 +44,58 @@ public class KepubConversionService {
|
||||
}
|
||||
}
|
||||
|
||||
private Path setupKepubifyBinary(File tempDir) throws IOException {
|
||||
Path tempKepubify = tempDir.toPath().resolve("kepubify");
|
||||
String resourcePath = getKepubifyResourcePath();
|
||||
|
||||
try (InputStream in = getClass().getResourceAsStream(resourcePath)) {
|
||||
if (in == null) {
|
||||
throw new IOException("Resource not found: " + resourcePath);
|
||||
}
|
||||
Files.copy(in, tempKepubify, StandardCopyOption.REPLACE_EXISTING);
|
||||
private Path setupKepubifyBinary() throws IOException {
|
||||
String binaryName = getKepubifyBinaryName();
|
||||
String toolsDirPath = fileService.getToolsKepubifyPath();
|
||||
Path toolsDir = Paths.get(toolsDirPath);
|
||||
if (!Files.exists(toolsDir)) {
|
||||
Files.createDirectories(toolsDir);
|
||||
}
|
||||
tempKepubify.toFile().setExecutable(true);
|
||||
return tempKepubify;
|
||||
Path binaryPath = toolsDir.resolve(binaryName);
|
||||
|
||||
if (!Files.exists(binaryPath)) {
|
||||
String downloadUrl = KEPUBIFY_GITHUB_BASE_URL + binaryName;
|
||||
log.info("Downloading kepubify binary '{}' from {}", binaryName, downloadUrl);
|
||||
try (InputStream in = java.net.URI.create(downloadUrl).toURL().openStream()) {
|
||||
Files.copy(in, binaryPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
if (!binaryPath.toFile().setExecutable(true)) {
|
||||
log.warn("Failed to set executable permission for '{}'", binaryPath.toAbsolutePath());
|
||||
}
|
||||
log.info("Downloaded kepubify binary to {}", binaryPath.toAbsolutePath());
|
||||
} else {
|
||||
if (!binaryPath.toFile().setExecutable(true)) {
|
||||
log.warn("Failed to set executable permission for '{}'", binaryPath.toAbsolutePath());
|
||||
}
|
||||
log.debug("Using existing kepubify binary at {}", binaryPath.toAbsolutePath());
|
||||
}
|
||||
return binaryPath;
|
||||
}
|
||||
|
||||
private String getKepubifyResourcePath() {
|
||||
private String getKepubifyBinaryName() {
|
||||
String osName = System.getProperty("os.name").toLowerCase();
|
||||
String osArch = System.getProperty("os.arch").toLowerCase();
|
||||
|
||||
log.debug("Detected OS: {} ({})", osName, osArch);
|
||||
|
||||
if (osName.contains("mac") || osName.contains("darwin")) {
|
||||
return KEPUBIFY_BINARY_MACOS_ARM64;
|
||||
if (osArch.contains("arm") || osArch.contains("aarch64")) {
|
||||
return BIN_DARWIN_ARM64;
|
||||
} else {
|
||||
return BIN_DARWIN_X64;
|
||||
}
|
||||
} else if (osName.contains("linux")) {
|
||||
return KEPUBIFY_BINARY_LINUX_X64;
|
||||
} else {
|
||||
throw new IllegalStateException("Unsupported operating system: " + osName);
|
||||
if (osArch.contains("arm64") || osArch.contains("aarch64")) {
|
||||
return BIN_LINUX_ARM64;
|
||||
} else if (osArch.contains("arm")) {
|
||||
return BIN_LINUX_ARM;
|
||||
} else if (osArch.contains("64")) {
|
||||
return BIN_LINUX_X64;
|
||||
} else if (osArch.contains("86")) {
|
||||
return BIN_LINUX_X86;
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("Unsupported operating system or architecture: " + osName + " / " + osArch);
|
||||
}
|
||||
|
||||
private File executeKepubifyConversion(File epubFile, File tempDir, Path kepubifyBinary) throws IOException, InterruptedException {
|
||||
|
||||
@@ -91,6 +91,9 @@ public class KoboResourcesComponent {
|
||||
"image_host": "//cdn.kobo.com/book-images/",
|
||||
"image_url_quality_template": "https://cdn.kobo.com/book-images/{ImageId}/{Width}/{Height}/{Quality}/{IsGreyscale}/image.jpg",
|
||||
"image_url_template": "https://cdn.kobo.com/book-images/{ImageId}/{Width}/{Height}/false/image.jpg",
|
||||
"instapaper_enabled": "True",
|
||||
"instapaper_env_url": "https://www.instapaper.com/api/kobo",
|
||||
"instapaper_link_account_start": "https://authorize.kobo.com/{region}/{language}/linkinstapaper",
|
||||
"kobo_audiobooks_credit_redemption": "True",
|
||||
"kobo_audiobooks_enabled": "True",
|
||||
"kobo_audiobooks_orange_deal_enabled": "True",
|
||||
|
||||
@@ -77,22 +77,8 @@ public class BookMetadataUpdater {
|
||||
MetadataPersistenceSettings settings = appSettingService.getAppSettings().getMetadataPersistenceSettings();
|
||||
boolean writeToFile = settings.isSaveToOriginalFile();
|
||||
boolean convertCbrCb7ToCbz = settings.isConvertCbrCb7ToCbz();
|
||||
boolean backupEnabled = settings.isBackupMetadata();
|
||||
boolean backupCover = settings.isBackupCover();
|
||||
BookFileType bookType = bookEntity.getBookType();
|
||||
|
||||
if (writeToFile && backupEnabled && (bookType != BookFileType.CBX || convertCbrCb7ToCbz)) {
|
||||
try {
|
||||
MetadataBackupRestore service = metadataBackupRestoreFactory.getService(bookType);
|
||||
if (service != null) {
|
||||
boolean coverBackup = bookType == BookFileType.EPUB && backupCover;
|
||||
service.backupEmbeddedMetadataIfNotExists(bookEntity, coverBackup);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Metadata backup failed for book ID {}: {}", bookId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
updateBasicFields(newMetadata, metadata, clearFlags);
|
||||
updateAuthorsIfNeeded(newMetadata, metadata, clearFlags);
|
||||
updateCategoriesIfNeeded(newMetadata, metadata, clearFlags, mergeCategories);
|
||||
|
||||
@@ -111,6 +111,10 @@ public class FileService {
|
||||
return Paths.get(appProperties.getPathConfig(), "bookdrop_temp", bookdropFileId + ".jpg").toString();
|
||||
}
|
||||
|
||||
public String getToolsKepubifyPath() {
|
||||
return Paths.get(appProperties.getPathConfig(), "tools", "kepubify").toString();
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// VALIDATION
|
||||
// ========================================
|
||||
|
||||
@@ -36,6 +36,8 @@ public class PathPatternResolver {
|
||||
? metadata.getTitle()
|
||||
: "Untitled");
|
||||
|
||||
String subtitle = sanitize(metadata != null ? metadata.getSubtitle() : "");
|
||||
|
||||
String authors = sanitize(
|
||||
metadata != null
|
||||
? String.join(", ", metadata.getAuthors())
|
||||
@@ -70,6 +72,7 @@ public class PathPatternResolver {
|
||||
Map<String, String> values = new LinkedHashMap<>();
|
||||
values.put("authors", authors);
|
||||
values.put("title", title);
|
||||
values.put("subtitle", subtitle);
|
||||
values.put("year", year);
|
||||
values.put("series", series);
|
||||
values.put("seriesIndex", seriesIndex);
|
||||
@@ -169,6 +172,8 @@ public class PathPatternResolver {
|
||||
private interface MetadataProvider {
|
||||
String getTitle();
|
||||
|
||||
String getSubtitle();
|
||||
|
||||
List<String> getAuthors();
|
||||
|
||||
Integer getYear();
|
||||
@@ -211,6 +216,11 @@ public class PathPatternResolver {
|
||||
return metadata.getTitle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSubtitle() {
|
||||
return metadata.getSubtitle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getAuthors() {
|
||||
return metadata.getAuthors() != null ? metadata.getAuthors().stream().toList() : Collections.emptyList();
|
||||
@@ -264,6 +274,11 @@ public class PathPatternResolver {
|
||||
return metadata.getTitle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSubtitle() {
|
||||
return metadata.getSubtitle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getAuthors() {
|
||||
return metadata.getAuthors() != null
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -17,7 +17,7 @@ import static org.mockito.Mockito.when;
|
||||
|
||||
class PathPatternResolverTest {
|
||||
|
||||
private BookEntity createBook(String title, List<String> authors, LocalDate date,
|
||||
private BookEntity createBook(String title, String subtitle, List<String> authors, LocalDate date,
|
||||
String series, Float seriesNum, String lang,
|
||||
String publisher, String isbn13, String isbn10,
|
||||
String fileName) {
|
||||
@@ -28,6 +28,7 @@ class PathPatternResolverTest {
|
||||
when(book.getFileName()).thenReturn(fileName);
|
||||
|
||||
when(metadata.getTitle()).thenReturn(title);
|
||||
when(metadata.getSubtitle()).thenReturn(subtitle);
|
||||
|
||||
if (authors == null) {
|
||||
when(metadata.getAuthors()).thenReturn(null);
|
||||
@@ -51,6 +52,14 @@ class PathPatternResolverTest {
|
||||
return book;
|
||||
}
|
||||
|
||||
// Helper method for backward compatibility
|
||||
private BookEntity createBook(String title, List<String> authors, LocalDate date,
|
||||
String series, Float seriesNum, String lang,
|
||||
String publisher, String isbn13, String isbn10,
|
||||
String fileName) {
|
||||
return createBook(title, null, authors, date, series, seriesNum, lang, publisher, isbn13, isbn10, fileName);
|
||||
}
|
||||
|
||||
@Test void emptyPattern_returnsOnlyExtension() {
|
||||
var book = createBook("Title", List.of("Author"), LocalDate.now(), null, null, null, null, null, null, "file.pdf");
|
||||
assertThat(PathPatternResolver.resolvePattern(book, "")).isEqualTo("file.pdf");
|
||||
@@ -210,4 +219,58 @@ class PathPatternResolverTest {
|
||||
String pattern = "{title}.{extension}";
|
||||
assertThat(PathPatternResolver.resolvePattern(book, pattern)).isEqualTo("X.mobi");
|
||||
}
|
||||
|
||||
@Test void subtitleInPattern_replacedCorrectly() {
|
||||
var book = createBook("Main Title", "The Subtitle", List.of("Author"), LocalDate.now(), null, null, null, null, null, null, "file.epub");
|
||||
assertThat(PathPatternResolver.resolvePattern(book, "{title} - {subtitle}")).isEqualTo("Main Title - The Subtitle.epub");
|
||||
}
|
||||
|
||||
@Test void subtitleEmpty_replacedWithEmpty() {
|
||||
var book = createBook("Title", "", List.of("Author"), LocalDate.now(), null, null, null, null, null, null, "file.epub");
|
||||
assertThat(PathPatternResolver.resolvePattern(book, "{title} - {subtitle}")).isEqualTo("Title - .epub");
|
||||
}
|
||||
|
||||
@Test void subtitleNull_replacedWithEmpty() {
|
||||
var book = createBook("Title", null, List.of("Author"), LocalDate.now(), null, null, null, null, null, null, "file.epub");
|
||||
assertThat(PathPatternResolver.resolvePattern(book, "{title} - {subtitle}")).isEqualTo("Title - .epub");
|
||||
}
|
||||
|
||||
@Test void subtitleInOptionalBlock_withValue_blockIncluded() {
|
||||
var book = createBook("Title", "Subtitle", List.of("Author"), LocalDate.now(), null, null, null, null, null, null, "file.epub");
|
||||
assertThat(PathPatternResolver.resolvePattern(book, "{title}< - {subtitle}>")).isEqualTo("Title - Subtitle.epub");
|
||||
}
|
||||
|
||||
@Test void subtitleInOptionalBlock_withoutValue_blockRemoved() {
|
||||
var book = createBook("Title", null, List.of("Author"), LocalDate.now(), null, null, null, null, null, null, "file.epub");
|
||||
assertThat(PathPatternResolver.resolvePattern(book, "{title}< - {subtitle}>")).isEqualTo("Title.epub");
|
||||
}
|
||||
|
||||
@Test void subtitleWithIllegalChars_sanitized() {
|
||||
var book = createBook("Title", "Sub:title<>|*?", List.of("Author"), LocalDate.now(), null, null, null, null, null, null, "file.epub");
|
||||
String result = PathPatternResolver.resolvePattern(book, "{title} - {subtitle}");
|
||||
assertThat(result).doesNotContain(":", "<", ">", "|", "*", "?")
|
||||
.contains("Title").contains("Subtitle");
|
||||
}
|
||||
|
||||
@Test void subtitleWithWhitespace_trimmedAndSanitized() {
|
||||
var book = createBook("Title", " Sub title ", List.of("Author"), LocalDate.now(), null, null, null, null, null, null, "file.epub");
|
||||
assertThat(PathPatternResolver.resolvePattern(book, "{title} - {subtitle}")).isEqualTo("Title - Sub title.epub");
|
||||
}
|
||||
|
||||
@Test void complexPatternWithSubtitle_allPlaceholdersPresent() {
|
||||
var book = createBook("Main Title", "The Great Subtitle", List.of("Author One"), LocalDate.of(2010, 5, 5),
|
||||
"Series", 1f, "English", "Publisher", "ISBN13", "ISBN10", "complex.epub");
|
||||
String pattern = "<{series}/>{title}< - {subtitle}> - {authors} - {year}";
|
||||
assertThat(PathPatternResolver.resolvePattern(book, pattern))
|
||||
.isEqualTo("Series/Main Title - The Great Subtitle - Author One - 2010.epub");
|
||||
}
|
||||
|
||||
@Test void optionalBlockWithTitleAndSubtitle_partialValues() {
|
||||
var book1 = createBook("Title", "Subtitle", List.of("Author"), LocalDate.now(), null, null, null, null, null, null, "file.epub");
|
||||
var book2 = createBook("Title", null, List.of("Author"), LocalDate.now(), null, null, null, null, null, null, "file.epub");
|
||||
String pattern = "<{title} - {subtitle}>";
|
||||
|
||||
assertThat(PathPatternResolver.resolvePattern(book1, pattern)).isEqualTo("Title - Subtitle.epub");
|
||||
assertThat(PathPatternResolver.resolvePattern(book2, pattern)).isEqualTo("file.epub");
|
||||
}
|
||||
}
|
||||
@@ -84,8 +84,6 @@ export interface MetadataPersistenceSettings {
|
||||
moveFilesToLibraryPattern: boolean;
|
||||
saveToOriginalFile: boolean;
|
||||
convertCbrCb7ToCbz: boolean;
|
||||
backupMetadata: boolean;
|
||||
backupCover: boolean;
|
||||
}
|
||||
|
||||
export interface ReviewProviderConfig {
|
||||
|
||||
@@ -451,17 +451,7 @@
|
||||
pTooltip="Automatically fetch metadata using default sources"
|
||||
tooltipPosition="top">
|
||||
</p-button>
|
||||
|
||||
@if (book.bookType === 'CBX') {
|
||||
<p-button label="Restore ComicInfo" icon="pi pi-info-circle" [outlined]="true" severity="info" (onClick)="restoreCbxMetadata()" pTooltip="Retrieve metadata from ComicInfo.xml file" tooltipPosition="top"></p-button>
|
||||
<p-divider layout="vertical"/>
|
||||
}
|
||||
|
||||
@if (book.bookType === 'PDF' || book.bookType === 'EPUB') {
|
||||
<p-button label="Restore" icon="pi pi-refresh" [outlined]="true" severity="danger" (onClick)="restoreMetadata()" pTooltip="Revert all changes to original metadata" tooltipPosition="top"></p-button>
|
||||
<p-divider layout="vertical"/>
|
||||
}
|
||||
|
||||
<p-divider layout="vertical"/>
|
||||
<p-button label="Unlock All" icon="pi pi-lock-open" [outlined]="true" severity="success" (onClick)="unlockAll()" pTooltip="Unlock all metadata fields for editing" tooltipPosition="top"></p-button>
|
||||
<p-button label="Lock All" icon="pi pi-lock" [outlined]="true" severity="warn" (onClick)="lockAll()" pTooltip="Lock all metadata fields to prevent changes" tooltipPosition="top"></p-button>
|
||||
<p-divider layout="vertical"/>
|
||||
@@ -491,28 +481,6 @@
|
||||
tooltipPosition="top">
|
||||
</p-button>
|
||||
|
||||
@if (book.bookType === 'CBX') {
|
||||
<p-button
|
||||
icon="pi pi-info-circle"
|
||||
[outlined]="true"
|
||||
severity="warn"
|
||||
(onClick)="restoreCbxMetadata()"
|
||||
pTooltip="Restore ComicInfo"
|
||||
tooltipPosition="top">
|
||||
</p-button>
|
||||
}
|
||||
|
||||
@if (book.bookType === 'PDF' || book.bookType === 'EPUB') {
|
||||
<p-button
|
||||
icon="pi pi-refresh"
|
||||
[outlined]="true"
|
||||
severity="danger"
|
||||
(onClick)="restoreMetadata()"
|
||||
pTooltip="Restore"
|
||||
tooltipPosition="top">
|
||||
</p-button>
|
||||
}
|
||||
|
||||
<p-button
|
||||
icon="pi pi-lock-open"
|
||||
[outlined]="true"
|
||||
|
||||
@@ -130,6 +130,7 @@
|
||||
<h4 class="subsection-title">Available Placeholders</h4>
|
||||
<ul>
|
||||
<li><code class="placeholder-code">{{ '{title}' }}</code> – Book title</li>
|
||||
<li><code class="placeholder-code">{{ '{subtitle}' }}</code> – Book subtitle</li>
|
||||
<li><code class="placeholder-code">{{ '{authors}' }}</code> – Author(s)</li>
|
||||
<li><code class="placeholder-code">{{ '{year}' }}</code> – Full year (e.g. 2025)</li>
|
||||
<li><code class="placeholder-code">{{ '{series}' }}</code> – Series name</li>
|
||||
@@ -180,6 +181,7 @@
|
||||
<h4 class="subsection-title">Examples with Full Metadata</h4>
|
||||
<div class="metadata-sample">
|
||||
<span>title: <code>Harry Potter and the Sorcerer's Stone</code></span>
|
||||
<span>subtitle: <code>The Boy Who Lived</code></span>
|
||||
<span>authors: <code>J.K. Rowling</code></span>
|
||||
<span>series: <code>Harry Potter</code></span>
|
||||
<span>seriesIndex: <code>01</code></span>
|
||||
@@ -217,6 +219,11 @@
|
||||
<p class="example-pattern"><strong>Reuse original filename in path:</strong> <code>{{ '{authors}/{series}/{currentFilename}' }}</code></p>
|
||||
<p class="example-output"><strong>Output:</strong> <code>J.K. Rowling/Harry Potter/harry1_original.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div class="example-item">
|
||||
<p class="example-pattern"><strong>Title + Subtitle:</strong> <code>{{ '{title}: {subtitle}' }}</code></p>
|
||||
<p class="example-output"><strong>Output:</strong> <code>Harry Potter and the Sorcerer's Stone: The Boy Who Lived.epub</code></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -226,6 +233,7 @@
|
||||
<h4 class="subsection-title">Examples with Missing Optional Fields</h4>
|
||||
<div class="metadata-sample">
|
||||
<span>title: <code>Project Hail Mary</code></span>
|
||||
<span>subtitle: <code>(not provided)</code></span>
|
||||
<span>authors: <code>Andy Weir</code></span>
|
||||
<span>year: <code>2021</code></span>
|
||||
<span>series: <code>(not provided)</code></span>
|
||||
@@ -258,6 +266,11 @@
|
||||
<p class="example-pattern"><strong>Use original filename with year suffix:</strong> <code>{{ '{authors}/{year}__{currentFilename}' }}</code></p>
|
||||
<p class="example-output"><strong>Output:</strong> <code>Andy Weir/2021__project_hail_mary_final.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div class="example-item">
|
||||
<p class="example-pattern"><strong>Title + Subtitle fallback:</strong> <code>{{ '{title}<: {subtitle}>' }}</code></p>
|
||||
<p class="example-output"><strong>Output:</strong> <code>Project Hail Mary.epub</code></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,14 +20,15 @@ import {Divider} from 'primeng/divider';
|
||||
})
|
||||
export class FileNamingPatternComponent implements OnInit {
|
||||
readonly exampleMetadata: Record<string, string> = {
|
||||
title: 'The Fellowship of the Ring',
|
||||
authors: 'J.R.R. Tolkien',
|
||||
year: '1954',
|
||||
series: 'The Lord of the Rings',
|
||||
title: "Harry Potter and the Sorcerer's Stone",
|
||||
subtitle: 'The Boy Who Lived',
|
||||
authors: 'J.K. Rowling',
|
||||
year: '1997',
|
||||
series: 'Harry Potter',
|
||||
seriesIndex: '01',
|
||||
language: 'en',
|
||||
publisher: 'Allen & Unwin',
|
||||
isbn: '9780618574940',
|
||||
publisher: 'Bloomsbury',
|
||||
isbn: '9780747532699',
|
||||
};
|
||||
|
||||
defaultPattern = '';
|
||||
|
||||
@@ -47,40 +47,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item setting-item-indented">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label-row">
|
||||
<label class="setting-label">Backup Metadata</label>
|
||||
<p-toggleswitch
|
||||
[ngModel]="metadataPersistence.backupMetadata"
|
||||
(onChange)="onPersistenceToggle('backupMetadata')"
|
||||
[disabled]="!metadataPersistence.saveToOriginalFile">
|
||||
</p-toggleswitch>
|
||||
</div>
|
||||
<p class="setting-description">
|
||||
<i class="pi pi-info-circle"></i>
|
||||
Save a JSON copy of the current metadata before writing new data to the file.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item setting-item-indented">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label-row">
|
||||
<label class="setting-label">Backup Cover</label>
|
||||
<p-toggleswitch
|
||||
[ngModel]="metadataPersistence.backupCover"
|
||||
(onChange)="onPersistenceToggle('backupCover')"
|
||||
[disabled]="!metadataPersistence.saveToOriginalFile">
|
||||
</p-toggleswitch>
|
||||
</div>
|
||||
<p class="setting-description">
|
||||
<i class="pi pi-info-circle"></i>
|
||||
Save a copy of the existing embedded cover image before it is replaced.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label-row">
|
||||
|
||||
@@ -21,9 +21,7 @@ export class MetadataPersistenceSettingsComponent implements OnInit {
|
||||
metadataPersistence: MetadataPersistenceSettings = {
|
||||
saveToOriginalFile: false,
|
||||
convertCbrCb7ToCbz: false,
|
||||
moveFilesToLibraryPattern: false,
|
||||
backupMetadata: true,
|
||||
backupCover: true
|
||||
moveFilesToLibraryPattern: false
|
||||
};
|
||||
|
||||
private readonly appSettingsService = inject(AppSettingsService);
|
||||
@@ -65,8 +63,6 @@ export class MetadataPersistenceSettingsComponent implements OnInit {
|
||||
|
||||
if (!this.metadataPersistence.saveToOriginalFile) {
|
||||
this.metadataPersistence.convertCbrCb7ToCbz = false;
|
||||
this.metadataPersistence.backupMetadata = false;
|
||||
this.metadataPersistence.backupCover = false;
|
||||
}
|
||||
} else {
|
||||
this.metadataPersistence[key] = !this.metadataPersistence[key];
|
||||
|
||||
Reference in New Issue
Block a user