From 1081f5b1d92e5ec36337182b7532cb497956a17c Mon Sep 17 00:00:00 2001 From: George Miao Date: Wed, 19 Oct 2022 13:58:32 -0400 Subject: [PATCH] feat: `SharableTransClient` --- src/lib.rs | 313 ++++++++++++++------ src/sync.rs | 836 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1058 insertions(+), 91 deletions(-) create mode 100644 src/sync.rs diff --git a/src/lib.rs b/src/lib.rs index a1cb230..09a82c0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,13 @@ -extern crate env_logger; #[macro_use] extern crate log; -extern crate reqwest; -use reqwest::{header::CONTENT_TYPE, Url}; -use reqwest::{Client, StatusCode}; +use reqwest::{header::CONTENT_TYPE, Client, StatusCode, Url}; use serde::de::DeserializeOwned; +mod sync; pub mod types; +pub use sync::SharableTransClient; use types::{ BasicAuth, BlocklistUpdate, FreeSpace, Id, Nothing, PortTest, Result, RpcRequest, RpcResponse, RpcResponseArgument, SessionClose, SessionGet, SessionStats, Torrent, TorrentAction, @@ -89,21 +88,27 @@ impl TransClient { /// extern crate transmission_rpc; /// /// use std::env; + /// /// use dotenvy::dotenv; - /// use transmission_rpc::TransClient; - /// use transmission_rpc::types::{Result, RpcResponse, SessionGet, BasicAuth}; + /// use transmission_rpc::{ + /// types::{BasicAuth, Result, RpcResponse, SessionGet}, + /// TransClient, + /// }; /// /// #[tokio::main] /// async fn main() -> Result<()> { /// dotenv().ok(); /// env_logger::init(); - /// let url= env::var("TURL")?; - /// let basic_auth = BasicAuth{user: env::var("TUSER")?, password: env::var("TPWD")?}; + /// let url = env::var("TURL")?; + /// let basic_auth = BasicAuth { + /// user: env::var("TUSER")?, + /// password: env::var("TPWD")?, + /// }; /// let mut client = TransClient::with_auth(url.parse()?, basic_auth); /// let response: Result> = client.session_get().await; /// match response { /// Ok(_) => println!("Yay!"), - /// Err(_) => panic!("Oh no!") + /// Err(_) => panic!("Oh no!"), /// } /// println!("Rpc response is ok: {}", response?.is_ok()); /// Ok(()) @@ -125,21 +130,27 @@ impl TransClient { /// extern crate transmission_rpc; /// /// use std::env; + /// /// use dotenvy::dotenv; - /// use transmission_rpc::TransClient; - /// use transmission_rpc::types::{Result, RpcResponse, SessionStats, BasicAuth}; + /// use transmission_rpc::{ + /// types::{BasicAuth, Result, RpcResponse, SessionStats}, + /// TransClient, + /// }; /// /// #[tokio::main] /// async fn main() -> Result<()> { /// dotenv().ok(); /// env_logger::init(); - /// let url= env::var("TURL")?; - /// let basic_auth = BasicAuth{user: env::var("TUSER")?, password: env::var("TPWD")?}; + /// let url = env::var("TURL")?; + /// let basic_auth = BasicAuth { + /// user: env::var("TUSER")?, + /// password: env::var("TPWD")?, + /// }; /// let mut client = TransClient::with_auth(url.parse()?, basic_auth); /// let response: Result> = client.session_stats().await; /// match response { /// Ok(_) => println!("Yay!"), - /// Err(_) => panic!("Oh no!") + /// Err(_) => panic!("Oh no!"), /// } /// println!("Rpc response is ok: {}", response?.is_ok()); /// Ok(()) @@ -161,21 +172,27 @@ impl TransClient { /// extern crate transmission_rpc; /// /// use std::env; + /// /// use dotenvy::dotenv; - /// use transmission_rpc::TransClient; - /// use transmission_rpc::types::{Result, RpcResponse, BasicAuth, SessionClose}; + /// use transmission_rpc::{ + /// types::{BasicAuth, Result, RpcResponse, SessionClose}, + /// TransClient, + /// }; /// /// #[tokio::main] /// async fn main() -> Result<()> { /// dotenv().ok(); /// env_logger::init(); - /// let url= env::var("TURL")?; - /// let basic_auth = BasicAuth{user: env::var("TUSER")?, password: env::var("TPWD")?}; + /// let url = env::var("TURL")?; + /// let basic_auth = BasicAuth { + /// user: env::var("TUSER")?, + /// password: env::var("TPWD")?, + /// }; /// let mut client = TransClient::with_auth(url.parse()?, basic_auth); /// let response: Result> = client.session_close().await; /// match response { /// Ok(_) => println!("Yay!"), - /// Err(_) => panic!("Oh no!") + /// Err(_) => panic!("Oh no!"), /// } /// println!("Rpc response is ok: {}", response?.is_ok()); /// Ok(()) @@ -197,21 +214,27 @@ impl TransClient { /// extern crate transmission_rpc; /// /// use std::env; + /// /// use dotenvy::dotenv; - /// use transmission_rpc::TransClient; - /// use transmission_rpc::types::{Result, BlocklistUpdate, RpcResponse, BasicAuth}; + /// use transmission_rpc::{ + /// types::{BasicAuth, BlocklistUpdate, Result, RpcResponse}, + /// TransClient, + /// }; /// /// #[tokio::main] /// async fn main() -> Result<()> { /// dotenv().ok(); /// env_logger::init(); - /// let url= env::var("TURL")?; - /// let basic_auth = BasicAuth{user: env::var("TUSER")?, password: env::var("TPWD")?}; + /// let url = env::var("TURL")?; + /// let basic_auth = BasicAuth { + /// user: env::var("TUSER")?, + /// password: env::var("TPWD")?, + /// }; /// let mut client = TransClient::with_auth(url.parse()?, basic_auth); /// let response: Result> = client.blocklist_update().await; /// match response { /// Ok(_) => println!("Yay!"), - /// Err(_) => panic!("Oh no!") + /// Err(_) => panic!("Oh no!"), /// } /// println!("Rpc response is ok: {}", response?.is_ok()); /// Ok(()) @@ -233,22 +256,28 @@ impl TransClient { /// extern crate transmission_rpc; /// /// use std::env; + /// /// use dotenvy::dotenv; - /// use transmission_rpc::TransClient; - /// use transmission_rpc::types::{Result, RpcResponse, BasicAuth, FreeSpace}; + /// use transmission_rpc::{ + /// types::{BasicAuth, FreeSpace, Result, RpcResponse}, + /// TransClient, + /// }; /// /// #[tokio::main] /// async fn main() -> Result<()> { /// dotenv().ok(); /// env_logger::init(); - /// let url= env::var("TURL")?; + /// let url = env::var("TURL")?; /// let dir = env::var("TDIR")?; - /// let basic_auth = BasicAuth{user: env::var("TUSER")?, password: env::var("TPWD")?}; + /// let basic_auth = BasicAuth { + /// user: env::var("TUSER")?, + /// password: env::var("TPWD")?, + /// }; /// let mut client = TransClient::with_auth(url.parse()?, basic_auth); /// let response: Result> = client.free_space(dir).await; /// match response { /// Ok(_) => println!("Yay!"), - /// Err(_) => panic!("Oh no!") + /// Err(_) => panic!("Oh no!"), /// } /// println!("Rpc response is ok: {}", response?.is_ok()); /// Ok(()) @@ -270,21 +299,27 @@ impl TransClient { /// extern crate transmission_rpc; /// /// use std::env; + /// /// use dotenvy::dotenv; - /// use transmission_rpc::TransClient; - /// use transmission_rpc::types::{Result, RpcResponse, BasicAuth, PortTest}; + /// use transmission_rpc::{ + /// types::{BasicAuth, PortTest, Result, RpcResponse}, + /// TransClient, + /// }; /// /// #[tokio::main] /// async fn main() -> Result<()> { /// dotenv().ok(); /// env_logger::init(); - /// let url= env::var("TURL")?; - /// let basic_auth = BasicAuth{user: env::var("TUSER")?, password: env::var("TPWD")?}; + /// let url = env::var("TURL")?; + /// let basic_auth = BasicAuth { + /// user: env::var("TUSER")?, + /// password: env::var("TPWD")?, + /// }; /// let mut client = TransClient::with_auth(url.parse()?, basic_auth); /// let response: Result> = client.port_test().await; /// match response { /// Ok(_) => println!("Yay!"), - /// Err(_) => panic!("Oh no!") + /// Err(_) => panic!("Oh no!"), /// } /// println!("Rpc response is ok: {}", response?.is_ok()); /// Ok(()) @@ -308,37 +343,78 @@ impl TransClient { /// extern crate transmission_rpc; /// /// use std::env; + /// /// use dotenvy::dotenv; - /// use transmission_rpc::TransClient; - /// use transmission_rpc::types::{Result, RpcResponse, BasicAuth}; - /// use transmission_rpc::types::{Torrents, Torrent, TorrentGetField, Id}; + /// use transmission_rpc::{ + /// types::{BasicAuth, Id, Result, RpcResponse, Torrent, TorrentGetField, Torrents}, + /// TransClient, + /// }; /// /// #[tokio::main] /// async fn main() -> Result<()> { /// dotenv().ok(); /// env_logger::init(); - /// let url= env::var("TURL")?; - /// let basic_auth = BasicAuth{user: env::var("TUSER")?, password: env::var("TPWD")?}; + /// let url = env::var("TURL")?; + /// let basic_auth = BasicAuth { + /// user: env::var("TUSER")?, + /// password: env::var("TPWD")?, + /// }; /// let mut client = TransClient::with_auth(url.parse()?, basic_auth); /// /// let res: RpcResponse> = client.torrent_get(None, None).await?; - /// let names: Vec<&String> = res.arguments.torrents.iter().map(|it| it.name.as_ref().unwrap()).collect(); + /// let names: Vec<&String> = res + /// .arguments + /// .torrents + /// .iter() + /// .map(|it| it.name.as_ref().unwrap()) + /// .collect(); /// println!("{:#?}", names); /// - /// let res1: RpcResponse> = client.torrent_get(Some(vec![TorrentGetField::Id, TorrentGetField::Name]), Some(vec![Id::Id(1), Id::Id(2), Id::Id(3)])).await?; - /// let first_three: Vec = res1.arguments.torrents.iter().map(|it| - /// format!("{}. {}",&it.id.as_ref().unwrap(), &it.name.as_ref().unwrap()) - /// ).collect(); + /// let res1: RpcResponse> = client + /// .torrent_get( + /// Some(vec![TorrentGetField::Id, TorrentGetField::Name]), + /// Some(vec![Id::Id(1), Id::Id(2), Id::Id(3)]), + /// ) + /// .await?; + /// let first_three: Vec = res1 + /// .arguments + /// .torrents + /// .iter() + /// .map(|it| { + /// format!( + /// "{}. {}", + /// &it.id.as_ref().unwrap(), + /// &it.name.as_ref().unwrap() + /// ) + /// }) + /// .collect(); /// println!("{:#?}", first_three); /// - /// - /// let res2: RpcResponse> = client.torrent_get(Some(vec![TorrentGetField::Id, TorrentGetField::HashString, TorrentGetField::Name]), Some(vec![Id::Hash(String::from("64b0d9a53ac9cd1002dad1e15522feddb00152fe"))])).await?; - /// let info: Vec = res2.arguments.torrents.iter().map(|it| - /// format!("{:5}. {:^45} {}", - /// &it.id.as_ref().unwrap(), - /// &it.hash_string.as_ref().unwrap(), - /// &it.name.as_ref().unwrap()) - /// ).collect(); + /// let res2: RpcResponse> = client + /// .torrent_get( + /// Some(vec![ + /// TorrentGetField::Id, + /// TorrentGetField::HashString, + /// TorrentGetField::Name, + /// ]), + /// Some(vec![Id::Hash(String::from( + /// "64b0d9a53ac9cd1002dad1e15522feddb00152fe", + /// ))]), + /// ) + /// .await?; + /// let info: Vec = res2 + /// .arguments + /// .torrents + /// .iter() + /// .map(|it| { + /// format!( + /// "{:5}. {:^45} {}", + /// &it.id.as_ref().unwrap(), + /// &it.hash_string.as_ref().unwrap(), + /// &it.name.as_ref().unwrap() + /// ) + /// }) + /// .collect(); /// println!("{:#?}", info); /// /// Ok(()) @@ -366,25 +442,35 @@ impl TransClient { /// extern crate transmission_rpc; /// /// use std::env; + /// /// use dotenvy::dotenv; - /// use transmission_rpc::TransClient; - /// use transmission_rpc::types::{Result, RpcResponse, BasicAuth}; - /// use transmission_rpc::types::{Torrents, Torrent, TorrentSetArgs, Id}; + /// use transmission_rpc::{ + /// types::{BasicAuth, Id, Result, RpcResponse, Torrent, TorrentSetArgs, Torrents}, + /// TransClient, + /// }; /// /// #[tokio::main] /// async fn main() -> Result<()> { /// dotenv().ok(); /// env_logger::init(); /// - /// let url= env::var("TURL")?.parse()?; - /// let basic_auth = BasicAuth{user: env::var("TUSER")?, password: env::var("TPWD")?}; + /// let url = env::var("TURL")?.parse()?; + /// let basic_auth = BasicAuth { + /// user: env::var("TUSER")?, + /// password: env::var("TPWD")?, + /// }; /// let mut client = TransClient::with_auth(url, basic_auth); /// /// let args = TorrentSetArgs { /// labels: Some(vec![String::from("blue")]), /// ..Default::default() /// }; - /// assert!(client.torrent_set(args, Some(vec![Id::Id(0)])).await?.is_ok()); + /// assert!( + /// client + /// .torrent_set(args, Some(vec![Id::Id(0)])) + /// .await? + /// .is_ok() + /// ); /// /// Ok(()) /// } @@ -409,21 +495,30 @@ impl TransClient { /// extern crate transmission_rpc; /// /// use std::env; + /// /// use dotenvy::dotenv; - /// use transmission_rpc::TransClient; - /// use transmission_rpc::types::{Result, RpcResponse, BasicAuth}; - /// use transmission_rpc::types::{TorrentAction, Nothing, Id}; + /// use transmission_rpc::{ + /// types::{BasicAuth, Id, Nothing, Result, RpcResponse, TorrentAction}, + /// TransClient, + /// }; /// /// #[tokio::main] /// async fn main() -> Result<()> { /// dotenv().ok(); /// env_logger::init(); - /// let url= env::var("TURL")?; - /// let basic_auth = BasicAuth{user: env::var("TUSER")?, password: env::var("TPWD")?}; + /// let url = env::var("TURL")?; + /// let basic_auth = BasicAuth { + /// user: env::var("TUSER")?, + /// password: env::var("TPWD")?, + /// }; /// let mut client = TransClient::with_auth(url.parse()?, basic_auth); - /// let res1: RpcResponse = client.torrent_action(TorrentAction::Start, vec![Id::Id(1)]).await?; + /// let res1: RpcResponse = client + /// .torrent_action(TorrentAction::Start, vec![Id::Id(1)]) + /// .await?; /// println!("Start result: {:?}", &res1.is_ok()); - /// let res2: RpcResponse = client.torrent_action(TorrentAction::Stop, vec![Id::Id(1)]).await?; + /// let res2: RpcResponse = client + /// .torrent_action(TorrentAction::Stop, vec![Id::Id(1)]) + /// .await?; /// println!("Stop result: {:?}", &res2.is_ok()); /// /// Ok(()) @@ -449,17 +544,22 @@ impl TransClient { /// extern crate transmission_rpc; /// /// use std::env; + /// /// use dotenvy::dotenv; - /// use transmission_rpc::TransClient; - /// use transmission_rpc::types::{Result, RpcResponse, BasicAuth}; - /// use transmission_rpc::types::{Nothing, Id}; + /// use transmission_rpc::{ + /// types::{BasicAuth, Id, Nothing, Result, RpcResponse}, + /// TransClient, + /// }; /// /// #[tokio::main] /// async fn main() -> Result<()> { /// dotenv().ok(); /// env_logger::init(); - /// let url= env::var("TURL")?; - /// let basic_auth = BasicAuth{user: env::var("TUSER")?, password: env::var("TPWD")?}; + /// let url = env::var("TURL")?; + /// let basic_auth = BasicAuth { + /// user: env::var("TUSER")?, + /// password: env::var("TPWD")?, + /// }; /// let mut client = TransClient::with_auth(url.parse()?, basic_auth); /// let res: RpcResponse = client.torrent_remove(vec![Id::Id(1)], false).await?; /// println!("Remove result: {:?}", &res.is_ok()); @@ -488,19 +588,30 @@ impl TransClient { /// extern crate transmission_rpc; /// /// use std::env; + /// /// use dotenvy::dotenv; - /// use transmission_rpc::TransClient; - /// use transmission_rpc::types::{Result, RpcResponse, BasicAuth}; - /// use transmission_rpc::types::{Nothing, Id}; + /// use transmission_rpc::{ + /// types::{BasicAuth, Id, Nothing, Result, RpcResponse}, + /// TransClient, + /// }; /// /// #[tokio::main] /// async fn main() -> Result<()> { /// dotenv().ok(); /// env_logger::init(); - /// let url= env::var("TURL")?; - /// let basic_auth = BasicAuth{user: env::var("TUSER")?, password: env::var("TPWD")?}; + /// let url = env::var("TURL")?; + /// let basic_auth = BasicAuth { + /// user: env::var("TUSER")?, + /// password: env::var("TPWD")?, + /// }; /// let mut client = TransClient::with_auth(url.parse()?, basic_auth); - /// let res: RpcResponse = client.torrent_set_location(vec![Id::Id(1)], String::from("/new/location"), Option::from(false)).await?; + /// let res: RpcResponse = client + /// .torrent_set_location( + /// vec![Id::Id(1)], + /// String::from("/new/location"), + /// Option::from(false), + /// ) + /// .await?; /// println!("Set-location result: {:?}", &res.is_ok()); /// /// Ok(()) @@ -528,19 +639,30 @@ impl TransClient { /// extern crate transmission_rpc; /// /// use std::env; + /// /// use dotenvy::dotenv; - /// use transmission_rpc::TransClient; - /// use transmission_rpc::types::{Result, RpcResponse, BasicAuth}; - /// use transmission_rpc::types::{TorrentRenamePath, Id}; + /// use transmission_rpc::{ + /// types::{BasicAuth, Id, Result, RpcResponse, TorrentRenamePath}, + /// TransClient, + /// }; /// /// #[tokio::main] /// async fn main() -> Result<()> { /// dotenv().ok(); /// env_logger::init(); - /// let url= env::var("TURL")?; - /// let basic_auth = BasicAuth{user: env::var("TUSER")?, password: env::var("TPWD")?}; + /// let url = env::var("TURL")?; + /// let basic_auth = BasicAuth { + /// user: env::var("TUSER")?, + /// password: env::var("TPWD")?, + /// }; /// let mut client = TransClient::with_auth(url.parse()?, basic_auth); - /// let res: RpcResponse = client.torrent_rename_path(vec![Id::Id(1)], String::from("Folder/OldFile.jpg"), String::from("NewFile.jpg")).await?; + /// let res: RpcResponse = client + /// .torrent_rename_path( + /// vec![Id::Id(1)], + /// String::from("Folder/OldFile.jpg"), + /// String::from("NewFile.jpg"), + /// ) + /// .await?; /// println!("rename-path result: {:#?}", res); /// /// Ok(()) @@ -568,20 +690,28 @@ impl TransClient { /// extern crate transmission_rpc; /// /// use std::env; + /// /// use dotenvy::dotenv; - /// use transmission_rpc::TransClient; - /// use transmission_rpc::types::{Result, RpcResponse, BasicAuth}; - /// use transmission_rpc::types::{TorrentAddArgs, TorrentAddedOrDuplicate}; + /// use transmission_rpc::{ + /// types::{BasicAuth, Result, RpcResponse, TorrentAddArgs, TorrentAddedOrDuplicate}, + /// TransClient, + /// }; /// /// #[tokio::main] /// async fn main() -> Result<()> { /// dotenv().ok(); /// env_logger::init(); - /// let url= env::var("TURL")?; - /// let basic_auth = BasicAuth{user: env::var("TUSER")?, password: env::var("TPWD")?}; + /// let url = env::var("TURL")?; + /// let basic_auth = BasicAuth { + /// user: env::var("TUSER")?, + /// password: env::var("TPWD")?, + /// }; /// let mut client = TransClient::with_auth(url.parse()?, basic_auth); /// let add: TorrentAddArgs = TorrentAddArgs { - /// filename: Some("https://releases.ubuntu.com/jammy/ubuntu-22.04.1-desktop-amd64.iso.torrent".to_string()), + /// filename: Some( + /// "https://releases.ubuntu.com/jammy/ubuntu-22.04.1-desktop-amd64.iso.torrent" + /// .to_string(), + /// ), /// ..TorrentAddArgs::default() /// }; /// let res: RpcResponse = client.torrent_add(add).await?; @@ -594,7 +724,6 @@ impl TransClient { /// /// # Panics /// Either metainfo or torrent filename must be set or this call will panic. - /// pub async fn torrent_add( &mut self, add: TorrentAddArgs, @@ -669,10 +798,12 @@ impl BodyString for reqwest::RequestBuilder { #[cfg(test)] mod tests { - use super::*; - use dotenvy::dotenv; use std::env; + use dotenvy::dotenv; + + use super::*; + #[tokio::test] pub async fn test_malformed_url() -> Result<()> { dotenv().ok(); diff --git a/src/sync.rs b/src/sync.rs new file mode 100644 index 0000000..871646d --- /dev/null +++ b/src/sync.rs @@ -0,0 +1,836 @@ +use std::{ops::Deref, sync::RwLock}; + +use reqwest::{header::CONTENT_TYPE, Client, StatusCode, Url}; +use serde::de::DeserializeOwned; + +use crate::{ + types::{ + BasicAuth, BlocklistUpdate, FreeSpace, Id, Nothing, PortTest, Result, RpcRequest, + RpcResponse, RpcResponseArgument, SessionClose, SessionGet, SessionStats, Torrent, + TorrentAction, TorrentAddArgs, TorrentAddedOrDuplicate, TorrentGetField, TorrentRenamePath, + TorrentSetArgs, Torrents, + }, + MAX_RETRIES, +}; + +#[derive(Clone, Debug)] +enum TransError { + MaxRetriesReached, + NoSessionIdReceived, +} + +impl std::fmt::Display for TransError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match *self { + TransError::MaxRetriesReached => write!(f, "Max retries reached!"), + TransError::NoSessionIdReceived => write!(f, "No session id received!"), + } + } +} + +impl std::error::Error for TransError {} + +pub struct SharableTransClient { + url: Url, + auth: Option, + session_id: RwLock>, + client: Client, +} + +impl SharableTransClient { + /// Returns HTTP(S) client with configured Basic Auth + #[must_use] + pub fn with_auth(url: Url, basic_auth: BasicAuth) -> SharableTransClient { + SharableTransClient { + url, + auth: Some(basic_auth), + session_id: RwLock::new(None), + client: Client::new(), + } + } + + /// Returns HTTP(S) client + #[must_use] + pub fn new(url: Url) -> SharableTransClient { + SharableTransClient { + url, + auth: None, + session_id: RwLock::new(None), + client: Client::new(), + } + } + + /// Prepares a request for provided server and auth + fn rpc_request(&self) -> reqwest::RequestBuilder { + if let Some(auth) = &self.auth { + self.client + .post(self.url.clone()) + .basic_auth(&auth.user, Some(&auth.password)) + } else { + self.client.post(self.url.clone()) + } + .header(CONTENT_TYPE, "application/json") + } + + /// Performs a session get call + /// + /// # Errors + /// + /// Any IO Error or Deserialization error + /// + /// # Example + /// + /// ``` + /// extern crate transmission_rpc; + /// + /// use std::env; + /// + /// use dotenvy::dotenv; + /// use transmission_rpc::{ + /// types::{BasicAuth, Result, RpcResponse, SessionGet}, + /// TransClient, + /// }; + /// + /// #[tokio::main] + /// async fn main() -> Result<()> { + /// dotenv().ok(); + /// env_logger::init(); + /// let url = env::var("TURL")?; + /// let basic_auth = BasicAuth { + /// user: env::var("TUSER")?, + /// password: env::var("TPWD")?, + /// }; + /// let mut client = TransClient::with_auth(url.parse()?, basic_auth); + /// let response: Result> = client.session_get().await; + /// match response { + /// Ok(_) => println!("Yay!"), + /// Err(_) => panic!("Oh no!"), + /// } + /// println!("Rpc response is ok: {}", response?.is_ok()); + /// Ok(()) + /// } + /// ``` + pub async fn session_get(&mut self) -> Result> { + self.call(RpcRequest::session_get()).await + } + + /// Performs a session stats call + /// + /// # Errors + /// + /// Any IO Error or Deserialization error + /// + /// # Example + /// + /// ``` + /// extern crate transmission_rpc; + /// + /// use std::env; + /// + /// use dotenvy::dotenv; + /// use transmission_rpc::{ + /// types::{BasicAuth, Result, RpcResponse, SessionStats}, + /// TransClient, + /// }; + /// + /// #[tokio::main] + /// async fn main() -> Result<()> { + /// dotenv().ok(); + /// env_logger::init(); + /// let url = env::var("TURL")?; + /// let basic_auth = BasicAuth { + /// user: env::var("TUSER")?, + /// password: env::var("TPWD")?, + /// }; + /// let mut client = TransClient::with_auth(url.parse()?, basic_auth); + /// let response: Result> = client.session_stats().await; + /// match response { + /// Ok(_) => println!("Yay!"), + /// Err(_) => panic!("Oh no!"), + /// } + /// println!("Rpc response is ok: {}", response?.is_ok()); + /// Ok(()) + /// } + /// ``` + pub async fn session_stats(&mut self) -> Result> { + self.call(RpcRequest::session_stats()).await + } + + /// Performs a session close call + /// + /// # Errors + /// + /// Any IO Error or Deserialization error + /// + /// # Example + /// + /// ``` + /// extern crate transmission_rpc; + /// + /// use std::env; + /// + /// use dotenvy::dotenv; + /// use transmission_rpc::{ + /// types::{BasicAuth, Result, RpcResponse, SessionClose}, + /// TransClient, + /// }; + /// + /// #[tokio::main] + /// async fn main() -> Result<()> { + /// dotenv().ok(); + /// env_logger::init(); + /// let url = env::var("TURL")?; + /// let basic_auth = BasicAuth { + /// user: env::var("TUSER")?, + /// password: env::var("TPWD")?, + /// }; + /// let mut client = TransClient::with_auth(url.parse()?, basic_auth); + /// let response: Result> = client.session_close().await; + /// match response { + /// Ok(_) => println!("Yay!"), + /// Err(_) => panic!("Oh no!"), + /// } + /// println!("Rpc response is ok: {}", response?.is_ok()); + /// Ok(()) + /// } + /// ``` + pub async fn session_close(&mut self) -> Result> { + self.call(RpcRequest::session_close()).await + } + + /// Performs a blocklist update call + /// + /// # Errors + /// + /// Any IO Error or Deserialization error + /// + /// # Example + /// + /// ``` + /// extern crate transmission_rpc; + /// + /// use std::env; + /// + /// use dotenvy::dotenv; + /// use transmission_rpc::{ + /// types::{BasicAuth, BlocklistUpdate, Result, RpcResponse}, + /// TransClient, + /// }; + /// + /// #[tokio::main] + /// async fn main() -> Result<()> { + /// dotenv().ok(); + /// env_logger::init(); + /// let url = env::var("TURL")?; + /// let basic_auth = BasicAuth { + /// user: env::var("TUSER")?, + /// password: env::var("TPWD")?, + /// }; + /// let mut client = TransClient::with_auth(url.parse()?, basic_auth); + /// let response: Result> = client.blocklist_update().await; + /// match response { + /// Ok(_) => println!("Yay!"), + /// Err(_) => panic!("Oh no!"), + /// } + /// println!("Rpc response is ok: {}", response?.is_ok()); + /// Ok(()) + /// } + /// ``` + pub async fn blocklist_update(&mut self) -> Result> { + self.call(RpcRequest::blocklist_update()).await + } + + /// Performs a session stats call + /// + /// # Errors + /// + /// Any IO Error or Deserialization error + /// + /// # Example + /// + /// ``` + /// extern crate transmission_rpc; + /// + /// use std::env; + /// + /// use dotenvy::dotenv; + /// use transmission_rpc::{ + /// types::{BasicAuth, FreeSpace, Result, RpcResponse}, + /// TransClient, + /// }; + /// + /// #[tokio::main] + /// async fn main() -> Result<()> { + /// dotenv().ok(); + /// env_logger::init(); + /// let url = env::var("TURL")?; + /// let dir = env::var("TDIR")?; + /// let basic_auth = BasicAuth { + /// user: env::var("TUSER")?, + /// password: env::var("TPWD")?, + /// }; + /// let mut client = TransClient::with_auth(url.parse()?, basic_auth); + /// let response: Result> = client.free_space(dir).await; + /// match response { + /// Ok(_) => println!("Yay!"), + /// Err(_) => panic!("Oh no!"), + /// } + /// println!("Rpc response is ok: {}", response?.is_ok()); + /// Ok(()) + /// } + /// ``` + pub async fn free_space(&self, path: String) -> Result> { + self.call(RpcRequest::free_space(path)).await + } + + /// Performs a port test call + /// + /// # Errors + /// + /// Any IO Error or Deserialization error + /// + /// # Example + /// + /// ``` + /// extern crate transmission_rpc; + /// + /// use std::env; + /// + /// use dotenvy::dotenv; + /// use transmission_rpc::{ + /// types::{BasicAuth, PortTest, Result, RpcResponse}, + /// TransClient, + /// }; + /// + /// #[tokio::main] + /// async fn main() -> Result<()> { + /// dotenv().ok(); + /// env_logger::init(); + /// let url = env::var("TURL")?; + /// let basic_auth = BasicAuth { + /// user: env::var("TUSER")?, + /// password: env::var("TPWD")?, + /// }; + /// let mut client = TransClient::with_auth(url.parse()?, basic_auth); + /// let response: Result> = client.port_test().await; + /// match response { + /// Ok(_) => println!("Yay!"), + /// Err(_) => panic!("Oh no!"), + /// } + /// println!("Rpc response is ok: {}", response?.is_ok()); + /// Ok(()) + /// } + /// ``` + pub async fn port_test(&mut self) -> Result> { + self.call(RpcRequest::port_test()).await + } + + /// Performs a torrent get call + /// fields - if None then ALL fields + /// ids - if None then All items + /// + /// # Errors + /// + /// Any IO Error or Deserialization error + /// + /// # Example + /// + /// ``` + /// extern crate transmission_rpc; + /// + /// use std::env; + /// + /// use dotenvy::dotenv; + /// use transmission_rpc::{ + /// types::{BasicAuth, Id, Result, RpcResponse, Torrent, TorrentGetField, Torrents}, + /// TransClient, + /// }; + /// + /// #[tokio::main] + /// async fn main() -> Result<()> { + /// dotenv().ok(); + /// env_logger::init(); + /// let url = env::var("TURL")?; + /// let basic_auth = BasicAuth { + /// user: env::var("TUSER")?, + /// password: env::var("TPWD")?, + /// }; + /// let mut client = TransClient::with_auth(url.parse()?, basic_auth); + /// + /// let res: RpcResponse> = client.torrent_get(None, None).await?; + /// let names: Vec<&String> = res + /// .arguments + /// .torrents + /// .iter() + /// .map(|it| it.name.as_ref().unwrap()) + /// .collect(); + /// println!("{:#?}", names); + /// + /// let res1: RpcResponse> = client + /// .torrent_get( + /// Some(vec![TorrentGetField::Id, TorrentGetField::Name]), + /// Some(vec![Id::Id(1), Id::Id(2), Id::Id(3)]), + /// ) + /// .await?; + /// let first_three: Vec = res1 + /// .arguments + /// .torrents + /// .iter() + /// .map(|it| { + /// format!( + /// "{}. {}", + /// &it.id.as_ref().unwrap(), + /// &it.name.as_ref().unwrap() + /// ) + /// }) + /// .collect(); + /// println!("{:#?}", first_three); + /// + /// let res2: RpcResponse> = client + /// .torrent_get( + /// Some(vec![ + /// TorrentGetField::Id, + /// TorrentGetField::HashString, + /// TorrentGetField::Name, + /// ]), + /// Some(vec![Id::Hash(String::from( + /// "64b0d9a53ac9cd1002dad1e15522feddb00152fe", + /// ))]), + /// ) + /// .await?; + /// let info: Vec = res2 + /// .arguments + /// .torrents + /// .iter() + /// .map(|it| { + /// format!( + /// "{:5}. {:^45} {}", + /// &it.id.as_ref().unwrap(), + /// &it.hash_string.as_ref().unwrap(), + /// &it.name.as_ref().unwrap() + /// ) + /// }) + /// .collect(); + /// println!("{:#?}", info); + /// + /// Ok(()) + /// } + /// ``` + pub async fn torrent_get( + &self, + fields: Option>, + ids: Option>, + ) -> Result>> { + self.call(RpcRequest::torrent_get(fields, ids)).await + } + + /// Performs a torrent set call + /// args - the fields to update + /// ids - if None then All items + /// + /// # Errors + /// + /// Any IO Error or Deserialization error + /// + /// # Example + /// + /// ``` + /// extern crate transmission_rpc; + /// + /// use std::env; + /// + /// use dotenvy::dotenv; + /// use transmission_rpc::{ + /// types::{BasicAuth, Id, Result, RpcResponse, Torrent, TorrentSetArgs, Torrents}, + /// TransClient, + /// }; + /// + /// #[tokio::main] + /// async fn main() -> Result<()> { + /// dotenv().ok(); + /// env_logger::init(); + /// + /// let url = env::var("TURL")?.parse()?; + /// let basic_auth = BasicAuth { + /// user: env::var("TUSER")?, + /// password: env::var("TPWD")?, + /// }; + /// let mut client = TransClient::with_auth(url, basic_auth); + /// + /// let args = TorrentSetArgs { + /// labels: Some(vec![String::from("blue")]), + /// ..Default::default() + /// }; + /// assert!( + /// client + /// .torrent_set(args, Some(vec![Id::Id(0)])) + /// .await? + /// .is_ok() + /// ); + /// + /// Ok(()) + /// } + /// ``` + pub async fn torrent_set( + &self, + args: TorrentSetArgs, + ids: Option>, + ) -> Result> { + self.call(RpcRequest::torrent_set(args, ids)).await + } + + /// Performs a torrent action call + /// + /// # Errors + /// + /// Any IO Error or Deserialization error + /// + /// # Example + /// + /// ``` + /// extern crate transmission_rpc; + /// + /// use std::env; + /// + /// use dotenvy::dotenv; + /// use transmission_rpc::{ + /// types::{BasicAuth, Id, Nothing, Result, RpcResponse, TorrentAction}, + /// TransClient, + /// }; + /// + /// #[tokio::main] + /// async fn main() -> Result<()> { + /// dotenv().ok(); + /// env_logger::init(); + /// let url = env::var("TURL")?; + /// let basic_auth = BasicAuth { + /// user: env::var("TUSER")?, + /// password: env::var("TPWD")?, + /// }; + /// let mut client = TransClient::with_auth(url.parse()?, basic_auth); + /// let res1: RpcResponse = client + /// .torrent_action(TorrentAction::Start, vec![Id::Id(1)]) + /// .await?; + /// println!("Start result: {:?}", &res1.is_ok()); + /// let res2: RpcResponse = client + /// .torrent_action(TorrentAction::Stop, vec![Id::Id(1)]) + /// .await?; + /// println!("Stop result: {:?}", &res2.is_ok()); + /// + /// Ok(()) + /// } + /// ``` + pub async fn torrent_action( + &self, + action: TorrentAction, + ids: Vec, + ) -> Result> { + self.call(RpcRequest::torrent_action(action, ids)).await + } + + /// Performs a torrent remove call + /// + /// # Errors + /// + /// Any IO Error or Deserialization error + /// + /// # Example + /// + /// ``` + /// extern crate transmission_rpc; + /// + /// use std::env; + /// + /// use dotenvy::dotenv; + /// use transmission_rpc::{ + /// types::{BasicAuth, Id, Nothing, Result, RpcResponse}, + /// TransClient, + /// }; + /// + /// #[tokio::main] + /// async fn main() -> Result<()> { + /// dotenv().ok(); + /// env_logger::init(); + /// let url = env::var("TURL")?; + /// let basic_auth = BasicAuth { + /// user: env::var("TUSER")?, + /// password: env::var("TPWD")?, + /// }; + /// let mut client = TransClient::with_auth(url.parse()?, basic_auth); + /// let res: RpcResponse = client.torrent_remove(vec![Id::Id(1)], false).await?; + /// println!("Remove result: {:?}", &res.is_ok()); + /// + /// Ok(()) + /// } + /// ``` + pub async fn torrent_remove( + &self, + ids: Vec, + delete_local_data: bool, + ) -> Result> { + self.call(RpcRequest::torrent_remove(ids, delete_local_data)) + .await + } + + /// Performs a torrent set location call + /// + /// # Errors + /// + /// Any IO Error or Deserialization error + /// + /// # Example + /// + /// ``` + /// extern crate transmission_rpc; + /// + /// use std::env; + /// + /// use dotenvy::dotenv; + /// use transmission_rpc::{ + /// types::{BasicAuth, Id, Nothing, Result, RpcResponse}, + /// TransClient, + /// }; + /// + /// #[tokio::main] + /// async fn main() -> Result<()> { + /// dotenv().ok(); + /// env_logger::init(); + /// let url = env::var("TURL")?; + /// let basic_auth = BasicAuth { + /// user: env::var("TUSER")?, + /// password: env::var("TPWD")?, + /// }; + /// let mut client = TransClient::with_auth(url.parse()?, basic_auth); + /// let res: RpcResponse = client + /// .torrent_set_location( + /// vec![Id::Id(1)], + /// String::from("/new/location"), + /// Option::from(false), + /// ) + /// .await?; + /// println!("Set-location result: {:?}", &res.is_ok()); + /// + /// Ok(()) + /// } + /// ``` + pub async fn torrent_set_location( + &self, + ids: Vec, + location: String, + move_from: Option, + ) -> Result> { + self.call(RpcRequest::torrent_set_location(ids, location, move_from)) + .await + } + + /// Performs a torrent rename path call + /// + /// # Errors + /// + /// Any IO Error or Deserialization error + /// + /// # Example + /// + /// ``` + /// extern crate transmission_rpc; + /// + /// use std::env; + /// + /// use dotenvy::dotenv; + /// use transmission_rpc::{ + /// types::{BasicAuth, Id, Result, RpcResponse, TorrentRenamePath}, + /// TransClient, + /// }; + /// + /// #[tokio::main] + /// async fn main() -> Result<()> { + /// dotenv().ok(); + /// env_logger::init(); + /// let url = env::var("TURL")?; + /// let basic_auth = BasicAuth { + /// user: env::var("TUSER")?, + /// password: env::var("TPWD")?, + /// }; + /// let mut client = TransClient::with_auth(url.parse()?, basic_auth); + /// let res: RpcResponse = client + /// .torrent_rename_path( + /// vec![Id::Id(1)], + /// String::from("Folder/OldFile.jpg"), + /// String::from("NewFile.jpg"), + /// ) + /// .await?; + /// println!("rename-path result: {:#?}", res); + /// + /// Ok(()) + /// } + /// ``` + pub async fn torrent_rename_path( + &self, + ids: Vec, + path: String, + name: String, + ) -> Result> { + self.call(RpcRequest::torrent_rename_path(ids, path, name)) + .await + } + + /// Performs a torrent add call + /// + /// # Errors + /// + /// Any IO Error or Deserialization error + /// + /// # Example + /// + /// ``` + /// extern crate transmission_rpc; + /// + /// use std::env; + /// + /// use dotenvy::dotenv; + /// use transmission_rpc::{ + /// types::{BasicAuth, Result, RpcResponse, TorrentAddArgs, TorrentAddedOrDuplicate}, + /// TransClient, + /// }; + /// + /// #[tokio::main] + /// async fn main() -> Result<()> { + /// dotenv().ok(); + /// env_logger::init(); + /// let url = env::var("TURL")?; + /// let basic_auth = BasicAuth { + /// user: env::var("TUSER")?, + /// password: env::var("TPWD")?, + /// }; + /// let mut client = TransClient::with_auth(url.parse()?, basic_auth); + /// let add: TorrentAddArgs = TorrentAddArgs { + /// filename: Some( + /// "https://releases.ubuntu.com/jammy/ubuntu-22.04.1-desktop-amd64.iso.torrent" + /// .to_string(), + /// ), + /// ..TorrentAddArgs::default() + /// }; + /// let res: RpcResponse = client.torrent_add(add).await?; + /// println!("Add result: {:?}", &res.is_ok()); + /// println!("response: {:?}", &res); + /// + /// Ok(()) + /// } + /// ``` + /// + /// # Panics + /// Either metainfo or torrent filename must be set or this call will panic. + pub async fn torrent_add( + &self, + add: TorrentAddArgs, + ) -> Result> { + assert!( + !(add.metainfo.is_none() && add.filename.is_none()), + "Metainfo or Filename should be provided" + ); + self.call(RpcRequest::torrent_add(add)).await + } + + /// Performs a JRPC call to the server + /// + /// # Errors + /// + /// Any IO Error or Deserialization error + async fn call(&self, request: RpcRequest) -> Result> + where + RS: RpcResponseArgument + DeserializeOwned + std::fmt::Debug, + { + let mut remaining_retries = MAX_RETRIES; + loop { + remaining_retries = remaining_retries + .checked_sub(1) + .ok_or(TransError::MaxRetriesReached)?; + + info!("Loaded auth: {:?}", &self.auth); + let rq = match &self.session_id.read().expect("lock being poisoned").deref() { + None => self.rpc_request(), + Some(id) => self.rpc_request().header("X-Transmission-Session-Id", id), + } + .json(&request); + + info!( + "Request body: {:?}", + rq.try_clone() + .expect("Unable to get the request body") + .body_string()? + ); + + let rsp: reqwest::Response = rq.send().await?; + if matches!(rsp.status(), StatusCode::CONFLICT) { + let session_id = rsp + .headers() + .get("X-Transmission-Session-Id") + .ok_or(TransError::NoSessionIdReceived)? + .to_str()?; + *self.session_id.write().expect("lock being poisoned") = + Some(String::from(session_id)); + + info!("Got new session_id: {}. Retrying request.", session_id); + } else { + let rpc_response: RpcResponse = rsp.json().await?; + info!("Response body: {:#?}", rpc_response); + + return Ok(rpc_response); + } + } + } +} + +trait BodyString { + fn body_string(self) -> Result; +} + +impl BodyString for reqwest::RequestBuilder { + fn body_string(self) -> Result { + let rq = self.build()?; + let body = rq.body().unwrap().as_bytes().unwrap(); + Ok(std::str::from_utf8(body)?.to_string()) + } +} + +#[cfg(test)] +mod tests { + use std::env; + + use dotenvy::dotenv; + + use super::*; + + #[tokio::test] + pub async fn test_malformed_url() -> Result<()> { + dotenv().ok(); + env_logger::init(); + let url = env::var("TURL")?; + let mut client; + if let (Ok(user), Ok(password)) = (env::var("TUSER"), env::var("TPWD")) { + client = SharableTransClient::with_auth(url.parse()?, BasicAuth { user, password }); + } else { + client = SharableTransClient::new(url.parse()?); + } + info!("Client is ready!"); + let add: TorrentAddArgs = TorrentAddArgs { + filename: Some( + "https://releases.ubuntu.com/jammy/ubuntu-22.04.1-desktop-amd64.iso.torrentt" + .to_string(), + ), + ..TorrentAddArgs::default() + }; + match client.torrent_add(add).await { + Ok(res) => { + println!("Add result: {:?}", &res.is_ok()); + println!("response: {:?}", &res); + assert!(!&res.is_ok()); + } + Err(e) => { + println!("Error: {:#?}", e); + } + } + + Ok(()) + } +}