1use std::num::NonZeroU32;
9use std::path::PathBuf;
10use std::sync::Arc;
11use std::time::Duration;
12
13use anyhow::{Context, Result};
14use librqbit::api::{Api, TorrentIdOrHash};
15use librqbit::limits::LimitsConfig;
16use librqbit::{
17 AddTorrent, AddTorrentOptions, AddTorrentResponse, Session, SessionOptions,
18 SessionPersistenceConfig,
19};
20use url::Url;
21
22fn default_add_options() -> AddTorrentOptions {
26 AddTorrentOptions {
27 overwrite: true,
28 ..Default::default()
29 }
30}
31
32use crate::config::Config;
33
34pub struct Engine {
35 api: Api,
36 session: Arc<Session>,
37 pub download_dir: PathBuf,
38}
39
40impl Engine {
41 pub async fn start(config: &Config) -> Result<Arc<Self>> {
44 let opts = SessionOptions {
45 fastresume: true,
46 dht_config: Some(librqbit::dht::PersistentDhtConfig {
47 dump_interval: Some(std::time::Duration::from_secs(300)),
48 config_filename: Some(config.state_dir.join("dht.json")),
49 }),
50 persistence: Some(SessionPersistenceConfig::Json {
51 folder: Some(config.state_dir.join("session")),
52 }),
53 socks_proxy_url: config.socks_proxy.clone(),
54 ratelimits: LimitsConfig {
55 upload_bps: config.upload_bps.and_then(NonZeroU32::new),
56 download_bps: config.download_bps.and_then(NonZeroU32::new),
57 },
58 trackers: config
59 .trackers
60 .iter()
61 .filter_map(|t| Url::parse(t).ok())
62 .collect(),
63 ..Default::default()
64 };
65
66 let session = Session::new_with_opts(config.download_dir.clone(), opts)
67 .await
68 .context("starting librqbit session")?;
69
70 Ok(Arc::new(Self {
71 api: Api::new(session.clone(), None),
72 session,
73 download_dir: config.download_dir.clone(),
74 }))
75 }
76
77 pub fn api(&self) -> &Api {
78 &self.api
79 }
80
81 pub fn session(&self) -> &Arc<Session> {
82 &self.session
83 }
84
85 pub async fn add_magnet(&self, magnet: &str) -> Result<AddTorrentResponse> {
90 let fut = self.session.add_torrent(
91 AddTorrent::from_url(magnet.to_string()),
92 Some(default_add_options()),
93 );
94 tokio::time::timeout(Duration::from_secs(30), fut)
95 .await
96 .context("metadata resolution timed out (no reachable peers?)")?
97 .context("adding torrent")
98 }
99
100 pub fn set_limits(&self, upload_bps: Option<u32>, download_bps: Option<u32>) {
102 self.session
103 .ratelimits
104 .set_upload_bps(upload_bps.and_then(NonZeroU32::new));
105 self.session
106 .ratelimits
107 .set_download_bps(download_bps.and_then(NonZeroU32::new));
108 }
109
110 pub async fn add_magnet_with_output(
113 &self,
114 magnet: &str,
115 output_folder: PathBuf,
116 ) -> Result<AddTorrentResponse> {
117 let opts = AddTorrentOptions {
118 output_folder: Some(output_folder.to_string_lossy().into_owned()),
119 ..default_add_options()
120 };
121 let fut = self
122 .session
123 .add_torrent(AddTorrent::from_url(magnet.to_string()), Some(opts));
124 tokio::time::timeout(Duration::from_secs(30), fut)
125 .await
126 .context("metadata resolution timed out (no reachable peers?)")?
127 .context("adding torrent")
128 }
129
130 pub async fn add_torrent_bytes(&self, bytes: Vec<u8>) -> Result<AddTorrentResponse> {
132 let fut = self
133 .session
134 .add_torrent(AddTorrent::from_bytes(bytes), Some(default_add_options()));
135 tokio::time::timeout(Duration::from_secs(30), fut)
136 .await
137 .context("metadata resolution timed out")?
138 .context("adding torrent")
139 }
140
141 pub async fn remove(&self, id: TorrentIdOrHash, delete_files: bool) -> Result<()> {
143 self.session
144 .delete(id, delete_files)
145 .await
146 .context("removing torrent")
147 }
148
149 pub async fn pause(&self, id: TorrentIdOrHash) -> Result<()> {
150 let handle = self.api.mgr_handle(id)?;
151 self.session.pause(&handle).await.context("pausing torrent")
152 }
153
154 pub async fn resume(&self, id: TorrentIdOrHash) -> Result<()> {
155 let handle = self.api.mgr_handle(id)?;
156 self.session
157 .unpause(&handle)
158 .await
159 .context("resuming torrent")
160 }
161
162 pub fn list(&self) -> librqbit::api::TorrentListResponse {
164 self.api.api_torrent_list()
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 #[test]
173 fn parse_id_accepts_hash_and_numeric_id() {
174 let hash = "cab507494d02ebb1178b38f2e9d7be299c86b862";
175 assert!(matches!(
176 TorrentIdOrHash::parse(hash).unwrap(),
177 TorrentIdOrHash::Hash(_)
178 ));
179 let id = TorrentIdOrHash::parse("3").unwrap();
180 assert!(matches!(id, TorrentIdOrHash::Id(_)));
181 assert!(TorrentIdOrHash::parse("not-an-id").is_err());
182 }
183}