transmission_rs 0.1.1

A safe, ergonomic, async client for the Transmission BitTorrent client implemented in pure Rust
Documentation
//! Client for download management.

use std::convert::TryInto;
use std::error::Error;

use serde::Serialize;

mod payload;
use payload::Request;
use payload::Response;
mod list;
use list::FieldList;
use list::TorrentList;
mod torrent;
use torrent::Torrent;
use torrent::AddTorrent;
use torrent::AddTorrentResponse;
//use torrent::AddedTorrent;

/// Interface into the major functions of Transmission
/// including adding, and removing torrents.
///
/// The `Client` does not keep track of the created torrents itself.
///
/// Example of creating a session and adding a torrent and waiting for it to complete.
/// ```no_run
/// use transmission::{ ClientConfig, Client};
///
/// # let test_dir = "/tmp/tr-test-long";
/// # let config_dir = test_dir;
/// # let download_dir = test_dir;
/// let file_path = "./alpine.torrent";
///
/// # std::fs::create_dir(test_dir).unwrap();
///
/// let c = ClientConfig::new()
///    .app_name("testing")
///    .config_dir(config_dir)
///    .download_dir(download_dir);
/// let mut c = Client::new(c);
///
/// let t = c.add_torrent_file(file_path).unwrap();
/// t.start();
///
/// // Run until done
/// while t.stats().percent_complete < 1.0 {
///     print!("{:#?}\r", t.stats().percent_complete);
/// }
/// c.close();
///
/// # std::fs::remove_dir_all(test_dir).unwrap();
/// ```
#[derive(Clone)]
pub struct Client {
	host: String,
	port: u16,
	tls: bool,
	auth: Option<(String, String)>,

	base_url: String,
	session_id: Option<String>,

	http: reqwest::Client,
}

impl Client {
	// TODO:  Take Option<(&str, &str)> for auth
    pub fn new(host: impl ToString, port: u16, tls: bool, auth: Option<(String, String)>) -> Result<Self, Box<dyn Error>> {
		let mut headers = reqwest::header::HeaderMap::new();
		headers.insert(reqwest::header::USER_AGENT, "transmission-rs/0.1".try_into()?);
		let this = Self{
			host: host.to_string(),
			port: port,
			tls: tls,
			auth: auth,
			base_url: {
				let protocol = match tls {
					true => "https",
					false => "http"
				};
				format!("{}://{}:{}", protocol, host.to_string(), port)
			},
			session_id: None,
			http: reqwest::Client::builder()
				.gzip(true)
				.default_headers(headers)
				.build()?
		};
		Ok(this)
	}

	fn build_url(&self, path: impl AsRef<str>) -> String {
		format!("{}{}", self.base_url, path.as_ref()).to_string()
	}

	pub async fn authenticate(&mut self) -> Result<(), Box<dyn Error>> {
		let response = self.post("/transmission/rpc/")
			.header("Content-Type", "application/x-www-form-urlencoded")
			.send()
			.await?;
		self.session_id = match response.headers().get("X-Transmission-Session-Id") {
			Some(v) => Some(v.to_str()?.to_string()),
			None => None // TODO:  Return an error
		};
		Ok(())
	}

	pub fn post(&self, path: impl AsRef<str>) -> reqwest::RequestBuilder {
		let url = self.build_url(path);
		//println!("{}", url);
		let mut request = self.http.post(&url);
		request = match &self.auth {
			Some(auth) => request.basic_auth(&auth.0, Some(&auth.1)),
			None => request
		};
		request = match &self.session_id {
			Some(token) => request.header("X-Transmission-Session-Id", token),
			None => request
		};
		request
	}

	pub async fn rpc(&self, method: impl ToString, tag: u8, body: impl Serialize) -> Result<reqwest::Response, Box<dyn Error>> {
		let request = Request::new(method, tag, body);
		Ok(self.post("/transmission/rpc/")
			// Content-Type doesn't actually appear to be necessary, and is
			//    technically a lie, since there's no key/value being passed,
			//    just some JSON, but the official client sends this, so we
			//    will too.
			.header("Content-Type", "application/x-www-form-urlencoded")
			.body(serde_json::to_string(&request)?)
			.send().await?
			.error_for_status()?
		)
	}

	pub async fn list(&self) -> Result<Vec<Torrent>, Box<dyn Error>> {
		let response: Response<TorrentList> = self.rpc("torrent-get", 4, FieldList::new()).await?.error_for_status()?.json().await?;
		Ok(response.arguments.torrents)
	}

	pub async fn add_torrent_from_link(&self, url: impl ToString) -> Result<AddTorrentResponse, Box<dyn Error>> {
		let response: Response<AddTorrentResponse> = self.rpc("torrent-add", 8, AddTorrent{filename: url.to_string()}).await?.error_for_status()?.json().await?;
		Ok(response.arguments)
	}
}