// #[allow(unused_imports)] // #[allow(dead_code)] extern crate env_logger; #[macro_use] extern crate log; extern crate reqwest; use serde::de::DeserializeOwned; use reqwest::header::CONTENT_TYPE; pub mod types; use types::BasicAuth; use types::{Result, RpcResponse, RpcResponseArgument, RpcRequest, Nothing}; use types::SessionGet; use types::{TorrentGetField, Torrents, Torrent, Id}; use types::TorrentAction; 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 /// /// Panics if any IO error happens async fn get_session_id(&self) -> String { info!("Requesting session id info"); let response: reqwest::Response = self.rpc_request() .json(&RpcRequest::session_get()) .send() .await .unwrap(); let session_id = response.headers() .get("x-transmission-session-id") .expect("Unable to get session id") .to_str() .unwrap() .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 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 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().unwrap().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()) } }