The Python Transmission Client module is a Python API for the Transmission Bit TorrentClient.
The 'official' BitTorrent software is in fact already implemented in Python: the Windows, Macintosh Linux GUI clients available at http://www.bittorrent.com/ each contain a standalone python interpreter and lots and lots of python code. Also the command line tools such as btlaunch-many or make-torrent are pure python scripts. So why bother with Python bindings for another implementation of the BitTorrent prototcol? Two reasons: Performance and Simplicity.
Transmission is written in C and much more efficient than the official Python client. Running several large torrents with a total throughput of perhaps 1Mb/s can seriously tax even a modern machine and sends your CPU-usage easily up into the 20 - 40% region and will noticably affect the performance of other processes on that machine. With transmission the difference to normal operation becomes hardly even measurable ;-)
Transmission has recently introduced a RPC architecture in which it launches an independant daemon which listens on a local socket for commands and exposes a rich API for monitoring and controlling transmission. This makes it much easier and 'cleaner' to implement clients in other languages, as one doesn't have to deal with issues such as threading or memory management (possibly cross-platform, too!) but rather just needs to implement a simple RPC API. Which is exactly what this package aims to do.
It does this by providing a wrapper class TransmissionClient, an instance of which represents a locally running transmission-daemon and provides wrapper methods to (most of) the RPC methods listed in the specification.
Simply import the TransmissionClient class from the TransmissionClient module:
>>> from TransmissionClient import TransmissionClient
To create an instance you must supply the constructor with the path to the socket of a locally running transmission-daemon.
>>> daemon = TransmissionClient(SOCKETPATH)
Important note regarding running this test:
If you want to run this doctest (as opposed to just reading it for documentation) you need to first start up an actual instance of transmission-daemon and then execute this test by calling python testTransmissionClient.py <SOCKETPATH> where SOCKETPATH is the path to the socket used by the daemon. On Linux or *BSD this is typically ~/.transmission/daemon/socket, on Mac OSX normally $HOME/Library/Application\ Support/Transmission/daemon/socket, YMMV.
In order for all tests to pass you need to make sure, that the daemon isn't serving any torrents while running the tests. Also, currently you will have to restart the daemon before each testrun -- it will timeout and fail otherwise on subsequent runs because it thinks the torrent added during the second run is a duplicate, eventhough the test cleans up after itself removes it in the final step. This is a known Transmission bug.
To add a torrent, simply provide a path to its location using add_torrent(). The add method returns a numerical id of the newly added torrent, which is used for all further operations on that torrent -- more on that later.
Initially, there aren't any torrents active:
>>> len(daemon.get_status_all()) 0
We add one (since it's the first, its numerical id will be 1.)
>>> daemon.add_torrent("test/data/test_torrent.torrent") 1
Now the number of torrents is 1:
>>> len(daemon.get_status_all()) 1
Trying to add a non-existing torrent file raises a standard OSError:
>>> daemon.add_torrent("/foo/bar.torrent") Traceback (most recent call last): ... OSError: [Errno 2] No such file or directory: '/foo/bar.torrent'
The Transmission API offers status and info data on torrent(s). You can either request it for a particular torrent by providing its numerical id or request a status or info list of all currently known torrents:
>>> info = daemon.get_info(1) >>> status = daemon.get_status(1) >>> info_all = daemon.get_info_all() >>> status_all = daemon.get_status_all()
The actual info or status information is the same, though in each case:
>>> self.assertEqual(info, info_all[0]) >>> self.assertEqual(status, status_all[0])
In each case we receive a dictionary. Here are the keys for status:
>>> status.keys() ['peers-downloading', 'peers-uploading', 'scrape-leechers', 'swarm-speed', 'error-message', 'state', 'download-speed', 'upload-speed', 'completed', 'scrape-seeders', 'peers-total', 'upload-total', 'running', 'scrape-completed', 'peers-from', 'eta', 'tracker', 'error', 'download-total', 'id']
And here for info:
>>> info.keys() ['comment', 'files', 'hash', 'name', 'creator', 'trackers', 'private', 'date', 'path', 'saved', 'id', 'size']
Detailed explanations of the meaning and format of the values returned for the keys mentioned above can be found in the specification and are not within the scope of this documentation. Just mentally substitute all occurrences of ('foo', 'bar') with ['foo', 'bar'] as it uses (Python) tuples to represent lists.
Calling get_info and get_status for non-existing ids raises an exception:
>>> info = daemon.get_info(2) Traceback (most recent call last): ... NoSuchTorrent: No torrent with id `2`>>> status = daemon.get_status(2) Traceback (most recent call last): ... NoSuchTorrent: No torrent with id `2`
Depending on the global setting, the newly added torrent might be running already. Let's make sure and stop it (the method returns True upon success, i.e. the torrent exists and is now stopped):
>>> daemon.stop(1) True
Now we can start it again (the method returns True upon success, i.e. the torrent exists and is now running):
>>> daemon.start(1) True
Being paranoid, we verify this explicitly:
>>> daemon.get_status(1)['running'] 1
The specification allows for operations on an arbitrary number of torrents by supplying a list of ids. For the sake of simplicity the Python wrapper supports only operations on single torrents or on all torrents at once. In order to test for that, let's first turn autostart off and add some more torrents:
>>> daemon.set_autostart(False) False>>> daemon.add_torrent("test/data/foo_torrent.txt.torrent", autostart=False) 2>>> daemon.get_info(2)['name'] 'foo_torrent.txt'
Lo and behold, the new torrent is running:
>>> daemon.get_status(2)['running'] 1
This seems to be a bug in the current implementation of the transmission-daemon. In a nutshell, currently all added torrents seem to be autostarted regardless of the global setting or any explicit flags passed to the add method. C'est la vie...
For the third torrent we override the default autostart behaviour by exlicitely passing autostart=True
>>> daemon.add_torrent("test/data/bar_torrent.txt.torrent", autostart=True) 3>>> daemon.get_info(3)['name'] 'bar_torrent.txt'>>> daemon.get_status(3)['running'] 1
Now we stop all torrents:
>>> daemon.stop_all() >>> daemon.get_status(1)['running'] 0>>> daemon.get_status(2)['running'] 0>>> daemon.get_status(3)['running'] 0
And start them again:
>>> daemon.start_all() >>> daemon.get_status(1)['running'] 1>>> daemon.get_status(2)['running'] 1>>> daemon.get_status(3)['running'] 1
To remove a torrent call remove_torrent with the numerical id of the torrent you want to remove. It will return True if removal succeeded:
>>> daemon.remove_torrent(1) True>>> len(daemon.get_status_all()) 2
More specifically, it will report True if the given torrent doesn't exist anymore after calling it, however calling it with the id of a (no longer) existing id raises the aforementioned NoSuchTorrent exception:
>>> daemon.remove_torrent(1) Traceback (most recent call last): ... NoSuchTorrent: No torrent with id `1`
Finally, we remove all torrents again and leave a clean slate:
>>> daemon.remove_all() True>>> len(daemon.get_status_all()) 0
Calling remove_all even if no torrents are active doesn't raise an exception but instead returns True:
>>> daemon.remove_all() True
Apart from commands dealing with specific torrents, there's a list of basic set- and get methods that all follow the pattern of get_foo() and set_foo(value) and that affect the daemon itself:
Let's look at get_port for example. Since we're running this test against an actual instance of transmission-daemon, we'll save the original port value before changing it:
>>> initial_value = daemon.get_port()
All of the aforementioned set methods provide the new value upon return, so testing the set method implicitely also tests the getter:
>>> daemon.set_port(9091) 9091
For completeness sake, an explicit Test of the get method:
>>> daemon.get_port() 9091
Finally, we clean up after ourselves and reset (and verify) the original value.
>>> self.failUnlessEqual(daemon.set_port(initial_value), initial_value)
The remaining methods are tested in a more compact fashion:
>>> init_downlimit = self.daemon.get_downlimit() >>> self.failUnlessEqual(self.daemon.set_downlimit(200), 200) >>> self.failUnlessEqual(self.daemon.set_downlimit(init_downlimit), init_downlimit)>>> init_uplimit = self.daemon.get_uplimit() >>> self.failUnlessEqual(self.daemon.set_uplimit(200), 200) >>> self.failUnlessEqual(self.daemon.set_uplimit(init_uplimit), init_uplimit)>>> init_autostart = self.daemon.get_autostart() >>> self.failUnlessEqual(self.daemon.set_autostart(True), True) >>> self.failUnlessEqual(self.daemon.set_autostart(False), False) >>> self.failUnlessEqual(self.daemon.set_autostart(init_autostart), init_autostart)>>> init_automap = self.daemon.get_automap() >>> self.failUnlessEqual(self.daemon.set_automap(True), True) >>> self.failUnlessEqual(self.daemon.set_automap(False), False) >>> self.failUnlessEqual(self.daemon.set_automap(init_automap), init_automap)>>> init_directory = self.daemon.get_directory() >>> self.failUnlessEqual(self.daemon.set_directory("/tmp/foo"), "/tmp/foo") >>> self.failUnlessEqual(self.daemon.set_directory(init_directory), init_directory)
For a more detailed explanation refer to the specification.
This packages uses the bencode and bdecode implementation of the official BitTorrent client which have been singled out as a standalone package. If you're using an egg-based distribution of this package you won't need to concern yourself with this dependency, though, as it's handled automatically for you.
The Python Transmission Client package was written by Tom Lazar <tom@tomster.org>, http://tomster.org and is licensed under the MIT licence (the same licence as Transmission).