steelseries_sonar/
sonar.rs1use crate::error::{Result, SonarError};
4use reqwest::Client;
5use serde::{Deserialize};
6use serde_json::Value;
7use std::path::Path;
8
9pub const CHANNEL_NAMES: &[&str] = &["master", "game", "chatRender", "media", "aux", "chatCapture"];
11
12pub const STREAMER_SLIDER_NAMES: &[&str] = &["streaming", "monitoring"];
14
15#[derive(Debug, Deserialize)]
17pub struct CoreProps {
18 #[serde(rename = "ggEncryptedAddress")]
19 pub gg_encrypted_address: String,
20}
21
22#[derive(Debug, Deserialize)]
24pub struct SubApp {
25 #[serde(rename = "isEnabled")]
26 pub is_enabled: bool,
27 #[serde(rename = "isReady")]
28 pub is_ready: bool,
29 #[serde(rename = "isRunning")]
30 pub is_running: bool,
31 pub metadata: SubAppMetadata,
32}
33
34#[derive(Debug, Deserialize)]
36pub struct SubAppMetadata {
37 #[serde(rename = "webServerAddress")]
38 pub web_server_address: String,
39}
40
41#[derive(Debug, Deserialize)]
43pub struct SubAppsResponse {
44 #[serde(rename = "subApps")]
45 pub sub_apps: SubApps,
46}
47
48#[derive(Debug, Deserialize)]
50pub struct SubApps {
51 pub sonar: SubApp,
52}
53
54#[derive(Debug)]
56pub struct Sonar {
57 client: Client,
58 #[allow(dead_code)]
59 base_url: String,
60 web_server_address: String,
61 streamer_mode: bool,
62 volume_path: String,
63}
64
65impl Sonar {
66 pub async fn new() -> Result<Self> {
72 Self::with_config(None, None).await
73 }
74
75 pub async fn with_config(app_data_path: Option<&Path>, streamer_mode: Option<bool>) -> Result<Self> {
86 let client = Client::builder()
87 .danger_accept_invalid_certs(true)
88 .build()?;
89
90 let app_data_path = app_data_path.unwrap_or_else(|| {
91 #[cfg(target_os = "windows")]
92 {
93 Path::new("C:\\ProgramData\\SteelSeries\\SteelSeries Engine 3\\coreProps.json")
94 }
95 #[cfg(not(target_os = "windows"))]
96 {
97 Path::new("/tmp/coreProps.json") }
101 });
102
103 let base_url = Self::load_base_url(app_data_path).await?;
104 let web_server_address = Self::load_server_address(&client, &base_url).await?;
105
106 let detected_streamer_mode = match streamer_mode {
107 Some(mode) => mode,
108 None => Self::is_streamer_mode_internal(&client, &web_server_address).await?,
109 };
110
111 let volume_path = if detected_streamer_mode {
112 "/volumeSettings/streamer".to_string()
113 } else {
114 "/volumeSettings/classic".to_string()
115 };
116
117 Ok(Self {
118 client,
119 base_url,
120 web_server_address,
121 streamer_mode: detected_streamer_mode,
122 volume_path,
123 })
124 }
125
126 pub async fn is_streamer_mode(&self) -> Result<bool> {
128 Self::is_streamer_mode_internal(&self.client, &self.web_server_address).await
129 }
130
131 async fn is_streamer_mode_internal(client: &Client, web_server_address: &str) -> Result<bool> {
132 let url = format!("{}/mode/", web_server_address);
133 let response = client.get(&url).send().await?;
134
135 if !response.status().is_success() {
136 return Err(SonarError::ServerNotAccessible(response.status().as_u16()));
137 }
138
139 let mode: String = response.json().await?;
140 Ok(mode == "stream")
141 }
142
143 pub async fn set_streamer_mode(&mut self, streamer_mode: bool) -> Result<bool> {
153 let mode = if streamer_mode { "stream" } else { "classic" };
154 let url = format!("{}/mode/{}", self.web_server_address, mode);
155
156 let response = self.client.put(&url).send().await?;
157
158 if !response.status().is_success() {
159 return Err(SonarError::ServerNotAccessible(response.status().as_u16()));
160 }
161
162 let new_mode: String = response.json().await?;
163 self.streamer_mode = new_mode == "stream";
164
165 self.volume_path = if self.streamer_mode {
166 "/volumeSettings/streamer".to_string()
167 } else {
168 "/volumeSettings/classic".to_string()
169 };
170
171 Ok(self.streamer_mode)
172 }
173
174 pub async fn get_volume_data(&self) -> Result<Value> {
176 let url = format!("{}{}", self.web_server_address, self.volume_path);
177 let response = self.client.get(&url).send().await?;
178
179 if !response.status().is_success() {
180 return Err(SonarError::ServerNotAccessible(response.status().as_u16()));
181 }
182
183 let volume_data: Value = response.json().await?;
184 Ok(volume_data)
185 }
186
187 pub async fn set_volume(&self, channel: &str, volume: f64, streamer_slider: Option<&str>) -> Result<Value> {
195 if !CHANNEL_NAMES.contains(&channel) {
196 return Err(SonarError::ChannelNotFound(channel.to_string()));
197 }
198
199 if !(0.0..=1.0).contains(&volume) {
200 return Err(SonarError::InvalidVolume(volume));
201 }
202
203 let streamer_slider = streamer_slider.unwrap_or("streaming");
204 if self.streamer_mode && !STREAMER_SLIDER_NAMES.contains(&streamer_slider) {
205 return Err(SonarError::SliderNotFound(streamer_slider.to_string()));
206 }
207
208 let full_volume_path = if self.streamer_mode {
209 format!("{}/{}", self.volume_path, streamer_slider)
210 } else {
211 self.volume_path.clone()
212 };
213
214 let url = format!("{}{}/{}/Volume/{}",
215 self.web_server_address, full_volume_path, channel, serde_json::to_string(&volume)?);
216
217 let response = self.client.put(&url).send().await?;
218
219 if !response.status().is_success() {
220 return Err(SonarError::ServerNotAccessible(response.status().as_u16()));
221 }
222
223 let result: Value = response.json().await?;
224 Ok(result)
225 }
226
227 pub async fn mute_channel(&self, channel: &str, muted: bool, streamer_slider: Option<&str>) -> Result<Value> {
235 if !CHANNEL_NAMES.contains(&channel) {
236 return Err(SonarError::ChannelNotFound(channel.to_string()));
237 }
238
239 let streamer_slider = streamer_slider.unwrap_or("streaming");
240 if self.streamer_mode && !STREAMER_SLIDER_NAMES.contains(&streamer_slider) {
241 return Err(SonarError::SliderNotFound(streamer_slider.to_string()));
242 }
243
244 let full_volume_path = if self.streamer_mode {
245 format!("{}/{}", self.volume_path, streamer_slider)
246 } else {
247 self.volume_path.clone()
248 };
249
250 let mute_keyword = if self.streamer_mode { "isMuted" } else { "Mute" };
251
252 let url = format!("{}{}/{}/{}/{}",
253 self.web_server_address, full_volume_path, channel, mute_keyword, serde_json::to_string(&muted)?);
254
255 let response = self.client.put(&url).send().await?;
256
257 if !response.status().is_success() {
258 return Err(SonarError::ServerNotAccessible(response.status().as_u16()));
259 }
260
261 let result: Value = response.json().await?;
262 Ok(result)
263 }
264
265 pub async fn get_chat_mix_data(&self) -> Result<Value> {
267 let url = format!("{}/chatMix", self.web_server_address);
268 let response = self.client.get(&url).send().await?;
269
270 if !response.status().is_success() {
271 return Err(SonarError::ServerNotAccessible(response.status().as_u16()));
272 }
273
274 let chat_mix_data: Value = response.json().await?;
275 Ok(chat_mix_data)
276 }
277
278 pub async fn set_chat_mix(&self, mix_volume: f64) -> Result<Value> {
284 if !(-1.0..=1.0).contains(&mix_volume) {
285 return Err(SonarError::InvalidMixVolume(mix_volume));
286 }
287
288 let url = format!("{}/chatMix?balance={}",
289 self.web_server_address, serde_json::to_string(&mix_volume)?);
290
291 let response = self.client.put(&url).send().await?;
292
293 if !response.status().is_success() {
294 return Err(SonarError::ServerNotAccessible(response.status().as_u16()));
295 }
296
297 let result: Value = response.json().await?;
298 Ok(result)
299 }
300
301 async fn load_base_url(app_data_path: &Path) -> Result<String> {
302 if !app_data_path.exists() {
303 return Err(SonarError::EnginePathNotFound);
304 }
305
306 let content = tokio::fs::read_to_string(app_data_path).await?;
307 let core_props: CoreProps = serde_json::from_str(&content)?;
308
309 Ok(format!("https://{}", core_props.gg_encrypted_address))
310 }
311
312 async fn load_server_address(client: &Client, base_url: &str) -> Result<String> {
313 let url = format!("{}/subApps", base_url);
314 let response = client.get(&url).send().await?;
315
316 if !response.status().is_success() {
317 return Err(SonarError::ServerNotAccessible(response.status().as_u16()));
318 }
319
320 let sub_apps_response: SubAppsResponse = response.json().await?;
321 let sonar = &sub_apps_response.sub_apps.sonar;
322
323 if !sonar.is_enabled {
324 return Err(SonarError::SonarNotEnabled);
325 }
326
327 if !sonar.is_ready {
328 return Err(SonarError::ServerNotReady);
329 }
330
331 if !sonar.is_running {
332 return Err(SonarError::ServerNotRunning);
333 }
334
335 let web_server_address = &sonar.metadata.web_server_address;
336 if web_server_address.is_empty() || web_server_address == "null" {
337 return Err(SonarError::WebServerAddressNotFound);
338 }
339
340 Ok(web_server_address.clone())
341 }
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347
348 #[test]
349 fn test_channel_names() {
350 assert!(CHANNEL_NAMES.contains(&"master"));
351 assert!(CHANNEL_NAMES.contains(&"game"));
352 assert!(CHANNEL_NAMES.contains(&"chatRender"));
353 assert!(CHANNEL_NAMES.contains(&"media"));
354 assert!(CHANNEL_NAMES.contains(&"aux"));
355 assert!(CHANNEL_NAMES.contains(&"chatCapture"));
356 }
357
358 #[test]
359 fn test_streamer_slider_names() {
360 assert!(STREAMER_SLIDER_NAMES.contains(&"streaming"));
361 assert!(STREAMER_SLIDER_NAMES.contains(&"monitoring"));
362 }
363}