spotify_cli/io/formatters/
player.rs

1//! Player-related formatting functions
2
3use serde_json::Value;
4
5use crate::io::common::{extract_artist_names, format_duration};
6
7pub fn format_player_status(payload: &Value, item: &Value) {
8    let is_playing = payload
9        .get("is_playing")
10        .and_then(|v| v.as_bool())
11        .unwrap_or(false);
12
13    let track_name = item
14        .get("name")
15        .and_then(|v| v.as_str())
16        .unwrap_or("Unknown");
17    let artists = extract_artist_names(item);
18
19    let album = item
20        .get("album")
21        .and_then(|a| a.get("name"))
22        .and_then(|v| v.as_str())
23        .unwrap_or("Unknown");
24
25    let progress_ms = payload
26        .get("progress_ms")
27        .and_then(|v| v.as_u64())
28        .unwrap_or(0);
29    let duration_ms = item
30        .get("duration_ms")
31        .and_then(|v| v.as_u64())
32        .unwrap_or(0);
33
34    let progress = format_duration(progress_ms);
35    let duration = format_duration(duration_ms);
36
37    let status_icon = if is_playing { "▶" } else { "⏸" };
38
39    println!("{} {} - {}", status_icon, track_name, artists);
40    println!("  Album: {}", album);
41    println!("  Progress: {} / {}", progress, duration);
42
43    if let Some(device) = payload.get("device") {
44        let device_name = device
45            .get("name")
46            .and_then(|v| v.as_str())
47            .unwrap_or("Unknown");
48        let volume = device
49            .get("volume_percent")
50            .and_then(|v| v.as_u64())
51            .unwrap_or(0);
52        println!("  Device: {} ({}%)", device_name, volume);
53    }
54
55    let shuffle = payload
56        .get("shuffle_state")
57        .and_then(|v| v.as_bool())
58        .unwrap_or(false);
59    let repeat = payload
60        .get("repeat_state")
61        .and_then(|v| v.as_str())
62        .unwrap_or("off");
63    if shuffle || repeat != "off" {
64        let mut modes = vec![];
65        if shuffle {
66            modes.push("shuffle");
67        }
68        if repeat != "off" {
69            modes.push(repeat);
70        }
71        println!("  Mode: {}", modes.join(", "));
72    }
73}
74
75pub fn format_queue(payload: &Value) {
76    if let Some(current) = payload.get("currently_playing")
77        && !current.is_null()
78    {
79        let name = current
80            .get("name")
81            .and_then(|v| v.as_str())
82            .unwrap_or("Unknown");
83        let artists = extract_artist_names(current);
84        println!("Now Playing: {} - {}", name, artists);
85    }
86
87    if let Some(queue) = payload.get("queue").and_then(|q| q.as_array()) {
88        if queue.is_empty() {
89            println!("Queue is empty.");
90        } else {
91            println!("Up Next:");
92            for (i, track) in queue.iter().take(10).enumerate() {
93                let name = track
94                    .get("name")
95                    .and_then(|v| v.as_str())
96                    .unwrap_or("Unknown");
97                let artists = extract_artist_names(track);
98                println!("  {}. {} - {}", i + 1, name, artists);
99            }
100            if queue.len() > 10 {
101                println!("  ... and {} more", queue.len() - 10);
102            }
103        }
104    }
105}
106
107pub fn format_devices(devices: &[Value]) {
108    if devices.is_empty() {
109        println!("No devices available.");
110        return;
111    }
112    println!("Available Devices:");
113    for device in devices {
114        let name = device
115            .get("name")
116            .and_then(|v| v.as_str())
117            .unwrap_or("Unknown");
118        let dtype = device
119            .get("type")
120            .and_then(|v| v.as_str())
121            .unwrap_or("Unknown");
122        let active = device
123            .get("is_active")
124            .and_then(|v| v.as_bool())
125            .unwrap_or(false);
126        let volume = device.get("volume_percent").and_then(|v| v.as_u64());
127
128        let active_marker = if active { " *" } else { "" };
129        let vol_str = volume.map(|v| format!(" ({}%)", v)).unwrap_or_default();
130
131        println!("  [{}] {}{}{}", dtype, name, vol_str, active_marker);
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use serde_json::json;
139
140    #[test]
141    fn format_player_status_playing() {
142        let payload = json!({
143            "is_playing": true,
144            "progress_ms": 60000,
145            "device": { "name": "My Speaker", "volume_percent": 75 },
146            "shuffle_state": false,
147            "repeat_state": "off"
148        });
149        let item = json!({
150            "name": "Test Track",
151            "artists": [{ "name": "Test Artist" }],
152            "album": { "name": "Test Album" },
153            "duration_ms": 180000
154        });
155        format_player_status(&payload, &item);
156    }
157
158    #[test]
159    fn format_player_status_paused() {
160        let payload = json!({
161            "is_playing": false,
162            "progress_ms": 30000
163        });
164        let item = json!({
165            "name": "Paused Track",
166            "artists": [{ "name": "Artist" }],
167            "album": { "name": "Album" },
168            "duration_ms": 200000
169        });
170        format_player_status(&payload, &item);
171    }
172
173    #[test]
174    fn format_player_status_with_shuffle_repeat() {
175        let payload = json!({
176            "is_playing": true,
177            "progress_ms": 0,
178            "shuffle_state": true,
179            "repeat_state": "track"
180        });
181        let item = json!({
182            "name": "Track",
183            "artists": [{ "name": "Artist" }],
184            "duration_ms": 120000
185        });
186        format_player_status(&payload, &item);
187    }
188
189    #[test]
190    fn format_player_status_minimal() {
191        let payload = json!({});
192        let item = json!({});
193        format_player_status(&payload, &item);
194    }
195
196    #[test]
197    fn format_queue_with_items() {
198        let payload = json!({
199            "currently_playing": {
200                "name": "Current Song",
201                "artists": [{ "name": "Current Artist" }]
202            },
203            "queue": [
204                { "name": "Next Song 1", "artists": [{ "name": "Artist 1" }] },
205                { "name": "Next Song 2", "artists": [{ "name": "Artist 2" }] }
206            ]
207        });
208        format_queue(&payload);
209    }
210
211    #[test]
212    fn format_queue_empty() {
213        let payload = json!({
214            "currently_playing": null,
215            "queue": []
216        });
217        format_queue(&payload);
218    }
219
220    #[test]
221    fn format_queue_large() {
222        let queue: Vec<Value> = (0..15)
223            .map(|i| json!({ "name": format!("Track {}", i), "artists": [{ "name": "Artist" }] }))
224            .collect();
225        let payload = json!({
226            "currently_playing": { "name": "Current", "artists": [{ "name": "Artist" }] },
227            "queue": queue
228        });
229        format_queue(&payload);
230    }
231
232    #[test]
233    fn format_queue_no_currently_playing() {
234        let payload = json!({
235            "queue": [
236                { "name": "Queued Track", "artists": [{ "name": "Artist" }] }
237            ]
238        });
239        format_queue(&payload);
240    }
241
242    #[test]
243    fn format_devices_multiple() {
244        let devices = vec![
245            json!({
246                "name": "Living Room Speaker",
247                "type": "Speaker",
248                "is_active": true,
249                "volume_percent": 80
250            }),
251            json!({
252                "name": "Phone",
253                "type": "Smartphone",
254                "is_active": false,
255                "volume_percent": 50
256            }),
257        ];
258        format_devices(&devices);
259    }
260
261    #[test]
262    fn format_devices_empty() {
263        let devices: Vec<Value> = vec![];
264        format_devices(&devices);
265    }
266
267    #[test]
268    fn format_devices_without_volume() {
269        let devices = vec![json!({
270            "name": "TV",
271            "type": "TV",
272            "is_active": false
273        })];
274        format_devices(&devices);
275    }
276
277    #[test]
278    fn format_devices_minimal() {
279        let devices = vec![json!({})];
280        format_devices(&devices);
281    }
282}