mirror of
https://github.com/yuhkix/wuwa-downloader.git
synced 2025-06-06 17:53:44 +00:00
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:
parent
a4e0248ea5
commit
06e10b1f78
3 changed files with 190 additions and 140 deletions
122
src/io/util.rs
122
src/io/util.rs
|
@ -1,9 +1,10 @@
|
||||||
use std::time::Duration;
|
use std::{fs::File, io, sync::Arc, thread, time::{Duration, Instant}};
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use reqwest::blocking::Client;
|
use reqwest::blocking::Client;
|
||||||
use serde_json::Value;
|
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 {
|
pub fn format_duration(duration: Duration) -> String {
|
||||||
let secs = duration.as_secs();
|
let secs = duration.as_secs();
|
||||||
|
@ -91,3 +92,120 @@ pub fn get_version(data: &Value, category: &str, version: &str) -> Result<String
|
||||||
.map(|s| s.to_string())
|
.map(|s| s.to_string())
|
||||||
.ok_or_else(|| format!("Missing {} URL", version))
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
139
src/main.rs
139
src/main.rs
|
@ -1,26 +1,19 @@
|
||||||
use colored::*;
|
use colored::*;
|
||||||
use reqwest::blocking::Client;
|
use reqwest::blocking::Client;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::{
|
|
||||||
io,
|
|
||||||
sync::Arc,
|
|
||||||
thread,
|
|
||||||
time::{Duration, Instant},
|
|
||||||
};
|
|
||||||
use winconsole::console::{clear, set_title};
|
use winconsole::console::{clear, set_title};
|
||||||
|
|
||||||
use wuwa_downloader::{
|
use wuwa_downloader::{
|
||||||
config::status::Status,
|
config::status::Status,
|
||||||
download::progress::DownloadProgress,
|
|
||||||
io::{
|
io::{
|
||||||
console::print_results,
|
console::print_results,
|
||||||
file::get_dir,
|
file::get_dir,
|
||||||
util::{bytes_to_human, format_duration, calculate_total_size},
|
util::{
|
||||||
logging::{
|
calculate_total_size, download_resources, exit_with_error, setup_ctrlc, track_progress, start_title_thread
|
||||||
log_error, setup_logging,
|
|
||||||
},
|
},
|
||||||
|
logging::setup_logging,
|
||||||
},
|
},
|
||||||
network::client::{download_file, fetch_index, get_predownload},
|
network::client::{fetch_index, get_config},
|
||||||
};
|
};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
@ -28,16 +21,9 @@ fn main() {
|
||||||
let log_file = setup_logging();
|
let log_file = setup_logging();
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
|
|
||||||
let config = match get_predownload(&client) {
|
let config = match get_config(&client) {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(e) => {
|
Err(e) => exit_with_error(&log_file, &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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let folder = get_dir();
|
let folder = get_dir();
|
||||||
|
@ -48,18 +34,10 @@ fn main() {
|
||||||
folder.display().to_string().cyan()
|
folder.display().to_string().cyan()
|
||||||
);
|
);
|
||||||
|
|
||||||
clear().unwrap();
|
|
||||||
let data = fetch_index(&client, &config, &log_file);
|
let data = fetch_index(&client, &config, &log_file);
|
||||||
|
|
||||||
let resources = match data.get("resource").and_then(Value::as_array) {
|
let resources = match data.get("resource").and_then(Value::as_array) {
|
||||||
Some(res) => res,
|
Some(res) => res,
|
||||||
None => {
|
None => exit_with_error(&log_file, "No resources found in index file"),
|
||||||
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;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
|
@ -67,108 +45,31 @@ fn main() {
|
||||||
Status::info(),
|
Status::info(),
|
||||||
resources.len().to_string().cyan()
|
resources.len().to_string().cyan()
|
||||||
);
|
);
|
||||||
|
|
||||||
let total_size = calculate_total_size(resources, &client, &config);
|
let total_size = calculate_total_size(resources, &client, &config);
|
||||||
clear().unwrap();
|
clear().unwrap();
|
||||||
|
|
||||||
let should_stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
let (should_stop, success, progress) = track_progress(total_size);
|
||||||
let success = Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
|
||||||
let total_files = resources.len();
|
|
||||||
|
|
||||||
let progress = DownloadProgress {
|
let title_thread = start_title_thread(
|
||||||
total_bytes: Arc::new(std::sync::atomic::AtomicU64::new(total_size)),
|
should_stop.clone(),
|
||||||
downloaded_bytes: Arc::new(std::sync::atomic::AtomicU64::new(0)),
|
success.clone(),
|
||||||
start_time: Instant::now(),
|
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);
|
|
||||||
|
|
||||||
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();
|
setup_ctrlc(should_stop.clone());
|
||||||
thread::sleep(Duration::from_secs(1));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let should_stop_ctrlc = should_stop.clone();
|
download_resources(
|
||||||
|
|
||||||
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,
|
&client,
|
||||||
&config,
|
&config,
|
||||||
dest,
|
resources,
|
||||||
&folder,
|
&folder,
|
||||||
md5,
|
|
||||||
&log_file,
|
&log_file,
|
||||||
&should_stop,
|
&should_stop,
|
||||||
&progress,
|
&progress,
|
||||||
) {
|
&success,
|
||||||
success.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
should_stop.store(true, std::sync::atomic::Ordering::SeqCst);
|
should_stop.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||||
title_thread.join().unwrap();
|
title_thread.join().unwrap();
|
||||||
|
@ -176,7 +77,7 @@ fn main() {
|
||||||
clear().unwrap();
|
clear().unwrap();
|
||||||
print_results(
|
print_results(
|
||||||
success.load(std::sync::atomic::Ordering::SeqCst),
|
success.load(std::sync::atomic::Ordering::SeqCst),
|
||||||
total_files,
|
resources.len(),
|
||||||
&folder,
|
&folder,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -291,7 +291,7 @@ fn download_single_file(
|
||||||
Ok(())
|
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)?;
|
let selected_index_url = fetch_gist(client)?;
|
||||||
println!("{} Fetching download configuration...", Status::info());
|
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))?
|
from_reader(response).map_err(|e| format!("Invalid JSON: {}", e))?
|
||||||
};
|
};
|
||||||
|
|
||||||
let predownload_config = config
|
let has_default = config.get("default").is_some();
|
||||||
.get("predownload")
|
let has_predownload = config.get("predownload").is_some();
|
||||||
.and_then(|p| p.get("config"))
|
|
||||||
.ok_or("Missing predownload.config in response")?;
|
|
||||||
|
|
||||||
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")
|
.get("baseUrl")
|
||||||
.and_then(Value::as_str)
|
.and_then(Value::as_str)
|
||||||
.ok_or("Missing or invalid baseUrl")?;
|
.ok_or("Missing or invalid baseUrl")?;
|
||||||
|
|
||||||
let index_file = predownload_config
|
let index_file = base_config
|
||||||
.get("indexFile")
|
.get("indexFile")
|
||||||
.and_then(Value::as_str)
|
.and_then(Value::as_str)
|
||||||
.ok_or("Missing or invalid indexFile")?;
|
.ok_or("Missing or invalid indexFile")?;
|
||||||
|
|
||||||
let default_config = config
|
let cdn_list = config_data
|
||||||
.get("default")
|
|
||||||
.ok_or("Missing default config in response")?;
|
|
||||||
|
|
||||||
let cdn_list = default_config
|
|
||||||
.get("cdnList")
|
.get("cdnList")
|
||||||
.and_then(Value::as_array)
|
.and_then(Value::as_array)
|
||||||
.ok_or("Missing or invalid cdnList")?;
|
.ok_or("Missing or invalid cdnList")?;
|
||||||
|
|
Loading…
Reference in a new issue