1use serde::{Deserialize, Serialize};
2
3use super::media::MediaItem;
4
5#[derive(specta::Type, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "lowercase")]
10pub enum EndReason {
11 Finished,
13 UserSkip,
15 UserStop,
17 Error,
19 NewTrackLoaded,
21}
22
23#[derive(specta::Type, Debug, Clone, Serialize, Deserialize, Default)]
27#[serde(tag = "kind", rename_all = "lowercase")]
28pub enum PlayerState {
29 #[default]
30 Idle,
32 Loading { media: MediaItem },
34 Playing {
36 media: MediaItem,
37 position: f64,
39 duration: f64,
41 },
42 Paused {
44 media: MediaItem,
45 position: f64,
47 duration: f64,
49 },
50 Seeking {
52 media: MediaItem,
53 target: f64,
55 },
56 Error {
58 media: Option<MediaItem>,
59 error: String,
60 },
61}
62
63impl PlayerState {
64 #[allow(dead_code)]
70 pub fn position(&self) -> Option<f64> {
71 match self {
72 PlayerState::Playing { position, .. } => Some(*position),
73 PlayerState::Paused { position, .. } => Some(*position),
74 _ => None,
75 }
76 }
77
78 pub fn is_playing(&self) -> bool {
80 matches!(self, PlayerState::Playing { .. })
81 }
82
83 #[allow(dead_code)]
85 pub fn is_paused(&self) -> bool {
86 matches!(self, PlayerState::Paused { .. })
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93
94 #[test]
95 fn test_end_reason_finished() {
96 let reason = EndReason::Finished;
97 let json = serde_json::to_string(&reason);
98 assert!(json.is_ok());
99 let serialized = json.unwrap();
100 assert!(serialized.contains("finished"));
101 }
102
103 #[test]
104 fn test_end_reason_user_skip() {
105 let reason = EndReason::UserSkip;
106 let json = serde_json::to_string(&reason);
107 assert!(json.is_ok());
108 let serialized = json.unwrap();
109 assert!(serialized.contains("userskip"));
110 }
111
112 #[test]
113 fn test_end_reason_user_stop() {
114 let reason = EndReason::UserStop;
115 let json = serde_json::to_string(&reason).unwrap();
116 assert!(json.contains("userstop"));
117 }
118
119 #[test]
120 fn test_end_reason_error() {
121 let reason = EndReason::Error;
122 let json = serde_json::to_string(&reason).unwrap();
123 assert!(json.contains("error"));
124 }
125
126 #[test]
127 fn test_end_reason_new_track_loaded() {
128 let reason = EndReason::NewTrackLoaded;
129 let json = serde_json::to_string(&reason).unwrap();
130 assert!(json.contains("newtrackloa"));
131 }
132
133 #[test]
134 fn test_end_reason_all_variants() {
135 let reasons = vec![
136 EndReason::Finished,
137 EndReason::UserSkip,
138 EndReason::UserStop,
139 EndReason::Error,
140 EndReason::NewTrackLoaded,
141 ];
142
143 for reason in reasons {
144 let json = serde_json::to_string(&reason);
145 assert!(json.is_ok());
146 }
147 }
148
149 #[test]
150 fn test_end_reason_equality() {
151 let reason1 = EndReason::Finished;
152 let reason2 = EndReason::Finished;
153 assert_eq!(reason1, reason2);
154
155 let reason3 = EndReason::UserSkip;
156 assert_ne!(reason1, reason3);
157 }
158
159 #[test]
160 fn test_end_reason_clone() {
161 let reason = EndReason::Finished;
162 #[allow(clippy::clone_on_copy)]
165 let cloned = reason.clone();
166 assert_eq!(reason, cloned);
167 }
168
169 #[test]
170 fn test_player_state_idle_default() {
171 let state = PlayerState::default();
172 assert!(matches!(state, PlayerState::Idle));
173 }
174
175 #[test]
176 fn test_player_state_idle_serialization() {
177 let state = PlayerState::Idle;
178 let json = serde_json::to_string(&state);
179 assert!(json.is_ok());
180 let serialized = json.unwrap();
181 assert!(serialized.contains("idle"));
182 }
183
184 #[test]
185 fn test_player_state_position_when_idle() {
186 let state = PlayerState::Idle;
187 assert_eq!(state.position(), None);
188 }
189
190 #[test]
191 fn test_player_state_is_playing_when_idle() {
192 let state = PlayerState::Idle;
193 assert!(!state.is_playing());
194 }
195
196 #[test]
197 fn test_player_state_is_paused_when_idle() {
198 let state = PlayerState::Idle;
199 assert!(!state.is_paused());
200 }
201
202 #[test]
203 fn test_player_state_loading() {
204 let media = create_test_media_item("item-1", "Test Item");
205 let state = PlayerState::Loading {
206 media: media.clone(),
207 };
208 assert!(matches!(state, PlayerState::Loading { .. }));
209 assert_eq!(state.position(), None);
210 assert!(!state.is_playing());
211 }
212
213 #[test]
214 fn test_player_state_playing_position() {
215 let media = create_test_media_item("item-2", "Playing Item");
216 let state = PlayerState::Playing {
217 media,
218 position: 45.5,
219 duration: 180.0,
220 };
221
222 assert!(state.is_playing());
223 assert!(!state.is_paused());
224 assert_eq!(state.position(), Some(45.5));
225 }
226
227 #[test]
228 fn test_player_state_paused_position() {
229 let media = create_test_media_item("item-3", "Paused Item");
230 let state = PlayerState::Paused {
231 media,
232 position: 123.75,
233 duration: 300.0,
234 };
235
236 assert!(state.is_paused());
237 assert!(!state.is_playing());
238 assert_eq!(state.position(), Some(123.75));
239 }
240
241 #[test]
242 fn test_player_state_seeking() {
243 let media = create_test_media_item("item-4", "Seeking Item");
244 let state = PlayerState::Seeking {
245 media,
246 target: 60.0,
247 };
248
249 assert!(!state.is_playing());
250 assert!(!state.is_paused());
251 assert_eq!(state.position(), None);
252 }
253
254 #[test]
255 fn test_player_state_error_with_media() {
256 let media = create_test_media_item("item-5", "Error Item");
257 let state = PlayerState::Error {
258 media: Some(media),
259 error: "Playback failed".to_string(),
260 };
261
262 assert!(!state.is_playing());
263 assert_eq!(state.position(), None);
264 }
265
266 #[test]
267 fn test_player_state_error_without_media() {
268 let state = PlayerState::Error {
269 media: None,
270 error: "Connection lost".to_string(),
271 };
272
273 assert!(!state.is_playing());
274 assert_eq!(state.position(), None);
275 }
276
277 #[test]
278 fn test_player_state_clone() {
279 let state = PlayerState::Idle;
280 let cloned = state.clone();
281 assert!(matches!(cloned, PlayerState::Idle));
282 }
283
284 #[test]
285 fn test_player_state_playing_serialization() {
286 let media = create_test_media_item("item-6", "Serial Item");
287 let state = PlayerState::Playing {
288 media,
289 position: 30.0,
290 duration: 120.0,
291 };
292
293 let json = serde_json::to_string(&state);
294 assert!(json.is_ok());
295 let serialized = json.unwrap();
296 assert!(serialized.contains("playing"));
297 assert!(serialized.contains("30"));
298 }
299
300 #[test]
301 fn test_player_state_multiple_positions() {
302 let positions = vec![0.0, 45.5, 100.0, 999.99];
303
304 for pos in positions {
305 let media = create_test_media_item("item-test", "Test");
306 let state = PlayerState::Playing {
307 media,
308 position: pos,
309 duration: 1000.0,
310 };
311
312 assert_eq!(state.position(), Some(pos));
313 }
314 }
315
316 fn create_test_media_item(id: &str, title: &str) -> MediaItem {
318 MediaItem {
319 id: id.to_string(),
320 title: title.to_string(),
321 name: None,
322 artist: None,
323 album: None,
324 album_name: None,
325 album_id: None,
326 artist_items: None,
327 artists: None,
328 primary_image_tag: None,
329 image_id: None,
330 item_type: Some("Video".to_string()),
331 playlist_id: None,
332 duration: Some(100.0),
333 artwork_url: None,
334 media_type: super::super::media::MediaType::Video,
335 source: super::super::media::MediaSource::DirectUrl {
336 url: "http://example.com/media".to_string(),
337 },
338 video_codec: None,
339 needs_transcoding: false,
340 video_width: None,
341 video_height: None,
342 subtitles: vec![],
343 series_id: None,
344 server_id: None,
345 }
346 }
347}