1use serde::{Deserialize, Serialize};
4
5use super::common::ExternalUrls;
6use super::track::Track;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct PlaybackState {
11 pub device: Option<Device>,
13 pub repeat_state: Option<String>,
15 pub shuffle_state: Option<bool>,
17 pub context: Option<PlaybackContext>,
19 pub timestamp: Option<u64>,
21 pub progress_ms: Option<u64>,
23 pub is_playing: bool,
25 pub item: Option<Track>,
27 pub currently_playing_type: Option<String>,
29 pub actions: Option<PlaybackActions>,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct PlaybackContext {
36 #[serde(rename = "type")]
38 pub context_type: Option<String>,
39 pub href: Option<String>,
41 pub external_urls: Option<ExternalUrls>,
43 pub uri: Option<String>,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct PlaybackActions {
50 pub interrupting_playback: Option<bool>,
52 pub pausing: Option<bool>,
54 pub resuming: Option<bool>,
56 pub seeking: Option<bool>,
58 pub skipping_next: Option<bool>,
60 pub skipping_prev: Option<bool>,
62 pub toggling_repeat_context: Option<bool>,
64 pub toggling_shuffle: Option<bool>,
66 pub toggling_repeat_track: Option<bool>,
68 pub transferring_playback: Option<bool>,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct Device {
75 pub id: Option<String>,
77 pub is_active: bool,
79 pub is_private_session: Option<bool>,
81 pub is_restricted: Option<bool>,
83 pub name: String,
85 #[serde(rename = "type")]
87 pub device_type: String,
88 pub volume_percent: Option<u32>,
90 pub supports_volume: Option<bool>,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct DevicesResponse {
97 pub devices: Vec<Device>,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct QueueResponse {
104 pub currently_playing: Option<Track>,
106 pub queue: Vec<Track>,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct PlayHistory {
113 pub track: Track,
115 pub played_at: String,
117 pub context: Option<PlaybackContext>,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct RecentlyPlayedResponse {
124 pub href: Option<String>,
126 pub limit: Option<u32>,
128 pub next: Option<String>,
130 pub cursors: Option<RecentlyPlayedCursors>,
132 pub total: Option<u32>,
134 pub items: Vec<PlayHistory>,
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct RecentlyPlayedCursors {
141 pub after: Option<String>,
143 pub before: Option<String>,
145}
146
147impl PlaybackState {
148 pub fn track_name(&self) -> Option<&str> {
150 self.item.as_ref().map(|t| t.name.as_str())
151 }
152
153 pub fn artist_name(&self) -> Option<&str> {
155 self.item.as_ref().and_then(|t| t.artist_name())
156 }
157
158 pub fn progress_str(&self) -> String {
160 let ms = self.progress_ms.unwrap_or(0);
161 let total_secs = ms / 1000;
162 let mins = total_secs / 60;
163 let secs = total_secs % 60;
164 format!("{}:{:02}", mins, secs)
165 }
166
167 pub fn duration_str(&self) -> String {
169 self.item
170 .as_ref()
171 .map(|t| t.duration_str())
172 .unwrap_or_else(|| "0:00".to_string())
173 }
174
175 pub fn device_name(&self) -> Option<&str> {
177 self.device.as_ref().map(|d| d.name.as_str())
178 }
179}
180
181impl Device {
182 pub fn device_type_display(&self) -> &str {
184 match self.device_type.as_str() {
185 "Computer" => "Computer",
186 "Smartphone" => "Phone",
187 "Speaker" => "Speaker",
188 "TV" => "TV",
189 "AVR" => "Receiver",
190 "STB" => "Set-top Box",
191 "AudioDongle" => "Audio Dongle",
192 "GameConsole" => "Game Console",
193 "CastVideo" => "Cast",
194 "CastAudio" => "Cast",
195 "Automobile" => "Car",
196 "Tablet" => "Tablet",
197 _ => &self.device_type,
198 }
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205 use serde_json::json;
206
207 #[test]
208 fn device_deserializes() {
209 let json = json!({
210 "id": "device123",
211 "is_active": true,
212 "name": "Living Room Speaker",
213 "type": "Speaker",
214 "volume_percent": 75
215 });
216 let device: Device = serde_json::from_value(json).unwrap();
217 assert_eq!(device.id, Some("device123".to_string()));
218 assert!(device.is_active);
219 assert_eq!(device.name, "Living Room Speaker");
220 assert_eq!(device.device_type, "Speaker");
221 }
222
223 #[test]
224 fn device_type_display_known_types() {
225 let make_device = |t: &str| Device {
226 id: None,
227 is_active: false,
228 is_private_session: None,
229 is_restricted: None,
230 name: "Test".to_string(),
231 device_type: t.to_string(),
232 volume_percent: None,
233 supports_volume: None,
234 };
235
236 assert_eq!(make_device("Computer").device_type_display(), "Computer");
237 assert_eq!(make_device("Smartphone").device_type_display(), "Phone");
238 assert_eq!(make_device("Speaker").device_type_display(), "Speaker");
239 assert_eq!(make_device("TV").device_type_display(), "TV");
240 assert_eq!(make_device("AVR").device_type_display(), "Receiver");
241 assert_eq!(make_device("Automobile").device_type_display(), "Car");
242 assert_eq!(make_device("Tablet").device_type_display(), "Tablet");
243 }
244
245 #[test]
246 fn device_type_display_unknown() {
247 let device = Device {
248 id: None,
249 is_active: false,
250 is_private_session: None,
251 is_restricted: None,
252 name: "Test".to_string(),
253 device_type: "NewDeviceType".to_string(),
254 volume_percent: None,
255 supports_volume: None,
256 };
257 assert_eq!(device.device_type_display(), "NewDeviceType");
258 }
259
260 #[test]
261 fn devices_response_deserializes() {
262 let json = json!({
263 "devices": [
264 {"id": "dev1", "is_active": true, "name": "Device 1", "type": "Computer"},
265 {"id": "dev2", "is_active": false, "name": "Device 2", "type": "Speaker"}
266 ]
267 });
268 let resp: DevicesResponse = serde_json::from_value(json).unwrap();
269 assert_eq!(resp.devices.len(), 2);
270 }
271
272 #[test]
273 fn playback_context_deserializes() {
274 let json = json!({
275 "type": "playlist",
276 "uri": "spotify:playlist:abc123",
277 "href": "https://api.spotify.com/v1/playlists/abc123"
278 });
279 let ctx: PlaybackContext = serde_json::from_value(json).unwrap();
280 assert_eq!(ctx.context_type, Some("playlist".to_string()));
281 }
282
283 #[test]
284 fn playback_actions_deserializes() {
285 let json = json!({
286 "pausing": true,
287 "resuming": true,
288 "seeking": true,
289 "skipping_next": true,
290 "skipping_prev": false
291 });
292 let actions: PlaybackActions = serde_json::from_value(json).unwrap();
293 assert_eq!(actions.pausing, Some(true));
294 assert_eq!(actions.skipping_prev, Some(false));
295 }
296
297 #[test]
298 fn playback_state_deserializes() {
299 let json = json!({
300 "is_playing": true,
301 "progress_ms": 60000,
302 "repeat_state": "off",
303 "shuffle_state": false
304 });
305 let state: PlaybackState = serde_json::from_value(json).unwrap();
306 assert!(state.is_playing);
307 assert_eq!(state.progress_ms, Some(60000));
308 assert_eq!(state.repeat_state, Some("off".to_string()));
309 }
310
311 #[test]
312 fn playback_state_track_name() {
313 let json = json!({
314 "is_playing": true,
315 "item": {
316 "id": "track1",
317 "name": "Test Track",
318 "type": "track",
319 "uri": "spotify:track:track1",
320 "duration_ms": 200000
321 }
322 });
323 let state: PlaybackState = serde_json::from_value(json).unwrap();
324 assert_eq!(state.track_name(), Some("Test Track"));
325 }
326
327 #[test]
328 fn playback_state_track_name_none() {
329 let json = json!({
330 "is_playing": false
331 });
332 let state: PlaybackState = serde_json::from_value(json).unwrap();
333 assert!(state.track_name().is_none());
334 }
335
336 #[test]
337 fn playback_state_progress_str() {
338 let json = json!({
339 "is_playing": true,
340 "progress_ms": 125000 });
342 let state: PlaybackState = serde_json::from_value(json).unwrap();
343 assert_eq!(state.progress_str(), "2:05");
344 }
345
346 #[test]
347 fn playback_state_progress_str_zero() {
348 let json = json!({
349 "is_playing": false
350 });
351 let state: PlaybackState = serde_json::from_value(json).unwrap();
352 assert_eq!(state.progress_str(), "0:00");
353 }
354
355 #[test]
356 fn playback_state_duration_str() {
357 let json = json!({
358 "is_playing": true,
359 "item": {
360 "id": "track1",
361 "name": "Test",
362 "type": "track",
363 "uri": "spotify:track:track1",
364 "duration_ms": 210000 }
366 });
367 let state: PlaybackState = serde_json::from_value(json).unwrap();
368 assert_eq!(state.duration_str(), "3:30");
369 }
370
371 #[test]
372 fn playback_state_duration_str_no_item() {
373 let json = json!({
374 "is_playing": false
375 });
376 let state: PlaybackState = serde_json::from_value(json).unwrap();
377 assert_eq!(state.duration_str(), "0:00");
378 }
379
380 #[test]
381 fn playback_state_device_name() {
382 let json = json!({
383 "is_playing": true,
384 "device": {
385 "id": "dev1",
386 "is_active": true,
387 "name": "My Computer",
388 "type": "Computer"
389 }
390 });
391 let state: PlaybackState = serde_json::from_value(json).unwrap();
392 assert_eq!(state.device_name(), Some("My Computer"));
393 }
394
395 #[test]
396 fn queue_response_deserializes() {
397 let json = json!({
398 "currently_playing": null,
399 "queue": []
400 });
401 let queue: QueueResponse = serde_json::from_value(json).unwrap();
402 assert!(queue.currently_playing.is_none());
403 assert!(queue.queue.is_empty());
404 }
405
406 #[test]
407 fn play_history_deserializes() {
408 let json = json!({
409 "track": {
410 "id": "track1",
411 "name": "Recent Track",
412 "type": "track",
413 "uri": "spotify:track:track1",
414 "duration_ms": 180000
415 },
416 "played_at": "2024-01-15T10:30:00Z"
417 });
418 let history: PlayHistory = serde_json::from_value(json).unwrap();
419 assert_eq!(history.track.name, "Recent Track");
420 assert_eq!(history.played_at, "2024-01-15T10:30:00Z");
421 }
422
423 #[test]
424 fn recently_played_response_deserializes() {
425 let json = json!({
426 "items": [],
427 "limit": 20
428 });
429 let resp: RecentlyPlayedResponse = serde_json::from_value(json).unwrap();
430 assert!(resp.items.is_empty());
431 assert_eq!(resp.limit, Some(20));
432 }
433
434 #[test]
435 fn recently_played_cursors_deserializes() {
436 let json = json!({
437 "after": "1234567890",
438 "before": "0987654321"
439 });
440 let cursors: RecentlyPlayedCursors = serde_json::from_value(json).unwrap();
441 assert_eq!(cursors.after, Some("1234567890".to_string()));
442 assert_eq!(cursors.before, Some("0987654321".to_string()));
443 }
444}