mirror of
https://github.com/adityachandelgit/BookLore.git
synced 2026-03-16 16:42:08 -05:00
Add per-library customizable file naming patterns
This commit is contained in:
committed by
Aditya Chandel
parent
1f83dfa85d
commit
978bd05b1e
+10
@@ -11,6 +11,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/libraries")
|
||||
@@ -72,4 +73,13 @@ public class LibraryController {
|
||||
libraryService.rescanLibrary(libraryId);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@PatchMapping("/{libraryId}/file-naming-pattern")
|
||||
@CheckLibraryAccess(libraryIdParam = "libraryId")
|
||||
@PreAuthorize("@securityUtil.canManipulateLibrary() or @securityUtil.isAdmin()")
|
||||
public ResponseEntity<Library> setFileNamingPattern(@PathVariable long libraryId, @RequestBody Map<String, String> body) {
|
||||
String pattern = body.get("fileNamingPattern");
|
||||
Library updated = libraryService.setFileNamingPattern(libraryId, pattern);
|
||||
return ResponseEntity.ok(updated);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ public class Library {
|
||||
private String name;
|
||||
private Sort sort;
|
||||
private String icon;
|
||||
private String fileNamingPattern;
|
||||
private boolean watch;
|
||||
private List<LibraryPath> paths;
|
||||
}
|
||||
|
||||
-1
@@ -7,7 +7,6 @@ import java.util.List;
|
||||
|
||||
@Data
|
||||
public class BookdropFinalizeRequest {
|
||||
private String uploadPattern;
|
||||
private Boolean selectAll;
|
||||
private List<Long> excludedIds;
|
||||
private List<BookdropFinalizeFile> files;
|
||||
|
||||
@@ -37,4 +37,7 @@ public class LibraryEntity {
|
||||
private boolean watch;
|
||||
|
||||
private String icon;
|
||||
|
||||
@Column(name = "file_naming_pattern")
|
||||
private String fileNamingPattern;
|
||||
}
|
||||
|
||||
+11
-11
@@ -22,6 +22,7 @@ import com.adityachandel.booklore.repository.BookRepository;
|
||||
import com.adityachandel.booklore.repository.BookdropFileRepository;
|
||||
import com.adityachandel.booklore.repository.LibraryRepository;
|
||||
import com.adityachandel.booklore.service.NotificationService;
|
||||
import com.adityachandel.booklore.service.appsettings.AppSettingService;
|
||||
import com.adityachandel.booklore.service.fileprocessor.BookFileProcessor;
|
||||
import com.adityachandel.booklore.service.fileprocessor.BookFileProcessorRegistry;
|
||||
import com.adityachandel.booklore.service.metadata.MetadataRefreshService;
|
||||
@@ -70,6 +71,7 @@ public class BookDropService {
|
||||
private final AppProperties appProperties;
|
||||
private final BookdropFileMapper mapper;
|
||||
private final ObjectMapper objectMapper;
|
||||
AppSettingService appSettingService;
|
||||
|
||||
public BookdropFileNotification getFileNotificationSummary() {
|
||||
long pendingCount = bookdropFileRepository.countByStatus(BookdropFileEntity.Status.PENDING_REVIEW);
|
||||
@@ -127,7 +129,7 @@ public class BookDropService {
|
||||
failedCount.incrementAndGet();
|
||||
continue;
|
||||
}
|
||||
processFile(file, metadataById.get(id), defaultLibraryId, defaultPathId, request.getUploadPattern(), results, failedCount);
|
||||
processFile(file, metadataById.get(id), defaultLibraryId, defaultPathId, results, failedCount);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -156,7 +158,7 @@ public class BookDropService {
|
||||
failedCount.incrementAndGet();
|
||||
continue;
|
||||
}
|
||||
processFile(file, metadataById.get(id), defaultLibraryId, defaultPathId, request.getUploadPattern(), results, failedCount);
|
||||
processFile(file, metadataById.get(id), defaultLibraryId, defaultPathId, results, failedCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -188,7 +190,6 @@ public class BookDropService {
|
||||
BookdropFinalizeRequest.BookdropFinalizeFile fileReq,
|
||||
Long defaultLibraryId,
|
||||
Long defaultPathId,
|
||||
String uploadPattern,
|
||||
BookdropFinalizeResult results,
|
||||
AtomicInteger failedCount
|
||||
) {
|
||||
@@ -217,13 +218,7 @@ public class BookDropService {
|
||||
log.debug("Processing fileId={}, fileName={} with default metadata, libraryId={}, pathId={}", fileEntity.getId(), fileEntity.getFileName(), libraryId, pathId);
|
||||
}
|
||||
|
||||
BookdropFileResult result = moveFile(
|
||||
libraryId,
|
||||
pathId,
|
||||
uploadPattern,
|
||||
metadata,
|
||||
fileEntity
|
||||
);
|
||||
BookdropFileResult result = moveFile(libraryId, pathId, metadata, fileEntity);
|
||||
|
||||
results.getResults().add(result);
|
||||
if (!result.isSuccess()) {
|
||||
@@ -241,7 +236,7 @@ public class BookDropService {
|
||||
}
|
||||
}
|
||||
|
||||
private BookdropFileResult moveFile(long libraryId, long pathId, String filePattern, BookMetadata metadata, BookdropFileEntity bookdropFile) throws Exception {
|
||||
private BookdropFileResult moveFile(long libraryId, long pathId, BookMetadata metadata, BookdropFileEntity bookdropFile) throws Exception {
|
||||
LibraryEntity library = libraryRepository.findById(libraryId)
|
||||
.orElseThrow(() -> ApiError.LIBRARY_NOT_FOUND.createException(libraryId));
|
||||
|
||||
@@ -250,6 +245,11 @@ public class BookDropService {
|
||||
.findFirst()
|
||||
.orElseThrow(() -> ApiError.INVALID_LIBRARY_PATH.createException(libraryId));
|
||||
|
||||
String filePattern = library.getFileNamingPattern();
|
||||
if (filePattern == null || filePattern.isBlank()) {
|
||||
filePattern = appSettingService.getAppSettings().getUploadPattern();
|
||||
}
|
||||
|
||||
if (filePattern.endsWith("/") || filePattern.endsWith("\\")) {
|
||||
filePattern += "{currentFilename}";
|
||||
}
|
||||
|
||||
+6
@@ -231,4 +231,10 @@ public class LibraryService {
|
||||
List<BookEntity> bookEntities = bookRepository.findAllWithMetadataByLibraryId(libraryId);
|
||||
return bookEntities.stream().map(bookMapper::toBook).toList();
|
||||
}
|
||||
|
||||
public Library setFileNamingPattern(long libraryId, String pattern) {
|
||||
LibraryEntity library = libraryRepository.findById(libraryId).orElseThrow(() -> ApiError.LIBRARY_NOT_FOUND.createException(libraryId));
|
||||
library.setFileNamingPattern(pattern);
|
||||
return libraryMapper.toLibrary(libraryRepository.save(library));
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE library
|
||||
ADD COLUMN IF NOT EXISTS file_naming_pattern VARCHAR(1000);
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface Library {
|
||||
name: string;
|
||||
icon: string;
|
||||
watch: boolean;
|
||||
fileNamingPattern?: string;
|
||||
sort?: SortOption;
|
||||
paths: LibraryPath[];
|
||||
}
|
||||
|
||||
@@ -122,14 +122,27 @@ export class LibraryService {
|
||||
|
||||
getLibraryPathById(pathId: number): string | undefined {
|
||||
const libraries = this.libraryStateSubject.value.libraries || [];
|
||||
|
||||
for (const library of libraries) {
|
||||
const match = library.paths.find(p => p.id === pathId);
|
||||
if (match) {
|
||||
return match.path;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
updateLibraryFileNamingPattern(libraryId: number, fileNamingPattern: string): Observable<Library> {
|
||||
return this.http.patch<Library>(`${this.url}/${libraryId}/file-naming-pattern`, { fileNamingPattern }).pipe(
|
||||
map(updatedLibrary => {
|
||||
const currentState = this.libraryStateSubject.value;
|
||||
const updatedLibraries = currentState.libraries ? currentState.libraries.map(existingLibrary =>
|
||||
existingLibrary.id === updatedLibrary.id ? updatedLibrary : existingLibrary) : [updatedLibrary];
|
||||
this.libraryStateSubject.next({...currentState, libraries: updatedLibraries});
|
||||
return updatedLibrary;
|
||||
}),
|
||||
catchError(error => {
|
||||
throw error;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-20
@@ -33,25 +33,6 @@
|
||||
}
|
||||
|
||||
@if (bookdropFileUis.length !== 0) {
|
||||
|
||||
<div class="flex gap-2 items-center px-1 pb-4 w-full">
|
||||
<label for="uploadPatternInput" class="text-sm text-gray-300 font-medium">File Naming Pattern:</label>
|
||||
<input
|
||||
id="uploadPatternInput"
|
||||
fluid
|
||||
class="min-w-[20rem] max-w-[45.5rem]"
|
||||
pSize="small"
|
||||
type="text"
|
||||
pInputText
|
||||
[(ngModel)]="uploadPattern"
|
||||
placeholder="e.g., {title} - {authors}"/>
|
||||
<i
|
||||
class="pi pi-info-circle text-blue-500 cursor-help"
|
||||
pTooltip="Sets the filename pattern used when moving files from bookdrop folder to the library, using metadata placeholders like {title}, {authors}, etc."
|
||||
tooltipPosition="top"
|
||||
style="font-size: 1rem; margin-left: 0.25rem;"></i>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center gap-4 px-1">
|
||||
|
||||
<div class="flex gap-4 items-center">
|
||||
@@ -126,7 +107,7 @@
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto px-6 space-y-2 pb-4">
|
||||
<div class="flex-1 overflow-y-auto px-6 pt-4 space-y-2 pb-4">
|
||||
@if (bookdropFileUis.length === 0) {
|
||||
<div class="h-full w-full flex items-center justify-center text-gray-400 italic py-8">
|
||||
No bookdrop files to review.
|
||||
|
||||
-3
@@ -11,7 +11,6 @@ import {DropdownModule} from 'primeng/dropdown';
|
||||
import {FormControl, FormGroup, FormsModule} from '@angular/forms';
|
||||
import {Button} from 'primeng/button';
|
||||
import {Select} from 'primeng/select';
|
||||
import {InputText} from 'primeng/inputtext';
|
||||
import {Tooltip} from 'primeng/tooltip';
|
||||
import {Divider} from 'primeng/divider';
|
||||
import {ConfirmationService, MessageService} from 'primeng/api';
|
||||
@@ -55,7 +54,6 @@ export interface BookdropFileUI {
|
||||
BookdropFileMetadataPickerComponent,
|
||||
Tooltip,
|
||||
Divider,
|
||||
InputText,
|
||||
Checkbox,
|
||||
NgStyle,
|
||||
NgClass,
|
||||
@@ -425,7 +423,6 @@ export class BookdropFileReviewComponent implements OnInit {
|
||||
});
|
||||
|
||||
const payload: BookdropFinalizePayload = {
|
||||
uploadPattern: this.uploadPattern,
|
||||
selectAll: this.selectAllAcrossPages,
|
||||
excludedIds: this.selectAllAcrossPages ? Array.from(this.excludedFiles) : undefined,
|
||||
defaultLibraryId: this.defaultLibraryId ? Number(this.defaultLibraryId) : undefined,
|
||||
|
||||
@@ -5,13 +5,11 @@ import {BookMetadata} from '../book/model/book.model';
|
||||
import {API_CONFIG} from '../config/api-config';
|
||||
|
||||
export enum BookdropFileStatus {
|
||||
PENDING_REVIEW = 'PENDING_REVIEW',
|
||||
ACCEPTED = 'ACCEPTED',
|
||||
REJECTED = 'REJECTED',
|
||||
}
|
||||
|
||||
export interface BookdropFinalizePayload {
|
||||
uploadPattern: string;
|
||||
selectAll?: boolean;
|
||||
excludedIds?: number[];
|
||||
defaultLibraryId?: number;
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
<div class="w-full h-[calc(100dvh-10.5rem)] md:h-[calc(100dvh-11.65rem)] overflow-y-auto border rounded-lg enclosing-container">
|
||||
<div class="pt-8 px-4">
|
||||
<p class="text-lg flex items-center gap-2">
|
||||
File Naming Patterns
|
||||
<i
|
||||
class="pi pi-info-circle text-sky-600"
|
||||
pTooltip="Define custom naming patterns for uploaded files and for moving files within your library. Use metadata placeholders to automate organization."
|
||||
tooltipPosition="right"
|
||||
style="cursor: pointer;">
|
||||
</i>
|
||||
</p>
|
||||
<div class="flex flex-col space-y-6 p-4 m-4 custom-border">
|
||||
|
||||
<div>
|
||||
<h2 class="mb-2 text-lg">Default File Naming Pattern:</h2>
|
||||
<p class="mb-3 text-sm text-gray-400">
|
||||
Define the default naming pattern for files. This pattern applies to all libraries unless overridden.
|
||||
</p>
|
||||
<div class="flex gap-4 items-center">
|
||||
<input type="text" [(ngModel)]="defaultPattern" class="p-inputtext w-[700px]" placeholder="e.g., {title} - {authors}" (input)="onDefaultPatternChange(defaultPattern)"/>
|
||||
<p-button label="Save" (onClick)="savePatterns()" severity="primary" outlined="true" [disabled]="!!defaultErrorMessage"></p-button>
|
||||
</div>
|
||||
@if (defaultErrorMessage) {
|
||||
<div class="text-red-500 mt-1">{{ defaultErrorMessage }}</div>
|
||||
}
|
||||
|
||||
<div class="flex gap-2 pt-2">
|
||||
<p class="text-zinc-400">Preview:</p>
|
||||
<p class="text-green-500">{{ generateDefaultPreview() }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="py-2 text-lg">Overrides for Libraries:</h2>
|
||||
<p class="mb-3 text-sm text-gray-400">
|
||||
Define custom naming patterns for specific libraries. Leave empty to use the default pattern.
|
||||
</p>
|
||||
|
||||
@for (library of libraries; track library.id) {
|
||||
<div class="flex flex-col gap-2 p-3">
|
||||
<div class="flex gap-3 items-center">
|
||||
<i [class]="'pi pi-' + library.icon"></i>
|
||||
<span class="min-w-[100px] font-medium">{{ library.name }}</span>
|
||||
<input
|
||||
type="text"
|
||||
[(ngModel)]="library.fileNamingPattern"
|
||||
class="p-inputtext flex-1"
|
||||
placeholder="Leave empty to use default pattern"
|
||||
(input)="onLibraryPatternChange(library)"/>
|
||||
<p-button
|
||||
label="Clear"
|
||||
(onClick)="clearLibraryPattern(library)"
|
||||
severity="secondary"
|
||||
outlined="true"
|
||||
size="small"
|
||||
[disabled]="!library.fileNamingPattern">
|
||||
</p-button>
|
||||
</div>
|
||||
<div class="ml-8 flex gap-2">
|
||||
<p class="text-sm text-zinc-400">Preview:</p>
|
||||
<p class="text-sm text-green-500">{{ generateLibraryPreview(library) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="flex justify-end">
|
||||
<p-button label="Save All Library Patterns" (onClick)="saveLibraryPatterns()" severity="primary" outlined="true"></p-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p-divider></p-divider>
|
||||
|
||||
<p class="pt-4">Available Placeholders:</p>
|
||||
|
||||
<p class="text-sm text-gray-400 mb-2 mt-2">
|
||||
Use placeholders to dynamically insert book metadata into file names and folder paths. These will be replaced with actual values when uploading or moving files.
|
||||
You can also wrap parts of the pattern in <code>{{ '{{<...>}' }}</code> to make them optional, if any placeholder inside is missing, that section will be omitted entirely.
|
||||
</p>
|
||||
|
||||
<div class="mt-2">
|
||||
<div class="mx-4 my-2 flex flex-wrap gap-4">
|
||||
<ul class="list-disc pl-4 min-w-[250px] text-gray-300">
|
||||
<li><code class="text-orange-400">{{ '{title}' }}</code> – Book title</li>
|
||||
<li><code class="text-orange-400">{{ '{authors}' }}</code> – Author(s)</li>
|
||||
<li><code class="text-orange-400">{{ '{year}' }}</code> – Full year (e.g. 2025)</li>
|
||||
<li><code class="text-orange-400">{{ '{series}' }}</code> – Series name</li>
|
||||
<li><code class="text-orange-400">{{ '{seriesIndex}' }}</code> – Series index (e.g. 01)</li>
|
||||
<li><code class="text-orange-400">{{ '{language}' }}</code> – Language code (e.g. en)</li>
|
||||
<li><code class="text-orange-400">{{ '{publisher}' }}</code> – Publisher name</li>
|
||||
<li><code class="text-orange-400">{{ '{isbn}' }}</code> – ISBN number</li>
|
||||
<li><code class="text-orange-400">{{ '{currentFilename}' }}</code> Original file name (with extension)</li>
|
||||
</ul>
|
||||
|
||||
<p-divider layout="vertical"></p-divider>
|
||||
|
||||
<div class="text-sm p-3 min-w-[250px]">
|
||||
<p class="mb-1 font-bold text-gray-200 text">Optional blocks</p>
|
||||
<p class="text-gray-300">
|
||||
Surround parts of your pattern with angle brackets
|
||||
<code class="text-blue-400">{{ '{<...>}' }}</code>
|
||||
to make them optional. <br> If any placeholder inside the block has no value, the whole block is excluded.
|
||||
</p>
|
||||
<p class="mt-2 font-semibold text-gray-300">Example:</p>
|
||||
<p><code>{{ '{<{seriesIndex} - >{title}' }}</code></p>
|
||||
<p class="pl-4 mt-1 text-gray-300">• <code>01 - Dune</code> (if <code>{{ '{seriesIndex}' }}</code> exists)</p>
|
||||
<p class="pl-4 text-gray-300">• <code>Dune</code> (if <code>{{ '{seriesIndex}' }}</code> is missing)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p-divider></p-divider>
|
||||
|
||||
<div class="mt-6">
|
||||
<p class="mb-4">Example Patterns & Output:</p>
|
||||
|
||||
<div class="px-4 text-sm space-y-4">
|
||||
<p class="text-base font-semibold text-gray-200 mt-6 mb-2">Examples with Full Metadata</p>
|
||||
<p class="text-gray-400 mb-4">
|
||||
<span class="block">title: <code>Harry Potter and the Sorcerer's Stone</code></span>
|
||||
<span class="block">authors: <code>J.K. Rowling</code></span>
|
||||
<span class="block">series: <code>Harry Potter</code></span>
|
||||
<span class="block">seriesIndex: <code>01</code></span>
|
||||
<span class="block">year: <code>1997</code></span>
|
||||
<span class="block">currentFilename: <code>harry1_original.epub</code></span>
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Basic pattern:</strong><code class="text-gray-400"> {{ '{authors} - {title}' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> J.K. Rowling - Harry Potter and the Sorcerer's Stone.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Pattern with punctuation:</strong><code class="text-gray-400"> {{ '{title}: {series}' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> Harry Potter and the Sorcerer's Stone: Harry Potter.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Series in folder path:</strong><code class="text-gray-400"> {{ '{authors}/{series}/{seriesIndex} - {title}' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> J.K. Rowling/Harry Potter/01 - Harry Potter and the Sorcerer's Stone.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Folder only:</strong><code class="text-gray-400"> {{ '{title}/' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> /Harry Potter and the Sorcerer's Stone/harry1_original.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Absolute path:</strong><code class="text-gray-400"> {{ '/{authors}/{title}' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> /J.K. Rowling/Harry Potter and the Sorcerer's Stone.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Reuse original filename in path:</strong><code class="text-gray-400"> {{ '{authors}/{series}/{currentFilename}' }}</code>
|
||||
</p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> J.K. Rowling/Harry Potter/harry1_original.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Preserve original filename only:</strong><code class="text-gray-400"> {{ '{currentFilename}' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> harry1_original.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Empty pattern (defaults to current filename):</strong><code class="text-gray-400"> {{ '' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> harry1_original.epub</code></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-4 text-sm space-y-4 mt-10">
|
||||
<p class="text-base font-semibold text-gray-200 mb-2">Examples with Missing Optional Fields</p>
|
||||
<p class="text-gray-400 mb-4">
|
||||
<span class="block">title: <code>Project Hail Mary</code></span>
|
||||
<span class="block">authors: <code>Andy Weir</code></span>
|
||||
<span class="block">year: <code>2021</code></span>
|
||||
<span class="block">series: <code>(not provided)</code></span>
|
||||
<span class="block">seriesIndex: <code>(not provided)</code></span>
|
||||
<span class="block">currentFilename: <code>project_hail_mary_final.epub</code></span>
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Pattern with optional blocks:</strong><code
|
||||
class="text-gray-400"> {{ '{authors}/<{series}/><{seriesIndex}. >{title}< ({year})>' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> Andy Weir/Project Hail Mary (2021).epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Fallback with seriesIndex and dash:</strong><code
|
||||
class="text-gray-400"> {{ '<{seriesIndex}. >{title}< - {authors}>' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> Project Hail Mary - Andy Weir.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Brackets & punctuation fallback:</strong><code class="text-gray-400"> {{ '<[{series}] >{title} - {authors}' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> Project Hail Mary - Andy Weir.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Series + punctuation fallback:</strong><code class="text-gray-400"> {{ '<{series}: >{title} by {authors}' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> Project Hail Mary by Andy Weir.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Only folders, no filename:</strong><code class="text-gray-400"> {{ '{authors}/' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> Andy Weir/project_hail_mary_final.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Deep nested folders:</strong><code class="text-gray-400"> {{ '{authors}/books/<{series}/>{title}/' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> Andy Weir/books/Project Hail Mary/project_hail_mary_final.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">With static folder prefix:</strong><code class="text-gray-400"> {{ 'Books/<{series}/>{authors} - {title}' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> Books/Andy Weir - Project Hail Mary.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Use original filename in path:</strong><code
|
||||
class="text-gray-400"> {{ '{authors}/books/<{series}/>{currentFilename}' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> Andy Weir/books/project_hail_mary_final.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Prefix current filename manually:</strong><code class="text-gray-400"> {{ '{authors}/final__{currentFilename}' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> Andy Weir/final__project_hail_mary_final.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Filename preserved, renamed folder:</strong><code class="text-gray-400"> {{ '{title}/source/{currentFilename}' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> Project Hail Mary/source/project_hail_mary_final.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Use current filename with year suffix:</strong><code
|
||||
class="text-gray-400"> {{ '{authors}/{year}__{currentFilename}' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> Andy Weir/2021__project_hail_mary_final.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Fallback with optional + extras folder:</strong><code
|
||||
class="text-gray-400"> {{ '<{series}/>{authors}/extras/{currentFilename}' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> Andy Weir/extras/project_hail_mary_final.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Archive structure using original name:</strong><code
|
||||
class="text-gray-400"> {{ 'archive/<{series}/>{year}/{currentFilename}' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> archive/2021/project_hail_mary_final.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Structured folder + renamed file:</strong><code
|
||||
class="text-gray-400"> {{ '{authors}/<{series}/>{title} ({year}) by {authors}' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> Andy Weir/Project Hail Mary (2021) by Andy Weir.epub</code></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
+4
@@ -1,3 +1,7 @@
|
||||
.enclosing-container {
|
||||
border-color: var(--p-content-border-color);
|
||||
}
|
||||
|
||||
.custom-border {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--card-border);
|
||||
+58
-52
@@ -2,21 +2,23 @@ import {Component, inject, OnInit} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {Button} from 'primeng/button';
|
||||
import {MessageService} from 'primeng/api';
|
||||
import {AppSettingsService} from '../../../core/service/app-settings.service';
|
||||
import {Observable} from 'rxjs';
|
||||
import {AppSettingKey, AppSettings} from '../../../core/model/app-settings.model';
|
||||
import {filter, take} from 'rxjs/operators';
|
||||
import {AppSettingsService} from '../../core/service/app-settings.service';
|
||||
import {forkJoin, Observable, of} from 'rxjs';
|
||||
import {AppSettingKey, AppSettings} from '../../core/model/app-settings.model';
|
||||
import {catchError, filter, take} from 'rxjs/operators';
|
||||
import {Divider} from 'primeng/divider';
|
||||
import {Tooltip} from 'primeng/tooltip';
|
||||
import {Library} from '../../book/model/library.model';
|
||||
import {LibraryService} from '../../book/service/library.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-file-upload-pattern',
|
||||
templateUrl: './file-upload-pattern.component.html',
|
||||
selector: 'app-file-naming-pattern',
|
||||
templateUrl: './file-naming-pattern.component.html',
|
||||
standalone: true,
|
||||
imports: [FormsModule, Button, Divider, Tooltip],
|
||||
styleUrls: ['./file-upload-pattern.component.scss'],
|
||||
styleUrls: ['./file-naming-pattern.component.scss'],
|
||||
})
|
||||
export class FileUploadPatternComponent implements OnInit {
|
||||
export class FileNamingPatternComponent implements OnInit {
|
||||
readonly exampleMetadata: Record<string, string> = {
|
||||
title: 'The Fellowship of the Ring',
|
||||
authors: 'J.R.R. Tolkien',
|
||||
@@ -28,14 +30,13 @@ export class FileUploadPatternComponent implements OnInit {
|
||||
isbn: '9780618574940',
|
||||
};
|
||||
|
||||
uploadPattern = '';
|
||||
movePattern = '';
|
||||
|
||||
uploadErrorMessage = '';
|
||||
moveErrorMessage = '';
|
||||
defaultPattern = '';
|
||||
libraries: Library[] = [];
|
||||
defaultErrorMessage = '';
|
||||
|
||||
private appSettingsService = inject(AppSettingsService);
|
||||
private messageService = inject(MessageService);
|
||||
private libraryService = inject(LibraryService);
|
||||
|
||||
appSettings$: Observable<AppSettings | null> = this.appSettingsService.appSettings$;
|
||||
|
||||
@@ -43,8 +44,13 @@ export class FileUploadPatternComponent implements OnInit {
|
||||
this.appSettings$
|
||||
.pipe(filter((settings) => settings != null), take(1))
|
||||
.subscribe((settings) => {
|
||||
this.uploadPattern = settings?.uploadPattern ?? '';
|
||||
this.movePattern = settings?.movePattern ?? '';
|
||||
this.defaultPattern = settings?.uploadPattern ?? '';
|
||||
});
|
||||
|
||||
this.libraryService.libraryState$
|
||||
.pipe(filter(state => state.loaded && !!state.libraries))
|
||||
.subscribe(state => {
|
||||
this.libraries = state.libraries ?? [];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -65,77 +71,77 @@ export class FileUploadPatternComponent implements OnInit {
|
||||
return hasExtension ? path : path + ext;
|
||||
}
|
||||
|
||||
generateUploadPreview(): string {
|
||||
let path = this.replacePlaceholders(this.uploadPattern || '', this.exampleMetadata);
|
||||
private generatePreview(pattern: string): string {
|
||||
let path = this.replacePlaceholders(pattern || '', this.exampleMetadata);
|
||||
|
||||
if (!path) return '/original_filename.pdf';
|
||||
|
||||
// If pattern ends with slash, append original filename
|
||||
if (path.endsWith('/')) {
|
||||
return path + 'original_filename.pdf';
|
||||
}
|
||||
|
||||
// Replace {originalFilename} placeholder if present
|
||||
if (path.endsWith('/')) return path + 'original_filename.pdf';
|
||||
if (path.includes('{originalFilename}')) {
|
||||
path = path.replace('{originalFilename}', 'original_filename.pdf');
|
||||
return path.startsWith('/') ? path : `/${path}`;
|
||||
}
|
||||
|
||||
path = this.appendExtensionIfMissing(path);
|
||||
return path.startsWith('/') ? path : `/${path}`;
|
||||
}
|
||||
|
||||
generateMovePreview(): string {
|
||||
let path = this.replacePlaceholders(this.movePattern || '', this.exampleMetadata);
|
||||
generateDefaultPreview(): string {
|
||||
return this.generatePreview(this.defaultPattern);
|
||||
}
|
||||
|
||||
if (!path) return '/original_filename.pdf';
|
||||
|
||||
if (path.endsWith('/')) {
|
||||
return path + 'original_filename.pdf';
|
||||
}
|
||||
|
||||
if (path.includes('{originalFilename}')) {
|
||||
path = path.replace('{originalFilename}', 'original_filename.pdf');
|
||||
return path.startsWith('/') ? path : `/${path}`;
|
||||
}
|
||||
|
||||
path = this.appendExtensionIfMissing(path);
|
||||
return path.startsWith('/') ? path : `/${path}`;
|
||||
generateLibraryPreview(library: Library): string {
|
||||
return this.generatePreview(library.fileNamingPattern || this.defaultPattern);
|
||||
}
|
||||
|
||||
validatePattern(pattern: string): boolean {
|
||||
// Allow letters, numbers, whitespace, common punctuation and placeholders syntax
|
||||
const validPatternRegex = /^[\w\s\-{}\/().<>.,:'"]*$/;
|
||||
return validPatternRegex.test(pattern);
|
||||
}
|
||||
|
||||
onUploadPatternChange(pattern: string): void {
|
||||
this.uploadPattern = pattern;
|
||||
this.uploadErrorMessage = this.validatePattern(pattern) ? '' : 'Pattern contains invalid characters.';
|
||||
onDefaultPatternChange(pattern: string): void {
|
||||
this.defaultPattern = pattern;
|
||||
this.defaultErrorMessage = this.validatePattern(pattern) ? '' : 'Pattern contains invalid characters.';
|
||||
}
|
||||
|
||||
onMovePatternChange(pattern: string): void {
|
||||
this.movePattern = pattern;
|
||||
this.moveErrorMessage = this.validatePattern(pattern) ? '' : 'Pattern contains invalid characters.';
|
||||
onLibraryPatternChange(library: Library): void {
|
||||
// Optionally add per-library validation here
|
||||
}
|
||||
|
||||
clearLibraryPattern(library: Library): void {
|
||||
library.fileNamingPattern = '';
|
||||
}
|
||||
|
||||
savePatterns(): void {
|
||||
if (this.uploadErrorMessage || this.moveErrorMessage) {
|
||||
if (this.defaultErrorMessage) {
|
||||
this.showMessage('error', 'Invalid Pattern', 'Please fix errors before saving.');
|
||||
return;
|
||||
}
|
||||
this.appSettingsService
|
||||
.saveSettings([
|
||||
{key: AppSettingKey.UPLOAD_FILE_PATTERN, newValue: this.uploadPattern},
|
||||
{key: AppSettingKey.MOVE_FILE_PATTERN, newValue: this.movePattern},
|
||||
{ key: AppSettingKey.UPLOAD_FILE_PATTERN, newValue: this.defaultPattern },
|
||||
])
|
||||
.subscribe({
|
||||
next: () => this.showMessage('success', 'Settings Saved', 'The patterns were successfully saved!'),
|
||||
next: () => this.showMessage('success', 'Settings Saved', 'The default pattern was successfully saved!'),
|
||||
error: () => this.showMessage('error', 'Error', 'There was an error saving the settings.'),
|
||||
});
|
||||
}
|
||||
|
||||
saveLibraryPatterns(): void {
|
||||
const patchRequests = this.libraries.map(library =>
|
||||
this.libraryService.updateLibraryFileNamingPattern(library.id!, library.fileNamingPattern || '').pipe(
|
||||
catchError(() => of(null))
|
||||
)
|
||||
);
|
||||
forkJoin(patchRequests).subscribe(results => {
|
||||
const failures = results.filter(result => result === null);
|
||||
if (failures.length === 0) {
|
||||
this.showMessage('success', 'Library Patterns Saved', 'Library-specific patterns were successfully saved!');
|
||||
} else {
|
||||
this.showMessage('error', 'Error', `Failed to save ${failures.length} library pattern(s).`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private showMessage(severity: 'success' | 'error', summary: string, detail: string): void {
|
||||
this.messageService.add({severity, summary, detail});
|
||||
this.messageService.add({ severity, summary, detail });
|
||||
}
|
||||
}
|
||||
-235
@@ -1,235 +0,0 @@
|
||||
<p class="text-lg flex items-center gap-2">
|
||||
File Naming Patterns
|
||||
<i
|
||||
class="pi pi-info-circle text-sky-600"
|
||||
pTooltip="Define custom naming patterns for uploaded files and for moving files within your library. Use metadata placeholders to automate organization."
|
||||
tooltipPosition="right"
|
||||
style="cursor: pointer;">
|
||||
</i>
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col space-y-6 p-4 m-4 custom-border">
|
||||
|
||||
<div>
|
||||
<h2 class="mb-3 text-lg">Enter Upload File Naming Pattern:</h2>
|
||||
<p class="mb-3 text-sm text-gray-400">
|
||||
Define how newly uploaded files are named and organized using book metadata.
|
||||
</p>
|
||||
<div class="flex gap-4 items-center">
|
||||
<input type="text" [(ngModel)]="uploadPattern" class="p-inputtext w-[700px]" placeholder="e.g., {title} - {authors}" (input)="onUploadPatternChange(uploadPattern)"/>
|
||||
<p-button label="Save" (onClick)="savePatterns()" severity="primary" outlined="true" [disabled]="!!uploadErrorMessage || !!moveErrorMessage"></p-button>
|
||||
</div>
|
||||
@if (uploadErrorMessage) {
|
||||
<div class="text-red-500 mt-1">{{ uploadErrorMessage }}</div>
|
||||
}
|
||||
|
||||
<h3 class="mt-4 mb-2">Upload Pattern Preview:</h3>
|
||||
<p class="text-lg text-green-500">{{ generateUploadPreview() }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="mb-3 text-lg">Enter File Move Naming Pattern:</h2>
|
||||
<p class="mb-3 text-sm text-gray-400">
|
||||
Specify how book files should be renamed and relocated using metadata such as title, authors, or series. This pattern is applied when using the Move Files feature.
|
||||
</p>
|
||||
<div class="flex gap-4 items-center">
|
||||
<input type="text" [(ngModel)]="movePattern" class="p-inputtext w-[700px]" placeholder="e.g., {authors}/<{series}/><{seriesIndex}. >{title} - {authors}< ({year})>.pdf"
|
||||
(input)="onMovePatternChange(movePattern)"/>
|
||||
<p-button label="Save" (onClick)="savePatterns()" severity="primary" outlined="true" [disabled]="!!uploadErrorMessage || !!moveErrorMessage"></p-button>
|
||||
</div>
|
||||
@if (moveErrorMessage) {
|
||||
<div class="text-red-500 mt-1">{{ moveErrorMessage }}</div>
|
||||
}
|
||||
|
||||
<h3 class="mt-4 mb-2">Move Pattern Preview:</h3>
|
||||
<p class="text-lg text-green-500">{{ generateMovePreview() }}</p>
|
||||
|
||||
<p-divider></p-divider>
|
||||
|
||||
<p class="pt-4">Available Placeholders:</p>
|
||||
|
||||
<p class="text-sm text-gray-400 mb-2 mt-2">
|
||||
Use placeholders to dynamically insert book metadata into file names and folder paths. These will be replaced with actual values when uploading or moving files.
|
||||
You can also wrap parts of the pattern in <code>{{ '{{<...>}' }}</code> to make them optional, if any placeholder inside is missing, that section will be omitted entirely.
|
||||
</p>
|
||||
|
||||
<div class="mt-2">
|
||||
<div class="mx-4 my-2 flex flex-wrap gap-4">
|
||||
<ul class="list-disc pl-4 min-w-[250px] text-gray-300">
|
||||
<li><code class="text-orange-400">{{ '{title}' }}</code> – Book title</li>
|
||||
<li><code class="text-orange-400">{{ '{authors}' }}</code> – Author(s)</li>
|
||||
<li><code class="text-orange-400">{{ '{year}' }}</code> – Full year (e.g. 2025)</li>
|
||||
<li><code class="text-orange-400">{{ '{series}' }}</code> – Series name</li>
|
||||
<li><code class="text-orange-400">{{ '{seriesIndex}' }}</code> – Series index (e.g. 01)</li>
|
||||
<li><code class="text-orange-400">{{ '{language}' }}</code> – Language code (e.g. en)</li>
|
||||
<li><code class="text-orange-400">{{ '{publisher}' }}</code> – Publisher name</li>
|
||||
<li><code class="text-orange-400">{{ '{isbn}' }}</code> – ISBN number</li>
|
||||
<li><code class="text-orange-400">{{ '{currentFilename}' }}</code> Original file name (with extension)</li>
|
||||
</ul>
|
||||
|
||||
<p-divider layout="vertical"></p-divider>
|
||||
|
||||
<div class="text-sm p-3 min-w-[250px]">
|
||||
<p class="mb-1 font-bold text-gray-200 text">Optional blocks</p>
|
||||
<p class="text-gray-300">
|
||||
Surround parts of your pattern with angle brackets
|
||||
<code class="text-blue-400">{{ '{<...>}' }}</code>
|
||||
to make them optional. <br> If any placeholder inside the block has no value, the whole block is excluded.
|
||||
</p>
|
||||
<p class="mt-2 font-semibold text-gray-300">Example:</p>
|
||||
<p><code>{{ '{<{seriesIndex} - >{title}' }}</code></p>
|
||||
<p class="pl-4 mt-1 text-gray-300">• <code>01 - Dune</code> (if <code>{{ '{seriesIndex}' }}</code> exists)</p>
|
||||
<p class="pl-4 text-gray-300">• <code>Dune</code> (if <code>{{ '{seriesIndex}' }}</code> is missing)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p-divider></p-divider>
|
||||
|
||||
<div class="mt-6">
|
||||
<p class="mb-4">Example Patterns & Output:</p>
|
||||
|
||||
<div class="px-4 text-sm space-y-4">
|
||||
<p class="text-base font-semibold text-gray-200 mt-6 mb-2">Examples with Full Metadata</p>
|
||||
<p class="text-gray-400 mb-4">
|
||||
<span class="block">title: <code>Harry Potter and the Sorcerer's Stone</code></span>
|
||||
<span class="block">authors: <code>J.K. Rowling</code></span>
|
||||
<span class="block">series: <code>Harry Potter</code></span>
|
||||
<span class="block">seriesIndex: <code>01</code></span>
|
||||
<span class="block">year: <code>1997</code></span>
|
||||
<span class="block">currentFilename: <code>harry1_original.epub</code></span>
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Basic pattern:</strong><code class="text-gray-400"> {{ '{authors} - {title}' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> J.K. Rowling - Harry Potter and the Sorcerer's Stone.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Pattern with punctuation:</strong><code class="text-gray-400"> {{ '{title}: {series}' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> Harry Potter and the Sorcerer's Stone: Harry Potter.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Series in folder path:</strong><code class="text-gray-400"> {{ '{authors}/{series}/{seriesIndex} - {title}' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> J.K. Rowling/Harry Potter/01 - Harry Potter and the Sorcerer's Stone.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Folder only:</strong><code class="text-gray-400"> {{ '{title}/' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> /Harry Potter and the Sorcerer's Stone/harry1_original.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Absolute path:</strong><code class="text-gray-400"> {{ '/{authors}/{title}' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> /J.K. Rowling/Harry Potter and the Sorcerer's Stone.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Reuse original filename in path:</strong><code class="text-gray-400"> {{ '{authors}/{series}/{currentFilename}' }}</code>
|
||||
</p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> J.K. Rowling/Harry Potter/harry1_original.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Preserve original filename only:</strong><code class="text-gray-400"> {{ '{currentFilename}' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> harry1_original.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Empty pattern (defaults to current filename):</strong><code class="text-gray-400"> {{ '' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> harry1_original.epub</code></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-4 text-sm space-y-4 mt-10">
|
||||
<p class="text-base font-semibold text-gray-200 mb-2">Examples with Missing Optional Fields</p>
|
||||
<p class="text-gray-400 mb-4">
|
||||
<span class="block">title: <code>Project Hail Mary</code></span>
|
||||
<span class="block">authors: <code>Andy Weir</code></span>
|
||||
<span class="block">year: <code>2021</code></span>
|
||||
<span class="block">series: <code>(not provided)</code></span>
|
||||
<span class="block">seriesIndex: <code>(not provided)</code></span>
|
||||
<span class="block">currentFilename: <code>project_hail_mary_final.epub</code></span>
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Pattern with optional blocks:</strong><code
|
||||
class="text-gray-400"> {{ '{authors}/<{series}/><{seriesIndex}. >{title}< ({year})>' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> Andy Weir/Project Hail Mary (2021).epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Fallback with seriesIndex and dash:</strong><code
|
||||
class="text-gray-400"> {{ '<{seriesIndex}. >{title}< - {authors}>' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> Project Hail Mary - Andy Weir.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Brackets & punctuation fallback:</strong><code class="text-gray-400"> {{ '<[{series}] >{title} - {authors}' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> Project Hail Mary - Andy Weir.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Series + punctuation fallback:</strong><code class="text-gray-400"> {{ '<{series}: >{title} by {authors}' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> Project Hail Mary by Andy Weir.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Only folders, no filename:</strong><code class="text-gray-400"> {{ '{authors}/' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> Andy Weir/project_hail_mary_final.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Deep nested folders:</strong><code class="text-gray-400"> {{ '{authors}/books/<{series}/>{title}/' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> Andy Weir/books/Project Hail Mary/project_hail_mary_final.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">With static folder prefix:</strong><code class="text-gray-400"> {{ 'Books/<{series}/>{authors} - {title}' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> Books/Andy Weir - Project Hail Mary.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Use original filename in path:</strong><code
|
||||
class="text-gray-400"> {{ '{authors}/books/<{series}/>{currentFilename}' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> Andy Weir/books/project_hail_mary_final.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Prefix current filename manually:</strong><code class="text-gray-400"> {{ '{authors}/final__{currentFilename}' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> Andy Weir/final__project_hail_mary_final.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Filename preserved, renamed folder:</strong><code class="text-gray-400"> {{ '{title}/source/{currentFilename}' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> Project Hail Mary/source/project_hail_mary_final.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Use current filename with year suffix:</strong><code
|
||||
class="text-gray-400"> {{ '{authors}/{year}__{currentFilename}' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> Andy Weir/2021__project_hail_mary_final.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Fallback with optional + extras folder:</strong><code
|
||||
class="text-gray-400"> {{ '<{series}/>{authors}/extras/{currentFilename}' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> Andy Weir/extras/project_hail_mary_final.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Archive structure using original name:</strong><code
|
||||
class="text-gray-400"> {{ 'archive/<{series}/>{year}/{currentFilename}' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> archive/2021/project_hail_mary_final.epub</code></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-1"><strong class="text-gray-300">Structured folder + renamed file:</strong><code
|
||||
class="text-gray-400"> {{ '{authors}/<{series}/>{title} ({year}) by {authors}' }}</code></p>
|
||||
<p><strong class="text-gray-300">Output:</strong><code class="text-gray-400"> Andy Weir/Project Hail Mary (2021) by Andy Weir.epub</code></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -130,8 +130,4 @@
|
||||
<p-divider></p-divider>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-2">
|
||||
<app-file-upload-pattern></app-file-upload-pattern>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,6 @@ import {AppSettingsService} from '../../core/service/app-settings.service';
|
||||
import {BookService} from '../../book/service/book.service';
|
||||
import {AppSettingKey, AppSettings} from '../../core/model/app-settings.model';
|
||||
import {filter, take} from 'rxjs/operators';
|
||||
import {FileUploadPatternComponent} from './file-upload-pattern/file-upload-pattern.component';
|
||||
import {InputText} from 'primeng/inputtext';
|
||||
|
||||
@Component({
|
||||
@@ -28,7 +27,6 @@ import {InputText} from 'primeng/inputtext';
|
||||
Tooltip,
|
||||
ToggleSwitch,
|
||||
FormsModule,
|
||||
FileUploadPatternComponent,
|
||||
InputText
|
||||
],
|
||||
templateUrl: './global-preferences.component.html',
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
<p-tab [value]="SettingsTab.EmailSettings">
|
||||
<i class="pi pi-envelope"></i> Email
|
||||
</p-tab>
|
||||
<p-tab [value]="SettingsTab.NamingPattern">
|
||||
<i class="pi pi-sitemap"></i> Patterns
|
||||
</p-tab>
|
||||
<p-tab [value]="SettingsTab.AuthenticationSettings">
|
||||
<i class="pi pi-lock"></i> Authentication
|
||||
</p-tab>
|
||||
@@ -55,6 +58,9 @@
|
||||
<p-tabpanel [value]="SettingsTab.EmailSettings">
|
||||
<app-email></app-email>
|
||||
</p-tabpanel>
|
||||
<p-tabpanel [value]="SettingsTab.NamingPattern">
|
||||
<app-file-naming-pattern></app-file-naming-pattern>
|
||||
</p-tabpanel>
|
||||
<p-tabpanel [value]="SettingsTab.AuthenticationSettings">
|
||||
<app-authentication-settings></app-authentication-settings>
|
||||
</p-tabpanel>
|
||||
|
||||
@@ -13,17 +13,19 @@ import {ReaderPreferences} from './reader-preferences/reader-preferences.compone
|
||||
import {MetadataSettingsComponent} from './metadata-settings-component/metadata-settings-component';
|
||||
import {OpdsSettingsComponent} from './global-preferences/opds-settings/opds-settings.component';
|
||||
import {DeviceSettingsComponent} from './device-settings-component/device-settings-component';
|
||||
import {FileNamingPatternComponent} from './file-naming-pattern/file-naming-pattern.component';
|
||||
|
||||
export enum SettingsTab {
|
||||
ReaderSettings = 'reader-settings',
|
||||
ViewPreferences = 'view-settings',
|
||||
DeviceSettings = 'device-settings',
|
||||
UserManagement = 'user-management',
|
||||
EmailSettings = 'email-settings',
|
||||
MetadataSettings = 'metadata-settings',
|
||||
ApplicationSettings = 'app-settings',
|
||||
AuthenticationSettings = 'auth-settings',
|
||||
Opds = 'opds-settings'
|
||||
ReaderSettings = 'reader',
|
||||
ViewPreferences = 'view',
|
||||
DeviceSettings = 'device',
|
||||
UserManagement = 'user',
|
||||
EmailSettings = 'email',
|
||||
NamingPattern = 'naming-pattern',
|
||||
MetadataSettings = 'metadata',
|
||||
ApplicationSettings = 'application',
|
||||
AuthenticationSettings = 'authentication',
|
||||
Opds = 'opds'
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -43,7 +45,8 @@ export enum SettingsTab {
|
||||
ReaderPreferences,
|
||||
MetadataSettingsComponent,
|
||||
OpdsSettingsComponent,
|
||||
DeviceSettingsComponent
|
||||
DeviceSettingsComponent,
|
||||
FileNamingPatternComponent
|
||||
],
|
||||
templateUrl: './settings.component.html',
|
||||
styleUrl: './settings.component.scss'
|
||||
|
||||
Reference in New Issue
Block a user