qb_api/
api.rs

1use crate::data::{AlternateLimits, BuildInfo, Category, GlobalTransferInfo, Log, Torrent};
2use crate::error::{Error, Result};
3use crate::queries::{AddTorrent, LogRequest};
4use log::*;
5use reqwest::{
6    header::{HeaderMap, SET_COOKIE},
7    Response,
8};
9use serde::{de::DeserializeOwned, Serialize};
10use std::collections::{HashMap, HashSet};
11use url::Url;
12
13/// Main handle and access point to working with qbittorrent.
14///
15/// Full documentation on provided methods is available
16/// [here](https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1))
17#[derive(Debug)]
18pub struct Api {
19    pub(crate) url: Url,
20    pub(crate) headers: HeaderMap,
21    pub(crate) client: reqwest::Client,
22}
23
24impl Api {
25    async fn new(url: &str, form: &HashMap<&str, &str>) -> Result<Self> {
26        let client = reqwest::Client::new();
27
28        let mut url: Url = Url::parse(url)?;
29        url.set_fragment(None);
30        url.set_query(None);
31        url.set_path("");
32
33        let mut headers = HeaderMap::new();
34        headers.insert("referer", url.as_str().parse()?);
35
36        let mut api = Self {
37            url,
38            headers,
39            client,
40        };
41
42        let response = api.post("/api/v2/auth/login", form).await?;
43
44        for cookie in response.headers().get_all(SET_COOKIE) {
45            let cookie = cookie.to_str()?;
46            if cookie.starts_with("SID=") {
47                let sid_cookie = cookie.split(";").next().unwrap();
48                api.headers.insert("cookie", sid_cookie.parse()?);
49                debug!("{:?}", api);
50                return Ok(api);
51            }
52        }
53
54        Err(Error::MissingCookie)
55    }
56
57    pub async fn auth(url: &str, username: &str, password: &str) -> Result<Self> {
58        let mut form = HashMap::new();
59        form.insert("username", username);
60        form.insert("password", password);
61        Self::new(url, &form).await
62    }
63
64    pub async fn local(url: &str) -> Result<Self> {
65        let form = HashMap::new();
66        Self::new(url, &form).await
67    }
68
69    //
70    // Internal post request functions and utils.
71    //
72
73    pub(crate) async fn post<F: Serialize + ?Sized>(
74        &self,
75        path: &str,
76        form: &F,
77    ) -> Result<Response> {
78        let mut url = self.url.clone();
79        url.set_path(path);
80        let request = self
81            .client
82            .post(url)
83            .headers(self.headers.clone())
84            .form(form);
85        debug!("POST -> {:?} {:?}", path, request);
86        let response = request.send().await?;
87        debug!("POST <- {:?} {:?}", path, response);
88        Ok(response)
89    }
90
91    pub(crate) async fn post_status<F: Serialize + ?Sized>(
92        &self,
93        path: &str,
94        form: &F,
95    ) -> Result<()> {
96        let response = self.post(path, form).await?;
97        match response.error_for_status() {
98            Ok(_) => Ok(()),
99            Err(e) => Err(Error::from(e)),
100        }
101    }
102
103    pub(crate) async fn post_decode<F: Serialize + ?Sized, T: DeserializeOwned>(
104        &self,
105        path: &str,
106        form: &F,
107    ) -> Result<T> {
108        let response = self.post(path, form).await?;
109        let data = response.bytes().await?;
110        debug!("POST <- {:?} DATA {:?}", path, std::str::from_utf8(&data));
111        let ret = serde_json::from_slice(&data)?;
112        Ok(ret)
113    }
114
115    pub(crate) async fn post_text<F: Serialize + ?Sized>(
116        &self,
117        path: &str,
118        form: &F,
119    ) -> Result<String> {
120        let response = self.post(path, form).await?;
121        let text = response.text().await?;
122        debug!("POST <- {:?} TEXT {:?}", path, text);
123        Ok(text)
124    }
125
126    //
127    // Application info / control
128    //
129
130    pub async fn get_app_version(&self) -> Result<String> {
131        self.post_text("/api/v2/app/version", &()).await
132    }
133
134    pub async fn get_api_version(&self) -> Result<String> {
135        self.post_text("/api/v2/app/webapiVersion", &()).await
136    }
137
138    pub async fn get_build_info(&self) -> Result<BuildInfo> {
139        self.post_decode("/api/v2/app/buildInfo", &()).await
140    }
141
142    pub async fn get_default_save_path(&self) -> Result<String> {
143        self.post_text("/api/v2/app/defaultSavePath", &()).await
144    }
145
146    pub async fn get_main_logs(&self, logs: &LogRequest) -> Result<Vec<Log>> {
147        self.post_decode("/api/v2/log/main", &logs).await
148    }
149
150    pub async fn shutdown(&self) -> Result<()> {
151        self.post_status("/api/v2/app/shutdown", &()).await
152    }
153
154    //
155    // Speed info / limits / control
156    //
157
158    pub async fn get_global_transfer_info(&self) -> Result<GlobalTransferInfo> {
159        self.post_decode("/api/v2/transfer/info", &()).await
160    }
161
162    pub async fn get_alt_speed_limits_state(&self) -> Result<AlternateLimits> {
163        let text = self
164            .post_text("/api/v2/transfer/speedLimitsMode", &())
165            .await?;
166        match text.as_str() {
167            "0" => Ok(AlternateLimits::Disabled),
168            "1" => Ok(AlternateLimits::Enabled),
169            _ => Err(Error::BadResponse),
170        }
171    }
172
173    pub async fn toggle_alt_speed_limits(&self) -> Result<()> {
174        self.post_status("/api/v2/transfer/toggleSpeedLimitsMode", &())
175            .await
176    }
177
178    //
179    // Torrents
180    //
181
182    pub async fn get_torrents(&self) -> Result<Vec<Torrent>> {
183        self.post_decode("/api/v2/torrents/info", &()).await
184    }
185
186    pub async fn add_torrent(&self, torrent: &AddTorrent) -> Result<()> {
187        self.post_status("/api/v2/torrents/add", &torrent).await
188    }
189
190    //
191    // Categories
192    //
193
194    pub async fn get_categories(&self) -> Result<HashMap<String, Category>> {
195        self.post_decode("/api/v2/torrents/categories", &()).await
196    }
197
198    pub async fn add_category(&self, name: &str, path: &str) -> Result<()> {
199        let mut form: HashMap<&str, &str> = HashMap::new();
200        form.insert("category", name);
201        form.insert("savePath", path);
202        self.post_status("/api/v2/torrents/createCategory", &form)
203            .await
204    }
205
206    pub async fn edit_category(&self, name: &str, path: &str) -> Result<()> {
207        let mut form: HashMap<&str, &str> = HashMap::new();
208        form.insert("category", name);
209        form.insert("savePath", path);
210        self.post_status("/api/v2/torrents/editCategory", &form)
211            .await
212    }
213
214    pub async fn remove_category(&self, name: &str) -> Result<()> {
215        let mut form: HashMap<&str, &str> = HashMap::new();
216        form.insert("categories", name);
217        self.post_status("/api/v2/torrents/removeCategories", &form)
218            .await
219    }
220
221    //
222    // Tags
223    //
224
225    pub async fn get_tags(&self) -> Result<HashSet<String>> {
226        self.post_decode("/api/v2/torrents/tags", &()).await
227    }
228
229    fn join_tags<T>(tags: T) -> String
230    where
231        T: IntoIterator,
232        T::Item: AsRef<str>,
233    {
234        let mut ret = String::new();
235        for (i, tag) in tags.into_iter().enumerate() {
236            if i > 0 {
237                ret.push(',');
238            }
239            ret.push_str(tag.as_ref());
240        }
241        ret
242    }
243
244    pub async fn create_tags<T>(&self, tags: T) -> Result<HashSet<String>>
245    where
246        T: IntoIterator,
247        T::Item: AsRef<str>,
248    {
249        let mut form: HashMap<&str, String> = HashMap::new();
250        form.insert("tags", Self::join_tags(tags));
251        self.post_decode("/api/v2/torrents/createTags", &form).await
252    }
253
254    pub async fn delete_tags<T>(&self, tags: T) -> Result<HashSet<String>>
255    where
256        T: IntoIterator,
257        T::Item: AsRef<str>,
258    {
259        let mut form: HashMap<&str, String> = HashMap::new();
260        form.insert("tags", Self::join_tags(tags));
261        self.post_decode("/api/v2/torrents/deleteTags", &form).await
262    }
263}