1use std::io::IsTerminal;
2
3use sonos_sdk::{SeekTarget, SonosSystem};
4
5use super::{
6 format_duration_human, format_time_ms, parse_duration, playback_icon, playback_label,
7 require_speaker_only, resolve_group, resolve_speaker, validate_seek_time, Commands,
8 GlobalFlags, OnOff, QueueAction,
9};
10use crate::config::Config;
11use crate::diagnostics;
12use crate::errors::CliError;
13
14pub fn run_command(
15 cmd: Commands,
16 system: &SonosSystem,
17 config: &Config,
18 global: &GlobalFlags,
19) -> Result<String, CliError> {
20 let spk = || resolve_speaker(system, config, global);
21
22 match cmd {
23 Commands::Speakers => cmd_speakers(system),
24 Commands::Groups => cmd_groups(system),
25 Commands::Status => cmd_status(system, config, global),
26 Commands::Join => cmd_join(system, global),
27 Commands::Leave => cmd_leave(system, global),
28 Commands::Bass { level } => cmd_bass(system, global, level),
29 Commands::Treble { level } => cmd_treble(system, global, level),
30 Commands::Loudness { state } => cmd_loudness(system, global, state),
31 Commands::Sleep { duration } => cmd_sleep(system, config, global, &duration),
32 Commands::Queue { action } => cmd_queue(system, config, global, action),
33
34 Commands::Play => {
35 let s = spk()?;
36 s.play()?;
37 Ok(format!("Playing ({})", s.name))
38 }
39 Commands::Pause => {
40 let s = spk()?;
41 s.pause()?;
42 Ok(format!("Paused ({})", s.name))
43 }
44 Commands::Stop => {
45 let s = spk()?;
46 s.stop()?;
47 Ok(format!("Stopped ({})", s.name))
48 }
49 Commands::Next => {
50 let s = spk()?;
51 s.next()?;
52 Ok(format!("Next track ({})", s.name))
53 }
54 Commands::Previous => {
55 let s = spk()?;
56 s.previous()?;
57 Ok(format!("Previous track ({})", s.name))
58 }
59 Commands::Seek { position } => {
60 validate_seek_time(&position)?;
61 let s = spk()?;
62 s.seek(SeekTarget::Time(position.clone()))?;
63 Ok(format!("Seeked to {} ({})", position, s.name))
64 }
65 Commands::Mode { mode } => {
66 let s = spk()?;
67 s.set_play_mode(mode.to_sdk())?;
68 Ok(format!("Mode set to {:?} ({})", mode, s.name))
69 }
70 Commands::Volume { level } => cmd_volume(system, config, global, level),
71 Commands::Mute => cmd_mute(system, config, global, true),
72 Commands::Unmute => cmd_mute(system, config, global, false),
73 }
74}
75
76fn cmd_speakers(system: &SonosSystem) -> Result<String, CliError> {
79 let speakers = system.speakers();
80 if speakers.is_empty() {
81 eprintln!("{}", diagnostics::discovery_hint());
82 return Ok("No speakers found".to_string());
83 }
84 let lines: Vec<String> = speakers
85 .iter()
86 .map(|s| {
87 let state = s.playback_state.fetch().ok();
88 let vol = s.volume.fetch().ok();
89 let group_name = s
90 .group()
91 .and_then(|g| g.coordinator().map(|c| c.name))
92 .unwrap_or_default();
93
94 let state_str = state
95 .as_ref()
96 .map(|st| format!("{} {}", playback_icon(st), playback_label(st)))
97 .unwrap_or_default();
98 let vol_str = vol.map(|v| format!("vol:{}", v.0)).unwrap_or_default();
99
100 let mut parts = vec![s.name.clone()];
101 if !state_str.is_empty() {
102 parts.push(state_str);
103 }
104 if !vol_str.is_empty() {
105 parts.push(vol_str);
106 }
107 if !group_name.is_empty() {
108 parts.push(format!("({group_name})"));
109 }
110 parts.join(" ")
111 })
112 .collect();
113 Ok(lines.join("\n"))
114}
115
116fn cmd_groups(system: &SonosSystem) -> Result<String, CliError> {
117 let groups = system.groups();
118 if groups.is_empty() {
119 eprintln!("{}", diagnostics::discovery_hint());
120 return Ok("No groups found".to_string());
121 }
122 let lines: Vec<String> = groups
123 .iter()
124 .map(|g| {
125 let coord = g.coordinator();
126 let coord_name = coord
127 .as_ref()
128 .map(|c| c.name.clone())
129 .unwrap_or_else(|| "unknown".to_string());
130
131 let state = coord.as_ref().and_then(|c| c.playback_state.fetch().ok());
132 let track = coord.as_ref().and_then(|c| c.current_track.fetch().ok());
133 let vol = g.volume.fetch().ok();
134
135 let state_str = state
136 .as_ref()
137 .map(|st| format!("{} {}", playback_icon(st), playback_label(st)))
138 .unwrap_or_default();
139 let track_str = track.as_ref().map(|t| t.display()).unwrap_or_default();
140 let vol_str = vol.map(|v| format!("vol:{}", v.0)).unwrap_or_default();
141
142 let mut parts = vec![coord_name];
143 if !state_str.is_empty() {
144 parts.push(state_str);
145 }
146 if !track_str.is_empty() {
147 parts.push(track_str);
148 }
149 if !vol_str.is_empty() {
150 parts.push(vol_str);
151 }
152 parts.join(" ")
153 })
154 .collect();
155 Ok(lines.join("\n"))
156}
157
158fn cmd_volume(
159 system: &SonosSystem,
160 config: &Config,
161 global: &GlobalFlags,
162 level: u8,
163) -> Result<String, CliError> {
164 if global.speaker.is_some() && global.group.is_none() {
166 let s = resolve_speaker(system, config, global)?;
167 s.set_volume(level)?;
168 return Ok(format!("Volume set to {} ({})", level, s.name));
169 }
170 let g = resolve_group(system, config, global)?;
172 let name = g
173 .coordinator()
174 .map(|c| c.name)
175 .unwrap_or_else(|| "unknown".to_string());
176 g.set_volume(level as u16)?;
177 Ok(format!("Volume set to {level} ({name})"))
178}
179
180fn cmd_mute(
181 system: &SonosSystem,
182 config: &Config,
183 global: &GlobalFlags,
184 muted: bool,
185) -> Result<String, CliError> {
186 let label = if muted { "Muted" } else { "Unmuted" };
187 if global.speaker.is_some() && global.group.is_none() {
189 let s = resolve_speaker(system, config, global)?;
190 s.set_mute(muted)?;
191 return Ok(format!("{} ({})", label, s.name));
192 }
193 let g = resolve_group(system, config, global)?;
195 let name = g
196 .coordinator()
197 .map(|c| c.name)
198 .unwrap_or_else(|| "unknown".to_string());
199 g.set_mute(muted)?;
200 Ok(format!("{label} ({name})"))
201}
202
203fn cmd_status(
204 system: &SonosSystem,
205 config: &Config,
206 global: &GlobalFlags,
207) -> Result<String, CliError> {
208 let spk = resolve_speaker(system, config, global)?;
209 let state = spk.playback_state.fetch().ok();
210 let track = spk.current_track.fetch().ok();
211 let pos = spk.position.fetch().ok();
212 let vol = spk.volume.fetch().ok();
213
214 let state_str = state
215 .as_ref()
216 .map(|st| format!("{} {}", playback_icon(st), playback_label(st)))
217 .unwrap_or_else(|| "unknown".to_string());
218 let track_str = track.as_ref().map(|t| t.display()).unwrap_or_default();
219 let pos_str = pos
220 .as_ref()
221 .map(|p| {
222 format!(
223 "{}/{}",
224 format_time_ms(p.position_ms),
225 format_time_ms(p.duration_ms)
226 )
227 })
228 .unwrap_or_default();
229 let vol_str = vol.map(|v| format!("vol:{}", v.0)).unwrap_or_default();
230
231 let mut parts = vec![spk.name.clone(), state_str];
232 if !track_str.is_empty() {
233 parts.push(track_str);
234 }
235 if !pos_str.is_empty() {
236 parts.push(pos_str);
237 }
238 if !vol_str.is_empty() {
239 parts.push(vol_str);
240 }
241 Ok(parts.join(" "))
242}
243
244fn cmd_join(system: &SonosSystem, global: &GlobalFlags) -> Result<String, CliError> {
245 let speaker_name = global
246 .speaker
247 .as_deref()
248 .ok_or_else(|| CliError::Validation("--speaker is required for join".into()))?;
249 let group_name = global
250 .group
251 .as_deref()
252 .ok_or_else(|| CliError::Validation("--group is required for join".into()))?;
253 let spk = system
254 .speaker(speaker_name)
255 .ok_or_else(|| CliError::SpeakerNotFound(speaker_name.into()))?;
256 let grp = system
257 .group(group_name)
258 .ok_or_else(|| CliError::GroupNotFound(group_name.into()))?;
259 grp.add_speaker(&spk)?;
260 Ok(format!("{speaker_name} joined {group_name}"))
261}
262
263fn cmd_leave(system: &SonosSystem, global: &GlobalFlags) -> Result<String, CliError> {
264 let speaker_name = global
265 .speaker
266 .as_deref()
267 .ok_or_else(|| CliError::Validation("--speaker is required for leave".into()))?;
268 let spk = system
269 .speaker(speaker_name)
270 .ok_or_else(|| CliError::SpeakerNotFound(speaker_name.into()))?;
271 let group_name = spk
272 .group()
273 .and_then(|g| g.coordinator().map(|c| c.name))
274 .unwrap_or_else(|| "its group".into());
275 spk.leave_group()?;
276 Ok(format!("{speaker_name} left {group_name}"))
277}
278
279fn cmd_bass(system: &SonosSystem, global: &GlobalFlags, level: i8) -> Result<String, CliError> {
280 let spk = require_speaker_only(system, global, "bass")?;
281 spk.set_bass(level)?;
282 Ok(format!("Bass set to {} ({})", level, spk.name))
283}
284
285fn cmd_treble(system: &SonosSystem, global: &GlobalFlags, level: i8) -> Result<String, CliError> {
286 let spk = require_speaker_only(system, global, "treble")?;
287 spk.set_treble(level)?;
288 Ok(format!("Treble set to {} ({})", level, spk.name))
289}
290
291fn cmd_loudness(
292 system: &SonosSystem,
293 global: &GlobalFlags,
294 state: OnOff,
295) -> Result<String, CliError> {
296 let spk = require_speaker_only(system, global, "loudness")?;
297 let enabled = matches!(state, OnOff::On);
298 spk.set_loudness(enabled)?;
299 if enabled {
300 Ok(format!("Loudness enabled ({})", spk.name))
301 } else {
302 Ok(format!("Loudness disabled ({})", spk.name))
303 }
304}
305
306fn cmd_sleep(
307 system: &SonosSystem,
308 config: &Config,
309 global: &GlobalFlags,
310 duration: &str,
311) -> Result<String, CliError> {
312 let spk = resolve_speaker(system, config, global)?;
313 if duration == "cancel" {
314 spk.cancel_sleep_timer()?;
315 Ok(format!("Sleep timer cancelled ({})", spk.name))
316 } else {
317 let hh_mm_ss = parse_duration(duration)?;
318 let human = format_duration_human(duration);
319 spk.configure_sleep_timer(&hh_mm_ss)?;
320 Ok(format!("Sleep timer set for {} ({})", human, spk.name))
321 }
322}
323
324fn cmd_queue(
325 system: &SonosSystem,
326 config: &Config,
327 global: &GlobalFlags,
328 action: Option<QueueAction>,
329) -> Result<String, CliError> {
330 let spk = resolve_speaker(system, config, global)?;
331 match action {
332 None => {
333 let info = spk.get_media_info()?;
334 if info.nr_tracks == 0 {
335 return Ok(format!("queue is empty ({})", spk.name));
336 }
337 Ok(format!("{} — {} tracks", spk.name, info.nr_tracks))
338 }
339 Some(QueueAction::Add { uri }) => {
340 spk.add_uri_to_queue(&uri, "", 0, false)?;
341 Ok(format!("Added to queue ({})", spk.name))
342 }
343 Some(QueueAction::Clear) => {
344 if std::io::stdin().is_terminal() && !global.no_input {
345 eprint!("Clear queue for {}? [y/N] ", spk.name);
346 let mut input = String::new();
347 std::io::stdin()
348 .read_line(&mut input)
349 .map_err(|e| CliError::Validation(e.to_string()))?;
350 if !input.trim().eq_ignore_ascii_case("y") {
351 return Ok("Cancelled".into());
352 }
353 }
354 spk.remove_all_tracks_from_queue()?;
355 Ok(format!("Queue cleared ({})", spk.name))
356 }
357 }
358}