1use crate::{define_operation_with_response, define_upnp_operation, Validate};
7use paste::paste;
8
9define_upnp_operation! {
14 operation: PlayOperation,
15 action: "Play",
16 service: AVTransport,
17 request: {
18 speed: String,
19 },
20 response: (),
21 payload: |req| {
22 format!("<InstanceID>{}</InstanceID><Speed>{}</Speed>", req.instance_id, req.speed)
23 },
24 parse: |_xml| Ok(()),
25}
26
27impl Validate for PlayOperationRequest {
28 fn validate_basic(&self) -> Result<(), crate::operation::ValidationError> {
29 if self.speed.is_empty() {
30 return Err(crate::operation::ValidationError::invalid_value(
31 "speed",
32 &self.speed,
33 ));
34 }
35 match self.speed.as_str() {
36 "1" | "0" => Ok(()),
37 other => {
38 if other.parse::<f32>().is_ok() {
39 Ok(())
40 } else {
41 Err(crate::operation::ValidationError::Custom {
42 parameter: "speed".to_string(),
43 message: "Speed must be '1', '0', or a numeric value".to_string(),
44 })
45 }
46 }
47 }
48 }
49}
50
51define_upnp_operation! {
52 operation: PauseOperation,
53 action: "Pause",
54 service: AVTransport,
55 request: {},
56 response: (),
57 payload: |req| format!("<InstanceID>{}</InstanceID>", req.instance_id),
58 parse: |_xml| Ok(()),
59}
60
61impl Validate for PauseOperationRequest {}
62
63define_upnp_operation! {
64 operation: StopOperation,
65 action: "Stop",
66 service: AVTransport,
67 request: {},
68 response: (),
69 payload: |req| format!("<InstanceID>{}</InstanceID>", req.instance_id),
70 parse: |_xml| Ok(()),
71}
72
73impl Validate for StopOperationRequest {}
74
75define_upnp_operation! {
76 operation: NextOperation,
77 action: "Next",
78 service: AVTransport,
79 request: {},
80 response: (),
81 payload: |req| format!("<InstanceID>{}</InstanceID>", req.instance_id),
82 parse: |_xml| Ok(()),
83}
84
85impl Validate for NextOperationRequest {}
86
87define_upnp_operation! {
88 operation: PreviousOperation,
89 action: "Previous",
90 service: AVTransport,
91 request: {},
92 response: (),
93 payload: |req| format!("<InstanceID>{}</InstanceID>", req.instance_id),
94 parse: |_xml| Ok(()),
95}
96
97impl Validate for PreviousOperationRequest {}
98
99define_upnp_operation! {
104 operation: SeekOperation,
105 action: "Seek",
106 service: AVTransport,
107 request: {
108 unit: String,
109 target: String,
110 },
111 response: (),
112 payload: |req| {
113 format!(
114 "<InstanceID>{}</InstanceID><Unit>{}</Unit><Target>{}</Target>",
115 req.instance_id,
116 crate::operation::xml_escape(&req.unit),
117 crate::operation::xml_escape(&req.target)
118 )
119 },
120 parse: |_xml| Ok(()),
121}
122
123impl Validate for SeekOperationRequest {
124 fn validate_basic(&self) -> Result<(), crate::operation::ValidationError> {
125 match self.unit.as_str() {
126 "TRACK_NR" | "REL_TIME" | "TIME_DELTA" => Ok(()),
127 other => Err(crate::operation::ValidationError::Custom {
128 parameter: "unit".to_string(),
129 message: format!(
130 "Invalid unit '{other}'. Must be 'TRACK_NR', 'REL_TIME', or 'TIME_DELTA'"
131 ),
132 }),
133 }
134 }
135}
136
137define_operation_with_response! {
138 operation: GetPositionInfoOperation,
139 action: "GetPositionInfo",
140 service: AVTransport,
141 request: {},
142 response: GetPositionInfoResponse {
143 track: u32,
144 track_duration: String,
145 track_meta_data: String,
146 track_uri: String,
147 rel_time: String,
148 abs_time: String,
149 rel_count: i32,
150 abs_count: i32,
151 },
152 xml_mapping: {
153 track: "Track",
154 track_duration: "TrackDuration",
155 track_meta_data: "TrackMetaData",
156 track_uri: "TrackURI",
157 rel_time: "RelTime",
158 abs_time: "AbsTime",
159 rel_count: "RelCount",
160 abs_count: "AbsCount",
161 },
162}
163
164impl Validate for GetPositionInfoOperationRequest {}
165
166define_operation_with_response! {
171 operation: GetTransportInfoOperation,
172 action: "GetTransportInfo",
173 service: AVTransport,
174 request: {},
175 response: GetTransportInfoResponse {
176 current_transport_state: String,
177 current_transport_status: String,
178 current_speed: String,
179 },
180 xml_mapping: {
181 current_transport_state: "CurrentTransportState",
182 current_transport_status: "CurrentTransportStatus",
183 current_speed: "CurrentSpeed",
184 },
185}
186
187impl Validate for GetTransportInfoOperationRequest {}
188
189define_operation_with_response! {
190 operation: GetTransportSettingsOperation,
191 action: "GetTransportSettings",
192 service: AVTransport,
193 request: {},
194 response: GetTransportSettingsResponse {
195 play_mode: String,
196 rec_quality_mode: String,
197 },
198 xml_mapping: {
199 play_mode: "PlayMode",
200 rec_quality_mode: "RecQualityMode",
201 },
202}
203
204impl Validate for GetTransportSettingsOperationRequest {}
205
206define_operation_with_response! {
207 operation: GetCurrentTransportActionsOperation,
208 action: "GetCurrentTransportActions",
209 service: AVTransport,
210 request: {},
211 response: GetCurrentTransportActionsResponse {
212 actions: String,
213 },
214 xml_mapping: {
215 actions: "Actions",
216 },
217}
218
219impl Validate for GetCurrentTransportActionsOperationRequest {}
220
221define_operation_with_response! {
222 operation: GetDeviceCapabilitiesOperation,
223 action: "GetDeviceCapabilities",
224 service: AVTransport,
225 request: {},
226 response: GetDeviceCapabilitiesResponse {
227 play_media: String,
228 rec_media: String,
229 rec_quality_modes: String,
230 },
231 xml_mapping: {
232 play_media: "PlayMedia",
233 rec_media: "RecMedia",
234 rec_quality_modes: "RecQualityModes",
235 },
236}
237
238impl Validate for GetDeviceCapabilitiesOperationRequest {}
239
240define_operation_with_response! {
245 operation: GetMediaInfoOperation,
246 action: "GetMediaInfo",
247 service: AVTransport,
248 request: {},
249 response: GetMediaInfoResponse {
250 nr_tracks: u32,
251 media_duration: String,
252 current_uri: String,
253 current_uri_meta_data: String,
254 next_uri: String,
255 next_uri_meta_data: String,
256 play_medium: String,
257 record_medium: String,
258 write_status: String,
259 },
260 xml_mapping: {
261 nr_tracks: "NrTracks",
262 media_duration: "MediaDuration",
263 current_uri: "CurrentURI",
264 current_uri_meta_data: "CurrentURIMetaData",
265 next_uri: "NextURI",
266 next_uri_meta_data: "NextURIMetaData",
267 play_medium: "PlayMedium",
268 record_medium: "RecordMedium",
269 write_status: "WriteStatus",
270 },
271}
272
273impl Validate for GetMediaInfoOperationRequest {}
274
275define_upnp_operation! {
276 operation: SetAVTransportURIOperation,
277 action: "SetAVTransportURI",
278 service: AVTransport,
279 request: {
280 current_uri: String,
281 current_uri_meta_data: String,
282 },
283 response: (),
284 payload: |req| {
285 format!(
286 "<InstanceID>{}</InstanceID><CurrentURI>{}</CurrentURI><CurrentURIMetaData>{}</CurrentURIMetaData>",
287 req.instance_id,
288 crate::operation::xml_escape(&req.current_uri),
289 crate::operation::xml_escape(&req.current_uri_meta_data)
290 )
291 },
292 parse: |_xml| Ok(()),
293}
294
295impl Validate for SetAVTransportURIOperationRequest {}
296
297define_upnp_operation! {
298 operation: SetNextAVTransportURIOperation,
299 action: "SetNextAVTransportURI",
300 service: AVTransport,
301 request: {
302 next_uri: String,
303 next_uri_meta_data: String,
304 },
305 response: (),
306 payload: |req| {
307 format!(
308 "<InstanceID>{}</InstanceID><NextURI>{}</NextURI><NextURIMetaData>{}</NextURIMetaData>",
309 req.instance_id,
310 crate::operation::xml_escape(&req.next_uri),
311 crate::operation::xml_escape(&req.next_uri_meta_data)
312 )
313 },
314 parse: |_xml| Ok(()),
315}
316
317impl Validate for SetNextAVTransportURIOperationRequest {}
318
319define_operation_with_response! {
324 operation: GetCrossfadeModeOperation,
325 action: "GetCrossfadeMode",
326 service: AVTransport,
327 request: {},
328 response: GetCrossfadeModeResponse {
329 crossfade_mode: String,
330 },
331 xml_mapping: {
332 crossfade_mode: "CrossfadeMode",
333 },
334}
335
336impl Validate for GetCrossfadeModeOperationRequest {}
337
338define_upnp_operation! {
339 operation: SetCrossfadeModeOperation,
340 action: "SetCrossfadeMode",
341 service: AVTransport,
342 request: {
343 crossfade_mode: bool,
344 },
345 response: (),
346 payload: |req| {
347 format!(
348 "<InstanceID>{}</InstanceID><CrossfadeMode>{}</CrossfadeMode>",
349 req.instance_id,
350 if req.crossfade_mode { "1" } else { "0" }
351 )
352 },
353 parse: |_xml| Ok(()),
354}
355
356impl Validate for SetCrossfadeModeOperationRequest {}
357
358define_upnp_operation! {
359 operation: SetPlayModeOperation,
360 action: "SetPlayMode",
361 service: AVTransport,
362 request: {
363 new_play_mode: String,
364 },
365 response: (),
366 payload: |req| {
367 format!(
368 "<InstanceID>{}</InstanceID><NewPlayMode>{}</NewPlayMode>",
369 req.instance_id,
370 crate::operation::xml_escape(&req.new_play_mode)
371 )
372 },
373 parse: |_xml| Ok(()),
374}
375
376impl Validate for SetPlayModeOperationRequest {
377 fn validate_basic(&self) -> Result<(), crate::operation::ValidationError> {
378 match self.new_play_mode.as_str() {
379 "NORMAL" | "REPEAT_ALL" | "REPEAT_ONE" | "SHUFFLE_NOREPEAT" | "SHUFFLE"
380 | "SHUFFLE_REPEAT_ONE" => Ok(()),
381 other => Err(crate::operation::ValidationError::Custom {
382 parameter: "new_play_mode".to_string(),
383 message: format!(
384 "Invalid play mode '{other}'. Must be NORMAL, REPEAT_ALL, REPEAT_ONE, SHUFFLE_NOREPEAT, SHUFFLE, or SHUFFLE_REPEAT_ONE"
385 ),
386 }),
387 }
388 }
389}
390
391define_upnp_operation! {
396 operation: ConfigureSleepTimerOperation,
397 action: "ConfigureSleepTimer",
398 service: AVTransport,
399 request: {
400 new_sleep_timer_duration: String,
401 },
402 response: (),
403 payload: |req| {
404 format!(
405 "<InstanceID>{}</InstanceID><NewSleepTimerDuration>{}</NewSleepTimerDuration>",
406 req.instance_id,
407 crate::operation::xml_escape(&req.new_sleep_timer_duration)
408 )
409 },
410 parse: |_xml| Ok(()),
411}
412
413impl Validate for ConfigureSleepTimerOperationRequest {}
414
415define_operation_with_response! {
416 operation: GetRemainingSleepTimerDurationOperation,
417 action: "GetRemainingSleepTimerDuration",
418 service: AVTransport,
419 request: {},
420 response: GetRemainingSleepTimerDurationResponse {
421 remaining_sleep_timer_duration: String,
422 current_sleep_timer_generation: u32,
423 },
424 xml_mapping: {
425 remaining_sleep_timer_duration: "RemainingSleepTimerDuration",
426 current_sleep_timer_generation: "CurrentSleepTimerGeneration",
427 },
428}
429
430impl Validate for GetRemainingSleepTimerDurationOperationRequest {}
431
432use serde::{Deserialize, Serialize};
439
440#[derive(Debug, Clone, Serialize, Deserialize)]
441pub struct AddURIToQueueOperationRequest {
442 pub instance_id: u32,
443 pub enqueued_uri: String,
444 pub enqueued_uri_meta_data: String,
445 pub desired_first_track_number_enqueued: u32,
446 pub enqueue_as_next: bool,
447}
448
449impl Validate for AddURIToQueueOperationRequest {}
450
451#[derive(Debug, Clone, Serialize, Deserialize, Default)]
452pub struct AddURIToQueueResponse {
453 pub first_track_number_enqueued: u32,
454 pub num_tracks_added: u32,
455 pub new_queue_length: u32,
456}
457
458pub struct AddURIToQueueOperation;
459
460impl crate::operation::UPnPOperation for AddURIToQueueOperation {
461 type Request = AddURIToQueueOperationRequest;
462 type Response = AddURIToQueueResponse;
463
464 const SERVICE: crate::service::Service = crate::service::Service::AVTransport;
465 const ACTION: &'static str = "AddURIToQueue";
466
467 fn build_payload(request: &Self::Request) -> Result<String, crate::operation::ValidationError> {
468 <Self::Request as Validate>::validate(request, crate::operation::ValidationLevel::Basic)?;
469 Ok(format!(
470 "<InstanceID>{}</InstanceID><EnqueuedURI>{}</EnqueuedURI><EnqueuedURIMetaData>{}</EnqueuedURIMetaData><DesiredFirstTrackNumberEnqueued>{}</DesiredFirstTrackNumberEnqueued><EnqueueAsNext>{}</EnqueueAsNext>",
471 request.instance_id,
472 crate::operation::xml_escape(&request.enqueued_uri),
473 crate::operation::xml_escape(&request.enqueued_uri_meta_data),
474 request.desired_first_track_number_enqueued,
475 if request.enqueue_as_next { "1" } else { "0" }
476 ))
477 }
478
479 fn parse_response(xml: &xmltree::Element) -> Result<Self::Response, crate::error::ApiError> {
480 Ok(AddURIToQueueResponse {
481 first_track_number_enqueued: xml
482 .get_child("FirstTrackNumberEnqueued")
483 .and_then(|e| e.get_text())
484 .and_then(|s| s.parse().ok())
485 .unwrap_or_default(),
486 num_tracks_added: xml
487 .get_child("NumTracksAdded")
488 .and_then(|e| e.get_text())
489 .and_then(|s| s.parse().ok())
490 .unwrap_or_default(),
491 new_queue_length: xml
492 .get_child("NewQueueLength")
493 .and_then(|e| e.get_text())
494 .and_then(|s| s.parse().ok())
495 .unwrap_or_default(),
496 })
497 }
498}
499
500pub fn add_uri_to_queue_operation(
501 enqueued_uri: String,
502 enqueued_uri_meta_data: String,
503 desired_first_track_number_enqueued: u32,
504 enqueue_as_next: bool,
505) -> crate::operation::OperationBuilder<AddURIToQueueOperation> {
506 let request = AddURIToQueueOperationRequest {
507 instance_id: 0,
508 enqueued_uri,
509 enqueued_uri_meta_data,
510 desired_first_track_number_enqueued,
511 enqueue_as_next,
512 };
513 crate::operation::OperationBuilder::new(request)
514}
515
516define_upnp_operation! {
517 operation: RemoveTrackFromQueueOperation,
518 action: "RemoveTrackFromQueue",
519 service: AVTransport,
520 request: {
521 object_id: String,
522 update_id: u32,
523 },
524 response: (),
525 payload: |req| {
526 format!(
527 "<InstanceID>{}</InstanceID><ObjectID>{}</ObjectID><UpdateID>{}</UpdateID>",
528 req.instance_id,
529 crate::operation::xml_escape(&req.object_id),
530 req.update_id
531 )
532 },
533 parse: |_xml| Ok(()),
534}
535
536impl Validate for RemoveTrackFromQueueOperationRequest {}
537
538define_operation_with_response! {
539 operation: RemoveTrackRangeFromQueueOperation,
540 action: "RemoveTrackRangeFromQueue",
541 service: AVTransport,
542 request: {
543 update_id: u32,
544 starting_index: u32,
545 number_of_tracks: u32,
546 },
547 response: RemoveTrackRangeFromQueueResponse {
548 new_update_id: u32,
549 },
550 request_xml_mapping: {
551 update_id: "UpdateID",
552 starting_index: "StartingIndex",
553 number_of_tracks: "NumberOfTracks",
554 },
555 xml_mapping: {
556 new_update_id: "NewUpdateID",
557 },
558}
559
560impl Validate for RemoveTrackRangeFromQueueOperationRequest {}
561
562define_upnp_operation! {
563 operation: RemoveAllTracksFromQueueOperation,
564 action: "RemoveAllTracksFromQueue",
565 service: AVTransport,
566 request: {},
567 response: (),
568 payload: |req| format!("<InstanceID>{}</InstanceID>", req.instance_id),
569 parse: |_xml| Ok(()),
570}
571
572impl Validate for RemoveAllTracksFromQueueOperationRequest {}
573
574define_operation_with_response! {
575 operation: SaveQueueOperation,
576 action: "SaveQueue",
577 service: AVTransport,
578 request: {
579 title: String,
580 object_id: String,
581 },
582 response: SaveQueueResponse {
583 assigned_object_id: String,
584 },
585 request_xml_mapping: {
586 title: "Title",
587 object_id: "ObjectID",
588 },
589 xml_mapping: {
590 assigned_object_id: "AssignedObjectID",
591 },
592}
593
594impl Validate for SaveQueueOperationRequest {}
595
596define_operation_with_response! {
597 operation: CreateSavedQueueOperation,
598 action: "CreateSavedQueue",
599 service: AVTransport,
600 request: {
601 title: String,
602 enqueued_uri: String,
603 enqueued_uri_meta_data: String,
604 },
605 response: CreateSavedQueueResponse {
606 num_tracks_added: u32,
607 new_queue_length: u32,
608 assigned_object_id: String,
609 new_update_id: u32,
610 },
611 request_xml_mapping: {
612 title: "Title",
613 enqueued_uri: "EnqueuedURI",
614 enqueued_uri_meta_data: "EnqueuedURIMetaData",
615 },
616 xml_mapping: {
617 num_tracks_added: "NumTracksAdded",
618 new_queue_length: "NewQueueLength",
619 assigned_object_id: "AssignedObjectID",
620 new_update_id: "NewUpdateID",
621 },
622}
623
624impl Validate for CreateSavedQueueOperationRequest {}
625
626define_upnp_operation! {
627 operation: BackupQueueOperation,
628 action: "BackupQueue",
629 service: AVTransport,
630 request: {},
631 response: (),
632 payload: |req| format!("<InstanceID>{}</InstanceID>", req.instance_id),
633 parse: |_xml| Ok(()),
634}
635
636impl Validate for BackupQueueOperationRequest {}
637
638define_operation_with_response! {
643 operation: BecomeCoordinatorOfStandaloneGroupOperation,
644 action: "BecomeCoordinatorOfStandaloneGroup",
645 service: AVTransport,
646 request: {},
647 response: BecomeCoordinatorOfStandaloneGroupResponse {
648 delegated_group_coordinator_id: String,
649 new_group_id: String,
650 },
651 xml_mapping: {
652 delegated_group_coordinator_id: "DelegatedGroupCoordinatorID",
653 new_group_id: "NewGroupID",
654 },
655}
656
657impl Validate for BecomeCoordinatorOfStandaloneGroupOperationRequest {}
658
659define_upnp_operation! {
660 operation: DelegateGroupCoordinationToOperation,
661 action: "DelegateGroupCoordinationTo",
662 service: AVTransport,
663 request: {
664 new_coordinator: String,
665 rejoin_group: bool,
666 },
667 response: (),
668 payload: |req| {
669 format!(
670 "<InstanceID>{}</InstanceID><NewCoordinator>{}</NewCoordinator><RejoinGroup>{}</RejoinGroup>",
671 req.instance_id,
672 crate::operation::xml_escape(&req.new_coordinator),
673 if req.rejoin_group { "true" } else { "false" }
674 )
675 },
676 parse: |_xml| Ok(()),
677}
678
679impl Validate for DelegateGroupCoordinationToOperationRequest {}
680
681define_upnp_operation! {
686 operation: SnoozeAlarmOperation,
687 action: "SnoozeAlarm",
688 service: AVTransport,
689 request: {
690 duration: String,
691 },
692 response: (),
693 payload: |req| {
694 format!(
695 "<InstanceID>{}</InstanceID><Duration>{}</Duration>",
696 req.instance_id,
697 crate::operation::xml_escape(&req.duration)
698 )
699 },
700 parse: |_xml| Ok(()),
701}
702
703impl Validate for SnoozeAlarmOperationRequest {}
704
705define_operation_with_response! {
706 operation: GetRunningAlarmPropertiesOperation,
707 action: "GetRunningAlarmProperties",
708 service: AVTransport,
709 request: {},
710 response: GetRunningAlarmPropertiesResponse {
711 alarm_id: u32,
712 group_id: String,
713 logged_start_time: String,
714 },
715 xml_mapping: {
716 alarm_id: "AlarmID",
717 group_id: "GroupID",
718 logged_start_time: "LoggedStartTime",
719 },
720}
721
722impl Validate for GetRunningAlarmPropertiesOperationRequest {}
723
724pub use next_operation as next;
730pub use pause_operation as pause;
731pub use play_operation as play;
732pub use previous_operation as previous;
733pub use stop_operation as stop;
734
735pub use get_position_info_operation as get_position_info;
737pub use seek_operation as seek;
738
739pub use get_current_transport_actions_operation as get_current_transport_actions;
741pub use get_device_capabilities_operation as get_device_capabilities;
742pub use get_transport_info_operation as get_transport_info;
743pub use get_transport_settings_operation as get_transport_settings;
744
745pub use get_media_info_operation as get_media_info;
747pub use set_a_v_transport_u_r_i_operation as set_av_transport_uri;
748pub use set_next_a_v_transport_u_r_i_operation as set_next_av_transport_uri;
749
750pub use get_crossfade_mode_operation as get_crossfade_mode;
752pub use set_crossfade_mode_operation as set_crossfade_mode;
753pub use set_play_mode_operation as set_play_mode;
754
755pub use configure_sleep_timer_operation as configure_sleep_timer;
757pub use get_remaining_sleep_timer_duration_operation as get_remaining_sleep_timer_duration;
758
759pub use add_uri_to_queue_operation as add_uri_to_queue;
761pub use backup_queue_operation as backup_queue;
762pub use create_saved_queue_operation as create_saved_queue;
763pub use remove_all_tracks_from_queue_operation as remove_all_tracks_from_queue;
764pub use remove_track_from_queue_operation as remove_track_from_queue;
765pub use remove_track_range_from_queue_operation as remove_track_range_from_queue;
766pub use save_queue_operation as save_queue;
767
768pub use become_coordinator_of_standalone_group_operation as become_coordinator_of_standalone_group;
770pub use delegate_group_coordination_to_operation as delegate_group_coordination_to;
771
772pub use get_running_alarm_properties_operation as get_running_alarm_properties;
774pub use snooze_alarm_operation as snooze_alarm;
775
776pub const SERVICE: crate::Service = crate::Service::AVTransport;
782
783pub fn subscribe(
785 client: &crate::SonosClient,
786 ip: &str,
787 callback_url: &str,
788) -> crate::Result<crate::ManagedSubscription> {
789 client.subscribe(ip, SERVICE, callback_url)
790}
791
792pub fn subscribe_with_timeout(
794 client: &crate::SonosClient,
795 ip: &str,
796 callback_url: &str,
797 timeout_seconds: u32,
798) -> crate::Result<crate::ManagedSubscription> {
799 client.subscribe_with_timeout(ip, SERVICE, callback_url, timeout_seconds)
800}
801
802#[cfg(test)]
807mod tests {
808 use super::*;
809 use crate::operation::UPnPOperation;
810
811 #[test]
814 fn test_play_operation_builder() {
815 let op = play_operation("1".to_string()).build().unwrap();
816 assert_eq!(op.request().speed, "1");
817 assert_eq!(op.metadata().action, "Play");
818 }
819
820 #[test]
821 fn test_play_validation() {
822 let request = PlayOperationRequest {
823 instance_id: 0,
824 speed: "".to_string(),
825 };
826 assert!(request.validate_basic().is_err());
827
828 let request = PlayOperationRequest {
829 instance_id: 0,
830 speed: "1".to_string(),
831 };
832 assert!(request.validate_basic().is_ok());
833 }
834
835 #[test]
836 fn test_play_payload() {
837 let request = PlayOperationRequest {
838 instance_id: 0,
839 speed: "1".to_string(),
840 };
841 let payload = PlayOperation::build_payload(&request).unwrap();
842 assert!(payload.contains("<InstanceID>0</InstanceID>"));
843 assert!(payload.contains("<Speed>1</Speed>"));
844 }
845
846 #[test]
847 fn test_pause_operation_builder() {
848 let op = pause_operation().build().unwrap();
849 assert_eq!(op.metadata().action, "Pause");
850 }
851
852 #[test]
853 fn test_stop_operation_builder() {
854 let op = stop_operation().build().unwrap();
855 assert_eq!(op.metadata().action, "Stop");
856 }
857
858 #[test]
859 fn test_next_operation_builder() {
860 let op = next_operation().build().unwrap();
861 assert_eq!(op.metadata().action, "Next");
862 }
863
864 #[test]
865 fn test_previous_operation_builder() {
866 let op = previous_operation().build().unwrap();
867 assert_eq!(op.metadata().action, "Previous");
868 }
869
870 #[test]
873 fn test_seek_operation_builder() {
874 let op = seek_operation("TRACK_NR".to_string(), "5".to_string())
875 .build()
876 .unwrap();
877 assert_eq!(op.request().unit, "TRACK_NR");
878 assert_eq!(op.request().target, "5");
879 assert_eq!(op.metadata().action, "Seek");
880 }
881
882 #[test]
883 fn test_seek_validation() {
884 let request = SeekOperationRequest {
885 instance_id: 0,
886 unit: "INVALID".to_string(),
887 target: "5".to_string(),
888 };
889 assert!(request.validate_basic().is_err());
890
891 let request = SeekOperationRequest {
892 instance_id: 0,
893 unit: "REL_TIME".to_string(),
894 target: "0:01:30".to_string(),
895 };
896 assert!(request.validate_basic().is_ok());
897 }
898
899 #[test]
900 fn test_seek_payload() {
901 let request = SeekOperationRequest {
902 instance_id: 0,
903 unit: "TRACK_NR".to_string(),
904 target: "3".to_string(),
905 };
906 let payload = SeekOperation::build_payload(&request).unwrap();
907 assert!(payload.contains("<Unit>TRACK_NR</Unit>"));
908 assert!(payload.contains("<Target>3</Target>"));
909 }
910
911 #[test]
914 fn test_get_transport_info_builder() {
915 let op = get_transport_info_operation().build().unwrap();
916 assert_eq!(op.metadata().action, "GetTransportInfo");
917 }
918
919 #[test]
920 fn test_get_position_info_builder() {
921 let op = get_position_info_operation().build().unwrap();
922 assert_eq!(op.metadata().action, "GetPositionInfo");
923 }
924
925 #[test]
926 fn test_get_media_info_builder() {
927 let op = get_media_info_operation().build().unwrap();
928 assert_eq!(op.metadata().action, "GetMediaInfo");
929 }
930
931 #[test]
932 fn test_get_transport_settings_builder() {
933 let op = get_transport_settings_operation().build().unwrap();
934 assert_eq!(op.metadata().action, "GetTransportSettings");
935 }
936
937 #[test]
940 fn test_get_crossfade_mode_builder() {
941 let op = get_crossfade_mode_operation().build().unwrap();
942 assert_eq!(op.metadata().action, "GetCrossfadeMode");
943 }
944
945 #[test]
946 fn test_set_crossfade_mode_builder() {
947 let op = set_crossfade_mode_operation(true).build().unwrap();
948 assert!(op.request().crossfade_mode);
949 assert_eq!(op.metadata().action, "SetCrossfadeMode");
950 }
951
952 #[test]
953 fn test_set_crossfade_mode_payload() {
954 let request = SetCrossfadeModeOperationRequest {
955 instance_id: 0,
956 crossfade_mode: true,
957 };
958 let payload = SetCrossfadeModeOperation::build_payload(&request).unwrap();
959 assert!(payload.contains("<CrossfadeMode>1</CrossfadeMode>"));
960
961 let request = SetCrossfadeModeOperationRequest {
962 instance_id: 0,
963 crossfade_mode: false,
964 };
965 let payload = SetCrossfadeModeOperation::build_payload(&request).unwrap();
966 assert!(payload.contains("<CrossfadeMode>0</CrossfadeMode>"));
967 }
968
969 #[test]
970 fn test_set_play_mode_builder() {
971 let op = set_play_mode_operation("SHUFFLE".to_string())
972 .build()
973 .unwrap();
974 assert_eq!(op.request().new_play_mode, "SHUFFLE");
975 assert_eq!(op.metadata().action, "SetPlayMode");
976 }
977
978 #[test]
979 fn test_set_play_mode_validation() {
980 let request = SetPlayModeOperationRequest {
981 instance_id: 0,
982 new_play_mode: "INVALID".to_string(),
983 };
984 assert!(request.validate_basic().is_err());
985
986 let request = SetPlayModeOperationRequest {
987 instance_id: 0,
988 new_play_mode: "REPEAT_ALL".to_string(),
989 };
990 assert!(request.validate_basic().is_ok());
991 }
992
993 #[test]
996 fn test_configure_sleep_timer_builder() {
997 let op = configure_sleep_timer_operation("0:30:00".to_string())
998 .build()
999 .unwrap();
1000 assert_eq!(op.request().new_sleep_timer_duration, "0:30:00");
1001 assert_eq!(op.metadata().action, "ConfigureSleepTimer");
1002 }
1003
1004 #[test]
1005 fn test_get_remaining_sleep_timer_duration_builder() {
1006 let op = get_remaining_sleep_timer_duration_operation()
1007 .build()
1008 .unwrap();
1009 assert_eq!(op.metadata().action, "GetRemainingSleepTimerDuration");
1010 }
1011
1012 #[test]
1015 fn test_remove_all_tracks_from_queue_builder() {
1016 let op = remove_all_tracks_from_queue_operation().build().unwrap();
1017 assert_eq!(op.metadata().action, "RemoveAllTracksFromQueue");
1018 }
1019
1020 #[test]
1021 fn test_backup_queue_builder() {
1022 let op = backup_queue_operation().build().unwrap();
1023 assert_eq!(op.metadata().action, "BackupQueue");
1024 }
1025
1026 #[test]
1031 fn test_save_queue_payload_element_names() {
1032 let request = SaveQueueOperationRequest {
1033 instance_id: 0,
1034 title: "My Mix".to_string(),
1035 object_id: "SQ:12".to_string(),
1036 };
1037 let payload = SaveQueueOperation::build_payload(&request).unwrap();
1038 assert!(payload.contains("<Title>My Mix</Title>"), "{payload}");
1039 assert!(payload.contains("<ObjectID>SQ:12</ObjectID>"), "{payload}");
1040 }
1041
1042 #[test]
1043 fn test_create_saved_queue_payload_element_names() {
1044 let request = CreateSavedQueueOperationRequest {
1045 instance_id: 0,
1046 title: "Playlist".to_string(),
1047 enqueued_uri: "x-file-cifs://nas/song.mp3".to_string(),
1048 enqueued_uri_meta_data: "meta".to_string(),
1049 };
1050 let payload = CreateSavedQueueOperation::build_payload(&request).unwrap();
1051 assert!(
1052 payload.contains("<EnqueuedURI>x-file-cifs://nas/song.mp3</EnqueuedURI>"),
1053 "{payload}"
1054 );
1055 assert!(
1056 payload.contains("<EnqueuedURIMetaData>meta</EnqueuedURIMetaData>"),
1057 "{payload}"
1058 );
1059 }
1060
1061 #[test]
1062 fn test_remove_track_range_payload_element_names() {
1063 let request = RemoveTrackRangeFromQueueOperationRequest {
1064 instance_id: 0,
1065 update_id: 7,
1066 starting_index: 2,
1067 number_of_tracks: 3,
1068 };
1069 let payload = RemoveTrackRangeFromQueueOperation::build_payload(&request).unwrap();
1070 assert!(payload.contains("<UpdateID>7</UpdateID>"), "{payload}");
1071 assert!(
1072 payload.contains("<StartingIndex>2</StartingIndex>"),
1073 "{payload}"
1074 );
1075 assert!(
1076 payload.contains("<NumberOfTracks>3</NumberOfTracks>"),
1077 "{payload}"
1078 );
1079 }
1080
1081 #[test]
1082 fn test_add_uri_to_queue_escapes_ampersand() {
1083 let request = AddURIToQueueOperationRequest {
1085 instance_id: 0,
1086 enqueued_uri: "x-sonosapi-stream:s1?sid=254&flags=32".to_string(),
1087 enqueued_uri_meta_data: "<DIDL-Lite>x</DIDL-Lite>".to_string(),
1088 desired_first_track_number_enqueued: 0,
1089 enqueue_as_next: false,
1090 };
1091 let payload = AddURIToQueueOperation::build_payload(&request).unwrap();
1092 assert!(payload.contains("sid=254&flags=32"), "{payload}");
1093 assert!(!payload.contains("sid=254&flags=32"), "{payload}");
1094 assert!(
1096 payload.contains("<DIDL-Lite>x</DIDL-Lite>"),
1097 "{payload}"
1098 );
1099 }
1100
1101 #[test]
1104 fn test_become_coordinator_of_standalone_group_builder() {
1105 let op = become_coordinator_of_standalone_group_operation()
1106 .build()
1107 .unwrap();
1108 assert_eq!(op.metadata().action, "BecomeCoordinatorOfStandaloneGroup");
1109 }
1110
1111 #[test]
1114 fn test_snooze_alarm_builder() {
1115 let op = snooze_alarm_operation("0:10:00".to_string())
1116 .build()
1117 .unwrap();
1118 assert_eq!(op.request().duration, "0:10:00");
1119 assert_eq!(op.metadata().action, "SnoozeAlarm");
1120 }
1121
1122 #[test]
1123 fn test_get_running_alarm_properties_builder() {
1124 let op = get_running_alarm_properties_operation().build().unwrap();
1125 assert_eq!(op.metadata().action, "GetRunningAlarmProperties");
1126 }
1127
1128 #[test]
1131 fn test_service_constant() {
1132 assert_eq!(SERVICE, crate::Service::AVTransport);
1133 }
1134
1135 #[test]
1136 fn test_subscription_helpers() {
1137 let client = crate::SonosClient::new();
1138 let _subscribe_fn = || subscribe(&client, "192.168.1.100", "http://callback.url");
1139 let _subscribe_timeout_fn =
1140 || subscribe_with_timeout(&client, "192.168.1.100", "http://callback.url", 3600);
1141 }
1142}