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