mecomp_core/state/
mod.rs

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    /// No repeat: after the queue is finished the player stops
30    #[default]
31    None,
32    /// Repeat the current Song: Repeats the current song, otherwise behaves like `RepeatMode::None`
33    One,
34    /// Repeat the queue Continuously: after going through the queue, the player goes back to the beginning and continues
35    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/// Information about the runtime of the song song
98#[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    /// Should match the defaults assigned to the [`AudioKernel`]
179    #[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}