├── .gitignore ├── .travis.yml ├── Cargo.toml ├── README.md └── src ├── error.rs ├── lib.rs └── query ├── mod.rs └── model.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: rust 3 | rust: 4 | - stable 5 | - beta 6 | - nightly 7 | matrix: 8 | allow_failures: 9 | - rust: nightly -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "omdb" 3 | version = "0.3.2" 4 | authors = ["Brandon Aldrich "] 5 | description = "OMDb API for Rust" 6 | documentation = "https://docs.rs/crate/omdb" 7 | homepage = "https://github.com/aldrio/omdb-rs" 8 | repository = "https://github.com/aldrio/omdb-rs" 9 | readme = "README.md" 10 | license = "MIT" 11 | edition = "2018" 12 | 13 | [dependencies.reqwest] 14 | version = "~0.11" 15 | default_features = false 16 | features = ["rustls-tls", "json"] 17 | 18 | [dependencies.serde] 19 | features = ["derive"] 20 | version = "~1.0" 21 | 22 | [dev-dependencies.tokio] 23 | features = ["macros"] 24 | version = "~1.0" 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [OMDb API](https://www.omdbapi.com) for Rust 2 | [![Build Status](https://travis-ci.org/aldrio/omdb-rs.svg?branch=master)](https://travis-ci.org/aldrio/omdb-rs) 3 | [![crates.io](https://img.shields.io/crates/v/omdb.svg?maxAge=2592000?style=plastic)](https://crates.io/crates/omdb) 4 | [![docs.rs](https://docs.rs/omdb/badge.svg)](https://docs.rs/crate/omdb/) 5 | 6 | Search movies, tv shows, and games using The Open Movie Database. 7 | 8 | ## Examples 9 | 10 | Find by title: 11 | 12 | ```rust 13 | let show = omdb::title("The Wizard of Oz") 14 | .apikey(APIKEY) 15 | .year(1939) 16 | .get() 17 | .await 18 | .unwrap(); 19 | 20 | assert!(show.imdb_id == "tt0032138"); 21 | ``` 22 | 23 | Find by IMDb ID: 24 | 25 | ```rust 26 | let movie = omdb::imdb_id("tt0111161") 27 | .apikey(APIKEY) 28 | .get() 29 | .await 30 | .unwrap(); 31 | 32 | assert!(movie.title == "The Shawshank Redemption"); 33 | ``` 34 | 35 | Search movies: 36 | 37 | ```rust 38 | use omdb::Kind; 39 | 40 | let movies = omdb::search("batman") 41 | .apikey(APIKEY) 42 | .kind(Kind::Movie) // Optionally filter results to movies only 43 | .get() 44 | .await 45 | .unwrap(); 46 | 47 | assert!(movies.total_results > 0); 48 | ``` 49 | 50 | ## Usage 51 | Add the crates.io `omdb` dependency to your Cargo.toml file. 52 | 53 | ```toml 54 | 55 | [dependencies] 56 | omdb = "*" 57 | 58 | ``` 59 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error as StdError; 2 | use std::fmt; 3 | 4 | use reqwest::StatusCode; 5 | 6 | #[derive(Debug)] 7 | pub enum Error { 8 | /// An error originating from Reqwest. 9 | Http(reqwest::Error), 10 | /// An unexpected HTTP status code. 11 | Status(StatusCode), 12 | /// An error from OMDb. 13 | Api(String), 14 | 15 | Other(&'static str), 16 | } 17 | 18 | impl StdError for Error { 19 | fn source(&self) -> Option<&(dyn StdError + 'static)> { 20 | match *self { 21 | Error::Http(ref err) => Some(err), 22 | _ => None, 23 | } 24 | } 25 | } 26 | 27 | impl From for Error { 28 | fn from(err: reqwest::Error) -> Error { 29 | Error::Http(err) 30 | } 31 | } 32 | 33 | impl fmt::Display for Error { 34 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 35 | match self { 36 | Error::Http(ref err) => err.fmt(f), 37 | Error::Status(status) => status.canonical_reason().unwrap_or("Unknown status").fmt(f), 38 | Error::Api(ref desc) => desc.fmt(f), 39 | Error::Other(desc) => desc.fmt(f), 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! OMDb API for Rust 2 | //! 3 | //! [Github Repo](https://github.com/aldrio/omdb-rs) 4 | use serde::{Deserialize, Serialize}; 5 | 6 | mod error; 7 | pub use error::Error; 8 | 9 | pub mod query; 10 | pub use query::imdb_id; 11 | pub use query::search; 12 | pub use query::title; 13 | 14 | /// A movie, series, episode, or game from OMDb. 15 | #[derive(Debug, Serialize, Deserialize)] 16 | pub struct Movie { 17 | pub title: String, 18 | pub year: String, 19 | pub rated: String, 20 | pub released: String, 21 | pub runtime: String, 22 | pub genre: String, 23 | pub director: String, 24 | pub writer: String, 25 | pub actors: String, 26 | pub plot: String, 27 | pub language: String, 28 | pub country: String, 29 | pub awards: String, 30 | pub poster: String, 31 | pub metascore: String, 32 | pub imdb_rating: String, 33 | pub imdb_votes: String, 34 | pub imdb_id: String, 35 | pub kind: Kind, 36 | } 37 | 38 | /// Search results from OMDb. 39 | #[derive(Debug)] 40 | pub struct SearchResults { 41 | pub results: Vec, 42 | pub total_results: usize, 43 | } 44 | 45 | /// A movie from an OMDb search. 46 | /// 47 | /// These contain less information than a regular `Movie`. 48 | #[derive(Debug)] 49 | pub struct SearchResultsMovie { 50 | pub title: String, 51 | pub year: String, 52 | pub imdb_id: String, 53 | pub poster: String, 54 | pub kind: Kind, 55 | } 56 | 57 | /// Distinguishes between the different types of media available. 58 | /// 59 | /// Note that `Kind` is the same thing as OMDb's `Type`. 60 | #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] 61 | pub enum Kind { 62 | Movie, 63 | Series, 64 | Episode, 65 | Game, 66 | } 67 | 68 | impl Kind { 69 | fn from_str(from: &str) -> Option { 70 | match from { 71 | "movie" => Some(Kind::Movie), 72 | "series" => Some(Kind::Series), 73 | "episode" => Some(Kind::Episode), 74 | "game" => Some(Kind::Game), 75 | _ => None, 76 | } 77 | } 78 | } 79 | 80 | impl From for &'static str { 81 | fn from(kind: Kind) -> &'static str { 82 | match kind { 83 | Kind::Movie => "movie", 84 | Kind::Series => "series", 85 | Kind::Episode => "episode", 86 | Kind::Game => "game", 87 | } 88 | } 89 | } 90 | 91 | /// Plot length. 92 | #[derive(Copy, Clone, Debug, PartialEq)] 93 | pub enum Plot { 94 | Short, 95 | Full, 96 | } 97 | 98 | impl From for &'static str { 99 | fn from(plot: Plot) -> &'static str { 100 | match plot { 101 | Plot::Short => "short", 102 | Plot::Full => "full", 103 | } 104 | } 105 | } 106 | 107 | #[cfg(test)] 108 | mod tests { 109 | use super::*; 110 | use std::env; 111 | 112 | #[tokio::test] 113 | async fn imdb_id() { 114 | let apikey = env::var("OMDB_APIKEY").expect("OMDB_APIKEY must be set"); 115 | let movie = super::imdb_id("tt0032138") 116 | .apikey(apikey) 117 | .year(1939) 118 | .get() 119 | .await 120 | .unwrap(); 121 | 122 | assert!(movie.title == "The Wizard of Oz"); 123 | } 124 | 125 | #[tokio::test] 126 | async fn title() { 127 | let apikey = env::var("OMDB_APIKEY").expect("OMDB_APIKEY must be set"); 128 | let show = super::title("silicon valley") 129 | .apikey(apikey) 130 | .year(2014) 131 | .kind(Kind::Series) 132 | .get() 133 | .await 134 | .unwrap(); 135 | 136 | assert!(show.imdb_id == "tt2575988"); 137 | } 138 | 139 | #[tokio::test] 140 | async fn search() { 141 | let apikey = env::var("OMDB_APIKEY").expect("OMDB_APIKEY must be set"); 142 | let search = super::search("Batman").apikey(apikey).get().await.unwrap(); 143 | 144 | assert!(search.total_results > 0); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/query/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | use std::borrow::Borrow; 3 | 4 | mod model; 5 | use self::model::{FindResponse, SearchResponse}; 6 | 7 | use crate::{Error, Kind, Movie, Plot, SearchResults}; 8 | 9 | /// A function to create and send a request to OMDb. 10 | async fn get_request(params: I) -> Result 11 | where 12 | I: IntoIterator, 13 | I::Item: Borrow<(K, V)> + Serialize, 14 | K: AsRef + Serialize, 15 | V: AsRef + Serialize, 16 | { 17 | const API_ENDPOINT: &str = "https://omdbapi.com"; 18 | const API_VERSION: &str = "1"; 19 | 20 | let params = params.into_iter().collect::>(); 21 | 22 | let response = reqwest::Client::new() 23 | .get(API_ENDPOINT) 24 | .query(&[("v", API_VERSION)]) 25 | .query(&[("r", "json")]) 26 | .query(¶ms) 27 | .send() 28 | .await?; 29 | 30 | let status = response.status(); 31 | 32 | if !status.is_success() { 33 | return Err(Error::Status(status)); 34 | } 35 | 36 | Ok(response) 37 | } 38 | 39 | /// Starts a new `FindQuery` with an imdb_id. 40 | /// 41 | /// This can be built upon to add other constraints while 42 | /// finding a movie on OMDb. 43 | /// Use this method when you want to select a single movie by *IMDb ID*. 44 | /// # Examples 45 | /// 46 | /// Find a movie using it's IMDb id: 47 | /// 48 | /// ``` 49 | /// # async fn test() { 50 | /// let apikey = std::env::var("OMDB_APIKEY").expect("OMDB_APIKEY must be set"); 51 | /// let movie = omdb::imdb_id("tt0032138") 52 | /// .apikey(apikey) 53 | /// .year(1939) 54 | /// .get() 55 | /// .await 56 | /// .unwrap(); 57 | /// 58 | /// assert!(movie.title == "The Wizard of Oz"); 59 | /// # } 60 | /// ``` 61 | pub fn imdb_id>(title: S) -> FindQuery { 62 | FindQuery { 63 | imdb_id: Some(title.into()), 64 | ..Default::default() 65 | } 66 | } 67 | 68 | /// Starts a new `FindQuery` with a title. 69 | /// 70 | /// This can be built upon to add other constraints while 71 | /// finding a movie on OMDb. 72 | /// Use this method when you want to select a single movie by *title*. 73 | /// # Examples 74 | /// 75 | /// Find a series using it's title: 76 | /// 77 | /// ``` 78 | /// # async fn test() { 79 | /// use omdb::Kind; 80 | /// let apikey = std::env::var("OMDB_APIKEY").expect("OMDB_APIKEY must be set"); 81 | /// 82 | /// let show = omdb::title("Silicon Valley") 83 | /// .apikey(apikey) 84 | /// .year(2014) 85 | /// .kind(Kind::Series) 86 | /// .get() 87 | /// .await 88 | /// .unwrap(); 89 | /// 90 | /// assert!(show.imdb_id == "tt2575988"); 91 | /// # } 92 | /// ``` 93 | pub fn title>(title: S) -> FindQuery { 94 | FindQuery { 95 | title: Some(title.into()), 96 | ..Default::default() 97 | } 98 | } 99 | 100 | /// Starts a new `SearchQuery`. 101 | /// 102 | /// This can be built upon to add other constraints while 103 | /// searchign for a movie on OMDb. 104 | /// Use this function when you're trying to select multiple movies 105 | /// that fit a set of constraints. 106 | /// # Examples 107 | /// 108 | /// Search for movies: 109 | /// 110 | /// ``` 111 | /// # async fn test() { 112 | /// let apikey = std::env::var("OMDB_APIKEY").expect("OMDB_APIKEY must be set"); 113 | /// let movies = omdb::search("batman").apikey(apikey).get().await.unwrap(); 114 | /// 115 | /// assert!(movies.total_results > 0); 116 | /// # } 117 | /// ``` 118 | pub fn search>(search: S) -> SearchQuery { 119 | SearchQuery { 120 | search: search.into(), 121 | ..Default::default() 122 | } 123 | } 124 | 125 | /// Represents a query being bulit for OMDb. 126 | /// Follows the Builder pattern. 127 | #[derive(Debug)] 128 | pub struct FindQuery { 129 | // One required 130 | imdb_id: Option, 131 | title: Option, 132 | 133 | apikey: Option, 134 | 135 | // Optional 136 | kind: Option, 137 | year: Option, 138 | plot: Option, // TODO: Season and Episode 139 | } 140 | 141 | impl Default for FindQuery { 142 | fn default() -> FindQuery { 143 | FindQuery { 144 | imdb_id: None, 145 | title: None, 146 | apikey: None, 147 | kind: None, 148 | year: None, 149 | plot: None, 150 | } 151 | } 152 | } 153 | 154 | impl FindQuery { 155 | /// Specify the kind of media. 156 | pub fn kind(&mut self, kind: Kind) -> &mut FindQuery { 157 | self.kind = Some(kind); 158 | self 159 | } 160 | 161 | /// Specify the year. 162 | pub fn year(&mut self, year: S) -> &mut FindQuery { 163 | self.year = Some(year.to_string()); 164 | self 165 | } 166 | 167 | pub fn apikey(&mut self, apikey: S) -> &mut FindQuery { 168 | self.apikey = Some(apikey.to_string()); 169 | self 170 | } 171 | 172 | /// Specify the plot length. 173 | pub fn plot(&mut self, plot: Plot) -> &mut FindQuery { 174 | self.plot = Some(plot); 175 | self 176 | } 177 | 178 | /// Perform OMDb Api request and attempt to find the movie 179 | /// this `FindQuery` is describing. 180 | pub async fn get(&self) -> Result { 181 | let mut params: Vec<(&str, String)> = Vec::new(); 182 | 183 | if let Some(i) = self.imdb_id.as_ref() { 184 | params.push(("i", i.clone())); 185 | } else if let Some(t) = self.title.as_ref() { 186 | params.push(("t", t.clone())); 187 | } 188 | 189 | if let Some(k) = self.apikey.as_ref() { 190 | params.push(("apikey", k.clone())); 191 | } 192 | 193 | if let Some(kind) = self.kind.as_ref() { 194 | let k: &str = (*kind).into(); 195 | params.push(("type", String::from(k))); 196 | } 197 | 198 | if let Some(year) = self.year.as_ref() { 199 | params.push(("y", year.clone())); 200 | } 201 | 202 | if let Some(plot) = self.plot.as_ref() { 203 | let p: &str = (*plot).into(); 204 | params.push(("plot", String::from(p))); 205 | } 206 | 207 | // Send our request 208 | let response: FindResponse = get_request(params).await?.json().await?; 209 | 210 | // Check if the Api's Response string equals true 211 | if response.response.to_lowercase() != "true" { 212 | // Return with the Api's Error field or "undefined" if empty 213 | return Err(Error::Api( 214 | response.error.unwrap_or_else(|| "undefined".to_owned()), 215 | )); 216 | } 217 | 218 | Ok(response.into()) 219 | } 220 | } 221 | 222 | /// Represents a query being bulit for OMDb. 223 | /// Follows the Builder pattern. 224 | #[derive(Debug)] 225 | pub struct SearchQuery { 226 | search: String, 227 | apikey: Option, 228 | 229 | // Optional 230 | kind: Option, 231 | year: Option, 232 | page: Option, 233 | } 234 | 235 | impl Default for SearchQuery { 236 | fn default() -> SearchQuery { 237 | SearchQuery { 238 | search: String::new(), 239 | apikey: None, 240 | kind: None, 241 | year: None, 242 | page: None, 243 | } 244 | } 245 | } 246 | 247 | impl SearchQuery { 248 | pub fn apikey(&mut self, apikey: S) -> &mut SearchQuery { 249 | self.apikey = Some(apikey.to_string()); 250 | self 251 | } 252 | 253 | /// Specify the kind of media. 254 | pub fn kind(&mut self, kind: Kind) -> &mut SearchQuery { 255 | self.kind = Some(kind); 256 | 257 | self 258 | } 259 | 260 | /// Specify the year. 261 | pub fn year(&mut self, year: S) -> &mut SearchQuery { 262 | self.year = Some(year.to_string()); 263 | self 264 | } 265 | 266 | /// Specify the page number. 267 | pub fn page(&mut self, page: usize) -> &mut SearchQuery { 268 | self.page = Some(page); 269 | self 270 | } 271 | 272 | /// Perform OMDb Api request and attempt to find the movie 273 | /// this `FindQuery` is describing. 274 | pub async fn get(&self) -> Result { 275 | let mut params: Vec<(&str, String)> = Vec::new(); 276 | 277 | params.push(("s", self.search.clone())); 278 | 279 | if let Some(k) = self.apikey.as_ref() { 280 | params.push(("apikey", k.clone())); 281 | } 282 | 283 | if let Some(kind) = self.kind.as_ref() { 284 | let k: &str = (*kind).into(); 285 | params.push(("type", String::from(k))); 286 | } 287 | 288 | if let Some(year) = self.year.as_ref() { 289 | params.push(("y", year.clone())); 290 | } 291 | 292 | if let Some(page) = self.page.as_ref() { 293 | params.push(("page", page.to_string())); 294 | } 295 | 296 | // Send our request 297 | let response: SearchResponse = get_request(params).await?.json().await?; 298 | 299 | // Check if the Api's Response string equals true 300 | if response.response.to_lowercase() != "true" { 301 | // Return with the Api's Error field or "undefined" if empty 302 | return Err(Error::Api( 303 | response.error.unwrap_or_else(|| "undefined".to_owned()), 304 | )); 305 | } 306 | 307 | Ok(response.into()) 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /src/query/model.rs: -------------------------------------------------------------------------------- 1 | use crate::{Kind, Movie, SearchResults, SearchResultsMovie}; 2 | use serde::Deserialize; 3 | 4 | #[derive(Debug, Deserialize)] 5 | pub struct FindResponse { 6 | #[serde(rename = "Response")] 7 | pub response: String, 8 | 9 | #[serde(rename = "Error")] 10 | pub error: Option, 11 | 12 | #[serde(rename = "Title")] 13 | pub title: Option, 14 | #[serde(rename = "Year")] 15 | pub year: Option, 16 | #[serde(rename = "Rated")] 17 | pub rated: Option, 18 | #[serde(rename = "Released")] 19 | pub released: Option, 20 | #[serde(rename = "Runtime")] 21 | pub runtime: Option, 22 | #[serde(rename = "Genre")] 23 | pub genre: Option, 24 | #[serde(rename = "Director")] 25 | pub director: Option, 26 | #[serde(rename = "Writer")] 27 | pub writer: Option, 28 | #[serde(rename = "Actors")] 29 | pub actors: Option, 30 | #[serde(rename = "Plot")] 31 | pub plot: Option, 32 | #[serde(rename = "Language")] 33 | pub language: Option, 34 | #[serde(rename = "Country")] 35 | pub country: Option, 36 | #[serde(rename = "Awards")] 37 | pub awards: Option, 38 | #[serde(rename = "Poster")] 39 | pub poster: Option, 40 | #[serde(rename = "Metascore")] 41 | pub metascore: Option, 42 | #[serde(rename = "imdbRating")] 43 | pub imdb_rating: Option, 44 | #[serde(rename = "imdbVotes")] 45 | pub imdb_votes: Option, 46 | #[serde(rename = "imdbID")] 47 | pub imdb_id: Option, 48 | #[serde(rename = "Type")] 49 | pub kind: Option, 50 | } 51 | 52 | impl From for Movie { 53 | fn from(find: FindResponse) -> Movie { 54 | Movie { 55 | title: find.title.unwrap_or_default(), 56 | year: find.year.unwrap_or_default(), 57 | rated: find.rated.unwrap_or_default(), 58 | released: find.released.unwrap_or_default(), 59 | runtime: find.runtime.unwrap_or_default(), 60 | genre: find.genre.unwrap_or_default(), 61 | director: find.director.unwrap_or_default(), 62 | writer: find.writer.unwrap_or_default(), 63 | actors: find.actors.unwrap_or_default(), 64 | plot: find.plot.unwrap_or_default(), 65 | language: find.language.unwrap_or_default(), 66 | country: find.country.unwrap_or_default(), 67 | awards: find.awards.unwrap_or_default(), 68 | poster: find.poster.unwrap_or_default(), 69 | metascore: find.metascore.unwrap_or_default(), 70 | imdb_rating: find.imdb_rating.unwrap_or_default(), 71 | imdb_votes: find.imdb_votes.unwrap_or_default(), 72 | imdb_id: find.imdb_id.unwrap_or_default(), 73 | kind: match find.kind { 74 | Some(kind_string) => match Kind::from_str(&kind_string) { 75 | Some(kind) => kind, 76 | None => Kind::Movie, 77 | }, 78 | None => Kind::Movie, 79 | }, 80 | } 81 | } 82 | } 83 | 84 | #[derive(Debug, Deserialize)] 85 | pub struct SearchResponse { 86 | #[serde(rename = "Response")] 87 | pub response: String, 88 | 89 | #[serde(rename = "Error")] 90 | pub error: Option, 91 | 92 | #[serde(rename = "Search")] 93 | pub search: Option>, 94 | #[serde(rename = "totalResults")] 95 | pub total_results: Option, 96 | } 97 | 98 | impl From for SearchResults { 99 | fn from(sr: SearchResponse) -> SearchResults { 100 | SearchResults { 101 | results: sr 102 | .search 103 | .unwrap_or_default() 104 | .into_iter() 105 | .map(|srm| srm.into()) 106 | .collect(), 107 | total_results: sr 108 | .total_results 109 | .map(|s| s.parse::().unwrap_or_default()) 110 | .unwrap_or_default(), 111 | } 112 | } 113 | } 114 | 115 | #[derive(Debug, Deserialize)] 116 | pub struct SearchResponseMovie { 117 | #[serde(rename = "Title")] 118 | pub title: Option, 119 | #[serde(rename = "Year")] 120 | pub year: Option, 121 | #[serde(rename = "imdbID")] 122 | pub imdb_id: Option, 123 | #[serde(rename = "Type")] 124 | pub kind: Option, 125 | #[serde(rename = "Poster")] 126 | pub poster: Option, 127 | } 128 | 129 | impl From for SearchResultsMovie { 130 | fn from(srm: SearchResponseMovie) -> SearchResultsMovie { 131 | SearchResultsMovie { 132 | title: srm.title.unwrap_or_default(), 133 | year: srm.year.unwrap_or_default(), 134 | poster: srm.poster.unwrap_or_default(), 135 | imdb_id: srm.imdb_id.unwrap_or_default(), 136 | kind: match srm.kind { 137 | Some(kind_string) => match Kind::from_str(&kind_string) { 138 | Some(kind) => kind, 139 | None => Kind::Movie, 140 | }, 141 | None => Kind::Movie, 142 | }, 143 | } 144 | } 145 | } 146 | --------------------------------------------------------------------------------