transmission-client-0.1.5/.cargo_vcs_info.json0000644000000001360000000000100150440ustar { "git": { "sha1": "9adc7aeb0e1c8d847b99576bc1858b922160bfcf" }, "path_in_vcs": "" }transmission-client-0.1.5/.gitignore000064400000000000000000000000271046102023000156230ustar 00000000000000/target Cargo.lock wip transmission-client-0.1.5/Cargo.toml0000644000000026460000000000100130520ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "transmission-client" version = "0.1.5" authors = ["Felix Häcker "] description = "Rust wrapper for Transmission rpc specs" homepage = "https://gitlab.gnome.org/haecker-felix/transmission-client" documentation = "https://docs.rs/transmission-client" readme = "README.md" keywords = [ "transmission", "torrent", "bittorrent", "rpc", "download", ] categories = ["api-bindings"] license = "MIT" repository = "https://gitlab.gnome.org/haecker-felix/transmission-client" [dependencies.base64] version = "0.22" [dependencies.isahc] version = "1.7" [dependencies.log] version = "0.4" [dependencies.serde] version = "1.0" [dependencies.serde_derive] version = "1.0" [dependencies.serde_json] version = "1.0" [dependencies.serde_path_to_error] version = "0.1" [dependencies.serde_with] version = "3.7" [dependencies.thiserror] version = "1.0" [dependencies.url] version = "2.5" features = ["serde"] transmission-client-0.1.5/Cargo.toml.orig000064400000000000000000000013251046102023000165240ustar 00000000000000[package] name = "transmission-client" version = "0.1.5" authors = ["Felix Häcker "] edition = "2021" description = "Rust wrapper for Transmission rpc specs" keywords = ["transmission", "torrent", "bittorrent", "rpc", "download"] categories = ["api-bindings"] license = "MIT" documentation = "https://docs.rs/transmission-client" homepage = "https://gitlab.gnome.org/haecker-felix/transmission-client" repository = "https://gitlab.gnome.org/haecker-felix/transmission-client" [dependencies] serde = "1.0" serde_json = "1.0" serde_derive = "1.0" serde_with = "3.7" serde_path_to_error = "0.1" url = { version = "2.5", features = ["serde"] } log = "0.4" isahc = "1.7" thiserror = "1.0" base64 = "0.22"transmission-client-0.1.5/README.md000064400000000000000000000016201046102023000151120ustar 00000000000000# transmission-client Rust wrapper for [Transmission rpc specs](https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md). This crate is primarily used by [transmission-gobject](https://crates.io/crates/transmission-gobject) and the GNOME app [Fragments](https://apps.gnome.org/Fragments/). ### Implemented method names #### Torrent - [x] torrent-start - [x] torrent-start-now - [x] torrent-stop - [x] torrent-verify - [x] torrent-reannounce - [x] torrent-set - [x] torrent-get - [x] torrent-add - [x] torrent-remove - [x] torrent-set-location - [ ] torrent-rename-path #### Queue - [x] queue-move-top - [x] queue-move-up - [x] queue-move-down - [x] queue-move-bottom #### Session - [x] session-get - [x] session-set - [x] session-stats - [ ] session-close #### Miscellaneous stuff - [ ] blocklist-update - [x] port-test - [x] session-close - [ ] free-space - [ ] group-set - [ ] group-get transmission-client-0.1.5/src/authentication.rs000064400000000000000000000005601046102023000200110ustar 00000000000000use base64::{engine::general_purpose::STANDARD_NO_PAD, Engine}; #[derive(Debug)] pub struct Authentication { pub username: String, pub password: String, } impl Authentication { pub fn base64_encoded(&self) -> String { let auth = format!("{}:{}", &self.username, &self.password); format!("Basic {}=", STANDARD_NO_PAD.encode(auth)) } } transmission-client-0.1.5/src/client.rs000064400000000000000000000277251046102023000162640ustar 00000000000000use std::cell::RefCell; use std::rc::Rc; use isahc::http::StatusCode; use isahc::prelude::*; use isahc::{HttpClient, Request}; use serde::de::DeserializeOwned; use url::Url; use crate::error::ClientError; use crate::rpc::{ RequestArgs, RpcRequest, RpcResponse, RpcResponseArguments, SessionSetArgs, TorrentActionArgs, TorrentAddArgs, TorrentGetArgs, TorrentRemoveArgs, TorrentSetArgs, TorrentSetLocationArgs, }; use crate::{ utils, Authentication, PortTest, Session, SessionMutator, SessionStats, Torrent, TorrentAdded, TorrentFiles, TorrentFilesList, TorrentList, TorrentMutator, TorrentPeers, TorrentPeersList, TorrentTrackers, TorrentTrackersList, }; #[derive(Debug, Clone)] pub struct Client { address: Url, authentication: Rc>>, http_client: HttpClient, session_id: Rc>, } impl Client { pub fn new(address: Url) -> Self { Client { address, ..Default::default() } } pub fn set_authentication(&self, auth: Option) { *self.authentication.borrow_mut() = auth; } pub async fn torrents(&self, ids: Option>) -> Result, ClientError> { let args = TorrentGetArgs { fields: utils::torrent_fields(), ids, }; let request_args = Some(RequestArgs::TorrentGet(args)); let response: RpcResponse = self.send_request("torrent-get", request_args).await?; Ok(response.arguments.unwrap().torrents) } pub async fn torrents_files( &self, ids: Option>, ) -> Result, ClientError> { let args = TorrentGetArgs { fields: utils::torrent_files_fields(), ids, }; let request_args = Some(RequestArgs::TorrentGet(args)); let response: RpcResponse = self.send_request("torrent-get", request_args).await?; Ok(response.arguments.unwrap().torrents) } pub async fn torrents_peers( &self, ids: Option>, ) -> Result, ClientError> { let args = TorrentGetArgs { fields: utils::torrent_peers_fields(), ids, }; let request_args = Some(RequestArgs::TorrentGet(args)); let response: RpcResponse = self.send_request("torrent-get", request_args).await?; Ok(response.arguments.unwrap().torrents) } pub async fn torrents_trackers( &self, ids: Option>, ) -> Result, ClientError> { let args = TorrentGetArgs { fields: utils::torrent_trackers_fields(), ids, }; let request_args = Some(RequestArgs::TorrentGet(args)); let response: RpcResponse = self.send_request("torrent-get", request_args).await?; Ok(response.arguments.unwrap().torrents) } pub async fn torrent_set( &self, ids: Option>, mutator: TorrentMutator, ) -> Result<(), ClientError> { let args = TorrentSetArgs { ids, mutator }; let request_args = Some(RequestArgs::TorrentSet(args)); let _: RpcResponse = self.send_request("torrent-set", request_args).await?; Ok(()) } pub async fn torrent_add_filename( &self, filename: &str, ) -> Result, ClientError> { let args = TorrentAddArgs { filename: Some(filename.into()), ..Default::default() }; let request_args = Some(RequestArgs::TorrentAdd(args)); self.torrent_add(request_args).await } pub async fn torrent_add_metainfo( &self, metainfo: &str, ) -> Result, ClientError> { let args = TorrentAddArgs { metainfo: Some(metainfo.into()), ..Default::default() }; let request_args = Some(RequestArgs::TorrentAdd(args)); self.torrent_add(request_args).await } async fn torrent_add( &self, request_args: Option, ) -> Result, ClientError> { let response: RpcResponse = self.send_request("torrent-add", request_args).await?; let result_args = response.arguments.unwrap(); if result_args.torrent_added.is_some() { Ok(result_args.torrent_added) } else { Ok(result_args.torrent_duplicate) } } pub async fn torrent_remove( &self, ids: Option>, delete_local_data: bool, ) -> Result<(), ClientError> { let args = TorrentRemoveArgs { ids, delete_local_data, }; let request_args = Some(RequestArgs::TorrentRemove(args)); let _: RpcResponse = self.send_request("torrent-remove", request_args).await?; Ok(()) } pub async fn torrent_start( &self, ids: Option>, bypass_queue: bool, ) -> Result<(), ClientError> { let args = TorrentActionArgs { ids }; let request_args = Some(RequestArgs::TorrentAction(args)); let method_name = if bypass_queue { "torrent-start-now" } else { "torrent-start" }; let _: RpcResponse = self.send_request(method_name, request_args).await?; Ok(()) } pub async fn torrent_stop(&self, ids: Option>) -> Result<(), ClientError> { self.send_torrent_action("torrent-stop", ids).await?; Ok(()) } pub async fn torrent_verify(&self, ids: Option>) -> Result<(), ClientError> { self.send_torrent_action("torrent-verify", ids).await?; Ok(()) } pub async fn torrent_reannounce(&self, ids: Option>) -> Result<(), ClientError> { self.send_torrent_action("torrent-reannounce", ids).await?; Ok(()) } pub async fn torrent_set_location( &self, ids: Option>, location: String, move_data: bool, ) -> Result<(), ClientError> { let args = TorrentSetLocationArgs { ids, location, move_data, }; let request_args = Some(RequestArgs::TorrentSetLocation(args)); let _: RpcResponse = self .send_request("torrent-set-location", request_args) .await?; Ok(()) } pub async fn queue_move_top(&self, ids: Option>) -> Result<(), ClientError> { self.send_torrent_action("queue-move-top", ids).await?; Ok(()) } pub async fn queue_move_up(&self, ids: Option>) -> Result<(), ClientError> { self.send_torrent_action("queue-move-up", ids).await?; Ok(()) } pub async fn queue_move_down(&self, ids: Option>) -> Result<(), ClientError> { self.send_torrent_action("queue-move-down", ids).await?; Ok(()) } pub async fn queue_move_bottom(&self, ids: Option>) -> Result<(), ClientError> { self.send_torrent_action("queue-move-bottom", ids).await?; Ok(()) } pub async fn session(&self) -> Result { let response: RpcResponse = self.send_request("session-get", None).await?; Ok(response.arguments.unwrap()) } pub async fn session_set(&self, mutator: SessionMutator) -> Result<(), ClientError> { let args = SessionSetArgs { mutator }; let request_args = Some(RequestArgs::SessionSet(args)); let _: RpcResponse = self.send_request("session-set", request_args).await?; Ok(()) } pub async fn session_stats(&self) -> Result { let response: RpcResponse = self.send_request("session-stats", None).await?; Ok(response.arguments.unwrap()) } pub async fn session_close(&self) -> Result<(), ClientError> { let _: RpcResponse = self.send_request("session-close", None).await?; Ok(()) } pub async fn port_test(&self) -> Result { let response: RpcResponse = self.send_request("port-test", None).await?; Ok(response.arguments.unwrap().port_is_open) } async fn send_torrent_action( &self, action: &str, ids: Option>, ) -> Result<(), ClientError> { let args = TorrentActionArgs { ids }; let request_args = Some(RequestArgs::TorrentAction(args)); let _: RpcResponse = self.send_request(action, request_args).await?; Ok(()) } async fn send_request( &self, method: &str, arguments: Option, ) -> Result, ClientError> { let request = RpcRequest { method: method.into(), arguments, }; let body = serde_json::to_string(&request)?; let post_result = self.send_post(body).await?; let de = &mut serde_json::Deserializer::from_str(&post_result); let serde_result: Result, _> = serde_path_to_error::deserialize(de); match serde_result { Ok(response) => { if response.result != "success" { return Err(ClientError::TransmissionError(response.result)); } Ok(response) } Err(err) => { let path = err.path().to_string(); error!("Unable to parse json: {} ({})", path, err.to_string()); warn!("Path: {path}"); warn!("JSON: {post_result}"); Err(err.into_inner().into()) } } } async fn send_post(&self, body: String) -> Result { let request = self.http_request(body.clone())?; let mut response = self.http_client.send_async(request).await?; // Update session id let headers = response.headers(); if let Some(session_id) = headers.get("X-Transmission-Session-Id") { let session_id = session_id.to_str().unwrap().to_string(); *self.session_id.borrow_mut() = session_id; } // Check html status code match response.status() { // Invalid session id header, resend the request StatusCode::CONFLICT => { debug!("Received status code 409, resend request."); let request = self.http_request(body.clone())?; response = self.http_client.send_async(request).await?; } // Authentication needed StatusCode::UNAUTHORIZED => { return Err(ClientError::TransmissionUnauthorized); } _ => (), } Ok(response.text().await.unwrap()) } fn http_request(&self, body: String) -> Result, ClientError> { let session_id = self.session_id.borrow().clone(); let request = if let Some(auth) = &*self.authentication.borrow() { Request::post(self.address.to_string()) .header("X-Transmission-Session-Id", session_id) .header("Authorization", auth.base64_encoded()) .body(body)? } else { Request::post(self.address.to_string()) .header("X-Transmission-Session-Id", session_id) .body(body)? }; Ok(request) } } impl Default for Client { fn default() -> Self { let address = Url::parse("http://127.0.0.1:9091/transmission/rpc/").unwrap(); let http_client = HttpClient::builder() .authentication(isahc::auth::Authentication::all()) .build() .unwrap(); let session_id = Rc::new(RefCell::new("0".into())); Self { address, authentication: Rc::default(), http_client, session_id, } } } transmission-client-0.1.5/src/error.rs000064400000000000000000000006771046102023000161340ustar 00000000000000use thiserror::Error; #[derive(Error, Debug)] pub enum ClientError { #[error("transmission authentication needed")] TransmissionUnauthorized, #[error("transmission error")] TransmissionError(String), #[error("isahc network error")] NetworkError(#[from] isahc::Error), #[error("isahc http error")] HttpError(#[from] isahc::http::Error), #[error("serde_json error")] SerdeError(#[from] serde_json::Error), } transmission-client-0.1.5/src/lib.rs000064400000000000000000000011711046102023000155370ustar 00000000000000#[macro_use] extern crate log; #[macro_use] extern crate serde_derive; mod authentication; mod client; mod error; mod port_test; mod rpc; mod session; mod session_stats; mod torrent; mod utils; pub use authentication::Authentication; pub use client::Client; pub use error::ClientError; use port_test::PortTest; pub use session::{Encryption, Session, SessionMutator}; pub use session_stats::{SessionStats, StatsDetails}; pub use torrent::{ File, FileStat, Torrent, TorrentFiles, TorrentMutator, TorrentPeers, TorrentTrackers, }; use torrent::{TorrentAdded, TorrentFilesList, TorrentList, TorrentPeersList, TorrentTrackersList}; transmission-client-0.1.5/src/port_test.rs000064400000000000000000000003231046102023000170120ustar 00000000000000use crate::rpc::RpcResponseArguments; #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct PortTest { pub port_is_open: bool, } impl RpcResponseArguments for PortTest {} transmission-client-0.1.5/src/rpc/mod.rs000064400000000000000000000004101046102023000163270ustar 00000000000000mod request; mod response; pub use request::{ RequestArgs, RpcRequest, SessionSetArgs, TorrentActionArgs, TorrentAddArgs, TorrentGetArgs, TorrentRemoveArgs, TorrentSetArgs, TorrentSetLocationArgs, }; pub use response::{RpcResponse, RpcResponseArguments}; transmission-client-0.1.5/src/rpc/request.rs000064400000000000000000000057731046102023000172610ustar 00000000000000use crate::{SessionMutator, TorrentMutator}; #[derive(Debug, Serialize, Default)] pub struct RpcRequest { pub method: String, pub arguments: Option, } #[derive(Serialize, Debug, Clone)] #[serde(untagged)] pub enum RequestArgs { TorrentGet(TorrentGetArgs), TorrentSet(TorrentSetArgs), TorrentAdd(TorrentAddArgs), TorrentRemove(TorrentRemoveArgs), TorrentAction(TorrentActionArgs), TorrentSetLocation(TorrentSetLocationArgs), SessionSet(SessionSetArgs), } #[derive(Serialize, Debug, Clone, Default)] pub struct TorrentGetArgs { pub fields: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub ids: Option>, } #[derive(Serialize, Debug, Clone, Default)] #[serde(rename_all = "kebab-case")] pub struct TorrentAddArgs { #[serde(skip_serializing_if = "Option::is_none")] pub cookies: Option, #[serde(skip_serializing_if = "Option::is_none")] pub download_dir: Option, #[serde(skip_serializing_if = "Option::is_none")] pub filename: Option, #[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")] pub peer_limit: Option, #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "bandwidthPriority")] pub bandwith_priority: Option, #[serde(skip_serializing_if = "Option::is_none")] pub files_wanted: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub files_unwanted: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub priority_high: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub priority_low: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub priority_normal: Option>, } #[derive(Serialize, Debug, Clone, Default)] #[serde(rename_all = "camelCase")] pub struct TorrentSetArgs { #[serde(skip_serializing_if = "Option::is_none")] pub ids: Option>, #[serde(flatten)] pub mutator: TorrentMutator, } #[derive(Serialize, Debug, Clone, Default)] #[serde(rename_all = "kebab-case")] pub struct TorrentRemoveArgs { pub delete_local_data: bool, #[serde(skip_serializing_if = "Option::is_none")] pub ids: Option>, } #[derive(Serialize, Debug, Clone, Default)] pub struct TorrentActionArgs { #[serde(skip_serializing_if = "Option::is_none")] pub ids: Option>, } #[derive(Serialize, Debug, Clone, Default)] #[serde(rename_all = "kebab-case")] pub struct TorrentSetLocationArgs { #[serde(rename = "move")] pub move_data: bool, pub location: String, #[serde(skip_serializing_if = "Option::is_none")] pub ids: Option>, } #[derive(Serialize, Debug, Clone, Default)] #[serde(rename_all = "kebab-case")] pub struct SessionSetArgs { #[serde(flatten)] pub mutator: SessionMutator, } transmission-client-0.1.5/src/rpc/response.rs000064400000000000000000000017161046102023000174200ustar 00000000000000use serde::de::{DeserializeOwned, Error}; use serde::{Deserialize, Deserializer}; use serde_json::Value; #[derive(Debug, Deserialize, Default)] pub struct RpcResponse { pub result: String, #[serde(deserialize_with = "ok_or_none")] pub arguments: Option, } pub trait RpcResponseArguments {} impl RpcResponseArguments for String {} /// When the rpc response `arguments` field is empty, replace it with `None` /// instead of returning an error fn ok_or_none<'de, D, T>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, T: Deserialize<'de>, { let v = Value::deserialize(deserializer)?; match v { Value::Object(ref obj) => { if obj.is_empty() { Ok(None) } else { T::deserialize(v).map(Some).map_err(Error::custom) } } _ => T::deserialize(v).map(Some).map_err(Error::custom), } } transmission-client-0.1.5/src/session.rs000064400000000000000000000154431046102023000164630ustar 00000000000000use std::path::PathBuf; use serde_with::{serde_as, DefaultOnError}; use url::Url; use crate::rpc::RpcResponseArguments; #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] pub enum Encryption { Required, Preferred, Tolerated, } impl Default for Encryption { fn default() -> Self { Self::Preferred } } #[serde_as] #[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "kebab-case")] pub struct Session { pub alt_speed_down: i32, pub alt_speed_enabled: bool, pub alt_speed_time_begin: i32, pub alt_speed_time_day: i32, pub alt_speed_time_enabled: bool, pub alt_speed_time_end: i32, pub alt_speed_up: i32, pub blocklist_enabled: bool, pub blocklist_size: i32, #[serde_as(deserialize_as = "DefaultOnError")] #[serde(default)] pub blocklist_url: Option, pub cache_size_mb: i32, pub config_dir: String, pub dht_enabled: bool, pub download_dir: PathBuf, pub download_queue_enabled: bool, pub download_queue_size: i32, pub encryption: Encryption, pub idle_seeding_limit: i32, pub idle_seeding_limit_enabled: bool, pub incomplete_dir: PathBuf, pub incomplete_dir_enabled: bool, pub lpd_enabled: bool, pub peer_limit_global: i32, pub peer_limit_per_torrent: i32, pub peer_port: i32, pub peer_port_random_on_start: bool, pub pex_enabled: bool, pub port_forwarding_enabled: bool, pub queue_stalled_enabled: bool, pub queue_stalled_minutes: i32, pub rename_partial_files: bool, pub rpc_version: i32, pub rpc_version_minimum: i32, #[serde(default)] pub rpc_version_semver: String, #[serde(default)] pub script_torrent_added_enabled: bool, #[serde(default)] pub script_torrent_added_filename: String, pub script_torrent_done_enabled: bool, pub script_torrent_done_filename: String, pub seed_queue_enabled: bool, pub seed_queue_size: i32, #[serde(rename = "seedRatioLimit")] pub seed_ratio_limit: f32, #[serde(rename = "seedRatioLimited")] pub seed_ratio_limited: bool, #[serde(default)] pub session_id: String, pub speed_limit_down: i32, pub speed_limit_down_enabled: bool, pub speed_limit_up: i32, pub speed_limit_up_enabled: bool, pub start_added_torrents: bool, pub trash_original_torrent_files: bool, // TODO: units pub utp_enabled: bool, pub version: String, } #[derive(Serialize, Debug, Clone, Default)] #[serde(rename_all = "kebab-case")] pub struct SessionMutator { #[serde(skip_serializing_if = "Option::is_none")] pub alt_speed_down: Option, #[serde(skip_serializing_if = "Option::is_none")] pub alt_speed_enabled: Option, #[serde(skip_serializing_if = "Option::is_none")] pub alt_speed_time_begin: Option, #[serde(skip_serializing_if = "Option::is_none")] pub alt_speed_time_day: Option, #[serde(skip_serializing_if = "Option::is_none")] pub alt_speed_time_enabled: Option, #[serde(skip_serializing_if = "Option::is_none")] pub alt_speed_time_end: Option, #[serde(skip_serializing_if = "Option::is_none")] pub alt_speed_up: Option, #[serde(skip_serializing_if = "Option::is_none")] pub blocklist_enabled: Option, #[serde(skip_serializing_if = "Option::is_none")] pub blocklist_url: Option, #[serde(skip_serializing_if = "Option::is_none")] pub cache_size_mb: Option, #[serde(skip_serializing_if = "Option::is_none")] pub dht_enabled: Option, #[serde(skip_serializing_if = "Option::is_none")] pub download_dir: Option, #[serde(skip_serializing_if = "Option::is_none")] pub download_queue_enabled: Option, #[serde(skip_serializing_if = "Option::is_none")] pub download_queue_size: Option, #[serde(skip_serializing_if = "Option::is_none")] pub encryption: Option, #[serde(skip_serializing_if = "Option::is_none")] pub idle_seeding_limit: Option, #[serde(skip_serializing_if = "Option::is_none")] pub idle_seeding_limit_enabled: Option, #[serde(skip_serializing_if = "Option::is_none")] pub incomplete_dir: Option, #[serde(skip_serializing_if = "Option::is_none")] pub incomplete_dir_enabled: Option, #[serde(skip_serializing_if = "Option::is_none")] pub lpd_enabled: Option, #[serde(skip_serializing_if = "Option::is_none")] pub peer_limit_global: Option, #[serde(skip_serializing_if = "Option::is_none")] pub peer_limit_per_torrent: Option, #[serde(skip_serializing_if = "Option::is_none")] pub peer_port: Option, #[serde(skip_serializing_if = "Option::is_none")] pub peer_port_random_on_start: Option, #[serde(skip_serializing_if = "Option::is_none")] pub pex_enabled: Option, #[serde(skip_serializing_if = "Option::is_none")] pub port_forwarding_enabled: Option, #[serde(skip_serializing_if = "Option::is_none")] pub queue_stalled_enabled: Option, #[serde(skip_serializing_if = "Option::is_none")] pub queue_stalled_minutes: Option, #[serde(skip_serializing_if = "Option::is_none")] pub rename_partial_files: Option, #[serde(skip_serializing_if = "Option::is_none")] pub script_torrent_added_enabled: Option, #[serde(skip_serializing_if = "Option::is_none")] pub script_torrent_added_filename: Option, #[serde(skip_serializing_if = "Option::is_none")] pub script_torrent_done_enabled: Option, #[serde(skip_serializing_if = "Option::is_none")] pub script_torrent_done_filename: Option, #[serde(skip_serializing_if = "Option::is_none")] pub seed_queue_enabled: Option, #[serde(skip_serializing_if = "Option::is_none")] pub seed_queue_size: Option, #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "seedRatioLimit")] pub seed_ratio_limit: Option, #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "seedRatioLimited")] pub seed_ratio_limited: Option, #[serde(skip_serializing_if = "Option::is_none")] pub speed_limit_down: Option, #[serde(skip_serializing_if = "Option::is_none")] pub speed_limit_down_enabled: Option, #[serde(skip_serializing_if = "Option::is_none")] pub speed_limit_up: Option, #[serde(skip_serializing_if = "Option::is_none")] pub speed_limit_up_enabled: Option, #[serde(skip_serializing_if = "Option::is_none")] pub start_added_torrents: Option, #[serde(skip_serializing_if = "Option::is_none")] pub trash_original_torrent_files: Option, #[serde(skip_serializing_if = "Option::is_none")] pub utp_enabled: Option, } impl RpcResponseArguments for Session {} transmission-client-0.1.5/src/session_stats.rs000064400000000000000000000013561046102023000176770ustar 00000000000000use crate::rpc::RpcResponseArguments; #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionStats { pub active_torrent_count: i32, #[serde(rename = "cumulative-stats")] pub cumulative_stats: StatsDetails, #[serde(rename = "current-stats")] pub current_stats: StatsDetails, pub download_speed: i32, pub paused_torrent_count: i32, pub torrent_count: i32, pub upload_speed: i32, } #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct StatsDetails { pub downloaded_bytes: i64, pub files_added: i64, pub seconds_active: i64, pub session_count: i64, pub uploaded_bytes: i64, } impl RpcResponseArguments for SessionStats {} transmission-client-0.1.5/src/torrent.rs000064400000000000000000000153531046102023000164750ustar 00000000000000use crate::rpc::RpcResponseArguments; use crate::utils::string_fallback; // Default torrent struct // #[derive(Deserialize, Debug, Default, PartialEq, Clone)] #[serde(rename_all = "camelCase")] #[serde(default)] pub struct Torrent { pub id: i32, pub activity_date: i32, pub added_date: i32, pub bandwidth_priority: i32, pub comment: String, pub corrupt_ever: i64, pub creator: String, pub date_created: i32, pub desired_available: i64, pub done_date: i32, pub download_dir: String, pub download_limit: i32, pub download_limited: bool, pub downloaded_ever: i64, pub edit_date: i32, pub error: i32, pub error_string: String, pub eta: i64, pub eta_idle: i64, pub hash_string: String, pub have_unchecked: i64, pub have_valid: i64, pub honors_session_limits: bool, pub is_finished: bool, pub is_private: bool, pub is_stalled: bool, // TODO: pub labels: Vec>, pub left_until_done: i64, pub magnet_link: String, pub manual_announce_time: i32, pub metadata_percent_complete: f32, pub name: String, pub percent_done: f32, pub piece_count: i64, pub piece_size: i64, pub pieces: String, #[serde(rename = "primary-mime-type")] #[serde(deserialize_with = "string_fallback")] pub primary_mime_type: String, pub queue_position: i32, pub rate_download: i32, pub rate_upload: i32, pub recheck_progress: f32, pub seconds_downloading: i32, pub seconds_seeding: i32, pub seed_idle_limit: i32, pub seed_idle_mode: i32, pub seed_ratio_limit: f32, pub seed_ratio_mode: i32, pub size_when_done: i64, pub start_date: i32, pub status: i32, pub torrent_file: String, pub total_size: i64, pub upload_limit: i32, pub upload_limited: bool, pub upload_ratio: f32, pub uploaded_ever: i64, } #[derive(Deserialize, Debug)] pub struct TorrentList { pub torrents: Vec, } #[derive(Serialize, Debug, Clone, Default)] #[serde(rename_all = "camelCase")] pub struct TorrentMutator { #[serde(skip_serializing_if = "Option::is_none")] pub bandwidth_priority: Option, #[serde(skip_serializing_if = "Option::is_none")] pub download_limit: Option, #[serde(skip_serializing_if = "Option::is_none")] pub download_limited: Option, #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "files-wanted")] pub files_wanted: Option>, #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "files-unwanted")] pub files_unwanted: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub honors_session_limits: Option, //#[serde(skip_serializing_if = "Option::is_none")] // TODO: pub labels: [array] #[serde(skip_serializing_if = "Option::is_none")] pub location: Option, #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "peer-limit")] pub peer_limit: Option, //#[serde(skip_serializing_if = "Option::is_none")] //#[serde(rename = "priority-high")] // TODO: pub priority_high: [array] //#[serde(skip_serializing_if = "Option::is_none")] //#[serde(rename = "priority-low")] // TODO: pub priority_low: [array] //#[serde(skip_serializing_if = "Option::is_none")] //#[serde(rename = "priority-normal")] // TODO: pub priority_normal: [array] #[serde(skip_serializing_if = "Option::is_none")] pub queue_position: Option, #[serde(skip_serializing_if = "Option::is_none")] pub seed_idle_limit: Option, #[serde(skip_serializing_if = "Option::is_none")] pub seed_idle_mode: Option, #[serde(skip_serializing_if = "Option::is_none")] pub seed_ratio_limit: Option, #[serde(skip_serializing_if = "Option::is_none")] pub seed_ratio_mode: Option, //#[serde(skip_serializing_if = "Option::is_none")] // TODO: pub tracker_add: [array] //#[serde(skip_serializing_if = "Option::is_none")] // TODO: pub tracker_remove: [array] //#[serde(skip_serializing_if = "Option::is_none")] // TODO: pub tracker_replace: [array] #[serde(skip_serializing_if = "Option::is_none")] pub upload_limit: Option, #[serde(skip_serializing_if = "Option::is_none")] pub upload_limited: Option, } #[derive(Deserialize, Debug)] #[serde(rename_all = "kebab-case")] pub struct TorrentAdded { pub torrent_added: Option, pub torrent_duplicate: Option, } // Torrent files // #[derive(Deserialize, Debug, Default, PartialEq, Clone)] #[serde(rename_all = "camelCase")] #[serde(default)] pub struct TorrentFiles { pub id: i32, #[serde(rename = "file-count")] pub file_count: i32, pub files: Vec, pub file_stats: Vec, pub wanted: Vec, pub priorities: Vec, } #[derive(Deserialize, Debug)] pub struct TorrentFilesList { pub torrents: Vec, } #[derive(Deserialize, Debug, Default, PartialEq, Clone)] #[serde(rename_all = "camelCase")] pub struct File { pub bytes_completed: i64, pub length: i64, pub name: String, } #[derive(Deserialize, Debug, Default, PartialEq, Clone)] #[serde(rename_all = "camelCase")] pub struct FileStat { pub bytes_completed: i64, pub wanted: bool, pub priority: i32, } // Torrent peers // #[derive(Deserialize, Debug, Default, PartialEq, Clone)] #[serde(rename_all = "camelCase")] #[serde(default)] pub struct TorrentPeers { pub id: i32, #[serde(rename = "peer-limit")] pub peer_limit: i32, // TODO: pub peers: Vec>, pub peers_connected: i32, // TODO: pub peers_from: PeersFrom, pub peers_getting_from_us: i32, pub peers_sending_to_us: i32, pub max_connected_peers: i32, pub webseeds_sending_to_us: i32, // TODO: pub webseeds: Vec>, } #[derive(Deserialize, Debug)] pub struct TorrentPeersList { pub torrents: Vec, } // Torrent trackers // #[derive(Deserialize, Debug, Default, PartialEq, Clone)] #[serde(rename_all = "camelCase")] #[serde(default)] pub struct TorrentTrackers { pub id: i32, // TODO: pub tracker_stats: Vec, // TODO: pub trackers: Vec, } #[derive(Deserialize, Debug)] pub struct TorrentTrackersList { pub torrents: Vec, } impl RpcResponseArguments for Torrent {} impl RpcResponseArguments for TorrentList {} impl RpcResponseArguments for TorrentAdded {} impl RpcResponseArguments for TorrentFiles {} impl RpcResponseArguments for TorrentFilesList {} impl RpcResponseArguments for TorrentPeers {} impl RpcResponseArguments for TorrentPeersList {} impl RpcResponseArguments for TorrentTrackers {} impl RpcResponseArguments for TorrentTrackersList {} transmission-client-0.1.5/src/utils.rs000064400000000000000000000055241046102023000161370ustar 00000000000000use serde::{Deserialize, Deserializer}; pub fn torrent_fields() -> Vec { vec![ "id".into(), "activityDate".into(), "addedDate".into(), "bandwidthPriority".into(), "comment".into(), "corruptEver".into(), "creator".into(), "dateCreated".into(), "desiredAvailable".into(), "doneDate".into(), "downloadDir".into(), "downloadedEver".into(), "downloadLimit".into(), "downloadLimited".into(), "editDate".into(), "error".into(), "errorString".into(), "eta".into(), "etaIdle".into(), "hashString".into(), "haveUnchecked".into(), "haveValid".into(), "honorsSessionLimits".into(), "isFinished".into(), "isPrivate".into(), "isStalled".into(), "labels".into(), "leftUntilDone".into(), "magnetLink".into(), "manualAnnounceTime".into(), "metadataPercentComplete".into(), "name".into(), "percentDone".into(), "pieces".into(), "pieceCount".into(), "pieceSize".into(), "primary-mime-type".into(), "queuePosition".into(), "rateDownload".into(), "rateUpload".into(), "recheckProgress".into(), "secondsDownloading".into(), "secondsSeeding".into(), "seedIdleLimit".into(), "seedIdleMode".into(), "seedRatioLimit".into(), "seedRatioMode".into(), "sizeWhenDone".into(), "startDate".into(), "status".into(), "totalSize".into(), "torrentFile".into(), "uploadedEver".into(), "uploadLimit".into(), "uploadLimited".into(), "uploadRatio".into(), ] } pub fn torrent_files_fields() -> Vec { vec![ "id".into(), "file-count".into(), "files".into(), "fileStats".into(), "wanted".into(), "priorities".into(), ] } pub fn torrent_peers_fields() -> Vec { vec![ "id".into(), "peer-limit".into(), "peers".into(), "peersConnected".into(), "peersFrom".into(), "peersGettingFromUs".into(), "peersSendingToUs".into(), "maxConnectedPeers".into(), "webseeds".into(), "webseedsSendingToUs".into(), ] } pub fn torrent_trackers_fields() -> Vec { vec!["id".into(), "trackers".into(), "trackerStats".into()] } pub fn string_fallback<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { #[derive(Deserialize)] #[serde(untagged)] enum StringOrNumber { String(String), Number(i64), } match StringOrNumber::deserialize(deserializer)? { StringOrNumber::String(s) => Ok(s), StringOrNumber::Number(i) => Ok(i.to_string()), } }