Skip to content

Commit eb96682

Browse files
committed
feat: Login is not mandatory, with QBittorrentClient::new_not_logged_in
1 parent 34782d8 commit eb96682

3 files changed

Lines changed: 51 additions & 31 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- `QBittorrentClient` methods `new_not_logged_in` and `do_login` enable building a client
13+
without actually performing a login
1214
- `QBittorrentClient::qbittorrent_version` returns the qbittorrent daemon version
1315

1416
## Version 0.2.1 (2025-08-28)

src/api_error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,8 @@ pub enum ApiError {
2222
},
2323
#[snafu(display("Invalid infohash: {source}"))]
2424
InfoHash { source: hightorrent::InfoHashError },
25+
#[snafu(display("Failed to initialize the torrent API client:\n{source}"))]
26+
ClientInit {
27+
source: Box<dyn std::error::Error + 'static + Send + Sync>,
28+
},
2529
}

src/qbittorrent/api.rs

Lines changed: 45 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,48 @@ pub struct QBittorrentClient {
2626
}
2727

2828
impl QBittorrentClient {
29+
/// Create a new client that's not logged in yet.
30+
///
31+
/// Then perform `QBittorrentClient::do_login` to actually login.
32+
pub fn new_not_logged_in(host: &str, user: &str, password: &str) -> Result<Self, Error> {
33+
let client = ClientBuilder::new()
34+
.cookie_store(true)
35+
.build()
36+
.boxed()
37+
.context(ClientInitError)?;
38+
39+
Ok(Self {
40+
host: host.to_string(),
41+
user: user.to_string(),
42+
password: password.to_string(),
43+
client,
44+
})
45+
}
46+
47+
pub async fn do_login(&self) -> Result<(), Error> {
48+
let form = Form::new()
49+
.text("username", self.user.to_string())
50+
.text("password", self.password.to_string());
51+
52+
let res = self
53+
.client
54+
.post(format!("{}/api/v2/auth/login", self.host))
55+
.multipart(form)
56+
.send()
57+
.await
58+
.boxed()
59+
.context(HttpError)?;
60+
61+
if res.headers().get("set-cookie").is_some() {
62+
Ok(())
63+
} else {
64+
Err(Error::InvalidLogin {
65+
host: self.host.to_string(),
66+
user: self.user.to_string(),
67+
})
68+
}
69+
}
70+
2971
/// Returns the qBittorrent version, in a `vX.Y.Z` format.
3072
pub async fn qbittorrent_version(&self) -> Result<String, Error> {
3173
let res = self._get(self._endpoint("app/version")).await?;
@@ -205,37 +247,9 @@ impl Api for QBittorrentClient {
205247
}
206248

207249
async fn login(host: &str, user: &str, password: &str) -> Result<Self, Error> {
208-
let client = ClientBuilder::new()
209-
.cookie_store(true)
210-
.build()
211-
.boxed()
212-
.context(HttpError)?;
213-
214-
let form = Form::new()
215-
.text("username", user.to_string())
216-
.text("password", password.to_string());
217-
218-
let res = client
219-
.post(format!("{}/api/v2/auth/login", host))
220-
.multipart(form)
221-
.send()
222-
.await
223-
.boxed()
224-
.context(HttpError)?;
225-
226-
if res.headers().get("set-cookie").is_some() {
227-
Ok(Self {
228-
host: host.to_string(),
229-
user: user.to_string(),
230-
password: password.to_string(),
231-
client,
232-
})
233-
} else {
234-
Err(Error::InvalidLogin {
235-
host: host.to_string(),
236-
user: user.to_string(),
237-
})
238-
}
250+
let api_client = Self::new_not_logged_in(host, user, password)?;
251+
api_client.do_login().await?;
252+
Ok(api_client)
239253
}
240254

241255
async fn list(&self) -> Result<TorrentList, Error> {

0 commit comments

Comments
 (0)