small cleanup in main.rs and updated config function to ask the user wether to download default or preload if both exist in index response

This commit is contained in:
Yuhki 2025-03-27 17:54:22 +01:00
parent a4e0248ea5
commit 06e10b1f78
3 changed files with 190 additions and 140 deletions

View file

@ -1,9 +1,10 @@
use std::time::Duration;
use std::{fs::File, io, sync::Arc, thread, time::{Duration, Instant}};
use colored::Colorize;
use reqwest::blocking::Client;
use serde_json::Value;
use winconsole::console::{clear, set_title};
use crate::config::{cfg::Config, status::Status};
use crate::{config::{cfg::Config, status::Status}, download::progress::DownloadProgress, io::logging::log_error, network::client::download_file};
pub fn format_duration(duration: Duration) -> String {
let secs = duration.as_secs();
@ -90,4 +91,121 @@ pub fn get_version(data: &Value, category: &str, version: &str) -> Result<String
.as_str()
.map(|s| s.to_string())
.ok_or_else(|| format!("Missing {} URL", version))
}
pub fn exit_with_error(log_file: &File, error: &str) -> ! {
log_error(log_file, error);
clear().unwrap();
println!("{} {}", Status::error(), error);
println!("\n{} Press Enter to exit...", Status::warning());
let _ = io::stdin().read_line(&mut String::new());
std::process::exit(1);
}
pub fn track_progress(total_size: u64) -> (
Arc<std::sync::atomic::AtomicBool>,
Arc<std::sync::atomic::AtomicUsize>,
DownloadProgress,
) {
let should_stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
let success = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let progress = DownloadProgress {
total_bytes: Arc::new(std::sync::atomic::AtomicU64::new(total_size)),
downloaded_bytes: Arc::new(std::sync::atomic::AtomicU64::new(0)),
start_time: Instant::now(),
};
(should_stop, success, progress)
}
pub fn start_title_thread(
should_stop: Arc<std::sync::atomic::AtomicBool>,
success: Arc<std::sync::atomic::AtomicUsize>,
progress: DownloadProgress,
total_files: usize,
) -> thread::JoinHandle<()> {
thread::spawn(move || {
while !should_stop.load(std::sync::atomic::Ordering::SeqCst) {
let elapsed = progress.start_time.elapsed();
let elapsed_secs = elapsed.as_secs();
let downloaded_bytes = progress.downloaded_bytes.load(std::sync::atomic::Ordering::SeqCst);
let total_bytes = progress.total_bytes.load(std::sync::atomic::Ordering::SeqCst);
let current_success = success.load(std::sync::atomic::Ordering::SeqCst);
let speed = if elapsed_secs > 0 { downloaded_bytes / elapsed_secs } else { 0 };
let (speed_value, speed_unit) = if speed > 1_000_000 {
(speed / 1_000_000, "MB/s")
} else {
(speed / 1_000, "KB/s")
};
let remaining_files = total_files - current_success;
let remaining_bytes = total_bytes.saturating_sub(downloaded_bytes);
let eta_secs = if speed > 0 && remaining_files > 0 { remaining_bytes / speed } else { 0 };
let eta_str = format_duration(Duration::from_secs(eta_secs));
let progress_percent = if total_bytes > 0 {
format!(" ({}%)", (downloaded_bytes * 100 / total_bytes))
} else {
String::new()
};
let title = format!(
"Wuthering Waves Downloader - {}/{} files - Current File: {}{} - Speed: {}{} - Total ETA: {}",
current_success,
total_files,
bytes_to_human(downloaded_bytes),
progress_percent,
speed_value,
speed_unit,
eta_str
);
set_title(&title).unwrap();
thread::sleep(Duration::from_secs(1));
}
})
}
pub fn setup_ctrlc(should_stop: Arc<std::sync::atomic::AtomicBool>) {
ctrlc::set_handler(move || {
should_stop.store(true, std::sync::atomic::Ordering::SeqCst);
clear().unwrap();
println!("\n{} Download interrupted by user", Status::warning());
})
.unwrap();
}
pub fn download_resources(
client: &Client,
config: &Config,
resources: &[Value],
folder: &std::path::Path,
log_file: &File,
should_stop: &Arc<std::sync::atomic::AtomicBool>,
progress: &DownloadProgress,
success: &Arc<std::sync::atomic::AtomicUsize>,
) {
for item in resources {
if should_stop.load(std::sync::atomic::Ordering::SeqCst) {
break;
}
if let Some(dest) = item.get("dest").and_then(Value::as_str) {
let md5 = item.get("md5").and_then(Value::as_str);
if download_file(
client,
config,
dest,
folder,
md5,
log_file,
should_stop,
progress,
) {
success.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
}
}
}
}

