tmdb_api/tvshow/
search.rs

1use std::borrow::Cow;
2
3const PATH: &str = "/search/tv";
4
5#[derive(Clone, Debug, Default, serde::Serialize)]
6pub struct Params<'a> {
7    /// ISO 639-1 value to display translated data for the fields that support
8    /// it.
9    #[serde(skip_serializing_if = "Option::is_none")]
10    pub language: Option<Cow<'a, str>>,
11    /// Which page to query.
12    #[serde(skip_serializing_if = "Option::is_none")]
13    pub page: Option<u32>,
14    /// Whether to include adult (pornography) content in the results.
15    #[serde(skip_serializing_if = "crate::util::is_false")]
16    pub include_adult: bool,
17    /// ISO 3166-1 code to filter release region. Must be uppercase.
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub region: Option<Cow<'a, str>>,
20    /// Search the first air date and all episode air dates. Valid values are:
21    /// 1000..9999
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub year: Option<u16>,
24    /// Search only the first air date. Valid values are: 1000..9999
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub first_air_date_year: Option<u16>,
27}
28
29impl<'a> Params<'a> {
30    pub fn set_language(&mut self, value: impl Into<Cow<'a, str>>) {
31        self.language = Some(value.into());
32    }
33
34    pub fn with_language(mut self, value: impl Into<Cow<'a, str>>) -> Self {
35        self.set_language(value);
36        self
37    }
38
39    pub fn set_page(&mut self, value: u32) {
40        self.page = Some(value);
41    }
42
43    pub fn with_page(mut self, value: u32) -> Self {
44        self.set_page(value);
45        self
46    }
47
48    pub fn set_include_adult(&mut self, value: bool) {
49        self.include_adult = value;
50    }
51
52    pub fn with_include_adult(mut self, value: bool) -> Self {
53        self.set_include_adult(value);
54        self
55    }
56
57    pub fn set_region(&mut self, value: impl Into<Cow<'a, str>>) {
58        self.region = Some(value.into());
59    }
60
61    pub fn with_region(mut self, value: impl Into<Cow<'a, str>>) -> Self {
62        self.set_region(value);
63        self
64    }
65
66    pub fn set_year(&mut self, value: u16) {
67        self.year = Some(value);
68    }
69
70    pub fn with_year(mut self, value: u16) -> Self {
71        self.set_year(value);
72        self
73    }
74
75    pub fn set_first_air_date_year(&mut self, value: u16) {
76        self.first_air_date_year = Some(value);
77    }
78
79    pub fn with_first_air_date_year(mut self, value: u16) -> Self {
80        self.set_first_air_date_year(value);
81        self
82    }
83}
84
85#[derive(serde::Serialize)]
86struct WithQuery<'a, V> {
87    query: Cow<'a, str>,
88    #[serde(flatten)]
89    inner: V,
90}
91
92impl<E: crate::client::Executor> crate::Client<E> {
93    /// Command to search for tvshows
94    ///
95    /// ```rust
96    /// use tmdb_api::client::Client;
97    /// use tmdb_api::client::reqwest::Client as ReqwestClient;
98    ///
99    /// #[tokio::main]
100    /// async fn main() {
101    ///     let client = Client::<ReqwestClient>::new("this-is-my-secret-token".into());
102    ///     match client.search_tvshows("simpsons", &Default::default()).await {
103    ///         Ok(res) => println!("found: {:#?}", res),
104    ///         Err(err) => eprintln!("error: {:?}", err),
105    ///     };
106    /// }
107    /// ```
108    pub async fn search_tvshows<'a>(
109        &self,
110        query: impl Into<Cow<'a, str>>,
111        params: &Params<'a>,
112    ) -> crate::Result<crate::common::PaginatedResult<super::TVShowShort>> {
113        self.execute(
114            PATH,
115            &WithQuery {
116                query: query.into(),
117                inner: params,
118            },
119        )
120        .await
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use mockito::Matcher;
127
128    use crate::client::Client;
129    use crate::client::reqwest::Client as ReqwestClient;
130
131    #[tokio::test]
132    async fn it_works() {
133        let mut server = mockito::Server::new_async().await;
134        let client = Client::<ReqwestClient>::builder()
135            .with_api_key("secret".into())
136            .with_base_url(server.url())
137            .build()
138            .unwrap();
139
140        let _m = server
141            .mock("GET", super::PATH)
142            .match_query(Matcher::AllOf(vec![
143                Matcher::UrlEncoded("api_key".into(), "secret".into()),
144                Matcher::UrlEncoded("query".into(), "Whatever".into()),
145            ]))
146            .with_status(200)
147            .with_header("content-type", "application/json")
148            .with_body(include_str!("../../assets/search-tv.json"))
149            .create_async()
150            .await;
151        let result = client
152            .search_tvshows("Whatever", &Default::default())
153            .await
154            .unwrap();
155        assert_eq!(result.page, 1);
156        assert!(!result.results.is_empty());
157        assert!(result.total_pages > 0);
158        assert!(result.total_results > 0);
159        let item = result.results.first().unwrap();
160        assert_eq!(item.inner.name, "Game of Thrones");
161    }
162
163    /// Refering to issue https://github.com/jdrouet/tmdb-api/issues/25
164    #[tokio::test]
165    async fn fix_issue_25() {
166        let mut server = mockito::Server::new_async().await;
167        let client = Client::<ReqwestClient>::builder()
168            .with_api_key("secret".into())
169            .with_base_url(server.url())
170            .build()
171            .unwrap();
172
173        let _m = server
174            .mock("GET", super::PATH)
175            .match_query(Matcher::AllOf(vec![
176                Matcher::UrlEncoded("api_key".into(), "secret".into()),
177                Matcher::UrlEncoded("query".into(), "rick and morty".into()),
178            ]))
179            .with_status(200)
180            .with_header("content-type", "application/json")
181            .with_body(include_str!("../../assets/search-tv-rick-and-morty.json"))
182            .create_async()
183            .await;
184        let result = client
185            .search_tvshows("rick and morty", &Default::default())
186            .await
187            .unwrap();
188        assert_eq!(result.page, 1);
189        assert_eq!(result.total_pages, 1);
190        assert_eq!(result.total_results, 2);
191        let item = result.results.first().unwrap();
192        assert_eq!(item.inner.name, "Rick and Morty");
193    }
194
195    #[tokio::test]
196    async fn invalid_api_key() {
197        let mut server = mockito::Server::new_async().await;
198        let client = Client::<ReqwestClient>::builder()
199            .with_api_key("secret".into())
200            .with_base_url(server.url())
201            .build()
202            .unwrap();
203
204        let _m = server
205            .mock("GET", super::PATH)
206            .match_query(Matcher::AllOf(vec![
207                Matcher::UrlEncoded("api_key".into(), "secret".into()),
208                Matcher::UrlEncoded("query".into(), "Whatever".into()),
209            ]))
210            .with_status(401)
211            .with_header("content-type", "application/json")
212            .with_body(include_str!("../../assets/invalid-api-key.json"))
213            .create_async()
214            .await;
215        let err = client
216            .search_tvshows("Whatever", &Default::default())
217            .await
218            .unwrap_err();
219        let server_err = err.as_server_error().unwrap();
220        assert_eq!(server_err.status_code, 7);
221    }
222
223    #[tokio::test]
224    async fn resource_not_found() {
225        let mut server = mockito::Server::new_async().await;
226        let client = Client::<ReqwestClient>::builder()
227            .with_api_key("secret".into())
228            .with_base_url(server.url())
229            .build()
230            .unwrap();
231        let _m = server
232            .mock("GET", super::PATH)
233            .match_query(Matcher::AllOf(vec![
234                Matcher::UrlEncoded("api_key".into(), "secret".into()),
235                Matcher::UrlEncoded("query".into(), "Whatever".into()),
236            ]))
237            .with_status(404)
238            .with_header("content-type", "application/json")
239            .with_body(include_str!("../../assets/resource-not-found.json"))
240            .create_async()
241            .await;
242        let err = client
243            .search_tvshows("Whatever", &Default::default())
244            .await
245            .unwrap_err();
246        let server_err = err.as_server_error().unwrap();
247        assert_eq!(server_err.status_code, 34);
248    }
249}
250
251#[cfg(all(test, feature = "integration"))]
252mod integration_tests {
253    use crate::client::Client;
254    use crate::client::reqwest::Client as ReqwestClient;
255
256    #[tokio::test]
257    async fn search_simpsons() {
258        let secret = std::env::var("TMDB_TOKEN_V3").unwrap();
259        let client = Client::<ReqwestClient>::new(secret);
260        let result = client
261            .search_tvshows("simpsons", &Default::default())
262            .await
263            .unwrap();
264        assert_eq!(result.page, 1);
265        assert!(result.results.len() > 1);
266        assert!(result.total_pages > 0);
267        assert!(result.total_results > 1);
268        let item = result.results.first().unwrap();
269        assert_eq!(item.inner.name, "The Simpsons");
270    }
271}