1use std::fmt::{Debug, Display};
2
3use serde::Serialize;
4use serde_with::{SerializeDisplay, skip_serializing_none};
5
6use crate::{client::Url, model::Sep};
7
8#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
9#[serde(rename_all = "camelCase")]
10pub enum TorrentFilter {
11 All,
12 Downloading,
13 Completed,
14 Paused,
15 Active,
16 Inactive,
17 Resumed,
18 Stalled,
19 StalledUploading,
20 StalledDownloading,
21 Errored,
22}
23
24#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
25pub struct Torrent {
26 pub added_on: Option<i64>,
28 pub amount_left: Option<i64>,
30 pub auto_tmm: Option<bool>,
32 pub availability: Option<f64>,
34 pub category: Option<String>,
36 pub comment: Option<String>,
38 pub completed: Option<i64>,
40 pub completion_on: Option<i64>,
42 pub content_path: Option<String>,
45 pub dl_limit: Option<i64>,
47 pub dlspeed: Option<i64>,
49 pub download_path: Option<String>,
51 pub downloaded: Option<i64>,
53 pub downloaded_session: Option<i64>,
55 pub eta: Option<i64>,
57 pub f_l_piece_prio: Option<bool>,
59 pub force_start: Option<bool>,
61 pub has_metadata: Option<bool>,
63 pub hash: Option<String>,
65 pub inactive_seeding_time_limit: Option<i64>,
67 pub infohash_v1: Option<String>,
69 pub infohash_v2: Option<String>,
71 pub last_activity: Option<i64>,
73 pub magnet_uri: Option<String>,
75 pub max_inactive_seeding_time: Option<i64>,
77 pub max_ratio: Option<f64>,
79 pub max_seeding_time: Option<i64>,
81 pub name: Option<String>,
83 pub num_complete: Option<i64>,
85 pub num_incomplete: Option<i64>,
87 pub num_leechs: Option<i64>,
89 pub num_seeds: Option<i64>,
91 pub popularity: Option<f64>,
93 pub priority: Option<i64>,
96 pub private: Option<bool>,
98 pub progress: Option<f64>,
100 pub ratio: Option<f64>,
102 pub ratio_limit: Option<f64>,
103 pub reannounce: Option<i64>,
105 pub root_path: Option<String>,
107 pub save_path: Option<String>,
109 pub seeding_time: Option<i64>,
111 pub seeding_time_limit: Option<i64>,
117 pub seen_complete: Option<i64>,
119 pub seq_dl: Option<bool>,
121 pub size: Option<i64>,
123 pub state: Option<State>,
125 pub super_seeding: Option<bool>,
127 pub tags: Option<String>,
129 pub time_active: Option<i64>,
131 pub total_size: Option<i64>,
134 pub tracker: Option<String>,
137 pub up_limit: Option<i64>,
139 pub uploaded: Option<i64>,
141 pub uploaded_session: Option<i64>,
143 pub upspeed: Option<i64>,
145}
146
147#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
148pub enum State {
149 #[serde(rename = "error")]
151 Error,
152 #[serde(rename = "missingFiles")]
154 MissingFiles,
155 #[serde(rename = "uploading")]
157 Uploading,
158 #[serde(rename = "pausedUP", alias = "stoppedUP")]
161 PausedUP,
162 #[serde(rename = "queuedUP")]
164 QueuedUP,
165 #[serde(rename = "stalledUP")]
167 StalledUP,
168 #[serde(rename = "checkingUP")]
170 CheckingUP,
171 #[serde(rename = "forcedUP")]
173 ForcedUP,
174 #[serde(rename = "allocating")]
176 Allocating,
177 #[serde(rename = "downloading")]
179 Downloading,
180 #[serde(rename = "metaDL")]
182 MetaDL,
183 #[serde(rename = "pausedDL", alias = "stoppedDL")]
186 PausedDL,
187 #[serde(rename = "queuedDL")]
189 QueuedDL,
190 #[serde(rename = "stalledDL")]
192 StalledDL,
193 #[serde(rename = "checkingDL")]
195 CheckingDL,
196 #[serde(rename = "forcedDL")]
198 ForcedDL,
199 #[serde(rename = "checkingResumeData")]
201 CheckingResumeData,
202 #[serde(rename = "moving")]
204 Moving,
205 #[serde(rename = "unknown")]
207 Unknown,
208}
209
210#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
211pub struct TorrentProperty {
212 pub save_path: Option<String>,
214 pub creation_date: Option<i64>,
216 pub piece_size: Option<i64>,
218 pub comment: Option<String>,
220 pub total_wasted: Option<i64>,
222 pub total_uploaded: Option<i64>,
224 pub total_uploaded_session: Option<i64>,
226 pub total_downloaded: Option<i64>,
228 pub total_downloaded_session: Option<i64>,
230 pub up_limit: Option<i64>,
232 pub dl_limit: Option<i64>,
234 pub time_elapsed: Option<i64>,
236 pub seeding_time: Option<i64>,
238 pub nb_connections: Option<i64>,
240 pub nb_connections_limit: Option<i64>,
242 pub share_ratio: Option<f64>,
244 pub addition_date: Option<i64>,
246 pub completion_date: Option<i64>,
248 pub availability: Option<f64>,
251 pub created_by: Option<String>,
253 pub dl_speed_avg: Option<i64>,
255 pub dl_speed: Option<i64>,
257 pub eta: Option<i64>,
259 pub last_seen: Option<i64>,
261 pub peers: Option<i64>,
263 pub peers_total: Option<i64>,
265 pub pieces_have: Option<i64>,
267 pub pieces_num: Option<i64>,
269 pub reannounce: Option<i64>,
271 pub seeds: Option<i64>,
273 pub seeds_total: Option<i64>,
275 pub total_size: Option<i64>,
277 pub up_speed_avg: Option<i64>,
279 pub up_speed: Option<i64>,
281}
282
283#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
284pub struct WebSeed {
285 pub url: Url,
287}
288
289#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
290pub struct TorrentContent {
291 pub index: u64,
293 pub name: String,
295 pub size: u64,
297 pub progress: f64,
299 pub priority: Priority,
301 pub is_seed: Option<bool>,
303 #[serde(default)]
306 pub piece_range: Vec<i64>,
307 #[serde(default)]
309 pub availability: f64,
310}
311
312#[derive(
313 Debug,
314 Clone,
315 Copy,
316 PartialEq,
317 Eq,
318 PartialOrd,
319 Ord,
320 serde_repr::Serialize_repr,
321 serde_repr::Deserialize_repr,
322)]
323#[repr(u8)]
324pub enum Priority {
325 DoNotDownload = 0,
327 Normal = 1,
329 Mixed = 4,
331 High = 6,
333 Maximal = 7,
335}
336
337#[derive(
338 Debug,
339 Clone,
340 Copy,
341 PartialEq,
342 Eq,
343 PartialOrd,
344 Ord,
345 serde_repr::Serialize_repr,
346 serde_repr::Deserialize_repr,
347)]
348#[repr(u8)]
349pub enum PieceState {
350 NotDownloaded = 0,
352 Downloading = 1,
354 Downloaded = 2,
356}
357
358#[derive(Debug, Clone, PartialEq, Eq, SerializeDisplay)]
360pub enum Hashes {
361 Hashes(Sep<String, '|'>),
363 All,
365}
366
367impl<V: Into<Vec<String>>> From<V> for Hashes {
368 fn from(hashes: V) -> Self {
369 Hashes::Hashes(Sep::from(hashes))
370 }
371}
372
373impl Display for Hashes {
374 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
375 match self {
376 Hashes::Hashes(hashes) => write!(f, "{}", hashes),
377 Hashes::All => write!(f, "all"),
378 }
379 }
380}
381
382#[cfg_attr(feature = "builder", derive(typed_builder::TypedBuilder))]
383#[cfg_attr(
384 feature = "builder",
385 builder(field_defaults(default, setter(strip_option)))
386)]
387#[derive(Debug, Clone, PartialEq, Default, serde::Serialize)]
388#[skip_serializing_none]
389pub struct GetTorrentListArg {
390 pub filter: Option<TorrentFilter>,
395 pub category: Option<String>,
397 pub tag: Option<String>,
401 pub sort: Option<String>,
404 pub reverse: Option<bool>,
406 pub limit: Option<u64>,
408 pub offset: Option<i64>,
410 pub hashes: Option<String>,
412}
413
414#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
415#[serde(untagged)]
416pub enum TorrentSource {
417 Urls { urls: Sep<Url, '\n'> },
419 TorrentFiles { torrents: Vec<TorrentFile> },
421}
422#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
423pub struct TorrentFile {
425 pub filename: String,
426 pub data: Vec<u8>,
427}
428impl Default for TorrentSource {
429 fn default() -> Self {
430 TorrentSource::Urls {
431 urls: Sep::from(vec![]),
432 }
433 }
434}
435fn is_torrent_files(source: &TorrentSource) -> bool {
436 matches!(source, TorrentSource::TorrentFiles { .. })
437}
438#[cfg_attr(feature = "builder", derive(typed_builder::TypedBuilder))]
439#[cfg_attr(
440 feature = "builder",
441 builder(field_defaults(default, setter(strip_option)))
442)]
443#[derive(Debug, Clone, PartialEq, serde::Serialize, Default)]
444#[skip_serializing_none]
445pub struct AddTorrentArg {
446 #[serde(flatten)]
447 #[cfg_attr(feature = "builder", builder(!default, setter(!strip_option)))]
448 #[serde(skip_serializing_if = "is_torrent_files")]
449 pub source: TorrentSource,
450 #[serde(skip_serializing_if = "Option::is_none")]
451 pub savepath: Option<String>,
453 #[serde(skip_serializing_if = "Option::is_none")]
455 pub cookie: Option<String>,
456 #[serde(skip_serializing_if = "Option::is_none")]
459 pub downloader: Option<String>,
460 #[serde(skip_serializing_if = "Option::is_none")]
462 pub category: Option<String>,
463
464 #[serde(skip_serializing_if = "Option::is_none")]
466 pub tags: Option<String>,
467
468 #[serde(skip_serializing_if = "Option::is_none")]
470 pub skip_checking: Option<String>,
471
472 #[serde(skip_serializing_if = "Option::is_none")]
481 pub paused: Option<String>,
482
483 #[serde(skip_serializing_if = "Option::is_none")]
492 pub stopped: Option<String>,
493
494 #[serde(skip_serializing_if = "Option::is_none")]
497 pub root_folder: Option<String>,
498
499 #[serde(rename = "contentLayout")]
503 #[serde(skip_serializing_if = "Option::is_none")]
504 pub content_layout: Option<ContentLayout>,
505
506 #[serde(skip_serializing_if = "Option::is_none")]
508 pub rename: Option<String>,
509
510 #[serde(rename = "upLimit")]
512 #[serde(skip_serializing_if = "Option::is_none")]
513 pub up_limit: Option<i64>,
514
515 #[serde(rename = "dlLimit")]
517 #[serde(skip_serializing_if = "Option::is_none")]
518 pub download_limit: Option<i64>,
519
520 #[serde(rename = "ratioLimit")]
522 #[serde(skip_serializing_if = "Option::is_none")]
523 pub ratio_limit: Option<f64>,
524
525 #[serde(rename = "seedingTimeLimit")]
527 #[serde(skip_serializing_if = "Option::is_none")]
528 pub seeding_time_limit: Option<i64>,
529
530 #[serde(rename = "autoTMM")]
532 #[serde(skip_serializing_if = "Option::is_none")]
533 pub auto_torrent_management: Option<bool>,
534
535 #[serde(rename = "sequentialDownload")]
538 #[serde(skip_serializing_if = "Option::is_none")]
539 pub sequential_download: Option<String>,
540
541 #[serde(rename = "firstLastPiecePrio")]
544 #[serde(skip_serializing_if = "Option::is_none")]
545 pub first_last_piece_priority: Option<String>,
546}
547
548#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
550pub enum ContentLayout {
551 Original,
553 Subfolder,
555 NoSubfolder,
557}
558
559#[cfg(test)]
560mod content_layout_tests {
561 use super::ContentLayout;
562
563 #[test]
564 fn serializes_to_api_names() {
565 for (variant, expected) in [
566 (ContentLayout::Original, "Original"),
567 (ContentLayout::Subfolder, "Subfolder"),
568 (ContentLayout::NoSubfolder, "NoSubfolder"),
569 ] {
570 assert_eq!(serde_json::to_value(variant).unwrap(), expected);
571 }
572 }
573}
574
575#[cfg(test)]
576mod torrent_source_tests {
577 use super::{AddTorrentArg, TorrentFile, TorrentSource};
578
579 #[test]
585 fn torrent_files_source_is_skipped_on_serialization() {
586 let arg = AddTorrentArg {
587 source: TorrentSource::TorrentFiles {
588 torrents: vec![TorrentFile {
589 filename: "test.torrent".to_owned(),
590 data: vec![0x64, 0x38, 0x00, 0xff, 0xfe],
592 }],
593 },
594 ..Default::default()
595 };
596
597 let value = serde_json::to_value(&arg).unwrap();
598 let obj = value.as_object().expect("should serialize to an object");
599 assert!(
600 !obj.contains_key("torrents") && !obj.contains_key("source"),
601 "binary torrent payload must not be serialized into the form: {obj:?}"
602 );
603
604 serde_urlencoded::to_string(&arg).expect("arg without binary source must urlencode");
606 }
607}
608
609#[cfg_attr(feature = "builder", derive(typed_builder::TypedBuilder))]
610#[derive(Debug, Clone, PartialEq, serde::Serialize)]
611#[serde(rename_all = "camelCase")]
612pub struct SetTorrentSharedLimitArg {
613 #[cfg_attr(feature = "builder", builder(setter(into)))]
614 pub hashes: Hashes,
615 #[cfg_attr(feature = "builder", builder(default, setter(strip_option)))]
616 pub ratio_limit: Option<RatioLimit>,
617 #[cfg_attr(feature = "builder", builder(default, setter(strip_option)))]
618 pub seeding_time_limit: Option<SeedingTimeLimit>,
619 #[cfg_attr(feature = "builder", builder(default, setter(strip_option)))]
620 pub inactive_seeding_time_limit: Option<SeedingTimeLimit>,
621}
622
623#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
624pub enum RatioLimit {
625 Global,
626 NoLimit,
627 Limited(f64),
628}
629
630impl Serialize for RatioLimit {
631 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
632 where
633 S: serde::Serializer,
634 {
635 match self {
636 Self::Global => serializer.serialize_i64(-2),
637 Self::NoLimit => serializer.serialize_i64(-1),
638 Self::Limited(limit) => serializer.serialize_f64(*limit),
639 }
640 }
641}
642
643#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
644pub enum SeedingTimeLimit {
645 Global,
646 NoLimit,
647 Limited(u64),
649}
650
651impl Serialize for SeedingTimeLimit {
652 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
653 where
654 S: serde::Serializer,
655 {
656 match self {
657 Self::Global => serializer.serialize_i64(-2),
658 Self::NoLimit => serializer.serialize_i64(-1),
659 Self::Limited(limit) => serializer.serialize_u64(*limit),
660 }
661 }
662}
663
664#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
665pub(crate) struct HashArg<'a> {
666 hash: &'a str,
667}
668
669impl<'a> HashArg<'a> {
670 pub(crate) fn new(hash: &'a str) -> Self {
671 Self { hash }
672 }
673}
674
675#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
676pub(crate) struct HashesArg {
677 hashes: Hashes,
678}
679
680impl HashesArg {
681 pub(crate) fn new(hashes: impl Into<Hashes> + Send + Sync) -> Self {
682 Self {
683 hashes: hashes.into(),
684 }
685 }
686}