liblastfm 0.3.2.0 → 0.4.0.0
raw patch · 45 files changed
+3108/−154 lines, 45 filesdep +HUnitdep +hspecdep +hspec-expectations-lensdep −http-conduitdep −http-typesdep −taggeddep ~basedep ~containers
Dependencies added: HUnit, hspec, hspec-expectations-lens, http-client, http-client-tls, lens, liblastfm, profunctors, xml-conduit, xml-html-conduit-lens
Dependencies removed: http-conduit, http-types, tagged
Dependency ranges changed: base, containers
Files
- CHANGELOG.md +11/−3
- LICENSE +1/−1
- README.md +73/−46
- examples/README.md +82/−0
- examples/desktop-authentication.hs +25/−0
- examples/liblastfm-examples.cabal +99/−0
- examples/mobile-authentication.hs +25/−0
- examples/multitag-search.hs +43/−0
- examples/playcount.hs +31/−0
- examples/recommendations.hs +48/−0
- examples/sort-friends.hs +73/−0
- examples/web-authentication.hs +32/−0
- liblastfm.cabal +108/−7
- src/Network/Lastfm/Internal.hs +30/−18
- src/Network/Lastfm/Response.hs +174/−79
- test/api/Json/AlbumSpec.hs +107/−0
- test/api/Json/ArtistSpec.hs +196/−0
- test/api/Json/ChartSpec.hs +53/−0
- test/api/Json/EventSpec.hs +36/−0
- test/api/Json/GeoSpec.hs +78/−0
- test/api/Json/GroupSpec.hs +47/−0
- test/api/Json/LibrarySpec.hs +56/−0
- test/api/Json/PlaylistSpec.hs +43/−0
- test/api/Json/TagSpec.hs +58/−0
- test/api/Json/TasteometerSpec.hs +38/−0
- test/api/Json/TrackSpec.hs +157/−0
- test/api/Json/UserSpec.hs +137/−0
- test/api/Json/VenueSpec.hs +28/−0
- test/api/Spec.hs +1/−0
- test/api/SpecHelper.hs +98/−0
- test/api/Xml/AlbumSpec.hs +106/−0
- test/api/Xml/ArtistSpec.hs +196/−0
- test/api/Xml/ChartSpec.hs +52/−0
- test/api/Xml/EventSpec.hs +36/−0
- test/api/Xml/GeoSpec.hs +77/−0
- test/api/Xml/GroupSpec.hs +47/−0
- test/api/Xml/LibrarySpec.hs +56/−0
- test/api/Xml/PlaylistSpec.hs +42/−0
- test/api/Xml/TagSpec.hs +58/−0
- test/api/Xml/TasteometerSpec.hs +37/−0
- test/api/Xml/TrackSpec.hs +157/−0
- test/api/Xml/UserSpec.hs +137/−0
- test/api/Xml/VenueSpec.hs +28/−0
- test/spec/Network/Lastfm/ResponseSpec.hs +90/−0
- test/spec/Spec.hs +1/−0
CHANGELOG.md view
@@ -1,13 +1,21 @@+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 ======= - * 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`+ * 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 ======= - * Support batch operations for `N.L.Track.scrobble`, `N.L.Library.{addAlbum,addArtist}`+ * Supported batch operations for `N.L.Track.scrobble`, `N.L.Library.{addAlbum,addArtist}` 0.2.0.0 =======
LICENSE view
@@ -1,4 +1,4 @@-Copyright (C) 2012 Matvey Aksenov, Dmitry Malikov+Copyright (C) 2012-2014 Matvey Aksenov, Dmitry Malikov 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
README.md view
@@ -1,25 +1,35 @@-# liblastfm-[](http://hackage.haskell.org/package/liblastfm)+liblastfm+=========+[](https://hackage.haskell.org/package/liblastfm) [](https://drone.io/github.com/supki/liblastfm/latest)-[](http://travis-ci.org/supki/liblastfm)+[](https://travis-ci.org/supki/liblastfm) -Complete API interface to [last.fm][1] service.+Complete API interface to [last.fm][last.fm] service. Documentation is available in two flavours:- * original [API reference][2]- * liblastfm [haddocks][3] -## General introduction+ * 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][4]++ * [signing][last.fm/sign]+ * url-encoding+ * miscellaneous stuff like choosing correct HTTP method, etc -Once request is ready, liblastfm may send it and get you back a response.+Once request is ready, liblastfm can 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+ * [aeson][aeson] `Value` for json queries++ * [xml-conduit][xml-conduit] `Document` for xml queries++Installation+------------ To install either use hackage: % cabal install liblastfm@@ -30,13 +40,14 @@ % 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].+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: - ghci> import Network.Lastfm -- a bunch of useful utilities- ghci> import qualified Network.Lastfm.Tag as Tag -- for Tag.search+ >>> 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:@@ -45,57 +56,73 @@ To send constructed request use `lastfm`: - ghci> lastfm $ Tag.search <*> tag "russian-folk" <* limit 10 <*> apiKey "29effec263316a1f8a97f753caaa83e0" <* json+ >>> 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].+[Wiki][liblastfm/wiki] describes how to parse responses. -## FAQ+FAQ+---+ **Q: I'm getting the following error. How do I fix it?**+ ```-> Artist.getInfo <*> artist "Pink Floyd" <*> apiKey "29effec263316a1f8a97f753caaa83e0"+>>> 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")+>>> 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+ 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 `Request`.++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 -```-> 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\" ...+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__" ```-- - [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+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
+ examples/README.md 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
+ examples/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+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 view
@@ -0,0 +1,99 @@+name: liblastfm-examples+version: 0.4.0.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.0+ 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.0+ , 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.0+ , 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.0+ 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.0+ , 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.0+ 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.0+ main-is: recommendations.hs+ ghc-options: -Wall+ -fno-warn-unused-do-bind
+ examples/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+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 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+ - 坂本真綾+ - 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 view
@@ -0,0 +1,31 @@+{-# 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+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 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+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 view
@@ -0,0 +1,73 @@+{-# 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+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 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+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.3.2.0+version: 0.4.0.0 synopsis: Lastfm API interface license: MIT license-file: LICENSE@@ -13,30 +13,46 @@ 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 source-repository head type: git location: https://github.com/supki/liblastfm +flag test-api+ description: a real API test+ default: False+ manual: True+ library default-language: Haskell2010 build-depends: aeson- , base >= 3 && < 5+ , base >= 4 && < 5 , bytestring , cereal- , containers >= 0.5+ , containers >= 0.5 , contravariant , crypto-api- , http-conduit >= 1.9- , http-types+ , http-client >= 0.3+ , http-client-tls >= 0.2 , network+ , profunctors , pureMD5 , semigroups- , tagged , text , void- hs-source-dirs: src+ , xml-conduit >= 1.1+ hs-source-dirs:+ src exposed-modules: Network.Lastfm Network.Lastfm.Album@@ -61,3 +77,88 @@ -Wall -fno-warn-unused-do-bind -funbox-strict-fields++test-suite api+ if !flag(test-api)+ buildable: False+ default-language: Haskell2010+ build-depends:+ aeson+ , base >= 4 && < 5+ , bytestring+ , HUnit+ , lens >= 4.0.1+ , liblastfm+ , hspec+ , text+ , xml-html-conduit-lens >= 0.3+ type: exitcode-stdio-1.0+ hs-source-dirs:+ test/api+ main-is: Spec.hs+ other-modules:+ SpecHelper+ Json.AlbumSpec+ Json.ArtistSpec+ Json.ChartSpec+ Json.EventSpec+ Json.GeoSpec+ Json.GroupSpec+ Json.LibrarySpec+ Json.PlaylistSpec+ Json.TagSpec+ Json.TasteometerSpec+ Json.TrackSpec+ Json.UserSpec+ Json.VenueSpec+ Xml.AlbumSpec+ Xml.ArtistSpec+ Xml.ChartSpec+ Xml.EventSpec+ Xml.GeoSpec+ Xml.GroupSpec+ Xml.LibrarySpec+ Xml.PlaylistSpec+ Xml.TagSpec+ Xml.TasteometerSpec+ Xml.TrackSpec+ Xml.UserSpec+ Xml.VenueSpec+ ghc-options:+ -Wall+ -fno-warn-orphans+ -fno-warn-unused-do-bind++test-suite spec+ default-language: Haskell2010+ build-depends:+ aeson+ , base >= 4 && < 5+ , bytestring+ , cereal+ , containers+ , contravariant+ , crypto-api+ , hspec+ , hspec-expectations-lens >= 0.3.0.0+ , http-client+ , http-client-tls+ , lens >= 4.0+ , network+ , profunctors+ , pureMD5+ , text+ , void+ , xml-conduit+ , xml-html-conduit-lens >= 0.3+ type: exitcode-stdio-1.0+ hs-source-dirs:+ src+ test/spec+ main-is: Spec.hs+ other-modules:+ Network.Lastfm.ResponseSpec+ ghc-options:+ -Wall+ cpp-options:+ -DTEST
src/Network/Lastfm/Internal.hs view
@@ -1,3 +1,7 @@+{-# LANGUAGE CPP #-}+#if __GLASGOW_HASKELL__ >= 708+{-# LANGUAGE AutoDeriveTypeable #-}+#endif {-# LANGUAGE DataKinds #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE KindSignatures #-}@@ -5,26 +9,35 @@ -- -- You shouldn't need to import this module unless you are doing something interesting. module Network.Lastfm.Internal- ( Request(..), Format(..), Ready, Sign- , R(..), wrap, unwrap, render, coerce- , absorbQuery, indexedWith+ ( Request(..)+ , Format(..)+ , Ready+ , Sign+ , R(..)+ , wrap+ , unwrap+ , render+ , coerce+ , absorbQuery+ , indexedWith -- * Lenses- , host, method, query+ , host+ , method+ , query ) where -import Control.Applicative-import Data.Foldable (Foldable(..))-import Data.Monoid-import Data.Traversable (Traversable(..))-+import Control.Applicative import Data.ByteString (ByteString)+import Data.Foldable (Foldable(..)) import Data.Functor.Contravariant (Contravariant(..)) import Data.Map.Strict (Map) import qualified Data.Map.Strict as M+import Data.Monoid import Data.Serialize (Serialize(..)) import Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.Encoding as T+import Data.Traversable (Traversable(..)) import Data.Void (absurd) import Network.URI (escapeURIChar, isUnreserved) @@ -50,11 +63,10 @@ -- | Lastfm API request data type ----- @a@ is authentication state. Might be 'Send' which indicates--- that you may send this request already or 'Sign', when request signature--- isn't computed yet+-- @a@ is the authentication state. Can be 'Ready', which means this 'Request' is+-- ready to be sent, or 'Sign', if the request signature hasn't been computed yet ----- @f@ is response format. liblastfm currently supports 'JSON' or 'XML'+-- @f@ is the response format (liblastfm supports both 'JSON' and 'XML') newtype Request f a = Request { unRequest :: Const (Dual (Endo (R f))) a } instance Functor (Request f) where@@ -130,16 +142,16 @@ put r = do put $ T.encodeUtf8 (_host r) put $ _method r- put $ mapmap T.encodeUtf8 T.encodeUtf8 (_query r)+ put $ bimap T.encodeUtf8 T.encodeUtf8 (_query r) get = do h <- T.decodeUtf8 <$> get m <- get- q <- mapmap T.decodeUtf8 T.decodeUtf8 <$> get+ q <- bimap T.decodeUtf8 T.decodeUtf8 <$> get return R { _host = h, _method = m, _query = q } -mapmap :: (Ord s, Ord t) => (s -> t) -> (a -> b) -> Map s a -> Map t b-mapmap f g = M.mapKeys f . M.map g-{-# INLINE mapmap #-}+bimap :: (Ord s, Ord t) => (s -> t) -> (a -> b) -> Map s a -> Map t b+bimap f g = M.mapKeys f . M.map g+{-# INLINE bimap #-} -- | 'Request' '_host'
src/Network/Lastfm/Response.hs view
@@ -1,136 +1,231 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TypeFamilies #-} -- | Request sending and Response parsing module Network.Lastfm.Response- ( -- * Sign Request+ ( -- * Compute request signature -- $sign- Secret(..), sign- -- * Get 'Response'- , Response, Supported, lastfm, lastfm_+ Secret(..)+ , sign+ -- * Get response+ , Supported+ , Format(..)+ , lastfm+ , lastfm_+ -- ** Errors+ , LastfmError(..)+ , _LastfmBadResponse+ , _LastfmEncodedError+ , _LastfmHttpError -- ** Internal- , lastfm', finalize+ , lastfmWith+ , finalize+#ifdef TEST+ , parse+ , md5+#endif ) where -import Control.Applicative-import Control.Exception (throw)-import Control.Monad-import Data.Monoid-import Data.String (IsString(..))-import Data.Proxy (Proxy(..))-+import Control.Applicative+import Control.Exception (SomeException(..), Exception(..), catch) import Crypto.Classes (hash')-import Data.Aeson ((.:), Value, decode, parseJSON)+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 Network.HTTP.Conduit as C-import qualified Network.HTTP.Types as C+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 Text.XML (Document, parseLBS, def)+import Text.XML.Cursor -import Network.Lastfm.Internal+import Network.Lastfm.Internal -- $sign ----- Signing is important part of every--- authentication requiring API request.--- Basically, every such request is appended--- with md5 footprint of its arguments as+-- The signature is required for every+-- authenticated API request. Basically,+-- every such request appends the md5 footprint+-- of its arguments to the query as -- described at <http://www.last.fm/api/authspec#8> --- | 'Supported' provides parsing for a chosen 'Format'+-- | 'Supported' provides parsing for the 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 :: proxy f -> Lazy.ByteString -> C.ResponseHeaders -> Response f- base :: R f+-- 'JSON' is parsed to 'Value' type from aeson, while 'XML'+-- is parsed to 'Document' from xml-conduit+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 JSON where- type Response JSON = Maybe Value- parse _ b hs = do- v <- decode b- 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- { _host = "https://ws.audioscrobbler.com/2.0/"- , _method = "GET"- , _query = M.fromList [("format", "json")]- }- {-# INLINE base #-}+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 -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"- , _query = mempty- }- {-# INLINE base #-}+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+ } ++-- | Different ways last.fm response can be unusable+data LastfmError =+ -- | last.fm thinks it responded with something legible, but it really isn't+ LastfmBadResponse Lazy.ByteString+ -- | last.fm error code and message string+ | LastfmEncodedError Int Text+ -- | wrapped http-conduit exception+ | LastfmHttpError N.HttpException+ deriving (Show, Typeable)++-- | Admittedly, this isn't the best 'Eq' instance ever+-- but not having 'Eq' 'C.HttpException' does not leave much a choice+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+ {-# INLINE _LastfmError #-}++instance AsLastfmError SomeException where+ _LastfmError = dimap (\e -> maybe (Left e) Right (fromException e)) (either pure (fmap toException)) . right'+ {-# INLINE _LastfmError #-}++-- | This is a @ Prism' 'LastfmError' 'Lazy.ByteString' @ in disguise+_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+ {-# INLINE go #-}+{-# INLINE _LastfmBadResponse #-}++-- | This is a @ Prism' 'LastfmError' ('Int', 'String') @ in disguise+_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+ {-# INLINE go #-}+{-# INLINE _LastfmEncodedError #-}++-- | 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)+_LastfmHttpError = _LastfmError . dimap go (either pure (fmap LastfmHttpError)) . right' where+ go (LastfmHttpError e) = Right e+ go x = Left x+ {-# INLINE go #-}+{-# INLINE _LastfmHttpError #-}++ -- | Application secret-newtype Secret = Secret Text deriving (Show)+newtype Secret = Secret Text deriving (Show, Eq, Typeable) instance IsString Secret where fromString = Secret . fromString --- | Sign 'Request' with 'Secret'+-- | Sign the 'Request' with the 'Secret' so it's ready to be sent sign :: Secret -> Request f Sign -> Request f Ready sign s = coerce . (<* signature) where signature = wrap $- \r@R { _query = q } -> r { _query = api_sig s . authToken $ q }+ \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)+ 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+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+ signer = md5 . M.foldrWithKey (\k v xs -> k <> v <> xs) s +-- | Get supplied string md5 hash hex representation 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' parse . finalize---- | Send 'Request' without parsing the 'Response'-lastfm_ :: Supported f => Request f Ready -> IO ()-lastfm_ = lastfm' (\_ _ _ -> ()) . finalize----- | Get 'R' from 'Request'-finalize :: Supported f => Request f Ready -> R f-finalize = ($ base) . unwrap+-- | Send the 'Request' and parse the 'Response'+lastfm :: Supported f r => Request f Ready -> IO (Either LastfmError r)+lastfm = lastfmWith parse . finalize +-- | Send the 'Request' without parsing the 'Response'+lastfm_ :: Supported f r => Request f Ready -> IO (Either LastfmError ())+lastfm_ = lastfmWith (\_ -> Right ()) . finalize --- | 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)+-- | Send the 'R' and parse the 'Response' with the supplied parser+lastfmWith+ :: Supported f r+ => (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) let req' = req- { C.method = _method request- , C.responseTimeout = Just 10000000+ { N.method = _method r+ , N.responseTimeout = Just 10000000 }- res <- C.httpLbs req' manager- return $ f Proxy (C.responseBody res) (C.responseHeaders res)+ p . N.responseBody <$> N.httpLbs req' manager+ `catch`+ (return . Left)++-- | Get the 'R' from the 'Request'+finalize :: Supported f r => Request f Ready -> R f+finalize x = (prepareRequest . unwrap x) base
+ test/api/Json/AlbumSpec.hs view
@@ -0,0 +1,107 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+module Json.AlbumSpec (spec) where++import Data.Aeson (Value)+import Data.Aeson.Lens+import Data.Text (Text)+import Network.Lastfm+import Network.Lastfm.Album+import Test.Hspec++import SpecHelper+++spec :: Spec+spec = do+ it "addTags" $+ shouldHaveJson_ . privately $+ addTags <*> artist "Pink Floyd" <*> album "The Wall" <*> tags ["70s", "awesome", "classic"]++ it "getTags-authenticated" $+ privately (getTags <*> artist "Pink Floyd" <*> album "The Wall")+ `shouldHaveJson`+ key "tags".key "tag".values.key "name"._String++ it "removeTag" $+ shouldHaveJson_ . privately $+ removeTag <*> artist "Pink Floyd" <*> album "The Wall" <*> tag "awesome"++ it "share" $+ shouldHaveJson_ . privately $+ share <*> album "Jerusalem" <*> artist "Sleep" <*> recipient "liblastfm" <* message "Just listen!"++ describe "getBuyLinks*" $ do+ let jsonQuery :: Fold Value Text+ jsonQuery = key "affiliations".key "physicals".key "affiliation".values.key "supplierName"._String++ it "getBuyLinks" $+ publicly (getBuyLinks <*> country "United Kingdom" <*> artist "Pink Floyd" <*> album "The Wall")+ `shouldHaveJson`+ jsonQuery++ it "getBuyLinks_mbid" $+ publicly (getBuyLinks <*> country "United Kingdom" <*> mbid "816abcd3-924a-3565-92b9-7ab750688f34")+ `shouldHaveJson`+ jsonQuery++ describe "getInfo*" $ do+ let jsonQuery :: Fold Value Text+ jsonQuery = key "album".key "toptags".key "tag".values.key "name"._String++ it "getInfo" $+ publicly (getInfo <*> artist "Pink Floyd" <*> album "The Wall")+ `shouldHaveJson`+ jsonQuery++ it "getInfo_mbid" $+ publicly (getInfo <*> mbid "816abcd3-924a-3565-92b9-7ab750688f34")+ `shouldHaveJson`+ jsonQuery++ describe "getShouts*" $ do+ let jsonQuery :: Fold Value Text+ jsonQuery = key "shouts".key "shout".values.key "body"._String++ it "getShouts" $+ publicly (getShouts <*> artist "Pink Floyd" <*> album "The Wall" <* limit 7)+ `shouldHaveJson`+ jsonQuery++ it "getShouts_mbid" $+ publicly (getShouts <*> mbid "816abcd3-924a-3565-92b9-7ab750688f34" <* limit 7)+ `shouldHaveJson`+ jsonQuery++ describe "getTags*" $ do+ let jsonQuery :: Fold Value Text+ jsonQuery = key "tags".key "tag".values.key "name"._String++ it "getTags" $+ publicly (getTags <*> artist "Pink Floyd" <*> album "The Wall" <* user "liblastfm")+ `shouldHaveJson`+ jsonQuery++ it "getTags_mbid" $+ publicly (getTags <*> mbid "816abcd3-924a-3565-92b9-7ab750688f34" <* user "liblastfm")+ `shouldHaveJson`+ jsonQuery++ describe "getTopTags*" $ do+ let jsonQuery :: Fold Value Text+ jsonQuery = key "toptags".key "tag".values.key "count"._String++ it "getTopTags" $+ publicly (getTopTags <*> artist "Pink Floyd" <*> album "The Wall")+ `shouldHaveJson`+ jsonQuery++ it "getTopTags_mbid" $+ publicly (getTopTags <*> mbid "816abcd3-924a-3565-92b9-7ab750688f34")+ `shouldHaveJson`+ jsonQuery++ it "search" $+ publicly (search <*> album "wall" <* limit 5)+ `shouldHaveJson`+ key "results".key "albummatches".key "album".values.key "name"._String
+ test/api/Json/ArtistSpec.hs view
@@ -0,0 +1,196 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+module Json.ArtistSpec (spec) where++import Data.Aeson (Value)+import Data.Aeson.Lens+import Data.Text (Text)+import Network.Lastfm+import Network.Lastfm.Artist+import Test.Hspec++import SpecHelper+++spec :: Spec+spec = do+ it "addTags" $+ shouldHaveJson_ . privately $+ addTags <*> artist "Егор Летов" <*> tags ["russian", "black metal"]++ it "getTags-authenticated" $+ privately (getTags <*> artist "Егор Летов")+ `shouldHaveJson`+ key "tags".key "tag".values.key "name"._String++ it "removeTag" $+ shouldHaveJson_ . privately $+ removeTag <*> artist "Егор Летов" <*> tag "russian"++ it "share" $+ shouldHaveJson_ . privately $+ share <*> artist "Sleep" <*> recipient "liblastfm" <* message "Just listen!"++ it "getCorrection" $+ publicly (getCorrection <*> artist "Meshugah")+ `shouldHaveJson`+ key "corrections".key "correction".key "artist".key "name"._String++ describe "getEvents*" $ do+ let jsonQuery :: Fold Value Text+ jsonQuery = key "events".key "artist"._String++ it "getEvents" $+ publicly (getEvents <*> artist "Meshuggah" <* limit 2)+ `shouldHaveJson`+ jsonQuery++ it "getEvents_mbid" $+ publicly (getEvents <*> mbid "cf8b3b8c-118e-4136-8d1d-c37091173413" <* limit 2)+ `shouldHaveJson`+ jsonQuery++ describe "getInfo*" $ do+ let jsonQuery :: Fold Value Text+ jsonQuery = key "artist".key "stats".key "listeners"._String++ it "getInfo" $+ publicly (getInfo <*> artist "Meshuggah")+ `shouldHaveJson`+ jsonQuery++ it "getInfo_mbid" $+ publicly (getInfo <*> mbid "cf8b3b8c-118e-4136-8d1d-c37091173413")+ `shouldHaveJson`+ jsonQuery++ describe "getPastEvents*" $ do+ let jsonQuery :: Fold Value Text+ jsonQuery = key "events".key "event".values.key "title"._String++ it "getPastEvents" $+ publicly (getPastEvents <*> artist "Meshuggah" <* autocorrect True)+ `shouldHaveJson`+ jsonQuery++ it "getPastEvents_mbid" $+ publicly (getPastEvents <*> mbid "cf8b3b8c-118e-4136-8d1d-c37091173413" <* autocorrect True)+ `shouldHaveJson`+ jsonQuery++ describe "getPodcast*" $ do+ let jsonQuery :: Fold Value Text+ jsonQuery = key "rss".key "channel".key "description"._String++ it "getPodcast" $+ publicly (getPodcast <*> artist "Meshuggah")+ `shouldHaveJson`+ jsonQuery++ it "getPodcast_mbid" $+ publicly (getPodcast <*> mbid "cf8b3b8c-118e-4136-8d1d-c37091173413")+ `shouldHaveJson`+ jsonQuery++ describe "getShouts*" $ do+ let jsonQuery :: Fold Value Text+ jsonQuery = key "shouts".key "shout".values.key "author"._String++ it "getShouts" $+ publicly (getShouts <*> artist "Meshuggah" <* limit 5)+ `shouldHaveJson`+ jsonQuery++ it "getShouts_mbid" $+ publicly (getShouts <*> mbid "cf8b3b8c-118e-4136-8d1d-c37091173413" <* limit 5)+ `shouldHaveJson`+ jsonQuery++ describe "getSimilar*" $ do+ let jsonQuery :: Fold Value Text+ jsonQuery = key "similarartists".key "artist".values.key "name"._String++ it "getSimilar" $+ publicly (getSimilar <*> artist "Meshuggah" <* limit 3)+ `shouldHaveJson`+ jsonQuery++ it "getSimilar_mbid" $+ publicly (getSimilar <*> mbid "cf8b3b8c-118e-4136-8d1d-c37091173413" <* limit 3)+ `shouldHaveJson`+ jsonQuery++ describe "getTags*" $ do+ let jsonQuery :: Fold Value Text+ jsonQuery = key "tags".key "tag".values.key "name"._String++ it "getTags" $+ publicly (getTags <*> artist "Егор Летов" <* user "liblastfm")+ `shouldHaveJson`+ jsonQuery++ it "getTags_mbid" $+ publicly (getTags <*> mbid "cfb3d32e-d095-4d63-946d-9daf06932180" <* user "liblastfm")+ `shouldHaveJson`+ jsonQuery++ describe "getTopAlbums*" $ do+ let jsonQuery :: Fold Value Text+ jsonQuery = key "topalbums".key "album".values.key "name"._String++ it "getTopAlbums" $+ publicly (getTopAlbums <*> artist "Meshuggah" <* limit 3)+ `shouldHaveJson`+ jsonQuery++ it "getTopAlbums_mbid" $+ publicly (getTopAlbums <*> mbid "cf8b3b8c-118e-4136-8d1d-c37091173413" <* limit 3)+ `shouldHaveJson`+ jsonQuery++ describe "getTopFans*" $ do+ let jsonQuery :: Fold Value Text+ jsonQuery = key "topfans".key "user".values.key "name"._String++ it "getTopFans" $+ publicly (getTopFans <*> artist "Meshuggah")+ `shouldHaveJson`+ jsonQuery++ it "getTopFans_mbid" $+ publicly (getTopFans <*> mbid "cf8b3b8c-118e-4136-8d1d-c37091173413")+ `shouldHaveJson`+ jsonQuery++ describe "getTopTags*" $ do+ let jsonQuery :: Fold Value Text+ jsonQuery = key "toptags".key "tag".values.key "name"._String++ it "getTopTags" $+ publicly (getTopTags <*> artist "Meshuggah")+ `shouldHaveJson`+ jsonQuery++ it "getTopTags_mbid" $+ publicly (getTopTags <*> mbid "cf8b3b8c-118e-4136-8d1d-c37091173413")+ `shouldHaveJson`+ jsonQuery++ describe "getTopTracks*" $ do+ let jsonQuery :: Fold Value Text+ jsonQuery = key "toptracks".key "track".values.key "name"._String++ it "getTopTracks" $+ publicly (getTopTracks <*> artist "Meshuggah" <* limit 3)+ `shouldHaveJson`+ jsonQuery++ it "getTopTracks_mbid" $+ publicly (getTopTracks <*> mbid "cf8b3b8c-118e-4136-8d1d-c37091173413" <* limit 3)+ `shouldHaveJson`+ jsonQuery++ it "search" $+ publicly (search <*> artist "Mesh" <* limit 3)+ `shouldHaveJson`+ key "results".key "artistmatches".key "artist".values.key "name"._String
+ test/api/Json/ChartSpec.hs view
@@ -0,0 +1,53 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+module Json.ChartSpec (spec) where++import Data.Aeson (Value)+import Data.Aeson.Lens+import Data.Text (Text)+import Network.Lastfm+import Network.Lastfm.Chart+import Test.Hspec++import SpecHelper+++spec :: Spec+spec = do+ describe "get*Artists" $ do+ let jsonQuery :: Fold Value Text+ jsonQuery = key "artists".key "artist".values.key "name"._String++ it "getHypedArtists" $+ publicly (getHypedArtists <* limit 3)+ `shouldHaveJson`+ jsonQuery++ it "getTopArtists" $+ publicly (getTopArtists <* limit 4)+ `shouldHaveJson`+ jsonQuery++ describe "get*Tracks" $ do+ let jsonQuery :: Fold Value Text+ jsonQuery = key "tracks".key "track".values.key "name"._String++ it "getHypedTracks" $+ publicly (getHypedTracks <* limit 2)+ `shouldHaveJson`+ jsonQuery++ it "getLovedTracks" $+ publicly (getLovedTracks <* limit 3)+ `shouldHaveJson`+ jsonQuery++ it "getTopTracks" $+ publicly (getTopTracks <* limit 2)+ `shouldHaveJson`+ jsonQuery++ it "getTopTags" $+ publicly (getTopTags <* limit 5)+ `shouldHaveJson`+ key "tags".key "tag".values.key "name"._String
+ test/api/Json/EventSpec.hs view
@@ -0,0 +1,36 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+module Json.EventSpec (spec) where++import Data.Aeson.Lens+import Network.Lastfm+import Network.Lastfm.Event+import Test.Hspec++import SpecHelper+++spec :: Spec+spec = do+ it "attend" $+ shouldHaveJson_ . privately $+ attend <*> event 3142549 <*> status Attending++ it "share" $+ shouldHaveJson_ . privately $+ share <*> event 3142549 <*> recipient "liblastfm" <* message "Just listen!"++ it "getAttendees" $+ publicly (getAttendees <*> event 3142549 <* limit 2)+ `shouldHaveJson`+ key "attendees".key "user".values.key "name"._String++ it "getInfo" $+ publicly (getInfo <*> event 3142549)+ `shouldHaveJson`+ key "event".key "venue".key "location".key "city"._String++ it "getShouts" $+ publicly (getShouts <*> event 3142549 <* limit 1)+ `shouldHaveJson`+ key "shouts".key "shout".key "body"._String
+ test/api/Json/GeoSpec.hs view
@@ -0,0 +1,78 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+module Json.GeoSpec (spec) where++import Data.Aeson (Value)+import Data.Aeson.Lens+import Data.Text (Text)+import Network.Lastfm+import Network.Lastfm.Geo+import Test.Hspec++import SpecHelper+++spec :: Spec+spec = do+ it "getEvents" $+ publicly (getEvents <* location "Moscow" <* limit 5)+ `shouldHaveJson`+ key "events".key "event".values.key "id"._String++ describe "get*Artist*" $ do+ let jsonQuery :: Fold Value Text+ jsonQuery = key "topartists".key "artist".values.key "name"._String++ it "getMetroArtistChart" $+ publicly (getMetroArtistChart <*> metro "Saint Petersburg" <*> country "Russia")+ `shouldHaveJson`+ jsonQuery++ it "getMetroHypeArtistChart" $+ publicly (getMetroHypeArtistChart <*> metro "New York" <*> country "United States")+ `shouldHaveJson`+ jsonQuery++ it "getMetroUniqueArtistChart" $+ publicly (getMetroUniqueArtistChart <*> metro "Minsk" <*> country "Belarus")+ `shouldHaveJson`+ jsonQuery++ it "getTopArtists" $+ publicly (getTopArtists <*> country "Belarus" <* limit 3)+ `shouldHaveJson`+ jsonQuery++ it "getMetroWeeklyChartlist" $+ publicly (getMetroWeeklyChartlist <*> metro "Moscow")+ `shouldHaveJson`+ key "weeklychartlist".key "chart".values.key "from"._String++ it "getMetros" $+ publicly (getMetros <* country "Russia")+ `shouldHaveJson`+ key "metros".key "metro".values.key "name"._String++ describe "get*Track*" $ do+ let jsonQuery :: Fold Value Text+ jsonQuery = key "toptracks".key "track".values.key "name"._String++ it "getTopTracks" $+ publicly (getTopTracks <*> country "Ukraine" <* limit 2)+ `shouldHaveJson`+ jsonQuery++ it "getMetroUniqueTrackChart" $+ publicly (getMetroUniqueTrackChart <*> metro "Moscow" <*> country "Russia")+ `shouldHaveJson`+ jsonQuery++ it "getMetroHypeTrackChart" $+ publicly (getMetroHypeTrackChart <*> metro "Moscow" <*> country "Russia")+ `shouldHaveJson`+ jsonQuery++ it "getMetroTrackChart" $+ publicly (getMetroTrackChart <*> metro "Boston" <*> country "United States")+ `shouldHaveJson`+ jsonQuery
+ test/api/Json/GroupSpec.hs view
@@ -0,0 +1,47 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+module Json.GroupSpec (spec) where++import Data.Aeson.Lens+import Network.Lastfm+import Network.Lastfm.Group+import Test.Hspec++import SpecHelper+++spec :: Spec+spec = do+ it "getHype" $+ publicly (getHype <*> groupname)+ `shouldHaveJson`+ key "weeklyartistchart".key "artist".values.key "mbid"._String++ it "getMembers" $+ publicly (getMembers <*> groupname <* limit 10)+ `shouldHaveJson`+ key "members".key "user".values.key "name"._String++ it "getWeeklyAlbumChart" $+ publicly (getWeeklyAlbumChart <*> groupname)+ `shouldHaveJson`+ key "weeklyalbumchart".key "album".values.key "playcount"._String++ it "getWeeklyArtistChart" $+ publicly (getWeeklyArtistChart <*> groupname)+ `shouldHaveJson`+ key "weeklyartistchart".key "artist".values.key "name"._String++ it "getWeeklyChartList" $+ publicly (getWeeklyChartList <*> groupname)+ `shouldHaveJson`+ key "weeklychartlist".key "chart".values.key "from"._String++ it "getWeeklyTrackChart" $+ publicly (getWeeklyTrackChart <*> groupname)+ `shouldHaveJson`+ key "weeklytrackchart".key "track".values.key "url"._String++groupname :: Request f Group+groupname = group+ "People with no social lives that listen to more music than is healthy who are slightly scared of spiders and can never seem to find a pen"
+ test/api/Json/LibrarySpec.hs view
@@ -0,0 +1,56 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+module Json.LibrarySpec (spec) where++import Data.Aeson.Lens+import Network.Lastfm+import Network.Lastfm.Library+import Test.Hspec++import SpecHelper+++spec :: Spec+spec = do+ it "addAlbum" $+ shouldHaveJson_ . privately $+ addAlbum (pure (albumItem <*> artist "Franz Ferdinand" <*> album "Franz Ferdinand"))++ it "addArtist" $+ shouldHaveJson_ . privately $+ addArtist (pure (artistItem <*> artist "Mobthrow"))++ it "addTrack" $+ shouldHaveJson_ . privately $+ addTrack <*> artist "Eminem" <*> track "Kim"++ it "removeAlbum" $+ shouldHaveJson_ . privately $+ removeAlbum <*> artist "Franz Ferdinand" <*> album "Franz Ferdinand"++ it "removeArtist" $+ shouldHaveJson_ . privately $+ removeArtist <*> artist "Burzum"++ it "removeTrack" $+ shouldHaveJson_ . privately $+ removeTrack <*> artist "Eminem" <*> track "Kim"++ it "removeScrobble" $+ shouldHaveJson_ . privately $+ removeScrobble <*> artist "Gojira" <*> track "Ocean" <*> timestamp 1328905590++ it "getAlbums" $+ publicly (getAlbums <*> user "smpcln" <* artist "Burzum" <* limit 5)+ `shouldHaveJson`+ key "albums".key "album".values.key "name"._String++ it "getArtists" $+ publicly (getArtists <*> user "smpcln" <* limit 7)+ `shouldHaveJson`+ key "artists".key "artist".values.key "name"._String++ it "getTracks" $+ publicly (getTracks <*> user "smpcln" <* artist "Burzum" <* limit 4)+ `shouldHaveJson`+ key "tracks".key "track".values.key "name"._String
+ test/api/Json/PlaylistSpec.hs view
@@ -0,0 +1,43 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+module Json.PlaylistSpec (spec) where++import Control.Lens+import Data.Aeson (Value)+import Data.Aeson.Lens+import Data.Int (Int64)+import Data.Text.Lens (unpacked)+import Network.Lastfm hiding (to)+import Network.Lastfm.Playlist+import Network.Lastfm.User+import Test.Hspec+import Test.HUnit (assertFailure)+import Text.Printf+import Text.Read (readMaybe)++import SpecHelper+++spec :: Spec+spec = do+ it "create" $ -- Order matters.+ privately (create <* title "Awesome playlist")+ `shouldHaveJson`+ key "playlists".key "playlist".key "title"._String++ it "addTrack" $ do+ r <- lastfm $ getPlaylists <*> user "liblastfm" <*> ak' <* json+ case r of+ Left e ->+ assertFailure (printf "last.fm error: %s" (show e))+ Right val ->+ case preview pl val of+ Just (Just pid) -> shouldHaveJson_ . privately $+ addTrack <*> playlist pid <*> artist "Ruby my dear" <*> track "Chazz"+ _ -> assertFailure (printf "bad JSON object: %s" (show val))++ak' :: Request f APIKey+ak' = apiKey "29effec263316a1f8a97f753caaa83e0"++pl :: Fold Value (Maybe Int64)+pl = key "playlists".key "playlist".values.key "id"._String.unpacked.to readMaybe
+ test/api/Json/TagSpec.hs view
@@ -0,0 +1,58 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+module Json.TagSpec (spec) where++import Data.Aeson.Lens+import Network.Lastfm+import Network.Lastfm.Tag+import Test.Hspec++import SpecHelper+++spec :: Spec+spec = do+ it "getInfo" $+ publicly (getInfo <*> tag "depressive")+ `shouldHaveJson`+ key "tag".key "taggings"._String++ it "getSimilar" $+ publicly (getSimilar <*> tag "depressive")+ `shouldHaveJson`+ key "similartags".key "tag".values.key "name"._String++ it "getTopAlbums" $+ publicly (getTopAlbums <*> tag "depressive" <* limit 2)+ `shouldHaveJson`+ key "topalbums".key "album".values.key "url"._String++ it "getTopArtists" $+ publicly (getTopArtists <*> tag "depressive" <* limit 3)+ `shouldHaveJson`+ key "topartists".key "artist".values.key "url"._String++ it "getTopTags" $+ publicly getTopTags+ `shouldHaveJson`+ key "toptags".key "tag".values.key "name"._String++ it "getTopTracks" $+ publicly (getTopTracks <*> tag "depressive" <* limit 2)+ `shouldHaveJson`+ key "toptracks".key "track".values.key "url"._String++ it "getWeeklyArtistChart" $+ publicly (getWeeklyArtistChart <*> tag "depressive" <* limit 3)+ `shouldHaveJson`+ key "weeklyartistchart".key "artist".values.key "name"._String++ it "getWeeklyChartList" $+ publicly (getWeeklyChartList <*> tag "depressive")+ `shouldHaveJson`+ key "weeklychartlist".key "chart".values.key "from"._String++ it "search" $+ publicly (search <*> tag "depressive" <* limit 3)+ `shouldHaveJson`+ key "results".key "tagmatches".key "tag".values.key "name"._String
+ test/api/Json/TasteometerSpec.hs view
@@ -0,0 +1,38 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+module Json.TasteometerSpec (spec) where++import Data.Aeson (Value)+import Data.Aeson.Lens+import Data.Text (Text)+import Network.Lastfm+import qualified Network.Lastfm.Tasteometer as Taste+import Test.Hspec++import SpecHelper+++spec :: Spec+spec = do+ it "compare" $+ publicly (Taste.compare (user "smpcln") (user "MCDOOMDESTROYER"))+ `shouldHaveJson`+ jsonQuery++ it "compare" $+ publicly (Taste.compare (user "smpcln") (artists ["enduser", "venetian snares"]))+ `shouldHaveJson`+ jsonQuery++ it "compare" $+ publicly (Taste.compare (artists ["enduser", "venetian snares"]) (user "smpcln"))+ `shouldHaveJson`+ jsonQuery++ it "compare" $+ publicly (Taste.compare (artists ["enduser", "venetian snares"]) (artists ["enduser", "venetian snares"]))+ `shouldHaveJson`+ jsonQuery++jsonQuery :: Fold Value Text+jsonQuery = key "comparison".key "result".key "score"._String
+ test/api/Json/TrackSpec.hs view
@@ -0,0 +1,157 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+module Json.TrackSpec (spec) where++import Data.Aeson (Value)+import Data.Aeson.Lens+import Data.Text (Text)+import Network.Lastfm+import Network.Lastfm.Track+import Test.Hspec++import SpecHelper+++spec :: Spec+spec = do+ it "addTags" $+ shouldHaveJson_ . privately $+ addTags <*> artist "Jefferson Airplane" <*> track "White rabbit" <*> tags ["60s", "awesome"]++ it "ban" $+ shouldHaveJson_ . privately $+ ban <*> artist "Eminem" <*> track "Kim"++ it "love" $+ shouldHaveJson_ . privately $+ love <*> artist "Gojira" <*> track "Ocean"++ it "removeTag" $+ shouldHaveJson_ . privately $+ removeTag <*> artist "Jefferson Airplane" <*> track "White rabbit" <*> tag "awesome"++ it "share" $+ shouldHaveJson_ . privately $+ share <*> artist "Led Zeppelin" <*> track "When the Levee Breaks" <*> recipient "liblastfm" <* message "Just listen!"++ it "unban" $+ shouldHaveJson_ . privately $+ unban <*> artist "Eminem" <*> track "Kim"++ it "unlove" $+ shouldHaveJson_ . privately $+ unlove <*> artist "Gojira" <*> track "Ocean"++ it "scrobble" $+ privately (scrobble (pure (item <*> artist "Gojira" <*> track "Ocean" <*> timestamp 1300000000)))+ `shouldHaveJson`+ key "scrobbles".key "scrobble".key "track".key "#text"._String++ it "updateNowPlaying" $+ privately (updateNowPlaying <*> artist "Gojira" <*> track "Ocean")+ `shouldHaveJson`+ key "nowplaying".key "track".key "#text"._String++ it "getBuylinks" $+ publicly (getBuyLinks <*> country "United Kingdom" <*> artist "Pink Floyd" <*> track "Brain Damage")+ `shouldHaveJson`+ key "affiliations".key "downloads".key "affiliation".values.key "supplierName"._String++ it "getCorrection" $+ publicly (getCorrection <*> artist "Pink Ployd" <*> track "Brain Damage")+ `shouldHaveJson`+ key "corrections".key "correction".key "track".key "artist".key "name"._String++ it "getFingerprintMetadata" $+ publicly (getFingerprintMetadata <*> fingerprint 1234)+ `shouldHaveJson`+ key "tracks".key "track".values.key "name"._String++ describe "getInfo*" $ do+ let jsonQuery :: Fold Value Text+ jsonQuery = key "track".key "userplaycount"._String++ it "getInfo" $+ publicly (getInfo <*> artist "Pink Floyd" <*> track "Comfortably Numb" <* username "aswalrus")+ `shouldHaveJson`+ jsonQuery++ it "getInfo_mbid" $+ publicly (getInfo <*> mbid "52d7c9ff-6ae4-48a6-acec-4c1a486f8c92" <* username "aswalrus")+ `shouldHaveJson`+ jsonQuery++ describe "getShouts*" $ do+ let jsonQuery :: Fold Value Text+ jsonQuery = key "shouts".key "shout".values.key "author"._String++ it "getShouts" $+ publicly (getShouts <*> artist "Pink Floyd" <*> track "Comfortably Numb" <* limit 7)+ `shouldHaveJson`+ jsonQuery++ it "getShouts_mbid" $+ publicly (getShouts <*> mbid "52d7c9ff-6ae4-48a6-acec-4c1a486f8c92" <* limit 7)+ `shouldHaveJson`+ jsonQuery++ describe "getSimilar*" $ do+ let jsonQuery :: Fold Value Text+ jsonQuery = key "similartracks".key "track".values.key "name"._String++ it "getSimilar" $+ publicly (getSimilar <*> artist "Pink Floyd" <*> track "Comfortably Numb" <* limit 4)+ `shouldHaveJson`+ jsonQuery++ it "getSimilar_mbid" $+ publicly (getSimilar <*> mbid "52d7c9ff-6ae4-48a6-acec-4c1a486f8c92" <* limit 4)+ `shouldHaveJson`+ jsonQuery++ describe "getTags*" $ do+ let jsonQuery :: Fold Value Text+ jsonQuery = key "tags".key "@attr".key "track"._String++ it "getTags" $+ publicly (getTags <*> artist "Jefferson Airplane" <*> track "White Rabbit" <* user "liblastfm")+ `shouldHaveJson`+ jsonQuery++ it "getTags_mbid" $+ publicly (getTags <*> mbid "001b3337-faf4-421a-a11f-45e0b60a7703" <* user "liblastfm")+ `shouldHaveJson`+ jsonQuery++ describe "getTopFans*" $ do+ let jsonQuery :: Fold Value Text+ jsonQuery = key "topfans".key "user".values.key "name"._String++ it "getTopFans" $+ publicly (getTopFans <*> artist "Pink Floyd" <*> track "Comfortably Numb")+ `shouldHaveJson`+ jsonQuery++ it "getTopFans_mbid" $+ publicly (getTopFans <*> mbid "52d7c9ff-6ae4-48a6-acec-4c1a486f8c92")+ `shouldHaveJson`+ jsonQuery++ describe "getTopTags*" $ do+ let jsonQuery :: Fold Value Text+ jsonQuery = key "toptags".key "tag".values.key "name"._String++ it "getTopTags" $+ publicly (getTopTags <*> artist "Pink Floyd" <*> track "Comfortably Numb")+ `shouldHaveJson`+ jsonQuery++ it "getTopTags_mbid" $+ publicly (getTopTags <*> mbid "52d7c9ff-6ae4-48a6-acec-4c1a486f8c92")+ `shouldHaveJson`+ jsonQuery++ it "search" $+ publicly (search <*> track "Believe" <* limit 12)+ `shouldHaveJson`+ key "results".key "trackmatches".key "track".values.key "name"._String
+ test/api/Json/UserSpec.hs view
@@ -0,0 +1,137 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+module Json.UserSpec (spec) where++import Data.Aeson.Lens+import Network.Lastfm+import Network.Lastfm.User+import Test.Hspec++import SpecHelper+++spec :: Spec+spec = do+ it "getRecentStations" $+ privately (getRecentStations <*> user "liblastfm" <* limit 10)+ `shouldHaveJson`+ key "recentstations".key "station".values.key "name"._String++ it "getRecommendedArtists" $+ privately (getRecommendedArtists <* limit 10)+ `shouldHaveJson`+ key "recommendations".key "artist".values.key "name"._String++ it "getRecommendedEvents" $+ privately (getRecommendedEvents <* limit 10)+ `shouldHaveJson`+ key "events".key "event".key "url"._String++ it "shout" $+ shouldHaveJson_ . privately $+ shout <*> user "liblastfm" <*> message "test message"++ it "getArtistTracks" $+ publicly (getArtistTracks <*> user "smpcln" <*> artist "Dvar")+ `shouldHaveJson`+ key "artisttracks".key "track".values.key "name"._String++ it "getBannedTracks" $+ publicly (getBannedTracks <*> user "smpcln" <* limit 10)+ `shouldHaveJson`+ key "bannedtracks".key "track".values.key "name"._String++ it "getEvents" $+ publicly (getEvents <*> user "chansonnier" <* limit 5)+ `shouldHaveJson`+ key "events".key "event".values.key "venue".key "url"._String++ it "getFriends" $+ publicly (getFriends <*> user "smpcln" <* limit 10)+ `shouldHaveJson`+ key "friends".key "user".values.key "name"._String++ it "getPlayCount" $+ publicly (getInfo <*> user "smpcln")+ `shouldHaveJson`+ key "user".key "playcount"._String++ it "getGetLovedTracks" $+ publicly (getLovedTracks <*> user "smpcln" <* limit 10)+ `shouldHaveJson`+ key "lovedtracks".key "track".values.key "name"._String++ it "getNeighbours" $+ publicly (getNeighbours <*> user "smpcln" <* limit 10)+ `shouldHaveJson`+ key "neighbours".key "user".values.key "name"._String++ it "getNewReleases" $+ publicly (getNewReleases <*> user "rj")+ `shouldHaveJson`+ key "albums".key "album".values.key "url"._String++ it "getPastEvents" $+ publicly (getPastEvents <*> user "mokele" <* limit 5)+ `shouldHaveJson`+ key "events".key "event".values.key "url"._String++ it "getPersonalTags" $+ publicly (getPersonalTags <*> user "crackedcore" <*> tag "rhythmic noise" <*> taggingType "artist" <* limit 10)+ `shouldHaveJson`+ key "taggings".key "artists".key "artist".values.key "name"._String++ it "getPlaylists" $+ publicly (getPlaylists <*> user "mokele")+ `shouldHaveJson`+ key "playlists".key "playlist".values.key "title"._String++ it "getRecentTracks" $+ publicly (getRecentTracks <*> user "smpcln" <* limit 10)+ `shouldHaveJson`+ key "recenttracks".key "track".values.key "name"._String++ it "getShouts" $+ publicly (getShouts <*> user "smpcln" <* limit 2)+ `shouldHaveJson`+ key "shouts".key "shout".values.key "body"._String++ it "getTopAlbums" $+ publicly (getTopAlbums <*> user "smpcln" <* limit 5)+ `shouldHaveJson`+ key "topalbums".key "album".values.key "artist".key "name"._String++ it "getTopArtists" $+ publicly (getTopArtists <*> user "smpcln" <* limit 5)+ `shouldHaveJson`+ key "topartists".key "artist".values.key "name"._String++ it "getTopTags" $+ publicly (getTopTags <*> user "smpcln" <* limit 10)+ `shouldHaveJson`+ key "toptags".key "tag".values.key "name"._String++ it "getTopTracks" $+ publicly (getTopTracks <*> user "smpcln" <* limit 10)+ `shouldHaveJson`+ key "toptracks".key "track".values.key "url"._String++ it "getWeeklyAlbumChart" $+ publicly (getWeeklyAlbumChart <*> user "smpcln")+ `shouldHaveJson`+ key "weeklyalbumchart".key "album".values.key "url"._String++ it "getWeeklyArtistChart" $+ publicly (getWeeklyArtistChart <*> user "smpcln")+ `shouldHaveJson`+ key "weeklyartistchart".key "artist".values.key "url"._String++ it "getWeeklyChartList" $+ publicly (getWeeklyChartList <*> user "smpcln")+ `shouldHaveJson`+ key "weeklychartlist".key "chart".values.key "from"._String++ it "getWeeklyTrackChart" $+ publicly (getWeeklyTrackChart <*> user "smpcln")+ `shouldHaveJson`+ key "weeklytrackchart".key "track".values.key "url"._String
+ test/api/Json/VenueSpec.hs view
@@ -0,0 +1,28 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+module Json.VenueSpec (spec) where++import Data.Aeson.Lens+import Network.Lastfm+import Network.Lastfm.Venue+import Test.Hspec++import SpecHelper+++spec :: Spec+spec = do+ it "getEvents" $+ publicly (getEvents <*> venue 9163107)+ `shouldHaveJson`+ key "events".key "event".values.key "venue".key "name"._String++ it "getPastEvents" $+ publicly (getPastEvents <*> venue 9163107 <* limit 2)+ `shouldHaveJson`+ key "events".key "event".values.key "title"._String++ it "search" $+ publicly (search <*> venueName "Arena")+ `shouldHaveJson`+ key "results".key "venuematches".key "venue".values.key "id"._String
+ test/api/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
+ test/api/SpecHelper.hs view
@@ -0,0 +1,98 @@+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+module SpecHelper+ ( -- * Expectations+ Fold+ , shouldHaveResponse+ , shouldHaveJson+ , shouldHaveJson_+ , shouldHaveXml+ , shouldHaveXml_+ -- * public data+ , publicly+ , publicKey+ -- * private data+ -- $awful+ , privately+ , privateAPIKey+ , privateSessionKey+ , privateSecret+ ) 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++infixl 1 `shouldHaveJson`, `shouldHaveXml`+++-- | Inspect 'Response' with 'Query'+shouldHaveResponse :: (Show r, Supported f r) => Request f Ready -> Fold r a -> Expectation+shouldHaveResponse q l = do+ r <- lastfm q+ case preview (_Right.l) r of+ Just _ -> return ()+ Nothing -> assertFailure (printf "Query failed on %s" (show r))++shouldHaveJson :: Request JSON Ready -> Fold Value a -> Expectation+shouldHaveJson = shouldHaveResponse++shouldHaveXml :: Request XML Ready -> Fold Document a -> Expectation+shouldHaveXml = shouldHaveResponse++-- | Check success stuff for POST requests+shouldHaveJson_ :: Request JSON Ready -> Expectation+shouldHaveJson_ l = shouldHaveResponse l (key "status".only "ok")++shouldHaveXml_ :: Request XML Ready -> Expectation+shouldHaveXml_ l = shouldHaveResponse l (root.attributed (ix "status".only "ok"))++-- | Make a request using public API key+publicly :: Request f (APIKey -> Ready) -> Request f Ready+publicly r = r <*> publicKey++-- | API Key used for requests that do not require authentification+publicKey :: Request f APIKey+publicKey = apiKey "234fc6e0f41f6ef99b7bd62ebaf8d318"++-- | Environment is missing a variable+data EnvironmentMissing = EnvironmentMissing String deriving (Show, Eq, Typeable)++instance Exception EnvironmentMissing++-- $awful++-- | Make a request signed by a secret using private API and session keys+privately :: Request f (APIKey -> SessionKey -> Sign) -> Request f Ready+privately r = sign privateSecret $ r <*> privateAPIKey <*> privateSessionKey++privateAPIKey :: Request f APIKey+privateAPIKey = apiKey (liblastfmEnv "HASKELL_LIBLASTFM_APIKEY")++privateSessionKey :: Request f SessionKey+privateSessionKey = sessionKey (liblastfmEnv "HASKELL_LIBLASTFM_SESSIONKEY")++privateSecret :: Secret+privateSecret = Secret (liblastfmEnv "HASKELL_LIBLASTFM_SECRET")++-- | Get an environment variable or throw a 'EnvironmentMissing' exception+liblastfmEnv :: String -> Text+liblastfmEnv var = unsafePerformIO $ do+ mv <- lookupEnv var+ case mv of+ Just v -> return (pack v)+ Nothing -> throwIO (EnvironmentMissing var)
+ test/api/Xml/AlbumSpec.hs view
@@ -0,0 +1,106 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+module Xml.AlbumSpec (spec) where++import Data.Text (Text)+import Network.Lastfm+import Network.Lastfm.Album+import Test.Hspec+import Text.Xml.Lens++import SpecHelper+++spec :: Spec+spec = do+ it "addTags" $+ shouldHaveXml_ . privately $+ addTags <*> artist "Pink Floyd" <*> album "The Wall" <*> tags ["70s", "awesome", "classic"]++ it "getTags-authenticated" $+ privately (getTags <*> artist "Pink Floyd" <*> album "The Wall")+ `shouldHaveXml`+ root.node "tags".node "tag".node "name".text++ it "removeTag" $+ shouldHaveXml_ . privately $+ removeTag <*> artist "Pink Floyd" <*> album "The Wall" <*> tag "awesome"++ it "share" $+ shouldHaveXml_ . privately $+ share <*> album "Jerusalem" <*> artist "Sleep" <*> recipient "liblastfm" <* message "Just listen!"++ describe "getBuyLinks*" $ do+ let xmlQuery :: Fold Document Text+ xmlQuery = root.node "affiliations".node "physicals".node "affiliation".node "supplierName".text++ it "getBuyLinks" $+ publicly (getBuyLinks <*> country "United Kingdom" <*> artist "Pink Floyd" <*> album "The Wall")+ `shouldHaveXml`+ xmlQuery++ it "getBuyLinks_mbid" $+ publicly (getBuyLinks <*> country "United Kingdom" <*> mbid "816abcd3-924a-3565-92b9-7ab750688f34")+ `shouldHaveXml`+ xmlQuery++ describe "getInfo*" $ do+ let jsonQuery :: Fold Document Text+ jsonQuery = root.node "album".node "toptags".node "tag".node "name".text++ it "getInfo" $+ publicly (getInfo <*> artist "Pink Floyd" <*> album "The Wall")+ `shouldHaveXml`+ jsonQuery++ it "getInfo_mbid" $+ publicly (getInfo <*> mbid "816abcd3-924a-3565-92b9-7ab750688f34")+ `shouldHaveXml`+ jsonQuery++ describe "getShouts*" $ do+ let xmlQuery :: Fold Document Text+ xmlQuery = root.node "shouts".node "shout".node "body".text++ it "getShouts" $+ publicly (getShouts <*> artist "Pink Floyd" <*> album "The Wall" <* limit 7)+ `shouldHaveXml`+ xmlQuery++ it "getShouts_mbid" $+ publicly (getShouts <*> mbid "816abcd3-924a-3565-92b9-7ab750688f34" <* limit 7)+ `shouldHaveXml`+ xmlQuery++ describe "getTags*" $ do+ let xmlQuery :: Fold Document Text+ xmlQuery = root.node "tags".node "tag".node "name".text++ it "getTags" $+ publicly (getTags <*> artist "Pink Floyd" <*> album "The Wall" <* user "liblastfm")+ `shouldHaveXml`+ xmlQuery++ it "getTags_mbid" $+ publicly (getTags <*> mbid "816abcd3-924a-3565-92b9-7ab750688f34" <* user "liblastfm")+ `shouldHaveXml`+ xmlQuery++ describe "getTopTags*" $ do+ let xmlQuery :: Fold Document Text+ xmlQuery = root.node "toptags".node "tag".node "count".text++ it "getTopTags" $+ publicly (getTopTags <*> artist "Pink Floyd" <*> album "The Wall")+ `shouldHaveXml`+ xmlQuery++ it "getTopTags_mbid" $+ publicly (getTopTags <*> mbid "816abcd3-924a-3565-92b9-7ab750688f34")+ `shouldHaveXml`+ xmlQuery++ it "search" $+ publicly (search <*> album "wall" <* limit 5)+ `shouldHaveXml`+ root.node "results".node "albummatches".node "album".node "name".text
+ test/api/Xml/ArtistSpec.hs view
@@ -0,0 +1,196 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+module Xml.ArtistSpec (spec) where++import Data.Text (Text)+import Data.Traversable (traverse)+import Network.Lastfm+import Network.Lastfm.Artist+import Test.Hspec+import Text.Xml.Lens++import SpecHelper+++spec :: Spec+spec = do+ it "addTags" $+ shouldHaveXml_ . privately $+ addTags <*> artist "Егор Летов" <*> tags ["russian", "black metal"]++ it "getTags-authenticated" $+ privately (getTags <*> artist "Егор Летов")+ `shouldHaveXml`+ root.node "tags".node "tag".node "name".text++ it "removeTag" $+ shouldHaveXml_ . privately $+ removeTag <*> artist "Егор Летов" <*> tag "russian"++ it "share" $+ shouldHaveXml_ . privately $+ share <*> artist "Sleep" <*> recipient "liblastfm" <* message "Just listen!"++ it "getCorrection" $+ publicly (getCorrection <*> artist "Meshugah")+ `shouldHaveXml`+ root.node "corrections".node "correction".node "artist".node "name".text++ describe "getEvents*" $ do+ let xmlQuery :: Fold Document Text+ xmlQuery = root.node "events".attr "artist".traverse++ it "getEvents" $+ publicly (getEvents <*> artist "Meshuggah" <* limit 2)+ `shouldHaveXml`+ xmlQuery++ it "getEvents_mbid" $+ publicly (getEvents <*> mbid "cf8b3b8c-118e-4136-8d1d-c37091173413" <* limit 2)+ `shouldHaveXml`+ xmlQuery++ describe "getInfo*" $ do+ let xmlQuery :: Fold Document Text+ xmlQuery = root.node "artist".node "stats".node "listeners".text++ it "getInfo" $+ publicly (getInfo <*> artist "Meshuggah")+ `shouldHaveXml`+ xmlQuery++ it "getInfo_mbid" $+ publicly (getInfo <*> mbid "cf8b3b8c-118e-4136-8d1d-c37091173413")+ `shouldHaveXml`+ xmlQuery++ describe "getPastEvents*" $ do+ let xmlQuery :: Fold Document Text+ xmlQuery = root.node "events".node "event".node "title".text++ it "getPastEvents" $+ publicly (getPastEvents <*> artist "Meshuggah" <* autocorrect True)+ `shouldHaveXml`+ xmlQuery++ it "getPastEvents_mbid" $+ publicly (getPastEvents <*> mbid "cf8b3b8c-118e-4136-8d1d-c37091173413" <* autocorrect True)+ `shouldHaveXml`+ xmlQuery++ describe "getPodcast*" $ do+ let xmlQuery :: Fold Document Text+ xmlQuery = root.node "rss".node "channel".node "description".text++ it "getPodcast" $+ publicly (getPodcast <*> artist "Meshuggah")+ `shouldHaveXml`+ xmlQuery++ it "getPodcast_mbid" $+ publicly (getPodcast <*> mbid "cf8b3b8c-118e-4136-8d1d-c37091173413")+ `shouldHaveXml`+ xmlQuery++ describe "getShouts*" $ do+ let xmlQuery :: Fold Document Text+ xmlQuery = root.node "shouts".node "shout".node "author".text++ it "getShouts" $+ publicly (getShouts <*> artist "Meshuggah" <* limit 5)+ `shouldHaveXml`+ xmlQuery++ it "getShouts_mbid" $+ publicly (getShouts <*> mbid "cf8b3b8c-118e-4136-8d1d-c37091173413" <* limit 5)+ `shouldHaveXml`+ xmlQuery++ describe "getSimilar*" $ do+ let xmlQuery :: Fold Document Text+ xmlQuery = root.node "similarartists".node "artist".node "name".text++ it "getSimilar" $+ publicly (getSimilar <*> artist "Meshuggah" <* limit 3)+ `shouldHaveXml`+ xmlQuery++ it "getSimilar_mbid" $+ publicly (getSimilar <*> mbid "cf8b3b8c-118e-4136-8d1d-c37091173413" <* limit 3)+ `shouldHaveXml`+ xmlQuery++ describe "getTags*" $ do+ let xmlQuery :: Fold Document Text+ xmlQuery = root.node "tags".node "tag".node "name".text++ it "getTags" $+ publicly (getTags <*> artist "Егор Летов" <* user "liblastfm")+ `shouldHaveXml`+ xmlQuery++ it "getTags_mbid" $+ publicly (getTags <*> mbid "cfb3d32e-d095-4d63-946d-9daf06932180" <* user "liblastfm")+ `shouldHaveXml`+ xmlQuery++ describe "getTopAlbums*" $ do+ let xmlQuery :: Fold Document Text+ xmlQuery = root.node "topalbums".node "album".node "name".text++ it "getTopAlbums" $+ publicly (getTopAlbums <*> artist "Meshuggah" <* limit 3)+ `shouldHaveXml`+ xmlQuery++ it "getTopAlbums_mbid" $+ publicly (getTopAlbums <*> mbid "cf8b3b8c-118e-4136-8d1d-c37091173413" <* limit 3)+ `shouldHaveXml`+ xmlQuery++ describe "getTopFans*" $ do+ let xmlQuery :: Fold Document Text+ xmlQuery = root.node "topfans".node "user".node "name".text++ it "getTopFans" $+ publicly (getTopFans <*> artist "Meshuggah")+ `shouldHaveXml`+ xmlQuery++ it "getTopFans_mbid" $+ publicly (getTopFans <*> mbid "cf8b3b8c-118e-4136-8d1d-c37091173413")+ `shouldHaveXml`+ xmlQuery++ describe "getTopTags*" $ do+ let xmlQuery :: Fold Document Text+ xmlQuery = root.node "toptags".node "tag".node "name".text++ it "getTopTags" $+ publicly (getTopTags <*> artist "Meshuggah")+ `shouldHaveXml`+ xmlQuery++ it "getTopTags_mbid" $+ publicly (getTopTags <*> mbid "cf8b3b8c-118e-4136-8d1d-c37091173413")+ `shouldHaveXml`+ xmlQuery++ describe "getTopTracks*" $ do+ let xmlQuery :: Fold Document Text+ xmlQuery = root.node "toptracks".node "track".node "name".text++ it "getTopTracks" $+ publicly (getTopTracks <*> artist "Meshuggah" <* limit 3)+ `shouldHaveXml`+ xmlQuery++ it "getTopTracks_mbid" $+ publicly (getTopTracks <*> mbid "cf8b3b8c-118e-4136-8d1d-c37091173413" <* limit 3)+ `shouldHaveXml`+ xmlQuery++ it "search" $+ publicly (search <*> artist "Mesh" <* limit 3)+ `shouldHaveXml`+ root.node "results".node "artistmatches".node "artist".node "name".text
+ test/api/Xml/ChartSpec.hs view
@@ -0,0 +1,52 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+module Xml.ChartSpec (spec) where++import Data.Text (Text)+import Network.Lastfm+import Network.Lastfm.Chart+import Test.Hspec+import Text.Xml.Lens++import SpecHelper+++spec :: Spec+spec = do+ describe "get*Artists" $ do+ let xmlQuery :: Fold Document Text+ xmlQuery = root.node "artists".node "artist".node "name".text++ it "getHypedArtists" $+ publicly (getHypedArtists <* limit 3)+ `shouldHaveXml`+ xmlQuery++ it "getTopArtists" $+ publicly (getTopArtists <* limit 4)+ `shouldHaveXml`+ xmlQuery++ describe "get*Tracks" $ do+ let xmlQuery :: Fold Document Text+ xmlQuery = root.node "tracks".node "track".node "name".text++ it "getHypedTracks" $+ publicly (getHypedTracks <* limit 2)+ `shouldHaveXml`+ xmlQuery++ it "getLovedTracks" $+ publicly (getLovedTracks <* limit 3)+ `shouldHaveXml`+ xmlQuery++ it "getTopTracks" $+ publicly (getTopTracks <* limit 2)+ `shouldHaveXml`+ xmlQuery++ it "getTopTags" $+ publicly (getTopTags <* limit 5)+ `shouldHaveXml`+ root.node "tags".node "tag".node "name".text
+ test/api/Xml/EventSpec.hs view
@@ -0,0 +1,36 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+module Xml.EventSpec (spec) where++import Network.Lastfm+import Network.Lastfm.Event+import Test.Hspec+import Text.Xml.Lens++import SpecHelper+++spec :: Spec+spec = do+ it "attend" $+ shouldHaveXml_ . privately $+ attend <*> event 3142549 <*> status Attending++ it "share" $+ shouldHaveXml_ . privately $+ share <*> event 3142549 <*> recipient "liblastfm" <* message "Just listen!"++ it "getAttendees" $+ publicly (getAttendees <*> event 3142549 <* limit 2)+ `shouldHaveXml`+ root.node "attendees".node "user".node "name".text++ it "getInfo" $+ publicly (getInfo <*> event 3142549)+ `shouldHaveXml`+ root.node "event".node "venue".node "location".node "country".text++ it "getShouts" $+ publicly (getShouts <*> event 3142549 <* limit 1)+ `shouldHaveXml`+ root.node "shouts".node "shout".node "body".text
+ test/api/Xml/GeoSpec.hs view
@@ -0,0 +1,77 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+module Xml.GeoSpec (spec) where++import Data.Text (Text)+import Network.Lastfm+import Network.Lastfm.Geo+import Test.Hspec+import Text.Xml.Lens++import SpecHelper+++spec :: Spec+spec = do+ it "getEvents" $+ publicly (getEvents <* location "Moscow" <* limit 5)+ `shouldHaveXml`+ root.node "events".node "event".node "id".text++ describe "get*Artist*" $ do+ let xmlQuery :: Fold Document Text+ xmlQuery = root.node "topartists".node "artist".node "name".text++ it "getMetroArtistChart" $+ publicly (getMetroArtistChart <*> metro "Saint Petersburg" <*> country "Russia")+ `shouldHaveXml`+ xmlQuery++ it "getMetroHypeArtistChart" $+ publicly (getMetroHypeArtistChart <*> metro "New York" <*> country "United States")+ `shouldHaveXml`+ xmlQuery++ it "getMetroUniqueArtistChart" $+ publicly (getMetroUniqueArtistChart <*> metro "Minsk" <*> country "Belarus")+ `shouldHaveXml`+ xmlQuery++ it "getTopArtists" $+ publicly (getTopArtists <*> country "Belarus" <* limit 3)+ `shouldHaveXml`+ xmlQuery++ it "getMetroWeeklyChartlist" $+ publicly (getMetroWeeklyChartlist <*> metro "Moscow")+ `shouldHaveXml`+ root.node "weeklychartlist".node "chart".attr "from"++ it "getMetros" $+ publicly (getMetros <* country "Russia")+ `shouldHaveXml`+ root.node "metros".node "metro".node "name".text++ describe "get*Track*" $ do+ let xmlQuery :: Fold Document Text+ xmlQuery = root.node "toptracks".node "track".node "name".text++ it "getTopTracks" $+ publicly (getTopTracks <*> country "Ukraine" <* limit 2)+ `shouldHaveXml`+ xmlQuery++ it "getMetroUniqueTrackChart" $+ publicly (getMetroUniqueTrackChart <*> metro "Moscow" <*> country "Russia")+ `shouldHaveXml`+ xmlQuery++ it "getMetroHypeTrackChart" $+ publicly (getMetroHypeTrackChart <*> metro "Moscow" <*> country "Russia")+ `shouldHaveXml`+ xmlQuery++ it "getMetroTrackChart" $+ publicly (getMetroTrackChart <*> metro "Boston" <*> country "United States")+ `shouldHaveXml`+ xmlQuery
+ test/api/Xml/GroupSpec.hs view
@@ -0,0 +1,47 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+module Xml.GroupSpec (spec) where++import Network.Lastfm+import Network.Lastfm.Group+import Test.Hspec+import Text.Xml.Lens++import SpecHelper+++spec :: Spec+spec = do+ it "getHype" $+ publicly (getHype <*> groupname)+ `shouldHaveXml`+ root.node "weeklyartistchart".node "artist".node "mbid".text++ it "getMembers" $+ publicly (getMembers <*> groupname <* limit 10)+ `shouldHaveXml`+ root.node "members".node "user".node "name".text++ it "getWeeklyAlbumChart" $+ publicly (getWeeklyAlbumChart <*> groupname)+ `shouldHaveXml`+ root.node "weeklyalbumchart".node "album".node "playcount".text++ it "getWeeklyArtistChart" $+ publicly (getWeeklyArtistChart <*> groupname)+ `shouldHaveXml`+ root.node "weeklyartistchart".node "artist".node "name".text++ it "getWeeklyChartList" $+ publicly (getWeeklyChartList <*> groupname)+ `shouldHaveXml`+ root.node "weeklychartlist".node "chart".attr "from"++ it "getWeeklyTrackChart" $+ publicly (getWeeklyTrackChart <*> groupname)+ `shouldHaveXml`+ root.node "weeklytrackchart".node "track".node "url".text++groupname :: Request f Group+groupname = group+ "People with no social lives that listen to more music than is healthy who are slightly scared of spiders and can never seem to find a pen"
+ test/api/Xml/LibrarySpec.hs view
@@ -0,0 +1,56 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+module Xml.LibrarySpec (spec) where++import Network.Lastfm+import Network.Lastfm.Library+import Test.Hspec+import Text.Xml.Lens++import SpecHelper+++spec :: Spec+spec = do+ it "addAlbum" $+ shouldHaveXml_ . privately $+ addAlbum (pure (albumItem <*> artist "Franz Ferdinand" <*> album "Franz Ferdinand"))++ it "addArtist" $+ shouldHaveXml_ . privately $+ addArtist (pure (artistItem <*> artist "Mobthrow"))++ it "addTrack" $+ shouldHaveXml_ . privately $+ addTrack <*> artist "Eminem" <*> track "Kim"++ it "removeAlbum" $+ shouldHaveXml_ . privately $+ removeAlbum <*> artist "Franz Ferdinand" <*> album "Franz Ferdinand"++ it "removeArtist" $+ shouldHaveXml_ . privately $+ removeArtist <*> artist "Burzum"++ it "removeTrack" $+ shouldHaveXml_ . privately $+ removeTrack <*> artist "Eminem" <*> track "Kim"++ it "removeScrobble" $+ shouldHaveXml_ . privately $+ removeScrobble <*> artist "Gojira" <*> track "Ocean" <*> timestamp 1328905590++ it "getAlbums" $+ publicly (getAlbums <*> user "smpcln" <* artist "Burzum" <* limit 5)+ `shouldHaveXml`+ root.node "albums".node "album".node "name".text++ it "getArtists" $+ publicly (getArtists <*> user "smpcln" <* limit 7)+ `shouldHaveXml`+ root.node "artists".node "artist".node "name".text++ it "getTracks" $+ publicly (getTracks <*> user "smpcln" <* artist "Burzum" <* limit 4)+ `shouldHaveXml`+ root.node "tracks".node "track".node "name".text
+ test/api/Xml/PlaylistSpec.hs view
@@ -0,0 +1,42 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+module Xml.PlaylistSpec (spec) where++import Control.Lens+import Data.Int (Int64)+import Data.Text.Lens (unpacked)+import Network.Lastfm hiding (to)+import Network.Lastfm.Playlist+import Network.Lastfm.User+import Test.Hspec+import Test.HUnit (assertFailure)+import Text.Printf+import Text.Read (readMaybe)+import Text.Xml.Lens++import SpecHelper+++spec :: Spec+spec = do+ it "create" $ -- Order matters.+ privately (create <* title "Awesome playlist")+ `shouldHaveXml`+ root.node "playlists".node "playlist".node "title".text++ it "addTrack" $ do+ r <- lastfm $ getPlaylists <*> user "liblastfm" <*> ak' <* Network.Lastfm.xml+ case r of+ Left e ->+ assertFailure (printf "last.fm error: %s" (show e))+ Right val ->+ case preview pl val of+ Just (Just pid) -> shouldHaveXml_ . privately $+ addTrack <*> playlist pid <*> artist "Ruby my dear" <*> track "Chazz"+ _ -> assertFailure (printf "bad XML object: %s" (show val))++ak' :: Request f APIKey+ak' = apiKey "29effec263316a1f8a97f753caaa83e0"++pl :: Fold Document (Maybe Int64)+pl = root.node "playlists".node "playlist".node "id".text.unpacked.to readMaybe
+ test/api/Xml/TagSpec.hs view
@@ -0,0 +1,58 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+module Xml.TagSpec (spec) where++import Network.Lastfm+import Network.Lastfm.Tag+import Test.Hspec+import Text.Xml.Lens++import SpecHelper+++spec :: Spec+spec = do+ it "getInfo" $+ publicly (getInfo <*> tag "depressive")+ `shouldHaveXml`+ root.node "tag".node "taggings".text++ it "getSimilar" $+ publicly (getSimilar <*> tag "depressive")+ `shouldHaveXml`+ root.node "similartags".node "tag".node "name".text++ it "getTopAlbums" $+ publicly (getTopAlbums <*> tag "depressive" <* limit 2)+ `shouldHaveXml`+ root.node "topalbums".node "album".node "url".text++ it "getTopArtists" $+ publicly (getTopArtists <*> tag "depressive" <* limit 3)+ `shouldHaveXml`+ root.node "topartists".node "artist".node "url".text++ it "getTopTags" $+ publicly getTopTags+ `shouldHaveXml`+ root.node "toptags".node "tag".node "name".text++ it "getTopTracks" $+ publicly (getTopTracks <*> tag "depressive" <* limit 2)+ `shouldHaveXml`+ root.node "toptracks".node "track".node "url".text++ it "getWeeklyArtistChart" $+ publicly (getWeeklyArtistChart <*> tag "depressive" <* limit 3)+ `shouldHaveXml`+ root.node "weeklyartistchart".node "artist".node "name".text++ it "getWeeklyChartList" $+ publicly (getWeeklyChartList <*> tag "depressive")+ `shouldHaveXml`+ root.node "weeklychartlist".node "chart".attr "from"++ it "search" $+ publicly (search <*> tag "depressive" <* limit 3)+ `shouldHaveXml`+ root.node "results".node "tagmatches".node "tag".node "name".text
+ test/api/Xml/TasteometerSpec.hs view
@@ -0,0 +1,37 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+module Xml.TasteometerSpec (spec) where++import Data.Text (Text)+import Network.Lastfm+import qualified Network.Lastfm.Tasteometer as Taste+import Test.Hspec+import Text.Xml.Lens++import SpecHelper+++spec :: Spec+spec = do+ it "compare" $+ publicly (Taste.compare (user "smpcln") (user "MCDOOMDESTROYER"))+ `shouldHaveXml`+ xmlQuery++ it "compare" $+ publicly (Taste.compare (user "smpcln") (artists ["enduser", "venetian snares"]))+ `shouldHaveXml`+ xmlQuery++ it "compare" $+ publicly (Taste.compare (artists ["enduser", "venetian snares"]) (user "smpcln"))+ `shouldHaveXml`+ xmlQuery++ it "compare" $+ publicly (Taste.compare (artists ["enduser", "venetian snares"]) (artists ["enduser", "venetian snares"]))+ `shouldHaveXml`+ xmlQuery++xmlQuery :: Fold Document Text+xmlQuery = root.node "comparison".node "result".node "score".text
+ test/api/Xml/TrackSpec.hs view
@@ -0,0 +1,157 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+module Xml.TrackSpec (spec) where++import Data.Text (Text)+import Data.Traversable (traverse)+import Network.Lastfm+import Network.Lastfm.Track+import Test.Hspec+import Text.Xml.Lens++import SpecHelper+++spec :: Spec+spec = do+ it "addTags" $+ shouldHaveXml_ . privately $+ addTags <*> artist "Jefferson Airplane" <*> track "White rabbit" <*> tags ["60s", "awesome"]++ it "ban" $+ shouldHaveXml_ . privately $+ ban <*> artist "Eminem" <*> track "Kim"++ it "love" $+ shouldHaveXml_ . privately $+ love <*> artist "Gojira" <*> track "Ocean"++ it "removeTag" $+ shouldHaveXml_ . privately $+ removeTag <*> artist "Jefferson Airplane" <*> track "White rabbit" <*> tag "awesome"++ it "share" $+ shouldHaveXml_ . privately $+ share <*> artist "Led Zeppelin" <*> track "When the Levee Breaks" <*> recipient "liblastfm" <* message "Just listen!"++ it "unban" $+ shouldHaveXml_ . privately $+ unban <*> artist "Eminem" <*> track "Kim"++ it "unlove" $+ shouldHaveXml_ . privately $+ unlove <*> artist "Gojira" <*> track "Ocean"++ it "scrobble" $+ privately (scrobble (pure (item <*> artist "Gojira" <*> track "Ocean" <*> timestamp 1300000000)))+ `shouldHaveXml`+ root.node "scrobbles".node "scrobble".node "track".text++ it "updateNowPlaying" $+ privately (updateNowPlaying <*> artist "Gojira" <*> track "Ocean")+ `shouldHaveXml`+ root.node "nowplaying".node "track".text++ it "getBuylinks" $+ publicly (getBuyLinks <*> country "United Kingdom" <*> artist "Pink Floyd" <*> track "Brain Damage")+ `shouldHaveXml`+ root.node "affiliations".node "downloads".node "affiliation".node "supplierName".text++ it "getCorrection" $+ publicly (getCorrection <*> artist "Pink Ployd" <*> track "Brain Damage")+ `shouldHaveXml`+ root.node "corrections".node "correction".node "track".node "artist".node "name".text++ it "getFingerprintMetadata" $+ publicly (getFingerprintMetadata <*> fingerprint 1234)+ `shouldHaveXml`+ root.node "tracks".node "track".node "name".text++ describe "getInfo*" $ do+ let xmlQuery :: Fold Document Text+ xmlQuery = root.node "track".node "userplaycount".text++ it "getInfo" $+ publicly (getInfo <*> artist "Pink Floyd" <*> track "Comfortably Numb" <* username "aswalrus")+ `shouldHaveXml`+ xmlQuery++ it "getInfo_mbid" $+ publicly (getInfo <*> mbid "52d7c9ff-6ae4-48a6-acec-4c1a486f8c92" <* username "aswalrus")+ `shouldHaveXml`+ xmlQuery++ describe "getShouts*" $ do+ let xmlQuery :: Fold Document Text+ xmlQuery = root.node "shouts".node "shout".node "author".text++ it "getShouts" $+ publicly (getShouts <*> artist "Pink Floyd" <*> track "Comfortably Numb" <* limit 7)+ `shouldHaveXml`+ xmlQuery++ it "getShouts_mbid" $+ publicly (getShouts <*> mbid "52d7c9ff-6ae4-48a6-acec-4c1a486f8c92" <* limit 7)+ `shouldHaveXml`+ xmlQuery++ describe "getSimilar*" $ do+ let xmlQuery :: Fold Document Text+ xmlQuery = root.node "similartracks".node "track".node "name".text++ it "getSimilar" $+ publicly (getSimilar <*> artist "Pink Floyd" <*> track "Comfortably Numb" <* limit 4)+ `shouldHaveXml`+ xmlQuery++ it "getSimilar_mbid" $+ publicly (getSimilar <*> mbid "52d7c9ff-6ae4-48a6-acec-4c1a486f8c92" <* limit 4)+ `shouldHaveXml`+ xmlQuery++ describe "getTags*" $ do+ let xmlQuery :: Fold Document Text+ xmlQuery = root.node "tags".attr "track".traverse++ it "getTags" $+ publicly (getTags <*> artist "Jefferson Airplane" <*> track "White Rabbit" <* user "liblastfm")+ `shouldHaveXml`+ xmlQuery++ it "getTags_mbid" $+ publicly (getTags <*> mbid "001b3337-faf4-421a-a11f-45e0b60a7703" <* user "liblastfm")+ `shouldHaveXml`+ xmlQuery++ describe "getTopFans*" $ do+ let xmlQuery :: Fold Document Text+ xmlQuery = root.node "topfans".node "user".node "name".text++ it "getTopFans" $+ publicly (getTopFans <*> artist "Pink Floyd" <*> track "Comfortably Numb")+ `shouldHaveXml`+ xmlQuery++ it "getTopFans_mbid" $+ publicly (getTopFans <*> mbid "52d7c9ff-6ae4-48a6-acec-4c1a486f8c92")+ `shouldHaveXml`+ xmlQuery++ describe "getTopTags*" $ do+ let xmlQuery :: Fold Document Text+ xmlQuery = root.node "toptags".node "tag".node "name".text++ it "getTopTags" $+ publicly (getTopTags <*> artist "Pink Floyd" <*> track "Comfortably Numb")+ `shouldHaveXml`+ xmlQuery++ it "getTopTags_mbid" $+ publicly (getTopTags <*> mbid "52d7c9ff-6ae4-48a6-acec-4c1a486f8c92")+ `shouldHaveXml`+ xmlQuery++ it "search" $+ publicly (search <*> track "Believe" <* limit 12)+ `shouldHaveXml`+ root.node "results".node "trackmatches".node "track".node "name".text
+ test/api/Xml/UserSpec.hs view
@@ -0,0 +1,137 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+module Xml.UserSpec (spec) where++import Network.Lastfm+import Network.Lastfm.User+import Test.Hspec+import Text.Xml.Lens++import SpecHelper+++spec :: Spec+spec = do+ it "getRecentStations" $+ privately (getRecentStations <*> user "liblastfm" <* limit 10)+ `shouldHaveXml`+ root.node "recentstations".node "station".node "name".text++ it "getRecommendedArtists" $+ privately (getRecommendedArtists <* limit 10)+ `shouldHaveXml`+ root.node "recommendations".node "artist".node "name".text++ it "getRecommendedEvents" $+ privately (getRecommendedEvents <* limit 10)+ `shouldHaveXml`+ root.node "events".node "event".node "url".text++ it "shout" $+ shouldHaveXml_ . privately $+ shout <*> user "liblastfm" <*> message "test message"++ it "getArtistTracks" $+ publicly (getArtistTracks <*> user "smpcln" <*> artist "Dvar")+ `shouldHaveXml`+ root.node "artisttracks".node "track".node "name".text++ it "getBannedTracks" $+ publicly (getBannedTracks <*> user "smpcln" <* limit 10)+ `shouldHaveXml`+ root.node "bannedtracks".node "track".node "name".text++ it "getEvents" $+ publicly (getEvents <*> user "chansonnier" <* limit 5)+ `shouldHaveXml`+ root.node "events".node "event".node "venue".node "url".text++ it "getFriends" $+ publicly (getFriends <*> user "smpcln" <* limit 10)+ `shouldHaveXml`+ root.node "friends".node "user".node "name".text++ it "getPlayCount" $+ publicly (getInfo <*> user "smpcln")+ `shouldHaveXml`+ root.node "user".node "playcount".text++ it "getGetLovedTracks" $+ publicly (getLovedTracks <*> user "smpcln" <* limit 10)+ `shouldHaveXml`+ root.node "lovedtracks".node "track".node "name".text++ it "getNeighbours" $+ publicly (getNeighbours <*> user "smpcln" <* limit 10)+ `shouldHaveXml`+ root.node "neighbours".node "user".node "name".text++ it "getNewReleases" $+ publicly (getNewReleases <*> user "rj")+ `shouldHaveXml`+ root.node "albums".node "album".node "url".text++ it "getPastEvents" $+ publicly (getPastEvents <*> user "mokele" <* limit 5)+ `shouldHaveXml`+ root.node "events".node "event".node "url".text++ it "getPersonalTags" $+ publicly (getPersonalTags <*> user "crackedcore" <*> tag "rhythmic noise" <*> taggingType "artist" <* limit 10)+ `shouldHaveXml`+ root.node "taggings".node "artists".node "artist".node "name".text++ it "getPlaylists" $+ publicly (getPlaylists <*> user "mokele")+ `shouldHaveXml`+ root.node "playlists".node "playlist".node "title".text++ it "getRecentTracks" $+ publicly (getRecentTracks <*> user "smpcln" <* limit 10)+ `shouldHaveXml`+ root.node "recenttracks".node "track".node "name".text++ it "getShouts" $+ publicly (getShouts <*> user "smpcln" <* limit 2)+ `shouldHaveXml`+ root.node "shouts".node "shout".node "body".text++ it "getTopAlbums" $+ publicly (getTopAlbums <*> user "smpcln" <* limit 5)+ `shouldHaveXml`+ root.node "topalbums".node "album".node "artist".node "name".text++ it "getTopArtists" $+ publicly (getTopArtists <*> user "smpcln" <* limit 5)+ `shouldHaveXml`+ root.node "topartists".node "artist".node "name".text++ it "getTopTags" $+ publicly (getTopTags <*> user "smpcln" <* limit 10)+ `shouldHaveXml`+ root.node "toptags".node "tag".node "name".text++ it "getTopTracks" $+ publicly (getTopTracks <*> user "smpcln" <* limit 10)+ `shouldHaveXml`+ root.node "toptracks".node "track".node "url".text++ it "getWeeklyAlbumChart" $+ publicly (getWeeklyAlbumChart <*> user "smpcln")+ `shouldHaveXml`+ root.node "weeklyalbumchart".node "album".node "url".text++ it "getWeeklyArtistChart" $+ publicly (getWeeklyArtistChart <*> user "smpcln")+ `shouldHaveXml`+ root.node "weeklyartistchart".node "artist".node "url".text++ it "getWeeklyChartList" $+ publicly (getWeeklyChartList <*> user "smpcln")+ `shouldHaveXml`+ root.node "weeklychartlist".node "chart".attr "from"++ it "getWeeklyTrackChart" $+ publicly (getWeeklyTrackChart <*> user "smpcln")+ `shouldHaveXml`+ root.node "weeklytrackchart".node "track".node "url".text
+ test/api/Xml/VenueSpec.hs view
@@ -0,0 +1,28 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+module Xml.VenueSpec (spec) where++import Network.Lastfm+import Network.Lastfm.Venue+import Test.Hspec+import Text.Xml.Lens++import SpecHelper+++spec :: Spec+spec = do+ it "getEvents" $+ publicly (getEvents <*> venue 9163107)+ `shouldHaveXml`+ root.node "events".node "event".node "venue".node "name".text++ it "getPastEvents" $+ publicly (getPastEvents <*> venue 9163107 <* limit 2)+ `shouldHaveXml`+ root.node "events".node "event".node "title".text++ it "search" $+ publicly (search <*> venueName "Arena")+ `shouldHaveXml`+ root.node "results".node "venuematches".node "venue".node "id".text
+ test/spec/Network/Lastfm/ResponseSpec.hs view
@@ -0,0 +1,90 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}+module Network.Lastfm.ResponseSpec (spec) where++import Control.Exception (ArithException(..), throwIO, try)+import Control.Exception.Lens+import Control.Lens+import Data.Aeson (Value)+import Data.ByteString.Lazy (ByteString)+import Data.Aeson.Lens+import Test.Hspec.Lens+import Text.Xml.Lens+import Network.HTTP.Client (HttpException(..))++import Network.Lastfm.Response+++instance Eq HttpException where+ _ == _ = True+++spec :: Spec+spec = do+ describe "md5" $ do+ it "calculates the right hash for an empty string" $+ md5 "" `shouldBe` "d41d8cd98f00b204e9800998ecf8427e"++ it "calculates the right hash for an ascii string" $+ md5 "Enduser" `shouldBe` "28ced1fafec20ae302a04e9f27f4800f"++ it "calculates the right hash for a unicode string" $+ md5 "ДМИТРИЙ МАЛИКОВ" `shouldBe` "e02e5affef1004a02d9762619dc2a585"++ describe "try for LastfmError" $ do+ let tryLastfmError :: IO a -> IO (Either LastfmError a)+ tryLastfmError = try++ it "catches 'HttpException'" $ do+ val <- tryLastfmError (throwIO ResponseTimeout) :: IO (Either LastfmError ())+ val `shouldHave` _Left._LastfmHttpError.only ResponseTimeout++ it "does not catch other exceptions" $+ tryLastfmError (throwIO DivideByZero) `shouldThrow` _DivideByZero++ describe "parse" $ do+ context "JSON" $ do+ let parseJSON :: ByteString -> Either LastfmError Value+ parseJSON = parse++ it "handles good input" $+ let+ good = "{ \"a\": { \"b\": 4 } }"+ in+ parseJSON good `shouldHave` _Right.key "a".key "b"._Integer.only 4++ it "handles malformed input" $+ let+ malformed = "not a json"+ in+ parseJSON malformed `shouldHave` _Left._LastfmBadResponse.only malformed++ it "handles input with encoded errors" $+ let+ encodedError = "{ \"error\": 5, \"message\": \"foo\" }"+ in+ parseJSON encodedError `shouldHave` _Left._LastfmEncodedError.only (5, "foo")++ context "XML" $ do+ let parseXML :: ByteString -> Either LastfmError Document+ parseXML = parse++ it "handles good input" $+ let+ good = "<root><foo><bar>baz</bar></foo></root>"+ in+ parseXML good `shouldHave` _Right.root.node "foo".node "bar".text.only "baz"++ it "handles malformed input" $+ let+ malformed = "not a xml"+ in+ parseXML malformed `shouldHave` _Left._LastfmBadResponse.only malformed++ it "handles input with encoded errors" $+ let+ encodedError = "<lfm><error code=\"5\">foo</error></lfm>"+ in+ parseXML encodedError `shouldHave` _Left._LastfmEncodedError.only (5, "foo")
+ test/spec/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}