rtorrent-rpc (empty) → 0.2.0.0
raw patch · 16 files changed
+1524/−0 lines, 16 filesdep +attoparsecdep +basedep +blaze-buildersetup-changed
Dependencies added: attoparsec, base, blaze-builder, blaze-textual, bytestring, deepseq, haxr, mtl, network, split
Files
- LICENSE +20/−0
- Network/RTorrent.hs +16/−0
- Network/RTorrent/Action.hs +63/−0
- Network/RTorrent/Action/Internals.hs +169/−0
- Network/RTorrent/Command.hs +20/−0
- Network/RTorrent/Command/Internals.hs +161/−0
- Network/RTorrent/CommandList.hs +161/−0
- Network/RTorrent/File.hs +124/−0
- Network/RTorrent/Peer.hs +157/−0
- Network/RTorrent/Priority.hs +72/−0
- Network/RTorrent/RPC.hs +137/−0
- Network/RTorrent/SCGI.hs +74/−0
- Network/RTorrent/Torrent.hs +180/−0
- Network/RTorrent/Tracker.hs +138/−0
- Setup.hs +2/−0
- rtorrent-rpc.cabal +30/−0
+ LICENSE view
@@ -0,0 +1,20 @@+Copyright (c) 2014 Kai Lindholm++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 the Software without restriction, including+without limitation the rights to use, copy, modify, merge, publish,+distribute, sublicense, and/or sell copies of the Software, and to+permit persons to whom the Software is furnished to do so, subject to+the following conditions:++The above copyright notice and this permission notice shall be included+in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ Network/RTorrent.hs view
@@ -0,0 +1,16 @@+{-|+Module : RTorrent+Copyright : (c) Kai Lindholm, 2014+License : MIT+Maintainer : megantti@gmail.com+Stability : experimental++A module that re-exports "Network.RTorrent.RPC" for convenience.++-}++module Network.RTorrent (+ module Network.RTorrent.RPC+) where++import Network.RTorrent.RPC
+ Network/RTorrent/Action.hs view
@@ -0,0 +1,63 @@+{-# LANGUAGE TypeOperators, TypeFamilies, RankNTypes #-}++{-|+Module : Action+Copyright : (c) Kai Lindholm, 2014+License : MIT+Maintainer : megantti@gmail.com+Stability : experimental++@Action@ is a command acting on various kinds of objects:++* torrents ('Network.RTorrent.Torrent.TorrentAction')+* files ('Network.RTorrent.File.FileAction')+* peers ('Network.RTorrent.Peer.PeerAction')+* trackers ('Network.RTorrent.Tracker.TrackerAction').++They all have the property that they can be executed on a single object or+on a group of objects.++For example, ++@+callRTorrent "localhost" 5000 $ some_action (some_id :: SomeId)+@+is a valid thing to write when @some@ is one of the previous objects.++To call an action on all torrents, you can use 'Network.RTorrent.Torrent.allTorrents', so that++@+callRTorrent "localhost" 5000 $ allTorrents getTorrentId+@+will return a list of torrent ids.++To call a action on other types of objects, you can use 'Network.RTorrent.Peer.allPeers',+'Network.RTorrent.File.allFiles', or 'Network.RTorrent.Tracker.allTrackers',+which will act on all peers, files, or trackers that are associated to a torrent.+They will also return ids for each object.+Then for example ++> allFiles getFileSizeBytes :: TorrentId -> TorrentAction [FileId :*: Int]+is an action that will return a list of ids and file sizes when run on a torrent.+These can further be used with 'Network.RTorrent.Torrent.allTorrents'.++To combine actions, you can use '<+>' and 'sequenceActions'+which correspond to ':*:' and @[]@ for commands.++In order to write new actions, 'simpleAction' can be used.++-}++module Network.RTorrent.Action (+ Action + , simpleAction++ , sequenceActions+ , (<+>)++ , Param (..)+ , ActionB (..)++) where++import Network.RTorrent.Action.Internals
+ Network/RTorrent/Action/Internals.hs view
@@ -0,0 +1,169 @@+{-# LANGUAGE TypeOperators, TypeFamilies, RankNTypes #-}++{-|+Module : Action.Internals+Copyright : (c) Kai Lindholm, 2014+License : MIT+Maintainer : megantti@gmail.com+Stability : experimental++-}++module Network.RTorrent.Action.Internals (+ Action (..)+ , simpleAction++ , sequenceActions+ , (<+>)++ , Param (..)+ , ActionB (..)++ , AllAction (..)+ , allToMulti +) where++import Control.Applicative+import Control.Monad++import Data.Monoid+import Data.Traversable hiding (mapM)++import Network.XmlRpc.Internals+import Network.RTorrent.Command.Internals+import Network.RTorrent.Priority++-- | A type for actions that can act on different things like torrents and files.+--+-- @a@ is the return type.+data Action i a + = Action [(String, [Param])] (forall m. (Monad m, Applicative m) => Value -> m a) i++-- | Wrapper to get monoid and applicative instances.+newtype ActionB i a = ActionB { runActionB :: i -> Action i a} ++-- | A simple action that can be used when constructing new ones.+-- +-- Watch out for using @Bool@ as @a@ since using it with this function will probably result in an error,+-- since RTorrent actually returns 0 or 1 instead of a bool.+-- One workaround is to get an @Int@ and use @Bool@'s @Enum@ instance.+simpleAction :: XmlRpcType a => + String+ -> [Param] + -> i+ -> Action i a+simpleAction cmd params = Action [(cmd, params)] parseSingle++instance Functor (Action i) where+ fmap f (Action cmds p fid) = Action cmds (fmap f . p) fid++instance Functor (ActionB i) where+ fmap f = ActionB . (fmap f .) . runActionB++instance Applicative (ActionB i) where+ pure a = ActionB $ Action [] (const (pure a)) ++ (ActionB a) <*> (ActionB b) = ActionB $ \tid -> let + parse :: (Monad m, Applicative m) => (Value -> m (a -> b)) -> (Value -> m a) -> Value -> m b+ parse parseA parseB arr = do+ (valsA, valsB) <- splitAt len <$> getArray arr+ parseA (ValueArray valsA) + <*> parseB (ValueArray valsB) + len = length cmdsA+ Action cmdsA pA _ = a tid+ Action cmdsB pB _ = b tid+ in Action (cmdsA ++ cmdsB) (parse pA pB) tid++instance Monoid a => Monoid (ActionB i a) where+ mempty = pure mempty+ mappend = liftA2 mappend ++instance XmlRpcType i => Command (Action i a) where+ type Ret (Action i a) = a++ commandCall (Action cmds _ tid) = + RTMethodCall + . ValueArray+ . concatMap (\(cmd, params) -> + getArray'+ . runRTMethodCall $ mkRTMethodCall cmd + (toValue tid : map toValue params))+ $ cmds++ commandValue (Action _ parse _) = parse+ levels (Action cmds _ _) = length cmds++-- | Parameters for actions.+data Param = + PString String+ | PInt Int+ | PTorrentPriority TorrentPriority+ | PFilePriority FilePriority++instance Show Param where+ show (PString str) = show str+ show (PInt i) = show i+ show (PTorrentPriority p) = show (fromEnum p)+ show (PFilePriority p) = show (fromEnum p)++instance XmlRpcType Param where+ toValue (PString str) = toValue str+ toValue (PInt i) = toValue i+ toValue (PTorrentPriority p) = toValue p+ toValue (PFilePriority p) = toValue p++ fromValue = fail "No fromValue for Params"+ getType _ = TUnknown++-- | Sequence multiple actions, for example with @f = []@.+sequenceActions :: Traversable f => f (i -> Action i a) -> i -> Action i (f a)+sequenceActions = runActionB . traverse ActionB++infixr 6 <+>+-- | Combine two actions to get a new one.+(<+>) :: (i -> Action i a) -> (i -> Action i b) -> i -> Action i (a :*: b)+a <+> b = runActionB $ (:*:) <$> ActionB a <*> ActionB b++data AllAction i a = AllAction i String (i -> Action i a)++makeMultiCall :: [(String, [Param])] -> [String]+makeMultiCall = ("" :) + . map (\(cmd, params) -> cmd ++ "=" ++ makeList params)+ where+ makeList :: Show a => [a] -> String+ makeList params = ('{' :) . go params $ "}"+ where+ go :: Show a => [a] -> ShowS+ go [x] = shows x + go (x:xs) = shows x . (',' :) . go xs+ go [] = id++wrapForParse :: Monad m => Value -> m [Value]+wrapForParse = mapM ( + return . ValueArray + . map (ValueArray . (:[])) + <=< getArray) + <=< getArray <=< single <=< single++allToMulti :: AllAction i a -> j -> Action j [a]+allToMulti (AllAction emptyId multicall action) = + Action [(multicall, map PString $ makeMultiCall cmds)] + (mapM parse <=< wrapForParse)+ where + Action cmds parse _ = action emptyId+ +instance Command (AllAction i a) where+ type Ret (AllAction i a) = [a]+ commandCall (AllAction emptyId multicall action) = + mkRTMethodCall multicall+ . map ValueString+ . makeMultiCall + $ cmds+ where+ Action cmds _ _ = action emptyId++ commandValue (AllAction emptyId _ action) = + mapM parse <=< wrapForParse+ where+ Action _ parse _ = action emptyId+
+ Network/RTorrent/Command.hs view
@@ -0,0 +1,20 @@+{-# LANGUAGE TypeOperators #-}++{-|+Module : Command+Copyright : (c) Kai Lindholm, 2014+License : MIT+Maintainer : megantti@gmail.com+Stability : experimental+-}++module Network.RTorrent.Command (+ (:*:)(..)+ , Command (Ret) ++ -- * AnyCommand++ , AnyCommand (..)+) where++import Network.RTorrent.Command.Internals
+ Network/RTorrent/Command/Internals.hs view
@@ -0,0 +1,161 @@+{-# LANGUAGE TypeOperators, TypeFamilies #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE GADTs #-}++{-|+Module : Command.Internals+Copyright : (c) Kai Lindholm, 2014+License : MIT+Maintainer : megantti@gmail.com+Stability : experimental+-}++module Network.RTorrent.Command.Internals (+ (:*:)(..)+ , Command (Ret, commandCall, commandValue, levels) ++ , AnyCommand (..)+ + , RTMethodCall (..)+ , runRTMethodCall+ , mkRTMethodCall+ , parseSingle+ , getArray+ , getArray'+ , single+) where++import Control.Applicative+import Control.DeepSeq+import Control.Monad.Error+import Control.Monad.Identity++import Data.List.Split (splitPlaces)++import Network.XmlRpc.Internals++-- | A strict 2-tuple for easy combining of commands.+data (:*:) a b = (:*:) !a !b+infixr 6 :*:++instance (NFData a, NFData b) => NFData (a :*: b) where+ rnf (a :*: b) = rnf a `seq` rnf b++instance (Show a, Show b) => Show (a :*: b) where+ show (a :*: b) = show a ++ " :*: " ++ show b++instance (Command a, Command b) => Command (a :*: b) where+ type Ret (a :*: b) = Ret a :*: Ret b++ commandCall (a :*: b) = RTMethodCall $ ValueArray (val a ++ val b)+ where+ val :: Command c => c -> [Value]+ val = getArray' . runRTMethodCall . commandCall++ commandValue (a :*: b) (ValueArray xs) = + (:*:) <$> (commandValue a . ValueArray $ as)+ <*> (commandValue b . ValueArray $ bs)+ where+ (as, bs) = splitAt (levels a) xs+ commandValue _ _ = fail "commandValue in Command (a :*: b) instance failed"+ + levels (a :*: b) = levels a + levels b ++-- Helpers for values+getArray :: Monad m => Value -> m [Value]+getArray (ValueArray ar) = return ar+getArray _ = fail "getArray in Network.RTorrent.Commands failed"++getArray' :: Value -> [Value]+getArray' (ValueArray ar) = ar+getArray' _ = error "getArray' in Network.RTorrent.Commands failed"++-- | Extract a value from a singleton array.+single :: Monad m => Value -> m Value+single (ValueArray [ar]) = return ar+single v@(ValueStruct vars) = maybe + err+ (\(c, s) -> do+ i <- int c+ s' <- str s+ fail $ "Server returned error " ++ show i ++ + ": " ++ s')+ (liftA2 (,) (lookup "faultCode" vars)+ (lookup "faultString" vars))+ where+ int (ValueInt i) = return i+ int _ = err+ str (ValueString s) = return s+ str _ = err+ err :: Monad m => m a+ err = fail $ "Failed to match a singleton array, got: " ++ show v+single v = fail $ "Failed to match a singleton array, got: " ++ show v++parseValue :: (Monad m, XmlRpcType a) => Value -> m a+parseValue = fromRight . runIdentity . runErrorT . fromValue + where+ fromRight (Right r) = return r+ fromRight (Left e) = fail $ "parseValue failed: " ++ e++-- | Parse a value wrapped in two singleton arrays.+parseSingle :: (Monad m, XmlRpcType a) => Value -> m a+parseSingle = parseValue <=< single <=< single++-- | A newtype wrapper for method calls. +-- +-- You shouldn't directly use the constructor +-- if you don't know what you are doing.+newtype RTMethodCall = RTMethodCall Value+ deriving Show++runRTMethodCall :: RTMethodCall -> Value+runRTMethodCall (RTMethodCall v) = v++-- | Make a command that should be used when defining 'commandCall'.+mkRTMethodCall :: String -- ^ The name of the method (i.e. get_up_rate)+ -> [Value] -- ^ List of parameters+ -> RTMethodCall+mkRTMethodCall name params = RTMethodCall $ ValueArray [ValueStruct + [ ("methodName", ValueString name)+ , ("params", ValueArray params)]]++-- | A typeclass for commands that can be send to RTorrent.+class Command a where+ -- | Return type of the command.+ type Ret a ++ -- | Construct a request.+ commandCall :: a -> RTMethodCall + -- | Parse the resulting value.+ commandValue :: (Applicative m, Monad m) => + a -> Value -> m (Ret a)++ levels :: a -> Int+ levels _ = 1++-- | Existential wrapper for any command.+-- +-- @Command@s wrapped in @AnyCommand@ won't parse their results.+--+-- @AnyCommand@ can be used when you want to call multiple commands+-- but don't care about their return values.+data AnyCommand where+ AnyCommand :: Command a => a -> AnyCommand++instance Command AnyCommand where+ type Ret AnyCommand = Value+ commandCall (AnyCommand cmd) = commandCall cmd+ commandValue _ = single <=< single+ levels (AnyCommand cmd) = levels cmd++instance Command a => Command [a] where+ type Ret [a] = [Ret a]+ commandCall = RTMethodCall . ValueArray + . concatMap ( getArray'+ . runRTMethodCall . commandCall)+ commandValue cmds = + zipWithM (\cmd -> commandValue cmd . ValueArray) cmds+ . splitPlaces (map levels cmds) + <=< getArray + levels = sum . map levels +
+ Network/RTorrent/CommandList.hs view
@@ -0,0 +1,161 @@+{-# LANGUAGE TypeFamilies, RankNTypes #-}++{-|+Module : Commands+Copyright : (c) Kai Lindholm, 2014+License : MIT+Maintainer : megantti@gmail.com+Stability : experimental++A module for defined commands.++To combine multiple commands, use ':*:',+or store them in a list, +as both of these types have 'Command' instances.++-}++module Network.RTorrent.CommandList + ( module Network.RTorrent.File+ , module Network.RTorrent.Peer+ , module Network.RTorrent.Priority+ , module Network.RTorrent.Torrent+ , module Network.RTorrent.Tracker++ -- * Functions for global variables+ , Global + , getUpRate + , getDownRate+ , getDirectory+ , getPid++ , getUploadRate+ , getDownloadRate+ , setUploadRate+ , setDownloadRate+ + -- * Loading new torrents++ , loadTorrent+ , loadTorrentRaw+ , loadStartTorrent+ , loadStartTorrentRaw++ -- * Constructing new commands++ , commandSimple+ , commandArgs+ , commandInt+ , commandString++ -- * Re-exported from "Network.RTorrent.Action"+ , (<+>)+ , sequenceActions++ -- * Re-exported from "Network.RTorrent.Command"+ , (:*:)(..)+ , AnyCommand (..)+ , Command (Ret)+ )+ where++import Network.XmlRpc.Internals++import Control.Applicative++import Data.ByteString (ByteString)++import Network.RTorrent.Action+import Network.RTorrent.Command.Internals+import Network.RTorrent.File+import Network.RTorrent.Peer+import Network.RTorrent.Priority+import Network.RTorrent.Torrent+import Network.RTorrent.Tracker++-- | Run a command with no arguments.+commandSimple :: XmlRpcType a => String -> Global a+commandSimple cmd = commandArgs cmd []++-- | Run a command with the given arguments.+commandArgs :: XmlRpcType a => String -> [Value] -> Global a+commandArgs = flip $ Global parseSingle++-- | Run a command with the @Int@ given as an argument.+commandInt :: XmlRpcType a => String -> Int -> Global a+commandInt cmd i = commandArgs cmd [ValueInt i]++-- | Run a command with the @String@ given as an argument.+commandString :: XmlRpcType a => + String -- ^ Command+ -> String -- ^ Argument+ -> Global a+commandString cmd s = commandArgs cmd [ValueString s]++-- | Get the current up rate, in bytes per second.+getUpRate :: Global Int+getUpRate = commandSimple "get_up_rate"++-- | Get the current down rate, in bytes per second.+getDownRate :: Global Int+getDownRate = commandSimple "get_down_rate"++-- | Get the default download directory.+getDirectory :: Global String+getDirectory = commandSimple "get_directory"++-- | Get the maximum upload rate, in bytes per second.+--+-- @0@ means no limit.+getUploadRate :: Global Int+getUploadRate = commandSimple "get_upload_rate"++-- | Get the maximum download rate, in bytes per second.+--+-- @0@ means no limit.+getDownloadRate :: Global Int+getDownloadRate = commandSimple "get_download_rate"++-- | Set the maximum upload rate, in bytes per second.+setUploadRate :: Int -> Global Int+setUploadRate = commandInt "set_upload_rate"++-- | Set the maximum download rate, in bytes per second.+setDownloadRate :: Int -> Global Int+setDownloadRate = commandInt "set_download_rate"++-- | Get the process id.+getPid :: Global Int+getPid = commandSimple "system.pid"++-- | Load a torrent file.+loadTorrent :: String -- ^ A path / URL+ -> Global Int+loadTorrent = commandString "load"++-- | Load a torrent file.+loadTorrentRaw :: ByteString -- ^ A torrent file as data+ -> Global Int+loadTorrentRaw torrentData = commandArgs "load_raw" [ValueBase64 torrentData] ++-- | Load a torrent file and start downloading it.+loadStartTorrent :: String -- ^ A path / URL+ -> Global Int+loadStartTorrent = commandString "load_start"++-- | Load a torrent file and start downloading it.+loadStartTorrentRaw :: ByteString -- ^ A torrent file as data+ -> Global Int+loadStartTorrentRaw torrentData = commandArgs "load_raw_start" [ValueBase64 torrentData] + ++-- | Execute a command with a result type @t@.+data Global t = Global (forall m. (Monad m, Applicative m) => Value -> m t) [Value] String+instance Command (Global a) where+ type Ret (Global a) = a+ commandCall (Global _ args cmd) = mkRTMethodCall cmd args+ commandValue (Global parse _ _) = parse++instance Functor Global where+ fmap f (Global g args s) = Global (fmap f . g) args s+
+ Network/RTorrent/File.hs view
@@ -0,0 +1,124 @@+{-# LANGUAGE TypeOperators, TypeFamilies #-}++{-|+Module : File+Copyright : (c) Kai Lindholm, 2014+License : MIT+Maintainer : megantti@gmail.com+Stability : experimental++For more info on actions, see "Network.RTorrent.Action".+-}++module Network.RTorrent.File (+ FileId (..)+ , FileInfo (..)+ , FileAction + , getFilePartial+ , getTorrentFiles+ , allFiles++ -- * Functions dealing with a single variable+ , getFilePath+ , getFileAbsolutePath+ , getFileSizeBytes+ , getFileSizeChunks+ , getFileCompletedChunks+ , getFilePriority+ , setFilePriority+) where++import Control.Applicative+import Control.DeepSeq++import Network.RTorrent.Action.Internals+import Network.RTorrent.Torrent+import Network.RTorrent.Command+import Network.RTorrent.Priority+import Network.XmlRpc.Internals++import Data.List.Split (splitOn)++data FileId = FileId !TorrentId !Int deriving Show++instance XmlRpcType FileId where+ toValue (FileId (TorrentId tid) i) = ValueString $ tid ++ ":f" ++ show i+ fromValue v = return . uncurry FileId =<< parse =<< fromValue v+ where+ parse :: Monad m => String -> m (TorrentId, Int)+ parse str = do+ [hash, i] <- return $ splitOn ":f" str+ return (TorrentId hash, read i)+ getType _ = TString++data FileInfo = FileInfo {+ filePath :: String+ , fileSizeBytes :: !Int+ , fileSizeChunks :: !Int+ , fileCompletedChunks :: !Int+ , filePriority :: !FilePriority+ , fileId :: FileId+} deriving Show++instance NFData FileId where+ rnf (FileId tid i) = rnf tid `seq` rnf i++instance NFData FileInfo where+ rnf (FileInfo fid fp fsb fsc fcc fpt) = + rnf fp + `seq` rnf fsb + `seq` rnf fsc+ `seq` rnf fcc+ `seq` rnf fpt+ `seq` rnf fid++type FileAction = Action FileId++-- | Run the file action on all files that a torrent has.+allFiles :: (FileId -> FileAction a) -> TorrentId -> TorrentAction [FileId :*: a]+allFiles f = fmap addId . (getTorrentId <+> allToMulti (allF f))+ where+ addId (hash :*: files) = + zipWith (\index -> (:*:) (FileId hash index)) [0..] files + allF :: (FileId -> FileAction a) -> AllAction FileId a+ allF = AllAction (FileId (TorrentId "") 0) "f.multicall"++-- | Get the file name relative to the torrent base directory.+getFilePath :: FileId -> FileAction String+getFilePath = simpleAction "f.path" []++-- | Get the absolute path.+getFileAbsolutePath :: FileId -> FileAction String+getFileAbsolutePath = simpleAction "f.frozen_path" []++getFileSizeBytes :: FileId -> FileAction Int+getFileSizeBytes = simpleAction "f.size_bytes" []++getFileSizeChunks :: FileId -> FileAction Int+getFileSizeChunks = simpleAction "f.size_chunks" []++getFileCompletedChunks :: FileId -> FileAction Int+getFileCompletedChunks = simpleAction "f.completed_chunks" []++getFilePriority :: FileId -> FileAction FilePriority+getFilePriority = simpleAction "f.priority" []++setFilePriority :: FilePriority -> FileId -> FileAction Int+setFilePriority pr = simpleAction "f.priority.set" [PFilePriority pr]++-- | Get a file except for @FileId@. The @FileId@ can be got by running @allFiles@.+getFilePartial :: FileId -> FileAction (FileId -> FileInfo)+getFilePartial = runActionB $ FileInfo+ <$> b getFilePath+ <*> b getFileSizeBytes+ <*> b getFileSizeChunks+ <*> b getFileCompletedChunks+ <*> b getFilePriority+ where+ b = ActionB++getTorrentFiles :: TorrentId -> TorrentAction [FileInfo]+getTorrentFiles = fmap (map contract) . allFiles getFilePartial+ where+ contract (x :*: f) = f x+
+ Network/RTorrent/Peer.hs view
@@ -0,0 +1,157 @@+{-# LANGUAGE TypeOperators, TypeFamilies #-}++{-|+Module : Peer+Copyright : (c) Kai Lindholm, 2014+License : MIT+Maintainer : megantti@gmail.com+Stability : experimental++For more info on actions, see "Network.RTorrent.Action".+-}++module Network.RTorrent.Peer (+ PeerId (..)+ , PeerInfo (..)+ , PeerAction ++ , getPeerPartial+ , allPeers+ , getTorrentPeers++ -- * Control peers+ , banPeer+ , disconnectPeer++ -- * Functions for single variables+ , getPeerHash+ , getPeerIp++ , getPeerClientVersion+ , getPeerUpRate+ , getPeerDownRate+ , getPeerUpTotal+ , getPeerDownTotal+ , getPeerEncrypted+ , getPeerCompletedPercent+ , getPeerPort+) where++import Control.Applicative+import Control.DeepSeq++import Network.RTorrent.Action.Internals+import Network.RTorrent.Torrent+import Network.RTorrent.Command+import Network.XmlRpc.Internals++import Data.List.Split (splitOn)++data PeerId = PeerId !TorrentId !String + deriving Show++instance XmlRpcType PeerId where+ toValue (PeerId (TorrentId tid) i) = ValueString $ tid ++ ":p" ++ i+ fromValue v = return . uncurry PeerId =<< parse =<< fromValue v+ where+ parse :: Monad m => String -> m (TorrentId, String)+ parse str = do+ [hash, s] <- return $ splitOn ":p" str+ return (TorrentId hash, s)+ getType _ = TString++instance NFData PeerId where+ rnf (PeerId tid i) = rnf tid `seq` rnf i++data PeerInfo = PeerInfo {+ peerClientVersion :: String+ , peerIp :: String+ , peerUpRate :: !Int+ , peerDownRate :: !Int+ , peerUpTotal :: !Int+ , peerDownTotal :: !Int+ , peerEncrypted :: !Bool+ , peerCompletedPercent :: !Int+ , peerPort :: !Int+ , peerId :: PeerId+} deriving Show++instance NFData PeerInfo where+ rnf (PeerInfo a0 a1 a2 a3 a4 a5 a6 a7 a8 a9) =+ rnf a0+ `seq` rnf a1 + `seq` rnf a2+ `seq` rnf a3+ `seq` rnf a4+ `seq` rnf a5+ `seq` rnf a6+ `seq` rnf a7+ `seq` rnf a8+ `seq` rnf a9++getPeerHash :: PeerId -> PeerAction String+getPeerHash = simpleAction "p.get_id" []++getPeerIp :: PeerId -> PeerAction String+getPeerIp = simpleAction "p.get_address" []++getPeerClientVersion :: PeerId -> PeerAction String+getPeerClientVersion = simpleAction "p.get_client_version" []++getPeerUpRate :: PeerId -> PeerAction Int+getPeerUpRate = simpleAction "p.get_up_rate" []++getPeerDownRate :: PeerId -> PeerAction Int+getPeerDownRate = simpleAction "p.get_down_rate" []++getPeerUpTotal :: PeerId -> PeerAction Int+getPeerUpTotal = simpleAction "p.get_up_total" []++getPeerDownTotal :: PeerId -> PeerAction Int+getPeerDownTotal = simpleAction "p.get_down_total" []++getPeerEncrypted :: PeerId -> PeerAction Bool+getPeerEncrypted = fmap toEnum . simpleAction "p.is_encrypted" []++getPeerCompletedPercent :: PeerId -> PeerAction Int+getPeerCompletedPercent = simpleAction "p.get_completed_percent" []++getPeerPort :: PeerId -> PeerAction Int+getPeerPort = simpleAction "p.get_port" []++-- | Get a partial peer. @PeerId@ can be gotten by running @allPeers@.+getPeerPartial :: PeerId -> PeerAction (PeerId -> PeerInfo)+getPeerPartial = runActionB $ PeerInfo+ <$> b getPeerClientVersion+ <*> b getPeerIp+ <*> b getPeerUpRate+ <*> b getPeerDownRate+ <*> b getPeerUpTotal+ <*> b getPeerDownTotal+ <*> b getPeerEncrypted+ <*> b getPeerCompletedPercent+ <*> b getPeerPort+ where+ b = ActionB++disconnectPeer :: PeerId -> PeerAction Int+disconnectPeer = simpleAction "p.disconnect" []++banPeer :: PeerId -> PeerAction Int+banPeer = simpleAction "p.banned.set" [PInt 1]++type PeerAction = Action PeerId++getTorrentPeers :: TorrentId -> TorrentAction [PeerInfo]+getTorrentPeers = fmap (map contract) . allPeers getPeerPartial+ where+ contract (x :*: f) = f x++-- | Run the peer action on all peers that a torrent has.+allPeers :: (PeerId -> PeerAction a) -> TorrentId -> TorrentAction [PeerId :*: a]+allPeers p = fmap addId . (getTorrentId <+> allToMulti (allP (getPeerHash <+> p)))+ where+ addId (hash :*: peers) = + map (\(phash :*: f) -> PeerId hash phash :*: f) peers+ allP :: (PeerId -> PeerAction a) -> AllAction PeerId a+ allP = AllAction (PeerId (TorrentId "") "") "p.multicall"
+ Network/RTorrent/Priority.hs view
@@ -0,0 +1,72 @@+{-|+Module : Priority+Copyright : (c) Kai Lindholm, 2014+License : MIT+Maintainer : megantti@gmail.com+Stability : experimental+-}++module Network.RTorrent.Priority (+ TorrentPriority (..)+ , FilePriority (..)+) where++import Control.DeepSeq+import Network.XmlRpc.Internals++data TorrentPriority = + TorrentPriorityOff+ | TorrentPriorityLow+ | TorrentPriorityNormal+ | TorrentPriorityHigh+ deriving (Show, Eq, Ord)++instance NFData TorrentPriority++instance Enum TorrentPriority where+ toEnum 0 = TorrentPriorityOff+ toEnum 1 = TorrentPriorityLow+ toEnum 2 = TorrentPriorityNormal+ toEnum 3 = TorrentPriorityHigh+ toEnum i = error $ "toEnum :: Int -> TorrentPriority failed, got : " ++ show i++ fromEnum TorrentPriorityOff = 0+ fromEnum TorrentPriorityLow = 1+ fromEnum TorrentPriorityNormal = 2+ fromEnum TorrentPriorityHigh = 3++instance XmlRpcType TorrentPriority where+ toValue = toValue . fromEnum+ fromValue v = return . toEnum =<< check =<< fromValue v+ where+ check i + | 0 <= i && i <= 3 = return i+ | otherwise = fail $ "Invalid TorrentPriority, got : " ++ show i+ getType _ = TInt++data FilePriority = + FilePriorityOff+ | FilePriorityNormal+ | FilePriorityHigh+ deriving (Show, Eq, Ord)++instance NFData FilePriority++instance Enum FilePriority where+ toEnum 0 = FilePriorityOff+ toEnum 1 = FilePriorityNormal+ toEnum 2 = FilePriorityHigh+ toEnum i = error $ "toEnum :: Int -> FilePriority failed, got : " ++ show i++ fromEnum FilePriorityOff = 0+ fromEnum FilePriorityNormal = 1+ fromEnum FilePriorityHigh = 2++instance XmlRpcType FilePriority where+ toValue = toValue . fromEnum+ fromValue v = return . toEnum =<< check =<< fromValue v+ where+ check i + | 0 <= i && i <= 2 = return i+ | otherwise = fail $ "Invalid FilePriority, got : " ++ show i+ getType _ = TInt
+ Network/RTorrent/RPC.hs view
@@ -0,0 +1,137 @@+{-# LANGUAGE FlexibleContexts #-}++{-|+Module : RPC+Copyright : (c) Kai Lindholm, 2014+License : MIT+Maintainer : megantti@gmail.com+Stability : experimental++This package can be used for communicating with RTorrent over its XML-RPC interface.++For example, you can request torrent info and bandwidth usage:++@+result <- callRTorrent "localhost" 5000 $ + 'getTorrents' 'Network.RTorrent.Command.:*:' 'getUpRate' 'Network.RTorrent.Command.:*:' 'getDownRate'+case result of + Right (torrentInfo :*: uploadRate :*: downloadRate) -> ...+@+where++>>> :t torrentInfo+[TorrentInfo]+>>> :t uploadRate+Int++This requires you to have set @scgi_port = localhost:5000@ in your @.rtorrent.rc@.++Note that 'Network.RTorrent.Command.:*:' is both a data constructor and a type constructor,+and therefore:++>>> :t True :*: False+Bool :*: Bool++However, using @:*:@ in types needs the @TypeOperators@ extension to work.+++As a more complete example, the following code finds all files that are over+100 megabytes, prints them along with the torrent they belong to and +sets their priorities to high.++@+{-\# LANGUAGE TypeOperators \#-}++import Control.Monad+import Network.RTorrent++-- This is an action, and they can be combined with ('<+>').+torrentInfo :: 'TorrentId'+ -> 'TorrentAction' (String :*: [FileId :*: String :*: Int])+torrentInfo = 'getTorrentName' + '<+>' 'allFiles' ('getFilePath' '<+>' 'getFileSizeBytes')++ -- 'allFiles' takes a file action ('FileId' -> 'FileAction' a)+ -- and returns a torrent action: TorrentId -> 'TorrentAction' [FileId :*: a].+ -- Note that it automatically adds 'FileId's to all elements.++main :: IO ()+main = do+ Right torrents <- callRTorrent "localhost" 5000 $+ 'allTorrents' torrentInfo+ let largeFiles = + filter (\\(_ :*: _ :*: _ :*: size) -> size > 10^8)+ . concatMap (\\(tName :*: fileList) -> + map ((:*:) tName) fileList) + -- Move the torrent name into the list+ $ torrents++ putStrLn "Large files:"+ forM_ largeFiles $ \\(torrent :*: _ :*: fPath :*: _) ->+ putStrLn $ "\\t" ++ torrent ++ ": " ++ fPath++ -- There is instance ('Network.RTorrent.Command.Command' cmdA, 'Network.RTorrent.Command.Command' cmdB) => 'Network.RTorrent.Command.Command' (cmdA :*: cmdB)+ -- The return value for the command cmdA is 'Network.RTorrent.Command.Ret' cmdA, which is an associated type+ -- in the Command type class.+ -- The return value for the command cmdA :*: cmdB is Ret cmdA :*: Ret cmdB.+ + let cmd :: String :*: FileId :*: String :*: Int + -> FileAction FilePriority :*: FileAction Int+ cmd (_ :*: fid :*: _ :*: _) = + 'getFilePriority' fid :*: 'setFilePriority' 'FilePriorityHigh' fid++ -- Get the old priority and set the new one to high.+ -- setFilePriority returns a not-so-useful Int.++ -- There is also an instance Command a => Command [a],+ -- and the return value for [a] is [Ret a].+ + Right ret <- callRTorrent "localhost" 5000 $ map cmd largeFiles++ putStrLn "Old priorities:"+ forM_ ret $ \\(oldPriority :*: _) -> do+ putStrLn $ "\\t" ++ show oldPriority+@+-}++module Network.RTorrent.RPC (+ module Network.RTorrent.CommandList+ , callRTorrent + ) where++import Control.Monad.Error (ErrorT(..), throwError, strMsg)+import Data.Monoid+import qualified Data.ByteString as BS+import qualified Data.ByteString.Lazy as LB+import Network+import Network.XmlRpc.Internals++import qualified Network.RTorrent.Command.Internals as C+import Network.RTorrent.CommandList+import Network.RTorrent.SCGI++callRTorrentRaw :: HostName -> Int -> C.RTMethodCall -> ErrorT String IO Value+callRTorrentRaw host port calls = do+ let request = Body [] . mconcat . LB.toChunks $ renderCall call + Body _ content <- ErrorT $ query host port request+ let cs = map (toEnum . fromEnum) $ BS.unpack content+ response <- parseResponse cs+ case response of+ Return ret -> case ret of+ ValueArray arr -> return $ ValueArray arr+ val -> throwError . strMsg $ "Got value of type " ++ show (getType val)+ Fault code err -> throwError . strMsg $ show code ++ ": " ++ err+ where+ call = MethodCall "system.multicall" [C.runRTMethodCall calls]++-- | Call RTorrent with a command.+-- Only one connection is opened even when combining commands+-- for example by using ':*:' or lists.+callRTorrent :: Command a =>+ HostName+ -> Int+ -> a + -> IO (Either String (Ret a))+callRTorrent host port command = + runErrorT $ + C.commandValue command =<< callRTorrentRaw host port (C.commandCall command)
+ Network/RTorrent/SCGI.hs view
@@ -0,0 +1,74 @@+{-# LANGUAGE OverloadedStrings #-}++{-# OPTIONS_HADDOCK hide #-}++{-|+Module : SCGI+Copyright : (c) Kai Lindholm, 2014+License : MIT+Maintainer : megantti@gmail.com+Stability : experimental++An internal module for establishing a connection with RTorrent.+-}++module Network.RTorrent.SCGI (Headers, Body(..), query) where++import Control.Applicative+import Data.Either (partitionEithers)+import Data.Monoid++import Blaze.ByteString.Builder+import Blaze.ByteString.Builder.Char8+import Blaze.Text+import Data.Attoparsec.ByteString.Char8 (Parser)+import qualified Data.Attoparsec.ByteString.Char8 as A+import Data.ByteString (ByteString)+import qualified Data.ByteString as BS++import Network++type Headers = [(ByteString, ByteString)]++data Body = Body {+ headers :: Headers+ , body :: ByteString+} deriving Show++makeRequest :: Body -> ByteString+makeRequest (Body hd bd) = res + where+ res = toByteString . mconcat $ [ + integral len+ , fromChar ':'+ , fromByteString hdbs+ , fromChar ','+ , fromByteString bd+ ]+ fromBS0 bs = fromWrite $ writeByteString bs <> writeWord8 0+ hdbs = toByteString . mconcat $ + (fromBS0 "CONTENT_LENGTH" <> integral (BS.length bd) <> fromWord8 0): + map (\(a, b) -> fromBS0 a <> fromBS0 b) hd+ len = BS.length hdbs++parseResponse :: Parser Body+parseResponse = parseBody+ where+ lineParser :: Parser ByteString+ lineParser = + mappend <$> A.takeTill (== '\r') <*> (("\r\n" *> pure "") <|> lineParser)+ headerParser = (,) <$> A.takeTill (== ':') <*> (": " *> lineParser) ++ contentParser = "\r\n" *> A.takeByteString+ parseBody = do+ header <- headerParser+ (moreHeaders, [content]) <- + partitionEithers <$> A.many' (A.eitherP headerParser contentParser)+ return $ Body (header : moreHeaders) content++query :: HostName -> Int -> Body -> IO (Either String Body)+query host port queryBody = do+ h <- connectTo host (PortNumber (toEnum port))+ BS.hPut h (makeRequest queryBody) + A.parseOnly parseResponse <$> BS.hGetContents h+
+ Network/RTorrent/Torrent.hs view
@@ -0,0 +1,180 @@+{-|+Module : Torrent+Copyright : (c) Kai Lindholm, 2014+License : MIT+Maintainer : megantti@gmail.com+Stability : experimental++For more info on actions, see "Network.RTorrent.Action".+-}++module Network.RTorrent.Torrent+ ( TorrentInfo (..)+ , TorrentId (..)+ , TorrentAction++ -- * Functions for handling torrents+ , start+ , close+ , erase+ , checkHash+ , getTorrent+ , getTorrents++ , allTorrents ++ -- * Functions for single variables+ , setTorrentPriority+ , getTorrentPriority+ , getTorrentId + , getTorrentOpen+ , getTorrentUpRate+ , getTorrentDownRate+ , getTorrentSizeBytes+ , getTorrentLeftBytes+ , getTorrentName+ , getTorrentPath+ , getTorrentDir+ , setTorrentDir+ , getTorrentRatio+ , getTorrentFileCount+ )+ where++import Control.Applicative+import Control.DeepSeq+import Network.XmlRpc.Internals++import Network.RTorrent.Action.Internals+import Network.RTorrent.Priority++-- | A newtype wrapper for torrent identifiers.+newtype TorrentId = TorrentId String + deriving Show++type TorrentAction = Action TorrentId++instance NFData TorrentId where+ rnf (TorrentId str) = rnf str++instance XmlRpcType TorrentId where+ toValue (TorrentId s) = ValueString s+ fromValue v = return . TorrentId =<< fromValue v+ getType _ = TString++data TorrentInfo = TorrentInfo {+ torrentId :: TorrentId+ , torrentName :: String+ , torrentOpen :: !Bool+ , torrentDownRate :: !Int+ , torrentUpRate :: !Int+ , torrentSize :: !Int+ , torrentBytesLeft :: !Int+ , torrentPath :: String+ , torrentDir :: String+ , torrentTorrentPriority :: !TorrentPriority+ } deriving Show++instance NFData TorrentInfo where+ rnf (TorrentInfo i a0 a1 a2 a3 a4 a5 a6 a7 a8) = + rnf i `seq`+ rnf a0 `seq`+ rnf a1 `seq`+ rnf a2 `seq`+ rnf a3 `seq`+ rnf a4 `seq`+ rnf a5 `seq`+ rnf a6 `seq`+ rnf a7 `seq`+ rnf a8++-- | Get a TorrentInfo for a torrent.+getTorrent :: TorrentId -> TorrentAction TorrentInfo+getTorrent = runActionB $ TorrentInfo+ <$> b getTorrentId+ <*> b getTorrentName+ <*> b getTorrentOpen+ <*> b getTorrentDownRate+ <*> b getTorrentUpRate+ <*> b getTorrentSizeBytes+ <*> b getTorrentLeftBytes+ <*> b getTorrentPath+ <*> b getTorrentDir+ <*> b getTorrentPriority+ where+ b = ActionB+ +-- | Start downloading a torrent.+start :: TorrentId -> TorrentAction Int+start = simpleAction "d.start" []++-- | Close a torrent. +close :: TorrentId -> TorrentAction Int+close = simpleAction "d.close" []++-- | Erase a torrent. +erase :: TorrentId -> TorrentAction Int+erase = simpleAction "d.erase" []++-- | Initiate a hash check for a torrent.+checkHash :: TorrentId -> TorrentAction Int+checkHash = simpleAction "d.check_hash" []++-- | Set the download priority of a torrent.+setTorrentPriority :: TorrentPriority -> TorrentId -> TorrentAction Int+setTorrentPriority pr = simpleAction "d.priority.set" [PTorrentPriority pr]++getTorrentId :: TorrentId -> TorrentAction TorrentId+getTorrentId = simpleAction "d.hash" []++getTorrentName :: TorrentId -> TorrentAction String+getTorrentName = simpleAction "d.get_name" []++-- | Get the absolute path to the torrent's directory or file.+getTorrentPath :: TorrentId -> TorrentAction String+getTorrentPath = simpleAction "d.get_base_path" []++-- | Get the absolute path to the directory in which the torrent's directory or+-- file resides.+getTorrentDir :: TorrentId -> TorrentAction String+getTorrentDir = simpleAction "d.get_directory" []++setTorrentDir :: String -> TorrentId -> TorrentAction Int+setTorrentDir dir = simpleAction "d.set_directory" [PString dir]++getTorrentOpen :: TorrentId -> TorrentAction Bool+getTorrentOpen = fmap toEnum . simpleAction "d.is_open" []++getTorrentUpRate :: TorrentId -> TorrentAction Int+getTorrentUpRate = simpleAction "d.get_up_rate" []++getTorrentDownRate :: TorrentId -> TorrentAction Int+getTorrentDownRate = simpleAction "d.get_down_rate" []++getTorrentSizeBytes :: TorrentId -> TorrentAction Int+getTorrentSizeBytes = simpleAction "d.get_size_bytes" []++getTorrentLeftBytes :: TorrentId -> TorrentAction Int+getTorrentLeftBytes = simpleAction "d.get_left_bytes" []++getTorrentPriority :: TorrentId -> TorrentAction TorrentPriority+getTorrentPriority = simpleAction "d.priority" []++-- | Get the ratio (which is multiplied by a thousand)+getTorrentRatio :: TorrentId -> TorrentAction Int+getTorrentRatio = simpleAction "d.get_ratio" []++getTorrentFileCount :: TorrentId -> TorrentAction Int+getTorrentFileCount = simpleAction "d.get_size_files" []++-- | Execute a command on all torrents.+-- For example the command+--+-- > allTorrents (setTorrentPriority TorrentPriorityNormal)+-- will set the priority of all torrents to normal.+allTorrents :: (TorrentId -> TorrentAction a) -> AllAction TorrentId a+allTorrents = AllAction (TorrentId "") "d.multicall"++-- | A command for getting torrent info for all torrents.+getTorrents :: AllAction TorrentId TorrentInfo+getTorrents = allTorrents getTorrent
+ Network/RTorrent/Tracker.hs view
@@ -0,0 +1,138 @@+{-# LANGUAGE TypeOperators, TypeFamilies #-}++{-|+Module : Tracker+Copyright : (c) Kai Lindholm, 2014+License : MIT+Maintainer : megantti@gmail.com+Stability : experimental++For more info on actions, see "Network.RTorrent.Action".+-}++module Network.RTorrent.Tracker (+ TrackerId (..)+ , TrackerType (..)+ , TrackerInfo (..)+ , TrackerAction + , getTrackerPartial+ , getTorrentTrackers+ , allTrackers++ -- * Functions dealing with a single variable+ , getTrackerUrl+ , getTrackerEnabled+ , setTrackerEnabled+ , getTrackerType+ , getTrackerOpen+) where++import Control.Applicative+import Control.DeepSeq++import Network.RTorrent.Action.Internals+import Network.RTorrent.Torrent+import Network.RTorrent.Command+import Network.XmlRpc.Internals++import Data.List.Split (splitOn)++data TrackerId = TrackerId !TorrentId !Int deriving Show++instance XmlRpcType TrackerId where+ toValue (TrackerId (TorrentId tid) i) = ValueString $ tid ++ ":t" ++ show i+ fromValue v = return . uncurry TrackerId =<< parse =<< fromValue v+ where+ parse :: Monad m => String -> m (TorrentId, Int)+ parse str = do+ [hash, i] <- return $ splitOn ":t" str+ return (TorrentId hash, read i)+ getType _ = TString++data TrackerType = + TrackerHTTP+ | TrackerUDP+ | TrackerDHT+ deriving (Show, Eq)++instance Enum TrackerType where+ toEnum 1 = TrackerHTTP+ toEnum 2 = TrackerUDP+ toEnum 3 = TrackerDHT+ toEnum i = error $ "toEnum :: Int -> TrackerType failed, got : " ++ show i++ fromEnum TrackerHTTP = 1+ fromEnum TrackerUDP = 2+ fromEnum TrackerDHT = 3++instance XmlRpcType TrackerType where+ toValue = toValue . fromEnum+ fromValue v = return . toEnum =<< check =<< fromValue v+ where+ check i + | 1 <= i && i <= 3 = return i+ | otherwise = fail $ "Invalid TrackerType, got : " ++ show i+ getType _ = TInt++instance NFData TrackerType++data TrackerInfo = TrackerInfo {+ trackerUrl :: String+ , trackerType :: !TrackerType+ , trackerEnabled :: !Bool+ , trackerOpen :: !Bool+ , trackerId :: TrackerId+} deriving Show++instance NFData TrackerId where+ rnf (TrackerId tid i) = rnf tid `seq` rnf i++instance NFData TrackerInfo where+ rnf (TrackerInfo a0 a1 a2 a3 a4) = + rnf a0 + `seq` rnf a1+ `seq` rnf a2+ `seq` rnf a3+ `seq` rnf a4++type TrackerAction = Action TrackerId++-- | Run the tracker action on all trackers that a torrent has.+allTrackers :: (TrackerId -> TrackerAction a) -> TorrentId -> TorrentAction [TrackerId :*: a]+allTrackers t = fmap addId . (getTorrentId <+> allToMulti (allT t))+ where+ addId (hash :*: trackers) = + zipWith (\index -> (:*:) (TrackerId hash index)) [0..] trackers + allT :: (TrackerId -> TrackerAction a) -> AllAction TrackerId a+ allT = AllAction (TrackerId (TorrentId "") 0) "t.multicall"++getTrackerUrl :: TrackerId -> TrackerAction String+getTrackerUrl = simpleAction "t.get_url" []++getTrackerEnabled :: TrackerId -> TrackerAction Bool+getTrackerEnabled = fmap toEnum . simpleAction "t.is_enabled" []++setTrackerEnabled :: Bool -> TrackerId -> TrackerAction Int+setTrackerEnabled i = simpleAction "t.is_enabled" [PInt (fromEnum i)]++getTrackerType :: TrackerId -> TrackerAction TrackerType+getTrackerType = simpleAction "t.get_type" []++getTrackerOpen :: TrackerId -> TrackerAction Bool+getTrackerOpen = fmap toEnum . simpleAction "t.is_open" []++-- | Get a tracker except for @TrackerId@. The @TrackerId@ can be got by running @allTrackers@.+getTrackerPartial :: TrackerId -> TrackerAction (TrackerId -> TrackerInfo)+getTrackerPartial = runActionB $ TrackerInfo+ <$> b getTrackerUrl+ <*> b getTrackerType+ <*> b getTrackerEnabled+ <*> b getTrackerOpen+ where+ b = ActionB++getTorrentTrackers :: TorrentId -> TorrentAction [TrackerInfo]+getTorrentTrackers = fmap (map contract) . allTrackers getTrackerPartial+ where+ contract (x :*: f) = f x+
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ rtorrent-rpc.cabal view
@@ -0,0 +1,30 @@+name: rtorrent-rpc+version: 0.2.0.0+synopsis: A library for communicating with RTorrent over its XML-RPC interface.+-- description: +license: MIT+license-file: LICENSE+author: Kai Lindholm+maintainer: megantti@gmail.com+homepage: https://github.com/megantti/rtorrent-rpc+-- copyright: +category: Network+build-type: Simple+-- extra-source-files: +cabal-version: >=1.10++library+ exposed-modules: Network.RTorrent, Network.RTorrent.RPC, Network.RTorrent.CommandList, + Network.RTorrent.Command, Network.RTorrent.Torrent, Network.RTorrent.File,+ Network.RTorrent.Action, Network.RTorrent.Priority, Network.RTorrent.Peer, + Network.RTorrent.Tracker, Network.RTorrent.Command.Internals,+ Network.RTorrent.Action.Internals+ other-modules: Network.RTorrent.SCGI+ other-extensions: TypeFamilies, OverloadedStrings, TypeOperators, ScopedTypeVariables, + GADTs, FlexibleContexts, RankNTypes+ build-depends: base >=4.7 && <4.8, mtl >=2.1 && < 2.3, bytestring >=0.10 && <0.11, + network >=2.6, haxr >=3000.10 && <3000.11, blaze-builder >=0.3 && <0.4, + blaze-textual >=0.2 && <0.3, attoparsec >=0.12 && <0.13, deepseq >= 1.3.0.0, + split >=0.2.0.0+ -- hs-source-dirs: + default-language: Haskell2010