A tool to read animebox backup files and export the data in alternate formats.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

266 lines
8.8 KiB

4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
  1. use std::env;
  2. use std::error::Error;
  3. use std::fs::{create_dir_all, File, OpenOptions};
  4. use std::io::{stdout, BufReader, Write};
  5. use std::path::Path;
  6. use std::process::exit;
  7. use clap::{App, Arg};
  8. use crossterm::{cursor, QueueableCommand};
  9. use curl::easy::{Easy, WriteError};
  10. use serde_json::from_reader;
  11. use url::Url;
  12. use lazy_static::lazy_static;
  13. use crate::config::ConfigManager;
  14. use crate::state::{State, StateManager};
  15. const VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
  16. lazy_static! {
  17. static ref COMMANDS: Vec<&'static str> = vec!["download", "favorites", "print", "searches"];
  18. }
  19. mod config;
  20. mod model;
  21. mod state;
  22. fn read_animeboxes_backup<P: AsRef<Path>>(
  23. path: P,
  24. ) -> Result<model::anime_boxes::Backup, Box<dyn Error>> {
  25. let file = File::open(path)?;
  26. let reader = BufReader::new(file);
  27. let result: model::anime_boxes::Backup = from_reader(reader)?;
  28. Ok(result)
  29. }
  30. fn write_to_file(data: &[u8], path: &Path) -> std::io::Result<usize> {
  31. if !path.exists() {
  32. let mut file = File::create(path)?;
  33. file.write_all(data)?;
  34. } else {
  35. let mut file = OpenOptions::new().append(true).open(path)?;
  36. file.write_all(data)?;
  37. }
  38. Ok(data.len())
  39. }
  40. fn curl_write_to_file_function(data: &[u8], path: &Path) -> Result<usize, WriteError> {
  41. match write_to_file(data, &path) {
  42. Err(e) => {
  43. println!("Error writing to {:#?}: {}", &path, e);
  44. Err(WriteError::Pause)
  45. }
  46. Ok(d) => Ok(d),
  47. }
  48. }
  49. fn calculate_percentage(numerator: f64, denominator: f64) -> f64 {
  50. if numerator <= 0.0 {
  51. 0.0
  52. } else {
  53. (numerator / denominator) * 100.0
  54. }
  55. }
  56. fn print_progress(data: &[u8]) -> Result<(), Box<dyn Error>> {
  57. let mut stdout = stdout();
  58. stdout.queue(cursor::SavePosition)?;
  59. stdout.write(data)?;
  60. stdout.queue(cursor::RestorePosition)?;
  61. stdout.flush()?;
  62. Ok(())
  63. }
  64. fn download_command(
  65. backup: model::anime_boxes::Backup,
  66. config_manager: config::ConfigManager,
  67. mut state_manager: state::StateManager,
  68. ) {
  69. let temp_directory = env::current_dir().unwrap().join(
  70. config_manager
  71. .config
  72. .save_directory
  73. .as_ref()
  74. .unwrap_or(&String::from("tmp")),
  75. );
  76. match create_dir_all(&temp_directory) {
  77. Err(e) => println!("Failed to create directory {:#?}: {}", &temp_directory, e),
  78. Ok(_) => (),
  79. };
  80. let favorites: Vec<String> = backup
  81. .favorites
  82. .iter()
  83. .map(|f| String::from(&f.file.url))
  84. .collect();
  85. let mut favorites_to_download: Vec<String> = Vec::new();
  86. for favorite in favorites {
  87. if !state_manager.state.downloaded.contains(&favorite) {
  88. favorites_to_download.push(favorite);
  89. }
  90. }
  91. let to_download_count = favorites_to_download.len();
  92. let mut count = 0;
  93. let mut favorites_downloaded = Vec::new();
  94. for favorite in favorites_to_download {
  95. count += 1;
  96. let mut easy = Easy::new();
  97. let favorite_url = Url::parse(&favorite);
  98. match favorite_url {
  99. Err(e) => {
  100. println!("Error downloading {}. Not a valid URL. {}", favorite, e);
  101. continue;
  102. }
  103. Ok(url) => {
  104. let path_segments = url.path_segments().unwrap();
  105. let filename = path_segments.last().unwrap();
  106. let target_path = temp_directory.as_path().join(&filename);
  107. let progress_count = format!("{:}/{:}", count, to_download_count);
  108. let progress_filename = String::from(filename);
  109. match easy.url(url.as_str()) {
  110. Err(e) => println!("Error setting download URL {}: {}", &filename, e),
  111. Ok(_) => (),
  112. };
  113. match easy.progress(true) {
  114. Err(e) => println!("Error setting progress setting: {}", e),
  115. Ok(_) => (),
  116. };
  117. let mut progress_length: usize = 0;
  118. match easy.progress_function(
  119. move |total_bytes_to_download,
  120. bytes_downloaded,
  121. _total_bytes_to_upload,
  122. _bytes_uploaded| {
  123. let percentage =
  124. calculate_percentage(bytes_downloaded, total_bytes_to_download);
  125. let mut progress = format!(
  126. "{}: {:.2}% {}",
  127. progress_filename, percentage, &progress_count
  128. );
  129. progress_length = if progress.len() > progress_length {
  130. progress.len()
  131. } else {
  132. progress_length
  133. };
  134. if progress_length > progress.len() {
  135. progress = format!(
  136. "{}{}",
  137. progress,
  138. " ".repeat(progress_length - progress.len())
  139. );
  140. }
  141. match print_progress(progress.as_bytes()) {
  142. Err(e) => println!("Error showing downloading progress: {}", e),
  143. Ok(_) => (),
  144. };
  145. true
  146. },
  147. ) {
  148. Err(e) => println!("Error showing downloading progress: {}", e),
  149. Ok(_) => (),
  150. };
  151. match easy
  152. .write_function(move |data| curl_write_to_file_function(data, &target_path))
  153. {
  154. Err(e) => println!("Error downloading {}: {}", &favorite, e),
  155. Ok(_) => favorites_downloaded.push(String::from(&favorite)),
  156. }
  157. easy.perform().unwrap();
  158. }
  159. }
  160. }
  161. let favorites_downloaded_count = favorites_downloaded.len();
  162. let mut all_downloaded: Vec<String> = Vec::new();
  163. for downloaded in &state_manager.state.downloaded {
  164. all_downloaded.push(String::from(downloaded));
  165. }
  166. for downloaded in favorites_downloaded {
  167. println!("{}", downloaded);
  168. all_downloaded.push(downloaded);
  169. }
  170. let all_downloaded_count = all_downloaded.len();
  171. let new_state = State {
  172. downloaded: all_downloaded,
  173. };
  174. state_manager.save(new_state);
  175. println!(
  176. "Downloaded {:#?}/{:#?} images",
  177. favorites_downloaded_count, to_download_count
  178. );
  179. println!("Total Downloaded: {:#?}", all_downloaded_count);
  180. }
  181. fn favorites_command(backup: model::anime_boxes::Backup) {
  182. let favorites: Vec<String> = backup
  183. .favorites
  184. .iter()
  185. .map(|f| String::from(&f.file.url))
  186. .collect();
  187. for favorite in favorites {
  188. println!("{}", favorite);
  189. }
  190. }
  191. fn searches_command(backup: model::anime_boxes::Backup) {
  192. let mut searches: Vec<String> = backup
  193. .search_history
  194. .iter()
  195. .map(|s| String::from(&s.search_text))
  196. .collect();
  197. searches.sort();
  198. for search in searches {
  199. println!("{}", search);
  200. }
  201. }
  202. fn main() {
  203. let matches = App::new("AnimeBoxes Sync")
  204. .version(VERSION.unwrap_or("UNKNOWN"))
  205. .author("Drew Short <warrick@sothr.com>")
  206. .about("Parses AnimeBoxes backup files")
  207. .arg(
  208. Arg::with_name("config")
  209. .short("c")
  210. .value_name("FILE")
  211. .help("Set a custom config file")
  212. .takes_value(true),
  213. )
  214. .arg(
  215. Arg::with_name("INPUT")
  216. .help("The AnimeBoxes file to process")
  217. .required(true)
  218. .index(1),
  219. )
  220. .arg(
  221. Arg::with_name("COMMAND")
  222. .help("The command to run on the backup")
  223. .required(true)
  224. .index(2)
  225. .possible_values(&COMMANDS),
  226. )
  227. .get_matches();
  228. let config = matches.value_of("config").unwrap_or("config.json");
  229. let config_path = env::current_dir().unwrap().join(config);
  230. let config_manager = ConfigManager::new(config_path.as_path());
  231. let state_path = env::current_dir().unwrap().join("state.json");
  232. let state_manager = StateManager::new(state_path.as_path());
  233. let path = matches.value_of("INPUT").unwrap();
  234. let result = read_animeboxes_backup(path).unwrap();
  235. let command = matches.value_of("COMMAND").unwrap();
  236. match command {
  237. "download" => download_command(result, config_manager, state_manager),
  238. "favorites" => favorites_command(result),
  239. "print" => println!("{:#?}", result),
  240. "searches" => searches_command(result),
  241. _ => {
  242. println!("{} is unrecognized", command);
  243. exit(1)
  244. }
  245. }
  246. }