ytmapi_rs/lib.rs
1//! # ytmapi_rs
2//! Library into YouTube Music's internal API.
3//! ## Examples
4//! For additional examples using builder, see [`builder`] module.
5//! ### Basic usage with a pre-created cookie file.
6//! ```no_run
7//! #[tokio::main]
8//! pub async fn main() -> Result<(), ytmapi_rs::Error> {
9//! let cookie_path = std::path::Path::new("./cookie.txt");
10//! let yt = ytmapi_rs::YtMusic::from_cookie_file(cookie_path).await?;
11//! yt.get_search_suggestions("Beatles").await?;
12//! let result = yt.get_search_suggestions("Beatles").await?;
13//! println!("{:?}", result);
14//! Ok(())
15//! }
16//! ```
17//! ### OAuth usage, using the workflow, and builder method to re-use the `Client`.
18//! ```no_run
19//! #[tokio::main]
20//! pub async fn main() -> Result<(), ytmapi_rs::Error> {
21//! let client = ytmapi_rs::Client::new().unwrap();
22//! let (code, url) = ytmapi_rs::generate_oauth_code_and_url(&client).await?;
23//! println!("Go to {url}, finish the login flow, and press enter when done");
24//! let mut _buf = String::new();
25//! let _ = std::io::stdin().read_line(&mut _buf);
26//! let token = ytmapi_rs::generate_oauth_token(&client, code).await?;
27//! // NOTE: The token can be re-used until it expires, and refreshed once it has,
28//! // so it's recommended to save it to a file here.
29//! let yt = ytmapi_rs::YtMusicBuilder::new_with_client(client)
30//! .with_oauth_token(token)
31//! .build()
32//! .unwrap();
33//! let result = yt.get_search_suggestions("Beatles").await?;
34//! println!("{:?}", result);
35//! Ok(())
36//! }
37//! ```
38//! ## Optional Features
39//! ### TLS
40//! NOTE: reqwest will prefer to utilise default-tls if multiple features are
41//! built when using the standard constructors. Use `YtMusicBuilder` to ensure
42//! the preferred choice of TLS is used. See reqwest docs for more information <https://docs.rs/reqwest/latest/reqwest/tls/index.html>.
43//! - **default-tls** *(enabled by default)*: Utilises the default TLS from
44//! reqwest - at the time of writing is native-tls.
45//! - **native-tls**: This feature allows use of the the native-tls crate,
46//! reliant on vendors tls.
47//! - **rustls-tls**: This feature allows use of the rustls crate, written in
48//! rust.
49//! ### Other
50//! - **simplified_queries**: Adds convenience methods to [`YtMusic`].
51//! - **serde_json**: Enables some interoperability functions with `serde_json`.
52// For feature specific documentation.
53#![cfg_attr(docsrs, feature(doc_cfg))]
54#[cfg(not(any(
55 feature = "rustls-tls",
56 feature = "native-tls",
57 feature = "default-tls"
58)))]
59compile_error!("One of the TLS features must be enabled for this crate");
60use auth::{
61 browser::BrowserToken, oauth::OAuthDeviceCode, AuthToken, OAuthToken, OAuthTokenGenerator,
62};
63use continuations::Continuable;
64use futures::Stream;
65use parse::ParseFrom;
66use query::{PostQuery, Query, QueryMethod};
67use std::{
68 borrow::Borrow,
69 hash::{DefaultHasher, Hash, Hasher},
70 path::Path,
71};
72
73#[doc(inline)]
74pub use builder::YtMusicBuilder;
75#[doc(inline)]
76pub use client::Client;
77#[doc(inline)]
78pub use error::{Error, Result};
79#[doc(inline)]
80pub use parse::ProcessedResult;
81#[doc(inline)]
82pub use process::RawResult;
83
84#[macro_use]
85mod utils;
86mod nav_consts;
87mod process;
88mod youtube_enums;
89
90pub mod auth;
91pub mod builder;
92pub mod client;
93pub mod common;
94pub mod continuations;
95pub mod error;
96pub mod json;
97pub mod parse;
98pub mod query;
99
100#[cfg(feature = "simplified-queries")]
101#[cfg_attr(docsrs, doc(cfg(feature = "simplified-queries")))]
102pub mod simplified_queries;
103
104#[derive(Debug, Clone)]
105// XXX: Consider wrapping auth in reference counting for cheap cloning.
106// XXX: Note that we would then need to use a RwLock if we wanted to use mutability for
107// refresh_token().
108/// A handle to the YouTube Music API, wrapping a http client.
109/// Generic over AuthToken, as different AuthTokens may allow different queries
110/// to be executed.
111/// It is recommended to re-use these as they internally contain a connection
112/// pool.
113/// # Documentation note
114/// Examples given for methods on this struct will use fake or mock
115/// constructors. When using in a real environment, you will need to construct
116/// using a real token or cookie.
117pub struct YtMusic<A: AuthToken> {
118 // TODO: add language
119 // TODO: add location
120 client: Client,
121 token: A,
122}
123
124impl YtMusic<BrowserToken> {
125 /// Create a new API handle using a BrowserToken.
126 /// Utilises the default TLS option for the enabled features.
127 /// # Panics
128 /// This will panic in some situations - see <https://docs.rs/reqwest/latest/reqwest/struct.Client.html#panics>
129 pub fn from_browser_token(token: BrowserToken) -> YtMusic<BrowserToken> {
130 let client = Client::new().expect("Expected Client build to succeed");
131 YtMusic { client, token }
132 }
133 /// Create a new API handle using a real browser authentication cookie saved
134 /// to a file on disk.
135 /// Utilises the default TLS option for the enabled features.
136 /// # Panics
137 /// This will panic in some situations - see <https://docs.rs/reqwest/latest/reqwest/struct.Client.html#panics>
138 pub async fn from_cookie_file<P: AsRef<Path>>(path: P) -> Result<Self> {
139 let client = Client::new().expect("Expected Client build to succeed");
140 let token = BrowserToken::from_cookie_file(path, &client).await?;
141 Ok(Self { client, token })
142 }
143 /// Create a new API handle using a real browser authentication cookie in a
144 /// String.
145 /// Utilises the default TLS option for the enabled features.
146 /// # Panics
147 /// This will panic in some situations - see <https://docs.rs/reqwest/latest/reqwest/struct.Client.html#panics>
148 pub async fn from_cookie<S: AsRef<str>>(cookie: S) -> Result<Self> {
149 let client = Client::new().expect("Expected Client build to succeed");
150 let token = BrowserToken::from_str(cookie.as_ref(), &client).await?;
151 Ok(Self { client, token })
152 }
153}
154impl YtMusic<OAuthToken> {
155 /// Create a new API handle using an OAuthToken.
156 /// Utilises the default TLS option for the enabled features.
157 /// # Panics
158 /// This will panic in some situations - see <https://docs.rs/reqwest/latest/reqwest/struct.Client.html#panics>
159 pub fn from_oauth_token(token: OAuthToken) -> YtMusic<OAuthToken> {
160 let client = Client::new().expect("Expected Client build to succeed");
161 YtMusic { client, token }
162 }
163 /// Refresh the internal oauth token, and return a clone of it (for user to
164 /// store locally, e.g).
165 pub async fn refresh_token(&mut self) -> Result<OAuthToken> {
166 let refreshed_token = self.token.refresh(&self.client).await?;
167 self.token = refreshed_token.clone();
168 Ok(refreshed_token)
169 }
170 /// Get a hash of the internal oauth token, for use in comparison
171 /// operations.
172 pub fn get_token_hash(&self) -> u64 {
173 let mut h = DefaultHasher::new();
174 self.token.hash(&mut h);
175 h.finish()
176 }
177}
178impl<A: AuthToken> YtMusic<A> {
179 /// Return a raw result from YouTube music for query Q that requires further
180 /// processing.
181 /// # Note
182 /// The returned raw result will hold a reference to the query it was called
183 /// with. Therefore, passing an owned value is not permitted.
184 /// # Usage
185 /// ```no_run
186 /// use ytmapi_rs::auth::BrowserToken;
187 /// use ytmapi_rs::parse::ParseFrom;
188 ///
189 /// # async {
190 /// let yt = ytmapi_rs::YtMusic::from_cookie("FAKE COOKIE").await?;
191 /// let query =
192 /// ytmapi_rs::query::SearchQuery::new("Beatles").with_filter(ytmapi_rs::query::ArtistsFilter);
193 /// let raw_result = yt.raw_query(&query).await?;
194 /// let result: Vec<ytmapi_rs::parse::SearchResultArtist> =
195 /// ParseFrom::parse_from(raw_result.process()?)?;
196 /// assert_eq!(result[0].artist, "The Beatles");
197 /// # Ok::<(), ytmapi_rs::Error>(())
198 /// # };
199 /// ```
200 pub async fn raw_query<'a, Q: Query<A>>(&self, query: &'a Q) -> Result<RawResult<'a, Q, A>> {
201 // TODO: Check for a response the reflects an expired Headers token
202 Q::Method::call(query, &self.client, &self.token).await
203 }
204 /// Return a result from YouTube music that has had errors removed and been
205 /// processed into parsable JSON.
206 /// # Note
207 /// The returned raw result will hold a reference to the query it was called
208 /// with. Therefore, passing an owned value is not permitted.
209 /// # Usage
210 /// ```no_run
211 /// use ytmapi_rs::auth::BrowserToken;
212 /// use ytmapi_rs::parse::ParseFrom;
213 ///
214 /// # async {
215 /// let yt = ytmapi_rs::YtMusic::from_cookie("FAKE COOKIE").await?;
216 /// let query =
217 /// ytmapi_rs::query::SearchQuery::new("Beatles").with_filter(ytmapi_rs::query::ArtistsFilter);
218 /// let processed_result = yt.processed_query(&query).await?;
219 /// let result: Vec<ytmapi_rs::parse::SearchResultArtist> =
220 /// ParseFrom::parse_from(processed_result)?;
221 /// assert_eq!(result[0].artist, "The Beatles");
222 /// # Ok::<(), ytmapi_rs::Error>(())
223 /// # };
224 /// ```
225 pub async fn processed_query<'a, Q: Query<A>>(
226 &self,
227 query: &'a Q,
228 ) -> Result<ProcessedResult<'a, Q>> {
229 // TODO: Check for a response the reflects an expired Headers token
230 self.raw_query(query).await?.process()
231 }
232 /// Return the raw JSON returned by YouTube music for Query Q.
233 /// Return a result from YouTube music that has had errors removed and been
234 /// processed into parsable JSON.
235 /// # Usage
236 /// ```no_run
237 /// # async {
238 /// let yt = ytmapi_rs::YtMusic::from_cookie("FAKE COOKIE").await?;
239 /// let query =
240 /// ytmapi_rs::query::SearchQuery::new("Beatles").with_filter(ytmapi_rs::query::ArtistsFilter);
241 /// let json_string = yt.json_query(query).await?;
242 /// assert!(serde_json::from_str::<serde_json::Value>(&json_string).is_ok());
243 /// # Ok::<(), ytmapi_rs::Error>(())
244 /// # };
245 /// ```
246 pub async fn json_query<Q: Query<A>>(&self, query: impl Borrow<Q>) -> Result<String> {
247 // TODO: Remove allocation
248 let json = self
249 .raw_query(query.borrow())
250 .await?
251 .process()?
252 .clone_json();
253 Ok(json)
254 }
255 /// Return a result from YouTube music that has had errors removed and been
256 /// processed into parsable JSON.
257 /// # Usage
258 /// ```no_run
259 /// # async {
260 /// let yt = ytmapi_rs::YtMusic::from_cookie("").await?;
261 /// let query =
262 /// ytmapi_rs::query::SearchQuery::new("Beatles").with_filter(ytmapi_rs::query::ArtistsFilter);
263 /// let result = yt.query(query).await?;
264 /// assert_eq!(result[0].artist, "The Beatles");
265 /// # Ok::<(), ytmapi_rs::Error>(())
266 /// # };
267 /// ```
268 pub async fn query<Q: Query<A>>(&self, query: impl Borrow<Q>) -> Result<Q::Output> {
269 Q::Output::parse_from(self.processed_query(query.borrow()).await?)
270 }
271 /// Stream a query that has 'continuations', i.e can continue to stream
272 /// results.
273 /// # Return type lifetime notes
274 /// The returned Impl Stream is tied to the lifetime of self, since it's
275 /// self's client that will emit the results. It's also tied to the
276 /// lifetime of query, but ideally it could take either owned or
277 /// borrowed query.
278 /// # Usage
279 /// ```no_run
280 /// use futures::stream::TryStreamExt;
281 /// # async {
282 /// let yt = ytmapi_rs::YtMusic::from_cookie("").await?;
283 /// let query = ytmapi_rs::query::GetLibrarySongsQuery::default();
284 /// let results = yt.stream(&query).try_collect::<Vec<_>>().await?;
285 /// # Ok::<(), ytmapi_rs::Error>(())
286 /// # };
287 /// ```
288 pub fn stream<'a, Q>(&'a self, query: &'a Q) -> impl Stream<Item = Result<Q::Output>> + 'a
289 where
290 Q: Query<A>,
291 Q: PostQuery,
292 Q::Output: Continuable<Q>,
293 {
294 continuations::stream(query, &self.client, &self.token)
295 }
296}
297/// Generates a tuple containing fresh OAuthDeviceCode and corresponding url for
298/// you to authenticate yourself at.
299/// This requires a [`Client`] to run.
300/// (OAuthDeviceCode, URL)
301/// # Usage
302/// ```no_run
303/// # async {
304/// let client = ytmapi_rs::Client::new().unwrap();
305/// let (code, url) = ytmapi_rs::generate_oauth_code_and_url(&client).await?;
306/// # Ok::<(), ytmapi_rs::Error>(())
307/// # };
308/// ```
309pub async fn generate_oauth_code_and_url(client: &Client) -> Result<(OAuthDeviceCode, String)> {
310 let code = OAuthTokenGenerator::new(client).await?;
311 let url = format!("{}?user_code={}", code.verification_url, code.user_code);
312 Ok((code.device_code, url))
313}
314/// Generates an OAuth Token when given an OAuthDeviceCode.
315/// This requires a [`Client`] to run.
316/// # Usage
317/// ```no_run
318/// # async {
319/// let client = ytmapi_rs::Client::new().unwrap();
320/// let (code, url) = ytmapi_rs::generate_oauth_code_and_url(&client).await?;
321/// println!("Go to {url}, finish the login flow, and press enter when done");
322/// let mut buf = String::new();
323/// let _ = std::io::stdin().read_line(&mut buf);
324/// let token = ytmapi_rs::generate_oauth_token(&client, code).await;
325/// assert!(token.is_ok());
326/// # Ok::<(), ytmapi_rs::Error>(())
327/// # };
328/// ```
329pub async fn generate_oauth_token(client: &Client, code: OAuthDeviceCode) -> Result<OAuthToken> {
330 let token = OAuthToken::from_code(client, code).await?;
331 Ok(token)
332}
333/// Generates a Browser Token when given a browser cookie.
334/// This requires a [`Client`] to run.
335/// # Usage
336/// ```no_run
337/// # async {
338/// let client = ytmapi_rs::Client::new().unwrap();
339/// let cookie = "FAKE COOKIE";
340/// let token = ytmapi_rs::generate_browser_token(&client, cookie).await;
341/// assert!(matches!(
342/// token.unwrap_err().into_kind(),
343/// ytmapi_rs::error::ErrorKind::Header
344/// ));
345/// # };
346/// ```
347pub async fn generate_browser_token<S: AsRef<str>>(
348 client: &Client,
349 cookie: S,
350) -> Result<BrowserToken> {
351 let token = BrowserToken::from_str(cookie.as_ref(), client).await?;
352 Ok(token)
353}
354/// Process a string of JSON as if it had been directly received from the
355/// api for a query. Note that this is generic across AuthToken, and you may
356/// need to provide the AuthToken type using 'turbofish'.
357/// # Usage
358/// ```
359/// let json = r#"{ "test" : true }"#.to_string();
360/// let query = ytmapi_rs::query::SearchQuery::new("Beatles");
361/// let result = ytmapi_rs::process_json::<_, ytmapi_rs::auth::BrowserToken>(json, query);
362/// assert!(result.is_err());
363/// ```
364pub fn process_json<Q: Query<A>, A: AuthToken>(
365 json: String,
366 query: impl Borrow<Q>,
367) -> Result<Q::Output> {
368 Q::Output::parse_from(RawResult::from_raw(json, query.borrow()).process()?)
369}