packages feed

liblastfm 0.3.0.0 → 0.3.2.0

raw patch · 6 files changed

+204/−61 lines, 6 filesdep +tagged

Dependencies added: tagged

Files

+ CHANGELOG.md view
@@ -0,0 +1,15 @@+0.3.2.0+=======++  * Fix `auth.getMobileSession` method+  * Provide `lastfm_` function for making requests without parsing response. That is most useful for `POST` requests like `N.L.Track.love`++0.3.0.0+=======++  * Support batch operations for `N.L.Track.scrobble`, `N.L.Library.{addAlbum,addArtist}`++0.2.0.0+=======++  * Vastly simplified internal representations leading to simpler library interface
+ README.md view
@@ -0,0 +1,101 @@+# liblastfm+[![Hackage](https://budueba.com/hackage/liblastfm)](http://hackage.haskell.org/package/liblastfm)+[![Build Status](https://drone.io/github.com/supki/liblastfm/status.png)](https://drone.io/github.com/supki/liblastfm/latest)+[![Build Status](https://secure.travis-ci.org/supki/liblastfm.png?branch=develop)](http://travis-ci.org/supki/liblastfm)++Complete API interface to [last.fm][1] service.+Documentation is available in two flavours:+  * original [API reference][2]+  * liblastfm [haddocks][3]++## General introduction+liblastfm provides Applicative interface for constructing requests. Also, it handles all machinery needed to prepare request for sending:+  * [signing][4]+  * url-encoding+  * miscellaneous stuff like choosing correct HTTP method, etc++Once request is ready, liblastfm may send it and get you back a response.+Response format might be:+  * `Maybe Value` from [aeson][5] for json queries (nice interaction with [aeson-lens][6] for free!)+  * raw `ByteString` for xml queries++## Installation+To install either use hackage:++    % cabal install liblastfm++Or git:++    % git clone git@github.com:supki/liblastfm+    % cd liblastfm+    % cabal install++## Usage+Suppose, you need to use [`tag.search`](http://www.last.fm/api/show/tag.search) API method.+First find it in liblastfm: `Tag` would be the name of the module and `search` would be the name of function. [Here it is][7].+So import a couple of modules:++    ghci> import Network.Lastfm -- a bunch of useful utilities+    ghci> import qualified Network.Lastfm.Tag as Tag -- for Tag.search++Now you may you applicative `<*>` for required and `<*` or `*>` for optional parameters to construct+desired request:++    Tag.search <*> tag "russian-folk" <* limit 3 <*> apiKey "29effec263316a1f8a97f753caaa83e0" <* json++To send constructed request use `lastfm`:++    ghci> lastfm $ Tag.search <*> tag "russian-folk" <* limit 10 <*> apiKey "29effec263316a1f8a97f753caaa83e0" <* json+    Just (Object fromList [("results",Object fromList [("tagmatches", ...++How to parse responses is described [in wiki][8].++## FAQ+**Q: I'm getting the following error. How do I fix it?**+```+> Artist.getInfo <*> artist "Pink Floyd" <*> apiKey "29effec263316a1f8a97f753caaa83e0"++<interactive>:8:27:+    Couldn't match expected type `Data.Text.Lazy.Internal.Text'+                with actual type `[Char]+```+A: This means you haven't OverloadedStrings extension enabled.+To enable it (either one works):+  * type in `:set -XOverloadedStrings` while in ghci session.+  * add `{-# LANGUAGE OverloadedStrings #-}` to the top of the file+  * compile with `-XOverloadedStrings` switch++**Q: I'm getting the following error. How do I fix it?**+```+> lastfm (Artist.getInfo <*> artist "Pink Floyd" <*> apiKey "29effec263316a1f8a97f753caaa83e0")++<interactive>:13:1:+    No instance for (Network.Lastfm.Response.Supported f0)+      arising from a use of `lastfm'+    The type variable `f0' is ambiguous+    Possible fix: add a type signature that fixes these type variable(s)+    Note: there are several potential instances:+      instance Network.Lastfm.Response.Supported 'XML+        -- Defined at src/Network/Lastfm/Response.hs:66:10+      instance Network.Lastfm.Response.Supported 'JSON+        -- Defined at src/Network/Lastfm/Response.hs:51:10+```+A: This error message indicates that GHC cannot infer response format for `Request`.+To fix it, add use `json` or `xml` helpers, depending on your needs++```+> lastfm (Artist.getInfo <*> artist "Pink Floyd" <*> apiKey "29effec263316a1f8a97f753caaa83e0" <* json)+Just (Object fromList [("artist" ...+> lastfm (Artist.getInfo <*> artist "Pink Floyd" <*> apiKey "29effec263316a1f8a97f753caaa83e0" <* xml)+"<?xml version=\"1.0\" ...+```+-++ [1]: http://www.last.fm/+ [2]: http://www.last.fm/api/intro+ [3]: http://supki.github.com/liblastfm/+ [4]: http://www.last.fm/api/authspec#8+ [5]: http://hackage.haskell.org/package/aeson+ [6]: http://hackage.haskell.org/package/aeson-lens+ [7]: http://supki.github.com/liblastfm/Network-Lastfm-Tag.html#v:search+ [8]: https://github.com/supki/liblastfm/wiki/How-to-parse-JSON-response
liblastfm.cabal view
@@ -1,33 +1,41 @@-name: liblastfm-version: 0.3.0.0-synopsis: Lastfm API interface-license: MIT-license-file: LICENSE-author: Matvey Aksenov, Dmitry Malikov-maintainer: Matvey Aksenov <matvey.aksenov@gmail.com>-category: Network APIs+name:          liblastfm+version:       0.3.2.0+synopsis:      Lastfm API interface+license:       MIT+license-file:  LICENSE+author:        Matvey Aksenov, Dmitry Malikov+maintainer:    Matvey Aksenov <matvey.aksenov@gmail.com>+category:      Network APIs description:   Provides interface to Lastfm REST API, supports XML and JSON formats. cabal-version: >= 1.10-build-type: Simple+build-type:    Simple+extra-source-files:+  README.md+  CHANGELOG.md +source-repository head+  type:     git+  location: https://github.com/supki/liblastfm+ library   default-language: Haskell2010   build-depends:-    aeson,-    base >= 3 && < 5,-    bytestring,-    cereal,-    containers >= 0.5,-    contravariant,-    crypto-api,-    http-conduit >= 1.9,-    http-types,-    network,-    pureMD5,-    semigroups,-    text,-    void+      aeson+    , base >= 3 && < 5+    , bytestring+    , cereal+    , containers >= 0.5+    , contravariant+    , crypto-api+    , http-conduit >= 1.9+    , http-types+    , network+    , pureMD5+    , semigroups+    , tagged+    , text+    , void   hs-source-dirs: src   exposed-modules:     Network.Lastfm@@ -53,8 +61,3 @@     -Wall     -fno-warn-unused-do-bind     -funbox-strict-fields---source-repository head-  type:     git-  location: https://github.com/supki/liblastfm
src/Network/Lastfm/Authentication.hs view
@@ -4,11 +4,11 @@ -- -- Basically, lastfm provides 3 ways to authenticate user: -----  - web application - <http://www.lastfm.ru/api/webauth>+--  - web application - <http://www.last.fm/api/webauth> -----  - desktop application - <http://www.lastfm.ru/api/desktopauth>+--  - desktop application - <http://www.last.fm/api/desktopauth> -----  - modile application - <http://www.lastfm.ru/api/mobileauth>+--  - modile application - <http://www.last.fm/api/mobileauth> -- -- Note that you can use any of them in your -- application despite their names@@ -31,6 +31,7 @@   , link   ) where +import Control.Applicative ((<*)) import Data.Monoid  import Network.Lastfm.Internal@@ -45,7 +46,7 @@  -- | Get session key getMobileSession :: Request f (Username -> Password -> APIKey -> Sign)-getMobileSession = api "auth.getMobileSession"+getMobileSession = api "auth.getMobileSession" <* post {-# INLINE getMobileSession #-}  
src/Network/Lastfm/Response.hs view
@@ -1,23 +1,23 @@ {-# LANGUAGE DataKinds #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeFamilies #-} -- | Request sending and Response parsing module Network.Lastfm.Response   ( -- * Sign Request     -- $sign     Secret(..), sign-    -- * Get Response-  , Response, lastfm, lastfm', finalize+    -- * Get 'Response'+  , Response, Supported, lastfm, lastfm_+    -- ** Internal+  , lastfm', finalize   ) where  import Control.Applicative import Control.Exception (throw) import Control.Monad import Data.Monoid-import Data.String (IsString)+import Data.String (IsString(..))+import Data.Proxy (Proxy(..))  import           Crypto.Classes (hash') import           Data.Aeson ((.:), Value, decode, parseJSON)@@ -25,6 +25,7 @@ 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.Text (Text) import qualified Data.Text as T@@ -44,9 +45,13 @@ -- described at <http://www.last.fm/api/authspec#8>  +-- | 'Supported' provides parsing for a chosen 'Format'+--+-- 'JSON' is parsed to aeson's 'Value', 'XML' is to lazy 'ByteString'+-- (in other words, parsing XML is left to the user) class Supported (f :: Format) where   type Response f-  parse :: R f -> Lazy.ByteString -> C.ResponseHeaders -> Response f+  parse :: proxy f -> Lazy.ByteString -> C.ResponseHeaders -> Response f   base :: R f  @@ -54,8 +59,8 @@   type Response JSON = Maybe Value   parse _ b hs = do     v <- decode b-    case parseMaybe ((.: "error") <=< parseJSON) v of-      Just (_ :: Int) ->+    case parseMaybe ((.: "error") <=< parseJSON) v :: Maybe Int of+      Just _ ->         throw (C.StatusCodeException C.status400 (("Response", Strict.concat $ Lazy.toChunks b) : hs) (C.createCookieJar []))       _ -> return v   base = R@@ -68,6 +73,7 @@ instance Supported XML where   type Response XML = Lazy.ByteString   parse _ b _ = b+  {-# INLINE parse #-}   base = R     { _host = "https://ws.audioscrobbler.com/2.0/"     , _method = "GET"@@ -77,37 +83,54 @@   -- | Application secret-newtype Secret = Secret Text deriving (Show, IsString)+newtype Secret = Secret Text deriving (Show) +instance IsString Secret where+  fromString = Secret . fromString  -- | Sign 'Request' with 'Secret' sign :: Secret -> Request f Sign -> Request f Ready-sign (Secret s) = coerce . (<* signature)+sign s = coerce . (<* signature)  where-  signature = wrap $ \r@R { _query = q } ->-    r { _query = M.insert "api_sig" (signer (foldr M.delete q ["format", "callback"])) q }+  signature = wrap $+    \r@R { _query = q } -> r { _query = api_sig s . authToken $ q } -  signer = T.pack . show . (hash' :: Strict.ByteString -> MD5Digest) .-    T.encodeUtf8 . M.foldrWithKey(\k v xs -> k <> v <> xs) s+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) +api_sig :: Secret -> Map Text Text -> Map Text Text+api_sig (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 --- | Send Request and parse Response+md5 :: Text -> Text+md5 = T.pack . show . (hash' :: Strict.ByteString -> MD5Digest) . T.encodeUtf8+++-- | Send 'Request' and parse the 'Response' lastfm :: Supported f => Request f Ready -> IO (Response f)-lastfm = lastfm' . finalize+lastfm = lastfm' parse . finalize +-- | Send 'Request' without parsing the 'Response'+lastfm_ :: Supported f => Request f Ready -> IO ()+lastfm_ = lastfm' (\_ _ _ -> ()) . finalize --- | Get R from Request------ That's rarely needed unless you want low-level requests manipulation++-- | Get 'R' from 'Request' finalize :: Supported f => Request f Ready -> R f finalize = ($ base) . unwrap  --- | Send R and parse Response------ That's rarely needed unless you want low-level requests manipulation-lastfm' :: Supported f => R f -> IO (Response f)-lastfm' request =-  C.withManager $ \m -> C.parseUrl (render request) >>= \url -> do-    t <- C.httpLbs (url { C.method = _method request, C.responseTimeout = Just 10000000 }) m-    return $ parse request (C.responseBody t) (C.responseHeaders t)+-- | Send 'R' and parse 'Response' with the supplied function+lastfm' :: Supported f => (Proxy f -> Lazy.ByteString -> C.ResponseHeaders -> a) -> R f -> IO a+lastfm' f request = C.withManager $ \manager -> do+  req <- C.parseUrl (render request)+  let req' = req+       { C.method          = _method request+       , C.responseTimeout = Just 10000000+       }+  res <- C.httpLbs req' manager+  return $ f Proxy (C.responseBody res) (C.responseHeaders res)
src/Network/Lastfm/Tasteometer.hs view
@@ -19,7 +19,7 @@ -- -- Optional: 'limit' ----- <http://www.lastfm.ru/api/show/tasteometer.compare>+-- <http://www.last.fm/api/show/tasteometer.compare> compare :: (Targeted u, Targeted v) => Request f u -> Request f v -> Request f (APIKey -> Ready) compare u v = api "tasteometer.compare" <* comparison 1 u <* comparison 2 v {-# INLINE compare #-}