feat: add EPUB format support with spine-aware image extraction
Parse EPUB structure (container.xml → OPF → spine → XHTML) to extract images in reading order. Zero new dependencies — reuses zip + regex crates with pre-compiled regexes and per-file index cache for performance. Falls back to CBZ-style image listing when spine contains no images. Includes DB migration, API/indexer/backoffice updates. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -102,7 +102,7 @@ pub struct BookDetails {
|
||||
tag = "books",
|
||||
params(
|
||||
("library_id" = Option<String>, Query, description = "Filter by library ID"),
|
||||
("kind" = Option<String>, Query, description = "Filter by book kind (cbz, cbr, pdf)"),
|
||||
("kind" = Option<String>, Query, description = "Filter by book kind (cbz, cbr, pdf, epub)"),
|
||||
("series" = Option<String>, Query, description = "Filter by series name (use 'unclassified' for books without series)"),
|
||||
("reading_status" = Option<String>, Query, description = "Filter by reading status, comma-separated (e.g. 'unread,reading')"),
|
||||
("page" = Option<i64>, Query, description = "Page number (1-indexed, default 1)"),
|
||||
|
||||
@@ -351,6 +351,7 @@ async fn prefetch_page(state: AppState, params: &PrefetchParams<'_>) {
|
||||
Some(ref e) if e == "cbz" => "cbz",
|
||||
Some(ref e) if e == "cbr" => "cbr",
|
||||
Some(ref e) if e == "pdf" => "pdf",
|
||||
Some(ref e) if e == "epub" => "epub",
|
||||
_ => return,
|
||||
}
|
||||
.to_string();
|
||||
@@ -479,6 +480,7 @@ fn render_page(
|
||||
"cbz" => parsers::BookFormat::Cbz,
|
||||
"cbr" => parsers::BookFormat::Cbr,
|
||||
"pdf" => parsers::BookFormat::Pdf,
|
||||
"epub" => parsers::BookFormat::Epub,
|
||||
_ => return Err(ApiError::bad_request("unsupported source format")),
|
||||
};
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ pub struct SearchResponse {
|
||||
params(
|
||||
("q" = String, Query, description = "Search query (books + series via PostgreSQL full-text)"),
|
||||
("library_id" = Option<String>, Query, description = "Filter by library ID"),
|
||||
("type" = Option<String>, Query, description = "Filter by type (cbz, cbr, pdf)"),
|
||||
("type" = Option<String>, Query, description = "Filter by type (cbz, cbr, pdf, epub)"),
|
||||
("kind" = Option<String>, Query, description = "Filter by kind (alias for type)"),
|
||||
("limit" = Option<usize>, Query, description = "Max results per type (max 100)"),
|
||||
),
|
||||
|
||||
@@ -115,6 +115,7 @@ export function BookCard({ book, readingStatus }: BookCardProps) {
|
||||
${(book.format ?? book.kind) === 'cbz' ? 'bg-success/10 text-success' : ''}
|
||||
${(book.format ?? book.kind) === 'cbr' ? 'bg-warning/10 text-warning' : ''}
|
||||
${(book.format ?? book.kind) === 'pdf' ? 'bg-destructive/10 text-destructive' : ''}
|
||||
${(book.format ?? book.kind) === 'epub' ? 'bg-info/10 text-info' : ''}
|
||||
`}>
|
||||
{book.format ?? book.kind}
|
||||
</span>
|
||||
|
||||
@@ -290,6 +290,7 @@ fn book_format_from_str(s: &str) -> Option<BookFormat> {
|
||||
"cbz" => Some(BookFormat::Cbz),
|
||||
"cbr" => Some(BookFormat::Cbr),
|
||||
"pdf" => Some(BookFormat::Pdf),
|
||||
"epub" => Some(BookFormat::Epub),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ pub fn compute_fingerprint(path: &Path, size: u64, mtime: &DateTime<Utc>) -> Res
|
||||
|
||||
pub fn kind_from_format(format: BookFormat) -> &'static str {
|
||||
match format {
|
||||
BookFormat::Pdf => "ebook",
|
||||
BookFormat::Pdf | BookFormat::Epub => "ebook",
|
||||
BookFormat::Cbz | BookFormat::Cbr => "comic",
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user