module Network.Lastfm.Response
(
Secret(..)
, sign
, Connection
, withConnection
, newConnection
, closeConnection
, lastfm
, lastfm_
, Supported
, Format(..)
, LastfmError(..)
, _LastfmBadResponse
, _LastfmEncodedError
, _LastfmHttpError
#ifdef TEST
, parse
, md5
#endif
) where
import Control.Applicative
import Control.Exception (SomeException(..), Exception(..), bracket, catch)
import Crypto.Classes (hash')
import Data.Aeson ((.:), Value(..), decode)
import Data.Aeson.Types (parseMaybe)
import qualified Data.ByteString.Lazy as Lazy
import qualified Data.ByteString as Strict
import Data.Digest.Pure.MD5 (MD5Digest)
import Data.Map (Map)
import qualified Data.Map as M
import Data.Monoid
import Data.Profunctor (Choice, dimap, right')
import Data.String (IsString(..))
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.Text.Read as T
import Data.Typeable (Typeable, cast)
import qualified Network.HTTP.Client as Http
import qualified Network.HTTP.Client.TLS as Http
import Text.XML (Document, parseLBS, def)
import Text.XML.Cursor
import Network.Lastfm.Internal
class Supported f r | f -> r, r -> f where
prepareRequest :: R f -> R f
parseResponseBody :: Lazy.ByteString -> Maybe r
parseResponseEncodedError :: r -> Maybe LastfmError
instance Supported JSON Value where
prepareRequest r = r { _query = M.singleton "format" "json" `M.union` _query r }
parseResponseBody = decode
parseResponseEncodedError = parseMaybe $ \(Object o) -> do
code <- o .: "error"
msg <- o .: "message"
return (LastfmEncodedError code msg)
instance Supported XML Document where
prepareRequest = id
parseResponseBody = either (const Nothing) Just . parseLBS def
parseResponseEncodedError doc = case fromDocument doc of
cur
| [mcode] <- cur $| element "lfm" >=> child >=> element "error" >=> attribute "code"
, Right (code, _) <- T.decimal mcode
, [msg] <- cur $| element "lfm" >=> child >=> element "error" >=> child >=> content
-> Just (LastfmEncodedError code (T.strip msg))
|
otherwise -> Nothing
parse :: Supported f r => Lazy.ByteString -> Either LastfmError r
parse body = case parseResponseBody body of
Just v
| Just e <- parseResponseEncodedError v -> Left e
| otherwise -> Right v
Nothing -> Left (LastfmBadResponse body)
base :: R f
base = R
{ _host = "https://ws.audioscrobbler.com/2.0/"
, _method = "GET"
, _query = mempty
}
data LastfmError =
LastfmBadResponse Lazy.ByteString
| LastfmEncodedError Int Text
| LastfmHttpError Http.HttpException
deriving (Show, Typeable)
instance Eq LastfmError where
LastfmBadResponse bs == LastfmBadResponse bs' = bs == bs'
LastfmEncodedError e s == LastfmEncodedError e' t = e == e' && s == t
LastfmHttpError _ == LastfmHttpError _ = True
_ == _ = False
instance Exception LastfmError where
fromException e@(SomeException se)
| Just e' <- fromException e = Just (LastfmHttpError e')
| otherwise = cast se
class AsLastfmError t where
_LastfmError :: (Choice p, Applicative m) => p LastfmError (m LastfmError) -> p t (m t)
instance AsLastfmError LastfmError where
_LastfmError = id
instance AsLastfmError SomeException where
_LastfmError = dimap (\e -> maybe (Left e) Right (fromException e)) (either pure (fmap toException)) . right'
_LastfmBadResponse
:: (Choice p, Applicative m, AsLastfmError e)
=> p Lazy.ByteString (m Lazy.ByteString) -> p e (m e)
_LastfmBadResponse = _LastfmError . dimap go (either pure (fmap LastfmBadResponse)) . right' where
go (LastfmBadResponse bs) = Right bs
go x = Left x
_LastfmEncodedError
:: (Choice p, Applicative m, AsLastfmError e)
=> p (Int, Text) (m (Int, Text)) -> p e (m e)
_LastfmEncodedError = _LastfmError . dimap go (either pure (fmap (uncurry LastfmEncodedError))) . right' where
go (LastfmEncodedError n v) = Right (n, v)
go x = Left x
_LastfmHttpError
:: (Choice p, Applicative m, AsLastfmError e)
=> p Http.HttpException (m Http.HttpException) -> p e (m e)
_LastfmHttpError = _LastfmError . dimap go (either pure (fmap LastfmHttpError)) . right' where
go (LastfmHttpError e) = Right e
go x = Left x
newtype Secret = Secret Text deriving (Show, Eq, Typeable)
instance IsString Secret where
fromString = Secret . fromString
sign :: Secret -> Request f Sign -> Request f Ready
sign s = coerce . (<* signature)
where
signature = wrap $
\r@R { _query = q } -> r { _query = apiSig s . authToken $ q }
authToken :: Map Text Text -> Map Text Text
authToken q = maybe q (M.delete "password") $ do
password <- M.lookup "password" q
username <- M.lookup "username" q
return (M.insert "authToken" (md5 (username <> md5 password)) q)
apiSig :: Secret -> Map Text Text -> Map Text Text
apiSig (Secret s) q = M.insert "api_sig" (signer (foldr M.delete q ["format", "callback"])) q
where
signer = md5 . M.foldrWithKey (\k v xs -> k <> v <> xs) s
md5 :: Text -> Text
md5 = T.pack . show . (hash' :: Strict.ByteString -> MD5Digest) . T.encodeUtf8
newtype Connection = Connection Http.Manager
withConnection :: (Connection -> IO a) -> IO a
withConnection = bracket newConnection closeConnection
newConnection :: IO Connection
newConnection = Connection <$> Http.newManager Http.tlsManagerSettings
closeConnection :: Connection -> IO ()
closeConnection (Connection man) = Http.closeManager man
lastfm :: Supported f r => Connection -> Request f Ready -> IO (Either LastfmError r)
lastfm man = lastfmWith man parse . finalize
lastfm_ :: Supported f r => Connection -> Request f Ready -> IO (Either LastfmError ())
lastfm_ man = lastfmWith man (\_ -> Right ()) . finalize
lastfmWith
:: Supported f r
=> Connection
-> (Lazy.ByteString -> Either LastfmError a)
-> R f
-> IO (Either LastfmError a)
lastfmWith (Connection man) p r = do
req <- Http.parseUrl (render r)
let req' = req
{ Http.method = _method r
, Http.responseTimeout = Just 10000000
}
p . Http.responseBody <$> Http.httpLbs req' man
`catch`
(return . Left)
finalize :: Supported f r => Request f Ready -> R f
finalize x = (prepareRequest . unwrap x) base