ytmapi_rs/query/
search.rs

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