//! Client for download management. use std::collections::BTreeMap; use std::convert::TryInto; use std::time::Duration; use serde::Serialize; use tokio::time::sleep; use crate::Error; mod payload; use payload::Request; use payload::Response; mod list; use list::FieldList; use list::TorrentList; pub mod torrent; use torrent::Torrent; use torrent::TorrentFile; use torrent::AddTorrent; use torrent::AddTorrentResponse; use torrent::GetTorrentFilesRequest; use torrent::GetTorrentFilesResponse; //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. /// ``` #[derive(Clone)] pub struct Client { host: String, port: u16, tls: bool, auth: Option<(String, String)>, base_url: String, session_id: Option, 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 { 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) -> String { format!("{}{}", self.base_url, path.as_ref()).to_string() } pub async fn authenticate(&mut self) -> Result<(), Error> { self.session_id = None; 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) -> 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(&mut self, method: impl ToString, tag: u8, body: impl Serialize) -> Result { let request = Request::new(method, tag, body); let mut error = None; for i in 0..5 { sleep(Duration::from_secs(i)).await; let result = 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(); match result { Ok(r) => return Ok(r), Err(e) => match e.status() { Some(reqwest::StatusCode::CONFLICT) => { self.authenticate().await?; error = Some(e); continue; }, _ => return Err(e.into()) } }; } match error { Some(e) => Err(e.into()), // Should be unreachable None => Err(Error::HTTPUnknown) } } pub async fn list(&mut self) -> Result, Error> { let field_list = FieldList::from_vec(vec![ "error", "errorString", "eta", "id", "isFinished", "leftUntilDone", "name", "peersGettingFromUs", "peersSendingToUs", "rateDownload", "rateUpload", "sizeWhenDone", "status", "uploadRatio" ]); let response: Response = self.rpc("torrent-get", 4, field_list).await?.error_for_status()?.json().await?; Ok(response.arguments.torrents) } // TODO: Borrow key from value pub async fn list_by_name(&mut self) -> Result, Error> { Ok(self.list().await?.into_iter().map(|v| (v.name.clone(), v)).collect::>()) } pub async fn add_torrent_from_link(&mut self, url: impl ToString) -> Result { let response: Response = self.rpc("torrent-add", 8, AddTorrent{filename: url.to_string()}).await?.error_for_status()?.json().await?; Ok(response.arguments) } pub async fn get_files_by_id(&mut self, id: u16) -> Result, Error> { let response: Response = self.rpc("torrent-get", 3, GetTorrentFilesRequest::new(id)).await?.error_for_status()?.json().await?; if(response.arguments.torrents.len() == 0) { // TODO: Maybe make the Ok result an Option? Or maybe return a 404 error? return Ok(vec![]); } else if(response.arguments.torrents.len() > 1) { return Err(Error::WeirdGetFilesResponse); } // TODO: Figure out how to move files out of this structure, since we're discarding the rest anyway, instead of cloning Ok(response.arguments.torrents[0].files.clone()) } }