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 #[default]
31 None,
32 One,
34 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#[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 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}