spotify_cli/io/formatters/
track.rs1use 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}