1#![allow(unused_parens)]
2
3#[cfg(feature = "require-parse-names")]
4use std::convert::TryFrom;
5
6use itertools::Itertools;
7use tracing::instrument;
8
9mod error;
10pub use error::Error;
11mod model;
12use model::QueryResult;
13
14#[cfg(any(feature = "parse-names", feature = "require-parse-names"))]
15pub use torrent_common::Metadata;
17
18#[cfg(feature = "require-parse-names")]
19pub use torrent_common::ParseError;
21
22pub use torrent_common::Torrent;
24
25static MOVIE_CATEGORIES: [&str; 9] = ["2000", "2010", "2030", "2040", "2045", "2050", "2060", "2070", "2080"];
26static TV_CATEGORIES: [&str; 8] = ["5000", "5010", "5020", "5030", "5040", "5060", "5070", "5080"];
27static AUDIO_CATEGORIES: [&str; 6] = ["3000", "3010", "3020", "3030", "3040", "3050"];
28
29#[derive(Clone)]
30pub struct Client {
31 http: reqwest::Client,
32 base_url: String,
33 apikey: String
34}
35
36fn category_parameters(categories: &[&str]) -> String {
37 std::iter::repeat("&Category[]=").interleave_shortest(categories.iter().copied()).collect()
38}
39
40fn build_url(base_url: &str, apikey: &str, query: &str, categories: Option<&[&str]>) -> String {
41 format!(
42 "{}?apikey={}&Query={}{}",
43 base_url,
44 apikey,
45 urlencoding::encode(query),
46 categories.map(category_parameters).unwrap_or_default()
47 )
48}
49
50impl Client {
51 #[instrument(err, level = "info", skip(base_url, apikey), fields(base_url = %base_url.to_string(), apikey = %apikey.to_string()))]
52 pub fn new(base_url: impl ToString, apikey: impl ToString) -> Result<Self, reqwest::Error> {
53 let this = Self{
54 http: reqwest::Client::builder()
55 .gzip(true)
56 .build()?,
57 base_url: base_url.to_string(),
58 apikey: apikey.to_string()
59 };
60 Ok(this)
61 }
62
63 #[instrument(err, level = "debug", skip(self))]
64 async fn _get(&self, query: &str, categories: Option<&[&str]>) -> Result<reqwest::Response, reqwest::Error> {
65 let url = build_url(&self.base_url, &self.apikey, query, categories);
66 self.http.get(&url).send().await?.error_for_status()
67 }
68
69 #[cfg(not(feature = "require-parse-names"))]
70 #[instrument(err, level = "debug", skip(self))]
71 async fn get(&self, query: &str, categories: Option<&[&str]>) -> Result<Vec<Torrent>, Error> {
72 let response = self._get(query, categories).await?;
73 Ok(response.json::<QueryResult>().await?.results.into_iter().map(Torrent::from).collect())
74 }
75
76 #[cfg(feature = "require-parse-names")]
77 #[instrument(err, level = "debug", skip(self))]
78 async fn get(&self, query: &str, categories: Option<&[&str]>) -> Result<Vec<Result<Torrent, ParseError>>, Error> {
79 Ok(self._get(query, categories).await?.json::<QueryResult>().await?.results.into_iter().map(Torrent::try_from).collect())
80 }
81
82 #[cfg(not(feature = "require-parse-names"))]
83 #[instrument(err, level = "info", skip(self))]
84 pub async fn search(&self, query: &str, categories: Option<&[&str]>) -> Result<Vec<Torrent>, Error> {
85 if let Some(categories) = categories {
86 let categories = categories.iter().copied().map(urlencoding::encode).collect::<Vec<_>>();
87 self.get(query, Some(&categories.iter().map(AsRef::as_ref).collect::<Vec<_>>())).await
88 } else {
89 self.get(query, None).await
90 }
91 }
92
93 #[cfg(feature = "require-parse-names")]
94 #[instrument(err, level = "info", skip(self))]
95 pub async fn search(&self, query: &str, categories: Option<&[&str]>) -> Result<Vec<Result<Torrent, ParseError>>, Error> {
96 if let Some(categories) = categories {
97 let categories = categories.iter().copied().map(urlencoding::encode).collect::<Vec<_>>();
98 self.get(query, Some(&categories.iter().map(AsRef::as_ref).collect::<Vec<_>>())).await
99 } else {
100 self.get(query, None).await
101 }
102 }
103
104 #[cfg(not(feature = "require-parse-names"))]
105 #[instrument(err, level = "info", skip(self))]
106 pub async fn movie_search(&self, query: &str) -> Result<Vec<Torrent>, Error> {
107 self.get(query, Some(&MOVIE_CATEGORIES)).await
108 }
109
110 #[cfg(feature = "require-parse-names")]
111 #[instrument(err, level = "info", skip(self))]
112 pub async fn movie_search(&self, query: &str) -> Result<Vec<Result<Torrent, ParseError>>, Error> {
113 self.get(query, Some(&MOVIE_CATEGORIES)).await
114 }
115
116 #[cfg(not(feature = "require-parse-names"))]
117 #[instrument(err, level = "info", skip(self))]
118 pub async fn tv_search(&self, query: &str) -> Result<Vec<Torrent>, Error> {
119 self.get(query, Some(&TV_CATEGORIES)).await
120 }
121
122 #[cfg(feature = "require-parse-names")]
123 #[instrument(err, level = "info", skip(self))]
124 pub async fn tv_search(&self, query: &str) -> Result<Vec<Result<Torrent, ParseError>>, Error> {
125 self.get(query, Some(&TV_CATEGORIES)).await
126 }
127
128 #[cfg(not(feature = "require-parse-names"))]
129 #[instrument(err, level = "info", skip(self))]
130 pub async fn audio_search(&self, query: &str) -> Result<Vec<Torrent>, Error> {
131 self.get(query, Some(&AUDIO_CATEGORIES)).await
132 }
133
134 #[cfg(feature = "require-parse-names")]
135 #[instrument(err, level = "info", skip(self))]
136 pub async fn audio_search(&self, query: &str) -> Result<Vec<Result<Torrent, ParseError>>, Error> {
137 self.get(query, Some(&AUDIO_CATEGORIES)).await
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[cfg(not(feature = "require-parse-names"))]
146 fn torrent(name: &str, size: u64, categories: Vec<u32>, link: &str, seeders: u16, leechers: u16, minimum_ratio: f32, minimum_seedtime: u64) -> Torrent {
147 Torrent::new(
148 name.into(),
149 size,
150 categories,
151 link.into(),
152 Some(seeders),
153 Some(leechers),
154 Some(minimum_ratio),
155 Some(std::time::Duration::from_secs(minimum_seedtime))
156 )
157 }
158
159 #[cfg(feature = "require-parse-names")]
160 fn torrent(name: &str, size: u64, categories: Vec<u32>, link: &str, seeders: u16, leechers: u16, minimum_ratio: f32, minimum_seedtime: u64) -> Torrent {
161 Torrent::new(
162 name.into(),
163 size,
164 categories,
165 link.into(),
166 Some(seeders),
167 Some(leechers),
168 Some(minimum_ratio),
169 Some(std::time::Duration::from_secs(minimum_seedtime))
170 ).unwrap()
171 }
172
173 #[test]
174 fn simple_worst_cooks_results() {
175 let s = r#"
176 {
177 "Results": [
178 {
179 "FirstSeen": "0001-01-01T00:00:00",
180 "Tracker": "IPTorrents",
181 "TrackerId": "iptorrents",
182 "CategoryDesc": "TV/WEB-DL",
183 "BlackholeLink": null,
184 "Title": "Worst Cooks in America S22E00 Halloween Redemption 2 720p WEBRip x264-KOMPOST",
185 "Guid": "https://iptorrents.com/t/4492053",
186 "Link": "http://jackett.cronce.io:/dl/iptorrents/?jackett_apikey=ncc9usyrou9scugt8fddjsa8k2701ubs&path=Q2ZESjhBRDZKaGpva2RwQ3ZaZzZha19UUTdRVlFraVdVUzM4ZUVWTWFlNHhjZEFBVncwTHhFcFlJYVRrRTVucXdOM2czclNMdjdudUd5WHQyUjBNVmNLejFaeENIWXgtMFI1RVVSckpGa0M2RTJwZDFrRVduaURhYmtpS2tfZEl2Qlp3WWV6WndjVWVnRWlOOHdZS1M0c3lIaEt5X01tbUEwa1cyNVRZcEM5WlAzbjg2dWJ2cy1ULW9UN215a1NrY01WZm0wUTZJX1JtYTBvdG9IMUV3VnN3anZmV3dRQmZ6TlF0QTNlTXUwN2V2WEwwQ1psOEJzM1RMdGlKS1ZQd1Z1TFhjYU4wZUctSE1DX2VJRF8tMTNYdVdLUUxxZ0EyUEJXTmxBRGZiU2REcnhDeg&file=Worst+Cooks+in+America+S22E00+Halloween+Redemption+2+720p+WEBRip+x264-KOMPOST",
187 "Details": "https://iptorrents.com/t/4492053",
188 "PublishDate": "2021-09-27T05:57:24.8597709+00:00",
189 "Category": [
190 5010,
191 100022
192 ],
193 "Size": 930086912,
194 "Files": null,
195 "Grabs": 51,
196 "Description": "Tags: 6.4 2010 Comedy Game-Show Reality-TV 720p Uploaded by: TvTeam",
197 "RageID": null,
198 "TVDBId": null,
199 "Imdb": null,
200 "TMDb": null,
201 "Author": null,
202 "BookTitle": null,
203 "Seeders": 5,
204 "Peers": 0,
205 "Poster": null,
206 "InfoHash": null,
207 "MagnetUri": null,
208 "MinimumRatio": 1,
209 "MinimumSeedTime": 1209600,
210 "DownloadVolumeFactor": 1,
211 "UploadVolumeFactor": 1,
212 "Gain": 4.3310546875
213 },
214 {
215 "FirstSeen": "0001-01-01T00:00:00",
216 "Tracker": "IPTorrents",
217 "TrackerId": "iptorrents",
218 "CategoryDesc": "TV/SD",
219 "BlackholeLink": null,
220 "Title": "Worst Cooks in America S22E00 Halloween Redemption 2 XviD-AFG",
221 "Guid": "https://iptorrents.com/t/4492061",
222 "Link": "http://jackett.cronce.io:/dl/iptorrents/?jackett_apikey=ncc9usyrou9scugt8fddjsa8k2701ubs&path=Q2ZESjhBRDZKaGpva2RwQ3ZaZzZha19UUTdSMDBlSHg5YlpMZUw0NlNHUGx6cVd2cnpoekx3V2NwVFZRanlZXzdxUV90YTlZc1Zxdy1SR3dVeVZUd1NMTXpYeWVpZ09vcm52UXNzc2dSaEtCX1U5R3czZFNTTmlzVUh3VHkwelR3ZW13ckFxVEFLckdKWEpaY3V0Wm9KV19INlBzV05aNEUwREVyM3hfYnhMejhRTU1wNXdJYnhlUXNMNlNNU2tZekRwQkxCSHlvbUhWcm1hVGVWOVFyTUpqWVFScm42eVd3Zk0tOTVKV1pyOXg5SVJVVnZXWTNJb3BvSFZZVlNsT3V4VzVTM2d4QzhkMXQwUmpoVGpYOUdKV0RSNA&file=Worst+Cooks+in+America+S22E00+Halloween+Redemption+2+XviD-AFG",
223 "Details": "https://iptorrents.com/t/4492061",
224 "PublishDate": "2021-09-27T05:57:24.8596901+00:00",
225 "Category": [
226 5030,
227 100004
228 ],
229 "Size": 707788800,
230 "Files": null,
231 "Grabs": 20,
232 "Description": "Tags: 6.4 2010 Comedy Game-Show Reality-TV Uploaded by: TvTeam",
233 "RageID": null,
234 "TVDBId": null,
235 "Imdb": null,
236 "TMDb": null,
237 "Author": null,
238 "BookTitle": null,
239 "Seeders": 2,
240 "Peers": 0,
241 "Poster": null,
242 "InfoHash": null,
243 "MagnetUri": null,
244 "MinimumRatio": 1,
245 "MinimumSeedTime": 1209600,
246 "DownloadVolumeFactor": 1,
247 "UploadVolumeFactor": 1,
248 "Gain": 1.318359375
249 },
250 {
251 "FirstSeen": "0001-01-01T00:00:00",
252 "Tracker": "IPTorrents",
253 "TrackerId": "iptorrents",
254 "CategoryDesc": "TV/SD",
255 "BlackholeLink": null,
256 "Title": "Worst Cooks in America S22E00 Halloween Redemption 2 480p x264-mSD",
257 "Guid": "https://iptorrents.com/t/4492066",
258 "Link": "http://jackett.cronce.io:/dl/iptorrents/?jackett_apikey=ncc9usyrou9scugt8fddjsa8k2701ubs&path=Q2ZESjhBRDZKaGpva2RwQ3ZaZzZha19UUTdTM0p5ZHhYWXAtcUl3YWhZT3BUOGZnX2tzWFZJSXVaNTAxbUw1emg2TWUtTFhFRl9rSlhEc1ZyQ0RjRlk5Q3d3OU9WSHNxanRKR2dLNXZzUVFaSFVNb0FMcERtYmYyT2ZzUW5oMTcwRTFuTXJ2RWl2ampPeVRJRXdtNU1FLWJ1VUZJMUF2aW1xbUNVY1BkUnlMYkhwUDFPQ2V6djRLVkQ5MU1BUkExZjNBbHlWM2xLOEpycTl5b0pvMTY2dUcwYk5YQ1B1Z08zNUR4d2VaZmQ5T0Q0akdDbGcwNXhCXzQ5U0tRSnhpQi1lU1Z3YTZ1UzdLS0ZucVVleFYtQzN2d3dFZw&file=Worst+Cooks+in+America+S22E00+Halloween+Redemption+2+480p+x264-mSD",
259 "Details": "https://iptorrents.com/t/4492066",
260 "PublishDate": "2021-09-27T06:09:39.9005952+00:00",
261 "Category": [
262 5030,
263 100078
264 ],
265 "Size": 248512512,
266 "Files": null,
267 "Grabs": 25,
268 "Description": "Tags: 6.4 2010 Comedy Game-Show Reality-TV 480p Uploaded by: TvTeam",
269 "RageID": null,
270 "TVDBId": null,
271 "Imdb": null,
272 "TMDb": null,
273 "Author": null,
274 "BookTitle": null,
275 "Seeders": 1,
276 "Peers": 0,
277 "Poster": null,
278 "InfoHash": null,
279 "MagnetUri": null,
280 "MinimumRatio": 1,
281 "MinimumSeedTime": 1209600,
282 "DownloadVolumeFactor": 1,
283 "UploadVolumeFactor": 1,
284 "Gain": 0.2314453125
285 }
286 ],
287 "Indexers": [
288 {
289 "ID": "iptorrents",
290 "Name": "IPTorrents",
291 "Status": 2,
292 "Results": 255,
293 "Error": null
294 }
295 ]
296 }
297 "#; let result: QueryResult = serde_json::from_str(s).unwrap();
299 #[cfg(not(feature = "require-parse-names"))]
300 let torrents = result.results.into_iter().map(Torrent::from).collect::<Vec<_>>();
301 #[cfg(feature = "require-parse-names")]
302 let torrents = result.results.into_iter().map(Torrent::try_from).collect::<Result<Vec<_>, _>>().unwrap();
303 assert_eq!(torrents, vec![
304 torrent(
305 "Worst Cooks in America S22E00 Halloween Redemption 2 720p WEBRip x264-KOMPOST",
306 930086912,
307 vec![5010, 100022],
308 "http://jackett.cronce.io:/dl/iptorrents/?jackett_apikey=ncc9usyrou9scugt8fddjsa8k2701ubs&path=Q2ZESjhBRDZKaGpva2RwQ3ZaZzZha19UUTdRVlFraVdVUzM4ZUVWTWFlNHhjZEFBVncwTHhFcFlJYVRrRTVucXdOM2czclNMdjdudUd5WHQyUjBNVmNLejFaeENIWXgtMFI1RVVSckpGa0M2RTJwZDFrRVduaURhYmtpS2tfZEl2Qlp3WWV6WndjVWVnRWlOOHdZS1M0c3lIaEt5X01tbUEwa1cyNVRZcEM5WlAzbjg2dWJ2cy1ULW9UN215a1NrY01WZm0wUTZJX1JtYTBvdG9IMUV3VnN3anZmV3dRQmZ6TlF0QTNlTXUwN2V2WEwwQ1psOEJzM1RMdGlKS1ZQd1Z1TFhjYU4wZUctSE1DX2VJRF8tMTNYdVdLUUxxZ0EyUEJXTmxBRGZiU2REcnhDeg&file=Worst+Cooks+in+America+S22E00+Halloween+Redemption+2+720p+WEBRip+x264-KOMPOST",
309 5,
310 0,
311 1.0,
312 1209600
313 ),
314 torrent(
315 "Worst Cooks in America S22E00 Halloween Redemption 2 XviD-AFG",
316 707788800,
317 vec![5030, 100004],
318 "http://jackett.cronce.io:/dl/iptorrents/?jackett_apikey=ncc9usyrou9scugt8fddjsa8k2701ubs&path=Q2ZESjhBRDZKaGpva2RwQ3ZaZzZha19UUTdSMDBlSHg5YlpMZUw0NlNHUGx6cVd2cnpoekx3V2NwVFZRanlZXzdxUV90YTlZc1Zxdy1SR3dVeVZUd1NMTXpYeWVpZ09vcm52UXNzc2dSaEtCX1U5R3czZFNTTmlzVUh3VHkwelR3ZW13ckFxVEFLckdKWEpaY3V0Wm9KV19INlBzV05aNEUwREVyM3hfYnhMejhRTU1wNXdJYnhlUXNMNlNNU2tZekRwQkxCSHlvbUhWcm1hVGVWOVFyTUpqWVFScm42eVd3Zk0tOTVKV1pyOXg5SVJVVnZXWTNJb3BvSFZZVlNsT3V4VzVTM2d4QzhkMXQwUmpoVGpYOUdKV0RSNA&file=Worst+Cooks+in+America+S22E00+Halloween+Redemption+2+XviD-AFG",
319 2,
320 0,
321 1.0,
322 1209600
323 ),
324 torrent(
325 "Worst Cooks in America S22E00 Halloween Redemption 2 480p x264-mSD",
326 248512512,
327 vec![5030, 100078],
328 "http://jackett.cronce.io:/dl/iptorrents/?jackett_apikey=ncc9usyrou9scugt8fddjsa8k2701ubs&path=Q2ZESjhBRDZKaGpva2RwQ3ZaZzZha19UUTdTM0p5ZHhYWXAtcUl3YWhZT3BUOGZnX2tzWFZJSXVaNTAxbUw1emg2TWUtTFhFRl9rSlhEc1ZyQ0RjRlk5Q3d3OU9WSHNxanRKR2dLNXZzUVFaSFVNb0FMcERtYmYyT2ZzUW5oMTcwRTFuTXJ2RWl2ampPeVRJRXdtNU1FLWJ1VUZJMUF2aW1xbUNVY1BkUnlMYkhwUDFPQ2V6djRLVkQ5MU1BUkExZjNBbHlWM2xLOEpycTl5b0pvMTY2dUcwYk5YQ1B1Z08zNUR4d2VaZmQ5T0Q0akdDbGcwNXhCXzQ5U0tRSnhpQi1lU1Z3YTZ1UzdLS0ZucVVleFYtQzN2d3dFZw&file=Worst+Cooks+in+America+S22E00+Halloween+Redemption+2+480p+x264-mSD",
329 1,
330 0,
331 1.0,
332 1209600
333 )
334 ]);
335 }
336}
337