refactor: organize Rust modules and simplify deployment README

This commit is contained in:
junior
2026-10-17 22:59:25 +08:00
parent 68c972dadd
commit ceb3e5e2b5
37 changed files with 1029 additions and 1225 deletions
+130
View File
@@ -0,0 +1,130 @@
// SPDX-License-Identifier: GPL-2.0-only
use crate::{
config::QbitConfig,
error::{Error, Result},
network::bounded_body,
};
use reqwest::{multipart, Client};
use serde::Deserialize;
use std::time::Duration;
#[derive(Clone)]
pub struct Qbit {
client: Client,
config: QbitConfig,
}
#[derive(Deserialize)]
pub struct TorrentStatus {
pub hash: String,
pub progress: f64,
pub state: String,
#[serde(default)]
pub save_path: Option<String>,
}
#[derive(Deserialize)]
pub struct TorrentFile {
pub name: String,
pub size: i64,
pub priority: i32,
}
impl Qbit {
pub fn new(config: QbitConfig) -> Result<Self> {
// Keep downloader cookies in a separate client from untrusted RSS hosts.
let client = Client::builder()
.no_proxy()
.cookie_store(true)
.timeout(Duration::from_secs(30))
.redirect(reqwest::redirect::Policy::none())
.build()?;
Ok(Self { client, config })
}
pub async fn login(&self) -> Result<()> {
let response = self
.client
.post(format!("{}/api/v2/auth/login", self.config.url))
.header("Referer", format!("{}/", self.config.url))
.form(&[
("username", &self.config.username),
("password", &self.config.password),
])
.send()
.await?;
let bytes = bounded_body(response, 1024).await?;
if bytes != b"Ok." {
return Err(Error::Upstream("qBittorrent login failed".into()));
}
Ok(())
}
pub async fn torrents(&self) -> Result<Vec<TorrentStatus>> {
let response = self
.client
.get(format!("{}/api/v2/torrents/info", self.config.url))
.header("Referer", format!("{}/", self.config.url))
.send()
.await?;
let bytes = bounded_body(response, 8 * 1024 * 1024).await?;
serde_json::from_slice(&bytes)
.map_err(|_| Error::Upstream("invalid qBittorrent task response".into()))
}
pub async fn files(&self, hash: &str) -> Result<Vec<TorrentFile>> {
let response = self
.client
.get(format!("{}/api/v2/torrents/files", self.config.url))
.header("Referer", format!("{}/", self.config.url))
.query(&[("hash", hash)])
.send()
.await?;
let bytes = bounded_body(response, 8 * 1024 * 1024).await?;
let files: Vec<TorrentFile> = serde_json::from_slice(&bytes)
.map_err(|_| Error::Upstream("invalid qBittorrent file list".into()))?;
if files.len() > 5000 || files.iter().any(|file| file.size < 0) {
return Err(Error::Upstream(
"qBittorrent file list exceeds limits or contains invalid sizes".into(),
));
}
Ok(files)
}
pub async fn add(
&self,
url: &str,
bytes: Option<Vec<u8>>,
save_path: Option<&str>,
) -> Result<()> {
let mut form = multipart::Form::new()
.text(
"savepath",
save_path.unwrap_or(&self.config.save_path).to_owned(),
)
.text("tags", "rust-project")
.text("autoTMM", "false");
form = if let Some(bytes) = bytes {
form.part(
"torrents",
multipart::Part::bytes(bytes)
.file_name("release.torrent")
.mime_str("application/x-bittorrent")?,
)
} else {
form.text("urls", url.to_owned())
};
let response = self
.client
.post(format!("{}/api/v2/torrents/add", self.config.url))
.header("Referer", format!("{}/", self.config.url))
.multipart(form)
.send()
.await?;
let bytes = bounded_body(response, 1024).await?;
if bytes != b"Ok." {
return Err(Error::Upstream("qBittorrent rejected torrent".into()));
}
Ok(())
}
}