packages feed

themoviedb (empty) → 0.1.0.0

raw patch · 25 files changed

+866/−0 lines, 25 filesdep +HTTPdep +HUnitdep +aesonsetup-changed

Dependencies added: HTTP, HUnit, aeson, base, bytestring, network, old-locale, text, time, unix

Files

+ LICENSE view
@@ -0,0 +1,24 @@+# Copyright and Authors++    Copyright (C) 2012 Peter Jones <pjones@pmade.com>++# License (MIT)++    Permission is hereby granted, free of charge, to any person obtaining+    a copy of this software and associated documentation files (the+    "Software"), to deal in the Software without restriction, including+    without limitation the rights to use, copy, modify, merge, publish,+    distribute, sublicense, and/or sell copies of the Software, and to+    permit persons to whom the Software is furnished to do so, subject to+    the following conditions:++    The above copyright notice and this permission notice shall be+    included in all copies or substantial portions of the Software.++    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,+    EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF+    MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND+    NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE+    LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION+    OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION+    WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ Network/API/TheMovieDB.hs view
@@ -0,0 +1,73 @@+{-++This file is part of the Haskell package themoviedb. It is subject to+the license terms in the LICENSE file found in the top-level directory+of this distribution and at git://pmade.com/themoviedb/LICENSE. No+part of themoviedb package, including this file, may be copied,+modified, propagated, or distributed except according to the terms+contained in the LICENSE file.++-}++{- |++This library provides some data types and functions for fetching movie+metadata from <http://TheMovieDB.com>.  To use this library start by+requesting an API key from <http://docs.themoviedb.apiary.io>.++A typical workflow while using this library is:++  1. Place an API 'Key' inside a 'Context' using 'mkContext' or one of+     the utility functions in "Network.API.TheMovieDB.Util".++  2. Retrieve API 'Configuration' information using either the+     'config' function or the 'configErr' function.++  3. Search TheMovieDB using either the 'search' function or the+     'searchErr' function.++  4. Since the search functions don't return a full 'Movie' record+     follow them up with the 'fetch' function or the 'fetchErr'+     function.++This library also includes an example executable in the @example@+directory.+-}+module Network.API.TheMovieDB (+  -- * Types+  -- ** TheMovieDB Metadata+  Configuration(),+  Movie(..),+  Genre(..),++  -- ** Context and Errors+  Context(),+  Error(..),++  -- ** Type Synonyms+  Key,+  MovieID,+  GenreID,+  SearchQuery,++  -- * API Functions+  -- ** Functions That Fail+  config,+  fetch,+  search,++  -- ** Functions That Return Errors+  configErr,+  fetchErr,+  searchErr,++  -- ** Utility functions+  mkContext,+  apiKey,+  moviePosterURLs+  ) where++import Network.API.TheMovieDB.Types+import Network.API.TheMovieDB.Config+import Network.API.TheMovieDB.Fetch+import Network.API.TheMovieDB.Search
+ Network/API/TheMovieDB/Config.hs view
@@ -0,0 +1,23 @@+{-++This file is part of the Haskell package themoviedb. It is subject to+the license terms in the LICENSE file found in the top-level directory+of this distribution and at git://pmade.com/themoviedb/LICENSE. No+part of themoviedb package, including this file, may be copied,+modified, propagated, or distributed except according to the terms+contained in the LICENSE file.++-}+module Network.API.TheMovieDB.Config (configErr, config) where+import Network.API.TheMovieDB.Generic+import Network.API.TheMovieDB.Types++-- | Fetch the API configuration information such as base URLs for+-- movie posters.  Results in either an 'Error' or a 'Configuration'.+configErr :: Context -> IO (Either Error Configuration)+configErr ctx = getAndParse ctx "configuration" []++-- | Fetch the API configuration information or fail.  For a function+-- that returns an error instead of failing see 'configErr'.+config :: Context -> IO Configuration+config ctx = getOrFail $ configErr ctx
+ Network/API/TheMovieDB/Fetch.hs view
@@ -0,0 +1,24 @@+{-++This file is part of the Haskell package themoviedb. It is subject to+the license terms in the LICENSE file found in the top-level directory+of this distribution and at git://pmade.com/themoviedb/LICENSE. No+part of themoviedb package, including this file, may be copied,+modified, propagated, or distributed except according to the terms+contained in the LICENSE file.++-}+module Network.API.TheMovieDB.Fetch (fetchErr, fetch) where+import Network.API.TheMovieDB.Generic+import Network.API.TheMovieDB.Types++-- | Fetch the metadata for the movie with the given ID.  Returns+-- either an 'Error' or a 'Movie'.+fetchErr :: Context -> MovieID -> IO (Either Error Movie)+fetchErr ctx mid = getAndParse ctx ("movie/" ++ show mid) []++-- | Fetch the metadata for the movie with the given ID and fail if+-- any errors are encountered along the way.  For a function that+-- returns an error instead of failing see 'fetchErr'.+fetch :: Context -> MovieID -> IO Movie+fetch ctx mid = getOrFail $ fetchErr ctx mid
+ Network/API/TheMovieDB/Generic.hs view
@@ -0,0 +1,27 @@+{-++This file is part of the Haskell package themoviedb. It is subject to+the license terms in the LICENSE file found in the top-level directory+of this distribution and at git://pmade.com/themoviedb/LICENSE. No+part of themoviedb package, including this file, may be copied,+modified, propagated, or distributed except according to the terms+contained in the LICENSE file.++-}+module Network.API.TheMovieDB.Generic (getAndParse, getOrFail) where+import Network.API.TheMovieDB.Types.Context+import Network.API.TheMovieDB.Types.API+import Data.Aeson++-- Helper function to fetch and decode JSON.+getAndParse :: FromJSON a => Context -> Path -> Params -> IO (Either Error a)+getAndParse ctx path params =+  do httpResult <- ioFunc ctx (apiKey ctx) path params+     return $ case httpResult of+       Left  err  -> Left err+       Right body -> maybe (parseErr body) Right $ decode body+  where parseErr j = Left $ ParseError ("failed to parse JSON: " ++ show j)++-- Helper function to fail or return the right part of an either.+getOrFail :: FromJSON a => IO (Either Error a) -> IO a+getOrFail toCheck = toCheck >>= either (fail . show) return
+ Network/API/TheMovieDB/HTTP.hs view
@@ -0,0 +1,40 @@+{-++This file is part of the Haskell package themoviedb. It is subject to+the license terms in the LICENSE file found in the top-level directory+of this distribution and at git://pmade.com/themoviedb/LICENSE. No+part of themoviedb package, including this file, may be copied,+modified, propagated, or distributed except according to the terms+contained in the LICENSE file.++-}+module Network.API.TheMovieDB.HTTP+       ( apiGET+       ) where++import Network.API.TheMovieDB.Types.API+import qualified Network.HTTP as H+import Network.URI++-- The base URL for the version of the API we're using.+apiBaseURL :: String+apiBaseURL = "http://api.themoviedb.org/3/"++-- Build a Network.HTTP.Request that can be used to access the API.+mkAPIRequest :: Key -> Path -> Params -> H.Request Body+mkAPIRequest key path params =+  case parseURI url of+    Nothing  -> error ("generated an invalid URI: " ++ url)+    Just uri -> H.insertHeaders headers $ request uri+  where query   = H.urlEncodeVars $ params ++ [("api_key", key)]+        url     = apiBaseURL ++ path ++ "?" ++ query+        headers = [H.Header H.HdrAccept "application/json"]+        request = H.mkRequest H.GET++-- Build a URL and do an HTTP GET to TheMovieDB.+apiGET :: Key -> Path -> Params -> IO Response+apiGET key path params =+  do result <- H.simpleHTTP $ mkAPIRequest key path params+     return $ case result of+       Left err -> Left  $ NetworkError (show err)+       Right r  -> Right $ H.rspBody r
+ Network/API/TheMovieDB/Search.hs view
@@ -0,0 +1,49 @@+{-# LANGUAGE OverloadedStrings #-}++{-++This file is part of the Haskell package themoviedb. It is subject to+the license terms in the LICENSE file found in the top-level directory+of this distribution and at git://pmade.com/themoviedb/LICENSE. No+part of themoviedb package, including this file, may be copied,+modified, propagated, or distributed except according to the terms+contained in the LICENSE file.++-}+module Network.API.TheMovieDB.Search (searchErr, search) where+import Control.Applicative+import Control.Monad (liftM)+import Data.Aeson+import Network.API.TheMovieDB.Generic+import Network.API.TheMovieDB.Types++-- Internal wrapper to parse a list of movies from JSON.+newtype SearchResults = SearchResults+  {searchResults :: [Movie]} deriving (Eq, Show)++instance FromJSON SearchResults where+  parseJSON (Object v) = SearchResults <$> v .: "results"+  parseJSON _          = empty++-- Internal function to translate search results to a list of movies.+fetchSearchResults :: Context -> SearchQuery -> IO (Either Error SearchResults)+fetchSearchResults ctx query = getAndParse ctx "search/movie" [("query", query)]++-- | Search TheMovieDB using the given query string and return either+-- an 'Error' if something went wrong or a list of matching 'Movie's.+--+-- The movies returned will not have all their fields completely+-- filled out, to get a complete record you'll need to follow this+-- call up with a call to 'fetchErr' or 'fetch'.+searchErr :: Context -> SearchQuery -> IO (Either Error [Movie])+searchErr ctx query = liftM (fmap searchResults) $ fetchSearchResults ctx query++-- | Search TheMovieDB using the given query string and return a list+-- of movies.  This function fails if there are any errors.  For a+-- function that returns an error instead of failing see 'searchErr'.+--+-- The movies returned will not have all their fields completely+-- filled out, to get a complete record you'll need to follow this+-- call up with a call to 'fetchErr' or 'fetch'.+search :: Context -> SearchQuery -> IO [Movie]+search ctx query = getOrFail $ searchErr ctx query
+ Network/API/TheMovieDB/Types.hs view
@@ -0,0 +1,31 @@+{-++This file is part of the Haskell package themoviedb. It is subject to+the license terms in the LICENSE file found in the top-level directory+of this distribution and at git://pmade.com/themoviedb/LICENSE. No+part of themoviedb package, including this file, may be copied,+modified, propagated, or distributed except according to the terms+contained in the LICENSE file.++-}+module Network.API.TheMovieDB.Types+       ( Key+       , SearchQuery+       , Context(..)+       , Error(..)+       , ReleaseDate(..)+       , Configuration(..)+       , GenreID+       , Genre(..)+       , MovieID+       , Movie(..)+       , moviePosterURLs+       , mkContext+       ) where++import Network.API.TheMovieDB.Types.API+import Network.API.TheMovieDB.Types.Configuration+import Network.API.TheMovieDB.Types.Context+import Network.API.TheMovieDB.Types.Genre+import Network.API.TheMovieDB.Types.Movie+import Network.API.TheMovieDB.Types.ReleaseDate
+ Network/API/TheMovieDB/Types/API.hs view
@@ -0,0 +1,41 @@+{-++This file is part of the Haskell package themoviedb. It is subject to+the license terms in the LICENSE file found in the top-level directory+of this distribution and at git://pmade.com/themoviedb/LICENSE. No+part of themoviedb package, including this file, may be copied,+modified, propagated, or distributed except according to the terms+contained in the LICENSE file.++-}+module Network.API.TheMovieDB.Types.API+       ( Key+       , SearchQuery+       , Body+       , Response+       , Path+       , Params+       , IOFunc+       , Error(..)+       ) where++import qualified Data.ByteString.Lazy as B++-- | Type for the API key issued by TheMovieDB.+type Key = String++-- | A search query for TheMovieDB API.+type SearchQuery = String++-- | Possible errors returned by the API.+data Error+  = NetworkError String -- ^ Network or HTTP error.+  | ParseError   String -- ^ Invalid or error response from the API.+  deriving (Eq, Show)++-- Internal types.           +type Path     = String+type Params   = [(String, String)]+type Body     = B.ByteString+type Response = Either Error Body+type IOFunc   = Key -> Path -> Params -> IO Response           
+ Network/API/TheMovieDB/Types/Configuration.hs view
@@ -0,0 +1,42 @@+{-# LANGUAGE OverloadedStrings #-}++{-++This file is part of the Haskell package themoviedb. It is subject to+the license terms in the LICENSE file found in the top-level directory+of this distribution and at git://pmade.com/themoviedb/LICENSE. No+part of themoviedb package, including this file, may be copied,+modified, propagated, or distributed except according to the terms+contained in the LICENSE file.++-}+module Network.API.TheMovieDB.Types.Configuration+       ( Configuration(..)+       ) where++import Control.Applicative+import Data.Aeson+import Data.Aeson.Types (typeMismatch)++-- | TheMovieDB API tries to preserve bandwidth by omitting+-- information (such as full URLs for poster images) from most of the+-- API calls.  Therefore in order to construct a complete URL for a+-- movie poster you'll need to use the 'config' or 'configErr'+-- function to retrieve API configuration information.+--+-- A helper function is provided ('moviePosterURLs') that constructs a+-- list of all poster URLs given a 'Movie' and 'Configuration'.+data Configuration = Configuration+  { cfgImageBaseURL    :: String   -- ^ The base URL for images.+  , cfgImageSecBaseURL :: String   -- ^ Base URL for secure images.+  , cfgPosterSizes     :: [String] -- ^ List of possible image sizes.+  }++instance FromJSON Configuration where+  parseJSON (Object v) =+    Configuration <$> images  "base_url"+                  <*> images  "secure_base_url"+                  <*> imagesM "poster_sizes" []+    where images key      = (v .: "images") >>= (.:  key)+          imagesM key def = (v .: "images") >>= (\x -> x .:? key .!= def)+  parseJSON _ = empty
+ Network/API/TheMovieDB/Types/Context.hs view
@@ -0,0 +1,25 @@+{-++This file is part of the Haskell package themoviedb. It is subject to+the license terms in the LICENSE file found in the top-level directory+of this distribution and at git://pmade.com/themoviedb/LICENSE. No+part of themoviedb package, including this file, may be copied,+modified, propagated, or distributed except according to the terms+contained in the LICENSE file.++-}+module Network.API.TheMovieDB.Types.Context (Context(..), mkContext) where+import Network.API.TheMovieDB.Types.API+import Network.API.TheMovieDB.HTTP++-- | Data that needs to be given to the API functions.  Use the+-- 'mkContext' function to turn an API 'Key' into a 'Context'.+data Context = Context+  { apiKey :: Key    -- ^ Extract an API 'Key' from a 'Context'.+  , ioFunc :: IOFunc -- ^ The internal function that does the API IO.+  }++-- | Turns an API 'Key' into a 'Context' so that you can call the API+-- functions such as 'fetch' and 'search'.+mkContext :: Key -> Context+mkContext key = Context key apiGET
+ Network/API/TheMovieDB/Types/Genre.hs view
@@ -0,0 +1,29 @@+{-# LANGUAGE OverloadedStrings #-}++{-++This file is part of the Haskell package themoviedb. It is subject to+the license terms in the LICENSE file found in the top-level directory+of this distribution and at git://pmade.com/themoviedb/LICENSE. No+part of themoviedb package, including this file, may be copied,+modified, propagated, or distributed except according to the terms+contained in the LICENSE file.++-}+module Network.API.TheMovieDB.Types.Genre (GenreID, Genre(..)) where+import Control.Applicative+import Data.Aeson+import Data.Aeson.Types (typeMismatch)++-- | Type for representing unique genre IDs.+type GenreID = Int++-- | Metadata for a genre.+data Genre = Genre+  { genreID   :: GenreID -- ^ TheMovieDB unique ID.+  , genreName :: String  -- ^ The name of the genre.+  } deriving (Eq, Show)++instance FromJSON Genre where+  parseJSON (Object v) = Genre <$> v .: "id" <*> v .: "name"+  parseJSON v          = typeMismatch "Genre" v
+ Network/API/TheMovieDB/Types/Movie.hs view
@@ -0,0 +1,65 @@+{-# LANGUAGE OverloadedStrings #-}++{-++This file is part of the Haskell package themoviedb. It is subject to+the license terms in the LICENSE file found in the top-level directory+of this distribution and at git://pmade.com/themoviedb/LICENSE. No+part of themoviedb package, including this file, may be copied,+modified, propagated, or distributed except according to the terms+contained in the LICENSE file.++-}+module Network.API.TheMovieDB.Types.Movie+       (MovieID, Movie(..), moviePosterURLs) where++import Control.Applicative+import Control.Monad (liftM)+import Data.Aeson+import Data.Aeson.Types (typeMismatch)+import Data.Time (Day(..))+import Network.API.TheMovieDB.Types.Configuration (Configuration(..))+import Network.API.TheMovieDB.Types.Genre (Genre(..))+import Network.API.TheMovieDB.Types.ReleaseDate (ReleaseDate(..))++-- | Type for representing unique movie IDs.+type MovieID = Int++-- | Metadata for a movie.+--+--   * The 'moviePosterPath' is an incomplete URL.  To construct a+--     complete URL you'll need to use the 'Configuration' type.  You+--     can also use 'moviePosterURLs'.+data Movie = Movie+  { movieID          :: MovieID -- ^ TheMovieDB unique ID.+  , movieTitle       :: String  -- ^ The name/title of the movie.+  , movieOverview    :: String  -- ^ Short plot summary.+  , movieGenres      :: [Genre] -- ^ List of 'Genre's.+  , moviePopularity  :: Double  -- ^ Popularity ranking.+  , moviePosterPath  :: String  -- ^ Incomplete URL for poster image.+                                -- See 'moviePosterURLs'.+  , movieReleaseDate :: Day     -- ^ Movie release date.+  , movieAdult       :: Bool    -- ^ TheMovieDB adult movie flag.+  , movieIMDB        :: String  -- ^ IMDB.com ID.+  , movieRunTime     :: Int     -- ^ Movie length in minutes.+  } deriving (Eq, Show)++instance FromJSON Movie where+  parseJSON (Object v) =+    Movie <$> v .:  "id"+          <*> v .:  "title"+          <*> v .:? "overview"    .!= ""+          <*> v .:? "genres"      .!= []+          <*> v .:? "popularity"  .!= 0.0+          <*> v .:? "poster_path" .!= ""+          <*> liftM releaseDate (v .: "release_date")+          <*> v .:? "adult"       .!= False+          <*> v .:? "imdb_id"     .!= ""+          <*> v .:? "runtime"     .!= 0+  parseJSON _ = empty++-- | Return a list of URLs for all possible movie posters.+moviePosterURLs :: Configuration -> Movie -> [String]+moviePosterURLs c m = [base ++ size ++ poster | size <- cfgPosterSizes c]+  where base   = cfgImageBaseURL c+        poster = moviePosterPath m
+ Network/API/TheMovieDB/Types/ReleaseDate.hs view
@@ -0,0 +1,30 @@+{-# LANGUAGE OverloadedStrings #-}++{-++This file is part of the Haskell package themoviedb. It is subject to+the license terms in the LICENSE file found in the top-level directory+of this distribution and at git://pmade.com/themoviedb/LICENSE. No+part of themoviedb package, including this file, may be copied,+modified, propagated, or distributed except according to the terms+contained in the LICENSE file.++-}+module Network.API.TheMovieDB.Types.ReleaseDate (ReleaseDate(..)) where+import Data.Aeson+import Data.Aeson.Types (typeMismatch)+import Data.Text (unpack)+import Data.Time (parseTime, Day(..))+import System.Locale (defaultTimeLocale)++-- Type wrapper for Day to parse a movie's release date.+newtype ReleaseDate = ReleaseDate+  {releaseDate :: Day} deriving (Eq, Show)++-- Parse release dates in JSON.+instance FromJSON ReleaseDate where+  parseJSON (String t) =+    case parseTime defaultTimeLocale "%Y-%m-%d" (unpack t) of+      Just d -> return $ ReleaseDate d+      _      -> fail "could not parse release_date"+  parseJSON v = typeMismatch "ReleaseDate" v
+ Network/API/TheMovieDB/Util.hs view
@@ -0,0 +1,82 @@+{-++This file is part of the Haskell package themoviedb. It is subject to+the license terms in the LICENSE file found in the top-level directory+of this distribution and at git://pmade.com/themoviedb/LICENSE. No+part of themoviedb package, including this file, may be copied,+modified, propagated, or distributed except according to the terms+contained in the LICENSE file.++-}++{- |++This module provides some helper functions for loading an API 'Key' or+complete 'Context' from the user's environment or from the file+system.  Currently only POSIX systems are supported but patches are+welcome!++-}+module Network.API.TheMovieDB.Util (loadKey, loadContext) where+import Control.Monad (liftM, msum)+import Data.Char (isSpace)+import Network.API.TheMovieDB.HTTP+import Network.API.TheMovieDB.Types+import System.Environment (getEnv)+import System.IO.Error (isDoesNotExistError)+import System.Posix.Files+import System.Posix.User++-- Fetch the value of an environment variable without an exception.+getEnvMaybe :: String -> IO (Maybe String)+getEnvMaybe n = env `catch` err+  where env   = liftM Just $ getEnv n+        err e = if isDoesNotExistError e+                  then return Nothing+                  else ioError e++-- Expand @~@ at the front of a file path.+expandFile :: FilePath -> IO FilePath+expandFile ('~':rest) = do userID <- getEffectiveUserID+                           entry  <- getUserEntryForID userID+                           return  $ homeDirectory entry ++ rest+expandFile dir        = return dir++-- Return the contents of a file if it exists, otherwise Nothing.+readFileMaybe :: FilePath -> IO (Maybe String)+readFileMaybe n = do realName <- expandFile n+                     exists   <- fileExist realName+                     if exists+                       then liftM Just (readFirstLine realName)+                       else return Nothing+  where readFirstLine = liftM skipSpace . readFile+        skipSpace     = filter $ not . isSpace+++-- | Fetch an API 'Key' from the first of:+--+--     * @TMDB_KEY@ environment variable+--+--     * @XDG_CONFIG_HOME/tmdbkey@ file (where XDG_CONFIG_HOME is+--       an environment variable)+--+--     * @~\/.config\/tmdbkey@ file+--+--     * @~/.tmdbkey@ file+--+-- If the key can't be loaded from any of those places the result will+-- be 'Nothing'.+loadKey :: IO (Maybe Key)+loadKey = liftM msum . sequence $ [env, xdgConfig, config, home]+  where env       = getEnvMaybe "TMDB_KEY"+        config    = readFileMaybe "~/.config/tmdbkey"+        home      = readFileMaybe "~/.tmdbkey"+        xdgConfig = do dir <- getEnvMaybe "XDG_CONFIG_HOME"+                       case dir of+                         Nothing -> return Nothing+                         Just d  -> readFileMaybe (d ++ "/tmdbkey")++-- | Uses 'loadKey' to fetch an API 'Key' and wrap it into a default+-- 'Context' using 'mkContext'.+loadContext :: IO (Maybe Context)+loadContext = liftM (fmap mkContext) loadKey
+ README.md view
@@ -0,0 +1,28 @@+# TheMovieDB API for Haskell++This is a simple library that provides functions for retrieving movie+metadata from [TheMovieDB][] API.  To use this library you need to+request an API key from TheMovieDB.  Follow the directions on the+[API][] page.++[TheMovieDB]: http://themoviedb.com+[API]: http://docs.themoviedb.apiary.io++# Documentation++See the [Network.API.TheMovieDB][] module for complete documentation.++[Network.API.TheMovieDB]: https://github.com/pjones/themoviedb/blob/master/Network/API/TheMovieDB.hs++# Example++There's an [example][] application in the `example` directory.+Surprising, I know.++[example]: https://github.com/pjones/themoviedb/blob/master/example/Main.hs++# Warning++This library currently uses HTTP and not *HTTPS* while sending your+API key to TheMovieDB.  In order to fix this we'll need to use+something other than `Network.HTTP`.  Patches welcome!
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ TODO.org view
@@ -0,0 +1,5 @@+#+title: TheMovieDB To-do List++* Features+** TODO Switch to HTTPS/SSL/TLS+   - This requires using something other than Network.HTTP.
+ Test/TheMovieDB.hs view
@@ -0,0 +1,67 @@+-- http://www.haskell.org/haskellwiki/HUnit_1.0_User%27s_Guide+import Control.Monad (liftM, when)+import qualified Data.ByteString.Lazy.Char8 as B+import Network.API.TheMovieDB+import Network.API.TheMovieDB.Types.API+import Network.API.TheMovieDB.Types.Context+import System.Exit (exitFailure)+import Data.List (isPrefixOf)+import Data.Time (fromGregorian)++import Test.HUnit+  (Test(..), Counts(..),+   runTestTT, assertFailure,+   assertEqual, assertBool)++loadGoodMovieFile :: Key -> Path -> Params -> IO Response+loadGoodMovieFile _ _ _ =+  liftM Right $ fmap B.pack (readFile "Test/movie-good.json")++loadBadMovieFile :: Key -> Path -> Params -> IO Response+loadBadMovieFile _ _ _ =+  liftM Right $ fmap B.pack (readFile "Test/movie-bad.json")++fakeNetworkError :: Key -> Path -> Params -> IO Response+fakeNetworkError _ _ _ = return $ Left $ NetworkError "fake outage"++goodMovieFieldsTest = TestCase $ do+  result <- fetchErr (Context "" loadGoodMovieFile) 0+  case result of+    Left    err -> assertFailure $ show err+    Right movie -> do+      assertEqual "movieID" 105 (movieID movie)+      assertEqual "movieTitle" "Back to the Future" (movieTitle movie)+      assertBool "movieOverview" $+        "Eighties teenager" `isPrefixOf` movieOverview movie+      assertEqual "movieGenres" 4 (length $ movieGenres movie)+      assertEqual "moviePopularity" 80329.688 $ moviePopularity movie+      assertEqual "moviePosterPath" "/pTpxQB1N0waaSc3OSn0e9oc8kx9.jpg" $+        moviePosterPath movie+      assertEqual "movieReleaseDate" (fromGregorian 1985 7 3) $+        movieReleaseDate movie+      assertBool "movieAdult" $ not (movieAdult movie)+      assertEqual "movieIMDB" "tt0088763" $ movieIMDB movie+      assertEqual "movieRunTime" 116 $ movieRunTime movie++badMovieJSONTest = TestCase $ do+  result <- fetchErr (Context "" loadBadMovieFile) 0+  case result of+    Left (ParseError e) -> return ()+    _                   -> assertFailure "JSON should have been bad"++shouldHaveNetworkErrorTest = TestCase $ do+  result <- fetchErr (Context "" fakeNetworkError) 0+  case result of+    Left (NetworkError e) -> return ()+    _                     -> assertFailure "should have network error"++-- Why can't this be automatic?+unitTests = TestList+  [ TestLabel "All movie fields are parsed" goodMovieFieldsTest+  , TestLabel "Failure with bad JSON" badMovieJSONTest+  , TestLabel "Propagate network failure" shouldHaveNetworkErrorTest+  ]++main = do counts <- runTestTT unitTests+          let bad = errors counts + failures counts+          when (bad > 0) exitFailure
+ Test/config-good.json view
@@ -0,0 +1,1 @@+{"images":{"base_url":"http://cf2.imgobject.com/t/p/","secure_base_url":"https://d1zm4mmpsrckwj.cloudfront.net/t/p/","poster_sizes":["w92","w154","w185","w342","w500","original"],"backdrop_sizes":["w300","w780","w1280","original"],"profile_sizes":["w45","w185","h632","original"],"logo_sizes":["w45","w92","w154","w185","w300","w500","original"]},"change_keys":["adult","also_known_as","alternative_titles","biography","birthday","budget","cast","character_names","crew","deathday","general","genres","homepage","images","imdb_id","name","original_title","overview","plot_keywords","production_companies","production_countries","releases","revenue","runtime","spoken_languages","status","tagline","title","trailers","translations"]}
+ Test/movie-bad.json view
@@ -0,0 +1,1 @@+{"adult":1,"backdrop_path":"/x4N74cycZvKu5k3KDERJay4ajR3.jpg","belongs_to_collection":{"id":264,"name":"Back to the Future Collection","poster_path":"/qdVjh8PWGWqbssVqdwZIgr7Ckts.jpg","backdrop_path":"/c9C9Pg2QctyjZHRmS0P8rZg1OTA.jpg"},"budget":19000000,"genres":[{"id":12,"name":"Adventure"},{"id":35,"name":"Comedy"},{"id":878,"name":"Science Fiction"},{"id":10751,"name":"Family"}],"homepage":"http://www.bttfmovie.com/","id":105,"imdb_id":"tt0088763","original_title":"Back to the Future","overview":"Eighties teenager Marty McFly is accidentally sent back in time to 1955, inadvertently disrupting his parents' first meeting and attracting his mother's romantic interest. Marty must repair the damage to history by rekindling his parents' romance and - with the help of his eccentric inventor friend Doc Brown - returning to 1985.","popularity":80329.688,"poster_path":"/pTpxQB1N0waaSc3OSn0e9oc8kx9.jpg","production_companies":[{"name":"Universal Pictures","id":33},{"name":"Amblin Entertainment","id":56}],"production_countries":[{"iso_3166_1":"US","name":"United States of America"}],"release_date":"1985-07-03","revenue":381109762,"runtime":116,"spoken_languages":[{"iso_639_1":"en","name":"English"}],"status":"Released","tagline":"He's the only kid ever to get into trouble before he was born.","title":"Back to the Future","vote_average":8.9,"vote_count":123}
+ Test/movie-good.json view
@@ -0,0 +1,1 @@+{"adult":false,"backdrop_path":"/x4N74cycZvKu5k3KDERJay4ajR3.jpg","belongs_to_collection":{"id":264,"name":"Back to the Future Collection","poster_path":"/qdVjh8PWGWqbssVqdwZIgr7Ckts.jpg","backdrop_path":"/c9C9Pg2QctyjZHRmS0P8rZg1OTA.jpg"},"budget":19000000,"genres":[{"id":12,"name":"Adventure"},{"id":35,"name":"Comedy"},{"id":878,"name":"Science Fiction"},{"id":10751,"name":"Family"}],"homepage":"http://www.bttfmovie.com/","id":105,"imdb_id":"tt0088763","original_title":"Back to the Future","overview":"Eighties teenager Marty McFly is accidentally sent back in time to 1955, inadvertently disrupting his parents' first meeting and attracting his mother's romantic interest. Marty must repair the damage to history by rekindling his parents' romance and - with the help of his eccentric inventor friend Doc Brown - returning to 1985.","popularity":80329.688,"poster_path":"/pTpxQB1N0waaSc3OSn0e9oc8kx9.jpg","production_companies":[{"name":"Universal Pictures","id":33},{"name":"Amblin Entertainment","id":56}],"production_countries":[{"iso_3166_1":"US","name":"United States of America"}],"release_date":"1985-07-03","revenue":381109762,"runtime":116,"spoken_languages":[{"iso_639_1":"en","name":"English"}],"status":"Released","tagline":"He's the only kid ever to get into trouble before he was born.","title":"Back to the Future","vote_average":8.9,"vote_count":123}
+ Test/search-good.json view
@@ -0,0 +1,1 @@+{"page":1,"results":[{"adult":false,"backdrop_path":"/x4N74cycZvKu5k3KDERJay4ajR3.jpg","id":105,"original_title":"Back to the Future","release_date":"1985-07-03","poster_path":"/pTpxQB1N0waaSc3OSn0e9oc8kx9.jpg","popularity":80329.688,"title":"Back to the Future","vote_average":8.9,"vote_count":123},{"adult":false,"backdrop_path":"/snLFUFzWUZCdYkAbhZlaGJxiY3I.jpg","id":165,"original_title":"Back to the Future Part II","release_date":"1989-11-20","poster_path":"/k5dzvCQkXU2CAhLtlj9BHE7xmyK.jpg","popularity":11893.083,"title":"Back to the Future Part II","vote_average":8.4,"vote_count":56},{"adult":false,"backdrop_path":"/pP2a4MBQbIeZMbErBWMjxTT5Npb.jpg","id":196,"original_title":"Back to the Future Part III","release_date":"1990-05-25","poster_path":"/6DmgPTZYaug7QNDjOhUDWyjOQDl.jpg","popularity":11975.265,"title":"Back to the Future Part III","vote_average":8.4,"vote_count":59},{"adult":false,"backdrop_path":"/9gSVZIxWvFoEo03WylTHDSzpUvf.jpg","id":20803,"original_title":"Иван Васильевич меняет профессию","release_date":"1973-01-01","poster_path":"/f1y86qb0GAcUn0Vc7TQzglePxj8.jpg","popularity":0.576,"title":"Ivan Vasilievich: Back to the Future","vote_average":8.8,"vote_count":3},{"adult":false,"backdrop_path":"/4ag7bLf1edaEKuxKDQ5WB820PJw.jpg","id":12889,"original_title":"Futurama: The Beast with a Billion Backs","release_date":"2008-06-24","poster_path":"/8d1ljps74inNnsSP36iRjFpMbT2.jpg","popularity":3.424,"title":"Futurama: The Beast with a Billion Backs","vote_average":7.6,"vote_count":4},{"adult":false,"backdrop_path":null,"id":48499,"original_title":"Back to the Future Part 2 Behind-the-Scenes Special Presentation","release_date":"1989-01-01","poster_path":null,"popularity":0.011,"title":"Back to the Future Part 2 Behind-the-Scenes Special Presentation","vote_average":0.0,"vote_count":0},{"adult":false,"backdrop_path":null,"id":48501,"original_title":"Back to the Future: Making the Trilogy","release_date":"2002-01-01","poster_path":null,"popularity":0.01,"title":"Back to the Future: Making the Trilogy","vote_average":0.0,"vote_count":0},{"adult":false,"backdrop_path":null,"id":113268,"original_title":"The Secrets of the Back to the Future Trilogy","release_date":"1990-06-12","poster_path":"/sXxrpdsuyDDam1Jk2OSKTTEubXR.jpg","popularity":0.006,"title":"The Secrets of the Back to the Future Trilogy","vote_average":0.0,"vote_count":0}],"total_pages":1,"total_results":8}
+ example/Main.hs view
@@ -0,0 +1,66 @@+{-++This file is part of the Haskell package themoviedb. It is subject to+the license terms in the LICENSE file found in the top-level directory+of this distribution and at git://pmade.com/themoviedb/LICENSE. No+part of themoviedb package, including this file, may be copied,+modified, propagated, or distributed except according to the terms+contained in the LICENSE file.++-}+module Main where+import Control.Monad (when)+import Data.List (intercalate)+import Data.Maybe (isNothing, fromJust)+import Data.Time (toGregorian, formatTime)+import Network.API.TheMovieDB+import Network.API.TheMovieDB.Util (loadContext)+import System.Environment (getArgs)+import System.Exit (exitFailure)+import System.Locale (defaultTimeLocale)+import Text.Printf (printf)++-- Simple banner style printing of a 'Movie'+printMovieHeader :: Movie -> IO ()+printMovieHeader m =+  printf "%8d: %s (%s)\n" (movieID m) (movieTitle m) year+  where year = formatTime defaultTimeLocale "%Y" $ movieReleaseDate m++-- Print more detailed information for a 'Movie'.+printMovieDetails :: Movie -> IO ()+printMovieDetails m =+  do putStrLn $ "Popularity: " ++ show (moviePopularity m)+     putStrLn $ strJoin $ map genreName (movieGenres m)+     putStrLn "-- "+     putStrLn $ movieOverview m+  where strJoin = intercalate ", "++-- Well, of course, it's main!+main :: IO ()+main = do args <- getArgs+          contextM <- loadContext+          when (isNothing contextM) $ error "failed to load API key"+          let context = fromJust contextM+          cfg <- config context+          case args of+            ["key"]           -> putStrLn $ apiKey context++            ["search", query] -> do movies <- search context query+                                    mapM_ printMovieHeader movies++            ["fetch", mID]    -> do movie <- fetch context (read mID)+                                    printMovieHeader movie+                                    mapM_ putStrLn $ moviePosterURLs cfg movie+                                    printMovieDetails movie++            _                 -> do putStrLn usage+                                    exitFailure++  where usage = "Usage: tmdb {search QUERY|fetch ID|key}\n\n" +++                "Description:\n" +++                "       search: search TheMovieDB\n" +++                "       fetch:  fetch info about one movie\n" +++                "       key:    output your API key\n\n" +++                "Examples:\n" +++                "       tmdb search \"back to the future\"\n" +++                "       tmdb fetch 105"
+ themoviedb.cabal view
@@ -0,0 +1,89 @@+-- http://www.haskell.org/cabal/users-guide/developing-packages.html+name:          themoviedb+version:       0.1.0.0+synopsis:      Haskell API bindings for http://themoviedb.org+homepage:      http://github.com/pjones/themoviedb+bug-reports:   http://github.com/pjones/themoviedb/issues+license:       MIT+license-file:  LICENSE+author:        Peter Jones <pjones@pmade.com>+maintainer:    Peter Jones <pjones@pmade.com>+copyright:     Copyright: (c) 2012 Peter Jones <pjones@pmade.com>+category:      Network, API+stability:     experimental+tested-with:   GHC == 7.0.4+build-type:    Simple+cabal-version: >=1.8+description:   This library provides functions for retrieving metadata+               from the <http://TheMovieDB.org> API.  Documentation+               can be found in the "Network.API.TheMovieDB" module.++extra-source-files:+  README.md+  LICENSE+  TODO.org+  Test/config-good.json+  Test/movie-bad.json+  Test/movie-good.json+  Test/search-good.json+  Test/TheMovieDB.hs++library+  exposed-modules: Network.API.TheMovieDB,+                   Network.API.TheMovieDB.Util+  other-modules:   Network.API.TheMovieDB.Config,+                   Network.API.TheMovieDB.Fetch,+                   Network.API.TheMovieDB.Search,+                   Network.API.TheMovieDB.Generic,+                   Network.API.TheMovieDB.HTTP,+                   Network.API.TheMovieDB.Types,+                   Network.API.TheMovieDB.Types.API,+                   Network.API.TheMovieDB.Types.Configuration,+                   Network.API.TheMovieDB.Types.Context,+                   Network.API.TheMovieDB.Types.Genre,+                   Network.API.TheMovieDB.Types.Movie,+                   Network.API.TheMovieDB.Types.ReleaseDate++  build-depends:+    base ==4.3.*,+    old-locale ==1.0.*,+    time ==1.2.*,+    HTTP ==4000.2.*,+    network ==2.3.*,+    bytestring ==0.9.*,+    text ==0.11.*,+    aeson ==0.6.*,+    unix ==2.4.*++executable tmdb+  main-is: example/Main.hs++  build-depends:+    base,+    unix,+    old-locale,+    time,+    text,+    aeson,+    network,+    HTTP,+    bytestring++test-suite test+  type: exitcode-stdio-1.0+  main-is: Test/TheMovieDB.hs+  build-depends:+    base,+    unix,+    old-locale,+    time,+    text,+    aeson,+    network,+    HTTP,+    bytestring,+    HUnit++source-repository head+  type: git+  location: git://github.com/pjones/themoviedb.git