feat(scanner): regex metadata extraction (#1669)

feat(metadata): extract metadata from file paths with regex rules (#1669)

Let libraries define ordered regex rules that read title, authors, series,
series number and dates from each book's path relative to its library folder.
Run rules in a bounded worker pool, honour source precedence and field locks,
and apply them on scans, uploads and watcher moves. Also retry unprobed M4B
tag writes with the mp4 muxer.

Closes #1351

Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
This commit is contained in:
Dennis
2026-10-17 00:24:11 +02:00
committed by GitHub
parent fd10f89586
commit 428aefe8d1
50 changed files with 26474 additions and 74 deletions
+1
View File
@@ -87,6 +87,7 @@ export * from "./cron";
export * from "./tts";
export * from "./watch-download";
export * from "./podcast";
export * from "./regex-metadata";
export * from "./delivery";
export * from "./djvu";
export * from "./email";
+2
View File
@@ -1,3 +1,4 @@
import type { RegexMetadataConfig } from "./regex-metadata";
import type { LibraryLastScan } from "./scanner";
export type OrganizationMode = "book_per_file" | "book_per_folder";
@@ -59,6 +60,7 @@ export interface Library {
watchLocalFolders?: boolean;
autoScanCronExpression?: string | null;
metadataPrecedence: string[];
regexMetadata?: RegexMetadataConfig | null;
formatPriority: string[];
allowedFormats: string[];
organizationMode: OrganizationMode;
+48
View File
@@ -0,0 +1,48 @@
export const REGEX_METADATA_LIMITS = {
rules: 20,
patternLength: 2048,
pathLength: 4096,
} as const;
export const REGEX_METADATA_GROUPS = ["title", "author", "authors", "series", "seriesIndex", "publishedYear", "publishedDate"] as const;
export interface RegexMetadataRule {
pattern: string;
flags: string;
}
export interface RegexMetadataConfig {
rules: RegexMetadataRule[];
}
export interface RegexMetadataFields {
title?: string;
authors?: string[];
seriesName?: string;
seriesIndex?: string;
publishedYear?: number;
publishedDate?: string;
}
export type RegexMetadataDiagnosticCode =
"invalid_pattern" | "missing_group" | "conflicting_authors" | "invalid_value" | "timeout" | "busy" | "worker_failed";
export interface RegexMetadataDiagnostic {
code: RegexMetadataDiagnosticCode;
ruleIndex?: number;
field?: string;
}
export interface RegexMetadataPreview {
inputPath: string;
matched: boolean;
ruleIndex: number | null;
groups: Record<string, string>;
metadata: RegexMetadataFields;
diagnostics: RegexMetadataDiagnostic[];
}
export interface RegexMetadataValidation {
valid: boolean;
diagnostics: RegexMetadataDiagnostic[];
}