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}