diff --git a/couchpotato/core/downloaders/deluge/__init__.py b/couchpotato/core/downloaders/deluge/__init__.py new file mode 100644 index 00000000..4b122b38 --- /dev/null +++ b/couchpotato/core/downloaders/deluge/__init__.py @@ -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 Deluge 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 localhost:58846', + }, + { + '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.', + }, + ], + } + ], +}] diff --git a/couchpotato/core/downloaders/deluge/main.py b/couchpotato/core/downloaders/deluge/main.py new file mode 100644 index 00000000..e8e0b459 --- /dev/null +++ b/couchpotato/core/downloaders/deluge/main.py @@ -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() diff --git a/couchpotato/core/downloaders/rtorrent/__init__.py b/couchpotato/core/downloaders/rtorrent/__init__.py new file mode 100755 index 00000000..efc2234b --- /dev/null +++ b/couchpotato/core/downloaders/rtorrent/__init__.py @@ -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.', + }, + ], + } + ], +}] diff --git a/couchpotato/core/downloaders/rtorrent/main.py b/couchpotato/core/downloaders/rtorrent/main.py new file mode 100755 index 00000000..c2a9b8af --- /dev/null +++ b/couchpotato/core/downloaders/rtorrent/main.py @@ -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 diff --git a/couchpotato/core/plugins/renamer/__init__.py b/couchpotato/core/plugins/renamer/__init__.py index 56672b8e..6472a2df 100755 --- a/couchpotato/core/plugins/renamer/__init__.py +++ b/couchpotato/core/plugins/renamer/__init__.py @@ -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', diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index 21de32ce..82d74286 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -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^(?P(?:(?!\.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) diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py index 8c787e9a..a45367ef 100644 --- a/couchpotato/core/plugins/scanner/main.py +++ b/couchpotato/core/plugins/scanner/main.py @@ -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): diff --git a/libs/rtorrent/__init__.py b/libs/rtorrent/__init__.py new file mode 100755 index 00000000..d19c78b4 --- /dev/null +++ b/libs/rtorrent/__init__.py @@ -0,0 +1,588 @@ +# Copyright (c) 2013 Chris Lucas, +# 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) diff --git a/libs/rtorrent/common.py b/libs/rtorrent/common.py new file mode 100755 index 00000000..371c71c3 --- /dev/null +++ b/libs/rtorrent/common.py @@ -0,0 +1,86 @@ +# Copyright (c) 2013 Chris Lucas, +# 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) diff --git a/libs/rtorrent/compat.py b/libs/rtorrent/compat.py new file mode 100755 index 00000000..1778818b --- /dev/null +++ b/libs/rtorrent/compat.py @@ -0,0 +1,30 @@ +# Copyright (c) 2013 Chris Lucas, +# 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 diff --git a/libs/rtorrent/err.py b/libs/rtorrent/err.py new file mode 100755 index 00000000..920b8385 --- /dev/null +++ b/libs/rtorrent/err.py @@ -0,0 +1,40 @@ +# Copyright (c) 2013 Chris Lucas, +# 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) diff --git a/libs/rtorrent/file.py b/libs/rtorrent/file.py new file mode 100755 index 00000000..a3db35cf --- /dev/null +++ b/libs/rtorrent/file.py @@ -0,0 +1,91 @@ +# Copyright (c) 2013 Chris Lucas, +# 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 +] diff --git a/libs/rtorrent/group.py b/libs/rtorrent/group.py new file mode 100755 index 00000000..e8246aa8 --- /dev/null +++ b/libs/rtorrent/group.py @@ -0,0 +1,84 @@ +# Copyright (c) 2013 Dean Gardiner, +# 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]) diff --git a/libs/rtorrent/lib/__init__.py b/libs/rtorrent/lib/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/libs/rtorrent/lib/bencode.py b/libs/rtorrent/lib/bencode.py new file mode 100755 index 00000000..97bd2f0e --- /dev/null +++ b/libs/rtorrent/lib/bencode.py @@ -0,0 +1,281 @@ +# Copyright (C) 2011 by clueless +# +# 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 diff --git a/libs/rtorrent/lib/torrentparser.py b/libs/rtorrent/lib/torrentparser.py new file mode 100755 index 00000000..19dd12aa --- /dev/null +++ b/libs/rtorrent/lib/torrentparser.py @@ -0,0 +1,159 @@ +# Copyright (c) 2013 Chris Lucas, +# 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") diff --git a/libs/rtorrent/lib/xmlrpc/__init__.py b/libs/rtorrent/lib/xmlrpc/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/libs/rtorrent/lib/xmlrpc/http.py b/libs/rtorrent/lib/xmlrpc/http.py new file mode 100755 index 00000000..3eb85210 --- /dev/null +++ b/libs/rtorrent/lib/xmlrpc/http.py @@ -0,0 +1,23 @@ +# Copyright (c) 2013 Chris Lucas, +# 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 diff --git a/libs/rtorrent/peer.py b/libs/rtorrent/peer.py new file mode 100755 index 00000000..61ca0941 --- /dev/null +++ b/libs/rtorrent/peer.py @@ -0,0 +1,98 @@ +# Copyright (c) 2013 Chris Lucas, +# 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 +] diff --git a/libs/rtorrent/rpc/__init__.py b/libs/rtorrent/rpc/__init__.py new file mode 100755 index 00000000..034f4eef --- /dev/null +++ b/libs/rtorrent/rpc/__init__.py @@ -0,0 +1,369 @@ +# Copyright (c) 2013 Chris Lucas, +# 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) diff --git a/libs/rtorrent/torrent.py b/libs/rtorrent/torrent.py new file mode 100755 index 00000000..c610e368 --- /dev/null +++ b/libs/rtorrent/torrent.py @@ -0,0 +1,506 @@ +# Copyright (c) 2013 Chris Lucas, +# 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'), +] diff --git a/libs/rtorrent/tracker.py b/libs/rtorrent/tracker.py new file mode 100755 index 00000000..81af2e49 --- /dev/null +++ b/libs/rtorrent/tracker.py @@ -0,0 +1,138 @@ +# Copyright (c) 2013 Chris Lucas, +# 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'), +] diff --git a/libs/synchronousdeluge/__init__.py b/libs/synchronousdeluge/__init__.py new file mode 100644 index 00000000..208e45ca --- /dev/null +++ b/libs/synchronousdeluge/__init__.py @@ -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 + diff --git a/libs/synchronousdeluge/client.py b/libs/synchronousdeluge/client.py new file mode 100644 index 00000000..1b63b0b1 --- /dev/null +++ b/libs/synchronousdeluge/client.py @@ -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() + diff --git a/libs/synchronousdeluge/exceptions.py b/libs/synchronousdeluge/exceptions.py new file mode 100644 index 00000000..da6cf022 --- /dev/null +++ b/libs/synchronousdeluge/exceptions.py @@ -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) + diff --git a/libs/synchronousdeluge/protocol.py b/libs/synchronousdeluge/protocol.py new file mode 100644 index 00000000..756d4dfc --- /dev/null +++ b/libs/synchronousdeluge/protocol.py @@ -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 + diff --git a/libs/synchronousdeluge/rencode.py b/libs/synchronousdeluge/rencode.py new file mode 100644 index 00000000..e58c7154 --- /dev/null +++ b/libs/synchronousdeluge/rencode.py @@ -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 +Python: package UnRAR2 + + + + +
 
