mirror of
https://github.com/yuhkix/wuwa-downloader.git
synced 2025-06-04 08:53:41 +00:00
Compare commits
5 commits
Author | SHA1 | Date | |
---|---|---|---|
|
118b4f5fe0 | ||
|
29bd052ef4 | ||
|
bece26c433 | ||
|
cc837ec0cf | ||
|
f6477359e9 |
12 changed files with 266 additions and 38 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -4,6 +4,9 @@ debug/
|
|||
target/
|
||||
.idea/
|
||||
|
||||
# packaging
|
||||
dist/
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
|
|
|
@ -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:**
|
||||
|
|
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
|
|
@ -2,4 +2,4 @@
|
|||
pub struct Config {
|
||||
pub index_url: String,
|
||||
pub zip_bases: Vec<String>,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,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,4 +1,4 @@
|
|||
pub mod console;
|
||||
pub mod file;
|
||||
pub mod logging;
|
||||
pub mod util;
|
||||
pub mod util;
|
||||
|
|
|
@ -2,8 +2,16 @@ use colored::Colorize;
|
|||
use reqwest::blocking::Client;
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
fs::{self, File}, io::{self, Write}, process::Command, sync::Arc, thread, time::{Duration, Instant}
|
||||
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};
|
||||
|
||||
|
@ -120,7 +128,7 @@ pub fn calculate_total_size(resources: &[Value], client: &Client, config: &Confi
|
|||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
Command::new("clear").status().unwrap();
|
||||
|
||||
|
||||
total_size
|
||||
}
|
||||
|
||||
|
|
15
src/main.rs
15
src/main.rs
|
@ -1,8 +1,10 @@
|
|||
use std::process::Command;
|
||||
|
||||
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};
|
||||
|
||||
|
@ -11,10 +13,11 @@ 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},
|
||||
};
|
||||
|
@ -88,7 +91,7 @@ fn main() {
|
|||
|
||||
#[cfg(windows)]
|
||||
clear().unwrap();
|
||||
|
||||
|
||||
print_results(
|
||||
success.load(std::sync::atomic::Ordering::SeqCst),
|
||||
resources.len(),
|
||||
|
|
|
@ -2,10 +2,17 @@ use colored::Colorize;
|
|||
use flate2::read::GzDecoder;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use reqwest::blocking::Client;
|
||||
use serde_json::{Value, from_reader, from_str};
|
||||
use serde_json::{from_reader, from_str, Value};
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
use std::process::Command;
|
||||
use std::{
|
||||
fs, io::{self, Read, Write}, path::Path, process::Command, time::Duration, u64
|
||||
fs::{self, OpenOptions},
|
||||
io::{self, Read, Write},
|
||||
path::Path,
|
||||
time::Duration,
|
||||
u64,
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
use winconsole::console::clear;
|
||||
|
||||
|
@ -323,20 +330,46 @@ fn download_single_file(
|
|||
progress: &DownloadProgress,
|
||||
pb: &ProgressBar,
|
||||
) -> Result<(), String> {
|
||||
let mut response = client
|
||||
.get(url)
|
||||
.timeout(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).map_err(|e| format!("File error: {}", e))?;
|
||||
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];
|
||||
let mut downloaded: u64 = 0;
|
||||
loop {
|
||||
if should_stop.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
return Err("Download interrupted".into());
|
||||
|
@ -357,7 +390,7 @@ fn download_single_file(
|
|||
pb.set_position(downloaded);
|
||||
progress
|
||||
.downloaded_bytes
|
||||
.fetch_add(bytes_read as u64, std::sync::atomic::Ordering::SeqCst);
|
||||
.store(downloaded, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -518,12 +551,10 @@ pub fn fetch_gist(client: &Client) -> Result<String, String> {
|
|||
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))?
|
||||
|
@ -532,11 +563,63 @@ pub fn fetch_gist(client: &Client) -> Result<String, String> {
|
|||
#[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 (wicked-waifus-rs)");
|
||||
|
||||
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());
|
||||
|
|
|
@ -1 +1 @@
|
|||
pub mod client;
|
||||
pub mod client;
|
||||
|
|
Loading…
Reference in a new issue