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