+ 
UnRAR2 (version 0.99.1)
index
c:\python26\lib\site-packages\unrar2\__init__.py
+

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.

+

+ + + + + +
 
+Package Contents
       
rar_exceptions
+setup
+
test_UnRAR2
+unix
+
windows
+

+ + + + + +
 
+Classes
       
+
UnRAR2.windows.RarFileImplementation(__builtin__.object) +
+
+
RarFile +
+
+
__builtin__.object +
+
+
RarInfo +
+
+
+

+ + + + + +
 
+class RarFile(UnRAR2.windows.RarFileImplementation)
    
Method resolution order:
+
RarFile
+
UnRAR2.windows.RarFileImplementation
+
__builtin__.object
+
+
+Methods defined here:
+
__del__(self)
+ +
__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.
+ +
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.
+ +
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
+ +
infolist(self)
Return a list of RarInfos, descripting the contents of the archive.
+ +
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)
+ +
+Methods inherited from UnRAR2.windows.RarFileImplementation:
+
destruct(self)
+ +
init(self, password=None)
+ +
make_sure_ready(self)
+ +
+Data descriptors inherited from UnRAR2.windows.RarFileImplementation:
+
__dict__
+
dictionary for instance variables (if defined)
+
+
__weakref__
+
list of weak references to the object (if defined)
+
+

