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