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