steelseries_sonar/
blocking.rs1use crate::error::{Result, SonarError};
7use reqwest::blocking::Client;
8use serde_json::Value;
9use std::path::Path;
10
11#[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 pub fn new() -> Result<Self> {
27 Self::with_config(None, None)
28 }
29
30 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") }
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 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 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 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 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 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 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 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}