Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 65 additions & 9 deletions src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ use reqwest::Client;
use tap::Pipe;
use url::Url;

use crate::{ext::Cookie, model::Credential, LoginState, Qbit};
use crate::{LoginState, Qbit, ext::Cookie, model::Credential};

pub struct QbitBuilder<C = (), R = (), E = ()> {
pub struct QbitBuilder<C = (), R = (), E = (), B = ()> {
credential: C,
client: R,
endpoint: E,
basic_auth_credentials: B,
}

trait IntoLoginState {
Expand All @@ -36,6 +37,7 @@ impl QbitBuilder {
credential: (),
client: (),
endpoint: (),
basic_auth_credentials: (),
}
}
}
Expand All @@ -46,45 +48,61 @@ impl Default for QbitBuilder {
}
}

impl<C, R, E> QbitBuilder<C, R, E> {
pub fn client(self, client: Client) -> QbitBuilder<C, Client, E> {
impl<C, R, E, B> QbitBuilder<C, R, E, B> {
pub fn client(self, client: Client) -> QbitBuilder<C, Client, E, B> {
QbitBuilder {
credential: self.credential,
client,
endpoint: self.endpoint,
basic_auth_credentials: self.basic_auth_credentials,
}
}

#[allow(private_interfaces)]
pub fn cookie(self, cookie: impl Into<String>) -> QbitBuilder<Cookie, R, E> {
pub fn cookie(self, cookie: impl Into<String>) -> QbitBuilder<Cookie, R, E, B> {
QbitBuilder {
credential: Cookie(cookie.into()),
client: self.client,
endpoint: self.endpoint,
basic_auth_credentials: self.basic_auth_credentials,
}
}

pub fn credential(self, credential: Credential) -> QbitBuilder<Credential, R, E> {
pub fn credential(self, credential: Credential) -> QbitBuilder<Credential, R, E, B> {
QbitBuilder {
credential,
client: self.client,
endpoint: self.endpoint,
basic_auth_credentials: self.basic_auth_credentials,
}
}

pub fn endpoint<U>(self, endpoint: U) -> QbitBuilder<C, R, U>
pub fn endpoint<U>(self, endpoint: U) -> QbitBuilder<C, R, U, B>
where
U: TryInto<Url>,
{
QbitBuilder {
credential: self.credential,
client: self.client,
endpoint,
basic_auth_credentials: self.basic_auth_credentials,
}
}

pub fn basic_auth_credentials(
self,
basic_auth_credentials: Option<Credential>,
) -> QbitBuilder<C, R, E, Option<Credential>> {
QbitBuilder {
credential: self.credential,
client: self.client,
endpoint: self.endpoint,
basic_auth_credentials,
}
}
}

impl<C, U> QbitBuilder<C, reqwest::Client, U>
impl<C, U> QbitBuilder<C, reqwest::Client, U, Option<Credential>>
where
C: IntoLoginState,
U: TryInto<Url>,
Expand All @@ -98,11 +116,12 @@ where
client: self.client,
endpoint,
state,
basic_auth_credentials: self.basic_auth_credentials,
}
}
}

impl<C, U> QbitBuilder<C, (), U>
impl<C, U> QbitBuilder<C, (), U, Option<Credential>>
where
C: IntoLoginState,
U: TryInto<Url>,
Expand All @@ -113,6 +132,30 @@ where
}
}

// No basic auth credential provided
impl<C, U> QbitBuilder<C, (), U, ()>
where
C: IntoLoginState,
U: TryInto<Url>,
U::Error: Debug,
{
pub fn build(self) -> Qbit {
self.basic_auth_credentials(None).build()
}
}

// TODO: How to factorize with previous one?
impl<C, U> QbitBuilder<C, reqwest::Client, U, ()>
where
C: IntoLoginState,
U: TryInto<Url>,
U::Error: Debug,
{
pub fn build(self) -> Qbit {
self.basic_auth_credentials(None).build()
}
}

