| 1 | //! WeMod download functionality |
| 2 | |
| 3 | use crate::config::WandaConfig; |
| 4 | use crate::error::{Result, WandaError}; |
| 5 | use futures::StreamExt; |
| 6 | use std::path::PathBuf; |
| 7 | use tokio::fs::File; |
| 8 | use tokio::io::AsyncWriteExt; |
| 9 | use tracing::{debug, info, warn}; |
| 10 | |
| 11 | /// WeMod download API endpoint (latest version - may not work on Linux) |
| 12 | const WEMOD_DOWNLOAD_URL: &str = "https://api.wemod.com/client/download"; |
| 13 | |
| 14 | /// Pinned WeMod version known to work on Linux with Wine/Proton |
| 15 | /// Version 12.x has known compatibility issues (black window, renderer crashes) |
| 16 | /// See: https://community.wemod.com/t/wand-wemod-version-12-0-3-on-linux-proton-just-displays-a-black-window/373133 |
| 17 | /// Using 11.6.0 as it matches wemod-launcher and has better Cloudflare verification support |
| 18 | const WEMOD_LINUX_COMPATIBLE_VERSION: &str = "11.6.0"; |
| 19 | const WEMOD_LINUX_COMPATIBLE_URL: &str = |
| 20 | "https://storage-cdn.wemod.com/app/releases/stable/WeMod-11.6.0.exe"; |
| 21 | |
| 22 | /// Information about a WeMod release |
| 23 | #[derive(Debug, Clone)] |
| 24 | pub struct WemodRelease { |
| 25 | /// Download URL |
| 26 | pub url: String, |
| 27 | /// Version string (if known) |
| 28 | pub version: Option<String>, |
| 29 | /// File size in bytes (if known) |
| 30 | pub size: Option<u64>, |
| 31 | } |
| 32 | |
| 33 | /// Handles downloading WeMod |
| 34 | pub struct WemodDownloader { |
| 35 | /// HTTP client |
| 36 | client: reqwest::Client, |
| 37 | /// Cache directory for downloads |
| 38 | cache_dir: PathBuf, |
| 39 | } |
| 40 | |
| 41 | impl WemodDownloader { |
| 42 | /// Create a new downloader |
| 43 | pub fn new(_config: &WandaConfig) -> Self { |
| 44 | Self { |
| 45 | client: reqwest::Client::builder() |
| 46 | .user_agent("WANDA/0.1.0") |
| 47 | .build() |
| 48 | .expect("Failed to create HTTP client"), |
| 49 | cache_dir: WandaConfig::default_cache_dir().join("downloads"), |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | /// Get the Linux-compatible WeMod release info |
| 54 | /// |
| 55 | /// This returns a pinned version (11.5.0) known to work with Wine/Proton. |
| 56 | /// Version 12.x has compatibility issues on Linux (black window, renderer crashes). |
| 57 | pub async fn get_latest(&self) -> Result<WemodRelease> { |
| 58 | info!("Using WeMod {} (pinned for Linux compatibility)", WEMOD_LINUX_COMPATIBLE_VERSION); |
| 59 | warn!("WeMod 12.x has known Linux compatibility issues - using 11.5.0 instead"); |
| 60 | |
| 61 | // Use the pinned Linux-compatible version instead of fetching latest |
| 62 | // The latest 12.x versions have renderer crashes under Wine/Proton |
| 63 | let url = WEMOD_LINUX_COMPATIBLE_URL.to_string(); |
| 64 | let version = Some(WEMOD_LINUX_COMPATIBLE_VERSION.to_string()); |
| 65 | |
| 66 | // Get the file size via HEAD request |
| 67 | let content_length = self |
| 68 | .client |
| 69 | .head(&url) |
| 70 | .send() |
| 71 | .await |
| 72 | .ok() |
| 73 | .and_then(|r| r.headers().get(reqwest::header::CONTENT_LENGTH).cloned()) |
| 74 | .and_then(|v| v.to_str().ok().and_then(|s| s.parse().ok())); |
| 75 | |
| 76 | debug!("WeMod download URL: {}", url); |
| 77 | debug!("Using pinned version: {}", WEMOD_LINUX_COMPATIBLE_VERSION); |
| 78 | |
| 79 | Ok(WemodRelease { |
| 80 | url, |
| 81 | version, |
| 82 | size: content_length, |
| 83 | }) |
| 84 | } |
| 85 | |
| 86 | /// Get the actual latest WeMod release (may not work on Linux) |
| 87 | /// |
| 88 | /// WARNING: Version 12.x has known compatibility issues with Wine/Proton |
| 89 | /// Use `get_latest()` for Linux-compatible version |
| 90 | #[allow(dead_code)] |
| 91 | pub async fn get_latest_upstream(&self) -> Result<WemodRelease> { |
| 92 | info!("Fetching upstream WeMod download info..."); |
| 93 | warn!("Latest WeMod may have Linux compatibility issues!"); |
| 94 | |
| 95 | // The WeMod API redirects to the actual download URL |
| 96 | // We need to follow the redirect to get the final URL |
| 97 | let response = self |
| 98 | .client |
| 99 | .head(WEMOD_DOWNLOAD_URL) |
| 100 | .send() |
| 101 | .await |
| 102 | .map_err(|e| WandaError::WemodDownloadFailed { |
| 103 | reason: format!("Failed to fetch download info: {}", e), |
| 104 | })?; |
| 105 | |
| 106 | let final_url = response.url().to_string(); |
| 107 | let content_length = response |
| 108 | .headers() |
| 109 | .get(reqwest::header::CONTENT_LENGTH) |
| 110 | .and_then(|v| v.to_str().ok()) |
| 111 | .and_then(|v| v.parse().ok()); |
| 112 | |
| 113 | // Try to extract version from URL |
| 114 | // URL format is typically: https://storage.wemod.com/app/releases/stable/WeMod-X.Y.Z.exe |
| 115 | let version = final_url |
| 116 | .split('/') |
| 117 | .last() |
| 118 | .and_then(|filename| { |
| 119 | filename |
| 120 | .strip_prefix("WeMod-") |
| 121 | .and_then(|v| v.strip_suffix(".exe")) |
| 122 | .map(String::from) |
| 123 | }); |
| 124 | |
| 125 | debug!("WeMod download URL: {}", final_url); |
| 126 | if let Some(ref v) = version { |
| 127 | debug!("Detected version: {}", v); |
| 128 | } |
| 129 | |
| 130 | Ok(WemodRelease { |
| 131 | url: final_url, |
| 132 | version, |
| 133 | size: content_length, |
| 134 | }) |
| 135 | } |
| 136 | |
| 137 | /// Download WeMod installer to cache |
| 138 | pub async fn download<F>(&self, release: &WemodRelease, progress_callback: F) -> Result<PathBuf> |
| 139 | where |
| 140 | F: Fn(u64, u64), |
| 141 | { |
| 142 | // Ensure cache directory exists |
| 143 | tokio::fs::create_dir_all(&self.cache_dir).await?; |
| 144 | |
| 145 | let filename = release |
| 146 | .version |
| 147 | .as_ref() |
| 148 | .map(|v| format!("WeMod-{}.exe", v)) |
| 149 | .unwrap_or_else(|| "WeMod-latest.exe".to_string()); |
| 150 | |
| 151 | let dest_path = self.cache_dir.join(&filename); |
| 152 | |
| 153 | // Check if we already have this version cached |
| 154 | if dest_path.exists() { |
| 155 | if let Some(expected_size) = release.size { |
| 156 | let actual_size = tokio::fs::metadata(&dest_path).await?.len(); |
| 157 | if actual_size == expected_size { |
| 158 | info!("Using cached WeMod installer: {}", dest_path.display()); |
| 159 | return Ok(dest_path); |
| 160 | } |
| 161 | } |
| 162 | } |
| 163 | |
| 164 | info!("Downloading WeMod from {}...", release.url); |
| 165 | |
| 166 | let response = self |
| 167 | .client |
| 168 | .get(&release.url) |
| 169 | .send() |
| 170 | .await |
| 171 | .map_err(|e| WandaError::WemodDownloadFailed { |
| 172 | reason: format!("Download request failed: {}", e), |
| 173 | })?; |
| 174 | |
| 175 | if !response.status().is_success() { |
| 176 | return Err(WandaError::WemodDownloadFailed { |
| 177 | reason: format!("HTTP error: {}", response.status()), |
| 178 | }); |
| 179 | } |
| 180 | |
| 181 | let total_size = response.content_length().unwrap_or(0); |
| 182 | let mut downloaded: u64 = 0; |
| 183 | |
| 184 | let mut file = File::create(&dest_path).await?; |
| 185 | let mut stream = response.bytes_stream(); |
| 186 | |
| 187 | while let Some(chunk_result) = stream.next().await { |
| 188 | let chunk = chunk_result.map_err(|e| WandaError::WemodDownloadFailed { |
| 189 | reason: format!("Error reading download stream: {}", e), |
| 190 | })?; |
| 191 | |
| 192 | file.write_all(&chunk).await?; |
| 193 | |
| 194 | downloaded += chunk.len() as u64; |
| 195 | progress_callback(downloaded, total_size); |
| 196 | } |
| 197 | |
| 198 | file.flush().await?; |
| 199 | |
| 200 | info!("Downloaded WeMod to {}", dest_path.display()); |
| 201 | Ok(dest_path) |
| 202 | } |
| 203 | |
| 204 | /// Check if we have a cached version matching the release |
| 205 | pub async fn get_cached(&self, release: &WemodRelease) -> Option<PathBuf> { |
| 206 | if let Some(ref version) = release.version { |
| 207 | let path = self.cache_dir.join(format!("WeMod-{}.exe", version)); |
| 208 | if path.exists() { |
| 209 | return Some(path); |
| 210 | } |
| 211 | } |
| 212 | None |
| 213 | } |
| 214 | |
| 215 | /// Clear the download cache |
| 216 | pub async fn clear_cache(&self) -> Result<()> { |
| 217 | if self.cache_dir.exists() { |
| 218 | tokio::fs::remove_dir_all(&self.cache_dir).await?; |
| 219 | } |
| 220 | Ok(()) |
| 221 | } |
| 222 | } |
| 223 | |
| 224 | #[cfg(test)] |
| 225 | mod tests { |
| 226 | use super::*; |
| 227 | |
| 228 | #[test] |
| 229 | fn test_version_extraction() { |
| 230 | let url = "https://storage.wemod.com/app/releases/stable/WeMod-8.12.5.exe"; |
| 231 | let version = url |
| 232 | .split('/') |
| 233 | .last() |
| 234 | .and_then(|f| f.strip_prefix("WeMod-")) |
| 235 | .and_then(|v| v.strip_suffix(".exe")); |
| 236 | |
| 237 | assert_eq!(version, Some("8.12.5")); |
| 238 | } |
| 239 | } |
| 240 |