spotify_cli/io/formatters/
track.rs

1//! Track formatting functions
2
3use serde_json::Value;
4
5use crate::io::common::{extract_artist_names, format_duration, print_table, truncate};
6
7pub fn format_track_detail(payload: &Value) {
8    let name = payload
9        .get("name")
10        .and_then(|v| v.as_str())
11        .unwrap_or("Unknown");
12    let artists = extract_artist_names(payload);
13    let album = payload
14        .get("album")
15        .and_then(|a| a.get("name"))
16        .and_then(|v| v.as_str())
17        .unwrap_or("Unknown");
18    let duration_ms = payload
19        .get("duration_ms")
20        .and_then(|v| v.as_u64())
21        .unwrap_or(0);
22    let duration = format_duration(duration_ms);
23    let popularity = payload
24        .get("popularity")
25        .and_then(|v| v.as_u64())
26        .unwrap_or(0);
27    let explicit = payload
28        .get("explicit")
29        .and_then(|v| v.as_bool())
30        .unwrap_or(false);
31    let track_number = payload.get("track_number").and_then(|v| v.as_u64());
32    let uri = payload.get("uri").and_then(|v| v.as_str()).unwrap_or("");
33
34    println!("{}", name);
35    println!("  Artist: {}", artists);
36    println!("  Album: {}", album);
37    println!("  Duration: {}", duration);
38    if let Some(track_num) = track_number {
39        println!("  Track: #{}", track_num);
40    }
41    println!("  Popularity: {}%", popularity);
42    if explicit {
43        println!("  Explicit: Yes");
44    }
45    if !uri.is_empty() {
46        println!("  URI: {}", uri);
47    }
48}
49
50pub fn format_play_history(items: &[Value]) {
51    println!("Recently Played:");
52    for (i, item) in items.iter().take(10).enumerate() {
53        if let Some(track) = item.get("track") {
54            let name = track
55                .get("name")
56                .and_then(|v| v.as_str())
57                .unwrap_or("Unknown");
58            let artists = extract_artist_names(track);
59            println!("  {}. {} - {}", i + 1, name, artists);
60        }
61    }
62}
63
64pub fn format_top_tracks(tracks: &[Value], message: &str) {
65    println!("{}:", message);
66    println!();
67    print_table(
68        "Your Top Tracks",
69        &["#", "Title", "Artist", "Album"],
70        &tracks
71            .iter()
72            .enumerate()
73            .map(|(i, track)| {
74                let name = track
75                    .get("name")
76                    .and_then(|v| v.as_str())
77                    .unwrap_or("Unknown");
78                let artists = extract_artist_names(track);
79                let album = track
80                    .get("album")
81                    .and_then(|a| a.get("name"))
82                    .and_then(|v| v.as_str())
83                    .unwrap_or("Unknown");
84                vec![
85                    (i + 1).to_string(),
86                    truncate(name, 25),
87                    truncate(&artists, 18),
88                    truncate(album, 20),
89                ]
90            })
91            .collect::<Vec<_>>(),
92        &[3, 25, 18, 20],
93    );
94}
95
96pub fn format_artist_top_tracks(tracks: &[Value]) {
97    if tracks.is_empty() {
98        println!("No top tracks found.");
99        return;
100    }
101    println!("Top Tracks:");
102    for (i, track) in tracks.iter().enumerate() {
103        let name = track
104            .get("name")
105            .and_then(|v| v.as_str())
106            .unwrap_or("Unknown");
107        let album = track
108            .get("album")
109            .and_then(|a| a.get("name"))
110            .and_then(|v| v.as_str())
111            .unwrap_or("Unknown");
112        let popularity = track
113            .get("popularity")
114            .and_then(|v| v.as_u64())
115            .unwrap_or(0);
116        println!("  {}. {} - {} ({}%)", i + 1, name, album, popularity);
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use serde_json::json;
124
125    #[test]
126    fn format_track_detail_full() {
127        let payload = json!({
128            "name": "Test Track",
129            "artists": [{ "name": "Test Artist" }],
130            "album": { "name": "Test Album" },
131            "duration_ms": 210000,
132            "popularity": 75,
133            "explicit": true,
134            "track_number": 5,
135            "uri": "spotify:track:abc123"
136        });
137        format_track_detail(&payload);
138    }
139
140    #[test]
141    fn format_track_detail_minimal() {
142        let payload = json!({});
143        format_track_detail(&payload);
144    }
145
146    #[test]
147    fn format_track_detail_without_explicit() {
148        let payload = json!({
149            "name": "Clean Track",
150            "artists": [{ "name": "Artist" }],
151            "album": { "name": "Album" },
152            "duration_ms": 180000,
153            "popularity": 50,
154            "explicit": false
155        });
156        format_track_detail(&payload);
157    }
158
159    #[test]
160    fn format_track_detail_multiple_artists() {
161        let payload = json!({
162            "name": "Collab Track",
163            "artists": [
164                { "name": "Artist One" },
165                { "name": "Artist Two" },
166                { "name": "Artist Three" }
167            ],
168            "album": { "name": "Album" },
169            "duration_ms": 240000
170        });
171        format_track_detail(&payload);
172    }
173
174    #[test]
175    fn format_play_history_with_tracks() {
176        let items = vec![
177            json!({ "track": { "name": "Track 1", "artists": [{ "name": "Artist 1" }] } }),
178            json!({ "track": { "name": "Track 2", "artists": [{ "name": "Artist 2" }] } }),
179        ];
180        format_play_history(&items);
181    }
182
183    #[test]
184    fn format_play_history_empty() {
185        let items: Vec<Value> = vec![];
186        format_play_history(&items);
187    }
188
189    #[test]
190    fn format_play_history_more_than_ten() {
191        let items: Vec<Value> = (0..15)
192            .map(|i| json!({ "track": { "name": format!("Track {}", i), "artists": [{ "name": "Artist" }] } }))
193            .collect();
194        format_play_history(&items);
195    }
196
197    #[test]
198    fn format_top_tracks_with_data() {
199        let tracks = vec![
200            json!({
201                "name": "Popular Song",
202                "artists": [{ "name": "Famous Artist" }],
203                "album": { "name": "Hit Album" }
204            }),
205            json!({
206                "name": "Another Hit",
207                "artists": [{ "name": "Another Artist" }],
208                "album": { "name": "Great Album" }
209            }),
210        ];
211        format_top_tracks(&tracks, "Top Tracks This Month");
212    }
213
214    #[test]
215    fn format_top_tracks_empty() {
216        let tracks: Vec<Value> = vec![];
217        format_top_tracks(&tracks, "No Tracks Found");
218    }
219
220    #[test]
221    fn format_artist_top_tracks_with_data() {
222        let tracks = vec![
223            json!({
224                "name": "Best Song",
225                "album": { "name": "Greatest Hits" },
226                "popularity": 95
227            }),
228            json!({
229                "name": "Second Best",
230                "album": { "name": "New Album" },
231                "popularity": 80
232            }),
233        ];
234        format_artist_top_tracks(&tracks);
235    }
236
237    #[test]
238    fn format_artist_top_tracks_empty() {
239        let tracks: Vec<Value> = vec![];
240        format_artist_top_tracks(&tracks);
241    }
242
243    #[test]
244    fn format_artist_top_tracks_minimal_data() {
245        let tracks = vec![json!({})];
246        format_artist_top_tracks(&tracks);
247    }
248}