use std::collections::HashMap;
use std::io::{self, Seek, Write};
use std::path::Path;
use std::sync::{Arc, OnceLock, RwLock};
use crate::export::{Azw3Exporter, EpubExporter, Exporter, KfxExporter, MarkdownExporter};
use crate::import::{
Azw3Importer, ChapterId, EpubImporter, Importer, KfxImporter, MobiImporter, SpineEntry,
};
use crate::io::MemorySource;
use crate::model::{AnchorTarget, Chapter, Format, Landmark, Metadata, ResolvedLinks, TocEntry};
use crate::resolved::resolve_book_links;
pub struct Book {
backend: Box<dyn Importer>,
ir_cache: Arc<RwLock<HashMap<ChapterId, Arc<Chapter>>>>,
fixed_toc: OnceLock<Vec<TocEntry>>,
targeted_toc: OnceLock<Vec<TocEntry>>,
resolved_links: OnceLock<Arc<ResolvedLinks>>,
}
impl Book {
pub fn open(path: impl AsRef<Path>) -> crate::Result<Self> {
let path = path.as_ref();
let format = Format::from_path(path).ok_or_else(|| crate::Error::UnsupportedFormat {
detail: format!("unknown file format: {}", path.display()),
})?;
Self::open_format(path, format)
}
pub fn open_format(path: impl AsRef<Path>, format: Format) -> crate::Result<Self> {
let backend: Box<dyn Importer> = match format {
Format::Epub => Box::new(EpubImporter::open(path.as_ref())?),
Format::Azw3 => Box::new(Azw3Importer::open(path.as_ref())?),
Format::Mobi => Box::new(MobiImporter::open(path.as_ref())?),
Format::Kfx => Box::new(KfxImporter::open(path.as_ref())?),
Format::Markdown => {
return Err(crate::Error::UnsupportedFormat {
detail: "Markdown format is export-only".into(),
});
}
};
Ok(Self::from_backend(backend))
}
fn from_backend(backend: Box<dyn Importer>) -> Self {
Self {
backend,
ir_cache: Arc::new(RwLock::new(HashMap::new())),
fixed_toc: OnceLock::new(),
targeted_toc: OnceLock::new(),
resolved_links: OnceLock::new(),
}
}
pub fn from_bytes(data: &[u8], format: Format) -> crate::Result<Self> {
let source = Arc::new(MemorySource::new(data.to_vec()));
let backend: Box<dyn Importer> = match format {
Format::Epub => Box::new(EpubImporter::from_source(source)?),
Format::Azw3 => Box::new(Azw3Importer::from_source(source)?),
Format::Mobi => Box::new(MobiImporter::from_source(source)?),
Format::Kfx => Box::new(KfxImporter::from_source(source)?),
Format::Markdown => {
return Err(crate::Error::UnsupportedFormat {
detail: "Markdown format is export-only".into(),
});
}
};
Ok(Self::from_backend(backend))
}
pub fn metadata(&self) -> &Metadata {
self.backend.metadata()
}
pub fn toc(&self) -> &[TocEntry] {
if let Some(toc) = self.targeted_toc.get() {
return toc;
}
if let Some(toc) = self.fixed_toc.get() {
return toc;
}
self.backend.toc()
}
pub fn landmarks(&self) -> &[Landmark] {
self.backend.landmarks()
}
pub fn spine(&self) -> &[SpineEntry] {
self.backend.spine()
}
pub fn source_id(&self, id: ChapterId) -> Option<&str> {
self.backend.source_id(id)
}
pub fn load_raw(&self, id: ChapterId) -> crate::Result<Vec<u8>> {
self.backend.load_raw(id)
}
pub fn load_chapter(&self, id: ChapterId) -> crate::Result<Chapter> {
self.backend.load_chapter(id)
}
pub fn load_chapter_cached(&self, id: ChapterId) -> crate::Result<Arc<Chapter>> {
{
let cache = self
.ir_cache
.read()
.map_err(|_| io::Error::other("IR cache lock poisoned"))?;
if let Some(chapter) = cache.get(&id) {
return Ok(Arc::clone(chapter));
}
}
let chapter = self.backend.load_chapter(id)?;
let chapter_arc = Arc::new(chapter);
{
let mut cache = self
.ir_cache
.write()
.map_err(|_| io::Error::other("IR cache lock poisoned"))?;
cache.insert(id, Arc::clone(&chapter_arc));
}
Ok(chapter_arc)
}
pub fn load_chapters_cached(&self, ids: &[ChapterId]) -> crate::Result<Vec<Arc<Chapter>>> {
let missing: Vec<ChapterId> = {
let cache = self
.ir_cache
.read()
.map_err(|_| io::Error::other("IR cache lock poisoned"))?;
ids.iter()
.copied()
.filter(|id| !cache.contains_key(id))
.collect()
};
if !missing.is_empty() {
let loaded = self.backend.load_chapters(&missing);
let mut cache = self
.ir_cache
.write()
.map_err(|_| io::Error::other("IR cache lock poisoned"))?;
for (id, chapter) in missing.into_iter().zip(loaded) {
cache.insert(id, Arc::new(chapter?));
}
}
let cache = self
.ir_cache
.read()
.map_err(|_| io::Error::other("IR cache lock poisoned"))?;
ids.iter()
.map(|id| {
cache
.get(id)
.cloned()
.ok_or_else(|| crate::Error::NotFound {
what: format!("chapter {}", id.0),
})
})
.collect()
}
pub fn clear_cache(&self) {
if let Ok(mut cache) = self.ir_cache.write() {
cache.clear();
}
}
pub fn resolve_links(&self) -> crate::Result<Arc<ResolvedLinks>> {
if let Some(resolved) = self.resolved_links.get() {
return Ok(Arc::clone(resolved));
}
let resolved = Arc::new(resolve_book_links(self)?);
Ok(Arc::clone(self.resolved_links.get_or_init(|| resolved)))
}
pub(crate) fn index_anchors(&self, chapters: &[(ChapterId, Arc<Chapter>)]) {
self.backend.index_anchors(chapters);
}
pub(crate) fn resolve_toc(&self) {
if self.fixed_toc.get().is_none()
&& let Some(fixed) = self.backend.resolve_toc()
{
let _ = self.fixed_toc.set(fixed);
}
}
pub(crate) fn resolve_toc_targets(&self) {
if self.targeted_toc.get().is_some() {
return;
}
fn apply_targets(entries: &mut [TocEntry], backend: &dyn Importer) {
for entry in entries {
entry.target = backend.resolve_href(ChapterId(0), &entry.href);
apply_targets(&mut entry.children, backend);
}
}
let mut toc = self
.fixed_toc
.get()
.cloned()
.unwrap_or_else(|| self.backend.toc().to_vec());
apply_targets(&mut toc, &*self.backend);
let _ = self.targeted_toc.set(toc);
}
pub(crate) fn resolve_href(&self, from_chapter: ChapterId, href: &str) -> Option<AnchorTarget> {
self.backend.resolve_href(from_chapter, href)
}
pub fn load_asset(&self, path: &str) -> crate::Result<Vec<u8>> {
self.backend.load_asset(path)
}
pub fn list_assets(&self) -> &[String] {
self.backend.list_assets()
}
pub fn font_faces(&self) -> Vec<crate::model::FontFace> {
self.backend.font_faces()
}
pub fn requires_normalized_export(&self) -> bool {
self.backend.requires_normalized_export()
}
pub fn export<W: Write + Seek>(&self, format: Format, writer: &mut W) -> crate::Result<()> {
match format {
Format::Epub => EpubExporter::new().export(self, writer),
Format::Azw3 => Azw3Exporter::new().export(self, writer),
Format::Markdown => MarkdownExporter::new().export(self, writer),
Format::Kfx => KfxExporter::new().export(self, writer),
Format::Mobi => Err(crate::Error::UnsupportedFormat {
detail: format!("{:?} export is not supported", format),
}),
}
}
}