spotify_cli/cli/commands/search/
mod.rs1mod filters;
10mod pins;
11mod playback;
12mod scoring;
13
14use crate::endpoints::episodes::get_several_episodes;
15use crate::endpoints::search;
16use crate::endpoints::user::get_current_user;
17use crate::http::api::SpotifyApi;
18use crate::io::output::{ErrorKind, Response};
19use crate::storage::config::Config;
20use serde_json::Value;
21
22use super::{SearchFilters, with_client};
23use filters::{extract_first_uri, filter_exact_matches, filter_ghost_entries};
24use pins::search_pins;
25use playback::play_uri;
26use scoring::add_fuzzy_scores;
27
28pub struct SearchOptions {
30 pub limit: u8,
31 pub pins_only: bool,
32 pub exact: bool,
33 pub filters: SearchFilters,
34 pub play: bool,
35 pub sort: bool,
36}
37
38async fn enrich_episodes(client: &SpotifyApi, results: &mut Value) {
41 let episodes = match results
42 .get("episodes")
43 .and_then(|e| e.get("items"))
44 .and_then(|i| i.as_array())
45 {
46 Some(eps) => eps,
47 None => return,
48 };
49
50 let ids: Vec<String> = episodes
52 .iter()
53 .filter(|ep| ep.get("show").is_none() || ep.get("show").unwrap().is_null())
54 .filter_map(|ep| ep.get("id").and_then(|id| id.as_str()).map(String::from))
55 .collect();
56
57 if ids.is_empty() {
58 return;
59 }
60
61 let full_episodes = match get_several_episodes::get_several_episodes(client, &ids).await {
63 Ok(Some(data)) => data,
64 _ => return,
65 };
66
67 let mut show_cache: std::collections::HashMap<String, Value> = std::collections::HashMap::new();
69 let mut episode_show_map: std::collections::HashMap<String, String> =
71 std::collections::HashMap::new();
72
73 if let Some(eps) = full_episodes.get("episodes").and_then(|e| e.as_array()) {
74 for ep in eps {
75 if let (Some(ep_id), Some(show)) =
76 (ep.get("id").and_then(|id| id.as_str()), ep.get("show"))
77 && let Some(show_id) = show.get("id").and_then(|id| id.as_str())
78 {
79 show_cache
81 .entry(show_id.to_string())
82 .or_insert_with(|| show.clone());
83 episode_show_map.insert(ep_id.to_string(), show_id.to_string());
85 }
86 }
87 }
88
89 if let Some(items) = results
91 .get_mut("episodes")
92 .and_then(|e| e.get_mut("items"))
93 .and_then(|i| i.as_array_mut())
94 {
95 for ep in items.iter_mut() {
96 if let Some(ep_id) = ep.get("id").and_then(|id| id.as_str())
97 && let Some(show_id) = episode_show_map.get(ep_id)
98 && let Some(show) = show_cache.get(show_id)
99 {
100 ep.as_object_mut()
101 .map(|obj| obj.insert("show".to_string(), show.clone()));
102 }
103 }
104 }
105}
106
107pub async fn search_command(query: &str, types: &[String], options: SearchOptions) -> Response {
108 let full_query = options.filters.build_query(query);
110
111 if full_query.is_empty() {
113 return Response::err(
114 400,
115 "Search query is empty. Provide a query or use filters (--artist, --album, etc.)",
116 ErrorKind::Validation,
117 );
118 }
119
120 let pin_results = search_pins(query);
122
123 if options.pins_only {
124 return Response::success_with_payload(
125 200,
126 format!("Found {} pinned result(s)", pin_results.len()),
127 serde_json::json!({
128 "pins": pin_results,
129 "spotify": null
130 }),
131 );
132 }
133
134 let query = query.to_string();
136 let types = types.to_vec();
137
138 with_client(|client| async move {
139 let type_strs: Vec<&str> = if types.is_empty() {
140 search::SEARCH_TYPES.to_vec()
141 } else {
142 types.iter().map(|s| s.as_str()).collect()
143 };
144
145 let config = Config::load().ok();
147 let fuzzy_config = config
148 .as_ref()
149 .map(|c| c.fuzzy().clone())
150 .unwrap_or_default();
151 let sort_by_score =
153 options.sort || config.as_ref().map(|c| c.sort_by_score()).unwrap_or(false);
154
155 let market = match get_current_user::get_current_user(&client).await {
157 Ok(Some(user)) => user
158 .get("country")
159 .and_then(|c| c.as_str())
160 .map(String::from),
161 _ => None,
162 };
163
164 match search::search(
165 &client,
166 &full_query,
167 Some(&type_strs),
168 Some(options.limit),
169 market.as_deref(),
170 )
171 .await
172 {
173 Ok(Some(mut spotify_results)) => {
174 filter_ghost_entries(&mut spotify_results);
175
176 enrich_episodes(&client, &mut spotify_results).await;
178
179 if options.exact {
180 filter_exact_matches(&mut spotify_results, &query);
181 }
182
183 add_fuzzy_scores(&mut spotify_results, &query, &fuzzy_config, sort_by_score);
184
185 if options.play {
186 if let Some(uri) = extract_first_uri(&pin_results, &spotify_results) {
187 return play_uri(&client, &uri).await;
188 } else {
189 return Response::err(404, "No results to play", ErrorKind::NotFound);
190 }
191 }
192
193 Response::success_with_payload(
194 200,
195 format!("Found {} pinned + Spotify results", pin_results.len()),
196 serde_json::json!({
197 "pins": pin_results,
198 "spotify": spotify_results
199 }),
200 )
201 }
202 Ok(None) => {
203 if options.play
204 && !pin_results.is_empty()
205 && let Some(uri) = extract_first_uri(&pin_results, &serde_json::json!({}))
206 {
207 return play_uri(&client, &uri).await;
208 }
209 Response::success_with_payload(
210 200,
211 format!("Found {} pinned result(s)", pin_results.len()),
212 serde_json::json!({
213 "pins": pin_results,
214 "spotify": {}
215 }),
216 )
217 }
218 Err(e) => Response::from_http_error(&e, "Search failed"),
219 }
220 })
221 .await
222}