spotify_cli/endpoints/
search.rs1use crate::http::api::SpotifyApi;
2use crate::http::client::HttpError;
3use crate::http::endpoints::Endpoint;
4use serde_json::Value;
5
6pub const SEARCH_TYPES: &[&str] = &[
8 "track",
9 "artist",
10 "album",
11 "playlist",
12 "show",
13 "episode",
14 "audiobook",
15];
16
17pub const SEARCH_RESULT_KEYS: &[&str] = &[
19 "tracks",
20 "artists",
21 "albums",
22 "playlists",
23 "shows",
24 "episodes",
25 "audiobooks",
26];
27
28pub async fn search(
61 client: &SpotifyApi,
62 query: &str,
63 types: Option<&[&str]>,
64 limit: Option<u8>,
65 market: Option<&str>,
66) -> Result<Option<Value>, HttpError> {
67 let type_str = types
68 .map(|t| t.join(","))
69 .unwrap_or_else(|| SEARCH_TYPES.join(","));
70
71 let requested_limit = limit.unwrap_or(20).min(50);
72
73 let api_limit = if requested_limit == 1 {
77 2
78 } else {
79 requested_limit
80 };
81 let needs_truncation = requested_limit == 1;
82
83 let endpoint = Endpoint::Search {
84 query,
85 types: &type_str,
86 limit: api_limit,
87 market,
88 }
89 .path();
90
91 let response = client.get(&endpoint).await?;
92
93 if needs_truncation && let Some(mut data) = response {
95 truncate_search_results(&mut data);
96 return Ok(Some(data));
97 }
98
99 Ok(response)
100}
101
102fn truncate_search_results(data: &mut Value) {
108 for result_type in SEARCH_RESULT_KEYS {
109 if let Some(container) = data.get_mut(result_type) {
110 if let Some(items) = container.get_mut("items")
111 && let Some(arr) = items.as_array_mut()
112 {
113 arr.truncate(1);
114 }
115 if let Some(limit) = container.get_mut("limit") {
117 *limit = Value::Number(1.into());
118 }
119 }
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use serde_json::json;
127
128 #[test]
129 fn search_types_has_expected_types() {
130 assert!(SEARCH_TYPES.contains(&"track"));
131 assert!(SEARCH_TYPES.contains(&"artist"));
132 assert!(SEARCH_TYPES.contains(&"album"));
133 assert!(SEARCH_TYPES.contains(&"playlist"));
134 assert!(SEARCH_TYPES.contains(&"show"));
135 assert!(SEARCH_TYPES.contains(&"episode"));
136 assert!(SEARCH_TYPES.contains(&"audiobook"));
137 }
138
139 #[test]
140 fn search_types_count() {
141 assert_eq!(SEARCH_TYPES.len(), 7);
142 }
143
144 #[test]
145 fn search_result_keys_are_plural() {
146 for key in SEARCH_RESULT_KEYS {
147 assert!(key.ends_with('s'), "{} should be plural", key);
148 }
149 }
150
151 #[test]
152 fn search_result_keys_count() {
153 assert_eq!(SEARCH_RESULT_KEYS.len(), 7);
154 }
155
156 #[test]
157 fn truncate_search_results_works() {
158 let mut data = json!({
159 "tracks": {
160 "items": [
161 {"name": "track1"},
162 {"name": "track2"},
163 {"name": "track3"}
164 ],
165 "limit": 3
166 },
167 "artists": {
168 "items": [
169 {"name": "artist1"},
170 {"name": "artist2"}
171 ],
172 "limit": 2
173 }
174 });
175
176 truncate_search_results(&mut data);
177
178 let tracks = data["tracks"]["items"].as_array().unwrap();
179 assert_eq!(tracks.len(), 1);
180 assert_eq!(tracks[0]["name"], "track1");
181
182 let artists = data["artists"]["items"].as_array().unwrap();
183 assert_eq!(artists.len(), 1);
184 assert_eq!(artists[0]["name"], "artist1");
185
186 assert_eq!(data["tracks"]["limit"], 1);
187 assert_eq!(data["artists"]["limit"], 1);
188 }
189
190 #[test]
191 fn truncate_handles_missing_keys() {
192 let mut data = json!({
193 "unknown_key": {
194 "items": [1, 2, 3]
195 }
196 });
197
198 truncate_search_results(&mut data);
199
200 assert_eq!(data["unknown_key"]["items"].as_array().unwrap().len(), 3);
202 }
203
204 #[test]
205 fn truncate_handles_empty_items() {
206 let mut data = json!({
207 "tracks": {
208 "items": [],
209 "limit": 0
210 }
211 });
212
213 truncate_search_results(&mut data);
214
215 assert_eq!(data["tracks"]["items"].as_array().unwrap().len(), 0);
216 }
217
218 #[test]
219 fn truncate_handles_single_item() {
220 let mut data = json!({
221 "albums": {
222 "items": [{"name": "album1"}],
223 "limit": 1
224 }
225 });
226
227 truncate_search_results(&mut data);
228
229 assert_eq!(data["albums"]["items"].as_array().unwrap().len(), 1);
230 }
231}