Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

replaced failure with anyhow #8

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
94 changes: 48 additions & 46 deletions src/api/auth.rs
Original file line number Diff line number Diff line change
@@ -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<S: Into<String>>(api_key: S, client_id: String, client_secret: String, redirect_uri: Option<&str>) -> Result<Self, failure::Error> {
pub fn new_with_oauth<S: Into<String>>(
api_key: S,
client_id: String,
client_secret: String,
redirect_uri: Option<&str>,
) -> anyhow::Result<Self> {
let oauth_client = BasicClient::new(
ClientId::new(client_id.clone()),
Some(ClientSecret::new(client_secret.clone())),
Expand All @@ -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,
Expand All @@ -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<H>(&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<H>(&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?;
Expand All @@ -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;
Expand Down
38 changes: 22 additions & 16 deletions src/api/mod.rs
Original file line number Diff line number Diff line change
@@ -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::*;
Expand Down Expand Up @@ -29,7 +28,7 @@ pub struct YoutubeApi {
mod auth;

impl YoutubeApi {
pub async fn get_video_info(id: &str) -> Result<VideoMetadata, failure::Error> {
pub async fn get_video_info(id: &str) -> anyhow::Result<VideoMetadata> {
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)?;
Expand All @@ -46,24 +45,30 @@ impl YoutubeApi {
}
}

pub async fn search(&self, search_request: SearchRequestBuilder) -> Result<SearchResponse, failure::Error> {
pub async fn search(
&self,
search_request: SearchRequestBuilder,
) -> anyhow::Result<SearchResponse> {
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<ListPlaylistsResponse, failure::Error> {
pub async fn list_playlists(
&self,
request: ListPlaylistsRequestBuilder,
) -> anyhow::Result<ListPlaylistsResponse> {
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<ListPlaylistItemsResponse, failure::Error> {
pub async fn list_playlist_items(
&self,
request: ListPlaylistItemsRequestBuilder,
) -> anyhow::Result<ListPlaylistItemsResponse> {
let request = request.build();
let response = self.api_get(LIST_PLAYLIST_ITEMS_URL, request).await?;

Expand All @@ -74,7 +79,7 @@ impl YoutubeApi {
&self,
url: S,
params: T,
) -> Result<TResponse, Error> {
) -> anyhow::Result<TResponse> {
let req = self.client.get(&url.into()).query(&params);
let res = if let Some(oauth) = self.oauth.as_ref() {
if oauth.token.requires_new_token().await {
Expand All @@ -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?)
Expand All @@ -102,7 +107,7 @@ impl YoutubeApi {
req: RequestBuilder,
err: reqwest::Error,
oauth: &YoutubeOAuth,
) -> Result<reqwest::Response, Error> {
) -> anyhow::Result<reqwest::Response> {
if let Some(StatusCode::UNAUTHORIZED) = err.status() {
oauth.token.refresh(&oauth.client).await?;
let res = req
Expand All @@ -116,8 +121,9 @@ impl YoutubeApi {
}
}

async fn handle_error<TResponse>(response: reqwest::Response) -> Result<TResponse, Error>
where TResponse : DeserializeOwned
async fn handle_error<TResponse>(response: reqwest::Response) -> anyhow::Result<TResponse>
where
TResponse: DeserializeOwned,
{
if response.error_for_status_ref().is_ok() {
let res = response.json().await?;
Expand Down Expand Up @@ -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;
Expand Down
16 changes: 5 additions & 11 deletions src/auth.rs
Original file line number Diff line number Diff line change
@@ -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);

Expand Down Expand Up @@ -39,7 +33,7 @@ pub(crate) async fn request_token(
client: &BasicClient,
code: String,
verifier: PkceCodeVerifier,
) -> Result<BasicTokenResponse, Error> {
) -> anyhow::Result<BasicTokenResponse> {
let code = AuthorizationCode::new(code);

let token = client
Expand All @@ -54,7 +48,7 @@ pub(crate) async fn request_token(
pub(crate) async fn perform_oauth<H>(
client: &BasicClient,
handler: H,
) -> Result<BasicTokenResponse, Error>
) -> anyhow::Result<BasicTokenResponse>
where
H: Fn(String) -> String,
{
Expand Down
18 changes: 9 additions & 9 deletions src/token.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -36,29 +36,29 @@ impl AuthToken {
self.has_token.store(true, Ordering::Relaxed);
}

pub(crate) async fn get_token(&self) -> Result<BasicTokenResponse, Error> {
pub(crate) async fn get_token(&self) -> anyhow::Result<BasicTokenResponse> {
Ok(self
.token
.lock()
.await
.as_ref()
.ok_or_else(|| format_err!("Not logged in"))?
.context("Not logged in")?
.clone())
}

pub(crate) fn has_token(&self) -> bool {
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)
Expand All @@ -84,11 +84,11 @@ impl AuthToken {
}
}

pub(crate) async fn get_auth_header(&self) -> Result<String, Error> {
pub(crate) async fn get_auth_header(&self) -> anyhow::Result<String> {
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())
Expand Down
Loading