1use bytes::Bytes;
2use http_body_util::BodyExt;
3use http_body_util::Empty;
4use hyper_tls::HttpsConnector;
5use hyper_util::client::legacy::Client;
6use hyper_util::rt::TokioExecutor;
7use parse_display::Display;
8use serde::{Deserialize, Serialize};
9
10use std::fmt;
11
12#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
13#[serde(rename_all = "snake_case")]
14pub enum Status {
15 Ok,
16 Other(String),
17}
18
19#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
20pub struct Torrent {
21 pub url: String,
22 pub hash: String,
23 pub quality: String,
24 #[serde(rename = "type")]
25 pub _type: String,
26 pub seeds: u32,
27 pub peers: u32,
28 pub size: String,
29 pub size_bytes: u64,
30 pub date_uploaded: String,
31 pub date_uploaded_unix: u64,
32}
33
34#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
35pub struct Actor {
36 name: String,
37 character_name: String,
38 imdb_code: String,
39 url_small_image: Option<String>,
40}
41
42#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
43pub struct Movie {
44 pub id: u32,
45 pub url: String,
46 pub imdb_code: String,
47 pub title: String,
48 pub title_english: String,
49 pub title_long: String,
50 pub slug: String,
51 pub year: u32,
52 pub rating: f32,
53 pub runtime: u32,
54 pub genres: Vec<String>,
55 pub summary: Option<String>,
56 pub description_intro: Option<String>,
57 pub description_full: String,
58 pub synopsis: Option<String>,
59 pub yt_trailer_code: String,
60 pub language: String,
61 pub mpa_rating: String,
62 pub background_image: String,
63 pub background_image_original: String,
64 pub small_cover_image: String,
65 pub medium_cover_image: String,
66 pub large_cover_image: String,
67 pub medium_screenshot_image1: Option<String>,
68 pub medium_screenshot_image2: Option<String>,
69 pub medium_screenshot_image3: Option<String>,
70 pub large_screenshot_image1: Option<String>,
71 pub large_screenshot_image2: Option<String>,
72 pub large_screenshot_image3: Option<String>,
73 pub state: Option<Status>,
74 pub torrents: Vec<Torrent>,
75 pub date_uploaded: String,
76 pub date_uploaded_unix: u64,
77 pub download_count: Option<u32>,
78 pub like_count: Option<u32>,
79 pub cast: Option<Vec<Actor>>,
80}
81
82#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
83pub struct MovieDetail {
84 pub movie: Movie,
85}
86
87#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
88pub struct MovieList {
89 pub movie_count: u32,
90 pub limit: u32,
91 pub page_number: u32,
92 #[serde(skip_serializing_if = "Vec::is_empty", default)]
93 pub movies: Vec<Movie>,
94}
95
96#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
97#[serde(untagged)]
98pub enum Data {
99 MovieList(MovieList),
100 MovieDetails(MovieDetail),
101}
102
103#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
104pub struct Response {
105 pub status: Status,
106 pub status_message: String,
107 pub data: Option<Data>,
108}
109
110#[derive(Display, Copy, Clone, Debug)]
111pub enum Quality {
112 #[display("720p")]
113 Q720p,
114 #[display("1080p")]
115 Q1080p,
116 #[display("2160p")]
117 Q2160p,
118 #[display("3D")]
119 Q3D,
120}
121
122#[derive(Display, Copy, Clone, Debug)]
123#[display(style = "snake_case")]
124pub enum Sort {
125 Title,
126 Year,
127 Rating,
128 Peers,
129 Seeds,
130 DownloadCount,
131 LikeCount,
132 DateAdded,
133}
134
135#[derive(Display, Copy, Clone, Debug)]
136#[display(style = "snake_case")]
137pub enum Order {
138 Desc,
139 Asc,
140}
141
142pub trait ApiEndpoint {
143 fn get_url(&self) -> String;
144}
145
146fn add_query(url: &mut String, name: &str, value: Option<impl fmt::Display>) {
148 if let Some(value) = value {
149 url.push_str(&format!("{}={}&", name, value));
150 }
151}
152
153async fn execute(url: &str) -> Result<Data, Box<dyn std::error::Error + Send + Sync>> {
155 let https = HttpsConnector::new();
156 let client = Client::builder(TokioExecutor::new()).build::<_, Empty<Bytes>>(https);
157
158 let mut res = client.get(url.parse()?).await?;
159 let mut bytes = Vec::new();
160 while let Some(frame) = res.body_mut().frame().await {
161 let frame = frame?;
162 if let Some(data) = frame.data_ref() {
163 bytes.extend(data);
164 }
165 }
166 let body = String::from_utf8(bytes)?;
167 let response: Response = serde_json::from_str(&body)?;
168 if let Status::Other(status) = response.status {
169 return Err(format!("{}: {}", status, response.status_message).into());
170 }
171 let data = response.data.ok_or("Data missing")?;
172 Ok(data)
173}
174
175#[derive(Clone, Debug, Default)]
176pub struct ListMovies<'a> {
177 limit: Option<u8>,
179 page: Option<u32>,
181 quality: Option<Quality>,
183 minimum_rating: Option<u8>,
185 query_term: Option<&'a str>,
188 genre: Option<&'a str>,
190 sort_by: Option<Sort>,
192 order_by: Option<Order>,
194 wirth_rt_ratings: Option<bool>,
196}
197
198impl<'a> ListMovies<'a> {
199 pub fn new() -> ListMovies<'a> {
200 ListMovies::default()
201 }
202
203 pub fn limit(&mut self, limit: u8) -> &mut Self {
204 assert!(limit > 1 && limit <= 50, "limit out of range");
205 self.limit = Some(limit);
206 self
207 }
208
209 pub fn page(&mut self, page: u32) -> &mut Self {
210 assert!(page > 1, "page out of range");
211 self.page = Some(page);
212 self
213 }
214
215 pub fn quality(&mut self, quality: Quality) -> &mut Self {
216 self.quality = Some(quality);
217 self
218 }
219
220 pub fn query_term(&mut self, query_term: &'a str) -> &mut Self {
221 self.query_term = Some(query_term);
222 self
223 }
224
225 pub fn genre(&mut self, genre: &'a str) -> &mut Self {
226 self.genre = Some(genre);
227 self
228 }
229
230 pub fn sort_by(&mut self, sort_by: Sort) -> &mut Self {
231 self.sort_by = Some(sort_by);
232 self
233 }
234
235 pub fn order_by(&mut self, order_by: Order) -> &mut Self {
236 self.order_by = Some(order_by);
237 self
238 }
239
240 pub fn wirth_rt_ratings(&mut self, wirth_rt_ratings: bool) -> &mut Self {
241 self.wirth_rt_ratings = Some(wirth_rt_ratings);
242 self
243 }
244
245 pub async fn execute(&self) -> Result<MovieList, Box<dyn std::error::Error + Send + Sync>> {
246 let data = execute(&self.get_url()).await?;
247 match data {
248 Data::MovieList(movie_list) => Ok(movie_list),
249 _ => Err("Wrong data received".into()),
250 }
251 }
252}
253
254impl<'a> ApiEndpoint for ListMovies<'a> {
255 fn get_url(&self) -> String {
256 let mut url = "https://yts.mx/api/v2/list_movies.json?".to_owned();
257
258 add_query(&mut url, "limit", self.limit);
259 add_query(&mut url, "page", self.page);
260 add_query(&mut url, "quality", self.quality);
261 add_query(&mut url, "minimum_rating", self.minimum_rating);
262 add_query(&mut url, "query_term", self.query_term);
263 add_query(&mut url, "genre", self.genre);
264 add_query(&mut url, "sort_by", self.sort_by);
265 add_query(&mut url, "order_by", self.order_by);
266 add_query(&mut url, "wirth_rt_ratings", self.wirth_rt_ratings);
267 url
268 }
269}
270
271#[derive(Clone, Debug)]
272pub struct MovieDetails {
273 movie_id: u32,
275 with_images: Option<bool>,
277 with_cast: Option<bool>,
279}
280
281impl MovieDetails {
282 pub fn new(movie_id: u32) -> MovieDetails {
283 MovieDetails {
284 movie_id,
285 with_images: None,
286 with_cast: None,
287 }
288 }
289
290 pub fn with_images(&mut self, with_images: bool) -> &mut Self {
291 self.with_images = Some(with_images);
292 self
293 }
294
295 pub fn with_cast(&mut self, with_cast: bool) -> &mut Self {
296 self.with_cast = Some(with_cast);
297 self
298 }
299
300 pub async fn execute(&self) -> Result<MovieDetail, Box<dyn std::error::Error + Send + Sync>> {
301 let data = execute(&self.get_url()).await?;
302 match data {
303 Data::MovieDetails(movie) => Ok(movie),
304 _ => Err("Wrong data received".into()),
305 }
306 }
307}
308
309impl ApiEndpoint for MovieDetails {
310 fn get_url(&self) -> String {
311 let mut url = "https://yts.mx/api/v2/movie_details.json?".to_owned();
312 add_query(&mut url, "movie_id", Some(self.movie_id));
313 add_query(&mut url, "with_images", self.with_images);
314 add_query(&mut url, "with_cast", self.with_cast);
315 url
316 }
317}
318
319#[deprecated(
320 since = "0.2.0",
321 note = "Use ListMovies::new().query_term(...).execute() instead"
322)]
323pub async fn list_movies(
324 query_term: &str,
325) -> Result<MovieList, Box<dyn std::error::Error + Send + Sync>> {
326 ListMovies::new().query_term(query_term).execute().await
327}
328
329#[cfg(test)]
330mod tests {
331 static TEST_DATA: &str = include_str!("test/test.json");
332
333 use super::*;
334
335 #[test]
336 fn list_movies_url_build_empty() {
337 let url = ListMovies::new().get_url();
338 assert_eq!(url, "https://yts.mx/api/v2/list_movies.json?");
339 }
340
341 #[test]
342 fn list_movies_url_query_term() {
343 let url = ListMovies::new().query_term("test").get_url();
344 assert_eq!(
345 url,
346 "https://yts.mx/api/v2/list_movies.json?query_term=test&"
347 );
348 }
349
350 #[test]
351 fn deserialize_test_data() {
352 let response: Response = serde_json::from_str(TEST_DATA).unwrap();
353 assert_eq!(response.status, Status::Ok);
354 assert_eq!(response.status_message, "Query was successful");
355 let data = response.data.unwrap();
356 let movie_list = match data {
357 Data::MovieList(movie_list) => movie_list,
358 _ => panic!("Wrong data"),
359 };
360 assert_eq!(movie_list.movie_count, 10);
361 assert_eq!(movie_list.limit, 20);
362 assert_eq!(movie_list.page_number, 1);
363 assert_eq!(movie_list.movies.len(), 10);
364 }
365
366 #[test]
367 fn deserialize_empty_test_data() {
368 static TEST_DATA: &str = include_str!("test/test_empty.json");
369 let response: Response = serde_json::from_str(TEST_DATA).unwrap();
370 assert_eq!(response.status, Status::Ok);
371 assert_eq!(response.status_message, "Query was successful");
372 let data = response.data.unwrap();
373 let movie_list = match data {
374 Data::MovieList(movie_list) => movie_list,
375 _ => panic!("Wrong data"),
376 };
377 assert_eq!(movie_list.movie_count, 0);
378 assert_eq!(movie_list.limit, 20);
379 assert_eq!(movie_list.page_number, 1);
380 assert_eq!(movie_list.movies.len(), 0);
381 }
382
383 #[test]
384 fn deserialize_movie_details() {
385 static TEST_DATA: &str = include_str!("test/test_movie_details.json");
386 let response: Response = serde_json::from_str(TEST_DATA).unwrap();
387 assert_eq!(response.status, Status::Ok);
388 assert_eq!(response.status_message, "Query was successful");
389 let data = response.data.unwrap();
390 let movie_details = match data {
391 Data::MovieDetails(movie_details) => movie_details,
392 _ => panic!("Wrong data"),
393 };
394 assert_eq!(movie_details.movie.id, 10);
395 }
396
397 #[test]
398 fn deserialize_movie_details_full() {
399 static TEST_DATA: &str = include_str!("test/test_movie_details_full.json");
400
401 let response: Response = serde_json::from_str(TEST_DATA).unwrap();
402 assert_eq!(response.status, Status::Ok);
403 assert_eq!(response.status_message, "Query was successful");
404 let data = response.data.unwrap();
405 let movie_details = match data {
406 Data::MovieDetails(movie_details) => movie_details,
407 _ => panic!("Wrong data"),
408 };
409 assert_eq!(movie_details.movie.id, 15);
410 }
411}