Initial commit
This commit is contained in:
commit
6540a3755a
18 changed files with 2523 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
/target
|
||||||
|
/test_game_dl
|
||||||
|
/test_launcher_dl
|
||||||
|
/.idea
|
1630
Cargo.lock
generated
Normal file
1630
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
36
Cargo.toml
Normal file
36
Cargo.toml
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
[workspace]
|
||||||
|
resolver = "2"
|
||||||
|
members = [
|
||||||
|
"wicked-waifus-archiver/wicked-waifus-archiver-cli",
|
||||||
|
"wicked-waifus-downloader/wicked-waifus-downloader-cli",
|
||||||
|
"wicked-waifus-downloader/wicked-waifus-downloader-domain",
|
||||||
|
"wicked-waifus-downloader/wicked-waifus-downloader-lib"
|
||||||
|
]
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
clap = { version = "4.5.36", features = ["derive"] }
|
||||||
|
colog = "1.3.0"
|
||||||
|
constant_time_eq = "0.4.2"
|
||||||
|
hex = "0.4.3"
|
||||||
|
indicatif = "0.17.11"
|
||||||
|
indicatif-log-bridge = "0.2.3"
|
||||||
|
log = "0.4.27"
|
||||||
|
md-5 = "0.10.6"
|
||||||
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
|
serde_json = "1.0.140"
|
||||||
|
thiserror = "2.0.12"
|
||||||
|
ureq = { version = "3.0.10", features = ["charset", "cookies", "gzip", "json", "platform-verifier", "rustls"] }
|
||||||
|
url = "2.5.4"
|
||||||
|
wicked-waifus-downloader-domain = { path = "wicked-waifus-downloader/wicked-waifus-downloader-domain" }
|
||||||
|
wicked-waifus-downloader-lib = { path = "wicked-waifus-downloader/wicked-waifus-downloader-lib" }
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
strip = true # Automatically strip symbols from the binary.
|
||||||
|
lto = true # Link-time optimization.
|
||||||
|
opt-level = 3 # Optimization level 3.
|
||||||
|
codegen-units = 1 # Maximum size reduction optimizations.
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
[package]
|
||||||
|
name = "wicked-waifus-archiver-cli"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
|
@ -0,0 +1,3 @@
|
||||||
|
fn main() {
|
||||||
|
println!("Hello, world!");
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
[package]
|
||||||
|
name = "wicked-waifus-downloader-cli"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap.workspace = true
|
||||||
|
colog.workspace = true
|
||||||
|
constant_time_eq.workspace = true
|
||||||
|
hex.workspace = true
|
||||||
|
indicatif.workspace = true
|
||||||
|
indicatif-log-bridge.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
md-5.workspace = true
|
||||||
|
thiserror.workspace = true
|
||||||
|
url.workspace = true
|
||||||
|
|
||||||
|
wicked-waifus-downloader-domain.workspace = true
|
||||||
|
wicked-waifus-downloader-lib.workspace = true
|
|
@ -0,0 +1,23 @@
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub(crate) enum Error {
|
||||||
|
#[error("Hex FromHex Error {0}")]
|
||||||
|
HexFromHex(#[from] hex::FromHexError),
|
||||||
|
#[error("Url Parse error Error {0}")]
|
||||||
|
UrlParse(#[from] url::ParseError),
|
||||||
|
#[error("Io Error {0}")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
#[error("Wicked Waifus Downloader Lib Error {0}")]
|
||||||
|
WickedWaifusDownloaderLib(#[from] wicked_waifus_downloader_lib::Error),
|
||||||
|
#[error("No more CDN available")]
|
||||||
|
CdnListExhausted,
|
||||||
|
#[error("CRC Mismatch, expected: {0}, got: {1}")]
|
||||||
|
CrcMismatch(String, String),
|
||||||
|
#[error("Length mismatch, expected: {0}, got: {1}")]
|
||||||
|
LengthMismatch(u64, u64),
|
||||||
|
#[error("Preload selected but no preload switch exist")]
|
||||||
|
PreloadSwitchMissing,
|
||||||
|
#[error("Preload selected but no preload data exist")]
|
||||||
|
PreloadDataMissing,
|
||||||
|
}
|
|
@ -0,0 +1,170 @@
|
||||||
|
use crate::error::Error;
|
||||||
|
use crate::utils;
|
||||||
|
use crate::utils::verify_download;
|
||||||
|
use log::info;
|
||||||
|
use url::Url;
|
||||||
|
use wicked_waifus_downloader_domain::game::{
|
||||||
|
GameConfig, GameDefaultConfig, GameDefaultConfigV2Config, ResourcesData,
|
||||||
|
};
|
||||||
|
use wicked_waifus_downloader_domain::CdnInfo;
|
||||||
|
use wicked_waifus_downloader_lib::{
|
||||||
|
download_cdn_asset_in_mem, request_chunk_resources, request_resources,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) fn download_game(
|
||||||
|
use_predownload: bool,
|
||||||
|
use_chunked: bool,
|
||||||
|
full: bool,
|
||||||
|
game_config: GameConfig,
|
||||||
|
proxy: Option<&str>,
|
||||||
|
path: &str,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
if use_predownload {
|
||||||
|
game_config
|
||||||
|
.predownload_switch
|
||||||
|
.ok_or(Error::PreloadSwitchMissing)?;
|
||||||
|
let _predownload = game_config.predownload.ok_or(Error::PreloadDataMissing)?;
|
||||||
|
|
||||||
|
// TODO: Implement logic for download predownload
|
||||||
|
unreachable!("Implement logic for download predownload");
|
||||||
|
}
|
||||||
|
match game_config.default {
|
||||||
|
GameDefaultConfig::GameDefaultConfigV2(mut config) => {
|
||||||
|
utils::sort_cdn_list(&mut config.cdn_list);
|
||||||
|
let chunk_enabled = game_config.chunk_download_switch.unwrap_or(0) == 1;
|
||||||
|
|
||||||
|
if full && use_chunked && chunk_enabled {
|
||||||
|
download_full_game_chunked(&config.cdn_list, config.config, proxy, path)
|
||||||
|
} else if full {
|
||||||
|
download_full_game(&config.cdn_list, config.resources_data, proxy, path)
|
||||||
|
} else {
|
||||||
|
download_upgrade_game(&config.cdn_list, config.config, proxy, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GameDefaultConfig::GameDefaultConfigV1(mut config) => {
|
||||||
|
utils::sort_cdn_list(&mut config.cdn_list);
|
||||||
|
download_full_game(&config.cdn_list, config.resources_data, proxy, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_fetch_from_cdn<T>(
|
||||||
|
cdn_list: &[CdnInfo],
|
||||||
|
build_url: impl Fn(&str) -> Option<Url>,
|
||||||
|
fetch_fn: impl Fn(&str) -> Result<T, Error>,
|
||||||
|
) -> Result<T, Error> {
|
||||||
|
cdn_list
|
||||||
|
.iter()
|
||||||
|
.find_map(|cdn| {
|
||||||
|
let url = build_url(&cdn.url)?;
|
||||||
|
fetch_fn(url.as_str()).ok()
|
||||||
|
})
|
||||||
|
.ok_or(Error::CdnListExhausted)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn download_full_game(
|
||||||
|
cdn_list: &[CdnInfo],
|
||||||
|
resources_data: ResourcesData,
|
||||||
|
proxy: Option<&str>,
|
||||||
|
path: &str,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let resources = try_fetch_from_cdn(
|
||||||
|
cdn_list,
|
||||||
|
|base| Url::parse(base).ok()?.join(&resources_data.resources).ok(),
|
||||||
|
|url| Ok(request_resources(url, proxy)?),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let base_url = format!(
|
||||||
|
"{}/",
|
||||||
|
resources_data.resources_base_path.trim_end_matches('/')
|
||||||
|
);
|
||||||
|
let total_elements = resources.resource.len();
|
||||||
|
for (i, resource) in resources.resource.iter().enumerate() {
|
||||||
|
let expected_md5 = hex::decode(&resource.md5)?;
|
||||||
|
|
||||||
|
let data = download_resource_file(
|
||||||
|
cdn_list,
|
||||||
|
&base_url,
|
||||||
|
&resource.dest,
|
||||||
|
resource.size,
|
||||||
|
&expected_md5,
|
||||||
|
i,
|
||||||
|
total_elements,
|
||||||
|
proxy,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
utils::save_file_safe(path, &resource.dest, &data)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn download_full_game_chunked(
|
||||||
|
cdn_list: &[CdnInfo],
|
||||||
|
resources_data: GameDefaultConfigV2Config,
|
||||||
|
proxy: Option<&str>,
|
||||||
|
path: &str,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let resources = try_fetch_from_cdn(
|
||||||
|
cdn_list,
|
||||||
|
|base| Url::parse(base).ok()?.join(&resources_data.index_file).ok(),
|
||||||
|
|url| Ok(request_chunk_resources(url, proxy)?),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let base_url = format!("{}/", resources_data.base_url.trim_end_matches('/'));
|
||||||
|
let total_elements = resources.resource.len();
|
||||||
|
for (i, resource) in resources.resource.iter().enumerate() {
|
||||||
|
let expected_md5 = hex::decode(&resource.md5)?;
|
||||||
|
|
||||||
|
let data = if let Some(chunk_info) = &resource.chunk_infos {
|
||||||
|
for _chunk in chunk_info {}
|
||||||
|
unreachable!("Implement logic for chunk download");
|
||||||
|
} else {
|
||||||
|
download_resource_file(
|
||||||
|
cdn_list,
|
||||||
|
&base_url,
|
||||||
|
&resource.dest,
|
||||||
|
resource.size,
|
||||||
|
&expected_md5,
|
||||||
|
i,
|
||||||
|
total_elements,
|
||||||
|
proxy,
|
||||||
|
)?
|
||||||
|
};
|
||||||
|
|
||||||
|
utils::save_file_safe(path, &resource.dest, &data)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn download_upgrade_game(
|
||||||
|
_cdn_list: &[CdnInfo],
|
||||||
|
_resources_data: GameDefaultConfigV2Config,
|
||||||
|
_proxy: Option<&str>,
|
||||||
|
_path: &str,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
unreachable!("implement upgrade/patch download");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn download_resource_file(
|
||||||
|
cdn_list: &[CdnInfo],
|
||||||
|
base_url: &str,
|
||||||
|
dest: &str,
|
||||||
|
expected_length: u64,
|
||||||
|
expected_md5: &[u8],
|
||||||
|
cnt: usize,
|
||||||
|
total_elements: usize,
|
||||||
|
proxy: Option<&str>,
|
||||||
|
) -> Result<Vec<u8>, Error> {
|
||||||
|
try_fetch_from_cdn(
|
||||||
|
cdn_list,
|
||||||
|
|base| Url::parse(base).ok()?.join(base_url).ok()?.join(dest).ok(),
|
||||||
|
|url| {
|
||||||
|
info!("[{}/{}] Download started: {}", cnt + 1, total_elements, url);
|
||||||
|
let data = download_cdn_asset_in_mem(url, proxy)?;
|
||||||
|
verify_download(&data, expected_length, expected_md5)?;
|
||||||
|
Ok(data)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
use crate::error::Error;
|
||||||
|
use crate::utils;
|
||||||
|
use crate::utils::verify_download;
|
||||||
|
use log::{info, warn};
|
||||||
|
use url::Url;
|
||||||
|
use wicked_waifus_downloader_domain::launcher::LauncherConfig;
|
||||||
|
use wicked_waifus_downloader_lib::{download_cdn_asset_in_mem, InstallerInfo};
|
||||||
|
|
||||||
|
pub(crate) fn download_launcher(
|
||||||
|
launcher_config: LauncherConfig,
|
||||||
|
proxy: Option<&str>,
|
||||||
|
path: &str,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let mut info = InstallerInfo::from(launcher_config);
|
||||||
|
let expected_md5 = hex::decode(&info.expected_md5)?;
|
||||||
|
utils::sort_cdn_list(&mut info.cdn_list);
|
||||||
|
|
||||||
|
info.cdn_list.iter().find_map(|cdn| {
|
||||||
|
let Ok(url) = Url::parse(&cdn.url).and_then(|base| base.join(&info.installer)) else {
|
||||||
|
warn!("Invalid URL in CDN list: {}", cdn.url);
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("Download started: {}", url);
|
||||||
|
|
||||||
|
let data = match download_cdn_asset_in_mem(url.as_str(), proxy) {
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(err) => {
|
||||||
|
warn!("Download failed from {}: {:?}", url, err);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = verify_download(&data, info.expected_length, &expected_md5) {
|
||||||
|
warn!("Verification failed for {}: {:?}", url, e);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = utils::save_file_safe(path, "installer.zip", &data) {
|
||||||
|
warn!("Failed to save file: {:?}", e);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(())
|
||||||
|
}).ok_or(Error::CdnListExhausted)
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
mod error;
|
||||||
|
mod game;
|
||||||
|
mod launcher;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
use crate::game::download_game;
|
||||||
|
use crate::launcher::download_launcher;
|
||||||
|
use indicatif::MultiProgress;
|
||||||
|
use indicatif_log_bridge::LogWrapper;
|
||||||
|
use log::info;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let logger = colog::default_builder()
|
||||||
|
.filter_level(log::LevelFilter::Info)
|
||||||
|
.build();
|
||||||
|
let multi = MultiProgress::new();
|
||||||
|
LogWrapper::new(multi.clone(), logger).try_init().unwrap();
|
||||||
|
|
||||||
|
wicked_waifus_downloader_lib::enable_progress_bar(multi);
|
||||||
|
|
||||||
|
test_launcher_download();
|
||||||
|
test_game_download(false, false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_launcher_download() {
|
||||||
|
let launcher_config = wicked_waifus_downloader_lib::request_launcher(
|
||||||
|
"https://prod-cn-alicdn-gamestarter.kurogame.com/launcher/launcher/10008_Pa0Q0EMFxukjEqX33pF9Uyvdc8MaGPSz/G152/index.json",
|
||||||
|
None,
|
||||||
|
).unwrap();
|
||||||
|
info!("{:#?}", launcher_config);
|
||||||
|
|
||||||
|
download_launcher(launcher_config, None, "test_launcher_dl").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_game_download(use_predownload: bool, use_chunked: bool, full: bool) {
|
||||||
|
let game_config = wicked_waifus_downloader_lib::request_game(
|
||||||
|
"https://prod-cn-alicdn-gamestarter.kurogame.com/launcher/game/G152/10008_Pa0Q0EMFxukjEqX33pF9Uyvdc8MaGPSz/index.json",
|
||||||
|
None,
|
||||||
|
).unwrap();
|
||||||
|
info!("{:#?}", game_config);
|
||||||
|
|
||||||
|
download_game(
|
||||||
|
use_predownload,
|
||||||
|
use_chunked,
|
||||||
|
full,
|
||||||
|
game_config,
|
||||||
|
None,
|
||||||
|
"test_game_dl",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
use crate::error::Error;
|
||||||
|
use constant_time_eq::constant_time_eq;
|
||||||
|
use md5::{Digest, Md5};
|
||||||
|
use std::io::Write;
|
||||||
|
use wicked_waifus_downloader_domain::CdnInfo;
|
||||||
|
|
||||||
|
pub(crate) fn save_file_safe(prefix: &str, filename: &str, data: &[u8]) -> Result<String, Error> {
|
||||||
|
let root = std::path::Path::new(prefix);
|
||||||
|
let location = root.join(filename);
|
||||||
|
let parent = location.parent().unwrap();
|
||||||
|
std::fs::create_dir_all(parent)?;
|
||||||
|
|
||||||
|
let mut f = std::fs::OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.write(true)
|
||||||
|
.truncate(true)
|
||||||
|
.open(&location)?;
|
||||||
|
f.write_all(data)?;
|
||||||
|
f.flush()?;
|
||||||
|
|
||||||
|
Ok(location.to_string_lossy().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn sort_cdn_list(cdn_list: &mut Vec<CdnInfo>) {
|
||||||
|
// Highest priority first
|
||||||
|
// TODO: Implement K1 and K2 sorting
|
||||||
|
cdn_list.sort_by(|a, b| b.p.cmp(&a.p));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn verify_download(
|
||||||
|
data: &[u8],
|
||||||
|
expected_length: u64,
|
||||||
|
expected_md5: &[u8],
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let actual_length = data.len() as u64;
|
||||||
|
if actual_length != expected_length {
|
||||||
|
return Err(Error::LengthMismatch(expected_length, actual_length));
|
||||||
|
}
|
||||||
|
let actual_md5 = Md5::digest(data);
|
||||||
|
let actual_md5_bytes = actual_md5.as_slice();
|
||||||
|
match constant_time_eq(expected_md5, actual_md5_bytes) {
|
||||||
|
true => Ok(()),
|
||||||
|
false => Err(Error::CrcMismatch(
|
||||||
|
hex::encode(expected_md5),
|
||||||
|
hex::encode(actual_md5_bytes),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
[package]
|
||||||
|
name = "wicked-waifus-downloader-domain"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde.workspace = true
|
|
@ -0,0 +1,122 @@
|
||||||
|
use crate::{CdnInfo, Experiment, LanguageMap};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct GameConfig {
|
||||||
|
pub chunk_download_switch: Option<u32>,
|
||||||
|
pub default: GameDefaultConfig,
|
||||||
|
pub key_file_check_switch: Option<u32>,
|
||||||
|
pub predownload_switch: Option<u32>,
|
||||||
|
pub predownload: Option<ResourcesData>, // TODO: Might need changes
|
||||||
|
#[serde(rename = "RHIOptionSwitch")]
|
||||||
|
pub rhioption_switch: Option<u32>,
|
||||||
|
#[serde(rename = "RHIOptionList")]
|
||||||
|
pub rhioption_list: Option<Vec<RHIOption>>,
|
||||||
|
pub resources_login: Option<ResourcesLogin>,
|
||||||
|
pub experiment: Option<Experiment>,
|
||||||
|
pub check_exe_is_running: Option<u32>,
|
||||||
|
pub hash_cache_check_acc_switch: u32,
|
||||||
|
pub key_file_check_list: Option<Vec<String>>,
|
||||||
|
pub fingerprints: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The order V2 before V1, is because they share a common structure and only V2 has extra fields.
|
||||||
|
/// If V1 it's first will be included in V2 too
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum GameDefaultConfig {
|
||||||
|
GameDefaultConfigV2(GameDefaultConfigV2),
|
||||||
|
GameDefaultConfigV1(GameDefaultConfigV1),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct GameDefaultConfigV1 {
|
||||||
|
pub cdn_list: Vec<CdnInfo>,
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub resources_data: ResourcesData,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct GameDefaultConfigV2 {
|
||||||
|
pub cdn_list: Vec<CdnInfo>,
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub resources_data: ResourcesData,
|
||||||
|
pub config: GameDefaultConfigV2Config,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ResourcesData {
|
||||||
|
pub changelog: LanguageMap,
|
||||||
|
pub resources: String,
|
||||||
|
pub resources_base_path: String,
|
||||||
|
pub resources_diff: ResourcesDiff,
|
||||||
|
pub resources_exclude_path: Vec<String>,
|
||||||
|
pub resources_exclude_path_need_update: Vec<String>,
|
||||||
|
pub sample_hash_switch: u32,
|
||||||
|
pub version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct GameDefaultConfigV2Config {
|
||||||
|
pub index_file_md5: String,
|
||||||
|
pub un_compress_size: u64,
|
||||||
|
pub base_url: String,
|
||||||
|
pub size: u64,
|
||||||
|
pub patch_type: String, // TODO: Enum
|
||||||
|
pub index_file: String,
|
||||||
|
pub version: String,
|
||||||
|
pub patch_config: Vec<GameDefaultConfigV2PatchConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct GameDefaultConfigV2PatchConfig {
|
||||||
|
pub index_file_md5: String,
|
||||||
|
pub un_compress_size: u64,
|
||||||
|
pub ext: GameDefaultConfigV2PatchConfigExt,
|
||||||
|
pub base_url: String,
|
||||||
|
pub size: u64,
|
||||||
|
pub index_file: String,
|
||||||
|
pub version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct GameDefaultConfigV2PatchConfigExt {
|
||||||
|
pub max_file_size: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ResourcesDiff {
|
||||||
|
pub current_game_info: GameInfo,
|
||||||
|
pub previous_game_info: Option<GameInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct GameInfo {
|
||||||
|
pub file_name: String,
|
||||||
|
pub md5: String,
|
||||||
|
pub version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RHIOption {
|
||||||
|
pub cmd_option: String,
|
||||||
|
pub is_show: u32,
|
||||||
|
pub text: LanguageMap,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ResourcesLogin {
|
||||||
|
pub host: String,
|
||||||
|
pub login_switch: u32,
|
||||||
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
use crate::{CdnInfo, Experiment, LanguageMap};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum LauncherConfig {
|
||||||
|
LauncherConfigV1(LauncherConfigV1),
|
||||||
|
LauncherConfigV2(LauncherConfigV2),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct LauncherConfigV1 {
|
||||||
|
pub default: LauncherDefaultConfigV1,
|
||||||
|
pub crash_init_switch: u32,
|
||||||
|
pub animate_bg_switch: u32,
|
||||||
|
pub animate_background: Option<AnimateBackground>,
|
||||||
|
pub animate_background_language: Option<HashMap<String, AnimateBackground>>,
|
||||||
|
pub navigation_bar_switch: Option<u32>,
|
||||||
|
pub navigation_bar_language: Option<LanguageMap>,
|
||||||
|
pub resources_gray: Option<ResourcesGray>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct LauncherDefaultConfigCommon {
|
||||||
|
pub cdn_list: Vec<CdnInfo>,
|
||||||
|
pub changelog: LanguageMap,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct LauncherDefaultConfigV1 {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub common: LauncherDefaultConfigCommon,
|
||||||
|
pub installer: String,
|
||||||
|
#[serde(rename = "installerMD5")]
|
||||||
|
pub installer_md5: String,
|
||||||
|
pub installer_size: u64,
|
||||||
|
pub version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct AnimateBackground {
|
||||||
|
pub url: String,
|
||||||
|
pub md5: String,
|
||||||
|
pub frame_rate: u32,
|
||||||
|
pub duration_in_second: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct LauncherConfigV2 {
|
||||||
|
pub default: LauncherDefaultConfigV2,
|
||||||
|
pub crash_init_switch: u32,
|
||||||
|
pub navigation_bar_switch: Option<u32>,
|
||||||
|
pub navigation_bar_language: Option<LanguageMap>,
|
||||||
|
pub resources_gray: Option<ResourcesGray>,
|
||||||
|
pub function_code: Option<FunctionCode>,
|
||||||
|
pub experiment: Option<Experiment>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct LauncherDefaultConfigV2 {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub common: LauncherDefaultConfigCommon,
|
||||||
|
pub resource: LauncherDefaultConfigResource,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct LauncherDefaultConfigResource {
|
||||||
|
pub file_check_list_md5: String,
|
||||||
|
pub md5: String,
|
||||||
|
pub path: String,
|
||||||
|
pub size: u64,
|
||||||
|
pub version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ResourcesGray {
|
||||||
|
pub gray_url: Option<String>,
|
||||||
|
pub gray_switch: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct FunctionCode {}
|
|
@ -0,0 +1,42 @@
|
||||||
|
pub mod launcher;
|
||||||
|
pub mod game;
|
||||||
|
pub mod resources;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
pub type LanguageMap = HashMap<String, Option<String>>;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct CdnInfo {
|
||||||
|
#[serde(rename = "K1")]
|
||||||
|
pub k1: u32,
|
||||||
|
#[serde(rename = "K2")]
|
||||||
|
pub k2: u32,
|
||||||
|
#[serde(rename = "P")]
|
||||||
|
pub p: u32,
|
||||||
|
pub url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Experiment {
|
||||||
|
pub download: Option<ExperimentDownload>,
|
||||||
|
#[serde(rename = "res_check")]
|
||||||
|
pub res_check: Option<ExperimentResCheck>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ExperimentDownload {
|
||||||
|
pub download_read_block_timeout: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ExperimentResCheck {
|
||||||
|
pub file_chunk_check_switch: Option<String>,
|
||||||
|
pub file_size_check_switch: Option<String>,
|
||||||
|
pub res_valid_check_time_out: Option<String>,
|
||||||
|
pub file_check_white_list_config: Option<String>,
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Resources {
|
||||||
|
pub resource: Vec<Resource>,
|
||||||
|
pub sample_hash_info: SampleHashInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Resource {
|
||||||
|
pub dest: String,
|
||||||
|
pub md5: String,
|
||||||
|
pub sample_hash: String,
|
||||||
|
pub size: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SampleHashInfo {
|
||||||
|
pub sample_num: u32,
|
||||||
|
pub sample_block_max_size: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ChunkedResources {
|
||||||
|
pub resource: Vec<ChunkedResource>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ChunkedResource {
|
||||||
|
pub dest: String,
|
||||||
|
pub md5: String,
|
||||||
|
pub size: u64,
|
||||||
|
pub chunk_infos: Option<Vec<ChunkInfo>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ChunkInfo {
|
||||||
|
pub start: u64,
|
||||||
|
pub end: u64,
|
||||||
|
pub md5: String,
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
[package]
|
||||||
|
name = "wicked-waifus-downloader-lib"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
indicatif.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
thiserror.workspace = true
|
||||||
|
ureq.workspace = true
|
||||||
|
wicked-waifus-downloader-domain.workspace = true
|
165
wicked-waifus-downloader/wicked-waifus-downloader-lib/src/lib.rs
Normal file
165
wicked-waifus-downloader/wicked-waifus-downloader-lib/src/lib.rs
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
use indicatif::{MultiProgress, ProgressBar};
|
||||||
|
use log::{info, warn};
|
||||||
|
use std::fmt::Write;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use thiserror::Error;
|
||||||
|
use ureq::http::Response;
|
||||||
|
use ureq::tls::{RootCerts, TlsConfig};
|
||||||
|
use ureq::{Agent, Body, Proxy};
|
||||||
|
use wicked_waifus_downloader_domain::game::GameConfig;
|
||||||
|
use wicked_waifus_downloader_domain::launcher::LauncherConfig;
|
||||||
|
use wicked_waifus_downloader_domain::resources::{ChunkedResources, Resources};
|
||||||
|
use wicked_waifus_downloader_domain::CdnInfo;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("UREQ Error {0}")]
|
||||||
|
Ureq(#[from] ureq::Error),
|
||||||
|
#[error("ToStr Error {0}")]
|
||||||
|
ToStr(#[from] ureq::http::header::ToStrError),
|
||||||
|
#[error("ParseInt Error {0}")]
|
||||||
|
ParseInt(#[from] std::num::ParseIntError),
|
||||||
|
#[error("Io Error {0}")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
#[error("Content-Length header missing")]
|
||||||
|
ContentLengthHeaderMissing,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct InstallerInfo {
|
||||||
|
pub cdn_list: Vec<CdnInfo>,
|
||||||
|
pub installer: String,
|
||||||
|
pub expected_md5: String,
|
||||||
|
pub expected_length: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<LauncherConfig> for InstallerInfo {
|
||||||
|
fn from(config: LauncherConfig) -> Self {
|
||||||
|
match config {
|
||||||
|
LauncherConfig::LauncherConfigV1(cfg) => {
|
||||||
|
let default = cfg.default;
|
||||||
|
let common = default.common;
|
||||||
|
Self {
|
||||||
|
cdn_list: common.cdn_list,
|
||||||
|
installer: default.installer,
|
||||||
|
expected_md5: default.installer_md5,
|
||||||
|
expected_length: default.installer_size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LauncherConfig::LauncherConfigV2(cfg) => {
|
||||||
|
let default = cfg.default;
|
||||||
|
let common = default.common;
|
||||||
|
let resource = default.resource;
|
||||||
|
Self {
|
||||||
|
cdn_list: common.cdn_list,
|
||||||
|
installer: resource.path,
|
||||||
|
expected_md5: resource.md5,
|
||||||
|
expected_length: resource.size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static MULTI_PROGRESS_BAR: OnceLock<MultiProgress> = OnceLock::new();
|
||||||
|
|
||||||
|
pub fn enable_progress_bar(multi: MultiProgress) {
|
||||||
|
if MULTI_PROGRESS_BAR.set(multi).is_err() {
|
||||||
|
warn!("MultiProgress already set, ignoring.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! define_request_fn {
|
||||||
|
($name:ident, $ty:ty) => {
|
||||||
|
pub fn $name(url: &str, proxy: Option<&str>) -> Result<$ty, Error> {
|
||||||
|
request_json::<$ty>(url, proxy)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
define_request_fn!(request_launcher, LauncherConfig);
|
||||||
|
define_request_fn!(request_game, GameConfig);
|
||||||
|
define_request_fn!(request_resources, Resources);
|
||||||
|
define_request_fn!(request_chunk_resources, ChunkedResources);
|
||||||
|
|
||||||
|
pub fn download_cdn_asset_in_mem(url: &str, proxy: Option<&str>) -> Result<Vec<u8>, Error> {
|
||||||
|
let (mut res, len) = prepare_download(url, proxy)?;
|
||||||
|
let mut bytes = Vec::with_capacity(len);
|
||||||
|
download_internal(url, &mut res, len as u64, &mut bytes)?;
|
||||||
|
Ok(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn download_cdn_asset_writer<W: std::io::Write>(
|
||||||
|
url: &str,
|
||||||
|
proxy: Option<&str>,
|
||||||
|
writer: &mut W,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let (mut res, len) = prepare_download(url, proxy)?;
|
||||||
|
download_internal(url, &mut res, len as u64, writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_agent(proxy: Option<&str>) -> Result<Agent, Error> {
|
||||||
|
Ok(Agent::config_builder()
|
||||||
|
.tls_config(
|
||||||
|
TlsConfig::builder()
|
||||||
|
.root_certs(RootCerts::PlatformVerifier)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.proxy(proxy.map(Proxy::new).transpose()?)
|
||||||
|
.build()
|
||||||
|
.new_agent())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn request_json<T: DeserializeOwned>(url: &str, proxy: Option<&str>) -> Result<T, Error> {
|
||||||
|
Ok(get_agent(proxy)?
|
||||||
|
.get(url)
|
||||||
|
.call()?
|
||||||
|
.body_mut()
|
||||||
|
.read_json::<T>()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_download(url: &str, proxy: Option<&str>) -> Result<(Response<Body>, usize), Error> {
|
||||||
|
let res = get_agent(proxy)?.get(url).call()?;
|
||||||
|
let content_length = res
|
||||||
|
.headers()
|
||||||
|
.get("Content-Length")
|
||||||
|
.ok_or(Error::ContentLengthHeaderMissing)?
|
||||||
|
.to_str()?
|
||||||
|
.parse::<usize>()?;
|
||||||
|
Ok((res, content_length))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_progress_bar(len: u64, multi: &MultiProgress) -> ProgressBar {
|
||||||
|
let pb = multi.add(ProgressBar::new(len));
|
||||||
|
pb.set_style(
|
||||||
|
indicatif::ProgressStyle::with_template(
|
||||||
|
"{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})"
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.with_key("eta", |state: &indicatif::ProgressState, w: &mut dyn Write| {
|
||||||
|
write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap();
|
||||||
|
})
|
||||||
|
.progress_chars("#>-")
|
||||||
|
);
|
||||||
|
pb
|
||||||
|
}
|
||||||
|
|
||||||
|
fn download_internal<W: std::io::Write>(
|
||||||
|
url: &str,
|
||||||
|
res: &mut Response<Body>,
|
||||||
|
len: u64,
|
||||||
|
writer: &mut W,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
if let Some(multi) = MULTI_PROGRESS_BAR.get() {
|
||||||
|
let pb = create_progress_bar(len, multi);
|
||||||
|
std::io::copy(&mut pb.wrap_read(res.body_mut().as_reader()), writer)?;
|
||||||
|
pb.finish();
|
||||||
|
multi.remove(&pb);
|
||||||
|
} else {
|
||||||
|
std::io::copy(&mut res.body_mut().as_reader(), writer)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Download completed: {}", url);
|
||||||
|
Ok(())
|
||||||
|
}
|
Loading…
Reference in a new issue