packages feed

scrobble 0.1.0.1 → 0.2.1.0

raw patch · 5 files changed

+264/−10 lines, 5 filesdep +MissingHdep +containersdep +curlPVP ok

version bump matches the API change (PVP)

Dependencies added: MissingH, containers, curl, pureMD5

API changes (from Hackage documentation)

+ Scrobble.Client: defaultServer :: URI
+ Scrobble.Client: instance Show CurlGrab
+ Scrobble.Client: newClient :: Details -> IO Client
+ Scrobble.Client: nowPlaying :: Client -> NowPlaying -> IO ()
+ Scrobble.Client: submitTracks :: Client -> [Submission] -> IO ()
+ Scrobble.Types: BADAUTH :: Response
+ Scrobble.Types: BADSESSION :: Response
+ Scrobble.Types: BANNED :: Response
+ Scrobble.Types: Ban :: Rating
+ Scrobble.Types: Client :: String -> URI -> URI -> Client
+ Scrobble.Types: Config :: PortNumber -> String -> NominalDiffTime -> Config
+ Scrobble.Types: Details :: String -> String -> String -> String -> URI -> Details
+ Scrobble.Types: FAILED :: String -> Response
+ Scrobble.Types: Handlers :: (Session -> IO ()) -> (Session -> IO ()) -> (Session -> NowPlaying -> IO ()) -> (Session -> [Submission] -> IO Bool) -> Handlers
+ Scrobble.Types: LastFm :: Source
+ Scrobble.Types: Love :: Rating
+ Scrobble.Types: NonPersonlizedBroadcast :: Source
+ Scrobble.Types: NowPlaying :: String -> String -> Maybe String -> Maybe Integer -> Maybe Integer -> Maybe String -> NowPlaying
+ Scrobble.Types: OK :: Response
+ Scrobble.Types: Personalized :: Source
+ Scrobble.Types: ScrobblerBadAuth :: ScrobblerError
+ Scrobble.Types: ScrobblerBadTime :: ScrobblerError
+ Scrobble.Types: ScrobblerBanned :: ScrobblerError
+ Scrobble.Types: ScrobblerFailed :: String -> ScrobblerError
+ Scrobble.Types: ScrobblerHardFail :: ScrobblerError
+ Scrobble.Types: ScrobblerNowPlayingFail :: String -> ScrobblerError
+ Scrobble.Types: ScrobblerSubmitFail :: String -> ScrobblerError
+ Scrobble.Types: Session :: Bool -> String -> String -> String -> String -> UTCTime -> String -> Session
+ Scrobble.Types: Skip :: Rating
+ Scrobble.Types: Submission :: String -> String -> UTCTime -> Source -> Maybe Rating -> Maybe Integer -> Maybe String -> Maybe Integer -> Maybe String -> Submission
+ Scrobble.Types: Unknown :: Source
+ Scrobble.Types: UserChosen :: Source
+ Scrobble.Types: cfgExpire :: Config -> NominalDiffTime
+ Scrobble.Types: cfgHost :: Config -> String
+ Scrobble.Types: cfgPort :: Config -> PortNumber
+ Scrobble.Types: cliNowPlaying :: Client -> URI
+ Scrobble.Types: cliSubmit :: Client -> URI
+ Scrobble.Types: cliToken :: Client -> String
+ Scrobble.Types: data Client
+ Scrobble.Types: data Config
+ Scrobble.Types: data Details
+ Scrobble.Types: data Handlers
+ Scrobble.Types: data NowPlaying
+ Scrobble.Types: data Rating
+ Scrobble.Types: data Response
+ Scrobble.Types: data ScrobblerError
+ Scrobble.Types: data Session
+ Scrobble.Types: data Source
+ Scrobble.Types: data Submission
+ Scrobble.Types: detClient :: Details -> String
+ Scrobble.Types: detPassword :: Details -> String
+ Scrobble.Types: detServer :: Details -> URI
+ Scrobble.Types: detUsername :: Details -> String
+ Scrobble.Types: detVersion :: Details -> String
+ Scrobble.Types: handleExpire :: Handlers -> Session -> IO ()
+ Scrobble.Types: handleHandshake :: Handlers -> Session -> IO ()
+ Scrobble.Types: handleNowPlaying :: Handlers -> Session -> NowPlaying -> IO ()
+ Scrobble.Types: handleSubmissions :: Handlers -> Session -> [Submission] -> IO Bool
+ Scrobble.Types: instance Data ScrobblerError
+ Scrobble.Types: instance Enum Rating
+ Scrobble.Types: instance Enum Source
+ Scrobble.Types: instance Eq Rating
+ Scrobble.Types: instance Eq Source
+ Scrobble.Types: instance Exception ScrobblerError
+ Scrobble.Types: instance Read Rating
+ Scrobble.Types: instance Read Source
+ Scrobble.Types: instance Show Client
+ Scrobble.Types: instance Show Config
+ Scrobble.Types: instance Show Details
+ Scrobble.Types: instance Show NowPlaying
+ Scrobble.Types: instance Show Rating
+ Scrobble.Types: instance Show Response
+ Scrobble.Types: instance Show ScrobblerError
+ Scrobble.Types: instance Show Session
+ Scrobble.Types: instance Show Source
+ Scrobble.Types: instance Show Submission
+ Scrobble.Types: instance Typeable ScrobblerError
+ Scrobble.Types: npAlbum :: NowPlaying -> Maybe String
+ Scrobble.Types: npArtist :: NowPlaying -> String
+ Scrobble.Types: npLength :: NowPlaying -> Maybe Integer
+ Scrobble.Types: npMusicBrainz :: NowPlaying -> Maybe String
+ Scrobble.Types: npPosition :: NowPlaying -> Maybe Integer
+ Scrobble.Types: npTrack :: NowPlaying -> String
+ Scrobble.Types: sesClientId :: Session -> String
+ Scrobble.Types: sesClientVer :: Session -> String
+ Scrobble.Types: sesHandshake :: Session -> Bool
+ Scrobble.Types: sesTimestamp :: Session -> UTCTime
+ Scrobble.Types: sesToken :: Session -> String
+ Scrobble.Types: sesUser :: Session -> String
+ Scrobble.Types: sesVersion :: Session -> String
+ Scrobble.Types: subAlbum :: Submission -> Maybe String
+ Scrobble.Types: subArtist :: Submission -> String
+ Scrobble.Types: subLength :: Submission -> Maybe Integer
+ Scrobble.Types: subMusicBrainz :: Submission -> Maybe String
+ Scrobble.Types: subPosition :: Submission -> Maybe Integer
+ Scrobble.Types: subRating :: Submission -> Maybe Rating
+ Scrobble.Types: subSource :: Submission -> Source
+ Scrobble.Types: subTimestamp :: Submission -> UTCTime
+ Scrobble.Types: subTrack :: Submission -> String

