comics: add .cbr import support behind an optional cbr feature
Build Debug APK / build (push) Failing after 9m27s
Build Debug APK / build (push) Failing after 9m27s
Adds import_cbr_file + extract_cbr_pages to rust/lib/src/agents/comics.rs (the unrar crate wraps unrar's C source, which reads from a file path rather than an in-memory buffer like `zip`, unlike the .cbz path). Both share the same import_comic_from_pages builder and compare_page_names ordering as .cbz and directory import, so a .cbr gets the identical Comic-container/Image-children tree. RAR support is decode-only by construction: a .cbr is decomposed and its bytes discarded at import, same as .cbz, so export (if implemented) would only ever need to write a .cbz. This is why pulling in unrar's C source is worth it despite the licensing quirks around RAR compressors — we only ever need the free-to-use decoder. Kept optional (new `cbr` Cargo feature on lib, mirroring `multimedia`) rather than a hard dependency, since unrar's bundled C source is a native build-time dependency not worth risking on the Android NDK cross-compile path, which has no comics UI wired up yet anyway. cli-app and gui-app both opt in explicitly; the default `synchronicity_lib` build (what Android's build-rust recipe uses) is unaffected. Wires dispatch-by-extension into every comic import call site: the generic documents::import::import_comic adapter (feature-gated internally so it still compiles without cbr), cli-app's two hand-rolled import dispatches (now sharing one import_comic_by_path helper), and gui-app's Comics agent file-picker import (button relabeled "Import .cbz/.cbr…"). COMIC_FILE_TYPES gains the cbr extension/mime-types so every picker/filter derived from it picks this up automatically. Testing: RAR compression is proprietary, so there's no way to build a .cbr fixture in-process the way build_cbz builds one — committed a small real RAR5 fixture (rust/lib/src/agents/comics_fixtures/sample.cbr, generated with the actual `rar` tool) and two tests against it, mirroring the .cbz cases. 16/16 comics tests pass with --features cbr; the default build (no cbr) still compiles clean with all 221 lib tests passing. Verified end to end via `just build-gui-nix` (nix build .#syn-gui) with the new unrar/unrar_sys native dependency: builds clean, resulting binary runs (`syn-gui --help` prints usage, exit 0).
This commit is contained in:
@@ -50,14 +50,16 @@
|
||||
|
||||
# Source filtering — include Cargo sources, .slint UI files (the archived
|
||||
# gui-slint app), and data files referenced by include_str!() / include_bytes!():
|
||||
# .txt (lib's wordlist) and .svg (gui-app's icons, baked in via assets.rs).
|
||||
# .txt (lib's wordlist), .svg (gui-app's icons, baked in via assets.rs), and
|
||||
# .cbr (comics.rs's cfg(test)-only RAR fixture).
|
||||
rustSrc = pkgs.lib.cleanSourceWith {
|
||||
src = ./rust;
|
||||
filter = path: type:
|
||||
(craneLib.filterCargoSources path type)
|
||||
|| (builtins.match ".*\\.slint$" path != null)
|
||||
|| (builtins.match ".*\\.txt$" path != null)
|
||||
|| (builtins.match ".*\\.svg$" path != null);
|
||||
|| (builtins.match ".*\\.svg$" path != null)
|
||||
|| (builtins.match ".*\\.cbr$" path != null);
|
||||
};
|
||||
|
||||
# Common arguments shared across all builds
|
||||
|
||||
Generated
+24
@@ -10085,6 +10085,7 @@ dependencies = [
|
||||
"typst",
|
||||
"typst-pdf",
|
||||
"uniffi",
|
||||
"unrar",
|
||||
"ur",
|
||||
"uuid",
|
||||
"webpki-roots",
|
||||
@@ -11477,6 +11478,29 @@ dependencies = [
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unrar"
|
||||
version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ec61343a630d2b50d13216dea5125e157d3fc180a7d3f447d22fe146b648fc"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"regex",
|
||||
"unrar_sys",
|
||||
"widestring",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unrar_sys"
|
||||
version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b77675b883cfbe6bf41e6b7a5cd6008e0a83ba497de3d96e41a064bbeead765"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unsafe-libyaml"
|
||||
version = "0.2.11"
|
||||
|
||||
@@ -9,7 +9,7 @@ path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
syn-daemon = { path = "../syn-daemon" }
|
||||
synchronicity = { path = "../lib", package = "synchronicity_lib" }
|
||||
synchronicity = { path = "../lib", package = "synchronicity_lib", features = ["cbr"] }
|
||||
anyhow = { workspace = true }
|
||||
iroh = { workspace = true }
|
||||
pkarr = { workspace = true }
|
||||
|
||||
@@ -178,6 +178,25 @@ fn build_command(cmd: cli::AgentCmd) -> Result<DaemonCommand> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Import a single comic archive, dispatching on extension to the `.cbz` or `.cbr`
|
||||
/// importer. Shared by [`cmd_docs_import`] and [`import_directory_tree`] so the two
|
||||
/// hand-rolled per-file-type dispatches stay in sync on this one point.
|
||||
fn import_comic_by_path(
|
||||
blob_store: &synchronicity::stores::blob_store::BlobStore,
|
||||
doc_store: &synchronicity::stores::document::DocumentStore,
|
||||
path: &Path,
|
||||
node: synchronicity::node::NodePubkey,
|
||||
parent: Option<&DocumentID>,
|
||||
) -> Result<DocumentID> {
|
||||
use synchronicity::agents::comics::{import_cbr_file, import_cbz_file};
|
||||
|
||||
if path.extension().and_then(|e| e.to_str()).is_some_and(|e| e.eq_ignore_ascii_case("cbr")) {
|
||||
import_cbr_file(blob_store, doc_store, path, node, parent).map_err(anyhow::Error::from)
|
||||
} else {
|
||||
import_cbz_file(blob_store, doc_store, path, node, parent).map_err(anyhow::Error::from)
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_docs_import(
|
||||
store_path: &Path,
|
||||
paths: Vec<std::path::PathBuf>,
|
||||
@@ -185,7 +204,7 @@ fn cmd_docs_import(
|
||||
as_comic: bool,
|
||||
) -> Result<()> {
|
||||
use colored::Colorize;
|
||||
use synchronicity::agents::comics::{import_cbz_file, import_comic_directory};
|
||||
use synchronicity::agents::comics::import_comic_directory;
|
||||
use synchronicity::agents::music::import_mp3_file;
|
||||
use synchronicity::agents::pdf::import_pdf_file;
|
||||
use synchronicity::agents::pictures::import_image_file;
|
||||
@@ -278,8 +297,7 @@ fn cmd_docs_import(
|
||||
import_mp3_file(&store.blob_store, &store.doc_store, path, local_node, None)
|
||||
.map_err(anyhow::Error::from)
|
||||
} else if COMIC_FILE_TYPES.matches_path(path) {
|
||||
import_cbz_file(&store.blob_store, &store.doc_store, path, local_node, None)
|
||||
.map_err(anyhow::Error::from)
|
||||
import_comic_by_path(&store.blob_store, &store.doc_store, path, local_node, None)
|
||||
} else {
|
||||
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("(none)");
|
||||
eprintln!(
|
||||
@@ -351,7 +369,6 @@ fn import_directory_tree(
|
||||
imported: &mut usize,
|
||||
) {
|
||||
use colored::Colorize;
|
||||
use synchronicity::agents::comics::import_cbz_file;
|
||||
use synchronicity::agents::music::import_mp3_file;
|
||||
use synchronicity::agents::pdf::import_pdf_file;
|
||||
use synchronicity::agents::pictures::import_image_file;
|
||||
@@ -417,8 +434,7 @@ fn import_directory_tree(
|
||||
import_mp3_file(&store.blob_store, &store.doc_store, &path, node, parent)
|
||||
.map_err(anyhow::Error::from)
|
||||
} else if COMIC_FILE_TYPES.matches_path(&path) {
|
||||
import_cbz_file(&store.blob_store, &store.doc_store, &path, node, parent)
|
||||
.map_err(anyhow::Error::from)
|
||||
import_comic_by_path(&store.blob_store, &store.doc_store, &path, node, parent)
|
||||
} else {
|
||||
log::info!("Skipping unsupported file: {}", path.display());
|
||||
continue;
|
||||
|
||||
@@ -10,7 +10,7 @@ path = "src/main.rs"
|
||||
[dependencies]
|
||||
gpui = { package = "gpui", git = "https://github.com/gpui-ce/gpui-ce" }
|
||||
gpui_platform = { git = "https://github.com/gpui-ce/gpui-ce" }
|
||||
synchronicity = { path = "../lib", package = "synchronicity_lib", features = ["multimedia"] }
|
||||
synchronicity = { path = "../lib", package = "synchronicity_lib", features = ["multimedia", "cbr"] }
|
||||
anyhow = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
chrono-tz = { workspace = true }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! The Comics & Manga agent screen: a [`MediaLibrary`] of `.cbz` comics (cards or
|
||||
//! The Comics & Manga agent screen: a [`MediaLibrary`] of `.cbz`/`.cbr` comics (cards or
|
||||
//! list, under a Library / Recommended tab bar) that opens into a page reader with
|
||||
//! a back button.
|
||||
//!
|
||||
@@ -21,9 +21,9 @@ use gpui::{
|
||||
};
|
||||
use synchronicity::agents::AgentId;
|
||||
use synchronicity::agents::comics::{
|
||||
ComicListItem, build_comic_nodes, import_cbz_file, import_comic_directory, is_supported_comic,
|
||||
list_comic_pages, load_comic_cover_bytes, load_comic_page_bytes, load_reading_page,
|
||||
save_reading_page,
|
||||
ComicListItem, build_comic_nodes, import_cbr_file, import_cbz_file, import_comic_directory,
|
||||
is_supported_comic, list_comic_pages, load_comic_cover_bytes, load_comic_page_bytes,
|
||||
load_reading_page, save_reading_page,
|
||||
};
|
||||
use synchronicity::documents::DocumentID;
|
||||
use synchronicity::documents::import::COMIC_FILE_TYPES;
|
||||
@@ -206,15 +206,19 @@ impl ComicsView {
|
||||
.spawn(async move {
|
||||
let mut errors: Vec<String> = Vec::new();
|
||||
for path in &paths {
|
||||
if is_supported_comic(path)
|
||||
&& let Err(e) = import_cbz_file(
|
||||
&store.blob_store,
|
||||
&store.doc_store,
|
||||
path,
|
||||
node,
|
||||
None,
|
||||
)
|
||||
{
|
||||
if !is_supported_comic(path) {
|
||||
continue;
|
||||
}
|
||||
let is_cbr = path
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.is_some_and(|e| e.eq_ignore_ascii_case("cbr"));
|
||||
let result = if is_cbr {
|
||||
import_cbr_file(&store.blob_store, &store.doc_store, path, node, None)
|
||||
} else {
|
||||
import_cbz_file(&store.blob_store, &store.doc_store, path, node, None)
|
||||
};
|
||||
if let Err(e) = result {
|
||||
errors.push(format!("Failed to import {}: {e}", path.display()));
|
||||
}
|
||||
}
|
||||
@@ -232,7 +236,7 @@ impl ComicsView {
|
||||
|
||||
/// Import a directory of sequentially-named page images (e.g. `01.jpg`, `02.jpg`,
|
||||
/// ...) as a single Comic document, exactly as [`import_comics`](Self::import_comics)
|
||||
/// does for a picked `.cbz` file.
|
||||
/// does for a picked `.cbz`/`.cbr` file.
|
||||
fn import_comic_folder(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(path) = rfd::FileDialog::new().set_title("Import Comic Folder").pick_folder()
|
||||
else {
|
||||
@@ -470,7 +474,7 @@ impl ComicsView {
|
||||
)
|
||||
.when(self.open.is_none(), |d| {
|
||||
d.child(
|
||||
button(theme, "comics-import", "Import .cbz…", true, true)
|
||||
button(theme, "comics-import", "Import .cbz/.cbr…", true, true)
|
||||
.on_click(cx.listener(|this, _, _, cx| this.import_comics(cx))),
|
||||
)
|
||||
.child(
|
||||
|
||||
@@ -56,9 +56,16 @@ quick-xml = { workspace = true }
|
||||
symphonia = { version = "0.5", features = ["mp3"], optional = true }
|
||||
rodio = { version = "0.21", optional = true }
|
||||
soundtouch = { version = "0.5", features = ["bundled"], optional = true }
|
||||
unrar = { version = "0.5.8", optional = true }
|
||||
|
||||
[features]
|
||||
multimedia = ["dep:rodio", "dep:symphonia", "dep:soundtouch"]
|
||||
# .cbr comic import (RAR decode only — a .cbr is never written, only read at import
|
||||
# time and decomposed into the same Comic/Image document tree as .cbz). Kept optional,
|
||||
# mirroring `multimedia`: unrar's bundled C source is a native build-time dependency
|
||||
# not worth risking on the Android NDK cross-compile path, which doesn't wire up a
|
||||
# comics UI yet anyway. cli-app and gui-app opt in explicitly.
|
||||
cbr = ["dep:unrar"]
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { workspace = true }
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
//! The Comics & Manga agent: importing, reading, and tracking `.cbz` comic
|
||||
//! archives as [`Comic`](DocumentType::Comic) documents.
|
||||
//! The Comics & Manga agent: importing, reading, and tracking `.cbz`/`.cbr` comic
|
||||
//! archives (and directories of page images — see [`import_comic_directory`]) as
|
||||
//! [`Comic`](DocumentType::Comic) documents.
|
||||
//!
|
||||
//! A `.cbz` file is a ZIP archive whose entries are page images (JPEG/PNG/…),
|
||||
//! conventionally named so a lexical sort yields reading order, optionally
|
||||
//! accompanied by a `ComicInfo.xml` metadata sidecar (the ComicRack standard).
|
||||
//! A `.cbz` file is a ZIP archive, and a `.cbr` file a RAR archive (behind the `cbr`
|
||||
//! Cargo feature, since it pulls in unrar's native C source — see that feature's doc
|
||||
//! comment in `Cargo.toml`), whose entries are page images (JPEG/PNG/…), conventionally
|
||||
//! named so a lexical sort yields reading order, optionally accompanied by a
|
||||
//! `ComicInfo.xml` metadata sidecar (the ComicRack standard). RAR support is decode-only
|
||||
//! by construction: see [`import_cbr_file`]'s doc comment for why that's not a
|
||||
//! limitation here.
|
||||
//!
|
||||
//! Unlike a PDF (whose bytes are one opaque blob), a comic is decomposed at
|
||||
//! import time: the archive is unpacked into one [`Image`](DocumentType::Image)
|
||||
@@ -141,6 +146,9 @@ pub enum ImportComicError {
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("not a valid .cbz archive: {0}")]
|
||||
Zip(#[from] zip::result::ZipError),
|
||||
#[cfg(feature = "cbr")]
|
||||
#[error("not a valid .cbr archive: {0}")]
|
||||
Rar(#[from] unrar::error::UnrarError),
|
||||
#[error("archive contains no page images")]
|
||||
NoPages,
|
||||
#[error(transparent)]
|
||||
@@ -169,6 +177,58 @@ pub fn import_cbz_file(
|
||||
import_comic_from_pages(blob_store, doc_store, &stem, &metadata, pages, local_node, parent_id)
|
||||
}
|
||||
|
||||
/// Import a `.cbr` (RAR) file as a new [`Comic`](DocumentType::Comic) document, exactly
|
||||
/// as [`import_cbz_file`] does for a `.cbz`. RAR support is read-only by design: a
|
||||
/// `.cbr` is decomposed into the same page-per-`Image`-child tree and its bytes
|
||||
/// discarded, so this never needs to *write* a RAR archive (export, if implemented,
|
||||
/// would only ever produce a `.cbz`).
|
||||
#[cfg(feature = "cbr")]
|
||||
pub fn import_cbr_file(
|
||||
blob_store: &BlobStore,
|
||||
doc_store: &DocumentStore,
|
||||
path: &Path,
|
||||
local_node: NodePubkey,
|
||||
parent_id: Option<&DocumentID>,
|
||||
) -> Result<DocumentID, ImportComicError> {
|
||||
let (pages, metadata) = extract_cbr_pages(path)?;
|
||||
let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("Untitled").to_string();
|
||||
|
||||
import_comic_from_pages(blob_store, doc_store, &stem, &metadata, pages, local_node, parent_id)
|
||||
}
|
||||
|
||||
/// Read every page image and the `ComicInfo.xml` sidecar (if present) out of a `.cbr`
|
||||
/// archive in one pass, in reading order (see [`compare_page_names`]). The RAR C library
|
||||
/// unrar wraps only reads from a file path, not an in-memory buffer, unlike `zip` — so,
|
||||
/// unlike [`extract_pages`], this takes the archive's path rather than its bytes.
|
||||
#[cfg(feature = "cbr")]
|
||||
fn extract_cbr_pages(
|
||||
path: &Path,
|
||||
) -> Result<(Vec<(String, Vec<u8>)>, ComicMetadata), ImportComicError> {
|
||||
let mut pages: Vec<(String, Vec<u8>)> = Vec::new();
|
||||
let mut comic_info_xml: Option<String> = None;
|
||||
|
||||
let mut archive = unrar::Archive::new(path).open_for_processing()?;
|
||||
while let Some(header) = archive.read_header()? {
|
||||
let entry = header.entry();
|
||||
let name = entry.filename.to_string_lossy().into_owned();
|
||||
archive = if entry.is_file() && is_page_image(&name) {
|
||||
let (data, rest) = header.read()?;
|
||||
pages.push((name, data));
|
||||
rest
|
||||
} else if entry.is_file() && name.eq_ignore_ascii_case(COMIC_INFO_FILE) {
|
||||
let (data, rest) = header.read()?;
|
||||
comic_info_xml = String::from_utf8(data).ok();
|
||||
rest
|
||||
} else {
|
||||
header.skip()?
|
||||
};
|
||||
}
|
||||
pages.sort_by(|(a, _), (b, _)| compare_page_names(a, b));
|
||||
|
||||
let metadata = comic_info_xml.map(|xml| parse_comic_info(&xml)).unwrap_or_default();
|
||||
Ok((pages, metadata))
|
||||
}
|
||||
|
||||
/// Import a directory of sequentially-named page images (`01.jpg`, `02.jpg`, ...) as a
|
||||
/// new [`Comic`](DocumentType::Comic) document, exactly as [`import_cbz_file`] would for
|
||||
/// the equivalent `.cbz` — same page ordering (see [`compare_page_names`]), same
|
||||
@@ -1060,4 +1120,82 @@ mod tests {
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, ImportComicError::NoPages));
|
||||
}
|
||||
|
||||
/// `.cbr` (RAR) tests, gated on the `cbr` feature exactly like the code under test.
|
||||
/// `unrar` reads from a real file path rather than an in-memory buffer (unlike
|
||||
/// `zip`), so these write the fixture out to a tempfile first.
|
||||
///
|
||||
/// `comics_fixtures/sample.cbr` was built with real `rar` (`rar a -ep`, RAR5
|
||||
/// format) from three page files (`001.jpg` = `"one"`, `002.jpg` = `"two"`,
|
||||
/// `003.jpg` = `"three"`) plus a `ComicInfo.xml` — the same fixture shape as
|
||||
/// [`build_cbz`]'s cases above, so these tests mirror the `.cbz` ones directly.
|
||||
/// There's no `unrar`-side equivalent of `ZipWriter` to build one in-process: RAR's
|
||||
/// compressor is proprietary, so — fittingly, given `import_cbr_file` is designed to
|
||||
/// never need to *write* RAR — a small checked-in binary fixture is the pragmatic
|
||||
/// choice here instead.
|
||||
#[cfg(feature = "cbr")]
|
||||
mod cbr {
|
||||
use super::*;
|
||||
|
||||
const SAMPLE_CBR: &[u8] = include_bytes!("comics_fixtures/sample.cbr");
|
||||
|
||||
fn write_cbr(dir: &Path, name: &str) -> std::path::PathBuf {
|
||||
let path = dir.join(name);
|
||||
std::fs::write(&path, SAMPLE_CBR).unwrap();
|
||||
path
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_creates_ordered_page_children_and_no_archive_blob() {
|
||||
let (doc_store, blob_store, dir) = make_stores();
|
||||
let path = write_cbr(dir.path(), "test.cbr");
|
||||
|
||||
let comic_id =
|
||||
import_cbr_file(&blob_store, &doc_store, &path, test_node(), None).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
doc_store.get_document(&comic_id).unwrap().unwrap().doc_type,
|
||||
DocumentType::Comic
|
||||
);
|
||||
assert!(
|
||||
doc_store
|
||||
.get_blob_ref_by_role(&comic_id, &BlobRole::PrimaryContent)
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
|
||||
let pages = list_comic_pages(&doc_store, &comic_id).unwrap();
|
||||
assert_eq!(pages.len(), 3);
|
||||
for (id, expected) in pages.iter().zip([b"one" as &[u8], b"two", b"three"]) {
|
||||
assert_eq!(
|
||||
doc_store.get_document(id).unwrap().unwrap().doc_type,
|
||||
DocumentType::Image
|
||||
);
|
||||
assert_eq!(load_comic_page_bytes(&blob_store, &doc_store, id).unwrap(), expected);
|
||||
}
|
||||
|
||||
assert_eq!(page_count(&doc_store, &comic_id).unwrap(), 3);
|
||||
assert!(comic_is_local(&doc_store, &blob_store, &comic_id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_reads_comic_info_sidecar() {
|
||||
let (doc_store, blob_store, dir) = make_stores();
|
||||
let path = write_cbr(dir.path(), "test.cbr");
|
||||
|
||||
let comic_id =
|
||||
import_cbr_file(&blob_store, &doc_store, &path, test_node(), None).unwrap();
|
||||
|
||||
let meta = comic_metadata(&doc_store, &comic_id).unwrap();
|
||||
assert_eq!(meta.title, "The First Issue");
|
||||
assert_eq!(meta.series, "Saga of Tests");
|
||||
assert_eq!(meta.number, "1");
|
||||
assert_eq!(meta.writer, "Ada Lovelace");
|
||||
assert_eq!(meta.summary, "A thrilling unit test.");
|
||||
// The real extracted page count (3), not the stale <PageCount> of 99.
|
||||
assert_eq!(meta.page_count, 3);
|
||||
// Series wins over the file stem once ComicInfo.xml is present.
|
||||
assert_eq!(doc_store.get_title(&comic_id).unwrap().unwrap(), "Saga of Tests");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -36,8 +36,13 @@ pub const MUSIC_FILE_TYPES: FileTypeFilter =
|
||||
|
||||
pub const COMIC_FILE_TYPES: FileTypeFilter = FileTypeFilter {
|
||||
label: "Comics",
|
||||
extensions: &["cbz"],
|
||||
mime_types: &["application/vnd.comicbook+zip", "application/x-cbz"],
|
||||
extensions: &["cbz", "cbr"],
|
||||
mime_types: &[
|
||||
"application/vnd.comicbook+zip",
|
||||
"application/x-cbz",
|
||||
"application/vnd.comicbook-rar",
|
||||
"application/x-cbr",
|
||||
],
|
||||
};
|
||||
|
||||
/// A supported document type: the filter that recognises it (for file pickers)
|
||||
@@ -107,6 +112,16 @@ fn import_comic(
|
||||
node: NodePubkey,
|
||||
parent: Option<&DocumentID>,
|
||||
) -> Result<DocumentID, String> {
|
||||
// `cbr` is an optional feature (unrar's bundled C source isn't worth risking on
|
||||
// every build, e.g. the Android NDK cross-compile — see its doc comment in
|
||||
// Cargo.toml). When it's off, a `.cbr` falls through to the `.cbz` importer, which
|
||||
// reports a clear-enough "not a valid .cbz archive" rather than silently mis-importing
|
||||
// it; in practice every real consumer (cli-app, gui-app) enables the feature.
|
||||
#[cfg(feature = "cbr")]
|
||||
if path.extension().and_then(|e| e.to_str()).is_some_and(|e| e.eq_ignore_ascii_case("cbr")) {
|
||||
return comics::import_cbr_file(blob_store, doc_store, path, node, parent)
|
||||
.map_err(|e| e.to_string());
|
||||
}
|
||||
comics::import_cbz_file(blob_store, doc_store, path, node, parent).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user