ytmapi_rs/query/
search.rs

1use super::*;
2use crate::common::SearchSuggestion;
3use crate::parse::SearchResults;
4pub use filteredsearch::*;
5use std::borrow::Cow;
6
7pub mod filteredsearch;
8
9const SPECIALIZED_PLAYLIST_EXACT_MATCH_PARAMS: &str = "BagwQDhAKEAMQBBAJEAU%3D";
10const SPECIALIZED_PLAYLIST_WITH_SUGGESTIONS_PARAMS: &str = "BQgIIAWoMEA4QChADEAQQCRAF";
11const SPECIALIZED_PLAYLIST_PREFIX_PARAMS: &str = "EgeKAQQoA";
12const SEARCH_QUERY_PATH: &str = "search";
13
14// TODO Seal
15// TODO: Add relevant parameters.
16// Implements Default to allow simple implementation of Into<SearchQuery<S>>
17pub trait SearchType: Default {
18    fn specialised_params(&self, spelling_mode: &SpellingMode) -> Option<Cow<str>>;
19}
20
21// Trait constraint - to simplify implementation of Query for BasicSearch,
22// LibrarySearch and UploadSearch.
23pub trait UnfilteredSearchType: SearchType {}
24
25/// An API search query.
26#[derive(PartialEq, Debug, Clone)]
27pub struct SearchQuery<'a, S: SearchType> {
28    query: Cow<'a, str>,
29    spelling_mode: SpellingMode,
30    search_type: S,
31}
32
33/// Whether or not to allow Google to attempt to auto correct spelling as part
34/// of the results. Has no affect on Uploads or Library.
35// XXX: May actually affect Library. To confirm.
36#[derive(PartialEq, Debug, Clone, Default)]
37pub enum SpellingMode {
38    // My personal preference is to use ExactMatch by default, so that's what I've set.
39    // Google's is WithSuggestions.
40    #[default]
41    ExactMatch,
42    WithSuggestions,
43}
44
45/// Helper struct for SearchQuery type state pattern.
46#[derive(Default, Debug, Clone, PartialEq)]
47pub struct BasicSearch;
48/// Helper struct for SearchQuery type state pattern.
49#[derive(Default, Debug, Clone, PartialEq)]
50pub struct LibrarySearch;
51/// Helper struct for SearchQuery type state pattern.
52#[derive(Default, Debug, Clone, PartialEq)]
53pub struct UploadSearch;
54
55impl SearchType for BasicSearch {
56    fn specialised_params(&self, spelling_mode: &SpellingMode) -> Option<Cow<str>> {
57        match spelling_mode {
58            SpellingMode::ExactMatch => Some("EhGKAQ4IARABGAEgASgAOAFAAUICCAE%3D".into()),
59            SpellingMode::WithSuggestions => None,
60        }
61    }
62}
63impl SearchType for UploadSearch {
64    fn specialised_params(&self, _: &SpellingMode) -> Option<Cow<str>> {
65        // TODO: Investigate if spelling suggestions take affect here.
66        Some("agIYAw%3D%3D".into())
67    }
68}
69impl SearchType for LibrarySearch {
70    fn specialised_params(&self, _: &SpellingMode) -> Option<Cow<str>> {
71        // XXX: It may be possible to actually filter these, see sigma67/ytmusicapi for
72        // details. TODO: Investigate if spelling suggestions take affect here.
73        Some("agIYBA%3D%3D".into())
74    }
75}
76
77impl UnfilteredSearchType for BasicSearch {}
78impl UnfilteredSearchType for UploadSearch {}
79impl UnfilteredSearchType for LibrarySearch {}
80
81impl<S: UnfilteredSearchType, A: AuthToken> Query<A> for SearchQuery<'_, S> {
82    type Output = SearchResults;
83    type Method = PostMethod;
84}
85impl<S: UnfilteredSearchType> PostQuery for SearchQuery<'_, S> {
86    fn header(&self) -> serde_json::Map<String, serde_json::Value> {
87        search_query_header(self)
88    }
89    fn path(&self) -> &str {
90        SEARCH_QUERY_PATH
91    }
92    fn params(&self) -> Vec<(&str, Cow<str>)> {
93        vec![]
94    }
95}
96
97// This currently requires type annotations.
98// By default, uses SpellingMode exactmatch.
99impl<'a, Q: Into<Cow<'a, str>>, S: SearchType> From<Q> for SearchQuery<'a, S> {
100    fn from(value: Q) -> SearchQuery<'a, S> {
101        SearchQuery {
102            query: value.into(),
103            spelling_mode: SpellingMode::default(),
104            search_type: S::default(),
105        }
106    }
107}
108
109// By default, uses SpellingMode exactmatch.
110impl<'a> SearchQuery<'a, BasicSearch> {
111    pub fn new<Q: Into<Cow<'a, str>>>(q: Q) -> SearchQuery<'a, BasicSearch> {
112        SearchQuery {
113            query: q.into(),
114            spelling_mode: SpellingMode::default(),
115            search_type: BasicSearch {},
116        }
117    }
118}
119
120impl<'a, S: SearchType> SearchQuery<'a, S> {
121    /// Set spelling mode.
122    pub fn with_spelling_mode(mut self, spelling_mode: SpellingMode) -> Self {
123        self.spelling_mode = spelling_mode;
124        self
125    }
126    /// Chnage the set query.
127    pub fn with_query<Q: Into<Cow<'a, str>>>(mut self, query: Q) -> Self {
128        self.query = query.into();
129        self
130    }
131}
132
133impl<'a> SearchQuery<'a, BasicSearch> {
134    /// Apply a filter to the search. May change type of results returned.
135    pub fn with_filter<F: FilteredSearchType>(
136        self,
137        filter: F,
138    ) -> SearchQuery<'a, FilteredSearch<F>> {
139        SearchQuery {
140            query: self.query,
141            spelling_mode: self.spelling_mode,
142            search_type: FilteredSearch { filter },
143        }
144    }
145    /// Search only uploads.
146    pub fn uploads(self) -> SearchQuery<'a, UploadSearch> {
147        SearchQuery {
148            query: self.query,
149            spelling_mode: self.spelling_mode,
150            search_type: UploadSearch,
151        }
152    }
153    /// Search only library.
154    pub fn library(self) -> SearchQuery<'a, LibrarySearch> {
155        SearchQuery {
156            query: self.query,
157            spelling_mode: self.spelling_mode,
158            search_type: LibrarySearch,
159        }
160    }
161}
162
163impl<'a, F: FilteredSearchType> SearchQuery<'a, FilteredSearch<F>> {
164    /// Apply a filter to the search. May change type of results returned.
165    pub fn with_filter<F2: FilteredSearchType>(
166        self,
167        filter: F2,
168    ) -> SearchQuery<'a, FilteredSearch<F2>> {
169        SearchQuery {
170            query: self.query,
171            spelling_mode: self.spelling_mode,
172            search_type: FilteredSearch { filter },
173        }
174    }
175    /// Remove filter from the query.
176    pub fn unfiltered(self) -> SearchQuery<'a, BasicSearch> {
177        SearchQuery {
178            query: self.query,
179            spelling_mode: self.spelling_mode,
180            search_type: BasicSearch,
181        }
182    }
183}
184
185impl<'a> SearchQuery<'a, UploadSearch> {
186    /// Change scope to search generally instead of Uploads.
187    pub fn with_scope_public(self) -> SearchQuery<'a, BasicSearch> {
188        SearchQuery {
189            query: self.query,
190            spelling_mode: self.spelling_mode,
191            search_type: BasicSearch,
192        }
193    }
194}
195impl<'a> SearchQuery<'a, LibrarySearch> {
196    /// Change scope to search generally instead of Library.
197    pub fn with_scope_public(self) -> SearchQuery<'a, BasicSearch> {
198        SearchQuery {
199            query: self.query,
200            spelling_mode: self.spelling_mode,
201            search_type: BasicSearch,
202        }
203    }
204}
205
206#[derive(PartialEq, Debug, Clone)]
207pub struct GetSearchSuggestionsQuery<'a> {
208    query: Cow<'a, str>,
209}
210
211impl<'a> GetSearchSuggestionsQuery<'a> {
212    pub fn new<S: Into<Cow<'a, str>>>(value: S) -> GetSearchSuggestionsQuery<'a> {
213        GetSearchSuggestionsQuery {
214            query: value.into(),
215        }
216    }
217}
218
219impl<'a, S: Into<Cow<'a, str>>> From<S> for GetSearchSuggestionsQuery<'a> {
220    fn from(value: S) -> GetSearchSuggestionsQuery<'a> {
221        GetSearchSuggestionsQuery::new(value)
222    }
223}
224
225impl<A: AuthToken> Query<A> for GetSearchSuggestionsQuery<'_> {
226    type Output = Vec<SearchSuggestion>;
227    type Method = PostMethod;
228}
229impl PostQuery for GetSearchSuggestionsQuery<'_> {
230    fn header(&self) -> serde_json::Map<String, serde_json::Value> {
231        let value = self.query.as_ref().into();
232        serde_json::Map::from_iter([("input".into(), value)])
233    }
234    fn path(&self) -> &str {
235        "music/get_search_suggestions"
236    }
237    fn params(&self) -> Vec<(&str, Cow<str>)> {
238        vec![]
239    }
240}
241
242fn search_query_header<S: SearchType>(
243    query: &SearchQuery<S>,
244) -> serde_json::Map<String, serde_json::Value> {
245    let value = query.query.as_ref().into();
246    let params = search_query_params(query);
247    if let Some(params) = params {
248        serde_json::Map::from_iter([
249            ("query".to_string(), value),
250            ("params".to_string(), params.into()),
251        ])
252    } else {
253        serde_json::Map::from_iter([("query".to_string(), value)])
254    }
255}
256fn search_query_params<'a, S: SearchType>(query: &'a SearchQuery<'a, S>) -> Option<Cow<'a, str>> {
257    query.search_type.specialised_params(&query.spelling_mode)
258}