1#![allow(clippy::module_name_repetitions)]
2use std::{fmt::Display, time::Duration};
3
4use mecomp_storage::db::schemas::song::SongBrief;
5use serde::{Deserialize, Serialize};
6
7use crate::format_duration;
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
10pub enum SeekType {
11 Absolute,
12 RelativeForwards,
13 RelativeBackwards,
14}
15
16impl Display for SeekType {
17 #[inline]
18 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19 match self {
20 Self::Absolute => write!(f, "Absolute"),
21 Self::RelativeForwards => write!(f, "Forwards"),
22 Self::RelativeBackwards => write!(f, "Backwards"),
23 }
24 }
25}
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize, Default)]
28pub enum RepeatMode {
29 #[default]
31 None,
32 One,
34 All,
36}
37
38impl Display for RepeatMode {
39 #[inline]
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 match self {
42 Self::None => write!(f, "None"),
43 Self::One => write!(f, "One"),
44 Self::All => write!(f, "All"),
45 }
46 }
47}
48
49impl RepeatMode {
50 #[must_use]
51 #[inline]
52 pub const fn is_none(&self) -> bool {
53 matches!(self, Self::None)
54 }
55
56 #[must_use]
57 #[inline]
58 pub const fn is_one(&self) -> bool {
59 matches!(self, Self::One)
60 }
61
62 #[must_use]
63 #[inline]
64 pub const fn is_all(&self) -> bool {
65 matches!(self, Self::All)
66 }
67}
68
69#[derive(Copy, Clone, Debug, PartialEq, Deserialize, Serialize, Default)]
70pub struct Percent(f32);
71
72impl Percent {
73 #[must_use]
74 #[inline]
75 pub const fn new(value: f32) -> Self {
76 Self(if value.is_finite() {
77 value.clamp(0.0, 100.0)
78 } else {
79 0.0
80 })
81 }
82
83 #[must_use]
84 #[inline]
85 pub const fn into_inner(self) -> f32 {
86 self.0
87 }
88}
89
90impl Display for Percent {
91 #[inline]
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 write!(f, "{:.2}%", self.into_inner())
94 }
95}
96
97#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Default)]
99pub struct StateRuntime {
100 pub seek_position: Duration,
101 pub seek_percent: Percent,
102 pub duration: Duration,
103}
104
105impl Display for StateRuntime {
106 #[inline]
107 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108 write!(
109 f,
110 "StateRuntime {{ seek_position: {}, seek_percent: {}, duration: {} }}",
111 format_duration(&self.seek_position),
112 self.seek_percent,
113 format_duration(&self.duration)
114 )
115 }
116}
117
118#[derive(
119 Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default,
120)]
121pub enum Status {
122 #[default]
123 Stopped,
124 Paused,
125 Playing,
126}
127
128impl Display for Status {
129 #[inline]
130 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131 match self {
132 Self::Paused => write!(f, "Paused"),
133 Self::Playing => write!(f, "Playing"),
134 Self::Stopped => write!(f, "Stopped"),
135 }
136 }
137}
138
139#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
140pub struct StateAudio {
141 pub queue: Box<[SongBrief]>,
142 pub queue_position: Option<usize>,
143 pub current_song: Option<SongBrief>,
144 pub repeat_mode: RepeatMode,
145 pub runtime: Option<StateRuntime>,
146 pub status: Status,
147 pub muted: bool,
148 pub volume: f32,
149}
150
151impl Display for StateAudio {
152 #[inline]
153 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154 write!(
155 f,
156 "StateAudio {{ queue: {:?}, queue_position: {}, current_song: {}, repeat_mode: {}, runtime: {}, status: {}, muted: {}, volume: {:.0}% }}",
157 self.queue
158 .iter()
159 .map(|song| &song.title)
160 .collect::<Vec<_>>(),
161 self.queue_position
162 .map_or_else(|| "None".to_string(), |pos| pos.to_string()),
163 self.current_song
164 .as_ref()
165 .map_or_else(|| "None".to_string(), |song| format!("\"{}\"", song.title)),
166 self.repeat_mode,
167 self.runtime
168 .as_ref()
169 .map_or_else(|| "None".to_string(), ToString::to_string),
170 self.status,
171 self.muted,
172 self.volume * 100.0,
173 )
174 }
175}
176
177impl Default for StateAudio {
178 #[inline]
180 fn default() -> Self {
181 Self {
182 queue: Box::default(),
183 queue_position: None,
184 current_song: None,
185 repeat_mode: RepeatMode::default(),
186 runtime: None,
187 status: Status::default(),
188 muted: false,
189 volume: 1.0,
190 }
191 }
192}
193
194impl StateAudio {
195 #[must_use]
196 #[inline]
197 pub const fn paused(&self) -> bool {
198 !matches!(self.status, Status::Playing)
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use std::time::Duration;
205
206 use super::*;
207 use mecomp_storage::db::schemas::song::Song;
208 use one_or_many::OneOrMany;
209 use pretty_assertions::{assert_eq, assert_str_eq};
210 use rstest::rstest;
211
212 #[test]
213 fn test_state_audio_default() {
214 let state = StateAudio::default();
215 assert_eq!(state.queue.as_ref(), &[]);
216 assert_eq!(state.queue_position, None);
217 assert_eq!(state.current_song, None);
218 assert_eq!(state.repeat_mode, RepeatMode::None);
219 assert_eq!(state.runtime, None);
220 assert_eq!(state.status, Status::Stopped);
221 assert_eq!(state.muted, false);
222 assert!(
223 f32::EPSILON > (state.volume - 1.0).abs(),
224 "{} != 1.0",
225 state.volume
226 );
227 }
228
229 #[rstest]
230 #[case::none(RepeatMode::None, [true, false, false])]
231 #[case::one(RepeatMode::One, [false, true, false])]
232 #[case::all(RepeatMode::All, [false, false, true])]
233 fn test_repeat_mode(#[case] mode: RepeatMode, #[case] expected: [bool; 3]) {
234 assert_eq!(mode.is_none(), expected[0]);
235 assert_eq!(mode.is_one(), expected[1]);
236 assert_eq!(mode.is_all(), expected[2]);
237 }
238
239 #[rstest]
240 #[case::seek_type(SeekType::Absolute, "Absolute")]
241 #[case::seek_type(SeekType::RelativeForwards, "Forwards")]
242 #[case::seek_type(SeekType::RelativeBackwards, "Backwards")]
243 #[case::repeat_mode(RepeatMode::None, "None")]
244 #[case::repeat_mode(RepeatMode::One, "One")]
245 #[case::repeat_mode(RepeatMode::All, "All")]
246 #[case::percent(Percent::new(50.0), "50.00%")]
247 #[case::state_runtimme(
248 StateRuntime {
249 seek_position: Duration::from_secs(3),
250 seek_percent: Percent::new(50.0),
251 duration: Duration::from_secs(6),
252 },
253 "StateRuntime { seek_position: 00:00:03.00, seek_percent: 50.00%, duration: 00:00:06.00 }"
254 )]
255 #[case::state_audio_empty(
256 StateAudio {
257 queue: Box::new([]),
258 queue_position: None,
259 current_song: None,
260 repeat_mode: RepeatMode::None,
261 runtime: None,
262 status: Status::Paused,
263 muted: false,
264 volume: 1.0,
265 },
266 "StateAudio { queue: [], queue_position: None, current_song: None, repeat_mode: None, runtime: None, status: Paused, muted: false, volume: 100% }"
267 )]
268 #[case::state_audio_empty(
269 StateAudio {
270 queue: Box::new([]),
271 queue_position: None,
272 current_song: None,
273 repeat_mode: RepeatMode::None,
274 runtime: None,
275 status: Status::Paused,
276 muted: false,
277 volume: 1.0,
278 },
279 "StateAudio { queue: [], queue_position: None, current_song: None, repeat_mode: None, runtime: None, status: Paused, muted: false, volume: 100% }"
280 )]
281 #[case::state_audio(
282 StateAudio {
283 queue: Box::new([
284 SongBrief {
285 id: Song::generate_id(),
286 title: "Song 1".into(),
287 artist: OneOrMany::None,
288 album_artist: OneOrMany::None,
289 album: "album".into(),
290 genre: OneOrMany::None,
291 runtime: Duration::from_secs(100),
292 track: None,
293 disc: None,
294 release_year: None,
295 path: "foo/bar.mp3".into(),
296 }
297 ]),
298 queue_position: Some(1),
299 current_song: Some(
300 SongBrief {
301 id: Song::generate_id(),
302 title: "Song 1".into(),
303 artist: OneOrMany::None,
304 album_artist: OneOrMany::None,
305 album: "album".into(),
306 genre: OneOrMany::None,
307 runtime: Duration::from_secs(100),
308 track: None,
309 disc: None,
310 release_year: None,
311 path: "foo/bar.mp3".into(),
312 }
313 ),
314 repeat_mode: RepeatMode::None,
315 runtime: Some(StateRuntime {
316 seek_position: Duration::from_secs(20),
317 seek_percent: Percent::new(20.0),
318 duration: Duration::from_secs(100),
319 }),
320 status: Status::Playing,
321 muted: false,
322 volume: 1.0,
323 },
324 "StateAudio { queue: [\"Song 1\"], queue_position: 1, current_song: \"Song 1\", repeat_mode: None, runtime: StateRuntime { seek_position: 00:00:20.00, seek_percent: 20.00%, duration: 00:01:40.00 }, status: Playing, muted: false, volume: 100% }"
325 )]
326 fn test_display_impls<T: Display>(#[case] input: T, #[case] expected: &str) {
327 assert_str_eq!(input.to_string(), expected);
328 }
329}