mod filters;
mod pins;
mod playback;
mod scoring;
use crate::endpoints::episodes::get_several_episodes;
use crate::endpoints::search;
use crate::endpoints::user::get_current_user;
use crate::http::api::SpotifyApi;
use crate::io::output::{ErrorKind, Response};
use crate::storage::config::Config;
use serde_json::Value;
use super::{SearchFilters, with_client};
use filters::{extract_first_uri, filter_exact_matches, filter_ghost_entries};
use pins::search_pins;
use playback::play_uri;
use scoring::add_fuzzy_scores;
pub struct SearchOptions {
pub limit: u8,
pub pins_only: bool,
pub exact: bool,
pub filters: SearchFilters,
pub play: bool,
pub sort: bool,
}
async fn enrich_episodes(client: &SpotifyApi, results: &mut Value) {
let episodes = match results
.get("episodes")
.and_then(|e| e.get("items"))
.and_then(|i| i.as_array())
{
Some(eps) => eps,
None => return,
};
let ids: Vec<String> = episodes
.iter()
.filter(|ep| ep.get("show").is_none() || ep.get("show").unwrap().is_null())
.filter_map(|ep| ep.get("id").and_then(|id| id.as_str()).map(String::from))
.collect();
if ids.is_empty() {
return;
}
let full_episodes = match get_several_episodes::get_several_episodes(client, &ids).await {
Ok(Some(data)) => data,
_ => return,
};
let mut show_cache: std::collections::HashMap<String, Value> = std::collections::HashMap::new();
let mut episode_show_map: std::collections::HashMap<String, String> =
std::collections::HashMap::new();
if let Some(eps) = full_episodes.get("episodes").and_then(|e| e.as_array()) {
for ep in eps {
if let (Some(ep_id), Some(show)) =
(ep.get("id").and_then(|id| id.as_str()), ep.get("show"))
&& let Some(show_id) = show.get("id").and_then(|id| id.as_str())
{
show_cache
.entry(show_id.to_string())
.or_insert_with(|| show.clone());
episode_show_map.insert(ep_id.to_string(), show_id.to_string());
}
}
}
if let Some(items) = results
.get_mut("episodes")
.and_then(|e| e.get_mut("items"))
.and_then(|i| i.as_array_mut())
{
for ep in items.iter_mut() {
if let Some(ep_id) = ep.get("id").and_then(|id| id.as_str())
&& let Some(show_id) = episode_show_map.get(ep_id)
&& let Some(show) = show_cache.get(show_id)
{
ep.as_object_mut()
.map(|obj| obj.insert("show".to_string(), show.clone()));
}
}
}
}
pub async fn search_command(query: &str, types: &[String], options: SearchOptions) -> Response {
let full_query = options.filters.build_query(query);
if full_query.is_empty() {
return Response::err(
400,
"Search query is empty. Provide a query or use filters (--artist, --album, etc.)",
ErrorKind::Validation,
);
}
let pin_results = search_pins(query);
if options.pins_only {
return Response::success_with_payload(
200,
format!("Found {} pinned result(s)", pin_results.len()),
serde_json::json!({
"pins": pin_results,
"spotify": null
}),
);
}
let query = query.to_string();
let types = types.to_vec();
with_client(|client| async move {
let type_strs: Vec<&str> = if types.is_empty() {
search::SEARCH_TYPES.to_vec()
} else {
types.iter().map(|s| s.as_str()).collect()
};
let config = Config::load().ok();
let fuzzy_config = config
.as_ref()
.map(|c| c.fuzzy().clone())
.unwrap_or_default();
let sort_by_score =
options.sort || config.as_ref().map(|c| c.sort_by_score()).unwrap_or(false);
let market = match get_current_user::get_current_user(&client).await {
Ok(Some(user)) => user
.get("country")
.and_then(|c| c.as_str())
.map(String::from),
_ => None,
};
match search::search(
&client,
&full_query,
Some(&type_strs),
Some(options.limit),
market.as_deref(),
)
.await
{
Ok(Some(mut spotify_results)) => {
filter_ghost_entries(&mut spotify_results);
enrich_episodes(&client, &mut spotify_results).await;
if options.exact {
filter_exact_matches(&mut spotify_results, &query);
}
add_fuzzy_scores(&mut spotify_results, &query, &fuzzy_config, sort_by_score);
if options.play {
if let Some(uri) = extract_first_uri(&pin_results, &spotify_results) {
return play_uri(&client, &uri).await;
} else {
return Response::err(404, "No results to play", ErrorKind::NotFound);
}
}
Response::success_with_payload(
200,
format!("Found {} pinned + Spotify results", pin_results.len()),
serde_json::json!({
"pins": pin_results,
"spotify": spotify_results
}),
)
}
Ok(None) => {
if options.play
&& !pin_results.is_empty()
&& let Some(uri) = extract_first_uri(&pin_results, &serde_json::json!({}))
{
return play_uri(&client, &uri).await;
}
Response::success_with_payload(
200,
format!("Found {} pinned result(s)", pin_results.len()),
serde_json::json!({
"pins": pin_results,
"spotify": {}
}),
)
}
Err(e) => Response::from_http_error(&e, "Search failed"),
}
})
.await
}