1#![allow(clippy::module_name_repetitions)]
3
4use std::{fmt::Display, ops::Range, time::Duration};
5
6use mecomp_storage::db::schemas::song::SongBrief;
7use one_or_many::OneOrMany;
8
9use crate::{
10 format_duration,
11 state::{RepeatMode, SeekType, StateAudio},
12};
13
14#[derive(Debug)]
16pub enum AudioCommand {
17 Play,
18 Pause,
19 Stop,
20 TogglePlayback,
21 RestartSong,
22 ClearPlayer,
24 Queue(QueueCommand),
26 Exit,
28 ReportStatus(tokio::sync::oneshot::Sender<StateAudio>),
30 Volume(VolumeCommand),
32 Seek(SeekType, Duration),
34}
35
36impl PartialEq for AudioCommand {
37 #[inline]
38 fn eq(&self, other: &Self) -> bool {
39 match (self, other) {
40 (Self::Play, Self::Play)
41 | (Self::Pause, Self::Pause)
42 | (Self::TogglePlayback, Self::TogglePlayback)
43 | (Self::ClearPlayer, Self::ClearPlayer)
44 | (Self::RestartSong, Self::RestartSong)
45 | (Self::Exit, Self::Exit)
46 | (Self::Stop, Self::Stop)
47 | (Self::ReportStatus(_), Self::ReportStatus(_)) => true,
48 (Self::Queue(a), Self::Queue(b)) => a == b,
49 (Self::Volume(a), Self::Volume(b)) => a == b,
50 (Self::Seek(a, b), Self::Seek(c, d)) => a == c && b == d,
51 #[cfg(not(tarpaulin_include))]
52 _ => false,
53 }
54 }
55}
56
57impl Display for AudioCommand {
58 #[inline]
59 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 match self {
61 Self::Play => write!(f, "Play"),
62 Self::Pause => write!(f, "Pause"),
63 Self::Stop => write!(f, "Stop"),
64 Self::TogglePlayback => write!(f, "Toggle Playback"),
65 Self::RestartSong => write!(f, "Restart Song"),
66 Self::ClearPlayer => write!(f, "Clear Player"),
67 Self::Queue(command) => write!(f, "Queue: {command}"),
68 Self::Exit => write!(f, "Exit"),
69 Self::ReportStatus(_) => write!(f, "Report Status"),
70 Self::Volume(command) => write!(f, "Volume: {command}"),
71 Self::Seek(seek_type, duration) => {
72 write!(
73 f,
74 "Seek: {seek_type} {} (HH:MM:SS)",
75 format_duration(duration)
76 )
77 }
78 }
79 }
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
84pub enum QueueCommand {
85 PlayNextSong,
88 SkipForward(usize),
90 SkipBackward(usize),
92 SetPosition(usize),
94 Shuffle,
96 AddToQueue(OneOrMany<SongBrief>),
98 RemoveRange(Range<usize>),
100 Clear,
102 SetRepeatMode(RepeatMode),
104}
105
106impl Display for QueueCommand {
107 #[inline]
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 match self {
110 Self::SkipForward(n) => write!(f, "Skip Forward by {n}"),
111 Self::SkipBackward(n) => write!(f, "Skip Backward by {n}"),
112 Self::SetPosition(n) => write!(f, "Set Position to {n}"),
113 Self::Shuffle => write!(f, "Shuffle"),
114 Self::AddToQueue(OneOrMany::None) => write!(f, "Add nothing"),
115 Self::AddToQueue(OneOrMany::One(song)) => {
116 write!(f, "Add \"{}\"", song.title)
117 }
118 Self::AddToQueue(OneOrMany::Many(songs)) => {
119 write!(
120 f,
121 "Add {:?}",
122 songs
123 .iter()
124 .map(|song| song.title.to_string())
125 .collect::<Vec<_>>()
126 )
127 }
128 Self::RemoveRange(range) => {
129 write!(f, "Remove items {}..{}", range.start, range.end)
130 }
131 Self::Clear => write!(f, "Clear"),
132 Self::SetRepeatMode(mode) => {
133 write!(f, "Set Repeat Mode to {mode}")
134 }
135 Self::PlayNextSong => write!(f, "Play Next Song"),
136 }
137 }
138}
139
140#[derive(Debug, Copy, Clone, PartialEq)]
142pub enum VolumeCommand {
143 Up(f32),
144 Down(f32),
145 Set(f32),
146 Mute,
147 Unmute,
148 ToggleMute,
149}
150
151impl Display for VolumeCommand {
152 #[inline]
153 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154 match self {
155 Self::Up(percent) => write!(f, "+{percent:.0}%", percent = percent * 100.0),
156 Self::Down(percent) => write!(f, "-{percent:.0}%", percent = percent * 100.0),
157 Self::Set(percent) => write!(f, "={percent:.0}%", percent = percent * 100.0),
158 Self::Mute => write!(f, "Mute"),
159 Self::Unmute => write!(f, "Unmute"),
160 Self::ToggleMute => write!(f, "Toggle Mute"),
161 }
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use mecomp_storage::db::schemas::song::Song;
168 use pretty_assertions::assert_str_eq;
169 use rstest::rstest;
170 use std::time::Duration;
171
172 use super::*;
173
174 #[rstest]
175 #[case(AudioCommand::Play, AudioCommand::Play, true)]
176 #[case(AudioCommand::Play, AudioCommand::Pause, false)]
177 #[case(AudioCommand::Pause, AudioCommand::Pause, true)]
178 #[case(AudioCommand::TogglePlayback, AudioCommand::TogglePlayback, true)]
179 #[case(AudioCommand::RestartSong, AudioCommand::RestartSong, true)]
180 #[case(
181 AudioCommand::Queue(QueueCommand::Clear),
182 AudioCommand::Queue(QueueCommand::Clear),
183 true
184 )]
185 #[case(
186 AudioCommand::Queue(QueueCommand::Clear),
187 AudioCommand::Queue(QueueCommand::Shuffle),
188 false
189 )]
190 #[case(
191 AudioCommand::Queue(QueueCommand::SkipForward(1)),
192 AudioCommand::Queue(QueueCommand::SkipForward(1)),
193 true
194 )]
195 #[case(
196 AudioCommand::Queue(QueueCommand::SkipForward(1)),
197 AudioCommand::Queue(QueueCommand::SkipForward(2)),
198 false
199 )]
200 #[case(
201 AudioCommand::Queue(QueueCommand::SkipBackward(1)),
202 AudioCommand::Queue(QueueCommand::SkipBackward(1)),
203 true
204 )]
205 #[case(
206 AudioCommand::Queue(QueueCommand::SkipBackward(1)),
207 AudioCommand::Queue(QueueCommand::SkipBackward(2)),
208 false
209 )]
210 #[case(
211 AudioCommand::Queue(QueueCommand::SetPosition(1)),
212 AudioCommand::Queue(QueueCommand::SetPosition(1)),
213 true
214 )]
215 #[case(
216 AudioCommand::Queue(QueueCommand::SetPosition(1)),
217 AudioCommand::Queue(QueueCommand::SetPosition(2)),
218 false
219 )]
220 #[case(
221 AudioCommand::Queue(QueueCommand::Shuffle),
222 AudioCommand::Queue(QueueCommand::Shuffle),
223 true
224 )]
225 #[case(
226 AudioCommand::Queue(QueueCommand::Shuffle),
227 AudioCommand::Queue(QueueCommand::Clear),
228 false
229 )]
230 #[case(
231 AudioCommand::Volume(VolumeCommand::Up(0.1)),
232 AudioCommand::Volume(VolumeCommand::Up(0.1)),
233 true
234 )]
235 #[case(
236 AudioCommand::Volume(VolumeCommand::Up(0.1)),
237 AudioCommand::Volume(VolumeCommand::Up(0.2)),
238 false
239 )]
240 #[case(
241 AudioCommand::Volume(VolumeCommand::Down(0.1)),
242 AudioCommand::Volume(VolumeCommand::Down(0.1)),
243 true
244 )]
245 #[case(
246 AudioCommand::Volume(VolumeCommand::Down(0.1)),
247 AudioCommand::Volume(VolumeCommand::Down(0.2)),
248 false
249 )]
250 #[case(
251 AudioCommand::Volume(VolumeCommand::Set(0.1)),
252 AudioCommand::Volume(VolumeCommand::Set(0.1)),
253 true
254 )]
255 #[case(
256 AudioCommand::Volume(VolumeCommand::Set(0.1)),
257 AudioCommand::Volume(VolumeCommand::Set(0.2)),
258 false
259 )]
260 #[case(
261 AudioCommand::Volume(VolumeCommand::Mute),
262 AudioCommand::Volume(VolumeCommand::Mute),
263 true
264 )]
265 #[case(
266 AudioCommand::Volume(VolumeCommand::Mute),
267 AudioCommand::Volume(VolumeCommand::Unmute),
268 false
269 )]
270 #[case(
271 AudioCommand::Volume(VolumeCommand::Unmute),
272 AudioCommand::Volume(VolumeCommand::Unmute),
273 true
274 )]
275 #[case(
276 AudioCommand::Volume(VolumeCommand::Unmute),
277 AudioCommand::Volume(VolumeCommand::Mute),
278 false
279 )]
280 #[case(
281 AudioCommand::Volume(VolumeCommand::ToggleMute),
282 AudioCommand::Volume(VolumeCommand::ToggleMute),
283 true
284 )]
285 #[case(
286 AudioCommand::Seek(SeekType::Absolute, Duration::from_secs(10)),
287 AudioCommand::Seek(SeekType::Absolute, Duration::from_secs(10)),
288 true
289 )]
290 #[case(
291 AudioCommand::Seek(SeekType::Absolute, Duration::from_secs(10)),
292 AudioCommand::Seek(SeekType::Absolute, Duration::from_secs(20)),
293 false
294 )]
295 #[case(
296 AudioCommand::Seek(SeekType::RelativeForwards, Duration::from_secs(10)),
297 AudioCommand::Seek(SeekType::RelativeForwards, Duration::from_secs(10)),
298 true
299 )]
300 #[case(
301 AudioCommand::Seek(SeekType::RelativeForwards, Duration::from_secs(10)),
302 AudioCommand::Seek(SeekType::RelativeForwards, Duration::from_secs(20)),
303 false
304 )]
305 #[case(
306 AudioCommand::Seek(SeekType::RelativeBackwards, Duration::from_secs(10)),
307 AudioCommand::Seek(SeekType::RelativeBackwards, Duration::from_secs(10)),
308 true
309 )]
310 #[case(
311 AudioCommand::Seek(SeekType::RelativeBackwards, Duration::from_secs(10)),
312 AudioCommand::Seek(SeekType::RelativeBackwards, Duration::from_secs(20)),
313 false
314 )]
315 #[case(
316 AudioCommand::Seek(SeekType::Absolute, Duration::from_secs(10)),
317 AudioCommand::Seek(SeekType::RelativeBackwards, Duration::from_secs(10)),
318 false
319 )]
320 #[case(
321 AudioCommand::Seek(SeekType::Absolute, Duration::from_secs(10)),
322 AudioCommand::Seek(SeekType::RelativeForwards, Duration::from_secs(10)),
323 false
324 )]
325 #[case(
326 AudioCommand::Seek(SeekType::RelativeForwards, Duration::from_secs(10)),
327 AudioCommand::Seek(SeekType::RelativeBackwards, Duration::from_secs(10)),
328 false
329 )]
330 fn test_audio_command_equality(
331 #[case] lhs: AudioCommand,
332 #[case] rhs: AudioCommand,
333 #[case] expected: bool,
334 ) {
335 let actual = lhs == rhs;
336 assert_eq!(actual, expected);
337 let actual = rhs == lhs;
338 assert_eq!(actual, expected);
339 }
340
341 fn dummy_song() -> SongBrief {
343 SongBrief {
344 id: Song::generate_id(),
345 title: "Song 1".into(),
346 artist: OneOrMany::None,
347 album_artist: OneOrMany::None,
348 album: "album".into(),
349 genre: OneOrMany::None,
350 runtime: Duration::from_secs(100),
351 track: None,
352 disc: None,
353 release_year: None,
354 extension: "mp3".into(),
355 path: "foo/bar.mp3".into(),
356 }
357 }
358
359 #[rstest]
360 #[case(AudioCommand::Play, "Play")]
361 #[case(AudioCommand::Pause, "Pause")]
362 #[case(AudioCommand::TogglePlayback, "Toggle Playback")]
363 #[case(AudioCommand::ClearPlayer, "Clear Player")]
364 #[case(AudioCommand::RestartSong, "Restart Song")]
365 #[case(AudioCommand::Queue(QueueCommand::Clear), "Queue: Clear")]
366 #[case(AudioCommand::Queue(QueueCommand::Shuffle), "Queue: Shuffle")]
367 #[case(
368 AudioCommand::Queue(QueueCommand::AddToQueue(OneOrMany::None)),
369 "Queue: Add nothing"
370 )]
371 #[case(
372 AudioCommand::Queue(QueueCommand::AddToQueue(dummy_song().into())),
373 "Queue: Add \"Song 1\""
374 )]
375 #[case(
376 AudioCommand::Queue(QueueCommand::AddToQueue(OneOrMany::Many(vec![dummy_song()]))),
377 "Queue: Add [\"Song 1\"]"
378 )]
379 #[case(
380 AudioCommand::Queue(QueueCommand::RemoveRange(0..1)),
381 "Queue: Remove items 0..1"
382 )]
383 #[case(
384 AudioCommand::Queue(QueueCommand::SetRepeatMode(RepeatMode::None)),
385 "Queue: Set Repeat Mode to None"
386 )]
387 #[case(
388 AudioCommand::Queue(QueueCommand::SkipForward(1)),
389 "Queue: Skip Forward by 1"
390 )]
391 #[case(
392 AudioCommand::Queue(QueueCommand::SkipBackward(1)),
393 "Queue: Skip Backward by 1"
394 )]
395 #[case(
396 AudioCommand::Queue(QueueCommand::SetPosition(1)),
397 "Queue: Set Position to 1"
398 )]
399 #[case(AudioCommand::Volume(VolumeCommand::Up(0.1)), "Volume: +10%")]
400 #[case(AudioCommand::Volume(VolumeCommand::Down(0.1)), "Volume: -10%")]
401 #[case(AudioCommand::Volume(VolumeCommand::Set(0.1)), "Volume: =10%")]
402 #[case(AudioCommand::Volume(VolumeCommand::Mute), "Volume: Mute")]
403 #[case(AudioCommand::Volume(VolumeCommand::Unmute), "Volume: Unmute")]
404 #[case(AudioCommand::Volume(VolumeCommand::ToggleMute), "Volume: Toggle Mute")]
405 #[case(AudioCommand::Exit, "Exit")]
406 #[case(AudioCommand::ReportStatus(tokio::sync::oneshot::channel().0), "Report Status")]
407 #[case(
408 AudioCommand::Seek(SeekType::Absolute, Duration::from_secs(10)),
409 "Seek: Absolute 00:00:10.00 (HH:MM:SS)"
410 )]
411 #[case(
412 AudioCommand::Seek(SeekType::RelativeForwards, Duration::from_secs(10)),
413 "Seek: Forwards 00:00:10.00 (HH:MM:SS)"
414 )]
415 #[case(
416 AudioCommand::Seek(SeekType::RelativeBackwards, Duration::from_secs(10)),
417 "Seek: Backwards 00:00:10.00 (HH:MM:SS)"
418 )]
419 #[case(
420 AudioCommand::Seek(SeekType::Absolute, Duration::from_secs(3600 + 120 + 1)),
421 "Seek: Absolute 01:02:01.00 (HH:MM:SS)"
422 )]
423 fn test_audio_command_display(#[case] command: AudioCommand, #[case] expected: &str) {
424 let actual = command.to_string();
425 assert_str_eq!(actual, expected);
426 }
427}