extern crate env_logger; #[macro_use] extern crate log; extern crate reqwest; use reqwest::header::CONTENT_TYPE; use serde::de::DeserializeOwned; pub mod types; use types::BasicAuth; use types::BlocklistUpdate; use types::SessionGet; use types::SessionStats; use types::PortTest; use types::TorrentAction; use types::{Id, Torrent, TorrentGetField, Torrents}; use types::{Nothing, Result, RpcRequest, RpcResponse, RpcResponseArgument, TorrentRenamePath}; use types::{TorrentAddArgs, TorrentAdded}; pub struct TransClient { url: String, auth: Option, } impl TransClient { /// Returns HTTP(S) client with configured Basic Auth pub fn with_auth(url: &str, basic_auth: BasicAuth) -> TransClient { TransClient { url: url.to_string(), auth: Some(basic_auth), } } /// Returns HTTP(S) client pub fn new(url: &str) -> TransClient { TransClient { url: url.to_string(), auth: None, } } /// Prepares a request for provided server and auth fn rpc_request(&self) -> reqwest::RequestBuilder { let client = reqwest::Client::new(); if let Some(auth) = &self.auth { client .post(&self.url) .basic_auth(&auth.user, Some(&auth.password)) } else { client.post(&self.url) } .header(CONTENT_TYPE, "application/json") } /// Performs session-get call and takes the x-transmission-session-id /// header to perform calls, using it's value /// /// # Errors /// /// If response is impossible to unwrap then it will return an empty session_id async fn get_session_id(&self) -> String { info!("Requesting session id info"); let response: reqwest::Result = self .rpc_request() .json(&RpcRequest::session_get()) .send() .await; let session_id = match response { Ok(ref resp) => match resp.headers().get("x-transmission-session-id") { Some(res) => res.to_str().expect("header value should be a string"), _ => "", }, _ => "", } .to_owned(); info!("Received session id: {}", session_id); session_id } /// Performs a session get call /// /// # Errors /// /// Any IO Error or Deserialization error /// /// # Example /// /// ``` /// extern crate transmission_rpc; /// /// use std::env; /// use dotenv::dotenv; /// use transmission_rpc::TransClient; /// use transmission_rpc::types::{Result, RpcResponse, SessionGet, BasicAuth}; /// /// #[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 client = TransClient::with_auth(&url, basic_auth); /// let response: Result> = client.session_get().await; /// match response { /// Ok(_) => println!("Yay!"), /// Err(_) => panic!("Oh no!") /// } /// println!("Rpc reqsponse is ok: {}", response?.is_ok()); /// Ok(()) /// } /// ``` pub async fn session_get(&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 dotenv::dotenv; /// use transmission_rpc::TransClient; /// use transmission_rpc::types::{Result, RpcResponse, SessionStats, BasicAuth}; /// /// #[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 client = TransClient::with_auth(&url, basic_auth); /// let response: Result> = client.session_stats().await; /// match response { /// Ok(_) => println!("Yay!"), /// Err(_) => panic!("Oh no!") /// } /// println!("Rpc reqsponse is ok: {}", response?.is_ok()); /// Ok(()) /// } /// ``` pub async fn session_stats(&self) -> Result> { self.call(RpcRequest::session_stats()).await } /// Performs a blocklist update call /// /// # Errors /// /// Any IO Error or Deserialization error /// /// # Example /// /// ``` /// extern crate transmission_rpc; /// /// use std::env; /// use dotenv::dotenv; /// use transmission_rpc::TransClient; /// use transmission_rpc::types::{Result, BlockListUpdate, RpcResponse, BasicAuth}; /// /// #[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 client = TransClient::with_auth(&url, basic_auth); /// let response: Result> = client.blocklist_update().await; /// match response { /// Ok(_) => println!("Yay!"), /// Err(_) => panic!("Oh no!") /// } /// println!("Rpc reqsponse is ok: {}", response?.is_ok()); /// Ok(()) /// } /// ``` pub async fn blocklist_update(&self) -> Result> { self.call(RpcRequest::blocklist_update()).await } /// Performs a port test call /// /// # Errors /// /// Any IO Error or Deserialization error /// /// # Example /// /// ``` /// extern crate transmission_rpc; /// /// use std::env; /// use dotenv::dotenv; /// use transmission_rpc::TransClient; /// use transmission_rpc::types::{Result, RpcResponse, BasicAuth, PortTest}; /// /// #[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 client = TransClient::with_auth(&url, basic_auth); /// let response: Result> = client.port_test().await; /// match response { /// Ok(_) => println!("Yay!"), /// Err(_) => panic!("Oh no!") /// } /// println!("Rpc reqsponse is ok: {}", response?.is_ok()); /// Ok(()) /// } /// ``` pub async fn port_test(&self) -> Result> { self.call(RpcRequest::port_test()).await } /// Performs a torrent get call /// fileds - 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 dotenv::dotenv; /// use transmission_rpc::TransClient; /// use transmission_rpc::types::{Result, RpcResponse, BasicAuth}; /// use transmission_rpc::types::{Torrents, Torrent, TorrentGetField, Id}; /// /// #[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 client = TransClient::with_auth(&url, 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 action call /// /// # Errors /// /// Any IO Error or Deserialization error /// /// # Example /// /// ``` /// extern crate transmission_rpc; /// /// use std::env; /// use dotenv::dotenv; /// use transmission_rpc::TransClient; /// use transmission_rpc::types::{Result, RpcResponse, BasicAuth}; /// use transmission_rpc::types::{TorrentAction, Nothing, Id}; /// /// #[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 client = TransClient::with_auth(&url, 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 dotenv::dotenv; /// use transmission_rpc::TransClient; /// use transmission_rpc::types::{Result, RpcResponse, BasicAuth}; /// use transmission_rpc::types::{Nothing, Id}; /// /// #[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 client = TransClient::with_auth(&url, 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 dotenv::dotenv; /// use transmission_rpc::TransClient; /// use transmission_rpc::types::{Result, RpcResponse, BasicAuth}; /// use transmission_rpc::types::{Nothing, Id}; /// /// #[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 client = TransClient::with_auth(&url, 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 dotenv::dotenv; /// use transmission_rpc::TransClient; /// use transmission_rpc::types::{Result, RpcResponse, BasicAuth}; /// use transmission_rpc::types::{TorrentRenamePath, Id}; /// /// #[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 client = TransClient::with_auth(&url, 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 dotenv::dotenv; /// use transmission_rpc::TransClient; /// use transmission_rpc::types::{Result, RpcResponse, BasicAuth}; /// use transmission_rpc::types::{TorrentAddArgs, TorrentAdded}; /// /// #[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 client = TransClient::with_auth(&url, basic_auth); /// let add: TorrentAddArgs = TorrentAddArgs { /// filename: Some("https://releases.ubuntu.com/20.04/ubuntu-20.04-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(()) /// } /// ``` pub async fn torrent_add(&self, add: TorrentAddArgs) -> Result> { if add.metainfo == None && add.filename == None { panic!("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, { info!("Loaded auth: {:?}", &self.auth); let rq: reqwest::RequestBuilder = self .rpc_request() .header("X-Transmission-Session-Id", self.get_session_id().await) .json(&request); info!( "Request body: {:?}", rq.try_clone() .expect("Unable to get the request body") .body_string()? ); let resp: reqwest::Response = rq.send().await?; let rpc_response: RpcResponse = resp.json().await?; info!("Response body: {:#?}", rpc_response); 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 super::*; use dotenv::dotenv; use std::env; #[tokio::test] pub async fn test_malformed_url() -> Result<()> { dotenv().ok(); env_logger::init(); let url = env::var("TURL")?; let client; if let (Ok(user), Ok(password)) = (env::var("TUSER"), env::var("TPWD")) { client = TransClient::with_auth(&url, BasicAuth {user, password}); } else { client = TransClient::new(&url); } info!("Client is ready!"); let add: TorrentAddArgs = TorrentAddArgs { filename: Some( "https://releases.ubuntu.com/20.04/ubuntu-20.04-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(()) } }