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: &str) -> Result<Self::Response, crate::error::ApiError> {
480 use crate::operation::response_field;
481 Ok(AddURIToQueueResponse {
482 first_track_number_enqueued: response_field(xml, "FirstTrackNumberEnqueued"),
483 num_tracks_added: response_field(xml, "NumTracksAdded"),
484 new_queue_length: response_field(xml, "NewQueueLength"),
485 })
486 }
487}
488
489pub fn add_uri_to_queue_operation(
490 enqueued_uri: String,
491 enqueued_uri_meta_data: String,
492 desired_first_track_number_enqueued: u32,
493 enqueue_as_next: bool,
494) -> crate::operation::OperationBuilder<AddURIToQueueOperation> {
495 let request = AddURIToQueueOperationRequest {
496 instance_id: 0,
497 enqueued_uri,
498 enqueued_uri_meta_data,
499 desired_first_track_number_enqueued,
500 enqueue_as_next,
501 };
502 crate::operation::OperationBuilder::new(request)
503}
504
505define_upnp_operation! {
506 operation: RemoveTrackFromQueueOperation,
507 action: "RemoveTrackFromQueue",
508 service: AVTransport,
509 request: {
510 object_id: String,
511 update_id: u32,
512 },
513 response: (),
514 payload: |req| {
515 format!(
516 "<InstanceID>{}</InstanceID><ObjectID>{}</ObjectID><UpdateID>{}</UpdateID>",
517 req.instance_id,
518 crate::operation::xml_escape(&req.object_id),
519 req.update_id
520 )
521 },
522 parse: |_xml| Ok(()),
523}
524
525impl Validate for RemoveTrackFromQueueOperationRequest {}
526
527define_operation_with_response! {
528 operation: RemoveTrackRangeFromQueueOperation,
529 action: "RemoveTrackRangeFromQueue",
530 service: AVTransport,
531 request: {
532 update_id: u32,
533 starting_index: u32,
534 number_of_tracks: u32,
535 },
536 response: RemoveTrackRangeFromQueueResponse {
537 new_update_id: u32,
538 },
539 request_xml_mapping: {
540 update_id: "UpdateID",
541 starting_index: "StartingIndex",
542 number_of_tracks: "NumberOfTracks",
543 },
544 xml_mapping: {
545 new_update_id: "NewUpdateID",
546 },
547}
548
549impl Validate for RemoveTrackRangeFromQueueOperationRequest {}
550
551define_upnp_operation! {
552 operation: RemoveAllTracksFromQueueOperation,
553 action: "RemoveAllTracksFromQueue",
554 service: AVTransport,
555 request: {},
556 response: (),
557 payload: |req| format!("<InstanceID>{}</InstanceID>", req.instance_id),
558 parse: |_xml| Ok(()),
559}
560
561impl Validate for RemoveAllTracksFromQueueOperationRequest {}
562
563define_operation_with_response! {
564 operation: SaveQueueOperation,
565 action: "SaveQueue",
566 service: AVTransport,
567 request: {
568 title: String,
569 object_id: String,
570 },
571 response: SaveQueueResponse {
572 assigned_object_id: String,
573 },
574 request_xml_mapping: {
575 title: "Title",
576 object_id: "ObjectID",
577 },
578 xml_mapping: {
579 assigned_object_id: "AssignedObjectID",
580 },
581}
582
583impl Validate for SaveQueueOperationRequest {}
584
585define_operation_with_response! {
586 operation: CreateSavedQueueOperation,
587 action: "CreateSavedQueue",
588 service: AVTransport,
589 request: {
590 title: String,
591 enqueued_uri: String,
592 enqueued_uri_meta_data: String,
593 },
594 response: CreateSavedQueueResponse {
595 num_tracks_added: u32,
596 new_queue_length: u32,
597 assigned_object_id: String,
598 new_update_id: u32,
599 },
600 request_xml_mapping: {
601 title: "Title",
602 enqueued_uri: "EnqueuedURI",
603 enqueued_uri_meta_data: "EnqueuedURIMetaData",
604 },
605 xml_mapping: {
606 num_tracks_added: "NumTracksAdded",
607 new_queue_length: "NewQueueLength",
608 assigned_object_id: "AssignedObjectID",
609 new_update_id: "NewUpdateID",
610 },
611}
612
613impl Validate for CreateSavedQueueOperationRequest {}
614
615define_upnp_operation! {
616 operation: BackupQueueOperation,
617 action: "BackupQueue",
618 service: AVTransport,
619 request: {},
620 response: (),
621 payload: |req| format!("<InstanceID>{}</InstanceID>", req.instance_id),
622 parse: |_xml| Ok(()),
623}
624
625impl Validate for BackupQueueOperationRequest {}
626
627define_operation_with_response! {
632 operation: BecomeCoordinatorOfStandaloneGroupOperation,
633 action: "BecomeCoordinatorOfStandaloneGroup",
634 service: AVTransport,
635 request: {},
636 response: BecomeCoordinatorOfStandaloneGroupResponse {
637 delegated_group_coordinator_id: String,
638 new_group_id: String,
639 },
640 xml_mapping: {
641 delegated_group_coordinator_id: "DelegatedGroupCoordinatorID",
642 new_group_id: "NewGroupID",
643 },
644}
645
646impl Validate for BecomeCoordinatorOfStandaloneGroupOperationRequest {}
647
648define_upnp_operation! {
649 operation: DelegateGroupCoordinationToOperation,
650 action: "DelegateGroupCoordinationTo",
651 service: AVTransport,
652 request: {
653 new_coordinator: String,
654 rejoin_group: bool,
655 },
656 response: (),
657 payload: |req| {
658 format!(
659 "<InstanceID>{}</InstanceID><NewCoordinator>{}</NewCoordinator><RejoinGroup>{}</RejoinGroup>",
660 req.instance_id,
661 crate::operation::xml_escape(&req.new_coordinator),
662 if req.rejoin_group { "true" } else { "false" }
663 )
664 },
665 parse: |_xml| Ok(()),
666}
667
668impl Validate for DelegateGroupCoordinationToOperationRequest {}
669
670define_upnp_operation! {
675 operation: SnoozeAlarmOperation,
676 action: "SnoozeAlarm",
677 service: AVTransport,
678 request: {
679 duration: String,
680 },
681 response: (),
682 payload: |req| {
683 format!(
684 "<InstanceID>{}</InstanceID><Duration>{}</Duration>",
685 req.instance_id,
686 crate::operation::xml_escape(&req.duration)
687 )
688 },
689 parse: |_xml| Ok(()),
690}
691
692impl Validate for SnoozeAlarmOperationRequest {}
693
694define_operation_with_response! {
695 operation: GetRunningAlarmPropertiesOperation,
696 action: "GetRunningAlarmProperties",
697 service: AVTransport,
698 request: {},
699 response: GetRunningAlarmPropertiesResponse {
700 alarm_id: u32,
701 group_id: String,
702 logged_start_time: String,
703 },
704 xml_mapping: {
705 alarm_id: "AlarmID",
706 group_id: "GroupID",
707 logged_start_time: "LoggedStartTime",
708 },
709}
710
711impl Validate for GetRunningAlarmPropertiesOperationRequest {}
712
713pub use next_operation as next;
719pub use pause_operation as pause;
720pub use play_operation as play;
721pub use previous_operation as previous;
722pub use stop_operation as stop;
723
724pub use get_position_info_operation as get_position_info;
726pub use seek_operation as seek;
727
728pub use get_current_transport_actions_operation as get_current_transport_actions;
730pub use get_device_capabilities_operation as get_device_capabilities;
731pub use get_transport_info_operation as get_transport_info;
732pub use get_transport_settings_operation as get_transport_settings;
733
734pub use get_media_info_operation as get_media_info;
736pub use set_a_v_transport_u_r_i_operation as set_av_transport_uri;
737pub use set_next_a_v_transport_u_r_i_operation as set_next_av_transport_uri;
738
739pub use get_crossfade_mode_operation as get_crossfade_mode;
741pub use set_crossfade_mode_operation as set_crossfade_mode;
742pub use set_play_mode_operation as set_play_mode;
743
744pub use configure_sleep_timer_operation as configure_sleep_timer;
746pub use get_remaining_sleep_timer_duration_operation as get_remaining_sleep_timer_duration;
747
748pub use add_uri_to_queue_operation as add_uri_to_queue;
750pub use backup_queue_operation as backup_queue;
751pub use create_saved_queue_operation as create_saved_queue;
752pub use remove_all_tracks_from_queue_operation as remove_all_tracks_from_queue;
753pub use remove_track_from_queue_operation as remove_track_from_queue;
754pub use remove_track_range_from_queue_operation as remove_track_range_from_queue;
755pub use save_queue_operation as save_queue;
756
757pub use become_coordinator_of_standalone_group_operation as become_coordinator_of_standalone_group;
759pub use delegate_group_coordination_to_operation as delegate_group_coordination_to;
760
761pub use get_running_alarm_properties_operation as get_running_alarm_properties;
763pub use snooze_alarm_operation as snooze_alarm;
764
765pub const SERVICE: crate::Service = crate::Service::AVTransport;
771
772pub fn subscribe(
774 client: &crate::SonosClient,
775 ip: &str,
776 callback_url: &str,
777) -> crate::Result<crate::ManagedSubscription> {
778 client.subscribe(ip, SERVICE, callback_url)
779}
780
781pub fn subscribe_with_timeout(
783 client: &crate::SonosClient,
784 ip: &str,
785 callback_url: &str,
786 timeout_seconds: u32,
787) -> crate::Result<crate::ManagedSubscription> {
788 client.subscribe_with_timeout(ip, SERVICE, callback_url, timeout_seconds)
789}
790
791#[cfg(test)]
796mod tests {
797 use super::*;
798 use crate::operation::UPnPOperation;
799
800 #[test]
803 fn test_play_operation_builder() {
804 let op = play_operation("1".to_string()).build().unwrap();
805 assert_eq!(op.request().speed, "1");
806 assert_eq!(op.metadata().action, "Play");
807 }
808
809 #[test]
810 fn test_play_validation() {
811 let request = PlayOperationRequest {
812 instance_id: 0,
813 speed: "".to_string(),
814 };
815 assert!(request.validate_basic().is_err());
816
817 let request = PlayOperationRequest {
818 instance_id: 0,
819 speed: "1".to_string(),
820 };
821 assert!(request.validate_basic().is_ok());
822 }
823
824 #[test]
825 fn test_play_payload() {
826 let request = PlayOperationRequest {
827 instance_id: 0,
828 speed: "1".to_string(),
829 };
830 let payload = PlayOperation::build_payload(&request).unwrap();
831 assert!(payload.contains("<InstanceID>0</InstanceID>"));
832 assert!(payload.contains("<Speed>1</Speed>"));
833 }
834
835 #[test]
836 fn test_pause_operation_builder() {
837 let op = pause_operation().build().unwrap();
838 assert_eq!(op.metadata().action, "Pause");
839 }
840
841 #[test]
842 fn test_stop_operation_builder() {
843 let op = stop_operation().build().unwrap();
844 assert_eq!(op.metadata().action, "Stop");
845 }
846
847 #[test]
848 fn test_next_operation_builder() {
849 let op = next_operation().build().unwrap();
850 assert_eq!(op.metadata().action, "Next");
851 }
852
853 #[test]
854 fn test_previous_operation_builder() {
855 let op = previous_operation().build().unwrap();
856 assert_eq!(op.metadata().action, "Previous");
857 }
858
859 #[test]
862 fn test_seek_operation_builder() {
863 let op = seek_operation("TRACK_NR".to_string(), "5".to_string())
864 .build()
865 .unwrap();
866 assert_eq!(op.request().unit, "TRACK_NR");
867 assert_eq!(op.request().target, "5");
868 assert_eq!(op.metadata().action, "Seek");
869 }
870
871 #[test]
872 fn test_seek_validation() {
873 let request = SeekOperationRequest {
874 instance_id: 0,
875 unit: "INVALID".to_string(),
876 target: "5".to_string(),
877 };
878 assert!(request.validate_basic().is_err());
879
880 let request = SeekOperationRequest {
881 instance_id: 0,
882 unit: "REL_TIME".to_string(),
883 target: "0:01:30".to_string(),
884 };
885 assert!(request.validate_basic().is_ok());
886 }
887
888 #[test]
889 fn test_seek_payload() {
890 let request = SeekOperationRequest {
891 instance_id: 0,
892 unit: "TRACK_NR".to_string(),
893 target: "3".to_string(),
894 };
895 let payload = SeekOperation::build_payload(&request).unwrap();
896 assert!(payload.contains("<Unit>TRACK_NR</Unit>"));
897 assert!(payload.contains("<Target>3</Target>"));
898 }
899
900 #[test]
903 fn test_get_transport_info_builder() {
904 let op = get_transport_info_operation().build().unwrap();
905 assert_eq!(op.metadata().action, "GetTransportInfo");
906 }
907
908 #[test]
909 fn test_get_position_info_builder() {
910 let op = get_position_info_operation().build().unwrap();
911 assert_eq!(op.metadata().action, "GetPositionInfo");
912 }
913
914 #[test]
915 fn test_get_media_info_builder() {
916 let op = get_media_info_operation().build().unwrap();
917 assert_eq!(op.metadata().action, "GetMediaInfo");
918 }
919
920 #[test]
921 fn test_get_transport_settings_builder() {
922 let op = get_transport_settings_operation().build().unwrap();
923 assert_eq!(op.metadata().action, "GetTransportSettings");
924 }
925
926 #[test]
929 fn test_get_crossfade_mode_builder() {
930 let op = get_crossfade_mode_operation().build().unwrap();
931 assert_eq!(op.metadata().action, "GetCrossfadeMode");
932 }
933
934 #[test]
935 fn test_set_crossfade_mode_builder() {
936 let op = set_crossfade_mode_operation(true).build().unwrap();
937 assert!(op.request().crossfade_mode);
938 assert_eq!(op.metadata().action, "SetCrossfadeMode");
939 }
940
941 #[test]
942 fn test_set_crossfade_mode_payload() {
943 let request = SetCrossfadeModeOperationRequest {
944 instance_id: 0,
945 crossfade_mode: true,
946 };
947 let payload = SetCrossfadeModeOperation::build_payload(&request).unwrap();
948 assert!(payload.contains("<CrossfadeMode>1</CrossfadeMode>"));
949
950 let request = SetCrossfadeModeOperationRequest {
951 instance_id: 0,
952 crossfade_mode: false,
953 };
954 let payload = SetCrossfadeModeOperation::build_payload(&request).unwrap();
955 assert!(payload.contains("<CrossfadeMode>0</CrossfadeMode>"));
956 }
957
958 #[test]
959 fn test_set_play_mode_builder() {
960 let op = set_play_mode_operation("SHUFFLE".to_string())
961 .build()
962 .unwrap();
963 assert_eq!(op.request().new_play_mode, "SHUFFLE");
964 assert_eq!(op.metadata().action, "SetPlayMode");
965 }
966
967 #[test]
968 fn test_set_play_mode_validation() {
969 let request = SetPlayModeOperationRequest {
970 instance_id: 0,
971 new_play_mode: "INVALID".to_string(),
972 };
973 assert!(request.validate_basic().is_err());
974
975 let request = SetPlayModeOperationRequest {
976 instance_id: 0,
977 new_play_mode: "REPEAT_ALL".to_string(),
978 };
979 assert!(request.validate_basic().is_ok());
980 }
981
982 #[test]
985 fn test_configure_sleep_timer_builder() {
986 let op = configure_sleep_timer_operation("0:30:00".to_string())
987 .build()
988 .unwrap();
989 assert_eq!(op.request().new_sleep_timer_duration, "0:30:00");
990 assert_eq!(op.metadata().action, "ConfigureSleepTimer");
991 }
992
993 #[test]
994 fn test_get_remaining_sleep_timer_duration_builder() {
995 let op = get_remaining_sleep_timer_duration_operation()
996 .build()
997 .unwrap();
998 assert_eq!(op.metadata().action, "GetRemainingSleepTimerDuration");
999 }
1000
1001 #[test]
1004 fn test_remove_all_tracks_from_queue_builder() {
1005 let op = remove_all_tracks_from_queue_operation().build().unwrap();
1006 assert_eq!(op.metadata().action, "RemoveAllTracksFromQueue");
1007 }
1008
1009 #[test]
1010 fn test_backup_queue_builder() {
1011 let op = backup_queue_operation().build().unwrap();
1012 assert_eq!(op.metadata().action, "BackupQueue");
1013 }
1014
1015 #[test]
1020 fn test_save_queue_payload_element_names() {
1021 let request = SaveQueueOperationRequest {
1022 instance_id: 0,
1023 title: "My Mix".to_string(),
1024 object_id: "SQ:12".to_string(),
1025 };
1026 let payload = SaveQueueOperation::build_payload(&request).unwrap();
1027 assert!(payload.contains("<Title>My Mix</Title>"), "{payload}");
1028 assert!(payload.contains("<ObjectID>SQ:12</ObjectID>"), "{payload}");
1029 }
1030
1031 #[test]
1032 fn test_create_saved_queue_payload_element_names() {
1033 let request = CreateSavedQueueOperationRequest {
1034 instance_id: 0,
1035 title: "Playlist".to_string(),
1036 enqueued_uri: "x-file-cifs://nas/song.mp3".to_string(),
1037 enqueued_uri_meta_data: "meta".to_string(),
1038 };
1039 let payload = CreateSavedQueueOperation::build_payload(&request).unwrap();
1040 assert!(
1041 payload.contains("<EnqueuedURI>x-file-cifs://nas/song.mp3</EnqueuedURI>"),
1042 "{payload}"
1043 );
1044 assert!(
1045 payload.contains("<EnqueuedURIMetaData>meta</EnqueuedURIMetaData>"),
1046 "{payload}"
1047 );
1048 }
1049
1050 #[test]
1051 fn test_remove_track_range_payload_element_names() {
1052 let request = RemoveTrackRangeFromQueueOperationRequest {
1053 instance_id: 0,
1054 update_id: 7,
1055 starting_index: 2,
1056 number_of_tracks: 3,
1057 };
1058 let payload = RemoveTrackRangeFromQueueOperation::build_payload(&request).unwrap();
1059 assert!(payload.contains("<UpdateID>7</UpdateID>"), "{payload}");
1060 assert!(
1061 payload.contains("<StartingIndex>2</StartingIndex>"),
1062 "{payload}"
1063 );
1064 assert!(
1065 payload.contains("<NumberOfTracks>3</NumberOfTracks>"),
1066 "{payload}"
1067 );
1068 }
1069
1070 #[test]
1071 fn test_add_uri_to_queue_escapes_ampersand() {
1072 let request = AddURIToQueueOperationRequest {
1074 instance_id: 0,
1075 enqueued_uri: "x-sonosapi-stream:s1?sid=254&flags=32".to_string(),
1076 enqueued_uri_meta_data: "<DIDL-Lite>x</DIDL-Lite>".to_string(),
1077 desired_first_track_number_enqueued: 0,
1078 enqueue_as_next: false,
1079 };
1080 let payload = AddURIToQueueOperation::build_payload(&request).unwrap();
1081 assert!(payload.contains("sid=254&flags=32"), "{payload}");
1082 assert!(!payload.contains("sid=254&flags=32"), "{payload}");
1083 assert!(
1085 payload.contains("<DIDL-Lite>x</DIDL-Lite>"),
1086 "{payload}"
1087 );
1088 }
1089
1090 #[test]
1093 fn test_become_coordinator_of_standalone_group_builder() {
1094 let op = become_coordinator_of_standalone_group_operation()
1095 .build()
1096 .unwrap();
1097 assert_eq!(op.metadata().action, "BecomeCoordinatorOfStandaloneGroup");
1098 }
1099
1100 #[test]
1103 fn test_snooze_alarm_builder() {
1104 let op = snooze_alarm_operation("0:10:00".to_string())
1105 .build()
1106 .unwrap();
1107 assert_eq!(op.request().duration, "0:10:00");
1108 assert_eq!(op.metadata().action, "SnoozeAlarm");
1109 }
1110
1111 #[test]
1112 fn test_get_running_alarm_properties_builder() {
1113 let op = get_running_alarm_properties_operation().build().unwrap();
1114 assert_eq!(op.metadata().action, "GetRunningAlarmProperties");
1115 }
1116
1117 #[test]
1120 fn test_service_constant() {
1121 assert_eq!(SERVICE, crate::Service::AVTransport);
1122 }
1123
1124 #[test]
1125 fn test_subscription_helpers() {
1126 let client = crate::SonosClient::new();
1127 let _subscribe_fn = || subscribe(&client, "192.168.1.100", "http://callback.url");
1128 let _subscribe_timeout_fn =
1129 || subscribe_with_timeout(&client, "192.168.1.100", "http://callback.url", 3600);
1130 }
1131}