Implement login, get torrent list and getting a torrent trackers
This commit is contained in:
parent
0b8c537903
commit
5f9b53b175
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
.vscode
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "qbittorrent"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0.137", features = ["derive"] }
|
||||
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"] }
|
|
@ -0,0 +1,103 @@
|
|||
use crate::{error::ClientError, TorrentInfo, TorrentTracker};
|
||||
|
||||
pub struct ConnectionInfo {
|
||||
pub url: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
pub type ClientResult<T> = Result<T, ClientError>;
|
||||
|
||||
pub struct QBittorrentClient {
|
||||
client: reqwest::Client,
|
||||
connection_info: Option<ConnectionInfo>,
|
||||
auth_string: Option<String>,
|
||||
}
|
||||
|
||||
impl QBittorrentClient {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
client: reqwest::Client::new(),
|
||||
connection_info: None,
|
||||
auth_string: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Login to qBittorrent. This must be ran so that the client can make requests.
|
||||
pub async fn login(&mut self, url: String, username: String, password: String) -> ClientResult<()> {
|
||||
// Send response to get auth string
|
||||
let resp = self.client.post(format!("{}/api/v2/auth/login", url.clone()))
|
||||
.form(&[
|
||||
("username", username.clone()),
|
||||
("password", password.clone()),
|
||||
])
|
||||
.send().await?.error_for_status()?;
|
||||
|
||||
let headers = resp.headers().clone();
|
||||
let content = resp.text().await?;
|
||||
|
||||
if content == "Ok." {
|
||||
// Extract cookies
|
||||
let cookies: Vec<_> = headers.get(reqwest::header::SET_COOKIE)
|
||||
.unwrap().to_str().unwrap().split(';').collect();
|
||||
|
||||
// Extract auth string and store it.
|
||||
let auth_string = cookies.iter().find(|c| c.starts_with("SID=")).unwrap();
|
||||
self.auth_string = Some(auth_string.to_string());
|
||||
|
||||
// Store connection info
|
||||
self.connection_info = Some(ConnectionInfo {
|
||||
url: url.clone(),
|
||||
username: username.clone(),
|
||||
password: password.clone(),
|
||||
});
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ClientError::Authorization)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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())
|
||||
.send().await?.error_for_status()?;
|
||||
|
||||
// Deserialize response
|
||||
let content = resp.text().await?;
|
||||
let torrents: Vec<TorrentInfo> = serde_json::from_str(&content)?;
|
||||
|
||||
Ok(torrents)
|
||||
} else {
|
||||
Err(ClientError::Authorization)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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())
|
||||
.form(&[
|
||||
("hash", torrent.hash.clone()),
|
||||
])
|
||||
.send().await?.error_for_status()?;
|
||||
|
||||
// Deserialize response
|
||||
let content = resp.text().await?;
|
||||
let trackers: Vec<TorrentTracker> = serde_json::from_str(&content)?;
|
||||
|
||||
Ok(trackers)
|
||||
} else {
|
||||
Err(ClientError::Authorization)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
#[derive(Debug)]
|
||||
pub enum ClientError {
|
||||
/// Http error
|
||||
Http(reqwest::Error),
|
||||
|
||||
/// Authorization error
|
||||
Authorization,
|
||||
|
||||
/// Json parsing error
|
||||
Json(serde_json::Error),
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for ClientError {
|
||||
fn from(err: reqwest::Error) -> Self {
|
||||
ClientError::Http(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for ClientError {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
ClientError::Json(err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
pub mod torrent;
|
||||
pub use torrent::*;
|
||||
|
||||
pub mod client;
|
||||
pub use client::*;
|
||||
|
||||
pub mod error;
|
||||
pub use error::*;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let mut client = QBittorrentClient::new();
|
||||
|
||||
client.login(
|
||||
String::from("http://localhost:8080"),
|
||||
String::from("admin"),
|
||||
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!");
|
||||
}
|
|
@ -0,0 +1,274 @@
|
|||
use serde::{Serialize, Deserialize};
|
||||
use serde_repr::*;
|
||||
use serde_with::{CommaSeparator};
|
||||
|
||||
/// A torrent's information from the qbittorrent client.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct TorrentInfo {
|
||||
/// Time (Unix Epoch) when the torrent was added to the client
|
||||
pub added_on: u64,
|
||||
|
||||
/// Amount of data left to download (bytes)
|
||||
pub amount_left: u64,
|
||||
|
||||
/// Whether this torrent is managed by Automatic Torrent Management
|
||||
pub auto_tmm: bool,
|
||||
|
||||
/// Percentage of file pieces currently available
|
||||
pub availability: f32,
|
||||
|
||||
/// Category of the torrent
|
||||
pub category: String,
|
||||
|
||||
/// Amount of transfer data completed (bytes)
|
||||
pub completed: u64,
|
||||
|
||||
/// Time (Unix Epoch) when the torrent completed
|
||||
pub completion_on: u64,
|
||||
|
||||
/// Absolute path of torrent content (root path for multi-file torrents, absolute file path for single-file torrents)
|
||||
pub content_path: String,
|
||||
|
||||
/// Torrent download speed limit (bytes/s). -1 if unlimited.
|
||||
pub dl_limit: i64,
|
||||
|
||||
/// Torrent download speed (bytes/s)
|
||||
pub dlspeed: u64,
|
||||
|
||||
/// Amount of data downloaded
|
||||
pub downloaded: u64,
|
||||
|
||||
/// Amount of data downloaded this session
|
||||
pub downloaded_session: u64,
|
||||
|
||||
/// Torrent ETA (seconds)
|
||||
pub eta: u64,
|
||||
|
||||
/// True if first last piece are prioritized
|
||||
pub f_l_piece_prio: bool,
|
||||
|
||||
/// True if force start is enabled for this torrent
|
||||
pub force_start: bool,
|
||||
|
||||
/// Torrent hash
|
||||
pub hash: String,
|
||||
|
||||
/// Last time (Unix Epoch) when a chunk was downloaded/uploaded
|
||||
pub last_activity: u64,
|
||||
|
||||
/// Magnet URI corresponding to this torrent
|
||||
pub magnet_uri: String,
|
||||
|
||||
/// Maximum share ratio until torrent is stopped from seeding/uploading
|
||||
pub max_ratio: f32,
|
||||
|
||||
/// Maximum seeding time (seconds) until torrent is stopped from seeding
|
||||
pub max_seeding_time: i32,
|
||||
|
||||
/// Torrent name
|
||||
pub name: String,
|
||||
|
||||
/// Number of seeds in the swarm
|
||||
pub num_complete: i32,
|
||||
|
||||
/// Number of leechers in the swarm
|
||||
pub num_incomplete: i32,
|
||||
|
||||
/// Number of leechers connected to
|
||||
pub num_leechs: i32,
|
||||
|
||||
/// Number of seeds connected to
|
||||
pub num_seeds: i32,
|
||||
|
||||
/// Torrent priority. Returns -1 if queuing is disabled or torrent is in seed mode
|
||||
pub priority: i32,
|
||||
|
||||
/// Torrent progress (percentage/100)
|
||||
pub progress: f32,
|
||||
|
||||
/// Torrent share ratio. Max ratio value: 9999.
|
||||
pub ratio: f32,
|
||||
|
||||
pub ratio_limit: f32,
|
||||
|
||||
/// Path where this torrent's data is stored
|
||||
pub save_path: String,
|
||||
|
||||
/// Torrent elapsed time while complete (seconds)
|
||||
pub seeding_time: i32,
|
||||
|
||||
/// per torrent setting, when Automatic Torrent Management is disabled,
|
||||
/// furthermore then max_seeding_time is set to seeding_time_limit for this
|
||||
/// torrent. If Automatic Torrent Management is enabled, the value is -2. And if
|
||||
/// max_seeding_time is unset it have a default value -1.
|
||||
pub seeding_time_limit: i32,
|
||||
|
||||
/// Time (Unix Epoch) when this torrent was last seen complete
|
||||
pub seen_complete: i32,
|
||||
|
||||
/// True if sequential download is enabled
|
||||
pub seq_dl: bool,
|
||||
|
||||
/// Total size (bytes) of files selected for download
|
||||
pub size: u64,
|
||||
|
||||
/// Torrent state. See table here below for the possible values
|
||||
pub state: TorrentState,
|
||||
|
||||
/// True if super seeding is enabled
|
||||
pub super_seeding: bool,
|
||||
|
||||
/// Tag list of the torrent
|
||||
#[serde(with = "serde_with::rust::StringWithSeparator::<CommaSeparator>")]
|
||||
pub tags: Vec<String>,
|
||||
|
||||
/// Total active time (seconds)
|
||||
pub time_active: i32,
|
||||
|
||||
/// Total size (bytes) of all file in this torrent (including unselected ones)
|
||||
pub total_size: u64,
|
||||
|
||||
/// The first tracker with working status. Returns empty string if no tracker is working.
|
||||
pub tracker: String,
|
||||
|
||||
/// Torrent upload speed limit (bytes/s). -1 if unlimited.
|
||||
pub up_limit: i64,
|
||||
|
||||
/// Amount of data uploaded
|
||||
pub uploaded: u64,
|
||||
|
||||
/// Amount of data uploaded this session
|
||||
pub uploaded_session: u64,
|
||||
|
||||
/// Torrent upload speed (bytes/s)
|
||||
pub upspeed: u64,
|
||||
}
|
||||
|
||||
/// An enum representing the state of a torrent in the client.
|
||||
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||
pub enum TorrentState {
|
||||
/// Some error occurred, applies to paused torrents
|
||||
#[serde(rename = "error")]
|
||||
Error,
|
||||
|
||||
/// Torrent data files is missing
|
||||
#[serde(rename = "missingFiles")]
|
||||
MissingFiles,
|
||||
|
||||
/// Torrent is being seeded and data is being transferred
|
||||
#[serde(rename = "uploading")]
|
||||
Uploading,
|
||||
|
||||
/// Torrent is paused and has finished downloading
|
||||
#[serde(rename = "pausedUP")]
|
||||
PausedUP,
|
||||
|
||||
/// Queuing is enabled and torrent is queued for upload
|
||||
#[serde(rename = "queuedUP")]
|
||||
QueuedUP,
|
||||
|
||||
/// Torrent is being seeded, but no connection were made
|
||||
#[serde(rename = "stalledUP")]
|
||||
StalledUP,
|
||||
|
||||
/// Torrent has finished downloading and is being checked
|
||||
#[serde(rename = "checkingUP")]
|
||||
CheckingUP,
|
||||
|
||||
/// Torrent is forced to uploading and ignore queue limit
|
||||
#[serde(rename = "forcedUP")]
|
||||
ForcedUP,
|
||||
|
||||
/// Torrent is allocating disk space for download
|
||||
#[serde(rename = "allocating")]
|
||||
Allocating,
|
||||
|
||||
/// Torrent is being downloaded and data is being transferred
|
||||
#[serde(rename = "downloading")]
|
||||
Downloading,
|
||||
|
||||
/// Torrent has just started downloading and is fetching metadata
|
||||
#[serde(rename = "metaDL")]
|
||||
MetaDownloading,
|
||||
|
||||
/// Torrent is paused and has NOT finished downloading
|
||||
#[serde(rename = "pausedDL")]
|
||||
PausedDL,
|
||||
|
||||
/// Queuing is enabled and torrent is queued for download
|
||||
#[serde(rename = "queuedDL")]
|
||||
QueuedDL,
|
||||
|
||||
/// Torrent is being downloaded, but no connection were made
|
||||
#[serde(rename = "stalledDL")]
|
||||
StalledDL,
|
||||
|
||||
/// Same as checkingUP, but torrent has NOT finished downloading
|
||||
#[serde(rename = "checkingDL")]
|
||||
CheckingDL,
|
||||
|
||||
/// Torrent is forced to downloading to ignore queue limit
|
||||
#[serde(rename = "forcedDL")]
|
||||
ForcedDL,
|
||||
|
||||
/// Checking resume data on qBt startup
|
||||
#[serde(rename = "checkingResumeData")]
|
||||
CheckingResumeData,
|
||||
|
||||
/// Torrent is moving to another location
|
||||
#[serde(rename = "moving")]
|
||||
Moving,
|
||||
|
||||
/// Unknown status
|
||||
#[serde(rename = "unknown")]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct TorrentTracker {
|
||||
/// Tracker URL
|
||||
pub url: String,
|
||||
|
||||
/// Tracker status. See the table below for possible values
|
||||
pub status: TrackerStatus,
|
||||
|
||||
/// Tracker priority tier. Lower tier trackers are tried before higher
|
||||
/// tiers. Tier numbers are valid when >= 0, < 0 is used as placeholder
|
||||
/// when tier does not exist for special entries (such as DHT).
|
||||
pub tier: i32,
|
||||
|
||||
/// Number of peers for current torrent, as reported by the tracker
|
||||
pub num_peers: i32,
|
||||
|
||||
/// Number of seeds for current torrent, as reported by the tracker
|
||||
pub num_seeds: i32,
|
||||
|
||||
/// Number of leeches for current torrent, as reported by the tracker
|
||||
pub num_leeches: i32,
|
||||
|
||||
/// Number of completed downloads for current torrent, as reported by the tracker
|
||||
pub num_downloaded: i32,
|
||||
|
||||
/// Tracker message (there is no way of knowing what this message is - it's up to tracker admins)
|
||||
#[serde(rename = "msg")]
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize_repr, Deserialize_repr, PartialEq, Debug)]
|
||||
#[repr(u8)]
|
||||
pub enum TrackerStatus {
|
||||
/// Tracker is disabled (used for DHT, PeX, and LSD)
|
||||
Disabled = 0,
|
||||
|
||||
/// Tracker has not been contacted yet
|
||||
NotContacted = 1,
|
||||
|
||||
/// Tracker has been contacted and is working
|
||||
Working = 2,
|
||||
|
||||
/// Tracker is updating
|
||||
Updating = 3,
|
||||
|
||||
/// Tracker has been contacted, but it is not working (or doesn't send proper replies)
|
||||
NotWorking = 4
|
||||
}
|
Loading…
Reference in New Issue