1use std::cell::RefCell;
2use std::rc::Rc;
3use std::time::Duration;
4
5use reqwest::header::{self, HeaderMap};
6use reqwest::{RequestBuilder, StatusCode};
7use serde::de::DeserializeOwned;
8use url::Url;
9
10use crate::error::ClientError;
11use crate::rpc::{
12 RequestArgs, RpcRequest, RpcResponse, RpcResponseArguments, SessionSetArgs, TorrentActionArgs,
13 TorrentAddArgs, TorrentGetArgs, TorrentRemoveArgs, TorrentSetArgs, TorrentSetLocationArgs,
14};
15use crate::{
16 Authentication, PortTest, Session, SessionMutator, SessionStats, Torrent, TorrentAdded,
17 TorrentFiles, TorrentFilesList, TorrentList, TorrentMutator, TorrentPeers, TorrentPeersList,
18 TorrentTrackers, TorrentTrackersList, utils,
19};
20
21#[derive(Debug, Clone)]
22pub struct Client {
23 address: Url,
24 authentication: Rc<RefCell<Option<Authentication>>>,
25 http_client: reqwest::Client,
26 session_id: Rc<RefCell<String>>,
27}
28
29impl Client {
30 pub fn new(address: Url) -> Self {
31 Client {
32 address,
33 ..Default::default()
34 }
35 }
36
37 pub fn set_authentication(&self, auth: Option<Authentication>) {
38 *self.authentication.borrow_mut() = auth;
39 }
40
41 pub async fn torrents(&self, ids: Option<Vec<i32>>) -> Result<Vec<Torrent>, ClientError> {
42 let args = TorrentGetArgs {
43 fields: utils::torrent_fields(),
44 ids,
45 };
46 let request_args = Some(RequestArgs::TorrentGet(args));
47
48 let response: RpcResponse<TorrentList> =
49 self.send_request("torrent-get", request_args).await?;
50 Ok(response.arguments.unwrap().torrents)
51 }
52
53 pub async fn torrents_files(
54 &self,
55 ids: Option<Vec<i32>>,
56 ) -> Result<Vec<TorrentFiles>, ClientError> {
57 let args = TorrentGetArgs {
58 fields: utils::torrent_files_fields(),
59 ids,
60 };
61 let request_args = Some(RequestArgs::TorrentGet(args));
62
63 let response: RpcResponse<TorrentFilesList> =
64 self.send_request("torrent-get", request_args).await?;
65 Ok(response.arguments.unwrap().torrents)
66 }
67
68 pub async fn torrents_peers(
69 &self,
70 ids: Option<Vec<i32>>,
71 ) -> Result<Vec<TorrentPeers>, ClientError> {
72 let args = TorrentGetArgs {
73 fields: utils::torrent_peers_fields(),
74 ids,
75 };
76 let request_args = Some(RequestArgs::TorrentGet(args));
77
78 let response: RpcResponse<TorrentPeersList> =
79 self.send_request("torrent-get", request_args).await?;
80 Ok(response.arguments.unwrap().torrents)
81 }
82
83 pub async fn torrents_trackers(
84 &self,
85 ids: Option<Vec<i32>>,
86 ) -> Result<Vec<TorrentTrackers>, ClientError> {
87 let args = TorrentGetArgs {
88 fields: utils::torrent_trackers_fields(),
89 ids,
90 };
91 let request_args = Some(RequestArgs::TorrentGet(args));
92
93 let response: RpcResponse<TorrentTrackersList> =
94 self.send_request("torrent-get", request_args).await?;
95 Ok(response.arguments.unwrap().torrents)
96 }
97
98 pub async fn torrent_set(
99 &self,
100 ids: Option<Vec<String>>,
101 mutator: TorrentMutator,
102 ) -> Result<(), ClientError> {
103 let args = TorrentSetArgs { ids, mutator };
104 let request_args = Some(RequestArgs::TorrentSet(args));
105
106 let _: RpcResponse<String> = self.send_request("torrent-set", request_args).await?;
107 Ok(())
108 }
109
110 pub async fn torrent_add_filename(
111 &self,
112 filename: &str,
113 ) -> Result<Option<Torrent>, ClientError> {
114 let args = TorrentAddArgs {
115 filename: Some(filename.into()),
116 ..Default::default()
117 };
118 let request_args = Some(RequestArgs::TorrentAdd(args));
119 self.torrent_add(request_args).await
120 }
121
122 pub async fn torrent_add_metainfo(
123 &self,
124 metainfo: &str,
125 ) -> Result<Option<Torrent>, ClientError> {
126 let args = TorrentAddArgs {
127 metainfo: Some(metainfo.into()),
128 ..Default::default()
129 };
130 let request_args = Some(RequestArgs::TorrentAdd(args));
131 self.torrent_add(request_args).await
132 }
133
134 async fn torrent_add(
135 &self,
136 request_args: Option<RequestArgs>,
137 ) -> Result<Option<Torrent>, ClientError> {
138 let response: RpcResponse<TorrentAdded> =
139 self.send_request("torrent-add", request_args).await?;
140
141 let result_args = response.arguments.unwrap();
142 if result_args.torrent_added.is_some() {
143 Ok(result_args.torrent_added)
144 } else {
145 Ok(result_args.torrent_duplicate)
146 }
147 }
148
149 pub async fn torrent_remove(
150 &self,
151 ids: Option<Vec<String>>,
152 delete_local_data: bool,
153 ) -> Result<(), ClientError> {
154 let args = TorrentRemoveArgs {
155 ids,
156 delete_local_data,
157 };
158 let request_args = Some(RequestArgs::TorrentRemove(args));
159
160 let _: RpcResponse<String> = self.send_request("torrent-remove", request_args).await?;
161 Ok(())
162 }
163
164 pub async fn torrent_start(
165 &self,
166 ids: Option<Vec<String>>,
167 bypass_queue: bool,
168 ) -> Result<(), ClientError> {
169 let args = TorrentActionArgs { ids };
170 let request_args = Some(RequestArgs::TorrentAction(args));
171
172 let method_name = if bypass_queue {
173 "torrent-start-now"
174 } else {
175 "torrent-start"
176 };
177
178 let _: RpcResponse<String> = self.send_request(method_name, request_args).await?;
179 Ok(())
180 }
181
182 pub async fn torrent_stop(&self, ids: Option<Vec<String>>) -> Result<(), ClientError> {
183 self.send_torrent_action("torrent-stop", ids).await?;
184 Ok(())
185 }
186
187 pub async fn torrent_verify(&self, ids: Option<Vec<String>>) -> Result<(), ClientError> {
188 self.send_torrent_action("torrent-verify", ids).await?;
189 Ok(())
190 }
191
192 pub async fn torrent_reannounce(&self, ids: Option<Vec<String>>) -> Result<(), ClientError> {
193 self.send_torrent_action("torrent-reannounce", ids).await?;
194 Ok(())
195 }
196
197 pub async fn torrent_set_location(
198 &self,
199 ids: Option<Vec<String>>,
200 location: String,
201 move_data: bool,
202 ) -> Result<(), ClientError> {
203 let args = TorrentSetLocationArgs {
204 ids,
205 location,
206 move_data,
207 };
208 let request_args = Some(RequestArgs::TorrentSetLocation(args));
209
210 let _: RpcResponse<String> = self
211 .send_request("torrent-set-location", request_args)
212 .await?;
213 Ok(())
214 }
215
216 pub async fn queue_move_top(&self, ids: Option<Vec<String>>) -> Result<(), ClientError> {
217 self.send_torrent_action("queue-move-top", ids).await?;
218 Ok(())
219 }
220
221 pub async fn queue_move_up(&self, ids: Option<Vec<String>>) -> Result<(), ClientError> {
222 self.send_torrent_action("queue-move-up", ids).await?;
223 Ok(())
224 }
225
226 pub async fn queue_move_down(&self, ids: Option<Vec<String>>) -> Result<(), ClientError> {
227 self.send_torrent_action("queue-move-down", ids).await?;
228 Ok(())
229 }
230
231 pub async fn queue_move_bottom(&self, ids: Option<Vec<String>>) -> Result<(), ClientError> {
232 self.send_torrent_action("queue-move-bottom", ids).await?;
233 Ok(())
234 }
235
236 pub async fn session(&self) -> Result<Session, ClientError> {
237 let response: RpcResponse<Session> = self.send_request("session-get", None).await?;
238 Ok(response.arguments.unwrap())
239 }
240
241 pub async fn session_set(&self, mutator: SessionMutator) -> Result<(), ClientError> {
242 let args = SessionSetArgs { mutator };
243 let request_args = Some(RequestArgs::SessionSet(args));
244
245 let _: RpcResponse<String> = self.send_request("session-set", request_args).await?;
246 Ok(())
247 }
248
249 pub async fn session_stats(&self) -> Result<SessionStats, ClientError> {
250 let response: RpcResponse<SessionStats> = self.send_request("session-stats", None).await?;
251 Ok(response.arguments.unwrap())
252 }
253
254 pub async fn session_close(&self) -> Result<(), ClientError> {
255 let _: RpcResponse<String> = self.send_request("session-close", None).await?;
256 Ok(())
257 }
258
259 pub async fn port_test(&self) -> Result<bool, ClientError> {
260 let response: RpcResponse<PortTest> = self.send_request("port-test", None).await?;
261 Ok(response.arguments.unwrap().port_is_open)
262 }
263
264 async fn send_torrent_action(
265 &self,
266 action: &str,
267 ids: Option<Vec<String>>,
268 ) -> Result<(), ClientError> {
269 let args = TorrentActionArgs { ids };
270 let request_args = Some(RequestArgs::TorrentAction(args));
271
272 let _: RpcResponse<String> = self.send_request(action, request_args).await?;
273 Ok(())
274 }
275
276 async fn send_request<T: RpcResponseArguments + DeserializeOwned>(
277 &self,
278 method: &str,
279 arguments: Option<RequestArgs>,
280 ) -> Result<RpcResponse<T>, ClientError> {
281 let request = RpcRequest {
282 method: method.into(),
283 arguments,
284 };
285
286 let body = serde_json::to_string(&request)?;
287 let post_result = self.send_post(body).await?;
288
289 let de = &mut serde_json::Deserializer::from_str(&post_result);
290 let serde_result: Result<RpcResponse<T>, _> = serde_path_to_error::deserialize(de);
291
292 match serde_result {
293 Ok(response) => {
294 if response.result != "success" {
295 return Err(ClientError::TransmissionError(response.result));
296 }
297
298 Ok(response)
299 }
300 Err(err) => {
301 let path = err.path().to_string();
302 error!("Unable to parse json: {path} ({err})");
303 warn!("Path: {path}");
304 warn!("JSON: {post_result}");
305
306 Err(err.into_inner().into())
307 }
308 }
309 }
310
311 async fn send_post(&self, body: String) -> Result<String, ClientError> {
312 let request = self.http_request(body.clone())?;
313 let mut response = request.send().await?;
314
315 let headers = response.headers();
317 if let Some(session_id) = headers.get("X-Transmission-Session-Id") {
318 let session_id = session_id.to_str().unwrap().to_string();
319 *self.session_id.borrow_mut() = session_id;
320 }
321
322 match response.status() {
324 StatusCode::CONFLICT => {
326 debug!("Received status code 409, resend request.");
327 let request = self.http_request(body.clone())?;
328 response = request.send().await?;
329 }
330 StatusCode::UNAUTHORIZED => {
332 return Err(ClientError::TransmissionUnauthorized);
333 }
334 _ => (),
335 }
336
337 Ok(response.text().await.unwrap())
338 }
339
340 fn http_request(&self, body: String) -> Result<RequestBuilder, ClientError> {
341 let session_id = self.session_id.borrow().clone();
342
343 let request = if let Some(auth) = &*self.authentication.borrow() {
344 self.http_client
345 .post(self.address.clone())
346 .header("X-Transmission-Session-Id", session_id)
347 .basic_auth(&auth.username, Some(&auth.password))
348 .body(body)
349 } else {
350 self.http_client
351 .post(self.address.clone())
352 .header("X-Transmission-Session-Id", session_id)
353 .body(body)
354 };
355
356 Ok(request)
357 }
358}
359
360impl Default for Client {
361 fn default() -> Self {
362 let address = Url::parse("http://127.0.0.1:9091/transmission/rpc/").unwrap();
363
364 let mut headers = HeaderMap::new();
365 headers.insert(
366 "content-type",
367 header::HeaderValue::from_static("application/json"),
368 );
369
370 let http_client = reqwest::ClientBuilder::new()
371 .default_headers(headers)
372 .timeout(Duration::from_secs(15))
373 .build()
374 .unwrap();
375
376 let session_id = Rc::new(RefCell::new("0".into()));
377
378 Self {
379 address,
380 authentication: Rc::default(),
381 http_client,
382 session_id,
383 }
384 }
385}