mirror of
https://github.com/correl/Transmission-XBMC.git
synced 2024-11-21 19:18:41 +00:00
Cleanup
This commit is contained in:
parent
e5337b1983
commit
5eb21488c1
13 changed files with 0 additions and 3054 deletions
|
@ -1,80 +0,0 @@
|
|||
diff -rupN transmissionrpc-0.3/transmission.py transmissionrpc/transmission.py
|
||||
--- transmissionrpc-0.3/transmission.py 2009-12-10 16:39:33.134130829 -0500
|
||||
+++ transmissionrpc/transmission.py 2009-12-10 16:45:37.385258836 -0500
|
||||
@@ -69,7 +69,7 @@ class Torrent(object):
|
||||
wanted = self.fields['wanted']
|
||||
index = 1
|
||||
for item in zip(indicies, files, priorities, wanted):
|
||||
- selected = True if item[3] else False
|
||||
+ selected = bool(item[3])
|
||||
priority = PRIORITY[item[2]]
|
||||
result[item[0]] = {
|
||||
'selected': selected,
|
||||
@@ -252,29 +252,30 @@ class Client(object):
|
||||
while True:
|
||||
error_data = ""
|
||||
try:
|
||||
- self._debug_request(request)
|
||||
- socket.setdefaulttimeout(10)
|
||||
- if (sys.version_info[0] == 2 and sys.version_info[1] > 5) or sys.version_info[0] > 2:
|
||||
- response = urllib2.urlopen(request, timeout=60)
|
||||
- else:
|
||||
- response = urllib2.urlopen(request)
|
||||
- break
|
||||
- except urllib2.HTTPError, error:
|
||||
- error_data = error.read()
|
||||
- if error.code == 409:
|
||||
- logger.info('Server responded with 409, trying to set session-id.')
|
||||
- if request_count > 1:
|
||||
- raise TransmissionError('Session ID negotiation failed.', error)
|
||||
- if 'X-Transmission-Session-Id' in error.headers:
|
||||
- self.sessionid = error.headers['X-Transmission-Session-Id']
|
||||
- request.add_header('X-Transmission-Session-Id', self.sessionid)
|
||||
+ try:
|
||||
+ self._debug_request(request)
|
||||
+ socket.setdefaulttimeout(10)
|
||||
+ if (sys.version_info[0] == 2 and sys.version_info[1] > 5) or sys.version_info[0] > 2:
|
||||
+ response = urllib2.urlopen(request, timeout=60)
|
||||
else:
|
||||
- raise TransmissionError('Unknown conflict.', error)
|
||||
- except urllib2.URLError, error:
|
||||
- raise TransmissionError('Failed to connect to daemon.', error)
|
||||
- except httplib.BadStatusLine, error:
|
||||
- if (request_count > 1):
|
||||
- raise TransmissionError('Failed to request %s "%s".' % (self.url, query), error)
|
||||
+ response = urllib2.urlopen(request)
|
||||
+ break
|
||||
+ except urllib2.HTTPError, error:
|
||||
+ error_data = error.read()
|
||||
+ if error.code == 409:
|
||||
+ logger.info('Server responded with 409, trying to set session-id.')
|
||||
+ if request_count > 1:
|
||||
+ raise TransmissionError('Session ID negotiation failed.', error)
|
||||
+ if 'X-Transmission-Session-Id' in error.headers:
|
||||
+ self.sessionid = error.headers['X-Transmission-Session-Id']
|
||||
+ request.add_header('X-Transmission-Session-Id', self.sessionid)
|
||||
+ else:
|
||||
+ raise TransmissionError('Unknown conflict.', error)
|
||||
+ except urllib2.URLError, error:
|
||||
+ raise TransmissionError('Failed to connect to daemon.', error)
|
||||
+ except httplib.BadStatusLine, error:
|
||||
+ if (request_count > 1):
|
||||
+ raise TransmissionError('Failed to request %s "%s".' % (self.url, query), error)
|
||||
finally:
|
||||
if error_data:
|
||||
self._debug_response(error, error_data)
|
||||
diff -rupN transmissionrpc-0.3/utils.py transmissionrpc/utils.py
|
||||
--- transmissionrpc-0.3/utils.py 2009-12-10 16:39:33.134130829 -0500
|
||||
+++ transmissionrpc/utils.py 2009-12-10 16:45:37.386133947 -0500
|
||||
@@ -64,7 +64,10 @@ def rpc_bool(arg):
|
||||
arg = bool(int(arg))
|
||||
except:
|
||||
arg = arg.lower() in [u'true', u'yes']
|
||||
- return 1 if bool(arg) else 0
|
||||
+ if bool(arg):
|
||||
+ return 1
|
||||
+ else:
|
||||
+ return 0
|
||||
|
||||
TR_TYPE_MAP = {
|
||||
'number' : int,
|
|
@ -1,10 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# 2008-08, Erik Svensson <erik.public@gmail.com>
|
||||
|
||||
from constants import *
|
||||
from transmission import TransmissionError, Torrent, Session, Client
|
||||
|
||||
__author__ = u'Erik Svensson <erik.public@gmail.com>'
|
||||
__version__ = u'0.3'
|
||||
__copyright__ = u'Copyright (c) 2008 Erik Svensson'
|
||||
__license__ = u'MIT'
|
|
@ -1,230 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# 2008-07, Erik Svensson <erik.public@gmail.com>
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger('transmissionrpc')
|
||||
logger.setLevel(logging.ERROR)
|
||||
|
||||
def mirror_dict(d):
|
||||
d.update(dict((v, k) for k, v in d.iteritems()))
|
||||
return d
|
||||
|
||||
DEFAULT_PORT = 9091
|
||||
|
||||
TR_STATUS_CHECK_WAIT = (1<<0)
|
||||
TR_STATUS_CHECK = (1<<1)
|
||||
TR_STATUS_DOWNLOAD = (1<<2)
|
||||
TR_STATUS_SEED = (1<<3)
|
||||
TR_STATUS_STOPPED = (1<<4)
|
||||
|
||||
STATUS = mirror_dict({
|
||||
'check pending' : TR_STATUS_CHECK_WAIT,
|
||||
'checking' : TR_STATUS_CHECK,
|
||||
'downloading' : TR_STATUS_DOWNLOAD,
|
||||
'seeding' : TR_STATUS_SEED,
|
||||
'stopped' : TR_STATUS_STOPPED,
|
||||
})
|
||||
|
||||
TR_PRI_LOW = -1
|
||||
TR_PRI_NORMAL = 0
|
||||
TR_PRI_HIGH = 1
|
||||
|
||||
PRIORITY = mirror_dict({
|
||||
'low' : TR_PRI_LOW,
|
||||
'normal' : TR_PRI_NORMAL,
|
||||
'high' : TR_PRI_HIGH
|
||||
})
|
||||
|
||||
TR_RATIOLIMIT_GLOBAL = 0 # follow the global settings
|
||||
TR_RATIOLIMIT_SINGLE = 1 # override the global settings, seeding until a certain ratio
|
||||
TR_RATIOLIMIT_UNLIMITED = 2 # override the global settings, seeding regardless of ratio
|
||||
|
||||
RATIO_LIMIT = mirror_dict({
|
||||
'global' : TR_RATIOLIMIT_GLOBAL,
|
||||
'single' : TR_RATIOLIMIT_SINGLE,
|
||||
'unlimeted' : TR_RATIOLIMIT_UNLIMITED
|
||||
})
|
||||
|
||||
# A note on argument maps
|
||||
# These maps are used to verify *-set methods. The information is structured in
|
||||
# a tree.
|
||||
# set +- <argument1> - [<type>, <added version>, <removed version>, <previous argument name>, <next argument name>]
|
||||
# | +- <argument2> - [<type>, <added version>, <removed version>, <previous argument name>, <next argument name>]
|
||||
# |
|
||||
# get +- <argument1> - [<type>, <added version>, <removed version>, <previous argument name>, <next argument name>]
|
||||
# +- <argument2> - [<type>, <added version>, <removed version>, <previous argument name>, <next argument name>]
|
||||
|
||||
# Arguments for torrent methods
|
||||
TORRENT_ARGS = {
|
||||
'get' : {
|
||||
'activityDate': ('number', 1, None, None, None),
|
||||
'addedDate': ('number', 1, None, None, None),
|
||||
'announceResponse': ('string', 1, None, None, None),
|
||||
'announceURL': ('string', 1, None, None, None),
|
||||
'bandwidthPriority': ('number', 5, None, None, None),
|
||||
'comment': ('string', 1, None, None, None),
|
||||
'corruptEver': ('number', 1, None, None, None),
|
||||
'creator': ('string', 1, None, None, None),
|
||||
'dateCreated': ('number', 1, None, None, None),
|
||||
'desiredAvailable': ('number', 1, None, None, None),
|
||||
'doneDate': ('number', 1, None, None, None),
|
||||
'downloadDir': ('string', 4, None, None, None),
|
||||
'downloadedEver': ('number', 1, None, None, None),
|
||||
'downloaders': ('number', 4, None, None, None),
|
||||
'downloadLimit': ('number', 1, None, None, None),
|
||||
'downloadLimited': ('boolean', 5, None, None, None),
|
||||
'downloadLimitMode': ('number', 1, 5, None, None),
|
||||
'error': ('number', 1, None, None, None),
|
||||
'errorString': ('number', 1, None, None, None),
|
||||
'eta': ('number', 1, None, None, None),
|
||||
'files': ('array', 1, None, None, None),
|
||||
'fileStats': ('array', 5, None, None, None),
|
||||
'hashString': ('string', 1, None, None, None),
|
||||
'haveUnchecked': ('number', 1, None, None, None),
|
||||
'haveValid': ('number', 1, None, None, None),
|
||||
'honorsSessionLimits': ('boolean', 5, None, None, None),
|
||||
'id': ('number', 1, None, None, None),
|
||||
'isPrivate': ('boolean', 1, None, None, None),
|
||||
'lastAnnounceTime': ('number', 1, None, None, None),
|
||||
'lastScrapeTime': ('number', 1, None, None, None),
|
||||
'leechers': ('number', 1, None, None, None),
|
||||
'leftUntilDone': ('number', 1, None, None, None),
|
||||
'manualAnnounceTime': ('number', 1, None, None, None),
|
||||
'maxConnectedPeers': ('number', 1, None, None, None),
|
||||
'name': ('string', 1, None, None, None),
|
||||
'nextAnnounceTime': ('number', 1, None, None, None),
|
||||
'nextScrapeTime': ('number', 1, None, None, None),
|
||||
'peer-limit': ('number', 5, None, None, None),
|
||||
'peers': ('array', 2, None, None, None),
|
||||
'peersConnected': ('number', 1, None, None, None),
|
||||
'peersFrom': ('object', 1, None, None, None),
|
||||
'peersGettingFromUs': ('number', 1, None, None, None),
|
||||
'peersKnown': ('number', 1, None, None, None),
|
||||
'peersSendingToUs': ('number', 1, None, None, None),
|
||||
'percentDone': ('double', 5, None, None, None),
|
||||
'pieces': ('string', 5, None, None, None),
|
||||
'pieceCount': ('number', 1, None, None, None),
|
||||
'pieceSize': ('number', 1, None, None, None),
|
||||
'priorities': ('array', 1, None, None, None),
|
||||
'rateDownload': ('number', 1, None, None, None),
|
||||
'rateUpload': ('number', 1, None, None, None),
|
||||
'recheckProgress': ('double', 1, None, None, None),
|
||||
'scrapeResponse': ('string', 1, None, None, None),
|
||||
'scrapeURL': ('string', 1, None, None, None),
|
||||
'seeders': ('number', 1, None, None, None),
|
||||
'seedRatioLimit': ('double', 5, None, None, None),
|
||||
'seedRatioMode': ('number', 5, None, None, None),
|
||||
'sizeWhenDone': ('number', 1, None, None, None),
|
||||
'startDate': ('number', 1, None, None, None),
|
||||
'status': ('number', 1, None, None, None),
|
||||
'swarmSpeed': ('number', 1, None, None, None),
|
||||
'timesCompleted': ('number', 1, None, None, None),
|
||||
'trackers': ('array', 1, None, None, None),
|
||||
'totalSize': ('number', 1, None, None, None),
|
||||
'torrentFile': ('string', 5, None, None, None),
|
||||
'uploadedEver': ('number', 1, None, None, None),
|
||||
'uploadLimit': ('number', 1, None, None, None),
|
||||
'uploadLimitMode': ('number', 1, 5, None, None),
|
||||
'uploadLimited': ('boolean', 5, None, None, None),
|
||||
'uploadRatio': ('double', 1, None, None, None),
|
||||
'wanted': ('array', 1, None, None, None),
|
||||
'webseeds': ('array', 1, None, None, None),
|
||||
'webseedsSendingToUs': ('number', 1, None, None, None),
|
||||
},
|
||||
'set': {
|
||||
'bandwidthPriority': ('number', 5, None, None, None),
|
||||
'downloadLimit': ('number', 5, None, 'speed-limit-down', None),
|
||||
'downloadLimited': ('boolean', 5, None, 'speed-limit-down-enabled', None),
|
||||
'files-wanted': ('array', 1, None, None, None),
|
||||
'files-unwanted': ('array', 1, None, None, None),
|
||||
'honorsSessionLimits': ('boolean', 5, None, None, None),
|
||||
'ids': ('array', 1, None, None, None),
|
||||
'peer-limit': ('number', 1, None, None, None),
|
||||
'priority-high': ('array', 1, None, None, None),
|
||||
'priority-low': ('array', 1, None, None, None),
|
||||
'priority-normal': ('array', 1, None, None, None),
|
||||
'seedRatioLimit': ('double', 5, None, None, None),
|
||||
'seedRatioMode': ('number', 5, None, None, None),
|
||||
'speed-limit-down': ('number', 1, 5, None, 'downloadLimit'),
|
||||
'speed-limit-down-enabled': ('boolean', 1, 5, None, 'downloadLimited'),
|
||||
'speed-limit-up': ('number', 1, 5, None, 'uploadLimit'),
|
||||
'speed-limit-up-enabled': ('boolean', 1, 5, None, 'uploadLimited'),
|
||||
'uploadLimit': ('number', 5, None, 'speed-limit-up', None),
|
||||
'uploadLimited': ('boolean', 5, None, 'speed-limit-up-enabled', None),
|
||||
},
|
||||
'add': {
|
||||
'download-dir': ('string', 1, None, None, None),
|
||||
'filename': ('string', 1, None, None, None),
|
||||
'files-wanted': ('array', 1, None, None, None),
|
||||
'files-unwanted': ('array', 1, None, None, None),
|
||||
'metainfo': ('string', 1, None, None, None),
|
||||
'paused': ('boolean', 1, None, None, None),
|
||||
'peer-limit': ('number', 1, None, None, None),
|
||||
'priority-high': ('array', 1, None, None, None),
|
||||
'priority-low': ('array', 1, None, None, None),
|
||||
'priority-normal': ('array', 1, None, None, None),
|
||||
}
|
||||
}
|
||||
|
||||
# Arguments for session methods
|
||||
SESSION_ARGS = {
|
||||
'get': {
|
||||
"alt-speed-down": ('number', 5, None, None, None),
|
||||
"alt-speed-enabled": ('boolean', 5, None, None, None),
|
||||
"alt-speed-time-begin": ('number', 5, None, None, None),
|
||||
"alt-speed-time-enabled": ('boolean', 5, None, None, None),
|
||||
"alt-speed-time-end": ('number', 5, None, None, None),
|
||||
"alt-speed-time-day": ('number', 5, None, None, None),
|
||||
"alt-speed-up": ('number', 5, None, None, None),
|
||||
"blocklist-enabled": ('boolean', 5, None, None, None),
|
||||
"blocklist-size": ('number', 5, None, None, None),
|
||||
"encryption": ('string', 1, None, None, None),
|
||||
"download-dir": ('string', 1, None, None, None),
|
||||
"peer-limit": ('number', 1, 5, None, None),
|
||||
"peer-limit-global": ('number', 5, None, None, None),
|
||||
"peer-limit-per-torrent": ('number', 5, None, None, None),
|
||||
"pex-allowed": ('boolean', 1, 5, None, None),
|
||||
"pex-enabled": ('boolean', 5, None, None, None),
|
||||
"port": ('number', 1, 5, None, None),
|
||||
"peer-port": ('number', 5, None, None, None),
|
||||
"peer-port-random-on-start": ('boolean', 5, None, None, None),
|
||||
"port-forwarding-enabled": ('boolean', 1, None, None, None),
|
||||
"rpc-version": ('number', 4, None, None, None),
|
||||
"rpc-version-minimum": ('number', 4, None, None, None),
|
||||
"seedRatioLimit": ('double', 5, None, None, None),
|
||||
"seedRatioLimited": ('boolean', 5, None, None, None),
|
||||
"speed-limit-down": ('number', 1, None, None, None),
|
||||
"speed-limit-down-enabled": ('boolean', 1, None, None, None),
|
||||
"speed-limit-up": ('number', 1, None, None, None),
|
||||
"speed-limit-up-enabled": ('boolean', 1, None, None, None),
|
||||
"version": ('string', 3, None, None, None),
|
||||
},
|
||||
'set': {
|
||||
"alt-speed-down": ('number', 5, None, None, None),
|
||||
"alt-speed-enabled": ('boolean', 5, None, None, None),
|
||||
"alt-speed-time-begin": ('number', 5, None, None, None),
|
||||
"alt-speed-time-enabled": ('boolean', 5, None, None, None),
|
||||
"alt-speed-time-end": ('number', 5, None, None, None),
|
||||
"alt-speed-time-day": ('number', 5, None, None, None),
|
||||
"alt-speed-up": ('number', 5, None, None, None),
|
||||
"blocklist-enabled": ('boolean', 5, None, None, None),
|
||||
"encryption": ('string', 1, None, None, None),
|
||||
"download-dir": ('string', 1, None, None, None),
|
||||
"peer-limit": ('number', 1, 5, None, 'peer-limit-global'),
|
||||
"peer-limit-global": ('number', 5, None, 'peer-limit', None),
|
||||
"peer-limit-per-torrent": ('number', 5, None, None, None),
|
||||
"pex-allowed": ('boolean', 1, 5, None, 'pex-enabled'),
|
||||
"pex-enabled": ('boolean', 5, None, 'pex-allowed', None),
|
||||
"port": ('number', 1, 5, None, 'peer-port'),
|
||||
"peer-port": ('number', 5, None, 'port', None),
|
||||
"peer-port-random-on-start": ('boolean', 5, None, None, None),
|
||||
"port-forwarding-enabled": ('boolean', 1, None, None, None),
|
||||
"seedRatioLimit": ('double', 5, None, None, None),
|
||||
"seedRatioLimited": ('boolean', 5, None, None, None),
|
||||
"speed-limit-down": ('number', 1, None, None, None),
|
||||
"speed-limit-down-enabled": ('boolean', 1, None, None, None),
|
||||
"speed-limit-up": ('number', 1, None, None, None),
|
||||
"speed-limit-up-enabled": ('boolean', 1, None, None, None),
|
||||
},
|
||||
}
|
|
@ -1,606 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# 2008-07, Erik Svensson <erik.public@gmail.com>
|
||||
|
||||
import sys, os, time, datetime
|
||||
import re
|
||||
import httplib, urllib2, base64, socket
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
from constants import *
|
||||
from utils import *
|
||||
|
||||
class TransmissionError(Exception):
|
||||
def __init__(self, message='', original=None):
|
||||
Exception.__init__(self, message)
|
||||
self.message = message
|
||||
self.original = original
|
||||
|
||||
def __str__(self):
|
||||
if self.original:
|
||||
original_name = type(self.original).__name__
|
||||
return '%s Original exception: %s, "%s"' % (self.message, original_name, self.original.args)
|
||||
else:
|
||||
return self.args
|
||||
|
||||
class Torrent(object):
|
||||
"""
|
||||
Torrent is a class holding the data raceived from Transmission regarding a bittorrent transfer.
|
||||
All fetched torrent fields are accessable through this class using attributes.
|
||||
This class has a few convenience properties using the torrent data.
|
||||
"""
|
||||
|
||||
def __init__(self, fields):
|
||||
if 'id' not in fields:
|
||||
raise ValueError('Torrent requires an id')
|
||||
self.fields = {}
|
||||
self.update(fields)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Torrent %d \"%s\">' % (self.fields['id'], self.fields['name'])
|
||||
|
||||
def __str__(self):
|
||||
return 'torrent %s' % self.fields['name']
|
||||
|
||||
def update(self, other):
|
||||
"""Update the torrent data from a Transmission arguments dictinary"""
|
||||
fields = None
|
||||
if isinstance(other, dict):
|
||||
fields = other
|
||||
elif isinstance(other, Torrent):
|
||||
fields = other.fields
|
||||
else:
|
||||
raise ValueError('Cannot update with supplied data')
|
||||
for k, v in fields.iteritems():
|
||||
self.fields[k.replace('-', '_')] = v
|
||||
|
||||
def files(self):
|
||||
"""
|
||||
Get list of files for this torrent. This function returns a dictionary with file information for each file.
|
||||
"""
|
||||
result = {}
|
||||
if 'files' in self.fields:
|
||||
indicies = xrange(len(self.fields['files']))
|
||||
files = self.fields['files']
|
||||
priorities = self.fields['priorities']
|
||||
wanted = self.fields['wanted']
|
||||
index = 1
|
||||
for item in zip(indicies, files, priorities, wanted):
|
||||
selected = bool(item[3])
|
||||
priority = PRIORITY[item[2]]
|
||||
result[item[0]] = {
|
||||
'selected': selected,
|
||||
'priority': priority,
|
||||
'size': item[1]['length'],
|
||||
'name': item[1]['name'],
|
||||
'completed': item[1]['bytesCompleted']}
|
||||
return result
|
||||
|
||||
def __getattr__(self, name):
|
||||
try:
|
||||
return self.fields[name]
|
||||
except KeyError, e:
|
||||
raise AttributeError('No attribute %s' % name)
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
"""Get the status as string."""
|
||||
return STATUS[self.fields['status']]
|
||||
|
||||
@property
|
||||
def progress(self):
|
||||
"""Get the download progress in percent as float."""
|
||||
try:
|
||||
return 100.0 * (self.fields['sizeWhenDone'] - self.fields['leftUntilDone']) / float(self.fields['sizeWhenDone'])
|
||||
except ZeroDivisionError:
|
||||
return 0.0
|
||||
|
||||
@property
|
||||
def ratio(self):
|
||||
"""Get the upload/download ratio."""
|
||||
try:
|
||||
return self.fields['uploadedEver'] / float(self.fields['downloadedEver'])
|
||||
except ZeroDivisionError:
|
||||
return 0.0
|
||||
|
||||
@property
|
||||
def eta(self):
|
||||
"""Get the "eta" as datetime.timedelta."""
|
||||
eta = self.fields['eta']
|
||||
if eta >= 0:
|
||||
return datetime.timedelta(seconds=eta)
|
||||
else:
|
||||
ValueError('eta not valid')
|
||||
|
||||
@property
|
||||
def date_active(self):
|
||||
"""Get the attribute "activityDate" as datetime.datetime."""
|
||||
return datetime.datetime.fromtimestamp(self.fields['activityDate'])
|
||||
|
||||
@property
|
||||
def date_added(self):
|
||||
"""Get the attribute "addedDate" as datetime.datetime."""
|
||||
return datetime.datetime.fromtimestamp(self.fields['addedDate'])
|
||||
|
||||
@property
|
||||
def date_started(self):
|
||||
"""Get the attribute "startDate" as datetime.datetime."""
|
||||
return datetime.datetime.fromtimestamp(self.fields['startDate'])
|
||||
|
||||
@property
|
||||
def date_done(self):
|
||||
"""Get the attribute "doneDate" as datetime.datetime."""
|
||||
return datetime.datetime.fromtimestamp(self.fields['doneDate'])
|
||||
|
||||
def format_eta(self):
|
||||
"""Returns the attribute "eta" formatted as a string."""
|
||||
eta = self.fields['eta']
|
||||
if eta == -1:
|
||||
return 'not available'
|
||||
elif eta == -2:
|
||||
return 'unknown'
|
||||
else:
|
||||
return format_timedelta(self.eta)
|
||||
|
||||
class Session(object):
|
||||
"""
|
||||
Session is a class holding the session data for a Transmission daemon.
|
||||
|
||||
Access the session field can be done through attributes.
|
||||
The attributes available are the same as the session arguments in the
|
||||
Transmission RPC specification, but with underscore instead of hypen.
|
||||
``download-dir`` -> ``download_dir``.
|
||||
"""
|
||||
|
||||
def __init__(self, fields={}):
|
||||
self.fields = {}
|
||||
self.update(fields)
|
||||
|
||||
def update(self, other):
|
||||
"""Update the session data from a session arguments dictinary"""
|
||||
|
||||
fields = None
|
||||
if isinstance(other, dict):
|
||||
fields = other
|
||||
elif isinstance(other, Session):
|
||||
fields = other.fields
|
||||
else:
|
||||
raise ValueError('Cannot update with supplied data')
|
||||
|
||||
for k, v in fields.iteritems():
|
||||
self.fields[k.replace('-', '_')] = v
|
||||
|
||||
def __getattr__(self, name):
|
||||
try:
|
||||
return self.fields[name]
|
||||
except KeyError, e:
|
||||
raise AttributeError('No attribute %s' % name)
|
||||
|
||||
def __str__(self):
|
||||
text = ''
|
||||
for k, v in self.fields.iteritems():
|
||||
text += "% 32s: %s\n" % (k[-32:], v)
|
||||
return text
|
||||
|
||||
class Client(object):
|
||||
"""
|
||||
This is it. This class implements the json-RPC protocol to communicate with Transmission.
|
||||
"""
|
||||
|
||||
def __init__(self, address='localhost', port=DEFAULT_PORT, user=None, password=None):
|
||||
base_url = 'http://' + address + ':' + str(port)
|
||||
self.url = base_url + '/transmission/rpc'
|
||||
if user and password:
|
||||
password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
|
||||
password_manager.add_password(realm=None, uri=self.url, user=user, passwd=password)
|
||||
opener = urllib2.build_opener(
|
||||
urllib2.HTTPBasicAuthHandler(password_manager)
|
||||
, urllib2.HTTPDigestAuthHandler(password_manager)
|
||||
)
|
||||
urllib2.install_opener(opener)
|
||||
elif user or password:
|
||||
logger.warning('Either user or password missing, not using authentication.')
|
||||
self._sequence = 0
|
||||
self.session = Session()
|
||||
self.sessionid = 0
|
||||
self.protocol_version = None
|
||||
self.get_session()
|
||||
self.torrent_get_arguments = get_arguments('torrent-get'
|
||||
, self.rpc_version)
|
||||
|
||||
def _debug_request(self, request):
|
||||
logger.debug(
|
||||
json.dumps(
|
||||
{
|
||||
'request': {
|
||||
'url': request.get_full_url(),
|
||||
'request-headers': dict(request.header_items()),
|
||||
'request-data': json.loads(request.data),
|
||||
}
|
||||
},
|
||||
indent=2
|
||||
)
|
||||
)
|
||||
|
||||
def _debug_response(self, response, response_data):
|
||||
try:
|
||||
response_data = json.loads(response_data)
|
||||
except:
|
||||
pass
|
||||
logger.debug(
|
||||
json.dumps(
|
||||
{
|
||||
'response': {
|
||||
'url': response.url,
|
||||
'code': response.code,
|
||||
'msg': response.msg,
|
||||
'headers': dict(response.headers),
|
||||
'data': response_data,
|
||||
}
|
||||
},
|
||||
indent=2
|
||||
)
|
||||
)
|
||||
|
||||
def _http_query(self, query):
|
||||
headers = {'X-Transmission-Session-Id': self.sessionid}
|
||||
request = urllib2.Request(self.url, query, headers)
|
||||
request_count = 0
|
||||
while True:
|
||||
error_data = ""
|
||||
try:
|
||||
try:
|
||||
self._debug_request(request)
|
||||
socket.setdefaulttimeout(10)
|
||||
if (sys.version_info[0] == 2 and sys.version_info[1] > 5) or sys.version_info[0] > 2:
|
||||
response = urllib2.urlopen(request, timeout=60)
|
||||
else:
|
||||
response = urllib2.urlopen(request)
|
||||
break
|
||||
except urllib2.HTTPError, error:
|
||||
error_data = error.read()
|
||||
if error.code == 409:
|
||||
logger.info('Server responded with 409, trying to set session-id.')
|
||||
if request_count > 1:
|
||||
raise TransmissionError('Session ID negotiation failed.', error)
|
||||
if 'X-Transmission-Session-Id' in error.headers:
|
||||
self.sessionid = error.headers['X-Transmission-Session-Id']
|
||||
request.add_header('X-Transmission-Session-Id', self.sessionid)
|
||||
else:
|
||||
raise TransmissionError('Unknown conflict.', error)
|
||||
except urllib2.URLError, error:
|
||||
raise TransmissionError('Failed to connect to daemon.', error)
|
||||
except httplib.BadStatusLine, error:
|
||||
if (request_count > 1):
|
||||
raise TransmissionError('Failed to request %s "%s".' % (self.url, query), error)
|
||||
finally:
|
||||
if error_data:
|
||||
self._debug_response(error, error_data)
|
||||
request_count = request_count + 1
|
||||
result = response.read()
|
||||
self._debug_response(response, result)
|
||||
return result
|
||||
|
||||
def _request(self, method, arguments={}, ids=[], require_ids = False):
|
||||
"""Send json-rpc request to Transmission using http POST"""
|
||||
|
||||
if not isinstance(method, (str, unicode)):
|
||||
raise ValueError('request takes method as string')
|
||||
if not isinstance(arguments, dict):
|
||||
raise ValueError('request takes arguments as dict')
|
||||
ids = self._format_ids(ids)
|
||||
if len(ids) > 0:
|
||||
arguments['ids'] = ids
|
||||
elif require_ids:
|
||||
raise ValueError('request require ids')
|
||||
|
||||
query = json.dumps({'tag': self._sequence, 'method': method
|
||||
, 'arguments': arguments})
|
||||
logger.info(query)
|
||||
self._sequence += 1
|
||||
start = time.time()
|
||||
http_data = self._http_query(query)
|
||||
elapsed = time.time() - start
|
||||
logger.info('http request took %.3f s' % (elapsed))
|
||||
|
||||
try:
|
||||
data = json.loads(http_data)
|
||||
except ValueError, e:
|
||||
logger.error('Error: ' + str(e))
|
||||
logger.error('Request: \"%s\"' % (query))
|
||||
logger.error('HTTP data: \"%s\"' % (http_data))
|
||||
raise
|
||||
|
||||
logger.info(json.dumps(data, indent=2))
|
||||
|
||||
if data['result'] != 'success':
|
||||
raise TransmissionError('Query failed with result \"%s\"'
|
||||
% data['result'])
|
||||
|
||||
results = {}
|
||||
if method == 'torrent-get':
|
||||
for item in data['arguments']['torrents']:
|
||||
results[item['id']] = Torrent(item)
|
||||
if self.protocol_version == 2 and 'peers' not in item:
|
||||
self.protocol_version = 1
|
||||
elif method == 'torrent-add':
|
||||
item = data['arguments']['torrent-added']
|
||||
results[item['id']] = Torrent(item)
|
||||
elif method == 'session-get':
|
||||
self._update_session(data['arguments'])
|
||||
elif method == 'session-stats':
|
||||
# older versions of T has the return data in "session-stats"
|
||||
if 'session-stats' in data['arguments']:
|
||||
self._update_session(data['arguments']['session-stats'])
|
||||
else:
|
||||
self._update_session(data['arguments'])
|
||||
elif method in ('port-test', 'blocklist-update'):
|
||||
results = data['arguments']
|
||||
else:
|
||||
return None
|
||||
|
||||
return results
|
||||
|
||||
def _format_ids(self, args):
|
||||
"""Take things and make them valid torrent identifiers"""
|
||||
ids = []
|
||||
|
||||
if isinstance(args, (int, long)):
|
||||
ids.append(args)
|
||||
elif isinstance(args, (str, unicode)):
|
||||
for item in re.split(u'[ ,]+', args):
|
||||
if len(item) == 0:
|
||||
continue
|
||||
addition = None
|
||||
try:
|
||||
# handle index
|
||||
addition = [int(item)]
|
||||
except ValueError:
|
||||
pass
|
||||
if not addition:
|
||||
# handle hashes
|
||||
try:
|
||||
int(item, 16)
|
||||
addition = [item]
|
||||
except:
|
||||
pass
|
||||
if not addition:
|
||||
# handle index ranges i.e. 5:10
|
||||
match = re.match(u'^(\d+):(\d+)$', item)
|
||||
if match:
|
||||
try:
|
||||
idx_from = int(match.group(1))
|
||||
idx_to = int(match.group(2))
|
||||
addition = range(idx_from, idx_to + 1)
|
||||
except:
|
||||
pass
|
||||
if not addition:
|
||||
raise ValueError(u'Invalid torrent id, \"%s\"' % item)
|
||||
ids.extend(addition)
|
||||
elif isinstance(args, (list)):
|
||||
for item in args:
|
||||
ids.extend(self._format_ids(item))
|
||||
else:
|
||||
raise ValueError(u'Invalid torrent id')
|
||||
return ids
|
||||
|
||||
def _update_session(self, data):
|
||||
self.session.update(data)
|
||||
|
||||
@property
|
||||
def rpc_version(self):
|
||||
if self.protocol_version == None:
|
||||
if hasattr(self.session, 'rpc_version'):
|
||||
self.protocol_version = self.session.rpc_version
|
||||
elif hasattr(self.session, 'version'):
|
||||
self.protocol_version = 3
|
||||
else:
|
||||
self.protocol_version = 2
|
||||
return self.protocol_version
|
||||
|
||||
def _rpc_version_warning(self, version):
|
||||
if self.rpc_version < version:
|
||||
logger.warning('Using feature not supported by server. RPC version for server %d, feature introduced in %d.' % (self.rpc_version, version))
|
||||
|
||||
def add(self, data, **kwargs):
|
||||
"""
|
||||
Add torrent to transfers list. Takes a base64 encoded .torrent file in data.
|
||||
Additional arguments are:
|
||||
|
||||
* `paused`, boolean, Whether to pause the transfer on add.
|
||||
* `download_dir`, path, The directory where the downloaded
|
||||
contents will be saved in.
|
||||
* `peer_limit`, number, Limits the number of peers for this
|
||||
transfer.
|
||||
* `files_unwanted`,
|
||||
* `files_wanted`,
|
||||
* `priority_high`,
|
||||
* `priority_low`,
|
||||
* `priority_normal`,
|
||||
"""
|
||||
args = {'metainfo': data}
|
||||
for key, value in kwargs.iteritems():
|
||||
argument = make_rpc_name(key)
|
||||
(arg, val) = argument_value_convert('torrent-add',
|
||||
argument, value, self.rpc_version)
|
||||
args[arg] = val
|
||||
return self._request('torrent-add', args)
|
||||
|
||||
def add_url(self, torrent_url, **kwargs):
|
||||
"""
|
||||
Add torrent to transfers list. Takes a url to a .torrent file.
|
||||
Additional arguments are:
|
||||
|
||||
* `paused`, boolean, Whether to pause the transfer on add.
|
||||
* `download_dir`, path, The directory where the downloaded
|
||||
contents will be saved in.
|
||||
* `peer_limit`, number, Limits the number of peers for this
|
||||
transfer.
|
||||
* `files_unwanted`,
|
||||
* `files_wanted`,
|
||||
* `priority_high`,
|
||||
* `priority_low`,
|
||||
* `priority_normal`,
|
||||
"""
|
||||
torrent_file = None
|
||||
if os.path.exists(torrent_url):
|
||||
torrent_file = open(torrent_url, 'r')
|
||||
else:
|
||||
try:
|
||||
torrent_file = urllib2.urlopen(torrent_url)
|
||||
except:
|
||||
torrent_file = None
|
||||
|
||||
if not torrent_file:
|
||||
raise TransmissionError('File does not exist.')
|
||||
|
||||
torrent_data = base64.b64encode(torrent_file.read())
|
||||
return self.add(torrent_data, **kwargs)
|
||||
|
||||
def remove(self, ids, delete_data=False):
|
||||
"""
|
||||
remove torrent(s) with provided id(s). Local data is removed if
|
||||
delete_data is True, otherwise not.
|
||||
"""
|
||||
self._rpc_version_warning(3)
|
||||
self._request('torrent-remove',
|
||||
{'delete-local-data':rpc_bool(delete_data)}, ids, True)
|
||||
|
||||
def start(self, ids):
|
||||
"""start torrent(s) with provided id(s)"""
|
||||
self._request('torrent-start', {}, ids, True)
|
||||
|
||||
def stop(self, ids):
|
||||
"""stop torrent(s) with provided id(s)"""
|
||||
self._request('torrent-stop', {}, ids, True)
|
||||
|
||||
def verify(self, ids):
|
||||
"""verify torrent(s) with provided id(s)"""
|
||||
self._request('torrent-verify', {}, ids, True)
|
||||
|
||||
def reannounce(self, ids):
|
||||
"""reannounce torrent(s) with provided id(s)"""
|
||||
self._rpc_version_warning(5)
|
||||
self._request('torrent-reannounce', {}, ids, True)
|
||||
|
||||
def info(self, ids=[], arguments={}):
|
||||
"""Get detailed information for torrent(s) with provided id(s)."""
|
||||
if not arguments:
|
||||
arguments = self.torrent_get_arguments
|
||||
return self._request('torrent-get', {'fields': arguments}, ids)
|
||||
|
||||
def get_files(self, ids=[]):
|
||||
"""
|
||||
Get list of files for provided torrent id(s).
|
||||
This function returns a dictonary for each requested torrent id holding
|
||||
the information about the files.
|
||||
"""
|
||||
fields = ['id', 'name', 'hashString', 'files', 'priorities', 'wanted']
|
||||
request_result = self._request('torrent-get', {'fields': fields}, ids)
|
||||
result = {}
|
||||
for id, torrent in request_result.iteritems():
|
||||
result[id] = torrent.files()
|
||||
return result
|
||||
|
||||
def set_files(self, items):
|
||||
"""
|
||||
Set file properties. Takes a dictonary with similar contents as the
|
||||
result of get_files.
|
||||
"""
|
||||
if not isinstance(items, dict):
|
||||
raise ValueError('Invalid file description')
|
||||
for tid, files in items.iteritems():
|
||||
if not isinstance(files, dict):
|
||||
continue
|
||||
wanted = []
|
||||
unwanted = []
|
||||
priority_high = []
|
||||
priority_normal = []
|
||||
priority_low = []
|
||||
for fid, file in files.iteritems():
|
||||
if not isinstance(file, dict):
|
||||
continue
|
||||
if 'selected' in file and file['selected']:
|
||||
wanted.append(fid)
|
||||
else:
|
||||
unwanted.append(fid)
|
||||
if 'priority' in file:
|
||||
if file['priority'] == 'high':
|
||||
priority_high.append(fid)
|
||||
elif file['priority'] == 'normal':
|
||||
priority_normal.append(fid)
|
||||
elif file['priority'] == 'low':
|
||||
priority_low.append(fid)
|
||||
self.change([tid], files_wanted = wanted
|
||||
, files_unwanted = unwanted
|
||||
, priority_high = priority_high
|
||||
, priority_normal = priority_normal
|
||||
, priority_low = priority_low)
|
||||
|
||||
def list(self):
|
||||
"""list all torrents"""
|
||||
fields = ['id', 'hashString', 'name', 'sizeWhenDone', 'leftUntilDone'
|
||||
, 'eta', 'status', 'rateUpload', 'rateDownload', 'uploadedEver'
|
||||
, 'downloadedEver']
|
||||
return self._request('torrent-get', {'fields': fields})
|
||||
|
||||
def change(self, ids, **kwargs):
|
||||
"""
|
||||
Change torrent parameters. This is the list of parameters that.
|
||||
"""
|
||||
args = {}
|
||||
for key, value in kwargs.iteritems():
|
||||
argument = make_rpc_name(key)
|
||||
(arg, val) = argument_value_convert('torrent-set'
|
||||
, argument, value, self.rpc_version)
|
||||
args[arg] = val
|
||||
|
||||
if len(args) > 0:
|
||||
self._request('torrent-set', args, ids, True)
|
||||
else:
|
||||
ValueError("No arguments to set")
|
||||
|
||||
def get_session(self):
|
||||
"""Get session parameters"""
|
||||
self._request('session-get')
|
||||
return self.session
|
||||
|
||||
def set_session(self, **kwargs):
|
||||
"""Set session parameters"""
|
||||
args = {}
|
||||
for key, value in kwargs.iteritems():
|
||||
if key == 'encryption' and value not in ['required', 'preferred', 'tolerated']:
|
||||
raise ValueError('Invalid encryption value')
|
||||
argument = make_rpc_name(key)
|
||||
(arg, val) = argument_value_convert('session-set'
|
||||
, argument, value, self.rpc_version)
|
||||
args[arg] = val
|
||||
if len(args) > 0:
|
||||
self._request('session-set', args)
|
||||
|
||||
def blocklist_update(self):
|
||||
"""Update block list. Returns the size of the block list."""
|
||||
self._rpc_version_warning(5)
|
||||
result = self._request('blocklist-update')
|
||||
if 'blocklist-size' in result:
|
||||
return result['blocklist-size']
|
||||
return None
|
||||
|
||||
def port_test(self):
|
||||
"""
|
||||
Tests to see if your incoming peer port is accessible from the
|
||||
outside world.
|
||||
"""
|
||||
self._rpc_version_warning(5)
|
||||
result = self._request('port-test')
|
||||
if 'port-is-open' in result:
|
||||
return result['port-is-open']
|
||||
return None
|
||||
|
||||
def session_stats(self):
|
||||
"""Get session statistics"""
|
||||
self._request('session-stats')
|
||||
return self.session
|
|
@ -1,138 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# 2008-07, Erik Svensson <erik.public@gmail.com>
|
||||
|
||||
import socket, datetime
|
||||
import constants
|
||||
from constants import logger
|
||||
|
||||
UNITS = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB']
|
||||
|
||||
def format_size(size):
|
||||
s = float(size)
|
||||
i = 0
|
||||
while size >= 1024.0 and i < len(UNITS):
|
||||
i += 1
|
||||
size /= 1024.0
|
||||
return (size, UNITS[i])
|
||||
|
||||
def format_speed(size):
|
||||
(size, unit) = format_size(size)
|
||||
return (size, unit + '/s')
|
||||
|
||||
def format_timedelta(delta):
|
||||
minutes, seconds = divmod(delta.seconds, 60)
|
||||
hours, minutes = divmod(minutes, 60)
|
||||
return '%d %02d:%02d:%02d' % (delta.days, hours, minutes, seconds)
|
||||
|
||||
def format_timestamp(timestamp):
|
||||
if timestamp > 0:
|
||||
dt = datetime.datetime.fromtimestamp(timestamp)
|
||||
return dt.isoformat(' ')
|
||||
else:
|
||||
return '-'
|
||||
|
||||
class INetAddressError(Exception):
|
||||
pass
|
||||
|
||||
def inet_address(address, default_port, default_address='localhost'):
|
||||
addr = address.split(':')
|
||||
if len(addr) == 1:
|
||||
try:
|
||||
port = int(addr[0])
|
||||
addr = default_address
|
||||
except:
|
||||
addr = addr[0]
|
||||
port = default_port
|
||||
elif len(addr) == 2:
|
||||
port = int(addr[1])
|
||||
if len(addr[0]) == 0:
|
||||
addr = default_address
|
||||
else:
|
||||
addr = addr[0]
|
||||
else:
|
||||
addr = default_address
|
||||
port = default_port
|
||||
try:
|
||||
socket.getaddrinfo(addr, port, socket.AF_INET, socket.SOCK_STREAM)
|
||||
except socket.gaierror, e:
|
||||
raise INetAddressError('Cannot look up address "%s".' % address)
|
||||
return (addr, port)
|
||||
|
||||
def rpc_bool(arg):
|
||||
if isinstance(arg, (str, unicode)):
|
||||
try:
|
||||
arg = bool(int(arg))
|
||||
except:
|
||||
arg = arg.lower() in [u'true', u'yes']
|
||||
if bool(arg):
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
TR_TYPE_MAP = {
|
||||
'number' : int,
|
||||
'string' : str,
|
||||
'double': float,
|
||||
'boolean' : rpc_bool,
|
||||
'array': list,
|
||||
'object': dict
|
||||
}
|
||||
|
||||
def make_python_name(name):
|
||||
return name.replace('-', '_')
|
||||
|
||||
def make_rpc_name(name):
|
||||
return name.replace('_', '-')
|
||||
|
||||
def argument_value_convert(method, argument, value, rpc_version):
|
||||
if method in ('torrent-add', 'torrent-get', 'torrent-set'):
|
||||
args = constants.TORRENT_ARGS[method[-3:]]
|
||||
elif method in ('session-get', 'session-set'):
|
||||
args = constants.SESSION_ARGS[method[-3:]]
|
||||
else:
|
||||
return ValueError('Method "%s" not supported' % (method))
|
||||
if argument in args:
|
||||
info = args[argument]
|
||||
invalid_version = True
|
||||
while invalid_version:
|
||||
invalid_version = False
|
||||
replacement = None
|
||||
if rpc_version < info[1]:
|
||||
invalid_version = True
|
||||
replacement = info[3]
|
||||
if info[2] and info[2] <= rpc_version:
|
||||
invalid_version = True
|
||||
replacement = info[4]
|
||||
if invalid_version:
|
||||
if replacement:
|
||||
logger.warning(
|
||||
'Replacing requested argument "%s" with "%s".'
|
||||
% (argument, replacement))
|
||||
argument = replacement
|
||||
info = args[argument]
|
||||
else:
|
||||
raise ValueError(
|
||||
'Method "%s" Argument "%s" does not exist in version %d.'
|
||||
% (method, argument, rpc_version))
|
||||
return (argument, TR_TYPE_MAP[info[0]](value))
|
||||
else:
|
||||
raise ValueError('Argument "%s" does not exists for method "%s".',
|
||||
(argument, method))
|
||||
|
||||
def get_arguments(method, rpc_version):
|
||||
if method in ('torrent-add', 'torrent-get', 'torrent-set'):
|
||||
args = constants.TORRENT_ARGS[method[-3:]]
|
||||
elif method in ('session-get', 'session-set'):
|
||||
args = constants.SESSION_ARGS[method[-3:]]
|
||||
else:
|
||||
return ValueError('Method "%s" not supported' % (method))
|
||||
accessible = []
|
||||
for argument, info in args.iteritems():
|
||||
valid_version = True
|
||||
if rpc_version < info[1]:
|
||||
valid_version = False
|
||||
if info[2] and info[2] <= rpc_version:
|
||||
valid_version = False
|
||||
if valid_version:
|
||||
accessible.append(argument)
|
||||
return accessible
|
|
@ -1,10 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# 2008-08, Erik Svensson <erik.public@gmail.com>
|
||||
|
||||
from constants import *
|
||||
from transmission import TransmissionError, Torrent, Session, Client
|
||||
|
||||
__author__ = u'Erik Svensson <erik.public@gmail.com>'
|
||||
__version__ = u'0.3'
|
||||
__copyright__ = u'Copyright (c) 2008 Erik Svensson'
|
||||
__license__ = u'MIT'
|
|
@ -1,230 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# 2008-07, Erik Svensson <erik.public@gmail.com>
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger('transmissionrpc')
|
||||
logger.setLevel(logging.ERROR)
|
||||
|
||||
def mirror_dict(d):
|
||||
d.update(dict((v, k) for k, v in d.iteritems()))
|
||||
return d
|
||||
|
||||
DEFAULT_PORT = 9091
|
||||
|
||||
TR_STATUS_CHECK_WAIT = (1<<0)
|
||||
TR_STATUS_CHECK = (1<<1)
|
||||
TR_STATUS_DOWNLOAD = (1<<2)
|
||||
TR_STATUS_SEED = (1<<3)
|
||||
TR_STATUS_STOPPED = (1<<4)
|
||||
|
||||
STATUS = mirror_dict({
|
||||
'check pending' : TR_STATUS_CHECK_WAIT,
|
||||
'checking' : TR_STATUS_CHECK,
|
||||
'downloading' : TR_STATUS_DOWNLOAD,
|
||||
'seeding' : TR_STATUS_SEED,
|
||||
'stopped' : TR_STATUS_STOPPED,
|
||||
})
|
||||
|
||||
TR_PRI_LOW = -1
|
||||
TR_PRI_NORMAL = 0
|
||||
TR_PRI_HIGH = 1
|
||||
|
||||
PRIORITY = mirror_dict({
|
||||
'low' : TR_PRI_LOW,
|
||||
'normal' : TR_PRI_NORMAL,
|
||||
'high' : TR_PRI_HIGH
|
||||
})
|
||||
|
||||
TR_RATIOLIMIT_GLOBAL = 0 # follow the global settings
|
||||
TR_RATIOLIMIT_SINGLE = 1 # override the global settings, seeding until a certain ratio
|
||||
TR_RATIOLIMIT_UNLIMITED = 2 # override the global settings, seeding regardless of ratio
|
||||
|
||||
RATIO_LIMIT = mirror_dict({
|
||||
'global' : TR_RATIOLIMIT_GLOBAL,
|
||||
'single' : TR_RATIOLIMIT_SINGLE,
|
||||
'unlimeted' : TR_RATIOLIMIT_UNLIMITED
|
||||
})
|
||||
|
||||
# A note on argument maps
|
||||
# These maps are used to verify *-set methods. The information is structured in
|
||||
# a tree.
|
||||
# set +- <argument1> - [<type>, <added version>, <removed version>, <previous argument name>, <next argument name>]
|
||||
# | +- <argument2> - [<type>, <added version>, <removed version>, <previous argument name>, <next argument name>]
|
||||
# |
|
||||
# get +- <argument1> - [<type>, <added version>, <removed version>, <previous argument name>, <next argument name>]
|
||||
# +- <argument2> - [<type>, <added version>, <removed version>, <previous argument name>, <next argument name>]
|
||||
|
||||
# Arguments for torrent methods
|
||||
TORRENT_ARGS = {
|
||||
'get' : {
|
||||
'activityDate': ('number', 1, None, None, None),
|
||||
'addedDate': ('number', 1, None, None, None),
|
||||
'announceResponse': ('string', 1, None, None, None),
|
||||
'announceURL': ('string', 1, None, None, None),
|
||||
'bandwidthPriority': ('number', 5, None, None, None),
|
||||
'comment': ('string', 1, None, None, None),
|
||||
'corruptEver': ('number', 1, None, None, None),
|
||||
'creator': ('string', 1, None, None, None),
|
||||
'dateCreated': ('number', 1, None, None, None),
|
||||
'desiredAvailable': ('number', 1, None, None, None),
|
||||
'doneDate': ('number', 1, None, None, None),
|
||||
'downloadDir': ('string', 4, None, None, None),
|
||||
'downloadedEver': ('number', 1, None, None, None),
|
||||
'downloaders': ('number', 4, None, None, None),
|
||||
'downloadLimit': ('number', 1, None, None, None),
|
||||
'downloadLimited': ('boolean', 5, None, None, None),
|
||||
'downloadLimitMode': ('number', 1, 5, None, None),
|
||||
'error': ('number', 1, None, None, None),
|
||||
'errorString': ('number', 1, None, None, None),
|
||||
'eta': ('number', 1, None, None, None),
|
||||
'files': ('array', 1, None, None, None),
|
||||
'fileStats': ('array', 5, None, None, None),
|
||||
'hashString': ('string', 1, None, None, None),
|
||||
'haveUnchecked': ('number', 1, None, None, None),
|
||||
'haveValid': ('number', 1, None, None, None),
|
||||
'honorsSessionLimits': ('boolean', 5, None, None, None),
|
||||
'id': ('number', 1, None, None, None),
|
||||
'isPrivate': ('boolean', 1, None, None, None),
|
||||
'lastAnnounceTime': ('number', 1, None, None, None),
|
||||
'lastScrapeTime': ('number', 1, None, None, None),
|
||||
'leechers': ('number', 1, None, None, None),
|
||||
'leftUntilDone': ('number', 1, None, None, None),
|
||||
'manualAnnounceTime': ('number', 1, None, None, None),
|
||||
'maxConnectedPeers': ('number', 1, None, None, None),
|
||||
'name': ('string', 1, None, None, None),
|
||||
'nextAnnounceTime': ('number', 1, None, None, None),
|
||||
'nextScrapeTime': ('number', 1, None, None, None),
|
||||
'peer-limit': ('number', 5, None, None, None),
|
||||
'peers': ('array', 2, None, None, None),
|
||||
'peersConnected': ('number', 1, None, None, None),
|
||||
'peersFrom': ('object', 1, None, None, None),
|
||||
'peersGettingFromUs': ('number', 1, None, None, None),
|
||||
'peersKnown': ('number', 1, None, None, None),
|
||||
'peersSendingToUs': ('number', 1, None, None, None),
|
||||
'percentDone': ('double', 5, None, None, None),
|
||||
'pieces': ('string', 5, None, None, None),
|
||||
'pieceCount': ('number', 1, None, None, None),
|
||||
'pieceSize': ('number', 1, None, None, None),
|
||||
'priorities': ('array', 1, None, None, None),
|
||||
'rateDownload': ('number', 1, None, None, None),
|
||||
'rateUpload': ('number', 1, None, None, None),
|
||||
'recheckProgress': ('double', 1, None, None, None),
|
||||
'scrapeResponse': ('string', 1, None, None, None),
|
||||
'scrapeURL': ('string', 1, None, None, None),
|
||||
'seeders': ('number', 1, None, None, None),
|
||||
'seedRatioLimit': ('double', 5, None, None, None),
|
||||
'seedRatioMode': ('number', 5, None, None, None),
|
||||
'sizeWhenDone': ('number', 1, None, None, None),
|
||||
'startDate': ('number', 1, None, None, None),
|
||||
'status': ('number', 1, None, None, None),
|
||||
'swarmSpeed': ('number', 1, None, None, None),
|
||||
'timesCompleted': ('number', 1, None, None, None),
|
||||
'trackers': ('array', 1, None, None, None),
|
||||
'totalSize': ('number', 1, None, None, None),
|
||||
'torrentFile': ('string', 5, None, None, None),
|
||||
'uploadedEver': ('number', 1, None, None, None),
|
||||
'uploadLimit': ('number', 1, None, None, None),
|
||||
'uploadLimitMode': ('number', 1, 5, None, None),
|
||||
'uploadLimited': ('boolean', 5, None, None, None),
|
||||
'uploadRatio': ('double', 1, None, None, None),
|
||||
'wanted': ('array', 1, None, None, None),
|
||||
'webseeds': ('array', 1, None, None, None),
|
||||
'webseedsSendingToUs': ('number', 1, None, None, None),
|
||||
},
|
||||
'set': {
|
||||
'bandwidthPriority': ('number', 5, None, None, None),
|
||||
'downloadLimit': ('number', 5, None, 'speed-limit-down', None),
|
||||
'downloadLimited': ('boolean', 5, None, 'speed-limit-down-enabled', None),
|
||||
'files-wanted': ('array', 1, None, None, None),
|
||||
'files-unwanted': ('array', 1, None, None, None),
|
||||
'honorsSessionLimits': ('boolean', 5, None, None, None),
|
||||
'ids': ('array', 1, None, None, None),
|
||||
'peer-limit': ('number', 1, None, None, None),
|
||||
'priority-high': ('array', 1, None, None, None),
|
||||
'priority-low': ('array', 1, None, None, None),
|
||||
'priority-normal': ('array', 1, None, None, None),
|
||||
'seedRatioLimit': ('double', 5, None, None, None),
|
||||
'seedRatioMode': ('number', 5, None, None, None),
|
||||
'speed-limit-down': ('number', 1, 5, None, 'downloadLimit'),
|
||||
'speed-limit-down-enabled': ('boolean', 1, 5, None, 'downloadLimited'),
|
||||
'speed-limit-up': ('number', 1, 5, None, 'uploadLimit'),
|
||||
'speed-limit-up-enabled': ('boolean', 1, 5, None, 'uploadLimited'),
|
||||
'uploadLimit': ('number', 5, None, 'speed-limit-up', None),
|
||||
'uploadLimited': ('boolean', 5, None, 'speed-limit-up-enabled', None),
|
||||
},
|
||||
'add': {
|
||||
'download-dir': ('string', 1, None, None, None),
|
||||
'filename': ('string', 1, None, None, None),
|
||||
'files-wanted': ('array', 1, None, None, None),
|
||||
'files-unwanted': ('array', 1, None, None, None),
|
||||
'metainfo': ('string', 1, None, None, None),
|
||||
'paused': ('boolean', 1, None, None, None),
|
||||
'peer-limit': ('number', 1, None, None, None),
|
||||
'priority-high': ('array', 1, None, None, None),
|
||||
'priority-low': ('array', 1, None, None, None),
|
||||
'priority-normal': ('array', 1, None, None, None),
|
||||
}
|
||||
}
|
||||
|
||||
# Arguments for session methods
|
||||
SESSION_ARGS = {
|
||||
'get': {
|
||||
"alt-speed-down": ('number', 5, None, None, None),
|
||||
"alt-speed-enabled": ('boolean', 5, None, None, None),
|
||||
"alt-speed-time-begin": ('number', 5, None, None, None),
|
||||
"alt-speed-time-enabled": ('boolean', 5, None, None, None),
|
||||
"alt-speed-time-end": ('number', 5, None, None, None),
|
||||
"alt-speed-time-day": ('number', 5, None, None, None),
|
||||
"alt-speed-up": ('number', 5, None, None, None),
|
||||
"blocklist-enabled": ('boolean', 5, None, None, None),
|
||||
"blocklist-size": ('number', 5, None, None, None),
|
||||
"encryption": ('string', 1, None, None, None),
|
||||
"download-dir": ('string', 1, None, None, None),
|
||||
"peer-limit": ('number', 1, 5, None, None),
|
||||
"peer-limit-global": ('number', 5, None, None, None),
|
||||
"peer-limit-per-torrent": ('number', 5, None, None, None),
|
||||
"pex-allowed": ('boolean', 1, 5, None, None),
|
||||
"pex-enabled": ('boolean', 5, None, None, None),
|
||||
"port": ('number', 1, 5, None, None),
|
||||
"peer-port": ('number', 5, None, None, None),
|
||||
"peer-port-random-on-start": ('boolean', 5, None, None, None),
|
||||
"port-forwarding-enabled": ('boolean', 1, None, None, None),
|
||||
"rpc-version": ('number', 4, None, None, None),
|
||||
"rpc-version-minimum": ('number', 4, None, None, None),
|
||||
"seedRatioLimit": ('double', 5, None, None, None),
|
||||
"seedRatioLimited": ('boolean', 5, None, None, None),
|
||||
"speed-limit-down": ('number', 1, None, None, None),
|
||||
"speed-limit-down-enabled": ('boolean', 1, None, None, None),
|
||||
"speed-limit-up": ('number', 1, None, None, None),
|
||||
"speed-limit-up-enabled": ('boolean', 1, None, None, None),
|
||||
"version": ('string', 3, None, None, None),
|
||||
},
|
||||
'set': {
|
||||
"alt-speed-down": ('number', 5, None, None, None),
|
||||
"alt-speed-enabled": ('boolean', 5, None, None, None),
|
||||
"alt-speed-time-begin": ('number', 5, None, None, None),
|
||||
"alt-speed-time-enabled": ('boolean', 5, None, None, None),
|
||||
"alt-speed-time-end": ('number', 5, None, None, None),
|
||||
"alt-speed-time-day": ('number', 5, None, None, None),
|
||||
"alt-speed-up": ('number', 5, None, None, None),
|
||||
"blocklist-enabled": ('boolean', 5, None, None, None),
|
||||
"encryption": ('string', 1, None, None, None),
|
||||
"download-dir": ('string', 1, None, None, None),
|
||||
"peer-limit": ('number', 1, 5, None, 'peer-limit-global'),
|
||||
"peer-limit-global": ('number', 5, None, 'peer-limit', None),
|
||||
"peer-limit-per-torrent": ('number', 5, None, None, None),
|
||||
"pex-allowed": ('boolean', 1, 5, None, 'pex-enabled'),
|
||||
"pex-enabled": ('boolean', 5, None, 'pex-allowed', None),
|
||||
"port": ('number', 1, 5, None, 'peer-port'),
|
||||
"peer-port": ('number', 5, None, 'port', None),
|
||||
"peer-port-random-on-start": ('boolean', 5, None, None, None),
|
||||
"port-forwarding-enabled": ('boolean', 1, None, None, None),
|
||||
"seedRatioLimit": ('double', 5, None, None, None),
|
||||
"seedRatioLimited": ('boolean', 5, None, None, None),
|
||||
"speed-limit-down": ('number', 1, None, None, None),
|
||||
"speed-limit-down-enabled": ('boolean', 1, None, None, None),
|
||||
"speed-limit-up": ('number', 1, None, None, None),
|
||||
"speed-limit-up-enabled": ('boolean', 1, None, None, None),
|
||||
},
|
||||
}
|
|
@ -1,605 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# 2008-07, Erik Svensson <erik.public@gmail.com>
|
||||
|
||||
import sys, os, time, datetime
|
||||
import re
|
||||
import httplib, urllib2, base64, socket
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
from constants import *
|
||||
from utils import *
|
||||
|
||||
class TransmissionError(Exception):
|
||||
def __init__(self, message='', original=None):
|
||||
Exception.__init__(self, message)
|
||||
self.message = message
|
||||
self.original = original
|
||||
|
||||
def __str__(self):
|
||||
if self.original:
|
||||
original_name = type(self.original).__name__
|
||||
return '%s Original exception: %s, "%s"' % (self.message, original_name, self.original.args)
|
||||
else:
|
||||
return self.args
|
||||
|
||||
class Torrent(object):
|
||||
"""
|
||||
Torrent is a class holding the data raceived from Transmission regarding a bittorrent transfer.
|
||||
All fetched torrent fields are accessable through this class using attributes.
|
||||
This class has a few convenience properties using the torrent data.
|
||||
"""
|
||||
|
||||
def __init__(self, fields):
|
||||
if 'id' not in fields:
|
||||
raise ValueError('Torrent requires an id')
|
||||
self.fields = {}
|
||||
self.update(fields)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Torrent %d \"%s\">' % (self.fields['id'], self.fields['name'])
|
||||
|
||||
def __str__(self):
|
||||
return 'torrent %s' % self.fields['name']
|
||||
|
||||
def update(self, other):
|
||||
"""Update the torrent data from a Transmission arguments dictinary"""
|
||||
fields = None
|
||||
if isinstance(other, dict):
|
||||
fields = other
|
||||
elif isinstance(other, Torrent):
|
||||
fields = other.fields
|
||||
else:
|
||||
raise ValueError('Cannot update with supplied data')
|
||||
for k, v in fields.iteritems():
|
||||
self.fields[k.replace('-', '_')] = v
|
||||
|
||||
def files(self):
|
||||
"""
|
||||
Get list of files for this torrent. This function returns a dictionary with file information for each file.
|
||||
"""
|
||||
result = {}
|
||||
if 'files' in self.fields:
|
||||
indicies = xrange(len(self.fields['files']))
|
||||
files = self.fields['files']
|
||||
priorities = self.fields['priorities']
|
||||
wanted = self.fields['wanted']
|
||||
index = 1
|
||||
for item in zip(indicies, files, priorities, wanted):
|
||||
selected = True if item[3] else False
|
||||
priority = PRIORITY[item[2]]
|
||||
result[item[0]] = {
|
||||
'selected': selected,
|
||||
'priority': priority,
|
||||
'size': item[1]['length'],
|
||||
'name': item[1]['name'],
|
||||
'completed': item[1]['bytesCompleted']}
|
||||
return result
|
||||
|
||||
def __getattr__(self, name):
|
||||
try:
|
||||
return self.fields[name]
|
||||
except KeyError, e:
|
||||
raise AttributeError('No attribute %s' % name)
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
"""Get the status as string."""
|
||||
return STATUS[self.fields['status']]
|
||||
|
||||
@property
|
||||
def progress(self):
|
||||
"""Get the download progress in percent as float."""
|
||||
try:
|
||||
return 100.0 * (self.fields['sizeWhenDone'] - self.fields['leftUntilDone']) / float(self.fields['sizeWhenDone'])
|
||||
except ZeroDivisionError:
|
||||
return 0.0
|
||||
|
||||
@property
|
||||
def ratio(self):
|
||||
"""Get the upload/download ratio."""
|
||||
try:
|
||||
return self.fields['uploadedEver'] / float(self.fields['downloadedEver'])
|
||||
except ZeroDivisionError:
|
||||
return 0.0
|
||||
|
||||
@property
|
||||
def eta(self):
|
||||
"""Get the "eta" as datetime.timedelta."""
|
||||
eta = self.fields['eta']
|
||||
if eta >= 0:
|
||||
return datetime.timedelta(seconds=eta)
|
||||
else:
|
||||
ValueError('eta not valid')
|
||||
|
||||
@property
|
||||
def date_active(self):
|
||||
"""Get the attribute "activityDate" as datetime.datetime."""
|
||||
return datetime.datetime.fromtimestamp(self.fields['activityDate'])
|
||||
|
||||
@property
|
||||
def date_added(self):
|
||||
"""Get the attribute "addedDate" as datetime.datetime."""
|
||||
return datetime.datetime.fromtimestamp(self.fields['addedDate'])
|
||||
|
||||
@property
|
||||
def date_started(self):
|
||||
"""Get the attribute "startDate" as datetime.datetime."""
|
||||
return datetime.datetime.fromtimestamp(self.fields['startDate'])
|
||||
|
||||
@property
|
||||
def date_done(self):
|
||||
"""Get the attribute "doneDate" as datetime.datetime."""
|
||||
return datetime.datetime.fromtimestamp(self.fields['doneDate'])
|
||||
|
||||
def format_eta(self):
|
||||
"""Returns the attribute "eta" formatted as a string."""
|
||||
eta = self.fields['eta']
|
||||
if eta == -1:
|
||||
return 'not available'
|
||||
elif eta == -2:
|
||||
return 'unknown'
|
||||
else:
|
||||
return format_timedelta(self.eta)
|
||||
|
||||
class Session(object):
|
||||
"""
|
||||
Session is a class holding the session data for a Transmission daemon.
|
||||
|
||||
Access the session field can be done through attributes.
|
||||
The attributes available are the same as the session arguments in the
|
||||
Transmission RPC specification, but with underscore instead of hypen.
|
||||
``download-dir`` -> ``download_dir``.
|
||||
"""
|
||||
|
||||
def __init__(self, fields={}):
|
||||
self.fields = {}
|
||||
self.update(fields)
|
||||
|
||||
def update(self, other):
|
||||
"""Update the session data from a session arguments dictinary"""
|
||||
|
||||
fields = None
|
||||
if isinstance(other, dict):
|
||||
fields = other
|
||||
elif isinstance(other, Session):
|
||||
fields = other.fields
|
||||
else:
|
||||
raise ValueError('Cannot update with supplied data')
|
||||
|
||||
for k, v in fields.iteritems():
|
||||
self.fields[k.replace('-', '_')] = v
|
||||
|
||||
def __getattr__(self, name):
|
||||
try:
|
||||
return self.fields[name]
|
||||
except KeyError, e:
|
||||
raise AttributeError('No attribute %s' % name)
|
||||
|
||||
def __str__(self):
|
||||
text = ''
|
||||
for k, v in self.fields.iteritems():
|
||||
text += "% 32s: %s\n" % (k[-32:], v)
|
||||
return text
|
||||
|
||||
class Client(object):
|
||||
"""
|
||||
This is it. This class implements the json-RPC protocol to communicate with Transmission.
|
||||
"""
|
||||
|
||||
def __init__(self, address='localhost', port=DEFAULT_PORT, user=None, password=None):
|
||||
base_url = 'http://' + address + ':' + str(port)
|
||||
self.url = base_url + '/transmission/rpc'
|
||||
if user and password:
|
||||
password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
|
||||
password_manager.add_password(realm=None, uri=self.url, user=user, passwd=password)
|
||||
opener = urllib2.build_opener(
|
||||
urllib2.HTTPBasicAuthHandler(password_manager)
|
||||
, urllib2.HTTPDigestAuthHandler(password_manager)
|
||||
)
|
||||
urllib2.install_opener(opener)
|
||||
elif user or password:
|
||||
logger.warning('Either user or password missing, not using authentication.')
|
||||
self._sequence = 0
|
||||
self.session = Session()
|
||||
self.sessionid = 0
|
||||
self.protocol_version = None
|
||||
self.get_session()
|
||||
self.torrent_get_arguments = get_arguments('torrent-get'
|
||||
, self.rpc_version)
|
||||
|
||||
def _debug_request(self, request):
|
||||
logger.debug(
|
||||
json.dumps(
|
||||
{
|
||||
'request': {
|
||||
'url': request.get_full_url(),
|
||||
'request-headers': dict(request.header_items()),
|
||||
'request-data': json.loads(request.data),
|
||||
}
|
||||
},
|
||||
indent=2
|
||||
)
|
||||
)
|
||||
|
||||
def _debug_response(self, response, response_data):
|
||||
try:
|
||||
response_data = json.loads(response_data)
|
||||
except:
|
||||
pass
|
||||
logger.debug(
|
||||
json.dumps(
|
||||
{
|
||||
'response': {
|
||||
'url': response.url,
|
||||
'code': response.code,
|
||||
'msg': response.msg,
|
||||
'headers': dict(response.headers),
|
||||
'data': response_data,
|
||||
}
|
||||
},
|
||||
indent=2
|
||||
)
|
||||
)
|
||||
|
||||
def _http_query(self, query):
|
||||
headers = {'X-Transmission-Session-Id': self.sessionid}
|
||||
request = urllib2.Request(self.url, query, headers)
|
||||
request_count = 0
|
||||
while True:
|
||||
error_data = ""
|
||||
try:
|
||||
self._debug_request(request)
|
||||
socket.setdefaulttimeout(10)
|
||||
if (sys.version_info[0] == 2 and sys.version_info[1] > 5) or sys.version_info[0] > 2:
|
||||
response = urllib2.urlopen(request, timeout=60)
|
||||
else:
|
||||
response = urllib2.urlopen(request)
|
||||
break
|
||||
except urllib2.HTTPError, error:
|
||||
error_data = error.read()
|
||||
if error.code == 409:
|
||||
logger.info('Server responded with 409, trying to set session-id.')
|
||||
if request_count > 1:
|
||||
raise TransmissionError('Session ID negotiation failed.', error)
|
||||
if 'X-Transmission-Session-Id' in error.headers:
|
||||
self.sessionid = error.headers['X-Transmission-Session-Id']
|
||||
request.add_header('X-Transmission-Session-Id', self.sessionid)
|
||||
else:
|
||||
raise TransmissionError('Unknown conflict.', error)
|
||||
except urllib2.URLError, error:
|
||||
raise TransmissionError('Failed to connect to daemon.', error)
|
||||
except httplib.BadStatusLine, error:
|
||||
if (request_count > 1):
|
||||
raise TransmissionError('Failed to request %s "%s".' % (self.url, query), error)
|
||||
finally:
|
||||
if error_data:
|
||||
self._debug_response(error, error_data)
|
||||
request_count = request_count + 1
|
||||
result = response.read()
|
||||
self._debug_response(response, result)
|
||||
return result
|
||||
|
||||
def _request(self, method, arguments={}, ids=[], require_ids = False):
|
||||
"""Send json-rpc request to Transmission using http POST"""
|
||||
|
||||
if not isinstance(method, (str, unicode)):
|
||||
raise ValueError('request takes method as string')
|
||||
if not isinstance(arguments, dict):
|
||||
raise ValueError('request takes arguments as dict')
|
||||
ids = self._format_ids(ids)
|
||||
if len(ids) > 0:
|
||||
arguments['ids'] = ids
|
||||
elif require_ids:
|
||||
raise ValueError('request require ids')
|
||||
|
||||
query = json.dumps({'tag': self._sequence, 'method': method
|
||||
, 'arguments': arguments})
|
||||
logger.info(query)
|
||||
self._sequence += 1
|
||||
start = time.time()
|
||||
http_data = self._http_query(query)
|
||||
elapsed = time.time() - start
|
||||
logger.info('http request took %.3f s' % (elapsed))
|
||||
|
||||
try:
|
||||
data = json.loads(http_data)
|
||||
except ValueError, e:
|
||||
logger.error('Error: ' + str(e))
|
||||
logger.error('Request: \"%s\"' % (query))
|
||||
logger.error('HTTP data: \"%s\"' % (http_data))
|
||||
raise
|
||||
|
||||
logger.info(json.dumps(data, indent=2))
|
||||
|
||||
if data['result'] != 'success':
|
||||
raise TransmissionError('Query failed with result \"%s\"'
|
||||
% data['result'])
|
||||
|
||||
results = {}
|
||||
if method == 'torrent-get':
|
||||
for item in data['arguments']['torrents']:
|
||||
results[item['id']] = Torrent(item)
|
||||
if self.protocol_version == 2 and 'peers' not in item:
|
||||
self.protocol_version = 1
|
||||
elif method == 'torrent-add':
|
||||
item = data['arguments']['torrent-added']
|
||||
results[item['id']] = Torrent(item)
|
||||
elif method == 'session-get':
|
||||
self._update_session(data['arguments'])
|
||||
elif method == 'session-stats':
|
||||
# older versions of T has the return data in "session-stats"
|
||||
if 'session-stats' in data['arguments']:
|
||||
self._update_session(data['arguments']['session-stats'])
|
||||
else:
|
||||
self._update_session(data['arguments'])
|
||||
elif method in ('port-test', 'blocklist-update'):
|
||||
results = data['arguments']
|
||||
else:
|
||||
return None
|
||||
|
||||
return results
|
||||
|
||||
def _format_ids(self, args):
|
||||
"""Take things and make them valid torrent identifiers"""
|
||||
ids = []
|
||||
|
||||
if isinstance(args, (int, long)):
|
||||
ids.append(args)
|
||||
elif isinstance(args, (str, unicode)):
|
||||
for item in re.split(u'[ ,]+', args):
|
||||
if len(item) == 0:
|
||||
continue
|
||||
addition = None
|
||||
try:
|
||||
# handle index
|
||||
addition = [int(item)]
|
||||
except ValueError:
|
||||
pass
|
||||
if not addition:
|
||||
# handle hashes
|
||||
try:
|
||||
int(item, 16)
|
||||
addition = [item]
|
||||
except:
|
||||
pass
|
||||
if not addition:
|
||||
# handle index ranges i.e. 5:10
|
||||
match = re.match(u'^(\d+):(\d+)$', item)
|
||||
if match:
|
||||
try:
|
||||
idx_from = int(match.group(1))
|
||||
idx_to = int(match.group(2))
|
||||
addition = range(idx_from, idx_to + 1)
|
||||
except:
|
||||
pass
|
||||
if not addition:
|
||||
raise ValueError(u'Invalid torrent id, \"%s\"' % item)
|
||||
ids.extend(addition)
|
||||
elif isinstance(args, (list)):
|
||||
for item in args:
|
||||
ids.extend(self._format_ids(item))
|
||||
else:
|
||||
raise ValueError(u'Invalid torrent id')
|
||||
return ids
|
||||
|
||||
def _update_session(self, data):
|
||||
self.session.update(data)
|
||||
|
||||
@property
|
||||
def rpc_version(self):
|
||||
if self.protocol_version == None:
|
||||
if hasattr(self.session, 'rpc_version'):
|
||||
self.protocol_version = self.session.rpc_version
|
||||
elif hasattr(self.session, 'version'):
|
||||
self.protocol_version = 3
|
||||
else:
|
||||
self.protocol_version = 2
|
||||
return self.protocol_version
|
||||
|
||||
def _rpc_version_warning(self, version):
|
||||
if self.rpc_version < version:
|
||||
logger.warning('Using feature not supported by server. RPC version for server %d, feature introduced in %d.' % (self.rpc_version, version))
|
||||
|
||||
def add(self, data, **kwargs):
|
||||
"""
|
||||
Add torrent to transfers list. Takes a base64 encoded .torrent file in data.
|
||||
Additional arguments are:
|
||||
|
||||
* `paused`, boolean, Whether to pause the transfer on add.
|
||||
* `download_dir`, path, The directory where the downloaded
|
||||
contents will be saved in.
|
||||
* `peer_limit`, number, Limits the number of peers for this
|
||||
transfer.
|
||||
* `files_unwanted`,
|
||||
* `files_wanted`,
|
||||
* `priority_high`,
|
||||
* `priority_low`,
|
||||
* `priority_normal`,
|
||||
"""
|
||||
args = {'metainfo': data}
|
||||
for key, value in kwargs.iteritems():
|
||||
argument = make_rpc_name(key)
|
||||
(arg, val) = argument_value_convert('torrent-add',
|
||||
argument, value, self.rpc_version)
|
||||
args[arg] = val
|
||||
return self._request('torrent-add', args)
|
||||
|
||||
def add_url(self, torrent_url, **kwargs):
|
||||
"""
|
||||
Add torrent to transfers list. Takes a url to a .torrent file.
|
||||
Additional arguments are:
|
||||
|
||||
* `paused`, boolean, Whether to pause the transfer on add.
|
||||
* `download_dir`, path, The directory where the downloaded
|
||||
contents will be saved in.
|
||||
* `peer_limit`, number, Limits the number of peers for this
|
||||
transfer.
|
||||
* `files_unwanted`,
|
||||
* `files_wanted`,
|
||||
* `priority_high`,
|
||||
* `priority_low`,
|
||||
* `priority_normal`,
|
||||
"""
|
||||
torrent_file = None
|
||||
if os.path.exists(torrent_url):
|
||||
torrent_file = open(torrent_url, 'r')
|
||||
else:
|
||||
try:
|
||||
torrent_file = urllib2.urlopen(torrent_url)
|
||||
except:
|
||||
torrent_file = None
|
||||
|
||||
if not torrent_file:
|
||||
raise TransmissionError('File does not exist.')
|
||||
|
||||
torrent_data = base64.b64encode(torrent_file.read())
|
||||
return self.add(torrent_data, **kwargs)
|
||||
|
||||
def remove(self, ids, delete_data=False):
|
||||
"""
|
||||
remove torrent(s) with provided id(s). Local data is removed if
|
||||
delete_data is True, otherwise not.
|
||||
"""
|
||||
self._rpc_version_warning(3)
|
||||
self._request('torrent-remove',
|
||||
{'delete-local-data':rpc_bool(delete_data)}, ids, True)
|
||||
|
||||
def start(self, ids):
|
||||
"""start torrent(s) with provided id(s)"""
|
||||
self._request('torrent-start', {}, ids, True)
|
||||
|
||||
def stop(self, ids):
|
||||
"""stop torrent(s) with provided id(s)"""
|
||||
self._request('torrent-stop', {}, ids, True)
|
||||
|
||||
def verify(self, ids):
|
||||
"""verify torrent(s) with provided id(s)"""
|
||||
self._request('torrent-verify', {}, ids, True)
|
||||
|
||||
def reannounce(self, ids):
|
||||
"""reannounce torrent(s) with provided id(s)"""
|
||||
self._rpc_version_warning(5)
|
||||
self._request('torrent-reannounce', {}, ids, True)
|
||||
|
||||
def info(self, ids=[], arguments={}):
|
||||
"""Get detailed information for torrent(s) with provided id(s)."""
|
||||
if not arguments:
|
||||
arguments = self.torrent_get_arguments
|
||||
return self._request('torrent-get', {'fields': arguments}, ids)
|
||||
|
||||
def get_files(self, ids=[]):
|
||||
"""
|
||||
Get list of files for provided torrent id(s).
|
||||
This function returns a dictonary for each requested torrent id holding
|
||||
the information about the files.
|
||||
"""
|
||||
fields = ['id', 'name', 'hashString', 'files', 'priorities', 'wanted']
|
||||
request_result = self._request('torrent-get', {'fields': fields}, ids)
|
||||
result = {}
|
||||
for id, torrent in request_result.iteritems():
|
||||
result[id] = torrent.files()
|
||||
return result
|
||||
|
||||
def set_files(self, items):
|
||||
"""
|
||||
Set file properties. Takes a dictonary with similar contents as the
|
||||
result of get_files.
|
||||
"""
|
||||
if not isinstance(items, dict):
|
||||
raise ValueError('Invalid file description')
|
||||
for tid, files in items.iteritems():
|
||||
if not isinstance(files, dict):
|
||||
continue
|
||||
wanted = []
|
||||
unwanted = []
|
||||
priority_high = []
|
||||
priority_normal = []
|
||||
priority_low = []
|
||||
for fid, file in files.iteritems():
|
||||
if not isinstance(file, dict):
|
||||
continue
|
||||
if 'selected' in file and file['selected']:
|
||||
wanted.append(fid)
|
||||
else:
|
||||
unwanted.append(fid)
|
||||
if 'priority' in file:
|
||||
if file['priority'] == 'high':
|
||||
priority_high.append(fid)
|
||||
elif file['priority'] == 'normal':
|
||||
priority_normal.append(fid)
|
||||
elif file['priority'] == 'low':
|
||||
priority_low.append(fid)
|
||||
self.change([tid], files_wanted = wanted
|
||||
, files_unwanted = unwanted
|
||||
, priority_high = priority_high
|
||||
, priority_normal = priority_normal
|
||||
, priority_low = priority_low)
|
||||
|
||||
def list(self):
|
||||
"""list all torrents"""
|
||||
fields = ['id', 'hashString', 'name', 'sizeWhenDone', 'leftUntilDone'
|
||||
, 'eta', 'status', 'rateUpload', 'rateDownload', 'uploadedEver'
|
||||
, 'downloadedEver']
|
||||
return self._request('torrent-get', {'fields': fields})
|
||||
|
||||
def change(self, ids, **kwargs):
|
||||
"""
|
||||
Change torrent parameters. This is the list of parameters that.
|
||||
"""
|
||||
args = {}
|
||||
for key, value in kwargs.iteritems():
|
||||
argument = make_rpc_name(key)
|
||||
(arg, val) = argument_value_convert('torrent-set'
|
||||
, argument, value, self.rpc_version)
|
||||
args[arg] = val
|
||||
|
||||
if len(args) > 0:
|
||||
self._request('torrent-set', args, ids, True)
|
||||
else:
|
||||
ValueError("No arguments to set")
|
||||
|
||||
def get_session(self):
|
||||
"""Get session parameters"""
|
||||
self._request('session-get')
|
||||
return self.session
|
||||
|
||||
def set_session(self, **kwargs):
|
||||
"""Set session parameters"""
|
||||
args = {}
|
||||
for key, value in kwargs.iteritems():
|
||||
if key == 'encryption' and value not in ['required', 'preferred', 'tolerated']:
|
||||
raise ValueError('Invalid encryption value')
|
||||
argument = make_rpc_name(key)
|
||||
(arg, val) = argument_value_convert('session-set'
|
||||
, argument, value, self.rpc_version)
|
||||
args[arg] = val
|
||||
if len(args) > 0:
|
||||
self._request('session-set', args)
|
||||
|
||||
def blocklist_update(self):
|
||||
"""Update block list. Returns the size of the block list."""
|
||||
self._rpc_version_warning(5)
|
||||
result = self._request('blocklist-update')
|
||||
if 'blocklist-size' in result:
|
||||
return result['blocklist-size']
|
||||
return None
|
||||
|
||||
def port_test(self):
|
||||
"""
|
||||
Tests to see if your incoming peer port is accessible from the
|
||||
outside world.
|
||||
"""
|
||||
self._rpc_version_warning(5)
|
||||
result = self._request('port-test')
|
||||
if 'port-is-open' in result:
|
||||
return result['port-is-open']
|
||||
return None
|
||||
|
||||
def session_stats(self):
|
||||
"""Get session statistics"""
|
||||
self._request('session-stats')
|
||||
return self.session
|
|
@ -1,135 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# 2008-07, Erik Svensson <erik.public@gmail.com>
|
||||
|
||||
import socket, datetime
|
||||
import constants
|
||||
from constants import logger
|
||||
|
||||
UNITS = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB']
|
||||
|
||||
def format_size(size):
|
||||
s = float(size)
|
||||
i = 0
|
||||
while size >= 1024.0 and i < len(UNITS):
|
||||
i += 1
|
||||
size /= 1024.0
|
||||
return (size, UNITS[i])
|
||||
|
||||
def format_speed(size):
|
||||
(size, unit) = format_size(size)
|
||||
return (size, unit + '/s')
|
||||
|
||||
def format_timedelta(delta):
|
||||
minutes, seconds = divmod(delta.seconds, 60)
|
||||
hours, minutes = divmod(minutes, 60)
|
||||
return '%d %02d:%02d:%02d' % (delta.days, hours, minutes, seconds)
|
||||
|
||||
def format_timestamp(timestamp):
|
||||
if timestamp > 0:
|
||||
dt = datetime.datetime.fromtimestamp(timestamp)
|
||||
return dt.isoformat(' ')
|
||||
else:
|
||||
return '-'
|
||||
|
||||
class INetAddressError(Exception):
|
||||
pass
|
||||
|
||||
def inet_address(address, default_port, default_address='localhost'):
|
||||
addr = address.split(':')
|
||||
if len(addr) == 1:
|
||||
try:
|
||||
port = int(addr[0])
|
||||
addr = default_address
|
||||
except:
|
||||
addr = addr[0]
|
||||
port = default_port
|
||||
elif len(addr) == 2:
|
||||
port = int(addr[1])
|
||||
if len(addr[0]) == 0:
|
||||
addr = default_address
|
||||
else:
|
||||
addr = addr[0]
|
||||
else:
|
||||
addr = default_address
|
||||
port = default_port
|
||||
try:
|
||||
socket.getaddrinfo(addr, port, socket.AF_INET, socket.SOCK_STREAM)
|
||||
except socket.gaierror, e:
|
||||
raise INetAddressError('Cannot look up address "%s".' % address)
|
||||
return (addr, port)
|
||||
|
||||
def rpc_bool(arg):
|
||||
if isinstance(arg, (str, unicode)):
|
||||
try:
|
||||
arg = bool(int(arg))
|
||||
except:
|
||||
arg = arg.lower() in [u'true', u'yes']
|
||||
return 1 if bool(arg) else 0
|
||||
|
||||
TR_TYPE_MAP = {
|
||||
'number' : int,
|
||||
'string' : str,
|
||||
'double': float,
|
||||
'boolean' : rpc_bool,
|
||||
'array': list,
|
||||
'object': dict
|
||||
}
|
||||
|
||||
def make_python_name(name):
|
||||
return name.replace('-', '_')
|
||||
|
||||
def make_rpc_name(name):
|
||||
return name.replace('_', '-')
|
||||
|
||||
def argument_value_convert(method, argument, value, rpc_version):
|
||||
if method in ('torrent-add', 'torrent-get', 'torrent-set'):
|
||||
args = constants.TORRENT_ARGS[method[-3:]]
|
||||
elif method in ('session-get', 'session-set'):
|
||||
args = constants.SESSION_ARGS[method[-3:]]
|
||||
else:
|
||||
return ValueError('Method "%s" not supported' % (method))
|
||||
if argument in args:
|
||||
info = args[argument]
|
||||
invalid_version = True
|
||||
while invalid_version:
|
||||
invalid_version = False
|
||||
replacement = None
|
||||
if rpc_version < info[1]:
|
||||
invalid_version = True
|
||||
replacement = info[3]
|
||||
if info[2] and info[2] <= rpc_version:
|
||||
invalid_version = True
|
||||
replacement = info[4]
|
||||
if invalid_version:
|
||||
if replacement:
|
||||
logger.warning(
|
||||
'Replacing requested argument "%s" with "%s".'
|
||||
% (argument, replacement))
|
||||
argument = replacement
|
||||
info = args[argument]
|
||||
else:
|
||||
raise ValueError(
|
||||
'Method "%s" Argument "%s" does not exist in version %d.'
|
||||
% (method, argument, rpc_version))
|
||||
return (argument, TR_TYPE_MAP[info[0]](value))
|
||||
else:
|
||||
raise ValueError('Argument "%s" does not exists for method "%s".',
|
||||
(argument, method))
|
||||
|
||||
def get_arguments(method, rpc_version):
|
||||
if method in ('torrent-add', 'torrent-get', 'torrent-set'):
|
||||
args = constants.TORRENT_ARGS[method[-3:]]
|
||||
elif method in ('session-get', 'session-set'):
|
||||
args = constants.SESSION_ARGS[method[-3:]]
|
||||
else:
|
||||
return ValueError('Method "%s" not supported' % (method))
|
||||
accessible = []
|
||||
for argument, info in args.iteritems():
|
||||
valid_version = True
|
||||
if rpc_version < info[1]:
|
||||
valid_version = False
|
||||
if info[2] and info[2] <= rpc_version:
|
||||
valid_version = False
|
||||
if valid_version:
|
||||
accessible.append(argument)
|
||||
return accessible
|
|
@ -1,10 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# 2008-08, Erik Svensson <erik.public@gmail.com>
|
||||
|
||||
from constants import *
|
||||
from transmission import TransmissionError, Torrent, Session, Client
|
||||
|
||||
__author__ = u'Erik Svensson <erik.public@gmail.com>'
|
||||
__version__ = u'0.4'
|
||||
__copyright__ = u'Copyright (c) 2009 Erik Svensson'
|
||||
__license__ = u'MIT'
|
|
@ -1,234 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# 2008-07, Erik Svensson <erik.public@gmail.com>
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger('transmissionrpc')
|
||||
logger.setLevel(logging.ERROR)
|
||||
|
||||
def mirror_dict(d):
|
||||
d.update(dict((v, k) for k, v in d.iteritems()))
|
||||
return d
|
||||
|
||||
DEFAULT_PORT = 9091
|
||||
|
||||
DEFAULT_TIMEOUT = 30
|
||||
|
||||
TR_STATUS_CHECK_WAIT = (1<<0)
|
||||
TR_STATUS_CHECK = (1<<1)
|
||||
TR_STATUS_DOWNLOAD = (1<<2)
|
||||
TR_STATUS_SEED = (1<<3)
|
||||
TR_STATUS_STOPPED = (1<<4)
|
||||
|
||||
STATUS = mirror_dict({
|
||||
'check pending' : TR_STATUS_CHECK_WAIT,
|
||||
'checking' : TR_STATUS_CHECK,
|
||||
'downloading' : TR_STATUS_DOWNLOAD,
|
||||
'seeding' : TR_STATUS_SEED,
|
||||
'stopped' : TR_STATUS_STOPPED,
|
||||
})
|
||||
|
||||
TR_PRI_LOW = -1
|
||||
TR_PRI_NORMAL = 0
|
||||
TR_PRI_HIGH = 1
|
||||
|
||||
PRIORITY = mirror_dict({
|
||||
'low' : TR_PRI_LOW,
|
||||
'normal' : TR_PRI_NORMAL,
|
||||
'high' : TR_PRI_HIGH
|
||||
})
|
||||
|
||||
TR_RATIOLIMIT_GLOBAL = 0 # follow the global settings
|
||||
TR_RATIOLIMIT_SINGLE = 1 # override the global settings, seeding until a certain ratio
|
||||
TR_RATIOLIMIT_UNLIMITED = 2 # override the global settings, seeding regardless of ratio
|
||||
|
||||
RATIO_LIMIT = mirror_dict({
|
||||
'global' : TR_RATIOLIMIT_GLOBAL,
|
||||
'single' : TR_RATIOLIMIT_SINGLE,
|
||||
'unlimeted' : TR_RATIOLIMIT_UNLIMITED
|
||||
})
|
||||
|
||||
# A note on argument maps
|
||||
# These maps are used to verify *-set methods. The information is structured in
|
||||
# a tree.
|
||||
# set +- <argument1> - [<type>, <added version>, <removed version>, <previous argument name>, <next argument name>]
|
||||
# | +- <argument2> - [<type>, <added version>, <removed version>, <previous argument name>, <next argument name>]
|
||||
# |
|
||||
# get +- <argument1> - [<type>, <added version>, <removed version>, <previous argument name>, <next argument name>]
|
||||
# +- <argument2> - [<type>, <added version>, <removed version>, <previous argument name>, <next argument name>]
|
||||
|
||||
# Arguments for torrent methods
|
||||
TORRENT_ARGS = {
|
||||
'get' : {
|
||||
'activityDate': ('number', 1, None, None, None),
|
||||
'addedDate': ('number', 1, None, None, None),
|
||||
'announceResponse': ('string', 1, None, None, None),
|
||||
'announceURL': ('string', 1, None, None, None),
|
||||
'bandwidthPriority': ('number', 5, None, None, None),
|
||||
'comment': ('string', 1, None, None, None),
|
||||
'corruptEver': ('number', 1, None, None, None),
|
||||
'creator': ('string', 1, None, None, None),
|
||||
'dateCreated': ('number', 1, None, None, None),
|
||||
'desiredAvailable': ('number', 1, None, None, None),
|
||||
'doneDate': ('number', 1, None, None, None),
|
||||
'downloadDir': ('string', 4, None, None, None),
|
||||
'downloadedEver': ('number', 1, None, None, None),
|
||||
'downloaders': ('number', 4, None, None, None),
|
||||
'downloadLimit': ('number', 1, None, None, None),
|
||||
'downloadLimited': ('boolean', 5, None, None, None),
|
||||
'downloadLimitMode': ('number', 1, 5, None, None),
|
||||
'error': ('number', 1, None, None, None),
|
||||
'errorString': ('number', 1, None, None, None),
|
||||
'eta': ('number', 1, None, None, None),
|
||||
'files': ('array', 1, None, None, None),
|
||||
'fileStats': ('array', 5, None, None, None),
|
||||
'hashString': ('string', 1, None, None, None),
|
||||
'haveUnchecked': ('number', 1, None, None, None),
|
||||
'haveValid': ('number', 1, None, None, None),
|
||||
'honorsSessionLimits': ('boolean', 5, None, None, None),
|
||||
'id': ('number', 1, None, None, None),
|
||||
'isPrivate': ('boolean', 1, None, None, None),
|
||||
'lastAnnounceTime': ('number', 1, None, None, None),
|
||||
'lastScrapeTime': ('number', 1, None, None, None),
|
||||
'leechers': ('number', 1, None, None, None),
|
||||
'leftUntilDone': ('number', 1, None, None, None),
|
||||
'manualAnnounceTime': ('number', 1, None, None, None),
|
||||
'maxConnectedPeers': ('number', 1, None, None, None),
|
||||
'name': ('string', 1, None, None, None),
|
||||
'nextAnnounceTime': ('number', 1, None, None, None),
|
||||
'nextScrapeTime': ('number', 1, None, None, None),
|
||||
'peer-limit': ('number', 5, None, None, None),
|
||||
'peers': ('array', 2, None, None, None),
|
||||
'peersConnected': ('number', 1, None, None, None),
|
||||
'peersFrom': ('object', 1, None, None, None),
|
||||
'peersGettingFromUs': ('number', 1, None, None, None),
|
||||
'peersKnown': ('number', 1, None, None, None),
|
||||
'peersSendingToUs': ('number', 1, None, None, None),
|
||||
'percentDone': ('double', 5, None, None, None),
|
||||
'pieces': ('string', 5, None, None, None),
|
||||
'pieceCount': ('number', 1, None, None, None),
|
||||
'pieceSize': ('number', 1, None, None, None),
|
||||
'priorities': ('array', 1, None, None, None),
|
||||
'rateDownload': ('number', 1, None, None, None),
|
||||
'rateUpload': ('number', 1, None, None, None),
|
||||
'recheckProgress': ('double', 1, None, None, None),
|
||||
'scrapeResponse': ('string', 1, None, None, None),
|
||||
'scrapeURL': ('string', 1, None, None, None),
|
||||
'seeders': ('number', 1, None, None, None),
|
||||
'seedRatioLimit': ('double', 5, None, None, None),
|
||||
'seedRatioMode': ('number', 5, None, None, None),
|
||||
'sizeWhenDone': ('number', 1, None, None, None),
|
||||
'startDate': ('number', 1, None, None, None),
|
||||
'status': ('number', 1, None, None, None),
|
||||
'swarmSpeed': ('number', 1, None, None, None),
|
||||
'timesCompleted': ('number', 1, None, None, None),
|
||||
'trackers': ('array', 1, None, None, None),
|
||||
'totalSize': ('number', 1, None, None, None),
|
||||
'torrentFile': ('string', 5, None, None, None),
|
||||
'uploadedEver': ('number', 1, None, None, None),
|
||||
'uploadLimit': ('number', 1, None, None, None),
|
||||
'uploadLimitMode': ('number', 1, 5, None, None),
|
||||
'uploadLimited': ('boolean', 5, None, None, None),
|
||||
'uploadRatio': ('double', 1, None, None, None),
|
||||
'wanted': ('array', 1, None, None, None),
|
||||
'webseeds': ('array', 1, None, None, None),
|
||||
'webseedsSendingToUs': ('number', 1, None, None, None),
|
||||
},
|
||||
'set': {
|
||||
'bandwidthPriority': ('number', 5, None, None, None),
|
||||
'downloadLimit': ('number', 5, None, 'speed-limit-down', None),
|
||||
'downloadLimited': ('boolean', 5, None, 'speed-limit-down-enabled', None),
|
||||
'files-wanted': ('array', 1, None, None, None),
|
||||
'files-unwanted': ('array', 1, None, None, None),
|
||||
'honorsSessionLimits': ('boolean', 5, None, None, None),
|
||||
'ids': ('array', 1, None, None, None),
|
||||
'peer-limit': ('number', 1, None, None, None),
|
||||
'priority-high': ('array', 1, None, None, None),
|
||||
'priority-low': ('array', 1, None, None, None),
|
||||
'priority-normal': ('array', 1, None, None, None),
|
||||
'seedRatioLimit': ('double', 5, None, None, None),
|
||||
'seedRatioMode': ('number', 5, None, None, None),
|
||||
'speed-limit-down': ('number', 1, 5, None, 'downloadLimit'),
|
||||
'speed-limit-down-enabled': ('boolean', 1, 5, None, 'downloadLimited'),
|
||||
'speed-limit-up': ('number', 1, 5, None, 'uploadLimit'),
|
||||
'speed-limit-up-enabled': ('boolean', 1, 5, None, 'uploadLimited'),
|
||||
'uploadLimit': ('number', 5, None, 'speed-limit-up', None),
|
||||
'uploadLimited': ('boolean', 5, None, 'speed-limit-up-enabled', None),
|
||||
},
|
||||
'add': {
|
||||
'download-dir': ('string', 1, None, None, None),
|
||||
'filename': ('string', 1, None, None, None),
|
||||
'files-wanted': ('array', 1, None, None, None),
|
||||
'files-unwanted': ('array', 1, None, None, None),
|
||||
'metainfo': ('string', 1, None, None, None),
|
||||
'paused': ('boolean', 1, None, None, None),
|
||||
'peer-limit': ('number', 1, None, None, None),
|
||||
'priority-high': ('array', 1, None, None, None),
|
||||
'priority-low': ('array', 1, None, None, None),
|
||||
'priority-normal': ('array', 1, None, None, None),
|
||||
}
|
||||
}
|
||||
|
||||
# Arguments for session methods
|
||||
SESSION_ARGS = {
|
||||
'get': {
|
||||
"alt-speed-down": ('number', 5, None, None, None),
|
||||
"alt-speed-enabled": ('boolean', 5, None, None, None),
|
||||
"alt-speed-time-begin": ('number', 5, None, None, None),
|
||||
"alt-speed-time-enabled": ('boolean', 5, None, None, None),
|
||||
"alt-speed-time-end": ('number', 5, None, None, None),
|
||||
"alt-speed-time-day": ('number', 5, None, None, None),
|
||||
"alt-speed-up": ('number', 5, None, None, None),
|
||||
"blocklist-enabled": ('boolean', 5, None, None, None),
|
||||
"blocklist-size": ('number', 5, None, None, None),
|
||||
"dht-enabled": ('boolean', 6, None, None, None),
|
||||
"download-dir": ('string', 1, None, None, None),
|
||||
"encryption": ('string', 1, None, None, None),
|
||||
"peer-limit": ('number', 1, 5, None, None),
|
||||
"peer-limit-global": ('number', 5, None, None, None),
|
||||
"peer-limit-per-torrent": ('number', 5, None, None, None),
|
||||
"pex-allowed": ('boolean', 1, 5, None, None),
|
||||
"pex-enabled": ('boolean', 5, None, None, None),
|
||||
"port": ('number', 1, 5, None, None),
|
||||
"peer-port": ('number', 5, None, None, None),
|
||||
"peer-port-random-on-start": ('boolean', 5, None, None, None),
|
||||
"port-forwarding-enabled": ('boolean', 1, None, None, None),
|
||||
"rpc-version": ('number', 4, None, None, None),
|
||||
"rpc-version-minimum": ('number', 4, None, None, None),
|
||||
"seedRatioLimit": ('double', 5, None, None, None),
|
||||
"seedRatioLimited": ('boolean', 5, None, None, None),
|
||||
"speed-limit-down": ('number', 1, None, None, None),
|
||||
"speed-limit-down-enabled": ('boolean', 1, None, None, None),
|
||||
"speed-limit-up": ('number', 1, None, None, None),
|
||||
"speed-limit-up-enabled": ('boolean', 1, None, None, None),
|
||||
"version": ('string', 3, None, None, None),
|
||||
},
|
||||
'set': {
|
||||
"alt-speed-down": ('number', 5, None, None, None),
|
||||
"alt-speed-enabled": ('boolean', 5, None, None, None),
|
||||
"alt-speed-time-begin": ('number', 5, None, None, None),
|
||||
"alt-speed-time-enabled": ('boolean', 5, None, None, None),
|
||||
"alt-speed-time-end": ('number', 5, None, None, None),
|
||||
"alt-speed-time-day": ('number', 5, None, None, None),
|
||||
"alt-speed-up": ('number', 5, None, None, None),
|
||||
"blocklist-enabled": ('boolean', 5, None, None, None),
|
||||
"dht-enabled": ('boolean', 6, None, None, None),
|
||||
"download-dir": ('string', 1, None, None, None),
|
||||
"encryption": ('string', 1, None, None, None),
|
||||
"peer-limit": ('number', 1, 5, None, 'peer-limit-global'),
|
||||
"peer-limit-global": ('number', 5, None, 'peer-limit', None),
|
||||
"peer-limit-per-torrent": ('number', 5, None, None, None),
|
||||
"pex-allowed": ('boolean', 1, 5, None, 'pex-enabled'),
|
||||
"pex-enabled": ('boolean', 5, None, 'pex-allowed', None),
|
||||
"port": ('number', 1, 5, None, 'peer-port'),
|
||||
"peer-port": ('number', 5, None, 'port', None),
|
||||
"peer-port-random-on-start": ('boolean', 5, None, None, None),
|
||||
"port-forwarding-enabled": ('boolean', 1, None, None, None),
|
||||
"seedRatioLimit": ('double', 5, None, None, None),
|
||||
"seedRatioLimited": ('boolean', 5, None, None, None),
|
||||
"speed-limit-down": ('number', 1, None, None, None),
|
||||
"speed-limit-down-enabled": ('boolean', 1, None, None, None),
|
||||
"speed-limit-up": ('number', 1, None, None, None),
|
||||
"speed-limit-up-enabled": ('boolean', 1, None, None, None),
|
||||
},
|
||||
}
|
|
@ -1,628 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# 2008-07, Erik Svensson <erik.public@gmail.com>
|
||||
|
||||
import sys, os, time, datetime
|
||||
import re
|
||||
import httplib, urllib2, base64, socket
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
from constants import *
|
||||
from utils import *
|
||||
|
||||
class TransmissionError(Exception):
|
||||
def __init__(self, message='', original=None):
|
||||
Exception.__init__(self, message)
|
||||
self.message = message
|
||||
self.original = original
|
||||
|
||||
def __str__(self):
|
||||
if self.original:
|
||||
original_name = type(self.original).__name__
|
||||
return '%s Original exception: %s, "%s"' % (self.message, original_name, self.original.args)
|
||||
else:
|
||||
return self.args
|
||||
|
||||
class Torrent(object):
|
||||
"""
|
||||
Torrent is a class holding the data raceived from Transmission regarding a bittorrent transfer.
|
||||
All fetched torrent fields are accessable through this class using attributes.
|
||||
This class has a few convenience properties using the torrent data.
|
||||
"""
|
||||
|
||||
def __init__(self, fields):
|
||||
if 'id' not in fields:
|
||||
raise ValueError('Torrent requires an id')
|
||||
self.fields = {}
|
||||
self.update(fields)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Torrent %d \"%s\">' % (self.fields['id'], self.fields['name'])
|
||||
|
||||
def __str__(self):
|
||||
return 'torrent %s' % self.fields['name']
|
||||
|
||||
def update(self, other):
|
||||
"""Update the torrent data from a Transmission arguments dictinary"""
|
||||
fields = None
|
||||
if isinstance(other, dict):
|
||||
fields = other
|
||||
elif isinstance(other, Torrent):
|
||||
fields = other.fields
|
||||
else:
|
||||
raise ValueError('Cannot update with supplied data')
|
||||
for k, v in fields.iteritems():
|
||||
self.fields[k.replace('-', '_')] = v
|
||||
|
||||
def files(self):
|
||||
"""
|
||||
Get list of files for this torrent. This function returns a dictionary with file information for each file.
|
||||
"""
|
||||
result = {}
|
||||
if 'files' in self.fields:
|
||||
indicies = xrange(len(self.fields['files']))
|
||||
files = self.fields['files']
|
||||
priorities = self.fields['priorities']
|
||||
wanted = self.fields['wanted']
|
||||
index = 1
|
||||
for item in zip(indicies, files, priorities, wanted):
|
||||
selected = bool(item[3])
|
||||
priority = PRIORITY[item[2]]
|
||||
result[item[0]] = {
|
||||
'selected': selected,
|
||||
'priority': priority,
|
||||
'size': item[1]['length'],
|
||||
'name': item[1]['name'],
|
||||
'completed': item[1]['bytesCompleted']}
|
||||
return result
|
||||
|
||||
def __getattr__(self, name):
|
||||
try:
|
||||
return self.fields[name]
|
||||
except KeyError, e:
|
||||
raise AttributeError('No attribute %s' % name)
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
"""Get the status as string."""
|
||||
return STATUS[self.fields['status']]
|
||||
|
||||
@property
|
||||
def progress(self):
|
||||
"""Get the download progress in percent as float."""
|
||||
try:
|
||||
return 100.0 * (self.fields['sizeWhenDone'] - self.fields['leftUntilDone']) / float(self.fields['sizeWhenDone'])
|
||||
except ZeroDivisionError:
|
||||
return 0.0
|
||||
|
||||
@property
|
||||
def ratio(self):
|
||||
"""Get the upload/download ratio."""
|
||||
try:
|
||||
return self.fields['uploadedEver'] / float(self.fields['downloadedEver'])
|
||||
except ZeroDivisionError:
|
||||
return 0.0
|
||||
|
||||
@property
|
||||
def eta(self):
|
||||
"""Get the "eta" as datetime.timedelta."""
|
||||
eta = self.fields['eta']
|
||||
if eta >= 0:
|
||||
return datetime.timedelta(seconds=eta)
|
||||
else:
|
||||
ValueError('eta not valid')
|
||||
|
||||
@property
|
||||
def date_active(self):
|
||||
"""Get the attribute "activityDate" as datetime.datetime."""
|
||||
return datetime.datetime.fromtimestamp(self.fields['activityDate'])
|
||||
|
||||
@property
|
||||
def date_added(self):
|
||||
"""Get the attribute "addedDate" as datetime.datetime."""
|
||||
return datetime.datetime.fromtimestamp(self.fields['addedDate'])
|
||||
|
||||
@property
|
||||
def date_started(self):
|
||||
"""Get the attribute "startDate" as datetime.datetime."""
|
||||
return datetime.datetime.fromtimestamp(self.fields['startDate'])
|
||||
|
||||
@property
|
||||
def date_done(self):
|
||||
"""Get the attribute "doneDate" as datetime.datetime."""
|
||||
return datetime.datetime.fromtimestamp(self.fields['doneDate'])
|
||||
|
||||
def format_eta(self):
|
||||
"""Returns the attribute "eta" formatted as a string."""
|
||||
eta = self.fields['eta']
|
||||
if eta == -1:
|
||||
return 'not available'
|
||||
elif eta == -2:
|
||||
return 'unknown'
|
||||
else:
|
||||
return format_timedelta(self.eta)
|
||||
|
||||
class Session(object):
|
||||
"""
|
||||
Session is a class holding the session data for a Transmission daemon.
|
||||
|
||||
Access the session field can be done through attributes.
|
||||
The attributes available are the same as the session arguments in the
|
||||
Transmission RPC specification, but with underscore instead of hypen.
|
||||
``download-dir`` -> ``download_dir``.
|
||||
"""
|
||||
|
||||
def __init__(self, fields={}):
|
||||
self.fields = {}
|
||||
self.update(fields)
|
||||
|
||||
def update(self, other):
|
||||
"""Update the session data from a session arguments dictinary"""
|
||||
|
||||
fields = None
|
||||
if isinstance(other, dict):
|
||||
fields = other
|
||||
elif isinstance(other, Session):
|
||||
fields = other.fields
|
||||
else:
|
||||
raise ValueError('Cannot update with supplied data')
|
||||
|
||||
for k, v in fields.iteritems():
|
||||
self.fields[k.replace('-', '_')] = v
|
||||
|
||||
def __getattr__(self, name):
|
||||
try:
|
||||
return self.fields[name]
|
||||
except KeyError, e:
|
||||
raise AttributeError('No attribute %s' % name)
|
||||
|
||||
def __str__(self):
|
||||
text = ''
|
||||
for k, v in self.fields.iteritems():
|
||||
text += "% 32s: %s\n" % (k[-32:], v)
|
||||
return text
|
||||
|
||||
class Client(object):
|
||||
"""
|
||||
This is it. This class implements the json-RPC protocol to communicate with Transmission.
|
||||
"""
|
||||
|
||||
def __init__(self, address='localhost', port=DEFAULT_PORT, user=None, password=None):
|
||||
base_url = 'http://' + address + ':' + str(port)
|
||||
self.url = base_url + '/transmission/rpc'
|
||||
if user and password:
|
||||
password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
|
||||
password_manager.add_password(realm=None, uri=self.url, user=user, passwd=password)
|
||||
opener = urllib2.build_opener(
|
||||
urllib2.HTTPBasicAuthHandler(password_manager)
|
||||
, urllib2.HTTPDigestAuthHandler(password_manager)
|
||||
)
|
||||
urllib2.install_opener(opener)
|
||||
elif user or password:
|
||||
logger.warning('Either user or password missing, not using authentication.')
|
||||
self._sequence = 0
|
||||
self.session = Session()
|
||||
self.sessionid = 0
|
||||
self.protocol_version = None
|
||||
self.get_session()
|
||||
self.torrent_get_arguments = get_arguments('torrent-get'
|
||||
, self.rpc_version)
|
||||
|
||||
def _debug_request(self, request):
|
||||
logger.debug(
|
||||
json.dumps(
|
||||
{
|
||||
'request': {
|
||||
'url': request.get_full_url(),
|
||||
'request-headers': dict(request.header_items()),
|
||||
'request-data': json.loads(request.data),
|
||||
}
|
||||
},
|
||||
indent=2
|
||||
)
|
||||
)
|
||||
|
||||
def _debug_response(self, response, response_data):
|
||||
try:
|
||||
response_data = json.loads(response_data)
|
||||
except:
|
||||
pass
|
||||
logger.debug(
|
||||
json.dumps(
|
||||
{
|
||||
'response': {
|
||||
'url': response.url,
|
||||
'code': response.code,
|
||||
'msg': response.msg,
|
||||
'headers': dict(response.headers),
|
||||
'data': response_data,
|
||||
}
|
||||
},
|
||||
indent=2
|
||||
)
|
||||
)
|
||||
|
||||
def _http_query(self, query, timeout=DEFAULT_TIMEOUT):
|
||||
headers = {'X-Transmission-Session-Id': self.sessionid}
|
||||
request = urllib2.Request(self.url, query, headers)
|
||||
request_count = 0
|
||||
while True:
|
||||
error_data = ""
|
||||
try:
|
||||
try:
|
||||
self._debug_request(request)
|
||||
socket.setdefaulttimeout(timeout) # 30 seconds
|
||||
if (sys.version_info[0] == 2 and sys.version_info[1] > 5) or sys.version_info[0] > 2:
|
||||
response = urllib2.urlopen(request, timeout=timeout)
|
||||
else:
|
||||
response = urllib2.urlopen(request)
|
||||
break
|
||||
except urllib2.HTTPError, error:
|
||||
error_data = error.read()
|
||||
if error.code == 409:
|
||||
logger.info('Server responded with 409, trying to set session-id.')
|
||||
if request_count > 1:
|
||||
raise TransmissionError('Session ID negotiation failed.', error)
|
||||
if 'X-Transmission-Session-Id' in error.headers:
|
||||
self.sessionid = error.headers['X-Transmission-Session-Id']
|
||||
request.add_header('X-Transmission-Session-Id', self.sessionid)
|
||||
else:
|
||||
raise TransmissionError('Unknown conflict.', error)
|
||||
except urllib2.URLError, error:
|
||||
raise TransmissionError('Failed to connect to daemon.', error)
|
||||
except httplib.BadStatusLine, error:
|
||||
if (request_count > 1):
|
||||
raise TransmissionError('Failed to request %s "%s".' % (self.url, query), error)
|
||||
finally:
|
||||
if error_data:
|
||||
self._debug_response(error, error_data)
|
||||
request_count = request_count + 1
|
||||
result = response.read()
|
||||
self._debug_response(response, result)
|
||||
return result
|
||||
|
||||
def _request(self, method, arguments={}, ids=[], require_ids=False, timeout=DEFAULT_TIMEOUT):
|
||||
"""Send json-rpc request to Transmission using http POST"""
|
||||
|
||||
if not isinstance(method, (str, unicode)):
|
||||
raise ValueError('request takes method as string')
|
||||
if not isinstance(arguments, dict):
|
||||
raise ValueError('request takes arguments as dict')
|
||||
ids = self._format_ids(ids)
|
||||
if len(ids) > 0:
|
||||
arguments['ids'] = ids
|
||||
elif require_ids:
|
||||
raise ValueError('request require ids')
|
||||
|
||||
query = json.dumps({'tag': self._sequence, 'method': method
|
||||
, 'arguments': arguments})
|
||||
logger.info(query)
|
||||
self._sequence += 1
|
||||
start = time.time()
|
||||
http_data = self._http_query(query, timeout)
|
||||
elapsed = time.time() - start
|
||||
logger.info('http request took %.3f s' % (elapsed))
|
||||
|
||||
try:
|
||||
data = json.loads(http_data)
|
||||
except ValueError, e:
|
||||
logger.error('Error: ' + str(e))
|
||||
logger.error('Request: \"%s\"' % (query))
|
||||
logger.error('HTTP data: \"%s\"' % (http_data))
|
||||
raise
|
||||
|
||||
logger.info(json.dumps(data, indent=2))
|
||||
|
||||
if data['result'] != 'success':
|
||||
raise TransmissionError('Query failed with result \"%s\"'
|
||||
% data['result'])
|
||||
|
||||
results = {}
|
||||
if method == 'torrent-get':
|
||||
for item in data['arguments']['torrents']:
|
||||
results[item['id']] = Torrent(item)
|
||||
if self.protocol_version == 2 and 'peers' not in item:
|
||||
self.protocol_version = 1
|
||||
elif method == 'torrent-add':
|
||||
item = data['arguments']['torrent-added']
|
||||
results[item['id']] = Torrent(item)
|
||||
elif method == 'session-get':
|
||||
self._update_session(data['arguments'])
|
||||
elif method == 'session-stats':
|
||||
# older versions of T has the return data in "session-stats"
|
||||
if 'session-stats' in data['arguments']:
|
||||
self._update_session(data['arguments']['session-stats'])
|
||||
else:
|
||||
self._update_session(data['arguments'])
|
||||
elif method in ('port-test', 'blocklist-update'):
|
||||
results = data['arguments']
|
||||
else:
|
||||
return None
|
||||
|
||||
return results
|
||||
|
||||
def _format_ids(self, args):
|
||||
"""Take things and make them valid torrent identifiers"""
|
||||
ids = []
|
||||
|
||||
if isinstance(args, (int, long)):
|
||||
ids.append(args)
|
||||
elif isinstance(args, (str, unicode)):
|
||||
for item in re.split(u'[ ,]+', args):
|
||||
if len(item) == 0:
|
||||
continue
|
||||
addition = None
|
||||
try:
|
||||
# handle index
|
||||
addition = [int(item)]
|
||||
except ValueError:
|
||||
pass
|
||||
if not addition:
|
||||
# handle hashes
|
||||
try:
|
||||
int(item, 16)
|
||||
addition = [item]
|
||||
except:
|
||||
pass
|
||||
if not addition:
|
||||
# handle index ranges i.e. 5:10
|
||||
match = re.match(u'^(\d+):(\d+)$', item)
|
||||
if match:
|
||||
try:
|
||||
idx_from = int(match.group(1))
|
||||
idx_to = int(match.group(2))
|
||||
addition = range(idx_from, idx_to + 1)
|
||||
except:
|
||||
pass
|
||||
if not addition:
|
||||
raise ValueError(u'Invalid torrent id, \"%s\"' % item)
|
||||
ids.extend(addition)
|
||||
elif isinstance(args, (list)):
|
||||
for item in args:
|
||||
ids.extend(self._format_ids(item))
|
||||
else:
|
||||
raise ValueError(u'Invalid torrent id')
|
||||
return ids
|
||||
|
||||
def _update_session(self, data):
|
||||
self.session.update(data)
|
||||
|
||||
@property
|
||||
def rpc_version(self):
|
||||
if self.protocol_version == None:
|
||||
if hasattr(self.session, 'rpc_version'):
|
||||
self.protocol_version = self.session.rpc_version
|
||||
elif hasattr(self.session, 'version'):
|
||||
self.protocol_version = 3
|
||||
else:
|
||||
self.protocol_version = 2
|
||||
return self.protocol_version
|
||||
|
||||
def _rpc_version_warning(self, version):
|
||||
if self.rpc_version < version:
|
||||
logger.warning('Using feature not supported by server. RPC version for server %d, feature introduced in %d.' % (self.rpc_version, version))
|
||||
|
||||
def add(self, data, timeout=DEFAULT_TIMEOUT, **kwargs):
|
||||
"""
|
||||
Add torrent to transfers list. Takes a base64 encoded .torrent file in data.
|
||||
Additional arguments are:
|
||||
|
||||
* `metainfo`, string, alternate way to pass base64 encoded torrent data
|
||||
* `filename`, path or url, provide torrent data as a file path or URL.
|
||||
* `paused`, boolean, Whether to pause the transfer on add.
|
||||
* `download_dir`, path, The directory where the downloaded
|
||||
contents will be saved in.
|
||||
* `peer_limit`, number, Limits the number of peers for this
|
||||
transfer.
|
||||
* `files_unwanted`,
|
||||
* `files_wanted`,
|
||||
* `priority_high`,
|
||||
* `priority_low`,
|
||||
* `priority_normal`,
|
||||
"""
|
||||
args = {}
|
||||
if data:
|
||||
args = {'metainfo': data}
|
||||
if 'metainfo' in kwargs:
|
||||
pass
|
||||
if 'filename' in kwargs:
|
||||
pass
|
||||
else:
|
||||
raise ValueError('No torrent data or torrent url.')
|
||||
for key, value in kwargs.iteritems():
|
||||
argument = make_rpc_name(key)
|
||||
(arg, val) = argument_value_convert('torrent-add',
|
||||
argument, value, self.rpc_version)
|
||||
args[arg] = val
|
||||
return self._request('torrent-add', args, timeout=timeout)
|
||||
|
||||
def add_url(self, torrent_url, **kwargs):
|
||||
"""
|
||||
Add torrent to transfers list. Takes a url to a .torrent file.
|
||||
Additional arguments are:
|
||||
|
||||
* `paused`, boolean, Whether to pause the transfer on add.
|
||||
* `download_dir`, path, The directory where the downloaded
|
||||
contents will be saved in.
|
||||
* `peer_limit`, number, Limits the number of peers for this
|
||||
transfer.
|
||||
* `files_unwanted`,
|
||||
* `files_wanted`,
|
||||
* `priority_high`,
|
||||
* `priority_low`,
|
||||
* `priority_normal`,
|
||||
"""
|
||||
torrent_file = None
|
||||
if os.path.exists(torrent_url):
|
||||
torrent_file = open(torrent_url, 'r')
|
||||
else:
|
||||
try:
|
||||
torrent_file = urllib2.urlopen(torrent_url)
|
||||
except:
|
||||
torrent_file = None
|
||||
|
||||
if not torrent_file:
|
||||
raise TransmissionError('File does not exist.')
|
||||
|
||||
torrent_data = base64.b64encode(torrent_file.read())
|
||||
return self.add(torrent_data, **kwargs)
|
||||
|
||||
def remove(self, ids, delete_data=False, timeout=DEFAULT_TIMEOUT):
|
||||
"""
|
||||
remove torrent(s) with provided id(s). Local data is removed if
|
||||
delete_data is True, otherwise not.
|
||||
"""
|
||||
self._rpc_version_warning(3)
|
||||
self._request('torrent-remove',
|
||||
{'delete-local-data':rpc_bool(delete_data)}, ids, True, timeout=timeout)
|
||||
|
||||
def start(self, ids, timeout=DEFAULT_TIMEOUT):
|
||||
"""start torrent(s) with provided id(s)"""
|
||||
self._request('torrent-start', {}, ids, True, timeout=timeout)
|
||||
|
||||
def stop(self, ids, timeout=DEFAULT_TIMEOUT):
|
||||
"""stop torrent(s) with provided id(s)"""
|
||||
self._request('torrent-stop', {}, ids, True, timeout=timeout)
|
||||
|
||||
def verify(self, ids, timeout=DEFAULT_TIMEOUT):
|
||||
"""verify torrent(s) with provided id(s)"""
|
||||
self._request('torrent-verify', {}, ids, True, timeout=timeout)
|
||||
|
||||
def reannounce(self, ids, timeout=DEFAULT_TIMEOUT):
|
||||
"""reannounce torrent(s) with provided id(s)"""
|
||||
self._rpc_version_warning(5)
|
||||
self._request('torrent-reannounce', {}, ids, True, timeout=timeout)
|
||||
|
||||
def info(self, ids=[], arguments={}, timeout=DEFAULT_TIMEOUT):
|
||||
"""Get detailed information for torrent(s) with provided id(s)."""
|
||||
if not arguments:
|
||||
arguments = self.torrent_get_arguments
|
||||
return self._request('torrent-get', {'fields': arguments}, ids, timeout=timeout)
|
||||
|
||||
def get_files(self, ids=[], timeout=DEFAULT_TIMEOUT):
|
||||
"""
|
||||
Get list of files for provided torrent id(s).
|
||||
This function returns a dictonary for each requested torrent id holding
|
||||
the information about the files.
|
||||
"""
|
||||
fields = ['id', 'name', 'hashString', 'files', 'priorities', 'wanted']
|
||||
request_result = self._request('torrent-get', {'fields': fields}, ids, timeout=timeout)
|
||||
result = {}
|
||||
for id, torrent in request_result.iteritems():
|
||||
result[id] = torrent.files()
|
||||
return result
|
||||
|
||||
def set_files(self, items, timeout=DEFAULT_TIMEOUT):
|
||||
"""
|
||||
Set file properties. Takes a dictonary with similar contents as the
|
||||
result of get_files.
|
||||
"""
|
||||
if not isinstance(items, dict):
|
||||
raise ValueError('Invalid file description')
|
||||
for tid, files in items.iteritems():
|
||||
if not isinstance(files, dict):
|
||||
continue
|
||||
wanted = []
|
||||
unwanted = []
|
||||
priority_high = []
|
||||
priority_normal = []
|
||||
priority_low = []
|
||||
for fid, file in files.iteritems():
|
||||
if not isinstance(file, dict):
|
||||
continue
|
||||
if 'selected' in file and file['selected']:
|
||||
wanted.append(fid)
|
||||
else:
|
||||
unwanted.append(fid)
|
||||
if 'priority' in file:
|
||||
if file['priority'] == 'high':
|
||||
priority_high.append(fid)
|
||||
elif file['priority'] == 'normal':
|
||||
priority_normal.append(fid)
|
||||
elif file['priority'] == 'low':
|
||||
priority_low.append(fid)
|
||||
self.change([tid], files_wanted = wanted
|
||||
, files_unwanted = unwanted
|
||||
, priority_high = priority_high
|
||||
, priority_normal = priority_normal
|
||||
, priority_low = priority_low, timeout=timeout)
|
||||
|
||||
def list(self, timeout=DEFAULT_TIMEOUT):
|
||||
"""list all torrents"""
|
||||
fields = ['id', 'hashString', 'name', 'sizeWhenDone', 'leftUntilDone'
|
||||
, 'eta', 'status', 'rateUpload', 'rateDownload', 'uploadedEver'
|
||||
, 'downloadedEver']
|
||||
return self._request('torrent-get', {'fields': fields}, timeout=timeout)
|
||||
|
||||
def change(self, ids, timeout=DEFAULT_TIMEOUT, **kwargs):
|
||||
"""
|
||||
Change torrent parameters. This is the list of parameters that.
|
||||
"""
|
||||
args = {}
|
||||
for key, value in kwargs.iteritems():
|
||||
argument = make_rpc_name(key)
|
||||
(arg, val) = argument_value_convert('torrent-set'
|
||||
, argument, value, self.rpc_version)
|
||||
args[arg] = val
|
||||
|
||||
if len(args) > 0:
|
||||
self._request('torrent-set', args, ids, True, timeout=timeout)
|
||||
else:
|
||||
ValueError("No arguments to set")
|
||||
|
||||
def move(self, ids, location, timeout=DEFAULT_TIMEOUT):
|
||||
"""Move torrent data to the new location."""
|
||||
self._rpc_version_warning(6)
|
||||
args = {'location': location, 'move': True}
|
||||
self._request('torrent-set-location', args, ids, True, timeout=timeout);
|
||||
|
||||
def locate(self, ids, location, timeout=DEFAULT_TIMEOUT):
|
||||
"""Locate torrent data at the location."""
|
||||
self._rpc_version_warning(6)
|
||||
args = {'location': location, 'move': False}
|
||||
self._request('torrent-set-location', args, ids, True, timeout=timeout);
|
||||
|
||||
def get_session(self, timeout=DEFAULT_TIMEOUT):
|
||||
"""Get session parameters"""
|
||||
self._request('session-get', timeout=timeout)
|
||||
return self.session
|
||||
|
||||
def set_session(self, timeout=DEFAULT_TIMEOUT, **kwargs):
|
||||
"""Set session parameters"""
|
||||
args = {}
|
||||
for key, value in kwargs.iteritems():
|
||||
if key == 'encryption' and value not in ['required', 'preferred', 'tolerated']:
|
||||
raise ValueError('Invalid encryption value')
|
||||
argument = make_rpc_name(key)
|
||||
(arg, val) = argument_value_convert('session-set'
|
||||
, argument, value, self.rpc_version)
|
||||
args[arg] = val
|
||||
if len(args) > 0:
|
||||
self._request('session-set', args, timeout=timeout)
|
||||
|
||||
def blocklist_update(self, timeout=DEFAULT_TIMEOUT):
|
||||
"""Update block list. Returns the size of the block list."""
|
||||
self._rpc_version_warning(5)
|
||||
result = self._request('blocklist-update', timeout=timeout)
|
||||
if 'blocklist-size' in result:
|
||||
return result['blocklist-size']
|
||||
return None
|
||||
|
||||
def port_test(self, timeout=DEFAULT_TIMEOUT):
|
||||
"""
|
||||
Tests to see if your incoming peer port is accessible from the
|
||||
outside world.
|
||||
"""
|
||||
self._rpc_version_warning(5)
|
||||
result = self._request('port-test', timeout=timeout)
|
||||
if 'port-is-open' in result:
|
||||
return result['port-is-open']
|
||||
return None
|
||||
|
||||
def session_stats(self, timeout=DEFAULT_TIMEOUT):
|
||||
"""Get session statistics"""
|
||||
self._request('session-stats', timeout=timeout)
|
||||
return self.session
|
|
@ -1,138 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# 2008-07, Erik Svensson <erik.public@gmail.com>
|
||||
|
||||
import socket, datetime
|
||||
import constants
|
||||
from constants import logger
|
||||
|
||||
UNITS = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB']
|
||||
|
||||
def format_size(size):
|
||||
s = float(size)
|
||||
i = 0
|
||||
while size >= 1024.0 and i < len(UNITS):
|
||||
i += 1
|
||||
size /= 1024.0
|
||||
return (size, UNITS[i])
|
||||
|
||||
def format_speed(size):
|
||||
(size, unit) = format_size(size)
|
||||
return (size, unit + '/s')
|
||||
|
||||
def format_timedelta(delta):
|
||||
minutes, seconds = divmod(delta.seconds, 60)
|
||||
hours, minutes = divmod(minutes, 60)
|
||||
return '%d %02d:%02d:%02d' % (delta.days, hours, minutes, seconds)
|
||||
|
||||
def format_timestamp(timestamp):
|
||||
if timestamp > 0:
|
||||
dt = datetime.datetime.fromtimestamp(timestamp)
|
||||
return dt.isoformat(' ')
|
||||
else:
|
||||
return '-'
|
||||
|
||||
class INetAddressError(Exception):
|
||||
pass
|
||||
|
||||
def inet_address(address, default_port, default_address='localhost'):
|
||||
addr = address.split(':')
|
||||
if len(addr) == 1:
|
||||
try:
|
||||
port = int(addr[0])
|
||||
addr = default_address
|
||||
except:
|
||||
addr = addr[0]
|
||||
port = default_port
|
||||
elif len(addr) == 2:
|
||||
port = int(addr[1])
|
||||
if len(addr[0]) == 0:
|
||||
addr = default_address
|
||||
else:
|
||||
addr = addr[0]
|
||||
else:
|
||||
addr = default_address
|
||||
port = default_port
|
||||
try:
|
||||
socket.getaddrinfo(addr, port, socket.AF_INET, socket.SOCK_STREAM)
|
||||
except socket.gaierror, e:
|
||||
raise INetAddressError('Cannot look up address "%s".' % address)
|
||||
return (addr, port)
|
||||
|
||||
def rpc_bool(arg):
|
||||
if isinstance(arg, (str, unicode)):
|
||||
try:
|
||||
arg = bool(int(arg))
|
||||
except:
|
||||
arg = arg.lower() in [u'true', u'yes']
|
||||
if bool(arg):
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
TR_TYPE_MAP = {
|
||||
'number' : int,
|
||||
'string' : str,
|
||||
'double': float,
|
||||
'boolean' : rpc_bool,
|
||||
'array': list,
|
||||
'object': dict
|
||||
}
|
||||
|
||||
def make_python_name(name):
|
||||
return name.replace('-', '_')
|
||||
|
||||
def make_rpc_name(name):
|
||||
return name.replace('_', '-')
|
||||
|
||||
def argument_value_convert(method, argument, value, rpc_version):
|
||||
if method in ('torrent-add', 'torrent-get', 'torrent-set'):
|
||||
args = constants.TORRENT_ARGS[method[-3:]]
|
||||
elif method in ('session-get', 'session-set'):
|
||||
args = constants.SESSION_ARGS[method[-3:]]
|
||||
else:
|
||||
return ValueError('Method "%s" not supported' % (method))
|
||||
if argument in args:
|
||||
info = args[argument]
|
||||
invalid_version = True
|
||||
while invalid_version:
|
||||
invalid_version = False
|
||||
replacement = None
|
||||
if rpc_version < info[1]:
|
||||
invalid_version = True
|
||||
replacement = info[3]
|
||||
if info[2] and info[2] <= rpc_version:
|
||||
invalid_version = True
|
||||
replacement = info[4]
|
||||
if invalid_version:
|
||||
if replacement:
|
||||
logger.warning(
|
||||
'Replacing requested argument "%s" with "%s".'
|
||||
% (argument, replacement))
|
||||
argument = replacement
|
||||
info = args[argument]
|
||||
else:
|
||||
raise ValueError(
|
||||
'Method "%s" Argument "%s" does not exist in version %d.'
|
||||
% (method, argument, rpc_version))
|
||||
return (argument, TR_TYPE_MAP[info[0]](value))
|
||||
else:
|
||||
raise ValueError('Argument "%s" does not exists for method "%s".',
|
||||
(argument, method))
|
||||
|
||||
def get_arguments(method, rpc_version):
|
||||
if method in ('torrent-add', 'torrent-get', 'torrent-set'):
|
||||
args = constants.TORRENT_ARGS[method[-3:]]
|
||||
elif method in ('session-get', 'session-set'):
|
||||
args = constants.SESSION_ARGS[method[-3:]]
|
||||
else:
|
||||
return ValueError('Method "%s" not supported' % (method))
|
||||
accessible = []
|
||||
for argument, info in args.iteritems():
|
||||
valid_version = True
|
||||
if rpc_version < info[1]:
|
||||
valid_version = False
|
||||
if info[2] and info[2] <= rpc_version:
|
||||
valid_version = False
|
||||
if valid_version:
|
||||
accessible.append(argument)
|
||||
return accessible
|
Loading…
Reference in a new issue