View file

@ -1,26 +1,19 @@
use colored::*;
use reqwest::blocking::Client;
use serde_json::Value;
use std::{
io,
sync::Arc,
thread,
time::{Duration, Instant},
};
use winconsole::console::{clear, set_title};
use wuwa_downloader::{
config::status::Status,
download::progress::DownloadProgress,
io::{
console::print_results,
file::get_dir,
util::{bytes_to_human, format_duration, calculate_total_size},
logging::{
log_error, setup_logging,
util::{
calculate_total_size, download_resources, exit_with_error, setup_ctrlc, track_progress, start_title_thread
},
logging::setup_logging,
},
network::client::{download_file, fetch_index, get_predownload},
network::client::{fetch_index, get_config},
};
fn main() {
@ -28,16 +21,9 @@ fn main() {
let log_file = setup_logging();
let client = Client::new();
let config = match get_predownload(&client) {
let config = match get_config(&client) {
Ok(c) => c,
Err(e) => {
log_error(&log_file, &e);
clear().unwrap();
println!("{} {}", Status::error(), e);
println!("\n{} Press Enter to exit...", Status::warning());
let _ = io::stdin().read_line(&mut String::new());
std::process::exit(1);
}
Err(e) => exit_with_error(&log_file, &e),
};
let folder = get_dir();
@ -48,18 +34,10 @@ fn main() {
folder.display().to_string().cyan()
);
clear().unwrap();
let data = fetch_index(&client, &config, &log_file);
let resources = match data.get("resource").and_then(Value::as_array) {
Some(res) => res,
None => {
log_error(&log_file, "No resources found in index file");
println!("{} No resources found in index file", Status::warning());
println!("\n{} Press Enter to exit...", Status::warning());
let _ = io::stdin().read_line(&mut String::new());
return;
}
None => exit_with_error(&log_file, "No resources found in index file"),
};
println!(
@ -67,108 +45,31 @@ fn main() {
Status::info(),
resources.len().to_string().cyan()
);
let total_size = calculate_total_size(resources, &client, &config);
clear().unwrap();
let should_stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
let success = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let total_files = resources.len();
let (should_stop, success, progress) = track_progress(total_size);
let progress = DownloadProgress {
total_bytes: Arc::new(std::sync::atomic::AtomicU64::new(total_size)),
downloaded_bytes: Arc::new(std::sync::atomic::AtomicU64::new(0)),
start_time: Instant::now(),
};
let title_thread = start_title_thread(
should_stop.clone(),
success.clone(),
progress.clone(),
resources.len(),
);
let success_clone = success.clone();
let should_stop_clone = should_stop.clone();
let progress_clone = progress.clone();
let title_thread = thread::spawn(move || {
while !should_stop_clone.load(std::sync::atomic::Ordering::SeqCst) {
let elapsed = progress_clone.start_time.elapsed();
let elapsed_secs = elapsed.as_secs();
let downloaded_bytes = progress_clone
.downloaded_bytes
.load(std::sync::atomic::Ordering::SeqCst);
let total_bytes = progress_clone
.total_bytes
.load(std::sync::atomic::Ordering::SeqCst);
let current_success = success_clone.load(std::sync::atomic::Ordering::SeqCst);
setup_ctrlc(should_stop.clone());
let speed = if elapsed_secs > 0 {
downloaded_bytes / elapsed_secs
} else {
0
};
let (speed_value, speed_unit) = if speed > 1_000_000 {
(speed / 1_000_000, "MB/s")
} else {
(speed / 1_000, "KB/s")
};
let remaining_files = total_files - current_success;
let remaining_bytes = total_bytes.saturating_sub(downloaded_bytes);
let eta_secs = if speed > 0 && remaining_files > 0 {
remaining_bytes / speed
} else {
0
};
let eta_str = format_duration(Duration::from_secs(eta_secs));
let progress_percent = if total_bytes > 0 {
format!(" ({}%)", (downloaded_bytes * 100 / total_bytes))
} else {
String::new()
};
let title = format!(
"Wuthering Waves Downloader - {}/{} files - Current File: {}{} - Speed: {}{} - Total ETA: {}",
current_success,
total_files,
bytes_to_human(downloaded_bytes),
progress_percent,
speed_value,
speed_unit,
eta_str
);
set_title(&title).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
let should_stop_ctrlc = should_stop.clone();
ctrlc::set_handler(move || {
should_stop_ctrlc.store(true, std::sync::atomic::Ordering::SeqCst);
clear().unwrap();
println!("\n{} Download interrupted by user", Status::warning());
})
.unwrap();
for item in resources.iter() {
if should_stop.load(std::sync::atomic::Ordering::SeqCst) {
break;
}
if let Some(dest) = item.get("dest").and_then(Value::as_str) {
let md5 = item.get("md5").and_then(Value::as_str);
if download_file(
&client,
&config,
dest,
&folder,
md5,
&log_file,
&should_stop,
&progress,
) {
success.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
}
}
}
download_resources(
&client,
&config,
resources,
&folder,
&log_file,
&should_stop,
&progress,
&success,
);
should_stop.store(true, std::sync::atomic::Ordering::SeqCst);
title_thread.join().unwrap();
@ -176,7 +77,7 @@ fn main() {
clear().unwrap();
print_results(
success.load(std::sync::atomic::Ordering::SeqCst),
total_files,
resources.len(),
&folder,
);
}

View file

@ -291,7 +291,7 @@ fn download_single_file(
Ok(())
}
pub fn get_predownload(client: &Client) -> Result<Config, String> {
pub fn get_config(client: &Client) -> Result<Config, String> {
let selected_index_url = fetch_gist(client)?;
println!("{} Fetching download configuration...", Status::info());
@ -327,26 +327,57 @@ pub fn get_predownload(client: &Client) -> Result<Config, String> {
from_reader(response).map_err(|e| format!("Invalid JSON: {}", e))?
};
let predownload_config = config
.get("predownload")
.and_then(|p| p.get("config"))
.ok_or("Missing predownload.config in response")?;
let has_default = config.get("default").is_some();
let has_predownload = config.get("predownload").is_some();
let base_url = predownload_config
let selected_config = match (has_default, has_predownload) {
(true, false) => {
println!("{} Using default.config", Status::info());
"default"
}
(false, true) => {
println!("{} Using predownload.config", Status::info());
"predownload"
}
(true, true) => {
loop {
print!("{} Choose config to use (1=default, 2=predownload): ", Status::question());
io::stdout().flush().map_err(|e| format!("Failed to flush stdout: {}", e))?;
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.map_err(|e| format!("Failed to read input: {}", e))?;
match input.trim() {
"1" => break "default",
"2" => break "predownload",
_ => println!("{} Invalid choice, please enter 1 or 2", Status::error()),
}
}
}
(false, false) => return Err("Neither default.config nor predownload.config found in response".to_string()),
};
let config_data = config
.get(selected_config)
.ok_or_else(|| format!("Missing {} config in response", selected_config))?;
let base_config = config_data
.get("config")
.ok_or_else(|| format!("Missing config in {} response", selected_config))?;
let base_url = base_config
.get("baseUrl")
.and_then(Value::as_str)
.ok_or("Missing or invalid baseUrl")?;
let index_file = predownload_config
let index_file = base_config
.get("indexFile")
.and_then(Value::as_str)
.ok_or("Missing or invalid indexFile")?;
let default_config = config
.get("default")
.ok_or("Missing default config in response")?;
let cdn_list = default_config
let cdn_list = config_data
.get("cdnList")
.and_then(Value::as_array)
.ok_or("Missing or invalid cdnList")?;