mirror of
https://github.com/yuhkix/wuwa-downloader.git
synced 2025-06-06 17:53:44 +00:00
added proper eta calculation and file processing
This commit is contained in:
parent
bc558a8714
commit
895e285c00
5 changed files with 153 additions and 78 deletions
|
@ -9,6 +9,6 @@ impl Status {
|
||||||
pub fn warning() -> ColoredString { "[!]".yellow() }
|
pub fn warning() -> ColoredString { "[!]".yellow() }
|
||||||
pub fn error() -> ColoredString { "[-]".red() }
|
pub fn error() -> ColoredString { "[-]".red() }
|
||||||
pub fn question() -> ColoredString { "[?]".blue() }
|
pub fn question() -> ColoredString { "[?]".blue() }
|
||||||
pub fn progress() -> ColoredString { "[→]".magenta() }
|
pub fn progress() -> ColoredString { "[→]".purple() }
|
||||||
pub fn matched() -> ColoredString { "[↓]".blue() }
|
pub fn matched() -> ColoredString { "[↓]".bright_purple() }
|
||||||
}
|
}
|
|
@ -1,8 +1,6 @@
|
||||||
use std::{io, path::Path, time::{Duration, Instant}};
|
use std::{io, path::Path};
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use winconsole::console::set_title;
|
use crate::config::status::Status;
|
||||||
use crate::{config::status::Status, download::progress::DownloadProgress};
|
|
||||||
use super::logging::{bytes_to_human, format_duration};
|
|
||||||
|
|
||||||
pub fn print_results(success: usize, total: usize, folder: &Path) {
|
pub fn print_results(success: usize, total: usize, folder: &Path) {
|
||||||
let title = if success == total {
|
let title = if success == total {
|
||||||
|
@ -30,54 +28,3 @@ pub fn print_results(success: usize, total: usize, folder: &Path) {
|
||||||
println!("\n{} Press Enter to exit...", Status::warning());
|
println!("\n{} Press Enter to exit...", Status::warning());
|
||||||
let _ = io::stdin().read_line(&mut String::new());
|
let _ = io::stdin().read_line(&mut String::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_title(
|
|
||||||
start_time: Instant,
|
|
||||||
success: usize,
|
|
||||||
total: usize,
|
|
||||||
progress: &DownloadProgress,
|
|
||||||
) {
|
|
||||||
let elapsed = 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 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_bytes = total_bytes.saturating_sub(downloaded_bytes);
|
|
||||||
let eta_secs = if speed > 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 - {}{} - Speed: {}{} - ETA: {}",
|
|
||||||
success,
|
|
||||||
total,
|
|
||||||
bytes_to_human(downloaded_bytes),
|
|
||||||
progress_percent,
|
|
||||||
speed_value,
|
|
||||||
speed_unit,
|
|
||||||
eta_str
|
|
||||||
);
|
|
||||||
|
|
||||||
set_title(&title).unwrap();
|
|
||||||
}
|
|
|
@ -1,5 +1,11 @@
|
||||||
use std::{fs::{self, OpenOptions}, io::Write, time::{Duration, SystemTime}};
|
use std::{fs::{self, OpenOptions}, io::Write, time::{Duration, SystemTime}};
|
||||||
|
|
||||||
|
use colored::Colorize;
|
||||||
|
use reqwest::blocking::Client;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::config::{cfg::Config, status::Status};
|
||||||
|
|
||||||
pub fn setup_logging() -> fs::File {
|
pub fn setup_logging() -> fs::File {
|
||||||
OpenOptions::new()
|
OpenOptions::new()
|
||||||
.create(true)
|
.create(true)
|
||||||
|
@ -29,3 +35,69 @@ pub fn bytes_to_human(bytes: u64) -> String {
|
||||||
b => format!("{} B", b),
|
b => format!("{} B", b),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn calculate_total_size(resources: &[Value], client: &Client, config: &Config) -> u64 {
|
||||||
|
let mut total_size = 0;
|
||||||
|
let mut failed_urls = 0;
|
||||||
|
|
||||||
|
println!("{} Processing files...", Status::info());
|
||||||
|
|
||||||
|
for (i, item) in resources.iter().enumerate() {
|
||||||
|
if let Some(dest) = item.get("dest").and_then(Value::as_str) {
|
||||||
|
let mut file_size = 0;
|
||||||
|
let mut found_valid_url = false;
|
||||||
|
|
||||||
|
for base_url in &config.zip_bases {
|
||||||
|
let url = format!("{}/{}", base_url, dest);
|
||||||
|
match client.head(&url).send() {
|
||||||
|
Ok(response) => {
|
||||||
|
if let Some(len) = response.headers().get("content-length") {
|
||||||
|
if let Ok(len_str) = len.to_str() {
|
||||||
|
if let Ok(len_num) = len_str.parse::<u64>() {
|
||||||
|
file_size = len_num;
|
||||||
|
found_valid_url = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("{} Failed to HEAD {}: {}", Status::warning(), url, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if found_valid_url {
|
||||||
|
total_size += file_size;
|
||||||
|
} else {
|
||||||
|
failed_urls += 1;
|
||||||
|
println!("{} Could not determine size for file: {}", Status::error(), dest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if i % 10 == 0 {
|
||||||
|
println!(
|
||||||
|
"{} Processed {}/{} files...",
|
||||||
|
Status::info(),
|
||||||
|
i + 1,
|
||||||
|
resources.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if failed_urls > 0 {
|
||||||
|
println!(
|
||||||
|
"{} Warning: Could not determine size for {} files",
|
||||||
|
Status::warning(),
|
||||||
|
failed_urls
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"{} Total download size: {}",
|
||||||
|
Status::info(),
|
||||||
|
bytes_to_human(total_size).cyan()
|
||||||
|
);
|
||||||
|
|
||||||
|
total_size
|
||||||
|
}
|
||||||
|
|
88
src/main.rs
88
src/main.rs
|
@ -1,17 +1,27 @@
|
||||||
use colored::*;
|
use colored::*;
|
||||||
use reqwest::blocking::Client;
|
use reqwest::blocking::Client;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::{sync::Arc, time::{Duration, Instant}, io, thread};
|
use std::{
|
||||||
use winconsole::console::clear;
|
io,
|
||||||
|
sync::Arc,
|
||||||
|
thread,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
use winconsole::console::{clear, set_title};
|
||||||
|
|
||||||
use wuwa_downloader::{
|
use wuwa_downloader::{
|
||||||
config::status::Status,
|
config::status::Status,
|
||||||
download::progress::DownloadProgress,
|
download::progress::DownloadProgress,
|
||||||
io::{console::{print_results, update_title}, file::get_dir, logging::{log_error, setup_logging}},
|
io::{
|
||||||
|
console::print_results,
|
||||||
|
file::get_dir,
|
||||||
|
logging::{bytes_to_human, format_duration, log_error, setup_logging, calculate_total_size},
|
||||||
|
},
|
||||||
network::client::{download_file, fetch_index, get_predownload},
|
network::client::{download_file, fetch_index, get_predownload},
|
||||||
};
|
};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
set_title("Wuthering Waves Downloader").unwrap();
|
||||||
let log_file = setup_logging();
|
let log_file = setup_logging();
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
|
|
||||||
|
@ -54,13 +64,15 @@ 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);
|
||||||
|
clear().unwrap();
|
||||||
|
|
||||||
let should_stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
let should_stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||||
let success = Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
let success = Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
||||||
let total_files = resources.len();
|
let total_files = resources.len();
|
||||||
|
|
||||||
let progress = DownloadProgress {
|
let progress = DownloadProgress {
|
||||||
total_bytes: Arc::new(std::sync::atomic::AtomicU64::new(0)),
|
total_bytes: Arc::new(std::sync::atomic::AtomicU64::new(total_size)),
|
||||||
downloaded_bytes: Arc::new(std::sync::atomic::AtomicU64::new(0)),
|
downloaded_bytes: Arc::new(std::sync::atomic::AtomicU64::new(0)),
|
||||||
start_time: Instant::now(),
|
start_time: Instant::now(),
|
||||||
};
|
};
|
||||||
|
@ -70,27 +82,71 @@ fn main() {
|
||||||
let progress_clone = progress.clone();
|
let progress_clone = progress.clone();
|
||||||
let title_thread = thread::spawn(move || {
|
let title_thread = thread::spawn(move || {
|
||||||
while !should_stop_clone.load(std::sync::atomic::Ordering::SeqCst) {
|
while !should_stop_clone.load(std::sync::atomic::Ordering::SeqCst) {
|
||||||
update_title(
|
let elapsed = progress_clone.start_time.elapsed();
|
||||||
progress_clone.start_time,
|
let elapsed_secs = elapsed.as_secs();
|
||||||
success_clone.load(std::sync::atomic::Ordering::SeqCst),
|
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 - {}{} - Speed: {}{} - Total ETA: {}",
|
||||||
|
current_success,
|
||||||
total_files,
|
total_files,
|
||||||
&progress_clone,
|
bytes_to_human(downloaded_bytes),
|
||||||
|
progress_percent,
|
||||||
|
speed_value,
|
||||||
|
speed_unit,
|
||||||
|
eta_str
|
||||||
);
|
);
|
||||||
|
|
||||||
|
set_title(&title).unwrap();
|
||||||
thread::sleep(Duration::from_secs(1));
|
thread::sleep(Duration::from_secs(1));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let success_clone = success.clone();
|
let should_stop_ctrlc = should_stop.clone();
|
||||||
let should_stop_clone = should_stop.clone();
|
let success_ctrlc = success.clone();
|
||||||
let log_file_clone = log_file.try_clone().unwrap();
|
let folder_ctrlc = folder.clone();
|
||||||
let folder_clone2 = folder.clone();
|
let log_file_ctrlc = log_file.try_clone().unwrap();
|
||||||
|
|
||||||
ctrlc::set_handler(move || {
|
ctrlc::set_handler(move || {
|
||||||
should_stop_clone.store(true, std::sync::atomic::Ordering::SeqCst);
|
should_stop_ctrlc.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||||
|
|
||||||
clear().unwrap();
|
clear().unwrap();
|
||||||
println!("{} Download interrupted by user", Status::warning());
|
println!("{} Download interrupted by user", Status::warning());
|
||||||
let success_count = success_clone.load(std::sync::atomic::Ordering::SeqCst);
|
let success_count = success_ctrlc.load(std::sync::atomic::Ordering::SeqCst);
|
||||||
|
|
||||||
let title = if success_count == total_files {
|
let title = if success_count == total_files {
|
||||||
" DOWNLOAD COMPLETE ".on_blue().white().bold()
|
" DOWNLOAD COMPLETE ".on_blue().white().bold()
|
||||||
|
@ -112,7 +168,7 @@ fn main() {
|
||||||
println!(
|
println!(
|
||||||
"{} Files saved to: {}",
|
"{} Files saved to: {}",
|
||||||
Status::info(),
|
Status::info(),
|
||||||
folder_clone2.display().to_string().cyan()
|
folder_ctrlc.display().to_string().cyan()
|
||||||
);
|
);
|
||||||
println!("\n{} Press Enter to exit...", Status::warning());
|
println!("\n{} Press Enter to exit...", Status::warning());
|
||||||
|
|
||||||
|
@ -120,7 +176,7 @@ fn main() {
|
||||||
io::stdin().read_line(&mut input).unwrap();
|
io::stdin().read_line(&mut input).unwrap();
|
||||||
|
|
||||||
log_error(
|
log_error(
|
||||||
&log_file_clone,
|
&log_file_ctrlc,
|
||||||
&format!(
|
&format!(
|
||||||
"Download interrupted by user. Success: {}/{}",
|
"Download interrupted by user. Success: {}/{}",
|
||||||
success_count, total_files
|
success_count, total_files
|
||||||
|
|
|
@ -2,7 +2,7 @@ use colored::Colorize;
|
||||||
use flate2::read::GzDecoder;
|
use flate2::read::GzDecoder;
|
||||||
use reqwest::blocking::Client;
|
use reqwest::blocking::Client;
|
||||||
use serde_json::{from_reader, from_str, Value};
|
use serde_json::{from_reader, from_str, Value};
|
||||||
use winconsole::console;
|
use winconsole::console::{self};
|
||||||
use std::{io::{Read, Write}, fs, io, path::Path, time::Duration};
|
use std::{io::{Read, Write}, fs, io, path::Path, time::Duration};
|
||||||
|
|
||||||
use crate::config::cfg::Config;
|
use crate::config::cfg::Config;
|
||||||
|
@ -137,7 +137,7 @@ pub fn download_file(
|
||||||
|
|
||||||
if let (Some(md5), Some(size)) = (expected_md5, file_size) {
|
if let (Some(md5), Some(size)) = (expected_md5, file_size) {
|
||||||
if should_skip_download(&path, Some(md5), Some(size)) {
|
if should_skip_download(&path, Some(md5), Some(size)) {
|
||||||
println!("{} File is valid: {}", Status::matched(), filename.blue());
|
println!("{} File is valid: {}", Status::matched(), filename.bright_purple());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -172,12 +172,12 @@ pub fn download_file(
|
||||||
|
|
||||||
if let (Some(md5), Some(size)) = (expected_md5, expected_size) {
|
if let (Some(md5), Some(size)) = (expected_md5, expected_size) {
|
||||||
if check_existing_file(&path, Some(md5), Some(size)) {
|
if check_existing_file(&path, Some(md5), Some(size)) {
|
||||||
println!("{} File is valid: {}", Status::matched(), filename.blue());
|
println!("{} File is valid: {}", Status::matched(), filename.bright_purple());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("{} Downloading: {}", Status::progress(), filename.magenta());
|
println!("{} Downloading: {}", Status::progress(), filename.purple());
|
||||||
|
|
||||||
let mut retries = MAX_RETRIES;
|
let mut retries = MAX_RETRIES;
|
||||||
let mut last_error = None;
|
let mut last_error = None;
|
||||||
|
|
Loading…
Reference in a new issue