Implement add torrent endpoint
This commit is contained in:
parent
5f9b53b175
commit
0d9e287fc9
|
@ -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"
|
||||
|
|
|
@ -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"] }
|
||||
tokio = { version = "1.19.2", features = ["full"] }
|
||||
reqwest = { version = "0.11.11", features = ["cookies", "multipart"] }
|
||||
tokio = { version = "1.19.2", features = ["full"] }
|
||||
|
||||
derive_builder = "0.11.2"
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
11
src/main.rs
11
src/main.rs
|
@ -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!");
|
||||
}
|
||||
|
|
284
src/torrent.rs
284
src/torrent.rs
|
@ -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,
|
||||
|
@ -271,4 +271,280 @@ 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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue