Skip to main content

novel_cli/cmd/
epub.rs

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