snapcast_control/protocol/
de.rs

1use dashmap::DashMap;
2use serde::de::{DeserializeSeed, MapAccess, 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<Message, DeserializationError> {
14    let mut deserializer = serde_json::Deserializer::from_str(message);
15
16    Ok(SnapcastDeserializer(state).deserialize(&mut deserializer)?)
17  }
18}
19
20impl<'a> TryFrom<(&'a str, &'a SentRequests)> for Message {
21  type Error = DeserializationError;
22
23  fn try_from(
24    (message, state): (&'a str, &'a SentRequests),
25  ) -> Result<Self, <crate::protocol::Message as TryFrom<(&'a str, &'a SentRequests)>>::Error> {
26    SnapcastDeserializer::de(message, state)
27  }
28}
29
30impl<'de, 'a> DeserializeSeed<'de> for SnapcastDeserializer<'a> {
31  type Value = Message;
32
33  fn deserialize<D>(self, d: D) -> Result<Self::Value, D::Error>
34  where
35    D: serde::de::Deserializer<'de>,
36  {
37    struct SnapcastDeserializerVisitor<'a>(&'a SentRequests);
38
39    impl<'de> Visitor<'de> for SnapcastDeserializerVisitor<'_> {
40      type Value = Message;
41
42      fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
43        write!(formatter, "a valid snapcast jsonrpc message")
44      }
45
46      fn visit_map<A>(self, mut access: A) -> Result<Self::Value, A::Error>
47      where
48        A: MapAccess<'de>,
49      {
50        use serde::de::Error;
51        use serde_json::Value;
52
53        let mut response: HashMap<String, Value> = HashMap::new();
54
55        while let Some((key, value)) = access.next_entry()? {
56          tracing::trace!("map key {:?} => {:?}", key, value);
57          response.insert(key, value);
58        }
59
60        let jsonrpc = response
61          .get("jsonrpc")
62          .unwrap_or(&Value::String("2.0".to_string()))
63          .as_str()
64          .unwrap_or("2.0")
65          .to_string();
66
67        if response.contains_key("method") {
68          Ok(Message::Notification {
69            jsonrpc,
70            method: Box::new(
71              NotificationMethodConverter(
72                serde_json::from_value(response.remove("method").expect("this should never fail"))
73                  .map_err(Error::custom)?,
74                response.remove("params").ok_or(Error::custom("no response found??"))?,
75              )
76              .try_into()
77              .map_err(Error::custom)?,
78            ),
79          })
80        } else if response.contains_key("result") {
81          let id: Uuid = serde_json::from_value(
82            response
83              .remove("id")
84              .ok_or(Error::custom("could not associate result with request"))?,
85          )
86          .map_err(Error::custom)?;
87          let result = response.remove("result").expect("this should never fail");
88          let result = if let Some(mapped_type) = self.0.remove(&id) {
89            SnapcastResult::try_from((mapped_type.1, result)).map_err(Error::custom)?
90          } else {
91            serde_json::from_value(result).map_err(Error::custom)?
92          };
93
94          Ok(Message::Result {
95            id,
96            jsonrpc,
97            result: Box::new(result),
98          })
99        } else if response.contains_key("error") {
100          let id: Uuid = serde_json::from_value(
101            response
102              .remove("id")
103              .ok_or(Error::custom("could not associate result with request"))?,
104          )
105          .map_err(Error::custom)?;
106          Ok(Message::Error {
107            id,
108            jsonrpc,
109            error: serde_json::from_value(response.remove("error").expect("this should never fail"))
110              .map_err(Error::custom)?,
111          })
112        } else {
113          Err(Error::custom("invalid snapcast message"))
114        }
115      }
116    }
117
118    d.deserialize_map(SnapcastDeserializerVisitor(self.0))
119  }
120}
121
122/// Errors that can occur during deserialization
123#[derive(Debug, thiserror::Error)]
124pub enum DeserializationError {
125  /// general deserialization error
126  #[error("Deserialization error: {0}")]
127  DeserializationError(#[from] serde::de::value::Error),
128  /// json deserialization error
129  #[error("JSON Deserialization error: {0}")]
130  SerdeJsonError(#[from] serde_json::Error),
131}
132
133#[cfg(test)]
134mod tests {
135  use crate::protocol::{client, group, Method, Notification, Request, SnapcastResult};
136
137  use super::*;
138
139  #[test]
140  fn deserialize_error() {
141    let map = DashMap::new();
142
143    let message = r#"{"id": "00000000-0000-0000-0000-000000000000", "jsonrpc": "2.0", "error": {"code": -32603, "message": "Internal error"}}"#;
144    let snapcast_message = SnapcastDeserializer::de(message, &map).unwrap();
145
146    assert_eq!(
147      snapcast_message,
148      Message::Error {
149        id: "00000000-0000-0000-0000-000000000000".try_into().unwrap(),
150        jsonrpc: "2.0".to_string(),
151        error: serde_json::from_str(r#"{"code": -32603, "message": "Internal error"}"#).unwrap()
152      }
153    );
154  }
155
156  #[test]
157  fn serialize_client_get_status() {
158    let message = r#"{"id":"00000000-0000-0000-0000-000000000000","jsonrpc":"2.0","method":"Client.GetStatus","params":{"id":"00:21:6a:7d:74:fc"}}"#;
159    let composed = Request {
160      id: "00000000-0000-0000-0000-000000000000".try_into().unwrap(),
161      jsonrpc: "2.0".to_string(),
162      method: Method::ClientGetStatus {
163        params: client::GetStatusParams {
164          id: "00:21:6a:7d:74:fc".to_string(),
165        },
166      },
167    };
168
169    assert_eq!(serde_json::to_string(&composed).unwrap(), message);
170  }
171
172  #[test]
173  fn deserialize_client_get_status() {
174    let map = DashMap::from_iter([(
175      "00000000-0000-0000-0000-000000000000".try_into().unwrap(),
176      RequestMethod::ClientGetStatus,
177    )]);
178
179    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"}}}}"#;
180    let snapcast_message = SnapcastDeserializer::de(message, &map).unwrap();
181
182    assert_eq!(
183      snapcast_message,
184      Message::Result {
185        id: "00000000-0000-0000-0000-000000000000".try_into().unwrap(),
186        jsonrpc: "2.0".to_string(),
187        result: Box::new(SnapcastResult::ClientGetStatus(client::GetStatusResult {
188          client: client::Client {
189            id: "00:21:6a:7d:74:fc".to_string(),
190            connected: true,
191            config: client::ClientConfig {
192              instance: 1,
193              latency: 0,
194              name: "".to_string(),
195              volume: client::ClientVolume {
196                muted: false,
197                percent: 74
198              }
199            },
200            host: client::Host {
201              arch: "x86_64".to_string(),
202              ip: "127.0.0.1".to_string(),
203              mac: "00:21:6a:7d:74:fc".to_string(),
204              name: "T400".to_string(),
205              os: "Linux Mint 17.3 Rosa".to_string()
206            },
207            last_seen: client::LastSeen {
208              sec: 1488026416,
209              usec: 135973
210            },
211            snapclient: client::Snapclient {
212              name: "Snapclient".to_string(),
213              protocol_version: 2,
214              version: "0.10.0".to_string()
215            }
216          }
217        }))
218      }
219    );
220  }
221
222  #[test]
223  fn serialize_group_get_status() {
224    let message = r#"{"id":"00000000-0000-0000-0000-000000000000","jsonrpc":"2.0","method":"Group.GetStatus","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1"}}"#;
225    let composed = Request {
226      id: "00000000-0000-0000-0000-000000000000".try_into().unwrap(),
227      jsonrpc: "2.0".to_string(),
228      method: Method::GroupGetStatus {
229        params: group::GetStatusParams {
230          id: "4dcc4e3b-c699-a04b-7f0c-8260d23c43e1".to_string(),
231        },
232      },
233    };
234
235    assert_eq!(serde_json::to_string(&composed).unwrap(), message);
236  }
237
238  #[test]
239  fn deserialize_group_get_status() {
240    let map = DashMap::new();
241
242    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"}}}"#;
243    let snapcast_message = SnapcastDeserializer::de(message, &map).unwrap();
244
245    assert_eq!(
246      snapcast_message,
247      Message::Result {
248        id: "00000000-0000-0000-0000-000000000000".try_into().unwrap(),
249        jsonrpc: "2.0".to_string(),
250        result: Box::new(SnapcastResult::GroupGetStatus(group::GetStatusResult {
251          group: group::Group {
252            id: "4dcc4e3b-c699-a04b-7f0c-8260d23c43e1".to_string(),
253            muted: true,
254            name: "".to_string(),
255            stream_id: "stream 1".to_string(),
256            clients: vec![
257              client::Client {
258                id: "00:21:6a:7d:74:fc#2".to_string(),
259                connected: true,
260                config: client::ClientConfig {
261                  instance: 2,
262                  latency: 10,
263                  name: "Laptop".to_string(),
264                  volume: client::ClientVolume {
265                    muted: false,
266                    percent: 48
267                  }
268                },
269                host: client::Host {
270                  arch: "x86_64".to_string(),
271                  ip: "127.0.0.1".to_string(),
272                  mac: "00:21:6a:7d:74:fc".to_string(),
273                  name: "T400".to_string(),
274                  os: "Linux Mint 17.3 Rosa".to_string()
275                },
276                last_seen: client::LastSeen {
277                  sec: 1488026485,
278                  usec: 644997
279                },
280                snapclient: client::Snapclient {
281                  name: "Snapclient".to_string(),
282                  protocol_version: 2,
283                  version: "0.10.0".to_string()
284                }
285              },
286              client::Client {
287                id: "00:21:6a:7d:74:fc".to_string(),
288                connected: true,
289                config: client::ClientConfig {
290                  instance: 1,
291                  latency: 0,
292                  name: "".to_string(),
293                  volume: client::ClientVolume {
294                    muted: false,
295                    percent: 74
296                  }
297                },
298                host: client::Host {
299                  arch: "x86_64".to_string(),
300                  ip: "127.0.0.1".to_string(),
301                  mac: "00:21:6a:7d:74:fc".to_string(),
302                  name: "T400".to_string(),
303                  os: "Linux Mint 17.3 Rosa".to_string()
304                },
305                last_seen: client::LastSeen {
306                  sec: 1488026481,
307                  usec: 223747
308                },
309                snapclient: client::Snapclient {
310                  name: "Snapclient".to_string(),
311                  protocol_version: 2,
312                  version: "0.10.0".to_string()
313                }
314              }
315            ]
316          }
317        }))
318      }
319    )
320  }
321
322  #[test]
323  fn serialize_server_get_status() {
324    let message = r#"{"id":"00000000-0000-0000-0000-000000000000","jsonrpc":"2.0","method":"Server.GetStatus"}"#;
325    let composed = Request {
326      id: "00000000-0000-0000-0000-000000000000".try_into().unwrap(),
327      jsonrpc: "2.0".to_string(),
328      method: Method::ServerGetStatus,
329    };
330
331    assert_eq!(serde_json::to_string(&composed).unwrap(), message);
332  }
333
334  #[test]
335  fn deserialize_server_get_status() {
336    let map = DashMap::new();
337
338    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"}}]}}}"#;
339    let snapcast_message: Message = SnapcastDeserializer::de(message, &map).unwrap();
340
341    println!("{:?}", snapcast_message);
342  }
343
344  #[test]
345  fn deserialize_notification() {
346    let map = DashMap::new();
347
348    let message = r#"{"jsonrpc":"2.0","method":"Client.OnVolumeChanged","params":{"id":"test","volume":{"muted":false,"percent":50}}}"#;
349    let snapcast_message = SnapcastDeserializer::de(message, &map).unwrap();
350
351    assert_eq!(
352      snapcast_message,
353      Message::Notification {
354        jsonrpc: "2.0".to_string(),
355        method: Box::new(Notification::ClientOnVolumeChanged {
356          params: Box::new(client::OnVolumeChangedParams {
357            id: "test".to_string(),
358            volume: client::ClientVolume {
359              muted: false,
360              percent: 50
361            }
362          })
363        })
364      }
365    );
366  }
367}