Skip to main content

twilight_lavalink/
model.rs

1//! Models to (de)serialize incoming/outgoing websocket events and HTTP
2//! responses.
3
4pub mod incoming;
5pub mod outgoing;
6
7pub use self::{
8    incoming::{
9        Exception, IncomingEvent, PlayerUpdate, PlayerUpdateState, Stats, StatsCpu, StatsFrame,
10        StatsMemory, Track, TrackEnd, TrackException, TrackStart, TrackStuck, WebSocketClosed,
11    },
12    outgoing::{
13        Destroy, Equalizer, EqualizerBand, OutgoingEvent, Pause, Play, Seek, Stop,
14        UpdatePlayerTrack, VoiceUpdate, Volume,
15    },
16};
17
18#[cfg(test)]
19mod lavalink_struct_tests {
20    use super::incoming::{Stats, StatsCpu, StatsMemory};
21    use serde_test::Token;
22
23    #[test]
24    fn stats_frames_not_provided() {
25        const LAVALINK_LOAD: f64 = 0.276_119_402_985_074_65;
26        const MEM_ALLOCATED: u64 = 62_914_560;
27        const MEM_FREE: u64 = 27_664_576;
28        const MEM_RESERVABLE: u64 = 4_294_967_296;
29        const MEM_USED: u64 = 35_249_984;
30        const SYSTEM_LOAD: f64 = 0.195_380_536_378_835_9;
31
32        let expected = Stats {
33            cpu: StatsCpu {
34                cores: 4,
35                lavalink_load: LAVALINK_LOAD,
36                system_load: SYSTEM_LOAD,
37            },
38            frame_stats: None,
39            memory: StatsMemory {
40                allocated: MEM_ALLOCATED,
41                free: MEM_FREE,
42                reservable: MEM_RESERVABLE,
43                used: MEM_USED,
44            },
45            players: 0,
46            playing_players: 0,
47            uptime: 18589,
48        };
49
50        serde_test::assert_de_tokens(
51            &expected,
52            &[
53                Token::Struct {
54                    name: "Stats",
55                    len: 6,
56                },
57                Token::Str("cpu"),
58                Token::Struct {
59                    name: "StatsCpu",
60                    len: 3,
61                },
62                Token::Str("cores"),
63                Token::U64(4),
64                Token::Str("lavalinkLoad"),
65                Token::F64(LAVALINK_LOAD),
66                Token::Str("systemLoad"),
67                Token::F64(SYSTEM_LOAD),
68                Token::StructEnd,
69                Token::Str("memory"),
70                Token::Struct {
71                    name: "StatsMemory",
72                    len: 4,
73                },
74                Token::Str("allocated"),
75                Token::U64(MEM_ALLOCATED),
76                Token::Str("free"),
77                Token::U64(MEM_FREE),
78                Token::Str("reservable"),
79                Token::U64(MEM_RESERVABLE),
80                Token::Str("used"),
81                Token::U64(MEM_USED),
82                Token::StructEnd,
83                Token::Str("op"),
84                Token::UnitVariant {
85                    name: "Opcode",
86                    variant: "stats",
87                },
88                Token::Str("players"),
89                Token::U64(0),
90                Token::Str("playingPlayers"),
91                Token::U64(0),
92                Token::Str("uptime"),
93                Token::U64(18589),
94                Token::StructEnd,
95            ],
96        );
97    }
98}
99
100#[cfg(test)]
101mod lavalink_incoming_model_tests {
102    use crate::model::{TrackEnd, TrackException, TrackStart, TrackStuck};
103    use twilight_model::id::{Id, marker::GuildMarker};
104
105    use super::{
106        WebSocketClosed,
107        incoming::{
108            Event, EventData, EventType, Exception, PlayerUpdate, PlayerUpdateState, Ready,
109            Severity, Stats, StatsCpu, StatsFrame, StatsMemory, Track, TrackEndReason, TrackInfo,
110        },
111    };
112
113    // These are incoming so we only need to check that the input json can deserialize into the struct.
114    fn compare_json_payload<
115        T: std::fmt::Debug + for<'a> serde::Deserialize<'a> + std::cmp::PartialEq,
116    >(
117        data_struct: &T,
118        json_payload: &str,
119    ) {
120        // Deserialize
121        let deserialized: T = serde_json::from_str(json_payload).unwrap();
122        assert_eq!(deserialized, *data_struct);
123    }
124
125    #[test]
126    fn should_deserialize_a_ready_response() {
127        let ready = Ready {
128            resumed: false,
129            session_id: "la3kfsdf5eafe848".to_string(),
130        };
131        compare_json_payload(
132            &ready,
133            r#"{"op":"ready","resumed":false,"sessionId":"la3kfsdf5eafe848"}"#,
134        );
135    }
136
137    #[test]
138    fn should_deserialize_a_player_update_response() {
139        let update = PlayerUpdate {
140            guild_id: Id::<GuildMarker>::new(987_654_321),
141            state: PlayerUpdateState {
142                time: 1_710_214_147_839,
143                position: 534,
144                connected: true,
145                ping: 0,
146            },
147        };
148        compare_json_payload(
149            &update,
150            r#"{"op":"playerUpdate","guildId":"987654321","state":{"time":1710214147839,"position":534,"connected":true,"ping":0}}"#,
151        );
152    }
153
154    #[test]
155    fn should_deserialize_stat_event() {
156        let stat_event = Stats {
157            players: 0,
158            playing_players: 0,
159            uptime: 1_139_738,
160            cpu: StatsCpu {
161                cores: 16,
162                lavalink_load: 3.497_090_420_769_919E-5,
163                system_load: 0.055_979_978_347_863_06,
164            },
165            frame_stats: None,
166            memory: StatsMemory {
167                allocated: 331_350_016,
168                free: 228_139_904,
169                reservable: 8_396_996_608,
170                used: 103_210_112,
171            },
172        };
173        compare_json_payload(
174            &stat_event.clone(),
175            r#"{"op":"stats","frameStats":null,"players":0,"playingPlayers":0,"uptime":1139738,"memory":{"free":228139904,"used":103210112,"allocated":331350016,"reservable":8396996608},"cpu":{"cores":16,"systemLoad":0.05597997834786306,"lavalinkLoad":3.497090420769919E-5}}"#,
176        );
177    }
178
179    #[test]
180    fn should_deserialize_stat_event_with_frame_stat() {
181        let stat_event = Stats {
182            players: 0,
183            playing_players: 0,
184            uptime: 1_139_738,
185            cpu: StatsCpu {
186                cores: 16,
187                lavalink_load: 3.497_090_420_769_919E-5,
188                system_load: 0.055_979_978_347_863_06,
189            },
190            frame_stats: Some(StatsFrame {
191                sent: 6000,
192                nulled: 10,
193                deficit: -3010,
194            }),
195            memory: StatsMemory {
196                allocated: 331_350_016,
197                free: 228_139_904,
198                reservable: 8_396_996_608,
199                used: 103_210_112,
200            },
201        };
202        compare_json_payload(
203            &stat_event.clone(),
204            r#"{"op":"stats","frameStats":{"sent":6000,"nulled":10,"deficit":-3010},"players":0,"playingPlayers":0,"uptime":1139738,"memory":{"free":228139904,"used":103210112,"allocated":331350016,"reservable":8396996608},"cpu":{"cores":16,"systemLoad":0.05597997834786306,"lavalinkLoad":3.497090420769919E-5}}"#,
205        );
206    }
207
208    #[test]
209    fn should_deserialize_track_start_event() {
210        let track_start_event = Event {
211            r#type: EventType::TrackStartEvent,
212            guild_id: Id::<GuildMarker>::new(987_654_321).to_string(),
213            data: EventData::TrackStartEvent(
214                TrackStart {
215                    track: Track {
216                        encoded: "QAAAzgMAMUJsZWVkIEl0IE91dCBbT2ZmaWNpYWwgTXVzaWMgVmlkZW9dIC0gTGlua2luIFBhcmsAC0xpbmtpbiBQYXJrAAAAAAAClCgAC09udXVZY3FoekNFAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9T251dVljcWh6Q0UBADRodHRwczovL2kueXRpbWcuY29tL3ZpL09udXVZY3FoekNFL21heHJlc2RlZmF1bHQuanBnAAAHeW91dHViZQAAAAAAAAAA".to_string(),
217                        info: TrackInfo {
218                            identifier: "OnuuYcqhzCE".to_string(),
219                            is_seekable: true,
220                            author: "Linkin Park".to_string(),
221                            length: 169_000,
222                            is_stream: false,
223                            position: 0,
224                            title: "Bleed It Out [Official Music Video] - Linkin Park".to_string(),
225                            uri:Some("https://www.youtube.com/watch?v=OnuuYcqhzCE".to_string()),
226                            source_name:"youtube".to_string(),
227                            artwork_url:Some("https://i.ytimg.com/vi/OnuuYcqhzCE/maxresdefault.jpg".to_string()),
228                            isrc: None
229                        }
230                    }
231                }
232            )
233
234        };
235        compare_json_payload(
236            &track_start_event.clone(),
237            r#"{"op":"event","guildId":"987654321","type":"TrackStartEvent","track":{"encoded":"QAAAzgMAMUJsZWVkIEl0IE91dCBbT2ZmaWNpYWwgTXVzaWMgVmlkZW9dIC0gTGlua2luIFBhcmsAC0xpbmtpbiBQYXJrAAAAAAAClCgAC09udXVZY3FoekNFAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9T251dVljcWh6Q0UBADRodHRwczovL2kueXRpbWcuY29tL3ZpL09udXVZY3FoekNFL21heHJlc2RlZmF1bHQuanBnAAAHeW91dHViZQAAAAAAAAAA","info":{"identifier":"OnuuYcqhzCE","isSeekable":true,"author":"Linkin Park","length":169000,"isStream":false,"position":0,"title":"Bleed It Out [Official Music Video] - Linkin Park","uri":"https://www.youtube.com/watch?v=OnuuYcqhzCE","artworkUrl":"https://i.ytimg.com/vi/OnuuYcqhzCE/maxresdefault.jpg","isrc":null,"sourceName":"youtube"},"pluginInfo":{},"userData":{}}}"#,
238        );
239    }
240
241    #[test]
242    fn should_deserialize_track_exception_event() {
243        let track_exception_event = Event {
244            r#type: EventType::TrackExceptionEvent,
245            guild_id: Id::<GuildMarker>::new(987_654_321).to_string(),
246            data: EventData::TrackExceptionEvent(
247                TrackException {
248                    track: Track {
249                        encoded: "QAAAjQIAJVJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAADlJpY2tBc3RsZXlWRVZPAAAAAAADPCAAC2RRdzR3OVdnWGNRAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9ZFF3NHc5V2dYY1EAB3lvdXR1YmUAAAAAAAAAAA==".to_string(),
250                        info: TrackInfo {
251                            identifier: "dQw4w9WgXcQ".to_string(),
252                            is_seekable: true,
253                            author: "RickAstleyVEVO".to_string(),
254                            length: 212_000,
255                            is_stream: false,
256                            position: 0,
257                            title: "Rick Astley - Never Gonna Give You Up".to_string(),
258                            uri:Some("https://www.youtube.com/watch?v=dQw4w9WgXcQ".to_string()),
259                            source_name:"youtube".to_string(),
260                            artwork_url:Some("https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg".to_string()),
261                            isrc: None
262                        }
263                    },
264                    exception: Exception {
265                        message: Some(String::new()),
266                        severity: Severity::Common,
267                        cause: "No video found.".to_string(),
268                        cause_stack_trace: String::new(),
269                    }
270
271                }
272            )
273
274        };
275        compare_json_payload(
276            &track_exception_event.clone(),
277            r#"{"op":"event","type":"TrackExceptionEvent","guildId":"987654321","track":{"encoded":"QAAAjQIAJVJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAADlJpY2tBc3RsZXlWRVZPAAAAAAADPCAAC2RRdzR3OVdnWGNRAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9ZFF3NHc5V2dYY1EAB3lvdXR1YmUAAAAAAAAAAA==","info":{"identifier":"dQw4w9WgXcQ","isSeekable":true,"author":"RickAstleyVEVO","length":212000,"isStream":false,"position":0,"title":"Rick Astley - Never Gonna Give You Up","uri":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","artworkUrl":"https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg","isrc":null,"sourceName":"youtube"},"pluginInfo":{}},"exception":{"message":"","severity":"common","cause":"No video found.","causeStackTrace":""}}"#,
278        );
279    }
280
281    #[test]
282    fn should_deserialize_track_stuck_event() {
283        let track_stuck_event = Event {
284            r#type: EventType::TrackStuckEvent,
285            guild_id: Id::<GuildMarker>::new(987_654_321).to_string(),
286            data: EventData::TrackStuckEvent(
287                TrackStuck {
288                    track: Track {
289                        encoded: "QAAAjQIAJVJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAADlJpY2tBc3RsZXlWRVZPAAAAAAADPCAAC2RRdzR3OVdnWGNRAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9ZFF3NHc5V2dYY1EAB3lvdXR1YmUAAAAAAAAAAA==".to_string(),
290                        info: TrackInfo {
291                            identifier: "dQw4w9WgXcQ".to_string(),
292                            is_seekable: true,
293                            author: "RickAstleyVEVO".to_string(),
294                            length: 212_000,
295                            is_stream: false,
296                            position: 0,
297                            title: "Rick Astley - Never Gonna Give You Up".to_string(),
298                            uri:Some("https://www.youtube.com/watch?v=dQw4w9WgXcQ".to_string()),
299                            source_name:"youtube".to_string(),
300                            artwork_url:Some("https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg".to_string()),
301                            isrc: None
302                        }
303                    },
304                    threshold_ms: 123_456_789,
305
306                }
307            )
308
309        };
310        compare_json_payload(
311            &track_stuck_event.clone(),
312            r#"{"op":"event","type":"TrackStuckEvent","guildId":"987654321","track":{"encoded":"QAAAjQIAJVJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAADlJpY2tBc3RsZXlWRVZPAAAAAAADPCAAC2RRdzR3OVdnWGNRAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9ZFF3NHc5V2dYY1EAB3lvdXR1YmUAAAAAAAAAAA==","info":{"identifier":"dQw4w9WgXcQ","isSeekable":true,"author":"RickAstleyVEVO","length":212000,"isStream":false,"position":0,"title":"Rick Astley - Never Gonna Give You Up","uri":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","artworkUrl":"https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg","isrc":null,"sourceName":"youtube"},"pluginInfo":{}},"thresholdMs":123456789}"#,
313        );
314    }
315
316    #[test]
317    fn should_deserialize_track_end_event() {
318        let track_stuck_event = Event {
319            r#type: EventType::TrackEndEvent,
320            guild_id: Id::<GuildMarker>::new(987_654_321).to_string(),
321            data: EventData::TrackEndEvent(
322                TrackEnd {
323                    track: Track {
324                        encoded: "QAAAjQIAJVJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAADlJpY2tBc3RsZXlWRVZPAAAAAAADPCAAC2RRdzR3OVdnWGNRAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9ZFF3NHc5V2dYY1EAB3lvdXR1YmUAAAAAAAAAAA==".to_string(),
325                        info: TrackInfo {
326                            identifier: "dQw4w9WgXcQ".to_string(),
327                            is_seekable: true,
328                            author: "RickAstleyVEVO".to_string(),
329                            length: 212_000,
330                            is_stream: false,
331                            position: 0,
332                            title: "Rick Astley - Never Gonna Give You Up".to_string(),
333                            uri:Some("https://www.youtube.com/watch?v=dQw4w9WgXcQ".to_string()),
334                            source_name:"youtube".to_string(),
335                            artwork_url:Some("https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg".to_string()),
336                            isrc: None
337                        }
338                    },
339                    reason: TrackEndReason::Finished,
340                }
341            )
342
343        };
344        compare_json_payload(
345            &track_stuck_event.clone(),
346            r#"{"op":"event","type":"TrackEndEvent","guildId":"987654321","track":{"encoded":"QAAAjQIAJVJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAADlJpY2tBc3RsZXlWRVZPAAAAAAADPCAAC2RRdzR3OVdnWGNRAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9ZFF3NHc5V2dYY1EAB3lvdXR1YmUAAAAAAAAAAA==","info":{"identifier":"dQw4w9WgXcQ","isSeekable":true,"author":"RickAstleyVEVO","length":212000,"isStream":false,"position":0,"title":"Rick Astley - Never Gonna Give You Up","uri":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","artworkUrl":"https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg","isrc":null,"sourceName":"youtube"},"pluginInfo":{}},"reason":"finished"}"#,
347        );
348    }
349
350    #[test]
351    fn should_deserialize_websocketclosed_event() {
352        let websocket_closed_event = Event {
353            r#type: EventType::WebSocketClosedEvent,
354            guild_id: Id::<GuildMarker>::new(987_654_321).to_string(),
355            data: EventData::WebSocketClosedEvent(WebSocketClosed {
356                code: 1000,
357                reason: String::new(),
358                by_remote: false,
359            }),
360        };
361        compare_json_payload(
362            &websocket_closed_event.clone(),
363            r#"{"op":"event","type":"WebSocketClosedEvent","guildId":"987654321","code":1000,"reason":"","byRemote":false}"#,
364        );
365    }
366}
367
368#[cfg(test)]
369mod lavalink_outgoing_model_tests {
370    use crate::model::outgoing::TrackOption;
371    use crate::model::{Destroy, Equalizer, Pause, Play, Seek, Stop, Volume};
372
373    use twilight_model::id::{Id, marker::GuildMarker};
374
375    use super::EqualizerBand;
376    use super::outgoing::{OutgoingEvent, UpdatePlayerTrack, Voice, VoiceUpdate};
377
378    // For some of the outgoing we have fields that don't get deserialized. We only need
379    // to check weather the serialization is working.
380    fn compare_json_payload<T: serde::Serialize + std::fmt::Debug + std::cmp::PartialEq>(
381        data_struct: &T,
382        json_payload: &str,
383    ) {
384        let serialized = serde_json::to_string(&data_struct).unwrap();
385        let expected_serialized = json_payload;
386        assert_eq!(serialized, expected_serialized);
387    }
388
389    #[test]
390    fn should_serialize_an_outgoing_voice_update() {
391        let voice = VoiceUpdate {
392            guild_id: Id::<GuildMarker>::new(987_654_321),
393            voice: Voice {
394                token: String::from("863ea8ef2ads8ef2"),
395                endpoint: String::from("eu-centra654863.discord.media:443"),
396                session_id: String::from("asdf5w1efa65feaf315e8a8effsa1e5f"),
397            },
398        };
399        compare_json_payload(
400            &voice,
401            r#"{"voice":{"endpoint":"eu-centra654863.discord.media:443","sessionId":"asdf5w1efa65feaf315e8a8effsa1e5f","token":"863ea8ef2ads8ef2"}}"#,
402        );
403    }
404
405    #[test]
406    fn should_serialize_an_outgoing_play() {
407        let play = OutgoingEvent::Play(Play{
408            track: Some(UpdatePlayerTrack {
409                track_string: TrackOption::Encoded(Some("QAAAzgMAMUJsZWVkIEl0IE91dCBbT2ZmaWNpYWwgTXVzaWMgVmlkZW9dIC0gTGlua2luIFBhcmsAC0xpbmtpbiBQYXJrAAAAAAAClCgAC09udXVZY3FoekNFAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9T251dVljcWh6Q0UBADRodHRwczovL2kueXRpbWcuY29tL3ZpL09udXVZY3FoekNFL21heHJlc2RlZmF1bHQuanBnAAAHeW91dHViZQAAAAAAAAAA".to_string())),
410            }),
411            position: None,
412            end_time: Some(None),
413            volume: None,
414            paused: None,
415            guild_id: Id::<GuildMarker>::new(987_654_321),
416            no_replace: true,
417        });
418        compare_json_payload(
419            &play,
420            r#"{"endTime":null,"track":{"encoded":"QAAAzgMAMUJsZWVkIEl0IE91dCBbT2ZmaWNpYWwgTXVzaWMgVmlkZW9dIC0gTGlua2luIFBhcmsAC0xpbmtpbiBQYXJrAAAAAAAClCgAC09udXVZY3FoekNFAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9T251dVljcWh6Q0UBADRodHRwczovL2kueXRpbWcuY29tL3ZpL09udXVZY3FoekNFL21heHJlc2RlZmF1bHQuanBnAAAHeW91dHViZQAAAAAAAAAA"}}"#,
421        );
422    }
423
424    #[test]
425    fn should_serialize_an_outgoing_stop() {
426        let stop = OutgoingEvent::Stop(Stop {
427            track: UpdatePlayerTrack {
428                track_string: TrackOption::Encoded(None),
429            },
430            guild_id: Id::<GuildMarker>::new(987_654_321),
431        });
432        compare_json_payload(&stop, r#"{"track":{"encoded":null}}"#);
433    }
434
435    #[test]
436    fn should_serialize_an_outgoing_pause() {
437        let pause = OutgoingEvent::Pause(Pause {
438            paused: true,
439            guild_id: Id::<GuildMarker>::new(987_654_321),
440        });
441        compare_json_payload(&pause, r#"{"guildId":"987654321","paused":true}"#);
442    }
443
444    #[test]
445    fn should_serialize_an_outgoing_seek() {
446        let seek = OutgoingEvent::Seek(Seek {
447            position: 66000,
448            guild_id: Id::<GuildMarker>::new(987_654_321),
449        });
450        compare_json_payload(&seek, r#"{"position":66000}"#);
451    }
452
453    #[test]
454    fn should_serialize_an_outgoing_volume() {
455        let volume = OutgoingEvent::Volume(Volume {
456            volume: 50,
457            guild_id: Id::<GuildMarker>::new(987_654_321),
458        });
459        compare_json_payload(&volume, r#"{"volume":50}"#);
460    }
461
462    #[test]
463    fn should_serialize_an_outgoing_destroy_aka_leave() {
464        let destroy = OutgoingEvent::Destroy(Destroy {
465            guild_id: Id::<GuildMarker>::new(987_654_321),
466        });
467        compare_json_payload(&destroy, r#"{"guildId":"987654321"}"#);
468    }
469
470    #[test]
471    fn should_serialize_an_outgoing_equalize() {
472        let equalize = OutgoingEvent::Equalizer(Equalizer {
473            equalizer: vec![EqualizerBand::new(5, -0.15)],
474            guild_id: Id::<GuildMarker>::new(987_654_321),
475        });
476        compare_json_payload(&equalize, r#"{"equalizer":[{"band":5,"gain":-0.15}]}"#);
477    }
478}