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",
|
"imdl",
|
||||||
"qbittorrent-web-api",
|
"qbittorrent-web-api",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"snafu 0.7.2",
|
"snafu 0.7.2",
|
||||||
"tokio",
|
"tokio",
|
||||||
"toml",
|
"toml",
|
||||||
|
|
|
@ -16,6 +16,7 @@ path = "src/main.rs"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
toml = "0.5"
|
toml = "0.5"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
xdg = "2.4"
|
xdg = "2.4"
|
||||||
snafu = "0.7"
|
snafu = "0.7"
|
||||||
qbittorrent-web-api = "0.6"
|
qbittorrent-web-api = "0.6"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use argh::FromArgs;
|
use argh::FromArgs;
|
||||||
|
|
||||||
use crate::api::ApiClient;
|
use crate::api::qbittorrent::ApiClient;
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
use crate::action::ActionExec;
|
use crate::action::ActionExec;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use argh::FromArgs;
|
use argh::FromArgs;
|
||||||
|
|
||||||
use crate::action::ActionExec;
|
use crate::action::ActionExec;
|
||||||
use crate::api::ApiClient;
|
use crate::api::qbittorrent::{ApiClient, RawApiClient};
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
|
|
||||||
|
@ -12,6 +12,9 @@ pub struct GetAction {
|
||||||
#[argh(switch)]
|
#[argh(switch)]
|
||||||
/// the positional argument is a magnet link, not an infohash
|
/// the positional argument is a magnet link, not an infohash
|
||||||
magnet: bool,
|
magnet: bool,
|
||||||
|
#[argh(switch, short = 'r')]
|
||||||
|
/// return raw JSON response from API
|
||||||
|
raw: bool,
|
||||||
#[argh(positional)]
|
#[argh(positional)]
|
||||||
/// the infohash (or magnet link with --magnet) to retrieve
|
/// the infohash (or magnet link with --magnet) to retrieve
|
||||||
torrent: String,
|
torrent: String,
|
||||||
|
@ -19,11 +22,17 @@ pub struct GetAction {
|
||||||
|
|
||||||
impl ActionExec for GetAction {
|
impl ActionExec for GetAction {
|
||||||
fn exec(&self, config: &Config) -> Result<(), Error> {
|
fn exec(&self, config: &Config) -> Result<(), Error> {
|
||||||
let api = ApiClient::from_config(&config)?;
|
if self.raw {
|
||||||
let res = api.get(&self.torrent)?;
|
let api = RawApiClient::from_config(&config)?;
|
||||||
if let Some(s) = res {
|
let res = api.get(&self.torrent)?;
|
||||||
println!("{}", s)
|
println!("{}", res);
|
||||||
|
} else {
|
||||||
|
let api = ApiClient::from_config(&config)?;
|
||||||
|
if let Some(t) = api.get(&self.torrent)? {
|
||||||
|
println!("{}", t.hash);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use argh::FromArgs;
|
use argh::FromArgs;
|
||||||
|
|
||||||
use crate::api::ApiClient;
|
use crate::api::qbittorrent::{ApiClient, RawApiClient};
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
use crate::action::ActionExec;
|
use crate::action::ActionExec;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
@ -8,13 +8,23 @@ use crate::config::Config;
|
||||||
#[derive(FromArgs, PartialEq, Debug)]
|
#[derive(FromArgs, PartialEq, Debug)]
|
||||||
#[argh(subcommand, name = "list")]
|
#[argh(subcommand, name = "list")]
|
||||||
/// list existing torrents on qBittorrent
|
/// 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 {
|
impl ActionExec for ListAction {
|
||||||
fn exec(&self, config: &Config) -> Result<(), Error> {
|
fn exec(&self, config: &Config) -> Result<(), Error> {
|
||||||
let api = ApiClient::from_config(&config)?;
|
if self.raw {
|
||||||
let res = api.list()?;
|
let api = RawApiClient::from_config(&config)?;
|
||||||
println!("{}", res);
|
println!("{}", api.list()?);
|
||||||
|
} else {
|
||||||
|
let api = ApiClient::from_config(&config)?;
|
||||||
|
for torrent in api.list()? {
|
||||||
|
println!("{}", torrent.hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(())
|
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 },
|
EmptyNameMagnet { magnet: String },
|
||||||
#[snafu(display("Failed to read file {} because of underlying IO error:\n{}", path.display(), source))]
|
#[snafu(display("Failed to read file {} because of underlying IO error:\n{}", path.display(), source))]
|
||||||
FailedToReadFile { path: PathBuf, source: std::io::Error },
|
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 {
|
impl Error {
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
#[macro_use] extern crate serde;
|
||||||
|
|
||||||
pub mod action;
|
pub mod action;
|
||||||
pub mod api;
|
pub mod api;
|
||||||
pub mod async_api;
|
pub use api::{Torrent, TorrentList, IntoTorrent};
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub use config::Config;
|
pub use config::Config;
|
||||||
pub mod cli;
|
pub mod cli;
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
|
#[macro_use] extern crate serde;
|
||||||
use snafu::prelude::*;
|
use snafu::prelude::*;
|
||||||
|
|
||||||
mod action;
|
mod action;
|
||||||
mod api;
|
mod api;
|
||||||
|
pub use api::{Torrent, TorrentList, IntoTorrent};
|
||||||
mod config;
|
mod config;
|
||||||
use config::Config;
|
use config::Config;
|
||||||
mod cli;
|
mod cli;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user