1use std::{collections::HashMap, net::IpAddr};
2
3use base64::{engine::general_purpose::STANDARD as base64, Engine as _};
4use chrono::serde::ts_seconds::deserialize as from_ts;
5use chrono::{DateTime, Utc};
6use serde::de::{Deserializer, Error as _};
7use serde::Deserialize;
8use serde_json::Value;
9use serde_repr::*;
10
11use crate::types::request::{IdleMode, Priority, RatioMode};
12use crate::types::Id;
13
14#[derive(Deserialize, Debug)]
15pub struct RpcResponse<T: RpcResponseArgument> {
16 pub arguments: T,
17 pub result: String,
18}
19
20impl<T: RpcResponseArgument> RpcResponse<T> {
21 pub fn is_ok(&self) -> bool {
22 self.result == "success"
23 }
24}
25pub trait RpcResponseArgument {}
26
27#[derive(Deserialize, Debug, Clone)]
28pub struct SessionSet {}
29impl RpcResponseArgument for SessionSet {}
30
31#[derive(Deserialize, Debug, Clone)]
32#[serde(rename_all = "kebab-case")]
33pub struct SessionGet {
34 pub blocklist_enabled: bool,
35 pub download_dir: String,
36 pub encryption: String,
37 pub peer_port: i32,
38 pub rpc_version: i32,
39 pub rpc_version_minimum: i32,
40 pub version: String,
41}
42impl RpcResponseArgument for SessionGet {}
43
44#[derive(Deserialize, Debug, Clone)]
45#[serde(rename_all = "camelCase")]
46pub struct SessionStats {
47 pub torrent_count: i32,
48 pub active_torrent_count: i32,
49 pub paused_torrent_count: i32,
50 pub download_speed: i64,
51 pub upload_speed: i64,
52 #[serde(rename = "current-stats")]
53 pub current_stats: Stats,
54 #[serde(rename = "cumulative-stats")]
55 pub cumulative_stats: Stats,
56}
57impl RpcResponseArgument for SessionStats {}
58
59#[derive(Deserialize, Debug, Clone)]
60pub struct SessionClose {}
61impl RpcResponseArgument for SessionClose {}
62
63#[derive(Deserialize, Debug, Clone)]
64#[serde(rename_all = "kebab-case")]
65pub struct BlocklistUpdate {
66 pub blocklist_size: Option<i32>,
67}
68impl RpcResponseArgument for BlocklistUpdate {}
69
70#[derive(Deserialize, Debug, Clone)]
71#[serde(rename_all = "kebab-case")]
72pub struct FreeSpace {
73 pub path: String,
74 pub size_bytes: i64,
75}
76impl RpcResponseArgument for FreeSpace {}
77
78#[derive(Deserialize, Debug, Clone)]
79#[serde(rename_all = "kebab-case")]
80pub struct PortTest {
81 pub port_is_open: bool,
82}
83impl RpcResponseArgument for PortTest {}
84
85#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug, Deserialize_repr)]
86#[repr(u8)]
87pub enum TorrentStatus {
88 Stopped = 0,
89 QueuedToVerify = 1,
90 Verifying = 2,
91 QueuedToDownload = 3,
92 Downloading = 4,
93 QueuedToSeed = 5,
94 Seeding = 6,
95}
96
97#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize_repr)]
98#[repr(u8)]
99pub enum ErrorType {
100 Ok = 0,
101 TrackerWarning = 1,
102 TrackerError = 2,
103 LocalError = 3,
104}
105
106#[derive(Deserialize, Debug, Clone)]
107#[serde(rename_all = "camelCase")]
108pub struct Torrent {
109 #[serde(deserialize_with = "from_ts_option", default)]
110 pub activity_date: Option<DateTime<Utc>>,
111 #[serde(deserialize_with = "from_ts_option", default)]
112 pub added_date: Option<DateTime<Utc>>,
113 pub availability: Option<Vec<i16>>,
118 pub bandwidth_priority: Option<Priority>,
119 pub comment: Option<String>,
120 pub corrupt_ever: Option<u64>,
121 pub creator: Option<String>,
122 #[serde(deserialize_with = "from_ts_option", default)]
123 pub date_created: Option<DateTime<Utc>>,
124 pub desired_available: Option<u64>,
125 #[serde(deserialize_with = "from_ts_option", default)]
126 pub done_date: Option<DateTime<Utc>>,
127 pub download_dir: Option<String>,
128 pub downloaded_ever: Option<u64>,
129 pub download_limit: Option<u64>,
130 pub download_limited: Option<bool>,
131 #[serde(deserialize_with = "from_ts_option", default)]
132 pub edit_date: Option<DateTime<Utc>>,
133 pub error: Option<ErrorType>,
134 pub error_string: Option<String>,
135 pub eta: Option<i64>,
136 pub eta_idle: Option<i64>,
137 pub group: Option<String>,
138 pub hash_string: Option<String>,
139 pub have_unchecked: Option<u64>,
140 pub have_valid: Option<u64>,
141 pub honors_session_limits: Option<bool>,
142 pub id: Option<i64>,
143 pub is_finished: Option<bool>,
144 pub is_private: Option<bool>,
145 pub is_stalled: Option<bool>,
146 pub labels: Option<Vec<String>>,
147 pub left_until_done: Option<i64>,
148 pub magnet_link: Option<String>,
149 #[serde(deserialize_with = "from_ts_option", default)]
151 pub manual_announce_time: Option<DateTime<Utc>>,
152 pub max_connected_peers: Option<u16>,
153 pub metadata_percent_complete: Option<f32>,
154 pub name: Option<String>,
155 #[serde(rename = "peer-limit")]
156 pub peer_limit: Option<u16>,
157 pub peers: Option<Vec<Peer>>,
158 pub peers_connected: Option<i64>,
159 pub peers_from: Option<PeersFrom>,
160 pub peers_getting_from_us: Option<i64>,
161 pub peers_sending_to_us: Option<i64>,
162 pub percent_complete: Option<f32>,
163 pub percent_done: Option<f32>,
164 #[serde(deserialize_with = "from_bitfield_option", default)]
168 pub pieces: Option<Vec<u8>>,
169 pub piece_count: Option<u64>,
170 pub piece_size: Option<u64>,
171 #[serde(rename = "primary-mime-type")]
172 pub primary_mime_type: Option<String>,
173 pub queue_position: Option<usize>,
174 pub rate_download: Option<i64>,
175 pub rate_upload: Option<i64>,
176 pub recheck_progress: Option<f32>,
177 pub seconds_downloading: Option<u64>,
178 pub seconds_seeding: Option<i64>,
179 pub seed_idle_limit: Option<u64>, pub seed_idle_mode: Option<IdleMode>,
181 pub seed_ratio_limit: Option<f32>,
182 pub seed_ratio_mode: Option<RatioMode>,
183 pub sequential_download: Option<bool>,
184 pub size_when_done: Option<i64>,
185 #[serde(deserialize_with = "from_ts_option", default)]
186 pub start_date: Option<DateTime<Utc>>,
187 pub status: Option<TorrentStatus>,
188 pub torrent_file: Option<String>,
189 pub total_size: Option<i64>,
190 pub trackers: Option<Vec<Trackers>>,
191 pub tracker_list: Option<String>,
192 pub tracker_stats: Option<Vec<TrackerStat>>,
193 pub upload_ratio: Option<f32>,
194 pub uploaded_ever: Option<i64>,
195 pub upload_limit: Option<u64>, pub upload_limited: Option<bool>,
197 pub files: Option<Vec<File>>,
198 #[serde(deserialize_with = "from_arr_bool_option", default)]
203 pub wanted: Option<Vec<bool>>,
204 pub webseeds: Option<Vec<String>>,
205 pub webseeds_sending_to_us: Option<u16>,
206 pub priorities: Option<Vec<Priority>>,
207 pub file_stats: Option<Vec<FileStat>>,
208 #[serde(rename = "file-count")]
209 pub file_count: Option<usize>,
210}
211
212fn from_ts_option<'de, D>(deserializer: D) -> Result<Option<DateTime<Utc>>, D::Error>
213where
214 D: Deserializer<'de>,
215{
216 let ts: i64 = Deserialize::deserialize(deserializer)?;
217 if ts <= 0 {
221 return Ok(Some(DateTime::UNIX_EPOCH));
222 }
223 Ok(DateTime::<Utc>::from_timestamp(ts, 0))
224}
225
226fn from_bitfield_option<'de, D>(deserializer: D) -> Result<Option<Vec<u8>>, D::Error>
228where
229 D: Deserializer<'de>,
230{
231 let encoded: &str = Deserialize::deserialize(deserializer)?;
232 let bitfield = base64.decode(encoded).map_err(D::Error::custom)?;
233 Ok(Some(bitfield))
234}
235
236fn from_arr_bool_option<'de, D>(deserializer: D) -> Result<Option<Vec<bool>>, D::Error>
239where
240 D: Deserializer<'de>,
241{
242 let unexpected = || D::Error::custom("unexpected type");
243
244 let wanted = match Deserialize::deserialize(deserializer)? {
245 Value::Array(arr) => arr
246 .into_iter()
247 .map(|val| match val {
248 Value::Bool(b) => Ok(b),
250 Value::Number(num) => num.as_i64().map(|n| n == 1).ok_or_else(unexpected),
252 _ => Err(unexpected()),
254 })
255 .collect::<Result<Vec<_>, _>>()?,
256 _ => Err(unexpected())?,
258 };
259 Ok(Some(wanted))
260}
261
262impl Torrent {
263 pub fn id(&self) -> Option<Id> {
266 self.id
267 .map(Id::Id)
268 .or_else(|| self.hash_string.clone().map(Id::Hash))
269 }
270}
271
272#[derive(Deserialize, Debug, Clone)]
273#[serde(rename_all = "camelCase")]
274pub struct Stats {
275 pub files_added: i32,
276 pub downloaded_bytes: i64,
277 pub uploaded_bytes: i64,
278 pub seconds_active: i64,
279 pub session_count: Option<i32>,
280}
281
282#[derive(Deserialize, Debug)]
283pub struct Torrents<T> {
284 pub torrents: Vec<T>,
285}
286impl RpcResponseArgument for Torrents<Torrent> {}
287
288#[derive(Deserialize, Debug, Clone)]
289pub struct Trackers {
290 pub id: i32,
291 pub announce: String,
292 pub scrape: String,
293 #[serde(default)]
297 pub sitename: String,
298 pub tier: usize,
299}
300
301#[derive(Deserialize, Debug, Clone)]
302#[serde(rename_all = "camelCase")]
303pub struct File {
304 pub length: i64,
306 pub bytes_completed: i64,
308 pub name: String,
310 pub begin_piece: Option<u64>,
317 pub end_piece: Option<u64>,
323}
324
325#[derive(Deserialize, Debug, Clone)]
326#[serde(rename_all = "camelCase")]
327pub struct FileStat {
328 pub bytes_completed: i64,
329 pub wanted: bool,
330 pub priority: Priority,
331}
332
333#[derive(Deserialize, Debug, Clone)]
334#[serde(rename_all = "camelCase")]
335pub struct Peer {
336 pub address: IpAddr,
339 pub client_name: String,
340 pub client_is_choked: bool,
341 pub client_is_interested: bool,
342 pub flag_str: String,
343 pub is_downloading_from: bool,
344 pub is_encrypted: bool,
345 pub is_incoming: bool,
346 pub is_uploading_to: bool,
347 #[serde(rename = "isUTP")]
348 pub is_utp: bool,
349 pub peer_is_choked: bool,
350 pub peer_is_interested: bool,
351 pub port: u16,
352 pub progress: f32,
353 pub rate_to_client: u64, pub rate_to_peer: u64, }
356
357#[derive(Deserialize, Debug, Clone)]
358#[serde(rename_all = "camelCase")]
359pub struct PeersFrom {
360 pub from_cache: u16,
361 pub from_dht: u16,
362 pub from_incoming: u16,
363 pub from_lpd: u16,
364 pub from_ltep: u16,
365 pub from_pex: u16,
366 pub from_tracker: u16,
367}
368
369#[derive(Deserialize, Debug, Clone)]
370#[serde(rename_all = "camelCase")]
371pub struct TrackerStat {
372 pub announce_state: TrackerState,
373 pub announce: String,
374 pub download_count: i64,
375 pub has_announced: bool,
376 pub has_scraped: bool,
377 pub host: String,
378 pub id: Id,
379 pub is_backup: bool,
380 pub last_announce_peer_count: i64,
381 pub last_announce_result: String,
382 #[serde(deserialize_with = "from_ts")]
383 pub last_announce_start_time: DateTime<Utc>,
384 pub last_announce_succeeded: bool,
385 #[serde(deserialize_with = "from_ts")]
386 pub last_announce_time: DateTime<Utc>,
387 pub last_announce_timed_out: bool,
388 pub last_scrape_result: String,
389 #[serde(deserialize_with = "from_ts")]
390 pub last_scrape_start_time: DateTime<Utc>,
391 pub last_scrape_succeeded: bool,
392 #[serde(deserialize_with = "from_ts")]
393 pub last_scrape_time: DateTime<Utc>,
394 pub last_scrape_timed_out: bool,
395 pub leecher_count: i64,
396 #[serde(deserialize_with = "from_ts")]
397 pub next_announce_time: DateTime<Utc>,
398 #[serde(deserialize_with = "from_ts")]
399 pub next_scrape_time: DateTime<Utc>,
400 pub scrape_state: TrackerState,
401 pub scrape: String,
402 pub seeder_count: i64,
403 #[serde(default)]
407 pub sitename: String,
408 pub tier: usize,
409}
410
411#[derive(Deserialize_repr, Debug, Clone)]
412#[repr(i8)]
413pub enum TrackerState {
414 Inactive = 0,
415 Waiting = 1,
416 Queued = 2,
417 Active = 3,
418}
419
420#[derive(Deserialize, Debug)]
421pub struct Nothing {}
422impl RpcResponseArgument for Nothing {}
423
424#[derive(Debug)]
425pub enum TorrentAddedOrDuplicate {
426 TorrentDuplicate(Torrent),
427 TorrentAdded(Torrent),
428 Error,
429}
430
431impl RpcResponseArgument for TorrentAddedOrDuplicate {}
432
433impl<'de> Deserialize<'de> for TorrentAddedOrDuplicate {
434 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, <D as Deserializer<'de>>::Error>
435 where
436 D: Deserializer<'de>,
437 {
438 let mut res: HashMap<String, Torrent> = Deserialize::deserialize(deserializer)?;
439
440 let added = res.remove("torrent-added");
441 let duplicate = res.remove("torrent-duplicate");
442 match (added, duplicate) {
443 (Some(torrent), None) => Ok(TorrentAddedOrDuplicate::TorrentAdded(torrent)),
444 (None, Some(torrent)) => Ok(TorrentAddedOrDuplicate::TorrentDuplicate(torrent)),
445 _ => Ok(TorrentAddedOrDuplicate::Error),
446 }
447 }
448}
449
450#[derive(Deserialize, Debug)]
451pub struct TorrentRenamePath {
452 pub path: Option<String>,
453 pub name: Option<String>,
454 pub id: Option<i64>,
455}
456impl RpcResponseArgument for TorrentRenamePath {}
457
458#[cfg(test)]
459mod tests {
460 use crate::types::{Result, RpcResponse, TorrentAddedOrDuplicate};
461 use serde_json;
462 use serde_json::Value;
463
464 #[test]
465 fn test_torrent_added_failure_with_torrent_added_or_duplicate() {
466 let v: RpcResponse<TorrentAddedOrDuplicate> =
467 serde_json::from_str(torrent_added_failure()).expect("Failure expected");
468 println!("{v:#?}");
469 assert!(!v.is_ok());
470 }
471
472 #[test]
473 fn test_torrent_added_success_with_torrent_added_or_duplicate() -> Result<()> {
474 let v: RpcResponse<TorrentAddedOrDuplicate> =
475 serde_json::from_str(torrent_added_success())?;
476 println!("{v:#?}");
477 Ok(())
478 }
479
480 #[test]
481 fn test_torrent_added_success_with_value() -> Result<()> {
482 let v: Value = serde_json::from_str(torrent_added_success())?;
483 println!("{v:?} {}", serde_json::to_string_pretty(&v).expect(""));
484 Ok(())
485 }
486
487 #[test]
488 fn test_torrent_added_failure_with_value() -> Result<()> {
489 let v: Value = serde_json::from_str(torrent_added_failure())?;
490 println!("{v:?} {}", serde_json::to_string_pretty(&v).expect(""));
491 Ok(())
492 }
493
494 fn torrent_added_success() -> &'static str {
495 r#"
496 {
497 "arguments": {
498 "torrent-added": {
499 "hashString": "bbdaece7c8daa85e1619469ab25d422a612cf923",
500 "id": 2,
501 "name": "toto.torrent"}
502 },
503 "result": "success"
504 }
505 "#
506 }
507
508 fn torrent_added_failure() -> &'static str {
509 r#"
510 {
511 "arguments": {},
512 "result": "download directory path is not absolute"
513 }
514 "#
515 }
516}