1#![allow(unused_parens)]
2#![warn(clippy::future_not_send)]
3use cervine::Cow;
4use compact_str::format_compact;
5use compact_str::CompactString;
6use compact_str::ToCompactString;
7use itertools::Itertools;
8pub use reqwest::Error;
9use serde::de::DeserializeOwned;
10
11mod model;
12use model::FindResult;
13pub use model::{Movie, MovieSearchResult, TV, TVExternalIds, TVSearchResult};
14
15#[cfg(test)]
16mod integration_tests;
17
18const BASE_URL: &str = "https://api.themoviedb.org/3";
19
20#[derive(Debug, Clone)]
21pub struct Client {
22 http: reqwest::Client,
23 api_key: String,
24 language: CompactString
25}
26
27#[inline]
28fn compact_str_url(k: &str, v: &str) -> CompactString {
29 format_compact!("{k}={v}")
30}
31
32impl Client {
33 pub fn new(api_key: String) -> Self {
34 Self::with_language(api_key, "en")
35 }
36
37 #[inline]
38 pub fn with_language(api_key: String, language: &str) -> Self {
39 Self{
40 http: reqwest::Client::new(),
41 api_key,
42 language: language.into()
43 }
44 }
45
46 #[inline]
47 async fn get<T: DeserializeOwned>(&self, path: &str, args: &[(&'static str, Cow<'_, CompactString, str>)]) -> Result<T, Error> {
48 let url = format!(
49 "{}{}?api_key={}&language={}&{}",
50 BASE_URL,
51 path,
52 self.api_key,
53 self.language,
54 args.iter().map(|(k, v)| compact_str_url(k, v)).join("&")
55 );
56 self.http.get(url).send().await?.json().await
57 }
58
59 #[inline]
60 pub async fn movie_search(&self, title: &str, year: Option<u16>) -> Result<MovieSearchResult, Error> {
61 let mut args = Vec::with_capacity(3);
62 args.push(("query", Cow::Borrowed(title)));
63 if let Some(year) = year {
64 args.push(("year", Cow::Owned(year.to_compact_string())));
65 }
66 args.push(("append_to_response", Cow::Borrowed("images")));
67 self.get("/search/movie", &args).await
68 }
69
70 #[inline]
71 pub async fn movie_by_id(&self, id: u32, include_videos: bool, include_credits: bool) -> Result<Movie, Error> {
72 let args = match (include_videos, include_credits) {
73 (false, false) => None,
74 (true, false) => Some(("append_to_response", Cow::Borrowed("videos"))),
75 (false, true) => Some(("append_to_response", Cow::Borrowed("credits"))),
76 (true, true) => Some(("append_to_response", Cow::Borrowed("videos,credits")))
77 };
78 let path = format_compact!("/movie/{}", id);
79 self.get(&path, args.as_ref().map(core::slice::from_ref).unwrap_or_default()).await
80 }
81
82 #[inline]
83 pub async fn movie_by_imdb_id(&self, id: u32) -> Result<Movie, Error> {
84 let path = format_compact!("/find/tt{:07}", id);
85 let result: FindResult = self.get(&path, &[
86 ("external_source", Cow::Borrowed("imdb_id")),
87 ("append_to_response", Cow::Borrowed("images"))
88 ]).await?;
89 self.movie_by_id(result.movie_results()[0].id(), false, false).await
90 }
91
92 #[inline]
93 pub async fn tv_search(&self, title: &str, year: Option<u16>) -> Result<TVSearchResult, Error> {
94 let mut args = Vec::with_capacity(3);
95 args.push(("query", Cow::Borrowed(title)));
96 if let Some(year) = year {
97 args.push(("year", Cow::Owned(year.to_compact_string())));
98 }
99 args.push(("append_to_response", Cow::Borrowed("images")));
100 self.get("/search/tv", &args).await
101 }
102
103 #[inline]
104 pub async fn tv_by_id(&self, id: u32, include_videos: bool, include_credits: bool) -> Result<TV, Error> {
105 let args = match (include_videos, include_credits) {
106 (false, false) => None,
107 (true, false) => Some(("append_to_response", Cow::Borrowed("videos"))),
108 (false, true) => Some(("append_to_response", Cow::Borrowed("credits"))),
109 (true, true) => Some(("append_to_response", Cow::Borrowed("videos,credits")))
110 };
111 let path = format_compact!("/tv/{}", id);
112 self.get(&path, args.as_ref().map(core::slice::from_ref).unwrap_or_default()).await
113 }
114
115 #[inline]
116 pub async fn tv_by_imdb_id(&self, id: u32) -> Result<TV, Error> {
117 let path = format_compact!("/find/tt{:07}", id);
118 let result: FindResult = self.get(&path, &[
119 ("external_source", Cow::Borrowed("imdb_id")),
120 ("append_to_response", Cow::Borrowed("images"))
121 ]).await?;
122 self.tv_by_id(result.tv_results()[0].id(), false, false).await
123 }
124
125 #[inline]
126 pub async fn tv_by_tvdb_id(&self, id: u32) -> Result<TV, Error> {
127 let path = format_compact!("/find/{}", id);
128 let result: FindResult = self.get(&path, &[
129 ("external_source", Cow::Borrowed("tvdb_id")),
130 ("append_to_response", Cow::Borrowed("images"))
131 ]).await?;
132 self.tv_by_id(result.tv_results()[0].id(), false, false).await
133 }
134
135 #[inline]
136 pub async fn tv_external_ids(&self, id: u32) -> Result<TVExternalIds, Error> {
137 let path = format_compact!("/tv/{}/external_ids", id);
138 self.get(&path, &[]).await
139 }
140}
141