use crate::Error;
use crate::List;
use crate::Playlist;
use crate::Resource;
use crate::ResourceType;
use crate::TIDAL_API_BASE_URL;
use crate::TidalClient;
use crate::album::Album;
use crate::artist::Artist;
use crate::track::Track;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SearchQuery<'a> {
pub query: &'a str,
pub offset: Option<u32>,
pub limit: Option<u32>,
pub include_contributions: Option<bool>,
pub include_did_you_mean: Option<bool>,
pub include_user_playlists: Option<bool>,
pub supports_user_data: Option<bool>,
pub search_types: Option<Vec<ResourceType>>,
}
impl<'a> SearchQuery<'a> {
pub fn new(query: &'a str) -> Self {
Self {
query,
offset: None,
limit: None,
include_contributions: None,
include_did_you_mean: None,
include_user_playlists: None,
supports_user_data: None,
search_types: None,
}
}
}
impl TidalClient {
#[allow(clippy::too_many_arguments)]
pub async fn search<'a>(&self, search: SearchQuery<'a>) -> Result<SearchResults, Error> {
let url = format!("{TIDAL_API_BASE_URL}/search/top-hits");
let mut params = serde_json::json!({ "query": search.query });
let search_types_string = {
match search.search_types {
Some(types) => {
let mut types_str = String::new();
for resource_type in types {
match resource_type {
ResourceType::Artist => types_str.push_str("ARTISTS"),
ResourceType::Album => types_str.push_str("ALBUMS"),
ResourceType::Track => types_str.push_str("TRACKS"),
ResourceType::Video => types_str.push_str("VIDEOS"),
ResourceType::Playlist => types_str.push_str("PLAYLISTS"),
ResourceType::UserProfile => types_str.push_str("USER_PROFILES"),
}
types_str.push(',');
}
types_str.pop();
types_str
}
None => "ARTISTS,ALBUMS,TRACKS,PLAYLISTS".to_string(),
}
};
params["types"] = Value::String(search_types_string.clone());
params["countryCode"] = Value::String(self.get_country_code());
params["locale"] = Value::String(self.get_locale());
params["deviceType"] = Value::String(self.get_device_type().as_ref().to_string());
if let Some(offset) = search.offset {
params["offset"] = Value::Number(offset.into());
}
if let Some(limit) = search.limit {
params["limit"] = Value::Number(limit.into());
}
if let Some(include_contributions) = search.include_contributions {
params["includeContributions"] = Value::Bool(include_contributions);
}
if let Some(include_did_you_mean) = search.include_did_you_mean {
params["includeDidYouMean"] = Value::Bool(include_did_you_mean);
}
if let Some(include_user_playlists) = search.include_user_playlists {
params["includeUserPlaylists"] = Value::Bool(include_user_playlists);
}
if let Some(supports_user_data) = search.supports_user_data {
params["supportsUserData"] = Value::Bool(supports_user_data);
}
let resp: SearchResults = self
.do_request(Method::GET, &url, Some(params), None)
.await?;
Ok(resp)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchResults {
#[serde(skip_serializing_if = "List::is_empty")]
#[serde(default)]
pub albums: List<Album>,
#[serde(skip_serializing_if = "List::is_empty")]
#[serde(default)]
pub artists: List<Artist>,
#[serde(skip_serializing_if = "List::is_empty")]
#[serde(default)]
pub tracks: List<Track>,
#[serde(skip_serializing_if = "List::is_empty")]
#[serde(default)]
pub playlists: List<Playlist>,
#[serde(skip_serializing_if = "List::is_empty")]
#[serde(default)]
pub user_profiles: List<serde_json::Value>,
#[serde(skip_serializing_if = "List::is_empty")]
#[serde(default)]
pub videos: List<serde_json::Value>,
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default)]
#[serde(rename = "topHits")]
pub top_hits: Vec<Resource>,
}
impl SearchResults {
pub fn max_total(&self) -> usize {
self.albums
.total
.max(self.artists.total)
.max(self.tracks.total)
.max(self.playlists.total)
.max(self.user_profiles.total)
.max(self.videos.total)
}
}