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

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<P: AsRef<Path>>(
path: P,
) -> Result<model::anime_boxes::Backup, Box<dyn Error>> {
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<usize> {
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<usize, WriteError> {
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<dyn Error>> {
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<String> = backup
.favorites
.iter()
.map(|f| String::from(&f.file.url))
.collect();
let mut favorites_to_download: Vec<String> = 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<String> = 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<String> = 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<String> = 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 <warrick@sothr.com>")
.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)
}
}
}