Skip to main content

snapcast_control/protocol/
de.rs

1use dashmap::DashMap;
2use serde::de::{DeserializeSeed, MapAccess, SeqAccess, Visitor};
3use std::collections::HashMap;
4use uuid::Uuid;
5
6use super::{notification::NotificationMethodConverter, request::RequestMethod, result::SnapcastResult};
7use crate::Message;
8
9pub type SentRequests = DashMap<Uuid, RequestMethod>;
10pub struct SnapcastDeserializer<'a>(&'a SentRequests);
11
12impl<'a> SnapcastDeserializer<'a> {
13  pub fn de(message: &str, state: &'a SentRequests) -> Result<Vec<Message>, DeserializationError> {
14    let mut deserializer = serde_json::Deserializer::from_str(message);
15
16    Ok(SnapcastDeserializer(state).deserialize(&mut deserializer)?)
17  }
18}
19
20impl<'de, 'a> DeserializeSeed<'de> for SnapcastDeserializer<'a> {
21  type Value = Vec<Message>;
22
23  fn deserialize<D>(self, d: D) -> Result<Self::Value, D::Error>
24  where
25    D: serde::de::Deserializer<'de>,
26  {
27    d.deserialize_any(SnapcastDeserializerVisitor(self.0))
28  }
29}
30
31struct SnapcastDeserializerVisitor<'a>(&'a SentRequests);
32
33impl SnapcastDeserializerVisitor<'_> {
34  fn parse_single_message(&self, value: &serde_json::Value) -> Result<Message, String> {
35    let obj = value.as_object().ok_or("batch item must be an object")?;
36    let response: HashMap<String, serde_json::Value> = obj.clone().into_iter().collect();
37    self.parse_message_from_map(response)
38  }
39
40  fn parse_message_from_map(&self, mut response: HashMap<String, serde_json::Value>) -> Result<Message, String> {
41    use serde_json::Value;
42
43    let jsonrpc = response
44      .get("jsonrpc")
45      .unwrap_or(&Value::String("2.0".to_string()))
46      .as_str()
47      .unwrap_or("2.0")
48      .to_string();
49
50    if response.contains_key("method") {
51      Ok(Message::Notification {
52        jsonrpc,
53        method: Box::new(
54          NotificationMethodConverter(
55            serde_json::from_value(response.remove("method").expect("this should never fail"))
56              .map_err(|e| e.to_string())?,
57            response.remove("params").ok_or("no params found")?,
58          )
59          .try_into()
60          .map_err(|e: serde_json::Error| e.to_string())?,
61        ),
62      })
63    } else if response.contains_key("result") {
64      let id: Uuid = serde_json::from_value(response.remove("id").ok_or("could not associate result with request")?)
65        .map_err(|e| e.to_string())?;
66      let result = response.remove("result").expect("this should never fail");
67      let result = if let Some(mapped_type) = self.0.remove(&id) {
68        SnapcastResult::try_from((mapped_type.1, result)).map_err(|e| e.to_string())?
69      } else {
70        serde_json::from_value(result).map_err(|e| e.to_string())?
71      };
72
73      Ok(Message::Result {
74        id,
75        jsonrpc,
76        result: Box::new(result),
77      })
78    } else if response.contains_key("error") {
79      let id: Uuid = serde_json::from_value(response.remove("id").ok_or("could not associate result with request")?)
80        .map_err(|e| e.to_string())?;
81      Ok(Message::Error {
82        id,
83        jsonrpc,
84        error: serde_json::from_value(response.remove("error").expect("this should never fail"))
85          .map_err(|e| e.to_string())?,
86      })
87    } else {
88      Err("invalid snapcast message".to_string())
89    }
90  }
91}
92
93impl<'de> Visitor<'de> for SnapcastDeserializerVisitor<'_> {
94  type Value = Vec<Message>;
95
96  fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
97    write!(formatter, "a valid snapcast jsonrpc message or array of messages")
98  }
99
100  fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
101  where
102    A: SeqAccess<'de>,
103  {
104    use serde::de::Error;
105    use serde_json::Value;
106
107    let mut messages = Vec::with_capacity(seq.size_hint().unwrap_or(0));
108
109    while let Some(item) = seq.next_element::<Value>()? {
110      let msg = self.parse_single_message(&item).map_err(Error::custom)?;
111      messages.push(msg);
112    }
113
114    Ok(messages)
115  }
116
117  fn visit_map<A>(self, mut access: A) -> Result<Self::Value, A::Error>
118  where
119    A: MapAccess<'de>,
120  {
121    use serde::de::Error;
122    use serde_json::Value;
123
124    let mut response: HashMap<String, Value> = HashMap::new();
125
126    while let Some((key, value)) = access.next_entry()? {
127      tracing::trace!("map key {:?} => {:?}", key, value);
128      response.insert(key, value);
129    }
130
131    let msg = self.parse_message_from_map(response).map_err(Error::custom)?;
132    Ok(vec![msg])
133  }
134}
135
136/// Errors that can occur during deserialization
137#[derive(Debug, thiserror::Error)]
138pub enum DeserializationError {
139  /// general deserialization error
140  #[error("Deserialization error: {0}")]
141  DeserializationError(#[from] serde::de::value::Error),
142  /// json deserialization error
143  #[error("JSON Deserialization error: {0}")]
144  SerdeJsonError(#[from] serde_json::Error),
145  /// top-level JSON was not an object or array
146  #[error("invalid top-level snapcast message: {0}")]
147  InvalidTopLevelMessage(String),
148}
149
150#[cfg(test)]
151mod tests {
152  use crate::protocol::{client, group, Method, Notification, Request, SnapcastResult};
153
154  use super::*;
155
156  #[test]
157  fn deserialize_error() {
158    let map = DashMap::new();
159
160    let message = r#"{"id": "00000000-0000-0000-0000-000000000000", "jsonrpc": "2.0", "error": {"code": -32603, "message": "Internal error"}}"#;
161    let snapcast_messages = SnapcastDeserializer::de(message, &map).unwrap();
162
163    assert_eq!(snapcast_messages.len(), 1);
164    assert_eq!(
165      snapcast_messages[0],
166      Message::Error {
167        id: "00000000-0000-0000-0000-000000000000".try_into().unwrap(),
168        jsonrpc: "2.0".to_string(),
169        error: serde_json::from_str(r#"{"code": -32603, "message": "Internal error"}"#).unwrap()
170      }
171    );
172  }
173
174  #[test]
175  fn serialize_client_get_status() {
176    let message = r#"{"id":"00000000-0000-0000-0000-000000000000","jsonrpc":"2.0","method":"Client.GetStatus","params":{"id":"00:21:6a:7d:74:fc"}}"#;
177    let composed = Request {
178      id: "00000000-0000-0000-0000-000000000000".try_into().unwrap(),
179      jsonrpc: "2.0".to_string(),
180      method: Method::ClientGetStatus {
181        params: client::GetStatusParams {
182          id: "00:21:6a:7d:74:fc".to_string(),
183        },
184      },
185    };
186
187    assert_eq!(serde_json::to_string(&composed).unwrap(), message);
188  }
189
190  #[test]
191  fn deserialize_client_get_status() {
192    let map = DashMap::from_iter([(
193      "00000000-0000-0000-0000-000000000000".try_into().unwrap(),
194      RequestMethod::ClientGetStatus,
195    )]);
196
197    let message = r#"{"id":"00000000-0000-0000-0000-000000000000","jsonrpc":"2.0","result":{"client":{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":74}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc","lastSeen":{"sec":1488026416,"usec":135973},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}}}"#;
198    let snapcast_messages = SnapcastDeserializer::de(message, &map).unwrap();
199
200    assert_eq!(snapcast_messages.len(), 1);
201    assert_eq!(
202      snapcast_messages[0],
203      Message::Result {
204        id: "00000000-0000-0000-0000-000000000000".try_into().unwrap(),
205        jsonrpc: "2.0".to_string(),
206        result: Box::new(SnapcastResult::ClientGetStatus(client::GetStatusResult {
207          client: client::Client {
208            id: "00:21:6a:7d:74:fc".to_string(),
209            connected: true,
210            config: client::ClientConfig {
211              instance: 1,
212              latency: 0,
213              name: "".to_string(),
214              volume: client::ClientVolume {
215                muted: false,
216                percent: 74
217              }
218            },
219            host: client::Host {
220              arch: "x86_64".to_string(),
221              ip: "127.0.0.1".to_string(),
222              mac: "00:21:6a:7d:74:fc".to_string(),
223              name: "T400".to_string(),
224              os: "Linux Mint 17.3 Rosa".to_string()
225            },
226            last_seen: client::LastSeen {
227              sec: 1488026416,
228              usec: 135973
229            },
230            snapclient: client::Snapclient {
231              name: "Snapclient".to_string(),
232              protocol_version: 2,
233              version: "0.10.0".to_string()
234            }
235          }
236        }))
237      }
238    );
239  }
240
241  #[test]
242  fn serialize_group_get_status() {
243    let message = r#"{"id":"00000000-0000-0000-0000-000000000000","jsonrpc":"2.0","method":"Group.GetStatus","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1"}}"#;
244    let composed = Request {
245      id: "00000000-0000-0000-0000-000000000000".try_into().unwrap(),
246      jsonrpc: "2.0".to_string(),
247      method: Method::GroupGetStatus {
248        params: group::GetStatusParams {
249          id: "4dcc4e3b-c699-a04b-7f0c-8260d23c43e1".to_string(),
250        },
251      },
252    };
253
254    assert_eq!(serde_json::to_string(&composed).unwrap(), message);
255  }
256
257  #[test]
258  fn deserialize_group_get_status() {
259    let map = DashMap::new();
260
261    let message = r#"{"id":"00000000-0000-0000-0000-000000000000","jsonrpc":"2.0","result":{"group":{"clients":[{"config":{"instance":2,"latency":10,"name":"Laptop","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488026485,"usec":644997},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}},{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":74}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc","lastSeen":{"sec":1488026481,"usec":223747},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":true,"name":"","stream_id":"stream 1"}}}"#;
262    let snapcast_messages = SnapcastDeserializer::de(message, &map).unwrap();
263
264    assert_eq!(snapcast_messages.len(), 1);
265    assert_eq!(
266      snapcast_messages[0],
267      Message::Result {
268        id: "00000000-0000-0000-0000-000000000000".try_into().unwrap(),
269        jsonrpc: "2.0".to_string(),
270        result: Box::new(SnapcastResult::GroupGetStatus(group::GetStatusResult {
271          group: group::Group {
272            id: "4dcc4e3b-c699-a04b-7f0c-8260d23c43e1".to_string(),
273            muted: true,
274            name: "".to_string(),
275            stream_id: "stream 1".to_string(),
276            clients: vec![
277              client::Client {
278                id: "00:21:6a:7d:74:fc#2".to_string(),
279                connected: true,
280                config: client::ClientConfig {
281                  instance: 2,
282                  latency: 10,
283                  name: "Laptop".to_string(),
284                  volume: client::ClientVolume {
285                    muted: false,
286                    percent: 48
287                  }
288                },
289                host: client::Host {
290                  arch: "x86_64".to_string(),
291                  ip: "127.0.0.1".to_string(),
292                  mac: "00:21:6a:7d:74:fc".to_string(),
293                  name: "T400".to_string(),
294                  os: "Linux Mint 17.3 Rosa".to_string()
295                },
296                last_seen: client::LastSeen {
297                  sec: 1488026485,
298                  usec: 644997
299                },
300                snapclient: client::Snapclient {
301                  name: "Snapclient".to_string(),
302                  protocol_version: 2,
303                  version: "0.10.0".to_string()
304                }
305              },
306              client::Client {
307                id: "00:21:6a:7d:74:fc".to_string(),
308                connected: true,
309                config: client::ClientConfig {
310                  instance: 1,
311                  latency: 0,
312                  name: "".to_string(),
313                  volume: client::ClientVolume {
314                    muted: false,
315                    percent: 74
316                  }
317                },
318                host: client::Host {
319                  arch: "x86_64".to_string(),
320                  ip: "127.0.0.1".to_string(),
321                  mac: "00:21:6a:7d:74:fc".to_string(),
322                  name: "T400".to_string(),
323                  os: "Linux Mint 17.3 Rosa".to_string()
324                },
325                last_seen: client::LastSeen {
326                  sec: 1488026481,
327                  usec: 223747
328                },
329                snapclient: client::Snapclient {
330                  name: "Snapclient".to_string(),
331                  protocol_version: 2,
332                  version: "0.10.0".to_string()
333                }
334              }
335            ]
336          }
337        }))
338      }
339    )
340  }
341
342  #[test]
343  fn serialize_server_get_status() {
344    let message = r#"{"id":"00000000-0000-0000-0000-000000000000","jsonrpc":"2.0","method":"Server.GetStatus"}"#;
345    let composed = Request {
346      id: "00000000-0000-0000-0000-000000000000".try_into().unwrap(),
347      jsonrpc: "2.0".to_string(),
348      method: Method::ServerGetStatus,
349    };
350
351    assert_eq!(serde_json::to_string(&composed).unwrap(), message);
352  }
353
354  #[test]
355  fn deserialize_server_get_status() {
356    let map = DashMap::new();
357
358    let message = r#"{"id":"00000000-0000-0000-0000-000000000000","jsonrpc":"2.0","result":{"server":{"groups":[{"clients":[{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":100}},"connected":true,"host":{"arch":"aarch64","ip":"172.16.3.109","mac":"2c:cf:67:47:cd:4a","name":"porch-musical-pi","os":"Debian GNU/Linux 12 (bookworm)"},"id":"Porches Pi","lastSeen":{"sec":1718314437,"usec":278423},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.28.0"}}],"id":"960ead7d-101a-88e9-1bee-b1c5f25efa9f","muted":false,"name":"","stream_id":"Porches Spotify"},{"clients":[{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":100}},"connected":true,"host":{"arch":"aarch64","ip":"172.16.2.171","mac":"d8:3a:dd:80:a0:87","name":"family-musical-pi","os":"Debian GNU/Linux 12 (bookworm)"},"id":"Family Pi","lastSeen":{"sec":1718314437,"usec":461576},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.28.0"}}],"id":"22a54ef3-54f6-949b-2eed-2ad83d1dab56","muted":false,"name":"","stream_id":"Kitchen Spotify"},{"clients":[{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":100}},"connected":true,"host":{"arch":"aarch64","ip":"172.16.3.38","mac":"2c:cf:67:47:cd:03","name":"bonus-musical-pi","os":"Debian GNU/Linux 12 (bookworm)"},"id":"Bonus Pi","lastSeen":{"sec":1718060095,"usec":922290},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.28.0"}}],"id":"a67bfc41-9286-48b9-a48c-383fcc16070f","muted":false,"name":"","stream_id":"Porches Spotify"},{"clients":[{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":100}},"connected":false,"host":{"arch":"aarch64","ip":"172.16.2.242","mac":"2c:cf:67:47:ca:ca","name":"bonus-sub-musical-pi","os":"Debian GNU/Linux 12 (bookworm)"},"id":"Bonus Sub Pi","lastSeen":{"sec":1718062516,"usec":632403},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.28.0"}}],"id":"46a2b853-5f6e-37a1-00e0-445c98e5826a","muted":false,"name":"","stream_id":"Porches Spotify"},{"clients":[{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":100}},"connected":true,"host":{"arch":"aarch64","ip":"172.16.2.240","mac":"d8:3a:dd:80:a0:cc","name":"family-sub-musical-pi","os":"Debian GNU/Linux 12 (bookworm)"},"id":"Family Sub Pi","lastSeen":{"sec":1718314437,"usec":344666},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.28.0"}}],"id":"28025fcd-1435-67f1-6fed-eb5117aa436c","muted":false,"name":"","stream_id":"Kitchen Spotify"},{"clients":[{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":100}},"connected":true,"host":{"arch":"armv6l","ip":"172.16.1.56","mac":"b8:27:eb:62:a0:01","name":"joey-room-musical-pi","os":"Raspbian GNU/Linux 12 (bookworm)"},"id":"Joey Room Pi","lastSeen":{"sec":1718314437,"usec":51860},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.28.0"}}],"id":"47d70477-d74d-38e1-b949-7a637b34ee27","muted":false,"name":"","stream_id":"Joey Room Spotify"}],"server":{"host":{"arch":"x86_64","ip":"","mac":"","name":"9960edc046a3","os":"Alpine Linux v3.19"},"snapserver":{"controlProtocolVersion":1,"name":"Snapserver","protocolVersion":1,"version":"0.28.0"}},"streams":[{"id":"Porches Spotify","properties":{"canControl":false,"canGoNext":false,"canGoPrevious":false,"canPause":false,"canPlay":false,"canSeek":false,"metadata":{"artData":{"data":"PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBoZWlnaHQ9IjE2OHB4IiB3aWR0aD0iMTY4cHgiIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAwIDE2OCAxNjgiPgogPHBhdGggZmlsbD0iIzFFRDc2MCIgZD0ibTgzLjk5NiAwLjI3N2MtNDYuMjQ5IDAtODMuNzQzIDM3LjQ5My04My43NDMgODMuNzQyIDAgNDYuMjUxIDM3LjQ5NCA4My43NDEgODMuNzQzIDgzLjc0MSA0Ni4yNTQgMCA4My43NDQtMzcuNDkgODMuNzQ0LTgzLjc0MSAwLTQ2LjI0Ni0zNy40OS04My43MzgtODMuNzQ1LTgzLjczOGwwLjAwMS0wLjAwNHptMzguNDA0IDEyMC43OGMtMS41IDIuNDYtNC43MiAzLjI0LTcuMTggMS43My0xOS42NjItMTIuMDEtNDQuNDE0LTE0LjczLTczLjU2NC04LjA3LTIuODA5IDAuNjQtNS42MDktMS4xMi02LjI0OS0zLjkzLTAuNjQzLTIuODEgMS4xMS01LjYxIDMuOTI2LTYuMjUgMzEuOS03LjI5MSA1OS4yNjMtNC4xNSA4MS4zMzcgOS4zNCAyLjQ2IDEuNTEgMy4yNCA0LjcyIDEuNzMgNy4xOHptMTAuMjUtMjIuODA1Yy0xLjg5IDMuMDc1LTUuOTEgNC4wNDUtOC45OCAyLjE1NS0yMi41MS0xMy44MzktNTYuODIzLTE3Ljg0Ni04My40NDgtOS43NjQtMy40NTMgMS4wNDMtNy4xLTAuOTAzLTguMTQ4LTQuMzUtMS4wNC0zLjQ1MyAwLjkwNy03LjA5MyA0LjM1NC04LjE0MyAzMC40MTMtOS4yMjggNjguMjIyLTQuNzU4IDk0LjA3MiAxMS4xMjcgMy4wNyAxLjg5IDQuMDQgNS45MSAyLjE1IDguOTc2di0wLjAwMXptMC44OC0yMy43NDRjLTI2Ljk5LTE2LjAzMS03MS41Mi0xNy41MDUtOTcuMjg5LTkuNjg0LTQuMTM4IDEuMjU1LTguNTE0LTEuMDgxLTkuNzY4LTUuMjE5LTEuMjU0LTQuMTQgMS4wOC04LjUxMyA1LjIyMS05Ljc3MSAyOS41ODEtOC45OCA3OC43NTYtNy4yNDUgMTA5LjgzIDExLjIwMiAzLjczIDIuMjA5IDQuOTUgNy4wMTYgMi43NCAxMC43MzMtMi4yIDMuNzIyLTcuMDIgNC45NDktMTAuNzMgMi43Mzl6Ii8+Cjwvc3ZnPgo=","extension":"svg"},"artUrl":"http://9960edc046a3:1780/__image_cache?name=cd91d51d70227e57d35950777b3d1aac.svg","duration":217.94500732421875,"title":"leave in five"}},"status":"idle","uri":{"fragment":"","host":"","path":"/usr/bin/librespot","query":{"autoplay":"true","bitrate":"320","chunk_ms":"20","codec":"flac","devicename":"Porches","name":"Porches Spotify","sampleformat":"44100:16:2","volume":"50"},"raw":"librespot:////usr/bin/librespot?autoplay=true&bitrate=320&chunk_ms=20&codec=flac&devicename=Porches&name=Porches Spotify&sampleformat=44100:16:2&volume=50","scheme":"librespot"}},{"id":"Kitchen Spotify","properties":{"canControl":false,"canGoNext":false,"canGoPrevious":false,"canPause":false,"canPlay":false,"canSeek":false,"metadata":{"artData":{"data":"PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBoZWlnaHQ9IjE2OHB4IiB3aWR0aD0iMTY4cHgiIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAwIDE2OCAxNjgiPgogPHBhdGggZmlsbD0iIzFFRDc2MCIgZD0ibTgzLjk5NiAwLjI3N2MtNDYuMjQ5IDAtODMuNzQzIDM3LjQ5My04My43NDMgODMuNzQyIDAgNDYuMjUxIDM3LjQ5NCA4My43NDEgODMuNzQzIDgzLjc0MSA0Ni4yNTQgMCA4My43NDQtMzcuNDkgODMuNzQ0LTgzLjc0MSAwLTQ2LjI0Ni0zNy40OS04My43MzgtODMuNzQ1LTgzLjczOGwwLjAwMS0wLjAwNHptMzguNDA0IDEyMC43OGMtMS41IDIuNDYtNC43MiAzLjI0LTcuMTggMS43My0xOS42NjItMTIuMDEtNDQuNDE0LTE0LjczLTczLjU2NC04LjA3LTIuODA5IDAuNjQtNS42MDktMS4xMi02LjI0OS0zLjkzLTAuNjQzLTIuODEgMS4xMS01LjYxIDMuOTI2LTYuMjUgMzEuOS03LjI5MSA1OS4yNjMtNC4xNSA4MS4zMzcgOS4zNCAyLjQ2IDEuNTEgMy4yNCA0LjcyIDEuNzMgNy4xOHptMTAuMjUtMjIuODA1Yy0xLjg5IDMuMDc1LTUuOTEgNC4wNDUtOC45OCAyLjE1NS0yMi41MS0xMy44MzktNTYuODIzLTE3Ljg0Ni04My40NDgtOS43NjQtMy40NTMgMS4wNDMtNy4xLTAuOTAzLTguMTQ4LTQuMzUtMS4wNC0zLjQ1MyAwLjkwNy03LjA5MyA0LjM1NC04LjE0MyAzMC40MTMtOS4yMjggNjguMjIyLTQuNzU4IDk0LjA3MiAxMS4xMjcgMy4wNyAxLjg5IDQuMDQgNS45MSAyLjE1IDguOTc2di0wLjAwMXptMC44OC0yMy43NDRjLTI2Ljk5LTE2LjAzMS03MS41Mi0xNy41MDUtOTcuMjg5LTkuNjg0LTQuMTM4IDEuMjU1LTguNTE0LTEuMDgxLTkuNzY4LTUuMjE5LTEuMjU0LTQuMTQgMS4wOC04LjUxMyA1LjIyMS05Ljc3MSAyOS41ODEtOC45OCA3OC43NTYtNy4yNDUgMTA5LjgzIDExLjIwMiAzLjczIDIuMjA5IDQuOTUgNy4wMTYgMi43NCAxMC43MzMtMi4yIDMuNzIyLTcuMDIgNC45NDktMTAuNzMgMi43Mzl6Ii8+Cjwvc3ZnPgo=","extension":"svg"},"artUrl":"http://9960edc046a3:1780/__image_cache?name=efc69e1ab3519570d890ee4f551bd908.svg","duration":169.99000549316406,"title":"BLEED"}},"status":"idle","uri":{"fragment":"","host":"","path":"/usr/bin/librespot","query":{"autoplay":"true","bitrate":"320","chunk_ms":"20","codec":"flac","devicename":"Kitchen","name":"Kitchen Spotify","sampleformat":"44100:16:2","volume":"50"},"raw":"librespot:////usr/bin/librespot?autoplay=true&bitrate=320&chunk_ms=20&codec=flac&devicename=Kitchen&name=Kitchen Spotify&sampleformat=44100:16:2&volume=50","scheme":"librespot"}},{"id":"Joey Room Spotify","properties":{"canControl":false,"canGoNext":false,"canGoPrevious":false,"canPause":false,"canPlay":false,"canSeek":false,"metadata":{"artData":{"data":"PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBoZWlnaHQ9IjE2OHB4IiB3aWR0aD0iMTY4cHgiIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAwIDE2OCAxNjgiPgogPHBhdGggZmlsbD0iIzFFRDc2MCIgZD0ibTgzLjk5NiAwLjI3N2MtNDYuMjQ5IDAtODMuNzQzIDM3LjQ5My04My43NDMgODMuNzQyIDAgNDYuMjUxIDM3LjQ5NCA4My43NDEgODMuNzQzIDgzLjc0MSA0Ni4yNTQgMCA4My43NDQtMzcuNDkgODMuNzQ0LTgzLjc0MSAwLTQ2LjI0Ni0zNy40OS04My43MzgtODMuNzQ1LTgzLjczOGwwLjAwMS0wLjAwNHptMzguNDA0IDEyMC43OGMtMS41IDIuNDYtNC43MiAzLjI0LTcuMTggMS43My0xOS42NjItMTIuMDEtNDQuNDE0LTE0LjczLTczLjU2NC04LjA3LTIuODA5IDAuNjQtNS42MDktMS4xMi02LjI0OS0zLjkzLTAuNjQzLTIuODEgMS4xMS01LjYxIDMuOTI2LTYuMjUgMzEuOS03LjI5MSA1OS4yNjMtNC4xNSA4MS4zMzcgOS4zNCAyLjQ2IDEuNTEgMy4yNCA0LjcyIDEuNzMgNy4xOHptMTAuMjUtMjIuODA1Yy0xLjg5IDMuMDc1LTUuOTEgNC4wNDUtOC45OCAyLjE1NS0yMi41MS0xMy44MzktNTYuODIzLTE3Ljg0Ni04My40NDgtOS43NjQtMy40NTMgMS4wNDMtNy4xLTAuOTAzLTguMTQ4LTQuMzUtMS4wNC0zLjQ1MyAwLjkwNy03LjA5MyA0LjM1NC04LjE0MyAzMC40MTMtOS4yMjggNjguMjIyLTQuNzU4IDk0LjA3MiAxMS4xMjcgMy4wNyAxLjg5IDQuMDQgNS45MSAyLjE1IDguOTc2di0wLjAwMXptMC44OC0yMy43NDRjLTI2Ljk5LTE2LjAzMS03MS41Mi0xNy41MDUtOTcuMjg5LTkuNjg0LTQuMTM4IDEuMjU1LTguNTE0LTEuMDgxLTkuNzY4LTUuMjE5LTEuMjU0LTQuMTQgMS4wOC04LjUxMyA1LjIyMS05Ljc3MSAyOS41ODEtOC45OCA3OC43NTYtNy4yNDUgMTA5LjgzIDExLjIwMiAzLjczIDIuMjA5IDQuOTUgNy4wMTYgMi43NCAxMC43MzMtMi4yIDMuNzIyLTcuMDIgNC45NDktMTAuNzMgMi43Mzl6Ii8+Cjwvc3ZnPgo=","extension":"svg"},"artUrl":"http://9960edc046a3:1780/__image_cache?name=db1b174342c6589a1b1786848c88176d.svg","duration":188.20799255371094,"title":"Endeavor"}},"status":"idle","uri":{"fragment":"","host":"","path":"/usr/bin/librespot","query":{"autoplay":"true","bitrate":"320","chunk_ms":"20","codec":"flac","devicename":"Joey%s Room","name":"Joey Room Spotify","sampleformat":"44100:16:2","volume":"50"},"raw":"librespot:////usr/bin/librespot?autoplay=true&bitrate=320&chunk_ms=20&codec=flac&devicename=Joey%s Room&name=Joey Room Spotify&sampleformat=44100:16:2&volume=50","scheme":"librespot"}}]}}}"#;
359    let snapcast_messages = SnapcastDeserializer::de(message, &map).unwrap();
360
361    assert_eq!(snapcast_messages.len(), 1);
362    println!("{:?}", snapcast_messages[0]);
363  }
364
365  #[test]
366  fn deserialize_notification() {
367    let map = DashMap::new();
368
369    let message = r#"{"jsonrpc":"2.0","method":"Client.OnVolumeChanged","params":{"id":"test","volume":{"muted":false,"percent":50}}}"#;
370    let snapcast_messages = SnapcastDeserializer::de(message, &map).unwrap();
371
372    assert_eq!(snapcast_messages.len(), 1);
373    assert_eq!(
374      snapcast_messages[0],
375      Message::Notification {
376        jsonrpc: "2.0".to_string(),
377        method: Box::new(Notification::ClientOnVolumeChanged {
378          params: Box::new(client::OnVolumeChangedParams {
379            id: "test".to_string(),
380            volume: client::ClientVolume {
381              muted: false,
382              percent: 50
383            }
384          })
385        })
386      }
387    );
388  }
389
390  #[test]
391  fn deserialize_notification_array() {
392    let map = DashMap::new();
393
394    let message = r#"[{"jsonrpc":"2.0","method":"Client.OnVolumeChanged","params":{"id":"e4:5f:01:41:53:4b","volume":{"muted":false,"percent":20}}},{"jsonrpc":"2.0","method":"Client.OnVolumeChanged","params":{"id":"b8:27:eb:19:34:8a","volume":{"muted":false,"percent":21}}},{"jsonrpc":"2.0","method":"Client.OnVolumeChanged","params":{"id":"b8:27:eb:62:26:ab","volume":{"muted":false,"percent":13}}}]"#;
395    let snapcast_messages = SnapcastDeserializer::de(message, &map).unwrap();
396
397    assert_eq!(snapcast_messages.len(), 3);
398    assert_eq!(
399      snapcast_messages[0],
400      Message::Notification {
401        jsonrpc: "2.0".to_string(),
402        method: Box::new(Notification::ClientOnVolumeChanged {
403          params: Box::new(client::OnVolumeChangedParams {
404            id: "e4:5f:01:41:53:4b".to_string(),
405            volume: client::ClientVolume {
406              muted: false,
407              percent: 20
408            }
409          })
410        })
411      }
412    );
413    assert_eq!(
414      snapcast_messages[1],
415      Message::Notification {
416        jsonrpc: "2.0".to_string(),
417        method: Box::new(Notification::ClientOnVolumeChanged {
418          params: Box::new(client::OnVolumeChangedParams {
419            id: "b8:27:eb:19:34:8a".to_string(),
420            volume: client::ClientVolume {
421              muted: false,
422              percent: 21
423            }
424          })
425        })
426      }
427    );
428    assert_eq!(
429      snapcast_messages[2],
430      Message::Notification {
431        jsonrpc: "2.0".to_string(),
432        method: Box::new(Notification::ClientOnVolumeChanged {
433          params: Box::new(client::OnVolumeChangedParams {
434            id: "b8:27:eb:62:26:ab".to_string(),
435            volume: client::ClientVolume {
436              muted: false,
437              percent: 13
438            }
439          })
440        })
441      }
442    );
443  }
444}