use std::env; use std::error::Error; use std::fs::{create_dir_all, File, OpenOptions}; use std::io::{stdout, BufReader, Write}; use std::path::Path; use std::process::exit; use clap::{App, Arg}; use crossterm::{cursor, QueueableCommand}; use curl::easy::{Easy, WriteError}; use serde_json::from_reader; use url::Url; use lazy_static::lazy_static; use crate::config::ConfigManager; use crate::state::{State, StateManager}; const VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION"); lazy_static! { static ref COMMANDS: Vec<&'static str> = vec!["download", "favorites", "print", "searches"]; } mod config; mod model; mod state; fn read_animeboxes_backup>( path: P, ) -> Result> { let file = File::open(path)?; let reader = BufReader::new(file); let result: model::anime_boxes::Backup = from_reader(reader)?; Ok(result) } fn write_to_file(data: &[u8], path: &Path) -> std::io::Result { if !path.exists() { let mut file = File::create(path)?; file.write_all(data)?; } else { let mut file = OpenOptions::new().append(true).open(path)?; file.write_all(data)?; } Ok(data.len()) } fn curl_write_to_file_function(data: &[u8], path: &Path) -> Result { match write_to_file(data, &path) { Err(e) => { println!("Error writing to {:#?}: {}", &path, e); Err(WriteError::Pause) } Ok(d) => Ok(d), } } fn calculate_percentage(numerator: f64, denominator: f64) -> f64 { if numerator <= 0.0 { 0.0 } else { (numerator / denominator) * 100.0 } } fn print_progress(data: &[u8]) -> Result<(), Box> { let mut stdout = stdout(); stdout.queue(cursor::SavePosition)?; stdout.write(data)?; stdout.queue(cursor::RestorePosition)?; stdout.flush()?; Ok(()) } fn download_command( backup: model::anime_boxes::Backup, config_manager: config::ConfigManager, mut state_manager: state::StateManager, ) { let temp_directory = env::current_dir().unwrap().join( config_manager .config .save_directory .as_ref() .unwrap_or(&String::from("tmp")), ); match create_dir_all(&temp_directory) { Err(e) => println!("Failed to create directory {:#?}: {}", &temp_directory, e), Ok(_) => (), }; let favorites: Vec = backup .favorites .iter() .map(|f| String::from(&f.file.url)) .collect(); let mut favorites_to_download: Vec = Vec::new(); for favorite in favorites { if !state_manager.state.downloaded.contains(&favorite) { favorites_to_download.push(favorite); } } let to_download_count = favorites_to_download.len(); let mut count = 0; let mut favorites_downloaded = Vec::new(); for favorite in favorites_to_download { count += 1; let mut easy = Easy::new(); let favorite_url = Url::parse(&favorite); match favorite_url { Err(e) => { println!("Error downloading {}. Not a valid URL. {}", favorite, e); continue; } Ok(url) => { let path_segments = url.path_segments().unwrap(); let filename = path_segments.last().unwrap(); let target_path = temp_directory.as_path().join(&filename); let progress_count = format!("{:}/{:}", count, to_download_count); let progress_filename = String::from(filename); match easy.url(url.as_str()) { Err(e) => println!("Error setting download URL {}: {}", &filename, e), Ok(_) => (), }; match easy.progress(true) { Err(e) => println!("Error setting progress setting: {}", e), Ok(_) => (), }; let mut progress_length: usize = 0; match easy.progress_function( move |total_bytes_to_download, bytes_downloaded, _total_bytes_to_upload, _bytes_uploaded| { let percentage = calculate_percentage(bytes_downloaded, total_bytes_to_download); let mut progress = format!( "{}: {:.2}% {}", progress_filename, percentage, &progress_count ); progress_length = if progress.len() > progress_length { progress.len() } else { progress_length }; if progress_length > progress.len() { progress = format!( "{}{}", progress, " ".repeat(progress_length - progress.len()) ); } match print_progress(progress.as_bytes()) { Err(e) => println!("Error showing downloading progress: {}", e), Ok(_) => (), }; true }, ) { Err(e) => println!("Error showing downloading progress: {}", e), Ok(_) => (), }; match easy .write_function(move |data| curl_write_to_file_function(data, &target_path)) { Err(e) => println!("Error downloading {}: {}", &favorite, e), Ok(_) => favorites_downloaded.push(String::from(&favorite)), } easy.perform().unwrap(); } } } let favorites_downloaded_count = favorites_downloaded.len(); let mut all_downloaded: Vec = Vec::new(); for downloaded in &state_manager.state.downloaded { all_downloaded.push(String::from(downloaded)); } for downloaded in favorites_downloaded { println!("{}", downloaded); all_downloaded.push(downloaded); } let all_downloaded_count = all_downloaded.len(); let new_state = State { downloaded: all_downloaded, }; state_manager.save(new_state); println!( "Downloaded {:#?}/{:#?} images", favorites_downloaded_count, to_download_count ); println!("Total Downloaded: {:#?}", all_downloaded_count); } fn favorites_command(backup: model::anime_boxes::Backup) { let favorites: Vec = backup .favorites .iter() .map(|f| String::from(&f.file.url)) .collect(); for favorite in favorites { println!("{}", favorite); } } fn searches_command(backup: model::anime_boxes::Backup) { let mut searches: Vec = backup .search_history .iter() .map(|s| String::from(&s.search_text)) .collect(); searches.sort(); for search in searches { println!("{}", search); } } fn main() { let matches = App::new("AnimeBoxes Sync") .version(VERSION.unwrap_or("UNKNOWN")) .author("Drew Short ") .about("Parses AnimeBoxes backup files") .arg( Arg::with_name("config") .short("c") .value_name("FILE") .help("Set a custom config file") .takes_value(true), ) .arg( Arg::with_name("INPUT") .help("The AnimeBoxes file to process") .required(true) .index(1), ) .arg( Arg::with_name("COMMAND") .help("The command to run on the backup") .required(true) .index(2) .possible_values(&COMMANDS), ) .get_matches(); let config = matches.value_of("config").unwrap_or("config.json"); let config_path = env::current_dir().unwrap().join(config); let config_manager = ConfigManager::new(config_path.as_path()); let state_path = env::current_dir().unwrap().join("state.json"); let state_manager = StateManager::new(state_path.as_path()); let path = matches.value_of("INPUT").unwrap(); let result = read_animeboxes_backup(path).unwrap(); let command = matches.value_of("COMMAND").unwrap(); match command { "download" => download_command(result, config_manager, state_manager), "favorites" => favorites_command(result), "print" => println!("{:#?}", result), "searches" => searches_command(result), _ => { println!("{} is unrecognized", command); exit(1) } } }