torrust_tracker/core/services/
torrent.rs1use std::sync::Arc;
8
9use torrust_tracker_primitives::info_hash::InfoHash;
10use torrust_tracker_primitives::pagination::Pagination;
11use torrust_tracker_primitives::peer;
12use torrust_tracker_torrent_repository::entry::EntrySync;
13use torrust_tracker_torrent_repository::repository::Repository;
14
15use crate::core::Tracker;
16
17#[derive(Debug, PartialEq)]
19pub struct Info {
20 pub info_hash: InfoHash,
22 pub seeders: u64,
24 pub completed: u64,
26 pub leechers: u64,
28 pub peers: Option<Vec<peer::Peer>>,
30}
31
32#[derive(Debug, PartialEq, Clone)]
36pub struct BasicInfo {
37 pub info_hash: InfoHash,
39 pub seeders: u64,
41 pub completed: u64,
43 pub leechers: u64,
45}
46
47pub async fn get_torrent_info(tracker: Arc<Tracker>, info_hash: &InfoHash) -> Option<Info> {
49 let torrent_entry_option = tracker.torrents.get(info_hash);
50
51 let torrent_entry = torrent_entry_option?;
52
53 let stats = torrent_entry.get_swarm_metadata();
54
55 let peers = torrent_entry.get_peers(None);
56
57 let peers = Some(peers.iter().map(|peer| (**peer)).collect());
58
59 Some(Info {
60 info_hash: *info_hash,
61 seeders: u64::from(stats.complete),
62 completed: u64::from(stats.downloaded),
63 leechers: u64::from(stats.incomplete),
64 peers,
65 })
66}
67
68pub async fn get_torrents_page(tracker: Arc<Tracker>, pagination: Option<&Pagination>) -> Vec<BasicInfo> {
70 let mut basic_infos: Vec<BasicInfo> = vec![];
71
72 for (info_hash, torrent_entry) in tracker.torrents.get_paginated(pagination) {
73 let stats = torrent_entry.get_swarm_metadata();
74
75 basic_infos.push(BasicInfo {
76 info_hash,
77 seeders: u64::from(stats.complete),
78 completed: u64::from(stats.downloaded),
79 leechers: u64::from(stats.incomplete),
80 });
81 }
82
83 basic_infos
84}
85
86pub async fn get_torrents(tracker: Arc<Tracker>, info_hashes: &[InfoHash]) -> Vec<BasicInfo> {
88 let mut basic_infos: Vec<BasicInfo> = vec![];
89
90 for info_hash in info_hashes {
91 if let Some(stats) = tracker.torrents.get(info_hash).map(|t| t.get_swarm_metadata()) {
92 basic_infos.push(BasicInfo {
93 info_hash: *info_hash,
94 seeders: u64::from(stats.complete),
95 completed: u64::from(stats.downloaded),
96 leechers: u64::from(stats.incomplete),
97 });
98 }
99 }
100
101 basic_infos
102}
103
104#[cfg(test)]
105mod tests {
106 use std::net::{IpAddr, Ipv4Addr, SocketAddr};
107
108 use aquatic_udp_protocol::{AnnounceEvent, NumberOfBytes, PeerId};
109 use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch};
110
111 fn sample_peer() -> peer::Peer {
112 peer::Peer {
113 peer_id: PeerId(*b"-qB00000000000000000"),
114 peer_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(126, 0, 0, 1)), 8080),
115 updated: DurationSinceUnixEpoch::new(1_669_397_478_934, 0),
116 uploaded: NumberOfBytes::new(0),
117 downloaded: NumberOfBytes::new(0),
118 left: NumberOfBytes::new(0),
119 event: AnnounceEvent::Started,
120 }
121 }
122
123 mod getting_a_torrent_info {
124
125 use std::str::FromStr;
126 use std::sync::Arc;
127
128 use torrust_tracker_configuration::Configuration;
129 use torrust_tracker_primitives::info_hash::InfoHash;
130 use torrust_tracker_test_helpers::configuration;
131
132 use crate::core::services::torrent::tests::sample_peer;
133 use crate::core::services::torrent::{get_torrent_info, Info};
134 use crate::core::services::tracker_factory;
135
136 pub fn tracker_configuration() -> Configuration {
137 configuration::ephemeral()
138 }
139
140 #[tokio::test]
141 async fn should_return_none_if_the_tracker_does_not_have_the_torrent() {
142 let tracker = Arc::new(tracker_factory(&tracker_configuration()));
143
144 let torrent_info = get_torrent_info(
145 tracker.clone(),
146 &InfoHash::from_str("0b3aea4adc213ce32295be85d3883a63bca25446").unwrap(),
147 )
148 .await;
149
150 assert!(torrent_info.is_none());
151 }
152
153 #[tokio::test]
154 async fn should_return_the_torrent_info_if_the_tracker_has_the_torrent() {
155 let tracker = Arc::new(tracker_factory(&tracker_configuration()));
156
157 let hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned();
158 let info_hash = InfoHash::from_str(&hash).unwrap();
159 tracker.upsert_peer_and_get_stats(&info_hash, &sample_peer());
160
161 let torrent_info = get_torrent_info(tracker.clone(), &info_hash).await.unwrap();
162
163 assert_eq!(
164 torrent_info,
165 Info {
166 info_hash: InfoHash::from_str(&hash).unwrap(),
167 seeders: 1,
168 completed: 0,
169 leechers: 0,
170 peers: Some(vec![sample_peer()]),
171 }
172 );
173 }
174 }
175
176 mod searching_for_torrents {
177
178 use std::str::FromStr;
179 use std::sync::Arc;
180
181 use torrust_tracker_configuration::Configuration;
182 use torrust_tracker_primitives::info_hash::InfoHash;
183 use torrust_tracker_test_helpers::configuration;
184
185 use crate::core::services::torrent::tests::sample_peer;
186 use crate::core::services::torrent::{get_torrents_page, BasicInfo, Pagination};
187 use crate::core::services::tracker_factory;
188
189 pub fn tracker_configuration() -> Configuration {
190 configuration::ephemeral()
191 }
192
193 #[tokio::test]
194 async fn should_return_an_empty_result_if_the_tracker_does_not_have_any_torrent() {
195 let tracker = Arc::new(tracker_factory(&tracker_configuration()));
196
197 let torrents = get_torrents_page(tracker.clone(), Some(&Pagination::default())).await;
198
199 assert_eq!(torrents, vec![]);
200 }
201
202 #[tokio::test]
203 async fn should_return_a_summarized_info_for_all_torrents() {
204 let tracker = Arc::new(tracker_factory(&tracker_configuration()));
205
206 let hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned();
207 let info_hash = InfoHash::from_str(&hash).unwrap();
208
209 tracker.upsert_peer_and_get_stats(&info_hash, &sample_peer());
210
211 let torrents = get_torrents_page(tracker.clone(), Some(&Pagination::default())).await;
212
213 assert_eq!(
214 torrents,
215 vec![BasicInfo {
216 info_hash: InfoHash::from_str(&hash).unwrap(),
217 seeders: 1,
218 completed: 0,
219 leechers: 0,
220 }]
221 );
222 }
223
224 #[tokio::test]
225 async fn should_allow_limiting_the_number_of_torrents_in_the_result() {
226 let tracker = Arc::new(tracker_factory(&tracker_configuration()));
227
228 let hash1 = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned();
229 let info_hash1 = InfoHash::from_str(&hash1).unwrap();
230 let hash2 = "03840548643af2a7b63a9f5cbca348bc7150ca3a".to_owned();
231 let info_hash2 = InfoHash::from_str(&hash2).unwrap();
232
233 tracker.upsert_peer_and_get_stats(&info_hash1, &sample_peer());
234 tracker.upsert_peer_and_get_stats(&info_hash2, &sample_peer());
235
236 let offset = 0;
237 let limit = 1;
238
239 let torrents = get_torrents_page(tracker.clone(), Some(&Pagination::new(offset, limit))).await;
240
241 assert_eq!(torrents.len(), 1);
242 }
243
244 #[tokio::test]
245 async fn should_allow_using_pagination_in_the_result() {
246 let tracker = Arc::new(tracker_factory(&tracker_configuration()));
247
248 let hash1 = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned();
249 let info_hash1 = InfoHash::from_str(&hash1).unwrap();
250 let hash2 = "03840548643af2a7b63a9f5cbca348bc7150ca3a".to_owned();
251 let info_hash2 = InfoHash::from_str(&hash2).unwrap();
252
253 tracker.upsert_peer_and_get_stats(&info_hash1, &sample_peer());
254 tracker.upsert_peer_and_get_stats(&info_hash2, &sample_peer());
255
256 let offset = 1;
257 let limit = 4000;
258
259 let torrents = get_torrents_page(tracker.clone(), Some(&Pagination::new(offset, limit))).await;
260
261 assert_eq!(torrents.len(), 1);
262 assert_eq!(
263 torrents,
264 vec![BasicInfo {
265 info_hash: InfoHash::from_str(&hash1).unwrap(),
266 seeders: 1,
267 completed: 0,
268 leechers: 0,
269 }]
270 );
271 }
272
273 #[tokio::test]
274 async fn should_return_torrents_ordered_by_info_hash() {
275 let tracker = Arc::new(tracker_factory(&tracker_configuration()));
276
277 let hash1 = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned();
278 let info_hash1 = InfoHash::from_str(&hash1).unwrap();
279 tracker.upsert_peer_and_get_stats(&info_hash1, &sample_peer());
280
281 let hash2 = "03840548643af2a7b63a9f5cbca348bc7150ca3a".to_owned();
282 let info_hash2 = InfoHash::from_str(&hash2).unwrap();
283 tracker.upsert_peer_and_get_stats(&info_hash2, &sample_peer());
284
285 let torrents = get_torrents_page(tracker.clone(), Some(&Pagination::default())).await;
286
287 assert_eq!(
288 torrents,
289 vec![
290 BasicInfo {
291 info_hash: InfoHash::from_str(&hash2).unwrap(),
292 seeders: 1,
293 completed: 0,
294 leechers: 0,
295 },
296 BasicInfo {
297 info_hash: InfoHash::from_str(&hash1).unwrap(),
298 seeders: 1,
299 completed: 0,
300 leechers: 0,
301 }
302 ]
303 );
304 }
305 }
306}