transmission_client/
client.rs

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        // Update session id
316        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        // Check html status code
323        match response.status() {
324            // Invalid session id header, resend the request
325            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            // Authentication needed
331            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}