spotify_cli/types/
playback.rs

1//! Playback state types from Spotify API.
2
3use serde::{Deserialize, Serialize};
4
5use super::common::ExternalUrls;
6use super::track::Track;
7
8/// Current playback state.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct PlaybackState {
11    /// The device currently playing.
12    pub device: Option<Device>,
13    /// Repeat mode: off, track, context.
14    pub repeat_state: Option<String>,
15    /// Whether shuffle is on.
16    pub shuffle_state: Option<bool>,
17    /// Playback context (album, playlist, etc.).
18    pub context: Option<PlaybackContext>,
19    /// Unix timestamp of when data was fetched.
20    pub timestamp: Option<u64>,
21    /// Progress into the currently playing track (ms).
22    pub progress_ms: Option<u64>,
23    /// Whether something is currently playing.
24    pub is_playing: bool,
25    /// The currently playing track/episode.
26    pub item: Option<Track>,
27    /// Currently playing type: track, episode, ad, unknown.
28    pub currently_playing_type: Option<String>,
29    /// Actions available/restricted.
30    pub actions: Option<PlaybackActions>,
31}
32
33/// Playback context (what's being played from).
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct PlaybackContext {
36    /// Context type: album, artist, playlist.
37    #[serde(rename = "type")]
38    pub context_type: Option<String>,
39    /// Spotify URL.
40    pub href: Option<String>,
41    /// External URLs.
42    pub external_urls: Option<ExternalUrls>,
43    /// Spotify URI.
44    pub uri: Option<String>,
45}
46
47/// Available playback actions.
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct PlaybackActions {
50    /// Whether interrupting playback is allowed.
51    pub interrupting_playback: Option<bool>,
52    /// Whether pausing is allowed.
53    pub pausing: Option<bool>,
54    /// Whether resuming is allowed.
55    pub resuming: Option<bool>,
56    /// Whether seeking is allowed.
57    pub seeking: Option<bool>,
58    /// Whether skipping next is allowed.
59    pub skipping_next: Option<bool>,
60    /// Whether skipping previous is allowed.
61    pub skipping_prev: Option<bool>,
62    /// Whether toggling repeat context is allowed.
63    pub toggling_repeat_context: Option<bool>,
64    /// Whether toggling shuffle is allowed.
65    pub toggling_shuffle: Option<bool>,
66    /// Whether toggling repeat track is allowed.
67    pub toggling_repeat_track: Option<bool>,
68    /// Whether transferring playback is allowed.
69    pub transferring_playback: Option<bool>,
70}
71
72/// Device information.
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct Device {
75    /// Device ID.
76    pub id: Option<String>,
77    /// Whether this is the currently active device.
78    pub is_active: bool,
79    /// Whether the device is in a private session.
80    pub is_private_session: Option<bool>,
81    /// Whether controlling this device is restricted.
82    pub is_restricted: Option<bool>,
83    /// Device name.
84    pub name: String,
85    /// Device type: computer, smartphone, speaker, etc.
86    #[serde(rename = "type")]
87    pub device_type: String,
88    /// Current volume percentage.
89    pub volume_percent: Option<u32>,
90    /// Whether the device supports volume control.
91    pub supports_volume: Option<bool>,
92}
93
94/// Devices response.
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct DevicesResponse {
97    /// List of devices.
98    pub devices: Vec<Device>,
99}
100
101/// Queue response.
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct QueueResponse {
104    /// Currently playing track.
105    pub currently_playing: Option<Track>,
106    /// Upcoming tracks in the queue.
107    pub queue: Vec<Track>,
108}
109
110/// Recently played item.
111#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct PlayHistory {
113    /// The track that was played.
114    pub track: Track,
115    /// When the track was played.
116    pub played_at: String,
117    /// Playback context.
118    pub context: Option<PlaybackContext>,
119}
120
121/// Recently played response (cursor-paginated).
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct RecentlyPlayedResponse {
124    /// URL to the API endpoint.
125    pub href: Option<String>,
126    /// Maximum number of items.
127    pub limit: Option<u32>,
128    /// URL to the next page.
129    pub next: Option<String>,
130    /// Cursors for pagination.
131    pub cursors: Option<RecentlyPlayedCursors>,
132    /// Total count (may be null).
133    pub total: Option<u32>,
134    /// The recently played items.
135    pub items: Vec<PlayHistory>,
136}
137
138/// Cursors for recently played pagination.
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct RecentlyPlayedCursors {
141    /// Cursor to the next page.
142    pub after: Option<String>,
143    /// Cursor to the previous page.
144    pub before: Option<String>,
145}
146
147impl PlaybackState {
148    /// Get the currently playing track name.
149    pub fn track_name(&self) -> Option<&str> {
150        self.item.as_ref().map(|t| t.name.as_str())
151    }
152
153    /// Get the currently playing artist name.
154    pub fn artist_name(&self) -> Option<&str> {
155        self.item.as_ref().and_then(|t| t.artist_name())
156    }
157
158    /// Get progress as MM:SS string.
159    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    /// Get duration as MM:SS string.
168    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    /// Get the device name.
176    pub fn device_name(&self) -> Option<&str> {
177        self.device.as_ref().map(|d| d.name.as_str())
178    }
179}
180
181impl Device {
182    /// Get a display-friendly device type.
183    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  // 2:05
341        });
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  // 3:30
365            }
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}