novel_cli/cmd/
epub.rs

1use std::fs::{self, File};
2use std::io::Cursor;
3use std::path::{Path, PathBuf};
4
5use clap::Args;
6use color_eyre::eyre::{self, Result};
7use fluent_templates::Loader;
8use quick_xml::events::{BytesText, Event};
9use quick_xml::{Reader, Writer};
10use serde::Deserialize;
11use walkdir::WalkDir;
12
13use crate::cmd::Convert;
14use crate::{LANG_ID, LOCALES, utils};
15
16#[must_use]
17#[derive(Args)]
18#[command(arg_required_else_help = true,
19    about = LOCALES.lookup(&LANG_ID, "epub_command"))]
20pub struct Epub {
21    #[arg(help = LOCALES.lookup(&LANG_ID, "epub_path"))]
22    pub epub_path: PathBuf,
23
24    #[arg(short, long, value_enum, value_delimiter = ',',
25        required = true, help = LOCALES.lookup(&LANG_ID, "converts"))]
26    pub converts: Vec<Convert>,
27
28    #[arg(short, long, default_value_t = false,
29        help = LOCALES.lookup(&LANG_ID, "delete"))]
30    pub delete: bool,
31}
32
33pub fn execute(config: Epub) -> Result<()> {
34    utils::ensure_epub_file(&config.epub_path)?;
35
36    let epub_file_path = dunce::canonicalize(&config.epub_path)?;
37    tracing::info!("Input file path: `{}`", epub_file_path.display());
38    let epub_file_stem = epub_file_path.file_stem().unwrap().to_str().unwrap();
39    let epub_dir_path = epub_file_path.with_extension("");
40
41    super::unzip(&epub_file_path)?;
42
43    let container_file_path = epub_dir_path.join("META-INF").join("container.xml");
44    let bytes = fs::read(&container_file_path)?;
45    let container = simdutf8::basic::from_utf8(&bytes)?;
46    let mut container: Container = quick_xml::de::from_str(container)?;
47
48    let rootfile_path = epub_dir_path.join(container.rootfiles.rootfiles.remove(0).full_path);
49    let bytes = fs::read(&rootfile_path)?;
50    let rootfile = simdutf8::basic::from_utf8(&bytes)?;
51    let rootfile: Package = quick_xml::de::from_str(rootfile)?;
52
53    let rootfile_parent_path = rootfile_path.parent().unwrap();
54    for item in rootfile.manifest.items {
55        if ["application/xhtml+xml", "application/x-dtbncx+xml"].contains(&item.media_type.as_str())
56        {
57            let content_file_path = rootfile_parent_path.join(item.href);
58            convert_xml(&content_file_path, &config.converts)?;
59        }
60    }
61
62    convert_xml(&rootfile_path, &config.converts)?;
63
64    if config.delete {
65        utils::remove_file_or_dir(&epub_file_path)?;
66    } else {
67        let backup_file_path = epub_file_path.with_file_name(format!("{epub_file_stem}.old.epub"));
68        tracing::info!("Backup file path: `{}`", backup_file_path.display());
69
70        fs::rename(&epub_file_path, backup_file_path)?;
71    }
72
73    let new_epub_file_stem = utils::convert_str(epub_file_stem, &config.converts, false)?;
74
75    let file = File::create(epub_file_path.with_file_name(format!("{new_epub_file_stem}.epub")))?;
76    let walkdir = WalkDir::new(&epub_dir_path);
77    super::zip_dir(
78        &mut walkdir.into_iter().filter_map(|e| e.ok()),
79        &epub_dir_path,
80        file,
81    )?;
82
83    utils::remove_file_or_dir(&epub_dir_path)?;
84
85    Ok(())
86}
87
88fn convert_xml<T, E>(xml_file_path: T, converts: E) -> Result<()>
89where
90    T: AsRef<Path>,
91    E: AsRef<[Convert]>,
92{
93    let bytes = fs::read(xml_file_path.as_ref())?;
94    let xml_content = simdutf8::basic::from_utf8(&bytes)?;
95
96    let mut reader = Reader::from_str(xml_content);
97    reader.config_mut().trim_text(true);
98    let mut writer = Writer::new(Cursor::new(Vec::new()));
99
100    loop {
101        match reader.read_event() {
102            Ok(Event::Text(e)) => {
103                let content = utils::convert_str(&e.decode()?, &converts, false)?;
104                writer.write_event(Event::Text(BytesText::new(&content)))?
105            }
106            Ok(Event::Eof) => break,
107            Ok(e) => writer.write_event(e)?,
108            Err(e) => eyre::bail!("Error at position {}: {:?}", reader.error_position(), e),
109        }
110    }
111
112    let result = writer.into_inner().into_inner();
113    fs::write(xml_file_path.as_ref(), &result)?;
114
115    Ok(())
116}
117
118#[derive(Deserialize)]
119struct Container {
120    rootfiles: Rootfiles,
121}
122
123#[derive(Deserialize)]
124struct Rootfiles {
125    #[serde(rename = "rootfile")]
126    rootfiles: Vec<Rootfile>,
127}
128
129#[derive(Deserialize)]
130struct Rootfile {
131    #[serde(rename = "@full-path")]
132    full_path: PathBuf,
133}
134
135#[derive(Deserialize)]
136struct Package {
137    manifest: Manifest,
138}
139
140#[derive(Deserialize)]
141struct Manifest {
142    #[serde(rename = "item")]
143    items: Vec<Item>,
144}
145
146#[derive(Deserialize)]
147struct Item {
148    #[serde(rename = "@href")]
149    href: PathBuf,
150    #[serde(rename = "@media-type")]
151    media_type: String,
152}