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