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