mecomp_core/state/
mod.rs

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