diff --git a/Cargo.toml b/Cargo.toml index ae373de..a51803d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,14 +13,14 @@ categories = ["api-bindings", "multimedia::video"] travis-ci = { repository = "maxjoehnk/youtube-rs", branch = "master" } [dependencies] -failure = "0.1" -serde = { version = "1", features = ["derive"] } -serde_json = "1.0" -serde_urlencoded = "0.6" -reqwest = { version = "0.11", features = ["json"] } -oauth2 = "4.0.0-beta.1" -log = "0.4" -tokio = { version = "1", features = ["fs", "process"] } +serde = { version = "1.0.135", features = ["derive"] } +serde_json = "1.0.78" +serde_urlencoded = "0.7.1" +reqwest = { version = "0.11.9", features = ["json"] } +oauth2 = "4.1.0" +log = "0.4.14" +tokio = { version = "1.15.0", features = ["fs", "process"] } +anyhow = "1.0.53" [dev-dependencies] -tokio = { version = "1", features = ["rt-multi-thread", "macros"] } +tokio = { version = "1.15.0", features = ["rt-multi-thread", "macros"] } diff --git a/src/api/auth.rs b/src/api/auth.rs index cfdd7ac..cd66c49 100644 --- a/src/api/auth.rs +++ b/src/api/auth.rs @@ -1,18 +1,23 @@ -use failure::{ensure, Error, format_err}; +use anyhow::Context; use oauth2::basic::BasicClient; -use oauth2::{PkceCodeVerifier, TokenUrl, RedirectUrl, ClientId, AuthUrl, ClientSecret}; +use oauth2::{AuthUrl, ClientId, ClientSecret, PkceCodeVerifier, RedirectUrl, TokenUrl}; use tokio::fs::{read_to_string, write}; +use crate::api::YoutubeOAuth; use crate::auth::{get_oauth_url, perform_oauth, request_token}; -use crate::YoutubeApi; use crate::token::AuthToken; -use crate::api::YoutubeOAuth; +use crate::YoutubeApi; use reqwest::Client; pub static CODE_REDIRECT_URI: &str = "urn:ietf:wg:oauth:2.0:oob"; impl YoutubeApi { - pub fn new_with_oauth>(api_key: S, client_id: String, client_secret: String, redirect_uri: Option<&str>) -> Result { + pub fn new_with_oauth>( + api_key: S, + client_id: String, + client_secret: String, + redirect_uri: Option<&str>, + ) -> anyhow::Result { let oauth_client = BasicClient::new( ClientId::new(client_id.clone()), Some(ClientSecret::new(client_secret.clone())), @@ -21,9 +26,9 @@ impl YoutubeApi { "https://www.googleapis.com/oauth2/v3/token".to_string(), )?), ) - .set_redirect_uri(RedirectUrl::new( - redirect_uri.unwrap_or(CODE_REDIRECT_URI).to_string(), - )?); + .set_redirect_uri(RedirectUrl::new( + redirect_uri.unwrap_or(CODE_REDIRECT_URI).to_string(), + )?); let oauth = YoutubeOAuth { client_id, @@ -39,43 +44,41 @@ impl YoutubeApi { }) } - /** - * Perform an OAuth Login - * - * Available handlers: - * * [auth::stdio_login](auth/fn.stdio_login.html) - * - * # Example - * ```rust,no_run - * use youtube_api::{YoutubeApi, auth::stdio_login}; - * - * #[tokio::main] - * async fn main() { - * let api = YoutubeApi::new_with_oauth("", String::new(), String::new(), None).unwrap(); - * - * api.login(stdio_login).await.unwrap(); - * } - * ``` - */ - pub async fn login(&self, handler: H) -> Result<(), Error> - where - H: Fn(String) -> String, + /// Perform an OAuth Login + /// + /// Available handlers: + /// * [auth::stdio_login](auth/fn.stdio_login.html) + /// + /// # Example + /// ```rust,no_run + /// use youtube_api::{YoutubeApi, auth::stdio_login}; + /// + /// #[tokio::main] + /// async fn main() { + /// let api = YoutubeApi::new_with_oauth("", String::new(), String::new(), None).unwrap(); + /// + /// api.login(stdio_login).await.unwrap(); + /// } + /// ``` + pub async fn login(&self, handler: H) -> anyhow::Result<()> + where + H: Fn(String) -> String, { - let oauth = self.oauth.as_ref().ok_or_else(|| format_err!("OAuth client not configured"))?; + let oauth = self.oauth.as_ref().context("OAuth client not configured")?; let token = perform_oauth(&oauth.client, handler).await?; oauth.token.set_token(token).await; Ok(()) } - pub fn get_oauth_url(&self) -> Result<(String, String), Error> { - let oauth = self.oauth.as_ref().ok_or_else(|| format_err!("OAuth client not configured"))?; + pub fn get_oauth_url(&self) -> anyhow::Result<(String, String)> { + let oauth = self.oauth.as_ref().context("OAuth client not configured")?; let (url, verifier) = get_oauth_url(&oauth.client); Ok((url, verifier.secret().clone())) } - pub async fn request_token(&mut self, code: String, verifier: String) -> Result<(), Error> { - let oauth = self.oauth.as_ref().ok_or_else(|| format_err!("OAuth client not configured"))?; + pub async fn request_token(&mut self, code: String, verifier: String) -> anyhow::Result<()> { + let oauth = self.oauth.as_ref().context("OAuth client not configured")?; let verifier = PkceCodeVerifier::new(verifier); let token = request_token(&oauth.client, code, verifier).await?; @@ -85,25 +88,24 @@ impl YoutubeApi { } pub fn has_token(&self) -> bool { - self.oauth.as_ref().map(|oauth| oauth.token.has_token()).unwrap_or_default() + self.oauth + .as_ref() + .map(|oauth| oauth.token.has_token()) + .unwrap_or_default() } - /** - * Stores the auth and refresh token in a `.google-auth.json` file for login without user input. - */ - pub async fn store_token(&self) -> Result<(), Error> { - let oauth = self.oauth.as_ref().ok_or_else(|| format_err!("OAuth client not configured"))?; - ensure!(oauth.token.has_token(), "No token available to persist"); + /// Stores the auth and refresh token in a `.google-auth.json` file for login without user input. + pub async fn store_token(&self) -> anyhow::Result<()> { + let oauth = self.oauth.as_ref().context("OAuth client not configured")?; + anyhow::ensure!(oauth.token.has_token(), "No token available to persist"); let token = serde_json::to_string(&oauth.token.get_token().await?)?; write(".youtube-auth.json", token).await?; // TODO: configure file path Ok(()) } - /** - * Stores the auth and refresh token from a `.google-auth.json` file for login without user input. - */ - pub async fn load_token(&self) -> Result<(), Error> { - let oauth = self.oauth.as_ref().ok_or_else(|| format_err!("OAuth client not configured"))?; + /// Stores the auth and refresh token from a `.google-auth.json` file for login without user input. + pub async fn load_token(&self) -> anyhow::Result<()> { + let oauth = self.oauth.as_ref().context("OAuth client not configured")?; let token = read_to_string(".youtube-auth.json").await?; let token = serde_json::from_str(&token)?; oauth.token.set_token(token).await; diff --git a/src/api/mod.rs b/src/api/mod.rs index df59b55..ccad46b 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,6 +1,5 @@ -use failure::Error; use oauth2::basic::BasicClient; -use reqwest::{Client, get, RequestBuilder, StatusCode}; +use reqwest::{get, Client, RequestBuilder, StatusCode}; use serde::Serialize; use crate::models::*; @@ -29,7 +28,7 @@ pub struct YoutubeApi { mod auth; impl YoutubeApi { - pub async fn get_video_info(id: &str) -> Result { + pub async fn get_video_info(id: &str) -> anyhow::Result { let url = format!("https://www.youtube.com/get_video_info?video_id={}", id); let res = get(&url).await?.error_for_status()?.text().await?; let response: VideoMetadataResponse = serde_urlencoded::from_str(&res)?; @@ -46,24 +45,30 @@ impl YoutubeApi { } } - pub async fn search(&self, search_request: SearchRequestBuilder) -> Result { + pub async fn search( + &self, + search_request: SearchRequestBuilder, + ) -> anyhow::Result { let request = search_request.build(&self.api_key); - let response = self.client.get(SEARCH_URL) - .query(&request) - .send() - .await?; + let response = self.client.get(SEARCH_URL).query(&request).send().await?; YoutubeApi::handle_error(response).await } - pub async fn list_playlists(&self, request: ListPlaylistsRequestBuilder) -> Result { + pub async fn list_playlists( + &self, + request: ListPlaylistsRequestBuilder, + ) -> anyhow::Result { let request = request.build(); let response = self.api_get(LIST_PLAYLISTS_URL, request).await?; Ok(response) } - pub async fn list_playlist_items(&self, request: ListPlaylistItemsRequestBuilder) -> Result { + pub async fn list_playlist_items( + &self, + request: ListPlaylistItemsRequestBuilder, + ) -> anyhow::Result { let request = request.build(); let response = self.api_get(LIST_PLAYLIST_ITEMS_URL, request).await?; @@ -74,7 +79,7 @@ impl YoutubeApi { &self, url: S, params: T, - ) -> Result { + ) -> anyhow::Result { let req = self.client.get(&url.into()).query(¶ms); let res = if let Some(oauth) = self.oauth.as_ref() { if oauth.token.requires_new_token().await { @@ -89,7 +94,7 @@ impl YoutubeApi { .error_for_status(); match res { Ok(res) => Ok(res), - Err(err) => self.retry_request(req, err, oauth).await + Err(err) => self.retry_request(req, err, oauth).await, } } else { Ok(req.send().await?) @@ -102,7 +107,7 @@ impl YoutubeApi { req: RequestBuilder, err: reqwest::Error, oauth: &YoutubeOAuth, - ) -> Result { + ) -> anyhow::Result { if let Some(StatusCode::UNAUTHORIZED) = err.status() { oauth.token.refresh(&oauth.client).await?; let res = req @@ -116,8 +121,9 @@ impl YoutubeApi { } } - async fn handle_error(response: reqwest::Response) -> Result - where TResponse : DeserializeOwned + async fn handle_error(response: reqwest::Response) -> anyhow::Result + where + TResponse: DeserializeOwned, { if response.error_for_status_ref().is_ok() { let res = response.json().await?; @@ -149,7 +155,7 @@ mod test { "uM7JjfHDuFM", "BgWpK28dt6I", "8xe6nLVXEC0", - "O3WKbJLai1g" + "O3WKbJLai1g", ]; for video_id in video_ids { let metadata = YoutubeApi::get_video_info(video_id).await; diff --git a/src/auth.rs b/src/auth.rs index f915ec5..6f17406 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,17 +1,11 @@ -use std::io; - -use failure::Error; use oauth2::basic::{BasicClient, BasicTokenResponse}; use oauth2::reqwest::async_http_client; -use oauth2::{ - AuthorizationCode, CsrfToken, PkceCodeChallenge, PkceCodeVerifier, Scope, -}; +use oauth2::{AuthorizationCode, CsrfToken, PkceCodeChallenge, PkceCodeVerifier, Scope}; +use std::io; static SCOPE: &str = "https://www.googleapis.com/auth/youtube.readonly"; -/** - * Prints the authorize url to stdout and waits for the authorization code from stdin - */ +/// Prints the authorize url to stdout and waits for the authorization code from stdin pub fn stdio_login(url: String) -> String { println!("Open this URL in your browser:\n{}\n", url); @@ -39,7 +33,7 @@ pub(crate) async fn request_token( client: &BasicClient, code: String, verifier: PkceCodeVerifier, -) -> Result { +) -> anyhow::Result { let code = AuthorizationCode::new(code); let token = client @@ -54,7 +48,7 @@ pub(crate) async fn request_token( pub(crate) async fn perform_oauth( client: &BasicClient, handler: H, -) -> Result +) -> anyhow::Result where H: Fn(String) -> String, { diff --git a/src/token.rs b/src/token.rs index 0c06550..a6628d1 100644 --- a/src/token.rs +++ b/src/token.rs @@ -1,9 +1,9 @@ +use anyhow::Context; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; -use tokio::sync::Mutex; use std::time::{Duration, Instant}; +use tokio::sync::Mutex; -use failure::{format_err, Error}; use log::debug; use oauth2::basic::{BasicClient, BasicTokenResponse}; use oauth2::reqwest::async_http_client; @@ -36,13 +36,13 @@ impl AuthToken { self.has_token.store(true, Ordering::Relaxed); } - pub(crate) async fn get_token(&self) -> Result { + pub(crate) async fn get_token(&self) -> anyhow::Result { Ok(self .token .lock() .await .as_ref() - .ok_or_else(|| format_err!("Not logged in"))? + .context("Not logged in")? .clone()) } @@ -50,15 +50,15 @@ impl AuthToken { self.has_token.load(Ordering::Relaxed) } - pub(crate) async fn refresh(&self, client: &BasicClient) -> Result<(), Error> { + pub(crate) async fn refresh(&self, client: &BasicClient) -> anyhow::Result<()> { debug!("refreshing access token"); let token = { let token = self.token.lock().await; let refresh_token = token .as_ref() - .ok_or_else(|| format_err!("Not logged in"))? + .context("Not logged in")? .refresh_token() - .ok_or_else(|| format_err!("No refresh token"))?; + .context("No refresh token")?; client .exchange_refresh_token(refresh_token) @@ -84,11 +84,11 @@ impl AuthToken { } } - pub(crate) async fn get_auth_header(&self) -> Result { + pub(crate) async fn get_auth_header(&self) -> anyhow::Result { let token = self.token.lock().await; let token = token .as_ref() - .ok_or_else(|| format_err!("Not logged in"))? + .context("Not logged in")? .access_token() .secret(); Ok(token.clone()) diff --git a/src/youtube_dl.rs b/src/youtube_dl.rs index 55430ce..8105776 100644 --- a/src/youtube_dl.rs +++ b/src/youtube_dl.rs @@ -1,16 +1,21 @@ use tokio::process::Command; -use failure::format_err; #[derive(Debug, Clone, Default)] pub struct YoutubeDl; impl YoutubeDl { - pub async fn get_audio_stream_url(&self, id: &str) -> Result { - let output = Command::new("youtube-dl").arg("-g").arg("-f").arg("bestaudio").arg(format!("https://www.youtube.com/watch?v={}", id)).output().await?; + pub async fn get_audio_stream_url(&self, id: &str) -> anyhow::Result { + let output = Command::new("youtube-dl") + .arg("-g") + .arg("-f") + .arg("bestaudio") + .arg(format!("https://www.youtube.com/watch?v={}", id)) + .output() + .await?; if !output.status.success() { let err = String::from_utf8(output.stderr)?; - return Err(format_err!("{}", err)); + anyhow::bail!("{}", err); } let url = String::from_utf8(output.stdout)?.trim().to_owned(); Ok(url) @@ -31,7 +36,7 @@ mod test { "uM7JjfHDuFM", "BgWpK28dt6I", "8xe6nLVXEC0", - "O3WKbJLai1g" + "O3WKbJLai1g", ]; for video_id in video_ids { let url = youtube_dl.get_audio_stream_url(video_id).await;