Skip to main content

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