1use std::{borrow::Borrow, collections::HashMap, path::Path};
2
3use bytes::Bytes;
4use serde::Serialize;
5use serde_with::skip_serializing_none;
6use tap::Pipe;
7
8#[cfg(feature = "cyper")]
9use crate::client::PartExt;
10use crate::{
11 ApiError, Error, Qbit, Result,
12 client::{CheckError, Method, RequestBuilder, StatusCode, Url, multipart},
13 ext::*,
14 model::*,
15};
16
17impl Qbit {
18 pub async fn get_torrent_list(&self, arg: GetTorrentListArg) -> Result<Vec<Torrent>> {
20 self.get_with("torrents/info", &arg)
21 .await?
22 .json()
23 .await
24 .map_err(Into::into)
25 }
26
27 pub async fn export_torrent(&self, hash: impl AsRef<str> + Send + Sync) -> Result<Bytes> {
29 self.get_with("torrents/export", &HashArg::new(hash.as_ref()))
30 .await?
31 .bytes()
32 .await
33 .map_err(Into::into)
34 }
35
36 pub async fn download_torrent_file(
43 &self,
44 hash: impl AsRef<str> + Send + Sync,
45 file: impl AsRef<str> + Send + Sync,
46 ) -> Result<Bytes> {
47 #[derive(Serialize)]
48 struct Arg<'a> {
49 hash: &'a str,
50 file: &'a str,
51 }
52 self.post_with(
53 "torrents/downloadFile",
54 &Arg {
55 hash: hash.as_ref(),
56 file: file.as_ref(),
57 },
58 )
59 .await?
60 .map_status(|c| match c {
61 StatusCode::NOT_FOUND => Some(Error::ApiError(ApiError::TorrentNotFound)),
62 StatusCode::FORBIDDEN => Some(Error::ApiError(ApiError::NotLoggedIn)),
63 _ => None,
64 })?
65 .bytes()
66 .await
67 .map_err(Into::into)
68 }
69
70 pub async fn get_torrent_properties(
72 &self,
73 hash: impl AsRef<str> + Send + Sync,
74 ) -> Result<TorrentProperty> {
75 self.get_with("torrents/properties", &HashArg::new(hash.as_ref()))
76 .await
77 .and_then(|r| r.map_status(TORRENT_NOT_FOUND))?
78 .json()
79 .await
80 .map_err(Into::into)
81 }
82
83 pub async fn get_torrent_piece_availability(
89 &self,
90 hash: impl AsRef<str> + Send + Sync,
91 ) -> Result<Vec<i64>> {
92 self.get_with("torrents/pieceAvailability", &HashArg::new(hash.as_ref()))
93 .await
94 .and_then(|r| r.map_status(TORRENT_NOT_FOUND))?
95 .json()
96 .await
97 .map_err(Into::into)
98 }
99
100 pub async fn get_torrent_trackers(
102 &self,
103 hash: impl AsRef<str> + Send + Sync,
104 ) -> Result<Vec<Tracker>> {
105 self.get_with("torrents/trackers", &HashArg::new(hash.as_ref()))
106 .await
107 .and_then(|r| r.map_status(TORRENT_NOT_FOUND))?
108 .json()
109 .await
110 .map_err(Into::into)
111 }
112
113 pub async fn get_torrent_web_seeds(
115 &self,
116 hash: impl AsRef<str> + Send + Sync,
117 ) -> Result<Vec<WebSeed>> {
118 self.get_with("torrents/webseeds", &HashArg::new(hash.as_ref()))
119 .await
120 .and_then(|r| r.map_status(TORRENT_NOT_FOUND))?
121 .json()
122 .await
123 .map_err(Into::into)
124 }
125
126 pub async fn get_torrent_contents(
128 &self,
129 hash: impl AsRef<str> + Send + Sync,
130 indexes: impl Into<Option<Sep<String, '|'>>> + Send + Sync,
131 ) -> Result<Vec<TorrentContent>> {
132 #[derive(Serialize)]
133 struct Arg<'a> {
134 hash: &'a str,
135 #[serde(skip_serializing_if = "Option::is_none")]
136 indexes: Option<String>,
137 }
138
139 self.get_with(
140 "torrents/files",
141 &Arg {
142 hash: hash.as_ref(),
143 indexes: indexes.into().map(|s| s.to_string()),
144 },
145 )
146 .await
147 .and_then(|r| r.map_status(TORRENT_NOT_FOUND))?
148 .json()
149 .await
150 .map_err(Into::into)
151 }
152
153 pub async fn get_torrent_pieces_states(
155 &self,
156 hash: impl AsRef<str> + Send + Sync,
157 ) -> Result<Vec<PieceState>> {
158 self.get_with("torrents/pieceStates", &HashArg::new(hash.as_ref()))
159 .await
160 .and_then(|r| r.map_status(TORRENT_NOT_FOUND))?
161 .json()
162 .await
163 .map_err(Into::into)
164 }
165
166 pub async fn get_torrent_pieces_hashes(
168 &self,
169 hash: impl AsRef<str> + Send + Sync,
170 ) -> Result<Vec<String>> {
171 self.get_with("torrents/pieceHashes", &HashArg::new(hash.as_ref()))
172 .await
173 .and_then(|r| r.map_status(TORRENT_NOT_FOUND))?
174 .json()
175 .await
176 .map_err(Into::into)
177 }
178
179 pub async fn stop_torrents(&self, hashes: impl Into<Hashes> + Send + Sync) -> Result<()> {
181 self.post_with("torrents/stop", &HashesArg::new(hashes))
182 .await?
183 .end()
184 }
185
186 pub async fn start_torrents(&self, hashes: impl Into<Hashes> + Send + Sync) -> Result<()> {
188 self.post_with("torrents/start", &HashesArg::new(hashes))
189 .await?
190 .end()
191 }
192
193 pub async fn delete_torrents(
196 &self,
197 hashes: impl Into<Hashes> + Send + Sync,
198 delete_files: impl Into<Option<bool>> + Send + Sync,
199 ) -> Result<()> {
200 #[derive(Serialize)]
201 #[skip_serializing_none]
202 #[serde(rename_all = "camelCase")]
203 struct Arg {
204 hashes: Hashes,
205 delete_files: Option<bool>,
206 }
207 self.post_with(
208 "torrents/delete",
209 &Arg {
210 hashes: hashes.into(),
211 delete_files: delete_files.into(),
212 },
213 )
214 .await?
215 .end()
216 }
217
218 pub async fn recheck_torrents(&self, hashes: impl Into<Hashes> + Send + Sync) -> Result<()> {
220 self.post_with("torrents/recheck", &HashesArg::new(hashes))
221 .await?
222 .end()
223 }
224
225 pub async fn reannounce_torrents(&self, hashes: impl Into<Hashes> + Send + Sync) -> Result<()> {
227 self.post_with("torrents/reannounce", &HashesArg::new(hashes))
228 .await?
229 .end()
230 }
231
232 pub async fn reannounce_torrents_with_trackers(
237 &self,
238 hashes: impl Into<Hashes> + Send + Sync,
239 trackers: impl Into<Sep<String, '|'>> + Send + Sync,
240 ) -> Result<()> {
241 #[derive(Serialize)]
242 struct Arg {
243 hashes: String,
244 trackers: String,
245 }
246 self.post_with(
247 "torrents/reannounce",
248 &Arg {
249 hashes: hashes.into().to_string(),
250 trackers: trackers.into().to_string(),
251 },
252 )
253 .await?
254 .end()
255 }
256
257 pub async fn add_torrent(&self, arg: impl Borrow<AddTorrentArg> + Send + Sync) -> Result<()> {
259 use multipart::Form;
260
261 fn make_form(
262 arg: &AddTorrentArg,
263 torrents: &[TorrentFile],
264 ) -> Result<Form, serde_json::Error> {
265 let form = serde_json::to_value(arg)?
266 .as_object()
267 .unwrap()
268 .into_iter()
269 .fold(Form::new(), |form, (k, v)| {
270 let v = match v.as_str() {
271 Some(v_str) => v_str.to_string(),
272 None => v.to_string(),
273 };
274 form.text(k.to_string(), v.to_string())
275 });
276
277 torrents
278 .iter()
279 .fold(form, |mut form, torrent| {
280 let p = multipart::Part::bytes(torrent.data.clone())
281 .file_name(torrent.filename.to_string())
282 .mime_str("application/x-bittorrent")
283 .unwrap();
284 form = form.part("torrents", p);
285 form
286 })
287 .pipe(Ok)
288 }
289
290 let args: &AddTorrentArg = arg.borrow();
291
292 let owned;
297 let args = if args.paused.is_some() ^ args.stopped.is_some() {
298 let mut args = args.clone();
299 if args.paused.is_none() {
300 args.paused = args.stopped.clone();
301 } else {
302 args.stopped = args.paused.clone();
303 }
304 owned = args;
305 &owned
306 } else {
307 args
308 };
309
310 match &args.source {
311 TorrentSource::Urls { urls: _ } => self.post_with("torrents/add", args).await?.end(),
312 TorrentSource::TorrentFiles { torrents } => self
313 .request(
314 Method::POST,
315 "torrents/add",
316 Some(|req: RequestBuilder| req.multipart(make_form(args, torrents)?).check()),
317 )
318 .await?
319 .map_status(|code| match code as _ {
320 StatusCode::FORBIDDEN => Some(Error::ApiError(ApiError::NotLoggedIn)),
321 StatusCode::UNSUPPORTED_MEDIA_TYPE => {
322 Some(Error::ApiError(ApiError::TorrentFileInvalid))
323 }
324 StatusCode::CONFLICT => Some(Error::ApiError(ApiError::TorrentAddFailed)),
326 _ => None,
327 })?
328 .end(),
329 }
330 }
331
332 pub async fn add_trackers(
334 &self,
335 hash: impl AsRef<str> + Send + Sync,
336 urls: impl Into<Sep<String, '\n'>> + Send + Sync,
337 ) -> Result<()> {
338 #[derive(Serialize)]
339 struct Arg<'a> {
340 hash: &'a str,
341 urls: String,
342 }
343
344 self.post_with(
345 "torrents/addTrackers",
346 &Arg {
347 hash: hash.as_ref(),
348 urls: urls.into().to_string(),
349 },
350 )
351 .await
352 .and_then(|r| r.map_status(TORRENT_NOT_FOUND))?
353 .json()
354 .await
355 .map_err(Into::into)
356 }
357
358 pub async fn edit_trackers(
360 &self,
361 hash: impl AsRef<str> + Send + Sync,
362 orig_url: Url,
363 new_url: Url,
364 ) -> Result<()> {
365 #[derive(Serialize)]
366 #[serde(rename_all = "camelCase")]
367 struct EditTrackerArg<'a> {
368 hash: &'a str,
369 orig_url: Url,
370 new_url: Url,
371 }
372 self.post_with(
373 "torrents/editTracker",
374 &EditTrackerArg {
375 hash: hash.as_ref(),
376 orig_url,
377 new_url,
378 },
379 )
380 .await?
381 .map_status(|c| match c {
382 StatusCode::BAD_REQUEST => Some(Error::ApiError(ApiError::InvalidTrackerUrl)),
383 StatusCode::NOT_FOUND => Some(Error::ApiError(ApiError::TorrentNotFound)),
384 StatusCode::CONFLICT => Some(Error::ApiError(ApiError::ConflictTrackerUrl)),
385 _ => None,
386 })?
387 .end()
388 }
389
390 pub async fn remove_trackers(
392 &self,
393 hash: impl AsRef<str> + Send + Sync,
394 urls: impl Into<Sep<Url, '|'>> + Send + Sync,
395 ) -> Result<()> {
396 #[derive(Serialize)]
397 struct Arg<'a> {
398 hash: &'a str,
399 urls: Sep<Url, '|'>,
400 }
401
402 self.post_with(
403 "torrents/removeTrackers",
404 &Arg {
405 hash: hash.as_ref(),
406 urls: urls.into(),
407 },
408 )
409 .await
410 .and_then(|r| r.map_status(TORRENT_NOT_FOUND))?
411 .end()
412 }
413
414 pub async fn add_peers(
416 &self,
417 hashes: impl Into<Hashes> + Send + Sync,
418 peers: impl Into<Sep<String, '|'>> + Send + Sync,
419 ) -> Result<()> {
420 #[derive(Serialize)]
421 struct AddPeersArg {
422 hash: String,
423 peers: Sep<String, '|'>,
424 }
425
426 self.post_with(
427 "torrents/addPeers",
428 &AddPeersArg {
429 hash: hashes.into().to_string(),
430 peers: peers.into(),
431 },
432 )
433 .await
434 .and_then(|r| {
435 r.map_status(|c| {
436 if c == StatusCode::BAD_REQUEST {
437 Some(Error::ApiError(ApiError::InvalidPeers))
438 } else {
439 None
440 }
441 })
442 })?
443 .end()
444 }
445
446 pub async fn increase_priority(&self, hashes: impl Into<Hashes> + Send + Sync) -> Result<()> {
448 self.post_with("torrents/increasePrio", &HashesArg::new(hashes))
449 .await?
450 .map_status(|c| {
451 if c == StatusCode::CONFLICT {
452 Some(Error::ApiError(ApiError::QueueingDisabled))
453 } else {
454 None
455 }
456 })?;
457 Ok(())
458 }
459
460 pub async fn decrease_priority(&self, hashes: impl Into<Hashes> + Send + Sync) -> Result<()> {
462 self.post_with("torrents/decreasePrio", &HashesArg::new(hashes))
463 .await?
464 .map_status(|c| {
465 if c == StatusCode::CONFLICT {
466 Some(Error::ApiError(ApiError::QueueingDisabled))
467 } else {
468 None
469 }
470 })?;
471 Ok(())
472 }
473
474 pub async fn maximal_priority(&self, hashes: impl Into<Hashes> + Send + Sync) -> Result<()> {
476 self.post_with("torrents/topPrio", &HashesArg::new(hashes))
477 .await?
478 .map_status(|c| {
479 if c == StatusCode::CONFLICT {
480 Some(Error::ApiError(ApiError::QueueingDisabled))
481 } else {
482 None
483 }
484 })?;
485 Ok(())
486 }
487
488 pub async fn minimal_priority(&self, hashes: impl Into<Hashes> + Send + Sync) -> Result<()> {
490 self.post_with("torrents/bottomPrio", &HashesArg::new(hashes))
491 .await?
492 .map_status(|c| {
493 if c == StatusCode::CONFLICT {
494 Some(Error::ApiError(ApiError::QueueingDisabled))
495 } else {
496 None
497 }
498 })?;
499 Ok(())
500 }
501
502 pub async fn set_file_priority(
504 &self,
505 hash: impl AsRef<str> + Send + Sync,
506 indexes: impl Into<Sep<i64, '|'>> + Send + Sync,
507 priority: Priority,
508 ) -> Result<()> {
509 #[derive(Serialize)]
510 struct SetFilePriorityArg<'a> {
511 hash: &'a str,
512 id: Sep<i64, '|'>,
513 priority: Priority,
514 }
515
516 self.post_with(
517 "torrents/filePrio",
518 &SetFilePriorityArg {
519 hash: hash.as_ref(),
520 id: indexes.into(),
521 priority,
522 },
523 )
524 .await?
525 .map_status(|c| match c {
526 StatusCode::BAD_REQUEST => panic!("Invalid priority or id. This is a bug."),
527 StatusCode::NOT_FOUND => Some(Error::ApiError(ApiError::TorrentNotFound)),
528 StatusCode::CONFLICT => Some(Error::ApiError(ApiError::MetaNotDownloadedOrIdNotFound)),
529 _ => None,
530 })?;
531 Ok(())
532 }
533
534 pub async fn get_torrent_download_limit(
536 &self,
537 hashes: impl Into<Hashes> + Send + Sync,
538 ) -> Result<HashMap<String, u64>> {
539 self.get_with("torrents/downloadLimit", &HashesArg::new(hashes))
540 .await?
541 .json()
542 .await
543 .map_err(Into::into)
544 }
545
546 pub async fn set_torrent_download_limit(
548 &self,
549 hashes: impl Into<Hashes> + Send + Sync,
550 limit: u64,
551 ) -> Result<()> {
552 #[derive(Serialize)]
553 struct Arg {
554 hashes: String,
555 limit: u64,
556 }
557
558 self.post_with(
559 "torrents/downloadLimit",
560 &Arg {
561 hashes: hashes.into().to_string(),
562 limit,
563 },
564 )
565 .await?
566 .end()
567 }
568
569 pub async fn set_torrent_shared_limit(
571 &self,
572 arg: impl Borrow<SetTorrentSharedLimitArg> + Send + Sync,
573 ) -> Result<()> {
574 self.post_with("torrents/setShareLimits", arg.borrow())
575 .await?
576 .end()
577 }
578
579 pub async fn get_torrent_upload_limit(
581 &self,
582 hashes: impl Into<Hashes> + Send + Sync,
583 ) -> Result<HashMap<String, u64>> {
584 self.get_with("torrents/uploadLimit", &HashesArg::new(hashes))
585 .await?
586 .json()
587 .await
588 .map_err(Into::into)
589 }
590
591 pub async fn set_torrent_upload_limit(
593 &self,
594 hashes: impl Into<Hashes> + Send + Sync,
595 limit: u64,
596 ) -> Result<()> {
597 #[derive(Serialize)]
598 struct Arg {
599 hashes: String,
600 limit: u64,
601 }
602
603 self.post_with(
604 "torrents/uploadLimit",
605 &Arg {
606 hashes: hashes.into().to_string(),
607 limit,
608 },
609 )
610 .await?
611 .end()
612 }
613
614 pub async fn set_torrent_location(
616 &self,
617 hashes: impl Into<Hashes> + Send + Sync,
618 location: impl AsRef<Path> + Send + Sync,
619 ) -> Result<()> {
620 #[derive(Serialize)]
621 struct Arg<'a> {
622 hashes: String,
623 location: &'a Path,
624 }
625
626 self.post_with(
627 "torrents/setLocation",
628 &Arg {
629 hashes: hashes.into().to_string(),
630 location: location.as_ref(),
631 },
632 )
633 .await?
634 .map_status(|c| match c {
635 StatusCode::BAD_REQUEST => Some(Error::ApiError(ApiError::SavePathEmpty)),
636 StatusCode::FORBIDDEN => Some(Error::ApiError(ApiError::NoWriteAccess)),
637 StatusCode::CONFLICT => Some(Error::ApiError(ApiError::UnableToCreateDir)),
638 _ => None,
639 })?
640 .end()
641 }
642
643 pub async fn set_torrent_name<T: AsRef<str> + Send + Sync>(
645 &self,
646 hash: impl AsRef<str> + Send + Sync,
647 name: NonEmptyStr<T>,
648 ) -> Result<()> {
649 #[derive(Serialize)]
650 struct RenameArg<'a> {
651 hash: &'a str,
652 name: &'a str,
653 }
654
655 self.post_with(
656 "torrents/rename",
657 &RenameArg {
658 hash: hash.as_ref(),
659 name: name.as_str(),
660 },
661 )
662 .await?
663 .map_status(|c| match c {
664 StatusCode::NOT_FOUND => Some(Error::ApiError(ApiError::TorrentNotFound)),
665 StatusCode::CONFLICT => panic!("Name should not be empty. This is a bug."),
666 _ => None,
667 })?
668 .end()
669 }
670
671 pub async fn set_torrent_comment(
675 &self,
676 hashes: impl Into<Hashes> + Send + Sync,
677 comment: &str,
678 ) -> Result<()> {
679 #[derive(Serialize)]
680 struct Arg<'a> {
681 hashes: String,
682 comment: &'a str,
683 }
684
685 self.post_with(
686 "torrents/setComment",
687 &Arg {
688 hashes: hashes.into().to_string(),
689 comment,
690 },
691 )
692 .await?
693 .map_status(|c| match c {
694 StatusCode::FORBIDDEN => Some(Error::ApiError(ApiError::NotLoggedIn)),
695 _ => None,
696 })?
697 .end()
698 }
699
700 pub async fn set_torrent_category(
702 &self,
703 hashes: impl Into<Hashes> + Send + Sync,
704 category: impl AsRef<str> + Send + Sync,
705 ) -> Result<()> {
706 #[derive(Serialize)]
707 struct Arg<'a> {
708 hashes: String,
709 category: &'a str,
710 }
711
712 self.post_with(
713 "torrents/setCategory",
714 &Arg {
715 hashes: hashes.into().to_string(),
716 category: category.as_ref(),
717 },
718 )
719 .await?
720 .map_status(|c| {
721 if c == StatusCode::CONFLICT {
722 Some(Error::ApiError(ApiError::CategoryNotFound))
723 } else {
724 None
725 }
726 })?
727 .end()
728 }
729
730 pub async fn get_categories(&self) -> Result<HashMap<String, Category>> {
732 self.get("torrents/categories")
733 .await?
734 .json()
735 .await
736 .map_err(Into::into)
737 }
738
739 pub async fn add_category<T: AsRef<str> + Send + Sync>(
741 &self,
742 category: NonEmptyStr<T>,
743 save_path: impl AsRef<Path> + Send + Sync,
744 ) -> Result<()> {
745 #[derive(Serialize)]
746 #[serde(rename_all = "camelCase")]
747 struct Arg<'a> {
748 category: &'a str,
749 save_path: &'a Path,
750 }
751
752 self.post_with(
753 "torrents/createCategory",
754 &Arg {
755 category: category.as_str(),
756 save_path: save_path.as_ref(),
757 },
758 )
759 .await?
760 .end()
761 }
762
763 pub async fn edit_category<T: AsRef<str> + Send + Sync>(
765 &self,
766 category: NonEmptyStr<T>,
767 save_path: impl AsRef<Path> + Send + Sync,
768 ) -> Result<()> {
769 #[derive(Serialize)]
770 #[serde(rename_all = "camelCase")]
771 struct Arg<'a> {
772 category: &'a str,
773 save_path: &'a Path,
774 }
775
776 self.post_with(
777 "torrents/createCategory",
778 &Arg {
779 category: category.as_str(),
780 save_path: save_path.as_ref(),
781 },
782 )
783 .await?
784 .map_status(|c| {
785 if c == StatusCode::CONFLICT {
786 Some(Error::ApiError(ApiError::CategoryEditingFailed))
787 } else {
788 None
789 }
790 })?
791 .end()
792 }
793
794 pub async fn remove_categories(
796 &self,
797 categories: impl Into<Sep<String, '\n'>> + Send + Sync,
798 ) -> Result<()> {
799 #[derive(Serialize)]
800 struct Arg<'a> {
801 categories: &'a str,
802 }
803
804 self.post_with(
805 "torrents/removeCategories",
806 &Arg {
807 categories: &categories.into().to_string(),
808 },
809 )
810 .await?
811 .end()
812 }
813
814 pub async fn add_torrent_tags(
816 &self,
817 hashes: impl Into<Hashes> + Send + Sync,
818 tags: impl Into<Sep<String, '\n'>> + Send + Sync,
819 ) -> Result<()> {
820 #[derive(Serialize)]
821 struct Arg<'a> {
822 hashes: String,
823 tags: &'a str,
824 }
825
826 self.post_with(
827 "torrents/addTags",
828 &Arg {
829 hashes: hashes.into().to_string(),
830 tags: &tags.into().to_string(),
831 },
832 )
833 .await?
834 .end()
835 }
836
837 pub async fn remove_torrent_tags(
839 &self,
840 hashes: impl Into<Hashes> + Send + Sync,
841 tags: Option<impl Into<Sep<String, ','>> + Send>,
842 ) -> Result<()> {
843 #[derive(Serialize)]
844 #[skip_serializing_none]
845 struct Arg {
846 hashes: String,
847 tags: Option<String>,
848 }
849
850 self.post_with(
851 "torrents/removeTags",
852 &Arg {
853 hashes: hashes.into().to_string(),
854 tags: tags.map(|t| t.into().to_string()),
855 },
856 )
857 .await?
858 .end()
859 }
860
861 pub async fn get_all_tags(&self) -> Result<Vec<String>> {
863 self.get("torrents/tags")
864 .await?
865 .json()
866 .await
867 .map_err(Into::into)
868 }
869
870 pub async fn create_tags(&self, tags: impl Into<Sep<String, ','>> + Send + Sync) -> Result<()> {
872 #[derive(Serialize)]
873 struct Arg {
874 tags: String,
875 }
876
877 self.post_with(
878 "torrents/createTags",
879 &Arg {
880 tags: tags.into().to_string(),
881 },
882 )
883 .await?
884 .end()
885 }
886
887 pub async fn delete_tags(&self, tags: impl Into<Sep<String, ','>> + Send + Sync) -> Result<()> {
889 #[derive(Serialize)]
890 struct Arg {
891 tags: String,
892 }
893
894 self.post_with(
895 "torrents/deleteTags",
896 &Arg {
897 tags: tags.into().to_string(),
898 },
899 )
900 .await?
901 .end()
902 }
903
904 pub async fn set_auto_management(
907 &self,
908 hashes: impl Into<Hashes> + Send + Sync,
909 enable: bool,
910 ) -> Result<()> {
911 #[derive(Serialize)]
912 struct Arg {
913 hashes: String,
914 enable: bool,
915 }
916
917 self.post_with(
918 "torrents/setAutoManagement",
919 &Arg {
920 hashes: hashes.into().to_string(),
921 enable,
922 },
923 )
924 .await?
925 .end()
926 }
927
928 pub async fn toggle_sequential_download(
930 &self,
931 hashes: impl Into<Hashes> + Send + Sync,
932 ) -> Result<()> {
933 self.post_with("torrents/toggleSequentialDownload", &HashesArg::new(hashes))
934 .await?
935 .end()
936 }
937
938 pub async fn toggle_first_last_piece_priority(
940 &self,
941 hashes: impl Into<Hashes> + Send + Sync,
942 ) -> Result<()> {
943 self.post_with("torrents/toggleFirstLastPiecePrio", &HashesArg::new(hashes))
944 .await?
945 .end()
946 }
947
948 pub async fn set_force_start(
950 &self,
951 hashes: impl Into<Hashes> + Send + Sync,
952 value: bool,
953 ) -> Result<()> {
954 #[derive(Serialize)]
955 struct Arg {
956 hashes: String,
957 value: bool,
958 }
959
960 self.post_with(
961 "torrents/setForceStart",
962 &Arg {
963 hashes: hashes.into().to_string(),
964 value,
965 },
966 )
967 .await?
968 .end()
969 }
970
971 pub async fn set_super_seeding(
973 &self,
974 hashes: impl Into<Hashes> + Send + Sync,
975 value: bool,
976 ) -> Result<()> {
977 #[derive(Serialize)]
978 struct Arg {
979 hashes: String,
980 value: bool,
981 }
982
983 self.post_with(
984 "torrents/setSuperSeeding",
985 &Arg {
986 hashes: hashes.into().to_string(),
987 value,
988 },
989 )
990 .await?
991 .end()
992 }
993
994 pub async fn rename_file(
996 &self,
997 hash: impl AsRef<str> + Send + Sync,
998 old_path: impl AsRef<Path> + Send + Sync,
999 new_path: impl AsRef<Path> + Send + Sync,
1000 ) -> Result<()> {
1001 #[derive(Serialize)]
1002 #[serde(rename_all = "camelCase")]
1003 struct Arg<'a> {
1004 hash: &'a str,
1005 old_path: &'a Path,
1006 new_path: &'a Path,
1007 }
1008
1009 self.post_with(
1010 "torrents/renameFile",
1011 &Arg {
1012 hash: hash.as_ref(),
1013 old_path: old_path.as_ref(),
1014 new_path: new_path.as_ref(),
1015 },
1016 )
1017 .await?
1018 .map_status(|c| {
1019 if c == StatusCode::CONFLICT {
1020 Error::ApiError(ApiError::InvalidPath).pipe(Some)
1021 } else {
1022 None
1023 }
1024 })?
1025 .end()
1026 }
1027
1028 pub async fn rename_folder(
1030 &self,
1031 hash: impl AsRef<str> + Send + Sync,
1032 old_path: impl AsRef<Path> + Send + Sync,
1033 new_path: impl AsRef<Path> + Send + Sync,
1034 ) -> Result<()> {
1035 #[derive(Serialize)]
1036 #[serde(rename_all = "camelCase")]
1037 struct Arg<'a> {
1038 hash: &'a str,
1039 old_path: &'a Path,
1040 new_path: &'a Path,
1041 }
1042
1043 self.post_with(
1044 "torrents/renameFolder",
1045 &Arg {
1046 hash: hash.as_ref(),
1047 old_path: old_path.as_ref(),
1048 new_path: new_path.as_ref(),
1049 },
1050 )
1051 .await?
1052 .map_status(|c| {
1053 if c == StatusCode::CONFLICT {
1054 Error::ApiError(ApiError::InvalidPath).pipe(Some)
1055 } else {
1056 None
1057 }
1058 })?
1059 .end()
1060 }
1061}