use std::fs;
use std::io::Cursor;
use std::path::{Path, PathBuf};
use clap::Args;
use color_eyre::eyre::{self, Result};
use fluent_templates::Loader;
use quick_xml::events::{BytesText, Event};
use quick_xml::{Reader, Writer};
use serde::Deserialize;
use crate::cmd::Convert;
use crate::{LANG_ID, LOCALES, utils};
#[must_use]
#[derive(Args)]
#[command(arg_required_else_help = true,
about = LOCALES.lookup(&LANG_ID, "epub_command"))]
pub struct Epub {
#[arg(help = LOCALES.lookup(&LANG_ID, "epub_path"))]
pub epub_path: PathBuf,
#[arg(short, long, value_enum, value_delimiter = ',',
required = true, help = LOCALES.lookup(&LANG_ID, "converts"))]
pub converts: Vec<Convert>,
#[arg(short, long, default_value_t = false,
help = LOCALES.lookup(&LANG_ID, "delete"))]
pub delete: bool,
}
pub fn execute(config: Epub) -> Result<()> {
utils::ensure_epub_file(&config.epub_path)?;
let epub_file_path = dunce::canonicalize(&config.epub_path)?;
tracing::info!("Input file path: `{}`", epub_file_path.display());
let epub_file_stem = epub_file_path.file_stem().unwrap().to_str().unwrap();
let epub_dir_path = epub_file_path.with_extension("");
super::unzip(&epub_file_path)?;
let container_file_path = epub_dir_path.join("META-INF").join("container.xml");
let bytes = fs::read(&container_file_path)?;
let container = simdutf8::basic::from_utf8(&bytes)?;
let mut container: Container = quick_xml::de::from_str(container)?;
let rootfile_path = epub_dir_path.join(container.rootfiles.rootfiles.remove(0).full_path);
let bytes = fs::read(&rootfile_path)?;
let rootfile = simdutf8::basic::from_utf8(&bytes)?;
let rootfile: Package = quick_xml::de::from_str(rootfile)?;
let rootfile_parent_path = rootfile_path.parent().unwrap();
for item in rootfile.manifest.items {
if ["application/xhtml+xml", "application/x-dtbncx+xml"].contains(&item.media_type.as_str())
{
let content_file_path = rootfile_parent_path.join(item.href);
convert_xml(&content_file_path, &config.converts)?;
}
}
convert_xml(&rootfile_path, &config.converts)?;
if config.delete {
utils::remove_file_or_dir(&epub_file_path)?;
} else {
let backup_file_path = epub_file_path.with_file_name(format!("{epub_file_stem}.old.epub"));
tracing::info!("Backup file path: `{}`", backup_file_path.display());
fs::rename(&epub_file_path, backup_file_path)?;
}
let new_epub_file_stem = utils::convert_str(epub_file_stem, &config.converts, false)?;
novel_api::zip_dir(
&epub_dir_path,
epub_file_path.with_file_name(format!("{new_epub_file_stem}.epub")),
)?;
utils::remove_file_or_dir(&epub_dir_path)?;
Ok(())
}
fn convert_xml<T, E>(xml_file_path: T, converts: E) -> Result<()>
where
T: AsRef<Path>,
E: AsRef<[Convert]>,
{
let bytes = fs::read(xml_file_path.as_ref())?;
let xml_content = simdutf8::basic::from_utf8(&bytes)?;
let mut reader = Reader::from_str(xml_content);
reader.config_mut().trim_text(true);
let mut writer = Writer::new(Cursor::new(Vec::new()));
loop {
match reader.read_event() {
Ok(Event::Text(e)) => {
let content = utils::convert_str(&e.decode()?, &converts, false)?;
writer.write_event(Event::Text(BytesText::new(&content)))?
}
Ok(Event::Eof) => break,
Ok(e) => writer.write_event(e)?,
Err(e) => eyre::bail!("Error at position {}: {:?}", reader.error_position(), e),
}
}
let result = writer.into_inner().into_inner();
fs::write(xml_file_path.as_ref(), &result)?;
Ok(())
}
#[derive(Deserialize)]
struct Container {
rootfiles: Rootfiles,
}
#[derive(Deserialize)]
struct Rootfiles {
#[serde(rename = "rootfile")]
rootfiles: Vec<Rootfile>,
}
#[derive(Deserialize)]
struct Rootfile {
#[serde(rename = "@full-path")]
full_path: PathBuf,
}
#[derive(Deserialize)]
struct Package {
manifest: Manifest,
}
#[derive(Deserialize)]
struct Manifest {
#[serde(rename = "item")]
items: Vec<Item>,
}
#[derive(Deserialize)]
struct Item {
#[serde(rename = "@href")]
href: PathBuf,
#[serde(rename = "@media-type")]
media_type: String,
}