spotify_cli/spotify/
search.rs

1use anyhow::bail;
2use reqwest::blocking::Client as HttpClient;
3use serde::Deserialize;
4
5use crate::domain::search::{SearchItem, SearchResults, SearchType};
6use crate::error::Result;
7use crate::spotify::auth::AuthService;
8use crate::spotify::base::api_base;
9use crate::spotify::error::format_api_error;
10
11
12/// Spotify search API client.
13#[derive(Debug, Clone)]
14pub struct SearchClient {
15    http: HttpClient,
16    auth: AuthService,
17}
18
19impl SearchClient {
20    pub fn new(http: HttpClient, auth: AuthService) -> Self {
21        Self { http, auth }
22    }
23
24    pub fn search(
25        &self,
26        query: &str,
27        kind: SearchType,
28        limit: u32,
29        market_from_token: bool,
30    ) -> Result<SearchResults> {
31        if kind == SearchType::All {
32            let mut items = Vec::new();
33            let kinds = [
34                SearchType::Track,
35                SearchType::Album,
36                SearchType::Artist,
37                SearchType::Playlist,
38            ];
39            for kind in kinds {
40                let results = self.search(query, kind, limit, market_from_token)?;
41                items.extend(results.items);
42            }
43            return Ok(SearchResults {
44                kind: SearchType::All,
45                items,
46            });
47        }
48
49        let token = self.auth.token()?;
50        let kind_param = search_type_param(kind);
51        let mut url = format!(
52            "{}/search?q={}&type={}&limit={}",
53            api_base(),
54            urlencoding::encode(query),
55            kind_param,
56            limit
57        );
58
59        if market_from_token {
60            url.push_str("&market=from_token");
61        }
62
63        let response = self
64            .http
65            .get(url)
66            .bearer_auth(token.access_token)
67            .send()?;
68
69        if !response.status().is_success() {
70            let status = response.status();
71            let body = response.text().unwrap_or_else(|_| "<no body>".to_string());
72            bail!(format_api_error("spotify search failed", status, &body));
73        }
74
75        let payload: SearchResponse = response.json()?;
76        let items = match kind {
77            SearchType::Track => payload
78                .tracks
79                .map(|list| {
80                    list.items
81                        .into_iter()
82                        .filter_map(|item| item)
83                        .map(|item| SearchItem {
84                            id: item.id,
85                            name: item.name,
86                            uri: item.uri,
87                            kind: SearchType::Track,
88                            artists: item.artists.into_iter().map(|artist| artist.name).collect(),
89                            owner: None,
90                            score: None,
91                        })
92                        .collect::<Vec<_>>()
93                })
94                .unwrap_or_default(),
95            SearchType::Album => payload
96                .albums
97                .map(|list| {
98                    list.items
99                        .into_iter()
100                        .filter_map(|item| item)
101                        .map(|item| SearchItem {
102                            id: item.id,
103                            name: item.name,
104                            uri: item.uri,
105                            kind: SearchType::Album,
106                            artists: item.artists.into_iter().map(|artist| artist.name).collect(),
107                            owner: None,
108                            score: None,
109                        })
110                        .collect::<Vec<_>>()
111                })
112                .unwrap_or_default(),
113            SearchType::Artist => payload
114                .artists
115                .map(|list| {
116                    list.items
117                        .into_iter()
118                        .filter_map(|item| item)
119                        .map(|item| SearchItem {
120                            id: item.id,
121                            name: item.name,
122                            uri: item.uri,
123                            kind: SearchType::Artist,
124                            artists: Vec::new(),
125                            owner: None,
126                            score: None,
127                        })
128                        .collect::<Vec<_>>()
129                })
130                .unwrap_or_default(),
131            SearchType::Playlist => payload
132                .playlists
133                .map(|list| {
134                    list.items
135                        .into_iter()
136                        .filter_map(|item| item)
137                        .map(|item| SearchItem {
138                            id: item.id,
139                            name: item.name,
140                            uri: item.uri,
141                            kind: SearchType::Playlist,
142                            artists: Vec::new(),
143                            owner: item.owner.and_then(|owner| owner.display_name),
144                            score: None,
145                        })
146                        .collect::<Vec<_>>()
147                })
148                .unwrap_or_default(),
149            SearchType::All => Vec::new(),
150        };
151
152        Ok(SearchResults { kind, items })
153    }
154}
155
156fn search_type_param(kind: SearchType) -> &'static str {
157    match kind {
158        SearchType::All => "track,album,artist,playlist",
159        SearchType::Track => "track",
160        SearchType::Album => "album",
161        SearchType::Artist => "artist",
162        SearchType::Playlist => "playlist",
163    }
164}
165
166#[derive(Debug, Deserialize)]
167struct SearchResponse {
168    tracks: Option<ItemList<SpotifyTrack>>,
169    albums: Option<ItemList<SpotifyAlbum>>,
170    artists: Option<ItemList<SpotifyArtist>>,
171    playlists: Option<ItemList<SpotifyPlaylist>>,
172}
173
174#[derive(Debug, Deserialize)]
175struct ItemList<T> {
176    items: Vec<Option<T>>,
177}
178
179#[derive(Debug, Deserialize)]
180struct SpotifyTrack {
181    id: String,
182    name: String,
183    uri: String,
184    artists: Vec<SpotifyArtistRef>,
185}
186
187#[derive(Debug, Deserialize)]
188struct SpotifyAlbum {
189    id: String,
190    name: String,
191    uri: String,
192    artists: Vec<SpotifyArtistRef>,
193}
194
195#[derive(Debug, Deserialize)]
196struct SpotifyArtist {
197    id: String,
198    name: String,
199    uri: String,
200}
201
202#[derive(Debug, Deserialize)]
203struct SpotifyPlaylist {
204    id: String,
205    name: String,
206    uri: String,
207    owner: Option<SpotifyOwner>,
208}
209
210#[derive(Debug, Deserialize)]
211struct SpotifyArtistRef {
212    name: String,
213}
214
215#[derive(Debug, Deserialize)]
216struct SpotifyOwner {
217    display_name: Option<String>,
218}