Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions rspotify-model/src/search.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! All object related to search

use serde::{Deserialize, Serialize};
use strum::Display;

use crate::{
FullArtist, FullTrack, Page, SimplifiedAlbum, SimplifiedEpisode, SimplifiedPlaylist,
Expand Down Expand Up @@ -60,3 +61,20 @@ pub enum SearchResult {
#[serde(rename = "episodes")]
Episodes(Page<SimplifiedEpisode>),
}

/// Valid filters to used in the search endpoint's query string
#[derive(Debug, Display, PartialEq, Eq, PartialOrd, Ord)]
#[strum(serialize_all = "snake_case")]
pub enum SearchFilter {
Album,
Artist,
Track,
Year,
Upc,
#[strum(serialize = "tag:hipster")]
TagHipster,
#[strum(serialize = "tag:new")]
TagNew,
Isrc,
Genre,
}
7 changes: 4 additions & 3 deletions src/clients/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -436,9 +436,9 @@ where
/// relevant audio content that is hosted externally.
///
/// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/search)
async fn search(
async fn search<T: Into<String> + Send>(
&self,
q: &str,
q: T,
_type: SearchType,
market: Option<Market>,
include_external: Option<IncludeExternal>,
Expand All @@ -447,8 +447,9 @@ where
) -> ClientResult<SearchResult> {
let limit = limit.map(|s| s.to_string());
let offset = offset.map(|s| s.to_string());
let q: String = q.into();
let params = build_map([
("q", Some(q)),
("q", Some(&q)),
("type", Some(_type.into())),
("market", market.map(Into::into)),
("include_external", include_external.map(Into::into)),
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ mod auth_code;
mod auth_code_pkce;
mod client_creds;
pub mod clients;
pub mod search;
pub mod sync;
mod util;

Expand Down
149 changes: 149 additions & 0 deletions src/search/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
use std::collections::BTreeMap;

use rspotify_model::SearchFilter;

/// Builder used to create search query.
///
/// Note that when calling the same function multiple time, the filter will be the text from the
Comment thread
GartoxFR marked this conversation as resolved.
Outdated
/// last call
///
/// This is converted to the query string using into()
///
/// Exemple
/// ```rust
/// SearchQuery::default()
/// .any("foo")
/// .album("bar")
/// // Filter on album containing "bar" and anything containing "foo"
/// ```
///
/// For more informations on the different filters, look at the [soptify
Comment thread
GartoxFR marked this conversation as resolved.
Outdated
Comment thread
GartoxFR marked this conversation as resolved.
Outdated
/// documentation](https://developer.spotify.com/documentation/web-api/reference/#/operations/search)
#[derive(Debug, Default)]
pub struct SearchQuery<'a> {
no_filter_query: &'a str,
query_map: BTreeMap<SearchFilter, &'a str>,
}
Comment thread
GartoxFR marked this conversation as resolved.

impl<'a> SearchQuery<'a> {
/// Basic filter where the given string can be anything
pub fn any(&mut self, str: &'a str) -> &mut Self {
self.no_filter_query = str;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me, it'd be more intuitive if calling any multiple times appended its contents, instead of replace them.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So any would comport differently from the other fields ? Or should I change this for all fields ?

self
}

pub fn album(&mut self, str: &'a str) -> &mut Self {
self.query_map.insert(SearchFilter::Album, str);
self
}

pub fn artist(&mut self, str: &'a str) -> &mut Self {
self.query_map.insert(SearchFilter::Artist, str);
self
}

pub fn track(&mut self, str: &'a str) -> &mut Self {
self.query_map.insert(SearchFilter::Track, str);
self
}

pub fn year(&mut self, str: &'a str) -> &mut Self {
self.query_map.insert(SearchFilter::Year, str);
self
}

pub fn upc(&mut self, str: &'a str) -> &mut Self {
self.query_map.insert(SearchFilter::Upc, str);
self
}

pub fn tag_new(&mut self) -> &mut Self {
self.query_map.insert(SearchFilter::TagNew, "");
self
}

pub fn tag_hipster(&mut self) -> &mut Self {
self.query_map.insert(SearchFilter::TagHipster, "");
self
}

pub fn isrc(&mut self, str: &'a str) -> &mut Self {
self.query_map.insert(SearchFilter::Isrc, str);
self
}

pub fn genre(&mut self, str: &'a str) -> &mut Self {
self.query_map.insert(SearchFilter::Genre, str);
self
}
}

impl From<&SearchQuery<'_>> for String {
fn from(val: &SearchQuery) -> Self {
let mut rep = val.no_filter_query.to_owned();

if val.query_map.is_empty() {
return rep;
}

rep.push(' ');
rep.push_str(
val.query_map
.iter()
.map(|entry| match entry.0 {
SearchFilter::TagNew | SearchFilter::TagHipster => format!("{} ", entry.0),
_ => format!("{}:{} ", entry.0, entry.1),
})
.collect::<String>()
.trim(),
);

rep
}
}

impl From<&mut SearchQuery<'_>> for String {
fn from(val: &mut SearchQuery) -> Self {
String::from(&(*val))
}
}

impl From<SearchQuery<'_>> for String {
fn from(val: SearchQuery) -> Self {
String::from(&val)
}
}

#[cfg(test)]
mod test {
use super::SearchQuery;

#[test]
fn test_search_query() {
let query: String = SearchQuery::default()
.any("foo bar")
.album("wrong album")
.album("arrival")
.artist("abba")
.tag_new()
.tag_hipster()
.track("foo")
.year("2020")
.upc("bar")
.isrc("foo")
.genre("metal")
.into();

let expected = "foo bar album:arrival artist:abba track:foo year:2020 upc:bar \
tag:hipster tag:new isrc:foo genre:metal";

assert_eq!(expected, query);
}

#[test]
fn test_empty_query() {
let query: String = SearchQuery::default().into();

assert_eq!(query, "");
}
}
14 changes: 11 additions & 3 deletions tests/test_with_oauth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ use rspotify::{
SearchType, ShowId, TimeLimits, TimeRange, TrackId, UserId,
},
prelude::*,
scopes, AuthCodeSpotify, ClientResult, Credentials, OAuth, Token,
scopes,
search::SearchQuery,
AuthCodeSpotify, ClientResult, Credentials, OAuth, Token,
};

use std::env;
Expand Down Expand Up @@ -444,10 +446,16 @@ async fn test_repeat() {
#[maybe_async::test(feature = "__sync", async(feature = "__async", tokio::test))]
#[ignore]
async fn test_search_album() {
let query = "album:arrival artist:abba";
oauth_client()
.await
.search(query, SearchType::Album, None, None, Some(10), Some(0))
.search(
SearchQuery::default().album("arrival").artist("abba"),
SearchType::Album,
None,
None,
Some(10),
Some(0),
)
.await
.unwrap();
}
Expand Down