Files

scrobble.cabal view
@@ -1,5 +1,5 @@ name:                scrobble-version:             0.1.0.1+version:             0.2.1.0 synopsis:            Scrobbling server. description:         A library providing server-side support                      for the Audioscrobbler Realtime Submission protocol:@@ -19,13 +19,16 @@  library   hs-source-dirs:    src-  exposed-modules:   Scrobble.Server-  other-modules:     Scrobble.Types+  exposed-modules:   Scrobble.Server, Scrobble.Client, Scrobble.Types   build-depends:     base >4 && <5,                      network,                      url,                      time,-                     old-locale+                     old-locale,+                     pureMD5,+                     MissingH,+                     curl,+                     containers  executable scrobble-server   hs-source-dirs:    src@@ -35,4 +38,7 @@                      network,                      url,                      time,-                     old-locale+                     old-locale,+                     MissingH,+                     curl,+                     containers
src/Scrobble.hs view
@@ -2,8 +2,10 @@  module Scrobble   (module Scrobble.Types-  ,module Scrobble.Server)+  ,module Scrobble.Server+  ,module Scrobble.Client)   where  import           Scrobble.Server+import           Scrobble.Client import           Scrobble.Types
+ src/Scrobble/Client.hs view
@@ -0,0 +1,213 @@+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE ViewPatterns #-}+{-# LANGUAGE RecordWildCards #-}++-- | A client for scrobbling, based upon the Audioscrobbler Realtime+-- Submission protocol v1.2+-- <http://www.audioscrobbler.net/development/protocol/>+--+-- Example:+--+-- @+-- import Scrobble.Client+-- import Data.Time+--+-- example = do+--   client <- newClient Details+--     { detPassword = \"YOURPASS\"+--     , detUsername = \"YOURUSER\"+--     , detClient   = \"qlb\"+--     , detVersion  = \"0.9.2\"+--     , detServer   = defaultServer+--     }+--   nowPlaying client NowPlaying+--     { npArtist      = \"Kasabian\"+--     , npTrack       = \"Ladies and Gentlemen\"+--     , npAlbum       = Just \"West Ryder Pauper Lunatic Asylum\"+--     , npLength      = Just 288+--     , npPosition    = Nothing+--     , npMusicBrainz = Nothing+--     }+--   timestamp <- getCurrentTime+--   submitTracks client+--                [Submission { subArtist      = \"Kasabian\"+--                            , subTrack       = \"Ladies and Gentlemen\"+--                            , subTimestamp   = timestamp+--                            , subSource      = UserChosen+--                            , subRating      = Nothing+--                            , subLength      = Just 288+--                            , subAlbum       = Just \"West Ryder Pauper Lunatic Asylum\"+--                            , subPosition    = Nothing+--                            , subMusicBrainz = Nothing+--                            }]+-- @++module Scrobble.Client+  (newClient+  ,nowPlaying+  ,submitTracks+  ,defaultServer+  ,module Scrobble.Types)+  where++import Scrobble.Types++import Control.Arrow+import Control.Exception+import Control.Monad+import Data.Hash.MD5 (Str(..),md5s)+import Data.List+import Data.Maybe+import Data.Time+import Data.Time.Clock.POSIX+import Network.Curl+import Network.URI+import System.Locale++-- | Get a session token.+--+-- The algorithm for generating this token is as follows:+-- token := md5(md5(password) + timestamp)+--+-- The md5() function takes a string and returns the 32-byte ASCII+-- hexadecimal representation of the MD5 hash, using lower case+-- characters for the hex values.+getToken :: String    -- ^ The password.+         -> IO (String,String) -- ^ A session token and timestamp.+getToken password = do+  timestamp <- getCurrentTime+  let et = epoch timestamp+  return (md5 (md5 password ++ et),et)++    where md5 = md5s . Str++-- | Create a client session. Throws "ScrobblerError".+newClient :: Details -> IO Client+newClient Details{..} = withCurlDo $ do+  (token,timestamp) <- getToken detPassword+  let params = [("hs","true")+               ,("p","1.2")+               ,("c",detClient)+               ,("v",detVersion)+               ,("u",detUsername)+               ,("t",timestamp)+               ,("a",token)]+  response <- curlGrab (setQuery detServer params)+           [CurlHttpHeaders [ "Host: " ++ host+                            | Just host <- [fmap uriRegName (uriAuthority detServer)] ]]+  parseAuth response++-- | Parse the auth response.+parseAuth :: CurlGrab -> IO Client+parseAuth CurlGrab{..} =+  case lines grabBody of+    ["OK",token,parseURI -> Just nowplaying,parseURI -> Just submit] ->+      return (Client token nowplaying submit)+    ["BANNED"] -> throw ScrobblerBanned+    ["BADAUTH"] -> throw ScrobblerBadAuth+    ["BADTIME"] -> throw ScrobblerBadTime+    [other] | isPrefixOf failed other ->+              throw (ScrobblerFailed (drop (length failed) other))+            | otherwise -> throw ScrobblerHardFail+  where failed = "FAILED "++-- | Default Audioscrobbler server: http://post.audioscrobbler.com/+defaultServer :: URI+defaultServer = fromJust (parseURI "http://post.audioscrobbler.com/")++-- | Send a now playing message.  Throws "ScrobblerError".+nowPlaying :: Client -> NowPlaying -> IO ()+nowPlaying client@Client{..} nowplaying = do+  CurlGrab{grabBody} <- curlGrab cliNowPlaying+                                 [CurlPost True+                                 ,CurlPostFields (map keyval (makeNowPlaying client nowplaying))]+  unless (trim grabBody == "OK") $+    throw (ScrobblerNowPlayingFail grabBody)++-- | Make a now playing query.+makeNowPlaying :: Client -> NowPlaying -> [(String,String)]+makeNowPlaying Client{..} NowPlaying{..} =+  [("s",cliToken)+  ,("a",npArtist)+  ,("t",npTrack)+  ,("b",fromMaybe "" npAlbum)+  ,("l",maybe "" show npLength)+  ,("n",maybe "" show npPosition)+  ,("m",fromMaybe "" npMusicBrainz)]++-- | Submit track(s).  Throws "ScrobblerError".+submitTracks :: Client -> [Submission] -> IO ()+submitTracks client@Client{..} submissions = do+  CurlGrab{grabBody} <- curlGrab cliSubmit+                                 [CurlPost True+                                 ,CurlPostFields (map keyval params)]+  unless (trim grabBody == "OK") $+    throw (ScrobblerSubmitFail grabBody)++  where params = [("s",cliToken)] +++                 concat (zipWith (makeSubmission client) [0..] submissions)++-- | Make a now playing query.+makeSubmission :: Client -> Integer -> Submission -> [(String,String)]+makeSubmission Client{..} i Submission{..} = map hookup+  [("a",subArtist)+  ,("t",subTrack)+  ,("i",epoch subTimestamp)+  ,("o",fromMaybe "U" (lookup subSource sources))+  ,("r",fromMaybe "" (subRating >>= \r -> lookup r ratings))+  ,("l",maybe "" show subLength)+  ,("b",fromMaybe "" subAlbum)+  ,("n",maybe "" show subPosition)+  ,("m",fromMaybe "" subMusicBrainz)]++  where sources = [(UserChosen,"P")+                  ,(NonPersonlizedBroadcast,"R")+                  ,(Personalized,"E")+                  ,(LastFm,"L")]+        ratings = [(Love,"L"),(Ban,"B"),(Skip,"S")]+        hookup (k,v) = (k ++ "[" ++ show i ++ "]",v)++--------------------------------------------------------------------------------+-- Utilities++-- | Encode post parameters to a string.+encodePost :: [(String,String)] -> String+encodePost = intercalate "&" . map (keyval . (encode *** encode)) where+  encode = escapeURIString isUnescapedInURI++-- | Make a key=val string.+keyval :: (String,String) -> String+keyval (key,val) = key ++ "=" ++ val++-- | Set a URI's query.+setQuery :: URI -> [(String,String)] -> URI+setQuery uri assoc = uri { uriQuery = "?" ++ encodePost assoc }++-- | Format a time to UNIX number.+epoch :: UTCTime -> String+epoch = formatTime defaultTimeLocale "%s"++-- | Just strip whitespace.+trim :: String -> String+trim = unwords . words++--------------------------------------------------------------------------------+-- Make Curl's API not crappy.++-- | Grab a URL with curl.+curlGrab :: URI -> [CurlOption] -> IO CurlGrab+curlGrab url options = do+  CurlResponse{..} <- curlGetResponse_ (show url) options+  return $ CurlGrab respCurlCode respStatus respStatusLine respHeaders respBody++-- | A sane data type.+data CurlGrab = CurlGrab+  { grabCode       :: CurlCode+  , grabStatus     :: Int+  , grabStatusLine :: String+  , grabHeaders    :: [(String,String)]+  , grabBody       :: String+  } deriving (Show)
src/Scrobble/Server.hs view
@@ -6,7 +6,8 @@ -- <http://www.audioscrobbler.net/development/protocol/>  module Scrobble.Server-  (startScrobbleServer)+  (startScrobbleServer+  ,module Scrobble.Types)   where  import           Scrobble.Types
src/Scrobble/Types.hs view
@@ -1,9 +1,14 @@+{-# LANGUAGE DeriveDataTypeable #-}+ -- | Scrobbling data types.  module Scrobble.Types where -import           Data.Time-import           Network+import Control.Exception+import Data.Data+import Data.Time+import Network+import Network.URI (URI)  -- | Server configuration. data Config = Config@@ -93,7 +98,34 @@    deriving (Show,Enum,Eq,Read) - -- | Server response. data Response = OK | BANNED | BADAUTH | FAILED String | BADSESSION   deriving Show++-- | A scrobbling client.+data Client = Client+  { cliToken      :: String -- ^ Session token.+  , cliNowPlaying :: URI    -- ^ Now playing URL to submit to.+  , cliSubmit     :: URI    -- ^ URL to submit listened tracks to.+  } deriving (Show)++-- | Details for creating a scrobbling client.+data Details = Details+  { detPassword :: String+  , detUsername :: String+  , detClient :: String -- ^ E.g. “qlb”.+  , detVersion :: String -- ^ E.g. “0.9.2”.+  , detServer :: URI -- ^ See defaultServer in "Scrobble.Client".+  } deriving (Show)++-- | Scrobble exception.+data ScrobblerError+  = ScrobblerBanned+  | ScrobblerBadAuth+  | ScrobblerBadTime+  | ScrobblerFailed String+  | ScrobblerHardFail+  | ScrobblerSubmitFail String+  | ScrobblerNowPlayingFail String+  deriving (Show,Typeable,Data)+instance Exception ScrobblerError