mirror of
https://github.com/yuhkix/wuwa-downloader.git
synced 2025-06-06 17:53:44 +00:00
Compare commits
18 commits
Author | SHA1 | Date | |
---|---|---|---|
|
118b4f5fe0 | ||
|
29bd052ef4 | ||
|
bece26c433 | ||
|
cc837ec0cf | ||
|
f6477359e9 | ||
|
8c0ee23828 | ||
|
278b7a5794 | ||
|
808f929e62 | ||
|
7bd0b77bd0 | ||
|
9d1c7143ec | ||
|
1ba74834fd | ||
|
4a19233285 | ||
|
df699792e3 | ||
|
b52e2010f0 | ||
|
09f3712390 | ||
|
4d70933c4f | ||
|
ef9cbad14a | ||
|
31979bc66e |
19 changed files with 711 additions and 242 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -4,11 +4,15 @@ debug/
|
|||
target/
|
||||
.idea/
|
||||
|
||||
# packaging
|
||||
dist/
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# Error / Debug Logging
|
||||
*.log
|
||||
*.txt
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
|
73
Cargo.lock
generated
73
Cargo.lock
generated
|
@ -113,6 +113,19 @@ dependencies = [
|
|||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.15.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
|
||||
dependencies = [
|
||||
"encode_unicode",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"unicode-width",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
|
@ -150,9 +163,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "ctrlc"
|
||||
version = "3.4.5"
|
||||
version = "3.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3"
|
||||
checksum = "46f93780a459b7d656ef7f071fe699c4d3d2cb201c4b24d085b6ddc505276e73"
|
||||
dependencies = [
|
||||
"nix",
|
||||
"windows-sys 0.59.0",
|
||||
|
@ -200,6 +213,12 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encode_unicode"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.35"
|
||||
|
@ -647,6 +666,19 @@ dependencies = [
|
|||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indicatif"
|
||||
version = "0.17.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235"
|
||||
dependencies = [
|
||||
"console",
|
||||
"number_prefix",
|
||||
"portable-atomic",
|
||||
"unicode-width",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.11.0"
|
||||
|
@ -770,9 +802,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.29.0"
|
||||
version = "0.30.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
|
@ -780,6 +812,12 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "number_prefix"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.36.7"
|
||||
|
@ -869,6 +907,12 @@ version = "0.3.32"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.94"
|
||||
|
@ -1120,9 +1164,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "shellexpand"
|
||||
version = "3.1.0"
|
||||
version = "3.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b"
|
||||
checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
]
|
||||
|
@ -1386,6 +1430,12 @@ version = "1.0.18"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
|
@ -1532,6 +1582,16 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-time"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
|
@ -1849,6 +1909,7 @@ dependencies = [
|
|||
"colored",
|
||||
"ctrlc",
|
||||
"flate2",
|
||||
"indicatif",
|
||||
"md-5",
|
||||
"reqwest",
|
||||
"serde",
|
||||
|
|
21
Cargo.toml
21
Cargo.toml
|
@ -4,16 +4,25 @@ version = "0.1.0"
|
|||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
[build-dependencies]
|
||||
winres = "0.1.12"
|
||||
|
||||
[dependencies]
|
||||
colored = "3.0.0"
|
||||
md-5 = "0.10.6"
|
||||
reqwest = { version = "0.12.15", features = ["blocking", "json"] }
|
||||
serde_json = "1.0.140"
|
||||
serde = "1.0.219"
|
||||
winconsole = "0.11.1"
|
||||
ctrlc = "3.4.5"
|
||||
shellexpand = "3.1.0"
|
||||
ctrlc = "3.4.7"
|
||||
shellexpand = "3.1.1"
|
||||
flate2 = "1.1.1"
|
||||
indicatif = "0.17.11"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winconsole = "0.11.1"
|
||||
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
winres = "0.1.12"
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
lto = true
|
||||
opt-level = 3
|
||||
codegen-units = 1
|
|
@ -10,6 +10,7 @@
|
|||
## 📦 Requirements
|
||||
- **Rust nightly toolchain** - 1.87.0-nightly or newer
|
||||
- **Windows** - for full console feature support
|
||||
- **Linux** - for simply being better
|
||||
|
||||
### 🛠️ Installation & Usage
|
||||
- **Install the nightly toolchain:**
|
||||
|
@ -20,8 +21,8 @@ rustup default nightly
|
|||
|
||||
- **Clone the repository:**
|
||||
```bash
|
||||
git clone https://github.com/yourusername/wuthering-waves-downloader.git
|
||||
cd wuthering-waves-downloader
|
||||
git clone https://github.com/yuhkix/wuwa-downloader.git
|
||||
cd wuwa-downloader
|
||||
```
|
||||
|
||||
- **Build the project:**
|
||||
|
@ -74,4 +75,4 @@ cargo build --release
|
|||
|
||||
- **Multi-threaded** - Safe concurrent progress tracking
|
||||
|
||||
- **Configurable Timeouts** - 30s for metadata, 300s for downloads
|
||||
- **Configurable Timeouts** - 30s for metadata, 10000s for downloads
|
||||
|
|
5
build.rs
5
build.rs
|
@ -1,7 +1,8 @@
|
|||
fn main() {
|
||||
if std::env::var("CARGO_CFG_TARGET_OS").unwrap() == "windows" {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let mut res = winres::WindowsResource::new();
|
||||
res.set_icon("zani.ico");
|
||||
res.set_icon("cartethyia.ico");
|
||||
res.compile().unwrap();
|
||||
}
|
||||
}
|
BIN
cartethyia.ico
Normal file
BIN
cartethyia.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 64 KiB |
113
package.sh
Normal file
113
package.sh
Normal file
|
@ -0,0 +1,113 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
BIN_NAME="wuwa-downloader"
|
||||
DIST_DIR="dist"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
CYAN='\033[0;36m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
function info() {
|
||||
echo -e "${CYAN}==> $1${NC}"
|
||||
}
|
||||
|
||||
function success() {
|
||||
echo -e "${GREEN}✔ $1${NC}"
|
||||
}
|
||||
|
||||
function warn() {
|
||||
echo -e "${YELLOW}⚠ $1${NC}"
|
||||
}
|
||||
|
||||
function error() {
|
||||
echo -e "${RED}✖ $1${NC}"
|
||||
}
|
||||
|
||||
function build_linux() {
|
||||
local OUT_DIR="target/release"
|
||||
local PACKAGE_NAME="${BIN_NAME}-linux-x86_64"
|
||||
local PACKAGE_DIR="${DIST_DIR}/${PACKAGE_NAME}"
|
||||
local ARCHIVE_NAME="${PACKAGE_NAME}.tar.gz"
|
||||
|
||||
clear
|
||||
|
||||
info "Cleaning binaries to rebuild..."
|
||||
cargo clean
|
||||
|
||||
info "Building release binary for Linux..."
|
||||
cargo build --release
|
||||
clear
|
||||
success "Linux build finished"
|
||||
|
||||
info "Creating package directory..."
|
||||
rm -rf "$PACKAGE_DIR"
|
||||
mkdir -p "$PACKAGE_DIR"
|
||||
success "Package directory ready: $PACKAGE_DIR"
|
||||
|
||||
info "Copying binary..."
|
||||
cp "$OUT_DIR/$BIN_NAME" "$PACKAGE_DIR/"
|
||||
success "Copied binary to package directory"
|
||||
|
||||
info "Creating archive..."
|
||||
cd "$DIST_DIR"
|
||||
tar -czf "$ARCHIVE_NAME" "$PACKAGE_NAME"
|
||||
cd -
|
||||
success "Archive created: ${DIST_DIR}/${ARCHIVE_NAME}"
|
||||
}
|
||||
|
||||
function build_windows() {
|
||||
local TARGET="x86_64-pc-windows-gnu"
|
||||
local OUT_DIR="target/${TARGET}/release"
|
||||
local PACKAGE_NAME="${BIN_NAME}-windows-x86_64"
|
||||
local PACKAGE_DIR="${DIST_DIR}/${PACKAGE_NAME}"
|
||||
local ARCHIVE_NAME="${PACKAGE_NAME}.zip"
|
||||
|
||||
clear
|
||||
|
||||
info "Cleaning binaries to rebuild..."
|
||||
cargo clean
|
||||
|
||||
info "Building release binary for Windows..."
|
||||
cargo build --release --target "$TARGET"
|
||||
clear
|
||||
success "Windows build finished"
|
||||
|
||||
info "Creating package directory..."
|
||||
rm -rf "$PACKAGE_DIR"
|
||||
mkdir -p "$PACKAGE_DIR"
|
||||
success "Package directory ready: $PACKAGE_DIR"
|
||||
|
||||
info "Copying binary..."
|
||||
cp "$OUT_DIR/${BIN_NAME}.exe" "$PACKAGE_DIR/"
|
||||
success "Copied binary to package directory"
|
||||
|
||||
info "Creating archive..."
|
||||
cd "$DIST_DIR"
|
||||
zip -r "$ARCHIVE_NAME" "$PACKAGE_NAME"
|
||||
cd -
|
||||
success "Archive created: ${DIST_DIR}/${ARCHIVE_NAME}"
|
||||
}
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
error "Usage: $0 [linux|windows]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "$1" in
|
||||
linux)
|
||||
build_linux
|
||||
;;
|
||||
windows)
|
||||
build_windows
|
||||
;;
|
||||
*)
|
||||
error "Unknown target: $1"
|
||||
error "Usage: $0 [linux|windows]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
|
@ -4,11 +4,25 @@ use colored::*;
|
|||
pub struct Status;
|
||||
|
||||
impl Status {
|
||||
pub fn info() -> ColoredString { "[*]".cyan() }
|
||||
pub fn success() -> ColoredString { "[+]".green() }
|
||||
pub fn warning() -> ColoredString { "[!]".yellow() }
|
||||
pub fn error() -> ColoredString { "[-]".red() }
|
||||
pub fn question() -> ColoredString { "[?]".blue() }
|
||||
pub fn progress() -> ColoredString { "[→]".purple() }
|
||||
pub fn matched() -> ColoredString { "[↓]".bright_purple() }
|
||||
pub fn info() -> ColoredString {
|
||||
"[*]".cyan()
|
||||
}
|
||||
pub fn success() -> ColoredString {
|
||||
"[+]".green()
|
||||
}
|
||||
pub fn warning() -> ColoredString {
|
||||
"[!]".yellow()
|
||||
}
|
||||
pub fn error() -> ColoredString {
|
||||
"[-]".red()
|
||||
}
|
||||
pub fn question() -> ColoredString {
|
||||
"[?]".blue()
|
||||
}
|
||||
pub fn progress() -> ColoredString {
|
||||
"[→]".purple()
|
||||
}
|
||||
pub fn matched() -> ColoredString {
|
||||
"[↓]".bright_purple()
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
use std::{io, path::Path};
|
||||
use colored::Colorize;
|
||||
use crate::config::status::Status;
|
||||
use colored::Colorize;
|
||||
use std::{io, path::Path};
|
||||
|
||||
pub fn print_results(success: usize, total: usize, folder: &Path) {
|
||||
let title = if success == total {
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
use std::{fs, io, io::Write, path::{Path, PathBuf}};
|
||||
use md5::{Digest, Md5};
|
||||
use std::{
|
||||
fs, io,
|
||||
io::Write,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crate::config::status::Status;
|
||||
|
||||
|
@ -10,7 +14,11 @@ pub fn calculate_md5(path: &Path) -> String {
|
|||
format!("{:x}", hasher.finalize())
|
||||
}
|
||||
|
||||
pub fn check_existing_file(path: &Path, expected_md5: Option<&str>, expected_size: Option<u64>) -> bool {
|
||||
pub fn check_existing_file(
|
||||
path: &Path,
|
||||
expected_md5: Option<&str>,
|
||||
expected_size: Option<u64>,
|
||||
) -> bool {
|
||||
if !path.exists() {
|
||||
return false;
|
||||
}
|
||||
|
@ -41,7 +49,7 @@ pub fn get_filename(path: &str) -> String {
|
|||
pub fn get_dir() -> PathBuf {
|
||||
loop {
|
||||
print!(
|
||||
"{} Enter download directory (Enter for current): ",
|
||||
"{} Please specify the directory where the game should be downloaded (press Enter to use the current directory): ",
|
||||
Status::question()
|
||||
);
|
||||
io::stdout().flush().unwrap();
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
use std::{fs::{self, OpenOptions}, io::Write, time::SystemTime};
|
||||
|
||||
use std::{
|
||||
fs::{self, OpenOptions},
|
||||
io::Write,
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
pub fn setup_logging() -> fs::File {
|
||||
OpenOptions::new()
|
||||
|
|
|
@ -1,14 +1,35 @@
|
|||
use std::{fs::File, io, sync::Arc, thread, time::{Duration, Instant}};
|
||||
use colored::Colorize;
|
||||
use reqwest::blocking::Client;
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::{self, Write},
|
||||
sync::Arc,
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
use std::process::Command;
|
||||
|
||||
#[cfg(windows)]
|
||||
use winconsole::console::{clear, set_title};
|
||||
|
||||
use crate::{config::{cfg::Config, status::Status}, download::progress::DownloadProgress, io::logging::log_error, network::client::download_file};
|
||||
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();
|
||||
format!("{:02}:{:02}:{:02}", secs / 3600, (secs % 3600) / 60, secs % 60)
|
||||
format!(
|
||||
"{:02}:{:02}:{:02}",
|
||||
secs / 3600,
|
||||
(secs % 3600) / 60,
|
||||
secs % 60
|
||||
)
|
||||
}
|
||||
|
||||
pub fn bytes_to_human(bytes: u64) -> String {
|
||||
|
@ -20,6 +41,23 @@ pub fn bytes_to_human(bytes: u64) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
fn log_url(url: &str) {
|
||||
let sanitized_url = if let Some(index) = url.find("://") {
|
||||
let (scheme, rest) = url.split_at(index + 3);
|
||||
format!("{}{}", scheme, rest.replace("//", "/"))
|
||||
} else {
|
||||
url.replace("//", "/")
|
||||
};
|
||||
|
||||
if let Ok(mut url_log) = fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open("urls.txt")
|
||||
{
|
||||
let _ = writeln!(url_log, "{}", sanitized_url);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn calculate_total_size(resources: &[Value], client: &Client, config: &Config) -> u64 {
|
||||
let mut total_size = 0;
|
||||
let mut failed_urls = 0;
|
||||
|
@ -33,6 +71,7 @@ pub fn calculate_total_size(resources: &[Value], client: &Client, config: &Confi
|
|||
|
||||
for base_url in &config.zip_bases {
|
||||
let url = format!("{}/{}", base_url, dest);
|
||||
log_url(&url);
|
||||
match client.head(&url).send() {
|
||||
Ok(response) => {
|
||||
if let Some(len) = response.headers().get("content-length") {
|
||||
|
@ -55,7 +94,11 @@ pub fn calculate_total_size(resources: &[Value], client: &Client, config: &Confi
|
|||
total_size += file_size;
|
||||
} else {
|
||||
failed_urls += 1;
|
||||
println!("{} Could not determine size for file: {}", Status::error(), dest);
|
||||
println!(
|
||||
"{} Could not determine size for file: {}",
|
||||
Status::error(),
|
||||
dest
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -83,6 +126,9 @@ pub fn calculate_total_size(resources: &[Value], client: &Client, config: &Confi
|
|||
bytes_to_human(total_size).cyan()
|
||||
);
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
Command::new("clear").status().unwrap();
|
||||
|
||||
total_size
|
||||
}
|
||||
|
||||
|
@ -95,14 +141,19 @@ pub fn get_version(data: &Value, category: &str, version: &str) -> Result<String
|
|||
|
||||
pub fn exit_with_error(log_file: &File, error: &str) -> ! {
|
||||
log_error(log_file, error);
|
||||
|
||||
#[cfg(windows)]
|
||||
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) -> (
|
||||
pub fn track_progress(
|
||||
total_size: u64,
|
||||
) -> (
|
||||
Arc<std::sync::atomic::AtomicBool>,
|
||||
Arc<std::sync::atomic::AtomicUsize>,
|
||||
DownloadProgress,
|
||||
|
@ -129,11 +180,19 @@ pub fn start_title_thread(
|
|||
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 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 = 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 {
|
||||
|
@ -142,7 +201,11 @@ pub fn start_title_thread(
|
|||
|
||||
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_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 {
|
||||
|
@ -152,7 +215,7 @@ pub fn start_title_thread(
|
|||
};
|
||||
|
||||
let title = format!(
|
||||
"Wuthering Waves Downloader - {}/{} files - Current File: {}{} - Speed: {}{} - Total ETA: {}",
|
||||
"Wuthering Waves Downloader - {}/{} files - Total Downloaded: {}{} - Speed: {}{} - Total ETA: {}",
|
||||
current_success,
|
||||
total_files,
|
||||
bytes_to_human(downloaded_bytes),
|
||||
|
@ -162,7 +225,9 @@ pub fn start_title_thread(
|
|||
eta_str
|
||||
);
|
||||
|
||||
#[cfg(windows)]
|
||||
set_title(&title).unwrap();
|
||||
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
})
|
||||
|
@ -171,7 +236,10 @@ pub fn start_title_thread(
|
|||
pub fn setup_ctrlc(should_stop: Arc<std::sync::atomic::AtomicBool>) {
|
||||
ctrlc::set_handler(move || {
|
||||
should_stop.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
|
||||
#[cfg(windows)]
|
||||
clear().unwrap();
|
||||
|
||||
println!("\n{} Download interrupted by user", Status::warning());
|
||||
})
|
||||
.unwrap();
|
||||
|
|
23
src/main.rs
23
src/main.rs
|
@ -1,6 +1,11 @@
|
|||
use colored::*;
|
||||
use reqwest::blocking::Client;
|
||||
use serde_json::Value;
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
use std::process::Command;
|
||||
|
||||
#[cfg(windows)]
|
||||
use winconsole::console::{clear, set_title};
|
||||
|
||||
use wuwa_downloader::{
|
||||
|
@ -8,16 +13,19 @@ use wuwa_downloader::{
|
|||
io::{
|
||||
console::print_results,
|
||||
file::get_dir,
|
||||
util::{
|
||||
calculate_total_size, download_resources, exit_with_error, setup_ctrlc, track_progress, start_title_thread
|
||||
},
|
||||
logging::setup_logging,
|
||||
util::{
|
||||
calculate_total_size, download_resources, exit_with_error, setup_ctrlc,
|
||||
start_title_thread, track_progress,
|
||||
},
|
||||
},
|
||||
network::client::{fetch_index, get_config},
|
||||
};
|
||||
|
||||
fn main() {
|
||||
#[cfg(windows)]
|
||||
set_title("Wuthering Waves Downloader").unwrap();
|
||||
|
||||
let log_file = setup_logging();
|
||||
let client = Client::new();
|
||||
|
||||
|
@ -27,7 +35,12 @@ fn main() {
|
|||
};
|
||||
|
||||
let folder = get_dir();
|
||||
|
||||
#[cfg(windows)]
|
||||
clear().unwrap();
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
Command::new("clear").status().unwrap();
|
||||
|
||||
println!(
|
||||
"\n{} Download folder: {}\n",
|
||||
Status::info(),
|
||||
|
@ -47,6 +60,8 @@ fn main() {
|
|||
);
|
||||
|
||||
let total_size = calculate_total_size(resources, &client, &config);
|
||||
|
||||
#[cfg(windows)]
|
||||
clear().unwrap();
|
||||
|
||||
let (should_stop, success, progress) = track_progress(total_size);
|
||||
|
@ -74,7 +89,9 @@ fn main() {
|
|||
should_stop.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
title_thread.join().unwrap();
|
||||
|
||||
#[cfg(windows)]
|
||||
clear().unwrap();
|
||||
|
||||
print_results(
|
||||
success.load(std::sync::atomic::Ordering::SeqCst),
|
||||
resources.len(),
|
||||
|
|
|
@ -1,20 +1,31 @@
|
|||
use colored::Colorize;
|
||||
use flate2::read::GzDecoder;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use reqwest::blocking::Client;
|
||||
use serde_json::{from_reader, from_str, Value};
|
||||
use winconsole::console::{self};
|
||||
use std::{io::{Read, Write}, fs, io, path::Path, time::Duration};
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
use std::process::Command;
|
||||
use std::{
|
||||
fs::{self, OpenOptions},
|
||||
io::{self, Read, Write},
|
||||
path::Path,
|
||||
time::Duration,
|
||||
u64,
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
use winconsole::console::clear;
|
||||
|
||||
use crate::config::cfg::Config;
|
||||
use crate::config::status::Status;
|
||||
use crate::download::progress::DownloadProgress;
|
||||
use crate::io::file::{calculate_md5, check_existing_file, get_filename};
|
||||
use crate::io::{logging::log_error, util::get_version};
|
||||
use crate::config::status::Status;
|
||||
|
||||
const INDEX_URL: &str = "https://gist.githubusercontent.com/yuhkix/b8796681ac2cd3bab11b7e8cdc022254/raw/4435fd290c07f7f766a6d2ab09ed3096d83b02e3/wuwa.json";
|
||||
const MAX_RETRIES: usize = 3;
|
||||
const DOWNLOAD_TIMEOUT: u64 = 300;
|
||||
const BUFFER_SIZE: usize = 8192;
|
||||
const DOWNLOAD_TIMEOUT: Duration = Duration::from_secs(10000);
|
||||
const BUFFER_SIZE: usize = 65536;
|
||||
|
||||
pub fn fetch_index(client: &Client, config: &Config, log_file: &fs::File) -> Value {
|
||||
println!("{} Fetching index file...", Status::info());
|
||||
|
@ -27,7 +38,10 @@ pub fn fetch_index(client: &Client, config: &Config, log_file: &fs::File) -> Val
|
|||
Ok(resp) => resp,
|
||||
Err(e) => {
|
||||
log_error(log_file, &format!("Error fetching index file: {}", e));
|
||||
console::clear().unwrap();
|
||||
|
||||
#[cfg(windows)]
|
||||
clear().unwrap();
|
||||
|
||||
println!("{} Error fetching index file: {}", Status::error(), e);
|
||||
println!("\n{} Press Enter to exit...", Status::warning());
|
||||
let _ = io::stdin().read_line(&mut String::new());
|
||||
|
@ -38,7 +52,10 @@ pub fn fetch_index(client: &Client, config: &Config, log_file: &fs::File) -> Val
|
|||
if !response.status().is_success() {
|
||||
let msg = format!("Error fetching index file: HTTP {}", response.status());
|
||||
log_error(log_file, &msg);
|
||||
console::clear().unwrap();
|
||||
|
||||
#[cfg(windows)]
|
||||
clear().unwrap();
|
||||
|
||||
println!("{} {}", Status::error(), msg);
|
||||
println!("\n{} Press Enter to exit...", Status::warning());
|
||||
let _ = io::stdin().read_line(&mut String::new());
|
||||
|
@ -55,7 +72,10 @@ pub fn fetch_index(client: &Client, config: &Config, log_file: &fs::File) -> Val
|
|||
let mut buffer = Vec::new();
|
||||
if let Err(e) = response.copy_to(&mut buffer) {
|
||||
log_error(log_file, &format!("Error reading index file bytes: {}", e));
|
||||
console::clear().unwrap();
|
||||
|
||||
#[cfg(windows)]
|
||||
clear().unwrap();
|
||||
|
||||
println!("{} Error reading index file: {}", Status::error(), e);
|
||||
println!("\n{} Press Enter to exit...", Status::warning());
|
||||
let _ = io::stdin().read_line(&mut String::new());
|
||||
|
@ -66,7 +86,10 @@ pub fn fetch_index(client: &Client, config: &Config, log_file: &fs::File) -> Val
|
|||
let mut decompressed_text = String::new();
|
||||
if let Err(e) = gz.read_to_string(&mut decompressed_text) {
|
||||
log_error(log_file, &format!("Error decompressing index file: {}", e));
|
||||
console::clear().unwrap();
|
||||
|
||||
#[cfg(windows)]
|
||||
clear().unwrap();
|
||||
|
||||
println!("{} Error decompressing index file: {}", Status::error(), e);
|
||||
println!("\n{} Press Enter to exit...", Status::warning());
|
||||
let _ = io::stdin().read_line(&mut String::new());
|
||||
|
@ -81,7 +104,10 @@ pub fn fetch_index(client: &Client, config: &Config, log_file: &fs::File) -> Val
|
|||
log_file,
|
||||
&format!("Error reading index file response: {}", e),
|
||||
);
|
||||
console::clear().unwrap();
|
||||
|
||||
#[cfg(windows)]
|
||||
clear().unwrap();
|
||||
|
||||
println!("{} Error reading index file: {}", Status::error(), e);
|
||||
println!("\n{} Press Enter to exit...", Status::warning());
|
||||
let _ = io::stdin().read_line(&mut String::new());
|
||||
|
@ -96,7 +122,10 @@ pub fn fetch_index(client: &Client, config: &Config, log_file: &fs::File) -> Val
|
|||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
log_error(log_file, &format!("Error parsing index file JSON: {}", e));
|
||||
console::clear().unwrap();
|
||||
|
||||
#[cfg(windows)]
|
||||
clear().unwrap();
|
||||
|
||||
println!("{} Error parsing index file: {}", Status::error(), e);
|
||||
println!("\n{} Press Enter to exit...", Status::warning());
|
||||
let _ = io::stdin().read_line(&mut String::new());
|
||||
|
@ -133,13 +162,16 @@ pub fn download_file(
|
|||
let url = format!("{}{}", base_url, dest);
|
||||
|
||||
if let Ok(head_response) = client.head(&url).timeout(Duration::from_secs(10)).send() {
|
||||
if let Some(size) = head_response.headers()
|
||||
if let Some(size) = head_response
|
||||
.headers()
|
||||
.get("content-length")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|s| s.parse::<u64>().ok())
|
||||
{
|
||||
file_size = Some(size);
|
||||
progress.total_bytes.fetch_add(size, std::sync::atomic::Ordering::SeqCst);
|
||||
progress
|
||||
.total_bytes
|
||||
.fetch_add(size, std::sync::atomic::Ordering::SeqCst);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -147,7 +179,11 @@ pub fn download_file(
|
|||
|
||||
if let (Some(md5), Some(size)) = (expected_md5, file_size) {
|
||||
if should_skip_download(&path, Some(md5), Some(size)) {
|
||||
println!("{} File is valid: {}", Status::matched(), filename.bright_purple());
|
||||
println!(
|
||||
"{} File is valid: {}",
|
||||
Status::matched(),
|
||||
filename.bright_purple()
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -166,39 +202,59 @@ pub fn download_file(
|
|||
let head_response = match client.head(&url).timeout(Duration::from_secs(10)).send() {
|
||||
Ok(resp) if resp.status().is_success() => resp,
|
||||
Ok(resp) => {
|
||||
log_error(log_file, &format!("CDN {} failed for {} (HTTP {})", i+1, dest, resp.status()));
|
||||
log_error(
|
||||
log_file,
|
||||
&format!("CDN {} failed for {} (HTTP {})", i + 1, dest, resp.status()),
|
||||
);
|
||||
continue;
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
log_error(log_file, &format!("CDN {} failed for {}: {}", i+1, dest, e));
|
||||
log_error(
|
||||
log_file,
|
||||
&format!("CDN {} failed for {}: {}", i + 1, dest, e),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let expected_size = file_size.or_else(|| head_response.headers()
|
||||
let expected_size = file_size.or_else(|| {
|
||||
head_response
|
||||
.headers()
|
||||
.get("content-length")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|s| s.parse::<u64>().ok()));
|
||||
.and_then(|s| s.parse::<u64>().ok())
|
||||
});
|
||||
|
||||
if let (Some(md5), Some(size)) = (expected_md5, expected_size) {
|
||||
if check_existing_file(&path, Some(md5), Some(size)) {
|
||||
println!("{} File is valid: {}", Status::matched(), filename.bright_purple());
|
||||
println!(
|
||||
"{} File is valid: {}",
|
||||
Status::matched(),
|
||||
filename.bright_purple()
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
println!("{} Downloading: {}", Status::progress(), filename.purple());
|
||||
|
||||
let pb = ProgressBar::new(expected_size.unwrap_or(0));
|
||||
pb.set_style(ProgressStyle::default_bar()
|
||||
.template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta}, {binary_bytes_per_sec})")
|
||||
.unwrap()
|
||||
.progress_chars("#>-"));
|
||||
|
||||
let mut retries = MAX_RETRIES;
|
||||
let mut last_error = None;
|
||||
|
||||
while retries > 0 {
|
||||
let result = download_single_file(&client, &url, &path, should_stop, progress);
|
||||
let result = download_single_file(&client, &url, &path, should_stop, progress, &pb);
|
||||
|
||||
match result {
|
||||
Ok(_) => break,
|
||||
Err(e) => {
|
||||
if should_stop.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
pb.finish_and_clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -207,20 +263,32 @@ pub fn download_file(
|
|||
let _ = fs::remove_file(&path);
|
||||
|
||||
if retries > 0 {
|
||||
println!("{} Retrying {}... ({} left)",
|
||||
Status::warning(), filename.yellow(), retries);
|
||||
println!(
|
||||
"{} Retrying {}... ({} left)",
|
||||
Status::warning(),
|
||||
filename.yellow(),
|
||||
retries
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pb.finish_and_clear();
|
||||
|
||||
if should_stop.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if retries == 0 {
|
||||
log_error(log_file, &format!("Failed after retries for {}: {}", dest,
|
||||
last_error.unwrap_or_default()));
|
||||
log_error(
|
||||
log_file,
|
||||
&format!(
|
||||
"Failed after retries for {}: {}",
|
||||
dest,
|
||||
last_error.unwrap_or_default()
|
||||
),
|
||||
);
|
||||
println!("{} Failed: {}", Status::error(), filename.red());
|
||||
return false;
|
||||
}
|
||||
|
@ -232,8 +300,13 @@ pub fn download_file(
|
|||
|
||||
let actual = calculate_md5(&path);
|
||||
if actual != expected {
|
||||
log_error(log_file, &format!("Checksum failed for {}: expected {}, got {}",
|
||||
dest, expected, actual));
|
||||
log_error(
|
||||
log_file,
|
||||
&format!(
|
||||
"Checksum failed for {}: expected {}, got {}",
|
||||
dest, expected, actual
|
||||
),
|
||||
);
|
||||
fs::remove_file(&path).unwrap();
|
||||
println!("{} Checksum failed: {}", Status::error(), filename.red());
|
||||
return false;
|
||||
|
@ -255,27 +328,55 @@ fn download_single_file(
|
|||
path: &Path,
|
||||
should_stop: &std::sync::atomic::AtomicBool,
|
||||
progress: &DownloadProgress,
|
||||
pb: &ProgressBar,
|
||||
) -> Result<(), String> {
|
||||
let mut response = client
|
||||
.get(url)
|
||||
.timeout(Duration::from_secs(DOWNLOAD_TIMEOUT))
|
||||
let mut downloaded: u64 = 0;
|
||||
if path.exists() {
|
||||
downloaded = fs::metadata(path)
|
||||
.map_err(|e| format!("Metadata error: {}", e))?
|
||||
.len();
|
||||
}
|
||||
|
||||
let request = client.get(url).timeout(DOWNLOAD_TIMEOUT);
|
||||
|
||||
let request = if downloaded > 0 {
|
||||
request.header("Range", format!("bytes={}-", downloaded))
|
||||
} else {
|
||||
request
|
||||
};
|
||||
|
||||
let mut response = request
|
||||
.send()
|
||||
.map_err(|e| format!("Network error: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
if response.status() == reqwest::StatusCode::RANGE_NOT_SATISFIABLE {
|
||||
return Err("Range not satisfiable. File may already be fully downloaded.".into());
|
||||
}
|
||||
|
||||
if !response.status().is_success() && response.status() != reqwest::StatusCode::PARTIAL_CONTENT
|
||||
{
|
||||
return Err(format!("HTTP error: {}", response.status()));
|
||||
}
|
||||
|
||||
let mut file = fs::File::create(path)
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(path)
|
||||
.map_err(|e| format!("File error: {}", e))?;
|
||||
|
||||
pb.set_position(downloaded);
|
||||
progress
|
||||
.downloaded_bytes
|
||||
.store(downloaded, std::sync::atomic::Ordering::SeqCst);
|
||||
|
||||
let mut buffer = [0; BUFFER_SIZE];
|
||||
loop {
|
||||
if should_stop.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
return Err("Download interrupted".into());
|
||||
}
|
||||
|
||||
let bytes_read = response.read(&mut buffer)
|
||||
let bytes_read = response
|
||||
.read(&mut buffer)
|
||||
.map_err(|e| format!("Read error: {}", e))?;
|
||||
|
||||
if bytes_read == 0 {
|
||||
|
@ -285,7 +386,11 @@ fn download_single_file(
|
|||
file.write_all(&buffer[..bytes_read])
|
||||
.map_err(|e| format!("Write error: {}", e))?;
|
||||
|
||||
progress.downloaded_bytes.fetch_add(bytes_read as u64, std::sync::atomic::Ordering::SeqCst);
|
||||
downloaded += bytes_read as u64;
|
||||
pb.set_position(downloaded);
|
||||
progress
|
||||
.downloaded_bytes
|
||||
.store(downloaded, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -293,6 +398,10 @@ fn download_single_file(
|
|||
|
||||
pub fn get_config(client: &Client) -> Result<Config, String> {
|
||||
let selected_index_url = fetch_gist(client)?;
|
||||
|
||||
#[cfg(windows)]
|
||||
clear().unwrap();
|
||||
|
||||
println!("{} Fetching download configuration...", Status::info());
|
||||
|
||||
let mut response = client
|
||||
|
@ -339,10 +448,14 @@ pub fn get_config(client: &Client) -> Result<Config, String> {
|
|||
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))?;
|
||||
(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()
|
||||
|
@ -354,9 +467,12 @@ pub fn get_config(client: &Client) -> Result<Config, String> {
|
|||
"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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
(false, false) => return Err("Neither default.config nor predownload.config found in response".to_string()),
|
||||
};
|
||||
|
||||
let config_data = config
|
||||
|
@ -393,10 +509,10 @@ pub fn get_config(client: &Client) -> Result<Config, String> {
|
|||
return Err("No valid CDN URLs found".to_string());
|
||||
}
|
||||
|
||||
let full_index_url = format!("{}/{}", cdn_urls[0], index_file.trim_start_matches('/'));
|
||||
let full_index_url = format!("{}//{}", cdn_urls[0], index_file.trim_start_matches('/'));
|
||||
let zip_bases = cdn_urls
|
||||
.iter()
|
||||
.map(|cdn| format!("{}/{}", cdn, base_url.trim_start_matches('/')))
|
||||
.map(|cdn| format!("{}//{}", cdn, base_url.trim_start_matches('/')))
|
||||
.collect();
|
||||
|
||||
Ok(Config {
|
||||
|
@ -432,24 +548,78 @@ pub fn fetch_gist(client: &Client) -> Result<String, String> {
|
|||
|
||||
let gist_data: Value = if content_encoding.contains("gzip") {
|
||||
let mut buffer = Vec::new();
|
||||
response.copy_to(&mut buffer)
|
||||
response
|
||||
.copy_to(&mut buffer)
|
||||
.map_err(|e| format!("Error reading response: {}", e))?;
|
||||
|
||||
let mut gz = GzDecoder::new(&buffer[..]);
|
||||
let mut decompressed = String::new();
|
||||
gz.read_to_string(&mut decompressed)
|
||||
.map_err(|e| format!("Error decompressing: {}", e))?;
|
||||
|
||||
from_str(&decompressed).map_err(|e| format!("Invalid JSON: {}", e))?
|
||||
} else {
|
||||
from_reader(response).map_err(|e| format!("Invalid JSON: {}", e))?
|
||||
};
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
Command::new("clear").status().unwrap();
|
||||
|
||||
let entries = [
|
||||
("live", "os", "Live - OS"),
|
||||
("live", "cn", "Live - CN"),
|
||||
("beta", "os", "Beta - OS"),
|
||||
("beta", "cn", "Beta - CN"),
|
||||
];
|
||||
|
||||
println!("{} Available versions:", Status::info());
|
||||
println!("1. Live - OS");
|
||||
println!("2. Live - CN");
|
||||
println!("3. Beta - OS");
|
||||
println!("4. Beta - CN");
|
||||
|
||||
for (i, (cat, ver, label)) in entries.iter().enumerate() {
|
||||
let index_url = get_version(&gist_data, cat, ver)?;
|
||||
|
||||
let mut resp = client
|
||||
.get(&index_url)
|
||||
.send()
|
||||
.map_err(|e| format!("Error fetching index.json: {}", e))
|
||||
.unwrap();
|
||||
|
||||
let version_json: Value = {
|
||||
let content_encoding = resp
|
||||
.headers()
|
||||
.get("content-encoding")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("");
|
||||
|
||||
if content_encoding.contains("gzip") {
|
||||
let mut buffer = Vec::new();
|
||||
resp.copy_to(&mut buffer)
|
||||
.map_err(|e| format!("Error reading response: {}", e))
|
||||
.unwrap();
|
||||
|
||||
let mut gz = GzDecoder::new(&buffer[..]);
|
||||
let mut decompressed = String::new();
|
||||
gz.read_to_string(&mut decompressed)
|
||||
.map_err(|e| format!("Error decompressing: {}", e))
|
||||
.unwrap();
|
||||
|
||||
from_str(&decompressed)
|
||||
.map_err(|e| format!("Invalid JSON: {}", e))
|
||||
.unwrap()
|
||||
} else {
|
||||
from_reader(resp)
|
||||
.map_err(|e| format!("Invalid JSON: {}", e))
|
||||
.unwrap()
|
||||
}
|
||||
};
|
||||
|
||||
let version = version_json
|
||||
.get("default")
|
||||
.and_then(|d| d.get("config"))
|
||||
.and_then(|c| c.get("version"))
|
||||
.or_else(|| version_json.get("default").and_then(|d| d.get("version")))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("unknown");
|
||||
|
||||
println!("{}. {} ({})", i + 1, label, version);
|
||||
}
|
||||
|
||||
loop {
|
||||
print!("{} Select version: ", Status::question());
|
||||
|
|
BIN
zani.ico
BIN
zani.ico
Binary file not shown.
Before Width: | Height: | Size: 42 KiB |
Loading…
Reference in a new issue