1use std::collections::HashMap;
4use std::path::Path;
5
6use anyhow::Result;
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct Volume {
12 pub percent: u16,
14 pub muted: bool,
16}
17
18impl Default for Volume {
19 fn default() -> Self {
20 Self {
21 percent: 100,
22 muted: false,
23 }
24 }
25}
26
27#[derive(Debug, Clone, Default, Serialize, Deserialize)]
29pub struct ClientConfig {
30 #[serde(default)]
32 pub name: String,
33 #[serde(default)]
35 pub volume: Volume,
36 #[serde(default)]
38 pub latency: i32,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct Client {
44 pub id: String,
46 pub host_name: String,
48 pub mac: String,
50 pub connected: bool,
52 pub config: ClientConfig,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct Group {
59 pub id: String,
61 pub name: String,
63 pub stream_id: String,
65 pub muted: bool,
67 pub clients: Vec<String>,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct StreamInfo {
74 pub id: String,
76 pub status: String,
78 pub uri: String,
80 #[serde(default)]
82 pub properties: std::collections::HashMap<String, serde_json::Value>,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize, Default)]
87pub struct ServerState {
88 pub clients: HashMap<String, Client>,
90 pub groups: Vec<Group>,
92 pub streams: Vec<StreamInfo>,
94}
95
96impl ServerState {
97 pub fn load(path: &Path) -> Self {
99 let state: Self = std::fs::read_to_string(path)
100 .ok()
101 .and_then(|s| serde_json::from_str(&s).ok())
102 .unwrap_or_default();
103 tracing::debug!(path = %path.display(), clients = state.clients.len(), groups = state.groups.len(), "state loaded");
104 state
105 }
106
107 pub fn save(&self, path: &Path) -> Result<()> {
109 if let Some(parent) = path.parent() {
110 std::fs::create_dir_all(parent)?;
111 }
112 let json = serde_json::to_string_pretty(self)?;
113 std::fs::write(path, json)?;
114 tracing::debug!(path = %path.display(), "state saved");
115 Ok(())
116 }
117
118 pub fn get_or_create_client(&mut self, id: &str, host_name: &str, mac: &str) -> &mut Client {
120 if !self.clients.contains_key(id) {
121 tracing::debug!(client_id = id, host_name, mac, "client created");
122 self.clients.insert(
123 id.to_string(),
124 Client {
125 id: id.to_string(),
126 host_name: host_name.to_string(),
127 mac: mac.to_string(),
128 connected: false,
129 config: ClientConfig::default(),
130 },
131 );
132 }
133 self.clients.get_mut(id).expect("just inserted")
134 }
135
136 pub fn group_for_client(&mut self, client_id: &str, default_stream: &str) -> &mut Group {
138 let idx = self
140 .groups
141 .iter()
142 .position(|g| g.clients.contains(&client_id.to_string()));
143
144 if let Some(idx) = idx {
145 return &mut self.groups[idx];
146 }
147
148 let group = Group {
150 id: generate_id(),
151 name: String::new(),
152 stream_id: default_stream.to_string(),
153 muted: false,
154 clients: vec![client_id.to_string()],
155 };
156 self.groups.push(group);
157 self.groups.last_mut().expect("just pushed")
158 }
159
160 pub fn remove_client_from_groups(&mut self, client_id: &str) {
162 for group in &mut self.groups {
163 group.clients.retain(|c| c != client_id);
164 }
165 self.groups.retain(|g| !g.clients.is_empty());
167 }
168
169 pub fn move_client_to_group(&mut self, client_id: &str, group_id: &str) {
171 tracing::debug!(client_id, group_id, "moving client to group");
172 self.remove_client_from_groups(client_id);
173 if let Some(group) = self.groups.iter_mut().find(|g| g.id == group_id) {
174 group.clients.push(client_id.to_string());
175 }
176 }
177
178 pub fn set_group_stream(&mut self, group_id: &str, stream_id: &str) {
180 if let Some(group) = self.groups.iter_mut().find(|g| g.id == group_id) {
181 group.stream_id = stream_id.to_string();
182 }
183 }
184
185 pub fn to_status_json(&self) -> serde_json::Value {
187 let groups: Vec<serde_json::Value> = self
188 .groups
189 .iter()
190 .map(|g| {
191 let clients: Vec<serde_json::Value> = g
192 .clients
193 .iter()
194 .filter_map(|cid| self.clients.get(cid))
195 .map(|c| {
196 serde_json::json!({
197 "id": c.id,
198 "host": { "name": c.host_name, "mac": c.mac },
199 "connected": c.connected,
200 "config": {
201 "name": c.config.name,
202 "volume": { "percent": c.config.volume.percent, "muted": c.config.volume.muted },
203 "latency": c.config.latency,
204 }
205 })
206 })
207 .collect();
208 serde_json::json!({
209 "id": g.id,
210 "name": g.name,
211 "stream_id": g.stream_id,
212 "muted": g.muted,
213 "clients": clients,
214 })
215 })
216 .collect();
217
218 let streams: Vec<serde_json::Value> = self
219 .streams
220 .iter()
221 .map(|s| {
222 serde_json::json!({
223 "id": s.id,
224 "status": s.status,
225 "uri": { "raw": s.uri },
226 })
227 })
228 .collect();
229
230 serde_json::json!({
231 "server": {
232 "groups": groups,
233 "streams": streams,
234 }
235 })
236 }
237}
238
239fn generate_id() -> String {
240 use std::time::{SystemTime, UNIX_EPOCH};
241 let t = SystemTime::now()
242 .duration_since(UNIX_EPOCH)
243 .unwrap_or_default()
244 .as_nanos();
245 format!("{t:032x}")
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 #[test]
253 fn client_lifecycle() {
254 let mut state = ServerState::default();
255 let c = state.get_or_create_client("abc", "myhost", "aa:bb:cc:dd:ee:ff");
256 assert_eq!(c.id, "abc");
257 assert_eq!(c.config.volume.percent, 100);
258
259 let g = state.group_for_client("abc", "default");
260 assert_eq!(g.clients, vec!["abc"]);
261 let gid = g.id.clone();
262
263 let g2 = state.group_for_client("abc", "default");
265 assert_eq!(g2.id, gid);
266 }
267
268 #[test]
269 fn move_client() {
270 let mut state = ServerState::default();
271 state.get_or_create_client("c1", "h1", "m1");
272 state.get_or_create_client("c2", "h2", "m2");
273 state.group_for_client("c1", "s1");
274 state.group_for_client("c2", "s1");
275
276 assert_eq!(state.groups.len(), 2);
277
278 let g2_id = state.groups[1].id.clone();
279 state.move_client_to_group("c1", &g2_id);
280
281 assert_eq!(state.groups.len(), 1); assert_eq!(state.groups[0].clients.len(), 2);
283 }
284
285 #[test]
286 fn json_roundtrip() {
287 let mut state = ServerState::default();
288 state.get_or_create_client("c1", "host1", "mac1");
289 state.group_for_client("c1", "default");
290 state.streams.push(StreamInfo {
291 id: "default".into(),
292 status: "playing".into(),
293 uri: "pipe:///tmp/snapfifo".into(),
294 properties: Default::default(),
295 });
296
297 let json = serde_json::to_string(&state).unwrap();
298 let restored: ServerState = serde_json::from_str(&json).unwrap();
299 assert_eq!(restored.clients.len(), 1);
300 assert_eq!(restored.groups.len(), 1);
301 assert_eq!(restored.streams.len(), 1);
302 }
303
304 #[test]
305 fn status_json() {
306 let mut state = ServerState::default();
307 state.get_or_create_client("c1", "host1", "mac1");
308 state.group_for_client("c1", "default");
309 let status = state.to_status_json();
310 assert!(status["server"]["groups"].is_array());
311 }
312}