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