novel-cli 0.17.0

A set of tools for downloading novels from the web, manipulating text, and generating EPUB
Documentation
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,
}