44 changed files with 5717 additions and 9 deletions
@ -0,0 +1,89 @@ |
|||
from .main import Deluge |
|||
|
|||
def start(): |
|||
return Deluge() |
|||
|
|||
config = [{ |
|||
'name': 'deluge', |
|||
'groups': [ |
|||
{ |
|||
'tab': 'downloaders', |
|||
'list': 'download_providers', |
|||
'name': 'deluge', |
|||
'label': 'Deluge', |
|||
'description': 'Use <a href="http://www.deluge-torrent.org/" target="_blank">Deluge</a> to download torrents.', |
|||
'wizard': True, |
|||
'options': [ |
|||
{ |
|||
'name': 'enabled', |
|||
'default': 0, |
|||
'type': 'enabler', |
|||
'radio_group': 'torrent', |
|||
}, |
|||
{ |
|||
'name': 'host', |
|||
'default': 'localhost:58846', |
|||
'description': 'Hostname with port. Usually <strong>localhost:58846</strong>', |
|||
}, |
|||
{ |
|||
'name': 'username', |
|||
}, |
|||
{ |
|||
'name': 'password', |
|||
'type': 'password', |
|||
}, |
|||
{ |
|||
'name': 'paused', |
|||
'type': 'bool', |
|||
'default': False, |
|||
'description': 'Add the torrent paused.', |
|||
}, |
|||
{ |
|||
'name': 'directory', |
|||
'type': 'directory', |
|||
'description': 'Download to this directory. Keep empty for default Deluge download directory.', |
|||
}, |
|||
{ |
|||
'name': 'completed_directory', |
|||
'type': 'directory', |
|||
'description': 'Move completed torrent to this directory. Keep empty for default Deluge options.', |
|||
'advanced': True, |
|||
}, |
|||
{ |
|||
'name': 'label', |
|||
'description': 'Label to add to torrents in the Deluge UI.', |
|||
}, |
|||
{ |
|||
'name': 'remove_complete', |
|||
'label': 'Remove torrent', |
|||
'type': 'bool', |
|||
'default': True, |
|||
'advanced': True, |
|||
'description': 'Remove the torrent from Deluge after it has finished seeding.', |
|||
}, |
|||
{ |
|||
'name': 'delete_files', |
|||
'label': 'Remove files', |
|||
'default': True, |
|||
'type': 'bool', |
|||
'advanced': True, |
|||
'description': 'Also remove the leftover files.', |
|||
}, |
|||
{ |
|||
'name': 'manual', |
|||
'default': 0, |
|||
'type': 'bool', |
|||
'advanced': True, |
|||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', |
|||
}, |
|||
{ |
|||
'name': 'delete_failed', |
|||
'default': True, |
|||
'advanced': True, |
|||
'type': 'bool', |
|||
'description': 'Delete a release after the download has failed.', |
|||
}, |
|||
], |
|||
} |
|||
], |
|||
}] |
@ -0,0 +1,239 @@ |
|||
from base64 import b64encode |
|||
from couchpotato.core.helpers.variable import tryInt, tryFloat |
|||
from couchpotato.core.downloaders.base import Downloader, StatusList |
|||
from couchpotato.core.helpers.encoding import isInt |
|||
from couchpotato.core.logger import CPLog |
|||
from couchpotato.environment import Env |
|||
from datetime import timedelta |
|||
from synchronousdeluge import DelugeClient |
|||
import os.path |
|||
import traceback |
|||
|
|||
log = CPLog(__name__) |
|||
|
|||
|
|||
class Deluge(Downloader): |
|||
|
|||
protocol = ['torrent', 'torrent_magnet'] |
|||
log = CPLog(__name__) |
|||
drpc = None |
|||
|
|||
def connect(self): |
|||
# Load host from config and split out port. |
|||
host = self.conf('host').split(':') |
|||
if not isInt(host[1]): |
|||
log.error('Config properties are not filled in correctly, port is missing.') |
|||
return False |
|||
|
|||
if not self.drpc: |
|||
self.drpc = DelugeRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password')) |
|||
|
|||
return self.drpc |
|||
|
|||
def download(self, data, movie, filedata = None): |
|||
|
|||
log.info('Sending "%s" (%s) to Deluge.', (data.get('name'), data.get('protocol'))) |
|||
|
|||
if not self.connect(): |
|||
return False |
|||
|
|||
if not filedata and data.get('protocol') == 'torrent': |
|||
log.error('Failed sending torrent, no data') |
|||
return False |
|||
|
|||
# Set parameters for Deluge |
|||
options = { |
|||
'add_paused': self.conf('paused', default = 0), |
|||
'label': self.conf('label') |
|||
} |
|||
|
|||
if self.conf('directory'): |
|||
if os.path.isdir(self.conf('directory')): |
|||
options['download_location'] = self.conf('directory') |
|||
else: |
|||
log.error('Download directory from Deluge settings: %s doesn\'t exist', self.conf('directory')) |
|||
|
|||
if self.conf('completed_directory'): |
|||
if os.path.isdir(self.conf('completed_directory')): |
|||
options['move_completed'] = 1 |
|||
options['move_completed_path'] = self.conf('completed_directory') |
|||
else: |
|||
log.error('Download directory from Deluge settings: %s doesn\'t exist', self.conf('directory')) |
|||
|
|||
if data.get('seed_ratio'): |
|||
options['stop_at_ratio'] = 1 |
|||
options['stop_ratio'] = tryFloat(data.get('seed_ratio')) |
|||
|
|||
# Deluge only has seed time as a global option. Might be added in |
|||
# in a future API release. |
|||
# if data.get('seed_time'): |
|||
|
|||
# Send request to Deluge |
|||
if data.get('protocol') == 'torrent_magnet': |
|||
remote_torrent = self.drpc.add_torrent_magnet(data.get('url'), options) |
|||
else: |
|||
remote_torrent = self.drpc.add_torrent_file(movie, b64encode(filedata), options) |
|||
|
|||
if not remote_torrent: |
|||
log.error('Failed sending torrent to Deluge') |
|||
return False |
|||
|
|||
log.info('Torrent sent to Deluge successfully.') |
|||
return self.downloadReturnId(remote_torrent) |
|||
|
|||
def getAllDownloadStatus(self): |
|||
|
|||
log.debug('Checking Deluge download status.') |
|||
|
|||
if not self.connect(): |
|||
return False |
|||
|
|||
statuses = StatusList(self) |
|||
|
|||
queue = self.drpc.get_alltorrents() |
|||
|
|||
if not (queue and queue.get('torrents')): |
|||
log.debug('Nothing in queue or error') |
|||
return False |
|||
|
|||
for torrent_id in queue: |
|||
item = queue[torrent_id] |
|||
log.debug('name=%s / id=%s / save_path=%s / hash=%s / progress=%s / state=%s / eta=%s / ratio=%s / conf_ratio=%s/ is_seed=%s / is_finished=%s', (item['name'], item['hash'], item['save_path'], item['hash'], item['progress'], item['state'], item['eta'], item['ratio'], self.conf('ratio'), item['is_seed'], item['is_finished'])) |
|||
|
|||
if not os.path.isdir(Env.setting('from', 'renamer')): |
|||
log.error('Renamer "from" folder doesn\'t to exist.') |
|||
return |
|||
|
|||
status = 'busy' |
|||
# Deluge seems to set both is_seed and is_finished once everything has been downloaded. |
|||
if item['is_seed'] or item['is_finished']: |
|||
status = 'seeding' |
|||
elif item['is_seed'] and item['is_finished'] and item['paused']: |
|||
status = 'completed' |
|||
|
|||
download_dir = item['save_path'] |
|||
if item['move_on_completed']: |
|||
download_dir = item['move_completed_path'] |
|||
|
|||
statuses.append({ |
|||
'id': item['hash'], |
|||
'name': item['name'], |
|||
'status': status, |
|||
'original_status': item['state'], |
|||
'seed_ratio': item['ratio'], |
|||
'timeleft': str(timedelta(seconds = item['eta'])), |
|||
'folder': os.path.join(download_dir, item['name']), |
|||
}) |
|||
|
|||
return statuses |
|||
|
|||
def pause(self, item, pause = True): |
|||
if pause: |
|||
return self.drpc.pause_torrent([item['id']]) |
|||
else: |
|||
return self.drpc.resume_torrent([item['id']]) |
|||
|
|||
def removeFailed(self, item): |
|||
log.info('%s failed downloading, deleting...', item['name']) |
|||
return self.drpc.remove_torrent(item['id'], True) |
|||
|
|||
def processComplete(self, item, delete_files = False): |
|||
log.debug('Requesting Deluge to remove the torrent %s%s.', (item['name'], ' and cleanup the downloaded files' if delete_files else '')) |
|||
return self.drpc.remove_torrent(item['id'], remove_local_data = delete_files) |
|||
|
|||
class DelugeRPC(object): |
|||
|
|||
host = 'localhost' |
|||
port = 58846 |
|||
username = None |
|||
password = None |
|||
client = None |
|||
|
|||
def __init__(self, host = 'localhost', port = 58846, username = None, password = None): |
|||
super(DelugeRPC, self).__init__() |
|||
|
|||
self.host = host |
|||
self.port = port |
|||
self.username = username |
|||
self.password = password |
|||
|
|||
def connect(self): |
|||
self.client = DelugeClient() |
|||
self.client.connect(self.host, int(self.port), self.username, self.password) |
|||
|
|||
def add_torrent_magnet(self, torrent, options): |
|||
torrent_id = False |
|||
try: |
|||
self.connect() |
|||
torrent_id = self.client.core.add_torrent_magnet(torrent, options).get() |
|||
if options['label']: |
|||
self.client.label.set_torrent(torrent_id, options['label']).get() |
|||
except Exception, err: |
|||
log.error('Failed to add torrent magnet: %s %s', err, traceback.format_exc()) |
|||
finally: |
|||
if self.client: |
|||
self.disconnect() |
|||
|
|||
return torrent_id |
|||
|
|||
def add_torrent_file(self, movie, torrent, options): |
|||
torrent_id = False |
|||
try: |
|||
self.connect() |
|||
torrent_id = self.client.core.add_torrent_file(movie, torrent, options).get() |
|||
if options['label']: |
|||
self.client.label.set_torrent(torrent_id, options['label']).get() |
|||
except Exception, err: |
|||
log.error('Failed to add torrent file: %s %s', err, traceback.format_exc()) |
|||
finally: |
|||
if self.client: |
|||
self.disconnect() |
|||
|
|||
return torrent_id |
|||
|
|||
def get_alltorrents(self): |
|||
ret = False |
|||
try: |
|||
self.connect() |
|||
ret = self.client.core.get_torrents_status({}, {}).get() |
|||
except Exception, err: |
|||
log.error('Failed to get all torrents: %s %s', err, traceback.format_exc()) |
|||
finally: |
|||
if self.client: |
|||
self.disconnect() |
|||
return ret |
|||
|
|||
def pause_torrent(self, torrent_ids): |
|||
try: |
|||
self.connect() |
|||
self.client.core.pause_torrent(torrent_ids).get() |
|||
except Exception, err: |
|||
log.error('Failed to pause torrent: %s %s', err, traceback.format_exc()) |
|||
finally: |
|||
if self.client: |
|||
self.disconnect() |
|||
|
|||
def resume_torrent(self, torrent_ids): |
|||
try: |
|||
self.connect() |
|||
self.client.core.resume_torrent(torrent_ids).get() |
|||
except Exception, err: |
|||
log.error('Failed to resume torrent: %s %s', err, traceback.format_exc()) |
|||
finally: |
|||
if self.client: |
|||
self.disconnect() |
|||
|
|||
def remove_torrent(self, torrent_id, remove_local_data): |
|||
ret = False |
|||
try: |
|||
self.connect() |
|||
ret = self.client.core.remove_torrent(torrent_id, remove_local_data).get() |
|||
except Exception, err: |
|||
log.error('Failed to remove torrent: %s %s', err, traceback.format_exc()) |
|||
finally: |
|||
if self.client: |
|||
self.disconnect() |
|||
return ret |
|||
|
|||
def disconnect(self): |
|||
self.client.disconnect() |
@ -0,0 +1,71 @@ |
|||
from .main import rTorrent |
|||
|
|||
def start(): |
|||
return rTorrent() |
|||
|
|||
config = [{ |
|||
'name': 'rtorrent', |
|||
'groups': [ |
|||
{ |
|||
'tab': 'downloaders', |
|||
'list': 'download_providers', |
|||
'name': 'rtorrent', |
|||
'label': 'rTorrent', |
|||
'description': '', |
|||
'wizard': True, |
|||
'options': [ |
|||
{ |
|||
'name': 'enabled', |
|||
'default': 0, |
|||
'type': 'enabler', |
|||
'radio_group': 'torrent', |
|||
}, |
|||
{ |
|||
'name': 'url', |
|||
'default': 'http://localhost:80/RPC2', |
|||
}, |
|||
{ |
|||
'name': 'username', |
|||
}, |
|||
{ |
|||
'name': 'password', |
|||
'type': 'password', |
|||
}, |
|||
{ |
|||
'name': 'label', |
|||
'description': 'Label to apply on added torrents.', |
|||
}, |
|||
{ |
|||
'name': 'remove_complete', |
|||
'label': 'Remove torrent', |
|||
'default': False, |
|||
'advanced': True, |
|||
'type': 'bool', |
|||
'description': 'Remove the torrent after it finishes seeding.', |
|||
}, |
|||
{ |
|||
'name': 'delete_files', |
|||
'label': 'Remove files', |
|||
'default': True, |
|||
'type': 'bool', |
|||
'advanced': True, |
|||
'description': 'Also remove the leftover files.', |
|||
}, |
|||
{ |
|||
'name': 'paused', |
|||
'type': 'bool', |
|||
'advanced': True, |
|||
'default': False, |
|||
'description': 'Add the torrent paused.', |
|||
}, |
|||
{ |
|||
'name': 'manual', |
|||
'default': 0, |
|||
'type': 'bool', |
|||
'advanced': True, |
|||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', |
|||
}, |
|||
], |
|||
} |
|||
], |
|||
}] |
@ -0,0 +1,202 @@ |
|||
from base64 import b16encode, b32decode |
|||
from datetime import timedelta |
|||
from hashlib import sha1 |
|||
import shutil |
|||
from rtorrent.err import MethodError |
|||
|
|||
from bencode import bencode, bdecode |
|||
from couchpotato.core.downloaders.base import Downloader, StatusList |
|||
from couchpotato.core.logger import CPLog |
|||
from rtorrent import RTorrent |
|||
|
|||
|
|||
log = CPLog(__name__) |
|||
|
|||
|
|||
class rTorrent(Downloader): |
|||
protocol = ['torrent', 'torrent_magnet'] |
|||
rt = None |
|||
|
|||
def connect(self): |
|||
# Already connected? |
|||
if self.rt is not None: |
|||
return self.rt |
|||
|
|||
# Ensure url is set |
|||
if not self.conf('url'): |
|||
log.error('Config properties are not filled in correctly, url is missing.') |
|||
return False |
|||
|
|||
if self.conf('username') and self.conf('password'): |
|||
self.rt = RTorrent( |
|||
self.conf('url'), |
|||
self.conf('username'), |
|||
self.conf('password') |
|||
) |
|||
else: |
|||
self.rt = RTorrent(self.conf('url')) |
|||
|
|||
return self.rt |
|||
|
|||
def _update_provider_group(self, name, data): |
|||
if data.get('seed_time'): |
|||
log.info('seeding time ignored, not supported') |
|||
|
|||
if not name: |
|||
return False |
|||
|
|||
if not self.connect(): |
|||
return False |
|||
|
|||
views = self.rt.get_views() |
|||
|
|||
if name not in views: |
|||
self.rt.create_group(name) |
|||
|
|||
group = self.rt.get_group(name) |
|||
|
|||
try: |
|||
if data.get('seed_ratio'): |
|||
ratio = int(float(data.get('seed_ratio')) * 100) |
|||
log.debug('Updating provider ratio to %s, group name: %s', (ratio, name)) |
|||
|
|||
# Explicitly set all group options to ensure it is setup correctly |
|||
group.set_upload('1M') |
|||
group.set_min(ratio) |
|||
group.set_max(ratio) |
|||
group.set_command('d.stop') |
|||
group.enable() |
|||
else: |
|||
# Reset group action and disable it |
|||
group.set_command() |
|||
group.disable() |
|||
except MethodError, err: |
|||
log.error('Unable to set group options: %s', err.message) |
|||
return False |
|||
|
|||
return True |
|||
|
|||
|
|||
def download(self, data, movie, filedata = None): |
|||
log.debug('Sending "%s" to rTorrent.', (data.get('name'))) |
|||
|
|||
if not self.connect(): |
|||
return False |
|||
|
|||
group_name = 'cp_' + data.get('provider').lower() |
|||
if not self._update_provider_group(group_name, data): |
|||
return False |
|||
|
|||
torrent_params = {} |
|||
if self.conf('label'): |
|||
torrent_params['label'] = self.conf('label') |
|||
|
|||
if not filedata and data.get('protocol') == 'torrent': |
|||
log.error('Failed sending torrent, no data') |
|||
return False |
|||
|
|||
# Try download magnet torrents |
|||
if data.get('protocol') == 'torrent_magnet': |
|||
filedata = self.magnetToTorrent(data.get('url')) |
|||
|
|||
if filedata is False: |
|||
return False |
|||
|
|||
data['protocol'] = 'torrent' |
|||
|
|||
info = bdecode(filedata)["info"] |
|||
torrent_hash = sha1(bencode(info)).hexdigest().upper() |
|||
|
|||
# Convert base 32 to hex |
|||
if len(torrent_hash) == 32: |
|||
torrent_hash = b16encode(b32decode(torrent_hash)) |
|||
|
|||
# Send request to rTorrent |
|||
try: |
|||
# Send torrent to rTorrent |
|||
torrent = self.rt.load_torrent(filedata) |
|||
|
|||
# Set label |
|||
if self.conf('label'): |
|||
torrent.set_custom(1, self.conf('label')) |
|||
|
|||
# Set Ratio Group |
|||
torrent.set_visible(group_name) |
|||
|
|||
# Start torrent |
|||
if not self.conf('paused', default = 0): |
|||
torrent.start() |
|||
|
|||
return self.downloadReturnId(torrent_hash) |
|||
except Exception, err: |
|||
log.error('Failed to send torrent to rTorrent: %s', err) |
|||
return False |
|||
|
|||
def getAllDownloadStatus(self): |
|||
log.debug('Checking rTorrent download status.') |
|||
|
|||
if not self.connect(): |
|||
return False |
|||
|
|||
try: |
|||
torrents = self.rt.get_torrents() |
|||
|
|||
statuses = StatusList(self) |
|||
|
|||
for item in torrents: |
|||
status = 'busy' |
|||
if item.complete: |
|||
if item.active: |
|||
status = 'seeding' |
|||
else: |
|||
status = 'completed' |
|||
|
|||
statuses.append({ |
|||
'id': item.info_hash, |
|||
'name': item.name, |
|||
'status': status, |
|||
'seed_ratio': item.ratio, |
|||
'original_status': item.state, |
|||
'timeleft': str(timedelta(seconds = float(item.left_bytes) / item.down_rate)) |
|||
if item.down_rate > 0 else -1, |
|||
'folder': item.directory |
|||
}) |
|||
|
|||
return statuses |
|||
|
|||
except Exception, err: |
|||
log.error('Failed to get status from rTorrent: %s', err) |
|||
return False |
|||
|
|||
def pause(self, download_info, pause = True): |
|||
if not self.connect(): |
|||
return False |
|||
|
|||
torrent = self.rt.find_torrent(download_info['id']) |
|||
if torrent is None: |
|||
return False |
|||
|
|||
if pause: |
|||
return torrent.pause() |
|||
return torrent.resume() |
|||
|
|||
def removeFailed(self, item): |
|||
log.info('%s failed downloading, deleting...', item['name']) |
|||
return self.processComplete(item, delete_files = True) |
|||
|
|||
def processComplete(self, item, delete_files): |
|||
log.debug('Requesting rTorrent to remove the torrent %s%s.', |
|||
(item['name'], ' and cleanup the downloaded files' if delete_files else '')) |
|||
if not self.connect(): |
|||
return False |
|||
|
|||
torrent = self.rt.find_torrent(item['id']) |
|||
if torrent is None: |
|||
return False |
|||
|
|||
torrent.erase() # just removes the torrent, doesn't delete data |
|||
|
|||
if delete_files: |
|||
shutil.rmtree(item['folder'], True) |
|||
|
|||
return True |
@ -0,0 +1,588 @@ |
|||
# Copyright (c) 2013 Chris Lucas, <chris@chrisjlucas.com> |
|||
# Permission is hereby granted, free of charge, to any person obtaining |
|||
# a copy of this software and associated documentation files (the |
|||
# "Software"), to deal in the Software without restriction, including |
|||
# without limitation the rights to use, copy, modify, merge, publish, |
|||
# distribute, sublicense, and/or sell copies of the Software, and to |
|||
# permit persons to whom the Software is furnished to do so, subject to |
|||
# the following conditions: |
|||
# |
|||
# The above copyright notice and this permission notice shall be |
|||
# included in all copies or substantial portions of the Software. |
|||
# |
|||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
|||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
|||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
|||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE |
|||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION |
|||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION |
|||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
|||
|
|||
from rtorrent.common import find_torrent, \ |
|||
is_valid_port, convert_version_tuple_to_str |
|||
from rtorrent.lib.torrentparser import TorrentParser |
|||
from rtorrent.lib.xmlrpc.http import HTTPServerProxy |
|||
from rtorrent.rpc import Method, BasicAuthTransport |
|||
from rtorrent.torrent import Torrent |
|||
from rtorrent.group import Group |
|||
import os.path |
|||
import rtorrent.rpc # @UnresolvedImport |
|||
import time |
|||
import xmlrpclib |
|||
|
|||
__version__ = "0.2.9" |
|||
__author__ = "Chris Lucas" |
|||
__contact__ = "chris@chrisjlucas.com" |
|||
__license__ = "MIT" |
|||
|
|||
MIN_RTORRENT_VERSION = (0, 8, 1) |
|||
MIN_RTORRENT_VERSION_STR = convert_version_tuple_to_str(MIN_RTORRENT_VERSION) |
|||
|
|||
|
|||
class RTorrent: |
|||
""" Create a new rTorrent connection """ |
|||
rpc_prefix = None |
|||
|
|||
def __init__(self, url, username=None, password=None, |
|||
verify=False, sp=HTTPServerProxy, sp_kwargs={}): |
|||
self.url = url # : From X{__init__(self, url)} |
|||
self.username = username |
|||
self.password = password |
|||
self.sp = sp |
|||
self.sp_kwargs = sp_kwargs |
|||
|
|||
self.torrents = [] # : List of L{Torrent} instances |
|||
self._rpc_methods = [] # : List of rTorrent RPC methods |
|||
self._torrent_cache = [] |
|||
self._client_version_tuple = () |
|||
|
|||
if verify is True: |
|||
self._verify_conn() |
|||
|
|||
def _get_conn(self): |
|||
"""Get ServerProxy instance""" |
|||
if self.username is not None and self.password is not None: |
|||
return self.sp( |
|||
self.url, |
|||
transport=BasicAuthTransport(self.username, self.password), |
|||
**self.sp_kwargs |
|||
) |
|||
return self.sp(self.url, **self.sp_kwargs) |
|||
|
|||
def _verify_conn(self): |
|||
# check for rpc methods that should be available |
|||
assert {"system.client_version", |
|||
"system.library_version"}.issubset(set(self._get_rpc_methods())),\ |
|||
"Required RPC methods not available." |
|||
|
|||
# minimum rTorrent version check |
|||
|
|||
assert self._meets_version_requirement() is True,\ |
|||
"Error: Minimum rTorrent version required is {0}".format( |
|||
MIN_RTORRENT_VERSION_STR) |
|||
|
|||
def _meets_version_requirement(self): |
|||
return self._get_client_version_tuple() >= MIN_RTORRENT_VERSION |
|||
|
|||
def _get_client_version_tuple(self): |
|||
conn = self._get_conn() |
|||
|
|||
if not self._client_version_tuple: |
|||
if not hasattr(self, "client_version"): |
|||
setattr(self, "client_version", |
|||
conn.system.client_version()) |
|||
|
|||
rtver = getattr(self, "client_version") |
|||
self._client_version_tuple = tuple([int(i) for i in |
|||
rtver.split(".")]) |
|||
|
|||
return self._client_version_tuple |
|||
|
|||
def _get_rpc_methods(self): |
|||
""" Get list of raw RPC commands |
|||
|
|||
@return: raw RPC commands |
|||
@rtype: list |
|||
""" |
|||
|
|||
if self._rpc_methods == []: |
|||
self._rpc_methods = self._get_conn().system.listMethods() |
|||
|
|||
return(self._rpc_methods) |
|||
|
|||
def get_torrents(self, view="main"): |
|||
"""Get list of all torrents in specified view |
|||
|
|||
@return: list of L{Torrent} instances |
|||
|
|||
@rtype: list |
|||
|
|||
@todo: add validity check for specified view |
|||
""" |
|||
self.torrents = [] |
|||
methods = rtorrent.torrent.methods |
|||
retriever_methods = [m for m in methods |
|||
if m.is_retriever() and m.is_available(self)] |
|||
|
|||
m = rtorrent.rpc.Multicall(self) |
|||
m.add("d.multicall", view, "d.get_hash=", |
|||
*[method.rpc_call + "=" for method in retriever_methods]) |
|||
|
|||
results = m.call()[0] # only sent one call, only need first result |
|||
|
|||
for result in results: |
|||
results_dict = {} |
|||
# build results_dict |
|||
for m, r in zip(retriever_methods, result[1:]): # result[0] is the info_hash |
|||
results_dict[m.varname] = rtorrent.rpc.process_result(m, r) |
|||
|
|||
self.torrents.append( |
|||
Torrent(self, info_hash=result[0], **results_dict) |
|||
) |
|||
|
|||
self._manage_torrent_cache() |
|||
return(self.torrents) |
|||
|
|||
def _manage_torrent_cache(self): |
|||
"""Carry tracker/peer/file lists over to new torrent list""" |
|||
for torrent in self._torrent_cache: |
|||
new_torrent = rtorrent.common.find_torrent(torrent.info_hash, |
|||
self.torrents) |
|||
if new_torrent is not None: |
|||
new_torrent.files = torrent.files |
|||
new_torrent.peers = torrent.peers |
|||
new_torrent.trackers = torrent.trackers |
|||
|
|||
self._torrent_cache = self.torrents |
|||
|
|||
def _get_load_function(self, file_type, start, verbose): |
|||
"""Determine correct "load torrent" RPC method""" |
|||
func_name = None |
|||
if file_type == "url": |
|||
# url strings can be input directly |
|||
if start and verbose: |
|||
func_name = "load_start_verbose" |
|||
elif start: |
|||
func_name = "load_start" |
|||
elif verbose: |
|||
func_name = "load_verbose" |
|||
else: |
|||
func_name = "load" |
|||
elif file_type in ["file", "raw"]: |
|||
if start and verbose: |
|||
func_name = "load_raw_start_verbose" |
|||
elif start: |
|||
func_name = "load_raw_start" |
|||
elif verbose: |
|||
func_name = "load_raw_verbose" |
|||
else: |
|||
func_name = "load_raw" |
|||
|
|||
return(func_name) |
|||
|
|||
def load_torrent(self, torrent, start=False, verbose=False, verify_load=True): |
|||
""" |
|||
Loads torrent into rTorrent (with various enhancements) |
|||
|
|||
@param torrent: can be a url, a path to a local file, or the raw data |
|||
of a torrent file |
|||
@type torrent: str |
|||
|
|||
@param start: start torrent when loaded |
|||
@type start: bool |
|||
|
|||
@param verbose: print error messages to rTorrent log |
|||
@type verbose: bool |
|||
|
|||
@param verify_load: verify that torrent was added to rTorrent successfully |
|||
@type verify_load: bool |
|||
|
|||
@return: Depends on verify_load: |
|||
- if verify_load is True, (and the torrent was |
|||
loaded successfully), it'll return a L{Torrent} instance |
|||
- if verify_load is False, it'll return None |
|||
|
|||
@rtype: L{Torrent} instance or None |
|||
|
|||
@raise AssertionError: If the torrent wasn't successfully added to rTorrent |
|||
- Check L{TorrentParser} for the AssertionError's |
|||
it raises |
|||
|
|||
|
|||
@note: Because this function includes url verification (if a url was input) |
|||
as well as verification as to whether the torrent was successfully added, |
|||
this function doesn't execute instantaneously. If that's what you're |
|||
looking for, use load_torrent_simple() instead. |
|||
""" |
|||
p = self._get_conn() |
|||
tp = TorrentParser(torrent) |
|||
torrent = xmlrpclib.Binary(tp._raw_torrent) |
|||
info_hash = tp.info_hash |
|||
|
|||
func_name = self._get_load_function("raw", start, verbose) |
|||
|
|||
# load torrent |
|||
getattr(p, func_name)(torrent) |
|||
|
|||
if verify_load: |
|||
MAX_RETRIES = 3 |
|||
i = 0 |
|||
while i < MAX_RETRIES: |
|||
self.get_torrents() |
|||
if info_hash in [t.info_hash for t in self.torrents]: |
|||
break |
|||
|
|||
# was still getting AssertionErrors, delay should help |
|||
time.sleep(1) |
|||
i += 1 |
|||
|
|||
assert info_hash in [t.info_hash for t in self.torrents],\ |
|||
"Adding torrent was unsuccessful." |
|||
|
|||
return(find_torrent(info_hash, self.torrents)) |
|||
|
|||
def load_torrent_simple(self, torrent, file_type, |
|||
start=False, verbose=False): |
|||
"""Loads torrent into rTorrent |
|||
|
|||
@param torrent: can be a url, a path to a local file, or the raw data |
|||
of a torrent file |
|||
@type torrent: str |
|||
|
|||
@param file_type: valid options: "url", "file", or "raw" |
|||
@type file_type: str |
|||
|
|||
@param start: start torrent when loaded |
|||
@type start: bool |
|||
|
|||
@param verbose: print error messages to rTorrent log |
|||
@type verbose: bool |
|||
|
|||
@return: None |
|||
|
|||
@raise AssertionError: if incorrect file_type is specified |
|||
|
|||
@note: This function was written for speed, it includes no enhancements. |
|||
If you input a url, it won't check if it's valid. You also can't get |
|||
verification that the torrent was successfully added to rTorrent. |
|||
Use load_torrent() if you would like these features. |
|||
""" |
|||
p = self._get_conn() |
|||
|
|||
assert file_type in ["raw", "file", "url"], \ |
|||
"Invalid file_type, options are: 'url', 'file', 'raw'." |
|||
func_name = self._get_load_function(file_type, start, verbose) |
|||
|
|||
if file_type == "file": |
|||
# since we have to assume we're connected to a remote rTorrent |
|||
# client, we have to read the file and send it to rT as raw |
|||
assert os.path.isfile(torrent), \ |
|||
"Invalid path: \"{0}\"".format(torrent) |
|||
torrent = open(torrent, "rb").read() |
|||
|
|||
if file_type in ["raw", "file"]: |
|||
finput = xmlrpclib.Binary(torrent) |
|||
elif file_type == "url": |
|||
finput = torrent |
|||
|
|||
getattr(p, func_name)(finput) |
|||
|
|||
def get_views(self): |
|||
p = self._get_conn() |
|||
return p.view_list() |
|||
|
|||
def create_group(self, name, persistent=True, view=None): |
|||
p = self._get_conn() |
|||
|
|||
if persistent is True: |
|||
p.group.insert_persistent_view('', name) |
|||
else: |
|||
assert view is not None, "view parameter required on non-persistent groups" |
|||
p.group.insert('', name, view) |
|||
|
|||
def get_group(self, name): |
|||
assert name is not None, "group name required" |
|||
|
|||
group = Group(self, name) |
|||
group.update() |
|||
return group |
|||
|
|||
def set_dht_port(self, port): |
|||
"""Set DHT port |
|||
|
|||
@param port: port |
|||
@type port: int |
|||
|
|||
@raise AssertionError: if invalid port is given |
|||
""" |
|||
assert is_valid_port(port), "Valid port range is 0-65535" |
|||
self.dht_port = self._p.set_dht_port(port) |
|||
|
|||
def enable_check_hash(self): |
|||
"""Alias for set_check_hash(True)""" |
|||
self.set_check_hash(True) |
|||
|
|||
def disable_check_hash(self): |
|||
"""Alias for set_check_hash(False)""" |
|||
self.set_check_hash(False) |
|||
|
|||
def find_torrent(self, info_hash): |
|||
"""Frontend for rtorrent.common.find_torrent""" |
|||
return(rtorrent.common.find_torrent(info_hash, self.get_torrents())) |
|||
|
|||
def poll(self): |
|||
""" poll rTorrent to get latest torrent/peer/tracker/file information |
|||
|
|||
@note: This essentially refreshes every aspect of the rTorrent |
|||
connection, so it can be very slow if working with a remote |
|||
connection that has a lot of torrents loaded. |
|||
|
|||
@return: None |
|||
""" |
|||
self.update() |
|||
torrents = self.get_torrents() |
|||
for t in torrents: |
|||
t.poll() |
|||
|
|||
def update(self): |
|||
"""Refresh rTorrent client info |
|||
|
|||
@note: All fields are stored as attributes to self. |
|||
|
|||
@return: None |
|||
""" |
|||
multicall = rtorrent.rpc.Multicall(self) |
|||
retriever_methods = [m for m in methods |
|||
if m.is_retriever() and m.is_available(self)] |
|||
for method in retriever_methods: |
|||
multicall.add(method) |
|||
|
|||
multicall.call() |
|||
|
|||
|
|||
def _build_class_methods(class_obj): |
|||
# multicall add class |
|||
caller = lambda self, multicall, method, *args:\ |
|||
multicall.add(method, self.rpc_id, *args) |
|||
|
|||
caller.__doc__ = """Same as Multicall.add(), but with automatic inclusion |
|||
of the rpc_id |
|||
|
|||
@param multicall: A L{Multicall} instance |
|||
@type: multicall: Multicall |
|||
|
|||
@param method: L{Method} instance or raw rpc method |
|||
@type: Method or str |
|||
|
|||
@param args: optional arguments to pass |
|||
""" |
|||
setattr(class_obj, "multicall_add", caller) |
|||
|
|||
|
|||
def __compare_rpc_methods(rt_new, rt_old): |
|||
from pprint import pprint |
|||
rt_new_methods = set(rt_new._get_rpc_methods()) |
|||
rt_old_methods = set(rt_old._get_rpc_methods()) |
|||
print("New Methods:") |
|||
pprint(rt_new_methods - rt_old_methods) |
|||
print("Methods not in new rTorrent:") |
|||
pprint(rt_old_methods - rt_new_methods) |
|||
|
|||
|
|||
def __check_supported_methods(rt): |
|||
from pprint import pprint |
|||
supported_methods = set([m.rpc_call for m in |
|||
methods + |
|||
rtorrent.file.methods + |
|||
rtorrent.torrent.methods + |
|||
rtorrent.tracker.methods + |
|||
rtorrent.peer.methods]) |
|||
all_methods = set(rt._get_rpc_methods()) |
|||
|
|||
print("Methods NOT in supported methods") |
|||
pprint(all_methods - supported_methods) |
|||
print("Supported methods NOT in all methods") |
|||
pprint(supported_methods - all_methods) |
|||
|
|||
methods = [ |
|||
# RETRIEVERS |
|||
Method(RTorrent, 'get_xmlrpc_size_limit', 'get_xmlrpc_size_limit'), |
|||
Method(RTorrent, 'get_proxy_address', 'get_proxy_address'), |
|||
Method(RTorrent, 'get_split_suffix', 'get_split_suffix'), |
|||
Method(RTorrent, 'get_up_limit', 'get_upload_rate'), |
|||
Method(RTorrent, 'get_max_memory_usage', 'get_max_memory_usage'), |
|||
Method(RTorrent, 'get_max_open_files', 'get_max_open_files'), |
|||
Method(RTorrent, 'get_min_peers_seed', 'get_min_peers_seed'), |
|||
Method(RTorrent, 'get_use_udp_trackers', 'get_use_udp_trackers'), |
|||
Method(RTorrent, 'get_preload_min_size', 'get_preload_min_size'), |
|||
Method(RTorrent, 'get_max_uploads', 'get_max_uploads'), |
|||
Method(RTorrent, 'get_max_peers', 'get_max_peers'), |
|||
Method(RTorrent, 'get_timeout_sync', 'get_timeout_sync'), |
|||
Method(RTorrent, 'get_receive_buffer_size', 'get_receive_buffer_size'), |
|||
Method(RTorrent, 'get_split_file_size', 'get_split_file_size'), |
|||
Method(RTorrent, 'get_dht_throttle', 'get_dht_throttle'), |
|||
Method(RTorrent, 'get_max_peers_seed', 'get_max_peers_seed'), |
|||
Method(RTorrent, 'get_min_peers', 'get_min_peers'), |
|||
Method(RTorrent, 'get_tracker_numwant', 'get_tracker_numwant'), |
|||
Method(RTorrent, 'get_max_open_sockets', 'get_max_open_sockets'), |
|||
Method(RTorrent, 'get_session', 'get_session'), |
|||
Method(RTorrent, 'get_ip', 'get_ip'), |
|||
Method(RTorrent, 'get_scgi_dont_route', 'get_scgi_dont_route'), |
|||
Method(RTorrent, 'get_hash_read_ahead', 'get_hash_read_ahead'), |
|||
Method(RTorrent, 'get_http_cacert', 'get_http_cacert'), |
|||
Method(RTorrent, 'get_dht_port', 'get_dht_port'), |
|||
Method(RTorrent, 'get_handshake_log', 'get_handshake_log'), |
|||
Method(RTorrent, 'get_preload_type', 'get_preload_type'), |
|||
Method(RTorrent, 'get_max_open_http', 'get_max_open_http'), |
|||
Method(RTorrent, 'get_http_capath', 'get_http_capath'), |
|||
Method(RTorrent, 'get_max_downloads_global', 'get_max_downloads_global'), |
|||
Method(RTorrent, 'get_name', 'get_name'), |
|||
Method(RTorrent, 'get_session_on_completion', 'get_session_on_completion'), |
|||
Method(RTorrent, 'get_down_limit', 'get_download_rate'), |
|||
Method(RTorrent, 'get_down_total', 'get_down_total'), |
|||
Method(RTorrent, 'get_up_rate', 'get_up_rate'), |
|||
Method(RTorrent, 'get_hash_max_tries', 'get_hash_max_tries'), |
|||
Method(RTorrent, 'get_peer_exchange', 'get_peer_exchange'), |
|||
Method(RTorrent, 'get_down_rate', 'get_down_rate'), |
|||
Method(RTorrent, 'get_connection_seed', 'get_connection_seed'), |
|||
Method(RTorrent, 'get_http_proxy', 'get_http_proxy'), |
|||
Method(RTorrent, 'get_stats_preloaded', 'get_stats_preloaded'), |
|||
Method(RTorrent, 'get_timeout_safe_sync', 'get_timeout_safe_sync'), |
|||
Method(RTorrent, 'get_hash_interval', 'get_hash_interval'), |
|||
Method(RTorrent, 'get_port_random', 'get_port_random'), |
|||
Method(RTorrent, 'get_directory', 'get_directory'), |
|||
Method(RTorrent, 'get_port_open', 'get_port_open'), |
|||
Method(RTorrent, 'get_max_file_size', 'get_max_file_size'), |
|||
Method(RTorrent, 'get_stats_not_preloaded', 'get_stats_not_preloaded'), |
|||
Method(RTorrent, 'get_memory_usage', 'get_memory_usage'), |
|||
Method(RTorrent, 'get_connection_leech', 'get_connection_leech'), |
|||
Method(RTorrent, 'get_check_hash', 'get_check_hash', |
|||
boolean=True, |
|||
), |
|||
Method(RTorrent, 'get_session_lock', 'get_session_lock'), |
|||
Method(RTorrent, 'get_preload_required_rate', 'get_preload_required_rate'), |
|||
Method(RTorrent, 'get_max_uploads_global', 'get_max_uploads_global'), |
|||
Method(RTorrent, 'get_send_buffer_size', 'get_send_buffer_size'), |
|||
Method(RTorrent, 'get_port_range', 'get_port_range'), |
|||
Method(RTorrent, 'get_max_downloads_div', 'get_max_downloads_div'), |
|||
Method(RTorrent, 'get_max_uploads_div', 'get_max_uploads_div'), |
|||
Method(RTorrent, 'get_safe_sync', 'get_safe_sync'), |
|||
Method(RTorrent, 'get_bind', 'get_bind'), |
|||
Method(RTorrent, 'get_up_total', 'get_up_total'), |
|||
Method(RTorrent, 'get_client_version', 'system.client_version'), |
|||
Method(RTorrent, 'get_library_version', 'system.library_version'), |
|||
Method(RTorrent, 'get_api_version', 'system.api_version', |
|||
min_version=(0, 9, 1) |
|||
), |
|||
Method(RTorrent, "get_system_time", "system.time", |
|||
docstring="""Get the current time of the system rTorrent is running on |
|||
|
|||
@return: time (posix) |
|||
@rtype: int""", |
|||
), |
|||
|
|||
# MODIFIERS |
|||
Method(RTorrent, 'set_http_proxy', 'set_http_proxy'), |
|||
Method(RTorrent, 'set_max_memory_usage', 'set_max_memory_usage'), |
|||
Method(RTorrent, 'set_max_file_size', 'set_max_file_size'), |
|||
Method(RTorrent, 'set_bind', 'set_bind', |
|||
docstring="""Set address bind |
|||
|
|||
@param arg: ip address |
|||
@type arg: str |
|||
""", |
|||
), |
|||
Method(RTorrent, 'set_up_limit', 'set_upload_rate', |
|||
docstring="""Set global upload limit (in bytes) |
|||
|
|||
@param arg: speed limit |
|||
@type arg: int |
|||
""", |
|||
), |
|||
Method(RTorrent, 'set_port_random', 'set_port_random'), |
|||
Method(RTorrent, 'set_connection_leech', 'set_connection_leech'), |
|||
Method(RTorrent, 'set_tracker_numwant', 'set_tracker_numwant'), |
|||
Method(RTorrent, 'set_max_peers', 'set_max_peers'), |
|||
Method(RTorrent, 'set_min_peers', 'set_min_peers'), |
|||
Method(RTorrent, 'set_max_uploads_div', 'set_max_uploads_div'), |
|||
Method(RTorrent, 'set_max_open_files', 'set_max_open_files'), |
|||
Method(RTorrent, 'set_max_downloads_global', 'set_max_downloads_global'), |
|||
Method(RTorrent, 'set_session_lock', 'set_session_lock'), |
|||
Method(RTorrent, 'set_session', 'set_session'), |
|||
Method(RTorrent, 'set_split_suffix', 'set_split_suffix'), |
|||
Method(RTorrent, 'set_hash_interval', 'set_hash_interval'), |
|||
Method(RTorrent, 'set_handshake_log', 'set_handshake_log'), |
|||
Method(RTorrent, 'set_port_range', 'set_port_range'), |
|||
Method(RTorrent, 'set_min_peers_seed', 'set_min_peers_seed'), |
|||
Method(RTorrent, 'set_scgi_dont_route', 'set_scgi_dont_route'), |
|||
Method(RTorrent, 'set_preload_min_size', 'set_preload_min_size'), |
|||
Method(RTorrent, 'set_log.tracker', 'set_log.tracker'), |
|||
Method(RTorrent, 'set_max_uploads_global', 'set_max_uploads_global'), |
|||
Method(RTorrent, 'set_down_limit', 'set_download_rate', |
|||
docstring="""Set global download limit (in bytes) |
|||
|
|||
@param arg: speed limit |
|||
@type arg: int |
|||
""", |
|||
), |
|||
Method(RTorrent, 'set_preload_required_rate', 'set_preload_required_rate'), |
|||
Method(RTorrent, 'set_hash_read_ahead', 'set_hash_read_ahead'), |
|||
Method(RTorrent, 'set_max_peers_seed', 'set_max_peers_seed'), |
|||
Method(RTorrent, 'set_max_uploads', 'set_max_uploads'), |
|||
Method(RTorrent, 'set_session_on_completion', 'set_session_on_completion'), |
|||
Method(RTorrent, 'set_max_open_http', 'set_max_open_http'), |
|||
Method(RTorrent, 'set_directory', 'set_directory'), |
|||
Method(RTorrent, 'set_http_cacert', 'set_http_cacert'), |
|||
Method(RTorrent, 'set_dht_throttle', 'set_dht_throttle'), |
|||
Method(RTorrent, 'set_hash_max_tries', 'set_hash_max_tries'), |
|||
Method(RTorrent, 'set_proxy_address', 'set_proxy_address'), |
|||
Method(RTorrent, 'set_split_file_size', 'set_split_file_size'), |
|||
Method(RTorrent, 'set_receive_buffer_size', 'set_receive_buffer_size'), |
|||
Method(RTorrent, 'set_use_udp_trackers', 'set_use_udp_trackers'), |
|||
Method(RTorrent, 'set_connection_seed', 'set_connection_seed'), |
|||
Method(RTorrent, 'set_xmlrpc_size_limit', 'set_xmlrpc_size_limit'), |
|||
Method(RTorrent, 'set_xmlrpc_dialect', 'set_xmlrpc_dialect'), |
|||
Method(RTorrent, 'set_safe_sync', 'set_safe_sync'), |
|||
Method(RTorrent, 'set_http_capath', 'set_http_capath'), |
|||
Method(RTorrent, 'set_send_buffer_size', 'set_send_buffer_size'), |
|||
Method(RTorrent, 'set_max_downloads_div', 'set_max_downloads_div'), |
|||
Method(RTorrent, 'set_name', 'set_name'), |
|||
Method(RTorrent, 'set_port_open', 'set_port_open'), |
|||
Method(RTorrent, 'set_timeout_sync', 'set_timeout_sync'), |
|||
Method(RTorrent, 'set_peer_exchange', 'set_peer_exchange'), |
|||
Method(RTorrent, 'set_ip', 'set_ip', |
|||
docstring="""Set IP |
|||
|
|||
@param arg: ip address |
|||
@type arg: str |
|||
""", |
|||
), |
|||
Method(RTorrent, 'set_timeout_safe_sync', 'set_timeout_safe_sync'), |
|||
Method(RTorrent, 'set_preload_type', 'set_preload_type'), |
|||
Method(RTorrent, 'set_check_hash', 'set_check_hash', |
|||
docstring="""Enable/Disable hash checking on finished torrents |
|||
|
|||
@param arg: True to enable, False to disable |
|||
@type arg: bool |
|||
""", |
|||
boolean=True, |
|||
), |
|||
] |
|||
|
|||
_all_methods_list = [methods, |
|||
rtorrent.file.methods, |
|||
rtorrent.torrent.methods, |
|||
rtorrent.tracker.methods, |
|||
rtorrent.peer.methods, |
|||
] |
|||
|
|||
class_methods_pair = { |
|||
RTorrent: methods, |
|||
rtorrent.file.File: rtorrent.file.methods, |
|||
rtorrent.torrent.Torrent: rtorrent.torrent.methods, |
|||
rtorrent.tracker.Tracker: rtorrent.tracker.methods, |
|||
rtorrent.peer.Peer: rtorrent.peer.methods, |
|||
} |
|||
for c in class_methods_pair.keys(): |
|||
rtorrent.rpc._build_rpc_methods(c, class_methods_pair[c]) |
|||
_build_class_methods(c) |
@ -0,0 +1,86 @@ |
|||
# Copyright (c) 2013 Chris Lucas, <chris@chrisjlucas.com> |
|||
# Permission is hereby granted, free of charge, to any person obtaining |
|||
# a copy of this software and associated documentation files (the |
|||
# "Software"), to deal in the Software without restriction, including |
|||
# without limitation the rights to use, copy, modify, merge, publish, |
|||
# distribute, sublicense, and/or sell copies of the Software, and to |
|||
# permit persons to whom the Software is furnished to do so, subject to |
|||
# the following conditions: |
|||
# |
|||
# The above copyright notice and this permission notice shall be |
|||
# included in all copies or substantial portions of the Software. |
|||
# |
|||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
|||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
|||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
|||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE |
|||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION |
|||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION |
|||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
|||
|
|||
|
|||
from rtorrent.compat import is_py3 |
|||
|
|||
|
|||
def bool_to_int(value): |
|||
"""Translates python booleans to RPC-safe integers""" |
|||
if value is True: |
|||
return("1") |
|||
elif value is False: |
|||
return("0") |
|||
else: |
|||
return(value) |
|||
|
|||
|
|||
def cmd_exists(cmds_list, cmd): |
|||
"""Check if given command is in list of available commands |
|||
|
|||
@param cmds_list: see L{RTorrent._rpc_methods} |
|||
@type cmds_list: list |
|||
|
|||
@param cmd: name of command to be checked |
|||
@type cmd: str |
|||
|
|||
@return: bool |
|||
""" |
|||
|
|||
return(cmd in cmds_list) |
|||
|
|||
|
|||
def find_torrent(info_hash, torrent_list): |
|||
"""Find torrent file in given list of Torrent classes |
|||
|
|||
@param info_hash: info hash of torrent |
|||
@type info_hash: str |
|||
|
|||
@param torrent_list: list of L{Torrent} instances (see L{RTorrent.get_torrents}) |
|||
@type torrent_list: list |
|||
|
|||
@return: L{Torrent} instance, or -1 if not found |
|||
""" |
|||
for t in torrent_list: |
|||
if t.info_hash == info_hash: |
|||
return t |
|||
|
|||
return None |
|||
|
|||
|
|||
def is_valid_port(port): |
|||
"""Check if given port is valid""" |
|||
return(0 <= int(port) <= 65535) |
|||
|
|||
|
|||
def convert_version_tuple_to_str(t): |
|||
return(".".join([str(n) for n in t])) |
|||
|
|||
|
|||
def safe_repr(fmt, *args, **kwargs): |
|||
""" Formatter that handles unicode arguments """ |
|||
|
|||
if not is_py3(): |
|||
# unicode fmt can take str args, str fmt cannot take unicode args |
|||
fmt = fmt.decode("utf-8") |
|||
out = fmt.format(*args, **kwargs) |
|||
return out.encode("utf-8") |
|||
else: |
|||
return fmt.format(*args, **kwargs) |
@ -0,0 +1,30 @@ |
|||
# Copyright (c) 2013 Chris Lucas, <chris@chrisjlucas.com> |
|||
# Permission is hereby granted, free of charge, to any person obtaining |
|||
# a copy of this software and associated documentation files (the |
|||
# "Software"), to deal in the Software without restriction, including |
|||
# without limitation the rights to use, copy, modify, merge, publish, |
|||
# distribute, sublicense, and/or sell copies of the Software, and to |
|||
# permit persons to whom the Software is furnished to do so, subject to |
|||
# the following conditions: |
|||
# |
|||
# The above copyright notice and this permission notice shall be |
|||
# included in all copies or substantial portions of the Software. |
|||
# |
|||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
|||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
|||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
|||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE |
|||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION |
|||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION |
|||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
|||
|
|||
import sys |
|||
|
|||
|
|||
def is_py3(): |
|||
return sys.version_info[0] == 3 |
|||
|
|||
if is_py3(): |
|||
import xmlrpc.client as xmlrpclib |
|||
else: |
|||
import xmlrpclib |
@ -0,0 +1,40 @@ |
|||
# Copyright (c) 2013 Chris Lucas, <chris@chrisjlucas.com> |
|||
# Permission is hereby granted, free of charge, to any person obtaining |
|||
# a copy of this software and associated documentation files (the |
|||
# "Software"), to deal in the Software without restriction, including |
|||
# without limitation the rights to use, copy, modify, merge, publish, |
|||
# distribute, sublicense, and/or sell copies of the Software, and to |
|||
# permit persons to whom the Software is furnished to do so, subject to |
|||
# the following conditions: |
|||
# |
|||
# The above copyright notice and this permission notice shall be |
|||
# included in all copies or substantial portions of the Software. |
|||
# |
|||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
|||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
|||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
|||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE |
|||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION |
|||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION |
|||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
|||
|
|||
from rtorrent.common import convert_version_tuple_to_str |
|||
|
|||
|
|||
class RTorrentVersionError(Exception): |
|||
def __init__(self, min_version, cur_version): |
|||
self.min_version = min_version |
|||
self.cur_version = cur_version |
|||
self.msg = "Minimum version required: {0}".format( |
|||
convert_version_tuple_to_str(min_version)) |
|||
|
|||
def __str__(self): |
|||
return(self.msg) |
|||
|
|||
|
|||
class MethodError(Exception): |
|||
def __init__(self, msg): |
|||
self.msg = msg |
|||
|
|||
def __str__(self): |
|||
return(self.msg) |
@ -0,0 +1,91 @@ |
|||
# Copyright (c) 2013 Chris Lucas, <chris@chrisjlucas.com> |
|||
# Permission is hereby granted, free of charge, to any person obtaining |
|||
# a copy of this software and associated documentation files (the |
|||
# "Software"), to deal in the Software without restriction, including |
|||
# without limitation the rights to use, copy, modify, merge, publish, |
|||
# distribute, sublicense, and/or sell copies of the Software, and to |
|||
# permit persons to whom the Software is furnished to do so, subject to |
|||
# the following conditions: |
|||
# |
|||
# The above copyright notice and this permission notice shall be |
|||
# included in all copies or substantial portions of the Software. |
|||
# |
|||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
|||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
|||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
|||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE |
|||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION |
|||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION |
|||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
|||
|
|||
# from rtorrent.rpc import Method |
|||
import rtorrent.rpc |
|||
|
|||
from rtorrent.common import safe_repr |
|||
|
|||
Method = rtorrent.rpc.Method |
|||
|
|||
|
|||
class File: |
|||
"""Represents an individual file within a L{Torrent} instance.""" |
|||
|
|||
def __init__(self, _rt_obj, info_hash, index, **kwargs): |
|||
self._rt_obj = _rt_obj |
|||
self.info_hash = info_hash # : info hash for the torrent the file is associated with |
|||
self.index = index # : The position of the file within the file list |
|||
for k in kwargs.keys(): |
|||
setattr(self, k, kwargs.get(k, None)) |
|||
|
|||
self.rpc_id = "{0}:f{1}".format( |
|||
self.info_hash, self.index) # : unique id to pass to rTorrent |
|||
|
|||
def update(self): |
|||
"""Refresh file data |
|||
|
|||
@note: All fields are stored as attributes to self. |
|||
|
|||
@return: None |
|||
""" |
|||
multicall = rtorrent.rpc.Multicall(self) |
|||
retriever_methods = [m for m in methods |
|||
if m.is_retriever() and m.is_available(self._rt_obj)] |
|||
for method in retriever_methods: |
|||
multicall.add(method, self.rpc_id) |
|||
|
|||
multicall.call() |
|||
|
|||
def __repr__(self): |
|||
return safe_repr("File(index={0} path=\"{1}\")", self.index, self.path) |
|||
|
|||
methods = [ |
|||
# RETRIEVERS |
|||
Method(File, 'get_last_touched', 'f.get_last_touched'), |
|||
Method(File, 'get_range_second', 'f.get_range_second'), |
|||
Method(File, 'get_size_bytes', 'f.get_size_bytes'), |
|||
Method(File, 'get_priority', 'f.get_priority'), |
|||
Method(File, 'get_match_depth_next', 'f.get_match_depth_next'), |
|||
Method(File, 'is_resize_queued', 'f.is_resize_queued', |
|||
boolean=True, |
|||
), |
|||
Method(File, 'get_range_first', 'f.get_range_first'), |
|||
Method(File, 'get_match_depth_prev', 'f.get_match_depth_prev'), |
|||
Method(File, 'get_path', 'f.get_path'), |
|||
Method(File, 'get_completed_chunks', 'f.get_completed_chunks'), |
|||
Method(File, 'get_path_components', 'f.get_path_components'), |
|||
Method(File, 'is_created', 'f.is_created', |
|||
boolean=True, |
|||
), |
|||
Method(File, 'is_open', 'f.is_open', |
|||
boolean=True, |
|||
), |
|||
Method(File, 'get_size_chunks', 'f.get_size_chunks'), |
|||
Method(File, 'get_offset', 'f.get_offset'), |
|||
Method(File, 'get_frozen_path', 'f.get_frozen_path'), |
|||
Method(File, 'get_path_depth', 'f.get_path_depth'), |
|||
Method(File, 'is_create_queued', 'f.is_create_queued', |
|||
boolean=True, |
|||
), |
|||
|
|||
|
|||
# MODIFIERS |
|||
] |
@ -0,0 +1,84 @@ |
|||
# Copyright (c) 2013 Dean Gardiner, <gardiner91@gmail.com> |
|||
# Permission is hereby granted, free of charge, to any person obtaining |
|||
# a copy of this software and associated documentation files (the |
|||
# "Software"), to deal in the Software without restriction, including |
|||
# without limitation the rights to use, copy, modify, merge, publish, |
|||
# distribute, sublicense, and/or sell copies of the Software, and to |
|||
# permit persons to whom the Software is furnished to do so, subject to |
|||
# the following conditions: |
|||
# |
|||
# The above copyright notice and this permission notice shall be |
|||
# included in all copies or substantial portions of the Software. |
|||
# |
|||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
|||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
|||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
|||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE |
|||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION |
|||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION |
|||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
|||
|
|||
import rtorrent.rpc |
|||
|
|||
Method = rtorrent.rpc.Method |
|||
|
|||
|
|||
class Group: |
|||
__name__ = 'Group' |
|||
|
|||
def __init__(self, _rt_obj, name): |
|||
self._rt_obj = _rt_obj |
|||
self.name = name |
|||
|
|||
self.methods = [ |
|||
# RETRIEVERS |
|||
Method(Group, 'get_max', 'group.' + self.name + '.ratio.max', varname='max'), |
|||
Method(Group, 'get_min', 'group.' + self.name + '.ratio.min', varname='min'), |
|||
Method(Group, 'get_upload', 'group.' + self.name + '.ratio.upload', varname='upload'), |
|||
|
|||
# MODIFIERS |
|||
Method(Group, 'set_max', 'group.' + self.name + '.ratio.max.set', varname='max'), |
|||
Method(Group, 'set_min', 'group.' + self.name + '.ratio.min.set', varname='min'), |
|||
Method(Group, 'set_upload', 'group.' + self.name + '.ratio.upload.set', varname='upload') |
|||
] |
|||
|
|||
rtorrent.rpc._build_rpc_methods(self, self.methods) |
|||
|
|||
# Setup multicall_add method |
|||
caller = lambda multicall, method, *args: \ |
|||
multicall.add(method, *args) |
|||
setattr(self, "multicall_add", caller) |
|||
|
|||
def _get_prefix(self): |
|||
return 'group.' + self.name + '.ratio.' |
|||
|
|||
def update(self): |
|||
multicall = rtorrent.rpc.Multicall(self) |
|||
|
|||
retriever_methods = [m for m in self.methods |
|||
if m.is_retriever() and m.is_available(self._rt_obj)] |
|||
|
|||
for method in retriever_methods: |
|||
multicall.add(method) |
|||
|
|||
multicall.call() |
|||
|
|||
def enable(self): |
|||
p = self._rt_obj._get_conn() |
|||
return getattr(p, self._get_prefix() + 'enable')() |
|||
|
|||
def disable(self): |
|||
p = self._rt_obj._get_conn() |
|||
return getattr(p, self._get_prefix() + 'disable')() |
|||
|
|||
def set_command(self, *methods): |
|||
methods = [m + '=' for m in methods] |
|||
|
|||
m = rtorrent.rpc.Multicall(self) |
|||
self.multicall_add( |
|||
m, 'system.method.set', |
|||
self._get_prefix() + 'command', |
|||
*methods |
|||
) |
|||
|
|||
return(m.call()[-1]) |
@ -0,0 +1,281 @@ |
|||
# Copyright (C) 2011 by clueless <clueless.nospam ! mail.com> |
|||
# |
|||
# Permission is hereby granted, free of charge, to any person obtaining a copy |
|||
# of this software and associated documentation files (the "Software"), to deal |
|||
# in the Software without restriction, including without limitation the rights |
|||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|||
# copies of the Software, and to permit persons to whom the Software is |
|||
# furnished to do so, subject to the following conditions: |
|||
# |
|||
# The above copyright notice and this permission notice shall be included in |
|||
# all copies or substantial portions of the Software. |
|||
# |
|||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
|||
# THE SOFTWARE. |
|||
# |
|||
# Version: 20111107 |
|||
# |
|||
# Changelog |
|||
# --------- |
|||
# 2011-11-07 - Added support for Python2 (tested on 2.6) |
|||
# 2011-10-03 - Fixed: moved check for end of list at the top of the while loop |
|||
# in _decode_list (in case the list is empty) (Chris Lucas) |
|||
# - Converted dictionary keys to str |
|||
# 2011-04-24 - Changed date format to YYYY-MM-DD for versioning, bigger |
|||
# integer denotes a newer version |
|||
# - Fixed a bug that would treat False as an integral type but |
|||
# encode it using the 'False' string, attempting to encode a |
|||
# boolean now results in an error |
|||
# - Fixed a bug where an integer value of 0 in a list or |
|||
# dictionary resulted in a parse error while decoding |
|||
# |
|||
# 2011-04-03 - Original release |
|||
|
|||
import sys |
|||
|
|||
_py3 = sys.version_info[0] == 3 |
|||
|
|||
if _py3: |
|||
_VALID_STRING_TYPES = (str,) |
|||
else: |
|||
_VALID_STRING_TYPES = (str, unicode) # @UndefinedVariable |
|||
|
|||
_TYPE_INT = 1 |
|||
_TYPE_STRING = 2 |
|||
_TYPE_LIST = 3 |
|||
_TYPE_DICTIONARY = 4 |
|||
_TYPE_END = 5 |
|||
_TYPE_INVALID = 6 |
|||
|
|||
# Function to determine the type of he next value/item |
|||
# Arguments: |
|||
# char First character of the string that is to be decoded |
|||
# Return value: |
|||
# Returns an integer that describes what type the next value/item is |
|||
|
|||
|
|||
def _gettype(char): |
|||
if not isinstance(char, int): |
|||
char = ord(char) |
|||
if char == 0x6C: # 'l' |
|||
return _TYPE_LIST |
|||
elif char == 0x64: # 'd' |
|||
return _TYPE_DICTIONARY |
|||
elif char == 0x69: # 'i' |
|||
return _TYPE_INT |
|||
elif char == 0x65: # 'e' |
|||
return _TYPE_END |
|||
elif char >= 0x30 and char <= 0x39: # '0' '9' |
|||
return _TYPE_STRING |
|||
else: |
|||
return _TYPE_INVALID |
|||
|
|||
# Function to parse a string from the bendcoded data |
|||
# Arguments: |
|||
# data bencoded data, must be guaranteed to be a string |
|||
# Return Value: |
|||
# Returns a tuple, the first member of the tuple is the parsed string |
|||
# The second member is whatever remains of the bencoded data so it can |
|||
# be used to parse the next part of the data |
|||
|
|||
|
|||
def _decode_string(data): |
|||
end = 1 |
|||
# if py3, data[end] is going to be an int |
|||
# if py2, data[end] will be a string |
|||
if _py3: |
|||
char = 0x3A |
|||
else: |
|||
char = chr(0x3A) |
|||
|
|||
while data[end] != char: # ':' |
|||
end = end + 1 |
|||
strlen = int(data[:end]) |
|||
return (data[end + 1:strlen + end + 1], data[strlen + end + 1:]) |
|||
|
|||
# Function to parse an integer from the bencoded data |
|||
# Arguments: |
|||
# data bencoded data, must be guaranteed to be an integer |
|||
# Return Value: |
|||
# Returns a tuple, the first member of the tuple is the parsed string |
|||
# The second member is whatever remains of the bencoded data so it can |
|||
# be used to parse the next part of the data |
|||
|
|||
|
|||
def _decode_int(data): |
|||
end = 1 |
|||
# if py3, data[end] is going to be an int |
|||
# if py2, data[end] will be a string |
|||
if _py3: |
|||
char = 0x65 |
|||
else: |
|||
char = chr(0x65) |
|||
|
|||
while data[end] != char: # 'e' |
|||
end = end + 1 |
|||
return (int(data[1:end]), data[end + 1:]) |
|||
|
|||
# Function to parse a bencoded list |
|||
# Arguments: |
|||
# data bencoded data, must be guaranted to be the start of a list |
|||
# Return Value: |
|||
# Returns a tuple, the first member of the tuple is the parsed list |
|||
# The second member is whatever remains of the bencoded data so it can |
|||
# be used to parse the next part of the data |
|||
|
|||
|
|||
def _decode_list(data): |
|||
x = [] |
|||
overflow = data[1:] |
|||
while True: # Loop over the data |
|||
if _gettype(overflow[0]) == _TYPE_END: # - Break if we reach the end of the list |
|||
return (x, overflow[1:]) # and return the list and overflow |
|||
|
|||
value, overflow = _decode(overflow) # |
|||
if isinstance(value, bool) or overflow == '': # - if we have a parse error |
|||
return (False, False) # Die with error |
|||
else: # - Otherwise |
|||
x.append(value) # add the value to the list |
|||
|
|||
|
|||
# Function to parse a bencoded list |
|||
# Arguments: |
|||
# data bencoded data, must be guaranted to be the start of a list |
|||
# Return Value: |
|||
# Returns a tuple, the first member of the tuple is the parsed dictionary |
|||
# The second member is whatever remains of the bencoded data so it can |
|||
# be used to parse the next part of the data |
|||
def _decode_dict(data): |
|||
x = {} |
|||
overflow = data[1:] |
|||
while True: # Loop over the data |
|||
if _gettype(overflow[0]) != _TYPE_STRING: # - If the key is not a string |
|||
return (False, False) # Die with error |
|||
key, overflow = _decode(overflow) # |
|||
if key == False or overflow == '': # - If parse error |
|||
return (False, False) # Die with error |
|||
value, overflow = _decode(overflow) # |
|||
if isinstance(value, bool) or overflow == '': # - If parse error |
|||
print("Error parsing value") |
|||
print(value) |
|||
print(overflow) |
|||
return (False, False) # Die with error |
|||
else: |
|||
# don't use bytes for the key |
|||
key = key.decode() |
|||
x[key] = value |
|||
if _gettype(overflow[0]) == _TYPE_END: |
|||
return (x, overflow[1:]) |
|||
|
|||
# Arguments: |
|||
# data bencoded data in bytes format |
|||
# Return Values: |
|||
# Returns a tuple, the first member is the parsed data, could be a string, |
|||
# an integer, a list or a dictionary, or a combination of those |
|||
# The second member is the leftover of parsing, if everything parses correctly this |
|||
# should be an empty byte string |
|||
|
|||
|
|||
def _decode(data): |
|||
btype = _gettype(data[0]) |
|||
if btype == _TYPE_INT: |
|||
return _decode_int(data) |
|||
elif btype == _TYPE_STRING: |
|||
return _decode_string(data) |
|||
elif btype == _TYPE_LIST: |
|||
return _decode_list(data) |
|||
elif btype == _TYPE_DICTIONARY: |
|||
return _decode_dict(data) |
|||
else: |
|||
return (False, False) |
|||
|
|||
# Function to decode bencoded data |
|||
# Arguments: |
|||
# data bencoded data, can be str or bytes |
|||
# Return Values: |
|||
# Returns the decoded data on success, this coud be bytes, int, dict or list |
|||
# or a combinatin of those |
|||
# If an error occurs the return value is False |
|||
|
|||
|
|||
def decode(data): |
|||
# if isinstance(data, str): |
|||
# data = data.encode() |
|||
decoded, overflow = _decode(data) |
|||
return decoded |
|||
|
|||
# Args: data as integer |
|||
# return: encoded byte string |
|||
|
|||
|
|||
def _encode_int(data): |
|||
return b'i' + str(data).encode() + b'e' |
|||
|
|||
# Args: data as string or bytes |
|||
# Return: encoded byte string |
|||
|
|||
|
|||
def _encode_string(data): |
|||
return str(len(data)).encode() + b':' + data |
|||
|
|||
# Args: data as list |
|||
# Return: Encoded byte string, false on error |
|||
|
|||
|
|||
def _encode_list(data): |
|||
elist = b'l' |
|||
for item in data: |
|||
eitem = encode(item) |
|||
if eitem == False: |
|||
return False |
|||
elist += eitem |
|||
return elist + b'e' |
|||
|
|||
# Args: data as dict |
|||
# Return: encoded byte string, false on error |
|||
|
|||
|
|||
def _encode_dict(data): |
|||
edict = b'd' |
|||
keys = [] |
|||
for key in data: |
|||
if not isinstance(key, _VALID_STRING_TYPES) and not isinstance(key, bytes): |
|||
return False |
|||
keys.append(key) |
|||
keys.sort() |
|||
for key in keys: |
|||
ekey = encode(key) |
|||
eitem = encode(data[key]) |
|||
if ekey == False or eitem == False: |
|||
return False |
|||
edict += ekey + eitem |
|||
return edict + b'e' |
|||
|
|||
# Function to encode a variable in bencoding |
|||
# Arguments: |
|||
# data Variable to be encoded, can be a list, dict, str, bytes, int or a combination of those |
|||
# Return Values: |
|||
# Returns the encoded data as a byte string when successful |
|||
# If an error occurs the return value is False |
|||
|
|||
|
|||
def encode(data): |
|||
if isinstance(data, bool): |
|||
return False |
|||
elif isinstance(data, int): |
|||
return _encode_int(data) |
|||
elif isinstance(data, bytes): |
|||
return _encode_string(data) |
|||
elif isinstance(data, _VALID_STRING_TYPES): |
|||
return _encode_string(data.encode()) |
|||
elif isinstance(data, list): |
|||
return _encode_list(data) |
|||
elif isinstance(data, dict): |
|||
return _encode_dict(data) |
|||
else: |
|||
return False |
@ -0,0 +1,159 @@ |
|||
# Copyright (c) 2013 Chris Lucas, <chris@chrisjlucas.com> |
|||
# Permission is hereby granted, free of charge, to any person obtaining |
|||
# a copy of this software and associated documentation files (the |
|||
# "Software"), to deal in the Software without restriction, including |
|||
# without limitation the rights to use, copy, modify, merge, publish, |
|||
# distribute, sublicense, and/or sell copies of the Software, and to |
|||
# permit persons to whom the Software is furnished to do so, subject to |
|||
# the following conditions: |
|||
# |
|||
# The above copyright notice and this permission notice shall be |
|||
# included in all copies or substantial portions of the Software. |
|||
# |
|||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
|||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
|||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
|||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE |
|||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION |
|||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION |
|||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
|||
|
|||
from rtorrent.compat import is_py3 |
|||
import os.path |
|||
import re |
|||
import rtorrent.lib.bencode as bencode |
|||
import hashlib |
|||
|
|||
if is_py3(): |
|||
from urllib.request import urlopen # @UnresolvedImport @UnusedImport |
|||
else: |
|||
from urllib2 import urlopen # @UnresolvedImport @Reimport |
|||
|
|||
|
|||
class TorrentParser(): |
|||
def __init__(self, torrent): |
|||
"""Decode and parse given torrent |
|||
|
|||
@param torrent: handles: urls, file paths, string of torrent data |
|||
@type torrent: str |
|||
|
|||
@raise AssertionError: Can be raised for a couple reasons: |
|||
- If _get_raw_torrent() couldn't figure out |
|||
what X{torrent} is |
|||
- if X{torrent} isn't a valid bencoded torrent file |
|||
""" |
|||
self.torrent = torrent |
|||
self._raw_torrent = None # : testing yo |
|||
self._torrent_decoded = None # : what up |
|||
self.file_type = None |
|||
|
|||
self._get_raw_torrent() |
|||
assert self._raw_torrent is not None, "Couldn't get raw_torrent." |
|||
if self._torrent_decoded is None: |
|||
self._decode_torrent() |
|||
assert isinstance(self._torrent_decoded, dict), "Invalid torrent file." |
|||
self._parse_torrent() |
|||
|
|||
def _is_raw(self): |
|||
raw = False |
|||
if isinstance(self.torrent, (str, bytes)): |
|||
if isinstance(self._decode_torrent(self.torrent), dict): |
|||
raw = True |
|||
else: |
|||
# reset self._torrent_decoded (currently equals False) |
|||
self._torrent_decoded = None |
|||
|
|||
return(raw) |
|||
|
|||
def _get_raw_torrent(self): |
|||
"""Get raw torrent data by determining what self.torrent is""" |
|||
# already raw? |
|||
if self._is_raw(): |
|||
self.file_type = "raw" |
|||
self._raw_torrent = self.torrent |
|||
return |
|||
# local file? |
|||
if os.path.isfile(self.torrent): |
|||
self.file_type = "file" |
|||
self._raw_torrent = open(self.torrent, "rb").read() |
|||
# url? |
|||
elif re.search("^(http|ftp):\/\/", self.torrent, re.I): |
|||
self.file_type = "url" |
|||
self._raw_torrent = urlopen(self.torrent).read() |
|||
|
|||
def _decode_torrent(self, raw_torrent=None): |
|||
if raw_torrent is None: |
|||
raw_torrent = self._raw_torrent |
|||
self._torrent_decoded = bencode.decode(raw_torrent) |
|||
return(self._torrent_decoded) |
|||
|
|||
def _calc_info_hash(self): |
|||
self.info_hash = None |
|||
if "info" in self._torrent_decoded.keys(): |
|||
info_dict = self._torrent_decoded["info"] |
|||
self.info_hash = hashlib.sha1(bencode.encode( |
|||
info_dict)).hexdigest().upper() |
|||
|
|||
return(self.info_hash) |
|||
|
|||
def _parse_torrent(self): |
|||
for k in self._torrent_decoded: |
|||
key = k.replace(" ", "_").lower() |
|||
setattr(self, key, self._torrent_decoded[k]) |
|||
|
|||
self._calc_info_hash() |
|||
|
|||
|
|||
class NewTorrentParser(object): |
|||
@staticmethod |
|||
def _read_file(fp): |
|||
return fp.read() |
|||
|
|||
@staticmethod |
|||
def _write_file(fp): |
|||
fp.write() |
|||
return fp |
|||
|
|||
@staticmethod |
|||
def _decode_torrent(data): |
|||
return bencode.decode(data) |
|||
|
|||
def __init__(self, input): |
|||
self.input = input |
|||
self._raw_torrent = None |
|||
self._decoded_torrent = None |
|||
self._hash_outdated = False |
|||
|
|||
if isinstance(self.input, (str, bytes)): |
|||
# path to file? |
|||
if os.path.isfile(self.input): |
|||
self._raw_torrent = self._read_file(open(self.input, "rb")) |
|||
else: |
|||
# assume input was the raw torrent data (do we really want |
|||
# this?) |
|||
self._raw_torrent = self.input |
|||
|
|||
# file-like object? |
|||
elif self.input.hasattr("read"): |
|||
self._raw_torrent = self._read_file(self.input) |
|||
|
|||
assert self._raw_torrent is not None, "Invalid input: input must be a path or a file-like object" |
|||
|
|||
self._decoded_torrent = self._decode_torrent(self._raw_torrent) |
|||
|
|||
assert isinstance( |
|||
self._decoded_torrent, dict), "File could not be decoded" |
|||
|
|||
def _calc_info_hash(self): |
|||
self.info_hash = None |
|||
info_dict = self._torrent_decoded["info"] |
|||
self.info_hash = hashlib.sha1(bencode.encode( |
|||
info_dict)).hexdigest().upper() |
|||
|
|||
return(self.info_hash) |
|||
|
|||
def set_tracker(self, tracker): |
|||
self._decoded_torrent["announce"] = tracker |
|||
|
|||
def get_tracker(self): |
|||
return self._decoded_torrent.get("announce") |
@ -0,0 +1,23 @@ |
|||
# Copyright (c) 2013 Chris Lucas, <chris@chrisjlucas.com> |
|||
# Permission is hereby granted, free of charge, to any person obtaining |
|||
# a copy of this software and associated documentation files (the |
|||
# "Software"), to deal in the Software without restriction, including |
|||
# without limitation the rights to use, copy, modify, merge, publish, |
|||
# distribute, sublicense, and/or sell copies of the Software, and to |
|||
# permit persons to whom the Software is furnished to do so, subject to |
|||
# the following conditions: |
|||
# |
|||
# The above copyright notice and this permission notice shall be |
|||
# included in all copies or substantial portions of the Software. |
|||
# |
|||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
|||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
|||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
|||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE |
|||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION |
|||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION |
|||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
|||
|
|||
from rtorrent.compat import xmlrpclib |
|||
|
|||
HTTPServerProxy = xmlrpclib.ServerProxy |
@ -0,0 +1,98 @@ |
|||
# Copyright (c) 2013 Chris Lucas, <chris@chrisjlucas.com> |
|||
# Permission is hereby granted, free of charge, to any person obtaining |
|||
# a copy of this software and associated documentation files (the |
|||
# "Software"), to deal in the Software without restriction, including |
|||
# without limitation the rights to use, copy, modify, merge, publish, |
|||
# distribute, sublicense, and/or sell copies of the Software, and to |
|||
# permit persons to whom the Software is furnished to do so, subject to |
|||
# the following conditions: |
|||
# |
|||
# The above copyright notice and this permission notice shall be |
|||
# included in all copies or substantial portions of the Software. |
|||
# |
|||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
|||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
|||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
|||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE |
|||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION |
|||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION |
|||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
|||
|
|||
# from rtorrent.rpc import Method |
|||
import rtorrent.rpc |
|||
|
|||
from rtorrent.common import safe_repr |
|||
|
|||
Method = rtorrent.rpc.Method |
|||
|
|||
|
|||
class Peer: |
|||
"""Represents an individual peer within a L{Torrent} instance.""" |
|||
def __init__(self, _rt_obj, info_hash, **kwargs): |
|||
self._rt_obj = _rt_obj |
|||
self.info_hash = info_hash # : info hash for the torrent the peer is associated with |
|||
for k in kwargs.keys(): |
|||
setattr(self, k, kwargs.get(k, None)) |
|||
|
|||
self.rpc_id = "{0}:p{1}".format( |
|||
self.info_hash, self.id) # : unique id to pass to rTorrent |
|||
|
|||
def __repr__(self): |
|||
return safe_repr("Peer(id={0})", self.id) |
|||
|
|||
def update(self): |
|||
"""Refresh peer data |
|||
|
|||
@note: All fields are stored as attributes to self. |
|||
|
|||
@return: None |
|||
""" |
|||
multicall = rtorrent.rpc.Multicall(self) |
|||
retriever_methods = [m for m in methods |
|||
if m.is_retriever() and m.is_available(self._rt_obj)] |
|||
for method in retriever_methods: |
|||
multicall.add(method, self.rpc_id) |
|||
|
|||
multicall.call() |
|||
|
|||
methods = [ |
|||
# RETRIEVERS |
|||
Method(Peer, 'is_preferred', 'p.is_preferred', |
|||
boolean=True, |
|||
), |
|||
Method(Peer, 'get_down_rate', 'p.get_down_rate'), |
|||
Method(Peer, 'is_unwanted', 'p.is_unwanted', |
|||
boolean=True, |
|||
), |
|||
Method(Peer, 'get_peer_total', 'p.get_peer_total'), |
|||
Method(Peer, 'get_peer_rate', 'p.get_peer_rate'), |
|||
Method(Peer, 'get_port', 'p.get_port'), |
|||
Method(Peer, 'is_snubbed', 'p.is_snubbed', |
|||
boolean=True, |
|||
), |
|||
Method(Peer, 'get_id_html', 'p.get_id_html'), |
|||
Method(Peer, 'get_up_rate', 'p.get_up_rate'), |
|||
Method(Peer, 'is_banned', 'p.banned', |
|||
boolean=True, |
|||
), |
|||
Method(Peer, 'get_completed_percent', 'p.get_completed_percent'), |
|||
Method(Peer, 'completed_percent', 'p.completed_percent'), |
|||
Method(Peer, 'get_id', 'p.get_id'), |
|||
Method(Peer, 'is_obfuscated', 'p.is_obfuscated', |
|||
boolean=True, |
|||
), |
|||
Method(Peer, 'get_down_total', 'p.get_down_total'), |
|||
Method(Peer, 'get_client_version', 'p.get_client_version'), |
|||
Method(Peer, 'get_address', 'p.get_address'), |
|||
Method(Peer, 'is_incoming', 'p.is_incoming', |
|||
boolean=True, |
|||
), |
|||
Method(Peer, 'is_encrypted', 'p.is_encrypted', |
|||
boolean=True, |
|||
), |
|||
Method(Peer, 'get_options_str', 'p.get_options_str'), |
|||
Method(Peer, 'get_client_version', 'p.client_version'), |
|||
Method(Peer, 'get_up_total', 'p.get_up_total'), |
|||
|
|||
# MODIFIERS |
|||
] |
@ -0,0 +1,369 @@ |
|||
# Copyright (c) 2013 Chris Lucas, <chris@chrisjlucas.com> |
|||
# Permission is hereby granted, free of charge, to any person obtaining |
|||
# a copy of this software and associated documentation files (the |
|||
# "Software"), to deal in the Software without restriction, including |
|||
# without limitation the rights to use, copy, modify, merge, publish, |
|||
# distribute, sublicense, and/or sell copies of the Software, and to |
|||
# permit persons to whom the Software is furnished to do so, subject to |
|||
# the following conditions: |
|||
# |
|||
# The above copyright notice and this permission notice shall be |
|||
# included in all copies or substantial portions of the Software. |
|||
# |
|||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
|||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
|||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
|||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE |
|||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION |
|||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION |
|||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
|||
from base64 import encodestring |
|||
import httplib |
|||
import inspect |
|||
import string |
|||
|
|||
import rtorrent |
|||
import re |
|||
from rtorrent.common import bool_to_int, convert_version_tuple_to_str,\ |
|||
safe_repr |
|||
from rtorrent.err import RTorrentVersionError, MethodError |
|||
from rtorrent.compat import xmlrpclib |
|||
|
|||
|
|||
class BasicAuthTransport(xmlrpclib.Transport): |
|||
def __init__(self, username=None, password=None): |
|||
xmlrpclib.Transport.__init__(self) |
|||
self.username = username |
|||
self.password = password |
|||
|
|||
def send_auth(self, h): |
|||
if self.username is not None and self.password is not None: |
|||
h.putheader('AUTHORIZATION', "Basic %s" % string.replace( |
|||
encodestring("%s:%s" % (self.username, self.password)), |
|||
"\012", "" |
|||
)) |
|||
|
|||
def single_request(self, host, handler, request_body, verbose=0): |
|||
# issue XML-RPC request |
|||
|
|||
h = self.make_connection(host) |
|||
if verbose: |
|||
h.set_debuglevel(1) |
|||
|
|||
try: |
|||
self.send_request(h, handler, request_body) |
|||
self.send_host(h, host) |
|||
self.send_user_agent(h) |
|||
self.send_auth(h) |
|||
self.send_content(h, request_body) |
|||
|
|||
response = h.getresponse(buffering=True) |
|||
if response.status == 200: |
|||
self.verbose = verbose |
|||
return self.parse_response(response) |
|||
except xmlrpclib.Fault: |
|||
raise |
|||
except Exception: |
|||
self.close() |
|||
raise |
|||
|
|||
#discard any response data and raise exception |
|||
if (response.getheader("content-length", 0)): |
|||
response.read() |
|||
raise xmlrpclib.ProtocolError( |
|||
host + handler, |
|||
response.status, response.reason, |
|||
response.msg, |
|||
) |
|||
|
|||
|
|||
def get_varname(rpc_call): |
|||
"""Transform rpc method into variable name. |
|||
|
|||
@newfield example: Example |
|||
@example: if the name of the rpc method is 'p.get_down_rate', the variable |
|||
name will be 'down_rate' |
|||
""" |
|||
# extract variable name from xmlrpc func name |
|||
r = re.search( |
|||
"([ptdf]\.|system\.|get\_|is\_|set\_)+([^=]*)", rpc_call, re.I) |
|||
if r: |
|||
return(r.groups()[-1]) |
|||
else: |
|||
return(None) |
|||
|
|||
|
|||
def _handle_unavailable_rpc_method(method, rt_obj): |
|||
msg = "Method isn't available." |
|||
if rt_obj._get_client_version_tuple() < method.min_version: |
|||
msg = "This method is only available in " \ |
|||
"RTorrent version v{0} or later".format( |
|||
convert_version_tuple_to_str(method.min_version)) |
|||
|
|||
raise MethodError(msg) |
|||
|
|||
|
|||
class DummyClass: |
|||
def __init__(self): |
|||
pass |
|||
|
|||
|
|||
class Method: |
|||
"""Represents an individual RPC method""" |
|||
|
|||
def __init__(self, _class, method_name, |
|||
rpc_call, docstring=None, varname=None, **kwargs): |
|||
self._class = _class # : Class this method is associated with |
|||
self.class_name = _class.__name__ |
|||
self.method_name = method_name # : name of public-facing method |
|||
self.rpc_call = rpc_call # : name of rpc method |
|||
self.docstring = docstring # : docstring for rpc method (optional) |
|||
self.varname = varname # : variable for the result of the method call, usually set to self.varname |
|||
self.min_version = kwargs.get("min_version", ( |
|||
0, 0, 0)) # : Minimum version of rTorrent required |
|||
self.boolean = kwargs.get("boolean", False) # : returns boolean value? |
|||
self.post_process_func = kwargs.get( |
|||
"post_process_func", None) # : custom post process function |
|||
self.aliases = kwargs.get( |
|||
"aliases", []) # : aliases for method (optional) |
|||
self.required_args = [] |
|||
#: Arguments required when calling the method (not utilized) |
|||
|
|||
self.method_type = self._get_method_type() |
|||
|
|||
if self.varname is None: |
|||
self.varname = get_varname(self.rpc_call) |
|||
assert self.varname is not None, "Couldn't get variable name." |
|||
|
|||
def __repr__(self): |
|||
return safe_repr("Method(method_name='{0}', rpc_call='{1}')", |
|||
self.method_name, self.rpc_call) |
|||
|
|||
def _get_method_type(self): |
|||
"""Determine whether method is a modifier or a retriever""" |
|||
if self.method_name[:4] == "set_": return('m') # modifier |
|||
else: |
|||
return('r') # retriever |
|||
|
|||
def is_modifier(self): |
|||
if self.method_type == 'm': |
|||
return(True) |
|||
else: |
|||
return(False) |
|||
|
|||
def is_retriever(self): |
|||
if self.method_type == 'r': |
|||
return(True) |
|||
else: |
|||
return(False) |
|||
|
|||
def is_available(self, rt_obj): |
|||
if rt_obj._get_client_version_tuple() < self.min_version or \ |
|||
self.rpc_call not in rt_obj._get_rpc_methods(): |
|||
return(False) |
|||
else: |
|||
return(True) |
|||
|
|||
|
|||
class Multicall: |
|||
def __init__(self, class_obj, **kwargs): |
|||
self.class_obj = class_obj |
|||
if class_obj.__class__.__name__ == "RTorrent": |
|||
self.rt_obj = class_obj |
|||
else: |
|||
self.rt_obj = class_obj._rt_obj |
|||
self.calls = [] |
|||
|
|||
def add(self, method, *args): |
|||
"""Add call to multicall |
|||
|
|||
@param method: L{Method} instance or name of raw RPC method |
|||
@type method: Method or str |
|||
|
|||
@param args: call arguments |
|||
""" |
|||
# if a raw rpc method was given instead of a Method instance, |
|||
# try and find the instance for it. And if all else fails, create a |
|||
# dummy Method instance |
|||
if isinstance(method, str): |
|||
result = find_method(method) |
|||
# if result not found |
|||
if result == -1: |
|||
method = Method(DummyClass, method, method) |
|||
else: |
|||
method = result |
|||
|
|||
# ensure method is available before adding |
|||
if not method.is_available(self.rt_obj): |
|||
_handle_unavailable_rpc_method(method, self.rt_obj) |
|||
|
|||
self.calls.append((method, args)) |
|||
|
|||
def list_calls(self): |
|||
for c in self.calls: |
|||
print(c) |
|||
|
|||
def call(self): |
|||
"""Execute added multicall calls |
|||
|
|||
@return: the results (post-processed), in the order they were added |
|||
@rtype: tuple |
|||
""" |
|||
m = xmlrpclib.MultiCall(self.rt_obj._get_conn()) |
|||
for call in self.calls: |
|||
method, args = call |
|||
rpc_call = getattr(method, "rpc_call") |
|||
getattr(m, rpc_call)(*args) |
|||
|
|||
results = m() |
|||
results = tuple(results) |
|||
results_processed = [] |
|||
|
|||
for r, c in zip(results, self.calls): |
|||
method = c[0] # Method instance |
|||
result = process_result(method, r) |
|||
results_processed.append(result) |
|||
# assign result to class_obj |
|||
exists = hasattr(self.class_obj, method.varname) |
|||
if not exists or not inspect.ismethod(getattr(self.class_obj, method.varname)): |
|||
setattr(self.class_obj, method.varname, result) |
|||
|
|||
return(tuple(results_processed)) |
|||
|
|||
|
|||
def call_method(class_obj, method, *args): |
|||
"""Handles single RPC calls |
|||
|
|||
@param class_obj: Peer/File/Torrent/Tracker/RTorrent instance |
|||
@type class_obj: object |
|||
|
|||
@param method: L{Method} instance or name of raw RPC method |
|||
@type method: Method or str |
|||
""" |
|||
if method.is_retriever(): |
|||
args = args[:-1] |
|||
else: |
|||
assert args[-1] is not None, "No argument given." |
|||
|
|||
if class_obj.__class__.__name__ == "RTorrent": |
|||
rt_obj = class_obj |
|||
else: |
|||
rt_obj = class_obj._rt_obj |
|||
|
|||
# check if rpc method is even available |
|||
if not method.is_available(rt_obj): |
|||
_handle_unavailable_rpc_method(method, rt_obj) |
|||
|
|||
m = Multicall(class_obj) |
|||
m.add(method, *args) |
|||
# only added one method, only getting one result back |
|||
ret_value = m.call()[0] |
|||
|
|||
####### OBSOLETE ########################################################## |
|||
# if method.is_retriever(): |
|||
# #value = process_result(method, ret_value) |
|||
# value = ret_value #MultiCall already processed the result |
|||
# else: |
|||
# # we're setting the user's input to method.varname |
|||
# # but we'll return the value that xmlrpc gives us |
|||
# value = process_result(method, args[-1]) |
|||
########################################################################## |
|||
|
|||
return(ret_value) |
|||
|
|||
|
|||
def find_method(rpc_call): |
|||
"""Return L{Method} instance associated with given RPC call""" |
|||
method_lists = [ |
|||
rtorrent.methods, |
|||
rtorrent.file.methods, |
|||
rtorrent.tracker.methods, |
|||
rtorrent.peer.methods, |
|||
rtorrent.torrent.methods, |
|||
] |
|||
|
|||
for l in method_lists: |
|||
for m in l: |
|||
if m.rpc_call.lower() == rpc_call.lower(): |
|||
return(m) |
|||
|
|||
return(-1) |
|||
|
|||
|
|||
def process_result(method, result): |
|||
"""Process given C{B{result}} based on flags set in C{B{method}} |
|||
|
|||
@param method: L{Method} instance |
|||
@type method: Method |
|||
|
|||
@param result: result to be processed (the result of given L{Method} instance) |
|||
|
|||
@note: Supported Processing: |
|||
- boolean - convert ones and zeros returned by rTorrent and |
|||
convert to python boolean values |
|||
""" |
|||
# handle custom post processing function |
|||
if method.post_process_func is not None: |
|||
result = method.post_process_func(result) |
|||
|
|||
# is boolean? |
|||
if method.boolean: |
|||
if result in [1, '1']: |
|||
result = True |
|||
elif result in [0, '0']: |
|||
result = False |
|||
|
|||
return(result) |
|||
|
|||
|
|||
def _build_rpc_methods(class_, method_list): |
|||
"""Build glorified aliases to raw RPC methods""" |
|||
instance = None |
|||
if not inspect.isclass(class_): |
|||
instance = class_ |
|||
class_ = instance.__class__ |
|||
|
|||
for m in method_list: |
|||
class_name = m.class_name |
|||
if class_name != class_.__name__: |
|||
continue |
|||
|
|||
if class_name == "RTorrent": |
|||
caller = lambda self, arg = None, method = m:\ |
|||
call_method(self, method, bool_to_int(arg)) |
|||
elif class_name == "Torrent": |
|||
caller = lambda self, arg = None, method = m:\ |
|||
call_method(self, method, self.rpc_id, |
|||
bool_to_int(arg)) |
|||
elif class_name in ["Tracker", "File"]: |
|||
caller = lambda self, arg = None, method = m:\ |
|||
call_method(self, method, self.rpc_id, |
|||
bool_to_int(arg)) |
|||
|
|||
elif class_name == "Peer": |
|||
caller = lambda self, arg = None, method = m:\ |
|||
call_method(self, method, self.rpc_id, |
|||
bool_to_int(arg)) |
|||
|
|||
elif class_name == "Group": |
|||
caller = lambda arg = None, method = m: \ |
|||
call_method(instance, method, bool_to_int(arg)) |
|||
|
|||
if m.docstring is None: |
|||
m.docstring = "" |
|||
|
|||
# print(m) |
|||
docstring = """{0} |
|||
|
|||
@note: Variable where the result for this method is stored: {1}.{2}""".format( |
|||
m.docstring, |
|||
class_name, |
|||
m.varname) |
|||
|
|||
caller.__doc__ = docstring |
|||
|
|||
for method_name in [m.method_name] + list(m.aliases): |
|||
if instance is None: |
|||
setattr(class_, method_name, caller) |
|||
else: |
|||
setattr(instance, method_name, caller) |
@ -0,0 +1,506 @@ |
|||
# Copyright (c) 2013 Chris Lucas, <chris@chrisjlucas.com> |
|||
# Permission is hereby granted, free of charge, to any person obtaining |
|||
# a copy of this software and associated documentation files (the |
|||
# "Software"), to deal in the Software without restriction, including |
|||
# without limitation the rights to use, copy, modify, merge, publish, |
|||
# distribute, sublicense, and/or sell copies of the Software, and to |
|||
# permit persons to whom the Software is furnished to do so, subject to |
|||
# the following conditions: |
|||
# |
|||
# The above copyright notice and this permission notice shall be |
|||
# included in all copies or substantial portions of the Software. |
|||
# |
|||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
|||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
|||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
|||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE |
|||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION |
|||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION |
|||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
|||
|
|||
import rtorrent.rpc |
|||
# from rtorrent.rpc import Method |
|||
import rtorrent.peer |
|||
import rtorrent.tracker |
|||
import rtorrent.file |
|||
import rtorrent.compat |
|||
|
|||
from rtorrent.common import safe_repr |
|||
|
|||
Peer = rtorrent.peer.Peer |
|||
Tracker = rtorrent.tracker.Tracker |
|||
File = rtorrent.file.File |
|||
Method = rtorrent.rpc.Method |
|||
|
|||
|
|||
class Torrent: |
|||
"""Represents an individual torrent within a L{RTorrent} instance.""" |
|||
|
|||
def __init__(self, _rt_obj, info_hash, **kwargs): |
|||
self._rt_obj = _rt_obj |
|||
self.info_hash = info_hash # : info hash for the torrent |
|||
self.rpc_id = self.info_hash # : unique id to pass to rTorrent |
|||
for k in kwargs.keys(): |
|||
setattr(self, k, kwargs.get(k, None)) |
|||
|
|||
self.peers = [] |
|||
self.trackers = [] |
|||
self.files = [] |
|||
|
|||
self._call_custom_methods() |
|||
|
|||
def __repr__(self): |
|||
return safe_repr("Torrent(info_hash=\"{0}\" name=\"{1}\")", |
|||
self.info_hash, self.name) |
|||
|
|||
def _call_custom_methods(self): |
|||
"""only calls methods that check instance variables.""" |
|||
self._is_hash_checking_queued() |
|||
self._is_started() |
|||
self._is_paused() |
|||
|
|||
def get_peers(self): |
|||
"""Get list of Peer instances for given torrent. |
|||
|
|||
@return: L{Peer} instances |
|||
@rtype: list |
|||
|
|||
@note: also assigns return value to self.peers |
|||
""" |
|||
self.peers = [] |
|||
retriever_methods = [m for m in rtorrent.peer.methods |
|||
if m.is_retriever() and m.is_available(self._rt_obj)] |
|||
# need to leave 2nd arg empty (dunno why) |
|||
m = rtorrent.rpc.Multicall(self) |
|||
m.add("p.multicall", self.info_hash, "", |
|||
*[method.rpc_call + "=" for method in retriever_methods]) |
|||
|
|||
results = m.call()[0] # only sent one call, only need first result |
|||
|
|||
for result in results: |
|||
results_dict = {} |
|||
# build results_dict |
|||
for m, r in zip(retriever_methods, result): |
|||
results_dict[m.varname] = rtorrent.rpc.process_result(m, r) |
|||
|
|||
self.peers.append(Peer( |
|||
self._rt_obj, self.info_hash, **results_dict)) |
|||
|
|||
return(self.peers) |
|||
|
|||
def get_trackers(self): |
|||
"""Get list of Tracker instances for given torrent. |
|||
|
|||
@return: L{Tracker} instances |
|||
@rtype: list |
|||
|
|||
@note: also assigns return value to self.trackers |
|||
""" |
|||
self.trackers = [] |
|||
retriever_methods = [m for m in rtorrent.tracker.methods |
|||
if m.is_retriever() and m.is_available(self._rt_obj)] |
|||
|
|||
# need to leave 2nd arg empty (dunno why) |
|||
m = rtorrent.rpc.Multicall(self) |
|||
m.add("t.multicall", self.info_hash, "", |
|||
*[method.rpc_call + "=" for method in retriever_methods]) |
|||
|
|||
results = m.call()[0] # only sent one call, only need first result |
|||
|
|||
for result in results: |
|||
results_dict = {} |
|||
# build results_dict |
|||
for m, r in zip(retriever_methods, result): |
|||
results_dict[m.varname] = rtorrent.rpc.process_result(m, r) |
|||
|
|||
self.trackers.append(Tracker( |
|||
self._rt_obj, self.info_hash, **results_dict)) |
|||
|
|||
return(self.trackers) |
|||
|
|||
def get_files(self): |
|||
"""Get list of File instances for given torrent. |
|||
|
|||
@return: L{File} instances |
|||
@rtype: list |
|||
|
|||
@note: also assigns return value to self.files |
|||
""" |
|||
|
|||
self.files = [] |
|||
retriever_methods = [m for m in rtorrent.file.methods |
|||
if m.is_retriever() and m.is_available(self._rt_obj)] |
|||
# 2nd arg can be anything, but it'll return all files in torrent |
|||
# regardless |
|||
m = rtorrent.rpc.Multicall(self) |
|||
m.add("f.multicall", self.info_hash, "", |
|||
*[method.rpc_call + "=" for method in retriever_methods]) |
|||
|
|||
results = m.call()[0] # only sent one call, only need first result |
|||
|
|||
offset_method_index = retriever_methods.index( |
|||
rtorrent.rpc.find_method("f.get_offset")) |
|||
|
|||
# make a list of the offsets of all the files, sort appropriately |
|||
offset_list = sorted([r[offset_method_index] for r in results]) |
|||
|
|||
for result in results: |
|||
results_dict = {} |
|||
# build results_dict |
|||
for m, r in zip(retriever_methods, result): |
|||
results_dict[m.varname] = rtorrent.rpc.process_result(m, r) |
|||
|
|||
# get proper index positions for each file (based on the file |
|||
# offset) |
|||
f_index = offset_list.index(results_dict["offset"]) |
|||
|
|||
self.files.append(File(self._rt_obj, self.info_hash, |
|||
f_index, **results_dict)) |
|||
|
|||
return(self.files) |
|||
|
|||
def set_directory(self, d): |
|||
"""Modify download directory |
|||
|
|||
@note: Needs to stop torrent in order to change the directory. |
|||
Also doesn't restart after directory is set, that must be called |
|||
separately. |
|||
""" |
|||
m = rtorrent.rpc.Multicall(self) |
|||
self.multicall_add(m, "d.try_stop") |
|||
self.multicall_add(m, "d.set_directory", d) |
|||
|
|||
self.directory = m.call()[-1] |
|||
|
|||
def start(self): |
|||
"""Start the torrent""" |
|||
m = rtorrent.rpc.Multicall(self) |
|||
self.multicall_add(m, "d.try_start") |
|||
self.multicall_add(m, "d.is_active") |
|||
|
|||
self.active = m.call()[-1] |
|||
return(self.active) |
|||
|
|||
def stop(self): |
|||
""""Stop the torrent""" |
|||
m = rtorrent.rpc.Multicall(self) |
|||
self.multicall_add(m, "d.try_stop") |
|||
self.multicall_add(m, "d.is_active") |
|||
|
|||
self.active = m.call()[-1] |
|||
return(self.active) |
|||
|
|||
def pause(self): |
|||
"""Pause the torrent""" |
|||
m = rtorrent.rpc.Multicall(self) |
|||
self.multicall_add(m, "d.pause") |
|||
|
|||
return(m.call()[-1]) |
|||
|
|||
def resume(self): |
|||
"""Resume the torrent""" |
|||
m = rtorrent.rpc.Multicall(self) |
|||
self.multicall_add(m, "d.resume") |
|||
|
|||
return(m.call()[-1]) |
|||
|
|||
def close(self): |
|||
"""Close the torrent and it's files""" |
|||
m = rtorrent.rpc.Multicall(self) |
|||
self.multicall_add(m, "d.close") |
|||
|
|||
return(m.call()[-1]) |
|||
|
|||
def erase(self): |
|||
"""Delete the torrent |
|||
|
|||
@note: doesn't delete the downloaded files""" |
|||
m = rtorrent.rpc.Multicall(self) |
|||
self.multicall_add(m, "d.erase") |
|||
|
|||
return(m.call()[-1]) |
|||
|
|||
def check_hash(self): |
|||
"""(Re)hash check the torrent""" |
|||
m = rtorrent.rpc.Multicall(self) |
|||
self.multicall_add(m, "d.check_hash") |
|||
|
|||
return(m.call()[-1]) |
|||
|
|||
def poll(self): |
|||
"""poll rTorrent to get latest peer/tracker/file information""" |
|||
self.get_peers() |
|||
self.get_trackers() |
|||
self.get_files() |
|||
|
|||
def update(self): |
|||
"""Refresh torrent data |
|||
|
|||
@note: All fields are stored as attributes to self. |
|||
|
|||
@return: None |
|||
""" |
|||
multicall = rtorrent.rpc.Multicall(self) |
|||
retriever_methods = [m for m in methods |
|||
if m.is_retriever() and m.is_available(self._rt_obj)] |
|||
for method in retriever_methods: |
|||
multicall.add(method, self.rpc_id) |
|||
|
|||
multicall.call() |
|||
|
|||
# custom functions (only call private methods, since they only check |
|||
# local variables and are therefore faster) |
|||
self._call_custom_methods() |
|||
|
|||
def accept_seeders(self, accept_seeds): |
|||
"""Enable/disable whether the torrent connects to seeders |
|||
|
|||
@param accept_seeds: enable/disable accepting seeders |
|||
@type accept_seeds: bool""" |
|||
if accept_seeds: |
|||
call = "d.accepting_seeders.enable" |
|||
else: |
|||
call = "d.accepting_seeders.disable" |
|||
|
|||
m = rtorrent.rpc.Multicall(self) |
|||
self.multicall_add(m, call) |
|||
|
|||
return(m.call()[-1]) |
|||
|
|||
def announce(self): |
|||
"""Announce torrent info to tracker(s)""" |
|||
m = rtorrent.rpc.Multicall(self) |
|||
self.multicall_add(m, "d.tracker_announce") |
|||
|
|||
return(m.call()[-1]) |
|||
|
|||
@staticmethod |
|||
def _assert_custom_key_valid(key): |
|||
assert type(key) == int and key > 0 and key < 6, \ |
|||
"key must be an integer between 1-5" |
|||
|
|||
def get_custom(self, key): |
|||
""" |
|||
Get custom value |
|||
|
|||
@param key: the index for the custom field (between 1-5) |
|||
@type key: int |
|||
|
|||
@rtype: str |
|||
""" |
|||
|
|||
self._assert_custom_key_valid(key) |
|||
m = rtorrent.rpc.Multicall(self) |
|||
|
|||
field = "custom{0}".format(key) |
|||
self.multicall_add(m, "d.get_{0}".format(field)) |
|||
setattr(self, field, m.call()[-1]) |
|||
|
|||
return (getattr(self, field)) |
|||
|
|||
def set_custom(self, key, value): |
|||
""" |
|||
Set custom value |
|||
|
|||
@param key: the index for the custom field (between 1-5) |
|||
@type key: int |
|||
|
|||
@param value: the value to be stored |
|||
@type value: str |
|||
|
|||
@return: if successful, value will be returned |
|||
@rtype: str |
|||
""" |
|||
|
|||
self._assert_custom_key_valid(key) |
|||
m = rtorrent.rpc.Multicall(self) |
|||
|
|||
self.multicall_add(m, "d.set_custom{0}".format(key), value) |
|||
|
|||
return(m.call()[-1]) |
|||
|
|||
def set_visible(self, view, visible=True): |
|||
p = self._rt_obj._get_conn() |
|||
|
|||
if visible: |
|||
return p.view.set_visible(self.info_hash, view) |
|||
else: |
|||
return p.view.set_not_visible(self.info_hash, view) |
|||
|
|||
############################################################################ |
|||
# CUSTOM METHODS (Not part of the official rTorrent API) |
|||
########################################################################## |
|||
def _is_hash_checking_queued(self): |
|||
"""Only checks instance variables, shouldn't be called directly""" |
|||
# if hashing == 3, then torrent is marked for hash checking |
|||
# if hash_checking == False, then torrent is waiting to be checked |
|||
self.hash_checking_queued = (self.hashing == 3 and |
|||
self.hash_checking is False) |
|||
|
|||
return(self.hash_checking_queued) |
|||
|
|||
def is_hash_checking_queued(self): |
|||
"""Check if torrent is waiting to be hash checked |
|||
|
|||
@note: Variable where the result for this method is stored Torrent.hash_checking_queued""" |
|||
m = rtorrent.rpc.Multicall(self) |
|||
self.multicall_add(m, "d.get_hashing") |
|||
self.multicall_add(m, "d.is_hash_checking") |
|||
results = m.call() |
|||
|
|||
setattr(self, "hashing", results[0]) |
|||
setattr(self, "hash_checking", results[1]) |
|||
|
|||
return(self._is_hash_checking_queued()) |
|||
|
|||
def _is_paused(self): |
|||
"""Only checks instance variables, shouldn't be called directly""" |
|||
self.paused = (self.state == 0) |
|||
return(self.paused) |
|||
|
|||
def is_paused(self): |
|||
"""Check if torrent is paused |
|||
|
|||
@note: Variable where the result for this method is stored: Torrent.paused""" |
|||
self.get_state() |
|||
return(self._is_paused()) |
|||
|
|||
def _is_started(self): |
|||
"""Only checks instance variables, shouldn't be called directly""" |
|||
self.started = (self.state == 1) |
|||
return(self.started) |
|||
|
|||
def is_started(self): |
|||
"""Check if torrent is started |
|||
|
|||
@note: Variable where the result for this method is stored: Torrent.started""" |
|||
self.get_state() |
|||
return(self._is_started()) |
|||
|
|||
|
|||
methods = [ |
|||
# RETRIEVERS |
|||
Method(Torrent, 'is_hash_checked', 'd.is_hash_checked', |
|||
boolean=True, |
|||
), |
|||
Method(Torrent, 'is_hash_checking', 'd.is_hash_checking', |
|||
boolean=True, |
|||
), |
|||
Method(Torrent, 'get_peers_max', 'd.get_peers_max'), |
|||
Method(Torrent, 'get_tracker_focus', 'd.get_tracker_focus'), |
|||
Method(Torrent, 'get_skip_total', 'd.get_skip_total'), |
|||
Method(Torrent, 'get_state', 'd.get_state'), |
|||
Method(Torrent, 'get_peer_exchange', 'd.get_peer_exchange'), |
|||
Method(Torrent, 'get_down_rate', 'd.get_down_rate'), |
|||
Method(Torrent, 'get_connection_seed', 'd.get_connection_seed'), |
|||
Method(Torrent, 'get_uploads_max', 'd.get_uploads_max'), |
|||
Method(Torrent, 'get_priority_str', 'd.get_priority_str'), |
|||
Method(Torrent, 'is_open', 'd.is_open', |
|||
boolean=True, |
|||
), |
|||
Method(Torrent, 'get_peers_min', 'd.get_peers_min'), |
|||
Method(Torrent, 'get_peers_complete', 'd.get_peers_complete'), |
|||
Method(Torrent, 'get_tracker_numwant', 'd.get_tracker_numwant'), |
|||
Method(Torrent, 'get_connection_current', 'd.get_connection_current'), |
|||
Method(Torrent, 'is_complete', 'd.get_complete', |
|||
boolean=True, |
|||
), |
|||
Method(Torrent, 'get_peers_connected', 'd.get_peers_connected'), |
|||
Method(Torrent, 'get_chunk_size', 'd.get_chunk_size'), |
|||
Method(Torrent, 'get_state_counter', 'd.get_state_counter'), |
|||
Method(Torrent, 'get_base_filename', 'd.get_base_filename'), |
|||
Method(Torrent, 'get_state_changed', 'd.get_state_changed'), |
|||
Method(Torrent, 'get_peers_not_connected', 'd.get_peers_not_connected'), |
|||
Method(Torrent, 'get_directory', 'd.get_directory'), |
|||
Method(Torrent, 'is_incomplete', 'd.incomplete', |
|||
boolean=True, |
|||
), |
|||
Method(Torrent, 'get_tracker_size', 'd.get_tracker_size'), |
|||
Method(Torrent, 'is_multi_file', 'd.is_multi_file', |
|||
boolean=True, |
|||
), |
|||
Method(Torrent, 'get_local_id', 'd.get_local_id'), |
|||
Method(Torrent, 'get_ratio', 'd.get_ratio', |
|||
post_process_func=lambda x: x / 1000.0, |
|||
), |
|||
Method(Torrent, 'get_loaded_file', 'd.get_loaded_file'), |
|||
Method(Torrent, 'get_max_file_size', 'd.get_max_file_size'), |
|||
Method(Torrent, 'get_size_chunks', 'd.get_size_chunks'), |
|||
Method(Torrent, 'is_pex_active', 'd.is_pex_active', |
|||
boolean=True, |
|||
), |
|||
Method(Torrent, 'get_hashing', 'd.get_hashing'), |
|||
Method(Torrent, 'get_bitfield', 'd.get_bitfield'), |
|||
Method(Torrent, 'get_local_id_html', 'd.get_local_id_html'), |
|||
Method(Torrent, 'get_connection_leech', 'd.get_connection_leech'), |
|||
Method(Torrent, 'get_peers_accounted', 'd.get_peers_accounted'), |
|||
Method(Torrent, 'get_message', 'd.get_message'), |
|||
Method(Torrent, 'is_active', 'd.is_active', |
|||
boolean=True, |
|||
), |
|||
Method(Torrent, 'get_size_bytes', 'd.get_size_bytes'), |
|||
Method(Torrent, 'get_ignore_commands', 'd.get_ignore_commands'), |
|||
Method(Torrent, 'get_creation_date', 'd.get_creation_date'), |
|||
Method(Torrent, 'get_base_path', 'd.get_base_path'), |
|||
Method(Torrent, 'get_left_bytes', 'd.get_left_bytes'), |
|||
Method(Torrent, 'get_size_files', 'd.get_size_files'), |
|||
Method(Torrent, 'get_size_pex', 'd.get_size_pex'), |
|||
Method(Torrent, 'is_private', 'd.is_private', |
|||
boolean=True, |
|||
), |
|||
Method(Torrent, 'get_max_size_pex', 'd.get_max_size_pex'), |
|||
Method(Torrent, 'get_num_chunks_hashed', 'd.get_chunks_hashed', |
|||
aliases=("get_chunks_hashed",)), |
|||
Method(Torrent, 'get_num_chunks_wanted', 'd.wanted_chunks'), |
|||
Method(Torrent, 'get_priority', 'd.get_priority'), |
|||
Method(Torrent, 'get_skip_rate', 'd.get_skip_rate'), |
|||
Method(Torrent, 'get_completed_bytes', 'd.get_completed_bytes'), |
|||
Method(Torrent, 'get_name', 'd.get_name'), |
|||
Method(Torrent, 'get_completed_chunks', 'd.get_completed_chunks'), |
|||
Method(Torrent, 'get_throttle_name', 'd.get_throttle_name'), |
|||
Method(Torrent, 'get_free_diskspace', 'd.get_free_diskspace'), |
|||
Method(Torrent, 'get_directory_base', 'd.get_directory_base'), |
|||
Method(Torrent, 'get_hashing_failed', 'd.get_hashing_failed'), |
|||
Method(Torrent, 'get_tied_to_file', 'd.get_tied_to_file'), |
|||
Method(Torrent, 'get_down_total', 'd.get_down_total'), |
|||
Method(Torrent, 'get_bytes_done', 'd.get_bytes_done'), |
|||
Method(Torrent, 'get_up_rate', 'd.get_up_rate'), |
|||
Method(Torrent, 'get_up_total', 'd.get_up_total'), |
|||
Method(Torrent, 'is_accepting_seeders', 'd.accepting_seeders', |
|||
boolean=True, |
|||
), |
|||
Method(Torrent, "get_chunks_seen", "d.chunks_seen", |
|||
min_version=(0, 9, 1), |
|||
), |
|||
Method(Torrent, "is_partially_done", "d.is_partially_done", |
|||
boolean=True, |
|||
), |
|||
Method(Torrent, "is_not_partially_done", "d.is_not_partially_done", |
|||
boolean=True, |
|||
), |
|||
Method(Torrent, "get_time_started", "d.timestamp.started"), |
|||
Method(Torrent, "get_custom1", "d.get_custom1"), |
|||
Method(Torrent, "get_custom2", "d.get_custom2"), |
|||
Method(Torrent, "get_custom3", "d.get_custom3"), |
|||
Method(Torrent, "get_custom4", "d.get_custom4"), |
|||
Method(Torrent, "get_custom5", "d.get_custom5"), |
|||
|
|||
# MODIFIERS |
|||
Method(Torrent, 'set_uploads_max', 'd.set_uploads_max'), |
|||
Method(Torrent, 'set_tied_to_file', 'd.set_tied_to_file'), |
|||
Method(Torrent, 'set_tracker_numwant', 'd.set_tracker_numwant'), |
|||
Method(Torrent, 'set_priority', 'd.set_priority'), |
|||
Method(Torrent, 'set_peers_max', 'd.set_peers_max'), |
|||
Method(Torrent, 'set_hashing_failed', 'd.set_hashing_failed'), |
|||
Method(Torrent, 'set_message', 'd.set_message'), |
|||
Method(Torrent, 'set_throttle_name', 'd.set_throttle_name'), |
|||
Method(Torrent, 'set_peers_min', 'd.set_peers_min'), |
|||
Method(Torrent, 'set_ignore_commands', 'd.set_ignore_commands'), |
|||
Method(Torrent, 'set_max_file_size', 'd.set_max_file_size'), |
|||
Method(Torrent, 'set_custom5', 'd.set_custom5'), |
|||
Method(Torrent, 'set_custom4', 'd.set_custom4'), |
|||
Method(Torrent, 'set_custom2', 'd.set_custom2'), |
|||
Method(Torrent, 'set_custom1', 'd.set_custom1'), |
|||
Method(Torrent, 'set_custom3', 'd.set_custom3'), |
|||
Method(Torrent, 'set_connection_current', 'd.set_connection_current'), |
|||
] |
@ -0,0 +1,138 @@ |
|||
# Copyright (c) 2013 Chris Lucas, <chris@chrisjlucas.com> |
|||
# Permission is hereby granted, free of charge, to any person obtaining |
|||
# a copy of this software and associated documentation files (the |
|||
# "Software"), to deal in the Software without restriction, including |
|||
# without limitation the rights to use, copy, modify, merge, publish, |
|||
# distribute, sublicense, and/or sell copies of the Software, and to |
|||
# permit persons to whom the Software is furnished to do so, subject to |
|||
# the following conditions: |
|||
# |
|||
# The above copyright notice and this permission notice shall be |
|||
# included in all copies or substantial portions of the Software. |
|||
# |
|||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
|||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
|||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
|||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE |
|||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION |
|||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION |
|||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
|||
|
|||
# from rtorrent.rpc import Method |
|||
import rtorrent.rpc |
|||
|
|||
from rtorrent.common import safe_repr |
|||
|
|||
Method = rtorrent.rpc.Method |
|||
|
|||
|
|||
class Tracker: |
|||
"""Represents an individual tracker within a L{Torrent} instance.""" |
|||
|
|||
def __init__(self, _rt_obj, info_hash, **kwargs): |
|||
self._rt_obj = _rt_obj |
|||
self.info_hash = info_hash # : info hash for the torrent using this tracker |
|||
for k in kwargs.keys(): |
|||
setattr(self, k, kwargs.get(k, None)) |
|||
|
|||
# for clarity's sake... |
|||
self.index = self.group # : position of tracker within the torrent's tracker list |
|||
self.rpc_id = "{0}:t{1}".format( |
|||
self.info_hash, self.index) # : unique id to pass to rTorrent |
|||
|
|||
def __repr__(self): |
|||
return safe_repr("Tracker(index={0}, url=\"{1}\")", |
|||
self.index, self.url) |
|||
|
|||
def enable(self): |
|||
"""Alias for set_enabled("yes")""" |
|||
self.set_enabled("yes") |
|||
|
|||
def disable(self): |
|||
"""Alias for set_enabled("no")""" |
|||
self.set_enabled("no") |
|||
|
|||
def update(self): |
|||
"""Refresh tracker data |
|||
|
|||
@note: All fields are stored as attributes to self. |
|||
|
|||
@return: None |
|||
""" |
|||
multicall = rtorrent.rpc.Multicall(self) |
|||
retriever_methods = [m for m in methods |
|||
if m.is_retriever() and m.is_available(self._rt_obj)] |
|||
for method in retriever_methods: |
|||
multicall.add(method, self.rpc_id) |
|||
|
|||
multicall.call() |
|||
|
|||
methods = [ |
|||
# RETRIEVERS |
|||
Method(Tracker, 'is_enabled', 't.is_enabled', boolean=True), |
|||
Method(Tracker, 'get_id', 't.get_id'), |
|||
Method(Tracker, 'get_scrape_incomplete', 't.get_scrape_incomplete'), |
|||
Method(Tracker, 'is_open', 't.is_open', boolean=True), |
|||
Method(Tracker, 'get_min_interval', 't.get_min_interval'), |
|||
Method(Tracker, 'get_scrape_downloaded', 't.get_scrape_downloaded'), |
|||
Method(Tracker, 'get_group', 't.get_group'), |
|||
Method(Tracker, 'get_scrape_time_last', 't.get_scrape_time_last'), |
|||
Method(Tracker, 'get_type', 't.get_type'), |
|||
Method(Tracker, 'get_normal_interval', 't.get_normal_interval'), |
|||
Method(Tracker, 'get_url', 't.get_url'), |
|||
Method(Tracker, 'get_scrape_complete', 't.get_scrape_complete', |
|||
min_version=(0, 8, 9), |
|||
), |
|||
Method(Tracker, 'get_activity_time_last', 't.activity_time_last', |
|||
min_version=(0, 8, 9), |
|||
), |
|||
Method(Tracker, 'get_activity_time_next', 't.activity_time_next', |
|||
min_version=(0, 8, 9), |
|||
), |
|||
Method(Tracker, 'get_failed_time_last', 't.failed_time_last', |
|||
min_version=(0, 8, 9), |
|||
), |
|||
Method(Tracker, 'get_failed_time_next', 't.failed_time_next', |
|||
min_version=(0, 8, 9), |
|||
), |
|||
Method(Tracker, 'get_success_time_last', 't.success_time_last', |
|||
min_version=(0, 8, 9), |
|||
), |
|||
Method(Tracker, 'get_success_time_next', 't.success_time_next', |
|||
min_version=(0, 8, 9), |
|||
), |
|||
Method(Tracker, 'can_scrape', 't.can_scrape', |
|||
min_version=(0, 9, 1), |
|||
boolean=True |
|||
), |
|||
Method(Tracker, 'get_failed_counter', 't.failed_counter', |
|||
min_version=(0, 8, 9) |
|||
), |
|||
Method(Tracker, 'get_scrape_counter', 't.scrape_counter', |
|||
min_version=(0, 8, 9) |
|||
), |
|||
Method(Tracker, 'get_success_counter', 't.success_counter', |
|||
min_version=(0, 8, 9) |
|||
), |
|||
Method(Tracker, 'is_usable', 't.is_usable', |
|||
min_version=(0, 9, 1), |
|||
boolean=True |
|||
), |
|||
Method(Tracker, 'is_busy', 't.is_busy', |
|||
min_version=(0, 9, 1), |
|||
boolean=True |
|||
), |
|||
Method(Tracker, 'is_extra_tracker', 't.is_extra_tracker', |
|||
min_version=(0, 9, 1), |
|||
boolean=True, |
|||
), |
|||
Method(Tracker, "get_latest_sum_peers", "t.latest_sum_peers", |
|||
min_version=(0, 9, 0) |
|||
), |
|||
Method(Tracker, "get_latest_new_peers", "t.latest_new_peers", |
|||
min_version=(0, 9, 0) |
|||
), |
|||
|
|||
# MODIFIERS |
|||
Method(Tracker, 'set_enabled', 't.set_enabled'), |
|||
] |
@ -0,0 +1,24 @@ |
|||
"""A synchronous implementation of the Deluge RPC protocol |
|||
based on gevent-deluge by Christopher Rosell. |
|||
|
|||
https://github.com/chrippa/gevent-deluge |
|||
|
|||
Example usage: |
|||
|
|||
from synchronousdeluge import DelgueClient |
|||
|
|||
client = DelugeClient() |
|||
client.connect() |
|||
|
|||
# Wait for value |
|||
download_location = client.core.get_config_value("download_location").get() |
|||
""" |
|||
|
|||
|
|||
__title__ = "synchronous-deluge" |
|||
__version__ = "0.1" |
|||
__author__ = "Christian Dale" |
|||
|
|||
from .synchronousdeluge.client import DelugeClient |
|||
from .synchronousdeluge.exceptions import DelugeRPCError |
|||
|
@ -0,0 +1,135 @@ |
|||
import os |
|||
|
|||
from collections import defaultdict |
|||
from itertools import imap |
|||
|
|||
from .synchronousdeluge.exceptions import DelugeRPCError |
|||
from .synchronousdeluge.protocol import DelugeRPCRequest, DelugeRPCResponse |
|||
from .synchronousdeluge.transfer import DelugeTransfer |
|||
|
|||
__all__ = ["DelugeClient"] |
|||
|
|||
|
|||
RPC_RESPONSE = 1 |
|||
RPC_ERROR = 2 |
|||
RPC_EVENT = 3 |
|||
|
|||
|
|||
class DelugeClient(object): |
|||
def __init__(self): |
|||
"""A deluge client session.""" |
|||
self.transfer = DelugeTransfer() |
|||
self.modules = [] |
|||
self._request_counter = 0 |
|||
|
|||
def _get_local_auth(self): |
|||
xdg_config = os.path.expanduser(os.environ.get("XDG_CONFIG_HOME", "~/.config")) |
|||
config_home = os.path.join(xdg_config, "deluge") |
|||
auth_file = os.path.join(config_home, "auth") |
|||
|
|||
username = password = "" |
|||
with open(auth_file) as fd: |
|||
for line in fd: |
|||
if line.startswith("#"): |
|||
continue |
|||
|
|||
auth = line.split(":") |
|||
if len(auth) >= 2 and auth[0] == "localclient": |
|||
username, password = auth[0], auth[1] |
|||
break |
|||
|
|||
return username, password |
|||
|
|||
def _create_module_method(self, module, method): |
|||
fullname = "{0}.{1}".format(module, method) |
|||
|
|||
def func(obj, *args, **kwargs): |
|||
return self.remote_call(fullname, *args, **kwargs) |
|||
|
|||
func.__name__ = method |
|||
|
|||
return func |
|||
|
|||
def _introspect(self): |
|||
self.modules = [] |
|||
|
|||
methods = self.remote_call("daemon.get_method_list").get() |
|||
methodmap = defaultdict(dict) |
|||
splitter = lambda v: v.split(".") |
|||
|
|||
for module, method in imap(splitter, methods): |
|||
methodmap[module][method] = self._create_module_method(module, method) |
|||
|
|||
for module, methods in methodmap.items(): |
|||
clsname = "DelugeModule{0}".format(module.capitalize()) |
|||
cls = type(clsname, (), methods) |
|||
setattr(self, module, cls()) |
|||
self.modules.append(module) |
|||
|
|||
def remote_call(self, method, *args, **kwargs): |
|||
req = DelugeRPCRequest(self._request_counter, method, *args, **kwargs) |
|||
message = next(self.transfer.send_request(req)) |
|||
|
|||
response = DelugeRPCResponse() |
|||
|
|||
if not isinstance(message, tuple): |
|||
return |
|||
|
|||
if len(message) < 3: |
|||
return |
|||
|
|||
message_type = message[0] |
|||
|
|||
# if message_type == RPC_EVENT: |
|||
# event = message[1] |
|||
# values = message[2] |
|||
# |
|||
# if event in self._event_handlers: |
|||
# for handler in self._event_handlers[event]: |
|||
# gevent.spawn(handler, *values) |
|||
# |
|||
# elif message_type in (RPC_RESPONSE, RPC_ERROR): |
|||
if message_type in (RPC_RESPONSE, RPC_ERROR): |
|||
request_id = message[1] |
|||
value = message[2] |
|||
|
|||
if request_id == self._request_counter : |
|||
if message_type == RPC_RESPONSE: |
|||
response.set(value) |
|||
elif message_type == RPC_ERROR: |
|||
err = DelugeRPCError(*value) |
|||
response.set_exception(err) |
|||
|
|||
self._request_counter += 1 |
|||
return response |
|||
|
|||
def connect(self, host="127.0.0.1", port=58846, username="", password=""): |
|||
"""Connects to a daemon process. |
|||
|
|||
:param host: str, the hostname of the daemon |
|||
:param port: int, the port of the daemon |
|||
:param username: str, the username to login with |
|||
:param password: str, the password to login with |
|||
""" |
|||
|
|||
# Connect transport |
|||
self.transfer.connect((host, port)) |
|||
|
|||
# Attempt to fetch local auth info if needed |
|||
if not username and host in ("127.0.0.1", "localhost"): |
|||
username, password = self._get_local_auth() |
|||
|
|||
# Authenticate |
|||
self.remote_call("daemon.login", username, password).get() |
|||
|
|||
# Introspect available methods |
|||
self._introspect() |
|||
|
|||
@property |
|||
def connected(self): |
|||
return self.transfer.connected |
|||
|
|||
def disconnect(self): |
|||
"""Disconnects from the daemon.""" |
|||
self.transfer.disconnect() |
|||
|
@ -0,0 +1,11 @@ |
|||
__all__ = ["DelugeRPCError"] |
|||
|
|||
class DelugeRPCError(Exception): |
|||
def __init__(self, name, msg, traceback): |
|||
self.name = name |
|||
self.msg = msg |
|||
self.traceback = traceback |
|||
|
|||
def __str__(self): |
|||
return "{0}: {1}: {2}".format(self.__class__.__name__, self.name, self.msg) |
|||
|
@ -0,0 +1,38 @@ |
|||
__all__ = ["DelugeRPCRequest", "DelugeRPCResponse"] |
|||
|
|||
class DelugeRPCRequest(object): |
|||
def __init__(self, request_id, method, *args, **kwargs): |
|||
self.request_id = request_id |
|||
self.method = method |
|||
self.args = args |
|||
self.kwargs = kwargs |
|||
|
|||
def format(self): |
|||
return (self.request_id, self.method, self.args, self.kwargs) |
|||
|
|||
class DelugeRPCResponse(object): |
|||
def __init__(self): |
|||
self.value = None |
|||
self._exception = None |
|||
|
|||
def successful(self): |
|||
return self._exception is None |
|||
|
|||
@property |
|||
def exception(self): |
|||
if self._exception is not None: |
|||
return self._exception |
|||
|
|||
def set(self, value=None): |
|||
self.value = value |
|||
self._exception = None |
|||
|
|||
def set_exception(self, exception): |
|||
self._exception = exception |
|||
|
|||
def get(self): |
|||
if self._exception is None: |
|||
return self.value |
|||
else: |
|||
raise self._exception |
|||
|
@ -0,0 +1,433 @@ |
|||
|
|||
""" |
|||
rencode -- Web safe object pickling/unpickling. |
|||
|
|||
Public domain, Connelly Barnes 2006-2007. |
|||
|
|||
The rencode module is a modified version of bencode from the |
|||
BitTorrent project. For complex, heterogeneous data structures with |
|||
many small elements, r-encodings take up significantly less space than |
|||
b-encodings: |
|||
|
|||
>>> len(rencode.dumps({'a':0, 'b':[1,2], 'c':99})) |
|||
13 |
|||
>>> len(bencode.bencode({'a':0, 'b':[1,2], 'c':99})) |
|||
26 |
|||
|
|||
The rencode format is not standardized, and may change with different |
|||
rencode module versions, so you should check that you are using the |
|||
same rencode version throughout your project. |
|||
""" |
|||
|
|||
__version__ = '1.0.1' |
|||
__all__ = ['dumps', 'loads'] |
|||
|
|||
# Original bencode module by Petru Paler, et al. |
|||
# |
|||
# Modifications by Connelly Barnes: |
|||
# |
|||
# - Added support for floats (sent as 32-bit or 64-bit in network |
|||
# order), bools, None. |
|||
# - Allowed dict keys to be of any serializable type. |
|||
# - Lists/tuples are always decoded as tuples (thus, tuples can be |
|||
# used as dict keys). |
|||
# - Embedded extra information in the 'typecodes' to save some space. |
|||
# - Added a restriction on integer length, so that malicious hosts |
|||
# cannot pass us large integers which take a long time to decode. |
|||
# |
|||
# Licensed by Bram Cohen under the "MIT license": |
|||
# |
|||
# "Copyright (C) 2001-2002 Bram Cohen |
|||
# |
|||
# Permission is hereby granted, free of charge, to any person |
|||
# obtaining a copy of this software and associated documentation files |
|||
# (the "Software"), to deal in the Software without restriction, |
|||
# including without limitation the rights to use, copy, modify, merge, |
|||
# publish, distribute, sublicense, and/or sell copies of the Software, |
|||
# and to permit persons to whom the Software is furnished to do so, |
|||
# subject to the following conditions: |
|||
# |
|||
# The above copyright notice and this permission notice shall be |
|||
# included in all copies or substantial portions of the Software. |
|||
# |
|||
# The Software is provided "AS IS", without warranty of any kind, |
|||
# express or implied, including but not limited to the warranties of |
|||
# merchantability, fitness for a particular purpose and |
|||
# noninfringement. In no event shall the authors or copyright holders |
|||
# be liable for any claim, damages or other liability, whether in an |
|||
# action of contract, tort or otherwise, arising from, out of or in |
|||
# connection with the Software or the use or other dealings in the |
|||
# Software." |
|||
# |
|||
# (The rencode module is licensed under the above license as well). |
|||
# |
|||
|
|||
import struct |
|||
import string |
|||
from threading import Lock |
|||
|
|||
# Default number of bits for serialized floats, either 32 or 64 (also a parameter for dumps()). |
|||
DEFAULT_FLOAT_BITS = 32 |
|||
|
|||
# Maximum length of integer when written as base 10 string. |
|||
MAX_INT_LENGTH = 64 |
|||
|
|||
# The bencode 'typecodes' such as i, d, etc have been extended and |
|||
# relocated on the base-256 character set. |
|||
CHR_LIST = chr(59) |
|||
CHR_DICT = chr(60) |
|||
CHR_INT = chr(61) |
|||
CHR_INT1 = chr(62) |
|||
CHR_INT2 = chr(63) |
|||
CHR_INT4 = chr(64) |
|||
CHR_INT8 = chr(65) |
|||
CHR_FLOAT32 = chr(66) |
|||
CHR_FLOAT64 = chr(44) |
|||
CHR_TRUE = chr(67) |
|||
CHR_FALSE = chr(68) |
|||
CHR_NONE = chr(69) |
|||
CHR_TERM = chr(127) |
|||
|
|||
# Positive integers with value embedded in typecode. |
|||
INT_POS_FIXED_START = 0 |
|||
INT_POS_FIXED_COUNT = 44 |
|||
|
|||
# Dictionaries with length embedded in typecode. |
|||
DICT_FIXED_START = 102 |
|||
DICT_FIXED_COUNT = 25 |
|||
|
|||
# Negative integers with value embedded in typecode. |
|||
INT_NEG_FIXED_START = 70 |
|||
INT_NEG_FIXED_COUNT = 32 |
|||
|
|||
# Strings with length embedded in typecode. |
|||
STR_FIXED_START = 128 |
|||
STR_FIXED_COUNT = 64 |
|||
|
|||
# Lists with length embedded in typecode. |
|||
LIST_FIXED_START = STR_FIXED_START+STR_FIXED_COUNT |
|||
LIST_FIXED_COUNT = 64 |
|||
|
|||
def decode_int(x, f): |
|||
f += 1 |
|||
newf = x.index(CHR_TERM, f) |
|||
if newf - f >= MAX_INT_LENGTH: |
|||
raise ValueError('overflow') |
|||
try: |
|||
n = int(x[f:newf]) |
|||
except (OverflowError, ValueError): |
|||
n = long(x[f:newf]) |
|||
if x[f] == '-': |
|||
if x[f + 1] == '0': |
|||
raise ValueError |
|||
elif x[f] == '0' and newf != f+1: |
|||
raise ValueError |
|||
return (n, newf+1) |
|||
|
|||
def decode_intb(x, f): |
|||
f += 1 |
|||
return (struct.unpack('!b', x[f:f+1])[0], f+1) |
|||
|
|||
def decode_inth(x, f): |
|||
f += 1 |
|||
return (struct.unpack('!h', x[f:f+2])[0], f+2) |
|||
|
|||
def decode_intl(x, f): |
|||
f += 1 |
|||
return (struct.unpack('!l', x[f:f+4])[0], f+4) |
|||
|
|||
def decode_intq(x, f): |
|||
f += 1 |
|||
return (struct.unpack('!q', x[f:f+8])[0], f+8) |
|||
|
|||
def decode_float32(x, f): |
|||
f += 1 |
|||
n = struct.unpack('!f', x[f:f+4])[0] |
|||
return (n, f+4) |
|||
|
|||
def decode_float64(x, f): |
|||
f += 1 |
|||
n = struct.unpack('!d', x[f:f+8])[0] |
|||
return (n, f+8) |
|||
|
|||
def decode_string(x, f): |
|||
colon = x.index(':', f) |
|||
try: |
|||
n = int(x[f:colon]) |
|||
except (OverflowError, ValueError): |
|||
n = long(x[f:colon]) |
|||
if x[f] == '0' and colon != f+1: |
|||
raise ValueError |
|||
colon += 1 |
|||
s = x[colon:colon+n] |
|||
try: |
|||
t = s.decode("utf8") |
|||
if len(t) != len(s): |
|||
s = t |
|||
except UnicodeDecodeError: |
|||
pass |
|||
return (s, colon+n) |
|||
|
|||
def decode_list(x, f): |
|||
r, f = [], f+1 |
|||
while x[f] != CHR_TERM: |
|||
v, f = decode_func[x[f]](x, f) |
|||
r.append(v) |
|||
return (tuple(r), f + 1) |
|||
|
|||
def decode_dict(x, f): |
|||
r, f = {}, f+1 |
|||
while x[f] != CHR_TERM: |
|||
k, f = decode_func[x[f]](x, f) |
|||
r[k], f = decode_func[x[f]](x, f) |
|||
return (r, f + 1) |
|||
|
|||
def decode_true(x, f): |
|||
return (True, f+1) |
|||
|
|||
def decode_false(x, f): |
|||
return (False, f+1) |
|||
|
|||
def decode_none(x, f): |
|||
return (None, f+1) |
|||
|
|||
decode_func = {} |
|||
decode_func['0'] = decode_string |
|||
decode_func['1'] = decode_string |
|||
decode_func['2'] = decode_string |
|||
decode_func['3'] = decode_string |
|||
decode_func['4'] = decode_string |
|||
decode_func['5'] = decode_string |
|||
decode_func['6'] = decode_string |
|||
decode_func['7'] = decode_string |
|||
decode_func['8'] = decode_string |
|||
decode_func['9'] = decode_string |
|||
decode_func[CHR_LIST ] = decode_list |
|||
decode_func[CHR_DICT ] = decode_dict |
|||
decode_func[CHR_INT ] = decode_int |
|||
decode_func[CHR_INT1 ] = decode_intb |
|||
decode_func[CHR_INT2 ] = decode_inth |
|||
decode_func[CHR_INT4 ] = decode_intl |
|||
decode_func[CHR_INT8 ] = decode_intq |
|||
decode_func[CHR_FLOAT32] = decode_float32 |
|||
decode_func[CHR_FLOAT64] = decode_float64 |
|||
decode_func[CHR_TRUE ] = decode_true |
|||
decode_func[CHR_FALSE ] = decode_false |
|||
decode_func[CHR_NONE ] = decode_none |
|||
|
|||
def make_fixed_length_string_decoders(): |
|||
def make_decoder(slen): |
|||
def f(x, f): |
|||
s = x[f+1:f+1+slen] |
|||
try: |
|||
t = s.decode("utf8") |
|||
if len(t) != len(s): |
|||
s = t |
|||
except UnicodeDecodeError: |
|||
pass |
|||
return (s, f+1+slen) |
|||
return f |
|||
for i in range(STR_FIXED_COUNT): |
|||
decode_func[chr(STR_FIXED_START+i)] = make_decoder(i) |
|||
|
|||
make_fixed_length_string_decoders() |
|||
|
|||
def make_fixed_length_list_decoders(): |
|||
def make_decoder(slen): |
|||
def f(x, f): |
|||
r, f = [], f+1 |
|||
for i in range(slen): |
|||
v, f = decode_func[x[f]](x, f) |
|||
r.append(v) |
|||
return (tuple(r), f) |
|||
return f |
|||
for i in range(LIST_FIXED_COUNT): |
|||
decode_func[chr(LIST_FIXED_START+i)] = make_decoder(i) |
|||
|
|||
make_fixed_length_list_decoders() |
|||
|
|||
def make_fixed_length_int_decoders(): |
|||
def make_decoder(j): |
|||
def f(x, f): |
|||
return (j, f+1) |
|||
return f |
|||
for i in range(INT_POS_FIXED_COUNT): |
|||
decode_func[chr(INT_POS_FIXED_START+i)] = make_decoder(i) |
|||
for i in range(INT_NEG_FIXED_COUNT): |
|||
decode_func[chr(INT_NEG_FIXED_START+i)] = make_decoder(-1-i) |
|||
|
|||
make_fixed_length_int_decoders() |
|||
|
|||
def make_fixed_length_dict_decoders(): |
|||
def make_decoder(slen): |
|||
def f(x, f): |
|||
r, f = {}, f+1 |
|||
for j in range(slen): |
|||
k, f = decode_func[x[f]](x, f) |
|||
r[k], f = decode_func[x[f]](x, f) |
|||
return (r, f) |
|||
return f |
|||
for i in range(DICT_FIXED_COUNT): |
|||
decode_func[chr(DICT_FIXED_START+i)] = make_decoder(i) |
|||
|
|||
make_fixed_length_dict_decoders() |
|||
|
|||
def encode_dict(x,r): |
|||
r.append(CHR_DICT) |
|||
for k, v in x.items(): |
|||
encode_func[type(k)](k, r) |
|||
encode_func[type(v)](v, r) |
|||
r.append(CHR_TERM) |
|||
|
|||
|
|||
def loads(x): |
|||
try: |
|||
r, l = decode_func[x[0]](x, 0) |
|||
except (IndexError, KeyError): |
|||
raise ValueError |
|||
if l != len(x): |
|||
raise ValueError |
|||
return r |
|||
|
|||
from types import StringType, IntType, LongType, DictType, ListType, TupleType, FloatType, NoneType, UnicodeType |
|||
|
|||
def encode_int(x, r): |
|||
if 0 <= x < INT_POS_FIXED_COUNT: |
|||
r.append(chr(INT_POS_FIXED_START+x)) |
|||
elif -INT_NEG_FIXED_COUNT <= x < 0: |
|||
r.append(chr(INT_NEG_FIXED_START-1-x)) |
|||
elif -128 <= x < 128: |
|||
r.extend((CHR_INT1, struct.pack('!b', x))) |
|||
elif -32768 <= x < 32768: |
|||
r.extend((CHR_INT2, struct.pack('!h', x))) |
|||
elif -2147483648 <= x < 2147483648: |
|||
r.extend((CHR_INT4, struct.pack('!l', x))) |
|||
elif -9223372036854775808 <= x < 9223372036854775808: |
|||
r.extend((CHR_INT8, struct.pack('!q', x))) |
|||
else: |
|||
s = str(x) |
|||
if len(s) >= MAX_INT_LENGTH: |
|||
raise ValueError('overflow') |
|||
r.extend((CHR_INT, s, CHR_TERM)) |
|||
|
|||
def encode_float32(x, r): |
|||
r.extend((CHR_FLOAT32, struct.pack('!f', x))) |
|||
|
|||
def encode_float64(x, r): |
|||
r.extend((CHR_FLOAT64, struct.pack('!d', x))) |
|||
|
|||
def encode_bool(x, r): |
|||
r.extend({False: CHR_FALSE, True: CHR_TRUE}[bool(x)]) |
|||
|
|||
def encode_none(x, r): |
|||
r.extend(CHR_NONE) |
|||
|
|||
def encode_string(x, r): |
|||
if len(x) < STR_FIXED_COUNT: |
|||
r.extend((chr(STR_FIXED_START + len(x)), x)) |
|||
else: |
|||
r.extend((str(len(x)), ':', x)) |
|||
|
|||
def encode_unicode(x, r): |
|||
encode_string(x.encode("utf8"), r) |
|||
|
|||
def encode_list(x, r): |
|||
if len(x) < LIST_FIXED_COUNT: |
|||
r.append(chr(LIST_FIXED_START + len(x))) |
|||
for i in x: |
|||
encode_func[type(i)](i, r) |
|||
else: |
|||
r.append(CHR_LIST) |
|||
for i in x: |
|||
encode_func[type(i)](i, r) |
|||
r.append(CHR_TERM) |
|||
|
|||
def encode_dict(x,r): |
|||
if len(x) < DICT_FIXED_COUNT: |
|||
r.append(chr(DICT_FIXED_START + len(x))) |
|||
for k, v in x.items(): |
|||
encode_func[type(k)](k, r) |
|||
encode_func[type(v)](v, r) |
|||
else: |
|||
r.append(CHR_DICT) |
|||
for k, v in x.items(): |
|||
encode_func[type(k)](k, r) |
|||
encode_func[type(v)](v, r) |
|||
r.append(CHR_TERM) |
|||
|
|||
encode_func = {} |
|||
encode_func[IntType] = encode_int |
|||
encode_func[LongType] = encode_int |
|||
encode_func[StringType] = encode_string |
|||
encode_func[ListType] = encode_list |
|||
encode_func[TupleType] = encode_list |
|||
encode_func[DictType] = encode_dict |
|||
encode_func[NoneType] = encode_none |
|||
encode_func[UnicodeType] = encode_unicode |
|||
|
|||
lock = Lock() |
|||
|
|||
try: |
|||
from types import BooleanType |
|||
encode_func[BooleanType] = encode_bool |
|||
except ImportError: |
|||
pass |
|||
|
|||
def dumps(x, float_bits=DEFAULT_FLOAT_BITS): |
|||
""" |
|||
Dump data structure to str. |
|||
|
|||
Here float_bits is either 32 or 64. |
|||
""" |
|||
lock.acquire() |
|||
try: |
|||
if float_bits == 32: |
|||
encode_func[FloatType] = encode_float32 |
|||
elif float_bits == 64: |
|||
encode_func[FloatType] = encode_float64 |
|||
else: |
|||
raise ValueError('Float bits (%d) is not 32 or 64' % float_bits) |
|||
r = [] |
|||
encode_func[type(x)](x, r) |
|||
finally: |
|||
lock.release() |
|||
return ''.join(r) |
|||
|
|||
def test(): |
|||
f1 = struct.unpack('!f', struct.pack('!f', 25.5))[0] |
|||
f2 = struct.unpack('!f', struct.pack('!f', 29.3))[0] |
|||
f3 = struct.unpack('!f', struct.pack('!f', -0.6))[0] |
|||
L = (({'a':15, 'bb':f1, 'ccc':f2, '':(f3,(),False,True,'')},('a',10**20),tuple(range(-100000,100000)),'b'*31,'b'*62,'b'*64,2**30,2**33,2**62,2**64,2**30,2**33,2**62,2**64,False,False, True, -1, 2, 0),) |
|||
assert loads(dumps(L)) == L |
|||
d = dict(zip(range(-100000,100000),range(-100000,100000))) |
|||
d.update({'a':20, 20:40, 40:41, f1:f2, f2:f3, f3:False, False:True, True:False}) |
|||
L = (d, {}, {5:6}, {7:7,True:8}, {9:10, 22:39, 49:50, 44: ''}) |
|||
assert loads(dumps(L)) == L |
|||
L = ('', 'a'*10, 'a'*100, 'a'*1000, 'a'*10000, 'a'*100000, 'a'*1000000, 'a'*10000000) |
|||
assert loads(dumps(L)) == L |
|||
L = tuple([dict(zip(range(n),range(n))) for n in range(100)]) + ('b',) |
|||
assert loads(dumps(L)) == L |
|||
L = tuple([dict(zip(range(n),range(-n,0))) for n in range(100)]) + ('b',) |
|||
assert loads(dumps(L)) == L |
|||
L = tuple([tuple(range(n)) for n in range(100)]) + ('b',) |
|||
assert loads(dumps(L)) == L |
|||
L = tuple(['a'*n for n in range(1000)]) + ('b',) |
|||
assert loads(dumps(L)) == L |
|||
L = tuple(['a'*n for n in range(1000)]) + (None,True,None) |
|||
assert loads(dumps(L)) == L |
|||
assert loads(dumps(None)) == None |
|||
assert loads(dumps({None:None})) == {None:None} |
|||
assert 1e-10<abs(loads(dumps(1.1))-1.1)<1e-6 |
|||
assert 1e-10<abs(loads(dumps(1.1,32))-1.1)<1e-6 |
|||
assert abs(loads(dumps(1.1,64))-1.1)<1e-12 |
|||
assert loads(dumps(u"Hello World!!")) |
|||
try: |
|||
import psyco |
|||
psyco.bind(dumps) |
|||
psyco.bind(loads) |
|||
except ImportError: |
|||
pass |
|||
|
|||
|
|||
if __name__ == '__main__': |
|||
test() |
@ -0,0 +1,57 @@ |
|||
import zlib |
|||
import struct |
|||
import socket |
|||
import ssl |
|||
|
|||
from synchronousdeluge import rencode |
|||
|
|||
|
|||
__all__ = ["DelugeTransfer"] |
|||
|
|||
class DelugeTransfer(object): |
|||
def __init__(self): |
|||
self.sock = None |
|||
self.conn = None |
|||
self.connected = False |
|||
|
|||
def connect(self, hostport): |
|||
if self.connected: |
|||
self.disconnect() |
|||
|
|||
self.sock = socket.create_connection(hostport) |
|||
self.conn = ssl.wrap_socket(self.sock) |
|||
self.connected = True |
|||
|
|||
def disconnect(self): |
|||
if self.conn: |
|||
self.conn.close() |
|||
self.connected = False |
|||
|
|||
def send_request(self, request): |
|||
data = (request.format(),) |
|||
payload = zlib.compress(rencode.dumps(data)) |
|||
self.conn.sendall(payload) |
|||
|
|||
buf = b"" |
|||
|
|||
while True: |
|||
data = self.conn.recv(1024) |
|||
|
|||
if not data: |
|||
self.connected = False |
|||
break |
|||
|
|||
buf += data |
|||
dobj = zlib.decompressobj() |
|||
|
|||
try: |
|||
message = rencode.loads(dobj.decompress(buf)) |
|||
except (ValueError, zlib.error, struct.error): |
|||
# Probably incomplete data, read more |
|||
continue |
|||
else: |
|||
buf = dobj.unused_data |
|||
|
|||
yield message |
|||
|
|||
|
@ -0,0 +1,27 @@ |
|||
Metadata-Version: 1.0 |
|||
Name: pyUnRAR2 |
|||
Version: 0.99.2 |
|||
Summary: Improved Python wrapper around the free UnRAR.dll |
|||
Home-page: http://code.google.com/py-unrar2 |
|||
Author: Konstantin Yegupov |
|||
Author-email: yk4ever@gmail.com |
|||
License: MIT |
|||
Description: pyUnRAR2 is a ctypes based wrapper around the free UnRAR.dll. |
|||
|
|||
It is an modified version of Jimmy Retzlaff's pyUnRAR - more simple, |
|||
stable and foolproof. |
|||
Notice that it has INCOMPATIBLE interface. |
|||
|
|||
It enables reading and unpacking of archives created with the |
|||
RAR/WinRAR archivers. There is a low-level interface which is very |
|||
similar to the C interface provided by UnRAR. There is also a |
|||
higher level interface which makes some common operations easier. |
|||
Platform: Windows |
|||
Classifier: Development Status :: 4 - Beta |
|||
Classifier: Environment :: Win32 (MS Windows) |
|||
Classifier: License :: OSI Approved :: MIT License |
|||
Classifier: Natural Language :: English |
|||
Classifier: Operating System :: Microsoft :: Windows |
|||
Classifier: Programming Language :: Python |
|||
Classifier: Topic :: Software Development :: Libraries :: Python Modules |
|||
Classifier: Topic :: System :: Archiving :: Compression |
@ -0,0 +1,191 @@ |
|||
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> |
|||
<html><head><title>Python: package UnRAR2</title> |
|||
</head><body bgcolor="#f0f0f8"> |
|||
|
|||
<table width="100%" cellspacing=0 cellpadding=2 border=0 summary="heading"> |
|||
<tr bgcolor="#7799ee"> |
|||
<td valign=bottom> <br> |
|||
<font color="#ffffff" face="helvetica, arial"> <br><big><big><strong>UnRAR2</strong></big></big> (version 0.99.1)</font></td |
|||
><td align=right valign=bottom |
|||
><font color="#ffffff" face="helvetica, arial"><a href=".">index</a><br><a href="file:///C|/python26/lib/site-packages/unrar2/__init__.py">c:\python26\lib\site-packages\unrar2\__init__.py</a></font></td></tr></table> |
|||
<p><tt>pyUnRAR2 is a ctypes based wrapper around the free UnRAR.dll. <br> |
|||
<br> |
|||
It is an modified version of Jimmy Retzlaff's pyUnRAR - more simple,<br> |
|||
stable and foolproof.<br> |
|||
Notice that it has INCOMPATIBLE interface.<br> |
|||
<br> |
|||
It enables reading and unpacking of archives created with the<br> |
|||
RAR/WinRAR archivers. There is a low-level interface which is very<br> |
|||
similar to the C interface provided by UnRAR. There is also a<br> |
|||
higher level interface which makes some common operations easier.</tt></p> |
|||
<p> |
|||
<table width="100%" cellspacing=0 cellpadding=2 border=0 summary="section"> |
|||
<tr bgcolor="#aa55cc"> |
|||
<td colspan=3 valign=bottom> <br> |
|||
<font color="#ffffff" face="helvetica, arial"><big><strong>Package Contents</strong></big></font></td></tr> |
|||
|
|||
<tr><td bgcolor="#aa55cc"><tt> </tt></td><td> </td> |
|||
<td width="100%"><table width="100%" summary="list"><tr><td width="25%" valign=top><a href="UnRAR2.rar_exceptions.html">rar_exceptions</a><br> |
|||
<a href="UnRAR2.setup.html">setup</a><br> |
|||
</td><td width="25%" valign=top><a href="UnRAR2.test_UnRAR2.html">test_UnRAR2</a><br> |
|||
<a href="UnRAR2.unix.html">unix</a><br> |
|||
</td><td width="25%" valign=top><a href="UnRAR2.windows.html">windows</a><br> |
|||
</td><td width="25%" valign=top></td></tr></table></td></tr></table><p> |
|||
<table width="100%" cellspacing=0 cellpadding=2 border=0 summary="section"> |
|||
<tr bgcolor="#ee77aa"> |
|||
<td colspan=3 valign=bottom> <br> |
|||
<font color="#ffffff" face="helvetica, arial"><big><strong>Classes</strong></big></font></td></tr> |
|||
|
|||
<tr><td bgcolor="#ee77aa"><tt> </tt></td><td> </td> |
|||
<td width="100%"><dl> |
|||
<dt><font face="helvetica, arial"><a href="UnRAR2.windows.html#RarFileImplementation">UnRAR2.windows.RarFileImplementation</a>(<a href="__builtin__.html#object">__builtin__.object</a>) |
|||
</font></dt><dd> |
|||
<dl> |
|||
<dt><font face="helvetica, arial"><a href="UnRAR2.html#RarFile">RarFile</a> |
|||
</font></dt></dl> |
|||
</dd> |
|||
<dt><font face="helvetica, arial"><a href="__builtin__.html#object">__builtin__.object</a> |
|||
</font></dt><dd> |
|||
<dl> |
|||
<dt><font face="helvetica, arial"><a href="UnRAR2.html#RarInfo">RarInfo</a> |
|||
</font></dt></dl> |
|||
</dd> |
|||
</dl> |
|||
<p> |
|||
<table width="100%" cellspacing=0 cellpadding=2 border=0 summary="section"> |
|||
<tr bgcolor="#ffc8d8"> |
|||
<td colspan=3 valign=bottom> <br> |
|||
<font color="#000000" face="helvetica, arial"><a name="RarFile">class <strong>RarFile</strong></a>(<a href="UnRAR2.windows.html#RarFileImplementation">UnRAR2.windows.RarFileImplementation</a>)</font></td></tr> |
|||
|
|||
<tr><td bgcolor="#ffc8d8"><tt> </tt></td><td> </td> |
|||
<td width="100%"><dl><dt>Method resolution order:</dt> |
|||
<dd><a href="UnRAR2.html#RarFile">RarFile</a></dd> |
|||
<dd><a href="UnRAR2.windows.html#RarFileImplementation">UnRAR2.windows.RarFileImplementation</a></dd> |
|||
<dd><a href="__builtin__.html#object">__builtin__.object</a></dd> |
|||
</dl> |
|||
<hr> |
|||
Methods defined here:<br> |
|||
<dl><dt><a name="RarFile-__del__"><strong>__del__</strong></a>(self)</dt></dl> |
|||
|
|||
<dl><dt><a name="RarFile-__init__"><strong>__init__</strong></a>(self, archiveName, password<font color="#909090">=None</font>)</dt><dd><tt>Instantiate the archive.<br> |
|||
<br> |
|||
archiveName is the name of the RAR file.<br> |
|||
password is used to decrypt the files in the archive.<br> |
|||
<br> |
|||
Properties:<br> |
|||
comment - comment associated with the archive<br> |
|||
<br> |
|||
>>> print <a href="#RarFile">RarFile</a>('test.rar').comment<br> |
|||
This is a test.</tt></dd></dl> |
|||
|
|||
<dl><dt><a name="RarFile-extract"><strong>extract</strong></a>(self, condition<font color="#909090">='*'</font>, path<font color="#909090">='.'</font>, withSubpath<font color="#909090">=True</font>, overwrite<font color="#909090">=True</font>)</dt><dd><tt>Extract specific files from archive to disk.<br> |
|||
<br> |
|||
If "condition" is a list of numbers, then extract files which have those positions in infolist.<br> |
|||
If "condition" is a string, then it is treated as a wildcard for names of files to extract.<br> |
|||
If "condition" is a function, it is treated as a callback function, which accepts a <a href="#RarInfo">RarInfo</a> <a href="__builtin__.html#object">object</a><br> |
|||
and returns either boolean True (extract) or boolean False (skip).<br> |
|||
DEPRECATED: If "condition" callback returns string (only supported for Windows) - <br> |
|||
that string will be used as a new name to save the file under.<br> |
|||
If "condition" is omitted, all files are extracted.<br> |
|||
<br> |
|||
"path" is a directory to extract to<br> |
|||
"withSubpath" flag denotes whether files are extracted with their full path in the archive.<br> |
|||
"overwrite" flag denotes whether extracted files will overwrite old ones. Defaults to true.<br> |
|||
<br> |
|||
Returns list of RarInfos for extracted files.</tt></dd></dl> |
|||
|
|||
<dl><dt><a name="RarFile-infoiter"><strong>infoiter</strong></a>(self)</dt><dd><tt>Iterate over all the files in the archive, generating RarInfos.<br> |
|||
<br> |
|||
>>> import os<br> |
|||
>>> for fileInArchive in <a href="#RarFile">RarFile</a>('test.rar').<a href="#RarFile-infoiter">infoiter</a>():<br> |
|||
... print os.path.split(fileInArchive.filename)[-1],<br> |
|||
... print fileInArchive.isdir,<br> |
|||
... print fileInArchive.size,<br> |
|||
... print fileInArchive.comment,<br> |
|||
... print tuple(fileInArchive.datetime)[0:5],<br> |
|||
... print time.strftime('%a, %d %b %Y %H:%M', fileInArchive.datetime)<br> |
|||
test True 0 None (2003, 6, 30, 1, 59) Mon, 30 Jun 2003 01:59<br> |
|||
test.txt False 20 None (2003, 6, 30, 2, 1) Mon, 30 Jun 2003 02:01<br> |
|||
this.py False 1030 None (2002, 2, 8, 16, 47) Fri, 08 Feb 2002 16:47</tt></dd></dl> |
|||
|
|||
<dl><dt><a name="RarFile-infolist"><strong>infolist</strong></a>(self)</dt><dd><tt>Return a list of RarInfos, descripting the contents of the archive.</tt></dd></dl> |
|||
|
|||
<dl><dt><a name="RarFile-read_files"><strong>read_files</strong></a>(self, condition<font color="#909090">='*'</font>)</dt><dd><tt>Read specific files from archive into memory.<br> |
|||
If "condition" is a list of numbers, then return files which have those positions in infolist.<br> |
|||
If "condition" is a string, then it is treated as a wildcard for names of files to extract.<br> |
|||
If "condition" is a function, it is treated as a callback function, which accepts a <a href="#RarInfo">RarInfo</a> <a href="__builtin__.html#object">object</a> <br> |
|||
and returns boolean True (extract) or False (skip).<br> |
|||
If "condition" is omitted, all files are returned.<br> |
|||
<br> |
|||
Returns list of tuples (<a href="#RarInfo">RarInfo</a> info, str contents)</tt></dd></dl> |
|||
|
|||
<hr> |
|||
Methods inherited from <a href="UnRAR2.windows.html#RarFileImplementation">UnRAR2.windows.RarFileImplementation</a>:<br> |
|||
<dl><dt><a name="RarFile-destruct"><strong>destruct</strong></a>(self)</dt></dl> |
|||
|
|||
<dl><dt><a name="RarFile-init"><strong>init</strong></a>(self, password<font color="#909090">=None</font>)</dt></dl> |
|||
|
|||
<dl><dt><a name="RarFile-make_sure_ready"><strong>make_sure_ready</strong></a>(self)</dt></dl> |
|||
|
|||
<hr> |
|||
Data descriptors inherited from <a href="UnRAR2.windows.html#RarFileImplementation">UnRAR2.windows.RarFileImplementation</a>:<br> |
|||
<dl><dt><strong>__dict__</strong></dt> |
|||
<dd><tt>dictionary for instance variables (if defined)</tt></dd> |
|||
</dl> |
|||
<dl><dt><strong>__weakref__</strong></dt> |
|||
<dd><tt>list of weak references to the object (if defined)</tt></dd> |
|||
</dl> |
|||
</td></tr></table> <p> |
|||
<table width="100%" cellspacing=0 cellpadding=2 border=0 summary="section"> |
|||
<tr bgcolor="#ffc8d8"> |
|||
<td colspan=3 valign=bottom> <br> |
|||
<font color="#000000" face="helvetica, arial"><a name="RarInfo">class <strong>RarInfo</strong></a>(<a href="__builtin__.html#object">__builtin__.object</a>)</font></td></tr> |
|||
|
|||
<tr bgcolor="#ffc8d8"><td rowspan=2><tt> </tt></td> |
|||
<td colspan=2><tt>Represents a file header in an archive. Don't instantiate directly.<br> |
|||
Use only to obtain information about file.<br> |
|||
YOU CANNOT EXTRACT FILE CONTENTS USING THIS OBJECT.<br> |
|||
USE METHODS OF <a href="#RarFile">RarFile</a> CLASS INSTEAD.<br> |
|||
<br> |
|||
Properties:<br> |
|||
index - index of file within the archive<br> |
|||
filename - name of the file in the archive including path (if any)<br> |
|||
datetime - file date/time as a struct_time suitable for time.strftime<br> |
|||
isdir - True if the file is a directory<br> |
|||
size - size in bytes of the uncompressed file<br> |
|||
comment - comment associated with the file<br> |
|||
<br> |
|||
Note - this is not currently intended to be a Python file-like <a href="__builtin__.html#object">object</a>.<br> </tt></td></tr> |
|||
<tr><td> </td> |
|||
<td width="100%">Methods defined here:<br> |
|||
<dl><dt><a name="RarInfo-__init__"><strong>__init__</strong></a>(self, rarfile, data)</dt></dl> |
|||
|
|||
<dl><dt><a name="RarInfo-__str__"><strong>__str__</strong></a>(self)</dt></dl> |
|||
|
|||
<hr> |
|||
Data descriptors defined here:<br> |
|||
<dl><dt><strong>__dict__</strong></dt> |
|||
<dd><tt>dictionary for instance variables (if defined)</tt></dd> |
|||
</dl> |
|||
<dl><dt><strong>__weakref__</strong></dt> |
|||
<dd><tt>list of weak references to the object (if defined)</tt></dd> |
|||
</dl> |
|||
</td></tr></table></td></tr></table><p> |
|||
<table width="100%" cellspacing=0 cellpadding=2 border=0 summary="section"> |
|||
<tr bgcolor="#eeaa77"> |
|||
<td colspan=3 valign=bottom> <br> |
|||
<font color="#ffffff" face="helvetica, arial"><big><strong>Functions</strong></big></font></td></tr> |
|||
|
|||
<tr><td bgcolor="#eeaa77"><tt> </tt></td><td> </td> |
|||
<td width="100%"><dl><dt><a name="-condition2checker"><strong>condition2checker</strong></a>(condition)</dt><dd><tt>Converts different condition types to callback</tt></dd></dl> |
|||
</td></tr></table><p> |
|||
<table width="100%" cellspacing=0 cellpadding=2 border=0 summary="section"> |
|||
<tr bgcolor="#55aa55"> |
|||
<td colspan=3 valign=bottom> <br> |
|||
<font color="#ffffff" face="helvetica, arial"><big><strong>Data</strong></big></font></td></tr> |
|||
|
|||
<tr><td bgcolor="#55aa55"><tt> </tt></td><td> </td> |
|||
<td width="100%"><strong>__version__</strong> = '0.99.1'<br> |
|||
<strong>in_windows</strong> = True</td></tr></table> |
|||
</body></html> |
@ -0,0 +1,18 @@ |
|||
The unrar.dll library is freeware. This means: |
|||
|
|||
1. All copyrights to RAR and the unrar.dll are exclusively |
|||
owned by the author - Alexander Roshal. |
|||
|
|||
2. The unrar.dll library may be used in any software to handle RAR |
|||
archives without limitations free of charge. |
|||
|
|||
3. THE RAR ARCHIVER AND THE UNRAR.DLL LIBRARY ARE DISTRIBUTED "AS IS". |
|||
NO WARRANTY OF ANY KIND IS EXPRESSED OR IMPLIED. YOU USE AT |
|||
YOUR OWN RISK. THE AUTHOR WILL NOT BE LIABLE FOR DATA LOSS, |
|||
DAMAGES, LOSS OF PROFITS OR ANY OTHER KIND OF LOSS WHILE USING |
|||
OR MISUSING THIS SOFTWARE. |
|||
|
|||
Thank you for your interest in RAR and unrar.dll. |
|||
|
|||
|
|||
Alexander L. Roshal |
Binary file not shown.
@ -0,0 +1,140 @@ |
|||
#ifndef _UNRAR_DLL_ |
|||
#define _UNRAR_DLL_ |
|||
|
|||
#define ERAR_END_ARCHIVE 10 |
|||
#define ERAR_NO_MEMORY 11 |
|||
#define ERAR_BAD_DATA 12 |
|||
#define ERAR_BAD_ARCHIVE 13 |
|||
#define ERAR_UNKNOWN_FORMAT 14 |
|||
#define ERAR_EOPEN 15 |
|||
#define ERAR_ECREATE 16 |
|||
#define ERAR_ECLOSE 17 |
|||
#define ERAR_EREAD 18 |
|||
#define ERAR_EWRITE 19 |
|||
#define ERAR_SMALL_BUF 20 |
|||
#define ERAR_UNKNOWN 21 |
|||
#define ERAR_MISSING_PASSWORD 22 |
|||
|
|||
#define RAR_OM_LIST 0 |
|||
#define RAR_OM_EXTRACT 1 |
|||
#define RAR_OM_LIST_INCSPLIT 2 |
|||
|
|||
#define RAR_SKIP 0 |
|||
#define RAR_TEST 1 |
|||
#define RAR_EXTRACT 2 |
|||
|
|||
#define RAR_VOL_ASK 0 |
|||
#define RAR_VOL_NOTIFY 1 |
|||
|
|||
#define RAR_DLL_VERSION 4 |
|||
|
|||
#ifdef _UNIX |
|||
#define CALLBACK |
|||
#define PASCAL |
|||
#define LONG long |
|||
#define HANDLE void * |
|||
#define LPARAM long |
|||
#define UINT unsigned int |
|||
#endif |
|||
|
|||
struct RARHeaderData |
|||
{ |
|||
char ArcName[260]; |
|||
char FileName[260]; |
|||
unsigned int Flags; |
|||
unsigned int PackSize; |
|||
unsigned int UnpSize; |
|||
unsigned int HostOS; |
|||
unsigned int FileCRC; |
|||
unsigned int FileTime; |
|||
unsigned int UnpVer; |
|||
unsigned int Method; |
|||
unsigned int FileAttr; |
|||
char *CmtBuf; |
|||
unsigned int CmtBufSize; |
|||
unsigned int CmtSize; |
|||
unsigned int CmtState; |
|||
}; |
|||
|
|||
|
|||
struct RARHeaderDataEx |
|||
{ |
|||
char ArcName[1024]; |
|||
wchar_t ArcNameW[1024]; |
|||
char FileName[1024]; |
|||
wchar_t FileNameW[1024]; |
|||
unsigned int Flags; |
|||
unsigned int PackSize; |
|||
unsigned int PackSizeHigh; |
|||
unsigned int UnpSize; |
|||
unsigned int UnpSizeHigh; |
|||
unsigned int HostOS; |
|||
unsigned int FileCRC; |
|||
unsigned int FileTime; |
|||
unsigned int UnpVer; |
|||
unsigned int Method; |
|||
unsigned int FileAttr; |
|||
char *CmtBuf; |
|||
unsigned int CmtBufSize; |
|||
unsigned int CmtSize; |
|||
unsigned int CmtState; |
|||
unsigned int Reserved[1024]; |
|||
}; |
|||
|
|||
|
|||
struct RAROpenArchiveData |
|||
{ |
|||
char *ArcName; |
|||
unsigned int OpenMode; |
|||
unsigned int OpenResult; |
|||
char *CmtBuf; |
|||
unsigned int CmtBufSize; |
|||
unsigned int CmtSize; |
|||
unsigned int CmtState; |
|||
}; |
|||
|
|||
struct RAROpenArchiveDataEx |
|||
{ |
|||
char *ArcName; |
|||
wchar_t *ArcNameW; |
|||
unsigned int OpenMode; |
|||
unsigned int OpenResult; |
|||
char *CmtBuf; |
|||
unsigned int CmtBufSize; |
|||
unsigned int CmtSize; |
|||
unsigned int CmtState; |
|||
unsigned int Flags; |
|||
unsigned int Reserved[32]; |
|||
}; |
|||
|
|||
enum UNRARCALLBACK_MESSAGES { |
|||
UCM_CHANGEVOLUME,UCM_PROCESSDATA,UCM_NEEDPASSWORD |
|||
}; |
|||
|
|||
typedef int (CALLBACK *UNRARCALLBACK)(UINT msg,LPARAM UserData,LPARAM P1,LPARAM P2); |
|||
|
|||
typedef int (PASCAL *CHANGEVOLPROC)(char *ArcName,int Mode); |
|||
typedef int (PASCAL *PROCESSDATAPROC)(unsigned char *Addr,int Size); |
|||
|
|||
#ifdef __cplusplus |
|||
extern "C" { |
|||
#endif |
|||
|
|||
HANDLE PASCAL RAROpenArchive(struct RAROpenArchiveData *ArchiveData); |
|||
HANDLE PASCAL RAROpenArchiveEx(struct RAROpenArchiveDataEx *ArchiveData); |
|||
int PASCAL RARCloseArchive(HANDLE hArcData); |
|||
int PASCAL RARReadHeader(HANDLE hArcData,struct RARHeaderData *HeaderData); |
|||
int PASCAL RARReadHeaderEx(HANDLE hArcData,struct RARHeaderDataEx *HeaderData); |
|||
int PASCAL RARProcessFile(HANDLE hArcData,int Operation,char *DestPath,char *DestName); |
|||
int PASCAL RARProcessFileW(HANDLE hArcData,int Operation,wchar_t *DestPath,wchar_t *DestName); |
|||
void PASCAL RARSetCallback(HANDLE hArcData,UNRARCALLBACK Callback,LPARAM UserData); |
|||
void PASCAL RARSetChangeVolProc(HANDLE hArcData,CHANGEVOLPROC ChangeVolProc); |
|||
void PASCAL RARSetProcessDataProc(HANDLE hArcData,PROCESSDATAPROC ProcessDataProc); |
|||
void PASCAL RARSetPassword(HANDLE hArcData,char *Password); |
|||
int PASCAL RARGetDllVersion(); |
|||
|
|||
#ifdef __cplusplus |
|||
} |
|||
#endif |
|||
|
|||
#endif |
Binary file not shown.
@ -0,0 +1,606 @@ |
|||
|
|||
UnRAR.dll Manual |
|||
~~~~~~~~~~~~~~~~ |
|||
|
|||
UnRAR.dll is a 32-bit Windows dynamic-link library which provides |
|||
file extraction from RAR archives. |
|||
|
|||
|
|||
Exported functions |
|||
|
|||
==================================================================== |
|||
HANDLE PASCAL RAROpenArchive(struct RAROpenArchiveData *ArchiveData) |
|||
==================================================================== |
|||
|
|||
Description |
|||
~~~~~~~~~~~ |
|||
Open RAR archive and allocate memory structures |
|||
|
|||
Parameters |
|||
~~~~~~~~~~ |
|||
ArchiveData Points to RAROpenArchiveData structure |
|||
|
|||
struct RAROpenArchiveData |
|||
{ |
|||
char *ArcName; |
|||
UINT OpenMode; |
|||
UINT OpenResult; |
|||
char *CmtBuf; |
|||
UINT CmtBufSize; |
|||
UINT CmtSize; |
|||
UINT CmtState; |
|||
}; |
|||
|
|||
Structure fields: |
|||
|
|||
ArcName |
|||
Input parameter which should point to zero terminated string |
|||
containing the archive name. |
|||
|
|||
OpenMode |
|||
Input parameter. |
|||
|
|||
Possible values |
|||
|
|||
RAR_OM_LIST |
|||
Open archive for reading file headers only. |
|||
|
|||
RAR_OM_EXTRACT |
|||
Open archive for testing and extracting files. |
|||
|
|||
RAR_OM_LIST_INCSPLIT |
|||
Open archive for reading file headers only. If you open an archive |
|||
in such mode, RARReadHeader[Ex] will return all file headers, |
|||
including those with "file continued from previous volume" flag. |
|||
In case of RAR_OM_LIST such headers are automatically skipped. |
|||
So if you process RAR volumes in RAR_OM_LIST_INCSPLIT mode, you will |
|||
get several file header records for same file if file is split between |
|||
volumes. For such files only the last file header record will contain |
|||
the correct file CRC and if you wish to get the correct packed size, |
|||
you need to sum up packed sizes of all parts. |
|||
|
|||
OpenResult |
|||
Output parameter. |
|||
|
|||
Possible values |
|||
|
|||
0 Success |
|||
ERAR_NO_MEMORY Not enough memory to initialize data structures |
|||
ERAR_BAD_DATA Archive header broken |
|||
ERAR_BAD_ARCHIVE File is not valid RAR archive |
|||
ERAR_UNKNOWN_FORMAT Unknown encryption used for archive headers |
|||
ERAR_EOPEN File open error |
|||
|
|||
CmtBuf |
|||
Input parameter which should point to the buffer for archive |
|||
comments. Maximum comment size is limited to 64Kb. Comment text is |
|||
zero terminated. If the comment text is larger than the buffer |
|||
size, the comment text will be truncated. If CmtBuf is set to |
|||
NULL, comments will not be read. |
|||
|
|||
CmtBufSize |
|||
Input parameter which should contain size of buffer for archive |
|||
comments. |
|||
|
|||
CmtSize |
|||
Output parameter containing size of comments actually read into the |
|||
buffer, cannot exceed CmtBufSize. |
|||
|
|||
CmtState |
|||
Output parameter. |
|||
|
|||
Possible values |
|||
|
|||
0 comments not present |
|||
1 Comments read completely |
|||
ERAR_NO_MEMORY Not enough memory to extract comments |
|||
ERAR_BAD_DATA Broken comment |
|||
ERAR_UNKNOWN_FORMAT Unknown comment format |
|||
ERAR_SMALL_BUF Buffer too small, comments not completely read |
|||
|
|||
Return values |
|||
~~~~~~~~~~~~~ |
|||
Archive handle or NULL in case of error |
|||
|
|||
|
|||
======================================================================== |
|||
HANDLE PASCAL RAROpenArchiveEx(struct RAROpenArchiveDataEx *ArchiveData) |
|||
======================================================================== |
|||
|
|||
Description |
|||
~~~~~~~~~~~ |
|||
Similar to RAROpenArchive, but uses RAROpenArchiveDataEx structure |
|||
allowing to specify Unicode archive name and returning information |
|||
about archive flags. |
|||
|
|||
Parameters |
|||
~~~~~~~~~~ |
|||
ArchiveData Points to RAROpenArchiveDataEx structure |
|||
|
|||
struct RAROpenArchiveDataEx |
|||
{ |
|||
char *ArcName; |
|||
wchar_t *ArcNameW; |
|||
unsigned int OpenMode; |
|||
unsigned int OpenResult; |
|||
char *CmtBuf; |
|||
unsigned int CmtBufSize; |
|||
unsigned int CmtSize; |
|||
unsigned int CmtState; |
|||
unsigned int Flags; |
|||
unsigned int Reserved[32]; |
|||
}; |
|||
|
|||
Structure fields: |
|||
|
|||
ArcNameW |
|||
Input parameter which should point to zero terminated Unicode string |
|||
containing the archive name or NULL if Unicode name is not specified. |
|||
|
|||
Flags |
|||
Output parameter. Combination of bit flags. |
|||
|
|||
Possible values |
|||
|
|||
0x0001 - Volume attribute (archive volume) |
|||
0x0002 - Archive comment present |
|||
0x0004 - Archive lock attribute |
|||
0x0008 - Solid attribute (solid archive) |
|||
0x0010 - New volume naming scheme ('volname.partN.rar') |
|||
0x0020 - Authenticity information present |
|||
0x0040 - Recovery record present |
|||
0x0080 - Block headers are encrypted |
|||
0x0100 - First volume (set only by RAR 3.0 and later) |
|||
|
|||
Reserved[32] |
|||
Reserved for future use. Must be zero. |
|||
|
|||
Information on other structure fields and function return values |
|||
is available above, in RAROpenArchive function description. |
|||
|
|||
|
|||
==================================================================== |
|||
int PASCAL RARCloseArchive(HANDLE hArcData) |
|||
==================================================================== |
|||
|
|||
Description |
|||
~~~~~~~~~~~ |
|||
Close RAR archive and release allocated memory. It must be called when |
|||
archive processing is finished, even if the archive processing was stopped |
|||
due to an error. |
|||
|
|||
Parameters |
|||
~~~~~~~~~~ |
|||
hArcData |
|||
This parameter should contain the archive handle obtained from the |
|||
RAROpenArchive function call. |
|||
|
|||
Return values |
|||
~~~~~~~~~~~~~ |
|||
0 Success |
|||
ERAR_ECLOSE Archive close error |
|||
|
|||
|
|||
==================================================================== |
|||
int PASCAL RARReadHeader(HANDLE hArcData, |
|||
struct RARHeaderData *HeaderData) |
|||
==================================================================== |
|||
|
|||
Description |
|||
~~~~~~~~~~~ |
|||
Read header of file in archive. |
|||
|
|||
Parameters |
|||
~~~~~~~~~~ |
|||
hArcData |
|||
This parameter should contain the archive handle obtained from the |
|||
RAROpenArchive function call. |
|||
|
|||
HeaderData |
|||
It should point to RARHeaderData structure: |
|||
|
|||
struct RARHeaderData |
|||
{ |
|||
char ArcName[260]; |
|||
char FileName[260]; |
|||
UINT Flags; |
|||
UINT PackSize; |
|||
UINT UnpSize; |
|||
UINT HostOS; |
|||
UINT FileCRC; |
|||
UINT FileTime; |
|||
UINT UnpVer; |
|||
UINT Method; |
|||
UINT FileAttr; |
|||
char *CmtBuf; |
|||
UINT CmtBufSize; |
|||
UINT CmtSize; |
|||
UINT CmtState; |
|||
}; |
|||
|
|||
Structure fields: |
|||
|
|||
ArcName |
|||
Output parameter which contains a zero terminated string of the |
|||
current archive name. May be used to determine the current volume |
|||
name. |
|||
|
|||
FileName |
|||
Output parameter which contains a zero terminated string of the |
|||
file name in OEM (DOS) encoding. |
|||
|
|||
Flags |
|||
Output parameter which contains file flags: |
|||
|
|||
0x01 - file continued from previous volume |
|||
0x02 - file continued on next volume |
|||
0x04 - file encrypted with password |
|||
0x08 - file comment present |
|||
0x10 - compression of previous files is used (solid flag) |
|||
|
|||
bits 7 6 5 |
|||
|
|||
0 0 0 - dictionary size 64 Kb |
|||
0 0 1 - dictionary size 128 Kb |
|||
0 1 0 - dictionary size 256 Kb |
|||
0 1 1 - dictionary size 512 Kb |
|||
1 0 0 - dictionary size 1024 Kb |
|||
1 0 1 - dictionary size 2048 KB |
|||
1 1 0 - dictionary size 4096 KB |
|||
1 1 1 - file is directory |
|||
|
|||
Other bits are reserved. |
|||
|
|||
PackSize |
|||
Output parameter means packed file size or size of the |
|||
file part if file was split between volumes. |
|||
|
|||
UnpSize |
|||
Output parameter - unpacked file size. |
|||
|
|||
HostOS |
|||
Output parameter - operating system used for archiving: |
|||
|
|||
0 - MS DOS; |
|||
1 - OS/2. |
|||
2 - Win32 |
|||
3 - Unix |
|||
|
|||
FileCRC |
|||
Output parameter which contains unpacked file CRC. In case of file parts |
|||
split between volumes only the last part contains the correct CRC |
|||
and it is accessible only in RAR_OM_LIST_INCSPLIT listing mode. |
|||
|
|||
FileTime |
|||
Output parameter - contains date and time in standard MS DOS format. |
|||
|
|||
UnpVer |
|||
Output parameter - RAR version needed to extract file. |
|||
It is encoded as 10 * Major version + minor version. |
|||
|
|||
Method |
|||
Output parameter - packing method. |
|||
|
|||
FileAttr |
|||
Output parameter - file attributes. |
|||
|
|||
CmtBuf |
|||
File comments support is not implemented in the new DLL version yet. |
|||
Now CmtState is always 0. |
|||
|
|||
/* |
|||
* Input parameter which should point to the buffer for file |
|||
* comments. Maximum comment size is limited to 64Kb. Comment text is |
|||
* a zero terminated string in OEM encoding. If the comment text is |
|||
* larger than the buffer size, the comment text will be truncated. |
|||
* If CmtBuf is set to NULL, comments will not be read. |
|||
*/ |
|||
|
|||
CmtBufSize |
|||
Input parameter which should contain size of buffer for archive |
|||
comments. |
|||
|
|||
CmtSize |
|||
Output parameter containing size of comments actually read into the |
|||
buffer, should not exceed CmtBufSize. |
|||
|
|||
CmtState |
|||
Output parameter. |
|||
|
|||
Possible values |
|||
|
|||
0 Absent comments |
|||
1 Comments read completely |
|||
ERAR_NO_MEMORY Not enough memory to extract comments |
|||
ERAR_BAD_DATA Broken comment |
|||
ERAR_UNKNOWN_FORMAT Unknown comment format |
|||
ERAR_SMALL_BUF Buffer too small, comments not completely read |
|||
|
|||
Return values |
|||
~~~~~~~~~~~~~ |
|||
|
|||
0 Success |
|||
ERAR_END_ARCHIVE End of archive |
|||
ERAR_BAD_DATA File header broken |
|||
|
|||
|
|||
==================================================================== |
|||
int PASCAL RARReadHeaderEx(HANDLE hArcData, |
|||
struct RARHeaderDataEx *HeaderData) |
|||
==================================================================== |
|||
|
|||
Description |
|||
~~~~~~~~~~~ |
|||
Similar to RARReadHeader, but uses RARHeaderDataEx structure, |
|||
containing information about Unicode file names and 64 bit file sizes. |
|||
|
|||
struct RARHeaderDataEx |
|||
{ |
|||
char ArcName[1024]; |
|||
wchar_t ArcNameW[1024]; |
|||
char FileName[1024]; |
|||
wchar_t FileNameW[1024]; |
|||
unsigned int Flags; |
|||
unsigned int PackSize; |
|||
unsigned int PackSizeHigh; |
|||
unsigned int UnpSize; |
|||
unsigned int UnpSizeHigh; |
|||
unsigned int HostOS; |
|||
unsigned int FileCRC; |
|||
unsigned int FileTime; |
|||
unsigned int UnpVer; |
|||
unsigned int Method; |
|||
unsigned int FileAttr; |
|||
char *CmtBuf; |
|||
unsigned int CmtBufSize; |
|||
unsigned int CmtSize; |
|||
unsigned int CmtState; |
|||
unsigned int Reserved[1024]; |
|||
}; |
|||
|
|||
|
|||
==================================================================== |
|||
int PASCAL RARProcessFile(HANDLE hArcData, |
|||
int Operation, |
|||
char *DestPath, |
|||
char *DestName) |
|||
==================================================================== |
|||
|
|||
Description |
|||
~~~~~~~~~~~ |
|||
Performs action and moves the current position in the archive to |
|||
the next file. Extract or test the current file from the archive |
|||
opened in RAR_OM_EXTRACT mode. If the mode RAR_OM_LIST is set, |
|||
then a call to this function will simply skip the archive position |
|||
to the next file. |
|||
|
|||
Parameters |
|||
~~~~~~~~~~ |
|||
hArcData |
|||
This parameter should contain the archive handle obtained from the |
|||
RAROpenArchive function call. |
|||
|
|||
Operation |
|||
File operation. |
|||
|
|||
Possible values |
|||
|
|||
RAR_SKIP Move to the next file in the archive. If the |
|||
archive is solid and RAR_OM_EXTRACT mode was set |
|||
when the archive was opened, the current file will |
|||
be processed - the operation will be performed |
|||
slower than a simple seek. |
|||
|
|||
RAR_TEST Test the current file and move to the next file in |
|||
the archive. If the archive was opened with |
|||
RAR_OM_LIST mode, the operation is equal to |
|||
RAR_SKIP. |
|||
|
|||
RAR_EXTRACT Extract the current file and move to the next file. |
|||
If the archive was opened with RAR_OM_LIST mode, |
|||
the operation is equal to RAR_SKIP. |
|||
|
|||
|
|||
DestPath |
|||
This parameter should point to a zero terminated string containing the |
|||
destination directory to which to extract files to. If DestPath is equal |
|||
to NULL, it means extract to the current directory. This parameter has |
|||
meaning only if DestName is NULL. |
|||
|
|||
DestName |
|||
This parameter should point to a string containing the full path and name |
|||
to assign to extracted file or it can be NULL to use the default name. |
|||
If DestName is defined (not NULL), it overrides both the original file |
|||
name saved in the archive and path specigied in DestPath setting. |
|||
|
|||
Both DestPath and DestName must be in OEM encoding. If necessary, |
|||
use CharToOem to convert text to OEM before passing to this function. |
|||
|
|||
Return values |
|||
~~~~~~~~~~~~~ |
|||
0 Success |
|||
ERAR_BAD_DATA File CRC error |
|||
ERAR_BAD_ARCHIVE Volume is not valid RAR archive |
|||
ERAR_UNKNOWN_FORMAT Unknown archive format |
|||
ERAR_EOPEN Volume open error |
|||
ERAR_ECREATE File create error |
|||
ERAR_ECLOSE File close error |
|||
ERAR_EREAD Read error |
|||
ERAR_EWRITE Write error |
|||
|
|||
|
|||
Note: if you wish to cancel extraction, return -1 when processing |
|||
UCM_PROCESSDATA callback message. |
|||
|
|||
|
|||
==================================================================== |
|||
int PASCAL RARProcessFileW(HANDLE hArcData, |
|||
int Operation, |
|||
wchar_t *DestPath, |
|||
wchar_t *DestName) |
|||
==================================================================== |
|||
|
|||
Description |
|||
~~~~~~~~~~~ |
|||
Unicode version of RARProcessFile. It uses Unicode DestPath |
|||
and DestName parameters, other parameters and return values |
|||
are the same as in RARProcessFile. |
|||
|
|||
|
|||
==================================================================== |
|||
void PASCAL RARSetCallback(HANDLE hArcData, |
|||
int PASCAL (*CallbackProc)(UINT msg,LPARAM UserData,LPARAM P1,LPARAM P2), |
|||
LPARAM UserData); |
|||
==================================================================== |
|||
|
|||
Description |
|||
~~~~~~~~~~~ |
|||
Set a user-defined callback function to process Unrar events. |
|||
|
|||
Parameters |
|||
~~~~~~~~~~ |
|||
hArcData |
|||
This parameter should contain the archive handle obtained from the |
|||
RAROpenArchive function call. |
|||
|
|||
CallbackProc |
|||
It should point to a user-defined callback function. |
|||
|
|||
The function will be passed four parameters: |
|||
|
|||
|
|||
msg Type of event. Described below. |
|||
|
|||
UserData User defined value passed to RARSetCallback. |
|||
|
|||
P1 and P2 Event dependent parameters. Described below. |
|||
|
|||
|
|||
Possible events |
|||
|
|||
UCM_CHANGEVOLUME Process volume change. |
|||
|
|||
P1 Points to the zero terminated name |
|||
of the next volume. |
|||
|
|||
P2 The function call mode: |
|||
|
|||
RAR_VOL_ASK Required volume is absent. The function should |
|||
prompt user and return a positive value |
|||
to retry or return -1 value to terminate |
|||
operation. The function may also specify a new |
|||
volume name, placing it to the address specified |
|||
by P1 parameter. |
|||
|
|||
RAR_VOL_NOTIFY Required volume is successfully opened. |
|||
This is a notification call and volume name |
|||
modification is not allowed. The function should |
|||
return a positive value to continue or -1 |
|||
to terminate operation. |
|||
|
|||
UCM_PROCESSDATA Process unpacked data. It may be used to read |
|||
a file while it is being extracted or tested |
|||
without actual extracting file to disk. |
|||
Return a positive value to continue process |
|||
or -1 to cancel the archive operation |
|||
|
|||
P1 Address pointing to the unpacked data. |
|||
Function may refer to the data but must not |
|||
change it. |
|||
|
|||
P2 Size of the unpacked data. It is guaranteed |
|||
only that the size will not exceed the maximum |
|||
dictionary size (4 Mb in RAR 3.0). |
|||
|
|||
UCM_NEEDPASSWORD DLL needs a password to process archive. |
|||
This message must be processed if you wish |
|||
to be able to handle archives with encrypted |
|||
file names. It can be also used as replacement |
|||
of RARSetPassword function even for usual |
|||
encrypted files with non-encrypted names. |
|||
|
|||
P1 Address pointing to the buffer for a password. |
|||
You need to copy a password here. |
|||
|
|||
P2 Size of the password buffer. |
|||
|
|||
|
|||
UserData |
|||
User data passed to callback function. |
|||
|
|||
Other functions of UnRAR.dll should not be called from the callback |
|||
function. |
|||
|
|||
Return values |
|||
~~~~~~~~~~~~~ |
|||
None |
|||
|
|||
|
|||
|
|||
==================================================================== |
|||
void PASCAL RARSetChangeVolProc(HANDLE hArcData, |
|||
int PASCAL (*ChangeVolProc)(char *ArcName,int Mode)); |
|||
==================================================================== |
|||
|
|||
Obsoleted, use RARSetCallback instead. |
|||
|
|||
|
|||
|
|||
==================================================================== |
|||
void PASCAL RARSetProcessDataProc(HANDLE hArcData, |
|||
int PASCAL (*ProcessDataProc)(unsigned char *Addr,int Size)) |
|||
==================================================================== |
|||
|
|||
Obsoleted, use RARSetCallback instead. |
|||
|
|||
|
|||
==================================================================== |
|||
void PASCAL RARSetPassword(HANDLE hArcData, |
|||
char *Password); |
|||
==================================================================== |
|||
|
|||
Description |
|||
~~~~~~~~~~~ |
|||
Set a password to decrypt files. |
|||
|
|||
Parameters |
|||
~~~~~~~~~~ |
|||
hArcData |
|||
This parameter should contain the archive handle obtained from the |
|||
RAROpenArchive function call. |
|||
|
|||
Password |
|||
It should point to a string containing a zero terminated password. |
|||
|
|||
Return values |
|||
~~~~~~~~~~~~~ |
|||
None |
|||
|
|||
|
|||
==================================================================== |
|||
void PASCAL RARGetDllVersion(); |
|||
==================================================================== |
|||
|
|||
Description |
|||
~~~~~~~~~~~ |
|||
Returns API version. |
|||
|
|||
Parameters |
|||
~~~~~~~~~~ |
|||
None. |
|||
|
|||
Return values |
|||
~~~~~~~~~~~~~ |
|||
Returns an integer value denoting UnRAR.dll API version, which is also |
|||
defined in unrar.h as RAR_DLL_VERSION. API version number is incremented |
|||
only in case of noticeable changes in UnRAR.dll API. Do not confuse it |
|||
with version of UnRAR.dll stored in DLL resources, which is incremented |
|||
with every DLL rebuild. |
|||
|
|||
If RARGetDllVersion() returns a value lower than UnRAR.dll which your |
|||
application was designed for, it may indicate that DLL version is too old |
|||
and it will fail to provide all necessary functions to your application. |
|||
|
|||
This function is absent in old versions of UnRAR.dll, so it is safer |
|||
to use LoadLibrary and GetProcAddress to access this function. |
|||
|
@ -0,0 +1,80 @@ |
|||
List of unrar.dll API changes. We do not include performance and reliability |
|||
improvements into this list, but this library and RAR/UnRAR tools share |
|||
the same source code. So the latest version of unrar.dll usually contains |
|||
same decompression algorithm changes as the latest UnRAR version. |
|||
============================================================================ |
|||
|
|||
-- 18 January 2008 |
|||
|
|||
all LONG parameters of CallbackProc function were changed |
|||
to LPARAM type for 64 bit mode compatibility. |
|||
|
|||
|
|||
-- 12 December 2007 |
|||
|
|||
Added new RAR_OM_LIST_INCSPLIT open mode for function RAROpenArchive. |
|||
|
|||
|
|||
-- 14 August 2007 |
|||
|
|||
Added NoCrypt\unrar_nocrypt.dll without decryption code for those |
|||
applications where presence of encryption or decryption code is not |
|||
allowed because of legal restrictions. |
|||
|
|||
|
|||
-- 14 December 2006 |
|||
|
|||
Added ERAR_MISSING_PASSWORD error type. This error is returned |
|||
if empty password is specified for encrypted file. |
|||
|
|||
|
|||
-- 12 June 2003 |
|||
|
|||
Added RARProcessFileW function, Unicode version of RARProcessFile |
|||
|
|||
|
|||
-- 9 August 2002 |
|||
|
|||
Added RAROpenArchiveEx function allowing to specify Unicode archive |
|||
name and get archive flags. |
|||
|
|||
|
|||
-- 24 January 2002 |
|||
|
|||
Added RARReadHeaderEx function allowing to read Unicode file names |
|||
and 64 bit file sizes. |
|||
|
|||
|
|||
-- 23 January 2002 |
|||
|
|||
Added ERAR_UNKNOWN error type (it is used for all errors which |
|||
do not have special ERAR code yet) and UCM_NEEDPASSWORD callback |
|||
message. |
|||
|
|||
Unrar.dll automatically opens all next volumes not only when extracting, |
|||
but also in RAR_OM_LIST mode. |
|||
|
|||
|
|||
-- 27 November 2001 |
|||
|
|||
RARSetChangeVolProc and RARSetProcessDataProc are replaced by |
|||
the single callback function installed with RARSetCallback. |
|||
Unlike old style callbacks, the new function accepts the user defined |
|||
parameter. Unrar.dll still supports RARSetChangeVolProc and |
|||
RARSetProcessDataProc for compatibility purposes, but if you write |
|||
a new application, better use RARSetCallback. |
|||
|
|||
File comments support is not implemented in the new DLL version yet. |
|||
Now CmtState is always 0. |
|||
|
|||
|
|||
-- 13 August 2001 |
|||
|
|||
Added RARGetDllVersion function, so you may distinguish old unrar.dll, |
|||
which used C style callback functions and the new one with PASCAL callbacks. |
|||
|
|||
|
|||
-- 10 May 2001 |
|||
|
|||
Callback functions in RARSetChangeVolProc and RARSetProcessDataProc |
|||
use PASCAL style call convention now. |
@ -0,0 +1 @@ |
|||
This is x64 version of unrar.dll. |
Binary file not shown.
Binary file not shown.
@ -0,0 +1,177 @@ |
|||
# Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov |
|||
# |
|||
# Permission is hereby granted, free of charge, to any person obtaining |
|||
# a copy of this software and associated documentation files (the |
|||
# "Software"), to deal in the Software without restriction, including |
|||
# without limitation the rights to use, copy, modify, merge, publish, |
|||
# distribute, sublicense, and/or sell copies of the Software, and to |
|||
# permit persons to whom the Software is furnished to do so, subject to |
|||
# the following conditions: |
|||
# |
|||
# The above copyright notice and this permission notice shall be |
|||
# included in all copies or substantial portions of the Software. |
|||
# |
|||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
|||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
|||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
|||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
|||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
|||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
|||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|||
# SOFTWARE. |
|||
|
|||
""" |
|||
pyUnRAR2 is a ctypes based wrapper around the free UnRAR.dll. |
|||
|
|||
It is an modified version of Jimmy Retzlaff's pyUnRAR - more simple, |
|||
stable and foolproof. |
|||
Notice that it has INCOMPATIBLE interface. |
|||
|
|||
It enables reading and unpacking of archives created with the |
|||
RAR/WinRAR archivers. There is a low-level interface which is very |
|||
similar to the C interface provided by UnRAR. There is also a |
|||
higher level interface which makes some common operations easier. |
|||
""" |
|||
|
|||
__version__ = '0.99.2' |
|||
|
|||
try: |
|||
WindowsError |
|||
in_windows = True |
|||
except NameError: |
|||
in_windows = False |
|||
|
|||
if in_windows: |
|||
from windows import RarFileImplementation |
|||
else: |
|||
from unix import RarFileImplementation |
|||
|
|||
|
|||
import fnmatch, time, weakref |
|||
|
|||
class RarInfo(object): |
|||
"""Represents a file header in an archive. Don't instantiate directly. |
|||
Use only to obtain information about file. |
|||
YOU CANNOT EXTRACT FILE CONTENTS USING THIS OBJECT. |
|||
USE METHODS OF RarFile CLASS INSTEAD. |
|||
|
|||
Properties: |
|||
index - index of file within the archive |
|||
filename - name of the file in the archive including path (if any) |
|||
datetime - file date/time as a struct_time suitable for time.strftime |
|||
isdir - True if the file is a directory |
|||
size - size in bytes of the uncompressed file |
|||
comment - comment associated with the file |
|||
|
|||
Note - this is not currently intended to be a Python file-like object. |
|||
""" |
|||
|
|||
def __init__(self, rarfile, data): |
|||
self.rarfile = weakref.proxy(rarfile) |
|||
self.index = data['index'] |
|||
self.filename = data['filename'] |
|||
self.isdir = data['isdir'] |
|||
self.size = data['size'] |
|||
self.datetime = data['datetime'] |
|||
self.comment = data['comment'] |
|||
|
|||
|
|||
|
|||
def __str__(self): |
|||
try : |
|||
arcName = self.rarfile.archiveName |
|||
except ReferenceError: |
|||
arcName = "[ARCHIVE_NO_LONGER_LOADED]" |
|||
return '<RarInfo "%s" in "%s">' % (self.filename, arcName) |
|||
|
|||
class RarFile(RarFileImplementation): |
|||
|
|||
def __init__(self, archiveName, password=None): |
|||
"""Instantiate the archive. |
|||
|
|||
archiveName is the name of the RAR file. |
|||
password is used to decrypt the files in the archive. |
|||
|
|||
Properties: |
|||
comment - comment associated with the archive |
|||
|
|||
>>> print RarFile('test.rar').comment |
|||
This is a test. |
|||
""" |
|||
self.archiveName = archiveName |
|||
RarFileImplementation.init(self, password) |
|||
|
|||
def __del__(self): |
|||
self.destruct() |
|||
|
|||
def infoiter(self): |
|||
"""Iterate over all the files in the archive, generating RarInfos. |
|||
|
|||
>>> import os |
|||
>>> for fileInArchive in RarFile('test.rar').infoiter(): |
|||
... print os.path.split(fileInArchive.filename)[-1], |
|||
... print fileInArchive.isdir, |
|||
... print fileInArchive.size, |
|||
... print fileInArchive.comment, |
|||
... print tuple(fileInArchive.datetime)[0:5], |
|||
... print time.strftime('%a, %d %b %Y %H:%M', fileInArchive.datetime) |
|||
test True 0 None (2003, 6, 30, 1, 59) Mon, 30 Jun 2003 01:59 |
|||
test.txt False 20 None (2003, 6, 30, 2, 1) Mon, 30 Jun 2003 02:01 |
|||
this.py False 1030 None (2002, 2, 8, 16, 47) Fri, 08 Feb 2002 16:47 |
|||
""" |
|||
for params in RarFileImplementation.infoiter(self): |
|||
yield RarInfo(self, params) |
|||
|
|||
def infolist(self): |
|||
"""Return a list of RarInfos, descripting the contents of the archive.""" |
|||
return list(self.infoiter()) |
|||
|
|||
def read_files(self, condition='*'): |
|||
"""Read specific files from archive into memory. |
|||
If "condition" is a list of numbers, then return files which have those positions in infolist. |
|||
If "condition" is a string, then it is treated as a wildcard for names of files to extract. |
|||
If "condition" is a function, it is treated as a callback function, which accepts a RarInfo object |
|||
and returns boolean True (extract) or False (skip). |
|||
If "condition" is omitted, all files are returned. |
|||
|
|||
Returns list of tuples (RarInfo info, str contents) |
|||
""" |
|||
checker = condition2checker(condition) |
|||
return RarFileImplementation.read_files(self, checker) |
|||
|
|||
|
|||
def extract(self, condition='*', path='.', withSubpath=True, overwrite=True): |
|||
"""Extract specific files from archive to disk. |
|||
|
|||
If "condition" is a list of numbers, then extract files which have those positions in infolist. |
|||
If "condition" is a string, then it is treated as a wildcard for names of files to extract. |
|||
If "condition" is a function, it is treated as a callback function, which accepts a RarInfo object |
|||
and returns either boolean True (extract) or boolean False (skip). |
|||
DEPRECATED: If "condition" callback returns string (only supported for Windows) - |
|||
that string will be used as a new name to save the file under. |
|||
If "condition" is omitted, all files are extracted. |
|||
|
|||
"path" is a directory to extract to |
|||
"withSubpath" flag denotes whether files are extracted with their full path in the archive. |
|||
"overwrite" flag denotes whether extracted files will overwrite old ones. Defaults to true. |
|||
|
|||
Returns list of RarInfos for extracted files.""" |
|||
checker = condition2checker(condition) |
|||
return RarFileImplementation.extract(self, checker, path, withSubpath, overwrite) |
|||
|
|||
def condition2checker(condition): |
|||
"""Converts different condition types to callback""" |
|||
if type(condition) in [str, unicode]: |
|||
def smatcher(info): |
|||
return fnmatch.fnmatch(info.filename, condition) |
|||
return smatcher |
|||
elif type(condition) in [list, tuple] and type(condition[0]) in [int, long]: |
|||
def imatcher(info): |
|||
return info.index in condition |
|||
return imatcher |
|||
elif callable(condition): |
|||
return condition |
|||
else: |
|||
raise TypeError |
|||
|
|||
|
@ -0,0 +1,21 @@ |
|||
Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov |
|||
|
|||
Permission is hereby granted, free of charge, to any person obtaining |
|||
a copy of this software and associated documentation files (the |
|||
"Software"), to deal in the Software without restriction, including |
|||
without limitation the rights to use, copy, modify, merge, publish, |
|||
distribute, sublicense, and/or sell copies of the Software, and to |
|||
permit persons to whom the Software is furnished to do so, subject to |
|||
the following conditions: |
|||
|
|||
The above copyright notice and this permission notice shall be |
|||
included in all copies or substantial portions of the Software. |
|||
|
|||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
|||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
|||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
|||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
|||
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
|||
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
|||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|||
SOFTWARE. |
@ -0,0 +1,30 @@ |
|||
# Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov |
|||
# |
|||
# Permission is hereby granted, free of charge, to any person obtaining |
|||
# a copy of this software and associated documentation files (the |
|||
# "Software"), to deal in the Software without restriction, including |
|||
# without limitation the rights to use, copy, modify, merge, publish, |
|||
# distribute, sublicense, and/or sell copies of the Software, and to |
|||
# permit persons to whom the Software is furnished to do so, subject to |
|||
# the following conditions: |
|||
# |
|||
# The above copyright notice and this permission notice shall be |
|||
# included in all copies or substantial portions of the Software. |
|||
# |
|||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
|||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
|||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
|||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
|||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
|||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
|||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|||
# SOFTWARE. |
|||
|
|||
# Low level interface - see UnRARDLL\UNRARDLL.TXT |
|||
|
|||
|
|||
class ArchiveHeaderBroken(Exception): pass |
|||
class InvalidRARArchive(Exception): pass |
|||
class FileOpenError(Exception): pass |
|||
class IncorrectRARPassword(Exception): pass |
|||
class InvalidRARArchiveUsage(Exception): pass |
@ -0,0 +1,175 @@ |
|||
# Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov |
|||
# |
|||
# Permission is hereby granted, free of charge, to any person obtaining |
|||
# a copy of this software and associated documentation files (the |
|||
# "Software"), to deal in the Software without restriction, including |
|||
# without limitation the rights to use, copy, modify, merge, publish, |
|||
# distribute, sublicense, and/or sell copies of the Software, and to |
|||
# permit persons to whom the Software is furnished to do so, subject to |
|||
# the following conditions: |
|||
# |
|||
# The above copyright notice and this permission notice shall be |
|||
# included in all copies or substantial portions of the Software. |
|||
# |
|||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
|||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
|||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
|||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
|||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
|||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
|||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|||
# SOFTWARE. |
|||
|
|||
# Unix version uses unrar command line executable |
|||
|
|||
import subprocess |
|||
import gc |
|||
|
|||
import os, os.path |
|||
import time, re |
|||
|
|||
from rar_exceptions import * |
|||
|
|||
class UnpackerNotInstalled(Exception): pass |
|||
|
|||
rar_executable_cached = None |
|||
|
|||
def call_unrar(params): |
|||
"Calls rar/unrar command line executable, returns stdout pipe" |
|||
global rar_executable_cached |
|||
if rar_executable_cached is None: |
|||
for command in ('unrar', 'rar'): |
|||
try: |
|||
subprocess.Popen([command], stdout=subprocess.PIPE) |
|||
rar_executable_cached = command |
|||
break |
|||
except OSError: |
|||
pass |
|||
if rar_executable_cached is None: |
|||
raise UnpackerNotInstalled("No suitable RAR unpacker installed") |
|||
|
|||
assert type(params) == list, "params must be list" |
|||
args = [rar_executable_cached] + params |
|||
try: |
|||
gc.disable() # See http://bugs.python.org/issue1336 |
|||
return subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
|||
finally: |
|||
gc.enable() |
|||
|
|||
class RarFileImplementation(object): |
|||
|
|||
def init(self, password=None): |
|||
self.password = password |
|||
|
|||
|
|||
|
|||
stdoutdata, stderrdata = self.call('v', []).communicate() |
|||
|
|||
for line in stderrdata.splitlines(): |
|||
if line.strip().startswith("Cannot open"): |
|||
raise FileOpenError |
|||
if line.find("CRC failed")>=0: |
|||
raise IncorrectRARPassword |
|||
accum = [] |
|||
source = iter(stdoutdata.splitlines()) |
|||
line = '' |
|||
while not (line.startswith('Comment:') or line.startswith('Pathname/Comment')): |
|||
if line.strip().endswith('is not RAR archive'): |
|||
raise InvalidRARArchive |
|||
line = source.next() |
|||
while not line.startswith('Pathname/Comment'): |
|||
accum.append(line.rstrip('\n')) |
|||
line = source.next() |
|||
if len(accum): |
|||
accum[0] = accum[0][9:] |
|||
self.comment = '\n'.join(accum[:-1]) |
|||
else: |
|||
self.comment = None |
|||
|
|||
def escaped_password(self): |
|||
return '-' if self.password == None else self.password |
|||
|
|||
|
|||
def call(self, cmd, options=[], files=[]): |
|||
options2 = options + ['p'+self.escaped_password()] |
|||
soptions = ['-'+x for x in options2] |
|||
return call_unrar([cmd]+soptions+['--',self.archiveName]+files) |
|||
|
|||
def infoiter(self): |
|||
|
|||
stdoutdata, stderrdata = self.call('v', ['c-']).communicate() |
|||
|
|||
for line in stderrdata.splitlines(): |
|||
if line.strip().startswith("Cannot open"): |
|||
raise FileOpenError |
|||
|
|||
accum = [] |
|||
source = iter(stdoutdata.splitlines()) |
|||
line = '' |
|||
while not line.startswith('--------------'): |
|||
if line.strip().endswith('is not RAR archive'): |
|||
raise InvalidRARArchive |
|||
if line.find("CRC failed")>=0: |
|||
raise IncorrectRARPassword |
|||
line = source.next() |
|||
line = source.next() |
|||
i = 0 |
|||
re_spaces = re.compile(r"\s+") |
|||
while not line.startswith('--------------'): |
|||
accum.append(line) |
|||
if len(accum)==2: |
|||
data = {} |
|||
data['index'] = i |
|||
data['filename'] = accum[0].strip() |
|||
info = re_spaces.split(accum[1].strip()) |
|||
data['size'] = int(info[0]) |
|||
attr = info[5] |
|||
data['isdir'] = 'd' in attr.lower() |
|||
data['datetime'] = time.strptime(info[3]+" "+info[4], '%d-%m-%y %H:%M') |
|||
data['comment'] = None |
|||
yield data |
|||
accum = [] |
|||
i += 1 |
|||
line = source.next() |
|||
|
|||
def read_files(self, checker): |
|||
res = [] |
|||
for info in self.infoiter(): |
|||
checkres = checker(info) |
|||
if checkres==True and not info.isdir: |
|||
pipe = self.call('p', ['inul'], [info.filename]).stdout |
|||
res.append((info, pipe.read())) |
|||
return res |
|||
|
|||
|
|||
def extract(self, checker, path, withSubpath, overwrite): |
|||
res = [] |
|||
command = 'x' |
|||
if not withSubpath: |
|||
command = 'e' |
|||
options = [] |
|||
if overwrite: |
|||
options.append('o+') |
|||
else: |
|||
options.append('o-') |
|||
if not path.endswith(os.sep): |
|||
path += os.sep |
|||
names = [] |
|||
for info in self.infoiter(): |
|||
checkres = checker(info) |
|||
if type(checkres) in [str, unicode]: |
|||
raise NotImplementedError("Condition callbacks returning strings are deprecated and only supported in Windows") |
|||
if checkres==True and not info.isdir: |
|||
names.append(info.filename) |
|||
res.append(info) |
|||
names.append(path) |
|||
proc = self.call(command, options, names) |
|||
stdoutdata, stderrdata = proc.communicate() |
|||
if stderrdata.find("CRC failed")>=0: |
|||
raise IncorrectRARPassword |
|||
return res |
|||
|
|||
def destruct(self): |
|||
pass |
|||
|
|||
|
@ -0,0 +1,309 @@ |
|||
# Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov |
|||
# |
|||
# Permission is hereby granted, free of charge, to any person obtaining |
|||
# a copy of this software and associated documentation files (the |
|||
# "Software"), to deal in the Software without restriction, including |
|||
# without limitation the rights to use, copy, modify, merge, publish, |
|||
# distribute, sublicense, and/or sell copies of the Software, and to |
|||
# permit persons to whom the Software is furnished to do so, subject to |
|||
# the following conditions: |
|||
# |
|||
# The above copyright notice and this permission notice shall be |
|||
# included in all copies or substantial portions of the Software. |
|||
# |
|||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
|||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
|||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
|||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
|||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
|||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
|||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|||
# SOFTWARE. |
|||
|
|||
# Low level interface - see UnRARDLL\UNRARDLL.TXT |
|||
|
|||
from __future__ import generators |
|||
|
|||
import ctypes, ctypes.wintypes |
|||
import os, os.path, sys |
|||
import Queue |
|||
import time |
|||
|
|||
from rar_exceptions import * |
|||
|
|||
ERAR_END_ARCHIVE = 10 |
|||
ERAR_NO_MEMORY = 11 |
|||
ERAR_BAD_DATA = 12 |
|||
ERAR_BAD_ARCHIVE = 13 |
|||
ERAR_UNKNOWN_FORMAT = 14 |
|||
ERAR_EOPEN = 15 |
|||
ERAR_ECREATE = 16 |
|||
ERAR_ECLOSE = 17 |
|||
ERAR_EREAD = 18 |
|||
ERAR_EWRITE = 19 |
|||
ERAR_SMALL_BUF = 20 |
|||
ERAR_UNKNOWN = 21 |
|||
|
|||
RAR_OM_LIST = 0 |
|||
RAR_OM_EXTRACT = 1 |
|||
|
|||
RAR_SKIP = 0 |
|||
RAR_TEST = 1 |
|||
RAR_EXTRACT = 2 |
|||
|
|||
RAR_VOL_ASK = 0 |
|||
RAR_VOL_NOTIFY = 1 |
|||
|
|||
RAR_DLL_VERSION = 3 |
|||
|
|||
# enum UNRARCALLBACK_MESSAGES |
|||
UCM_CHANGEVOLUME = 0 |
|||
UCM_PROCESSDATA = 1 |
|||
UCM_NEEDPASSWORD = 2 |
|||
|
|||
architecture_bits = ctypes.sizeof(ctypes.c_voidp)*8 |
|||
dll_name = "unrar.dll" |
|||
if architecture_bits == 64: |
|||
dll_name = "x64\\unrar64.dll" |
|||
|
|||
|
|||
try: |
|||
unrar = ctypes.WinDLL(os.path.join(os.path.split(__file__)[0], 'UnRARDLL', dll_name)) |
|||
except WindowsError: |
|||
unrar = ctypes.WinDLL(dll_name) |
|||
|
|||
|
|||
class RAROpenArchiveDataEx(ctypes.Structure): |
|||
def __init__(self, ArcName=None, ArcNameW=u'', OpenMode=RAR_OM_LIST): |
|||
self.CmtBuf = ctypes.c_buffer(64*1024) |
|||
ctypes.Structure.__init__(self, ArcName=ArcName, ArcNameW=ArcNameW, OpenMode=OpenMode, _CmtBuf=ctypes.addressof(self.CmtBuf), CmtBufSize=ctypes.sizeof(self.CmtBuf)) |
|||
|
|||
_fields_ = [ |
|||
('ArcName', ctypes.c_char_p), |
|||
('ArcNameW', ctypes.c_wchar_p), |
|||
('OpenMode', ctypes.c_uint), |
|||
('OpenResult', ctypes.c_uint), |
|||
('_CmtBuf', ctypes.c_voidp), |
|||
('CmtBufSize', ctypes.c_uint), |
|||
('CmtSize', ctypes.c_uint), |
|||
('CmtState', ctypes.c_uint), |
|||
('Flags', ctypes.c_uint), |
|||
('Reserved', ctypes.c_uint*32), |
|||
] |
|||
|
|||
class RARHeaderDataEx(ctypes.Structure): |
|||
def __init__(self): |
|||
self.CmtBuf = ctypes.c_buffer(64*1024) |
|||
ctypes.Structure.__init__(self, _CmtBuf=ctypes.addressof(self.CmtBuf), CmtBufSize=ctypes.sizeof(self.CmtBuf)) |
|||
|
|||
_fields_ = [ |
|||
('ArcName', ctypes.c_char*1024), |
|||
('ArcNameW', ctypes.c_wchar*1024), |
|||
('FileName', ctypes.c_char*1024), |
|||
('FileNameW', ctypes.c_wchar*1024), |
|||
('Flags', ctypes.c_uint), |
|||
('PackSize', ctypes.c_uint), |
|||
('PackSizeHigh', ctypes.c_uint), |
|||
('UnpSize', ctypes.c_uint), |
|||
('UnpSizeHigh', ctypes.c_uint), |
|||
('HostOS', ctypes.c_uint), |
|||
('FileCRC', ctypes.c_uint), |
|||
('FileTime', ctypes.c_uint), |
|||
('UnpVer', ctypes.c_uint), |
|||
('Method', ctypes.c_uint), |
|||
('FileAttr', ctypes.c_uint), |
|||
('_CmtBuf', ctypes.c_voidp), |
|||
('CmtBufSize', ctypes.c_uint), |
|||
('CmtSize', ctypes.c_uint), |
|||
('CmtState', ctypes.c_uint), |
|||
('Reserved', ctypes.c_uint*1024), |
|||
] |
|||
|
|||
def DosDateTimeToTimeTuple(dosDateTime): |
|||
"""Convert an MS-DOS format date time to a Python time tuple. |
|||
""" |
|||
dosDate = dosDateTime >> 16 |
|||
dosTime = dosDateTime & 0xffff |
|||
day = dosDate & 0x1f |
|||
month = (dosDate >> 5) & 0xf |
|||
year = 1980 + (dosDate >> 9) |
|||
second = 2*(dosTime & 0x1f) |
|||
minute = (dosTime >> 5) & 0x3f |
|||
hour = dosTime >> 11 |
|||
return time.localtime(time.mktime((year, month, day, hour, minute, second, 0, 1, -1))) |
|||
|
|||
def _wrap(restype, function, argtypes): |
|||
result = function |
|||
result.argtypes = argtypes |
|||
result.restype = restype |
|||
return result |
|||
|
|||
RARGetDllVersion = _wrap(ctypes.c_int, unrar.RARGetDllVersion, []) |
|||
|
|||
RAROpenArchiveEx = _wrap(ctypes.wintypes.HANDLE, unrar.RAROpenArchiveEx, [ctypes.POINTER(RAROpenArchiveDataEx)]) |
|||
|
|||
RARReadHeaderEx = _wrap(ctypes.c_int, unrar.RARReadHeaderEx, [ctypes.wintypes.HANDLE, ctypes.POINTER(RARHeaderDataEx)]) |
|||
|
|||
_RARSetPassword = _wrap(ctypes.c_int, unrar.RARSetPassword, [ctypes.wintypes.HANDLE, ctypes.c_char_p]) |
|||
def RARSetPassword(*args, **kwargs): |
|||
_RARSetPassword(*args, **kwargs) |
|||
|
|||
RARProcessFile = _wrap(ctypes.c_int, unrar.RARProcessFile, [ctypes.wintypes.HANDLE, ctypes.c_int, ctypes.c_char_p, ctypes.c_char_p]) |
|||
|
|||
RARCloseArchive = _wrap(ctypes.c_int, unrar.RARCloseArchive, [ctypes.wintypes.HANDLE]) |
|||
|
|||
UNRARCALLBACK = ctypes.WINFUNCTYPE(ctypes.c_int, ctypes.c_uint, ctypes.c_long, ctypes.c_long, ctypes.c_long) |
|||
RARSetCallback = _wrap(ctypes.c_int, unrar.RARSetCallback, [ctypes.wintypes.HANDLE, UNRARCALLBACK, ctypes.c_long]) |
|||
|
|||
|
|||
|
|||
RARExceptions = { |
|||
ERAR_NO_MEMORY : MemoryError, |
|||
ERAR_BAD_DATA : ArchiveHeaderBroken, |
|||
ERAR_BAD_ARCHIVE : InvalidRARArchive, |
|||
ERAR_EOPEN : FileOpenError, |
|||
} |
|||
|
|||
class PassiveReader: |
|||
"""Used for reading files to memory""" |
|||
def __init__(self, usercallback = None): |
|||
self.buf = [] |
|||
self.ucb = usercallback |
|||
|
|||
def _callback(self, msg, UserData, P1, P2): |
|||
if msg == UCM_PROCESSDATA: |
|||
data = (ctypes.c_char*P2).from_address(P1).raw |
|||
if self.ucb!=None: |
|||
self.ucb(data) |
|||
else: |
|||
self.buf.append(data) |
|||
return 1 |
|||
|
|||
def get_result(self): |
|||
return ''.join(self.buf) |
|||
|
|||
class RarInfoIterator(object): |
|||
def __init__(self, arc): |
|||
self.arc = arc |
|||
self.index = 0 |
|||
self.headerData = RARHeaderDataEx() |
|||
self.res = RARReadHeaderEx(self.arc._handle, ctypes.byref(self.headerData)) |
|||
if self.res==ERAR_BAD_DATA: |
|||
raise IncorrectRARPassword |
|||
self.arc.lockStatus = "locked" |
|||
self.arc.needskip = False |
|||
|
|||
def __iter__(self): |
|||
return self |
|||
|
|||
def next(self): |
|||
if self.index>0: |
|||
if self.arc.needskip: |
|||
RARProcessFile(self.arc._handle, RAR_SKIP, None, None) |
|||
self.res = RARReadHeaderEx(self.arc._handle, ctypes.byref(self.headerData)) |
|||
|
|||
if self.res: |
|||
raise StopIteration |
|||
self.arc.needskip = True |
|||
|
|||
data = {} |
|||
data['index'] = self.index |
|||
data['filename'] = self.headerData.FileName |
|||
data['datetime'] = DosDateTimeToTimeTuple(self.headerData.FileTime) |
|||
data['isdir'] = ((self.headerData.Flags & 0xE0) == 0xE0) |
|||
data['size'] = self.headerData.UnpSize + (self.headerData.UnpSizeHigh << 32) |
|||
if self.headerData.CmtState == 1: |
|||
data['comment'] = self.headerData.CmtBuf.value |
|||
else: |
|||
data['comment'] = None |
|||
self.index += 1 |
|||
return data |
|||
|
|||
|
|||
def __del__(self): |
|||
self.arc.lockStatus = "finished" |
|||
|
|||
def generate_password_provider(password): |
|||
def password_provider_callback(msg, UserData, P1, P2): |
|||
if msg == UCM_NEEDPASSWORD and password!=None: |
|||
(ctypes.c_char*P2).from_address(P1).value = password |
|||
return 1 |
|||
return password_provider_callback |
|||
|
|||
class RarFileImplementation(object): |
|||
|
|||
def init(self, password=None): |
|||
self.password = password |
|||
archiveData = RAROpenArchiveDataEx(ArcNameW=self.archiveName, OpenMode=RAR_OM_EXTRACT) |
|||
self._handle = RAROpenArchiveEx(ctypes.byref(archiveData)) |
|||
self.c_callback = UNRARCALLBACK(generate_password_provider(self.password)) |
|||
RARSetCallback(self._handle, self.c_callback, 1) |
|||
|
|||
if archiveData.OpenResult != 0: |
|||
raise RARExceptions[archiveData.OpenResult] |
|||
|
|||
if archiveData.CmtState == 1: |
|||
self.comment = archiveData.CmtBuf.value |
|||
else: |
|||
self.comment = None |
|||
|
|||
if password: |
|||
RARSetPassword(self._handle, password) |
|||
|
|||
self.lockStatus = "ready" |
|||
|
|||
|
|||
|
|||
def destruct(self): |
|||
if self._handle and RARCloseArchive: |
|||
RARCloseArchive(self._handle) |
|||
|
|||
def make_sure_ready(self): |
|||
if self.lockStatus == "locked": |
|||
raise InvalidRARArchiveUsage("cannot execute infoiter() without finishing previous one") |
|||
if self.lockStatus == "finished": |
|||
self.destruct() |
|||
self.init(self.password) |
|||
|
|||
def infoiter(self): |
|||
self.make_sure_ready() |
|||
return RarInfoIterator(self) |
|||
|
|||
def read_files(self, checker): |
|||
res = [] |
|||
for info in self.infoiter(): |
|||
if checker(info) and not info.isdir: |
|||
reader = PassiveReader() |
|||
c_callback = UNRARCALLBACK(reader._callback) |
|||
RARSetCallback(self._handle, c_callback, 1) |
|||
tmpres = RARProcessFile(self._handle, RAR_TEST, None, None) |
|||
if tmpres==ERAR_BAD_DATA: |
|||
raise IncorrectRARPassword |
|||
self.needskip = False |
|||
res.append((info, reader.get_result())) |
|||
return res |
|||
|
|||
|
|||
def extract(self, checker, path, withSubpath, overwrite): |
|||
res = [] |
|||
for info in self.infoiter(): |
|||
checkres = checker(info) |
|||
if checkres!=False and not info.isdir: |
|||
if checkres==True: |
|||
fn = info.filename |
|||
if not withSubpath: |
|||
fn = os.path.split(fn)[-1] |
|||
target = os.path.join(path, fn) |
|||
else: |
|||
raise DeprecationWarning, "Condition callbacks returning strings are deprecated and only supported in Windows" |
|||
target = checkres |
|||
if overwrite or (not os.path.exists(target)): |
|||
tmpres = RARProcessFile(self._handle, RAR_EXTRACT, None, target) |
|||
if tmpres==ERAR_BAD_DATA: |
|||
raise IncorrectRARPassword |
|||
|
|||
self.needskip = False |
|||
res.append(info) |
|||
return res |
|||
|
|||
|
Loading…
Reference in new issue