diff --git a/README.md b/README.md index bde5689..53b9359 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ inspired by [NewPipe](https://github.com/TeamNewPipe/NewPipeExtractor). - [X] **Channel** (videos, playlists, info) - [X] **ChannelRSS** - [X] **Search** (with filters) -- [ ] **Search suggestions** +- [X] **Search suggestions** - [ ] **Trending** - [ ] **URL resolver** diff --git a/src/client/search.rs b/src/client/search.rs index 249c89f..c0f324b 100644 --- a/src/client/search.rs +++ b/src/client/search.rs @@ -1,4 +1,4 @@ -use serde::Serialize; +use serde::{de::IgnoredAny, Serialize}; use crate::{ deobfuscate::Deobfuscator, @@ -79,6 +79,32 @@ impl RustyPipeQuery { ) .await } + + pub async fn search_suggestion(self, query: &str) -> Result, Error> { + let url = url::Url::parse_with_params("https://suggestqueries-clients6.youtube.com/complete/search?client=youtube&gs_rn=64&gs_ri=youtube&ds=yt&cp=1&gs_id=4&xhr=t&xssi=t", + &[("hl", self.opts.lang.to_string()), ("gl", self.opts.country.to_string()), ("q", query.to_string())] + ).map_err(|_| Error::Other("could not build url".into()))?; + + let response = self + .client + .http_request_txt(self.client.inner.http.get(url).build()?) + .await?; + + let trimmed = response.get(5..).ok_or_else(|| { + Error::Extraction(ExtractionError::InvalidData( + "could not get string slice".into(), + )) + })?; + + let parsed = serde_json::from_str::<( + IgnoredAny, + Vec<(String, IgnoredAny, IgnoredAny)>, + IgnoredAny, + )>(trimmed) + .map_err(|e| Error::Extraction(ExtractionError::InvalidData(e.to_string().into())))?; + + Ok(parsed.1.into_iter().map(|item| item.0).collect()) + } } impl MapResponse for response::Search { diff --git a/tests/youtube.rs b/tests/youtube.rs index 24a519b..c7849b7 100644 --- a/tests/youtube.rs +++ b/tests/youtube.rs @@ -1185,6 +1185,26 @@ async fn search_empty() { assert!(result.items.is_empty()); } +#[tokio::test] +async fn search_suggestion() { + let rp = RustyPipe::builder().strict().build(); + let result = rp.query().search_suggestion("hunger ga").await.unwrap(); + + assert!(result.contains(&"hunger games".to_owned())); +} + +#[tokio::test] +async fn search_suggestion_empty() { + let rp = RustyPipe::builder().strict().build(); + let result = rp + .query() + .search_suggestion("fjew327%4ifjelwfvnewg49") + .await + .unwrap(); + + assert!(result.is_empty()); +} + //#TESTUTIL /// Assert equality within 10% margin