1use sonos_sdk::{Group, SonosSystem, Speaker};
2
3use crate::cli::GlobalFlags;
4use crate::config::Config;
5use crate::errors::CliError;
6
7pub fn resolve_speaker(
12 system: &SonosSystem,
13 config: &Config,
14 global: &GlobalFlags,
15) -> Result<Speaker, CliError> {
16 if let Some(group_name) = &global.group {
18 let g = system
19 .group(group_name)
20 .ok_or_else(|| CliError::GroupNotFound(group_name.to_string()))?;
21 return g
22 .coordinator()
23 .ok_or_else(|| CliError::GroupNotFound(group_name.to_string()));
24 }
25
26 if let Some(speaker_name) = &global.speaker {
27 return system
28 .speaker(speaker_name)
29 .ok_or_else(|| CliError::SpeakerNotFound(speaker_name.to_string()));
30 }
31
32 if let Some(default_group) = &config.default_group {
34 if let Some(g) = system.group(default_group) {
35 if let Some(coordinator) = g.coordinator() {
36 return Ok(coordinator);
37 }
38 }
39 }
40
41 if let Some(coordinator) = system
44 .groups()
45 .into_iter()
46 .next()
47 .and_then(|g| g.coordinator())
48 {
49 return Ok(coordinator);
50 }
51
52 system
54 .speakers()
55 .into_iter()
56 .next()
57 .ok_or_else(|| CliError::SpeakerNotFound("no speakers available".to_string()))
58}
59
60pub fn resolve_group(
65 system: &SonosSystem,
66 config: &Config,
67 global: &GlobalFlags,
68) -> Result<Group, CliError> {
69 if let Some(group_name) = &global.group {
70 return system
71 .group(group_name)
72 .ok_or_else(|| CliError::GroupNotFound(group_name.to_string()));
73 }
74
75 if let Some(default_group) = &config.default_group {
77 if let Some(g) = system.group(default_group) {
78 return Ok(g);
79 }
80 }
81
82 system
83 .groups()
84 .into_iter()
85 .next()
86 .ok_or_else(|| CliError::GroupNotFound("no groups available".to_string()))
87}
88
89pub fn require_speaker_only(
92 system: &SonosSystem,
93 global: &GlobalFlags,
94 command_name: &str,
95) -> Result<Speaker, CliError> {
96 if global.group.is_some() {
97 return Err(CliError::Validation(format!(
98 "--speaker is required for {command_name}"
99 )));
100 }
101 let name = global
102 .speaker
103 .as_deref()
104 .ok_or_else(|| CliError::Validation(format!("--speaker is required for {command_name}")))?;
105 system
106 .speaker(name)
107 .ok_or_else(|| CliError::SpeakerNotFound(name.to_string()))
108}
109
110#[cfg(all(test, feature = "test-helpers"))]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn resolve_speaker_by_name() {
116 let system = SonosSystem::with_speakers(&["Kitchen"]);
117 let config = Config::default();
118 let global = GlobalFlags {
119 speaker: Some("Kitchen".into()),
120 group: None,
121 quiet: false,
122 verbose: 0,
123 no_input: false,
124 };
125 let spk = resolve_speaker(&system, &config, &global).unwrap();
126 assert_eq!(spk.name, "Kitchen");
127 }
128
129 #[test]
130 fn resolve_speaker_not_found() {
131 let system = SonosSystem::with_speakers(&["Kitchen"]);
132 let config = Config::default();
133 let global = GlobalFlags {
134 speaker: Some("Nonexistent".into()),
135 group: None,
136 quiet: false,
137 verbose: 0,
138 no_input: false,
139 };
140 let result = resolve_speaker(&system, &config, &global);
141 assert!(matches!(result, Err(CliError::SpeakerNotFound(_))));
142 }
143
144 #[test]
145 fn resolve_speaker_falls_back_to_first() {
146 let system = SonosSystem::with_speakers(&["Kitchen"]);
147 let config = Config::default();
148 let global = GlobalFlags {
149 speaker: None,
150 group: None,
151 quiet: false,
152 verbose: 0,
153 no_input: false,
154 };
155 let spk = resolve_speaker(&system, &config, &global).unwrap();
156 assert_eq!(spk.name, "Kitchen");
157 }
158
159 #[test]
160 fn resolve_speaker_prefers_group_coordinator() {
161 let system = SonosSystem::with_groups(&["Kitchen", "Bedroom"]);
162 let config = Config::default();
163 let global = GlobalFlags {
164 speaker: None,
165 group: None,
166 quiet: false,
167 verbose: 0,
168 no_input: false,
169 };
170 let spk = resolve_speaker(&system, &config, &global).unwrap();
171 let first_group = system.groups().into_iter().next().unwrap();
173 let expected_coordinator = first_group.coordinator().unwrap();
174 assert_eq!(spk.name, expected_coordinator.name);
175 }
176
177 #[test]
178 fn resolve_speaker_empty_system_fails() {
179 let system = SonosSystem::with_speakers(&[]);
180 let config = Config::default();
181 let global = GlobalFlags {
182 speaker: None,
183 group: None,
184 quiet: false,
185 verbose: 0,
186 no_input: false,
187 };
188 let result = resolve_speaker(&system, &config, &global);
189 assert!(result.is_err());
190 }
191
192 #[test]
193 fn require_speaker_only_rejects_group() {
194 let system = SonosSystem::with_speakers(&["Kitchen"]);
195 let global = GlobalFlags {
196 speaker: None,
197 group: Some("Living Room".into()),
198 quiet: false,
199 verbose: 0,
200 no_input: false,
201 };
202 let result = require_speaker_only(&system, &global, "bass");
203 assert!(matches!(result, Err(CliError::Validation(_))));
204 }
205
206 #[test]
207 fn require_speaker_only_requires_speaker_flag() {
208 let system = SonosSystem::with_speakers(&["Kitchen"]);
209 let global = GlobalFlags {
210 speaker: None,
211 group: None,
212 quiet: false,
213 verbose: 0,
214 no_input: false,
215 };
216 let result = require_speaker_only(&system, &global, "bass");
217 assert!(matches!(result, Err(CliError::Validation(_))));
218 }
219
220 #[test]
221 fn require_speaker_only_finds_speaker() {
222 let system = SonosSystem::with_speakers(&["Kitchen"]);
223 let global = GlobalFlags {
224 speaker: Some("Kitchen".into()),
225 group: None,
226 quiet: false,
227 verbose: 0,
228 no_input: false,
229 };
230 let spk = require_speaker_only(&system, &global, "bass").unwrap();
231 assert_eq!(spk.name, "Kitchen");
232 }
233
234 #[test]
235 fn resolve_group_by_name() {
236 let system = SonosSystem::with_groups(&["Kitchen", "Bedroom"]);
237 let config = Config::default();
238 let global = GlobalFlags {
239 speaker: None,
240 group: Some("Kitchen".into()),
241 quiet: false,
242 verbose: 0,
243 no_input: false,
244 };
245 let grp = resolve_group(&system, &config, &global).unwrap();
246 let coord = grp.coordinator().unwrap();
247 assert_eq!(coord.name, "Kitchen");
248 }
249
250 #[test]
251 fn resolve_group_not_found() {
252 let system = SonosSystem::with_groups(&["Kitchen"]);
253 let config = Config::default();
254 let global = GlobalFlags {
255 speaker: None,
256 group: Some("Nonexistent".into()),
257 quiet: false,
258 verbose: 0,
259 no_input: false,
260 };
261 let result = resolve_group(&system, &config, &global);
262 assert!(matches!(result, Err(CliError::GroupNotFound(_))));
263 }
264
265 #[test]
266 fn resolve_group_falls_back_to_first() {
267 let system = SonosSystem::with_groups(&["Kitchen"]);
268 let config = Config::default();
269 let global = GlobalFlags {
270 speaker: None,
271 group: None,
272 quiet: false,
273 verbose: 0,
274 no_input: false,
275 };
276 let grp = resolve_group(&system, &config, &global).unwrap();
277 let coord = grp.coordinator().unwrap();
278 assert_eq!(coord.name, "Kitchen");
279 }
280
281 #[test]
282 fn resolve_group_uses_config_default() {
283 let system = SonosSystem::with_groups(&["Kitchen", "Bedroom"]);
284 let config = Config {
285 default_group: Some("Bedroom".into()),
286 ..Config::default()
287 };
288 let global = GlobalFlags {
289 speaker: None,
290 group: None,
291 quiet: false,
292 verbose: 0,
293 no_input: false,
294 };
295 let grp = resolve_group(&system, &config, &global).unwrap();
296 let coord = grp.coordinator().unwrap();
297 assert_eq!(coord.name, "Bedroom");
298 }
299
300 #[test]
301 fn resolve_group_empty_system_fails() {
302 let system = SonosSystem::with_groups(&[]);
303 let config = Config::default();
304 let global = GlobalFlags {
305 speaker: None,
306 group: None,
307 quiet: false,
308 verbose: 0,
309 no_input: false,
310 };
311 let result = resolve_group(&system, &config, &global);
312 assert!(matches!(result, Err(CliError::GroupNotFound(_))));
313 }
314
315 #[test]
316 fn resolve_group_flag_wins_over_speaker() {
317 let system = SonosSystem::with_groups(&["Kitchen", "Bedroom"]);
318 let config = Config::default();
319 let global = GlobalFlags {
320 speaker: Some("Bedroom".into()),
321 group: Some("Kitchen".into()),
322 quiet: false,
323 verbose: 0,
324 no_input: false,
325 };
326 let grp = resolve_group(&system, &config, &global).unwrap();
327 let coord = grp.coordinator().unwrap();
328 assert_eq!(coord.name, "Kitchen");
329 }
330}