spotify_cli/types/
artist.rs

1//! Artist types from Spotify API.
2
3use serde::{Deserialize, Serialize};
4
5use super::common::{ExternalUrls, Followers, Image};
6
7/// Simplified artist object (used in nested contexts).
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct ArtistSimplified {
10    /// External URLs for the artist.
11    pub external_urls: Option<ExternalUrls>,
12    /// Spotify URL for the artist.
13    pub href: Option<String>,
14    /// Spotify ID.
15    pub id: String,
16    /// Artist name.
17    pub name: String,
18    /// Object type (always "artist").
19    #[serde(rename = "type")]
20    pub item_type: String,
21    /// Spotify URI.
22    pub uri: String,
23}
24
25/// Full artist object.
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct Artist {
28    /// External URLs.
29    pub external_urls: Option<ExternalUrls>,
30    /// Follower information.
31    pub followers: Option<Followers>,
32    /// Genres associated with the artist.
33    pub genres: Option<Vec<String>>,
34    /// Spotify URL.
35    pub href: Option<String>,
36    /// Spotify ID.
37    pub id: String,
38    /// Artist images.
39    pub images: Option<Vec<Image>>,
40    /// Artist name.
41    pub name: String,
42    /// Popularity score (0-100).
43    pub popularity: Option<u32>,
44    /// Object type (always "artist").
45    #[serde(rename = "type")]
46    pub item_type: String,
47    /// Spotify URI.
48    pub uri: String,
49}
50
51impl Artist {
52    /// Get the largest image URL if available.
53    pub fn image_url(&self) -> Option<&str> {
54        self.images
55            .as_ref()
56            .and_then(|imgs| imgs.first())
57            .map(|img| img.url.as_str())
58    }
59}
60
61/// Response for artist's top tracks.
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct ArtistTopTracksResponse {
64    /// List of top tracks.
65    pub tracks: Vec<super::track::Track>,
66}
67
68/// Response for related artists.
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct RelatedArtistsResponse {
71    /// List of related artists.
72    pub artists: Vec<Artist>,
73}
74
75/// Response wrapper for followed artists (cursor-paginated).
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct FollowedArtistsResponse {
78    /// The artists container.
79    pub artists: FollowedArtistsCursored,
80}
81
82/// Cursor-paginated artists list.
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct FollowedArtistsCursored {
85    /// URL to the API endpoint.
86    pub href: Option<String>,
87    /// Maximum number of items.
88    pub limit: Option<u32>,
89    /// URL to the next page.
90    pub next: Option<String>,
91    /// Cursors for pagination.
92    pub cursors: Option<super::common::Cursors>,
93    /// Total count.
94    pub total: Option<u32>,
95    /// The followed artists.
96    pub items: Vec<Artist>,
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use serde_json::json;
103
104    #[test]
105    fn artist_simplified_deserializes() {
106        let json = json!({
107            "id": "abc123",
108            "name": "Test Artist",
109            "type": "artist",
110            "uri": "spotify:artist:abc123"
111        });
112        let artist: ArtistSimplified = serde_json::from_value(json).unwrap();
113        assert_eq!(artist.id, "abc123");
114        assert_eq!(artist.name, "Test Artist");
115    }
116
117    #[test]
118    fn artist_full_deserializes() {
119        let json = json!({
120            "id": "abc123",
121            "name": "Test Artist",
122            "type": "artist",
123            "uri": "spotify:artist:abc123",
124            "genres": ["rock", "alternative"],
125            "popularity": 75,
126            "followers": {"total": 1000000}
127        });
128        let artist: Artist = serde_json::from_value(json).unwrap();
129        assert_eq!(artist.id, "abc123");
130        assert_eq!(artist.popularity, Some(75));
131        assert_eq!(artist.genres.as_ref().unwrap().len(), 2);
132    }
133
134    #[test]
135    fn artist_image_url_returns_first_image() {
136        let json = json!({
137            "id": "abc123",
138            "name": "Test Artist",
139            "type": "artist",
140            "uri": "spotify:artist:abc123",
141            "images": [
142                {"url": "https://large.jpg", "height": 640, "width": 640},
143                {"url": "https://medium.jpg", "height": 300, "width": 300}
144            ]
145        });
146        let artist: Artist = serde_json::from_value(json).unwrap();
147        assert_eq!(artist.image_url(), Some("https://large.jpg"));
148    }
149
150    #[test]
151    fn artist_image_url_returns_none_when_no_images() {
152        let json = json!({
153            "id": "abc123",
154            "name": "Test Artist",
155            "type": "artist",
156            "uri": "spotify:artist:abc123"
157        });
158        let artist: Artist = serde_json::from_value(json).unwrap();
159        assert!(artist.image_url().is_none());
160    }
161
162    #[test]
163    fn artist_top_tracks_response_deserializes() {
164        let json = json!({
165            "tracks": []
166        });
167        let resp: ArtistTopTracksResponse = serde_json::from_value(json).unwrap();
168        assert!(resp.tracks.is_empty());
169    }
170
171    #[test]
172    fn related_artists_response_deserializes() {
173        let json = json!({
174            "artists": []
175        });
176        let resp: RelatedArtistsResponse = serde_json::from_value(json).unwrap();
177        assert!(resp.artists.is_empty());
178    }
179
180    #[test]
181    fn followed_artists_response_deserializes() {
182        let json = json!({
183            "artists": {
184                "items": [],
185                "limit": 20,
186                "total": 0
187            }
188        });
189        let resp: FollowedArtistsResponse = serde_json::from_value(json).unwrap();
190        assert!(resp.artists.items.is_empty());
191    }
192}