#[test]
fn test_builder() {
QbitBuilder::new()
Expand All @@ -136,4 +179,17 @@ fn test_builder() {
.endpoint("http://localhost:8080")
.cookie("SID=1234567890")
.build();

QbitBuilder::new()
.basic_auth_credentials(Some(Credential::new("basic", "auth")))
.client(reqwest::Client::new())
.endpoint("http://localhost:8080")
.credential(Credential::new("admin", "adminadmin"))
.build();

QbitBuilder::new()
.endpoint("http://localhost:8080")
.cookie("SID=1234567890")
.basic_auth_credentials(None)
.build();
}
82 changes: 52 additions & 30 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use std::{
pub mod model;
pub use builder::QbitBuilder;
use bytes::Bytes;
use reqwest::{header, Client, Method, Response, StatusCode};
use reqwest::{Client, Method, Response, StatusCode, header};
use serde::Serialize;
use serde_with::skip_serializing_none;
use tap::{Pipe, TapFallible};
Expand Down Expand Up @@ -75,6 +75,7 @@ pub struct Qbit {
client: Client,
endpoint: Url,
state: Mutex<LoginState>,
basic_auth_credentials: Option<Credential>,
}

impl Qbit {
Expand Down Expand Up @@ -207,12 +208,9 @@ impl Qbit {
last_known_id: Option<i64>,
}

self.get_with(
"log/peers",
&Arg {
last_known_id: last_known_id.into(),
},
)
self.get_with("log/peers", &Arg {
last_known_id: last_known_id.into(),
})
.await?
.json()
.await
Expand Down Expand Up @@ -244,13 +242,10 @@ impl Qbit {
rid: Option<i64>,
}

self.get_with(
"sync/torrentPeers",
&Arg {
hash: hash.as_ref(),
rid: rid.into(),
},
)
self.get_with("sync/torrentPeers", &Arg {
hash: hash.as_ref(),
rid: rid.into(),
})
.await
.and_then(|r| r.map_status(TORRENT_NOT_FOUND))?
.json()
Expand Down Expand Up @@ -282,7 +277,9 @@ impl Qbit {
}

pub async fn toggle_speed_limits_mode(&self) -> Result<()> {
self.post("transfer/toggleSpeedLimitsMode", None::<&()>).await?.end()
self.post("transfer/toggleSpeedLimitsMode", None::<&()>)
.await?
.end()
}

pub async fn get_download_limit(&self) -> Result<u64> {
Expand Down Expand Up @@ -413,13 +410,10 @@ impl Qbit {
indexes: Option<String>,
}

self.get_with(
"torrents/files",
&Arg {
hash: hash.as_ref(),
indexes: indexes.into().map(|s| s.to_string()),
},
)
self.get_with("torrents/files", &Arg {
hash: hash.as_ref(),
indexes: indexes.into().map(|s| s.to_string()),
})
.await
.and_then(|r| r.map_status(TORRENT_NOT_FOUND))?
.json()
Expand Down Expand Up @@ -536,6 +530,7 @@ impl Qbit {
let req = self
.client
.request(Method::POST, self.url("torrents/add"))
.pipe(|req| self.add_basic_auth(req))
.multipart(form)
.header(header::COOKIE, {
self.state()
Expand Down Expand Up @@ -1419,6 +1414,7 @@ impl Qbit {
debug!("Cookie not found, logging in");
self.client
.request(Method::POST, self.url("auth/login"))
.pipe(|req| self.add_basic_auth(req))
.pipe(|req| {
req.form(
self.state()
Expand All @@ -1443,6 +1439,28 @@ impl Qbit {
Ok(())
}

fn add_basic_auth(&self, req: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
if self.basic_auth_credentials.is_some() {
trace!("Adding basic auth credentials");
req.basic_auth(
self.basic_auth_credentials
.as_ref()
.unwrap()
.username
.clone(),
Some(
self.basic_auth_credentials
.as_ref()
.unwrap()
.password
.clone(),
),
)
} else {
req
}
}

async fn request(
&self,
method: Method,
Expand All @@ -1453,14 +1471,14 @@ impl Qbit {
// If it's not the first attempt, we need to re-login
self.login(i != 0).await?;

let mut req =
self.client
.request(method.clone(), self.url(path))
.header(header::COOKIE, {
self.state()
.as_cookie()
.expect("Cookie should be set after login")
});
let mut req: reqwest::RequestBuilder = self
.client
.request(method.clone(), self.url(path))
.header(header::COOKIE, {
self.state()
.as_cookie()
.expect("Cookie should be set after login")
});

if let Some(ref body) = body {
match method {
Expand All @@ -1469,6 +1487,9 @@ impl Qbit {
_ => unreachable!("Only GET and POST are supported"),
}
}

req = self.add_basic_auth(req);

trace!(request = ?req, "Sending request");
let res = req
.send()
Expand Down Expand Up @@ -1520,6 +1541,7 @@ impl Clone for Qbit {
client: self.client.clone(),
endpoint: self.endpoint.clone(),
state: Mutex::new(state),
basic_auth_credentials: self.basic_auth_credentials.clone(),
}
}
}
Expand Down
12 changes: 6 additions & 6 deletions src/model/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ mod_use::mod_use![app, log, sync, torrent, transfer, search];
/// Username and password used to authenticate with qBittorrent.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Credential {
username: String,
password: String,
pub username: String,
pub password: String,
}

impl Credential {
Expand Down Expand Up @@ -86,16 +86,16 @@ pub struct Tracker {
#[repr(i8)]
pub enum TrackerStatus {
/// Tracker is disabled (used for DHT, PeX, and LSD)
Disabled = 0,
Disabled = 0,
/// Tracker has not been contacted yet
NotContacted = 1,
/// Tracker has been contacted and is working
Working = 2,
Working = 2,
/// Tracker is updating
Updating = 3,
Updating = 3,
/// Tracker has been contacted, but it is not working (or doesn't send
/// proper replies)
NotWorking = 4,
NotWorking = 4,
}

/// Type that can be either an integer or a string.
Expand Down