diff --git a/src/action/get.rs b/src/action/get.rs index f8a5b50..b443b91 100644 --- a/src/action/get.rs +++ b/src/action/get.rs @@ -1,7 +1,7 @@ use argh::FromArgs; use crate::action::ActionExec; -use crate::api::qbittorrent::{ApiClient, RawApiClient}; +use crate::api::qbittorrent::ApiClient; use crate::config::Config; use crate::error::Error; @@ -12,9 +12,6 @@ pub struct GetAction { #[argh(switch)] /// the positional argument is a magnet link, not an infohash magnet: bool, - #[argh(switch, short = 'r')] - /// return raw JSON response from API - raw: bool, #[argh(switch, short = 'j')] /// return parsed JSON response from API json: bool, @@ -25,18 +22,12 @@ pub struct GetAction { impl ActionExec for GetAction { fn exec(&self, config: &Config) -> Result<(), Error> { - if self.raw { - let api = RawApiClient::from_config(&config)?; - let res = api.get(&self.torrent)?; - println!("{}", res); - } else { - let api = ApiClient::from_config(&config)?; - if let Some(t) = api.get(&self.torrent)? { - if self.json { - println!("{}", &serde_json::to_string(&t).unwrap()); - } else { - println!("{}", t.hash); - } + let api = ApiClient::from_config(&config)?; + if let Some(t) = api.get(&self.torrent)? { + if self.json { + println!("{}", &serde_json::to_string(&t).unwrap()); + } else { + println!("{}", t.hash); } } diff --git a/src/action/list.rs b/src/action/list.rs index 129bffb..a73f55a 100644 --- a/src/action/list.rs +++ b/src/action/list.rs @@ -1,6 +1,6 @@ use argh::FromArgs; -use crate::api::qbittorrent::{ApiClient, RawApiClient}; +use crate::api::qbittorrent::ApiClient; use crate::Error; use crate::action::ActionExec; use crate::config::Config; @@ -9,9 +9,6 @@ use crate::config::Config; #[argh(subcommand, name = "list")] /// list existing torrents on qBittorrent pub struct ListAction { - #[argh(switch, short = 'r')] - /// return raw JSON response from API - raw: bool, #[argh(switch, short = 'j')] /// return parsed JSON response from API json: bool, @@ -19,19 +16,15 @@ pub struct ListAction { impl ActionExec for ListAction { fn exec(&self, config: &Config) -> Result<(), Error> { - if self.raw { - let api = RawApiClient::from_config(&config)?; - println!("{}", api.list()?); - } else { - let api = ApiClient::from_config(&config)?; - for torrent in api.list()? { - if self.json { - println!("{}", &serde_json::to_string(&torrent).unwrap()); - } else { - println!("{}", torrent.hash); - } + let api = ApiClient::from_config(&config)?; + for torrent in api.list()? { + if self.json { + println!("{}", &serde_json::to_string(&torrent).unwrap()); + } else { + println!("{}", torrent.hash); } } + Ok(()) } } diff --git a/src/api/qbittorrent/asynchronous.rs b/src/api/qbittorrent/asynchronous.rs new file mode 100644 index 0000000..2bd9f33 --- /dev/null +++ b/src/api/qbittorrent/asynchronous.rs @@ -0,0 +1,94 @@ +use snafu::prelude::*; + +use crate::Config; +use crate::api::{Torrent, TorrentList, IntoTorrent, TorrentTracker}; +use crate::api::qbittorrent::{QBittorrentListTorrent, RawAsyncApiClient}; +use crate::error::{Error, FailedDeserializeSnafu as DeserializeError}; + +#[derive(Debug)] +pub struct AsyncApiClient { + pub raw_api: RawAsyncApiClient, +} + +impl AsyncApiClient { + #[allow(dead_code)] + pub async fn from_config(config: &Config) -> Result { + Self::new(&config.format_host(), &config.qbittorrent.login, &config.qbittorrent.password).await + } + + #[allow(dead_code)] + pub async fn new(host: &str, login: &str, password: &str) -> Result { + let api = RawAsyncApiClient::login(host, login, password).await?; + + Ok(AsyncApiClient { + raw_api: api, + }) + } + + #[allow(dead_code)] + pub async fn add(&self, magnet: &str, paused: bool) -> Result<(), Error> { + let res = self.raw_api.add(magnet, paused).await?; + if res == "Ok." { + Ok(()) + } else { + Err(Error::message(res.clone())) + } + } + + #[allow(dead_code)] + pub async fn get(&self, hash: &str) -> Result, Error> { + let res = self.raw_api.get(hash).await?; + if res == "" { + Ok(None) + } else { + // TODO: NOT OPTIMIZED AT ALL. API DOES NOT RETURN NAME/HASH OF TORRENT SO WE HAVE TO QUERY LIST... + let list = self.list().await?; + let filtered_list = list.into_iter().filter(|t| t.hash == hash).collect::>(); + let torrent = filtered_list.first() + .unwrap_or_else(|| panic!("Torrent was 'get' but could not be found in 'list': {}", &hash)); + Ok(Some(torrent.clone())) + } + } + + #[allow(dead_code)] + /// This method only exists because qBittorrent API 'properties' endpoint does not return torrent name/hash + /// so if you need to do a lot of 'get', please call 'list' once and use this method instead. + pub async fn get_with_cached_list(&self, hash: &str, list: &TorrentList) -> Result, Error> { + let res = self.raw_api.get(hash).await?; + if res == "" { + Ok(None) + } else { + let filtered_list = list.clone().into_iter().filter(|t| t.hash == hash).collect::>(); + let torrent = filtered_list.first() + .unwrap_or_else(|| panic!("Torrent was 'get' but could not be found in 'list': {}", &hash)); + Ok(Some(torrent.clone())) + } + } + + #[allow(dead_code)] + pub async fn list(&self) -> Result { + let res = self.raw_api.list().await?; + let concrete: Vec = serde_json::from_str(&res).context(DeserializeError)?; + Ok(concrete.iter().map(|t| t.into_torrent()).collect()) + } + + #[allow(dead_code)] + pub async fn add_tracker(&self, hash: &str, url: &str) -> Result<(), Error> { + let _ = self.raw_api.add_tracker(hash, url).await?; + Ok(()) + } + + #[allow(dead_code)] + pub async fn get_trackers(&self, hash: &str) -> Result, Error> { + let res = self.raw_api.get_trackers(hash).await?; + let concrete: Vec = serde_json::from_str(&res).context(DeserializeError)?; + let concrete_filter_dht = concrete.into_iter().filter(|t| t.is_tracker()).collect(); + Ok(concrete_filter_dht) + } + + #[allow(dead_code)] + pub async fn remove_tracker(&self, hash: &str, url: &str) -> Result<(), Error> { + let _ = self.raw_api.remove_tracker(hash, url).await?; + Ok(()) + } +} diff --git a/src/api/qbittorrent/mod.rs b/src/api/qbittorrent/mod.rs index 9e8c65c..f7999a2 100644 --- a/src/api/qbittorrent/mod.rs +++ b/src/api/qbittorrent/mod.rs @@ -1,270 +1,10 @@ -use snafu::prelude::*; -use qbittorrent_web_api::Api; -use tokio::runtime::{Builder, Runtime}; - -use crate::Config; -use crate::error::{Error, ApiSnafu as IOError, InternalApiSnafu as ApiError, FailedDeserializeSnafu as DeserializeError}; - mod types; pub use types::QBittorrentPropertiesTorrent; pub use types::QBittorrentListTorrent; -use crate::api::{Torrent, TorrentList, IntoTorrent, TorrentTracker}; -pub fn blocking_runtime() -> std::io::Result { - Builder::new_current_thread() - .enable_all() - .build() -} - -#[derive(Debug)] -pub struct UnauthenticatedRawAsyncApiClient; - -impl UnauthenticatedRawAsyncApiClient { - pub async fn login(host: &str, login: &str, password: &str) -> Result { - Ok(RawAsyncApiClient { - api: Api::login(host, login, password).await.context(ApiError)?, - }) - } -} - -#[derive(Debug)] -pub struct RawAsyncApiClient { - pub api: qbittorrent_web_api::api_impl::Authenticated, -} - -impl RawAsyncApiClient { - #[allow(dead_code)] - pub async fn add(&self, magnet: &str, paused: bool) -> Result { - if paused { - self.api.torrent_management().add(magnet).paused("true").send_raw().await.context(ApiError) - } else { - self.api.torrent_management().add(magnet).send_raw().await.context(ApiError) - } - } - - #[allow(dead_code)] - pub async fn get(&self, hash: &str) -> Result { - self.api.torrent_management().properties_raw(hash).await.context(ApiError) - } - - #[allow(dead_code)] - pub async fn list(&self) -> Result { - self.api.torrent_management().info().send_raw().await.context(ApiError) - } - - #[allow(dead_code)] - pub async fn add_tracker(&self, hash: &str, url: &str) -> Result { - self.api.torrent_management().add_trackers_raw(hash, url).await.context(ApiError) - } - - #[allow(dead_code)] - pub async fn get_trackers(&self, hash: &str) -> Result { - self.api.torrent_management().trackers_raw(hash).await.context(ApiError) - } - - #[allow(dead_code)] - pub async fn remove_tracker(&self, hash: &str, url: &str) -> Result { - self.api.torrent_management().remove_trackers_raw(hash, &vec!(url)).await.context(ApiError) - } -} - -#[derive(Debug)] -pub struct RawApiClient { - pub rt: Runtime, - pub api: RawAsyncApiClient, -} - -impl RawApiClient { - pub fn from_config(config: &Config) -> Result { - Self::new(&config.format_host(), &config.qbittorrent.login, &config.qbittorrent.password) - } - - /// Login into a qBittorrent backend and return a proper ApiClient instance - #[allow(dead_code)] - pub fn new(host: &str, login: &str, password: &str) -> Result { - let rt = blocking_runtime().context(IOError)?; - let api = rt.block_on(UnauthenticatedRawAsyncApiClient::login(host, login, password))?; - Ok(RawApiClient { - rt, - api, - }) - } - - #[allow(dead_code)] - pub fn add(&self, magnet: &str, paused: bool) -> Result { - self.rt.block_on(self.api.add(magnet, paused)) - } - - #[allow(dead_code)] - pub fn get(&self, hash: &str) -> Result { - self.rt.block_on(self.api.get(hash)) - } - - #[allow(dead_code)] - pub fn list(&self) -> Result { - self.rt.block_on(self.api.list()) - } - - #[allow(dead_code)] - pub fn add_tracker(&self, hash: &str, url: &str) -> Result { - self.rt.block_on(self.api.add_tracker(hash, url)) - } - - #[allow(dead_code)] - pub fn get_trackers(&self, hash: &str) -> Result { - self.rt.block_on(self.api.get_trackers(hash)) - } - - #[allow(dead_code)] - pub fn remove_tracker(&self, hash: &str, url: &str) -> Result { - self.rt.block_on(self.api.remove_tracker(hash, url)) - } -} - -#[derive(Debug)] -pub struct AsyncApiClient { - pub raw_api: RawAsyncApiClient, -} - -impl AsyncApiClient { - #[allow(dead_code)] - pub async fn from_config(config: &Config) -> Result { - Self::new(&config.format_host(), &config.qbittorrent.login, &config.qbittorrent.password).await - } - - #[allow(dead_code)] - pub async fn new(host: &str, login: &str, password: &str) -> Result { - let api = UnauthenticatedRawAsyncApiClient::login(host, login, password).await?; - - Ok(AsyncApiClient { - raw_api: api, - }) - } - - #[allow(dead_code)] - pub async fn add(&self, magnet: &str, paused: bool) -> Result<(), Error> { - let res = self.raw_api.add(magnet, paused).await?; - if res == "Ok." { - Ok(()) - } else { - Err(Error::message(res.clone())) - } - } - - #[allow(dead_code)] - pub async fn get(&self, hash: &str) -> Result, Error> { - let res = self.raw_api.get(hash).await?; - if res == "" { - Ok(None) - } else { - // TODO: NOT OPTIMIZED AT ALL. API DOES NOT RETURN NAME/HASH OF TORRENT SO WE HAVE TO QUERY LIST... - let list = self.list().await?; - let filtered_list = list.into_iter().filter(|t| t.hash == hash).collect::>(); - let torrent = filtered_list.first() - .unwrap_or_else(|| panic!("Torrent was 'get' but could not be found in 'list': {}", &hash)); - Ok(Some(torrent.clone())) - } - } - - #[allow(dead_code)] - /// This method only exists because qBittorrent API 'properties' endpoint does not return torrent name/hash - /// so if you need to do a lot of 'get', please call 'list' once and use this method instead. - pub async fn get_with_cached_list(&self, hash: &str, list: &TorrentList) -> Result, Error> { - let res = self.raw_api.get(hash).await?; - if res == "" { - Ok(None) - } else { - let filtered_list = list.clone().into_iter().filter(|t| t.hash == hash).collect::>(); - let torrent = filtered_list.first() - .unwrap_or_else(|| panic!("Torrent was 'get' but could not be found in 'list': {}", &hash)); - Ok(Some(torrent.clone())) - } - } - - #[allow(dead_code)] - pub async fn list(&self) -> Result { - let res = self.raw_api.list().await?; - let concrete: Vec = serde_json::from_str(&res).context(DeserializeError)?; - Ok(concrete.iter().map(|t| t.into_torrent()).collect()) - } - - #[allow(dead_code)] - pub async fn add_tracker(&self, hash: &str, url: &str) -> Result<(), Error> { - let _ = self.raw_api.add_tracker(hash, url).await?; - Ok(()) - } - - #[allow(dead_code)] - pub async fn get_trackers(&self, hash: &str) -> Result, Error> { - let res = self.raw_api.get_trackers(hash).await?; - let concrete: Vec = serde_json::from_str(&res).context(DeserializeError)?; - let concrete_filter_dht = concrete.into_iter().filter(|t| t.is_tracker()).collect(); - Ok(concrete_filter_dht) - } - - #[allow(dead_code)] - pub async fn remove_tracker(&self, hash: &str, url: &str) -> Result<(), Error> { - let _ = self.raw_api.remove_tracker(hash, url).await?; - Ok(()) - } -} - -#[derive(Debug)] -pub struct ApiClient { - pub rt: Runtime, - pub api: AsyncApiClient, -} - -impl ApiClient { - #[allow(dead_code)] - pub fn from_config(config: &Config) -> Result { - Self::new(&config.format_host(), &config.qbittorrent.login, &config.qbittorrent.password) - } - - /// Login into a qBittorrent backend and return a proper ApiClient instance - #[allow(dead_code)] - pub fn new(host: &str, login: &str, password: &str) -> Result { - let rt = blocking_runtime().context(IOError)?; - let api = rt.block_on(AsyncApiClient::new(host, login, password))?; - Ok(ApiClient { - rt, - api, - }) - } - - #[allow(dead_code)] - pub fn add(&self, magnet: &str, paused: bool) -> Result<(), Error> { - self.rt.block_on(self.api.add(magnet, paused)) - } - - #[allow(dead_code)] - pub fn get(&self, hash: &str) -> Result, Error> { - self.rt.block_on(self.api.get(hash)) - } - - #[allow(dead_code)] - // TODO: Workaround - pub fn get_with_cached_list(&self, hash: &str, list: &TorrentList) -> Result, Error> { - self.rt.block_on(self.api.get_with_cached_list(hash, list)) - } - - #[allow(dead_code)] - pub fn list(&self) -> Result { - self.rt.block_on(self.api.list()) - } - - #[allow(dead_code)] - pub fn add_tracker(&self, hash: &str, url: &str) -> Result<(), Error> { - self.rt.block_on(self.api.add_tracker(hash, url)) - } - - #[allow(dead_code)] - pub fn get_trackers(&self, hash: &str) -> Result, Error> { - self.rt.block_on(self.api.get_trackers(hash)) - } - - #[allow(dead_code)] - pub fn remove_tracker(&self, hash: &str, url: &str) -> Result<(), Error> { - self.rt.block_on(self.api.remove_tracker(hash, url)) - } -} +mod raw_asynchronous; +pub use raw_asynchronous::RawAsyncApiClient; +mod asynchronous; +pub use asynchronous::AsyncApiClient; +mod synchronous; +pub use synchronous::ApiClient; diff --git a/src/api/qbittorrent/raw_asynchronous.rs b/src/api/qbittorrent/raw_asynchronous.rs new file mode 100644 index 0000000..6beb392 --- /dev/null +++ b/src/api/qbittorrent/raw_asynchronous.rs @@ -0,0 +1,51 @@ +use snafu::prelude::*; +use qbittorrent_web_api::Api; + +use crate::error::{Error, InternalApiSnafu as ApiError}; + +#[derive(Debug)] +pub struct RawAsyncApiClient { + pub api: qbittorrent_web_api::api_impl::Authenticated, +} + +impl RawAsyncApiClient { + pub async fn login(host: &str, login: &str, password: &str) -> Result { + Ok(RawAsyncApiClient { + api: Api::login(host, login, password).await.context(ApiError)?, + }) + } + + #[allow(dead_code)] + pub async fn add(&self, magnet: &str, paused: bool) -> Result { + if paused { + self.api.torrent_management().add(magnet).paused("true").send_raw().await.context(ApiError) + } else { + self.api.torrent_management().add(magnet).send_raw().await.context(ApiError) + } + } + + #[allow(dead_code)] + pub async fn get(&self, hash: &str) -> Result { + self.api.torrent_management().properties_raw(hash).await.context(ApiError) + } + + #[allow(dead_code)] + pub async fn list(&self) -> Result { + self.api.torrent_management().info().send_raw().await.context(ApiError) + } + + #[allow(dead_code)] + pub async fn add_tracker(&self, hash: &str, url: &str) -> Result { + self.api.torrent_management().add_trackers_raw(hash, url).await.context(ApiError) + } + + #[allow(dead_code)] + pub async fn get_trackers(&self, hash: &str) -> Result { + self.api.torrent_management().trackers_raw(hash).await.context(ApiError) + } + + #[allow(dead_code)] + pub async fn remove_tracker(&self, hash: &str, url: &str) -> Result { + self.api.torrent_management().remove_trackers_raw(hash, &vec!(url)).await.context(ApiError) + } +} diff --git a/src/api/qbittorrent/synchronous.rs b/src/api/qbittorrent/synchronous.rs new file mode 100644 index 0000000..7eea61f --- /dev/null +++ b/src/api/qbittorrent/synchronous.rs @@ -0,0 +1,68 @@ +use snafu::prelude::*; +use tokio::runtime::Runtime; + +use crate::{Config, Error}; +use crate::api::{Torrent, TorrentList, TorrentTracker}; +use crate::api::qbittorrent::AsyncApiClient; +use crate::error::ApiSnafu as IOError; +use crate::utils::blocking_runtime; + +#[derive(Debug)] +pub struct ApiClient { + pub rt: Runtime, + pub api: AsyncApiClient, +} + +impl ApiClient { + #[allow(dead_code)] + pub fn from_config(config: &Config) -> Result { + Self::new(&config.format_host(), &config.qbittorrent.login, &config.qbittorrent.password) + } + + /// Login into a qBittorrent backend and return a proper ApiClient instance + #[allow(dead_code)] + pub fn new(host: &str, login: &str, password: &str) -> Result { + let rt = blocking_runtime().context(IOError)?; + let api = rt.block_on(AsyncApiClient::new(host, login, password))?; + Ok(ApiClient { + rt, + api, + }) + } + + #[allow(dead_code)] + pub fn add(&self, magnet: &str, paused: bool) -> Result<(), Error> { + self.rt.block_on(self.api.add(magnet, paused)) + } + + #[allow(dead_code)] + pub fn get(&self, hash: &str) -> Result, Error> { + self.rt.block_on(self.api.get(hash)) + } + + #[allow(dead_code)] + // TODO: Workaround + pub fn get_with_cached_list(&self, hash: &str, list: &TorrentList) -> Result, Error> { + self.rt.block_on(self.api.get_with_cached_list(hash, list)) + } + + #[allow(dead_code)] + pub fn list(&self) -> Result { + self.rt.block_on(self.api.list()) + } + + #[allow(dead_code)] + pub fn add_tracker(&self, hash: &str, url: &str) -> Result<(), Error> { + self.rt.block_on(self.api.add_tracker(hash, url)) + } + + #[allow(dead_code)] + pub fn get_trackers(&self, hash: &str) -> Result, Error> { + self.rt.block_on(self.api.get_trackers(hash)) + } + + #[allow(dead_code)] + pub fn remove_tracker(&self, hash: &str, url: &str) -> Result<(), Error> { + self.rt.block_on(self.api.remove_tracker(hash, url)) + } +} diff --git a/src/utils.rs b/src/utils.rs index 0989319..73efb04 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,5 @@ use snafu::prelude::*; +use tokio::runtime::{Builder,Runtime}; use imdl::infohash::Infohash; use imdl::input::Input; @@ -84,3 +85,9 @@ pub fn find_free_port() -> u16 { let bind = TcpListener::bind("127.0.0.1:0").unwrap(); bind.local_addr().unwrap().port() } + +pub fn blocking_runtime() -> std::io::Result { + Builder::new_current_thread() + .enable_all() + .build() +}