Modularize qBittorrent API in separate file

This commit is contained in:
programmer programmer 2022-11-03 15:41:59 +01:00
parent c6d9a0c07d
commit 6d5958df3f
7 changed files with 241 additions and 297 deletions

View File

@ -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);
}
}

View File

@ -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(())
}
}

View File

@ -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<AsyncApiClient, Error> {
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<AsyncApiClient, Error> {
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<Option<Torrent>, 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::<Vec<Torrent>>();
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<Option<Torrent>, 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::<Vec<Torrent>>();
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<TorrentList, Error> {
let res = self.raw_api.list().await?;
let concrete: Vec<QBittorrentListTorrent> = 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<Vec<TorrentTracker>, Error> {
let res = self.raw_api.get_trackers(hash).await?;
let concrete: Vec<TorrentTracker> = 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(())
}
}

View File

@ -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<Runtime> {
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<RawAsyncApiClient, Error> {
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<String, Error> {
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<String, Error> {
self.api.torrent_management().properties_raw(hash).await.context(ApiError)
}
#[allow(dead_code)]
pub async fn list(&self) -> Result<String, Error> {
self.api.torrent_management().info().send_raw().await.context(ApiError)
}
#[allow(dead_code)]
pub async fn add_tracker(&self, hash: &str, url: &str) -> Result<String, Error> {
self.api.torrent_management().add_trackers_raw(hash, url).await.context(ApiError)
}
#[allow(dead_code)]
pub async fn get_trackers(&self, hash: &str) -> Result<String, Error> {
self.api.torrent_management().trackers_raw(hash).await.context(ApiError)
}
#[allow(dead_code)]
pub async fn remove_tracker(&self, hash: &str, url: &str) -> Result<String, Error> {
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<RawApiClient, Error> {
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<RawApiClient, Error> {
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<String, Error> {
self.rt.block_on(self.api.add(magnet, paused))
}
#[allow(dead_code)]
pub fn get(&self, hash: &str) -> Result<String, Error> {
self.rt.block_on(self.api.get(hash))
}
#[allow(dead_code)]
pub fn list(&self) -> Result<String, Error> {
self.rt.block_on(self.api.list())
}
#[allow(dead_code)]
pub fn add_tracker(&self, hash: &str, url: &str) -> Result<String, Error> {
self.rt.block_on(self.api.add_tracker(hash, url))
}
#[allow(dead_code)]
pub fn get_trackers(&self, hash: &str) -> Result<String, Error> {
self.rt.block_on(self.api.get_trackers(hash))
}
#[allow(dead_code)]
pub fn remove_tracker(&self, hash: &str, url: &str) -> Result<String, Error> {
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<AsyncApiClient, Error> {
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<AsyncApiClient, Error> {
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<Option<Torrent>, 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::<Vec<Torrent>>();
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<Option<Torrent>, 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::<Vec<Torrent>>();
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<TorrentList, Error> {
let res = self.raw_api.list().await?;
let concrete: Vec<QBittorrentListTorrent> = 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<Vec<TorrentTracker>, Error> {
let res = self.raw_api.get_trackers(hash).await?;
let concrete: Vec<TorrentTracker> = 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<ApiClient, Error> {
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<ApiClient, Error> {
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<Option<Torrent>, 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<Option<Torrent>, Error> {
self.rt.block_on(self.api.get_with_cached_list(hash, list))
}
#[allow(dead_code)]
pub fn list(&self) -> Result<TorrentList, Error> {
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<Vec<TorrentTracker>, 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;

View File

@ -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<RawAsyncApiClient, Error> {
Ok(RawAsyncApiClient {
api: Api::login(host, login, password).await.context(ApiError)?,
})
}
#[allow(dead_code)]
pub async fn add(&self, magnet: &str, paused: bool) -> Result<String, Error> {
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<String, Error> {
self.api.torrent_management().properties_raw(hash).await.context(ApiError)
}
#[allow(dead_code)]
pub async fn list(&self) -> Result<String, Error> {
self.api.torrent_management().info().send_raw().await.context(ApiError)
}
#[allow(dead_code)]
pub async fn add_tracker(&self, hash: &str, url: &str) -> Result<String, Error> {
self.api.torrent_management().add_trackers_raw(hash, url).await.context(ApiError)
}
#[allow(dead_code)]
pub async fn get_trackers(&self, hash: &str) -> Result<String, Error> {
self.api.torrent_management().trackers_raw(hash).await.context(ApiError)
}
#[allow(dead_code)]
pub async fn remove_tracker(&self, hash: &str, url: &str) -> Result<String, Error> {
self.api.torrent_management().remove_trackers_raw(hash, &vec!(url)).await.context(ApiError)
}
}

View File

@ -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<ApiClient, Error> {
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<ApiClient, Error> {
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<Option<Torrent>, 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<Option<Torrent>, Error> {
self.rt.block_on(self.api.get_with_cached_list(hash, list))
}
#[allow(dead_code)]
pub fn list(&self) -> Result<TorrentList, Error> {
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<Vec<TorrentTracker>, 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))
}
}

View File

@ -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<Runtime> {
Builder::new_current_thread()
.enable_all()
.build()
}