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}