steelseries_sonar/
blocking.rs

1//! Synchronous (blocking) API for SteelSeries Sonar.
2//!
3//! This module provides a blocking interface for users who prefer synchronous operations
4//! or need to use the library in non-async contexts.
5
6use crate::error::{Result, SonarError};
7use reqwest::blocking::Client;
8use serde_json::Value;
9use std::path::Path;
10
11/// Blocking version of the SteelSeries Sonar API client.
12#[derive(Debug)]
13pub struct BlockingSonar {
14    client: Client,
15    web_server_address: String,
16    streamer_mode: bool,
17    volume_path: String,
18}
19
20impl BlockingSonar {
21    /// Create a new blocking Sonar client with default settings.
22    ///
23    /// # Errors
24    ///
25    /// Returns an error if the SteelSeries Engine is not found or accessible.
26    pub fn new() -> Result<Self> {
27        Self::with_config(None, None)
28    }
29
30    /// Create a new blocking Sonar client with custom configuration.
31    ///
32    /// # Arguments
33    ///
34    /// * `app_data_path` - Custom path to the coreProps.json file
35    /// * `streamer_mode` - Whether to use streamer mode (if None, will be auto-detected)
36    pub fn with_config(app_data_path: Option<&Path>, streamer_mode: Option<bool>) -> Result<Self> {
37        let client = Client::builder()
38            .danger_accept_invalid_certs(true)
39            .build()?;
40
41        let app_data_path = app_data_path.unwrap_or_else(|| {
42            #[cfg(target_os = "windows")]
43            {
44                Path::new("C:\\ProgramData\\SteelSeries\\SteelSeries Engine 3\\coreProps.json")
45            }
46            #[cfg(not(target_os = "windows"))]
47            {
48                Path::new("/tmp/coreProps.json") // Placeholder
49            }
50        });
51
52        let base_url = Self::load_base_url(app_data_path)?;
53        let web_server_address = Self::load_server_address(&client, &base_url)?;
54
55        let detected_streamer_mode = match streamer_mode {
56            Some(mode) => mode,
57            None => Self::is_streamer_mode_internal(&client, &web_server_address)?,
58        };
59
60        let volume_path = if detected_streamer_mode {
61            "/volumeSettings/streamer".to_string()
62        } else {
63            "/volumeSettings/classic".to_string()
64        };
65
66        Ok(Self {
67            client,
68            web_server_address,
69            streamer_mode: detected_streamer_mode,
70            volume_path,
71        })
72    }
73
74    /// Check if streamer mode is currently enabled.
75    pub fn is_streamer_mode(&self) -> Result<bool> {
76        Self::is_streamer_mode_internal(&self.client, &self.web_server_address)
77    }
78
79    fn is_streamer_mode_internal(client: &Client, web_server_address: &str) -> Result<bool> {
80        let url = format!("{}/mode/", web_server_address);
81        let response = client.get(&url).send()?;
82        
83        if !response.status().is_success() {
84            return Err(SonarError::ServerNotAccessible(response.status().as_u16()));
85        }
86
87        let mode: String = response.json()?;
88        Ok(mode == "stream")
89    }
90
91    /// Set streamer mode on or off.
92    pub fn set_streamer_mode(&mut self, streamer_mode: bool) -> Result<bool> {
93        let mode = if streamer_mode { "stream" } else { "classic" };
94        let url = format!("{}/mode/{}", self.web_server_address, mode);
95        
96        let response = self.client.put(&url).send()?;
97        
98        if !response.status().is_success() {
99            return Err(SonarError::ServerNotAccessible(response.status().as_u16()));
100        }
101
102        let new_mode: String = response.json()?;
103        self.streamer_mode = new_mode == "stream";
104        
105        self.volume_path = if self.streamer_mode {
106            "/volumeSettings/streamer".to_string()
107        } else {
108            "/volumeSettings/classic".to_string()
109        };
110
111        Ok(self.streamer_mode)
112    }
113
114    /// Get volume data for all channels.
115    pub fn get_volume_data(&self) -> Result<Value> {
116        let url = format!("{}{}", self.web_server_address, self.volume_path);
117        let response = self.client.get(&url).send()?;
118        
119        if !response.status().is_success() {
120            return Err(SonarError::ServerNotAccessible(response.status().as_u16()));
121        }
122
123        let volume_data: Value = response.json()?;
124        Ok(volume_data)
125    }
126
127    /// Set the volume for a specific channel.
128    pub fn set_volume(&self, channel: &str, volume: f64, streamer_slider: Option<&str>) -> Result<Value> {
129        if !crate::sonar::CHANNEL_NAMES.contains(&channel) {
130            return Err(SonarError::ChannelNotFound(channel.to_string()));
131        }
132
133        if !(0.0..=1.0).contains(&volume) {
134            return Err(SonarError::InvalidVolume(volume));
135        }
136
137        let streamer_slider = streamer_slider.unwrap_or("streaming");
138        if self.streamer_mode && !crate::sonar::STREAMER_SLIDER_NAMES.contains(&streamer_slider) {
139            return Err(SonarError::SliderNotFound(streamer_slider.to_string()));
140        }
141
142        let full_volume_path = if self.streamer_mode {
143            format!("{}/{}", self.volume_path, streamer_slider)
144        } else {
145            self.volume_path.clone()
146        };
147
148        let url = format!("{}{}/{}/Volume/{}", 
149            self.web_server_address, full_volume_path, channel, serde_json::to_string(&volume)?);
150        
151        let response = self.client.put(&url).send()?;
152        
153        if !response.status().is_success() {
154            return Err(SonarError::ServerNotAccessible(response.status().as_u16()));
155        }
156
157        let result: Value = response.json()?;
158        Ok(result)
159    }
160
161    /// Mute or unmute a specific channel.
162    pub fn mute_channel(&self, channel: &str, muted: bool, streamer_slider: Option<&str>) -> Result<Value> {
163        if !crate::sonar::CHANNEL_NAMES.contains(&channel) {
164            return Err(SonarError::ChannelNotFound(channel.to_string()));
165        }
166
167        let streamer_slider = streamer_slider.unwrap_or("streaming");
168        if self.streamer_mode && !crate::sonar::STREAMER_SLIDER_NAMES.contains(&streamer_slider) {
169            return Err(SonarError::SliderNotFound(streamer_slider.to_string()));
170        }
171
172        let full_volume_path = if self.streamer_mode {
173            format!("{}/{}", self.volume_path, streamer_slider)
174        } else {
175            self.volume_path.clone()
176        };
177
178        let mute_keyword = if self.streamer_mode { "isMuted" } else { "Mute" };
179
180        let url = format!("{}{}/{}/{}/{}", 
181            self.web_server_address, full_volume_path, channel, mute_keyword, serde_json::to_string(&muted)?);
182        
183        let response = self.client.put(&url).send()?;
184        
185        if !response.status().is_success() {
186            return Err(SonarError::ServerNotAccessible(response.status().as_u16()));
187        }
188
189        let result: Value = response.json()?;
190        Ok(result)
191    }
192
193    /// Get chat mix data.
194    pub fn get_chat_mix_data(&self) -> Result<Value> {
195        let url = format!("{}/chatMix", self.web_server_address);
196        let response = self.client.get(&url).send()?;
197        
198        if !response.status().is_success() {
199            return Err(SonarError::ServerNotAccessible(response.status().as_u16()));
200        }
201
202        let chat_mix_data: Value = response.json()?;
203        Ok(chat_mix_data)
204    }
205
206    /// Set the chat mix volume.
207    pub fn set_chat_mix(&self, mix_volume: f64) -> Result<Value> {
208        if !(-1.0..=1.0).contains(&mix_volume) {
209            return Err(SonarError::InvalidMixVolume(mix_volume));
210        }
211
212        let url = format!("{}/chatMix?balance={}", 
213            self.web_server_address, serde_json::to_string(&mix_volume)?);
214        
215        let response = self.client.put(&url).send()?;
216        
217        if !response.status().is_success() {
218            return Err(SonarError::ServerNotAccessible(response.status().as_u16()));
219        }
220
221        let result: Value = response.json()?;
222        Ok(result)
223    }
224
225    fn load_base_url(app_data_path: &Path) -> Result<String> {
226        use crate::sonar::CoreProps;
227        
228        if !app_data_path.exists() {
229            return Err(SonarError::EnginePathNotFound);
230        }
231
232        let content = std::fs::read_to_string(app_data_path)?;
233        let core_props: CoreProps = serde_json::from_str(&content)?;
234        
235        Ok(format!("https://{}", core_props.gg_encrypted_address))
236    }
237
238    fn load_server_address(client: &Client, base_url: &str) -> Result<String> {
239        use crate::sonar::SubAppsResponse;
240        
241        let url = format!("{}/subApps", base_url);
242        let response = client.get(&url).send()?;
243        
244        if !response.status().is_success() {
245            return Err(SonarError::ServerNotAccessible(response.status().as_u16()));
246        }
247
248        let sub_apps_response: SubAppsResponse = response.json()?;
249        let sonar = &sub_apps_response.sub_apps.sonar;
250
251        if !sonar.is_enabled {
252            return Err(SonarError::SonarNotEnabled);
253        }
254
255        if !sonar.is_ready {
256            return Err(SonarError::ServerNotReady);
257        }
258
259        if !sonar.is_running {
260            return Err(SonarError::ServerNotRunning);
261        }
262
263        let web_server_address = &sonar.metadata.web_server_address;
264        if web_server_address.is_empty() || web_server_address == "null" {
265            return Err(SonarError::WebServerAddressNotFound);
266        }
267
268        Ok(web_server_address.clone())
269    }
270}