+ + + + + + + +
 
+class RarInfo(__builtin__.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.
 
 Methods defined here:
+
__init__(self, rarfile, data)
+ +
__str__(self)
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables (if defined)
+
+
__weakref__
+
list of weak references to the object (if defined)
+
+

+ + + + + +
 
+Functions
       
condition2checker(condition)
Converts different condition types to callback
+

+ + + + + +
 
+Data
       __version__ = '0.99.1'
+in_windows = True
+ \ No newline at end of file diff --git a/libs/unrar2/UnRARDLL/license.txt b/libs/unrar2/UnRARDLL/license.txt new file mode 100644 index 00000000..0c1540e5 --- /dev/null +++ b/libs/unrar2/UnRARDLL/license.txt @@ -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 \ No newline at end of file diff --git a/libs/unrar2/UnRARDLL/unrar.dll b/libs/unrar2/UnRARDLL/unrar.dll new file mode 100644 index 00000000..9757bf3d Binary files /dev/null and b/libs/unrar2/UnRARDLL/unrar.dll differ diff --git a/libs/unrar2/UnRARDLL/unrar.h b/libs/unrar2/UnRARDLL/unrar.h new file mode 100644 index 00000000..4582f2c6 --- /dev/null +++ b/libs/unrar2/UnRARDLL/unrar.h @@ -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 diff --git a/libs/unrar2/UnRARDLL/unrar.lib b/libs/unrar2/UnRARDLL/unrar.lib new file mode 100644 index 00000000..0f6b3146 Binary files /dev/null and b/libs/unrar2/UnRARDLL/unrar.lib differ diff --git a/libs/unrar2/UnRARDLL/unrardll.txt b/libs/unrar2/UnRARDLL/unrardll.txt new file mode 100644 index 00000000..c49dd5b4 --- /dev/null +++ b/libs/unrar2/UnRARDLL/unrardll.txt @@ -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. + diff --git a/libs/unrar2/UnRARDLL/whatsnew.txt b/libs/unrar2/UnRARDLL/whatsnew.txt new file mode 100644 index 00000000..874d19b1 --- /dev/null +++ b/libs/unrar2/UnRARDLL/whatsnew.txt @@ -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. diff --git a/libs/unrar2/UnRARDLL/x64/readme.txt b/libs/unrar2/UnRARDLL/x64/readme.txt new file mode 100644 index 00000000..bbfb340d --- /dev/null +++ b/libs/unrar2/UnRARDLL/x64/readme.txt @@ -0,0 +1 @@ +This is x64 version of unrar.dll. diff --git a/libs/unrar2/UnRARDLL/x64/unrar64.dll b/libs/unrar2/UnRARDLL/x64/unrar64.dll new file mode 100644 index 00000000..e17a19e5 Binary files /dev/null and b/libs/unrar2/UnRARDLL/x64/unrar64.dll differ diff --git a/libs/unrar2/UnRARDLL/x64/unrar64.lib b/libs/unrar2/UnRARDLL/x64/unrar64.lib new file mode 100644 index 00000000..fd037919 Binary files /dev/null and b/libs/unrar2/UnRARDLL/x64/unrar64.lib differ diff --git a/libs/unrar2/__init__.py b/libs/unrar2/__init__.py new file mode 100644 index 00000000..b5c9a4d3 --- /dev/null +++ b/libs/unrar2/__init__.py @@ -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 '' % (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 + + diff --git a/libs/unrar2/license.txt b/libs/unrar2/license.txt new file mode 100644 index 00000000..a395801b --- /dev/null +++ b/libs/unrar2/license.txt @@ -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. diff --git a/libs/unrar2/rar_exceptions.py b/libs/unrar2/rar_exceptions.py new file mode 100644 index 00000000..d90d1c8d --- /dev/null +++ b/libs/unrar2/rar_exceptions.py @@ -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 diff --git a/libs/unrar2/unix.py b/libs/unrar2/unix.py new file mode 100644 index 00000000..9d87b18c --- /dev/null +++ b/libs/unrar2/unix.py @@ -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 + + diff --git a/libs/unrar2/windows.py b/libs/unrar2/windows.py new file mode 100644 index 00000000..bb92481b --- /dev/null +++ b/libs/unrar2/windows.py @@ -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 + +