diff --git a/Cargo.toml b/Cargo.toml index be99439..1c3ffb9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "transmission-rpc" -version = "0.1.1" +version = "0.2.0" authors = ["red "] edition = "2018" repository = "https://github.com/j0rsa/transmission-rpc" diff --git a/README.md b/README.md index e3d6cf4..c9b0221 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,8 @@ spec: https://github.com/transmission/transmission/blob/master/extras/rpc-spec.t - [ ] torrent-set - [X] torrent-get -- [ ] torrent-add -- [ ] torrent-remove +- [X] torrent-add +- [X] torrent-remove - [ ] torrent-set-location - [ ] torrent-rename-path - [ ] session-set diff --git a/examples/torrent-add.rs b/examples/torrent-add.rs new file mode 100644 index 0000000..71501b6 --- /dev/null +++ b/examples/torrent-add.rs @@ -0,0 +1,25 @@ +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(()) +} \ No newline at end of file diff --git a/examples/torrent-get.rs b/examples/torrent-get.rs index 0c6d4ef..cbb363f 100644 --- a/examples/torrent-get.rs +++ b/examples/torrent-get.rs @@ -16,5 +16,6 @@ async fn main() -> Result<()> { let res: RpcResponse> = client.torrent_get(vec![TorrentGetField::Id, TorrentGetField::Name]).await?; let names: Vec<&String> = res.arguments.torrents.iter().map(|it| it.name.as_ref().unwrap()).collect(); println!("{:#?}", names); + Ok(()) } \ No newline at end of file diff --git a/examples/torrent-remove.rs b/examples/torrent-remove.rs new file mode 100644 index 0000000..831b206 --- /dev/null +++ b/examples/torrent-remove.rs @@ -0,0 +1,20 @@ +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}; + +#[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![1], false).await?; + println!("Remove result: {:?}", &res.is_ok()); + + Ok(()) +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 9d348da..4f1e2c5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,7 @@ use types::{Result, RpcResponse, RpcResponseArgument, RpcRequest, Nothing}; use types::SessionGet; use types::{TorrentGetField, Torrents, Torrent}; use types::TorrentAction; +use types::{TorrentAddArgs, TorrentAdded}; pub struct TransClient { url: String, @@ -112,7 +113,36 @@ impl TransClient { self.call(RpcRequest::torrent_action(action, ids)).await } - /// Performs an JRPC call to the server + /// Performs a torrent remove call + /// + /// # Errors + /// + /// Any IO Error or Deserialization error + /// + /// # Example + /// + /// in examples/torrent-remove.rs + 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 + /// + /// in examples/torrent-add.rs + 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 /// @@ -142,26 +172,4 @@ impl BodyString for reqwest::RequestBuilder { let body = rq.body().unwrap().as_bytes().unwrap(); Ok(std::str::from_utf8(body)?.to_string()) } -} - -#[cfg(test)] -mod tests { - use crate::Result; - use crate::{TransClient, BasicAuth, TorrentGetField}; - use crate::{RpcResponse, Torrents, Torrent}; - use std::env; - use dotenv::dotenv; - - #[tokio::test] - async fn it_works() -> 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(vec![TorrentGetField::Id, TorrentGetField::Name]).await?; - let names: Vec<&String> = res.arguments.torrents.iter().map(|it| it.name.as_ref().unwrap()).collect(); - println!("{:#?}", names); - Ok(()) - } -} +} \ No newline at end of file diff --git a/src/types/mod.rs b/src/types/mod.rs index 1a11514..da626d7 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -14,10 +14,13 @@ pub(crate) use self::request::RpcRequest; pub use self::request::ArgumentFields; pub use self::request::TorrentGetField; pub use self::request::TorrentAction; +pub use self::request::TorrentAddArgs; +pub use self::request::File; pub use self::response::RpcResponse; pub(crate) use self::response::RpcResponseArgument; pub use self::response::SessionGet; pub use self::response::Torrents; pub use self::response::Torrent; +pub use self::response::TorrentAdded; pub use self::response::Nothing; \ No newline at end of file diff --git a/src/types/request.rs b/src/types/request.rs index 0df85e9..a8723e7 100644 --- a/src/types/request.rs +++ b/src/types/request.rs @@ -15,38 +15,126 @@ impl RpcRequest { } } - pub fn torrent_get(fields: Vec) -> RpcRequest { + pub fn torrent_get(fields: Vec) -> RpcRequest { let string_fields = fields.iter().map(|f| f.to_str()).collect(); RpcRequest { method: String::from("torrent-get"), - arguments: Some (Args { fields: Some(string_fields), ids: None }), + arguments: Some ( Args::TorrentGetArgs(TorrentGetArgs { fields: Some(string_fields)} )), + } + } + + pub fn torrent_remove(ids: Vec, delete_local_data: bool) -> RpcRequest { + RpcRequest { + method: String::from("torrent-remove"), + arguments: Some ( Args::TorrentRemoveArgs(TorrentRemoveArgs {ids, delete_local_data} ) ) + } + } + + pub fn torrent_add(add: TorrentAddArgs) -> RpcRequest { + RpcRequest { + method: String::from("torrent-add"), + arguments: Some ( Args::TorrentAddArgs(add) ) } } pub fn torrent_action(action: TorrentAction, ids: Vec) -> RpcRequest { RpcRequest { method: action.to_str(), - arguments: Some (Args { fields: None, ids: Some(ids) }), + arguments: Some ( Args::TorrentActionArgs(TorrentActionArgs { ids })), } } } - - pub trait ArgumentFields {} impl ArgumentFields for TorrentGetField{} -#[derive(Serialize, Debug, RustcEncodable)] -struct Args { +#[derive(Serialize, Debug, RustcEncodable, Clone)] +#[serde(untagged)] +pub enum Args{ + TorrentGetArgs(TorrentGetArgs), + TorrentActionArgs(TorrentActionArgs), + TorrentRemoveArgs(TorrentRemoveArgs), + TorrentAddArgs(TorrentAddArgs), +} + +#[derive(Serialize, Debug, RustcEncodable, Clone)] +pub struct TorrentGetArgs { #[serde(skip_serializing_if="Option::is_none")] - fields: Option>, + fields: Option> +} + +#[derive(Serialize, Debug, RustcEncodable, Clone)] +pub struct TorrentActionArgs { + ids: Vec, +} +#[derive(Serialize, Debug, RustcEncodable, Clone)] +pub struct TorrentRemoveArgs { + ids: Vec, + #[serde(rename="delete-local-data")] + delete_local_data: bool +} + +#[derive(Serialize, Debug, RustcEncodable, Clone)] +pub struct TorrentAddArgs { #[serde(skip_serializing_if="Option::is_none")] - ids: Option> + pub cookies: Option, + #[serde(skip_serializing_if="Option::is_none", rename="download-dir")] + pub download_dir: Option, + /// Either "filename" OR "metainfo" MUST be included + /// semi-optional + /// filename or URL of the .torrent file + #[serde(skip_serializing_if="Option::is_none")] + pub filename: Option, + /// semi-optional + /// base64-encoded .torrent content + #[serde(skip_serializing_if="Option::is_none")] + pub metainfo: Option, + #[serde(skip_serializing_if="Option::is_none")] + pub paused: Option, + #[serde(skip_serializing_if="Option::is_none", rename="peer-limit")] + pub peer_limit: Option, + #[serde(skip_serializing_if="Option::is_none", rename="bandwidthPriority")] + pub bandwidth_priority: Option, + #[serde(skip_serializing_if="Option::is_none", rename="files-wanted")] + pub files_wanted: Option>, + #[serde(skip_serializing_if="Option::is_none", rename="files-unwanted")] + pub files_unwanted: Option>, + #[serde(skip_serializing_if="Option::is_none", rename="priority-high")] + pub priority_high: Option>, + #[serde(skip_serializing_if="Option::is_none", rename="priority-low")] + pub priority_low: Option>, + #[serde(skip_serializing_if="Option::is_none", rename="priority-normal")] + pub priority_normal: Option>, +} + +impl Default for TorrentAddArgs { + fn default() -> Self { + TorrentAddArgs { + cookies: None, + download_dir: None, + filename: None, + metainfo: None, + paused: None, + peer_limit: None, + bandwidth_priority: None, + files_wanted: None, + files_unwanted: None, + priority_high: None, + priority_low: None, + priority_normal: None + } + } +} + +#[derive(Serialize, Debug, RustcEncodable, Clone)] +pub struct File { + //todo } pub enum TorrentGetField { Id, Addeddate, Name, + HashString, Totalsize, Error, Errorstring, @@ -80,6 +168,7 @@ impl TorrentGetField { TorrentGetField::Id => "id", TorrentGetField::Addeddate => "addedDate", TorrentGetField::Name => "name", + TorrentGetField::HashString => "hashString", TorrentGetField::Totalsize => "totalSize", TorrentGetField::Error => "error", TorrentGetField::Errorstring => "errorString", diff --git a/src/types/response.rs b/src/types/response.rs index fe0b285..2341c7d 100644 --- a/src/types/response.rs +++ b/src/types/response.rs @@ -27,7 +27,7 @@ pub struct SessionGet { } impl RpcResponseArgument for SessionGet{} -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, RustcEncodable)] pub struct Torrent { #[serde(rename="addedDate")] pub added_date: Option, @@ -47,6 +47,8 @@ pub struct Torrent { #[serde(rename="metadataPercentComplete")] pub metadata_percent_complete: Option, pub name: Option, + #[serde(rename="hashString")] + pub hash_string: Option, #[serde(rename="peersConnected")] pub peers_connected: Option, #[serde(rename="peersGettingFromUs")] @@ -74,12 +76,12 @@ pub struct Torrent { #[serde(rename="uploadedEver")] pub uploaded_ever: Option, } -impl RpcResponseArgument for Torrents{} #[derive(Deserialize, Debug, RustcEncodable)] pub struct Torrents { pub torrents: Vec } +impl RpcResponseArgument for Torrents{} #[derive(Deserialize, Debug, RustcEncodable)] pub struct Trackers { @@ -89,4 +91,11 @@ pub struct Trackers { #[derive(Deserialize, Debug, RustcEncodable)] pub struct Nothing{} -impl RpcResponseArgument for Nothing {} \ No newline at end of file +impl RpcResponseArgument for Nothing {} + +#[derive(Deserialize, Debug, RustcEncodable)] +pub struct TorrentAdded { + #[serde(rename="torrent-added")] + pub torrent_added: Torrent +} +impl RpcResponseArgument for TorrentAdded{} \ No newline at end of file