API has 4 variants: sync/async * raw/typed
This commit is contained in:
parent
3002a19b1c
commit
a144084ac2
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1231,6 +1231,7 @@ dependencies = [
|
|||
"imdl",
|
||||
"qbittorrent-web-api",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"snafu 0.7.2",
|
||||
"tokio",
|
||||
"toml",
|
||||
|
|
|
@ -16,6 +16,7 @@ path = "src/main.rs"
|
|||
[dependencies]
|
||||
toml = "0.5"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
xdg = "2.4"
|
||||
snafu = "0.7"
|
||||
qbittorrent-web-api = "0.6"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use argh::FromArgs;
|
||||
|
||||
use crate::api::ApiClient;
|
||||
use crate::api::qbittorrent::ApiClient;
|
||||
use crate::Error;
|
||||
use crate::action::ActionExec;
|
||||
use crate::config::Config;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use argh::FromArgs;
|
||||
|
||||
use crate::action::ActionExec;
|
||||
use crate::api::ApiClient;
|
||||
use crate::api::qbittorrent::{ApiClient, RawApiClient};
|
||||
use crate::config::Config;
|
||||
use crate::error::Error;
|
||||
|
||||
|
@ -12,6 +12,9 @@ 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(positional)]
|
||||
/// the infohash (or magnet link with --magnet) to retrieve
|
||||
torrent: String,
|
||||
|
@ -19,11 +22,17 @@ pub struct GetAction {
|
|||
|
||||
impl ActionExec for GetAction {
|
||||
fn exec(&self, config: &Config) -> Result<(), Error> {
|
||||
let api = ApiClient::from_config(&config)?;
|
||||
let res = api.get(&self.torrent)?;
|
||||
if let Some(s) = res {
|
||||
println!("{}", s)
|
||||
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)? {
|
||||
println!("{}", t.hash);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use argh::FromArgs;
|
||||
|
||||
use crate::api::ApiClient;
|
||||
use crate::api::qbittorrent::{ApiClient, RawApiClient};
|
||||
use crate::Error;
|
||||
use crate::action::ActionExec;
|
||||
use crate::config::Config;
|
||||
|
@ -8,13 +8,23 @@ use crate::config::Config;
|
|||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
#[argh(subcommand, name = "list")]
|
||||
/// list existing torrents on qBittorrent
|
||||
pub struct ListAction {}
|
||||
pub struct ListAction {
|
||||
#[argh(switch, short = 'r')]
|
||||
/// return raw JSON response from API
|
||||
raw: bool,
|
||||
}
|
||||
|
||||
impl ActionExec for ListAction {
|
||||
fn exec(&self, config: &Config) -> Result<(), Error> {
|
||||
let api = ApiClient::from_config(&config)?;
|
||||
let res = api.list()?;
|
||||
println!("{}", res);
|
||||
if self.raw {
|
||||
let api = RawApiClient::from_config(&config)?;
|
||||
println!("{}", api.list()?);
|
||||
} else {
|
||||
let api = ApiClient::from_config(&config)?;
|
||||
for torrent in api.list()? {
|
||||
println!("{}", torrent.hash);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
161
src/api.rs
161
src/api.rs
|
@ -1,161 +0,0 @@
|
|||
use snafu::ResultExt;
|
||||
use qbittorrent_web_api::Api;
|
||||
use qbittorrent_web_api::api_impl::Error as QBittorrentError;
|
||||
use tokio::runtime::Builder;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::error::{Error, ApiSnafu as IOError, InternalApiSnafu as ApiError};
|
||||
|
||||
pub fn blocking_runtime() -> std::io::Result<tokio::runtime::Runtime> {
|
||||
Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UnauthenticatedRawApiClient {
|
||||
pub rt: tokio::runtime::Runtime,
|
||||
}
|
||||
|
||||
impl UnauthenticatedRawApiClient {
|
||||
/// Initialize a blocking runtime for the API client
|
||||
pub fn new() -> Result<UnauthenticatedRawApiClient, std::io::Error> {
|
||||
Ok(UnauthenticatedRawApiClient {
|
||||
rt: blocking_runtime()?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Login into a qBittorrent backend and return a proper ApiClient instance
|
||||
pub fn login(self, host: &str, login: &str, password: &str) -> Result<RawApiClient, QBittorrentError> {
|
||||
let api = self.rt.block_on(Api::login(host, login, password))?;
|
||||
Ok(RawApiClient {
|
||||
rt: self.rt,
|
||||
api,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RawApiClient {
|
||||
rt: tokio::runtime::Runtime,
|
||||
api: qbittorrent_web_api::api_impl::Authenticated,
|
||||
}
|
||||
|
||||
impl RawApiClient {
|
||||
pub fn add(&self, magnet: &str, paused: bool) -> Result<String, QBittorrentError> {
|
||||
let base_call = self.api.torrent_management();
|
||||
let call = if paused {
|
||||
base_call.add(magnet).paused("true")
|
||||
} else {
|
||||
base_call.add(magnet)
|
||||
};
|
||||
self.rt.block_on(call.send_raw())
|
||||
}
|
||||
|
||||
pub fn get(&self, hash: &str) -> Result<String, QBittorrentError> {
|
||||
self.rt.block_on(self.api.torrent_management().properties_raw(hash))
|
||||
}
|
||||
|
||||
pub fn list(&self) -> Result<String, QBittorrentError> {
|
||||
self.rt.block_on(self.api.torrent_management().info().send_raw())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// ApiClient is a convenience struct around qbittorrent_web_api for use in async programs, using qbt::Error error types.
|
||||
pub struct ApiClient {
|
||||
pub raw_api: RawApiClient,
|
||||
}
|
||||
|
||||
impl ApiClient {
|
||||
/// Login to a qBittorrent backend and return a handle to the ApiClient
|
||||
pub fn new(host: &str, login: &str, password: &str) -> Result<ApiClient, Error> {
|
||||
let unauthenticated = UnauthenticatedRawApiClient::new().context(IOError)?;
|
||||
let authenticated = unauthenticated.login(host, login, password).map_err(|e| {
|
||||
match e {
|
||||
QBittorrentError::HttpError(_) => {
|
||||
Error::FailedToReachAPI { source: e }
|
||||
}, QBittorrentError::InvalidUsernameOrPassword => {
|
||||
Error::FailedLogin { user: login.to_string() }
|
||||
} _ => {
|
||||
panic!("Cookie error");
|
||||
}
|
||||
}
|
||||
})?;
|
||||
Ok(ApiClient {
|
||||
raw_api: authenticated,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_config(config: &Config) -> Result<ApiClient, Error> {
|
||||
Self::new(&config.format_host(), &config.qbittorrent.login, &config.qbittorrent.password)
|
||||
}
|
||||
|
||||
|
||||
pub fn add(&self, magnet: &str, paused: bool) -> Result<(), Error> {
|
||||
let res = self.raw_api.add(magnet, paused).context(ApiError)?;
|
||||
if res == "Ok." {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::message(res.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, hash: &str) -> Result<Option<String>, Error> {
|
||||
let res = self.raw_api.get(hash).context(ApiError)?;
|
||||
if res == "" {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(res))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn list(&self) -> Result<String, Error> {
|
||||
self.raw_api.list().context(ApiError)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// TODO: typestate builder https://www.greyblake.com/blog/builder-with-typestate-in-rust/
|
||||
//struct ApiClientBuilder {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::utils::*;
|
||||
use crate::api::{ApiClient, UnauthenticatedRawApiClient, QBittorrentError};
|
||||
use crate::error::Error;
|
||||
|
||||
#[test]
|
||||
fn raw_wrong_server() {
|
||||
let port = find_free_port();
|
||||
let api = UnauthenticatedRawApiClient::new().expect("IOERROR");
|
||||
let login = api.login(&format!("http://localhost:{}", &port), "admin", "adminadmin");
|
||||
assert!(login.is_err());
|
||||
let err = login.unwrap_err();
|
||||
match &err {
|
||||
QBittorrentError::HttpError(_) => {
|
||||
return;
|
||||
}, _ => {
|
||||
println!("{:?}", err);
|
||||
panic!("API CHANGE!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrong_server() {
|
||||
let port = find_free_port();
|
||||
let api = ApiClient::new(&format!("http://localhost:{}", &port), "admin", "adminadmin");
|
||||
assert!(api.is_err());
|
||||
let err = api.unwrap_err();
|
||||
match &err {
|
||||
Error::FailedToReachAPI { source: _ } => {
|
||||
return;
|
||||
}, _ => {
|
||||
println!("{:?}", err);
|
||||
panic!("ERROR CONVERSION PROBLEM");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
56
src/api/mod.rs
Normal file
56
src/api/mod.rs
Normal file
|
@ -0,0 +1,56 @@
|
|||
pub mod qbittorrent;
|
||||
|
||||
pub fn calc_progress(have: f64, total: f64) -> u8 {
|
||||
let ratio = have / total * f64::from(100);
|
||||
ratio.floor() as u8
|
||||
}
|
||||
|
||||
pub trait IntoTorrent {
|
||||
fn into_torrent(&self, hash: &str) -> Torrent;
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Torrent {
|
||||
pub hash: String,
|
||||
pub name: String,
|
||||
pub path: String,
|
||||
pub date_start: i64,
|
||||
pub date_end: i64,
|
||||
/// Progress percentage (0-100)
|
||||
pub progress: u8,
|
||||
pub size: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct TorrentList(Vec<Torrent>);
|
||||
|
||||
impl TorrentList {
|
||||
pub fn new() -> TorrentList {
|
||||
TorrentList(Vec::new())
|
||||
}
|
||||
|
||||
pub fn push(&mut self, entry: Torrent) {
|
||||
self.0.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for TorrentList {
|
||||
type Item = Torrent;
|
||||
type IntoIter = std::vec::IntoIter<Self::Item>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<Torrent> for TorrentList {
|
||||
fn from_iter<I: IntoIterator<Item=Torrent>>(iter: I) -> Self {
|
||||
let mut c = TorrentList::new();
|
||||
|
||||
for i in iter {
|
||||
c.push(i);
|
||||
}
|
||||
|
||||
c
|
||||
}
|
||||
}
|
181
src/api/qbittorrent/mod.rs
Normal file
181
src/api/qbittorrent/mod.rs
Normal file
|
@ -0,0 +1,181 @@
|
|||
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};
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
#[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())
|
||||
}
|
||||
}
|
||||
|
||||
#[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 {
|
||||
let concrete: QBittorrentPropertiesTorrent = serde_json::from_str(&res).context(DeserializeError)?;
|
||||
Ok(Some(concrete.into_torrent(hash)))
|
||||
}
|
||||
}
|
||||
|
||||
#[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(&t.hash)).collect())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[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)]
|
||||
pub fn list(&self) -> Result<TorrentList, Error> {
|
||||
self.rt.block_on(self.api.list())
|
||||
}
|
||||
}
|
65
src/api/qbittorrent/types.rs
Normal file
65
src/api/qbittorrent/types.rs
Normal file
|
@ -0,0 +1,65 @@
|
|||
use crate::api::{Torrent, IntoTorrent, calc_progress};
|
||||
|
||||
/// Deserializes from the 'properties' endpoint of QBittorrent API
|
||||
/// https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-torrent-generic-properties
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct QBittorrentPropertiesTorrent {
|
||||
pub name: String,
|
||||
#[serde(rename="save_path")]
|
||||
pub path: String,
|
||||
#[serde(rename="addition_date")]
|
||||
pub date_start: i64,
|
||||
#[serde(rename="completion_date")]
|
||||
pub date_end: i64,
|
||||
#[serde(rename="pieces_have")]
|
||||
pub pieces_have: i64,
|
||||
#[serde(rename="pieces_num")]
|
||||
pub pieces_total: i64,
|
||||
#[serde(rename="total_size")]
|
||||
pub size: i64,
|
||||
}
|
||||
|
||||
impl IntoTorrent for QBittorrentPropertiesTorrent {
|
||||
fn into_torrent(&self, hash: &str) -> Torrent {
|
||||
Torrent {
|
||||
hash: hash.to_string(),
|
||||
name: self.name.to_string(),
|
||||
path: self.path.to_string(),
|
||||
date_start: self.date_start,
|
||||
date_end: self.date_end,
|
||||
progress: calc_progress(self.pieces_have as f64, self.pieces_total as f64),
|
||||
size: self.size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Deserializes from the 'info' endpoint of QBittorrent API
|
||||
/// https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-torrent-list
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct QBittorrentListTorrent {
|
||||
pub hash: String,
|
||||
pub name: String,
|
||||
#[serde(rename="save_path")]
|
||||
pub path: String,
|
||||
#[serde(rename="added_on")]
|
||||
pub date_start: i64,
|
||||
#[serde(rename="completion_on")]
|
||||
pub date_end: i64,
|
||||
pub progress: f32,
|
||||
#[serde(rename="total_size")]
|
||||
pub size: i64,
|
||||
}
|
||||
|
||||
impl IntoTorrent for QBittorrentListTorrent {
|
||||
fn into_torrent(&self, _hash: &str) -> Torrent {
|
||||
Torrent {
|
||||
hash: self.hash.to_string(),
|
||||
name: self.name.to_string(),
|
||||
path: self.path.to_string(),
|
||||
date_start: self.date_start,
|
||||
date_end: self.date_end,
|
||||
progress: (self.progress * 100.0) as u8,
|
||||
size: self.size,
|
||||
}
|
||||
}
|
||||
}
|
105
src/async_api.rs
105
src/async_api.rs
|
@ -1,105 +0,0 @@
|
|||
use snafu::ResultExt;
|
||||
use qbittorrent_web_api::Api;
|
||||
use qbittorrent_web_api::api_impl::Error as QBittorrentError;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::error::{Error, InternalApiSnafu as ApiError};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UnauthenticatedRawApiClient;
|
||||
|
||||
impl UnauthenticatedRawApiClient {
|
||||
/// Initialize a blocking runtime for the API client
|
||||
pub fn new() -> UnauthenticatedRawApiClient {
|
||||
UnauthenticatedRawApiClient
|
||||
}
|
||||
|
||||
/// Login into a qBittorrent backend and return a proper ApiClient instance
|
||||
pub async fn login(self, host: &str, login: &str, password: &str) -> Result<RawApiClient, QBittorrentError> {
|
||||
let api = Api::login(host, login, password).await?;
|
||||
Ok(RawApiClient {
|
||||
api,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RawApiClient {
|
||||
api: qbittorrent_web_api::api_impl::Authenticated,
|
||||
}
|
||||
|
||||
impl RawApiClient {
|
||||
pub async fn add(&self, magnet: &str, paused: bool) -> Result<String, QBittorrentError> {
|
||||
let base_call = self.api.torrent_management();
|
||||
let call = if paused {
|
||||
base_call.add(magnet).paused("true")
|
||||
} else {
|
||||
base_call.add(magnet)
|
||||
};
|
||||
call.send_raw().await
|
||||
}
|
||||
|
||||
pub async fn get(&self, hash: &str) -> Result<String, QBittorrentError> {
|
||||
self.api.torrent_management().properties_raw(hash).await
|
||||
}
|
||||
|
||||
pub async fn list(&self) -> Result<String, QBittorrentError> {
|
||||
self.api.torrent_management().info().send_raw().await
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// ApiClient is a convenience struct around qbittorrent_web_api for use in async programs, using qbt::Error error types.
|
||||
pub struct ApiClient {
|
||||
pub raw_api: RawApiClient,
|
||||
}
|
||||
|
||||
impl ApiClient {
|
||||
/// Login to a qBittorrent backend and return a handle to the ApiClient
|
||||
pub async fn new(host: &str, login: &str, password: &str) -> Result<ApiClient, Error> {
|
||||
let unauthenticated = UnauthenticatedRawApiClient::new();
|
||||
let authenticated = unauthenticated.login(host, login, password).await.map_err(|e| {
|
||||
match e {
|
||||
QBittorrentError::HttpError(_) => {
|
||||
Error::FailedToReachAPI { source: e }
|
||||
}, QBittorrentError::InvalidUsernameOrPassword => {
|
||||
Error::FailedLogin { user: login.to_string() }
|
||||
} _ => {
|
||||
panic!("Cookie error");
|
||||
}
|
||||
}
|
||||
})?;
|
||||
Ok(ApiClient {
|
||||
raw_api: authenticated,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn from_config(config: &Config) -> Result<ApiClient, Error> {
|
||||
Self::new(&config.format_host(), &config.qbittorrent.login, &config.qbittorrent.password).await
|
||||
}
|
||||
|
||||
|
||||
pub async fn add(&self, magnet: &str, paused: bool) -> Result<(), Error> {
|
||||
let res = self.raw_api.add(magnet, paused).await.context(ApiError)?;
|
||||
if res == "Ok." {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::message(res.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get(&self, hash: &str) -> Result<Option<String>, Error> {
|
||||
let res = self.raw_api.get(hash).await.context(ApiError)?;
|
||||
if res == "" {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(res))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list(&self) -> Result<String, Error> {
|
||||
self.raw_api.list().await.context(ApiError)
|
||||
}
|
||||
|
||||
}
|
|
@ -34,6 +34,8 @@ pub enum Error {
|
|||
EmptyNameMagnet { magnet: String },
|
||||
#[snafu(display("Failed to read file {} because of underlying IO error:\n{}", path.display(), source))]
|
||||
FailedToReadFile { path: PathBuf, source: std::io::Error },
|
||||
#[snafu(display("Failed to deserialize JSON response into concrete type:\n{}", source))]
|
||||
FailedDeserialize { source: serde_json::Error },
|
||||
}
|
||||
|
||||
impl Error {
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
#[macro_use] extern crate serde;
|
||||
|
||||
pub mod action;
|
||||
pub mod api;
|
||||
pub mod async_api;
|
||||
pub use api::{Torrent, TorrentList, IntoTorrent};
|
||||
pub mod config;
|
||||
pub use config::Config;
|
||||
pub mod cli;
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
#[macro_use] extern crate serde;
|
||||
use snafu::prelude::*;
|
||||
|
||||
mod action;
|
||||
mod api;
|
||||
pub use api::{Torrent, TorrentList, IntoTorrent};
|
||||
mod config;
|
||||
use config::Config;
|
||||
mod cli;
|
||||
|
|
Loading…
Reference in New Issue
Block a user