spotify_cli/types/
track.rs

1//! Track types from Spotify API.
2
3use serde::{Deserialize, Serialize};
4
5use super::album::AlbumSimplified;
6use super::artist::ArtistSimplified;
7use super::common::{ExternalIds, ExternalUrls, LinkedFrom, Restrictions};
8
9/// Simplified track object (used in album tracks, etc.).
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct TrackSimplified {
12    /// Artists who performed the track.
13    pub artists: Option<Vec<ArtistSimplified>>,
14    /// Markets where the track is available.
15    pub available_markets: Option<Vec<String>>,
16    /// Disc number.
17    pub disc_number: Option<u32>,
18    /// Track duration in milliseconds.
19    pub duration_ms: u64,
20    /// Whether the track has explicit lyrics.
21    pub explicit: Option<bool>,
22    /// External URLs.
23    pub external_urls: Option<ExternalUrls>,
24    /// Spotify URL.
25    pub href: Option<String>,
26    /// Spotify ID.
27    pub id: String,
28    /// Whether the track is playable in the user's market.
29    pub is_playable: Option<bool>,
30    /// Linked track info if relinked.
31    pub linked_from: Option<LinkedFrom>,
32    /// Restrictions if any.
33    pub restrictions: Option<Restrictions>,
34    /// Track name.
35    pub name: String,
36    /// Preview URL (30 second preview).
37    pub preview_url: Option<String>,
38    /// Track number on the disc.
39    pub track_number: Option<u32>,
40    /// Object type (always "track").
41    #[serde(rename = "type")]
42    pub item_type: String,
43    /// Spotify URI.
44    pub uri: String,
45    /// Whether the track is a local file.
46    pub is_local: Option<bool>,
47}
48
49/// Full track object.
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct Track {
52    /// Album containing the track.
53    pub album: Option<AlbumSimplified>,
54    /// Artists who performed the track.
55    pub artists: Option<Vec<ArtistSimplified>>,
56    /// Markets where the track is available.
57    pub available_markets: Option<Vec<String>>,
58    /// Disc number.
59    pub disc_number: Option<u32>,
60    /// Track duration in milliseconds.
61    pub duration_ms: u64,
62    /// Whether the track has explicit lyrics.
63    pub explicit: Option<bool>,
64    /// External IDs (ISRC, etc.).
65    pub external_ids: Option<ExternalIds>,
66    /// External URLs.
67    pub external_urls: Option<ExternalUrls>,
68    /// Spotify URL.
69    pub href: Option<String>,
70    /// Spotify ID.
71    pub id: String,
72    /// Whether the track is playable.
73    pub is_playable: Option<bool>,
74    /// Linked track info if relinked.
75    pub linked_from: Option<LinkedFrom>,
76    /// Restrictions if any.
77    pub restrictions: Option<Restrictions>,
78    /// Track name.
79    pub name: String,
80    /// Popularity score (0-100).
81    pub popularity: Option<u32>,
82    /// Preview URL.
83    pub preview_url: Option<String>,
84    /// Track number on the disc.
85    pub track_number: Option<u32>,
86    /// Object type (always "track").
87    #[serde(rename = "type")]
88    pub item_type: String,
89    /// Spotify URI.
90    pub uri: String,
91    /// Whether the track is a local file.
92    pub is_local: Option<bool>,
93}
94
95impl Track {
96    /// Get the primary artist name.
97    pub fn artist_name(&self) -> Option<&str> {
98        self.artists
99            .as_ref()
100            .and_then(|artists| artists.first())
101            .map(|a| a.name.as_str())
102    }
103
104    /// Get all artist names joined.
105    pub fn artist_names(&self) -> String {
106        self.artists
107            .as_ref()
108            .map(|artists| {
109                artists
110                    .iter()
111                    .map(|a| a.name.as_str())
112                    .collect::<Vec<_>>()
113                    .join(", ")
114            })
115            .unwrap_or_default()
116    }
117
118    /// Get the album name.
119    pub fn album_name(&self) -> Option<&str> {
120        self.album.as_ref().map(|a| a.name.as_str())
121    }
122
123    /// Get duration as MM:SS string.
124    pub fn duration_str(&self) -> String {
125        let total_secs = self.duration_ms / 1000;
126        let mins = total_secs / 60;
127        let secs = total_secs % 60;
128        format!("{}:{:02}", mins, secs)
129    }
130
131    /// Get album image URL.
132    pub fn image_url(&self) -> Option<&str> {
133        self.album.as_ref().and_then(|a| a.image_url())
134    }
135}
136
137impl TrackSimplified {
138    /// Get the primary artist name.
139    pub fn artist_name(&self) -> Option<&str> {
140        self.artists
141            .as_ref()
142            .and_then(|artists| artists.first())
143            .map(|a| a.name.as_str())
144    }
145
146    /// Get duration as MM:SS string.
147    pub fn duration_str(&self) -> String {
148        let total_secs = self.duration_ms / 1000;
149        let mins = total_secs / 60;
150        let secs = total_secs % 60;
151        format!("{}:{:02}", mins, secs)
152    }
153}
154
155/// Saved track (wraps track with added_at timestamp).
156#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct SavedTrack {
158    /// When the track was saved.
159    pub added_at: String,
160    /// The track.
161    pub track: Track,
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167    use serde_json::json;
168
169    #[test]
170    fn track_simplified_deserializes() {
171        let json = json!({
172            "id": "track123",
173            "name": "Test Song",
174            "type": "track",
175            "uri": "spotify:track:track123",
176            "duration_ms": 210000,
177            "track_number": 5,
178            "disc_number": 1
179        });
180        let track: TrackSimplified = serde_json::from_value(json).unwrap();
181        assert_eq!(track.id, "track123");
182        assert_eq!(track.name, "Test Song");
183        assert_eq!(track.duration_ms, 210000);
184    }
185
186    #[test]
187    fn track_simplified_artist_name() {
188        let json = json!({
189            "id": "track123",
190            "name": "Test Song",
191            "type": "track",
192            "uri": "spotify:track:track123",
193            "duration_ms": 210000,
194            "artists": [{"id": "artist1", "name": "Test Artist", "type": "artist", "uri": "spotify:artist:artist1"}]
195        });
196        let track: TrackSimplified = serde_json::from_value(json).unwrap();
197        assert_eq!(track.artist_name(), Some("Test Artist"));
198    }
199
200    #[test]
201    fn track_simplified_artist_name_none() {
202        let json = json!({
203            "id": "track123",
204            "name": "Test Song",
205            "type": "track",
206            "uri": "spotify:track:track123",
207            "duration_ms": 210000
208        });
209        let track: TrackSimplified = serde_json::from_value(json).unwrap();
210        assert!(track.artist_name().is_none());
211    }
212
213    #[test]
214    fn track_simplified_duration_str() {
215        let json = json!({
216            "id": "track123",
217            "name": "Test Song",
218            "type": "track",
219            "uri": "spotify:track:track123",
220            "duration_ms": 210000  // 3:30
221        });
222        let track: TrackSimplified = serde_json::from_value(json).unwrap();
223        assert_eq!(track.duration_str(), "3:30");
224    }
225
226    #[test]
227    fn track_full_deserializes() {
228        let json = json!({
229            "id": "track123",
230            "name": "Test Song",
231            "type": "track",
232            "uri": "spotify:track:track123",
233            "duration_ms": 210000,
234            "popularity": 75,
235            "explicit": true
236        });
237        let track: Track = serde_json::from_value(json).unwrap();
238        assert_eq!(track.id, "track123");
239        assert_eq!(track.popularity, Some(75));
240        assert_eq!(track.explicit, Some(true));
241    }
242
243    #[test]
244    fn track_artist_name() {
245        let json = json!({
246            "id": "track123",
247            "name": "Test Song",
248            "type": "track",
249            "uri": "spotify:track:track123",
250            "duration_ms": 210000,
251            "artists": [{"id": "artist1", "name": "Primary Artist", "type": "artist", "uri": "spotify:artist:artist1"}]
252        });
253        let track: Track = serde_json::from_value(json).unwrap();
254        assert_eq!(track.artist_name(), Some("Primary Artist"));
255    }
256
257    #[test]
258    fn track_artist_names_multiple() {
259        let json = json!({
260            "id": "track123",
261            "name": "Test Song",
262            "type": "track",
263            "uri": "spotify:track:track123",
264            "duration_ms": 210000,
265            "artists": [
266                {"id": "artist1", "name": "Artist One", "type": "artist", "uri": "spotify:artist:artist1"},
267                {"id": "artist2", "name": "Artist Two", "type": "artist", "uri": "spotify:artist:artist2"}
268            ]
269        });
270        let track: Track = serde_json::from_value(json).unwrap();
271        assert_eq!(track.artist_names(), "Artist One, Artist Two");
272    }
273
274    #[test]
275    fn track_artist_names_empty() {
276        let json = json!({
277            "id": "track123",
278            "name": "Test Song",
279            "type": "track",
280            "uri": "spotify:track:track123",
281            "duration_ms": 210000
282        });
283        let track: Track = serde_json::from_value(json).unwrap();
284        assert_eq!(track.artist_names(), "");
285    }
286
287    #[test]
288    fn track_album_name() {
289        let json = json!({
290            "id": "track123",
291            "name": "Test Song",
292            "type": "track",
293            "uri": "spotify:track:track123",
294            "duration_ms": 210000,
295            "album": {"id": "album1", "name": "Test Album", "type": "album", "uri": "spotify:album:album1"}
296        });
297        let track: Track = serde_json::from_value(json).unwrap();
298        assert_eq!(track.album_name(), Some("Test Album"));
299    }
300
301    #[test]
302    fn track_album_name_none() {
303        let json = json!({
304            "id": "track123",
305            "name": "Test Song",
306            "type": "track",
307            "uri": "spotify:track:track123",
308            "duration_ms": 210000
309        });
310        let track: Track = serde_json::from_value(json).unwrap();
311        assert!(track.album_name().is_none());
312    }
313
314    #[test]
315    fn track_duration_str() {
316        let json = json!({
317            "id": "track123",
318            "name": "Test Song",
319            "type": "track",
320            "uri": "spotify:track:track123",
321            "duration_ms": 185000  // 3:05
322        });
323        let track: Track = serde_json::from_value(json).unwrap();
324        assert_eq!(track.duration_str(), "3:05");
325    }
326
327    #[test]
328    fn track_image_url() {
329        let json = json!({
330            "id": "track123",
331            "name": "Test Song",
332            "type": "track",
333            "uri": "spotify:track:track123",
334            "duration_ms": 210000,
335            "album": {
336                "id": "album1",
337                "name": "Test Album",
338                "type": "album",
339                "uri": "spotify:album:album1",
340                "images": [{"url": "https://cover.jpg", "height": 640, "width": 640}]
341            }
342        });
343        let track: Track = serde_json::from_value(json).unwrap();
344        assert_eq!(track.image_url(), Some("https://cover.jpg"));
345    }
346
347    #[test]
348    fn saved_track_deserializes() {
349        let json = json!({
350            "added_at": "2024-01-15T10:30:00Z",
351            "track": {
352                "id": "track123",
353                "name": "Test Song",
354                "type": "track",
355                "uri": "spotify:track:track123",
356                "duration_ms": 210000
357            }
358        });
359        let saved: SavedTrack = serde_json::from_value(json).unwrap();
360        assert_eq!(saved.added_at, "2024-01-15T10:30:00Z");
361        assert_eq!(saved.track.id, "track123");
362    }
363}