torrust_tracker/core/services/
torrent.rs

1//! Core tracker domain services.
2//!
3//! There are two services:
4//!
5//! - [`get_torrent_info`]: it returns all the data about one torrent.
6//! - [`get_torrents`]: it returns data about some torrent in bulk excluding the peer list.
7use 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/// It contains all the information the tracker has about a torrent
18#[derive(Debug, PartialEq)]
19pub struct Info {
20    /// The infohash of the torrent this data is related to
21    pub info_hash: InfoHash,
22    /// The total number of seeders for this torrent. Peer that actively serving a full copy of the torrent data
23    pub seeders: u64,
24    /// The total number of peers that have ever complete downloading this torrent
25    pub completed: u64,
26    /// The total number of leechers for this torrent. Peers that actively downloading this torrent
27    pub leechers: u64,
28    /// The swarm: the list of peers that are actively trying to download or serving this torrent
29    pub peers: Option<Vec<peer::Peer>>,
30}
31
32/// It contains only part of the information the tracker has about a torrent
33///
34/// It contains the same data as [Info] but without the list of peers in the swarm.
35#[derive(Debug, PartialEq, Clone)]
36pub struct BasicInfo {
37    /// The infohash of the torrent this data is related to
38    pub info_hash: InfoHash,
39    /// The total number of seeders for this torrent. Peer that actively serving a full copy of the torrent data
40    pub seeders: u64,
41    /// The total number of peers that have ever complete downloading this torrent
42    pub completed: u64,
43    /// The total number of leechers for this torrent. Peers that actively downloading this torrent
44    pub leechers: u64,
45}
46
47/// It returns all the information the tracker has about one torrent in a [Info] struct.
48pub 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
68/// It returns all the information the tracker has about multiple torrents in a [`BasicInfo`] struct, excluding the peer list.
69pub 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
86/// It returns all the information the tracker has about multiple torrents in a [`BasicInfo`] struct, excluding the peer list.
87pub 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}