Implement add torrent endpoint

This commit is contained in:
SeanOMik 2022-06-19 13:54:09 -04:00
parent 5f9b53b175
commit 0d9e287fc9
5 changed files with 395 additions and 21 deletions

95
Cargo.lock generated
View File

@ -99,8 +99,18 @@ version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c"
dependencies = [
"darling_core",
"darling_macro",
"darling_core 0.13.4",
"darling_macro 0.13.4",
]
[[package]]
name = "darling"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4529658bdda7fd6769b8614be250cdcfc3aeb0ee72fe66f9e41e5e5eb73eac02"
dependencies = [
"darling_core 0.14.1",
"darling_macro 0.14.1",
]
[[package]]
@ -117,17 +127,73 @@ dependencies = [
"syn",
]
[[package]]
name = "darling_core"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "649c91bc01e8b1eac09fb91e8dbc7d517684ca6be8ebc75bb9cafc894f9fdb6f"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835"
dependencies = [
"darling_core",
"darling_core 0.13.4",
"quote",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddfc69c5bfcbd2fc09a0f38451d2daf0e372e367986a83906d1b0dbc88134fb5"
dependencies = [
"darling_core 0.14.1",
"quote",
"syn",
]
[[package]]
name = "derive_builder"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d07adf7be193b71cc36b193d0f5fe60b918a3a9db4dad0449f57bcfd519704a3"
dependencies = [
"derive_builder_macro",
]
[[package]]
name = "derive_builder_core"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f91d4cfa921f1c05904dc3c57b4a32c38aed3340cce209f3a6fd1478babafc4"
dependencies = [
"darling 0.14.1",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "derive_builder_macro"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f0314b72bed045f3a68671b3c86328386762c93f82d98c65c3cb5e5f573dd68"
dependencies = [
"derive_builder_core",
"syn",
]
[[package]]
name = "encoding_rs"
version = "0.8.31"
@ -433,6 +499,16 @@ version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
[[package]]
name = "mime_guess"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
dependencies = [
"mime",
"unicase",
]
[[package]]
name = "mio"
version = "0.8.3"
@ -617,6 +693,7 @@ dependencies = [
name = "qbittorrent"
version = "0.1.0"
dependencies = [
"derive_builder",
"reqwest",
"serde",
"serde_json",
@ -675,6 +752,7 @@ dependencies = [
"lazy_static",
"log",
"mime",
"mime_guess",
"native-tls",
"percent-encoding",
"pin-project-lite",
@ -807,7 +885,7 @@ version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082"
dependencies = [
"darling",
"darling 0.13.4",
"proc-macro2",
"quote",
"syn",
@ -995,6 +1073,15 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
[[package]]
name = "unicase"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
dependencies = [
"version_check",
]
[[package]]
name = "unicode-bidi"
version = "0.3.8"

View File

@ -11,5 +11,7 @@ serde_json = "1.0.81"
serde_with = "1.14.0"
serde_repr = "0.1"
reqwest = { version = "0.11.11", features = ["cookies"] }
reqwest = { version = "0.11.11", features = ["cookies", "multipart"] }
tokio = { version = "1.19.2", features = ["full"] }
derive_builder = "0.11.2"

View File

@ -1,4 +1,4 @@
use crate::{error::ClientError, TorrentInfo, TorrentTracker};
use crate::{error::ClientError, TorrentInfo, TorrentTracker, TorrentUpload};
pub struct ConnectionInfo {
pub url: String,
@ -61,8 +61,6 @@ impl QBittorrentClient {
/// Get a list of all torrents in the client.
pub async fn get_torrent_list(&self) -> ClientResult<Vec<TorrentInfo>> {
if let (Some(auth_string), Some(conn)) = (self.auth_string.as_ref(), self.connection_info.as_ref()) {
println!("Authentication string: {}", auth_string);
// Construct and send request to qbittorrent
let resp = self.client.post(format!("{}/api/v2/torrents/info", conn.url.clone()))
.header(reqwest::header::COOKIE, auth_string.clone())
@ -81,8 +79,6 @@ impl QBittorrentClient {
/// Get a list of trackers for a torrent.
pub async fn get_torrent_trackers(&self, torrent: &TorrentInfo) -> ClientResult<Vec<TorrentTracker>> {
if let (Some(auth_string), Some(conn)) = (self.auth_string.as_ref(), self.connection_info.as_ref()) {
println!("Authentication string: {}", auth_string);
// Construct and send request to qbittorrent
let resp = self.client.post(format!("{}/api/v2/torrents/trackers", conn.url.clone()))
.header(reqwest::header::COOKIE, auth_string.clone())
@ -100,4 +96,18 @@ impl QBittorrentClient {
Err(ClientError::Authorization)
}
}
pub async fn add_torrent(&self, upload: &TorrentUpload) -> ClientResult<()> {
if let (Some(auth_string), Some(conn)) = (self.auth_string.as_ref(), self.connection_info.as_ref()) {
// Construct and send request to qbittorrent
let resp = self.client.post(format!("{}/api/v2/torrents/add", conn.url.clone()))
.header(reqwest::header::COOKIE, auth_string.clone())
.multipart(upload.to_multipart_form())
.send().await?.error_for_status()?;
Ok(())
} else {
Err(ClientError::Authorization)
}
}
}

View File

@ -1,4 +1,9 @@
#[macro_use]
extern crate derive_builder;
pub mod torrent;
use std::path::Path;
pub use torrent::*;
pub mod client;
@ -17,11 +22,5 @@ async fn main() {
String::from("adminadmin")
).await.unwrap();
let torrents = client.get_torrent_list().await.unwrap();
let first = torrents.first().unwrap();
client.get_torrent_trackers(first).await.unwrap();
println!("Hello, world!");
}

View File

@ -42,7 +42,7 @@ pub struct TorrentInfo {
pub downloaded_session: u64,
/// Torrent ETA (seconds)
pub eta: u64,
pub eta: i64,
/// True if first last piece are prioritized
pub f_l_piece_prio: bool,
@ -54,7 +54,7 @@ pub struct TorrentInfo {
pub hash: String,
/// Last time (Unix Epoch) when a chunk was downloaded/uploaded
pub last_activity: u64,
pub last_activity: i64,
/// Magnet URI corresponding to this torrent
pub magnet_uri: String,
@ -110,7 +110,7 @@ pub struct TorrentInfo {
pub seq_dl: bool,
/// Total size (bytes) of files selected for download
pub size: u64,
pub size: i64,
/// Torrent state. See table here below for the possible values
pub state: TorrentState,
@ -126,7 +126,7 @@ pub struct TorrentInfo {
pub time_active: i32,
/// Total size (bytes) of all file in this torrent (including unselected ones)
pub total_size: u64,
pub total_size: i64,
/// The first tracker with working status. Returns empty string if no tracker is working.
pub tracker: String,
@ -272,3 +272,279 @@ pub enum TrackerStatus {
/// Tracker has been contacted, but it is not working (or doesn't send proper replies)
NotWorking = 4
}
/// Represents a request to add torrents to the client.
#[derive(Debug, Default/* , Serialize, Deserialize */)]
pub struct TorrentUpload {
/// URL(s) of the torrent files. When specifying `http` or `https` URLs, they
/// don't always get downloaded by qbittorrent. The best way to verify if it was added
/// to the client is to check the torrent list after the request.
urls: Vec<String>, // NOTE: Separated by new lines
/// Binary data of the torrents that are being added.
/// Torrent file data that is being added. (Name, Bytes)
torrents: Vec<(String, Vec<u8>)>,
/// Download folder
save_path: Option<String>, // NOTE: Rename to `savepath` for (de)serialization
/// Cookie sent to download the .torrent file
cookie: Option<String>,
/// Category for the torrent
category: Option<String>,
/// Tags for the torrent
tags: Option<Vec<String>>, // NOTE: Split by commas
/// Skip hash checking.
skip_hash_check: Option<bool>, // NOTE: Convert to string and rename to `skip_hash_check` for (de)serialization
/// Add torrents in the paused state.
paused: Option<bool>,
/// Create the root folder.
root_folder: Option<bool>, // NOTE: Convert to string for (de)serialization
/// Rename torrent
rename: Option<String>,
/// Set torrent upload speed limit. Unit in bytes/second
upload_limit: Option<i64>, // NOTE: Rename to `upLimit` for (de)serialization
/// Set torrent download speed limit. Unit in bytes/second
download_limit: Option<i64>, // NOTE: Rename to `upLimit` for (de)serialization
/// Set torrent share ratio limit
ratio_limit: Option<f32>, // NOTE: Rename to `ratioLimit` for (de)serialization
/// Set torrent seeding time limit. Unit in seconds
seeding_time_limit: Option<u64>, // NOTE: Rename to `seedingTimeLimit` for (de)serialization
/// Whether Automatic Torrent Management should be used
auto_tmm: Option<bool>, // NOTE: Rename to `autoTMM` for (de)serialization
/// Enable sequential download. Possible values are true, false (default)
sequential_download: Option<bool>, // NOTE: Rename to `sequentialDownload` and convert to string for (de)serialization
/// Prioritize download first last piece. Possible values are true, false (default)
first_last_piece_prio: Option<bool>, // NOTE: Rename to `firstLastPiecePrio` and convert to string for (de)serialization
}
#[derive(Debug, Default)]
pub struct TorrentUploadBuilder {
params: TorrentUpload,
}
impl TorrentUploadBuilder {
pub fn url(&mut self, url: String) -> &mut Self {
self.params.urls.push(url);
self
}
pub fn torrent_file(&mut self, torrent_path: String) -> &mut Self {
let path = std::path::Path::new(&torrent_path);
self.torrent_path(path)
}
pub fn torrent_path(&mut self, torrent_path: &std::path::Path) -> &mut Self {
let torrents = &mut self.params.torrents;
torrents.push((
torrent_path.file_name().unwrap().to_str().unwrap().to_string(),
std::fs::read(torrent_path).unwrap(),
));
self
}
pub fn torrent_data(&mut self, filename: String, data: Vec<u8>) -> &mut Self {
let torrents = &mut self.params.torrents;
torrents.push((
filename,
data,
));
self
}
pub fn save_path(&mut self, save_path: String) -> &mut Self {
self.params.save_path = Some(save_path);
self
}
pub fn cookie(&mut self, cookie: String) -> &mut Self {
self.params.cookie = Some(cookie);
self
}
pub fn category(&mut self, category: String) -> &mut Self {
self.params.category = Some(category);
self
}
pub fn tag(&mut self, tag: String) -> &mut Self {
self.params.tags.as_mut().unwrap_or(&mut vec![]).push(tag);
self
}
pub fn tags(&mut self, tags: Vec<String>) -> &mut Self {
self.params.tags = Some(tags);
self
}
pub fn skip_hash_check(&mut self, skip_hash_check: bool) -> &mut Self {
self.params.skip_hash_check = Some(skip_hash_check);
self
}
pub fn paused(&mut self, paused: bool) -> &mut Self {
self.params.paused = Some(paused);
self
}
pub fn root_folder(&mut self, root_folder: bool) -> &mut Self {
self.params.root_folder = Some(root_folder);
self
}
pub fn rename(&mut self, rename: String) -> &mut Self {
self.params.rename = Some(rename);
self
}
pub fn upload_limit(&mut self, upload_limit: i64) -> &mut Self {
self.params.upload_limit = Some(upload_limit);
self
}
pub fn download_limit(&mut self, download_limit: i64) -> &mut Self {
self.params.download_limit = Some(download_limit);
self
}
pub fn ratio_limit(&mut self, ratio_limit: f32) -> &mut Self {
self.params.ratio_limit = Some(ratio_limit);
self
}
pub fn seeding_time_limit(&mut self, seeding_time_limit: u64) -> &mut Self {
self.params.seeding_time_limit = Some(seeding_time_limit);
self
}
pub fn auto_tmm(&mut self, auto_tmm: bool) -> &mut Self {
self.params.auto_tmm = Some(auto_tmm);
self
}
pub fn sequential_download(&mut self, sequential_download: bool) -> &mut Self {
self.params.sequential_download = Some(sequential_download);
self
}
pub fn first_last_piece_prio(&mut self, first_last_piece_prio: bool) -> &mut Self {
self.params.first_last_piece_prio = Some(first_last_piece_prio);
self
}
pub fn build(&self) -> &TorrentUpload {
&self.params
}
}
impl TorrentUpload {
/// Get a builder of `TorrentUpload`
pub fn builder() -> TorrentUploadBuilder {
TorrentUploadBuilder::default()
}
// TODO: Add result for when neither `urls` and `torrents` are not set. For now it just panics.
pub fn to_multipart_form(&self) -> reqwest::multipart::Form {
if self.urls.is_empty() && self.torrents.is_empty() {
panic!("Either `urls` or `torrents` must be set!!");
}
let mut form = reqwest::multipart::Form::new();
// Add urls separated by new lines
if !self.urls.is_empty() {
let urls = self.urls.join("\n");
form = form.text("urls", urls); // For some reason I have to do this :(
}
// Add the torrents as files
if !self.torrents.is_empty() {
for torrent in self.torrents.iter() {
// TODO: Avoid a clone here?
form = form.part("torrents", reqwest::multipart::Part::bytes(torrent.1.clone())
.file_name(torrent.0.clone())
.mime_str("application/x-bittorrent").unwrap());
}
}
if let Some(save_path) = &self.save_path {
form = form.text("savepath", save_path.to_owned());
}
if let Some(cookie) = &self.cookie {
form = form.text("cookie", cookie.to_owned());
}
if let Some(category) = &self.category {
form = form.text("category", category.to_owned());
}
if let Some(tags) = &self.tags {
let tags = tags.join(",");
form = form.text("tags", tags);
}
if let Some(skip_hash_check) = &self.skip_hash_check {
form = form.text("skip_checking", skip_hash_check.to_string());
}
if let Some(paused) = &self.paused {
form = form.text("paused", paused.to_string());
}
if let Some(root_folder) = &self.root_folder {
form = form.text("root_folder", root_folder.to_string());
}
if let Some(rename) = &self.rename {
form = form.text("rename", rename.to_owned());
}
if let Some(upload_limit) = &self.upload_limit {
form = form.text("upLimit", upload_limit.to_string());
}
if let Some(download_limit) = &self.download_limit {
form = form.text("dlLimit", download_limit.to_string());
}
if let Some(ratio_limit) = &self.ratio_limit {
form = form.text("ratioLimit", ratio_limit.to_string());
}
if let Some(seeding_time_limit) = &self.seeding_time_limit {
form = form.text("seedingTimeLimit", seeding_time_limit.to_string());
}
if let Some(auto_tmm) = &self.auto_tmm {
form = form.text("autoTMM", auto_tmm.to_string());
}
if let Some(sequential_download) = &self.sequential_download {
form = form.text("sequentialDownload", sequential_download.to_string());
}
if let Some(first_last_piece_prio) = &self.first_last_piece_prio {
form = form.text("firstLastPiecePrio", first_last_piece_prio.to_string());
}
form
}
}