liblastfm 0.4.1.0 → 0.5.0
raw patch · 27 files changed
+734/−678 lines, 27 filesdep ~http-clientdep ~http-client-tlsPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: http-client, http-client-tls
API changes (from Hackage documentation)
- Network.Lastfm.Response: finalize :: Supported f r => Request f Ready -> R f
- Network.Lastfm.Response: lastfmWith :: Supported f r => (ByteString -> Either LastfmError a) -> R f -> IO (Either LastfmError a)
+ Network.Lastfm.Response: data Connection
+ Network.Lastfm.Response: withConnection :: (Connection -> IO a) -> IO a
- Network.Lastfm.Response: lastfm :: Supported f r => Request f Ready -> IO (Either LastfmError r)
+ Network.Lastfm.Response: lastfm :: Supported f r => Connection -> Request f Ready -> IO (Either LastfmError r)
- Network.Lastfm.Response: lastfm_ :: Supported f r => Request f Ready -> IO (Either LastfmError ())
+ Network.Lastfm.Response: lastfm_ :: Supported f r => Connection -> Request f Ready -> IO (Either LastfmError ())
Files
- CHANGELOG.markdown +36/−0
- CHANGELOG.md +0/−30
- README.markdown +127/−0
- README.md +0/−127
- example/README.markdown +82/−0
- example/desktop-authentication.hs +25/−0
- example/liblastfm-examples.cabal +120/−0
- example/mobile-authentication.hs +25/−0
- example/multitag-search.hs +43/−0
- example/playcount.hs +38/−0
- example/recommendations.hs +48/−0
- example/sort-friends.hs +74/−0
- example/web-authentication.hs +32/−0
- examples/README.md +0/−82
- examples/desktop-authentication.hs +0/−25
- examples/liblastfm-examples.cabal +0/−105
- examples/mobile-authentication.hs +0/−25
- examples/multitag-search.hs +0/−43
- examples/playcount.hs +0/−31
- examples/recommendations.hs +0/−48
- examples/sort-friends.hs +0/−73
- examples/web-authentication.hs +0/−32
- liblastfm.cabal +28/−19
- src/Network/Lastfm/Response.hs +30/−22
- test/api/Json/PlaylistSpec.hs +1/−1
- test/api/SpecHelper.hs +24/−14
- test/api/Xml/PlaylistSpec.hs +1/−1
+ CHANGELOG.markdown view
@@ -0,0 +1,36 @@+0.5.0+=====++ * Fixed `http-client`'s `Manager` misuse: every call to `lastfm*` takes a+ `Connection` parameter now and multiple calls should share it.++0.4.1.0+=======++ * Updated `lens` dependency++ * Switched to `network-uri` package++0.4.0.0+=======++ * `http-conduit` exceptions aren't propagate further into user code anymore++ * XML responses are parsed into `Document` type from `xml-conduit`++0.3.2.0+=======++ * Fixed `auth.getMobileSession` API method++ * Provided `lastfm_` function for making requests without parsing response. That is most useful for `POST` requests like `N.L.Track.love`++0.3.0.0+=======++ * Supported 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
− CHANGELOG.md
@@ -1,30 +0,0 @@-0.4.1.0-=======-- * Updated `lens` dependency-- * Switched to `network-uri` package--0.4.0.0-=======-- * `http-conduit` exceptions aren't propagate further into user code anymore-- * XML responses are parsed into `Document` type from `xml-conduit`--0.3.2.0-=======-- * Fixed `auth.getMobileSession` API method-- * Provided `lastfm_` function for making requests without parsing response. That is most useful for `POST` requests like `N.L.Track.love`--0.3.0.0-=======-- * Supported 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.markdown view
@@ -0,0 +1,127 @@+liblastfm+=========+[](https://hackage.haskell.org/package/liblastfm)+[](https://travis-ci.org/supki/liblastfm)++Complete API interface to [last.fm][last.fm] service.+Documentation is available in two flavours:++ * original [API reference][last.fm/api]++ * liblastfm [haddocks][liblastfm/haddocks]++Introduction+------------+liblastfm provides Applicative interface for constructing requests. Also, it handles all machinery needed to prepare request for sending:++ * [signing][last.fm/sign]++ * url-encoding++ * miscellaneous stuff like choosing correct HTTP method, etc++Once request is ready, liblastfm can send it and get you back a response.+Response format might be:++ * [aeson][aeson] `Value` for json queries++ * [xml-conduit][xml-conduit] `Document` 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`][last.fm/api-usage] 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][liblastfm/haddocks-usage].+So import a couple of modules:++ >>> import Network.Lastfm -- a bunch of useful utilities+ >>> 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`:++ >>> lastfm $ Tag.search <*> tag "russian-folk" <* limit 10 <*> apiKey "29effec263316a1f8a97f753caaa83e0" <* json+ Just (Object fromList [("results",Object fromList [("tagmatches", ...++[Wiki][liblastfm/wiki] describes how to parse responses.++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 (Show (IO (Either LastfmError r0)))+ arising from a use of `print'+ Possible fix:+ add an instance declaration for (Show (IO (Either LastfmError r0)))+ In the first argument of `print', namely `it'+ In a stmt of an interactive GHCi command: print it+```++A: This error message indicates that GHC cannot infer response format for the `Request`.+To fix it, add use `json` or `xml` helpers, depending on your needs++A note on testing+-----------------++To test Lastfm API compatibility (`api` test suite)—specifically, authentication requiring+examples—you will need to set `HASKELL_LIBLASTFM_APIKEY`, `HASKELL_LIBLASTFM_SESSIONKEY`,+and `HASKELL_LIBLASTFM_SECRET` environment variables to your api key, session key, and+secret respectively; for example (bash):++```bash+export HASKELL_LIBLASTFM_APIKEY="__API_KEY__"+export HASKELL_LIBLASTFM_SESSIONKEY="__SESSION_KEY__"+export HASKELL_LIBLASTFM_SECRET="__SECRET__"+```++Please, consult Lastfm API documentation and `examples/*-authentication.hs`+examples if you don't know where to get your credentials.++ [last.fm]: http://www.last.fm/+ [last.fm/api]: http://www.last.fm/api/intro+ [last.fm/api-usage]: http://www.last.fm/api/show/tag.search+ [last.fm/sign]: http://www.last.fm/api/authspec#8+ [liblastfm/haddocks]: http://supki.github.io/liblastfm/+ [liblastfm/haddocks-usage]: http://supki.github.com/liblastfm/Network-Lastfm-Tag.html#v:search+ [liblastfm/wiki]: https://github.com/supki/liblastfm/wiki/How-to-parse-JSON-response+ [aeson]: https://hackage.haskell.org/package/aeson+ [xml-conduit]: https://hackage.haskell.org/package/xml-conduit
− README.md
@@ -1,127 +0,0 @@-liblastfm-=========-[](https://hackage.haskell.org/package/liblastfm)-[](https://travis-ci.org/supki/liblastfm)--Complete API interface to [last.fm][last.fm] service.-Documentation is available in two flavours:-- * original [API reference][last.fm/api]-- * liblastfm [haddocks][liblastfm/haddocks]--Introduction--------------liblastfm provides Applicative interface for constructing requests. Also, it handles all machinery needed to prepare request for sending:-- * [signing][last.fm/sign]-- * url-encoding-- * miscellaneous stuff like choosing correct HTTP method, etc--Once request is ready, liblastfm can send it and get you back a response.-Response format might be:-- * [aeson][aeson] `Value` for json queries-- * [xml-conduit][xml-conduit] `Document` 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`][last.fm/api-usage] 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][liblastfm/haddocks-usage].-So import a couple of modules:-- >>> import Network.Lastfm -- a bunch of useful utilities- >>> 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`:-- >>> lastfm $ Tag.search <*> tag "russian-folk" <* limit 10 <*> apiKey "29effec263316a1f8a97f753caaa83e0" <* json- Just (Object fromList [("results",Object fromList [("tagmatches", ...--[Wiki][liblastfm/wiki] describes how to parse responses.--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 (Show (IO (Either LastfmError r0)))- arising from a use of `print'- Possible fix:- add an instance declaration for (Show (IO (Either LastfmError r0)))- In the first argument of `print', namely `it'- In a stmt of an interactive GHCi command: print it-```--A: This error message indicates that GHC cannot infer response format for the `Request`.-To fix it, add use `json` or `xml` helpers, depending on your needs--A note on testing--------------------To test Lastfm API compatibility (`api` test suite)—specifically, authentication requiring-examples—you will need to set `HASKELL_LIBLASTFM_APIKEY`, `HASKELL_LIBLASTFM_SESSIONKEY`,-and `HASKELL_LIBLASTFM_SECRET` environment variables to your api key, session key, and-secret respectively; for example (bash):--```bash-export HASKELL_LIBLASTFM_APIKEY="__API_KEY__"-export HASKELL_LIBLASTFM_SESSIONKEY="__SESSION_KEY__"-export HASKELL_LIBLASTFM_SECRET="__SECRET__"-```--Please, consult Lastfm API documentation and `examples/*-authentication.hs`-examples if you don't know where to get your credentials.-- [last.fm]: http://www.last.fm/- [last.fm/api]: http://www.last.fm/api/intro- [last.fm/api-usage]: http://www.last.fm/api/show/tag.search- [last.fm/sign]: http://www.last.fm/api/authspec#8- [liblastfm/haddocks]: http://supki.github.io/liblastfm/- [liblastfm/haddocks-usage]: http://supki.github.com/liblastfm/Network-Lastfm-Tag.html#v:search- [liblastfm/wiki]: https://github.com/supki/liblastfm/wiki/How-to-parse-JSON-response- [aeson]: https://hackage.haskell.org/package/aeson- [xml-conduit]: https://hackage.haskell.org/package/xml-conduit
+ example/README.markdown view
@@ -0,0 +1,82 @@+# liblastfm examples++## sort-friends++Shows how to work with lastfm api and async to speed up getting 5 most compliable friends.++ * [async][async]++ * [lens-aeson][lens-aeson]++---++## multitag-search++Shows how to work lastfm api around to get search for multiple tags at once.++ * [aeson][aeson]++---++## playcount++Shows how to get user playcount.++ * [aeson][aeson]++ * [lens-aeson][lens-aeson]++---++## recommendations++Shows how to use signed queries++ * [lens-aeson][lens-aeson]++---++## desktop-authentication++Demonstrates [desktop application authentication][last.fm/desktop] flow.++ * [lens-aeson][lens-aeson]++---++## web-authentication++Demonstrates [web application authentication][last.fm/web] flow.++ * [happstack-server][happstack-server]++---++## mobile-authentication++Demonstrates [mobile application authentication][last.fm/mobile] flow.++ * [lens-aeson][lens-aeson]++---++## [scrobblers][scrobblers]++Advanced scrobbling techniques.++ * [netwire][netwire]++ * [libmpd][libmpd]++ * [lens-aeson][lens-aeson]++ [aeson]: https://hackage.haskell.org/package/aeson+ [async]: https://hackage.haskell.org/package/async+ [happstack-server]: https://hackage.haskell.org/package/happstack-server+ [lens-aeson]: https://hackage.haskell.org/package/lens-aeson+ [libmpd]: https://hackage.haskell.org/package/libmpd+ [netwire]: https://hackage.haskell.org/package/netwire+ [scrobblers]: https://github.com/supki/scrobblers+ [last.fm/desktop]: http://www.last.fm/api/desktopauth+ [last.fm/web]: http://www.last.fm/api/webauth+ [last.fm/mobile]: http://www.last.fm/api/mobileauth
+ example/desktop-authentication.hs view
@@ -0,0 +1,25 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Desktop application authentication flow example+--+-- Please remember to substitute __YOUR_API_KEY__+-- and __YOUR_SECRET__ for real values+import Control.Lens -- lens+import Data.Aeson.Lens -- lens-aeson+import Data.Foldable (for_) -- base+import Data.Text (unpack) -- text+import Network.Lastfm -- liblastfm+import Network.Lastfm.Authentication -- liblastfm+++main :: IO ()+main = withConnection $ \conn -> do+ r <- lastfm conn $ getToken <*> apiKey ak <* json+ for_ (r ^? folded.key "token"._String) $ \t -> do+ putStrLn $ "approve: " ++ link (apiKey ak <* token t)+ _ <- getChar+ r' <- lastfm conn . sign s $ getSession <*> token t <*> apiKey ak <* json+ for_ (r' ^? folded.key "session".key "key"._String) $ \sk ->+ putStrLn $ "session key: " ++ unpack sk+ where+ ak = "__YOUR_API_KEY__"+ s = "__YOUR_SECRET__"
+ example/liblastfm-examples.cabal view
@@ -0,0 +1,120 @@+name: liblastfm-examples+version: 0.5.0+synopsis: Liblastfm examples+license: MIT+license-file: LICENSE+author: Matvey Aksenov, Dmitry Malikov+maintainer: Matvey Aksenov <matvey.aksenov@gmail.com>+category: Network APIs+description: Haddock documentation isn't enough to get feeling of the library+cabal-version: >= 1.10+build-type: Simple++executable sort-friends+ default-language:+ Haskell2010+ build-depends:+ base >= 4 && < 5+ , liblastfm >= 0.4.0.0+ , async+ , text+ , aeson+ , lens >= 4.4+ , lens-aeson >= 1.0.0.1+ main-is:+ sort-friends.hs+ ghc-options:+ -Wall+ -fno-warn-unused-do-bind+ -threaded++executable multitag-search+ default-language:+ Haskell2010+ build-depends:+ base >= 4 && < 5+ , liblastfm >= 0.4.0.0+ , text+ , lens >= 4.4+ , aeson+ main-is:+ multitag-search.hs+ ghc-options:+ -Wall+ -fno-warn-unused-do-bind++executable web-authentication+ default-language:+ Haskell2010+ build-depends:+ base >= 4 && < 5+ , liblastfm >= 0.4.0.0+ , lens >= 4.4+ , lens-aeson >= 1.0.0.1+ , happstack-server >= 7.3+ , transformers+ main-is:+ web-authentication.hs+ ghc-options:+ -Wall+ -fno-warn-unused-do-bind++executable desktop-authentication+ default-language:+ Haskell2010+ build-depends:+ base >= 4 && < 5+ , liblastfm >= 0.4.0.0+ , text+ , lens >= 4.4+ , lens-aeson >= 1.0.0.1+ main-is:+ desktop-authentication.hs+ ghc-options:+ -Wall+ -fno-warn-unused-do-bind++executable mobile-authentication+ default-language:+ Haskell2010+ build-depends:+ base >= 4 && < 5+ , liblastfm >= 0.4.0.0+ , lens >= 4.4+ , lens-aeson >= 1.0.0.1+ , text+ main-is:+ mobile-authentication.hs+ ghc-options:+ -Wall++executable playcount+ default-language:+ Haskell2010+ build-depends:+ base >= 4 && < 5+ , liblastfm >= 0.4.0.0+ , text+ , lens >= 4.4+ , lens-aeson >= 1.0.0.1+ main-is:+ playcount.hs+ ghc-options:+ -Wall+ -fno-warn-unused-do-bind++executable recommendations+ default-language:+ Haskell2010+ build-depends:+ base >= 4 && < 5+ , liblastfm >= 0.4.0.0+ , text+ , aeson+ , lens >= 4.4+ , lens-aeson >= 1.0.0.1+ main-is:+ recommendations.hs+ ghc-options:+ -Wall+ -fno-warn-unused-do-bind
+ example/mobile-authentication.hs view
@@ -0,0 +1,25 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Mobile application authentication flow example+--+-- Please remember to substitute __YOUR_API_KEY__,+-- __YOUR_SECRET__, __USERNAME__ and __PASSWORD__+-- for real values+import Control.Lens -- lens+import Data.Aeson.Lens -- lens-aeson+import qualified Data.Text as Text -- text+import qualified Data.Text.IO as Text -- text+import Network.Lastfm -- liblastfm+import Network.Lastfm.Authentication -- liblastfm++main :: IO ()+main = withConnection $ \conn -> do+ r <- lastfm conn . sign s $ getMobileSession <*> username u <*> password p <*> apiKey ak <* json+ let maybeSk = r ^? folded.key "session".key "key"._String+ Text.putStrLn $ case maybeSk of+ Just sk -> "Mobile session key: " `Text.append` sk+ Nothing -> "Mobile session key wasn't retrieved, something goes wrong"+ where+ ak = "__YOUR_API_KEY__"+ s = "__YOUR_SECRET__"+ u = "__USERNAME__"+ p = "__PASSWORD__"
+ example/multitag-search.hs view
@@ -0,0 +1,43 @@+{-# LANGUAGE OverloadedStrings #-}+{- This example shows how to do multitag search even+ - if it's not provided by Lastfm API+ -+ - Sample output:+ -+ - $> ./examples/multitag-search.hs j-pop anime anime-ost+ - 坂本真綾+ - KOTOKO+ - Lisa+ - 茅原実里+ -}+import Data.Aeson.Types -- aeson+import Data.List (intersect) -- base+import Data.Maybe -- base+import Data.Text (Text) -- text+import qualified Data.Text as Text -- text+import qualified Data.Text.IO as Text -- text+import Network.Lastfm -- liblastfm+import qualified Network.Lastfm.Tag as Tag -- liblastfm+import System.Environment (getArgs) -- base++{-# ANN module ("HLint: ignore Use camelCase" :: String) #-}+++main :: IO ()+main = mapM_ Text.putStrLn =<< withConnection . getArtists . map Text.pack =<< getArgs+++getArtists :: [Text] -> Connection -> IO [Text]+getArtists ts conn = do+ names <- mapM request ts+ case catMaybes names of+ [] -> return []+ ns -> return (foldl1 intersect ns)+ where+ request = fmap (either (const Nothing) (parseMaybe gta)) . query++ query t = lastfm conn $+ Tag.getTopArtists <*> tag t <* limit 100 <*> apiKey "29effec263316a1f8a97f753caaa83e0" <* json++gta :: Value -> Parser [Text]+gta o = parseJSON o >>= (.: "topartists") >>= (.: "artist") >>= mapM (.: "name")
+ example/playcount.hs view
@@ -0,0 +1,38 @@+{-# LANGUAGE OverloadedStrings #-}+-- This example shows how to get user playcount with lens-aeson+--+-- Sample session:+--+-- $> ./examples/playcount.hs+-- >>> MCDOOMDESTROYER+-- 96698+-- >>> smpcln+-- 95518+-- >>> ^C+-- $>+--}+import Control.Lens -- lens+import Data.Aeson.Lens -- lens-aeson+import Control.Monad -- base+import Data.Text (Text) -- text+import qualified Data.Text.IO as Text -- text+import Network.Lastfm -- liblastfm+import qualified Network.Lastfm.User as User -- liblastfm+import qualified System.IO as IO -- base+++main :: IO ()+main = withConnection $ \conn ->+ forever $ do Text.putStr ">>> "; flush; Text.getLine >>= playcount conn >>= Text.putStrLn++flush :: IO ()+flush = IO.hFlush IO.stdout++playcount :: Connection -> Text -> IO Text+playcount conn u = do+ res <- lastfm conn+ (User.getInfo <*> user u <*> apiKey "29effec263316a1f8a97f753caaa83e0" <* json)+ return $ case res ^? folded.key "user".key "playcount"._String of+ Nothing -> "Playcount not found"+ Just x -> x+{-# ANN playcount ("HLint: ignore Use fromMaybe" :: String) #-}
+ example/recommendations.hs view
@@ -0,0 +1,48 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+{- This example shows how to use signed queries+ - to get user recommendations+ -+ - Sample output:+ -+ - Roger Rotor+ - Neurosis+ - Asche+ - SKYHARBOR+ - Cylob+ - Mouth of the Architect+ -}++import Control.Lens -- lens+import Data.Aeson.Lens -- lens-aeson+import Data.Traversable (for) -- base+import Data.Aeson (Value) -- aeson+import Data.Foldable (Foldable) -- base+import Data.Int (Int64) -- base+import Data.Text (Text) -- text+import qualified Data.Text.IO as Text -- text+import Network.Lastfm -- liblastfm+import qualified Network.Lastfm.User as User -- liblastfm+++main :: IO ()+main = withConnection $ \conn -> do+ r <- for [1..pages] $ \p -> parse `fmap` query conn (User.getRecommendedArtists <* page p)+ mapM_ Text.putStrLn (concat r)++-- Construct signed query+query :: Connection -> Request JSON (APIKey -> SessionKey -> Sign) -> IO (Either LastfmError Value)+query conn r = lastfm conn $ sign secret (r <*> ak <*> sk <* json)+ where+ ak = apiKey "__YOUR_API_KEY__"+ sk = sessionKey "__YOUR_SESSION_KEY__"+ secret = "__YOUR_SECRET__"++-- Other query parameters+total, pages :: Int64+total = 250+pages = total `div` 50 -- 50 is the default number of recommendations per page++-- Parse artist names from response+parse :: Foldable f => f Value -> [Text]+parse x = x ^.. folded.key "recommendations".key "artist".values.key "name"._String
+ example/sort-friends.hs view
@@ -0,0 +1,74 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+{- This example shows how to extract all+ - user friends, get their similarity score and+ - then sort them based on it outputting top 5+ -+ - User friends of which are searched is 'target'+ -+ - Most of hard work there is on 'aeson-lens' actually :)+ -+ - Sample output:+ -+ - % ./examples/sort-friends.hs+ - Artvart: 0.9969767332077+ - smpcln: 0.9965825676918+ - nv0id: 0.86804795265198+ - QueenrXy: 0.5932799577713+ - GhostOsaka: 0.2875879406929+ -+ - Notice: you may want to adjust maximum open files limit+ - if testing on users with relatively large friends count+ -}+import Control.Concurrent.Async -- async+import Control.Lens -- lens+import Data.Aeson.Lens -- lens-aeson+import Data.Aeson (Value) -- aeson+import Data.Function (on) -- base+import Data.List (sortBy) -- base+import Data.Maybe (fromMaybe) -- base+import Data.Monoid ((<>)) -- base+import Data.Text (Text) -- text+import Data.Text.Lens (unpacked) -- lens+import qualified Data.Text.IO as Text -- text+import Network.Lastfm hiding (to) -- liblastfm+import qualified Network.Lastfm.User as User -- liblastfm+import qualified Network.Lastfm.Tasteometer as Tasteometer -- liblastfm+import Text.Read (readMaybe) -- base+++type Score = Text+++main :: IO ()+main = withConnection $ \conn ->+ pages conn >>= scores conn . names >>= pretty+ where+ names x = x ^.. folded.folded.key "friends".key "user"._Array.folded.key "name"._String++pages :: Connection -> IO [Either LastfmError Value]+pages conn = do+ first <- query conn (User.getFriends <*> user target)+ let ps = fromMaybe 1 (preview total first)+ (first :) <$> forConcurrently [2..ps] (\p -> query conn (User.getFriends <*> user target <* page p))+ where+ total = folded.key "friends".key "@attr".key "totalPages"._String.unpacked.to readMaybe.folded++scores :: Connection -> [Text] -> IO [(Text, Score)]+scores conn xs = zip xs <$> forConcurrently xs (\x -> do+ r <- query conn (Tasteometer.compare (user target) (user x))+ return (fromMaybe "0" (preview score r)))+ where+ score = folded.key "comparison".key "result".key "score"._String++forConcurrently :: [a] -> (a -> IO b) -> IO [b]+forConcurrently = flip mapConcurrently++pretty :: [(Text, Score)] -> IO ()+pretty = mapM_ (\(n,s) -> Text.putStrLn $ n <> ": " <> s) . take 5 . sortBy (flip compare `on` snd)++query :: Connection -> Request JSON (APIKey -> Ready) -> IO (Either LastfmError Value)+query conn r = lastfm conn (r <*> apiKey "234fc6e0f41f6ef99b7bd62ebaf8d318" <* json)++target :: Text+target = "MCDOOMDESTROYER"
+ example/web-authentication.hs view
@@ -0,0 +1,32 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Web application authentication flow example+--+-- Please remember to substitute __YOUR_API_KEY__,+-- __YOUR_SECRET__ and __YOUR_CALLBACK__ for real values+import Control.Lens -- lens+import Data.Aeson.Lens -- lens-aeson+import Control.Monad -- base+import Control.Monad.IO.Class (liftIO) -- transformers+import Data.IORef -- base+import Happstack.Server -- happstack-server+import Network.Lastfm -- liblastfm+import Network.Lastfm.Authentication -- liblastfm+++main :: IO ()+main = withConnection $ \conn -> do+ sessions <- newIORef []+ simpleHTTP nullConf $ msum+ [ dir "authenticate" $ seeOther (link $ apiKey ak <* callback "__YOUR_CALLBACK__") ""+ , dir "save" $ do+ t <- lookText' "token"+ r <- liftIO . lastfm conn . sign s $ getSession <*> token t <*> apiKey ak <* json+ case r ^? folded.key "session".key "key"._String of+ Just sk -> liftIO $ modifyIORef' sessions (sk:)+ Nothing -> return ()+ ok "Saved."+ , dir "show" $ liftIO (readIORef sessions) >>= ok . show+ ]+ where+ ak = "__YOUR_API_KEY__"+ s = "__YOUR_SECRET__"
− examples/README.md
@@ -1,82 +0,0 @@-# liblastfm examples--## sort-friends--Shows how to work with lastfm api and async to speed up getting 5 most compliable friends.-- * [async][async]-- * [lens-aeson][lens-aeson]-------## multitag-search--Shows how to work lastfm api around to get search for multiple tags at once.-- * [aeson][aeson]-------## playcount--Shows how to get user playcount.-- * [aeson][aeson]-- * [lens-aeson][lens-aeson]-------## recommendations--Shows how to use signed queries-- * [lens-aeson][lens-aeson]-------## desktop-authentication--Demonstrates [desktop application authentication][last.fm/desktop] flow.-- * [lens-aeson][lens-aeson]-------## web-authentication--Demonstrates [web application authentication][last.fm/web] flow.-- * [happstack-server][happstack-server]-------## mobile-authentication--Demonstrates [mobile application authentication][last.fm/mobile] flow.-- * [lens-aeson][lens-aeson]-------## [scrobblers][scrobblers]--Advanced scrobbling techniques.-- * [netwire][netwire]-- * [libmpd][libmpd]-- * [lens-aeson][lens-aeson]-- [aeson]: https://hackage.haskell.org/package/aeson- [async]: https://hackage.haskell.org/package/async- [happstack-server]: https://hackage.haskell.org/package/happstack-server- [lens-aeson]: https://hackage.haskell.org/package/lens-aeson- [libmpd]: https://hackage.haskell.org/package/libmpd- [netwire]: https://hackage.haskell.org/package/netwire- [scrobblers]: https://github.com/supki/scrobblers- [last.fm/desktop]: http://www.last.fm/api/desktopauth- [last.fm/web]: http://www.last.fm/api/webauth- [last.fm/mobile]: http://www.last.fm/api/mobileauth
− examples/desktop-authentication.hs
@@ -1,25 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--- | Desktop application authentication flow example------ Please remember to substitute __YOUR_API_KEY__--- and __YOUR_SECRET__ for real values-import Control.Lens -- lens-import Data.Aeson.Lens -- lens-aeson-import Data.Foldable (for_) -- base-import Data.Text (unpack) -- text-import Network.Lastfm -- liblastfm-import Network.Lastfm.Authentication -- liblastfm---main :: IO ()-main = do- r <- lastfm $ getToken <*> apiKey ak <* json- for_ (r ^? folded.key "token"._String) $ \t -> do- putStrLn $ "approve: " ++ link (apiKey ak <* token t)- _ <- getChar- r' <- lastfm . sign s $ getSession <*> token t <*> apiKey ak <* json- for_ (r' ^? folded.key "session".key "key"._String) $ \sk ->- putStrLn $ "session key: " ++ unpack sk- where- ak = "__YOUR_API_KEY__"- s = "__YOUR_SECRET__"
− examples/liblastfm-examples.cabal
@@ -1,105 +0,0 @@-name: liblastfm-examples-version: 0.4.1.0-synopsis: Examples for liblastfm-license: MIT-license-file: LICENSE-author: Matvey Aksenov, Dmitry Malikov-maintainer: Matvey Aksenov <matvey.aksenov@gmail.com>-category: Network APIs-description: Haddock documentation isn't enough to get feeling of the library-cabal-version: >= 1.10-build-type: Simple--executable sort-friends- default-language: Haskell2010- build-depends:- base >= 4 && < 5- , liblastfm >= 0.4.0.0- , async- , text- , aeson- , lens >= 4.4- , lens-aeson >= 1.0.0.1- main-is: sort-friends.hs- ghc-options:- -Wall- -fno-warn-unused-do-bind- -threaded--executable multitag-search- default-language: Haskell2010- build-depends:- base >= 4 && < 5- , liblastfm >= 0.4.0.0- , text- , lens >= 4.4- , aeson- main-is: multitag-search.hs- ghc-options:- -Wall- -fno-warn-unused-do-bind--executable web-authentication- default-language: Haskell2010- build-depends:- base >= 4 && < 5- , liblastfm >= 0.4.0.0- , lens >= 4.4- , lens-aeson >= 1.0.0.1- , happstack-server >= 7.3- , transformers- main-is: web-authentication.hs- ghc-options:- -Wall- -fno-warn-unused-do-bind--executable desktop-authentication- default-language: Haskell2010- build-depends:- base >= 4 && < 5- , liblastfm >= 0.4.0.0- , text- , lens >= 4.4- , lens-aeson >= 1.0.0.1- main-is: desktop-authentication.hs- ghc-options:- -Wall- -fno-warn-unused-do-bind--executable mobile-authentication- default-language: Haskell2010- build-depends:- base >= 4 && < 5- , liblastfm >= 0.4.0.0- , lens >= 4.4- , lens-aeson >= 1.0.0.1- , text- main-is: mobile-authentication.hs- ghc-options:- -Wall--executable playcount- default-language: Haskell2010- build-depends:- base >= 4 && < 5- , liblastfm >= 0.4.0.0- , text- , lens >= 4.4- , lens-aeson >= 1.0.0.1- main-is: playcount.hs- ghc-options:- -Wall- -fno-warn-unused-do-bind--executable recommendations- default-language: Haskell2010- build-depends:- base >= 4 && < 5- , liblastfm >= 0.4.0.0- , text- , aeson- , lens >= 4.4- , lens-aeson >= 1.0.0.1- main-is: recommendations.hs- ghc-options: -Wall- -fno-warn-unused-do-bind
− examples/mobile-authentication.hs
@@ -1,25 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--- | Mobile application authentication flow example------ Please remember to substitute __YOUR_API_KEY__,--- __YOUR_SECRET__, __USERNAME__ and __PASSWORD__--- for real values-import Control.Lens -- lens-import Data.Aeson.Lens -- lens-aeson-import qualified Data.Text as Text -- text-import qualified Data.Text.IO as Text -- text-import Network.Lastfm -- liblastfm-import Network.Lastfm.Authentication -- liblastfm--main :: IO ()-main = do- r <- lastfm . sign s $ getMobileSession <*> username u <*> password p <*> apiKey ak <* json- let maybeSk = r ^? folded.key "session".key "key"._String- Text.putStrLn $ case maybeSk of- Just sk -> "Mobile session key: " `Text.append` sk- Nothing -> "Mobile session key wasn't retrieved, something goes wrong"- where- ak = "__YOUR_API_KEY__"- s = "__YOUR_SECRET__"- u = "__USERNAME__"- p = "__PASSWORD__"
− examples/multitag-search.hs
@@ -1,43 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-{- This example shows how to do multitag search even- - if it's not provided by Lastfm API- -- - Sample output:- -- - $> ./examples/multitag-search.hs- - 坂本真綾- - KOTOKO- - Lisa- - 茅原実里- -}-import Data.Aeson.Types -- aeson-import Data.List (intersect) -- base-import Data.Maybe -- base-import Data.Text (Text) -- text-import qualified Data.Text.IO as Text -- text-import Network.Lastfm -- liblastfm-import qualified Network.Lastfm.Tag as Tag -- liblastfm--{-# ANN module ("HLint: ignore Use camelCase" :: String) #-}---main :: IO ()-main = mapM_ Text.putStrLn =<< get_artists multitags---get_artists :: [Text] -> IO [Text]-get_artists ts = do- names <- mapM request ts- case catMaybes names of- [] -> return []- ns -> return (foldl1 intersect ns)- where- request = fmap (either (const Nothing) (parseMaybe gta)) . query-- query t = lastfm $ Tag.getTopArtists <*> tag t <* limit 100 <*> apiKey "29effec263316a1f8a97f753caaa83e0" <* json--gta :: Value -> Parser [Text]-gta o = parseJSON o >>= (.: "topartists") >>= (.: "artist") >>= mapM (.: "name")--multitags :: [Text]-multitags = ["j-pop", "anime", "anime-ost"]
− examples/playcount.hs
@@ -1,31 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-{- This example shows how to get user playcount with lens-aeson- -- - Sample session:- -- - $> ./examples/playcount.hs- - >>> MCDOOMDESTROYER- - 96698- - >>> smpcln- - 95518- - >>> ^C- - $>- -}-import Control.Lens -- lens-import Data.Aeson.Lens -- lens-aeson-import Control.Monad -- base-import Data.Maybe -- base-import Data.Text (Text) -- text-import qualified Data.Text.IO as Text -- text-import Network.Lastfm -- liblastfm-import qualified Network.Lastfm.User as User -- liblastfm-import System.IO (hFlush, stdout) -- base---main :: IO ()-main = forever $ Text.putStr ">>> " >> hFlush stdout >> Text.getLine >>= playcount >>= Text.putStrLn--playcount :: Text -> IO Text-playcount u =- lastfm (User.getInfo <*> user u <*> apiKey "29effec263316a1f8a97f753caaa83e0" <* json) <&> \rsp ->- fromMaybe "Playcount not found" (rsp ^? folded.key "user".key "playcount"._String)
− examples/recommendations.hs
@@ -1,48 +0,0 @@-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE OverloadedStrings #-}-{- This example shows how to use signed queries- - to get user recommendations- -- - Sample output:- -- - Roger Rotor- - Neurosis- - Asche- - SKYHARBOR- - Cylob- - Mouth of the Architect- -}--import Control.Lens -- lens-import Data.Aeson.Lens -- lens-aeson-import Data.Traversable (for) -- base-import Data.Aeson (Value) -- aeson-import Data.Foldable (Foldable) -- base-import Data.Int (Int64) -- base-import Data.Text (Text) -- text-import qualified Data.Text.IO as Text -- text-import Network.Lastfm -- liblastfm-import qualified Network.Lastfm.User as User -- liblastfm---main :: IO ()-main = do- r <- for [1..pages] $ \p -> parse `fmap` query (User.getRecommendedArtists <* page p)- mapM_ Text.putStrLn (concat r)---- Construct signed query-query :: Request JSON (APIKey -> SessionKey -> Sign) -> IO (Either LastfmError Value)-query r = lastfm $ sign secret (r <*> ak <*> sk <* json)- where- ak = apiKey "__YOUR_API_KEY__"- sk = sessionKey "__YOUR_SESSION_KEY__"- secret = "__YOUR_SECRET__"---- Other query parameters-total, pages :: Int64-total = 250-pages = total `div` 50 -- 50 is the default number of recommendations per page---- Parse artist names from response-parse :: Foldable f => f Value -> [Text]-parse x = x ^.. folded.key "recommendations".key "artist"._Array.folded.key "name"._String
− examples/sort-friends.hs
@@ -1,73 +0,0 @@-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE OverloadedStrings #-}-{- This example shows how to extract all- - user friends, get their similarity score and- - then sort them based on it outputting top 5- -- - User friends of which are searched is 'target'- -- - Most of hard work there is on 'aeson-lens' actually :)- -- - Sample output:- -- - % ./examples/sort-friends.hs- - Artvart: 0.9969767332077- - smpcln: 0.9965825676918- - nv0id: 0.86804795265198- - QueenrXy: 0.5932799577713- - GhostOsaka: 0.2875879406929- -- - Notice: you may want to adjust maximum open files limit- - if testing on users with relatively large friends count- -}-import Control.Concurrent.Async -- async-import Control.Lens -- lens-import Data.Aeson.Lens -- lens-aeson-import Data.Aeson (Value) -- aeson-import Data.Function (on) -- base-import Data.List (sortBy) -- base-import Data.Maybe (fromMaybe) -- base-import Data.Monoid ((<>)) -- base-import Data.Text (Text) -- text-import Data.Text.Lens (unpacked) -- lens-import qualified Data.Text.IO as Text -- text-import Network.Lastfm hiding (to) -- liblastfm-import qualified Network.Lastfm.User as User -- liblastfm-import qualified Network.Lastfm.Tasteometer as Tasteometer -- liblastfm-import Text.Read (readMaybe) -- base---type Score = Text---main :: IO ()-main = pages >>= scores . names >>= pretty- where- names x = x ^.. folded.folded.key "friends".key "user"._Array.folded.key "name"._String--pages :: IO [Either LastfmError Value]-pages = do- first <- query (User.getFriends <*> user target)- let ps = fromMaybe 1 (preview total first)- (first :) <$> forConcurrently [2..ps] (\p -> query (User.getFriends <*> user target <* page p))- where- total = folded.key "friends".key "@attr".key "totalPages"._String.unpacked.to readMaybe.folded--scores :: [Text] -> IO [(Text, Score)]-scores xs = zip xs <$> forConcurrently xs (\x -> do- r <- query (Tasteometer.compare (user target) (user x))- return (fromMaybe "0" (preview score r)))- where- score = folded.key "comparison".key "result".key "score"._String--forConcurrently :: [a] -> (a -> IO b) -> IO [b]-forConcurrently = flip mapConcurrently--pretty :: [(Text, Score)] -> IO ()-pretty = mapM_ (\(n,s) -> Text.putStrLn $ n <> ": " <> s) . take 5 . sortBy (flip compare `on` snd)--query :: Request JSON (APIKey -> Ready) -> IO (Either LastfmError Value)-query r = lastfm (r <*> apiKey "234fc6e0f41f6ef99b7bd62ebaf8d318" <* json)--target :: Text-target = "MCDOOMDESTROYER"
− examples/web-authentication.hs
@@ -1,32 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--- | Web application authentication flow example------ Please remember to substitute __YOUR_API_KEY__,--- __YOUR_SECRET__ and __YOUR_CALLBACK__ for real values-import Control.Lens -- lens-import Data.Aeson.Lens -- lens-aeson-import Control.Monad -- base-import Control.Monad.IO.Class (liftIO) -- transformers-import Data.IORef -- base-import Happstack.Server -- happstack-server-import Network.Lastfm -- liblastfm-import Network.Lastfm.Authentication -- liblastfm---main :: IO ()-main = do- sessions <- newIORef []- simpleHTTP nullConf $ msum- [ dir "authenticate" $ seeOther (link $ apiKey ak <* callback "__YOUR_CALLBACK__") ""- , dir "save" $ do- t <- lookText' "token"- r <- liftIO . lastfm . sign s $ getSession <*> token t <*> apiKey ak <* json- case r ^? folded.key "session".key "key"._String of- Just sk -> liftIO $ modifyIORef' sessions (sk:)- Nothing -> return ()- ok "Saved."- , dir "show" $ liftIO (readIORef sessions) >>= ok . show- ]- where- ak = "__YOUR_API_KEY__"- s = "__YOUR_SECRET__"
liblastfm.cabal view
@@ -1,5 +1,5 @@ name: liblastfm-version: 0.4.1.0+version: 0.5.0 synopsis: Lastfm API interface license: MIT license-file: LICENSE@@ -11,17 +11,17 @@ cabal-version: >= 1.10 build-type: Simple extra-source-files:- README.md- CHANGELOG.md- examples/README.md- examples/desktop-authentication.hs- examples/liblastfm-examples.cabal- examples/mobile-authentication.hs- examples/multitag-search.hs- examples/playcount.hs- examples/recommendations.hs- examples/sort-friends.hs- examples/web-authentication.hs+ README.markdown+ CHANGELOG.markdown+ example/README.markdown+ example/desktop-authentication.hs+ example/liblastfm-examples.cabal+ example/mobile-authentication.hs+ example/multitag-search.hs+ example/playcount.hs+ example/recommendations.hs+ example/sort-friends.hs+ example/web-authentication.hs source-repository head type: git@@ -33,7 +33,8 @@ manual: True library- default-language: Haskell2010+ default-language:+ Haskell2010 build-depends: aeson , base >= 4 && < 5@@ -81,22 +82,27 @@ test-suite api if !flag(test-api) buildable: False- default-language: Haskell2010+ default-language:+ Haskell2010+ type:+ exitcode-stdio-1.0 build-depends: aeson , base >= 4 && < 5 , bytestring , HUnit+ , http-client+ , http-client-tls , lens-aeson >= 1.0.0.1 , lens >= 4.4 , liblastfm , hspec , text , xml-html-conduit-lens >= 0.3- type: exitcode-stdio-1.0 hs-source-dirs: test/api- main-is: Spec.hs+ main-is:+ Spec.hs other-modules: SpecHelper Json.AlbumSpec@@ -131,7 +137,10 @@ -fno-warn-unused-do-bind test-suite spec- default-language: Haskell2010+ default-language:+ Haskell2010+ type:+ exitcode-stdio-1.0 build-depends: aeson , base >= 4 && < 5@@ -153,11 +162,11 @@ , void , xml-conduit , xml-html-conduit-lens >= 0.3- type: exitcode-stdio-1.0 hs-source-dirs: src test/spec- main-is: Spec.hs+ main-is:+ Spec.hs other-modules: Network.Lastfm.ResponseSpec ghc-options:
src/Network/Lastfm/Response.hs view
@@ -10,19 +10,18 @@ -- $sign Secret(..) , sign- -- * Get response- , Supported- , Format(..)+ -- * Perform requests+ , Connection+ , withConnection , lastfm , lastfm_+ , Supported+ , Format(..) -- ** Errors , LastfmError(..) , _LastfmBadResponse , _LastfmEncodedError , _LastfmHttpError- -- ** Internal- , lastfmWith- , finalize #ifdef TEST , parse , md5@@ -47,8 +46,8 @@ 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 N-import qualified Network.HTTP.Client.TLS as N+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 @@ -115,7 +114,7 @@ -- | last.fm error code and message string | LastfmEncodedError Int Text -- | wrapped http-conduit exception- | LastfmHttpError N.HttpException+ | LastfmHttpError Http.HttpException deriving (Show, Typeable) -- | Admittedly, this isn't the best 'Eq' instance ever@@ -165,7 +164,7 @@ -- | This is a @ Prism' 'LastfmError' 'C.HttpException' @ in disguise _LastfmHttpError :: (Choice p, Applicative m, AsLastfmError e)- => p N.HttpException (m N.HttpException) -> p e (m 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@@ -202,27 +201,36 @@ md5 = T.pack . show . (hash' :: Strict.ByteString -> MD5Digest) . T.encodeUtf8 --- | Send the 'Request' and parse the 'Response'-lastfm :: Supported f r => Request f Ready -> IO (Either LastfmError r)-lastfm = lastfmWith parse . finalize+-- | Lastfm connection manager+newtype Connection = Connection Http.Manager --- | Send the 'Request' without parsing the 'Response'-lastfm_ :: Supported f r => Request f Ready -> IO (Either LastfmError ())-lastfm_ = lastfmWith (\_ -> Right ()) . finalize+-- | Creating an HTTPS connection manager is expensive; it's advised to use+-- a single 'Connection' for all communications with last.fm+withConnection :: (Connection -> IO a) -> IO a+withConnection f = Http.withManager Http.tlsManagerSettings (f . Connection) +-- | Perform the 'Request' and parse the response+lastfm :: Supported f r => Connection -> Request f Ready -> IO (Either LastfmError r)+lastfm man = lastfmWith man parse . finalize++-- | Perform the 'Request' ignoring any responses+lastfm_ :: Supported f r => Connection -> Request f Ready -> IO (Either LastfmError ())+lastfm_ man = lastfmWith man (\_ -> Right ()) . finalize+ -- | Send the 'R' and parse the 'Response' with the supplied parser lastfmWith :: Supported f r- => (Lazy.ByteString -> Either LastfmError a)+ => Connection+ -> (Lazy.ByteString -> Either LastfmError a) -> R f -> IO (Either LastfmError a)-lastfmWith p r = N.withManager N.tlsManagerSettings $ \manager -> do- req <- N.parseUrl (render r)+lastfmWith (Connection man) p r = do+ req <- Http.parseUrl (render r) let req' = req- { N.method = _method r- , N.responseTimeout = Just 10000000+ { Http.method = _method r+ , Http.responseTimeout = Just 10000000 }- p . N.responseBody <$> N.httpLbs req' manager+ p . Http.responseBody <$> Http.httpLbs req' man `catch` (return . Left)
test/api/Json/PlaylistSpec.hs view
@@ -26,7 +26,7 @@ key "playlists".key "playlist".key "title"._String it "addTrack" $ do- r <- lastfm $ getPlaylists <*> user "liblastfm" <*> ak' <* json+ r <- lastfm man $ getPlaylists <*> user "liblastfm" <*> ak' <* json case r of Left e -> assertFailure (printf "last.fm error: %s" (show e))
test/api/SpecHelper.hs view
@@ -21,21 +21,26 @@ , privateAPIKey , privateSessionKey , privateSecret+ -- $really-awful+ , man ) where -import Control.Exception (Exception, throwIO)-import Control.Lens-import Data.Aeson (Value)-import Data.Aeson.Lens-import Data.Text (Text, pack)-import Data.Typeable (Typeable)-import Network.Lastfm-import System.Environment-import System.IO.Unsafe (unsafePerformIO)-import Test.Hspec-import Test.HUnit (assertFailure)-import Text.Printf-import Text.Xml.Lens+import Control.Exception (Exception, throwIO)+import Control.Lens+import Data.Aeson (Value)+import Data.Aeson.Lens+import Data.Text (Text, pack)+import Data.Typeable (Typeable)+import qualified Network.HTTP.Client as Http+import qualified Network.HTTP.Client.TLS as Http+import Network.Lastfm+import System.Environment+import System.IO.Unsafe (unsafePerformIO)+import Test.Hspec+import Test.HUnit (assertFailure)+import Text.Printf+import Text.Xml.Lens+import Unsafe.Coerce (unsafeCoerce) infixl 1 `shouldHaveJson`, `shouldHaveXml` @@ -43,7 +48,7 @@ -- | Inspect 'Response' with 'Query' shouldHaveResponse :: (Show r, Supported f r) => Request f Ready -> Fold r a -> Expectation shouldHaveResponse q l = do- r <- lastfm q+ r <- lastfm man q case preview (_Right.l) r of Just _ -> return () Nothing -> assertFailure (printf "Query failed on %s" (show r))@@ -96,3 +101,8 @@ case mv of Just v -> return (pack v) Nothing -> throwIO (EnvironmentMissing var)++-- $really-awful++man :: Connection+man = unsafeCoerce (unsafePerformIO (Http.newManager Http.tlsManagerSettings))
test/api/Xml/PlaylistSpec.hs view
@@ -25,7 +25,7 @@ root.node "playlists".node "playlist".node "title".text it "addTrack" $ do- r <- lastfm $ getPlaylists <*> user "liblastfm" <*> ak' <* Network.Lastfm.xml+ r <- lastfm man $ getPlaylists <*> user "liblastfm" <*> ak' <* Network.Lastfm.xml case r of Left e -> assertFailure (printf "last.fm error: %s" (show e))