Implement login, get torrent list and getting a torrent trackers

This commit is contained in:
SeanOMik 2022-06-19 01:05:08 -04:00
parent 0b8c537903
commit 5f9b53b175
7 changed files with 1651 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
.vscode

1207
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

15
Cargo.toml Normal file
View File

@ -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"] }

103
src/client.rs Normal file
View File

@ -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)
}
}
}

23
src/error.rs Normal file
View File

@ -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)
}
}

27
src/main.rs Normal file
View File

@ -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!");
}

274
src/torrent.rs Normal file
View File

@ -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
}