use std::collections::BTreeMap;
use std::io::{self, Seek, Write};
use std::path::Path;
use std::sync::{Arc, RwLock};
use crate::export::{Azw3Exporter, EpubExporter, Exporter, KfxExporter, TextExporter, TextFormat};
use crate::import::{
Azw3Importer, ChapterId, EpubImporter, Importer, KfxImporter, MobiImporter, SpineEntry,
};
use crate::io::MemorySource;
use crate::ir::IRChapter;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Format {
Epub,
Azw3,
Mobi,
Kfx,
Text,
Markdown,
}
#[derive(Debug, Clone)]
pub struct Resource {
pub data: Vec<u8>,
pub media_type: String,
}
#[derive(Debug, Clone, Default)]
pub struct Contributor {
pub name: String,
pub file_as: Option<String>,
pub role: Option<String>,
}
#[derive(Debug, Clone)]
pub struct CollectionInfo {
pub name: String,
pub collection_type: Option<String>,
pub position: Option<f64>,
}
#[derive(Debug, Clone, Default)]
pub struct Metadata {
pub title: String,
pub authors: Vec<String>,
pub language: String,
pub identifier: String,
pub publisher: Option<String>,
pub description: Option<String>,
pub subjects: Vec<String>,
pub date: Option<String>,
pub rights: Option<String>,
pub cover_image: Option<String>,
pub modified_date: Option<String>,
pub contributors: Vec<Contributor>,
pub title_sort: Option<String>,
pub author_sort: Option<String>,
pub collection: Option<CollectionInfo>,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct TocEntry {
pub title: String,
pub href: String,
pub children: Vec<TocEntry>,
pub play_order: Option<usize>,
}
impl Ord for TocEntry {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.play_order.cmp(&other.play_order)
}
}
impl PartialOrd for TocEntry {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LandmarkType {
Cover,
TitlePage,
Toc,
StartReading,
BodyMatter,
FrontMatter,
BackMatter,
Acknowledgements,
Bibliography,
Glossary,
Index,
Preface,
Endnotes,
Loi,
Lot,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Landmark {
pub landmark_type: LandmarkType,
pub href: String,
pub label: String,
}
pub struct Book {
backend: Box<dyn Importer>,
ir_cache: Arc<RwLock<BTreeMap<ChapterId, Arc<IRChapter>>>>,
}
impl Format {
pub fn from_path(path: impl AsRef<Path>) -> Option<Self> {
path.as_ref()
.extension()
.and_then(|e| e.to_str())
.and_then(|ext| match ext.to_lowercase().as_str() {
"epub" => Some(Format::Epub),
"azw3" => Some(Format::Azw3),
"mobi" => Some(Format::Mobi),
"kfx" => Some(Format::Kfx),
"txt" => Some(Format::Text),
"md" => Some(Format::Markdown),
_ => None,
})
}
pub fn can_import(&self) -> bool {
matches!(
self,
Format::Epub | Format::Azw3 | Format::Mobi | Format::Kfx
)
}
pub fn can_export(&self) -> bool {
!matches!(self, Format::Mobi)
}
}
impl Book {
pub fn open(path: impl AsRef<Path>) -> io::Result<Self> {
let path = path.as_ref();
let format = Format::from_path(path).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("unknown file format: {}", path.display()),
)
})?;
Self::open_format(path, format)
}
pub fn open_format(path: impl AsRef<Path>, format: Format) -> io::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::Text | Format::Markdown => {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
"Text and Markdown formats are export-only",
));
}
};
Ok(Self {
backend,
ir_cache: Arc::new(RwLock::new(BTreeMap::new())),
})
}
pub fn from_bytes(data: &[u8], format: Format) -> io::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::Text | Format::Markdown => {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
"Text and Markdown formats are export-only",
));
}
};
Ok(Self {
backend,
ir_cache: Arc::new(RwLock::new(BTreeMap::new())),
})
}
pub fn metadata(&self) -> &Metadata {
self.backend.metadata()
}
pub fn toc(&self) -> &[TocEntry] {
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(&mut self, id: ChapterId) -> io::Result<Vec<u8>> {
self.backend.load_raw(id)
}
pub fn load_chapter(&mut self, id: ChapterId) -> io::Result<IRChapter> {
self.backend.load_chapter(id)
}
pub fn load_chapter_cached(&mut self, id: ChapterId) -> io::Result<Arc<IRChapter>> {
{
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 clear_cache(&mut self) {
if let Ok(mut cache) = self.ir_cache.write() {
cache.clear();
}
}
pub fn load_asset(&mut self, path: &Path) -> io::Result<Vec<u8>> {
self.backend.load_asset(path)
}
pub fn list_assets(&self) -> Vec<std::path::PathBuf> {
self.backend.list_assets()
}
pub fn requires_normalized_export(&self) -> bool {
self.backend.requires_normalized_export()
}
pub fn export<W: Write + Seek>(&mut self, format: Format, writer: &mut W) -> io::Result<()> {
match format {
Format::Epub => EpubExporter::new().export(self, writer),
Format::Azw3 => Azw3Exporter::new().export(self, writer),
Format::Text => TextExporter::new()
.format(TextFormat::Plain)
.export(self, writer),
Format::Markdown => TextExporter::new()
.format(TextFormat::Markdown)
.export(self, writer),
Format::Kfx => KfxExporter::new().export(self, writer),
Format::Mobi => Err(io::Error::new(
io::ErrorKind::Unsupported,
format!("{:?} export is not supported", format),
)),
}
}
}
impl TocEntry {
pub fn new(title: impl Into<String>, href: impl Into<String>) -> Self {
Self {
title: title.into(),
href: href.into(),
children: Vec::new(),
play_order: None,
}
}
}