rtorrent-rpc-0.2.2.0: Network/RTorrent/RPC.hs
{-# 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.Except (ExceptT(..), throwError, runExceptT)
import Control.Exception
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 -> ExceptT String IO Value
callRTorrentRaw host port calls = do
let request = Body [] . mconcat . LB.toChunks $ renderCall call
Body _ content <- ExceptT $ 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 $ "Got value of type " ++ show (getType val)
Fault code err -> throwError $ 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 =
(runExceptT $ do
ret <- callRTorrentRaw host port (C.commandCall command)
C.commandValue command ret)
`catches` [ Handler (\e -> return . Left $ show (e :: IOException))
, Handler (\e -> return . Left $ show (e :: PatternMatchFail))]