Rust · 8093 bytes Raw Blame History
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