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 +15/−0
- README.md +101/−0
- liblastfm.cabal +31/−28
- src/Network/Lastfm/Authentication.hs +5/−4
- src/Network/Lastfm/Response.hs +51/−28
- src/Network/Lastfm/Tasteometer.hs +1/−1
+ 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+[](http://hackage.haskell.org/package/liblastfm)+[](https://drone.io/github.com/supki/liblastfm/latest)+[](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 #-}