Merge branch 'tv' of https://github.com/RuudBurger/CouchPotatoServer into tv_tvdb
This commit is contained in:
commit
23bde0b866
80 files changed
+6063
-242
No files matched your search
@@ -0,0 +1,90 @@
|
|||||||
|
from .main import Deluge
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return Deluge()
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'deluge',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'downloaders',
|
||||||
|
'list': 'download_providers',
|
||||||
|
'name': 'deluge',
|
||||||
|
'label': 'Deluge',
|
||||||
|
'description': 'Use <a href="http://www.deluge-torrent.org/" target="_blank">Deluge</a> to download torrents.',
|
||||||
|
'wizard': True,
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'enabler',
|
||||||
|
'radio_group': 'torrent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'host',
|
||||||
|
'default': 'localhost:58846',
|
||||||
|
'description': 'Hostname with port. Usually <strong>localhost:58846</strong>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'username',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'password',
|
||||||
|
'type': 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': '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': '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.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'delete_failed',
|
||||||
|
'default': True,
|
||||||
|
'advanced': True,
|
||||||
|
'type': 'bool',
|
||||||
|
'description': 'Delete a release after the download has failed.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
from base64 import b64encode
|
||||||
|
from couchpotato.core.helpers.variable import tryInt, tryFloat
|
||||||
|
from couchpotato.core.downloaders.base import Downloader, StatusList
|
||||||
|
from couchpotato.core.helpers.encoding import isInt
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.environment import Env
|
||||||
|
from datetime import timedelta
|
||||||
|
from synchronousdeluge import DelugeClient
|
||||||
|
import os.path
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Deluge(Downloader):
|
||||||
|
|
||||||
|
protocol = ['torrent', 'torrent_magnet']
|
||||||
|
log = CPLog(__name__)
|
||||||
|
drpc = None
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
# Load host from config and split out port.
|
||||||
|
host = self.conf('host').split(':')
|
||||||
|
if not isInt(host[1]):
|
||||||
|
log.error('Config properties are not filled in correctly, port is missing.')
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not self.drpc:
|
||||||
|
self.drpc = DelugeRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
|
||||||
|
|
||||||
|
return self.drpc
|
||||||
|
|
||||||
|
def download(self, data, movie, filedata = None):
|
||||||
|
|
||||||
|
log.info('Sending "%s" (%s) to Deluge.', (data.get('name'), data.get('protocol')))
|
||||||
|
|
||||||
|
if not self.connect():
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not filedata and data.get('protocol') == 'torrent':
|
||||||
|
log.error('Failed sending torrent, no data')
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Set parameters for Deluge
|
||||||
|
options = {
|
||||||
|
'add_paused': self.conf('paused', default = 0),
|
||||||
|
'label': self.conf('label')
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.conf('directory'):
|
||||||
|
if os.path.isdir(self.conf('directory')):
|
||||||
|
options['download_location'] = self.conf('directory')
|
||||||
|
else:
|
||||||
|
log.error('Download directory from Deluge settings: %s doesn\'t exist', self.conf('directory'))
|
||||||
|
|
||||||
|
if self.conf('completed_directory'):
|
||||||
|
if os.path.isdir(self.conf('completed_directory')):
|
||||||
|
options['move_completed'] = 1
|
||||||
|
options['move_completed_path'] = self.conf('completed_directory')
|
||||||
|
else:
|
||||||
|
log.error('Download directory from Deluge settings: %s doesn\'t exist', self.conf('directory'))
|
||||||
|
|
||||||
|
if data.get('seed_ratio'):
|
||||||
|
options['stop_at_ratio'] = 1
|
||||||
|
options['stop_ratio'] = tryFloat(data.get('seed_ratio'))
|
||||||
|
|
||||||
|
# Deluge only has seed time as a global option. Might be added in
|
||||||
|
# in a future API release.
|
||||||
|
# if data.get('seed_time'):
|
||||||
|
|
||||||
|
# Send request to Deluge
|
||||||
|
if data.get('protocol') == 'torrent_magnet':
|
||||||
|
remote_torrent = self.drpc.add_torrent_magnet(data.get('url'), options)
|
||||||
|
else:
|
||||||
|
remote_torrent = self.drpc.add_torrent_file(movie, b64encode(filedata), options)
|
||||||
|
|
||||||
|
if not remote_torrent:
|
||||||
|
log.error('Failed sending torrent to Deluge')
|
||||||
|
return False
|
||||||
|
|
||||||
|
log.info('Torrent sent to Deluge successfully.')
|
||||||
|
return self.downloadReturnId(remote_torrent)
|
||||||
|
|
||||||
|
def getAllDownloadStatus(self):
|
||||||
|
|
||||||
|
log.debug('Checking Deluge download status.')
|
||||||
|
|
||||||
|
if not self.connect():
|
||||||
|
return False
|
||||||
|
|
||||||
|
statuses = StatusList(self)
|
||||||
|
|
||||||
|
queue = self.drpc.get_alltorrents()
|
||||||
|
|
||||||
|
if not (queue and queue.get('torrents')):
|
||||||
|
log.debug('Nothing in queue or error')
|
||||||
|
return False
|
||||||
|
|
||||||
|
for torrent_id in queue:
|
||||||
|
item = queue[torrent_id]
|
||||||
|
log.debug('name=%s / id=%s / save_path=%s / hash=%s / progress=%s / state=%s / eta=%s / ratio=%s / conf_ratio=%s/ is_seed=%s / is_finished=%s', (item['name'], item['hash'], item['save_path'], item['hash'], item['progress'], item['state'], item['eta'], item['ratio'], self.conf('ratio'), item['is_seed'], item['is_finished']))
|
||||||
|
|
||||||
|
if not os.path.isdir(Env.setting('from', 'renamer')):
|
||||||
|
log.error('Renamer "from" folder doesn\'t to exist.')
|
||||||
|
return
|
||||||
|
|
||||||
|
status = 'busy'
|
||||||
|
# Deluge seems to set both is_seed and is_finished once everything has been downloaded.
|
||||||
|
if item['is_seed'] or item['is_finished']:
|
||||||
|
status = 'seeding'
|
||||||
|
elif item['is_seed'] and item['is_finished'] and item['paused']:
|
||||||
|
status = 'completed'
|
||||||
|
|
||||||
|
download_dir = item['save_path']
|
||||||
|
if item['move_on_completed']:
|
||||||
|
download_dir = item['move_completed_path']
|
||||||
|
|
||||||
|
statuses.append({
|
||||||
|
'id': item['hash'],
|
||||||
|
'name': item['name'],
|
||||||
|
'status': status,
|
||||||
|
'original_status': item['state'],
|
||||||
|
'seed_ratio': item['ratio'],
|
||||||
|
'timeleft': str(timedelta(seconds = item['eta'])),
|
||||||
|
'folder': os.path.join(download_dir, item['name']),
|
||||||
|
})
|
||||||
|
|
||||||
|
return statuses
|
||||||
|
|
||||||
|
def pause(self, item, pause = True):
|
||||||
|
if pause:
|
||||||
|
return self.drpc.pause_torrent([item['id']])
|
||||||
|
else:
|
||||||
|
return self.drpc.resume_torrent([item['id']])
|
||||||
|
|
||||||
|
def removeFailed(self, item):
|
||||||
|
log.info('%s failed downloading, deleting...', item['name'])
|
||||||
|
return self.drpc.remove_torrent(item['id'], True)
|
||||||
|
|
||||||
|
def processComplete(self, item, delete_files = False):
|
||||||
|
log.debug('Requesting Deluge to remove the torrent %s%s.', (item['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
||||||
|
return self.drpc.remove_torrent(item['id'], remove_local_data = delete_files)
|
||||||
|
|
||||||
|
class DelugeRPC(object):
|
||||||
|
|
||||||
|
host = 'localhost'
|
||||||
|
port = 58846
|
||||||
|
username = None
|
||||||
|
password = None
|
||||||
|
client = None
|
||||||
|
|
||||||
|
def __init__(self, host = 'localhost', port = 58846, username = None, password = None):
|
||||||
|
super(DelugeRPC, self).__init__()
|
||||||
|
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
self.client = DelugeClient()
|
||||||
|
self.client.connect(self.host, int(self.port), self.username, self.password)
|
||||||
|
|
||||||
|
def add_torrent_magnet(self, torrent, options):
|
||||||
|
torrent_id = False
|
||||||
|
try:
|
||||||
|
self.connect()
|
||||||
|
torrent_id = self.client.core.add_torrent_magnet(torrent, options).get()
|
||||||
|
if options['label']:
|
||||||
|
self.client.label.set_torrent(torrent_id, options['label']).get()
|
||||||
|
except Exception, err:
|
||||||
|
log.error('Failed to add torrent magnet: %s %s', err, traceback.format_exc())
|
||||||
|
finally:
|
||||||
|
if self.client:
|
||||||
|
self.disconnect()
|
||||||
|
|
||||||
|
return torrent_id
|
||||||
|
|
||||||
|
def add_torrent_file(self, movie, torrent, options):
|
||||||
|
torrent_id = False
|
||||||
|
try:
|
||||||
|
self.connect()
|
||||||
|
torrent_id = self.client.core.add_torrent_file(movie, torrent, options).get()
|
||||||
|
if options['label']:
|
||||||
|
self.client.label.set_torrent(torrent_id, options['label']).get()
|
||||||
|
except Exception, err:
|
||||||
|
log.error('Failed to add torrent file: %s %s', err, traceback.format_exc())
|
||||||
|
finally:
|
||||||
|
if self.client:
|
||||||
|
self.disconnect()
|
||||||
|
|
||||||
|
return torrent_id
|
||||||
|
|
||||||
|
def get_alltorrents(self):
|
||||||
|
ret = False
|
||||||
|
try:
|
||||||
|
self.connect()
|
||||||
|
ret = self.client.core.get_torrents_status({}, {}).get()
|
||||||
|
except Exception, err:
|
||||||
|
log.error('Failed to get all torrents: %s %s', err, traceback.format_exc())
|
||||||
|
finally:
|
||||||
|
if self.client:
|
||||||
|
self.disconnect()
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def pause_torrent(self, torrent_ids):
|
||||||
|
try:
|
||||||
|
self.connect()
|
||||||
|
self.client.core.pause_torrent(torrent_ids).get()
|
||||||
|
except Exception, err:
|
||||||
|
log.error('Failed to pause torrent: %s %s', err, traceback.format_exc())
|
||||||
|
finally:
|
||||||
|
if self.client:
|
||||||
|
self.disconnect()
|
||||||
|
|
||||||
|
def resume_torrent(self, torrent_ids):
|
||||||
|
try:
|
||||||
|
self.connect()
|
||||||
|
self.client.core.resume_torrent(torrent_ids).get()
|
||||||
|
except Exception, err:
|
||||||
|
log.error('Failed to resume torrent: %s %s', err, traceback.format_exc())
|
||||||
|
finally:
|
||||||
|
if self.client:
|
||||||
|
self.disconnect()
|
||||||
|
|
||||||
|
def remove_torrent(self, torrent_id, remove_local_data):
|
||||||
|
ret = False
|
||||||
|
try:
|
||||||
|
self.connect()
|
||||||
|
ret = self.client.core.remove_torrent(torrent_id, remove_local_data).get()
|
||||||
|
except Exception, err:
|
||||||
|
log.error('Failed to remove torrent: %s %s', err, traceback.format_exc())
|
||||||
|
finally:
|
||||||
|
if self.client:
|
||||||
|
self.disconnect()
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
self.client.disconnect()
|
||||||
+71
@@ -0,0 +1,71 @@
|
|||||||
|
from .main import rTorrent
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return rTorrent()
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'rtorrent',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'downloaders',
|
||||||
|
'list': 'download_providers',
|
||||||
|
'name': 'rtorrent',
|
||||||
|
'label': 'rTorrent',
|
||||||
|
'description': '',
|
||||||
|
'wizard': True,
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'enabler',
|
||||||
|
'radio_group': 'torrent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'url',
|
||||||
|
'default': 'http://localhost:80/RPC2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'username',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'password',
|
||||||
|
'type': 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'label',
|
||||||
|
'description': 'Label to apply on added torrents.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'remove_complete',
|
||||||
|
'label': 'Remove torrent',
|
||||||
|
'default': False,
|
||||||
|
'advanced': True,
|
||||||
|
'type': 'bool',
|
||||||
|
'description': 'Remove the torrent after it finishes seeding.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'delete_files',
|
||||||
|
'label': 'Remove files',
|
||||||
|
'default': True,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Also remove the leftover files.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'paused',
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'default': False,
|
||||||
|
'description': 'Add the torrent paused.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'manual',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}]
|
||||||
+202
@@ -0,0 +1,202 @@
|
|||||||
|
from base64 import b16encode, b32decode
|
||||||
|
from datetime import timedelta
|
||||||
|
from hashlib import sha1
|
||||||
|
import shutil
|
||||||
|
from rtorrent.err import MethodError
|
||||||
|
|
||||||
|
from bencode import bencode, bdecode
|
||||||
|
from couchpotato.core.downloaders.base import Downloader, StatusList
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from rtorrent import RTorrent
|
||||||
|
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class rTorrent(Downloader):
|
||||||
|
protocol = ['torrent', 'torrent_magnet']
|
||||||
|
rt = None
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
# Already connected?
|
||||||
|
if self.rt is not None:
|
||||||
|
return self.rt
|
||||||
|
|
||||||
|
# Ensure url is set
|
||||||
|
if not self.conf('url'):
|
||||||
|
log.error('Config properties are not filled in correctly, url is missing.')
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.conf('username') and self.conf('password'):
|
||||||
|
self.rt = RTorrent(
|
||||||
|
self.conf('url'),
|
||||||
|
self.conf('username'),
|
||||||
|
self.conf('password')
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.rt = RTorrent(self.conf('url'))
|
||||||
|
|
||||||
|
return self.rt
|
||||||
|
|
||||||
|
def _update_provider_group(self, name, data):
|
||||||
|
if data.get('seed_time'):
|
||||||
|
log.info('seeding time ignored, not supported')
|
||||||
|
|
||||||
|
if not name:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not self.connect():
|
||||||
|
return False
|
||||||
|
|
||||||
|
views = self.rt.get_views()
|
||||||
|
|
||||||
|
if name not in views:
|
||||||
|
self.rt.create_group(name)
|
||||||
|
|
||||||
|
group = self.rt.get_group(name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if data.get('seed_ratio'):
|
||||||
|
ratio = int(float(data.get('seed_ratio')) * 100)
|
||||||
|
log.debug('Updating provider ratio to %s, group name: %s', (ratio, name))
|
||||||
|
|
||||||
|
# Explicitly set all group options to ensure it is setup correctly
|
||||||
|
group.set_upload('1M')
|
||||||
|
group.set_min(ratio)
|
||||||
|
group.set_max(ratio)
|
||||||
|
group.set_command('d.stop')
|
||||||
|
group.enable()
|
||||||
|
else:
|
||||||
|
# Reset group action and disable it
|
||||||
|
group.set_command()
|
||||||
|
group.disable()
|
||||||
|
except MethodError, err:
|
||||||
|
log.error('Unable to set group options: %s', err.message)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def download(self, data, movie, filedata = None):
|
||||||
|
log.debug('Sending "%s" to rTorrent.', (data.get('name')))
|
||||||
|
|
||||||
|
if not self.connect():
|
||||||
|
return False
|
||||||
|
|
||||||
|
group_name = 'cp_' + data.get('provider').lower()
|
||||||
|
if not self._update_provider_group(group_name, data):
|
||||||
|
return False
|
||||||
|
|
||||||
|
torrent_params = {}
|
||||||
|
if self.conf('label'):
|
||||||
|
torrent_params['label'] = self.conf('label')
|
||||||
|
|
||||||
|
if not filedata and data.get('protocol') == 'torrent':
|
||||||
|
log.error('Failed sending torrent, no data')
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Try download magnet torrents
|
||||||
|
if data.get('protocol') == 'torrent_magnet':
|
||||||
|
filedata = self.magnetToTorrent(data.get('url'))
|
||||||
|
|
||||||
|
if filedata is False:
|
||||||
|
return False
|
||||||
|
|
||||||
|
data['protocol'] = 'torrent'
|
||||||
|
|
||||||
|
info = bdecode(filedata)["info"]
|
||||||
|
torrent_hash = sha1(bencode(info)).hexdigest().upper()
|
||||||
|
|
||||||
|
# Convert base 32 to hex
|
||||||
|
if len(torrent_hash) == 32:
|
||||||
|
torrent_hash = b16encode(b32decode(torrent_hash))
|
||||||
|
|
||||||
|
# Send request to rTorrent
|
||||||
|
try:
|
||||||
|
# Send torrent to rTorrent
|
||||||
|
torrent = self.rt.load_torrent(filedata)
|
||||||
|
|
||||||
|
# Set label
|
||||||
|
if self.conf('label'):
|
||||||
|
torrent.set_custom(1, self.conf('label'))
|
||||||
|
|
||||||
|
# Set Ratio Group
|
||||||
|
torrent.set_visible(group_name)
|
||||||
|
|
||||||
|
# Start torrent
|
||||||
|
if not self.conf('paused', default = 0):
|
||||||
|
torrent.start()
|
||||||
|
|
||||||
|
return self.downloadReturnId(torrent_hash)
|
||||||
|
except Exception, err:
|
||||||
|
log.error('Failed to send torrent to rTorrent: %s', err)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def getAllDownloadStatus(self):
|
||||||
|
log.debug('Checking rTorrent download status.')
|
||||||
|
|
||||||
|
if not self.connect():
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
torrents = self.rt.get_torrents()
|
||||||
|
|
||||||
|
statuses = StatusList(self)
|
||||||
|
|
||||||
|
for item in torrents:
|
||||||
|
status = 'busy'
|
||||||
|
if item.complete:
|
||||||
|
if item.active:
|
||||||
|
status = 'seeding'
|
||||||
|
else:
|
||||||
|
status = 'completed'
|
||||||
|
|
||||||
|
statuses.append({
|
||||||
|
'id': item.info_hash,
|
||||||
|
'name': item.name,
|
||||||
|
'status': status,
|
||||||
|
'seed_ratio': item.ratio,
|
||||||
|
'original_status': item.state,
|
||||||
|
'timeleft': str(timedelta(seconds = float(item.left_bytes) / item.down_rate))
|
||||||
|
if item.down_rate > 0 else -1,
|
||||||
|
'folder': item.directory
|
||||||
|
})
|
||||||
|
|
||||||
|
return statuses
|
||||||
|
|
||||||
|
except Exception, err:
|
||||||
|
log.error('Failed to get status from rTorrent: %s', err)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def pause(self, download_info, pause = True):
|
||||||
|
if not self.connect():
|
||||||
|
return False
|
||||||
|
|
||||||
|
torrent = self.rt.find_torrent(download_info['id'])
|
||||||
|
if torrent is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if pause:
|
||||||
|
return torrent.pause()
|
||||||
|
return torrent.resume()
|
||||||
|
|
||||||
|
def removeFailed(self, item):
|
||||||
|
log.info('%s failed downloading, deleting...', item['name'])
|
||||||
|
return self.processComplete(item, delete_files = True)
|
||||||
|
|
||||||
|
def processComplete(self, item, delete_files):
|
||||||
|
log.debug('Requesting rTorrent to remove the torrent %s%s.',
|
||||||
|
(item['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
||||||
|
if not self.connect():
|
||||||
|
return False
|
||||||
|
|
||||||
|
torrent = self.rt.find_torrent(item['id'])
|
||||||
|
if torrent is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
torrent.erase() # just removes the torrent, doesn't delete data
|
||||||
|
|
||||||
|
if delete_files:
|
||||||
|
shutil.rmtree(item['folder'], True)
|
||||||
|
|
||||||
|
return True
|
||||||
@@ -47,7 +47,7 @@ config = [{
|
|||||||
{
|
{
|
||||||
'name': 'remove_complete',
|
'name': 'remove_complete',
|
||||||
'label': 'Remove torrent',
|
'label': 'Remove torrent',
|
||||||
'default': False,
|
'default': True,
|
||||||
'advanced': True,
|
'advanced': True,
|
||||||
'type': 'bool',
|
'type': 'bool',
|
||||||
'description': 'Remove the torrent from Transmission after it finished seeding.',
|
'description': 'Remove the torrent from Transmission after it finished seeding.',
|
||||||
|
|||||||
@@ -129,9 +129,9 @@ class Transmission(Downloader):
|
|||||||
|
|
||||||
def pause(self, item, pause = True):
|
def pause(self, item, pause = True):
|
||||||
if pause:
|
if pause:
|
||||||
return self.trpc.stop_torrent(item['hashString'])
|
return self.trpc.stop_torrent(item['id'])
|
||||||
else:
|
else:
|
||||||
return self.trpc.start_torrent(item['hashString'])
|
return self.trpc.start_torrent(item['id'])
|
||||||
|
|
||||||
def removeFailed(self, item):
|
def removeFailed(self, item):
|
||||||
log.info('%s failed downloading, deleting...', item['name'])
|
log.info('%s failed downloading, deleting...', item['name'])
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ config = [{
|
|||||||
{
|
{
|
||||||
'name': 'remove_complete',
|
'name': 'remove_complete',
|
||||||
'label': 'Remove torrent',
|
'label': 'Remove torrent',
|
||||||
'default': False,
|
'default': True,
|
||||||
'advanced': True,
|
'advanced': True,
|
||||||
'type': 'bool',
|
'type': 'bool',
|
||||||
'description': 'Remove the torrent from uTorrent after it finished seeding.',
|
'description': 'Remove the torrent from uTorrent after it finished seeding.',
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ from multipartpost import MultipartPostHandler
|
|||||||
import cookielib
|
import cookielib
|
||||||
import httplib
|
import httplib
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
|
import stat
|
||||||
import time
|
import time
|
||||||
import urllib
|
import urllib
|
||||||
import urllib2
|
import urllib2
|
||||||
@@ -52,7 +54,7 @@ class uTorrent(Downloader):
|
|||||||
new_settings['seed_prio_limitul_flag'] = True
|
new_settings['seed_prio_limitul_flag'] = True
|
||||||
log.info('Updated uTorrent settings to set a torrent to complete after it the seeding requirements are met.')
|
log.info('Updated uTorrent settings to set a torrent to complete after it the seeding requirements are met.')
|
||||||
|
|
||||||
if settings.get('bt.read_only_on_complete'): #This doesnt work as this option seems to be not available through the api
|
if settings.get('bt.read_only_on_complete'): #This doesn't work as this option seems to be not available through the api. Mitigated with removeReadOnly function
|
||||||
new_settings['bt.read_only_on_complete'] = False
|
new_settings['bt.read_only_on_complete'] = False
|
||||||
log.info('Updated uTorrent settings to not set the files to read only after completing.')
|
log.info('Updated uTorrent settings to not set the files to read only after completing.')
|
||||||
|
|
||||||
@@ -93,7 +95,7 @@ class uTorrent(Downloader):
|
|||||||
else:
|
else:
|
||||||
self.utorrent_api.add_torrent_file(torrent_filename, filedata)
|
self.utorrent_api.add_torrent_file(torrent_filename, filedata)
|
||||||
|
|
||||||
# Change settings of added torrents
|
# Change settings of added torrent
|
||||||
self.utorrent_api.set_torrent(torrent_hash, torrent_params)
|
self.utorrent_api.set_torrent(torrent_hash, torrent_params)
|
||||||
if self.conf('paused', default = 0):
|
if self.conf('paused', default = 0):
|
||||||
self.utorrent_api.pause_torrent(torrent_hash)
|
self.utorrent_api.pause_torrent(torrent_hash)
|
||||||
@@ -130,8 +132,10 @@ class uTorrent(Downloader):
|
|||||||
status = 'busy'
|
status = 'busy'
|
||||||
if 'Finished' in item[21]:
|
if 'Finished' in item[21]:
|
||||||
status = 'completed'
|
status = 'completed'
|
||||||
|
self.removeReadOnly(item[26])
|
||||||
elif 'Seeding' in item[21]:
|
elif 'Seeding' in item[21]:
|
||||||
status = 'seeding'
|
status = 'seeding'
|
||||||
|
self.removeReadOnly(item[26])
|
||||||
|
|
||||||
statuses.append({
|
statuses.append({
|
||||||
'id': item[0],
|
'id': item[0],
|
||||||
@@ -145,10 +149,10 @@ class uTorrent(Downloader):
|
|||||||
|
|
||||||
return statuses
|
return statuses
|
||||||
|
|
||||||
def pause(self, download_info, pause = True):
|
def pause(self, item, pause = True):
|
||||||
if not self.connect():
|
if not self.connect():
|
||||||
return False
|
return False
|
||||||
return self.utorrent_api.pause_torrent(download_info['id'], pause)
|
return self.utorrent_api.pause_torrent(item['id'], pause)
|
||||||
|
|
||||||
def removeFailed(self, item):
|
def removeFailed(self, item):
|
||||||
log.info('%s failed downloading, deleting...', item['name'])
|
log.info('%s failed downloading, deleting...', item['name'])
|
||||||
@@ -161,6 +165,13 @@ class uTorrent(Downloader):
|
|||||||
if not self.connect():
|
if not self.connect():
|
||||||
return False
|
return False
|
||||||
return self.utorrent_api.remove_torrent(item['id'], remove_data = delete_files)
|
return self.utorrent_api.remove_torrent(item['id'], remove_data = delete_files)
|
||||||
|
|
||||||
|
def removeReadOnly(self, folder):
|
||||||
|
#Removes all read-only flags in a folder
|
||||||
|
if folder and os.path.isdir(folder):
|
||||||
|
for root, folders, filenames in os.walk(folder):
|
||||||
|
for filename in filenames:
|
||||||
|
os.chmod(os.path.join(root, filename), stat.S_IWRITE)
|
||||||
|
|
||||||
class uTorrentAPI(object):
|
class uTorrentAPI(object):
|
||||||
|
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ def getImdb(txt, check_inside = True, multiple = False):
|
|||||||
try:
|
try:
|
||||||
ids = re.findall('(tt\d{7})', txt)
|
ids = re.findall('(tt\d{7})', txt)
|
||||||
if multiple:
|
if multiple:
|
||||||
return ids if len(ids) > 0 else []
|
return list(set(ids)) if len(ids) > 0 else []
|
||||||
return ids[0]
|
return ids[0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
pass
|
pass
|
||||||
@@ -140,7 +140,11 @@ def tryInt(s):
|
|||||||
except: return 0
|
except: return 0
|
||||||
|
|
||||||
def tryFloat(s):
|
def tryFloat(s):
|
||||||
try: return float(s) if '.' in s else tryInt(s)
|
try:
|
||||||
|
if isinstance(s, str):
|
||||||
|
return float(s) if '.' in s else tryInt(s)
|
||||||
|
else:
|
||||||
|
return float(s)
|
||||||
except: return 0
|
except: return 0
|
||||||
|
|
||||||
def natsortKey(s):
|
def natsortKey(s):
|
||||||
|
|||||||
@@ -6,20 +6,20 @@ import traceback
|
|||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
class Loader(object):
|
|
||||||
|
|
||||||
|
class Loader(object):
|
||||||
plugins = {}
|
plugins = {}
|
||||||
providers = {}
|
providers = {}
|
||||||
modules = {}
|
modules = {}
|
||||||
|
|
||||||
def addPath(self, root, base_path, priority, recursive=False):
|
def addPath(self, root, base_path, priority, recursive = False):
|
||||||
for filename in os.listdir(os.path.join(root, *base_path)):
|
for filename in os.listdir(os.path.join(root, *base_path)):
|
||||||
path = os.path.join(os.path.join(root, *base_path), filename)
|
path = os.path.join(os.path.join(root, *base_path), filename)
|
||||||
if os.path.isdir(path) and filename[:2] != '__':
|
if os.path.isdir(path) and filename[:2] != '__':
|
||||||
if not u'__init__.py' in os.listdir(path):
|
if not u'__init__.py' in os.listdir(path):
|
||||||
return
|
return
|
||||||
new_base_path = ''.join(s + '.' for s in base_path) + filename
|
new_base_path = ''.join(s + '.' for s in base_path) + filename
|
||||||
self.paths[new_base_path.replace('.', '_')] = (priority, new_base_path, path)
|
self.paths[new_base_path.replace('.', '_')] = (priority, new_base_path, path)
|
||||||
if recursive:
|
if recursive:
|
||||||
self.addPath(root, base_path + [filename], priority, recursive = True)
|
self.addPath(root, base_path + [filename], priority, recursive = True)
|
||||||
|
|
||||||
@@ -98,14 +98,16 @@ class Loader(object):
|
|||||||
|
|
||||||
for cur_file in glob.glob(os.path.join(dir_name, '*')):
|
for cur_file in glob.glob(os.path.join(dir_name, '*')):
|
||||||
name = os.path.basename(cur_file)
|
name = os.path.basename(cur_file)
|
||||||
if os.path.isdir(os.path.join(dir_name, name)):
|
if os.path.isdir(os.path.join(dir_name, name)) and name != 'static':
|
||||||
module_name = '%s.%s' % (module, name)
|
module_name = '%s.%s' % (module, name)
|
||||||
self.addModule(priority, plugin_type, module_name, name)
|
self.addModule(priority, plugin_type, module_name, name)
|
||||||
|
|
||||||
def loadSettings(self, module, name, save = True):
|
def loadSettings(self, module, name, save = True):
|
||||||
|
|
||||||
if not hasattr(module, 'config'):
|
if not hasattr(module, 'config'):
|
||||||
log.warning('Skip loading settings for plugin %s as it has no config section' % module.__file__)
|
log.debug('Skip loading settings for plugin %s as it has no config section' % module.__file__)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for section in module.config:
|
for section in module.config:
|
||||||
fireEvent('settings.options', section['name'], section)
|
fireEvent('settings.options', section['name'], section)
|
||||||
@@ -120,8 +122,9 @@ class Loader(object):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def loadPlugins(self, module, name):
|
def loadPlugins(self, module, name):
|
||||||
|
|
||||||
if not hasattr(module, 'start'):
|
if not hasattr(module, 'start'):
|
||||||
log.warning('Skip startup for plugin %s as it has no start section' % module.__file__)
|
log.debug('Skip startup for plugin %s as it has no start section' % module.__file__)
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
module.start()
|
module.start()
|
||||||
@@ -150,7 +153,7 @@ class Loader(object):
|
|||||||
m = getattr(m, sub)
|
m = getattr(m, sub)
|
||||||
return m
|
return m
|
||||||
except ImportError:
|
except ImportError:
|
||||||
log.warning("Skip loading module plugin '%s' as it seems not to be a module." % name)
|
log.debug('Skip loading module plugin %s: %s', (name, traceback.format_exc()))
|
||||||
return None
|
return None
|
||||||
except:
|
except:
|
||||||
raise
|
raise
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
from couchpotato.core.event import addEvent
|
from couchpotato.core.event import addEvent
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,14 @@ config = [{
|
|||||||
'advanced': True,
|
'advanced': True,
|
||||||
'description': 'Only update the first host when movie snatched, useful for synced XBMC',
|
'description': 'Only update the first host when movie snatched, useful for synced XBMC',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'name': 'remote_dir_scan',
|
||||||
|
'label': 'Remote Folder Scan',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Only scan new movie folder at remote XBMC servers. Works if movie location is the same.',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'name': 'on_snatch',
|
'name': 'on_snatch',
|
||||||
'default': 0,
|
'default': 0,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ log = CPLog(__name__)
|
|||||||
|
|
||||||
class XBMC(Notification):
|
class XBMC(Notification):
|
||||||
|
|
||||||
listen_to = ['renamer.after']
|
listen_to = ['renamer.after', 'movie.snatched']
|
||||||
use_json_notifications = {}
|
use_json_notifications = {}
|
||||||
http_time_between_calls = 0
|
http_time_between_calls = 0
|
||||||
|
|
||||||
@@ -33,15 +33,19 @@ class XBMC(Notification):
|
|||||||
('GUI.ShowNotification', {'title': self.default_title, 'message': message, 'image': self.getNotificationImage('small')}),
|
('GUI.ShowNotification', {'title': self.default_title, 'message': message, 'image': self.getNotificationImage('small')}),
|
||||||
]
|
]
|
||||||
|
|
||||||
if not self.conf('only_first') or hosts.index(host) == 0:
|
if data and data.get('destination_dir') and (not self.conf('only_first') or hosts.index(host) == 0):
|
||||||
calls.append(('VideoLibrary.Scan', {}))
|
param = {}
|
||||||
|
if self.conf('remote_dir_scan') or socket.getfqdn('localhost') == socket.getfqdn(host.split(':')[0]):
|
||||||
|
param = {'directory': data['destination_dir']}
|
||||||
|
|
||||||
|
calls.append(('VideoLibrary.Scan', param))
|
||||||
|
|
||||||
max_successful += len(calls)
|
max_successful += len(calls)
|
||||||
response = self.request(host, calls)
|
response = self.request(host, calls)
|
||||||
else:
|
else:
|
||||||
response = self.notifyXBMCnoJSON(host, {'title':self.default_title, 'message':message})
|
response = self.notifyXBMCnoJSON(host, {'title':self.default_title, 'message':message})
|
||||||
|
|
||||||
if not self.conf('only_first') or hosts.index(host) == 0:
|
if data and data.get('destination_dir') and (not self.conf('only_first') or hosts.index(host) == 0):
|
||||||
response += self.request(host, [('VideoLibrary.Scan', {})])
|
response += self.request(host, [('VideoLibrary.Scan', {})])
|
||||||
max_successful += 1
|
max_successful += 1
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ class Automation(Plugin):
|
|||||||
movie_ids = []
|
movie_ids = []
|
||||||
|
|
||||||
for imdb_id in movies:
|
for imdb_id in movies:
|
||||||
|
|
||||||
|
if self.shuttingDown():
|
||||||
|
break
|
||||||
|
|
||||||
prop_name = 'automation.added.%s' % imdb_id
|
prop_name = 'automation.added.%s' % imdb_id
|
||||||
added = Env.prop(prop_name, default = False)
|
added = Env.prop(prop_name, default = False)
|
||||||
if not added:
|
if not added:
|
||||||
@@ -35,5 +39,11 @@ class Automation(Plugin):
|
|||||||
Env.prop(prop_name, True)
|
Env.prop(prop_name, True)
|
||||||
|
|
||||||
for movie_id in movie_ids:
|
for movie_id in movie_ids:
|
||||||
|
|
||||||
|
if self.shuttingDown():
|
||||||
|
break
|
||||||
|
|
||||||
movie_dict = fireEvent('movie.get', movie_id, single = True)
|
movie_dict = fireEvent('movie.get', movie_id, single = True)
|
||||||
fireEvent('movie.searcher.single', movie_dict)
|
fireEvent('movie.searcher.single', movie_dict)
|
||||||
|
|
||||||
|
return True
|
||||||
@@ -174,7 +174,7 @@ class Release(Plugin):
|
|||||||
# Get matching provider
|
# Get matching provider
|
||||||
provider = fireEvent('provider.belongs_to', item['url'], provider = item.get('provider'), single = True)
|
provider = fireEvent('provider.belongs_to', item['url'], provider = item.get('provider'), single = True)
|
||||||
|
|
||||||
if item['type'] != 'torrent_magnet':
|
if item['protocol'] != 'torrent_magnet':
|
||||||
item['download'] = provider.loginDownload if provider.urls.get('login') else provider.download
|
item['download'] = provider.loginDownload if provider.urls.get('login') else provider.download
|
||||||
|
|
||||||
success = fireEvent('searcher.download', data = item, movie = rel.movie.to_dict({
|
success = fireEvent('searcher.download', data = item, movie = rel.movie.to_dict({
|
||||||
|
|||||||
Regular → Executable
+10
-3
@@ -27,6 +27,7 @@ rename_options = {
|
|||||||
'imdb_id': 'IMDB id (tt0123456)',
|
'imdb_id': 'IMDB id (tt0123456)',
|
||||||
'cd': 'CD number (cd1)',
|
'cd': 'CD number (cd1)',
|
||||||
'cd_nr': 'Just the cd nr. (1)',
|
'cd_nr': 'Just the cd nr. (1)',
|
||||||
|
'mpaa': 'MPAA Rating',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +73,12 @@ config = [{
|
|||||||
'type': 'choice',
|
'type': 'choice',
|
||||||
'options': rename_options
|
'options': rename_options
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'name': 'unrar',
|
||||||
|
'type': 'bool',
|
||||||
|
'description': 'Extract rar files if found.',
|
||||||
|
'default': False,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'name': 'cleanup',
|
'name': 'cleanup',
|
||||||
'type': 'bool',
|
'type': 'bool',
|
||||||
@@ -119,10 +126,10 @@ config = [{
|
|||||||
{
|
{
|
||||||
'name': 'file_action',
|
'name': 'file_action',
|
||||||
'label': 'Torrent File Action',
|
'label': 'Torrent File Action',
|
||||||
'default': 'move',
|
'default': 'link',
|
||||||
'type': 'dropdown',
|
'type': 'dropdown',
|
||||||
'values': [('Move', 'move'), ('Copy', 'copy'), ('Hard link', 'hardlink'), ('Move & Sym link', 'move_symlink')],
|
'values': [('Link', 'link'), ('Copy', 'copy'), ('Move', 'move')],
|
||||||
'description': 'Define which kind of file operation you want to use for torrents. Before you start using <a href="http://en.wikipedia.org/wiki/Hard_link">hard links</a> or <a href="http://en.wikipedia.org/wiki/Sym_link">sym links</a>, PLEASE read about their possible drawbacks.',
|
'description': '<strong>Link</strong> or <strong>Copy</strong> after downloading completed (and allow for seeding), or <strong>Move</strong> after seeding completed. Link first tries <a href="http://en.wikipedia.org/wiki/Hard_link">hard link</a>, then <a href="http://en.wikipedia.org/wiki/Sym_link">sym link</a> and falls back to Copy.',
|
||||||
'advanced': True,
|
'advanced': True,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ from couchpotato.core.plugins.base import Plugin
|
|||||||
from couchpotato.core.settings.model import Library, File, Profile, Release, \
|
from couchpotato.core.settings.model import Library, File, Profile, Release, \
|
||||||
ReleaseInfo
|
ReleaseInfo
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
|
from unrar2 import RarFile, RarInfo
|
||||||
|
from unrar2.rar_exceptions import *
|
||||||
import errno
|
import errno
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import os
|
import os
|
||||||
@@ -126,6 +128,11 @@ class Renamer(Plugin):
|
|||||||
# Extend the download info with info stored in the downloaded release
|
# Extend the download info with info stored in the downloaded release
|
||||||
download_info = self.extendDownloadInfo(download_info)
|
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'),
|
groups = fireEvent('scanner.scan', folder = folder if folder else self.conf('from'),
|
||||||
files = files, download_info = download_info, return_ignored = False, single = True)
|
files = files, download_info = download_info, return_ignored = False, single = True)
|
||||||
|
|
||||||
@@ -179,6 +186,9 @@ class Renamer(Plugin):
|
|||||||
group['before_rename'] = []
|
group['before_rename'] = []
|
||||||
fireEvent('renamer.before', group)
|
fireEvent('renamer.before', group)
|
||||||
|
|
||||||
|
# Add extracted files to the before_rename list
|
||||||
|
group['before_rename'].extend(extr_files)
|
||||||
|
|
||||||
# Remove weird chars from moviename
|
# Remove weird chars from moviename
|
||||||
movie_name = re.sub(r"[\x00\/\\:\*\?\"<>\|]", '', movie_title)
|
movie_name = re.sub(r"[\x00\/\\:\*\?\"<>\|]", '', movie_title)
|
||||||
|
|
||||||
@@ -205,6 +215,7 @@ class Renamer(Plugin):
|
|||||||
'imdb_id': library['identifier'],
|
'imdb_id': library['identifier'],
|
||||||
'cd': '',
|
'cd': '',
|
||||||
'cd_nr': '',
|
'cd_nr': '',
|
||||||
|
'mpaa': library['info'].get('mpaa', ''),
|
||||||
}
|
}
|
||||||
|
|
||||||
for file_type in group['files']:
|
for file_type in group['files']:
|
||||||
@@ -212,8 +223,8 @@ class Renamer(Plugin):
|
|||||||
# Move nfo depending on settings
|
# Move nfo depending on settings
|
||||||
if file_type is 'nfo' and not self.conf('rename_nfo'):
|
if file_type is 'nfo' and not self.conf('rename_nfo'):
|
||||||
log.debug('Skipping, renaming of %s disabled', file_type)
|
log.debug('Skipping, renaming of %s disabled', file_type)
|
||||||
if self.conf('cleanup') and not (self.conf('file_action') != 'move' and 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.downloadIsTorrent(download_info) or self.fileIsAdded(current_file, group)):
|
||||||
remove_files.append(current_file)
|
remove_files.append(current_file)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -393,14 +404,15 @@ class Renamer(Plugin):
|
|||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
# Remove leftover files
|
# Remove leftover files
|
||||||
if self.conf('cleanup') and not self.conf('move_leftover') and remove_leftovers and \
|
if not remove_leftovers: # Don't remove anything
|
||||||
not (self.conf('file_action') != 'move' and 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
|
|
||||||
break
|
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.downloadIsTorrent(download_info) or self.fileIsAdded(current_file, group)):
|
||||||
|
remove_files.append(current_file)
|
||||||
|
|
||||||
# Remove files
|
# Remove files
|
||||||
delete_folders = []
|
delete_folders = []
|
||||||
for src in remove_files:
|
for src in remove_files:
|
||||||
@@ -451,8 +463,7 @@ class Renamer(Plugin):
|
|||||||
self.tagDir(group, 'failed_rename')
|
self.tagDir(group, 'failed_rename')
|
||||||
|
|
||||||
# Tag folder if it is in the 'from' folder and it will not be removed because it is a torrent
|
# Tag folder if it is in the 'from' folder and it will not be removed because it is a torrent
|
||||||
if self.movieInFromFolder(movie_folder) and \
|
if self.movieInFromFolder(movie_folder) and self.downloadIsTorrent(download_info):
|
||||||
self.conf('file_action') != 'move' and self.downloadIsTorrent(download_info):
|
|
||||||
self.tagDir(group, 'renamed_already')
|
self.tagDir(group, 'renamed_already')
|
||||||
|
|
||||||
# Remove matching releases
|
# Remove matching releases
|
||||||
@@ -463,8 +474,7 @@ class Renamer(Plugin):
|
|||||||
except:
|
except:
|
||||||
log.error('Failed removing %s: %s', (release.identifier, traceback.format_exc()))
|
log.error('Failed removing %s: %s', (release.identifier, traceback.format_exc()))
|
||||||
|
|
||||||
if group['dirname'] and group['parentdir'] and \
|
if group['dirname'] and group['parentdir'] and not self.downloadIsTorrent(download_info):
|
||||||
not (self.conf('file_action') != 'move' and self.downloadIsTorrent(download_info)):
|
|
||||||
try:
|
try:
|
||||||
log.info('Deleting folder: %s', group['parentdir'])
|
log.info('Deleting folder: %s', group['parentdir'])
|
||||||
self.deleteEmptyFolder(group['parentdir'])
|
self.deleteEmptyFolder(group['parentdir'])
|
||||||
@@ -524,22 +534,22 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
|||||||
if ignore_file:
|
if ignore_file:
|
||||||
self.createFile(ignore_file, text)
|
self.createFile(ignore_file, text)
|
||||||
|
|
||||||
def untagDir(self, folder, tag = None):
|
def untagDir(self, folder, tag = ''):
|
||||||
if not os.path.isdir(folder):
|
if not os.path.isdir(folder):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Remove any .ignore files
|
# Remove any .ignore files
|
||||||
for root, dirnames, filenames in os.walk(folder):
|
for root, dirnames, filenames in os.walk(folder):
|
||||||
for filename in fnmatch.filter(filenames, '%s.ignore' % tag if tag else '*'):
|
for filename in fnmatch.filter(filenames, '*%s.ignore' % tag):
|
||||||
os.remove((os.path.join(root, filename)))
|
os.remove((os.path.join(root, filename)))
|
||||||
|
|
||||||
def hastagDir(self, folder, tag = None):
|
def hastagDir(self, folder, tag = ''):
|
||||||
if not os.path.isdir(folder):
|
if not os.path.isdir(folder):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Find any .ignore files
|
# Find any .ignore files
|
||||||
for root, dirnames, filenames in os.walk(folder):
|
for root, dirnames, filenames in os.walk(folder):
|
||||||
if fnmatch.filter(filenames, '%s.ignore' % tag if tag else '*'):
|
if fnmatch.filter(filenames, '*%s.ignore' % tag):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
@@ -549,17 +559,23 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
|||||||
try:
|
try:
|
||||||
if forcemove:
|
if forcemove:
|
||||||
shutil.move(old, dest)
|
shutil.move(old, dest)
|
||||||
elif self.conf('file_action') == 'hardlink':
|
|
||||||
try:
|
|
||||||
link(old, dest)
|
|
||||||
except:
|
|
||||||
log.error('Couldn\'t hardlink file "%s" to "%s". Copying instead. Error: %s. ', (old, dest, traceback.format_exc()))
|
|
||||||
shutil.copy(old, dest)
|
|
||||||
elif self.conf('file_action') == 'copy':
|
elif self.conf('file_action') == 'copy':
|
||||||
shutil.copy(old, dest)
|
shutil.copy(old, dest)
|
||||||
elif self.conf('file_action') == 'move_symlink':
|
elif self.conf('file_action') == 'link':
|
||||||
shutil.move(old, dest)
|
# First try to hardlink
|
||||||
symlink(dest, old)
|
try:
|
||||||
|
log.debug('Hardlinking file "%s" to "%s"...', (old, dest))
|
||||||
|
link(old, dest)
|
||||||
|
except:
|
||||||
|
# Try to simlink next
|
||||||
|
log.debug('Couldn\'t hardlink file "%s" to "%s". Simlinking instead. Error: %s. ', (old, dest, traceback.format_exc()))
|
||||||
|
shutil.copy(old, dest)
|
||||||
|
try:
|
||||||
|
symlink(dest, old + '.link')
|
||||||
|
os.unlink(old)
|
||||||
|
os.rename(old + '.link', old)
|
||||||
|
except:
|
||||||
|
log.error('Couldn\'t symlink file "%s" to "%s". Copied instead. Error: %s. ', (old, dest, traceback.format_exc()))
|
||||||
else:
|
else:
|
||||||
shutil.move(old, dest)
|
shutil.move(old, dest)
|
||||||
|
|
||||||
@@ -764,10 +780,10 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
|||||||
for item in scan_items:
|
for item in scan_items:
|
||||||
# Ask the renamer to scan the item
|
# Ask the renamer to scan the item
|
||||||
if item['scan']:
|
if item['scan']:
|
||||||
if item['pause'] and self.conf('file_action') == 'move_symlink':
|
if item['pause'] and self.conf('file_action') == 'link':
|
||||||
fireEvent('download.pause', item = item, pause = True, single = True)
|
fireEvent('download.pause', item = item, pause = True, single = True)
|
||||||
fireEvent('renamer.scan', download_info = item)
|
fireEvent('renamer.scan', download_info = item)
|
||||||
if item['pause'] and self.conf('file_action') == 'move_symlink':
|
if item['pause'] and self.conf('file_action') == 'link':
|
||||||
fireEvent('download.pause', item = item, pause = False, single = True)
|
fireEvent('download.pause', item = item, pause = False, single = True)
|
||||||
if item['process_complete']:
|
if item['process_complete']:
|
||||||
#First make sure the files were succesfully processed
|
#First make sure the files were succesfully processed
|
||||||
@@ -811,13 +827,13 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
|||||||
download_info.update({
|
download_info.update({
|
||||||
'imdb_id': rls.movie.library.identifier,
|
'imdb_id': rls.movie.library.identifier,
|
||||||
'quality': rls.quality.identifier,
|
'quality': rls.quality.identifier,
|
||||||
'type': rls_dict.get('info', {}).get('type')
|
'protocol': rls_dict.get('info', {}).get('protocol') or rls_dict.get('info', {}).get('type'),
|
||||||
})
|
})
|
||||||
|
|
||||||
return download_info
|
return download_info
|
||||||
|
|
||||||
def downloadIsTorrent(self, download_info):
|
def downloadIsTorrent(self, download_info):
|
||||||
return download_info and download_info.get('type') in ['torrent', 'torrent_magnet']
|
return download_info and download_info.get('protocol') in ['torrent', 'torrent_magnet']
|
||||||
|
|
||||||
def fileIsAdded(self, src, group):
|
def fileIsAdded(self, src, group):
|
||||||
if not group or not group.get('before_rename'):
|
if not group or not group.get('before_rename'):
|
||||||
@@ -826,6 +842,130 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
|||||||
|
|
||||||
def statusInfoComplete(self, item):
|
def statusInfoComplete(self, item):
|
||||||
return item['id'] and item['downloader'] and item['folder']
|
return item['id'] and item['downloader'] and item['folder']
|
||||||
|
|
||||||
def movieInFromFolder(self, movie_folder):
|
def movieInFromFolder(self, movie_folder):
|
||||||
return movie_folder and self.conf('from') in movie_folder or not movie_folder
|
return movie_folder and self.conf('from') in movie_folder or not movie_folder
|
||||||
|
|
||||||
|
def extractFiles(self, folder = None, movie_folder = None, files = [], cleanup = False):
|
||||||
|
|
||||||
|
# RegEx for finding rar files
|
||||||
|
archive_regex = '(?P<file>^(?P<base>(?:(?!\.part\d+\.rar$).)*)\.(?:(?:part0*1\.)?rar)$)'
|
||||||
|
restfile_regex = '(^%s\.(?:part(?!0*1\.rar$)\d+\.rar$|[rstuvw]\d+$))'
|
||||||
|
extr_files = []
|
||||||
|
|
||||||
|
# Check input variables
|
||||||
|
if not folder:
|
||||||
|
folder = self.conf('from')
|
||||||
|
|
||||||
|
check_file_date = True
|
||||||
|
if movie_folder:
|
||||||
|
check_file_date = False
|
||||||
|
|
||||||
|
if not files:
|
||||||
|
for root, folders, names in os.walk(folder):
|
||||||
|
files.extend([os.path.join(root, name) for name in names])
|
||||||
|
|
||||||
|
# Find all archive files
|
||||||
|
archives = [re.search(archive_regex, name).groupdict() for name in files if re.search(archive_regex, name)]
|
||||||
|
|
||||||
|
#Extract all found archives
|
||||||
|
for archive in archives:
|
||||||
|
# Check if it has already been processed by CPS
|
||||||
|
if (self.hastagDir(os.path.dirname(archive['file']))):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Find all related archive files
|
||||||
|
archive['files'] = [name for name in files if re.search(restfile_regex % re.escape(archive['base']), name)]
|
||||||
|
archive['files'].append(archive['file'])
|
||||||
|
|
||||||
|
# Check if archive is fresh and maybe still copying/moving/downloading, ignore files newer than 1 minute
|
||||||
|
if check_file_date:
|
||||||
|
file_too_new = False
|
||||||
|
for cur_file in archive['files']:
|
||||||
|
if not os.path.isfile(cur_file):
|
||||||
|
file_too_new = time.time()
|
||||||
|
break
|
||||||
|
file_time = [os.path.getmtime(cur_file), os.path.getctime(cur_file)]
|
||||||
|
for t in file_time:
|
||||||
|
if t > time.time() - 60:
|
||||||
|
file_too_new = tryInt(time.time() - t)
|
||||||
|
break
|
||||||
|
|
||||||
|
if file_too_new:
|
||||||
|
break
|
||||||
|
|
||||||
|
if file_too_new:
|
||||||
|
try:
|
||||||
|
time_string = time.ctime(file_time[0])
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
time_string = time.ctime(file_time[1])
|
||||||
|
except:
|
||||||
|
time_string = 'unknown'
|
||||||
|
|
||||||
|
log.info('Archive seems to be still copying/moving/downloading or just copied/moved/downloaded (created on %s), ignoring for now: %s', (time_string, os.path.basename(archive['file'])))
|
||||||
|
continue
|
||||||
|
|
||||||
|
log.info('Archive %s found. Extracting...', os.path.basename(archive['file']))
|
||||||
|
try:
|
||||||
|
rar_handle = RarFile(archive['file'])
|
||||||
|
extr_path = os.path.join(self.conf('from'), os.path.relpath(os.path.dirname(archive['file']), folder))
|
||||||
|
self.makeDir(extr_path)
|
||||||
|
for packedinfo in rar_handle.infolist():
|
||||||
|
if not packedinfo.isdir and not os.path.isfile(os.path.join(extr_path, os.path.basename(packedinfo.filename))):
|
||||||
|
log.debug('Extracting %s...', packedinfo.filename)
|
||||||
|
rar_handle.extract(condition = [packedinfo.index], path = extr_path, withSubpath = False, overwrite = False)
|
||||||
|
extr_files.append(os.path.join(extr_path, os.path.basename(packedinfo.filename)))
|
||||||
|
del rar_handle
|
||||||
|
except Exception, e:
|
||||||
|
log.error('Failed to extract %s: %s %s', (archive['file'], e, traceback.format_exc()))
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Delete the archive files
|
||||||
|
for filename in archive['files']:
|
||||||
|
if cleanup:
|
||||||
|
try:
|
||||||
|
os.remove(filename)
|
||||||
|
except Exception, e:
|
||||||
|
log.error('Failed to remove %s: %s %s', (filename, e, traceback.format_exc()))
|
||||||
|
continue
|
||||||
|
files.remove(filename)
|
||||||
|
|
||||||
|
# Move the rest of the files and folders if any files are extracted to the from folder (only if folder was provided)
|
||||||
|
if extr_files and os.path.normpath(os.path.normcase(folder)) != os.path.normpath(os.path.normcase(self.conf('from'))):
|
||||||
|
for leftoverfile in list(files):
|
||||||
|
move_to = os.path.join(self.conf('from'), os.path.relpath(leftoverfile, folder))
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.makeDir(os.path.dirname(move_to))
|
||||||
|
self.moveFile(leftoverfile, move_to, cleanup)
|
||||||
|
except Exception, e:
|
||||||
|
log.error('Failed moving left over file %s to %s: %s %s',(leftoverfile, move_to, e, traceback.format_exc()))
|
||||||
|
# As we probably tried to overwrite the nfo file, check if it exists and then remove the original
|
||||||
|
if os.path.isfile(move_to):
|
||||||
|
if cleanup:
|
||||||
|
log.info('Deleting left over file %s instead...', leftoverfile)
|
||||||
|
os.unlink(leftoverfile)
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
files.remove(leftoverfile)
|
||||||
|
extr_files.append(move_to)
|
||||||
|
|
||||||
|
if cleanup:
|
||||||
|
# Remove all left over folders
|
||||||
|
log.debug('Removing old movie folder %s...', movie_folder)
|
||||||
|
self.deleteEmptyFolder(movie_folder)
|
||||||
|
|
||||||
|
movie_folder = os.path.join(self.conf('from'), os.path.relpath(movie_folder, folder))
|
||||||
|
folder = self.conf('from')
|
||||||
|
|
||||||
|
if extr_files:
|
||||||
|
files.extend(extr_files)
|
||||||
|
|
||||||
|
# Cleanup files and folder if movie_folder was not provided
|
||||||
|
if not movie_folder:
|
||||||
|
files = []
|
||||||
|
folder = None
|
||||||
|
|
||||||
|
return (folder, movie_folder, files, extr_files)
|
||||||
@@ -277,7 +277,7 @@ class Scanner(Plugin):
|
|||||||
except:
|
except:
|
||||||
break
|
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
|
file_too_new = False
|
||||||
for cur_file in group['unsorted_files']:
|
for cur_file in group['unsorted_files']:
|
||||||
if not os.path.isfile(cur_file):
|
if not os.path.isfile(cur_file):
|
||||||
@@ -329,14 +329,17 @@ class Scanner(Plugin):
|
|||||||
|
|
||||||
del movie_files
|
del movie_files
|
||||||
|
|
||||||
|
total_found = len(valid_files)
|
||||||
|
|
||||||
# Make sure only one movie was found if a download ID is provided
|
# Make sure only one movie was found if a download ID is provided
|
||||||
if download_info and not len(valid_files) == 1:
|
if download_info and total_found == 0:
|
||||||
|
log.info('Download ID provided (%s), but no groups found! Make sure the download contains valid media files (fully extracted).', download_info.get('imdb_id'))
|
||||||
|
elif download_info and total_found > 1:
|
||||||
log.info('Download ID provided (%s), but more than one group found (%s). Ignoring Download ID...', (download_info.get('imdb_id'), len(valid_files)))
|
log.info('Download ID provided (%s), but more than one group found (%s). Ignoring Download ID...', (download_info.get('imdb_id'), len(valid_files)))
|
||||||
download_info = None
|
download_info = None
|
||||||
|
|
||||||
# Determine file types
|
# Determine file types
|
||||||
processed_movies = {}
|
processed_movies = {}
|
||||||
total_found = len(valid_files)
|
|
||||||
while True and not self.shuttingDown():
|
while True and not self.shuttingDown():
|
||||||
try:
|
try:
|
||||||
identifier, group = valid_files.popitem()
|
identifier, group = valid_files.popitem()
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ config = [{
|
|||||||
{
|
{
|
||||||
'tab': 'automation',
|
'tab': 'automation',
|
||||||
'list': 'watchlist_providers',
|
'list': 'watchlist_providers',
|
||||||
'name': 'imdb_automation',
|
'name': 'imdb_automation_watchlist',
|
||||||
'label': 'IMDB',
|
'label': 'IMDB',
|
||||||
'description': 'From any <strong>public</strong> IMDB watchlists. Url should be the CSV link.',
|
'description': 'From any <strong>public</strong> IMDB watchlists. Url should be the CSV link.',
|
||||||
'options': [
|
'options': [
|
||||||
@@ -30,5 +30,33 @@ config = [{
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'tab': 'automation',
|
||||||
|
'list': 'automation_providers',
|
||||||
|
'name': 'imdb_automation_charts',
|
||||||
|
'label': 'IMDB',
|
||||||
|
'description': 'Import movies from IMDB Charts',
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'automation_providers_enabled',
|
||||||
|
'default': False,
|
||||||
|
'type': 'enabler',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'automation_charts_theater',
|
||||||
|
'type': 'bool',
|
||||||
|
'label': 'In Theaters',
|
||||||
|
'description': 'New Movies <a href="http://www.imdb.com/movies-in-theaters/">In-Theaters</a> chart',
|
||||||
|
'default': True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'automation_charts_top250',
|
||||||
|
'type': 'bool',
|
||||||
|
'label': 'TOP 250',
|
||||||
|
'description': 'IMDB <a href="http://www.imdb.com/chart/top/">TOP 250</a> chart',
|
||||||
|
'default': True,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}]
|
}]
|
||||||
@@ -1,38 +1,100 @@
|
|||||||
|
import traceback
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from couchpotato import fireEvent
|
||||||
from couchpotato.core.helpers.rss import RSS
|
from couchpotato.core.helpers.rss import RSS
|
||||||
from couchpotato.core.helpers.variable import getImdb, splitString, tryInt
|
from couchpotato.core.helpers.variable import getImdb, splitString, tryInt
|
||||||
|
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.providers.automation.base import Automation
|
from couchpotato.core.providers.automation.base import Automation
|
||||||
import traceback
|
|
||||||
|
from couchpotato.core.providers.base import MultiProvider
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class IMDB(Automation, RSS):
|
class IMDB(MultiProvider):
|
||||||
|
|
||||||
|
def getTypes(self):
|
||||||
|
return [IMDBWatchlist, IMDBAutomation]
|
||||||
|
|
||||||
|
|
||||||
|
class IMDBBase(Automation, RSS):
|
||||||
|
|
||||||
interval = 1800
|
interval = 1800
|
||||||
|
|
||||||
|
def getInfo(self, imdb_id):
|
||||||
|
return fireEvent('movie.info', identifier = imdb_id, merge = True)
|
||||||
|
|
||||||
|
|
||||||
|
class IMDBWatchlist(IMDBBase):
|
||||||
|
|
||||||
|
enabled_option = 'automation_enabled'
|
||||||
|
|
||||||
def getIMDBids(self):
|
def getIMDBids(self):
|
||||||
|
|
||||||
movies = []
|
movies = []
|
||||||
|
|
||||||
enablers = [tryInt(x) for x in splitString(self.conf('automation_urls_use'))]
|
watchlist_enablers = [tryInt(x) for x in splitString(self.conf('automation_urls_use'))]
|
||||||
urls = splitString(self.conf('automation_urls'))
|
watchlist_urls = splitString(self.conf('automation_urls'))
|
||||||
|
|
||||||
index = -1
|
index = -1
|
||||||
for url in urls:
|
for watchlist_url in watchlist_urls:
|
||||||
|
|
||||||
index += 1
|
index += 1
|
||||||
if not enablers[index]:
|
if not watchlist_enablers[index]:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
rss_data = self.getHTMLData(url)
|
log.debug('Started IMDB watchlists: %s', watchlist_url)
|
||||||
|
rss_data = self.getHTMLData(watchlist_url)
|
||||||
imdbs = getImdb(rss_data, multiple = True) if rss_data else []
|
imdbs = getImdb(rss_data, multiple = True) if rss_data else []
|
||||||
|
|
||||||
for imdb in imdbs:
|
for imdb in imdbs:
|
||||||
movies.append(imdb)
|
movies.append(imdb)
|
||||||
|
|
||||||
|
if self.shuttingDown():
|
||||||
|
break
|
||||||
|
|
||||||
except:
|
except:
|
||||||
log.error('Failed loading IMDB watchlist: %s %s', (url, traceback.format_exc()))
|
log.error('Failed loading IMDB watchlist: %s %s', (url, traceback.format_exc()))
|
||||||
|
|
||||||
return movies
|
return movies
|
||||||
|
|
||||||
|
|
||||||
|
class IMDBAutomation(IMDBBase):
|
||||||
|
|
||||||
|
enabled_option = 'automation_providers_enabled'
|
||||||
|
|
||||||
|
chart_urls = {
|
||||||
|
'theater': 'http://www.imdb.com/movies-in-theaters/',
|
||||||
|
'top250': 'http://www.imdb.com/chart/top',
|
||||||
|
}
|
||||||
|
|
||||||
|
def getIMDBids(self):
|
||||||
|
|
||||||
|
movies = []
|
||||||
|
|
||||||
|
for url in self.chart_urls:
|
||||||
|
if self.conf('automation_charts_%s' % url):
|
||||||
|
data = self.getHTMLData(self.chart_urls[url])
|
||||||
|
if data:
|
||||||
|
html = BeautifulSoup(data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result_div = html.find('div', attrs = {'id': 'main'})
|
||||||
|
imdb_ids = getImdb(str(result_div), multiple = True)
|
||||||
|
|
||||||
|
for imdb_id in imdb_ids:
|
||||||
|
info = self.getInfo(imdb_id)
|
||||||
|
if info and self.isMinimalMovie(info):
|
||||||
|
movies.append(imdb_id)
|
||||||
|
|
||||||
|
if self.shuttingDown():
|
||||||
|
break
|
||||||
|
|
||||||
|
except:
|
||||||
|
log.error('Failed loading IMDB chart results from %s: %s', (url, traceback.format_exc()))
|
||||||
|
|
||||||
|
return movies
|
||||||
@@ -11,19 +11,31 @@ config = [{
|
|||||||
'list': 'automation_providers',
|
'list': 'automation_providers',
|
||||||
'name': 'rottentomatoes_automation',
|
'name': 'rottentomatoes_automation',
|
||||||
'label': 'Rottentomatoes',
|
'label': 'Rottentomatoes',
|
||||||
'description': 'Imports movies from the rottentomatoes "in theaters"-feed.',
|
'description': 'Imports movies from rottentomatoes rss feeds specified below.',
|
||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
'name': 'automation_enabled',
|
'name': 'automation_enabled',
|
||||||
'default': False,
|
'default': False,
|
||||||
'type': 'enabler',
|
'type': 'enabler',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'name': 'automation_urls_use',
|
||||||
|
'label': 'Use',
|
||||||
|
'default': '1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'automation_urls',
|
||||||
|
'label': 'url',
|
||||||
|
'type': 'combined',
|
||||||
|
'combine': ['automation_urls_use', 'automation_urls'],
|
||||||
|
'default': 'http://www.rottentomatoes.com/syndication/rss/in_theaters.xml',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'name': 'tomatometer_percent',
|
'name': 'tomatometer_percent',
|
||||||
'default': '80',
|
'default': '80',
|
||||||
'label': 'Tomatometer',
|
'label': 'Tomatometer',
|
||||||
'description': 'Use as extra scoring requirement',
|
'description': 'Use as extra scoring requirement',
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from couchpotato.core.helpers.rss import RSS
|
from couchpotato.core.helpers.rss import RSS
|
||||||
from couchpotato.core.helpers.variable import tryInt
|
from couchpotato.core.helpers.variable import tryInt, splitString
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.providers.automation.base import Automation
|
from couchpotato.core.providers.automation.base import Automation
|
||||||
from xml.etree.ElementTree import QName
|
from xml.etree.ElementTree import QName
|
||||||
@@ -11,38 +11,42 @@ log = CPLog(__name__)
|
|||||||
class Rottentomatoes(Automation, RSS):
|
class Rottentomatoes(Automation, RSS):
|
||||||
|
|
||||||
interval = 1800
|
interval = 1800
|
||||||
urls = {
|
|
||||||
'namespace': 'http://www.rottentomatoes.com/xmlns/rtmovie/',
|
|
||||||
'theater': 'http://www.rottentomatoes.com/syndication/rss/in_theaters.xml',
|
|
||||||
}
|
|
||||||
|
|
||||||
def getIMDBids(self):
|
def getIMDBids(self):
|
||||||
|
|
||||||
movies = []
|
movies = []
|
||||||
|
|
||||||
rss_movies = self.getRSSData(self.urls['theater'])
|
rotten_tomatoes_namespace = 'http://www.rottentomatoes.com/xmlns/rtmovie/'
|
||||||
rating_tag = str(QName(self.urls['namespace'], 'tomatometer_percent'))
|
urls = dict(zip(splitString(self.conf('automation_urls')), [tryInt(x) for x in splitString(self.conf('automation_urls_use'))]))
|
||||||
|
|
||||||
for movie in rss_movies:
|
for url in urls:
|
||||||
|
|
||||||
value = self.getTextElement(movie, "title")
|
if not urls[url]:
|
||||||
result = re.search('(?<=%\s).*', value)
|
continue
|
||||||
|
|
||||||
if result:
|
rss_movies = self.getRSSData(url)
|
||||||
|
rating_tag = str(QName(rotten_tomatoes_namespace, 'tomatometer_percent'))
|
||||||
|
|
||||||
log.info2('Something smells...')
|
for movie in rss_movies:
|
||||||
rating = tryInt(self.getTextElement(movie, rating_tag))
|
|
||||||
name = result.group(0)
|
|
||||||
|
|
||||||
if rating < tryInt(self.conf('tomatometer_percent')):
|
value = self.getTextElement(movie, "title")
|
||||||
log.info2('%s seems to be rotten...', name)
|
result = re.search('(?<=%\s).*', value)
|
||||||
else:
|
|
||||||
|
|
||||||
log.info2('Found %s fresh enough movies, enqueuing: %s', (rating, name))
|
if result:
|
||||||
year = datetime.datetime.now().strftime("%Y")
|
|
||||||
imdb = self.search(name, year)
|
|
||||||
|
|
||||||
if imdb and self.isMinimalMovie(imdb):
|
log.info2('Something smells...')
|
||||||
movies.append(imdb['imdb'])
|
rating = tryInt(self.getTextElement(movie, rating_tag))
|
||||||
|
name = result.group(0)
|
||||||
|
|
||||||
|
if rating < tryInt(self.conf('tomatometer_percent')):
|
||||||
|
log.info2('%s seems to be rotten...', name)
|
||||||
|
else:
|
||||||
|
|
||||||
|
log.info2('Found %s fresh enough movies, enqueuing: %s', (rating, name))
|
||||||
|
year = datetime.datetime.now().strftime("%Y")
|
||||||
|
imdb = self.search(name, year)
|
||||||
|
|
||||||
|
if imdb and self.isMinimalMovie(imdb):
|
||||||
|
movies.append(imdb['imdb'])
|
||||||
|
|
||||||
return movies
|
return movies
|
||||||
File renamed without changes.
@@ -0,0 +1,7 @@
|
|||||||
|
from .main import InfoResultModifier
|
||||||
|
|
||||||
|
def start():
|
||||||
|
|
||||||
|
return InfoResultModifier()
|
||||||
|
|
||||||
|
config = []
|
||||||
+17
-1
@@ -3,6 +3,7 @@ from couchpotato.core.event import addEvent, fireEvent
|
|||||||
from couchpotato.core.helpers.variable import mergeDicts, randomString
|
from couchpotato.core.helpers.variable import mergeDicts, randomString
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
|
from couchpotato.core.providers.base import MultiProvider
|
||||||
from couchpotato.core.settings.model import Library
|
from couchpotato.core.settings.model import Library
|
||||||
import copy
|
import copy
|
||||||
import traceback
|
import traceback
|
||||||
@@ -10,7 +11,17 @@ import traceback
|
|||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class MovieResultModifier(Plugin):
|
class InfoResultModifier(MultiProvider):
|
||||||
|
|
||||||
|
def getTypes(self):
|
||||||
|
return [Movie, Show]
|
||||||
|
|
||||||
|
|
||||||
|
class ModifierBase(Plugin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Movie(ModifierBase):
|
||||||
|
|
||||||
default_info = {
|
default_info = {
|
||||||
'tmdb_id': 0,
|
'tmdb_id': 0,
|
||||||
@@ -28,6 +39,7 @@ class MovieResultModifier(Plugin):
|
|||||||
'tagline': '',
|
'tagline': '',
|
||||||
'imdb': '',
|
'imdb': '',
|
||||||
'genres': [],
|
'genres': [],
|
||||||
|
'mpaa': None
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -92,3 +104,7 @@ class MovieResultModifier(Plugin):
|
|||||||
if result and result.get('imdb'):
|
if result and result.get('imdb'):
|
||||||
return mergeDicts(result, self.getLibraryTags(result['imdb']))
|
return mergeDicts(result, self.getLibraryTags(result['imdb']))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class Show(ModifierBase):
|
||||||
|
pass
|
||||||
@@ -3,3 +3,7 @@ from couchpotato.core.providers.base import Provider
|
|||||||
|
|
||||||
class MovieProvider(Provider):
|
class MovieProvider(Provider):
|
||||||
type = 'movie'
|
type = 'movie'
|
||||||
|
|
||||||
|
|
||||||
|
class ShowProvider(Provider):
|
||||||
|
type = 'show'
|
||||||
File renamed without changes.
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
from couchpotato.core.event import addEvent, fireEvent
|
from couchpotato.core.event import addEvent, fireEvent
|
||||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.providers.movie.base import MovieProvider
|
from couchpotato.core.providers.info.base import MovieProvider
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
import time
|
import time
|
||||||
|
|
||||||
File renamed without changes.
Regular → Executable
+2
-1
@@ -2,7 +2,7 @@ from couchpotato.core.event import addEvent, fireEvent
|
|||||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||||
from couchpotato.core.helpers.variable import tryInt, tryFloat, splitString
|
from couchpotato.core.helpers.variable import tryInt, tryFloat, splitString
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.providers.movie.base import MovieProvider
|
from couchpotato.core.providers.info.base import MovieProvider
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import traceback
|
import traceback
|
||||||
@@ -95,6 +95,7 @@ class OMDBAPI(MovieProvider):
|
|||||||
#'rotten': (tryFloat(movie.get('tomatoRating', 0)), tryInt(movie.get('tomatoReviews', '').replace(',', ''))),
|
#'rotten': (tryFloat(movie.get('tomatoRating', 0)), tryInt(movie.get('tomatoReviews', '').replace(',', ''))),
|
||||||
},
|
},
|
||||||
'imdb': str(movie.get('imdbID', '')),
|
'imdb': str(movie.get('imdbID', '')),
|
||||||
|
'mpaa': str(movie.get('Rated', '')),
|
||||||
'runtime': self.runtimeToMinutes(movie.get('Runtime', '')),
|
'runtime': self.runtimeToMinutes(movie.get('Runtime', '')),
|
||||||
'released': movie.get('Released'),
|
'released': movie.get('Released'),
|
||||||
'year': year if isinstance(year, (int)) else None,
|
'year': year if isinstance(year, (int)) else None,
|
||||||
File renamed without changes.
+2
-1
@@ -1,7 +1,7 @@
|
|||||||
from couchpotato.core.event import addEvent
|
from couchpotato.core.event import addEvent
|
||||||
from couchpotato.core.helpers.encoding import simplifyString, toUnicode
|
from couchpotato.core.helpers.encoding import simplifyString, toUnicode
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.providers.movie.base import MovieProvider
|
from couchpotato.core.providers.info.base import MovieProvider
|
||||||
from themoviedb import tmdb
|
from themoviedb import tmdb
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
@@ -167,6 +167,7 @@ class TheMovieDb(MovieProvider):
|
|||||||
'backdrop_original': [backdrop_original] if backdrop_original else [],
|
'backdrop_original': [backdrop_original] if backdrop_original else [],
|
||||||
},
|
},
|
||||||
'imdb': movie.get('imdb_id'),
|
'imdb': movie.get('imdb_id'),
|
||||||
|
'mpaa': movie.get('certification', ''),
|
||||||
'runtime': movie.get('runtime'),
|
'runtime': movie.get('runtime'),
|
||||||
'released': movie.get('released'),
|
'released': movie.get('released'),
|
||||||
'year': year,
|
'year': year,
|
||||||
File renamed without changes.
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
from couchpotato.core.event import addEvent
|
from couchpotato.core.event import addEvent
|
||||||
from couchpotato.core.helpers.encoding import simplifyString, toUnicode
|
from couchpotato.core.helpers.encoding import simplifyString, toUnicode
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.providers.show.base import ShowProvider
|
from couchpotato.core.providers.info.base import ShowProvider
|
||||||
from tvdb_api import tvdb_api, tvdb_exceptions
|
from tvdb_api import tvdb_api, tvdb_exceptions
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import traceback
|
import traceback
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
from .main import MovieResultModifier
|
|
||||||
|
|
||||||
def start():
|
|
||||||
|
|
||||||
return MovieResultModifier()
|
|
||||||
|
|
||||||
config = []
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
from .main import ShowResultModifier
|
|
||||||
|
|
||||||
def start():
|
|
||||||
|
|
||||||
return ShowResultModifier()
|
|
||||||
|
|
||||||
config = []
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
from couchpotato import get_session
|
|
||||||
from couchpotato.core.event import addEvent, fireEvent
|
|
||||||
from couchpotato.core.helpers.variable import mergeDicts, randomString
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
from couchpotato.core.plugins.base import Plugin
|
|
||||||
from couchpotato.core.settings.model import Library
|
|
||||||
import copy
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class ShowResultModifier(Plugin):
|
|
||||||
|
|
||||||
default_info = {
|
|
||||||
'tmdb_id': 0,
|
|
||||||
'titles': [],
|
|
||||||
'original_title': '',
|
|
||||||
'year': 0,
|
|
||||||
'images': {
|
|
||||||
'poster': [],
|
|
||||||
'backdrop': [],
|
|
||||||
'poster_original': [],
|
|
||||||
'backdrop_original': []
|
|
||||||
},
|
|
||||||
'runtime': 0,
|
|
||||||
'plot': '',
|
|
||||||
'tagline': '',
|
|
||||||
'imdb': '',
|
|
||||||
'genres': [],
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
addEvent('result.modify.show.search', self.combineOnIMDB)
|
|
||||||
addEvent('result.modify.show.info', self.checkLibrary)
|
|
||||||
|
|
||||||
def combineOnIMDB(self, results):
|
|
||||||
|
|
||||||
temp = {}
|
|
||||||
order = []
|
|
||||||
|
|
||||||
# Combine on imdb id
|
|
||||||
for item in results:
|
|
||||||
random_string = randomString()
|
|
||||||
imdb = item.get('imdb', random_string)
|
|
||||||
imdb = imdb if imdb else random_string
|
|
||||||
|
|
||||||
if not temp.get(imdb):
|
|
||||||
temp[imdb] = self.getLibraryTags(imdb)
|
|
||||||
order.append(imdb)
|
|
||||||
|
|
||||||
# Merge dicts
|
|
||||||
temp[imdb] = mergeDicts(temp[imdb], item)
|
|
||||||
|
|
||||||
# Make it a list again
|
|
||||||
temp_list = [temp[x] for x in order]
|
|
||||||
|
|
||||||
return temp_list
|
|
||||||
|
|
||||||
def getLibraryTags(self, imdb):
|
|
||||||
|
|
||||||
temp = {
|
|
||||||
'in_wanted': False,
|
|
||||||
'in_library': False,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add release info from current library
|
|
||||||
db = get_session()
|
|
||||||
try:
|
|
||||||
l = db.query(Library).filter_by(identifier = imdb).first()
|
|
||||||
if l:
|
|
||||||
|
|
||||||
# Statuses
|
|
||||||
active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True)
|
|
||||||
|
|
||||||
for movie in l.media:
|
|
||||||
if movie.status_id == active_status['id']:
|
|
||||||
temp['in_wanted'] = fireEvent('movie.get', movie.id, single = True)
|
|
||||||
|
|
||||||
for release in movie.releases:
|
|
||||||
if release.status_id == done_status['id']:
|
|
||||||
temp['in_library'] = fireEvent('movie.get', movie.id, single = True)
|
|
||||||
except:
|
|
||||||
log.error('Tried getting more info on searched movies: %s', traceback.format_exc())
|
|
||||||
|
|
||||||
return temp
|
|
||||||
|
|
||||||
def checkLibrary(self, result):
|
|
||||||
|
|
||||||
result = mergeDicts(copy.deepcopy(self.default_info), copy.deepcopy(result))
|
|
||||||
|
|
||||||
if result and result.get('imdb'):
|
|
||||||
return mergeDicts(result, self.getLibraryTags(result['imdb']))
|
|
||||||
return result
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
from couchpotato.core.providers.base import Provider
|
|
||||||
|
|
||||||
|
|
||||||
class ShowProvider(Provider):
|
|
||||||
type = 'show'
|
|
||||||
@@ -3,7 +3,7 @@ from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode
|
|||||||
from couchpotato.core.helpers.variable import tryInt
|
from couchpotato.core.helpers.variable import tryInt
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.providers.base import MultiProvider
|
from couchpotato.core.providers.base import MultiProvider
|
||||||
from couchpotato.core.providers.movie.base import MovieProvider
|
from couchpotato.core.providers.info.base import MovieProvider
|
||||||
from couchpotato.core.providers.torrent.base import TorrentProvider
|
from couchpotato.core.providers.torrent.base import TorrentProvider
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
|
|||||||
# app.debug = development
|
# app.debug = development
|
||||||
config = {
|
config = {
|
||||||
'use_reloader': reloader,
|
'use_reloader': reloader,
|
||||||
'port': tryInt(Env.setting('port', default = 5000)),
|
'port': tryInt(Env.setting('port', default = 5050)),
|
||||||
'host': host if host and len(host) > 0 else '0.0.0.0',
|
'host': host if host and len(host) > 0 else '0.0.0.0',
|
||||||
'ssl_cert': Env.setting('ssl_cert', default = None),
|
'ssl_cert': Env.setting('ssl_cert', default = None),
|
||||||
'ssl_key': Env.setting('ssl_key', default = None),
|
'ssl_key': Env.setting('ssl_key', default = None),
|
||||||
|
|||||||
@@ -22,17 +22,18 @@
|
|||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
|
||||||
if($(window).getSize().x <= 480)
|
window.addEvent('load', function(){
|
||||||
window.addEvent('load', function() {
|
|
||||||
|
|
||||||
setTimeout(function(){
|
if(window.getSize().x <= 480)
|
||||||
window.scrollTo(0, 1);
|
setTimeout(function(){
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 1);
|
||||||
}, 100);
|
window.scrollTo(0, 0);
|
||||||
|
}, 100);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEvent('domready', function() {
|
window.addEvent('domready', function() {
|
||||||
|
|
||||||
new Uniform();
|
new Uniform();
|
||||||
|
|
||||||
Api.setup({
|
Api.setup({
|
||||||
|
|||||||
+60
-19
@@ -26,48 +26,89 @@ NAME=couchpotato
|
|||||||
# App name
|
# App name
|
||||||
DESC=CouchPotato
|
DESC=CouchPotato
|
||||||
|
|
||||||
# Path to app root
|
## Don't edit this file
|
||||||
CP_APP_PATH=${APP_PATH-/usr/local/sbin/CouchPotatoServer/}
|
## Edit user configuation in /etc/default/couchpotato to change
|
||||||
|
##
|
||||||
|
## CP_USER= #$RUN_AS, username to run couchpotato under, the default is couchpotato
|
||||||
|
## CP_HOME= #$APP_PATH, the location of couchpotato.py, the default is /opt/couchpotato
|
||||||
|
## CP_DATA= #$DATA_DIR, the location of couchpotato.db, cache, logs, the default is /var/couchpotato
|
||||||
|
## CP_PIDFILE= #$PID_FILE, the location of couchpotato.pid, the default is /var/run/couchpotato.pid
|
||||||
|
## PYTHON_BIN= #$DAEMON, the location of the python binary, the default is /usr/bin/python
|
||||||
|
## CP_OPTS= #$EXTRA_DAEMON_OPTS, extra cli option for couchpotato, i.e. " --config_file=/home/couchpotato/couchpotato.ini"
|
||||||
|
## SSD_OPTS= #$EXTRA_SSD_OPTS, extra start-stop-daemon option like " --group=users"
|
||||||
|
##
|
||||||
|
## EXAMPLE if want to run as different user
|
||||||
|
## add CP_USER=username to /etc/default/couchpotato
|
||||||
|
## otherwise default couchpotato is used
|
||||||
|
|
||||||
# User to run CP as
|
# Run CP as username
|
||||||
CP_RUN_AS=${RUN_AS-root}
|
RUN_AS=${CP_USER-couchpotato}
|
||||||
|
|
||||||
# Path to python bin
|
# Path to app
|
||||||
CP_DAEMON=${DAEMON_PATH-/usr/bin/python}
|
# CP_HOME=path_to_app_CouchPotato.py
|
||||||
|
APP_PATH=${CP_HOME-/opt/couchpotato/}
|
||||||
|
|
||||||
|
# Data directory where couchpotato.db, cache and logs are stored
|
||||||
|
DATA_DIR=${CP_DATA-/var/couchpotato}
|
||||||
|
|
||||||
# Path to store PID file
|
# Path to store PID file
|
||||||
CP_PID_FILE=${PID_FILE-/var/run/couchpotato.pid}
|
PID_FILE=${CP_PID_FILE-/var/run/couchpotato.pid}
|
||||||
|
|
||||||
# Other startup args
|
# path to python bin
|
||||||
CP_DAEMON_OPTS=" CouchPotato.py --daemon --pid_file=${CP_PID_FILE}"
|
DAEMON=${PYTHON_BIN-/usr/bin/python}
|
||||||
|
|
||||||
test -x $CP_DAEMON || exit 0
|
# Extra daemon option like: CP_OPTS=" --config=/home/couchpotato/couchpotato.ini"
|
||||||
|
EXTRA_DAEMON_OPTS=${CP_OPTS-}
|
||||||
|
|
||||||
|
# Extra start-stop-daemon option like START_OPTS=" --group=users"
|
||||||
|
EXTRA_SSD_OPTS=${SSD_OPTS-}
|
||||||
|
|
||||||
|
|
||||||
|
PID_PATH=`dirname $PID_FILE`
|
||||||
|
DAEMON_OPTS=" CouchPotato.py --quiet --daemon --pid_file=${PID_FILE} --data_dir=${DATA_DIR} ${EXTRA_DAEMON_OPTS}"
|
||||||
|
|
||||||
|
|
||||||
|
test -x $DAEMON || exit 0
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
. /lib/lsb/init-functions
|
# Create PID directory if not exist and ensure the CouchPotato user can write to it
|
||||||
|
if [ ! -d $PID_PATH ]; then
|
||||||
|
mkdir -p $PID_PATH
|
||||||
|
chown $RUN_AS $PID_PATH
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d $DATA_DIR ]; then
|
||||||
|
mkdir -p $DATA_DIR
|
||||||
|
chown $RUN_AS $DATA_DIR
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -e $PID_FILE ]; then
|
||||||
|
PID=`cat $PID_FILE`
|
||||||
|
if ! kill -0 $PID > /dev/null 2>&1; then
|
||||||
|
echo "Removing stale $PID_FILE"
|
||||||
|
rm $PID_FILE
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
case "$1" in
|
case "$1" in
|
||||||
start)
|
start)
|
||||||
echo "Starting $DESC"
|
echo "Starting $DESC"
|
||||||
rm -rf $CP_PID_FILE || return 1
|
start-stop-daemon -d $APP_PATH -c $RUN_AS $EXTRA_SSD_OPTS --start --pidfile $PID_FILE --exec $DAEMON -- $DAEMON_OPTS
|
||||||
touch $CP_PID_FILE
|
|
||||||
chown $CP_RUN_AS $CP_PID_FILE
|
|
||||||
start-stop-daemon -d $CP_APP_PATH -c $CP_RUN_AS --start --background --pidfile $CP_PID_FILE --exec $CP_DAEMON -- $CP_DAEMON_OPTS
|
|
||||||
;;
|
;;
|
||||||
stop)
|
stop)
|
||||||
echo "Stopping $DESC"
|
echo "Stopping $DESC"
|
||||||
start-stop-daemon --stop --pidfile $CP_PID_FILE --retry 15
|
start-stop-daemon --stop --pidfile $PID_FILE --retry 15
|
||||||
;;
|
;;
|
||||||
|
|
||||||
restart|force-reload)
|
restart|force-reload)
|
||||||
echo "Restarting $DESC"
|
echo "Restarting $DESC"
|
||||||
start-stop-daemon --stop --pidfile $CP_PID_FILE --retry 15
|
start-stop-daemon --stop --pidfile $PID_FILE --retry 15
|
||||||
start-stop-daemon -d $CP_APP_PATH -c $CP_RUN_AS --start --background --pidfile $CP_PID_FILE --exec $CP_DAEMON -- $CP_DAEMON_OPTS
|
start-stop-daemon -d $APP_PATH -c $RUN_AS $EXTRA_SSD_OPTS --start --pidfile $PID_FILE --exec $DAEMON -- $DAEMON_OPTS
|
||||||
;;
|
;;
|
||||||
|
|
||||||
status)
|
status)
|
||||||
status_of_proc -p $CP_PID_FILE "$CP_DAEMON" "$NAME"
|
status_of_proc -p $PID_FILE "$DAEMON" "$NAME"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
N=/etc/init.d/$NAME
|
N=/etc/init.d/$NAME
|
||||||
|
|||||||
+4
-4
@@ -1,5 +1,5 @@
|
|||||||
# COPY THIS FILE TO /etc/default/couchpotato
|
# COPY THIS FILE TO /etc/default/couchpotato
|
||||||
# OPTIONS: APP_PATH, RUN_AS, DAEMON_PATH, CP_PID_FILE
|
# OPTIONS: CP_HOME, CP_USER, CP_DATA, CP_PIDFILE, PYTHON_BIN, CP_OPTS, SSD_OPTS
|
||||||
|
|
||||||
APP_PATH=
|
CP_HOME=
|
||||||
RUN_AS=root
|
CP_USER=root
|
||||||
Executable
+588
@@ -0,0 +1,588 @@
|
|||||||
|
# Copyright (c) 2013 Chris Lucas, <chris@chrisjlucas.com>
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
# a copy of this software and associated documentation files (the
|
||||||
|
# "Software"), to deal in the Software without restriction, including
|
||||||
|
# without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
# permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
# the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be
|
||||||
|
# included in all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
from rtorrent.common import find_torrent, \
|
||||||
|
is_valid_port, convert_version_tuple_to_str
|
||||||
|
from rtorrent.lib.torrentparser import TorrentParser
|
||||||
|
from rtorrent.lib.xmlrpc.http import HTTPServerProxy
|
||||||
|
from rtorrent.rpc import Method, BasicAuthTransport
|
||||||
|
from rtorrent.torrent import Torrent
|
||||||
|
from rtorrent.group import Group
|
||||||
|
import os.path
|
||||||
|
import rtorrent.rpc # @UnresolvedImport
|
||||||
|
import time
|
||||||
|
import xmlrpclib
|
||||||
|
|
||||||
|
__version__ = "0.2.9"
|
||||||
|
__author__ = "Chris Lucas"
|
||||||
|
__contact__ = "chris@chrisjlucas.com"
|
||||||
|
__license__ = "MIT"
|
||||||
|
|
||||||
|
MIN_RTORRENT_VERSION = (0, 8, 1)
|
||||||
|
MIN_RTORRENT_VERSION_STR = convert_version_tuple_to_str(MIN_RTORRENT_VERSION)
|
||||||
|
|
||||||
|
|
||||||
|
class RTorrent:
|
||||||
|
""" Create a new rTorrent connection """
|
||||||
|
rpc_prefix = None
|
||||||
|
|
||||||
|
def __init__(self, url, username=None, password=None,
|
||||||
|
verify=False, sp=HTTPServerProxy, sp_kwargs={}):
|
||||||
|
self.url = url # : From X{__init__(self, url)}
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
self.sp = sp
|
||||||
|
self.sp_kwargs = sp_kwargs
|
||||||
|
|
||||||
|
self.torrents = [] # : List of L{Torrent} instances
|
||||||
|
self._rpc_methods = [] # : List of rTorrent RPC methods
|
||||||
|
self._torrent_cache = []
|
||||||
|
self._client_version_tuple = ()
|
||||||
|
|
||||||
|
if verify is True:
|
||||||
|
self._verify_conn()
|
||||||
|
|
||||||
|
def _get_conn(self):
|
||||||
|
"""Get ServerProxy instance"""
|
||||||
|
if self.username is not None and self.password is not None:
|
||||||
|
return self.sp(
|
||||||
|
self.url,
|
||||||
|
transport=BasicAuthTransport(self.username, self.password),
|
||||||
|
**self.sp_kwargs
|
||||||
|
)
|
||||||
|
return self.sp(self.url, **self.sp_kwargs)
|
||||||
|
|
||||||
|
def _verify_conn(self):
|
||||||
|
# check for rpc methods that should be available
|
||||||
|
assert {"system.client_version",
|
||||||
|
"system.library_version"}.issubset(set(self._get_rpc_methods())),\
|
||||||
|
"Required RPC methods not available."
|
||||||
|
|
||||||
|
# minimum rTorrent version check
|
||||||
|
|
||||||
|
assert self._meets_version_requirement() is True,\
|
||||||
|
"Error: Minimum rTorrent version required is {0}".format(
|
||||||
|
MIN_RTORRENT_VERSION_STR)
|
||||||
|
|
||||||
|
def _meets_version_requirement(self):
|
||||||
|
return self._get_client_version_tuple() >= MIN_RTORRENT_VERSION
|
||||||
|
|
||||||
|
def _get_client_version_tuple(self):
|
||||||
|
conn = self._get_conn()
|
||||||
|
|
||||||
|
if not self._client_version_tuple:
|
||||||
|
if not hasattr(self, "client_version"):
|
||||||
|
setattr(self, "client_version",
|
||||||
|
conn.system.client_version())
|
||||||
|
|
||||||
|
rtver = getattr(self, "client_version")
|
||||||
|
self._client_version_tuple = tuple([int(i) for i in
|
||||||
|
rtver.split(".")])
|
||||||
|
|
||||||
|
return self._client_version_tuple
|
||||||
|
|
||||||
|
def _get_rpc_methods(self):
|
||||||
|
""" Get list of raw RPC commands
|
||||||
|
|
||||||
|
@return: raw RPC commands
|
||||||
|
@rtype: list
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self._rpc_methods == []:
|
||||||
|
self._rpc_methods = self._get_conn().system.listMethods()
|
||||||
|
|
||||||
|
return(self._rpc_methods)
|
||||||
|
|
||||||
|
def get_torrents(self, view="main"):
|
||||||
|
"""Get list of all torrents in specified view
|
||||||
|
|
||||||
|
@return: list of L{Torrent} instances
|
||||||
|
|
||||||
|
@rtype: list
|
||||||
|
|
||||||
|
@todo: add validity check for specified view
|
||||||
|
"""
|
||||||
|
self.torrents = []
|
||||||
|
methods = rtorrent.torrent.methods
|
||||||
|
retriever_methods = [m for m in methods
|
||||||
|
if m.is_retriever() and m.is_available(self)]
|
||||||
|
|
||||||
|
m = rtorrent.rpc.Multicall(self)
|
||||||
|
m.add("d.multicall", view, "d.get_hash=",
|
||||||
|
*[method.rpc_call + "=" for method in retriever_methods])
|
||||||
|
|
||||||
|
results = m.call()[0] # only sent one call, only need first result
|
||||||
|
|
||||||
|
for result in results:
|
||||||
|
results_dict = {}
|
||||||
|
# build results_dict
|
||||||
|
for m, r in zip(retriever_methods, result[1:]): # result[0] is the info_hash
|
||||||
|
results_dict[m.varname] = rtorrent.rpc.process_result(m, r)
|
||||||
|
|
||||||
|
self.torrents.append(
|
||||||
|
Torrent(self, info_hash=result[0], **results_dict)
|
||||||
|
)
|
||||||
|
|
||||||
|
self._manage_torrent_cache()
|
||||||
|
return(self.torrents)
|
||||||
|
|
||||||
|
def _manage_torrent_cache(self):
|
||||||
|
"""Carry tracker/peer/file lists over to new torrent list"""
|
||||||
|
for torrent in self._torrent_cache:
|
||||||
|
new_torrent = rtorrent.common.find_torrent(torrent.info_hash,
|
||||||
|
self.torrents)
|
||||||
|
if new_torrent is not None:
|
||||||
|
new_torrent.files = torrent.files
|
||||||
|
new_torrent.peers = torrent.peers
|
||||||
|
new_torrent.trackers = torrent.trackers
|
||||||
|
|
||||||
|
self._torrent_cache = self.torrents
|
||||||
|
|
||||||
|
def _get_load_function(self, file_type, start, verbose):
|
||||||
|
"""Determine correct "load torrent" RPC method"""
|
||||||
|
func_name = None
|
||||||
|
if file_type == "url":
|
||||||
|
# url strings can be input directly
|
||||||
|
if start and verbose:
|
||||||
|
func_name = "load_start_verbose"
|
||||||
|
elif start:
|
||||||
|
func_name = "load_start"
|
||||||
|
elif verbose:
|
||||||
|
func_name = "load_verbose"
|
||||||
|
else:
|
||||||
|
func_name = "load"
|
||||||
|
elif file_type in ["file", "raw"]:
|
||||||
|
if start and verbose:
|
||||||
|
func_name = "load_raw_start_verbose"
|
||||||
|
elif start:
|
||||||
|
func_name = "load_raw_start"
|
||||||
|
elif verbose:
|
||||||
|
func_name = "load_raw_verbose"
|
||||||
|
else:
|
||||||
|
func_name = "load_raw"
|
||||||
|
|
||||||
|
return(func_name)
|
||||||
|
|
||||||
|
def load_torrent(self, torrent, start=False, verbose=False, verify_load=True):
|
||||||
|
"""
|
||||||
|
Loads torrent into rTorrent (with various enhancements)
|
||||||
|
|
||||||
|
@param torrent: can be a url, a path to a local file, or the raw data
|
||||||
|
of a torrent file
|
||||||
|
@type torrent: str
|
||||||
|
|
||||||
|
@param start: start torrent when loaded
|
||||||
|
@type start: bool
|
||||||
|
|
||||||
|
@param verbose: print error messages to rTorrent log
|
||||||
|
@type verbose: bool
|
||||||
|
|
||||||
|
@param verify_load: verify that torrent was added to rTorrent successfully
|
||||||
|
@type verify_load: bool
|
||||||
|
|
||||||
|
@return: Depends on verify_load:
|
||||||
|
- if verify_load is True, (and the torrent was
|
||||||
|
loaded successfully), it'll return a L{Torrent} instance
|
||||||
|
- if verify_load is False, it'll return None
|
||||||
|
|
||||||
|
@rtype: L{Torrent} instance or None
|
||||||
|
|
||||||
|
@raise AssertionError: If the torrent wasn't successfully added to rTorrent
|
||||||
|
- Check L{TorrentParser} for the AssertionError's
|
||||||
|
it raises
|
||||||
|
|
||||||
|
|
||||||
|
@note: Because this function includes url verification (if a url was input)
|
||||||
|
as well as verification as to whether the torrent was successfully added,
|
||||||
|
this function doesn't execute instantaneously. If that's what you're
|
||||||
|
looking for, use load_torrent_simple() instead.
|
||||||
|
"""
|
||||||
|
p = self._get_conn()
|
||||||
|
tp = TorrentParser(torrent)
|
||||||
|
torrent = xmlrpclib.Binary(tp._raw_torrent)
|
||||||
|
info_hash = tp.info_hash
|
||||||
|
|
||||||
|
func_name = self._get_load_function("raw", start, verbose)
|
||||||
|
|
||||||
|
# load torrent
|
||||||
|
getattr(p, func_name)(torrent)
|
||||||
|
|
||||||
|
if verify_load:
|
||||||
|
MAX_RETRIES = 3
|
||||||
|
i = 0
|
||||||
|
while i < MAX_RETRIES:
|
||||||
|
self.get_torrents()
|
||||||
|
if info_hash in [t.info_hash for t in self.torrents]:
|
||||||
|
break
|
||||||
|
|
||||||
|
# was still getting AssertionErrors, delay should help
|
||||||
|
time.sleep(1)
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
assert info_hash in [t.info_hash for t in self.torrents],\
|
||||||
|
"Adding torrent was unsuccessful."
|
||||||
|
|
||||||
|
return(find_torrent(info_hash, self.torrents))
|
||||||
|
|
||||||
|
def load_torrent_simple(self, torrent, file_type,
|
||||||
|
start=False, verbose=False):
|
||||||
|
"""Loads torrent into rTorrent
|
||||||
|
|
||||||
|
@param torrent: can be a url, a path to a local file, or the raw data
|
||||||
|
of a torrent file
|
||||||
|
@type torrent: str
|
||||||
|
|
||||||
|
@param file_type: valid options: "url", "file", or "raw"
|
||||||
|
@type file_type: str
|
||||||
|
|
||||||
|
@param start: start torrent when loaded
|
||||||
|
@type start: bool
|
||||||
|
|
||||||
|
@param verbose: print error messages to rTorrent log
|
||||||
|
@type verbose: bool
|
||||||
|
|
||||||
|
@return: None
|
||||||
|
|
||||||
|
@raise AssertionError: if incorrect file_type is specified
|
||||||
|
|
||||||
|
@note: This function was written for speed, it includes no enhancements.
|
||||||
|
If you input a url, it won't check if it's valid. You also can't get
|
||||||
|
verification that the torrent was successfully added to rTorrent.
|
||||||
|
Use load_torrent() if you would like these features.
|
||||||
|
"""
|
||||||
|
p = self._get_conn()
|
||||||
|
|
||||||
|
assert file_type in ["raw", "file", "url"], \
|
||||||
|
"Invalid file_type, options are: 'url', 'file', 'raw'."
|
||||||
|
func_name = self._get_load_function(file_type, start, verbose)
|
||||||
|
|
||||||
|
if file_type == "file":
|
||||||
|
# since we have to assume we're connected to a remote rTorrent
|
||||||
|
# client, we have to read the file and send it to rT as raw
|
||||||
|
assert os.path.isfile(torrent), \
|
||||||
|
"Invalid path: \"{0}\"".format(torrent)
|
||||||
|
torrent = open(torrent, "rb").read()
|
||||||
|
|
||||||
|
if file_type in ["raw", "file"]:
|
||||||
|
finput = xmlrpclib.Binary(torrent)
|
||||||
|
elif file_type == "url":
|
||||||
|
finput = torrent
|
||||||
|
|
||||||
|
getattr(p, func_name)(finput)
|
||||||
|
|
||||||
|
def get_views(self):
|
||||||
|
p = self._get_conn()
|
||||||
|
return p.view_list()
|
||||||
|
|
||||||
|
def create_group(self, name, persistent=True, view=None):
|
||||||
|
p = self._get_conn()
|
||||||
|
|
||||||
|
if persistent is True:
|
||||||
|
p.group.insert_persistent_view('', name)
|
||||||
|
else:
|
||||||
|
assert view is not None, "view parameter required on non-persistent groups"
|
||||||
|
p.group.insert('', name, view)
|
||||||
|
|
||||||
|
def get_group(self, name):
|
||||||
|
assert name is not None, "group name required"
|
||||||
|
|
||||||
|
group = Group(self, name)
|
||||||
|
group.update()
|
||||||
|
return group
|
||||||
|
|
||||||
|
def set_dht_port(self, port):
|
||||||
|
"""Set DHT port
|
||||||
|
|
||||||
|
@param port: port
|
||||||
|
@type port: int
|
||||||
|
|
||||||
|
@raise AssertionError: if invalid port is given
|
||||||
|
"""
|
||||||
|
assert is_valid_port(port), "Valid port range is 0-65535"
|
||||||
|
self.dht_port = self._p.set_dht_port(port)
|
||||||
|
|
||||||
|
def enable_check_hash(self):
|
||||||
|
"""Alias for set_check_hash(True)"""
|
||||||
|
self.set_check_hash(True)
|
||||||
|
|
||||||
|
def disable_check_hash(self):
|
||||||
|
"""Alias for set_check_hash(False)"""
|
||||||
|
self.set_check_hash(False)
|
||||||
|
|
||||||
|
def find_torrent(self, info_hash):
|
||||||
|
"""Frontend for rtorrent.common.find_torrent"""
|
||||||
|
return(rtorrent.common.find_torrent(info_hash, self.get_torrents()))
|
||||||
|
|
||||||
|
def poll(self):
|
||||||
|
""" poll rTorrent to get latest torrent/peer/tracker/file information
|
||||||
|
|
||||||
|
@note: This essentially refreshes every aspect of the rTorrent
|
||||||
|
connection, so it can be very slow if working with a remote
|
||||||
|
connection that has a lot of torrents loaded.
|
||||||
|
|
||||||
|
@return: None
|
||||||
|
"""
|
||||||
|
self.update()
|
||||||
|
torrents = self.get_torrents()
|
||||||
|
for t in torrents:
|
||||||
|
t.poll()
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Refresh rTorrent client info
|
||||||
|
|
||||||
|
@note: All fields are stored as attributes to self.
|
||||||
|
|
||||||
|
@return: None
|
||||||
|
"""
|
||||||
|
multicall = rtorrent.rpc.Multicall(self)
|
||||||
|
retriever_methods = [m for m in methods
|
||||||
|
if m.is_retriever() and m.is_available(self)]
|
||||||
|
for method in retriever_methods:
|
||||||
|
multicall.add(method)
|
||||||
|
|
||||||
|
multicall.call()
|
||||||
|
|
||||||
|
|
||||||
|
def _build_class_methods(class_obj):
|
||||||
|
# multicall add class
|
||||||
|
caller = lambda self, multicall, method, *args:\
|
||||||
|
multicall.add(method, self.rpc_id, *args)
|
||||||
|
|
||||||
|
caller.__doc__ = """Same as Multicall.add(), but with automatic inclusion
|
||||||
|
of the rpc_id
|
||||||
|
|
||||||
|
@param multicall: A L{Multicall} instance
|
||||||
|
@type: multicall: Multicall
|
||||||
|
|
||||||
|
@param method: L{Method} instance or raw rpc method
|
||||||
|
@type: Method or str
|
||||||
|
|
||||||
|
@param args: optional arguments to pass
|
||||||
|
"""
|
||||||
|
setattr(class_obj, "multicall_add", caller)
|
||||||
|
|
||||||
|
|
||||||
|
def __compare_rpc_methods(rt_new, rt_old):
|
||||||
|
from pprint import pprint
|
||||||
|
rt_new_methods = set(rt_new._get_rpc_methods())
|
||||||
|
rt_old_methods = set(rt_old._get_rpc_methods())
|
||||||
|
print("New Methods:")
|
||||||
|
pprint(rt_new_methods - rt_old_methods)
|
||||||
|
print("Methods not in new rTorrent:")
|
||||||
|
pprint(rt_old_methods - rt_new_methods)
|
||||||
|
|
||||||
|
|
||||||
|
def __check_supported_methods(rt):
|
||||||
|
from pprint import pprint
|
||||||
|
supported_methods = set([m.rpc_call for m in
|
||||||
|
methods +
|
||||||
|
rtorrent.file.methods +
|
||||||
|
rtorrent.torrent.methods +
|
||||||
|
rtorrent.tracker.methods +
|
||||||
|
rtorrent.peer.methods])
|
||||||
|
all_methods = set(rt._get_rpc_methods())
|
||||||
|
|
||||||
|
print("Methods NOT in supported methods")
|
||||||
|
pprint(all_methods - supported_methods)
|
||||||
|
print("Supported methods NOT in all methods")
|
||||||
|
pprint(supported_methods - all_methods)
|
||||||
|
|
||||||
|
methods = [
|
||||||
|
# RETRIEVERS
|
||||||
|
Method(RTorrent, 'get_xmlrpc_size_limit', 'get_xmlrpc_size_limit'),
|
||||||
|
Method(RTorrent, 'get_proxy_address', 'get_proxy_address'),
|
||||||
|
Method(RTorrent, 'get_split_suffix', 'get_split_suffix'),
|
||||||
|
Method(RTorrent, 'get_up_limit', 'get_upload_rate'),
|
||||||
|
Method(RTorrent, 'get_max_memory_usage', 'get_max_memory_usage'),
|
||||||
|
Method(RTorrent, 'get_max_open_files', 'get_max_open_files'),
|
||||||
|
Method(RTorrent, 'get_min_peers_seed', 'get_min_peers_seed'),
|
||||||
|
Method(RTorrent, 'get_use_udp_trackers', 'get_use_udp_trackers'),
|
||||||
|
Method(RTorrent, 'get_preload_min_size', 'get_preload_min_size'),
|
||||||
|
Method(RTorrent, 'get_max_uploads', 'get_max_uploads'),
|
||||||
|
Method(RTorrent, 'get_max_peers', 'get_max_peers'),
|
||||||
|
Method(RTorrent, 'get_timeout_sync', 'get_timeout_sync'),
|
||||||
|
Method(RTorrent, 'get_receive_buffer_size', 'get_receive_buffer_size'),
|
||||||
|
Method(RTorrent, 'get_split_file_size', 'get_split_file_size'),
|
||||||
|
Method(RTorrent, 'get_dht_throttle', 'get_dht_throttle'),
|
||||||
|
Method(RTorrent, 'get_max_peers_seed', 'get_max_peers_seed'),
|
||||||
|
Method(RTorrent, 'get_min_peers', 'get_min_peers'),
|
||||||
|
Method(RTorrent, 'get_tracker_numwant', 'get_tracker_numwant'),
|
||||||
|
Method(RTorrent, 'get_max_open_sockets', 'get_max_open_sockets'),
|
||||||
|
Method(RTorrent, 'get_session', 'get_session'),
|
||||||
|
Method(RTorrent, 'get_ip', 'get_ip'),
|
||||||
|
Method(RTorrent, 'get_scgi_dont_route', 'get_scgi_dont_route'),
|
||||||
|
Method(RTorrent, 'get_hash_read_ahead', 'get_hash_read_ahead'),
|
||||||
|
Method(RTorrent, 'get_http_cacert', 'get_http_cacert'),
|
||||||
|
Method(RTorrent, 'get_dht_port', 'get_dht_port'),
|
||||||
|
Method(RTorrent, 'get_handshake_log', 'get_handshake_log'),
|
||||||
|
Method(RTorrent, 'get_preload_type', 'get_preload_type'),
|
||||||
|
Method(RTorrent, 'get_max_open_http', 'get_max_open_http'),
|
||||||
|
Method(RTorrent, 'get_http_capath', 'get_http_capath'),
|
||||||
|
Method(RTorrent, 'get_max_downloads_global', 'get_max_downloads_global'),
|
||||||
|
Method(RTorrent, 'get_name', 'get_name'),
|
||||||
|
Method(RTorrent, 'get_session_on_completion', 'get_session_on_completion'),
|
||||||
|
Method(RTorrent, 'get_down_limit', 'get_download_rate'),
|
||||||
|
Method(RTorrent, 'get_down_total', 'get_down_total'),
|
||||||
|
Method(RTorrent, 'get_up_rate', 'get_up_rate'),
|
||||||
|
Method(RTorrent, 'get_hash_max_tries', 'get_hash_max_tries'),
|
||||||
|
Method(RTorrent, 'get_peer_exchange', 'get_peer_exchange'),
|
||||||
|
Method(RTorrent, 'get_down_rate', 'get_down_rate'),
|
||||||
|
Method(RTorrent, 'get_connection_seed', 'get_connection_seed'),
|
||||||
|
Method(RTorrent, 'get_http_proxy', 'get_http_proxy'),
|
||||||
|
Method(RTorrent, 'get_stats_preloaded', 'get_stats_preloaded'),
|
||||||
|
Method(RTorrent, 'get_timeout_safe_sync', 'get_timeout_safe_sync'),
|
||||||
|
Method(RTorrent, 'get_hash_interval', 'get_hash_interval'),
|
||||||
|
Method(RTorrent, 'get_port_random', 'get_port_random'),
|
||||||
|
Method(RTorrent, 'get_directory', 'get_directory'),
|
||||||
|
Method(RTorrent, 'get_port_open', 'get_port_open'),
|
||||||
|
Method(RTorrent, 'get_max_file_size', 'get_max_file_size'),
|
||||||
|
Method(RTorrent, 'get_stats_not_preloaded', 'get_stats_not_preloaded'),
|
||||||
|
Method(RTorrent, 'get_memory_usage', 'get_memory_usage'),
|
||||||
|
Method(RTorrent, 'get_connection_leech', 'get_connection_leech'),
|
||||||
|
Method(RTorrent, 'get_check_hash', 'get_check_hash',
|
||||||
|
boolean=True,
|
||||||
|
),
|
||||||
|
Method(RTorrent, 'get_session_lock', 'get_session_lock'),
|
||||||
|
Method(RTorrent, 'get_preload_required_rate', 'get_preload_required_rate'),
|
||||||
|
Method(RTorrent, 'get_max_uploads_global', 'get_max_uploads_global'),
|
||||||
|
Method(RTorrent, 'get_send_buffer_size', 'get_send_buffer_size'),
|
||||||
|
Method(RTorrent, 'get_port_range', 'get_port_range'),
|
||||||
|
Method(RTorrent, 'get_max_downloads_div', 'get_max_downloads_div'),
|
||||||
|
Method(RTorrent, 'get_max_uploads_div', 'get_max_uploads_div'),
|
||||||
|
Method(RTorrent, 'get_safe_sync', 'get_safe_sync'),
|
||||||
|
Method(RTorrent, 'get_bind', 'get_bind'),
|
||||||
|
Method(RTorrent, 'get_up_total', 'get_up_total'),
|
||||||
|
Method(RTorrent, 'get_client_version', 'system.client_version'),
|
||||||
|
Method(RTorrent, 'get_library_version', 'system.library_version'),
|
||||||
|
Method(RTorrent, 'get_api_version', 'system.api_version',
|
||||||
|
min_version=(0, 9, 1)
|
||||||
|
),
|
||||||
|
Method(RTorrent, "get_system_time", "system.time",
|
||||||
|
docstring="""Get the current time of the system rTorrent is running on
|
||||||
|
|
||||||
|
@return: time (posix)
|
||||||
|
@rtype: int""",
|
||||||
|
),
|
||||||
|
|
||||||
|
# MODIFIERS
|
||||||
|
Method(RTorrent, 'set_http_proxy', 'set_http_proxy'),
|
||||||
|
Method(RTorrent, 'set_max_memory_usage', 'set_max_memory_usage'),
|
||||||
|
Method(RTorrent, 'set_max_file_size', 'set_max_file_size'),
|
||||||
|
Method(RTorrent, 'set_bind', 'set_bind',
|
||||||
|
docstring="""Set address bind
|
||||||
|
|
||||||
|
@param arg: ip address
|
||||||
|
@type arg: str
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
Method(RTorrent, 'set_up_limit', 'set_upload_rate',
|
||||||
|
docstring="""Set global upload limit (in bytes)
|
||||||
|
|
||||||
|
@param arg: speed limit
|
||||||
|
@type arg: int
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
Method(RTorrent, 'set_port_random', 'set_port_random'),
|
||||||
|
Method(RTorrent, 'set_connection_leech', 'set_connection_leech'),
|
||||||
|
Method(RTorrent, 'set_tracker_numwant', 'set_tracker_numwant'),
|
||||||
|
Method(RTorrent, 'set_max_peers', 'set_max_peers'),
|
||||||
|
Method(RTorrent, 'set_min_peers', 'set_min_peers'),
|
||||||
|
Method(RTorrent, 'set_max_uploads_div', 'set_max_uploads_div'),
|
||||||
|
Method(RTorrent, 'set_max_open_files', 'set_max_open_files'),
|
||||||
|
Method(RTorrent, 'set_max_downloads_global', 'set_max_downloads_global'),
|
||||||
|
Method(RTorrent, 'set_session_lock', 'set_session_lock'),
|
||||||
|
Method(RTorrent, 'set_session', 'set_session'),
|
||||||
|
Method(RTorrent, 'set_split_suffix', 'set_split_suffix'),
|
||||||
|
Method(RTorrent, 'set_hash_interval', 'set_hash_interval'),
|
||||||
|
Method(RTorrent, 'set_handshake_log', 'set_handshake_log'),
|
||||||
|
Method(RTorrent, 'set_port_range', 'set_port_range'),
|
||||||
|
Method(RTorrent, 'set_min_peers_seed', 'set_min_peers_seed'),
|
||||||
|
Method(RTorrent, 'set_scgi_dont_route', 'set_scgi_dont_route'),
|
||||||
|
Method(RTorrent, 'set_preload_min_size', 'set_preload_min_size'),
|
||||||
|
Method(RTorrent, 'set_log.tracker', 'set_log.tracker'),
|
||||||
|
Method(RTorrent, 'set_max_uploads_global', 'set_max_uploads_global'),
|
||||||
|
Method(RTorrent, 'set_down_limit', 'set_download_rate',
|
||||||
|
docstring="""Set global download limit (in bytes)
|
||||||
|
|
||||||
|
@param arg: speed limit
|
||||||
|
@type arg: int
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
Method(RTorrent, 'set_preload_required_rate', 'set_preload_required_rate'),
|
||||||
|
Method(RTorrent, 'set_hash_read_ahead', 'set_hash_read_ahead'),
|
||||||
|
Method(RTorrent, 'set_max_peers_seed', 'set_max_peers_seed'),
|
||||||
|
Method(RTorrent, 'set_max_uploads', 'set_max_uploads'),
|
||||||
|
Method(RTorrent, 'set_session_on_completion', 'set_session_on_completion'),
|
||||||
|
Method(RTorrent, 'set_max_open_http', 'set_max_open_http'),
|
||||||
|
Method(RTorrent, 'set_directory', 'set_directory'),
|
||||||
|
Method(RTorrent, 'set_http_cacert', 'set_http_cacert'),
|
||||||
|
Method(RTorrent, 'set_dht_throttle', 'set_dht_throttle'),
|
||||||
|
Method(RTorrent, 'set_hash_max_tries', 'set_hash_max_tries'),
|
||||||
|
Method(RTorrent, 'set_proxy_address', 'set_proxy_address'),
|
||||||
|
Method(RTorrent, 'set_split_file_size', 'set_split_file_size'),
|
||||||
|
Method(RTorrent, 'set_receive_buffer_size', 'set_receive_buffer_size'),
|
||||||
|
Method(RTorrent, 'set_use_udp_trackers', 'set_use_udp_trackers'),
|
||||||
|
Method(RTorrent, 'set_connection_seed', 'set_connection_seed'),
|
||||||
|
Method(RTorrent, 'set_xmlrpc_size_limit', 'set_xmlrpc_size_limit'),
|
||||||
|
Method(RTorrent, 'set_xmlrpc_dialect', 'set_xmlrpc_dialect'),
|
||||||
|
Method(RTorrent, 'set_safe_sync', 'set_safe_sync'),
|
||||||
|
Method(RTorrent, 'set_http_capath', 'set_http_capath'),
|
||||||
|
Method(RTorrent, 'set_send_buffer_size', 'set_send_buffer_size'),
|
||||||
|
Method(RTorrent, 'set_max_downloads_div', 'set_max_downloads_div'),
|
||||||
|
Method(RTorrent, 'set_name', 'set_name'),
|
||||||
|
Method(RTorrent, 'set_port_open', 'set_port_open'),
|
||||||
|
Method(RTorrent, 'set_timeout_sync', 'set_timeout_sync'),
|
||||||
|
Method(RTorrent, 'set_peer_exchange', 'set_peer_exchange'),
|
||||||
|
Method(RTorrent, 'set_ip', 'set_ip',
|
||||||
|
docstring="""Set IP
|
||||||
|
|
||||||
|
@param arg: ip address
|
||||||
|
@type arg: str
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
Method(RTorrent, 'set_timeout_safe_sync', 'set_timeout_safe_sync'),
|
||||||
|
Method(RTorrent, 'set_preload_type', 'set_preload_type'),
|
||||||
|
Method(RTorrent, 'set_check_hash', 'set_check_hash',
|
||||||
|
docstring="""Enable/Disable hash checking on finished torrents
|
||||||
|
|
||||||
|
@param arg: True to enable, False to disable
|
||||||
|
@type arg: bool
|
||||||
|
""",
|
||||||
|
boolean=True,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
_all_methods_list = [methods,
|
||||||
|
rtorrent.file.methods,
|
||||||
|
rtorrent.torrent.methods,
|
||||||
|
rtorrent.tracker.methods,
|
||||||
|
rtorrent.peer.methods,
|
||||||
|
]
|
||||||
|
|
||||||
|
class_methods_pair = {
|
||||||
|
RTorrent: methods,
|
||||||
|
rtorrent.file.File: rtorrent.file.methods,
|
||||||
|
rtorrent.torrent.Torrent: rtorrent.torrent.methods,
|
||||||
|
rtorrent.tracker.Tracker: rtorrent.tracker.methods,
|
||||||
|
rtorrent.peer.Peer: rtorrent.peer.methods,
|
||||||
|
}
|
||||||
|
for c in class_methods_pair.keys():
|
||||||
|
rtorrent.rpc._build_rpc_methods(c, class_methods_pair[c])
|
||||||
|
_build_class_methods(c)
|
||||||
Executable
+86
@@ -0,0 +1,86 @@
|
|||||||
|
# Copyright (c) 2013 Chris Lucas, <chris@chrisjlucas.com>
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
# a copy of this software and associated documentation files (the
|
||||||
|
# "Software"), to deal in the Software without restriction, including
|
||||||
|
# without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
# permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
# the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be
|
||||||
|
# included in all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
|
from rtorrent.compat import is_py3
|
||||||
|
|
||||||
|
|
||||||
|
def bool_to_int(value):
|
||||||
|
"""Translates python booleans to RPC-safe integers"""
|
||||||
|
if value is True:
|
||||||
|
return("1")
|
||||||
|
elif value is False:
|
||||||
|
return("0")
|
||||||
|
else:
|
||||||
|
return(value)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_exists(cmds_list, cmd):
|
||||||
|
"""Check if given command is in list of available commands
|
||||||
|
|
||||||
|
@param cmds_list: see L{RTorrent._rpc_methods}
|
||||||
|
@type cmds_list: list
|
||||||
|
|
||||||
|
@param cmd: name of command to be checked
|
||||||
|
@type cmd: str
|
||||||
|
|
||||||
|
@return: bool
|
||||||
|
"""
|
||||||
|
|
||||||
|
return(cmd in cmds_list)
|
||||||
|
|
||||||
|
|
||||||
|
def find_torrent(info_hash, torrent_list):
|
||||||
|
"""Find torrent file in given list of Torrent classes
|
||||||
|
|
||||||
|
@param info_hash: info hash of torrent
|
||||||
|
@type info_hash: str
|
||||||
|
|
||||||
|
@param torrent_list: list of L{Torrent} instances (see L{RTorrent.get_torrents})
|
||||||
|
@type torrent_list: list
|
||||||
|
|
||||||
|
@return: L{Torrent} instance, or -1 if not found
|
||||||
|
"""
|
||||||
|
for t in torrent_list:
|
||||||
|
if t.info_hash == info_hash:
|
||||||
|
return t
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_port(port):
|
||||||
|
"""Check if given port is valid"""
|
||||||
|
return(0 <= int(port) <= 65535)
|
||||||
|
|
||||||
|
|
||||||
|
def convert_version_tuple_to_str(t):
|
||||||
|
return(".".join([str(n) for n in t]))
|
||||||
|
|
||||||
|
|
||||||
|
def safe_repr(fmt, *args, **kwargs):
|
||||||
|
""" Formatter that handles unicode arguments """
|
||||||
|
|
||||||
|
if not is_py3():
|
||||||
|
# unicode fmt can take str args, str fmt cannot take unicode args
|
||||||
|
fmt = fmt.decode("utf-8")
|
||||||
|
out = fmt.format(*args, **kwargs)
|
||||||
|
return out.encode("utf-8")
|
||||||
|
else:
|
||||||
|
return fmt.format(*args, **kwargs)
|
||||||
Executable
+30
@@ -0,0 +1,30 @@
|
|||||||
|
# Copyright (c) 2013 Chris Lucas, <chris@chrisjlucas.com>
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
# a copy of this software and associated documentation files (the
|
||||||
|
# "Software"), to deal in the Software without restriction, including
|
||||||
|
# without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
# permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
# the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be
|
||||||
|
# included in all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def is_py3():
|
||||||
|
return sys.version_info[0] == 3
|
||||||
|
|
||||||
|
if is_py3():
|
||||||
|
import xmlrpc.client as xmlrpclib
|
||||||
|
else:
|
||||||
|
import xmlrpclib
|
||||||
Executable
+40
@@ -0,0 +1,40 @@
|
|||||||
|
# Copyright (c) 2013 Chris Lucas, <chris@chrisjlucas.com>
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
# a copy of this software and associated documentation files (the
|
||||||
|
# "Software"), to deal in the Software without restriction, including
|
||||||
|
# without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
# permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
# the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be
|
||||||
|
# included in all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
from rtorrent.common import convert_version_tuple_to_str
|
||||||
|
|
||||||
|
|
||||||
|
class RTorrentVersionError(Exception):
|
||||||
|
def __init__(self, min_version, cur_version):
|
||||||
|
self.min_version = min_version
|
||||||
|
self.cur_version = cur_version
|
||||||
|
self.msg = "Minimum version required: {0}".format(
|
||||||
|
convert_version_tuple_to_str(min_version))
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return(self.msg)
|
||||||
|
|
||||||
|
|
||||||
|
class MethodError(Exception):
|
||||||
|
def __init__(self, msg):
|
||||||
|
self.msg = msg
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return(self.msg)
|
||||||
Executable
+91
@@ -0,0 +1,91 @@
|
|||||||
|
# Copyright (c) 2013 Chris Lucas, <chris@chrisjlucas.com>
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
# a copy of this software and associated documentation files (the
|
||||||
|
# "Software"), to deal in the Software without restriction, including
|
||||||
|
# without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
# permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
# the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be
|
||||||
|
# included in all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
# from rtorrent.rpc import Method
|
||||||
|
import rtorrent.rpc
|
||||||
|
|
||||||
|
from rtorrent.common import safe_repr
|
||||||
|
|
||||||
|
Method = rtorrent.rpc.Method
|
||||||
|
|
||||||
|
|
||||||
|
class File:
|
||||||
|
"""Represents an individual file within a L{Torrent} instance."""
|
||||||
|
|
||||||
|
def __init__(self, _rt_obj, info_hash, index, **kwargs):
|
||||||
|
self._rt_obj = _rt_obj
|
||||||
|
self.info_hash = info_hash # : info hash for the torrent the file is associated with
|
||||||
|
self.index = index # : The position of the file within the file list
|
||||||
|
for k in kwargs.keys():
|
||||||
|
setattr(self, k, kwargs.get(k, None))
|
||||||
|
|
||||||
|
self.rpc_id = "{0}:f{1}".format(
|
||||||
|
self.info_hash, self.index) # : unique id to pass to rTorrent
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Refresh file data
|
||||||
|
|
||||||
|
@note: All fields are stored as attributes to self.
|
||||||
|
|
||||||
|
@return: None
|
||||||
|
"""
|
||||||
|
multicall = rtorrent.rpc.Multicall(self)
|
||||||
|
retriever_methods = [m for m in methods
|
||||||
|
if m.is_retriever() and m.is_available(self._rt_obj)]
|
||||||
|
for method in retriever_methods:
|
||||||
|
multicall.add(method, self.rpc_id)
|
||||||
|
|
||||||
|
multicall.call()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return safe_repr("File(index={0} path=\"{1}\")", self.index, self.path)
|
||||||
|
|
||||||
|
methods = [
|
||||||
|
# RETRIEVERS
|
||||||
|
Method(File, 'get_last_touched', 'f.get_last_touched'),
|
||||||
|
Method(File, 'get_range_second', 'f.get_range_second'),
|
||||||
|
Method(File, 'get_size_bytes', 'f.get_size_bytes'),
|
||||||
|
Method(File, 'get_priority', 'f.get_priority'),
|
||||||
|
Method(File, 'get_match_depth_next', 'f.get_match_depth_next'),
|
||||||
|
Method(File, 'is_resize_queued', 'f.is_resize_queued',
|
||||||
|
boolean=True,
|
||||||
|
),
|
||||||
|
Method(File, 'get_range_first', 'f.get_range_first'),
|
||||||
|
Method(File, 'get_match_depth_prev', 'f.get_match_depth_prev'),
|
||||||
|
Method(File, 'get_path', 'f.get_path'),
|
||||||
|
Method(File, 'get_completed_chunks', 'f.get_completed_chunks'),
|
||||||
|
Method(File, 'get_path_components', 'f.get_path_components'),
|
||||||
|
Method(File, 'is_created', 'f.is_created',
|
||||||
|
boolean=True,
|
||||||
|
),
|
||||||
|
Method(File, 'is_open', 'f.is_open',
|
||||||
|
boolean=True,
|
||||||
|
),
|
||||||
|
Method(File, 'get_size_chunks', 'f.get_size_chunks'),
|
||||||
|
Method(File, 'get_offset', 'f.get_offset'),
|
||||||
|
Method(File, 'get_frozen_path', 'f.get_frozen_path'),
|
||||||
|
Method(File, 'get_path_depth', 'f.get_path_depth'),
|
||||||
|
Method(File, 'is_create_queued', 'f.is_create_queued',
|
||||||
|
boolean=True,
|
||||||
|
),
|
||||||
|
|
||||||
|
|
||||||
|
# MODIFIERS
|
||||||
|
]
|
||||||
Executable
+84
@@ -0,0 +1,84 @@
|
|||||||
|
# Copyright (c) 2013 Dean Gardiner, <gardiner91@gmail.com>
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
# a copy of this software and associated documentation files (the
|
||||||
|
# "Software"), to deal in the Software without restriction, including
|
||||||
|
# without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
# permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
# the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be
|
||||||
|
# included in all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
import rtorrent.rpc
|
||||||
|
|
||||||
|
Method = rtorrent.rpc.Method
|
||||||
|
|
||||||
|
|
||||||
|
class Group:
|
||||||
|
__name__ = 'Group'
|
||||||
|
|
||||||
|
def __init__(self, _rt_obj, name):
|
||||||
|
self._rt_obj = _rt_obj
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
self.methods = [
|
||||||
|
# RETRIEVERS
|
||||||
|
Method(Group, 'get_max', 'group.' + self.name + '.ratio.max', varname='max'),
|
||||||
|
Method(Group, 'get_min', 'group.' + self.name + '.ratio.min', varname='min'),
|
||||||
|
Method(Group, 'get_upload', 'group.' + self.name + '.ratio.upload', varname='upload'),
|
||||||
|
|
||||||
|
# MODIFIERS
|
||||||
|
Method(Group, 'set_max', 'group.' + self.name + '.ratio.max.set', varname='max'),
|
||||||
|
Method(Group, 'set_min', 'group.' + self.name + '.ratio.min.set', varname='min'),
|
||||||
|
Method(Group, 'set_upload', 'group.' + self.name + '.ratio.upload.set', varname='upload')
|
||||||
|
]
|
||||||
|
|
||||||
|
rtorrent.rpc._build_rpc_methods(self, self.methods)
|
||||||
|
|
||||||
|
# Setup multicall_add method
|
||||||
|
caller = lambda multicall, method, *args: \
|
||||||
|
multicall.add(method, *args)
|
||||||
|
setattr(self, "multicall_add", caller)
|
||||||
|
|
||||||
|
def _get_prefix(self):
|
||||||
|
return 'group.' + self.name + '.ratio.'
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
multicall = rtorrent.rpc.Multicall(self)
|
||||||
|
|
||||||
|
retriever_methods = [m for m in self.methods
|
||||||
|
if m.is_retriever() and m.is_available(self._rt_obj)]
|
||||||
|
|
||||||
|
for method in retriever_methods:
|
||||||
|
multicall.add(method)
|
||||||
|
|
||||||
|
multicall.call()
|
||||||
|
|
||||||
|
def enable(self):
|
||||||
|
p = self._rt_obj._get_conn()
|
||||||
|
return getattr(p, self._get_prefix() + 'enable')()
|
||||||
|
|
||||||
|
def disable(self):
|
||||||
|
p = self._rt_obj._get_conn()
|
||||||
|
return getattr(p, self._get_prefix() + 'disable')()
|
||||||
|
|
||||||
|
def set_command(self, *methods):
|
||||||
|
methods = [m + '=' for m in methods]
|
||||||
|
|
||||||
|
m = rtorrent.rpc.Multicall(self)
|
||||||
|
self.multicall_add(
|
||||||
|
m, 'system.method.set',
|
||||||
|
self._get_prefix() + 'command',
|
||||||
|
*methods
|
||||||
|
)
|
||||||
|
|
||||||
|
return(m.call()[-1])
|
||||||
Regular → Executable
File renamed without changes.
Executable
+281
@@ -0,0 +1,281 @@
|
|||||||
|
# Copyright (C) 2011 by clueless <clueless.nospam ! mail.com>
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be included in
|
||||||
|
# all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
# THE SOFTWARE.
|
||||||
|
#
|
||||||
|
# Version: 20111107
|
||||||
|
#
|
||||||
|
# Changelog
|
||||||
|
# ---------
|
||||||
|
# 2011-11-07 - Added support for Python2 (tested on 2.6)
|
||||||
|
# 2011-10-03 - Fixed: moved check for end of list at the top of the while loop
|
||||||
|
# in _decode_list (in case the list is empty) (Chris Lucas)
|
||||||
|
# - Converted dictionary keys to str
|
||||||
|
# 2011-04-24 - Changed date format to YYYY-MM-DD for versioning, bigger
|
||||||
|
# integer denotes a newer version
|
||||||
|
# - Fixed a bug that would treat False as an integral type but
|
||||||
|
# encode it using the 'False' string, attempting to encode a
|
||||||
|
# boolean now results in an error
|
||||||
|
# - Fixed a bug where an integer value of 0 in a list or
|
||||||
|
# dictionary resulted in a parse error while decoding
|
||||||
|
#
|
||||||
|
# 2011-04-03 - Original release
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
_py3 = sys.version_info[0] == 3
|
||||||
|
|
||||||
|
if _py3:
|
||||||
|
_VALID_STRING_TYPES = (str,)
|
||||||
|
else:
|
||||||
|
_VALID_STRING_TYPES = (str, unicode) # @UndefinedVariable
|
||||||
|
|
||||||
|
_TYPE_INT = 1
|
||||||
|
_TYPE_STRING = 2
|
||||||
|
_TYPE_LIST = 3
|
||||||
|
_TYPE_DICTIONARY = 4
|
||||||
|
_TYPE_END = 5
|
||||||
|
_TYPE_INVALID = 6
|
||||||
|
|
||||||
|
# Function to determine the type of he next value/item
|
||||||
|
# Arguments:
|
||||||
|
# char First character of the string that is to be decoded
|
||||||
|
# Return value:
|
||||||
|
# Returns an integer that describes what type the next value/item is
|
||||||
|
|
||||||
|
|
||||||
|
def _gettype(char):
|
||||||
|
if not isinstance(char, int):
|
||||||
|
char = ord(char)
|
||||||
|
if char == 0x6C: # 'l'
|
||||||
|
return _TYPE_LIST
|
||||||
|
elif char == 0x64: # 'd'
|
||||||
|
return _TYPE_DICTIONARY
|
||||||
|
elif char == 0x69: # 'i'
|
||||||
|
return _TYPE_INT
|
||||||
|
elif char == 0x65: # 'e'
|
||||||
|
return _TYPE_END
|
||||||
|
elif char >= 0x30 and char <= 0x39: # '0' '9'
|
||||||
|
return _TYPE_STRING
|
||||||
|
else:
|
||||||
|
return _TYPE_INVALID
|
||||||
|
|
||||||
|
# Function to parse a string from the bendcoded data
|
||||||
|
# Arguments:
|
||||||
|
# data bencoded data, must be guaranteed to be a string
|
||||||
|
# Return Value:
|
||||||
|
# Returns a tuple, the first member of the tuple is the parsed string
|
||||||
|
# The second member is whatever remains of the bencoded data so it can
|
||||||
|
# be used to parse the next part of the data
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_string(data):
|
||||||
|
end = 1
|
||||||
|
# if py3, data[end] is going to be an int
|
||||||
|
# if py2, data[end] will be a string
|
||||||
|
if _py3:
|
||||||
|
char = 0x3A
|
||||||
|
else:
|
||||||
|
char = chr(0x3A)
|
||||||
|
|
||||||
|
while data[end] != char: # ':'
|
||||||
|
end = end + 1
|
||||||
|
strlen = int(data[:end])
|
||||||
|
return (data[end + 1:strlen + end + 1], data[strlen + end + 1:])
|
||||||
|
|
||||||
|
# Function to parse an integer from the bencoded data
|
||||||
|
# Arguments:
|
||||||
|
# data bencoded data, must be guaranteed to be an integer
|
||||||
|
# Return Value:
|
||||||
|
# Returns a tuple, the first member of the tuple is the parsed string
|
||||||
|
# The second member is whatever remains of the bencoded data so it can
|
||||||
|
# be used to parse the next part of the data
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_int(data):
|
||||||
|
end = 1
|
||||||
|
# if py3, data[end] is going to be an int
|
||||||
|
# if py2, data[end] will be a string
|
||||||
|
if _py3:
|
||||||
|
char = 0x65
|
||||||
|
else:
|
||||||
|
char = chr(0x65)
|
||||||
|
|
||||||
|
while data[end] != char: # 'e'
|
||||||
|
end = end + 1
|
||||||
|
return (int(data[1:end]), data[end + 1:])
|
||||||
|
|
||||||
|
# Function to parse a bencoded list
|
||||||
|
# Arguments:
|
||||||
|
# data bencoded data, must be guaranted to be the start of a list
|
||||||
|
# Return Value:
|
||||||
|
# Returns a tuple, the first member of the tuple is the parsed list
|
||||||
|
# The second member is whatever remains of the bencoded data so it can
|
||||||
|
# be used to parse the next part of the data
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_list(data):
|
||||||
|
x = []
|
||||||
|
overflow = data[1:]
|
||||||
|
while True: # Loop over the data
|
||||||
|
if _gettype(overflow[0]) == _TYPE_END: # - Break if we reach the end of the list
|
||||||
|
return (x, overflow[1:]) # and return the list and overflow
|
||||||
|
|
||||||
|
value, overflow = _decode(overflow) #
|
||||||
|
if isinstance(value, bool) or overflow == '': # - if we have a parse error
|
||||||
|
return (False, False) # Die with error
|
||||||
|
else: # - Otherwise
|
||||||
|
x.append(value) # add the value to the list
|
||||||
|
|
||||||
|
|
||||||
|
# Function to parse a bencoded list
|
||||||
|
# Arguments:
|
||||||
|
# data bencoded data, must be guaranted to be the start of a list
|
||||||
|
# Return Value:
|
||||||
|
# Returns a tuple, the first member of the tuple is the parsed dictionary
|
||||||
|
# The second member is whatever remains of the bencoded data so it can
|
||||||
|
# be used to parse the next part of the data
|
||||||
|
def _decode_dict(data):
|
||||||
|
x = {}
|
||||||
|
overflow = data[1:]
|
||||||
|
while True: # Loop over the data
|
||||||
|
if _gettype(overflow[0]) != _TYPE_STRING: # - If the key is not a string
|
||||||
|
return (False, False) # Die with error
|
||||||
|
key, overflow = _decode(overflow) #
|
||||||
|
if key == False or overflow == '': # - If parse error
|
||||||
|
return (False, False) # Die with error
|
||||||
|
value, overflow = _decode(overflow) #
|
||||||
|
if isinstance(value, bool) or overflow == '': # - If parse error
|
||||||
|
print("Error parsing value")
|
||||||
|
print(value)
|
||||||
|
print(overflow)
|
||||||
|
return (False, False) # Die with error
|
||||||
|
else:
|
||||||
|
# don't use bytes for the key
|
||||||
|
key = key.decode()
|
||||||
|
x[key] = value
|
||||||
|
if _gettype(overflow[0]) == _TYPE_END:
|
||||||
|
return (x, overflow[1:])
|
||||||
|
|
||||||
|
# Arguments:
|
||||||
|
# data bencoded data in bytes format
|
||||||
|
# Return Values:
|
||||||
|
# Returns a tuple, the first member is the parsed data, could be a string,
|
||||||
|
# an integer, a list or a dictionary, or a combination of those
|
||||||
|
# The second member is the leftover of parsing, if everything parses correctly this
|
||||||
|
# should be an empty byte string
|
||||||
|
|
||||||
|
|
||||||
|
def _decode(data):
|
||||||
|
btype = _gettype(data[0])
|
||||||
|
if btype == _TYPE_INT:
|
||||||
|
return _decode_int(data)
|
||||||
|
elif btype == _TYPE_STRING:
|
||||||
|
return _decode_string(data)
|
||||||
|
elif btype == _TYPE_LIST:
|
||||||
|
return _decode_list(data)
|
||||||
|
elif btype == _TYPE_DICTIONARY:
|
||||||
|
return _decode_dict(data)
|
||||||
|
else:
|
||||||
|
return (False, False)
|
||||||
|
|
||||||
|
# Function to decode bencoded data
|
||||||
|
# Arguments:
|
||||||
|
# data bencoded data, can be str or bytes
|
||||||
|
# Return Values:
|
||||||
|
# Returns the decoded data on success, this coud be bytes, int, dict or list
|
||||||
|
# or a combinatin of those
|
||||||
|
# If an error occurs the return value is False
|
||||||
|
|
||||||
|
|
||||||
|
def decode(data):
|
||||||
|
# if isinstance(data, str):
|
||||||
|
# data = data.encode()
|
||||||
|
decoded, overflow = _decode(data)
|
||||||
|
return decoded
|
||||||
|
|
||||||
|
# Args: data as integer
|
||||||
|
# return: encoded byte string
|
||||||
|
|
||||||
|
|
||||||
|
def _encode_int(data):
|
||||||
|
return b'i' + str(data).encode() + b'e'
|
||||||
|
|
||||||
|
# Args: data as string or bytes
|
||||||
|
# Return: encoded byte string
|
||||||
|
|
||||||
|
|
||||||
|
def _encode_string(data):
|
||||||
|
return str(len(data)).encode() + b':' + data
|
||||||
|
|
||||||
|
# Args: data as list
|
||||||
|
# Return: Encoded byte string, false on error
|
||||||
|
|
||||||
|
|
||||||
|
def _encode_list(data):
|
||||||
|
elist = b'l'
|
||||||
|
for item in data:
|
||||||
|
eitem = encode(item)
|
||||||
|
if eitem == False:
|
||||||
|
return False
|
||||||
|
elist += eitem
|
||||||
|
return elist + b'e'
|
||||||
|
|
||||||
|
# Args: data as dict
|
||||||
|
# Return: encoded byte string, false on error
|
||||||
|
|
||||||
|
|
||||||
|
def _encode_dict(data):
|
||||||
|
edict = b'd'
|
||||||
|
keys = []
|
||||||
|
for key in data:
|
||||||
|
if not isinstance(key, _VALID_STRING_TYPES) and not isinstance(key, bytes):
|
||||||
|
return False
|
||||||
|
keys.append(key)
|
||||||
|
keys.sort()
|
||||||
|
for key in keys:
|
||||||
|
ekey = encode(key)
|
||||||
|
eitem = encode(data[key])
|
||||||
|
if ekey == False or eitem == False:
|
||||||
|
return False
|
||||||
|
edict += ekey + eitem
|
||||||
|
return edict + b'e'
|
||||||
|
|
||||||
|
# Function to encode a variable in bencoding
|
||||||
|
# Arguments:
|
||||||
|
# data Variable to be encoded, can be a list, dict, str, bytes, int or a combination of those
|
||||||
|
# Return Values:
|
||||||
|
# Returns the encoded data as a byte string when successful
|
||||||
|
# If an error occurs the return value is False
|
||||||
|
|
||||||
|
|
||||||
|
def encode(data):
|
||||||
|
if isinstance(data, bool):
|
||||||
|
return False
|
||||||
|
elif isinstance(data, int):
|
||||||
|
return _encode_int(data)
|
||||||
|
elif isinstance(data, bytes):
|
||||||
|
return _encode_string(data)
|
||||||
|
elif isinstance(data, _VALID_STRING_TYPES):
|
||||||
|
return _encode_string(data.encode())
|
||||||
|
elif isinstance(data, list):
|
||||||
|
return _encode_list(data)
|
||||||
|
elif isinstance(data, dict):
|
||||||
|
return _encode_dict(data)
|
||||||
|
else:
|
||||||
|
return False
|
||||||
Executable
+159
@@ -0,0 +1,159 @@
|
|||||||
|
# Copyright (c) 2013 Chris Lucas, <chris@chrisjlucas.com>
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
# a copy of this software and associated documentation files (the
|
||||||
|
# "Software"), to deal in the Software without restriction, including
|
||||||
|
# without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
# permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
# the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be
|
||||||
|
# included in all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
from rtorrent.compat import is_py3
|
||||||
|
import os.path
|
||||||
|
import re
|
||||||
|
import rtorrent.lib.bencode as bencode
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
if is_py3():
|
||||||
|
from urllib.request import urlopen # @UnresolvedImport @UnusedImport
|
||||||
|
else:
|
||||||
|
from urllib2 import urlopen # @UnresolvedImport @Reimport
|
||||||
|
|
||||||
|
|
||||||
|
class TorrentParser():
|
||||||
|
def __init__(self, torrent):
|
||||||
|
"""Decode and parse given torrent
|
||||||
|
|
||||||
|
@param torrent: handles: urls, file paths, string of torrent data
|
||||||
|
@type torrent: str
|
||||||
|
|
||||||
|
@raise AssertionError: Can be raised for a couple reasons:
|
||||||
|
- If _get_raw_torrent() couldn't figure out
|
||||||
|
what X{torrent} is
|
||||||
|
- if X{torrent} isn't a valid bencoded torrent file
|
||||||
|
"""
|
||||||
|
self.torrent = torrent
|
||||||
|
self._raw_torrent = None # : testing yo
|
||||||
|
self._torrent_decoded = None # : what up
|
||||||
|
self.file_type = None
|
||||||
|
|
||||||
|
self._get_raw_torrent()
|
||||||
|
assert self._raw_torrent is not None, "Couldn't get raw_torrent."
|
||||||
|
if self._torrent_decoded is None:
|
||||||
|
self._decode_torrent()
|
||||||
|
assert isinstance(self._torrent_decoded, dict), "Invalid torrent file."
|
||||||
|
self._parse_torrent()
|
||||||
|
|
||||||
|
def _is_raw(self):
|
||||||
|
raw = False
|
||||||
|
if isinstance(self.torrent, (str, bytes)):
|
||||||
|
if isinstance(self._decode_torrent(self.torrent), dict):
|
||||||
|
raw = True
|
||||||
|
else:
|
||||||
|
# reset self._torrent_decoded (currently equals False)
|
||||||
|
self._torrent_decoded = None
|
||||||
|
|
||||||
|
return(raw)
|
||||||
|
|
||||||
|
def _get_raw_torrent(self):
|
||||||
|
"""Get raw torrent data by determining what self.torrent is"""
|
||||||
|
# already raw?
|
||||||
|
if self._is_raw():
|
||||||
|
self.file_type = "raw"
|
||||||
|
self._raw_torrent = self.torrent
|
||||||
|
return
|
||||||
|
# local file?
|
||||||
|
if os.path.isfile(self.torrent):
|
||||||
|
self.file_type = "file"
|
||||||
|
self._raw_torrent = open(self.torrent, "rb").read()
|
||||||
|
# url?
|
||||||
|
elif re.search("^(http|ftp):\/\/", self.torrent, re.I):
|
||||||
|
self.file_type = "url"
|
||||||
|
self._raw_torrent = urlopen(self.torrent).read()
|
||||||
|
|
||||||
|
def _decode_torrent(self, raw_torrent=None):
|
||||||
|
if raw_torrent is None:
|
||||||
|
raw_torrent = self._raw_torrent
|
||||||
|
self._torrent_decoded = bencode.decode(raw_torrent)
|
||||||
|
return(self._torrent_decoded)
|
||||||
|
|
||||||
|
def _calc_info_hash(self):
|
||||||
|
self.info_hash = None
|
||||||
|
if "info" in self._torrent_decoded.keys():
|
||||||
|
info_dict = self._torrent_decoded["info"]
|
||||||
|
self.info_hash = hashlib.sha1(bencode.encode(
|
||||||
|
info_dict)).hexdigest().upper()
|
||||||
|
|
||||||
|
return(self.info_hash)
|
||||||
|
|
||||||
|
def _parse_torrent(self):
|
||||||
|
for k in self._torrent_decoded:
|
||||||
|
key = k.replace(" ", "_").lower()
|
||||||
|
setattr(self, key, self._torrent_decoded[k])
|
||||||
|
|
||||||
|
self._calc_info_hash()
|
||||||
|
|
||||||
|
|
||||||
|
class NewTorrentParser(object):
|
||||||
|
@staticmethod
|
||||||
|
def _read_file(fp):
|
||||||
|
return fp.read()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _write_file(fp):
|
||||||
|
fp.write()
|
||||||
|
return fp
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _decode_torrent(data):
|
||||||
|
return bencode.decode(data)
|
||||||
|
|
||||||
|
def __init__(self, input):
|
||||||
|
self.input = input
|
||||||
|
self._raw_torrent = None
|
||||||
|
self._decoded_torrent = None
|
||||||
|
self._hash_outdated = False
|
||||||
|
|
||||||
|
if isinstance(self.input, (str, bytes)):
|
||||||
|
# path to file?
|
||||||
|
if os.path.isfile(self.input):
|
||||||
|
self._raw_torrent = self._read_file(open(self.input, "rb"))
|
||||||
|
else:
|
||||||
|
# assume input was the raw torrent data (do we really want
|
||||||
|
# this?)
|
||||||
|
self._raw_torrent = self.input
|
||||||
|
|
||||||
|
# file-like object?
|
||||||
|
elif self.input.hasattr("read"):
|
||||||
|
self._raw_torrent = self._read_file(self.input)
|
||||||
|
|
||||||
|
assert self._raw_torrent is not None, "Invalid input: input must be a path or a file-like object"
|
||||||
|
|
||||||
|
self._decoded_torrent = self._decode_torrent(self._raw_torrent)
|
||||||
|
|
||||||
|
assert isinstance(
|
||||||
|
self._decoded_torrent, dict), "File could not be decoded"
|
||||||
|
|
||||||
|
def _calc_info_hash(self):
|
||||||
|
self.info_hash = None
|
||||||
|
info_dict = self._torrent_decoded["info"]
|
||||||
|
self.info_hash = hashlib.sha1(bencode.encode(
|
||||||
|
info_dict)).hexdigest().upper()
|
||||||
|
|
||||||
|
return(self.info_hash)
|
||||||
|
|
||||||
|
def set_tracker(self, tracker):
|
||||||
|
self._decoded_torrent["announce"] = tracker
|
||||||
|
|
||||||
|
def get_tracker(self):
|
||||||
|
return self._decoded_torrent.get("announce")
|
||||||
Executable
Whitespace-only changes.
Executable
+23
@@ -0,0 +1,23 @@
|
|||||||
|
# Copyright (c) 2013 Chris Lucas, <chris@chrisjlucas.com>
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
# a copy of this software and associated documentation files (the
|
||||||
|
# "Software"), to deal in the Software without restriction, including
|
||||||
|
# without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
# permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
# the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be
|
||||||
|
# included in all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
from rtorrent.compat import xmlrpclib
|
||||||
|
|
||||||
|
HTTPServerProxy = xmlrpclib.ServerProxy
|
||||||
Executable
+98
@@ -0,0 +1,98 @@
|
|||||||
|
# Copyright (c) 2013 Chris Lucas, <chris@chrisjlucas.com>
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
# a copy of this software and associated documentation files (the
|
||||||
|
# "Software"), to deal in the Software without restriction, including
|
||||||
|
# without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
# permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
# the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be
|
||||||
|
# included in all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
# from rtorrent.rpc import Method
|
||||||
|
import rtorrent.rpc
|
||||||
|
|
||||||
|
from rtorrent.common import safe_repr
|
||||||
|
|
||||||
|
Method = rtorrent.rpc.Method
|
||||||
|
|
||||||
|
|
||||||
|
class Peer:
|
||||||
|
"""Represents an individual peer within a L{Torrent} instance."""
|
||||||
|
def __init__(self, _rt_obj, info_hash, **kwargs):
|
||||||
|
self._rt_obj = _rt_obj
|
||||||
|
self.info_hash = info_hash # : info hash for the torrent the peer is associated with
|
||||||
|
for k in kwargs.keys():
|
||||||
|
setattr(self, k, kwargs.get(k, None))
|
||||||
|
|
||||||
|
self.rpc_id = "{0}:p{1}".format(
|
||||||
|
self.info_hash, self.id) # : unique id to pass to rTorrent
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return safe_repr("Peer(id={0})", self.id)
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Refresh peer data
|
||||||
|
|
||||||
|
@note: All fields are stored as attributes to self.
|
||||||
|
|
||||||
|
@return: None
|
||||||
|
"""
|
||||||
|
multicall = rtorrent.rpc.Multicall(self)
|
||||||
|
retriever_methods = [m for m in methods
|
||||||
|
if m.is_retriever() and m.is_available(self._rt_obj)]
|
||||||
|
for method in retriever_methods:
|
||||||
|
multicall.add(method, self.rpc_id)
|
||||||
|
|
||||||
|
multicall.call()
|
||||||
|
|
||||||
|
methods = [
|
||||||
|
# RETRIEVERS
|
||||||
|
Method(Peer, 'is_preferred', 'p.is_preferred',
|
||||||
|
boolean=True,
|
||||||
|
),
|
||||||
|
Method(Peer, 'get_down_rate', 'p.get_down_rate'),
|
||||||
|
Method(Peer, 'is_unwanted', 'p.is_unwanted',
|
||||||
|
boolean=True,
|
||||||
|
),
|
||||||
|
Method(Peer, 'get_peer_total', 'p.get_peer_total'),
|
||||||
|
Method(Peer, 'get_peer_rate', 'p.get_peer_rate'),
|
||||||
|
Method(Peer, 'get_port', 'p.get_port'),
|
||||||
|
Method(Peer, 'is_snubbed', 'p.is_snubbed',
|
||||||
|
boolean=True,
|
||||||
|
),
|
||||||
|
Method(Peer, 'get_id_html', 'p.get_id_html'),
|
||||||
|
Method(Peer, 'get_up_rate', 'p.get_up_rate'),
|
||||||
|
Method(Peer, 'is_banned', 'p.banned',
|
||||||
|
boolean=True,
|
||||||
|
),
|
||||||
|
Method(Peer, 'get_completed_percent', 'p.get_completed_percent'),
|
||||||
|
Method(Peer, 'completed_percent', 'p.completed_percent'),
|
||||||
|
Method(Peer, 'get_id', 'p.get_id'),
|
||||||
|
Method(Peer, 'is_obfuscated', 'p.is_obfuscated',
|
||||||
|
boolean=True,
|
||||||
|
),
|
||||||
|
Method(Peer, 'get_down_total', 'p.get_down_total'),
|
||||||
|
Method(Peer, 'get_client_version', 'p.get_client_version'),
|
||||||
|
Method(Peer, 'get_address', 'p.get_address'),
|
||||||
|
Method(Peer, 'is_incoming', 'p.is_incoming',
|
||||||
|
boolean=True,
|
||||||
|
),
|
||||||
|
Method(Peer, 'is_encrypted', 'p.is_encrypted',
|
||||||
|
boolean=True,
|
||||||
|
),
|
||||||
|
Method(Peer, 'get_options_str', 'p.get_options_str'),
|
||||||
|
Method(Peer, 'get_client_version', 'p.client_version'),
|
||||||
|
Method(Peer, 'get_up_total', 'p.get_up_total'),
|
||||||
|
|
||||||
|
# MODIFIERS
|
||||||
|
]
|
||||||
Executable
+369
@@ -0,0 +1,369 @@
|
|||||||
|
# Copyright (c) 2013 Chris Lucas, <chris@chrisjlucas.com>
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
# a copy of this software and associated documentation files (the
|
||||||
|
# "Software"), to deal in the Software without restriction, including
|
||||||
|
# without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
# permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
# the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be
|
||||||
|
# included in all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
from base64 import encodestring
|
||||||
|
import httplib
|
||||||
|
import inspect
|
||||||
|
import string
|
||||||
|
|
||||||
|
import rtorrent
|
||||||
|
import re
|
||||||
|
from rtorrent.common import bool_to_int, convert_version_tuple_to_str,\
|
||||||
|
safe_repr
|
||||||
|
from rtorrent.err import RTorrentVersionError, MethodError
|
||||||
|
from rtorrent.compat import xmlrpclib
|
||||||
|
|
||||||
|
|
||||||
|
class BasicAuthTransport(xmlrpclib.Transport):
|
||||||
|
def __init__(self, username=None, password=None):
|
||||||
|
xmlrpclib.Transport.__init__(self)
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
|
||||||
|
def send_auth(self, h):
|
||||||
|
if self.username is not None and self.password is not None:
|
||||||
|
h.putheader('AUTHORIZATION', "Basic %s" % string.replace(
|
||||||
|
encodestring("%s:%s" % (self.username, self.password)),
|
||||||
|
"\012", ""
|
||||||
|
))
|
||||||
|
|
||||||
|
def single_request(self, host, handler, request_body, verbose=0):
|
||||||
|
# issue XML-RPC request
|
||||||
|
|
||||||
|
h = self.make_connection(host)
|
||||||
|
if verbose:
|
||||||
|
h.set_debuglevel(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.send_request(h, handler, request_body)
|
||||||
|
self.send_host(h, host)
|
||||||
|
self.send_user_agent(h)
|
||||||
|
self.send_auth(h)
|
||||||
|
self.send_content(h, request_body)
|
||||||
|
|
||||||
|
response = h.getresponse(buffering=True)
|
||||||
|
if response.status == 200:
|
||||||
|
self.verbose = verbose
|
||||||
|
return self.parse_response(response)
|
||||||
|
except xmlrpclib.Fault:
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
self.close()
|
||||||
|
raise
|
||||||
|
|
||||||
|
#discard any response data and raise exception
|
||||||
|
if (response.getheader("content-length", 0)):
|
||||||
|
response.read()
|
||||||
|
raise xmlrpclib.ProtocolError(
|
||||||
|
host + handler,
|
||||||
|
response.status, response.reason,
|
||||||
|
response.msg,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_varname(rpc_call):
|
||||||
|
"""Transform rpc method into variable name.
|
||||||
|
|
||||||
|
@newfield example: Example
|
||||||
|
@example: if the name of the rpc method is 'p.get_down_rate', the variable
|
||||||
|
name will be 'down_rate'
|
||||||
|
"""
|
||||||
|
# extract variable name from xmlrpc func name
|
||||||
|
r = re.search(
|
||||||
|
"([ptdf]\.|system\.|get\_|is\_|set\_)+([^=]*)", rpc_call, re.I)
|
||||||
|
if r:
|
||||||
|
return(r.groups()[-1])
|
||||||
|
else:
|
||||||
|
return(None)
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_unavailable_rpc_method(method, rt_obj):
|
||||||
|
msg = "Method isn't available."
|
||||||
|
if rt_obj._get_client_version_tuple() < method.min_version:
|
||||||
|
msg = "This method is only available in " \
|
||||||
|
"RTorrent version v{0} or later".format(
|
||||||
|
convert_version_tuple_to_str(method.min_version))
|
||||||
|
|
||||||
|
raise MethodError(msg)
|
||||||
|
|
||||||
|
|
||||||
|
class DummyClass:
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Method:
|
||||||
|
"""Represents an individual RPC method"""
|
||||||
|
|
||||||
|
def __init__(self, _class, method_name,
|
||||||
|
rpc_call, docstring=None, varname=None, **kwargs):
|
||||||
|
self._class = _class # : Class this method is associated with
|
||||||
|
self.class_name = _class.__name__
|
||||||
|
self.method_name = method_name # : name of public-facing method
|
||||||
|
self.rpc_call = rpc_call # : name of rpc method
|
||||||
|
self.docstring = docstring # : docstring for rpc method (optional)
|
||||||
|
self.varname = varname # : variable for the result of the method call, usually set to self.varname
|
||||||
|
self.min_version = kwargs.get("min_version", (
|
||||||
|
0, 0, 0)) # : Minimum version of rTorrent required
|
||||||
|
self.boolean = kwargs.get("boolean", False) # : returns boolean value?
|
||||||
|
self.post_process_func = kwargs.get(
|
||||||
|
"post_process_func", None) # : custom post process function
|
||||||
|
self.aliases = kwargs.get(
|
||||||
|
"aliases", []) # : aliases for method (optional)
|
||||||
|
self.required_args = []
|
||||||
|
#: Arguments required when calling the method (not utilized)
|
||||||
|
|
||||||
|
self.method_type = self._get_method_type()
|
||||||
|
|
||||||
|
if self.varname is None:
|
||||||
|
self.varname = get_varname(self.rpc_call)
|
||||||
|
assert self.varname is not None, "Couldn't get variable name."
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return safe_repr("Method(method_name='{0}', rpc_call='{1}')",
|
||||||
|
self.method_name, self.rpc_call)
|
||||||
|
|
||||||
|
def _get_method_type(self):
|
||||||
|
"""Determine whether method is a modifier or a retriever"""
|
||||||
|
if self.method_name[:4] == "set_": return('m') # modifier
|
||||||
|
else:
|
||||||
|
return('r') # retriever
|
||||||
|
|
||||||
|
def is_modifier(self):
|
||||||
|
if self.method_type == 'm':
|
||||||
|
return(True)
|
||||||
|
else:
|
||||||
|
return(False)
|
||||||
|
|
||||||
|
def is_retriever(self):
|
||||||
|
if self.method_type == 'r':
|
||||||
|
return(True)
|
||||||
|
else:
|
||||||
|
return(False)
|
||||||
|
|
||||||
|
def is_available(self, rt_obj):
|
||||||
|
if rt_obj._get_client_version_tuple() < self.min_version or \
|
||||||
|
self.rpc_call not in rt_obj._get_rpc_methods():
|
||||||
|
return(False)
|
||||||
|
else:
|
||||||
|
return(True)
|
||||||
|
|
||||||
|
|
||||||
|
class Multicall:
|
||||||
|
def __init__(self, class_obj, **kwargs):
|
||||||
|
self.class_obj = class_obj
|
||||||
|
if class_obj.__class__.__name__ == "RTorrent":
|
||||||
|
self.rt_obj = class_obj
|
||||||
|
else:
|
||||||
|
self.rt_obj = class_obj._rt_obj
|
||||||
|
self.calls = []
|
||||||
|
|
||||||
|
def add(self, method, *args):
|
||||||
|
"""Add call to multicall
|
||||||
|
|
||||||
|
@param method: L{Method} instance or name of raw RPC method
|
||||||
|
@type method: Method or str
|
||||||
|
|
||||||
|
@param args: call arguments
|
||||||
|
"""
|
||||||
|
# if a raw rpc method was given instead of a Method instance,
|
||||||
|
# try and find the instance for it. And if all else fails, create a
|
||||||
|
# dummy Method instance
|
||||||
|
if isinstance(method, str):
|
||||||
|
result = find_method(method)
|
||||||
|
# if result not found
|
||||||
|
if result == -1:
|
||||||
|
method = Method(DummyClass, method, method)
|
||||||
|
else:
|
||||||
|
method = result
|
||||||
|
|
||||||
|
# ensure method is available before adding
|
||||||
|
if not method.is_available(self.rt_obj):
|
||||||
|
_handle_unavailable_rpc_method(method, self.rt_obj)
|
||||||
|
|
||||||
|
self.calls.append((method, args))
|
||||||
|
|
||||||
|
def list_calls(self):
|
||||||
|
for c in self.calls:
|
||||||
|
print(c)
|
||||||
|
|
||||||
|
def call(self):
|
||||||
|
"""Execute added multicall calls
|
||||||
|
|
||||||
|
@return: the results (post-processed), in the order they were added
|
||||||
|
@rtype: tuple
|
||||||
|
"""
|
||||||
|
m = xmlrpclib.MultiCall(self.rt_obj._get_conn())
|
||||||
|
for call in self.calls:
|
||||||
|
method, args = call
|
||||||
|
rpc_call = getattr(method, "rpc_call")
|
||||||
|
getattr(m, rpc_call)(*args)
|
||||||
|
|
||||||
|
results = m()
|
||||||
|
results = tuple(results)
|
||||||
|
results_processed = []
|
||||||
|
|
||||||
|
for r, c in zip(results, self.calls):
|
||||||
|
method = c[0] # Method instance
|
||||||
|
result = process_result(method, r)
|
||||||
|
results_processed.append(result)
|
||||||
|
# assign result to class_obj
|
||||||
|
exists = hasattr(self.class_obj, method.varname)
|
||||||
|
if not exists or not inspect.ismethod(getattr(self.class_obj, method.varname)):
|
||||||
|
setattr(self.class_obj, method.varname, result)
|
||||||
|
|
||||||
|
return(tuple(results_processed))
|
||||||
|
|
||||||
|
|
||||||
|
def call_method(class_obj, method, *args):
|
||||||
|
"""Handles single RPC calls
|
||||||
|
|
||||||
|
@param class_obj: Peer/File/Torrent/Tracker/RTorrent instance
|
||||||
|
@type class_obj: object
|
||||||
|
|
||||||
|
@param method: L{Method} instance or name of raw RPC method
|
||||||
|
@type method: Method or str
|
||||||
|
"""
|
||||||
|
if method.is_retriever():
|
||||||
|
args = args[:-1]
|
||||||
|
else:
|
||||||
|
assert args[-1] is not None, "No argument given."
|
||||||
|
|
||||||
|
if class_obj.__class__.__name__ == "RTorrent":
|
||||||
|
rt_obj = class_obj
|
||||||
|
else:
|
||||||
|
rt_obj = class_obj._rt_obj
|
||||||
|
|
||||||
|
# check if rpc method is even available
|
||||||
|
if not method.is_available(rt_obj):
|
||||||
|
_handle_unavailable_rpc_method(method, rt_obj)
|
||||||
|
|
||||||
|
m = Multicall(class_obj)
|
||||||
|
m.add(method, *args)
|
||||||
|
# only added one method, only getting one result back
|
||||||
|
ret_value = m.call()[0]
|
||||||
|
|
||||||
|
####### OBSOLETE ##########################################################
|
||||||
|
# if method.is_retriever():
|
||||||
|
# #value = process_result(method, ret_value)
|
||||||
|
# value = ret_value #MultiCall already processed the result
|
||||||
|
# else:
|
||||||
|
# # we're setting the user's input to method.varname
|
||||||
|
# # but we'll return the value that xmlrpc gives us
|
||||||
|
# value = process_result(method, args[-1])
|
||||||
|
##########################################################################
|
||||||
|
|
||||||
|
return(ret_value)
|
||||||
|
|
||||||
|
|
||||||
|
def find_method(rpc_call):
|
||||||
|
"""Return L{Method} instance associated with given RPC call"""
|
||||||
|
method_lists = [
|
||||||
|
rtorrent.methods,
|
||||||
|
rtorrent.file.methods,
|
||||||
|
rtorrent.tracker.methods,
|
||||||
|
rtorrent.peer.methods,
|
||||||
|
rtorrent.torrent.methods,
|
||||||
|
]
|
||||||
|
|
||||||
|
for l in method_lists:
|
||||||
|
for m in l:
|
||||||
|
if m.rpc_call.lower() == rpc_call.lower():
|
||||||
|
return(m)
|
||||||
|
|
||||||
|
return(-1)
|
||||||
|
|
||||||
|
|
||||||
|
def process_result(method, result):
|
||||||
|
"""Process given C{B{result}} based on flags set in C{B{method}}
|
||||||
|
|
||||||
|
@param method: L{Method} instance
|
||||||
|
@type method: Method
|
||||||
|
|
||||||
|
@param result: result to be processed (the result of given L{Method} instance)
|
||||||
|
|
||||||
|
@note: Supported Processing:
|
||||||
|
- boolean - convert ones and zeros returned by rTorrent and
|
||||||
|
convert to python boolean values
|
||||||
|
"""
|
||||||
|
# handle custom post processing function
|
||||||
|
if method.post_process_func is not None:
|
||||||
|
result = method.post_process_func(result)
|
||||||
|
|
||||||
|
# is boolean?
|
||||||
|
if method.boolean:
|
||||||
|
if result in [1, '1']:
|
||||||
|
result = True
|
||||||
|
elif result in [0, '0']:
|
||||||
|
result = False
|
||||||
|
|
||||||
|
return(result)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_rpc_methods(class_, method_list):
|
||||||
|
"""Build glorified aliases to raw RPC methods"""
|
||||||
|
instance = None
|
||||||
|
if not inspect.isclass(class_):
|
||||||
|
instance = class_
|
||||||
|
class_ = instance.__class__
|
||||||
|
|
||||||
|
for m in method_list:
|
||||||
|
class_name = m.class_name
|
||||||
|
if class_name != class_.__name__:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if class_name == "RTorrent":
|
||||||
|
caller = lambda self, arg = None, method = m:\
|
||||||
|
call_method(self, method, bool_to_int(arg))
|
||||||
|
elif class_name == "Torrent":
|
||||||
|
caller = lambda self, arg = None, method = m:\
|
||||||
|
call_method(self, method, self.rpc_id,
|
||||||
|
bool_to_int(arg))
|
||||||
|
elif class_name in ["Tracker", "File"]:
|
||||||
|
caller = lambda self, arg = None, method = m:\
|
||||||
|
call_method(self, method, self.rpc_id,
|
||||||
|
bool_to_int(arg))
|
||||||
|
|
||||||
|
elif class_name == "Peer":
|
||||||
|
caller = lambda self, arg = None, method = m:\
|
||||||
|
call_method(self, method, self.rpc_id,
|
||||||
|
bool_to_int(arg))
|
||||||
|
|
||||||
|
elif class_name == "Group":
|
||||||
|
caller = lambda arg = None, method = m: \
|
||||||
|
call_method(instance, method, bool_to_int(arg))
|
||||||
|
|
||||||
|
if m.docstring is None:
|
||||||
|
m.docstring = ""
|
||||||
|
|
||||||
|
# print(m)
|
||||||
|
docstring = """{0}
|
||||||
|
|
||||||
|
@note: Variable where the result for this method is stored: {1}.{2}""".format(
|
||||||
|
m.docstring,
|
||||||
|
class_name,
|
||||||
|
m.varname)
|
||||||
|
|
||||||
|
caller.__doc__ = docstring
|
||||||
|
|
||||||
|
for method_name in [m.method_name] + list(m.aliases):
|
||||||
|
if instance is None:
|
||||||
|
setattr(class_, method_name, caller)
|
||||||
|
else:
|
||||||
|
setattr(instance, method_name, caller)
|
||||||
Executable
+506
@@ -0,0 +1,506 @@
|
|||||||
|
# Copyright (c) 2013 Chris Lucas, <chris@chrisjlucas.com>
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
# a copy of this software and associated documentation files (the
|
||||||
|
# "Software"), to deal in the Software without restriction, including
|
||||||
|
# without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
# permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
# the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be
|
||||||
|
# included in all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
import rtorrent.rpc
|
||||||
|
# from rtorrent.rpc import Method
|
||||||
|
import rtorrent.peer
|
||||||
|
import rtorrent.tracker
|
||||||
|
import rtorrent.file
|
||||||
|
import rtorrent.compat
|
||||||
|
|
||||||
|
from rtorrent.common import safe_repr
|
||||||
|
|
||||||
|
Peer = rtorrent.peer.Peer
|
||||||
|
Tracker = rtorrent.tracker.Tracker
|
||||||
|
File = rtorrent.file.File
|
||||||
|
Method = rtorrent.rpc.Method
|
||||||
|
|
||||||
|
|
||||||
|
class Torrent:
|
||||||
|
"""Represents an individual torrent within a L{RTorrent} instance."""
|
||||||
|
|
||||||
|
def __init__(self, _rt_obj, info_hash, **kwargs):
|
||||||
|
self._rt_obj = _rt_obj
|
||||||
|
self.info_hash = info_hash # : info hash for the torrent
|
||||||
|
self.rpc_id = self.info_hash # : unique id to pass to rTorrent
|
||||||
|
for k in kwargs.keys():
|
||||||
|
setattr(self, k, kwargs.get(k, None))
|
||||||
|
|
||||||
|
self.peers = []
|
||||||
|
self.trackers = []
|
||||||
|
self.files = []
|
||||||
|
|
||||||
|
self._call_custom_methods()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return safe_repr("Torrent(info_hash=\"{0}\" name=\"{1}\")",
|
||||||
|
self.info_hash, self.name)
|
||||||
|
|
||||||
|
def _call_custom_methods(self):
|
||||||
|
"""only calls methods that check instance variables."""
|
||||||
|
self._is_hash_checking_queued()
|
||||||
|
self._is_started()
|
||||||
|
self._is_paused()
|
||||||
|
|
||||||
|
def get_peers(self):
|
||||||
|
"""Get list of Peer instances for given torrent.
|
||||||
|
|
||||||
|
@return: L{Peer} instances
|
||||||
|
@rtype: list
|
||||||
|
|
||||||
|
@note: also assigns return value to self.peers
|
||||||
|
"""
|
||||||
|
self.peers = []
|
||||||
|
retriever_methods = [m for m in rtorrent.peer.methods
|
||||||
|
if m.is_retriever() and m.is_available(self._rt_obj)]
|
||||||
|
# need to leave 2nd arg empty (dunno why)
|
||||||
|
m = rtorrent.rpc.Multicall(self)
|
||||||
|
m.add("p.multicall", self.info_hash, "",
|
||||||
|
*[method.rpc_call + "=" for method in retriever_methods])
|
||||||
|
|
||||||
|
results = m.call()[0] # only sent one call, only need first result
|
||||||
|
|
||||||
|
for result in results:
|
||||||
|
results_dict = {}
|
||||||
|
# build results_dict
|
||||||
|
for m, r in zip(retriever_methods, result):
|
||||||
|
results_dict[m.varname] = rtorrent.rpc.process_result(m, r)
|
||||||
|
|
||||||
|
self.peers.append(Peer(
|
||||||
|
self._rt_obj, self.info_hash, **results_dict))
|
||||||
|
|
||||||
|
return(self.peers)
|
||||||
|
|
||||||
|
def get_trackers(self):
|
||||||
|
"""Get list of Tracker instances for given torrent.
|
||||||
|
|
||||||
|
@return: L{Tracker} instances
|
||||||
|
@rtype: list
|
||||||
|
|
||||||
|
@note: also assigns return value to self.trackers
|
||||||
|
"""
|
||||||
|
self.trackers = []
|
||||||
|
retriever_methods = [m for m in rtorrent.tracker.methods
|
||||||
|
if m.is_retriever() and m.is_available(self._rt_obj)]
|
||||||
|
|
||||||
|
# need to leave 2nd arg empty (dunno why)
|
||||||
|
m = rtorrent.rpc.Multicall(self)
|
||||||
|
m.add("t.multicall", self.info_hash, "",
|
||||||
|
*[method.rpc_call + "=" for method in retriever_methods])
|
||||||
|
|
||||||
|
results = m.call()[0] # only sent one call, only need first result
|
||||||
|
|
||||||
|
for result in results:
|
||||||
|
results_dict = {}
|
||||||
|
# build results_dict
|
||||||
|
for m, r in zip(retriever_methods, result):
|
||||||
|
results_dict[m.varname] = rtorrent.rpc.process_result(m, r)
|
||||||
|
|
||||||
|
self.trackers.append(Tracker(
|
||||||
|
self._rt_obj, self.info_hash, **results_dict))
|
||||||
|
|
||||||
|
return(self.trackers)
|
||||||
|
|
||||||
|
def get_files(self):
|
||||||
|
"""Get list of File instances for given torrent.
|
||||||
|
|
||||||
|
@return: L{File} instances
|
||||||
|
@rtype: list
|
||||||
|
|
||||||
|
@note: also assigns return value to self.files
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.files = []
|
||||||
|
retriever_methods = [m for m in rtorrent.file.methods
|
||||||
|
if m.is_retriever() and m.is_available(self._rt_obj)]
|
||||||
|
# 2nd arg can be anything, but it'll return all files in torrent
|
||||||
|
# regardless
|
||||||
|
m = rtorrent.rpc.Multicall(self)
|
||||||
|
m.add("f.multicall", self.info_hash, "",
|
||||||
|
*[method.rpc_call + "=" for method in retriever_methods])
|
||||||
|
|
||||||
|
results = m.call()[0] # only sent one call, only need first result
|
||||||
|
|
||||||
|
offset_method_index = retriever_methods.index(
|
||||||
|
rtorrent.rpc.find_method("f.get_offset"))
|
||||||
|
|
||||||
|
# make a list of the offsets of all the files, sort appropriately
|
||||||
|
offset_list = sorted([r[offset_method_index] for r in results])
|
||||||
|
|
||||||
|
for result in results:
|
||||||
|
results_dict = {}
|
||||||
|
# build results_dict
|
||||||
|
for m, r in zip(retriever_methods, result):
|
||||||
|
results_dict[m.varname] = rtorrent.rpc.process_result(m, r)
|
||||||
|
|
||||||
|
# get proper index positions for each file (based on the file
|
||||||
|
# offset)
|
||||||
|
f_index = offset_list.index(results_dict["offset"])
|
||||||
|
|
||||||
|
self.files.append(File(self._rt_obj, self.info_hash,
|
||||||
|
f_index, **results_dict))
|
||||||
|
|
||||||
|
return(self.files)
|
||||||
|
|
||||||
|
def set_directory(self, d):
|
||||||
|
"""Modify download directory
|
||||||
|
|
||||||
|
@note: Needs to stop torrent in order to change the directory.
|
||||||
|
Also doesn't restart after directory is set, that must be called
|
||||||
|
separately.
|
||||||
|
"""
|
||||||
|
m = rtorrent.rpc.Multicall(self)
|
||||||
|
self.multicall_add(m, "d.try_stop")
|
||||||
|
self.multicall_add(m, "d.set_directory", d)
|
||||||
|
|
||||||
|
self.directory = m.call()[-1]
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""Start the torrent"""
|
||||||
|
m = rtorrent.rpc.Multicall(self)
|
||||||
|
self.multicall_add(m, "d.try_start")
|
||||||
|
self.multicall_add(m, "d.is_active")
|
||||||
|
|
||||||
|
self.active = m.call()[-1]
|
||||||
|
return(self.active)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
""""Stop the torrent"""
|
||||||
|
m = rtorrent.rpc.Multicall(self)
|
||||||
|
self.multicall_add(m, "d.try_stop")
|
||||||
|
self.multicall_add(m, "d.is_active")
|
||||||
|
|
||||||
|
self.active = m.call()[-1]
|
||||||
|
return(self.active)
|
||||||
|
|
||||||
|
def pause(self):
|
||||||
|
"""Pause the torrent"""
|
||||||
|
m = rtorrent.rpc.Multicall(self)
|
||||||
|
self.multicall_add(m, "d.pause")
|
||||||
|
|
||||||
|
return(m.call()[-1])
|
||||||
|
|
||||||
|
def resume(self):
|
||||||
|
"""Resume the torrent"""
|
||||||
|
m = rtorrent.rpc.Multicall(self)
|
||||||
|
self.multicall_add(m, "d.resume")
|
||||||
|
|
||||||
|
return(m.call()[-1])
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Close the torrent and it's files"""
|
||||||
|
m = rtorrent.rpc.Multicall(self)
|
||||||
|
self.multicall_add(m, "d.close")
|
||||||
|
|
||||||
|
return(m.call()[-1])
|
||||||
|
|
||||||
|
def erase(self):
|
||||||
|
"""Delete the torrent
|
||||||
|
|
||||||
|
@note: doesn't delete the downloaded files"""
|
||||||
|
m = rtorrent.rpc.Multicall(self)
|
||||||
|
self.multicall_add(m, "d.erase")
|
||||||
|
|
||||||
|
return(m.call()[-1])
|
||||||
|
|
||||||
|
def check_hash(self):
|
||||||
|
"""(Re)hash check the torrent"""
|
||||||
|
m = rtorrent.rpc.Multicall(self)
|
||||||
|
self.multicall_add(m, "d.check_hash")
|
||||||
|
|
||||||
|
return(m.call()[-1])
|
||||||
|
|
||||||
|
def poll(self):
|
||||||
|
"""poll rTorrent to get latest peer/tracker/file information"""
|
||||||
|
self.get_peers()
|
||||||
|
self.get_trackers()
|
||||||
|
self.get_files()
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Refresh torrent data
|
||||||
|
|
||||||
|
@note: All fields are stored as attributes to self.
|
||||||
|
|
||||||
|
@return: None
|
||||||
|
"""
|
||||||
|
multicall = rtorrent.rpc.Multicall(self)
|
||||||
|
retriever_methods = [m for m in methods
|
||||||
|
if m.is_retriever() and m.is_available(self._rt_obj)]
|
||||||
|
for method in retriever_methods:
|
||||||
|
multicall.add(method, self.rpc_id)
|
||||||
|
|
||||||
|
multicall.call()
|
||||||
|
|
||||||
|
# custom functions (only call private methods, since they only check
|
||||||
|
# local variables and are therefore faster)
|
||||||
|
self._call_custom_methods()
|
||||||
|
|
||||||
|
def accept_seeders(self, accept_seeds):
|
||||||
|
"""Enable/disable whether the torrent connects to seeders
|
||||||
|
|
||||||
|
@param accept_seeds: enable/disable accepting seeders
|
||||||
|
@type accept_seeds: bool"""
|
||||||
|
if accept_seeds:
|
||||||
|
call = "d.accepting_seeders.enable"
|
||||||
|
else:
|
||||||
|
call = "d.accepting_seeders.disable"
|
||||||
|
|
||||||
|
m = rtorrent.rpc.Multicall(self)
|
||||||
|
self.multicall_add(m, call)
|
||||||
|
|
||||||
|
return(m.call()[-1])
|
||||||
|
|
||||||
|
def announce(self):
|
||||||
|
"""Announce torrent info to tracker(s)"""
|
||||||
|
m = rtorrent.rpc.Multicall(self)
|
||||||
|
self.multicall_add(m, "d.tracker_announce")
|
||||||
|
|
||||||
|
return(m.call()[-1])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _assert_custom_key_valid(key):
|
||||||
|
assert type(key) == int and key > 0 and key < 6, \
|
||||||
|
"key must be an integer between 1-5"
|
||||||
|
|
||||||
|
def get_custom(self, key):
|
||||||
|
"""
|
||||||
|
Get custom value
|
||||||
|
|
||||||
|
@param key: the index for the custom field (between 1-5)
|
||||||
|
@type key: int
|
||||||
|
|
||||||
|
@rtype: str
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._assert_custom_key_valid(key)
|
||||||
|
m = rtorrent.rpc.Multicall(self)
|
||||||
|
|
||||||
|
field = "custom{0}".format(key)
|
||||||
|
self.multicall_add(m, "d.get_{0}".format(field))
|
||||||
|
setattr(self, field, m.call()[-1])
|
||||||
|
|
||||||
|
return (getattr(self, field))
|
||||||
|
|
||||||
|
def set_custom(self, key, value):
|
||||||
|
"""
|
||||||
|
Set custom value
|
||||||
|
|
||||||
|
@param key: the index for the custom field (between 1-5)
|
||||||
|
@type key: int
|
||||||
|
|
||||||
|
@param value: the value to be stored
|
||||||
|
@type value: str
|
||||||
|
|
||||||
|
@return: if successful, value will be returned
|
||||||
|
@rtype: str
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._assert_custom_key_valid(key)
|
||||||
|
m = rtorrent.rpc.Multicall(self)
|
||||||
|
|
||||||
|
self.multicall_add(m, "d.set_custom{0}".format(key), value)
|
||||||
|
|
||||||
|
return(m.call()[-1])
|
||||||
|
|
||||||
|
def set_visible(self, view, visible=True):
|
||||||
|
p = self._rt_obj._get_conn()
|
||||||
|
|
||||||
|
if visible:
|
||||||
|
return p.view.set_visible(self.info_hash, view)
|
||||||
|
else:
|
||||||
|
return p.view.set_not_visible(self.info_hash, view)
|
||||||
|
|
||||||
|
############################################################################
|
||||||
|
# CUSTOM METHODS (Not part of the official rTorrent API)
|
||||||
|
##########################################################################
|
||||||
|
def _is_hash_checking_queued(self):
|
||||||
|
"""Only checks instance variables, shouldn't be called directly"""
|
||||||
|
# if hashing == 3, then torrent is marked for hash checking
|
||||||
|
# if hash_checking == False, then torrent is waiting to be checked
|
||||||
|
self.hash_checking_queued = (self.hashing == 3 and
|
||||||
|
self.hash_checking is False)
|
||||||
|
|
||||||
|
return(self.hash_checking_queued)
|
||||||
|
|
||||||
|
def is_hash_checking_queued(self):
|
||||||
|
"""Check if torrent is waiting to be hash checked
|
||||||
|
|
||||||
|
@note: Variable where the result for this method is stored Torrent.hash_checking_queued"""
|
||||||
|
m = rtorrent.rpc.Multicall(self)
|
||||||
|
self.multicall_add(m, "d.get_hashing")
|
||||||
|
self.multicall_add(m, "d.is_hash_checking")
|
||||||
|
results = m.call()
|
||||||
|
|
||||||
|
setattr(self, "hashing", results[0])
|
||||||
|
setattr(self, "hash_checking", results[1])
|
||||||
|
|
||||||
|
return(self._is_hash_checking_queued())
|
||||||
|
|
||||||
|
def _is_paused(self):
|
||||||
|
"""Only checks instance variables, shouldn't be called directly"""
|
||||||
|
self.paused = (self.state == 0)
|
||||||
|
return(self.paused)
|
||||||
|
|
||||||
|
def is_paused(self):
|
||||||
|
"""Check if torrent is paused
|
||||||
|
|
||||||
|
@note: Variable where the result for this method is stored: Torrent.paused"""
|
||||||
|
self.get_state()
|
||||||
|
return(self._is_paused())
|
||||||
|
|
||||||
|
def _is_started(self):
|
||||||
|
"""Only checks instance variables, shouldn't be called directly"""
|
||||||
|
self.started = (self.state == 1)
|
||||||
|
return(self.started)
|
||||||
|
|
||||||
|
def is_started(self):
|
||||||
|
"""Check if torrent is started
|
||||||
|
|
||||||
|
@note: Variable where the result for this method is stored: Torrent.started"""
|
||||||
|
self.get_state()
|
||||||
|
return(self._is_started())
|
||||||
|
|
||||||
|
|
||||||
|
methods = [
|
||||||
|
# RETRIEVERS
|
||||||
|
Method(Torrent, 'is_hash_checked', 'd.is_hash_checked',
|
||||||
|
boolean=True,
|
||||||
|
),
|
||||||
|
Method(Torrent, 'is_hash_checking', 'd.is_hash_checking',
|
||||||
|
boolean=True,
|
||||||
|
),
|
||||||
|
Method(Torrent, 'get_peers_max', 'd.get_peers_max'),
|
||||||
|
Method(Torrent, 'get_tracker_focus', 'd.get_tracker_focus'),
|
||||||
|
Method(Torrent, 'get_skip_total', 'd.get_skip_total'),
|
||||||
|
Method(Torrent, 'get_state', 'd.get_state'),
|
||||||
|
Method(Torrent, 'get_peer_exchange', 'd.get_peer_exchange'),
|
||||||
|
Method(Torrent, 'get_down_rate', 'd.get_down_rate'),
|
||||||
|
Method(Torrent, 'get_connection_seed', 'd.get_connection_seed'),
|
||||||
|
Method(Torrent, 'get_uploads_max', 'd.get_uploads_max'),
|
||||||
|
Method(Torrent, 'get_priority_str', 'd.get_priority_str'),
|
||||||
|
Method(Torrent, 'is_open', 'd.is_open',
|
||||||
|
boolean=True,
|
||||||
|
),
|
||||||
|
Method(Torrent, 'get_peers_min', 'd.get_peers_min'),
|
||||||
|
Method(Torrent, 'get_peers_complete', 'd.get_peers_complete'),
|
||||||
|
Method(Torrent, 'get_tracker_numwant', 'd.get_tracker_numwant'),
|
||||||
|
Method(Torrent, 'get_connection_current', 'd.get_connection_current'),
|
||||||
|
Method(Torrent, 'is_complete', 'd.get_complete',
|
||||||
|
boolean=True,
|
||||||
|
),
|
||||||
|
Method(Torrent, 'get_peers_connected', 'd.get_peers_connected'),
|
||||||
|
Method(Torrent, 'get_chunk_size', 'd.get_chunk_size'),
|
||||||
|
Method(Torrent, 'get_state_counter', 'd.get_state_counter'),
|
||||||
|
Method(Torrent, 'get_base_filename', 'd.get_base_filename'),
|
||||||
|
Method(Torrent, 'get_state_changed', 'd.get_state_changed'),
|
||||||
|
Method(Torrent, 'get_peers_not_connected', 'd.get_peers_not_connected'),
|
||||||
|
Method(Torrent, 'get_directory', 'd.get_directory'),
|
||||||
|
Method(Torrent, 'is_incomplete', 'd.incomplete',
|
||||||
|
boolean=True,
|
||||||
|
),
|
||||||
|
Method(Torrent, 'get_tracker_size', 'd.get_tracker_size'),
|
||||||
|
Method(Torrent, 'is_multi_file', 'd.is_multi_file',
|
||||||
|
boolean=True,
|
||||||
|
),
|
||||||
|
Method(Torrent, 'get_local_id', 'd.get_local_id'),
|
||||||
|
Method(Torrent, 'get_ratio', 'd.get_ratio',
|
||||||
|
post_process_func=lambda x: x / 1000.0,
|
||||||
|
),
|
||||||
|
Method(Torrent, 'get_loaded_file', 'd.get_loaded_file'),
|
||||||
|
Method(Torrent, 'get_max_file_size', 'd.get_max_file_size'),
|
||||||
|
Method(Torrent, 'get_size_chunks', 'd.get_size_chunks'),
|
||||||
|
Method(Torrent, 'is_pex_active', 'd.is_pex_active',
|
||||||
|
boolean=True,
|
||||||
|
),
|
||||||
|
Method(Torrent, 'get_hashing', 'd.get_hashing'),
|
||||||
|
Method(Torrent, 'get_bitfield', 'd.get_bitfield'),
|
||||||
|
Method(Torrent, 'get_local_id_html', 'd.get_local_id_html'),
|
||||||
|
Method(Torrent, 'get_connection_leech', 'd.get_connection_leech'),
|
||||||
|
Method(Torrent, 'get_peers_accounted', 'd.get_peers_accounted'),
|
||||||
|
Method(Torrent, 'get_message', 'd.get_message'),
|
||||||
|
Method(Torrent, 'is_active', 'd.is_active',
|
||||||
|
boolean=True,
|
||||||
|
),
|
||||||
|
Method(Torrent, 'get_size_bytes', 'd.get_size_bytes'),
|
||||||
|
Method(Torrent, 'get_ignore_commands', 'd.get_ignore_commands'),
|
||||||
|
Method(Torrent, 'get_creation_date', 'd.get_creation_date'),
|
||||||
|
Method(Torrent, 'get_base_path', 'd.get_base_path'),
|
||||||
|
Method(Torrent, 'get_left_bytes', 'd.get_left_bytes'),
|
||||||
|
Method(Torrent, 'get_size_files', 'd.get_size_files'),
|
||||||
|
Method(Torrent, 'get_size_pex', 'd.get_size_pex'),
|
||||||
|
Method(Torrent, 'is_private', 'd.is_private',
|
||||||
|
boolean=True,
|
||||||
|
),
|
||||||
|
Method(Torrent, 'get_max_size_pex', 'd.get_max_size_pex'),
|
||||||
|
Method(Torrent, 'get_num_chunks_hashed', 'd.get_chunks_hashed',
|
||||||
|
aliases=("get_chunks_hashed",)),
|
||||||
|
Method(Torrent, 'get_num_chunks_wanted', 'd.wanted_chunks'),
|
||||||
|
Method(Torrent, 'get_priority', 'd.get_priority'),
|
||||||
|
Method(Torrent, 'get_skip_rate', 'd.get_skip_rate'),
|
||||||
|
Method(Torrent, 'get_completed_bytes', 'd.get_completed_bytes'),
|
||||||
|
Method(Torrent, 'get_name', 'd.get_name'),
|
||||||
|
Method(Torrent, 'get_completed_chunks', 'd.get_completed_chunks'),
|
||||||
|
Method(Torrent, 'get_throttle_name', 'd.get_throttle_name'),
|
||||||
|
Method(Torrent, 'get_free_diskspace', 'd.get_free_diskspace'),
|
||||||
|
Method(Torrent, 'get_directory_base', 'd.get_directory_base'),
|
||||||
|
Method(Torrent, 'get_hashing_failed', 'd.get_hashing_failed'),
|
||||||
|
Method(Torrent, 'get_tied_to_file', 'd.get_tied_to_file'),
|
||||||
|
Method(Torrent, 'get_down_total', 'd.get_down_total'),
|
||||||
|
Method(Torrent, 'get_bytes_done', 'd.get_bytes_done'),
|
||||||
|
Method(Torrent, 'get_up_rate', 'd.get_up_rate'),
|
||||||
|
Method(Torrent, 'get_up_total', 'd.get_up_total'),
|
||||||
|
Method(Torrent, 'is_accepting_seeders', 'd.accepting_seeders',
|
||||||
|
boolean=True,
|
||||||
|
),
|
||||||
|
Method(Torrent, "get_chunks_seen", "d.chunks_seen",
|
||||||
|
min_version=(0, 9, 1),
|
||||||
|
),
|
||||||
|
Method(Torrent, "is_partially_done", "d.is_partially_done",
|
||||||
|
boolean=True,
|
||||||
|
),
|
||||||
|
Method(Torrent, "is_not_partially_done", "d.is_not_partially_done",
|
||||||
|
boolean=True,
|
||||||
|
),
|
||||||
|
Method(Torrent, "get_time_started", "d.timestamp.started"),
|
||||||
|
Method(Torrent, "get_custom1", "d.get_custom1"),
|
||||||
|
Method(Torrent, "get_custom2", "d.get_custom2"),
|
||||||
|
Method(Torrent, "get_custom3", "d.get_custom3"),
|
||||||
|
Method(Torrent, "get_custom4", "d.get_custom4"),
|
||||||
|
Method(Torrent, "get_custom5", "d.get_custom5"),
|
||||||
|
|
||||||
|
# MODIFIERS
|
||||||
|
Method(Torrent, 'set_uploads_max', 'd.set_uploads_max'),
|
||||||
|
Method(Torrent, 'set_tied_to_file', 'd.set_tied_to_file'),
|
||||||
|
Method(Torrent, 'set_tracker_numwant', 'd.set_tracker_numwant'),
|
||||||
|
Method(Torrent, 'set_priority', 'd.set_priority'),
|
||||||
|
Method(Torrent, 'set_peers_max', 'd.set_peers_max'),
|
||||||
|
Method(Torrent, 'set_hashing_failed', 'd.set_hashing_failed'),
|
||||||
|
Method(Torrent, 'set_message', 'd.set_message'),
|
||||||
|
Method(Torrent, 'set_throttle_name', 'd.set_throttle_name'),
|
||||||
|
Method(Torrent, 'set_peers_min', 'd.set_peers_min'),
|
||||||
|
Method(Torrent, 'set_ignore_commands', 'd.set_ignore_commands'),
|
||||||
|
Method(Torrent, 'set_max_file_size', 'd.set_max_file_size'),
|
||||||
|
Method(Torrent, 'set_custom5', 'd.set_custom5'),
|
||||||
|
Method(Torrent, 'set_custom4', 'd.set_custom4'),
|
||||||
|
Method(Torrent, 'set_custom2', 'd.set_custom2'),
|
||||||
|
Method(Torrent, 'set_custom1', 'd.set_custom1'),
|
||||||
|
Method(Torrent, 'set_custom3', 'd.set_custom3'),
|
||||||
|
Method(Torrent, 'set_connection_current', 'd.set_connection_current'),
|
||||||
|
]
|
||||||
Executable
+138
@@ -0,0 +1,138 @@
|
|||||||
|
# Copyright (c) 2013 Chris Lucas, <chris@chrisjlucas.com>
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
# a copy of this software and associated documentation files (the
|
||||||
|
# "Software"), to deal in the Software without restriction, including
|
||||||
|
# without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
# permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
# the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be
|
||||||
|
# included in all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
# from rtorrent.rpc import Method
|
||||||
|
import rtorrent.rpc
|
||||||
|
|
||||||
|
from rtorrent.common import safe_repr
|
||||||
|
|
||||||
|
Method = rtorrent.rpc.Method
|
||||||
|
|
||||||
|
|
||||||
|
class Tracker:
|
||||||
|
"""Represents an individual tracker within a L{Torrent} instance."""
|
||||||
|
|
||||||
|
def __init__(self, _rt_obj, info_hash, **kwargs):
|
||||||
|
self._rt_obj = _rt_obj
|
||||||
|
self.info_hash = info_hash # : info hash for the torrent using this tracker
|
||||||
|
for k in kwargs.keys():
|
||||||
|
setattr(self, k, kwargs.get(k, None))
|
||||||
|
|
||||||
|
# for clarity's sake...
|
||||||
|
self.index = self.group # : position of tracker within the torrent's tracker list
|
||||||
|
self.rpc_id = "{0}:t{1}".format(
|
||||||
|
self.info_hash, self.index) # : unique id to pass to rTorrent
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return safe_repr("Tracker(index={0}, url=\"{1}\")",
|
||||||
|
self.index, self.url)
|
||||||
|
|
||||||
|
def enable(self):
|
||||||
|
"""Alias for set_enabled("yes")"""
|
||||||
|
self.set_enabled("yes")
|
||||||
|
|
||||||
|
def disable(self):
|
||||||
|
"""Alias for set_enabled("no")"""
|
||||||
|
self.set_enabled("no")
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Refresh tracker data
|
||||||
|
|
||||||
|
@note: All fields are stored as attributes to self.
|
||||||
|
|
||||||
|
@return: None
|
||||||
|
"""
|
||||||
|
multicall = rtorrent.rpc.Multicall(self)
|
||||||
|
retriever_methods = [m for m in methods
|
||||||
|
if m.is_retriever() and m.is_available(self._rt_obj)]
|
||||||
|
for method in retriever_methods:
|
||||||
|
multicall.add(method, self.rpc_id)
|
||||||
|
|
||||||
|
multicall.call()
|
||||||
|
|
||||||
|
methods = [
|
||||||
|
# RETRIEVERS
|
||||||
|
Method(Tracker, 'is_enabled', 't.is_enabled', boolean=True),
|
||||||
|
Method(Tracker, 'get_id', 't.get_id'),
|
||||||
|
Method(Tracker, 'get_scrape_incomplete', 't.get_scrape_incomplete'),
|
||||||
|
Method(Tracker, 'is_open', 't.is_open', boolean=True),
|
||||||
|
Method(Tracker, 'get_min_interval', 't.get_min_interval'),
|
||||||
|
Method(Tracker, 'get_scrape_downloaded', 't.get_scrape_downloaded'),
|
||||||
|
Method(Tracker, 'get_group', 't.get_group'),
|
||||||
|
Method(Tracker, 'get_scrape_time_last', 't.get_scrape_time_last'),
|
||||||
|
Method(Tracker, 'get_type', 't.get_type'),
|
||||||
|
Method(Tracker, 'get_normal_interval', 't.get_normal_interval'),
|
||||||
|
Method(Tracker, 'get_url', 't.get_url'),
|
||||||
|
Method(Tracker, 'get_scrape_complete', 't.get_scrape_complete',
|
||||||
|
min_version=(0, 8, 9),
|
||||||
|
),
|
||||||
|
Method(Tracker, 'get_activity_time_last', 't.activity_time_last',
|
||||||
|
min_version=(0, 8, 9),
|
||||||
|
),
|
||||||
|
Method(Tracker, 'get_activity_time_next', 't.activity_time_next',
|
||||||
|
min_version=(0, 8, 9),
|
||||||
|
),
|
||||||
|
Method(Tracker, 'get_failed_time_last', 't.failed_time_last',
|
||||||
|
min_version=(0, 8, 9),
|
||||||
|
),
|
||||||
|
Method(Tracker, 'get_failed_time_next', 't.failed_time_next',
|
||||||
|
min_version=(0, 8, 9),
|
||||||
|
),
|
||||||
|
Method(Tracker, 'get_success_time_last', 't.success_time_last',
|
||||||
|
min_version=(0, 8, 9),
|
||||||
|
),
|
||||||
|
Method(Tracker, 'get_success_time_next', 't.success_time_next',
|
||||||
|
min_version=(0, 8, 9),
|
||||||
|
),
|
||||||
|
Method(Tracker, 'can_scrape', 't.can_scrape',
|
||||||
|
min_version=(0, 9, 1),
|
||||||
|
boolean=True
|
||||||
|
),
|
||||||
|
Method(Tracker, 'get_failed_counter', 't.failed_counter',
|
||||||
|
min_version=(0, 8, 9)
|
||||||
|
),
|
||||||
|
Method(Tracker, 'get_scrape_counter', 't.scrape_counter',
|
||||||
|
min_version=(0, 8, 9)
|
||||||
|
),
|
||||||
|
Method(Tracker, 'get_success_counter', 't.success_counter',
|
||||||
|
min_version=(0, 8, 9)
|
||||||
|
),
|
||||||
|
Method(Tracker, 'is_usable', 't.is_usable',
|
||||||
|
min_version=(0, 9, 1),
|
||||||
|
boolean=True
|
||||||
|
),
|
||||||
|
Method(Tracker, 'is_busy', 't.is_busy',
|
||||||
|
min_version=(0, 9, 1),
|
||||||
|
boolean=True
|
||||||
|
),
|
||||||
|
Method(Tracker, 'is_extra_tracker', 't.is_extra_tracker',
|
||||||
|
min_version=(0, 9, 1),
|
||||||
|
boolean=True,
|
||||||
|
),
|
||||||
|
Method(Tracker, "get_latest_sum_peers", "t.latest_sum_peers",
|
||||||
|
min_version=(0, 9, 0)
|
||||||
|
),
|
||||||
|
Method(Tracker, "get_latest_new_peers", "t.latest_new_peers",
|
||||||
|
min_version=(0, 9, 0)
|
||||||
|
),
|
||||||
|
|
||||||
|
# MODIFIERS
|
||||||
|
Method(Tracker, 'set_enabled', 't.set_enabled'),
|
||||||
|
]
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
"""A synchronous implementation of the Deluge RPC protocol
|
||||||
|
based on gevent-deluge by Christopher Rosell.
|
||||||
|
|
||||||
|
https://github.com/chrippa/gevent-deluge
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
from synchronousdeluge import DelgueClient
|
||||||
|
|
||||||
|
client = DelugeClient()
|
||||||
|
client.connect()
|
||||||
|
|
||||||
|
# Wait for value
|
||||||
|
download_location = client.core.get_config_value("download_location").get()
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
__title__ = "synchronous-deluge"
|
||||||
|
__version__ = "0.1"
|
||||||
|
__author__ = "Christian Dale"
|
||||||
|
|
||||||
|
from synchronousdeluge.client import DelugeClient
|
||||||
|
from synchronousdeluge.exceptions import DelugeRPCError
|
||||||
|
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
|
from itertools import imap
|
||||||
|
|
||||||
|
from synchronousdeluge.exceptions import DelugeRPCError
|
||||||
|
from synchronousdeluge.protocol import DelugeRPCRequest, DelugeRPCResponse
|
||||||
|
from synchronousdeluge.transfer import DelugeTransfer
|
||||||
|
|
||||||
|
__all__ = ["DelugeClient"]
|
||||||
|
|
||||||
|
|
||||||
|
RPC_RESPONSE = 1
|
||||||
|
RPC_ERROR = 2
|
||||||
|
RPC_EVENT = 3
|
||||||
|
|
||||||
|
|
||||||
|
class DelugeClient(object):
|
||||||
|
def __init__(self):
|
||||||
|
"""A deluge client session."""
|
||||||
|
self.transfer = DelugeTransfer()
|
||||||
|
self.modules = []
|
||||||
|
self._request_counter = 0
|
||||||
|
|
||||||
|
def _get_local_auth(self):
|
||||||
|
xdg_config = os.path.expanduser(os.environ.get("XDG_CONFIG_HOME", "~/.config"))
|
||||||
|
config_home = os.path.join(xdg_config, "deluge")
|
||||||
|
auth_file = os.path.join(config_home, "auth")
|
||||||
|
|
||||||
|
username = password = ""
|
||||||
|
with open(auth_file) as fd:
|
||||||
|
for line in fd:
|
||||||
|
if line.startswith("#"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
auth = line.split(":")
|
||||||
|
if len(auth) >= 2 and auth[0] == "localclient":
|
||||||
|
username, password = auth[0], auth[1]
|
||||||
|
break
|
||||||
|
|
||||||
|
return username, password
|
||||||
|
|
||||||
|
def _create_module_method(self, module, method):
|
||||||
|
fullname = "{0}.{1}".format(module, method)
|
||||||
|
|
||||||
|
def func(obj, *args, **kwargs):
|
||||||
|
return self.remote_call(fullname, *args, **kwargs)
|
||||||
|
|
||||||
|
func.__name__ = method
|
||||||
|
|
||||||
|
return func
|
||||||
|
|
||||||
|
def _introspect(self):
|
||||||
|
self.modules = []
|
||||||
|
|
||||||
|
methods = self.remote_call("daemon.get_method_list").get()
|
||||||
|
methodmap = defaultdict(dict)
|
||||||
|
splitter = lambda v: v.split(".")
|
||||||
|
|
||||||
|
for module, method in imap(splitter, methods):
|
||||||
|
methodmap[module][method] = self._create_module_method(module, method)
|
||||||
|
|
||||||
|
for module, methods in methodmap.items():
|
||||||
|
clsname = "DelugeModule{0}".format(module.capitalize())
|
||||||
|
cls = type(clsname, (), methods)
|
||||||
|
setattr(self, module, cls())
|
||||||
|
self.modules.append(module)
|
||||||
|
|
||||||
|
def remote_call(self, method, *args, **kwargs):
|
||||||
|
req = DelugeRPCRequest(self._request_counter, method, *args, **kwargs)
|
||||||
|
message = next(self.transfer.send_request(req))
|
||||||
|
|
||||||
|
response = DelugeRPCResponse()
|
||||||
|
|
||||||
|
if not isinstance(message, tuple):
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(message) < 3:
|
||||||
|
return
|
||||||
|
|
||||||
|
message_type = message[0]
|
||||||
|
|
||||||
|
# if message_type == RPC_EVENT:
|
||||||
|
# event = message[1]
|
||||||
|
# values = message[2]
|
||||||
|
#
|
||||||
|
# if event in self._event_handlers:
|
||||||
|
# for handler in self._event_handlers[event]:
|
||||||
|
# gevent.spawn(handler, *values)
|
||||||
|
#
|
||||||
|
# elif message_type in (RPC_RESPONSE, RPC_ERROR):
|
||||||
|
if message_type in (RPC_RESPONSE, RPC_ERROR):
|
||||||
|
request_id = message[1]
|
||||||
|
value = message[2]
|
||||||
|
|
||||||
|
if request_id == self._request_counter :
|
||||||
|
if message_type == RPC_RESPONSE:
|
||||||
|
response.set(value)
|
||||||
|
elif message_type == RPC_ERROR:
|
||||||
|
err = DelugeRPCError(*value)
|
||||||
|
response.set_exception(err)
|
||||||
|
|
||||||
|
self._request_counter += 1
|
||||||
|
return response
|
||||||
|
|
||||||
|
def connect(self, host="127.0.0.1", port=58846, username="", password=""):
|
||||||
|
"""Connects to a daemon process.
|
||||||
|
|
||||||
|
:param host: str, the hostname of the daemon
|
||||||
|
:param port: int, the port of the daemon
|
||||||
|
:param username: str, the username to login with
|
||||||
|
:param password: str, the password to login with
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Connect transport
|
||||||
|
self.transfer.connect((host, port))
|
||||||
|
|
||||||
|
# Attempt to fetch local auth info if needed
|
||||||
|
if not username and host in ("127.0.0.1", "localhost"):
|
||||||
|
username, password = self._get_local_auth()
|
||||||
|
|
||||||
|
# Authenticate
|
||||||
|
self.remote_call("daemon.login", username, password).get()
|
||||||
|
|
||||||
|
# Introspect available methods
|
||||||
|
self._introspect()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def connected(self):
|
||||||
|
return self.transfer.connected
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
"""Disconnects from the daemon."""
|
||||||
|
self.transfer.disconnect()
|
||||||
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
__all__ = ["DelugeRPCError"]
|
||||||
|
|
||||||
|
class DelugeRPCError(Exception):
|
||||||
|
def __init__(self, name, msg, traceback):
|
||||||
|
self.name = name
|
||||||
|
self.msg = msg
|
||||||
|
self.traceback = traceback
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "{0}: {1}: {2}".format(self.__class__.__name__, self.name, self.msg)
|
||||||
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
__all__ = ["DelugeRPCRequest", "DelugeRPCResponse"]
|
||||||
|
|
||||||
|
class DelugeRPCRequest(object):
|
||||||
|
def __init__(self, request_id, method, *args, **kwargs):
|
||||||
|
self.request_id = request_id
|
||||||
|
self.method = method
|
||||||
|
self.args = args
|
||||||
|
self.kwargs = kwargs
|
||||||
|
|
||||||
|
def format(self):
|
||||||
|
return (self.request_id, self.method, self.args, self.kwargs)
|
||||||
|
|
||||||
|
class DelugeRPCResponse(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.value = None
|
||||||
|
self._exception = None
|
||||||
|
|
||||||
|
def successful(self):
|
||||||
|
return self._exception is None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def exception(self):
|
||||||
|
if self._exception is not None:
|
||||||
|
return self._exception
|
||||||
|
|
||||||
|
def set(self, value=None):
|
||||||
|
self.value = value
|
||||||
|
self._exception = None
|
||||||
|
|
||||||
|
def set_exception(self, exception):
|
||||||
|
self._exception = exception
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
if self._exception is None:
|
||||||
|
return self.value
|
||||||
|
else:
|
||||||
|
raise self._exception
|
||||||
|
|
||||||
@@ -0,0 +1,433 @@
|
|||||||
|
|
||||||
|
"""
|
||||||
|
rencode -- Web safe object pickling/unpickling.
|
||||||
|
|
||||||
|
Public domain, Connelly Barnes 2006-2007.
|
||||||
|
|
||||||
|
The rencode module is a modified version of bencode from the
|
||||||
|
BitTorrent project. For complex, heterogeneous data structures with
|
||||||
|
many small elements, r-encodings take up significantly less space than
|
||||||
|
b-encodings:
|
||||||
|
|
||||||
|
>>> len(rencode.dumps({'a':0, 'b':[1,2], 'c':99}))
|
||||||
|
13
|
||||||
|
>>> len(bencode.bencode({'a':0, 'b':[1,2], 'c':99}))
|
||||||
|
26
|
||||||
|
|
||||||
|
The rencode format is not standardized, and may change with different
|
||||||
|
rencode module versions, so you should check that you are using the
|
||||||
|
same rencode version throughout your project.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__version__ = '1.0.1'
|
||||||
|
__all__ = ['dumps', 'loads']
|
||||||
|
|
||||||
|
# Original bencode module by Petru Paler, et al.
|
||||||
|
#
|
||||||
|
# Modifications by Connelly Barnes:
|
||||||
|
#
|
||||||
|
# - Added support for floats (sent as 32-bit or 64-bit in network
|
||||||
|
# order), bools, None.
|
||||||
|
# - Allowed dict keys to be of any serializable type.
|
||||||
|
# - Lists/tuples are always decoded as tuples (thus, tuples can be
|
||||||
|
# used as dict keys).
|
||||||
|
# - Embedded extra information in the 'typecodes' to save some space.
|
||||||
|
# - Added a restriction on integer length, so that malicious hosts
|
||||||
|
# cannot pass us large integers which take a long time to decode.
|
||||||
|
#
|
||||||
|
# Licensed by Bram Cohen under the "MIT license":
|
||||||
|
#
|
||||||
|
# "Copyright (C) 2001-2002 Bram Cohen
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person
|
||||||
|
# obtaining a copy of this software and associated documentation files
|
||||||
|
# (the "Software"), to deal in the Software without restriction,
|
||||||
|
# including without limitation the rights to use, copy, modify, merge,
|
||||||
|
# publish, distribute, sublicense, and/or sell copies of the Software,
|
||||||
|
# and to permit persons to whom the Software is furnished to do so,
|
||||||
|
# subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be
|
||||||
|
# included in all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# The Software is provided "AS IS", without warranty of any kind,
|
||||||
|
# express or implied, including but not limited to the warranties of
|
||||||
|
# merchantability, fitness for a particular purpose and
|
||||||
|
# noninfringement. In no event shall the authors or copyright holders
|
||||||
|
# be liable for any claim, damages or other liability, whether in an
|
||||||
|
# action of contract, tort or otherwise, arising from, out of or in
|
||||||
|
# connection with the Software or the use or other dealings in the
|
||||||
|
# Software."
|
||||||
|
#
|
||||||
|
# (The rencode module is licensed under the above license as well).
|
||||||
|
#
|
||||||
|
|
||||||
|
import struct
|
||||||
|
import string
|
||||||
|
from threading import Lock
|
||||||
|
|
||||||
|
# Default number of bits for serialized floats, either 32 or 64 (also a parameter for dumps()).
|
||||||
|
DEFAULT_FLOAT_BITS = 32
|
||||||
|
|
||||||
|
# Maximum length of integer when written as base 10 string.
|
||||||
|
MAX_INT_LENGTH = 64
|
||||||
|
|
||||||
|
# The bencode 'typecodes' such as i, d, etc have been extended and
|
||||||
|
# relocated on the base-256 character set.
|
||||||
|
CHR_LIST = chr(59)
|
||||||
|
CHR_DICT = chr(60)
|
||||||
|
CHR_INT = chr(61)
|
||||||
|
CHR_INT1 = chr(62)
|
||||||
|
CHR_INT2 = chr(63)
|
||||||
|
CHR_INT4 = chr(64)
|
||||||
|
CHR_INT8 = chr(65)
|
||||||
|
CHR_FLOAT32 = chr(66)
|
||||||
|
CHR_FLOAT64 = chr(44)
|
||||||
|
CHR_TRUE = chr(67)
|
||||||
|
CHR_FALSE = chr(68)
|
||||||
|
CHR_NONE = chr(69)
|
||||||
|
CHR_TERM = chr(127)
|
||||||
|
|
||||||
|
# Positive integers with value embedded in typecode.
|
||||||
|
INT_POS_FIXED_START = 0
|
||||||
|
INT_POS_FIXED_COUNT = 44
|
||||||
|
|
||||||
|
# Dictionaries with length embedded in typecode.
|
||||||
|
DICT_FIXED_START = 102
|
||||||
|
DICT_FIXED_COUNT = 25
|
||||||
|
|
||||||
|
# Negative integers with value embedded in typecode.
|
||||||
|
INT_NEG_FIXED_START = 70
|
||||||
|
INT_NEG_FIXED_COUNT = 32
|
||||||
|
|
||||||
|
# Strings with length embedded in typecode.
|
||||||
|
STR_FIXED_START = 128
|
||||||
|
STR_FIXED_COUNT = 64
|
||||||
|
|
||||||
|
# Lists with length embedded in typecode.
|
||||||
|
LIST_FIXED_START = STR_FIXED_START+STR_FIXED_COUNT
|
||||||
|
LIST_FIXED_COUNT = 64
|
||||||
|
|
||||||
|
def decode_int(x, f):
|
||||||
|
f += 1
|
||||||
|
newf = x.index(CHR_TERM, f)
|
||||||
|
if newf - f >= MAX_INT_LENGTH:
|
||||||
|
raise ValueError('overflow')
|
||||||
|
try:
|
||||||
|
n = int(x[f:newf])
|
||||||
|
except (OverflowError, ValueError):
|
||||||
|
n = long(x[f:newf])
|
||||||
|
if x[f] == '-':
|
||||||
|
if x[f + 1] == '0':
|
||||||
|
raise ValueError
|
||||||
|
elif x[f] == '0' and newf != f+1:
|
||||||
|
raise ValueError
|
||||||
|
return (n, newf+1)
|
||||||
|
|
||||||
|
def decode_intb(x, f):
|
||||||
|
f += 1
|
||||||
|
return (struct.unpack('!b', x[f:f+1])[0], f+1)
|
||||||
|
|
||||||
|
def decode_inth(x, f):
|
||||||
|
f += 1
|
||||||
|
return (struct.unpack('!h', x[f:f+2])[0], f+2)
|
||||||
|
|
||||||
|
def decode_intl(x, f):
|
||||||
|
f += 1
|
||||||
|
return (struct.unpack('!l', x[f:f+4])[0], f+4)
|
||||||
|
|
||||||
|
def decode_intq(x, f):
|
||||||
|
f += 1
|
||||||
|
return (struct.unpack('!q', x[f:f+8])[0], f+8)
|
||||||
|
|
||||||
|
def decode_float32(x, f):
|
||||||
|
f += 1
|
||||||
|
n = struct.unpack('!f', x[f:f+4])[0]
|
||||||
|
return (n, f+4)
|
||||||
|
|
||||||
|
def decode_float64(x, f):
|
||||||
|
f += 1
|
||||||
|
n = struct.unpack('!d', x[f:f+8])[0]
|
||||||
|
return (n, f+8)
|
||||||
|
|
||||||
|
def decode_string(x, f):
|
||||||
|
colon = x.index(':', f)
|
||||||
|
try:
|
||||||
|
n = int(x[f:colon])
|
||||||
|
except (OverflowError, ValueError):
|
||||||
|
n = long(x[f:colon])
|
||||||
|
if x[f] == '0' and colon != f+1:
|
||||||
|
raise ValueError
|
||||||
|
colon += 1
|
||||||
|
s = x[colon:colon+n]
|
||||||
|
try:
|
||||||
|
t = s.decode("utf8")
|
||||||
|
if len(t) != len(s):
|
||||||
|
s = t
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
pass
|
||||||
|
return (s, colon+n)
|
||||||
|
|
||||||
|
def decode_list(x, f):
|
||||||
|
r, f = [], f+1
|
||||||
|
while x[f] != CHR_TERM:
|
||||||
|
v, f = decode_func[x[f]](x, f)
|
||||||
|
r.append(v)
|
||||||
|
return (tuple(r), f + 1)
|
||||||
|
|
||||||
|
def decode_dict(x, f):
|
||||||
|
r, f = {}, f+1
|
||||||
|
while x[f] != CHR_TERM:
|
||||||
|
k, f = decode_func[x[f]](x, f)
|
||||||
|
r[k], f = decode_func[x[f]](x, f)
|
||||||
|
return (r, f + 1)
|
||||||
|
|
||||||
|
def decode_true(x, f):
|
||||||
|
return (True, f+1)
|
||||||
|
|
||||||
|
def decode_false(x, f):
|
||||||
|
return (False, f+1)
|
||||||
|
|
||||||
|
def decode_none(x, f):
|
||||||
|
return (None, f+1)
|
||||||
|
|
||||||
|
decode_func = {}
|
||||||
|
decode_func['0'] = decode_string
|
||||||
|
decode_func['1'] = decode_string
|
||||||
|
decode_func['2'] = decode_string
|
||||||
|
decode_func['3'] = decode_string
|
||||||
|
decode_func['4'] = decode_string
|
||||||
|
decode_func['5'] = decode_string
|
||||||
|
decode_func['6'] = decode_string
|
||||||
|
decode_func['7'] = decode_string
|
||||||
|
decode_func['8'] = decode_string
|
||||||
|
decode_func['9'] = decode_string
|
||||||
|
decode_func[CHR_LIST ] = decode_list
|
||||||
|
decode_func[CHR_DICT ] = decode_dict
|
||||||
|
decode_func[CHR_INT ] = decode_int
|
||||||
|
decode_func[CHR_INT1 ] = decode_intb
|
||||||
|
decode_func[CHR_INT2 ] = decode_inth
|
||||||
|
decode_func[CHR_INT4 ] = decode_intl
|
||||||
|
decode_func[CHR_INT8 ] = decode_intq
|
||||||
|
decode_func[CHR_FLOAT32] = decode_float32
|
||||||
|
decode_func[CHR_FLOAT64] = decode_float64
|
||||||
|
decode_func[CHR_TRUE ] = decode_true
|
||||||
|
decode_func[CHR_FALSE ] = decode_false
|
||||||
|
decode_func[CHR_NONE ] = decode_none
|
||||||
|
|
||||||
|
def make_fixed_length_string_decoders():
|
||||||
|
def make_decoder(slen):
|
||||||
|
def f(x, f):
|
||||||
|
s = x[f+1:f+1+slen]
|
||||||
|
try:
|
||||||
|
t = s.decode("utf8")
|
||||||
|
if len(t) != len(s):
|
||||||
|
s = t
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
pass
|
||||||
|
return (s, f+1+slen)
|
||||||
|
return f
|
||||||
|
for i in range(STR_FIXED_COUNT):
|
||||||
|
decode_func[chr(STR_FIXED_START+i)] = make_decoder(i)
|
||||||
|
|
||||||
|
make_fixed_length_string_decoders()
|
||||||
|
|
||||||
|
def make_fixed_length_list_decoders():
|
||||||
|
def make_decoder(slen):
|
||||||
|
def f(x, f):
|
||||||
|
r, f = [], f+1
|
||||||
|
for i in range(slen):
|
||||||
|
v, f = decode_func[x[f]](x, f)
|
||||||
|
r.append(v)
|
||||||
|
return (tuple(r), f)
|
||||||
|
return f
|
||||||
|
for i in range(LIST_FIXED_COUNT):
|
||||||
|
decode_func[chr(LIST_FIXED_START+i)] = make_decoder(i)
|
||||||
|
|
||||||
|
make_fixed_length_list_decoders()
|
||||||
|
|
||||||
|
def make_fixed_length_int_decoders():
|
||||||
|
def make_decoder(j):
|
||||||
|
def f(x, f):
|
||||||
|
return (j, f+1)
|
||||||
|
return f
|
||||||
|
for i in range(INT_POS_FIXED_COUNT):
|
||||||
|
decode_func[chr(INT_POS_FIXED_START+i)] = make_decoder(i)
|
||||||
|
for i in range(INT_NEG_FIXED_COUNT):
|
||||||
|
decode_func[chr(INT_NEG_FIXED_START+i)] = make_decoder(-1-i)
|
||||||
|
|
||||||
|
make_fixed_length_int_decoders()
|
||||||
|
|
||||||
|
def make_fixed_length_dict_decoders():
|
||||||
|
def make_decoder(slen):
|
||||||
|
def f(x, f):
|
||||||
|
r, f = {}, f+1
|
||||||
|
for j in range(slen):
|
||||||
|
k, f = decode_func[x[f]](x, f)
|
||||||
|
r[k], f = decode_func[x[f]](x, f)
|
||||||
|
return (r, f)
|
||||||
|
return f
|
||||||
|
for i in range(DICT_FIXED_COUNT):
|
||||||
|
decode_func[chr(DICT_FIXED_START+i)] = make_decoder(i)
|
||||||
|
|
||||||
|
make_fixed_length_dict_decoders()
|
||||||
|
|
||||||
|
def encode_dict(x,r):
|
||||||
|
r.append(CHR_DICT)
|
||||||
|
for k, v in x.items():
|
||||||
|
encode_func[type(k)](k, r)
|
||||||
|
encode_func[type(v)](v, r)
|
||||||
|
r.append(CHR_TERM)
|
||||||
|
|
||||||
|
|
||||||
|
def loads(x):
|
||||||
|
try:
|
||||||
|
r, l = decode_func[x[0]](x, 0)
|
||||||
|
except (IndexError, KeyError):
|
||||||
|
raise ValueError
|
||||||
|
if l != len(x):
|
||||||
|
raise ValueError
|
||||||
|
return r
|
||||||
|
|
||||||
|
from types import StringType, IntType, LongType, DictType, ListType, TupleType, FloatType, NoneType, UnicodeType
|
||||||
|
|
||||||
|
def encode_int(x, r):
|
||||||
|
if 0 <= x < INT_POS_FIXED_COUNT:
|
||||||
|
r.append(chr(INT_POS_FIXED_START+x))
|
||||||
|
elif -INT_NEG_FIXED_COUNT <= x < 0:
|
||||||
|
r.append(chr(INT_NEG_FIXED_START-1-x))
|
||||||
|
elif -128 <= x < 128:
|
||||||
|
r.extend((CHR_INT1, struct.pack('!b', x)))
|
||||||
|
elif -32768 <= x < 32768:
|
||||||
|
r.extend((CHR_INT2, struct.pack('!h', x)))
|
||||||
|
elif -2147483648 <= x < 2147483648:
|
||||||
|
r.extend((CHR_INT4, struct.pack('!l', x)))
|
||||||
|
elif -9223372036854775808 <= x < 9223372036854775808:
|
||||||
|
r.extend((CHR_INT8, struct.pack('!q', x)))
|
||||||
|
else:
|
||||||
|
s = str(x)
|
||||||
|
if len(s) >= MAX_INT_LENGTH:
|
||||||
|
raise ValueError('overflow')
|
||||||
|
r.extend((CHR_INT, s, CHR_TERM))
|
||||||
|
|
||||||
|
def encode_float32(x, r):
|
||||||
|
r.extend((CHR_FLOAT32, struct.pack('!f', x)))
|
||||||
|
|
||||||
|
def encode_float64(x, r):
|
||||||
|
r.extend((CHR_FLOAT64, struct.pack('!d', x)))
|
||||||
|
|
||||||
|
def encode_bool(x, r):
|
||||||
|
r.extend({False: CHR_FALSE, True: CHR_TRUE}[bool(x)])
|
||||||
|
|
||||||
|
def encode_none(x, r):
|
||||||
|
r.extend(CHR_NONE)
|
||||||
|
|
||||||
|
def encode_string(x, r):
|
||||||
|
if len(x) < STR_FIXED_COUNT:
|
||||||
|
r.extend((chr(STR_FIXED_START + len(x)), x))
|
||||||
|
else:
|
||||||
|
r.extend((str(len(x)), ':', x))
|
||||||
|
|
||||||
|
def encode_unicode(x, r):
|
||||||
|
encode_string(x.encode("utf8"), r)
|
||||||
|
|
||||||
|
def encode_list(x, r):
|
||||||
|
if len(x) < LIST_FIXED_COUNT:
|
||||||
|
r.append(chr(LIST_FIXED_START + len(x)))
|
||||||
|
for i in x:
|
||||||
|
encode_func[type(i)](i, r)
|
||||||
|
else:
|
||||||
|
r.append(CHR_LIST)
|
||||||
|
for i in x:
|
||||||
|
encode_func[type(i)](i, r)
|
||||||
|
r.append(CHR_TERM)
|
||||||
|
|
||||||
|
def encode_dict(x,r):
|
||||||
|
if len(x) < DICT_FIXED_COUNT:
|
||||||
|
r.append(chr(DICT_FIXED_START + len(x)))
|
||||||
|
for k, v in x.items():
|
||||||
|
encode_func[type(k)](k, r)
|
||||||
|
encode_func[type(v)](v, r)
|
||||||
|
else:
|
||||||
|
r.append(CHR_DICT)
|
||||||
|
for k, v in x.items():
|
||||||
|
encode_func[type(k)](k, r)
|
||||||
|
encode_func[type(v)](v, r)
|
||||||
|
r.append(CHR_TERM)
|
||||||
|
|
||||||
|
encode_func = {}
|
||||||
|
encode_func[IntType] = encode_int
|
||||||
|
encode_func[LongType] = encode_int
|
||||||
|
encode_func[StringType] = encode_string
|
||||||
|
encode_func[ListType] = encode_list
|
||||||
|
encode_func[TupleType] = encode_list
|
||||||
|
encode_func[DictType] = encode_dict
|
||||||
|
encode_func[NoneType] = encode_none
|
||||||
|
encode_func[UnicodeType] = encode_unicode
|
||||||
|
|
||||||
|
lock = Lock()
|
||||||
|
|
||||||
|
try:
|
||||||
|
from types import BooleanType
|
||||||
|
encode_func[BooleanType] = encode_bool
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def dumps(x, float_bits=DEFAULT_FLOAT_BITS):
|
||||||
|
"""
|
||||||
|
Dump data structure to str.
|
||||||
|
|
||||||
|
Here float_bits is either 32 or 64.
|
||||||
|
"""
|
||||||
|
lock.acquire()
|
||||||
|
try:
|
||||||
|
if float_bits == 32:
|
||||||
|
encode_func[FloatType] = encode_float32
|
||||||
|
elif float_bits == 64:
|
||||||
|
encode_func[FloatType] = encode_float64
|
||||||
|
else:
|
||||||
|
raise ValueError('Float bits (%d) is not 32 or 64' % float_bits)
|
||||||
|
r = []
|
||||||
|
encode_func[type(x)](x, r)
|
||||||
|
finally:
|
||||||
|
lock.release()
|
||||||
|
return ''.join(r)
|
||||||
|
|
||||||
|
def test():
|
||||||
|
f1 = struct.unpack('!f', struct.pack('!f', 25.5))[0]
|
||||||
|
f2 = struct.unpack('!f', struct.pack('!f', 29.3))[0]
|
||||||
|
f3 = struct.unpack('!f', struct.pack('!f', -0.6))[0]
|
||||||
|
L = (({'a':15, 'bb':f1, 'ccc':f2, '':(f3,(),False,True,'')},('a',10**20),tuple(range(-100000,100000)),'b'*31,'b'*62,'b'*64,2**30,2**33,2**62,2**64,2**30,2**33,2**62,2**64,False,False, True, -1, 2, 0),)
|
||||||
|
assert loads(dumps(L)) == L
|
||||||
|
d = dict(zip(range(-100000,100000),range(-100000,100000)))
|
||||||
|
d.update({'a':20, 20:40, 40:41, f1:f2, f2:f3, f3:False, False:True, True:False})
|
||||||
|
L = (d, {}, {5:6}, {7:7,True:8}, {9:10, 22:39, 49:50, 44: ''})
|
||||||
|
assert loads(dumps(L)) == L
|
||||||
|
L = ('', 'a'*10, 'a'*100, 'a'*1000, 'a'*10000, 'a'*100000, 'a'*1000000, 'a'*10000000)
|
||||||
|
assert loads(dumps(L)) == L
|
||||||
|
L = tuple([dict(zip(range(n),range(n))) for n in range(100)]) + ('b',)
|
||||||
|
assert loads(dumps(L)) == L
|
||||||
|
L = tuple([dict(zip(range(n),range(-n,0))) for n in range(100)]) + ('b',)
|
||||||
|
assert loads(dumps(L)) == L
|
||||||
|
L = tuple([tuple(range(n)) for n in range(100)]) + ('b',)
|
||||||
|
assert loads(dumps(L)) == L
|
||||||
|
L = tuple(['a'*n for n in range(1000)]) + ('b',)
|
||||||
|
assert loads(dumps(L)) == L
|
||||||
|
L = tuple(['a'*n for n in range(1000)]) + (None,True,None)
|
||||||
|
assert loads(dumps(L)) == L
|
||||||
|
assert loads(dumps(None)) == None
|
||||||
|
assert loads(dumps({None:None})) == {None:None}
|
||||||
|
assert 1e-10<abs(loads(dumps(1.1))-1.1)<1e-6
|
||||||
|
assert 1e-10<abs(loads(dumps(1.1,32))-1.1)<1e-6
|
||||||
|
assert abs(loads(dumps(1.1,64))-1.1)<1e-12
|
||||||
|
assert loads(dumps(u"Hello World!!"))
|
||||||
|
try:
|
||||||
|
import psyco
|
||||||
|
psyco.bind(dumps)
|
||||||
|
psyco.bind(loads)
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
test()
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import zlib
|
||||||
|
import struct
|
||||||
|
import socket
|
||||||
|
import ssl
|
||||||
|
|
||||||
|
from synchronousdeluge import rencode
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["DelugeTransfer"]
|
||||||
|
|
||||||
|
class DelugeTransfer(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.sock = None
|
||||||
|
self.conn = None
|
||||||
|
self.connected = False
|
||||||
|
|
||||||
|
def connect(self, hostport):
|
||||||
|
if self.connected:
|
||||||
|
self.disconnect()
|
||||||
|
|
||||||
|
self.sock = socket.create_connection(hostport)
|
||||||
|
self.conn = ssl.wrap_socket(self.sock)
|
||||||
|
self.connected = True
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
if self.conn:
|
||||||
|
self.conn.close()
|
||||||
|
self.connected = False
|
||||||
|
|
||||||
|
def send_request(self, request):
|
||||||
|
data = (request.format(),)
|
||||||
|
payload = zlib.compress(rencode.dumps(data))
|
||||||
|
self.conn.sendall(payload)
|
||||||
|
|
||||||
|
buf = b""
|
||||||
|
|
||||||
|
while True:
|
||||||
|
data = self.conn.recv(1024)
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
self.connected = False
|
||||||
|
break
|
||||||
|
|
||||||
|
buf += data
|
||||||
|
dobj = zlib.decompressobj()
|
||||||
|
|
||||||
|
try:
|
||||||
|
message = rencode.loads(dobj.decompress(buf))
|
||||||
|
except (ValueError, zlib.error, struct.error):
|
||||||
|
# Probably incomplete data, read more
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
buf = dobj.unused_data
|
||||||
|
|
||||||
|
yield message
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
Metadata-Version: 1.0
|
||||||
|
Name: pyUnRAR2
|
||||||
|
Version: 0.99.2
|
||||||
|
Summary: Improved Python wrapper around the free UnRAR.dll
|
||||||
|
Home-page: http://code.google.com/py-unrar2
|
||||||
|
Author: Konstantin Yegupov
|
||||||
|
Author-email: yk4ever@gmail.com
|
||||||
|
License: MIT
|
||||||
|
Description: pyUnRAR2 is a ctypes based wrapper around the free UnRAR.dll.
|
||||||
|
|
||||||
|
It is an modified version of Jimmy Retzlaff's pyUnRAR - more simple,
|
||||||
|
stable and foolproof.
|
||||||
|
Notice that it has INCOMPATIBLE interface.
|
||||||
|
|
||||||
|
It enables reading and unpacking of archives created with the
|
||||||
|
RAR/WinRAR archivers. There is a low-level interface which is very
|
||||||
|
similar to the C interface provided by UnRAR. There is also a
|
||||||
|
higher level interface which makes some common operations easier.
|
||||||
|
Platform: Windows
|
||||||
|
Classifier: Development Status :: 4 - Beta
|
||||||
|
Classifier: Environment :: Win32 (MS Windows)
|
||||||
|
Classifier: License :: OSI Approved :: MIT License
|
||||||
|
Classifier: Natural Language :: English
|
||||||
|
Classifier: Operating System :: Microsoft :: Windows
|
||||||
|
Classifier: Programming Language :: Python
|
||||||
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
||||||
|
Classifier: Topic :: System :: Archiving :: Compression
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
|
||||||
|
<html><head><title>Python: package UnRAR2</title>
|
||||||
|
</head><body bgcolor="#f0f0f8">
|
||||||
|
|
||||||
|
<table width="100%" cellspacing=0 cellpadding=2 border=0 summary="heading">
|
||||||
|
<tr bgcolor="#7799ee">
|
||||||
|
<td valign=bottom> <br>
|
||||||
|
<font color="#ffffff" face="helvetica, arial"> <br><big><big><strong>UnRAR2</strong></big></big> (version 0.99.1)</font></td
|
||||||
|
><td align=right valign=bottom
|
||||||
|
><font color="#ffffff" face="helvetica, arial"><a href=".">index</a><br><a href="file:///C|/python26/lib/site-packages/unrar2/__init__.py">c:\python26\lib\site-packages\unrar2\__init__.py</a></font></td></tr></table>
|
||||||
|
<p><tt>pyUnRAR2 is a ctypes based wrapper around the free UnRAR.dll. <br>
|
||||||
|
<br>
|
||||||
|
It is an modified version of Jimmy Retzlaff's pyUnRAR - more simple,<br>
|
||||||
|
stable and foolproof.<br>
|
||||||
|
Notice that it has INCOMPATIBLE interface.<br>
|
||||||
|
<br>
|
||||||
|
It enables reading and unpacking of archives created with the<br>
|
||||||
|
RAR/WinRAR archivers. There is a low-level interface which is very<br>
|
||||||
|
similar to the C interface provided by UnRAR. There is also a<br>
|
||||||
|
higher level interface which makes some common operations easier.</tt></p>
|
||||||
|
<p>
|
||||||
|
<table width="100%" cellspacing=0 cellpadding=2 border=0 summary="section">
|
||||||
|
<tr bgcolor="#aa55cc">
|
||||||
|
<td colspan=3 valign=bottom> <br>
|
||||||
|
<font color="#ffffff" face="helvetica, arial"><big><strong>Package Contents</strong></big></font></td></tr>
|
||||||
|
|
||||||
|
<tr><td bgcolor="#aa55cc"><tt> </tt></td><td> </td>
|
||||||
|
<td width="100%"><table width="100%" summary="list"><tr><td width="25%" valign=top><a href="UnRAR2.rar_exceptions.html">rar_exceptions</a><br>
|
||||||
|
<a href="UnRAR2.setup.html">setup</a><br>
|
||||||
|
</td><td width="25%" valign=top><a href="UnRAR2.test_UnRAR2.html">test_UnRAR2</a><br>
|
||||||
|
<a href="UnRAR2.unix.html">unix</a><br>
|
||||||
|
</td><td width="25%" valign=top><a href="UnRAR2.windows.html">windows</a><br>
|
||||||
|
</td><td width="25%" valign=top></td></tr></table></td></tr></table><p>
|
||||||
|
<table width="100%" cellspacing=0 cellpadding=2 border=0 summary="section">
|
||||||
|
<tr bgcolor="#ee77aa">
|
||||||
|
<td colspan=3 valign=bottom> <br>
|
||||||
|
<font color="#ffffff" face="helvetica, arial"><big><strong>Classes</strong></big></font></td></tr>
|
||||||
|
|
||||||
|
<tr><td bgcolor="#ee77aa"><tt> </tt></td><td> </td>
|
||||||
|
<td width="100%"><dl>
|
||||||
|
<dt><font face="helvetica, arial"><a href="UnRAR2.windows.html#RarFileImplementation">UnRAR2.windows.RarFileImplementation</a>(<a href="__builtin__.html#object">__builtin__.object</a>)
|
||||||
|
</font></dt><dd>
|
||||||
|
<dl>
|
||||||
|
<dt><font face="helvetica, arial"><a href="UnRAR2.html#RarFile">RarFile</a>
|
||||||
|
</font></dt></dl>
|
||||||
|
</dd>
|
||||||
|
<dt><font face="helvetica, arial"><a href="__builtin__.html#object">__builtin__.object</a>
|
||||||
|
</font></dt><dd>
|
||||||
|
<dl>
|
||||||
|
<dt><font face="helvetica, arial"><a href="UnRAR2.html#RarInfo">RarInfo</a>
|
||||||
|
</font></dt></dl>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
<p>
|
||||||
|
<table width="100%" cellspacing=0 cellpadding=2 border=0 summary="section">
|
||||||
|
<tr bgcolor="#ffc8d8">
|
||||||
|
<td colspan=3 valign=bottom> <br>
|
||||||
|
<font color="#000000" face="helvetica, arial"><a name="RarFile">class <strong>RarFile</strong></a>(<a href="UnRAR2.windows.html#RarFileImplementation">UnRAR2.windows.RarFileImplementation</a>)</font></td></tr>
|
||||||
|
|
||||||
|
<tr><td bgcolor="#ffc8d8"><tt> </tt></td><td> </td>
|
||||||
|
<td width="100%"><dl><dt>Method resolution order:</dt>
|
||||||
|
<dd><a href="UnRAR2.html#RarFile">RarFile</a></dd>
|
||||||
|
<dd><a href="UnRAR2.windows.html#RarFileImplementation">UnRAR2.windows.RarFileImplementation</a></dd>
|
||||||
|
<dd><a href="__builtin__.html#object">__builtin__.object</a></dd>
|
||||||
|
</dl>
|
||||||
|
<hr>
|
||||||
|
Methods defined here:<br>
|
||||||
|
<dl><dt><a name="RarFile-__del__"><strong>__del__</strong></a>(self)</dt></dl>
|
||||||
|
|
||||||
|
<dl><dt><a name="RarFile-__init__"><strong>__init__</strong></a>(self, archiveName, password<font color="#909090">=None</font>)</dt><dd><tt>Instantiate the archive.<br>
|
||||||
|
<br>
|
||||||
|
archiveName is the name of the RAR file.<br>
|
||||||
|
password is used to decrypt the files in the archive.<br>
|
||||||
|
<br>
|
||||||
|
Properties:<br>
|
||||||
|
comment - comment associated with the archive<br>
|
||||||
|
<br>
|
||||||
|
>>> print <a href="#RarFile">RarFile</a>('test.rar').comment<br>
|
||||||
|
This is a test.</tt></dd></dl>
|
||||||
|
|
||||||
|
<dl><dt><a name="RarFile-extract"><strong>extract</strong></a>(self, condition<font color="#909090">='*'</font>, path<font color="#909090">='.'</font>, withSubpath<font color="#909090">=True</font>, overwrite<font color="#909090">=True</font>)</dt><dd><tt>Extract specific files from archive to disk.<br>
|
||||||
|
<br>
|
||||||
|
If "condition" is a list of numbers, then extract files which have those positions in infolist.<br>
|
||||||
|
If "condition" is a string, then it is treated as a wildcard for names of files to extract.<br>
|
||||||
|
If "condition" is a function, it is treated as a callback function, which accepts a <a href="#RarInfo">RarInfo</a> <a href="__builtin__.html#object">object</a><br>
|
||||||
|
and returns either boolean True (extract) or boolean False (skip).<br>
|
||||||
|
DEPRECATED: If "condition" callback returns string (only supported for Windows) - <br>
|
||||||
|
that string will be used as a new name to save the file under.<br>
|
||||||
|
If "condition" is omitted, all files are extracted.<br>
|
||||||
|
<br>
|
||||||
|
"path" is a directory to extract to<br>
|
||||||
|
"withSubpath" flag denotes whether files are extracted with their full path in the archive.<br>
|
||||||
|
"overwrite" flag denotes whether extracted files will overwrite old ones. Defaults to true.<br>
|
||||||
|
<br>
|
||||||
|
Returns list of RarInfos for extracted files.</tt></dd></dl>
|
||||||
|
|
||||||
|
<dl><dt><a name="RarFile-infoiter"><strong>infoiter</strong></a>(self)</dt><dd><tt>Iterate over all the files in the archive, generating RarInfos.<br>
|
||||||
|
<br>
|
||||||
|
>>> import os<br>
|
||||||
|
>>> for fileInArchive in <a href="#RarFile">RarFile</a>('test.rar').<a href="#RarFile-infoiter">infoiter</a>():<br>
|
||||||
|
... print os.path.split(fileInArchive.filename)[-1],<br>
|
||||||
|
... print fileInArchive.isdir,<br>
|
||||||
|
... print fileInArchive.size,<br>
|
||||||
|
... print fileInArchive.comment,<br>
|
||||||
|
... print tuple(fileInArchive.datetime)[0:5],<br>
|
||||||
|
... print time.strftime('%a, %d %b %Y %H:%M', fileInArchive.datetime)<br>
|
||||||
|
test True 0 None (2003, 6, 30, 1, 59) Mon, 30 Jun 2003 01:59<br>
|
||||||
|
test.txt False 20 None (2003, 6, 30, 2, 1) Mon, 30 Jun 2003 02:01<br>
|
||||||
|
this.py False 1030 None (2002, 2, 8, 16, 47) Fri, 08 Feb 2002 16:47</tt></dd></dl>
|
||||||
|
|
||||||
|
<dl><dt><a name="RarFile-infolist"><strong>infolist</strong></a>(self)</dt><dd><tt>Return a list of RarInfos, descripting the contents of the archive.</tt></dd></dl>
|
||||||
|
|
||||||
|
<dl><dt><a name="RarFile-read_files"><strong>read_files</strong></a>(self, condition<font color="#909090">='*'</font>)</dt><dd><tt>Read specific files from archive into memory.<br>
|
||||||
|
If "condition" is a list of numbers, then return files which have those positions in infolist.<br>
|
||||||
|
If "condition" is a string, then it is treated as a wildcard for names of files to extract.<br>
|
||||||
|
If "condition" is a function, it is treated as a callback function, which accepts a <a href="#RarInfo">RarInfo</a> <a href="__builtin__.html#object">object</a> <br>
|
||||||
|
and returns boolean True (extract) or False (skip).<br>
|
||||||
|
If "condition" is omitted, all files are returned.<br>
|
||||||
|
<br>
|
||||||
|
Returns list of tuples (<a href="#RarInfo">RarInfo</a> info, str contents)</tt></dd></dl>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
Methods inherited from <a href="UnRAR2.windows.html#RarFileImplementation">UnRAR2.windows.RarFileImplementation</a>:<br>
|
||||||
|
<dl><dt><a name="RarFile-destruct"><strong>destruct</strong></a>(self)</dt></dl>
|
||||||
|
|
||||||
|
<dl><dt><a name="RarFile-init"><strong>init</strong></a>(self, password<font color="#909090">=None</font>)</dt></dl>
|
||||||
|
|
||||||
|
<dl><dt><a name="RarFile-make_sure_ready"><strong>make_sure_ready</strong></a>(self)</dt></dl>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
Data descriptors inherited from <a href="UnRAR2.windows.html#RarFileImplementation">UnRAR2.windows.RarFileImplementation</a>:<br>
|
||||||
|
<dl><dt><strong>__dict__</strong></dt>
|
||||||
|
<dd><tt>dictionary for instance variables (if defined)</tt></dd>
|
||||||
|
</dl>
|
||||||
|
<dl><dt><strong>__weakref__</strong></dt>
|
||||||
|
<dd><tt>list of weak references to the object (if defined)</tt></dd>
|
||||||
|
</dl>
|
||||||
|
</td></tr></table> <p>
|
||||||
|
<table width="100%" cellspacing=0 cellpadding=2 border=0 summary="section">
|
||||||
|
<tr bgcolor="#ffc8d8">
|
||||||
|
<td colspan=3 valign=bottom> <br>
|
||||||
|
<font color="#000000" face="helvetica, arial"><a name="RarInfo">class <strong>RarInfo</strong></a>(<a href="__builtin__.html#object">__builtin__.object</a>)</font></td></tr>
|
||||||
|
|
||||||
|
<tr bgcolor="#ffc8d8"><td rowspan=2><tt> </tt></td>
|
||||||
|
<td colspan=2><tt>Represents a file header in an archive. Don't instantiate directly.<br>
|
||||||
|
Use only to obtain information about file.<br>
|
||||||
|
YOU CANNOT EXTRACT FILE CONTENTS USING THIS OBJECT.<br>
|
||||||
|
USE METHODS OF <a href="#RarFile">RarFile</a> CLASS INSTEAD.<br>
|
||||||
|
<br>
|
||||||
|
Properties:<br>
|
||||||
|
index - index of file within the archive<br>
|
||||||
|
filename - name of the file in the archive including path (if any)<br>
|
||||||
|
datetime - file date/time as a struct_time suitable for time.strftime<br>
|
||||||
|
isdir - True if the file is a directory<br>
|
||||||
|
size - size in bytes of the uncompressed file<br>
|
||||||
|
comment - comment associated with the file<br>
|
||||||
|
<br>
|
||||||
|
Note - this is not currently intended to be a Python file-like <a href="__builtin__.html#object">object</a>.<br> </tt></td></tr>
|
||||||
|
<tr><td> </td>
|
||||||
|
<td width="100%">Methods defined here:<br>
|
||||||
|
<dl><dt><a name="RarInfo-__init__"><strong>__init__</strong></a>(self, rarfile, data)</dt></dl>
|
||||||
|
|
||||||
|
<dl><dt><a name="RarInfo-__str__"><strong>__str__</strong></a>(self)</dt></dl>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
Data descriptors defined here:<br>
|
||||||
|
<dl><dt><strong>__dict__</strong></dt>
|
||||||
|
<dd><tt>dictionary for instance variables (if defined)</tt></dd>
|
||||||
|
</dl>
|
||||||
|
<dl><dt><strong>__weakref__</strong></dt>
|
||||||
|
<dd><tt>list of weak references to the object (if defined)</tt></dd>
|
||||||
|
</dl>
|
||||||
|
</td></tr></table></td></tr></table><p>
|
||||||
|
<table width="100%" cellspacing=0 cellpadding=2 border=0 summary="section">
|
||||||
|
<tr bgcolor="#eeaa77">
|
||||||
|
<td colspan=3 valign=bottom> <br>
|
||||||
|
<font color="#ffffff" face="helvetica, arial"><big><strong>Functions</strong></big></font></td></tr>
|
||||||
|
|
||||||
|
<tr><td bgcolor="#eeaa77"><tt> </tt></td><td> </td>
|
||||||
|
<td width="100%"><dl><dt><a name="-condition2checker"><strong>condition2checker</strong></a>(condition)</dt><dd><tt>Converts different condition types to callback</tt></dd></dl>
|
||||||
|
</td></tr></table><p>
|
||||||
|
<table width="100%" cellspacing=0 cellpadding=2 border=0 summary="section">
|
||||||
|
<tr bgcolor="#55aa55">
|
||||||
|
<td colspan=3 valign=bottom> <br>
|
||||||
|
<font color="#ffffff" face="helvetica, arial"><big><strong>Data</strong></big></font></td></tr>
|
||||||
|
|
||||||
|
<tr><td bgcolor="#55aa55"><tt> </tt></td><td> </td>
|
||||||
|
<td width="100%"><strong>__version__</strong> = '0.99.1'<br>
|
||||||
|
<strong>in_windows</strong> = True</td></tr></table>
|
||||||
|
</body></html>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
The unrar.dll library is freeware. This means:
|
||||||
|
|
||||||
|
1. All copyrights to RAR and the unrar.dll are exclusively
|
||||||
|
owned by the author - Alexander Roshal.
|
||||||
|
|
||||||
|
2. The unrar.dll library may be used in any software to handle RAR
|
||||||
|
archives without limitations free of charge.
|
||||||
|
|
||||||
|
3. THE RAR ARCHIVER AND THE UNRAR.DLL LIBRARY ARE DISTRIBUTED "AS IS".
|
||||||
|
NO WARRANTY OF ANY KIND IS EXPRESSED OR IMPLIED. YOU USE AT
|
||||||
|
YOUR OWN RISK. THE AUTHOR WILL NOT BE LIABLE FOR DATA LOSS,
|
||||||
|
DAMAGES, LOSS OF PROFITS OR ANY OTHER KIND OF LOSS WHILE USING
|
||||||
|
OR MISUSING THIS SOFTWARE.
|
||||||
|
|
||||||
|
Thank you for your interest in RAR and unrar.dll.
|
||||||
|
|
||||||
|
|
||||||
|
Alexander L. Roshal
|
||||||
Binary file not shown.
@@ -0,0 +1,140 @@
|
|||||||
|
#ifndef _UNRAR_DLL_
|
||||||
|
#define _UNRAR_DLL_
|
||||||
|
|
||||||
|
#define ERAR_END_ARCHIVE 10
|
||||||
|
#define ERAR_NO_MEMORY 11
|
||||||
|
#define ERAR_BAD_DATA 12
|
||||||
|
#define ERAR_BAD_ARCHIVE 13
|
||||||
|
#define ERAR_UNKNOWN_FORMAT 14
|
||||||
|
#define ERAR_EOPEN 15
|
||||||
|
#define ERAR_ECREATE 16
|
||||||
|
#define ERAR_ECLOSE 17
|
||||||
|
#define ERAR_EREAD 18
|
||||||
|
#define ERAR_EWRITE 19
|
||||||
|
#define ERAR_SMALL_BUF 20
|
||||||
|
#define ERAR_UNKNOWN 21
|
||||||
|
#define ERAR_MISSING_PASSWORD 22
|
||||||
|
|
||||||
|
#define RAR_OM_LIST 0
|
||||||
|
#define RAR_OM_EXTRACT 1
|
||||||
|
#define RAR_OM_LIST_INCSPLIT 2
|
||||||
|
|
||||||
|
#define RAR_SKIP 0
|
||||||
|
#define RAR_TEST 1
|
||||||
|
#define RAR_EXTRACT 2
|
||||||
|
|
||||||
|
#define RAR_VOL_ASK 0
|
||||||
|
#define RAR_VOL_NOTIFY 1
|
||||||
|
|
||||||
|
#define RAR_DLL_VERSION 4
|
||||||
|
|
||||||
|
#ifdef _UNIX
|
||||||
|
#define CALLBACK
|
||||||
|
#define PASCAL
|
||||||
|
#define LONG long
|
||||||
|
#define HANDLE void *
|
||||||
|
#define LPARAM long
|
||||||
|
#define UINT unsigned int
|
||||||
|
#endif
|
||||||
|
|
||||||
|
struct RARHeaderData
|
||||||
|
{
|
||||||
|
char ArcName[260];
|
||||||
|
char FileName[260];
|
||||||
|
unsigned int Flags;
|
||||||
|
unsigned int PackSize;
|
||||||
|
unsigned int UnpSize;
|
||||||
|
unsigned int HostOS;
|
||||||
|
unsigned int FileCRC;
|
||||||
|
unsigned int FileTime;
|
||||||
|
unsigned int UnpVer;
|
||||||
|
unsigned int Method;
|
||||||
|
unsigned int FileAttr;
|
||||||
|
char *CmtBuf;
|
||||||
|
unsigned int CmtBufSize;
|
||||||
|
unsigned int CmtSize;
|
||||||
|
unsigned int CmtState;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
struct RARHeaderDataEx
|
||||||
|
{
|
||||||
|
char ArcName[1024];
|
||||||
|
wchar_t ArcNameW[1024];
|
||||||
|
char FileName[1024];
|
||||||
|
wchar_t FileNameW[1024];
|
||||||
|
unsigned int Flags;
|
||||||
|
unsigned int PackSize;
|
||||||
|
unsigned int PackSizeHigh;
|
||||||
|
unsigned int UnpSize;
|
||||||
|
unsigned int UnpSizeHigh;
|
||||||
|
unsigned int HostOS;
|
||||||
|
unsigned int FileCRC;
|
||||||
|
unsigned int FileTime;
|
||||||
|
unsigned int UnpVer;
|
||||||
|
unsigned int Method;
|
||||||
|
unsigned int FileAttr;
|
||||||
|
char *CmtBuf;
|
||||||
|
unsigned int CmtBufSize;
|
||||||
|
unsigned int CmtSize;
|
||||||
|
unsigned int CmtState;
|
||||||
|
unsigned int Reserved[1024];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
struct RAROpenArchiveData
|
||||||
|
{
|
||||||
|
char *ArcName;
|
||||||
|
unsigned int OpenMode;
|
||||||
|
unsigned int OpenResult;
|
||||||
|
char *CmtBuf;
|
||||||
|
unsigned int CmtBufSize;
|
||||||
|
unsigned int CmtSize;
|
||||||
|
unsigned int CmtState;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RAROpenArchiveDataEx
|
||||||
|
{
|
||||||
|
char *ArcName;
|
||||||
|
wchar_t *ArcNameW;
|
||||||
|
unsigned int OpenMode;
|
||||||
|
unsigned int OpenResult;
|
||||||
|
char *CmtBuf;
|
||||||
|
unsigned int CmtBufSize;
|
||||||
|
unsigned int CmtSize;
|
||||||
|
unsigned int CmtState;
|
||||||
|
unsigned int Flags;
|
||||||
|
unsigned int Reserved[32];
|
||||||
|
};
|
||||||
|
|
||||||
|
enum UNRARCALLBACK_MESSAGES {
|
||||||
|
UCM_CHANGEVOLUME,UCM_PROCESSDATA,UCM_NEEDPASSWORD
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef int (CALLBACK *UNRARCALLBACK)(UINT msg,LPARAM UserData,LPARAM P1,LPARAM P2);
|
||||||
|
|
||||||
|
typedef int (PASCAL *CHANGEVOLPROC)(char *ArcName,int Mode);
|
||||||
|
typedef int (PASCAL *PROCESSDATAPROC)(unsigned char *Addr,int Size);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
HANDLE PASCAL RAROpenArchive(struct RAROpenArchiveData *ArchiveData);
|
||||||
|
HANDLE PASCAL RAROpenArchiveEx(struct RAROpenArchiveDataEx *ArchiveData);
|
||||||
|
int PASCAL RARCloseArchive(HANDLE hArcData);
|
||||||
|
int PASCAL RARReadHeader(HANDLE hArcData,struct RARHeaderData *HeaderData);
|
||||||
|
int PASCAL RARReadHeaderEx(HANDLE hArcData,struct RARHeaderDataEx *HeaderData);
|
||||||
|
int PASCAL RARProcessFile(HANDLE hArcData,int Operation,char *DestPath,char *DestName);
|
||||||
|
int PASCAL RARProcessFileW(HANDLE hArcData,int Operation,wchar_t *DestPath,wchar_t *DestName);
|
||||||
|
void PASCAL RARSetCallback(HANDLE hArcData,UNRARCALLBACK Callback,LPARAM UserData);
|
||||||
|
void PASCAL RARSetChangeVolProc(HANDLE hArcData,CHANGEVOLPROC ChangeVolProc);
|
||||||
|
void PASCAL RARSetProcessDataProc(HANDLE hArcData,PROCESSDATAPROC ProcessDataProc);
|
||||||
|
void PASCAL RARSetPassword(HANDLE hArcData,char *Password);
|
||||||
|
int PASCAL RARGetDllVersion();
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
Binary file not shown.
@@ -0,0 +1,606 @@
|
|||||||
|
|
||||||
|
UnRAR.dll Manual
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
UnRAR.dll is a 32-bit Windows dynamic-link library which provides
|
||||||
|
file extraction from RAR archives.
|
||||||
|
|
||||||
|
|
||||||
|
Exported functions
|
||||||
|
|
||||||
|
====================================================================
|
||||||
|
HANDLE PASCAL RAROpenArchive(struct RAROpenArchiveData *ArchiveData)
|
||||||
|
====================================================================
|
||||||
|
|
||||||
|
Description
|
||||||
|
~~~~~~~~~~~
|
||||||
|
Open RAR archive and allocate memory structures
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
~~~~~~~~~~
|
||||||
|
ArchiveData Points to RAROpenArchiveData structure
|
||||||
|
|
||||||
|
struct RAROpenArchiveData
|
||||||
|
{
|
||||||
|
char *ArcName;
|
||||||
|
UINT OpenMode;
|
||||||
|
UINT OpenResult;
|
||||||
|
char *CmtBuf;
|
||||||
|
UINT CmtBufSize;
|
||||||
|
UINT CmtSize;
|
||||||
|
UINT CmtState;
|
||||||
|
};
|
||||||
|
|
||||||
|
Structure fields:
|
||||||
|
|
||||||
|
ArcName
|
||||||
|
Input parameter which should point to zero terminated string
|
||||||
|
containing the archive name.
|
||||||
|
|
||||||
|
OpenMode
|
||||||
|
Input parameter.
|
||||||
|
|
||||||
|
Possible values
|
||||||
|
|
||||||
|
RAR_OM_LIST
|
||||||
|
Open archive for reading file headers only.
|
||||||
|
|
||||||
|
RAR_OM_EXTRACT
|
||||||
|
Open archive for testing and extracting files.
|
||||||
|
|
||||||
|
RAR_OM_LIST_INCSPLIT
|
||||||
|
Open archive for reading file headers only. If you open an archive
|
||||||
|
in such mode, RARReadHeader[Ex] will return all file headers,
|
||||||
|
including those with "file continued from previous volume" flag.
|
||||||
|
In case of RAR_OM_LIST such headers are automatically skipped.
|
||||||
|
So if you process RAR volumes in RAR_OM_LIST_INCSPLIT mode, you will
|
||||||
|
get several file header records for same file if file is split between
|
||||||
|
volumes. For such files only the last file header record will contain
|
||||||
|
the correct file CRC and if you wish to get the correct packed size,
|
||||||
|
you need to sum up packed sizes of all parts.
|
||||||
|
|
||||||
|
OpenResult
|
||||||
|
Output parameter.
|
||||||
|
|
||||||
|
Possible values
|
||||||
|
|
||||||
|
0 Success
|
||||||
|
ERAR_NO_MEMORY Not enough memory to initialize data structures
|
||||||
|
ERAR_BAD_DATA Archive header broken
|
||||||
|
ERAR_BAD_ARCHIVE File is not valid RAR archive
|
||||||
|
ERAR_UNKNOWN_FORMAT Unknown encryption used for archive headers
|
||||||
|
ERAR_EOPEN File open error
|
||||||
|
|
||||||
|
CmtBuf
|
||||||
|
Input parameter which should point to the buffer for archive
|
||||||
|
comments. Maximum comment size is limited to 64Kb. Comment text is
|
||||||
|
zero terminated. If the comment text is larger than the buffer
|
||||||
|
size, the comment text will be truncated. If CmtBuf is set to
|
||||||
|
NULL, comments will not be read.
|
||||||
|
|
||||||
|
CmtBufSize
|
||||||
|
Input parameter which should contain size of buffer for archive
|
||||||
|
comments.
|
||||||
|
|
||||||
|
CmtSize
|
||||||
|
Output parameter containing size of comments actually read into the
|
||||||
|
buffer, cannot exceed CmtBufSize.
|
||||||
|
|
||||||
|
CmtState
|
||||||
|
Output parameter.
|
||||||
|
|
||||||
|
Possible values
|
||||||
|
|
||||||
|
0 comments not present
|
||||||
|
1 Comments read completely
|
||||||
|
ERAR_NO_MEMORY Not enough memory to extract comments
|
||||||
|
ERAR_BAD_DATA Broken comment
|
||||||
|
ERAR_UNKNOWN_FORMAT Unknown comment format
|
||||||
|
ERAR_SMALL_BUF Buffer too small, comments not completely read
|
||||||
|
|
||||||
|
Return values
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
Archive handle or NULL in case of error
|
||||||
|
|
||||||
|
|
||||||
|
========================================================================
|
||||||
|
HANDLE PASCAL RAROpenArchiveEx(struct RAROpenArchiveDataEx *ArchiveData)
|
||||||
|
========================================================================
|
||||||
|
|
||||||
|
Description
|
||||||
|
~~~~~~~~~~~
|
||||||
|
Similar to RAROpenArchive, but uses RAROpenArchiveDataEx structure
|
||||||
|
allowing to specify Unicode archive name and returning information
|
||||||
|
about archive flags.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
~~~~~~~~~~
|
||||||
|
ArchiveData Points to RAROpenArchiveDataEx structure
|
||||||
|
|
||||||
|
struct RAROpenArchiveDataEx
|
||||||
|
{
|
||||||
|
char *ArcName;
|
||||||
|
wchar_t *ArcNameW;
|
||||||
|
unsigned int OpenMode;
|
||||||
|
unsigned int OpenResult;
|
||||||
|
char *CmtBuf;
|
||||||
|
unsigned int CmtBufSize;
|
||||||
|
unsigned int CmtSize;
|
||||||
|
unsigned int CmtState;
|
||||||
|
unsigned int Flags;
|
||||||
|
unsigned int Reserved[32];
|
||||||
|
};
|
||||||
|
|
||||||
|
Structure fields:
|
||||||
|
|
||||||
|
ArcNameW
|
||||||
|
Input parameter which should point to zero terminated Unicode string
|
||||||
|
containing the archive name or NULL if Unicode name is not specified.
|
||||||
|
|
||||||
|
Flags
|
||||||
|
Output parameter. Combination of bit flags.
|
||||||
|
|
||||||
|
Possible values
|
||||||
|
|
||||||
|
0x0001 - Volume attribute (archive volume)
|
||||||
|
0x0002 - Archive comment present
|
||||||
|
0x0004 - Archive lock attribute
|
||||||
|
0x0008 - Solid attribute (solid archive)
|
||||||
|
0x0010 - New volume naming scheme ('volname.partN.rar')
|
||||||
|
0x0020 - Authenticity information present
|
||||||
|
0x0040 - Recovery record present
|
||||||
|
0x0080 - Block headers are encrypted
|
||||||
|
0x0100 - First volume (set only by RAR 3.0 and later)
|
||||||
|
|
||||||
|
Reserved[32]
|
||||||
|
Reserved for future use. Must be zero.
|
||||||
|
|
||||||
|
Information on other structure fields and function return values
|
||||||
|
is available above, in RAROpenArchive function description.
|
||||||
|
|
||||||
|
|
||||||
|
====================================================================
|
||||||
|
int PASCAL RARCloseArchive(HANDLE hArcData)
|
||||||
|
====================================================================
|
||||||
|
|
||||||
|
Description
|
||||||
|
~~~~~~~~~~~
|
||||||
|
Close RAR archive and release allocated memory. It must be called when
|
||||||
|
archive processing is finished, even if the archive processing was stopped
|
||||||
|
due to an error.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
~~~~~~~~~~
|
||||||
|
hArcData
|
||||||
|
This parameter should contain the archive handle obtained from the
|
||||||
|
RAROpenArchive function call.
|
||||||
|
|
||||||
|
Return values
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
0 Success
|
||||||
|
ERAR_ECLOSE Archive close error
|
||||||
|
|
||||||
|
|
||||||
|
====================================================================
|
||||||
|
int PASCAL RARReadHeader(HANDLE hArcData,
|
||||||
|
struct RARHeaderData *HeaderData)
|
||||||
|
====================================================================
|
||||||
|
|
||||||
|
Description
|
||||||
|
~~~~~~~~~~~
|
||||||
|
Read header of file in archive.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
~~~~~~~~~~
|
||||||
|
hArcData
|
||||||
|
This parameter should contain the archive handle obtained from the
|
||||||
|
RAROpenArchive function call.
|
||||||
|
|
||||||
|
HeaderData
|
||||||
|
It should point to RARHeaderData structure:
|
||||||
|
|
||||||
|
struct RARHeaderData
|
||||||
|
{
|
||||||
|
char ArcName[260];
|
||||||
|
char FileName[260];
|
||||||
|
UINT Flags;
|
||||||
|
UINT PackSize;
|
||||||
|
UINT UnpSize;
|
||||||
|
UINT HostOS;
|
||||||
|
UINT FileCRC;
|
||||||
|
UINT FileTime;
|
||||||
|
UINT UnpVer;
|
||||||
|
UINT Method;
|
||||||
|
UINT FileAttr;
|
||||||
|
char *CmtBuf;
|
||||||
|
UINT CmtBufSize;
|
||||||
|
UINT CmtSize;
|
||||||
|
UINT CmtState;
|
||||||
|
};
|
||||||
|
|
||||||
|
Structure fields:
|
||||||
|
|
||||||
|
ArcName
|
||||||
|
Output parameter which contains a zero terminated string of the
|
||||||
|
current archive name. May be used to determine the current volume
|
||||||
|
name.
|
||||||
|
|
||||||
|
FileName
|
||||||
|
Output parameter which contains a zero terminated string of the
|
||||||
|
file name in OEM (DOS) encoding.
|
||||||
|
|
||||||
|
Flags
|
||||||
|
Output parameter which contains file flags:
|
||||||
|
|
||||||
|
0x01 - file continued from previous volume
|
||||||
|
0x02 - file continued on next volume
|
||||||
|
0x04 - file encrypted with password
|
||||||
|
0x08 - file comment present
|
||||||
|
0x10 - compression of previous files is used (solid flag)
|
||||||
|
|
||||||
|
bits 7 6 5
|
||||||
|
|
||||||
|
0 0 0 - dictionary size 64 Kb
|
||||||
|
0 0 1 - dictionary size 128 Kb
|
||||||
|
0 1 0 - dictionary size 256 Kb
|
||||||
|
0 1 1 - dictionary size 512 Kb
|
||||||
|
1 0 0 - dictionary size 1024 Kb
|
||||||
|
1 0 1 - dictionary size 2048 KB
|
||||||
|
1 1 0 - dictionary size 4096 KB
|
||||||
|
1 1 1 - file is directory
|
||||||
|
|
||||||
|
Other bits are reserved.
|
||||||
|
|
||||||
|
PackSize
|
||||||
|
Output parameter means packed file size or size of the
|
||||||
|
file part if file was split between volumes.
|
||||||
|
|
||||||
|
UnpSize
|
||||||
|
Output parameter - unpacked file size.
|
||||||
|
|
||||||
|
HostOS
|
||||||
|
Output parameter - operating system used for archiving:
|
||||||
|
|
||||||
|
0 - MS DOS;
|
||||||
|
1 - OS/2.
|
||||||
|
2 - Win32
|
||||||
|
3 - Unix
|
||||||
|
|
||||||
|
FileCRC
|
||||||
|
Output parameter which contains unpacked file CRC. In case of file parts
|
||||||
|
split between volumes only the last part contains the correct CRC
|
||||||
|
and it is accessible only in RAR_OM_LIST_INCSPLIT listing mode.
|
||||||
|
|
||||||
|
FileTime
|
||||||
|
Output parameter - contains date and time in standard MS DOS format.
|
||||||
|
|
||||||
|
UnpVer
|
||||||
|
Output parameter - RAR version needed to extract file.
|
||||||
|
It is encoded as 10 * Major version + minor version.
|
||||||
|
|
||||||
|
Method
|
||||||
|
Output parameter - packing method.
|
||||||
|
|
||||||
|
FileAttr
|
||||||
|
Output parameter - file attributes.
|
||||||
|
|
||||||
|
CmtBuf
|
||||||
|
File comments support is not implemented in the new DLL version yet.
|
||||||
|
Now CmtState is always 0.
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Input parameter which should point to the buffer for file
|
||||||
|
* comments. Maximum comment size is limited to 64Kb. Comment text is
|
||||||
|
* a zero terminated string in OEM encoding. If the comment text is
|
||||||
|
* larger than the buffer size, the comment text will be truncated.
|
||||||
|
* If CmtBuf is set to NULL, comments will not be read.
|
||||||
|
*/
|
||||||
|
|
||||||
|
CmtBufSize
|
||||||
|
Input parameter which should contain size of buffer for archive
|
||||||
|
comments.
|
||||||
|
|
||||||
|
CmtSize
|
||||||
|
Output parameter containing size of comments actually read into the
|
||||||
|
buffer, should not exceed CmtBufSize.
|
||||||
|
|
||||||
|
CmtState
|
||||||
|
Output parameter.
|
||||||
|
|
||||||
|
Possible values
|
||||||
|
|
||||||
|
0 Absent comments
|
||||||
|
1 Comments read completely
|
||||||
|
ERAR_NO_MEMORY Not enough memory to extract comments
|
||||||
|
ERAR_BAD_DATA Broken comment
|
||||||
|
ERAR_UNKNOWN_FORMAT Unknown comment format
|
||||||
|
ERAR_SMALL_BUF Buffer too small, comments not completely read
|
||||||
|
|
||||||
|
Return values
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
0 Success
|
||||||
|
ERAR_END_ARCHIVE End of archive
|
||||||
|
ERAR_BAD_DATA File header broken
|
||||||
|
|
||||||
|
|
||||||
|
====================================================================
|
||||||
|
int PASCAL RARReadHeaderEx(HANDLE hArcData,
|
||||||
|
struct RARHeaderDataEx *HeaderData)
|
||||||
|
====================================================================
|
||||||
|
|
||||||
|
Description
|
||||||
|
~~~~~~~~~~~
|
||||||
|
Similar to RARReadHeader, but uses RARHeaderDataEx structure,
|
||||||
|
containing information about Unicode file names and 64 bit file sizes.
|
||||||
|
|
||||||
|
struct RARHeaderDataEx
|
||||||
|
{
|
||||||
|
char ArcName[1024];
|
||||||
|
wchar_t ArcNameW[1024];
|
||||||
|
char FileName[1024];
|
||||||
|
wchar_t FileNameW[1024];
|
||||||
|
unsigned int Flags;
|
||||||
|
unsigned int PackSize;
|
||||||
|
unsigned int PackSizeHigh;
|
||||||
|
unsigned int UnpSize;
|
||||||
|
unsigned int UnpSizeHigh;
|
||||||
|
unsigned int HostOS;
|
||||||
|
unsigned int FileCRC;
|
||||||
|
unsigned int FileTime;
|
||||||
|
unsigned int UnpVer;
|
||||||
|
unsigned int Method;
|
||||||
|
unsigned int FileAttr;
|
||||||
|
char *CmtBuf;
|
||||||
|
unsigned int CmtBufSize;
|
||||||
|
unsigned int CmtSize;
|
||||||
|
unsigned int CmtState;
|
||||||
|
unsigned int Reserved[1024];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
====================================================================
|
||||||
|
int PASCAL RARProcessFile(HANDLE hArcData,
|
||||||
|
int Operation,
|
||||||
|
char *DestPath,
|
||||||
|
char *DestName)
|
||||||
|
====================================================================
|
||||||
|
|
||||||
|
Description
|
||||||
|
~~~~~~~~~~~
|
||||||
|
Performs action and moves the current position in the archive to
|
||||||
|
the next file. Extract or test the current file from the archive
|
||||||
|
opened in RAR_OM_EXTRACT mode. If the mode RAR_OM_LIST is set,
|
||||||
|
then a call to this function will simply skip the archive position
|
||||||
|
to the next file.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
~~~~~~~~~~
|
||||||
|
hArcData
|
||||||
|
This parameter should contain the archive handle obtained from the
|
||||||
|
RAROpenArchive function call.
|
||||||
|
|
||||||
|
Operation
|
||||||
|
File operation.
|
||||||
|
|
||||||
|
Possible values
|
||||||
|
|
||||||
|
RAR_SKIP Move to the next file in the archive. If the
|
||||||
|
archive is solid and RAR_OM_EXTRACT mode was set
|
||||||
|
when the archive was opened, the current file will
|
||||||
|
be processed - the operation will be performed
|
||||||
|
slower than a simple seek.
|
||||||
|
|
||||||
|
RAR_TEST Test the current file and move to the next file in
|
||||||
|
the archive. If the archive was opened with
|
||||||
|
RAR_OM_LIST mode, the operation is equal to
|
||||||
|
RAR_SKIP.
|
||||||
|
|
||||||
|
RAR_EXTRACT Extract the current file and move to the next file.
|
||||||
|
If the archive was opened with RAR_OM_LIST mode,
|
||||||
|
the operation is equal to RAR_SKIP.
|
||||||
|
|
||||||
|
|
||||||
|
DestPath
|
||||||
|
This parameter should point to a zero terminated string containing the
|
||||||
|
destination directory to which to extract files to. If DestPath is equal
|
||||||
|
to NULL, it means extract to the current directory. This parameter has
|
||||||
|
meaning only if DestName is NULL.
|
||||||
|
|
||||||
|
DestName
|
||||||
|
This parameter should point to a string containing the full path and name
|
||||||
|
to assign to extracted file or it can be NULL to use the default name.
|
||||||
|
If DestName is defined (not NULL), it overrides both the original file
|
||||||
|
name saved in the archive and path specigied in DestPath setting.
|
||||||
|
|
||||||
|
Both DestPath and DestName must be in OEM encoding. If necessary,
|
||||||
|
use CharToOem to convert text to OEM before passing to this function.
|
||||||
|
|
||||||
|
Return values
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
0 Success
|
||||||
|
ERAR_BAD_DATA File CRC error
|
||||||
|
ERAR_BAD_ARCHIVE Volume is not valid RAR archive
|
||||||
|
ERAR_UNKNOWN_FORMAT Unknown archive format
|
||||||
|
ERAR_EOPEN Volume open error
|
||||||
|
ERAR_ECREATE File create error
|
||||||
|
ERAR_ECLOSE File close error
|
||||||
|
ERAR_EREAD Read error
|
||||||
|
ERAR_EWRITE Write error
|
||||||
|
|
||||||
|
|
||||||
|
Note: if you wish to cancel extraction, return -1 when processing
|
||||||
|
UCM_PROCESSDATA callback message.
|
||||||
|
|
||||||
|
|
||||||
|
====================================================================
|
||||||
|
int PASCAL RARProcessFileW(HANDLE hArcData,
|
||||||
|
int Operation,
|
||||||
|
wchar_t *DestPath,
|
||||||
|
wchar_t *DestName)
|
||||||
|
====================================================================
|
||||||
|
|
||||||
|
Description
|
||||||
|
~~~~~~~~~~~
|
||||||
|
Unicode version of RARProcessFile. It uses Unicode DestPath
|
||||||
|
and DestName parameters, other parameters and return values
|
||||||
|
are the same as in RARProcessFile.
|
||||||
|
|
||||||
|
|
||||||
|
====================================================================
|
||||||
|
void PASCAL RARSetCallback(HANDLE hArcData,
|
||||||
|
int PASCAL (*CallbackProc)(UINT msg,LPARAM UserData,LPARAM P1,LPARAM P2),
|
||||||
|
LPARAM UserData);
|
||||||
|
====================================================================
|
||||||
|
|
||||||
|
Description
|
||||||
|
~~~~~~~~~~~
|
||||||
|
Set a user-defined callback function to process Unrar events.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
~~~~~~~~~~
|
||||||
|
hArcData
|
||||||
|
This parameter should contain the archive handle obtained from the
|
||||||
|
RAROpenArchive function call.
|
||||||
|
|
||||||
|
CallbackProc
|
||||||
|
It should point to a user-defined callback function.
|
||||||
|
|
||||||
|
The function will be passed four parameters:
|
||||||
|
|
||||||
|
|
||||||
|
msg Type of event. Described below.
|
||||||
|
|
||||||
|
UserData User defined value passed to RARSetCallback.
|
||||||
|
|
||||||
|
P1 and P2 Event dependent parameters. Described below.
|
||||||
|
|
||||||
|
|
||||||
|
Possible events
|
||||||
|
|
||||||
|
UCM_CHANGEVOLUME Process volume change.
|
||||||
|
|
||||||
|
P1 Points to the zero terminated name
|
||||||
|
of the next volume.
|
||||||
|
|
||||||
|
P2 The function call mode:
|
||||||
|
|
||||||
|
RAR_VOL_ASK Required volume is absent. The function should
|
||||||
|
prompt user and return a positive value
|
||||||
|
to retry or return -1 value to terminate
|
||||||
|
operation. The function may also specify a new
|
||||||
|
volume name, placing it to the address specified
|
||||||
|
by P1 parameter.
|
||||||
|
|
||||||
|
RAR_VOL_NOTIFY Required volume is successfully opened.
|
||||||
|
This is a notification call and volume name
|
||||||
|
modification is not allowed. The function should
|
||||||
|
return a positive value to continue or -1
|
||||||
|
to terminate operation.
|
||||||
|
|
||||||
|
UCM_PROCESSDATA Process unpacked data. It may be used to read
|
||||||
|
a file while it is being extracted or tested
|
||||||
|
without actual extracting file to disk.
|
||||||
|
Return a positive value to continue process
|
||||||
|
or -1 to cancel the archive operation
|
||||||
|
|
||||||
|
P1 Address pointing to the unpacked data.
|
||||||
|
Function may refer to the data but must not
|
||||||
|
change it.
|
||||||
|
|
||||||
|
P2 Size of the unpacked data. It is guaranteed
|
||||||
|
only that the size will not exceed the maximum
|
||||||
|
dictionary size (4 Mb in RAR 3.0).
|
||||||
|
|
||||||
|
UCM_NEEDPASSWORD DLL needs a password to process archive.
|
||||||
|
This message must be processed if you wish
|
||||||
|
to be able to handle archives with encrypted
|
||||||
|
file names. It can be also used as replacement
|
||||||
|
of RARSetPassword function even for usual
|
||||||
|
encrypted files with non-encrypted names.
|
||||||
|
|
||||||
|
P1 Address pointing to the buffer for a password.
|
||||||
|
You need to copy a password here.
|
||||||
|
|
||||||
|
P2 Size of the password buffer.
|
||||||
|
|
||||||
|
|
||||||
|
UserData
|
||||||
|
User data passed to callback function.
|
||||||
|
|
||||||
|
Other functions of UnRAR.dll should not be called from the callback
|
||||||
|
function.
|
||||||
|
|
||||||
|
Return values
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
====================================================================
|
||||||
|
void PASCAL RARSetChangeVolProc(HANDLE hArcData,
|
||||||
|
int PASCAL (*ChangeVolProc)(char *ArcName,int Mode));
|
||||||
|
====================================================================
|
||||||
|
|
||||||
|
Obsoleted, use RARSetCallback instead.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
====================================================================
|
||||||
|
void PASCAL RARSetProcessDataProc(HANDLE hArcData,
|
||||||
|
int PASCAL (*ProcessDataProc)(unsigned char *Addr,int Size))
|
||||||
|
====================================================================
|
||||||
|
|
||||||
|
Obsoleted, use RARSetCallback instead.
|
||||||
|
|
||||||
|
|
||||||
|
====================================================================
|
||||||
|
void PASCAL RARSetPassword(HANDLE hArcData,
|
||||||
|
char *Password);
|
||||||
|
====================================================================
|
||||||
|
|
||||||
|
Description
|
||||||
|
~~~~~~~~~~~
|
||||||
|
Set a password to decrypt files.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
~~~~~~~~~~
|
||||||
|
hArcData
|
||||||
|
This parameter should contain the archive handle obtained from the
|
||||||
|
RAROpenArchive function call.
|
||||||
|
|
||||||
|
Password
|
||||||
|
It should point to a string containing a zero terminated password.
|
||||||
|
|
||||||
|
Return values
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
None
|
||||||
|
|
||||||
|
|
||||||
|
====================================================================
|
||||||
|
void PASCAL RARGetDllVersion();
|
||||||
|
====================================================================
|
||||||
|
|
||||||
|
Description
|
||||||
|
~~~~~~~~~~~
|
||||||
|
Returns API version.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
~~~~~~~~~~
|
||||||
|
None.
|
||||||
|
|
||||||
|
Return values
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
Returns an integer value denoting UnRAR.dll API version, which is also
|
||||||
|
defined in unrar.h as RAR_DLL_VERSION. API version number is incremented
|
||||||
|
only in case of noticeable changes in UnRAR.dll API. Do not confuse it
|
||||||
|
with version of UnRAR.dll stored in DLL resources, which is incremented
|
||||||
|
with every DLL rebuild.
|
||||||
|
|
||||||
|
If RARGetDllVersion() returns a value lower than UnRAR.dll which your
|
||||||
|
application was designed for, it may indicate that DLL version is too old
|
||||||
|
and it will fail to provide all necessary functions to your application.
|
||||||
|
|
||||||
|
This function is absent in old versions of UnRAR.dll, so it is safer
|
||||||
|
to use LoadLibrary and GetProcAddress to access this function.
|
||||||
|
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
List of unrar.dll API changes. We do not include performance and reliability
|
||||||
|
improvements into this list, but this library and RAR/UnRAR tools share
|
||||||
|
the same source code. So the latest version of unrar.dll usually contains
|
||||||
|
same decompression algorithm changes as the latest UnRAR version.
|
||||||
|
============================================================================
|
||||||
|
|
||||||
|
-- 18 January 2008
|
||||||
|
|
||||||
|
all LONG parameters of CallbackProc function were changed
|
||||||
|
to LPARAM type for 64 bit mode compatibility.
|
||||||
|
|
||||||
|
|
||||||
|
-- 12 December 2007
|
||||||
|
|
||||||
|
Added new RAR_OM_LIST_INCSPLIT open mode for function RAROpenArchive.
|
||||||
|
|
||||||
|
|
||||||
|
-- 14 August 2007
|
||||||
|
|
||||||
|
Added NoCrypt\unrar_nocrypt.dll without decryption code for those
|
||||||
|
applications where presence of encryption or decryption code is not
|
||||||
|
allowed because of legal restrictions.
|
||||||
|
|
||||||
|
|
||||||
|
-- 14 December 2006
|
||||||
|
|
||||||
|
Added ERAR_MISSING_PASSWORD error type. This error is returned
|
||||||
|
if empty password is specified for encrypted file.
|
||||||
|
|
||||||
|
|
||||||
|
-- 12 June 2003
|
||||||
|
|
||||||
|
Added RARProcessFileW function, Unicode version of RARProcessFile
|
||||||
|
|
||||||
|
|
||||||
|
-- 9 August 2002
|
||||||
|
|
||||||
|
Added RAROpenArchiveEx function allowing to specify Unicode archive
|
||||||
|
name and get archive flags.
|
||||||
|
|
||||||
|
|
||||||
|
-- 24 January 2002
|
||||||
|
|
||||||
|
Added RARReadHeaderEx function allowing to read Unicode file names
|
||||||
|
and 64 bit file sizes.
|
||||||
|
|
||||||
|
|
||||||
|
-- 23 January 2002
|
||||||
|
|
||||||
|
Added ERAR_UNKNOWN error type (it is used for all errors which
|
||||||
|
do not have special ERAR code yet) and UCM_NEEDPASSWORD callback
|
||||||
|
message.
|
||||||
|
|
||||||
|
Unrar.dll automatically opens all next volumes not only when extracting,
|
||||||
|
but also in RAR_OM_LIST mode.
|
||||||
|
|
||||||
|
|
||||||
|
-- 27 November 2001
|
||||||
|
|
||||||
|
RARSetChangeVolProc and RARSetProcessDataProc are replaced by
|
||||||
|
the single callback function installed with RARSetCallback.
|
||||||
|
Unlike old style callbacks, the new function accepts the user defined
|
||||||
|
parameter. Unrar.dll still supports RARSetChangeVolProc and
|
||||||
|
RARSetProcessDataProc for compatibility purposes, but if you write
|
||||||
|
a new application, better use RARSetCallback.
|
||||||
|
|
||||||
|
File comments support is not implemented in the new DLL version yet.
|
||||||
|
Now CmtState is always 0.
|
||||||
|
|
||||||
|
|
||||||
|
-- 13 August 2001
|
||||||
|
|
||||||
|
Added RARGetDllVersion function, so you may distinguish old unrar.dll,
|
||||||
|
which used C style callback functions and the new one with PASCAL callbacks.
|
||||||
|
|
||||||
|
|
||||||
|
-- 10 May 2001
|
||||||
|
|
||||||
|
Callback functions in RARSetChangeVolProc and RARSetProcessDataProc
|
||||||
|
use PASCAL style call convention now.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
This is x64 version of unrar.dll.
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,177 @@
|
|||||||
|
# Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
# a copy of this software and associated documentation files (the
|
||||||
|
# "Software"), to deal in the Software without restriction, including
|
||||||
|
# without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
# permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
# the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be
|
||||||
|
# included in all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||||
|
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||||
|
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
|
|
||||||
|
"""
|
||||||
|
pyUnRAR2 is a ctypes based wrapper around the free UnRAR.dll.
|
||||||
|
|
||||||
|
It is an modified version of Jimmy Retzlaff's pyUnRAR - more simple,
|
||||||
|
stable and foolproof.
|
||||||
|
Notice that it has INCOMPATIBLE interface.
|
||||||
|
|
||||||
|
It enables reading and unpacking of archives created with the
|
||||||
|
RAR/WinRAR archivers. There is a low-level interface which is very
|
||||||
|
similar to the C interface provided by UnRAR. There is also a
|
||||||
|
higher level interface which makes some common operations easier.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__version__ = '0.99.2'
|
||||||
|
|
||||||
|
try:
|
||||||
|
WindowsError
|
||||||
|
in_windows = True
|
||||||
|
except NameError:
|
||||||
|
in_windows = False
|
||||||
|
|
||||||
|
if in_windows:
|
||||||
|
from windows import RarFileImplementation
|
||||||
|
else:
|
||||||
|
from unix import RarFileImplementation
|
||||||
|
|
||||||
|
|
||||||
|
import fnmatch, time, weakref
|
||||||
|
|
||||||
|
class RarInfo(object):
|
||||||
|
"""Represents a file header in an archive. Don't instantiate directly.
|
||||||
|
Use only to obtain information about file.
|
||||||
|
YOU CANNOT EXTRACT FILE CONTENTS USING THIS OBJECT.
|
||||||
|
USE METHODS OF RarFile CLASS INSTEAD.
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
index - index of file within the archive
|
||||||
|
filename - name of the file in the archive including path (if any)
|
||||||
|
datetime - file date/time as a struct_time suitable for time.strftime
|
||||||
|
isdir - True if the file is a directory
|
||||||
|
size - size in bytes of the uncompressed file
|
||||||
|
comment - comment associated with the file
|
||||||
|
|
||||||
|
Note - this is not currently intended to be a Python file-like object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, rarfile, data):
|
||||||
|
self.rarfile = weakref.proxy(rarfile)
|
||||||
|
self.index = data['index']
|
||||||
|
self.filename = data['filename']
|
||||||
|
self.isdir = data['isdir']
|
||||||
|
self.size = data['size']
|
||||||
|
self.datetime = data['datetime']
|
||||||
|
self.comment = data['comment']
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
try :
|
||||||
|
arcName = self.rarfile.archiveName
|
||||||
|
except ReferenceError:
|
||||||
|
arcName = "[ARCHIVE_NO_LONGER_LOADED]"
|
||||||
|
return '<RarInfo "%s" in "%s">' % (self.filename, arcName)
|
||||||
|
|
||||||
|
class RarFile(RarFileImplementation):
|
||||||
|
|
||||||
|
def __init__(self, archiveName, password=None):
|
||||||
|
"""Instantiate the archive.
|
||||||
|
|
||||||
|
archiveName is the name of the RAR file.
|
||||||
|
password is used to decrypt the files in the archive.
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
comment - comment associated with the archive
|
||||||
|
|
||||||
|
>>> print RarFile('test.rar').comment
|
||||||
|
This is a test.
|
||||||
|
"""
|
||||||
|
self.archiveName = archiveName
|
||||||
|
RarFileImplementation.init(self, password)
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
self.destruct()
|
||||||
|
|
||||||
|
def infoiter(self):
|
||||||
|
"""Iterate over all the files in the archive, generating RarInfos.
|
||||||
|
|
||||||
|
>>> import os
|
||||||
|
>>> for fileInArchive in RarFile('test.rar').infoiter():
|
||||||
|
... print os.path.split(fileInArchive.filename)[-1],
|
||||||
|
... print fileInArchive.isdir,
|
||||||
|
... print fileInArchive.size,
|
||||||
|
... print fileInArchive.comment,
|
||||||
|
... print tuple(fileInArchive.datetime)[0:5],
|
||||||
|
... print time.strftime('%a, %d %b %Y %H:%M', fileInArchive.datetime)
|
||||||
|
test True 0 None (2003, 6, 30, 1, 59) Mon, 30 Jun 2003 01:59
|
||||||
|
test.txt False 20 None (2003, 6, 30, 2, 1) Mon, 30 Jun 2003 02:01
|
||||||
|
this.py False 1030 None (2002, 2, 8, 16, 47) Fri, 08 Feb 2002 16:47
|
||||||
|
"""
|
||||||
|
for params in RarFileImplementation.infoiter(self):
|
||||||
|
yield RarInfo(self, params)
|
||||||
|
|
||||||
|
def infolist(self):
|
||||||
|
"""Return a list of RarInfos, descripting the contents of the archive."""
|
||||||
|
return list(self.infoiter())
|
||||||
|
|
||||||
|
def read_files(self, condition='*'):
|
||||||
|
"""Read specific files from archive into memory.
|
||||||
|
If "condition" is a list of numbers, then return files which have those positions in infolist.
|
||||||
|
If "condition" is a string, then it is treated as a wildcard for names of files to extract.
|
||||||
|
If "condition" is a function, it is treated as a callback function, which accepts a RarInfo object
|
||||||
|
and returns boolean True (extract) or False (skip).
|
||||||
|
If "condition" is omitted, all files are returned.
|
||||||
|
|
||||||
|
Returns list of tuples (RarInfo info, str contents)
|
||||||
|
"""
|
||||||
|
checker = condition2checker(condition)
|
||||||
|
return RarFileImplementation.read_files(self, checker)
|
||||||
|
|
||||||
|
|
||||||
|
def extract(self, condition='*', path='.', withSubpath=True, overwrite=True):
|
||||||
|
"""Extract specific files from archive to disk.
|
||||||
|
|
||||||
|
If "condition" is a list of numbers, then extract files which have those positions in infolist.
|
||||||
|
If "condition" is a string, then it is treated as a wildcard for names of files to extract.
|
||||||
|
If "condition" is a function, it is treated as a callback function, which accepts a RarInfo object
|
||||||
|
and returns either boolean True (extract) or boolean False (skip).
|
||||||
|
DEPRECATED: If "condition" callback returns string (only supported for Windows) -
|
||||||
|
that string will be used as a new name to save the file under.
|
||||||
|
If "condition" is omitted, all files are extracted.
|
||||||
|
|
||||||
|
"path" is a directory to extract to
|
||||||
|
"withSubpath" flag denotes whether files are extracted with their full path in the archive.
|
||||||
|
"overwrite" flag denotes whether extracted files will overwrite old ones. Defaults to true.
|
||||||
|
|
||||||
|
Returns list of RarInfos for extracted files."""
|
||||||
|
checker = condition2checker(condition)
|
||||||
|
return RarFileImplementation.extract(self, checker, path, withSubpath, overwrite)
|
||||||
|
|
||||||
|
def condition2checker(condition):
|
||||||
|
"""Converts different condition types to callback"""
|
||||||
|
if type(condition) in [str, unicode]:
|
||||||
|
def smatcher(info):
|
||||||
|
return fnmatch.fnmatch(info.filename, condition)
|
||||||
|
return smatcher
|
||||||
|
elif type(condition) in [list, tuple] and type(condition[0]) in [int, long]:
|
||||||
|
def imatcher(info):
|
||||||
|
return info.index in condition
|
||||||
|
return imatcher
|
||||||
|
elif callable(condition):
|
||||||
|
return condition
|
||||||
|
else:
|
||||||
|
raise TypeError
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||||
|
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
# a copy of this software and associated documentation files (the
|
||||||
|
# "Software"), to deal in the Software without restriction, including
|
||||||
|
# without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
# permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
# the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be
|
||||||
|
# included in all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||||
|
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||||
|
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
|
|
||||||
|
# Low level interface - see UnRARDLL\UNRARDLL.TXT
|
||||||
|
|
||||||
|
|
||||||
|
class ArchiveHeaderBroken(Exception): pass
|
||||||
|
class InvalidRARArchive(Exception): pass
|
||||||
|
class FileOpenError(Exception): pass
|
||||||
|
class IncorrectRARPassword(Exception): pass
|
||||||
|
class InvalidRARArchiveUsage(Exception): pass
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
# Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
# a copy of this software and associated documentation files (the
|
||||||
|
# "Software"), to deal in the Software without restriction, including
|
||||||
|
# without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
# permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
# the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be
|
||||||
|
# included in all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||||
|
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||||
|
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
|
|
||||||
|
# Unix version uses unrar command line executable
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import gc
|
||||||
|
|
||||||
|
import os, os.path
|
||||||
|
import time, re
|
||||||
|
|
||||||
|
from rar_exceptions import *
|
||||||
|
|
||||||
|
class UnpackerNotInstalled(Exception): pass
|
||||||
|
|
||||||
|
rar_executable_cached = None
|
||||||
|
|
||||||
|
def call_unrar(params):
|
||||||
|
"Calls rar/unrar command line executable, returns stdout pipe"
|
||||||
|
global rar_executable_cached
|
||||||
|
if rar_executable_cached is None:
|
||||||
|
for command in ('unrar', 'rar'):
|
||||||
|
try:
|
||||||
|
subprocess.Popen([command], stdout=subprocess.PIPE)
|
||||||
|
rar_executable_cached = command
|
||||||
|
break
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
if rar_executable_cached is None:
|
||||||
|
raise UnpackerNotInstalled("No suitable RAR unpacker installed")
|
||||||
|
|
||||||
|
assert type(params) == list, "params must be list"
|
||||||
|
args = [rar_executable_cached] + params
|
||||||
|
try:
|
||||||
|
gc.disable() # See http://bugs.python.org/issue1336
|
||||||
|
return subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
finally:
|
||||||
|
gc.enable()
|
||||||
|
|
||||||
|
class RarFileImplementation(object):
|
||||||
|
|
||||||
|
def init(self, password=None):
|
||||||
|
self.password = password
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
stdoutdata, stderrdata = self.call('v', []).communicate()
|
||||||
|
|
||||||
|
for line in stderrdata.splitlines():
|
||||||
|
if line.strip().startswith("Cannot open"):
|
||||||
|
raise FileOpenError
|
||||||
|
if line.find("CRC failed")>=0:
|
||||||
|
raise IncorrectRARPassword
|
||||||
|
accum = []
|
||||||
|
source = iter(stdoutdata.splitlines())
|
||||||
|
line = ''
|
||||||
|
while not (line.startswith('Comment:') or line.startswith('Pathname/Comment')):
|
||||||
|
if line.strip().endswith('is not RAR archive'):
|
||||||
|
raise InvalidRARArchive
|
||||||
|
line = source.next()
|
||||||
|
while not line.startswith('Pathname/Comment'):
|
||||||
|
accum.append(line.rstrip('\n'))
|
||||||
|
line = source.next()
|
||||||
|
if len(accum):
|
||||||
|
accum[0] = accum[0][9:]
|
||||||
|
self.comment = '\n'.join(accum[:-1])
|
||||||
|
else:
|
||||||
|
self.comment = None
|
||||||
|
|
||||||
|
def escaped_password(self):
|
||||||
|
return '-' if self.password == None else self.password
|
||||||
|
|
||||||
|
|
||||||
|
def call(self, cmd, options=[], files=[]):
|
||||||
|
options2 = options + ['p'+self.escaped_password()]
|
||||||
|
soptions = ['-'+x for x in options2]
|
||||||
|
return call_unrar([cmd]+soptions+['--',self.archiveName]+files)
|
||||||
|
|
||||||
|
def infoiter(self):
|
||||||
|
|
||||||
|
stdoutdata, stderrdata = self.call('v', ['c-']).communicate()
|
||||||
|
|
||||||
|
for line in stderrdata.splitlines():
|
||||||
|
if line.strip().startswith("Cannot open"):
|
||||||
|
raise FileOpenError
|
||||||
|
|
||||||
|
accum = []
|
||||||
|
source = iter(stdoutdata.splitlines())
|
||||||
|
line = ''
|
||||||
|
while not line.startswith('--------------'):
|
||||||
|
if line.strip().endswith('is not RAR archive'):
|
||||||
|
raise InvalidRARArchive
|
||||||
|
if line.find("CRC failed")>=0:
|
||||||
|
raise IncorrectRARPassword
|
||||||
|
line = source.next()
|
||||||
|
line = source.next()
|
||||||
|
i = 0
|
||||||
|
re_spaces = re.compile(r"\s+")
|
||||||
|
while not line.startswith('--------------'):
|
||||||
|
accum.append(line)
|
||||||
|
if len(accum)==2:
|
||||||
|
data = {}
|
||||||
|
data['index'] = i
|
||||||
|
data['filename'] = accum[0].strip()
|
||||||
|
info = re_spaces.split(accum[1].strip())
|
||||||
|
data['size'] = int(info[0])
|
||||||
|
attr = info[5]
|
||||||
|
data['isdir'] = 'd' in attr.lower()
|
||||||
|
data['datetime'] = time.strptime(info[3]+" "+info[4], '%d-%m-%y %H:%M')
|
||||||
|
data['comment'] = None
|
||||||
|
yield data
|
||||||
|
accum = []
|
||||||
|
i += 1
|
||||||
|
line = source.next()
|
||||||
|
|
||||||
|
def read_files(self, checker):
|
||||||
|
res = []
|
||||||
|
for info in self.infoiter():
|
||||||
|
checkres = checker(info)
|
||||||
|
if checkres==True and not info.isdir:
|
||||||
|
pipe = self.call('p', ['inul'], [info.filename]).stdout
|
||||||
|
res.append((info, pipe.read()))
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def extract(self, checker, path, withSubpath, overwrite):
|
||||||
|
res = []
|
||||||
|
command = 'x'
|
||||||
|
if not withSubpath:
|
||||||
|
command = 'e'
|
||||||
|
options = []
|
||||||
|
if overwrite:
|
||||||
|
options.append('o+')
|
||||||
|
else:
|
||||||
|
options.append('o-')
|
||||||
|
if not path.endswith(os.sep):
|
||||||
|
path += os.sep
|
||||||
|
names = []
|
||||||
|
for info in self.infoiter():
|
||||||
|
checkres = checker(info)
|
||||||
|
if type(checkres) in [str, unicode]:
|
||||||
|
raise NotImplementedError("Condition callbacks returning strings are deprecated and only supported in Windows")
|
||||||
|
if checkres==True and not info.isdir:
|
||||||
|
names.append(info.filename)
|
||||||
|
res.append(info)
|
||||||
|
names.append(path)
|
||||||
|
proc = self.call(command, options, names)
|
||||||
|
stdoutdata, stderrdata = proc.communicate()
|
||||||
|
if stderrdata.find("CRC failed")>=0:
|
||||||
|
raise IncorrectRARPassword
|
||||||
|
return res
|
||||||
|
|
||||||
|
def destruct(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,309 @@
|
|||||||
|
# Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
# a copy of this software and associated documentation files (the
|
||||||
|
# "Software"), to deal in the Software without restriction, including
|
||||||
|
# without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
# permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
# the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be
|
||||||
|
# included in all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||||
|
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||||
|
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
|
|
||||||
|
# Low level interface - see UnRARDLL\UNRARDLL.TXT
|
||||||
|
|
||||||
|
from __future__ import generators
|
||||||
|
|
||||||
|
import ctypes, ctypes.wintypes
|
||||||
|
import os, os.path, sys
|
||||||
|
import Queue
|
||||||
|
import time
|
||||||
|
|
||||||
|
from rar_exceptions import *
|
||||||
|
|
||||||
|
ERAR_END_ARCHIVE = 10
|
||||||
|
ERAR_NO_MEMORY = 11
|
||||||
|
ERAR_BAD_DATA = 12
|
||||||
|
ERAR_BAD_ARCHIVE = 13
|
||||||
|
ERAR_UNKNOWN_FORMAT = 14
|
||||||
|
ERAR_EOPEN = 15
|
||||||
|
ERAR_ECREATE = 16
|
||||||
|
ERAR_ECLOSE = 17
|
||||||
|
ERAR_EREAD = 18
|
||||||
|
ERAR_EWRITE = 19
|
||||||
|
ERAR_SMALL_BUF = 20
|
||||||
|
ERAR_UNKNOWN = 21
|
||||||
|
|
||||||
|
RAR_OM_LIST = 0
|
||||||
|
RAR_OM_EXTRACT = 1
|
||||||
|
|
||||||
|
RAR_SKIP = 0
|
||||||
|
RAR_TEST = 1
|
||||||
|
RAR_EXTRACT = 2
|
||||||
|
|
||||||
|
RAR_VOL_ASK = 0
|
||||||
|
RAR_VOL_NOTIFY = 1
|
||||||
|
|
||||||
|
RAR_DLL_VERSION = 3
|
||||||
|
|
||||||
|
# enum UNRARCALLBACK_MESSAGES
|
||||||
|
UCM_CHANGEVOLUME = 0
|
||||||
|
UCM_PROCESSDATA = 1
|
||||||
|
UCM_NEEDPASSWORD = 2
|
||||||
|
|
||||||
|
architecture_bits = ctypes.sizeof(ctypes.c_voidp)*8
|
||||||
|
dll_name = "unrar.dll"
|
||||||
|
if architecture_bits == 64:
|
||||||
|
dll_name = "x64\\unrar64.dll"
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
unrar = ctypes.WinDLL(os.path.join(os.path.split(__file__)[0], 'UnRARDLL', dll_name))
|
||||||
|
except WindowsError:
|
||||||
|
unrar = ctypes.WinDLL(dll_name)
|
||||||
|
|
||||||
|
|
||||||
|
class RAROpenArchiveDataEx(ctypes.Structure):
|
||||||
|
def __init__(self, ArcName=None, ArcNameW=u'', OpenMode=RAR_OM_LIST):
|
||||||
|
self.CmtBuf = ctypes.c_buffer(64*1024)
|
||||||
|
ctypes.Structure.__init__(self, ArcName=ArcName, ArcNameW=ArcNameW, OpenMode=OpenMode, _CmtBuf=ctypes.addressof(self.CmtBuf), CmtBufSize=ctypes.sizeof(self.CmtBuf))
|
||||||
|
|
||||||
|
_fields_ = [
|
||||||
|
('ArcName', ctypes.c_char_p),
|
||||||
|
('ArcNameW', ctypes.c_wchar_p),
|
||||||
|
('OpenMode', ctypes.c_uint),
|
||||||
|
('OpenResult', ctypes.c_uint),
|
||||||
|
('_CmtBuf', ctypes.c_voidp),
|
||||||
|
('CmtBufSize', ctypes.c_uint),
|
||||||
|
('CmtSize', ctypes.c_uint),
|
||||||
|
('CmtState', ctypes.c_uint),
|
||||||
|
('Flags', ctypes.c_uint),
|
||||||
|
('Reserved', ctypes.c_uint*32),
|
||||||
|
]
|
||||||
|
|
||||||
|
class RARHeaderDataEx(ctypes.Structure):
|
||||||
|
def __init__(self):
|
||||||
|
self.CmtBuf = ctypes.c_buffer(64*1024)
|
||||||
|
ctypes.Structure.__init__(self, _CmtBuf=ctypes.addressof(self.CmtBuf), CmtBufSize=ctypes.sizeof(self.CmtBuf))
|
||||||
|
|
||||||
|
_fields_ = [
|
||||||
|
('ArcName', ctypes.c_char*1024),
|
||||||
|
('ArcNameW', ctypes.c_wchar*1024),
|
||||||
|
('FileName', ctypes.c_char*1024),
|
||||||
|
('FileNameW', ctypes.c_wchar*1024),
|
||||||
|
('Flags', ctypes.c_uint),
|
||||||
|
('PackSize', ctypes.c_uint),
|
||||||
|
('PackSizeHigh', ctypes.c_uint),
|
||||||
|
('UnpSize', ctypes.c_uint),
|
||||||
|
('UnpSizeHigh', ctypes.c_uint),
|
||||||
|
('HostOS', ctypes.c_uint),
|
||||||
|
('FileCRC', ctypes.c_uint),
|
||||||
|
('FileTime', ctypes.c_uint),
|
||||||
|
('UnpVer', ctypes.c_uint),
|
||||||
|
('Method', ctypes.c_uint),
|
||||||
|
('FileAttr', ctypes.c_uint),
|
||||||
|
('_CmtBuf', ctypes.c_voidp),
|
||||||
|
('CmtBufSize', ctypes.c_uint),
|
||||||
|
('CmtSize', ctypes.c_uint),
|
||||||
|
('CmtState', ctypes.c_uint),
|
||||||
|
('Reserved', ctypes.c_uint*1024),
|
||||||
|
]
|
||||||
|
|
||||||
|
def DosDateTimeToTimeTuple(dosDateTime):
|
||||||
|
"""Convert an MS-DOS format date time to a Python time tuple.
|
||||||
|
"""
|
||||||
|
dosDate = dosDateTime >> 16
|
||||||
|
dosTime = dosDateTime & 0xffff
|
||||||
|
day = dosDate & 0x1f
|
||||||
|
month = (dosDate >> 5) & 0xf
|
||||||
|
year = 1980 + (dosDate >> 9)
|
||||||
|
second = 2*(dosTime & 0x1f)
|
||||||
|
minute = (dosTime >> 5) & 0x3f
|
||||||
|
hour = dosTime >> 11
|
||||||
|
return time.localtime(time.mktime((year, month, day, hour, minute, second, 0, 1, -1)))
|
||||||
|
|
||||||
|
def _wrap(restype, function, argtypes):
|
||||||
|
result = function
|
||||||
|
result.argtypes = argtypes
|
||||||
|
result.restype = restype
|
||||||
|
return result
|
||||||
|
|
||||||
|
RARGetDllVersion = _wrap(ctypes.c_int, unrar.RARGetDllVersion, [])
|
||||||
|
|
||||||
|
RAROpenArchiveEx = _wrap(ctypes.wintypes.HANDLE, unrar.RAROpenArchiveEx, [ctypes.POINTER(RAROpenArchiveDataEx)])
|
||||||
|
|
||||||
|
RARReadHeaderEx = _wrap(ctypes.c_int, unrar.RARReadHeaderEx, [ctypes.wintypes.HANDLE, ctypes.POINTER(RARHeaderDataEx)])
|
||||||
|
|
||||||
|
_RARSetPassword = _wrap(ctypes.c_int, unrar.RARSetPassword, [ctypes.wintypes.HANDLE, ctypes.c_char_p])
|
||||||
|
def RARSetPassword(*args, **kwargs):
|
||||||
|
_RARSetPassword(*args, **kwargs)
|
||||||
|
|
||||||
|
RARProcessFile = _wrap(ctypes.c_int, unrar.RARProcessFile, [ctypes.wintypes.HANDLE, ctypes.c_int, ctypes.c_char_p, ctypes.c_char_p])
|
||||||
|
|
||||||
|
RARCloseArchive = _wrap(ctypes.c_int, unrar.RARCloseArchive, [ctypes.wintypes.HANDLE])
|
||||||
|
|
||||||
|
UNRARCALLBACK = ctypes.WINFUNCTYPE(ctypes.c_int, ctypes.c_uint, ctypes.c_long, ctypes.c_long, ctypes.c_long)
|
||||||
|
RARSetCallback = _wrap(ctypes.c_int, unrar.RARSetCallback, [ctypes.wintypes.HANDLE, UNRARCALLBACK, ctypes.c_long])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
RARExceptions = {
|
||||||
|
ERAR_NO_MEMORY : MemoryError,
|
||||||
|
ERAR_BAD_DATA : ArchiveHeaderBroken,
|
||||||
|
ERAR_BAD_ARCHIVE : InvalidRARArchive,
|
||||||
|
ERAR_EOPEN : FileOpenError,
|
||||||
|
}
|
||||||
|
|
||||||
|
class PassiveReader:
|
||||||
|
"""Used for reading files to memory"""
|
||||||
|
def __init__(self, usercallback = None):
|
||||||
|
self.buf = []
|
||||||
|
self.ucb = usercallback
|
||||||
|
|
||||||
|
def _callback(self, msg, UserData, P1, P2):
|
||||||
|
if msg == UCM_PROCESSDATA:
|
||||||
|
data = (ctypes.c_char*P2).from_address(P1).raw
|
||||||
|
if self.ucb!=None:
|
||||||
|
self.ucb(data)
|
||||||
|
else:
|
||||||
|
self.buf.append(data)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def get_result(self):
|
||||||
|
return ''.join(self.buf)
|
||||||
|
|
||||||
|
class RarInfoIterator(object):
|
||||||
|
def __init__(self, arc):
|
||||||
|
self.arc = arc
|
||||||
|
self.index = 0
|
||||||
|
self.headerData = RARHeaderDataEx()
|
||||||
|
self.res = RARReadHeaderEx(self.arc._handle, ctypes.byref(self.headerData))
|
||||||
|
if self.res==ERAR_BAD_DATA:
|
||||||
|
raise IncorrectRARPassword
|
||||||
|
self.arc.lockStatus = "locked"
|
||||||
|
self.arc.needskip = False
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def next(self):
|
||||||
|
if self.index>0:
|
||||||
|
if self.arc.needskip:
|
||||||
|
RARProcessFile(self.arc._handle, RAR_SKIP, None, None)
|
||||||
|
self.res = RARReadHeaderEx(self.arc._handle, ctypes.byref(self.headerData))
|
||||||
|
|
||||||
|
if self.res:
|
||||||
|
raise StopIteration
|
||||||
|
self.arc.needskip = True
|
||||||
|
|
||||||
|
data = {}
|
||||||
|
data['index'] = self.index
|
||||||
|
data['filename'] = self.headerData.FileName
|
||||||
|
data['datetime'] = DosDateTimeToTimeTuple(self.headerData.FileTime)
|
||||||
|
data['isdir'] = ((self.headerData.Flags & 0xE0) == 0xE0)
|
||||||
|
data['size'] = self.headerData.UnpSize + (self.headerData.UnpSizeHigh << 32)
|
||||||
|
if self.headerData.CmtState == 1:
|
||||||
|
data['comment'] = self.headerData.CmtBuf.value
|
||||||
|
else:
|
||||||
|
data['comment'] = None
|
||||||
|
self.index += 1
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
self.arc.lockStatus = "finished"
|
||||||
|
|
||||||
|
def generate_password_provider(password):
|
||||||
|
def password_provider_callback(msg, UserData, P1, P2):
|
||||||
|
if msg == UCM_NEEDPASSWORD and password!=None:
|
||||||
|
(ctypes.c_char*P2).from_address(P1).value = password
|
||||||
|
return 1
|
||||||
|
return password_provider_callback
|
||||||
|
|
||||||
|
class RarFileImplementation(object):
|
||||||
|
|
||||||
|
def init(self, password=None):
|
||||||
|
self.password = password
|
||||||
|
archiveData = RAROpenArchiveDataEx(ArcNameW=self.archiveName, OpenMode=RAR_OM_EXTRACT)
|
||||||
|
self._handle = RAROpenArchiveEx(ctypes.byref(archiveData))
|
||||||
|
self.c_callback = UNRARCALLBACK(generate_password_provider(self.password))
|
||||||
|
RARSetCallback(self._handle, self.c_callback, 1)
|
||||||
|
|
||||||
|
if archiveData.OpenResult != 0:
|
||||||
|
raise RARExceptions[archiveData.OpenResult]
|
||||||
|
|
||||||
|
if archiveData.CmtState == 1:
|
||||||
|
self.comment = archiveData.CmtBuf.value
|
||||||
|
else:
|
||||||
|
self.comment = None
|
||||||
|
|
||||||
|
if password:
|
||||||
|
RARSetPassword(self._handle, password)
|
||||||
|
|
||||||
|
self.lockStatus = "ready"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def destruct(self):
|
||||||
|
if self._handle and RARCloseArchive:
|
||||||
|
RARCloseArchive(self._handle)
|
||||||
|
|
||||||
|
def make_sure_ready(self):
|
||||||
|
if self.lockStatus == "locked":
|
||||||
|
raise InvalidRARArchiveUsage("cannot execute infoiter() without finishing previous one")
|
||||||
|
if self.lockStatus == "finished":
|
||||||
|
self.destruct()
|
||||||
|
self.init(self.password)
|
||||||
|
|
||||||
|
def infoiter(self):
|
||||||
|
self.make_sure_ready()
|
||||||
|
return RarInfoIterator(self)
|
||||||
|
|
||||||
|
def read_files(self, checker):
|
||||||
|
res = []
|
||||||
|
for info in self.infoiter():
|
||||||
|
if checker(info) and not info.isdir:
|
||||||
|
reader = PassiveReader()
|
||||||
|
c_callback = UNRARCALLBACK(reader._callback)
|
||||||
|
RARSetCallback(self._handle, c_callback, 1)
|
||||||
|
tmpres = RARProcessFile(self._handle, RAR_TEST, None, None)
|
||||||
|
if tmpres==ERAR_BAD_DATA:
|
||||||
|
raise IncorrectRARPassword
|
||||||
|
self.needskip = False
|
||||||
|
res.append((info, reader.get_result()))
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def extract(self, checker, path, withSubpath, overwrite):
|
||||||
|
res = []
|
||||||
|
for info in self.infoiter():
|
||||||
|
checkres = checker(info)
|
||||||
|
if checkres!=False and not info.isdir:
|
||||||
|
if checkres==True:
|
||||||
|
fn = info.filename
|
||||||
|
if not withSubpath:
|
||||||
|
fn = os.path.split(fn)[-1]
|
||||||
|
target = os.path.join(path, fn)
|
||||||
|
else:
|
||||||
|
raise DeprecationWarning, "Condition callbacks returning strings are deprecated and only supported in Windows"
|
||||||
|
target = checkres
|
||||||
|
if overwrite or (not os.path.exists(target)):
|
||||||
|
tmpres = RARProcessFile(self._handle, RAR_EXTRACT, None, target)
|
||||||
|
if tmpres==ERAR_BAD_DATA:
|
||||||
|
raise IncorrectRARPassword
|
||||||
|
|
||||||
|
self.needskip = False
|
||||||
|
res.append(info)
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
Reference in new issue
Block a user