1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
use hyper::{body::HttpBody, Client};
use hyper_tls::HttpsConnector;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum Status {
    Ok,
    Other(String),
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct Torrent {
    pub url: String,
    pub hash: String,
    pub quality: String,
    #[serde(rename = "type")]
    pub _type: String,
    pub seeds: u32,
    pub peers: u32,
    pub size: String,
    pub size_bytes: u64,
    pub date_uploaded: String,
    pub date_uploaded_unix: u64,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct Movie {
    pub id: u32,
    pub url: String,
    pub imdb_code: String,
    pub title: String,
    pub title_english: String,
    pub title_long: String,
    pub slug: String,
    pub year: u32,
    pub rating: f32,
    pub runtime: u32,
    pub genres: Vec<String>,
    pub summary: String,
    pub description_full: String,
    pub synopsis: String,
    pub yt_trailer_code: String,
    pub language: String,
    pub mpa_rating: String,
    pub background_image: String,
    pub background_image_original: String,
    pub small_cover_image: String,
    pub medium_cover_image: String,
    pub large_cover_image: String,
    pub state: Status,
    pub torrents: Vec<Torrent>,
    pub date_uploaded: String,
    pub date_uploaded_unix: u64,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct MovieList {
    pub movie_count: u32,
    pub limit: u32,
    pub page_number: u32,
    #[serde(skip_serializing_if = "Vec::is_empty", default)]
    pub movies: Vec<Movie>,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(untagged)]
pub enum Data {
    MovieList(MovieList),
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct Response {
    pub status: Status,
    pub status_message: String,
    pub data: Option<Data>,
}

pub async fn list_movies(
    query_term: &str,
) -> Result<MovieList, Box<dyn std::error::Error + Send + Sync>> {
    let https = HttpsConnector::new();
    let client = Client::builder().build::<_, hyper::Body>(https);

    let url = format!(
        "https://yts.mx/api/v2/list_movies.json?query_term={}",
        query_term
    );
    let mut res = client.get(url.parse()?).await?;
    let mut bytes = Vec::new();
    while let Some(next) = res.data().await {
        let chunk = next?;
        bytes.extend(chunk);
    }
    let body = String::from_utf8(bytes)?;
    let response: Response = serde_json::from_str(&body)?;
    if let Status::Other(status) = response.status {
        return Err(format!("{}: {}", status, response.status_message).into());
    }
    let data = response.data.ok_or("Data missing")?;
    match data {
        Data::MovieList(movie_list) => Ok(movie_list),
    }
}

#[cfg(test)]
mod tests {
    static TEST_DATA: &str = include_str!("test/test.json");

    use super::*;

    #[test]
    fn deserialize_test_data() {
        let response: Response = serde_json::from_str(TEST_DATA).unwrap();
        assert_eq!(response.status, Status::Ok);
        assert_eq!(response.status_message, "Query was successful");
        let data = response.data.unwrap();
        let movie_list = match data {
            Data::MovieList(movie_list) => movie_list,
        };
        assert_eq!(movie_list.movie_count, 10);
        assert_eq!(movie_list.limit, 20);
        assert_eq!(movie_list.page_number, 1);
        assert_eq!(movie_list.movies.len(), 10);
    }

    #[test]
    fn deserialize_empty_test_data() {
        static TEST_DATA: &str = include_str!("test/test_empty.json");
        let response: Response = serde_json::from_str(TEST_DATA).unwrap();
        assert_eq!(response.status, Status::Ok);
        assert_eq!(response.status_message, "Query was successful");
        let data = response.data.unwrap();
        let movie_list = match data {
            Data::MovieList(movie_list) => movie_list,
        };
        assert_eq!(movie_list.movie_count, 0);
        assert_eq!(movie_list.limit, 20);
        assert_eq!(movie_list.page_number, 1);
        assert_eq!(movie_list.movies.len(), 0);
    }
}