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#[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 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 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 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 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 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 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}