From e494dad9bf49f5555c1cfd5981c2b032a319aa05 Mon Sep 17 00:00:00 2001 From: JohnDoee Date: Sat, 12 Aug 2017 13:27:32 +0200 Subject: [PATCH] better remote control --- README.md | 3 + setup.py | 2 +- streaming/core.py | 314 ++++++++++++++++++++---------------- streaming/data/config.glade | 42 +++++ streaming/data/streaming.js | 93 ++++++----- streaming/gtkui.py | 63 ++++---- 6 files changed, 307 insertions(+), 210 deletions(-) diff --git a/README.md b/README.md index 52690e9..0d94612 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,9 @@ The _allow remote_ option is to allow remote add and stream of torrents. # Version Info +## Version 0.8.0 +* Improved remote control of streaming to make it work as originally intended. + ## Version 0.7.1 * Trying to fix bug where piece buffer went empty * Added support for SSL. diff --git a/setup.py b/setup.py index 5306bf3..c173556 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ from setuptools import setup __plugin_name__ = "Streaming" __author__ = "John Doee" __author_email__ = "johndoee@tidalstream.org" -__version__ = "0.7.1" +__version__ = "0.8.0" __url__ = "https://github.com/JohnDoee/deluge-streaming" __license__ = "GPLv3" __description__ = "Enables streaming of files while downloading them." diff --git a/streaming/core.py b/streaming/core.py index 347dfaa..13697a5 100644 --- a/streaming/core.py +++ b/streaming/core.py @@ -89,14 +89,14 @@ class ServerContextFactory(object): def __init__(self, cert_file, key_file): self._cert_file = cert_file self._key_file = key_file - + def getContext(self): from OpenSSL import SSL - + method = getattr(SSL, 'TLSv1_1_METHOD', None) if method is None: method = getattr(SSL, 'SSLv23_METHOD', None) - + ctx = SSL.Context(method) ctx.use_certificate_file(self._cert_file) ctx.use_certificate_chain_file(self._cert_file) @@ -105,25 +105,25 @@ class ServerContextFactory(object): class FileServeResource(resource.Resource): isLeaf = True - + def __init__(self): self.file_mapping = {} resource.Resource.__init__(self) def generate_secure_token(self): return base64.urlsafe_b64encode(randbytes.RandomFactory().secureRandom(21, True)) - + def add_file(self, path): token = self.generate_secure_token() self.file_mapping[token] = path - + return token - + def render_GET(self, request): key = request.postpath[0] if key not in self.file_mapping: return resource.NoResource().render(request) - + f = self.file_mapping[key] if f.is_complete(): return static.File(f.full_path).render_GET(request) @@ -133,23 +133,51 @@ class FileServeResource(resource.Resource): class StreamResource(Resource): isLeaf = True - + def __init__(self, client, *args, **kwargs): self.client = client Resource.__init__(self, *args, **kwargs) - + + @defer.inlineCallbacks + def render_POST(self, request): + infohash = request.args.get('infohash') + path = request.args.get('path') + wait_for_end_pieces = bool(request.args.get('wait_for_end_pieces')) + + if path: + path = path[0] + else: + path = None + + if infohash: + infohash = infohash[0] + else: + infohash = infohash + + payload = request.content.read() + if not payload: + defer.returnValue(json.dumps({'status': 'error', 'message': 'invalid torrent'})) + + result = yield self.client.stream_torrent(infohash=infohash, filedump=payload, filepath_or_index=path, wait_for_end_pieces=wait_for_end_pieces) + defer.returnValue(json.dumps(result)) + @defer.inlineCallbacks def render_GET(self, request): - infohash = request.args.get('infohash', None) - path = request.args.get('path', None) - - if infohash is None: + infohash = request.args.get('infohash') + path = request.args.get('path') + wait_for_end_pieces = bool(request.args.get('wait_for_end_pieces')) + + if not infohash: defer.returnValue(json.dumps({'status': 'error', 'message': 'missing infohash'})) - - if path is None: - defer.returnValue(json.dumps({'status': 'error', 'message': 'missing path'})) - - result = yield self.client.stream_torrent(infohash[0], path[0]) + + infohash = infohash[0] + + if path: + path = path[0] + else: + path = None + + result = yield self.client.stream_torrent(infohash=infohash, filepath_or_index=path, wait_for_end_pieces=wait_for_end_pieces) defer.returnValue(json.dumps(result)) class UnknownTorrentException(Exception): @@ -163,34 +191,34 @@ class TorrentFileReader(object): self.torrent_file = torrent_file self.size = torrent_file.size self.position = 0 - + self.waiting_for_piece = None self.current_piece = None self.current_piece_data = None - + @defer.inlineCallbacks def read(self, size=1024): required_piece, read_position = self.torrent_file.get_piece_info(self.position) - + if self.current_piece != required_piece: logger.debug('We are missing piece %i and it is required, requesting' % (required_piece, )) self.waiting_for_piece = required_piece self.current_piece_data = yield self.torrent_file.get_piece_data(required_piece) self.current_piece = required_piece self.waiting_for_piece = None - + logger.debug('We can read from local piece from %s size %s from position %s' % (read_position, size, self.position)) data = self.current_piece_data[read_position:read_position+size] self.position += len(data) - + defer.returnValue(data) - + def tell(self): return self.position - + def close(self): self.torrent_file.close(self) - + def seek(self, offset, whence=os.SEEK_SET): self.position = offset @@ -205,7 +233,7 @@ class TorrentFile(object): # can be read from, knows about itself self.size = size self.full_path = full_path self.index = index - + self.file_requested = False self.file_requested_once = False self.do_shutdown = False @@ -213,10 +241,10 @@ class TorrentFile(object): # can be read from, knows about itself self.waiting_pieces = {} self.current_readers = [] self.registered_alert = False - + self.alerts = component.get("AlertManager") - - + + def open(self): """ Returns a filelike object @@ -224,78 +252,78 @@ class TorrentFile(object): # can be read from, knows about itself if not self.registered_alert: self.alerts.register_handler("read_piece_alert", self.on_alert_got_piece_data) self.registered_alert = True - + tfr = TorrentFileReader(self) self.current_readers.append(tfr) self.file_requested = False - + return tfr - + def close(self, tfr): self.current_readers.remove(tfr) - + def is_complete(self): torrent_status = self.torrent.torrent.get_status(['file_progress', 'state']) file_progress = torrent_status['file_progress'] return file_progress[self.index] == 1.0 - + def get_piece_info(self, tell): return divmod((self.offset + tell), self.piece_size) - + def on_alert_got_piece_data(self, alert): torrent_id = str(alert.handle.info_hash()) if torrent_id != self.torrent.infohash: return - + logger.debug('Got piece data for piece %s' % alert.piece) if alert.piece not in self.waiting_pieces: logger.debug('Got data for piece %i, but no data needed for this piece?' % alert.piece) return - + if alert.buffer is None: return - + piece_data = copy(alert.buffer) self.waiting_pieces[alert.piece].callback(piece_data) - + @defer.inlineCallbacks def wait_for_end_pieces(self): handle = self.torrent.torrent.handle for piece in [self.first_piece, self.last_piece]: handle.set_piece_deadline(piece, 0) handle.piece_priority(piece, 7) - + while not handle.have_piece(self.first_piece) and not handle.have_piece(self.last_piece): if self.do_shutdown: raise Exception('Shutting down') logger.debug('Did not have piece %i, waiting' % piece) yield sleep(1) - + @defer.inlineCallbacks def get_piece_data(self, piece): logger.debug('Trying to get piece data for piece %s' % piece) for reader in self.current_readers: if reader.current_piece == piece: defer.returnValue(reader.current_piece_data) - + if piece not in self.waiting_pieces: self.waiting_pieces[piece] = defer.Deferred() - + logger.debug('Waiting for %s' % piece) while not self.torrent.torrent.handle.have_piece(piece): if self.do_shutdown: raise Exception('Shutting down') logger.debug('Did not have piece %i, waiting' % piece) yield sleep(1) - + self.torrent.torrent.handle.read_piece(piece) - + data = yield self.waiting_pieces[piece] if piece in self.waiting_pieces: del self.waiting_pieces[piece] logger.debug('Done waiting for piece %i, returning data' % piece) defer.returnValue(data) - + def shutdown(self): self.do_shutdown = True @@ -304,51 +332,51 @@ class Torrent(object): self.infohash = infohash self.torrent = component.get("TorrentManager").torrents.get(infohash, None) self.torrent_handler = torrent_handler - + if not self.torrent: raise UnknownTorrentException('%s is not a known infohash' % infohash) - + self.torrent_files = None self.priority_increased = defaultdict(set) self.do_shutdown = False self.torrent_released = True # set to True if all the files are set to download - - + + self.populate_files() self.file_priorities = [0] * len(self.torrent_files) - + self.last_piece = self.torrent_files[-1].last_piece self.torrent.handle.set_sequential_download(True) self.torrent.handle.set_priority(1) reactor.callLater(0, self.update_piece_priority) - + def populate_files(self): self.torrent_files = [] - + status = self.torrent.get_status(['piece_length', 'files', 'save_path']) piece_length = status['piece_length'] files = status['files'] save_path = status['save_path'] - + for f in files: first_piece = f['offset'] / piece_length last_piece = (f['offset'] + f['size']) / piece_length full_path = os.path.join(save_path, f['path']) - + self.torrent_files.append(TorrentFile(self, first_piece, last_piece, piece_length, f['offset'], f['path'], full_path, f['size'], f['index'])) - + return files - + def find_file(self, file_or_index=None, includes_name=False): best_file = None biggest_file_size = 0 - + for i, f in enumerate(self.torrent_files): path = f.path if not includes_name and '/' in path: path = '/'.join(path.split('/')[1:]) - + logger.debug('Testing file %r against %s / %r' % (file_or_index, i, path)) if file_or_index is not None: if i == file_or_index or path == file_or_index: @@ -358,115 +386,115 @@ class Torrent(object): if f.size > biggest_file_size: best_file = f biggest_file_size = f.size - + return best_file - + def get_file(self, file_or_index=None, includes_name=False): f = self.find_file(file_or_index, includes_name) if f is None: raise UnknownFileException('Was unable to find %s' % file_or_index) - + return f - + def get_currently_downloading(self): currently_downloading = set() for peer in self.torrent.handle.get_peer_info(): if peer.downloading_piece_index != -1: currently_downloading.add(peer.downloading_piece_index) - + return currently_downloading - + def get_torrent_file(self, file_or_index, includes_name): f = self.get_file(file_or_index, includes_name) f.file_requested = True f.file_requested_once = True - + self.torrent.resume() - + should_update_priorities = False if self.file_priorities[f.index] == 0: self.file_priorities[f.index] = 3 should_update_priorities = True - + if self.torrent_released: should_update_priorities = True - + if should_update_priorities and not f.is_complete(): # Need to do this stuff on seek too self.torrent.set_file_priorities(self.file_priorities) - + return f - + def shutdown(self): logger.info('Shutting down torrent %s' % (self.infohash, )) - + self.torrent.handle.set_priority(0) - + for piece, status in enumerate(self.torrent.status.pieces[0:self.last_piece+1]): if status: continue - + priority = self.torrent.handle.piece_priority(piece) if priority == 0: self.torrent.handle.piece_priority(piece, 1) - + if not self.torrent_handler.config['download_only_streamed']: logger.debug('Resetting file priorities') file_priorities = [(1 if fp == 0 else fp) for fp in self.file_priorities] self.torrent.set_file_priorities(file_priorities) - + self.do_shutdown = True self.torrent_handler.remove_torrent(self.infohash) - + for tf in self.torrent_files: tf.shutdown() - + def update_piece_priority(self): # if file streamed has reached end, unblacklist all prior pieces if self.do_shutdown: return - + logger.debug('Updating piece priority for %s' % (self.infohash, )) currently_downloading = self.get_currently_downloading() - + for f in self.torrent_files: if not f.file_requested and not f.current_readers: # nobody wants the file and nobody is watching continue - + logger.debug('Rescheduling file %s' % (f.path, )) - + heads = set() if f.file_requested: # we expect a piece head to be at start heads.add(f.first_piece) - + waiting_for_pieces = set() - + for tfr in f.current_readers: if tfr.waiting_for_piece is not None: waiting_for_pieces.add(tfr.waiting_for_piece) - + piece = max(tfr.waiting_for_piece, tfr.current_piece) if piece is not None: heads.add(piece) - + if not heads: continue - + first_head = min(heads) - + for head_piece in heads: priority_increased = 0 for piece, status in enumerate(self.torrent.status.pieces[head_piece:f.last_piece+1], head_piece): #logger.debug('Checking status for %s/%s/%s/%s' % (head_piece, piece, status, self.torrent.handle.piece_priority(piece))) if status or piece in currently_downloading: continue - + priority = self.torrent.handle.piece_priority(piece) if priority_increased < PRIORITY_INCREASE: priority_increased += 1 - + if piece in waiting_for_pieces: if priority < 7: logger.debug('setting priority for %s to 7 with deadline 0' % (piece, )) - + self.torrent.handle.set_piece_deadline(piece, 0) self.torrent.handle.piece_priority(piece, 7) elif priority < 6: @@ -474,17 +502,17 @@ class Torrent(object): logger.debug('setting priority for %s to 6 with deadline %s' % (piece, deadline, )) self.torrent.handle.piece_priority(piece, 6) self.torrent.handle.set_piece_deadline(piece, deadline) - + elif priority == 0: self.torrent.handle.piece_priority(piece, 1) - + if head_piece == first_head: if priority_increased < PRIORITY_INCREASE: logger.debug('Everything we need has been scheduled, looking for pieces across file to unblacklist') for piece, status in enumerate(self.torrent.status.pieces[f.first_piece:f.last_piece+1], f.first_piece): if status: continue - + priority = self.torrent.handle.piece_priority(piece) if priority == 0: self.torrent.handle.piece_priority(piece, 1) @@ -493,11 +521,11 @@ class Torrent(object): for piece, status in enumerate(self.torrent.status.pieces[f.first_piece:first_head], f.first_piece): if status or piece in currently_downloading: continue - + if self.torrent.handle.piece_priority(piece) != 0: logger.debug('Blacklisting %i' % (piece, )) self.torrent.handle.piece_priority(piece, 0) - + found_requested = False for f in self.torrent_files: if f.file_requested_once: @@ -508,40 +536,40 @@ class Torrent(object): if found_requested: logger.debug('Nobody is currently using %s, shutting down torrent-handler' % (self.infohash, )) self.shutdown() - + reactor.callLater(1, self.update_piece_priority) class TorrentHandler(object): def __init__(self, config): self.torrents = {} self.config = config - + self.alerts = component.get("AlertManager") self.alerts.register_handler("torrent_removed_alert", self.on_alert_torrent_removed) - + def get_stream(self, infohash, file_or_index=None, includes_name=False): logger.info('Trying to stream infohash %s and file %s include_name %s' % (infohash, file_or_index, includes_name)) if infohash not in self.torrents: self.torrents[infohash] = Torrent(self, infohash) - + return self.torrents[infohash].get_torrent_file(file_or_index, includes_name) - + def on_alert_torrent_removed(self, alert): try: torrent_id = str(alert.handle.info_hash()) except (RuntimeError, KeyError): logger.warning('Failed to handle on torrent remove alert') return - + if torrent_id not in self.torrents: return - + self.torrents[torrent_id].shutdown() self.remove_torrent(torrent_id) - + def remove_torrent(self, torrent_id): del self.torrents[torrent_id] - + def shutdown(self): logger.debug('Shutting down TorrentHandler') self.alerts.deregister_handler(self.on_alert_torrent_removed) @@ -551,10 +579,10 @@ class TorrentHandler(object): class Core(CorePluginBase): listening = None base_url = None - + def enable(self): self.config = deluge.configmanager.ConfigManager("streaming.conf", DEFAULT_PREFS) - + try: session = component.get("Core").session settings = session.get_settings() @@ -562,7 +590,7 @@ class Core(CorePluginBase): session.set_settings(settings) except AttributeError: logger.warning('Unable to exclude partial pieces') - + self.fsr = FileServeResource() resource = Resource() resource.putChild('file', self.fsr) @@ -570,29 +598,29 @@ class Core(CorePluginBase): resource.putChild('stream', StreamResource(username=self.config['remote_username'], password=self.config['remote_password'], client=self)) - + base_resource = Resource() base_resource.putChild('streaming', resource) self.site = server.Site(base_resource) - + self.torrent_handler = TorrentHandler(self.config) - + plugin_manager = component.get("CorePluginManager") logger.warning('plugins %s' % (plugin_manager.get_enabled_plugins(), )) - + self.base_url = 'http' if self.config['serve_method'] == 'standalone': if self.config['use_ssl'] and self.check_ssl(): # use default deluge (or webui), input custom if self.config['ssl_source'] == 'daemon': web_config = configmanager.ConfigManager("web.conf", {"pkey": "ssl/daemon.pkey", "cert": "ssl/daemon.cert"}) - + context = ServerContextFactory(configmanager.get_config_dir(web_config['cert']), configmanager.get_config_dir(web_config['pkey'])) elif self.config['ssl_source'] == 'custom': context = ServerContextFactory(self.config['ssl_cert_path'], self.config['ssl_priv_key_path']) - + try: self.listening = reactor.listenSSL(self.config['port'], self.site, context, interface=self.config['ip']) except: @@ -603,22 +631,22 @@ class Core(CorePluginBase): self.listening = reactor.listenTCP(self.config['port'], self.site, interface=self.config['ip']) except: self.listening = reactor.listenTCP(self.config['port'], self.site, interface='0.0.0.0') - + port = self.config['port'] ip = self.config['ip'] elif self.config['serve_method'] == 'webui': # this webserver is fubar plugin_manager = component.get("CorePluginManager") - + webui_plugin = plugin_manager['WebUi'].plugin webui_plugin.server.top_level.putChild('streaming', resource) - + port = webui_plugin.server.port ip = getattr(webui_plugin.server, 'interface', None) or self.config['ip'] if webui_plugin.server.https: self.base_url += 's' else: raise NotImplementedError() - + self.base_url += '://' if ':' in ip: self.base_url += ip @@ -629,15 +657,15 @@ class Core(CorePluginBase): def disable(self): self.site.stopFactory() self.torrent_handler.shutdown() - + plugin_manager = component.get("CorePluginManager") webui_plugin = plugin_manager['WebUi'].plugin - + try: webui_plugin.server.top_level.delEntity('streaming') except KeyError: pass - + if self.listening: yield self.listening.stopListening() self.listening = None @@ -648,34 +676,34 @@ class Core(CorePluginBase): def check_ssl(self): if self.config['ssl_source'] == 'daemon': return True - + if not os.path.isfile(self.config['ssl_priv_key_path']) or not os.access(self.config['ssl_priv_key_path'], os.R_OK): return False - + if not os.path.isfile(self.config['ssl_cert_path']) or not os.access(self.config['ssl_cert_path'], os.R_OK): return False - + return True - + def check_webui(self): plugin_manager = component.get("CorePluginManager") return 'WebUi' in plugin_manager.get_enabled_plugins() - + def check_config(self): pass - + @export @defer.inlineCallbacks def set_config(self, config): self.previous_config = copy(self.config) - + for key in config.keys(): self.config[key] = config[key] self.config.save() - + yield self.disable() self.enable() - + if self.config['serve_method'] == 'standalone' and self.config['ssl_source'] == 'custom' and self.config['use_ssl']: if not self.check_ssl(): defer.returnValue(('error', 'ssl', 'SSL not enabled, make sure the private key and certificate exist and are accessible')) @@ -684,43 +712,45 @@ class Core(CorePluginBase): def get_config(self): """Returns the config dictionary""" return self.config.config - + @export @defer.inlineCallbacks def stream_torrent(self, infohash=None, url=None, filedump=None, filepath_or_index=None, includes_name=False, wait_for_end_pieces=False): tor = component.get("TorrentManager").torrents.get(infohash, None) - + if tor is None: logger.info('Did not find torrent, must add it') - + if not filedump and url: filedump = yield client.getPage(url) - + if not filedump: defer.returnValue({'status': 'error', 'message': 'unable to find torrent, provide infohash, url or filedump'}) - + torrent_info = lt.torrent_info(lt.bdecode(filedump)) infohash = str(torrent_info.info_hash()) - + core = component.get("Core") try: yield core.add_torrent_file('file.torrent', filedump.encode('base64'), {'add_paused': True}) except: defer.returnValue({'status': 'error', 'message': 'failed to add torrent'}) - + try: tf = self.torrent_handler.get_stream(infohash, filepath_or_index, includes_name) except UnknownTorrentException: defer.returnValue({'status': 'error', 'message': 'unable to find torrent, probably failed to add it'}) - + if wait_for_end_pieces: logger.debug('Waiting for end pieces') yield tf.wait_for_end_pieces() - + + filename = os.path.basename(tf.path).encode('utf-8') defer.returnValue({ 'status': 'success', + 'filename': filename, 'use_stream_urls': self.config['use_stream_urls'], 'auto_open_stream_urls': self.config['auto_open_stream_urls'], 'url': '%s/streaming/file/%s/%s' % (self.base_url, self.fsr.add_file(tf), - urllib.quote_plus(os.path.basename(tf.path).encode('utf-8'))) + urllib.quote_plus(filename)) }) \ No newline at end of file diff --git a/streaming/data/config.glade b/streaming/data/config.glade index 4632d93..6bd46b8 100644 --- a/streaming/data/config.glade +++ b/streaming/data/config.glade @@ -519,6 +519,48 @@ 1 + + + True + False + 5 + + + True + False + Remote control url: + + + False + False + 0 + + + + + True + True + + True + False + False + True + True + False + + + True + True + 1 + + + + + False + False + 2 + + diff --git a/streaming/data/streaming.js b/streaming/data/streaming.js index cab454e..ca50034 100644 --- a/streaming/data/streaming.js +++ b/streaming/data/streaming.js @@ -37,13 +37,13 @@ PreferencePage = Ext.extend(Ext.Panel, { layout: 'form', autoScroll: true, _fields: {}, - + initComponent: function() { PreferencePage.superclass.initComponent.call(this); - + var om = this.optionsManager = new Deluge.OptionsManager(); this.on('show', this.onPageShow, this); - + var fieldset = this.add({ xtype: 'fieldset', border: false, @@ -56,13 +56,13 @@ PreferencePage = Ext.extend(Ext.Panel, { width: 180, } }); - + om.bind('download_only_streamed', fieldset.add({ xtype: 'checkbox', name: 'download_only_streamed', boxLabel: 'Download only streamed files, skip the other files', })); - + fieldset = this.add({ xtype: 'fieldset', border: false, @@ -75,12 +75,12 @@ PreferencePage = Ext.extend(Ext.Panel, { width: 180, } }); - + om.bind('ip', fieldset.add({ name: 'ip', fieldLabel: 'Hostname', })); - + om.bind('port', fieldset.add({ name: 'port', fieldLabel: _('Port'), @@ -88,7 +88,7 @@ PreferencePage = Ext.extend(Ext.Panel, { minValue: -1, maxValue: 99999, })); - + fieldset = this.add({ xtype: 'fieldset', border: false, @@ -98,16 +98,16 @@ PreferencePage = Ext.extend(Ext.Panel, { width: 240, labelWidth: 1 }); - + this._fields['serve_method_webui'] = fieldset.add({ name: 'serve_method', boxLabel: 'Serve files via WebUI', inputValue: 'webui', disabled: true }); - + om.bind('serve_method', this._fields['serve_method_webui']); - + this._fields['serve_method_standalone'] = fieldset.add({ name: 'serve_method', boxLabel: 'Serve files via standalone', @@ -115,15 +115,15 @@ PreferencePage = Ext.extend(Ext.Panel, { disabled: true }); om.bind('serve_method', this._fields['serve_method_standalone']); - - + + om.bind('use_ssl', fieldset.add({ xtype: 'checkbox', name: 'use_ssl', boxLabel: 'Use SSL', style: 'margin-left: 12px;' })); - + fieldset = this.add({ xtype: 'fieldset', border: false, @@ -133,7 +133,7 @@ PreferencePage = Ext.extend(Ext.Panel, { width: 240, labelWidth: 1 }); - + this._fields['ssl_source_daemon'] = fieldset.add({ name: 'ssl_source', boxLabel: 'Use Daemon/WebUI Certificate', @@ -141,7 +141,7 @@ PreferencePage = Ext.extend(Ext.Panel, { value: 'daemon' }) om.bind('ssl_source', this._fields['ssl_source_daemon']); - + this._fields['ssl_source_custom'] = fieldset.add({ name: 'ssl_source', boxLabel: 'Custom Certificate', @@ -149,7 +149,7 @@ PreferencePage = Ext.extend(Ext.Panel, { value: 'custom' }); om.bind('ssl_source', this._fields['ssl_source_custom']); - + fieldset = this.add({ xtype: 'fieldset', border: false, @@ -161,17 +161,17 @@ PreferencePage = Ext.extend(Ext.Panel, { width: 130, } }); - + om.bind('ssl_priv_key_path', fieldset.add({ name: 'ssl_priv_key_path', fieldLabel: 'Private key file path' })); - + om.bind('ssl_cert_path', fieldset.add({ name: 'ssl_cert_path', fieldLabel: 'Certificate and chains file path' })); - + fieldset = this.add({ xtype: 'fieldset', border: false, @@ -184,14 +184,14 @@ PreferencePage = Ext.extend(Ext.Panel, { width: 180, } }); - + om.bind('allow_remote', fieldset.add({ xtype: 'checkbox', name: 'allow_remote', boxLabel: 'Allow remote control', style: 'margin-left: 12px;' })); - + fieldset = this.add({ xtype: 'fieldset', border: false, @@ -203,19 +203,28 @@ PreferencePage = Ext.extend(Ext.Panel, { width: 180, } }); - + om.bind('remote_username', fieldset.add({ xtype: 'textfield', name: 'remote_username', fieldLabel: 'Remote control username' })); - + om.bind('remote_password', fieldset.add({ xtype: 'textfield', + inputType: 'password', name: 'remote_password', fieldLabel: 'Remote control password' })); - + + fieldset.add({ + xtype: 'textfield', + id: 'remote_url', + name: 'remote_url', + readOnly: true, + fieldLabel: 'Remote control url' + }); + fieldset = this.add({ xtype: 'fieldset', border: false, @@ -227,14 +236,14 @@ PreferencePage = Ext.extend(Ext.Panel, { width: 180, } }); - + om.bind('use_stream_urls', fieldset.add({ xtype: 'checkbox', name: 'use_stream_urls', boxLabel: 'Use stream urls', style: 'margin-left: 12px;' })); - + om.bind('auto_open_stream_urls', fieldset.add({ xtype: 'checkbox', name: 'auto_open_stream_urls', @@ -244,7 +253,7 @@ PreferencePage = Ext.extend(Ext.Panel, { }, onApply: function() { - + var changed = this.optionsManager.getDirty(); for (var key in this._fields) { if (this._fields.hasOwnProperty(key)) { @@ -259,13 +268,13 @@ PreferencePage = Ext.extend(Ext.Panel, { success: this.onSetConfig, scope: this }); - + for (var key in deluge.config) { deluge.config[key] = this.optionsManager.get(key); } } }, - + onSetConfig: function(result) { this.optionsManager.commit(); if (result) { @@ -280,17 +289,27 @@ PreferencePage = Ext.extend(Ext.Panel, { Ext.Msg.alert(topic, message); } } + this.updateRemoteUrl(this.optionsManager); }, - + onGotConfig: function(config) { this.optionsManager.set(config); + this.updateRemoteUrl(this.optionsManager); }, - + onPageShow: function() { deluge.client.streaming.get_config({ success: this.onGotConfig, scope: this }) + }, + + updateRemoteUrl: function(optionsManager) { + var apiUrl = 'http'; + if (optionsManager.get('use_ssl')) + apiUrl += 's'; + apiUrl += '://' + optionsManager.get('ip') + ':' + optionsManager.get('port') + '/streaming/stream'; + Ext.getCmp('remote_url').setValue(apiUrl); } }); @@ -299,7 +318,7 @@ StreamingPlugin = Ext.extend(Deluge.Plugin, { onDisable: function() { deluge.menus.filePriorities.remove('streamthis'); - + deluge.preferences.selectPage(_('Plugins')); deluge.preferences.removePage(this.prefsPage); this.prefsPage.destroy(); @@ -308,7 +327,7 @@ StreamingPlugin = Ext.extend(Deluge.Plugin, { onEnable: function() { this.prefsPage = new PreferencePage(); deluge.preferences.addPage(this.prefsPage); - + console.log('Streaming plugin loaded'); var doStream = function (tid, fileIndex) { deluge.client.streaming.stream_torrent(tid, null, null, fileIndex, true, { @@ -330,8 +349,8 @@ StreamingPlugin = Ext.extend(Deluge.Plugin, { } }) } - - + + deluge.menus.filePriorities.addMenuItem({ id: 'streamthis', text: 'Stream this file', @@ -350,7 +369,7 @@ StreamingPlugin = Ext.extend(Deluge.Plugin, { return false; } }); - + deluge.menus.torrent.addMenuItem({ id: 'streamthistorrent', text: 'Stream this torrent', diff --git a/streaming/gtkui.py b/streaming/gtkui.py index 5efef0b..6e5b930 100644 --- a/streaming/gtkui.py +++ b/streaming/gtkui.py @@ -56,31 +56,31 @@ from common import get_resource class LocalAddResource(resource.Resource): gtkui = None isLeaf = True - + def __init__(self, gtkui): self.gtkui = gtkui resource.Resource.__init__(self) - + def render_GET(self, request): useragent = request.getHeader('User-Agent') if 'Deluge-Streamer' not in useragent: request.setResponseCode(401) return 'Unauthorized' - + torrent_url = request.args.get('url', None) if not torrent_url: return json.dumps({'status': 'error', 'message': 'missing url in request'}) - + torrent_file = request.args.get('file', None) if torrent_file: torrent_file = torrent_file[0] - + infohash = request.args.get('infohash', None) if infohash: infohash = infohash[0] - + client.streaming.stream_torrent(url=torrent_url[0], infohash=infohash, filepath_or_index=torrent_file).addCallback(self.gtkui.stream_ready) - + return json.dumps({'status': 'ok', 'message': 'queued'}) class GtkUI(GtkPluginBase): @@ -90,31 +90,31 @@ class GtkUI(GtkPluginBase): component.get("Preferences").add_page("Streaming", self.glade.get_widget("prefs_box")) component.get("PluginManager").register_hook("on_apply_prefs", self.on_apply_prefs) component.get("PluginManager").register_hook("on_show_prefs", self.on_show_prefs) - + file_menu = component.get("MainWindow").main_glade.get_widget('menu_file_tab') - + self.sep = gtk.SeparatorMenuItem() self.item = gtk.MenuItem(_("_Stream this file")) self.item.connect("activate", self.on_menuitem_stream) - + file_menu.append(self.sep) file_menu.append(self.item) self.sep.show() self.item.show() - + torrentmenu = component.get("MenuBar").torrentmenu - + self.sep_torrentmenu = gtk.SeparatorMenuItem() self.item_torrentmenu = gtk.MenuItem(_("_Stream this torrent")) self.item_torrentmenu.connect("activate", self.on_torrentmenu_menuitem_stream) - + torrentmenu.append(self.sep_torrentmenu) torrentmenu.append(self.item_torrentmenu) self.sep_torrentmenu.show() self.item_torrentmenu.show() - + self.resource = LocalAddResource(self) self.site = server.Site(self.resource) self.listening = reactor.listenTCP(40747, self.site, interface='127.0.0.1') @@ -124,34 +124,34 @@ class GtkUI(GtkPluginBase): component.get("Preferences").remove_page("Streaming") component.get("PluginManager").deregister_hook("on_apply_prefs", self.on_apply_prefs) component.get("PluginManager").deregister_hook("on_show_prefs", self.on_show_prefs) - + file_menu = component.get("MainWindow").main_glade.get_widget('menu_file_tab') file_menu.remove(self.item) file_menu.remove(self.sep) - + torrentmenu = component.get("MenuBar").torrentmenu - + torrentmenu.remove(self.item_torrentmenu) torrentmenu.remove(self.sep_torrentmenu) - + self.site.stopFactory() yield self.listening.stopListening() @defer.inlineCallbacks def on_apply_prefs(self): log.debug("applying prefs for Streaming") - + if self.glade.get_widget("input_serve_standalone").get_active(): serve_method = 'standalone' elif self.glade.get_widget("input_serve_webui").get_active(): serve_method = 'webui' - + if self.glade.get_widget("input_ssl_cert_daemon").get_active(): ssl_source = 'daemon' elif self.glade.get_widget("input_ssl_cert_custom").get_active(): ssl_source = 'custom' - + config = { "ip": self.glade.get_widget("input_ip").get_text(), "port": int(self.glade.get_widget("input_port").get_text()), @@ -167,16 +167,16 @@ class GtkUI(GtkPluginBase): "serve_method": serve_method, "ssl_source": ssl_source, } - + result = yield client.streaming.set_config(config) - + if result: message_type, message_class, message = result if message_type == 'error': topic = 'Unknown error type' if message_class == 'ssl': topic = 'SSL Failed' - + dialogs.ErrorDialog(topic, message).run() def on_show_prefs(self): @@ -195,13 +195,16 @@ class GtkUI(GtkPluginBase): self.glade.get_widget("input_remote_password").set_text(config["remote_password"]) self.glade.get_widget("input_ssl_priv_key_path").set_text(config["ssl_priv_key_path"]) self.glade.get_widget("input_ssl_cert_path").set_text(config["ssl_cert_path"]) - + self.glade.get_widget("input_serve_standalone").set_active(config["serve_method"] == "standalone") self.glade.get_widget("input_serve_webui").set_active(config["serve_method"] == "webui") - + self.glade.get_widget("input_ssl_cert_daemon").set_active(config["ssl_source"] == "daemon") self.glade.get_widget("input_ssl_cert_custom").set_active(config["ssl_source"] == "custom") + api_url = 'http%s://%s:%s/streaming/stream' % (('s' if config["use_ssl"] else ''), config["ip"], config["port"]) + self.glade.get_widget("output_remote_url").set_text(api_url) + def stream_ready(self, result): if result['status'] == 'success': if result.get('use_stream_urls', False): @@ -217,19 +220,19 @@ class GtkUI(GtkPluginBase): def on_menuitem_stream(self, data=None): torrent_id = component.get("TorrentView").get_selected_torrents()[0] - + ft = component.get("TorrentDetails").tabs['Files'] paths = ft.listview.get_selection().get_selected_rows()[1] - + selected = [] for path in paths: selected.append(ft.treestore.get_iter(path)) - + for select in selected: path = ft.get_file_path(select) client.streaming.stream_torrent(infohash=torrent_id, filepath_or_index=path, includes_name=True).addCallback(self.stream_ready) break - + def on_torrentmenu_menuitem_stream(self, data=None): torrent_id = component.get("TorrentView").get_selected_torrents()[0] client.streaming.stream_torrent(infohash=torrent_id).addCallback(self.stream_ready)