Merge branch 'refs/heads/develop' into tv
This commit is contained in:
@@ -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()
|
||||
+71
@@ -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.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
+202
@@ -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
|
||||
@@ -73,6 +73,12 @@ config = [{
|
||||
'type': 'choice',
|
||||
'options': rename_options
|
||||
},
|
||||
{
|
||||
'name': 'unrar',
|
||||
'type': 'bool',
|
||||
'description': 'Extract rar files if found.',
|
||||
'default': False,
|
||||
},
|
||||
{
|
||||
'name': 'cleanup',
|
||||
'type': 'bool',
|
||||
|
||||
@@ -9,6 +9,8 @@ from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Library, File, Profile, Release, \
|
||||
ReleaseInfo
|
||||
from couchpotato.environment import Env
|
||||
from unrar2 import RarFile, RarInfo
|
||||
from unrar2.rar_exceptions import *
|
||||
import errno
|
||||
import fnmatch
|
||||
import os
|
||||
@@ -126,6 +128,11 @@ class Renamer(Plugin):
|
||||
# Extend the download info with info stored in the downloaded release
|
||||
download_info = self.extendDownloadInfo(download_info)
|
||||
|
||||
# Unpack any archives
|
||||
if self.conf('unrar'):
|
||||
folder, movie_folder, files, extr_files = self.extractFiles(folder = folder, movie_folder = movie_folder, files = files, \
|
||||
cleanup = self.conf('cleanup') and not self.downloadIsTorrent(download_info))
|
||||
|
||||
groups = fireEvent('scanner.scan', folder = folder if folder else self.conf('from'),
|
||||
files = files, download_info = download_info, return_ignored = False, single = True)
|
||||
|
||||
@@ -179,6 +186,9 @@ class Renamer(Plugin):
|
||||
group['before_rename'] = []
|
||||
fireEvent('renamer.before', group)
|
||||
|
||||
# Add extracted files to the before_rename list
|
||||
group['before_rename'].extend(extr_files)
|
||||
|
||||
# Remove weird chars from moviename
|
||||
movie_name = re.sub(r"[\x00\/\\:\*\?\"<>\|]", '', movie_title)
|
||||
|
||||
@@ -213,8 +223,8 @@ class Renamer(Plugin):
|
||||
# Move nfo depending on settings
|
||||
if file_type is 'nfo' and not self.conf('rename_nfo'):
|
||||
log.debug('Skipping, renaming of %s disabled', file_type)
|
||||
if self.conf('cleanup') and not self.downloadIsTorrent(download_info):
|
||||
for current_file in group['files'][file_type]:
|
||||
for current_file in group['files'][file_type]:
|
||||
if self.conf('cleanup') and (not (self.conf('file_action') != 'move' and self.downloadIsTorrent(download_info)) or self.fileIsAdded(current_file, group)):
|
||||
remove_files.append(current_file)
|
||||
continue
|
||||
|
||||
@@ -394,14 +404,15 @@ class Renamer(Plugin):
|
||||
db.commit()
|
||||
|
||||
# Remove leftover files
|
||||
if self.conf('cleanup') and not self.conf('move_leftover') and remove_leftovers and \
|
||||
not self.downloadIsTorrent(download_info):
|
||||
log.debug('Removing leftover files')
|
||||
for current_file in group['files']['leftover']:
|
||||
remove_files.append(current_file)
|
||||
elif not remove_leftovers: # Don't remove anything
|
||||
if not remove_leftovers: # Don't remove anything
|
||||
break
|
||||
|
||||
log.debug('Removing leftover files')
|
||||
for current_file in group['files']['leftover']:
|
||||
if self.conf('cleanup') and not self.conf('move_leftover') and \
|
||||
(not (self.conf('file_action') != 'move' and self.downloadIsTorrent(download_info)) or self.fileIsAdded(current_file, group)):
|
||||
remove_files.append(current_file)
|
||||
|
||||
# Remove files
|
||||
delete_folders = []
|
||||
for src in remove_files:
|
||||
@@ -834,3 +845,127 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
||||
|
||||
def movieInFromFolder(self, movie_folder):
|
||||
return movie_folder and self.conf('from') in movie_folder or not movie_folder
|
||||
|
||||
def extractFiles(self, folder = None, movie_folder = None, files = [], cleanup = False):
|
||||
|
||||
# RegEx for finding rar files
|
||||
archive_regex = '(?P<file>^(?P<base>(?:(?!\.part\d+\.rar$).)*)\.(?:(?:part0*1\.)?rar)$)'
|
||||
restfile_regex = '(^%s\.(?:part(?!0*1\.rar$)\d+\.rar$|[rstuvw]\d+$))'
|
||||
extr_files = []
|
||||
|
||||
# Check input variables
|
||||
if not folder:
|
||||
folder = self.conf('from')
|
||||
|
||||
check_file_date = True
|
||||
if movie_folder:
|
||||
check_file_date = False
|
||||
|
||||
if not files:
|
||||
for root, folders, names in os.walk(folder):
|
||||
files.extend([os.path.join(root, name) for name in names])
|
||||
|
||||
# Find all archive files
|
||||
archives = [re.search(archive_regex, name).groupdict() for name in files if re.search(archive_regex, name)]
|
||||
|
||||
#Extract all found archives
|
||||
for archive in archives:
|
||||
# Check if it has already been processed by CPS
|
||||
if (self.hastagDir(os.path.dirname(archive['file']))):
|
||||
continue
|
||||
|
||||
# Find all related archive files
|
||||
archive['files'] = [name for name in files if re.search(restfile_regex % re.escape(archive['base']), name)]
|
||||
archive['files'].append(archive['file'])
|
||||
|
||||
# Check if archive is fresh and maybe still copying/moving/downloading, ignore files newer than 1 minute
|
||||
if check_file_date:
|
||||
file_too_new = False
|
||||
for cur_file in archive['files']:
|
||||
if not os.path.isfile(cur_file):
|
||||
file_too_new = time.time()
|
||||
break
|
||||
file_time = [os.path.getmtime(cur_file), os.path.getctime(cur_file)]
|
||||
for t in file_time:
|
||||
if t > time.time() - 60:
|
||||
file_too_new = tryInt(time.time() - t)
|
||||
break
|
||||
|
||||
if file_too_new:
|
||||
break
|
||||
|
||||
if file_too_new:
|
||||
try:
|
||||
time_string = time.ctime(file_time[0])
|
||||
except:
|
||||
try:
|
||||
time_string = time.ctime(file_time[1])
|
||||
except:
|
||||
time_string = 'unknown'
|
||||
|
||||
log.info('Archive seems to be still copying/moving/downloading or just copied/moved/downloaded (created on %s), ignoring for now: %s', (time_string, os.path.basename(archive['file'])))
|
||||
continue
|
||||
|
||||
log.info('Archive %s found. Extracting...', os.path.basename(archive['file']))
|
||||
try:
|
||||
rar_handle = RarFile(archive['file'])
|
||||
extr_path = os.path.join(self.conf('from'), os.path.relpath(os.path.dirname(archive['file']), folder))
|
||||
self.makeDir(extr_path)
|
||||
for packedinfo in rar_handle.infolist():
|
||||
if not packedinfo.isdir and not os.path.isfile(os.path.join(extr_path, os.path.basename(packedinfo.filename))):
|
||||
log.debug('Extracting %s...', packedinfo.filename)
|
||||
rar_handle.extract(condition = [packedinfo.index], path = extr_path, withSubpath = False, overwrite = False)
|
||||
extr_files.append(os.path.join(extr_path, os.path.basename(packedinfo.filename)))
|
||||
del rar_handle
|
||||
except Exception, e:
|
||||
log.error('Failed to extract %s: %s %s', (archive['file'], e, traceback.format_exc()))
|
||||
continue
|
||||
|
||||
# Delete the archive files
|
||||
for filename in archive['files']:
|
||||
if cleanup:
|
||||
try:
|
||||
os.remove(filename)
|
||||
except Exception, e:
|
||||
log.error('Failed to remove %s: %s %s', (filename, e, traceback.format_exc()))
|
||||
continue
|
||||
files.remove(filename)
|
||||
|
||||
# Move the rest of the files and folders if any files are extracted to the from folder (only if folder was provided)
|
||||
if extr_files and os.path.normpath(os.path.normcase(folder)) != os.path.normpath(os.path.normcase(self.conf('from'))):
|
||||
for leftoverfile in list(files):
|
||||
move_to = os.path.join(self.conf('from'), os.path.relpath(leftoverfile, folder))
|
||||
|
||||
try:
|
||||
self.makeDir(os.path.dirname(move_to))
|
||||
self.moveFile(leftoverfile, move_to, cleanup)
|
||||
except Exception, e:
|
||||
log.error('Failed moving left over file %s to %s: %s %s',(leftoverfile, move_to, e, traceback.format_exc()))
|
||||
# As we probably tried to overwrite the nfo file, check if it exists and then remove the original
|
||||
if os.path.isfile(move_to):
|
||||
if cleanup:
|
||||
log.info('Deleting left over file %s instead...', leftoverfile)
|
||||
os.unlink(leftoverfile)
|
||||
else:
|
||||
continue
|
||||
|
||||
files.remove(leftoverfile)
|
||||
extr_files.append(move_to)
|
||||
|
||||
if cleanup:
|
||||
# Remove all left over folders
|
||||
log.debug('Removing old movie folder %s...', movie_folder)
|
||||
self.deleteEmptyFolder(movie_folder)
|
||||
|
||||
movie_folder = os.path.join(self.conf('from'), os.path.relpath(movie_folder, folder))
|
||||
folder = self.conf('from')
|
||||
|
||||
if extr_files:
|
||||
files.extend(extr_files)
|
||||
|
||||
# Cleanup files and folder if movie_folder was not provided
|
||||
if not movie_folder:
|
||||
files = []
|
||||
folder = None
|
||||
|
||||
return (folder, movie_folder, files, extr_files)
|
||||
|
||||
@@ -277,7 +277,7 @@ class Scanner(Plugin):
|
||||
except:
|
||||
break
|
||||
|
||||
# Check if movie is fresh and maybe still unpacking, ignore files new then 1 minute
|
||||
# Check if movie is fresh and maybe still unpacking, ignore files newer than 1 minute
|
||||
file_too_new = False
|
||||
for cur_file in group['unsorted_files']:
|
||||
if not os.path.isfile(cur_file):
|
||||
|
||||
Executable
+588
@@ -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)
|
||||
Executable
+86
@@ -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)
|
||||
Executable
+30
@@ -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
|
||||
Executable
+40
@@ -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)
|
||||
Executable
+91
@@ -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
|
||||
]
|
||||
Executable
+84
@@ -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])
|
||||
Executable
Executable
+281
@@ -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
|
||||
Executable
+159
@@ -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")
|
||||
Executable
Executable
+23
@@ -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
|
||||
Executable
+98
@@ -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
|
||||
]
|
||||
Executable
+369
@@ -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)
|
||||
Executable
+506
@@ -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'),
|
||||
]
|
||||
Executable
+138
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user