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}