diff --git a/README.md b/README.md index e7ecc7d..991f158 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,10 @@ The _allow remote_ option is to allow remote add and stream of torrents. # Version Info +## Version 0.9.0 +* Few bugfixes +* Added support for Deluge 2 + ## Version 0.8.1 * Fixed some small problems and bugs * better URL execution with GTKUI diff --git a/setup.py b/setup.py index 1f3da80..fff6ac2 100644 --- a/setup.py +++ b/setup.py @@ -40,9 +40,9 @@ from setuptools import setup __plugin_name__ = "Streaming" -__author__ = "John Doee" +__author__ = "Anders Jensen" __author_email__ = "johndoee@tidalstream.org" -__version__ = "0.8.1" +__version__ = "0.9.0" __url__ = "https://github.com/JohnDoee/deluge-streaming" __license__ = "GPLv3" __description__ = "Enables streaming of files while downloading them." diff --git a/streaming/__init__.py b/streaming/__init__.py index f29035c..cccd34e 100644 --- a/streaming/__init__.py +++ b/streaming/__init__.py @@ -39,18 +39,21 @@ from deluge.plugins.init import PluginInitBase + class CorePlugin(PluginInitBase): def __init__(self, plugin_name): from core import Core as _plugin_cls self._plugin_cls = _plugin_cls super(CorePlugin, self).__init__(plugin_name) + class GtkUIPlugin(PluginInitBase): def __init__(self, plugin_name): from gtkui import GtkUI as _plugin_cls self._plugin_cls = _plugin_cls super(GtkUIPlugin, self).__init__(plugin_name) + class WebUIPlugin(PluginInitBase): def __init__(self, plugin_name): from webui import WebUI as _plugin_cls diff --git a/streaming/core.py b/streaming/core.py index e69e23a..a1bead7 100644 --- a/streaming/core.py +++ b/streaming/core.py @@ -50,12 +50,12 @@ from copy import copy from deluge import component, configmanager from deluge._libtorrent import lt -from deluge.core.rpcserver import export, check_ssl_keys +from deluge.core.rpcserver import export from deluge.plugins.pluginbase import CorePluginBase -from twisted.internet import reactor, defer, task +from twisted.internet import reactor, defer from twisted.python import randbytes -from twisted.web import server, resource, static, http, client +from twisted.web import server, resource, static, client from .filelike import FilelikeObjectResource from .resource import Resource @@ -80,11 +80,13 @@ DEFAULT_PREFS = { PRIORITY_INCREASE = 5 + def sleep(seconds): d = defer.Deferred() reactor.callLater(seconds, d.callback, seconds) return d + class ServerContextFactory(object): def __init__(self, cert_file, key_file): self._cert_file = cert_file @@ -103,6 +105,7 @@ class ServerContextFactory(object): ctx.use_privatekey_file(self._key_file) return ctx + class FileServeResource(resource.Resource): isLeaf = True @@ -131,6 +134,7 @@ class FileServeResource(resource.Resource): tfr = f.open() return FilelikeObjectResource(tfr, f.size).render_GET(request) + class StreamResource(Resource): isLeaf = True @@ -180,12 +184,15 @@ class StreamResource(Resource): 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): pass + class UnknownFileException(Exception): pass + class TorrentFileReader(object): def __init__(self, torrent_file): self.torrent_file = torrent_file @@ -207,7 +214,7 @@ class TorrentFileReader(object): 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)) + logger.debug('We can read from local piece from %s size %s from position %s - size of current payload %s' % (read_position, size, self.position, len(self.current_piece_data))) data = self.current_piece_data[read_position:read_position+size] self.position += len(data) @@ -222,7 +229,8 @@ class TorrentFileReader(object): def seek(self, offset, whence=os.SEEK_SET): self.position = offset -class TorrentFile(object): # can be read from, knows about itself + +class TorrentFile(object): # can be read from, knows about itself def __init__(self, torrent, first_piece, last_piece, piece_size, offset, path, full_path, size, index): self.torrent = torrent self.first_piece = first_piece @@ -244,7 +252,6 @@ class TorrentFile(object): # can be read from, knows about itself self.alerts = component.get("AlertManager") - def open(self): """ Returns a filelike object @@ -265,7 +272,7 @@ class TorrentFile(object): # can be read from, knows about itself 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 + return file_progress and file_progress[self.index] == 1.0 def get_piece_info(self, tell): return divmod((self.offset + tell), self.piece_size) @@ -284,7 +291,10 @@ class TorrentFile(object): # can be read from, knows about itself return piece_data = copy(alert.buffer) - self.waiting_pieces[alert.piece].callback(piece_data) + cbs = self.waiting_pieces.pop(alert.piece, []) + + for cb in cbs: + cb.callback(piece_data) @defer.inlineCallbacks def wait_for_end_pieces(self): @@ -307,7 +317,13 @@ class TorrentFile(object): # can be read from, knows about itself defer.returnValue(reader.current_piece_data) if piece not in self.waiting_pieces: - self.waiting_pieces[piece] = defer.Deferred() + created_waiting_defer = True + self.waiting_pieces[piece] = [] + else: + created_waiting_defer = False + + d = defer.Deferred() + self.waiting_pieces[piece].append(d) logger.debug('Waiting for %s' % piece) while not self.torrent.torrent.handle.have_piece(piece): @@ -316,17 +332,17 @@ class TorrentFile(object): # can be read from, knows about itself logger.debug('Did not have piece %i, waiting' % piece) yield sleep(1) - self.torrent.torrent.handle.read_piece(piece) + if created_waiting_defer: + self.torrent.torrent.handle.read_piece(piece) - data = yield self.waiting_pieces[piece] - if piece in self.waiting_pieces: - del self.waiting_pieces[piece] + data = yield d logger.debug('Done waiting for piece %i, returning data' % piece) defer.returnValue(data) def shutdown(self): self.do_shutdown = True + class Torrent(object): def __init__(self, torrent_handler, infohash): self.infohash = infohash @@ -339,8 +355,7 @@ class Torrent(object): 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.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) @@ -419,7 +434,7 @@ class Torrent(object): 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 + 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 @@ -448,7 +463,7 @@ class Torrent(object): for tf in self.torrent_files: tf.shutdown() - def update_piece_priority(self): # if file streamed has reached end, unblacklist all prior pieces + def update_piece_priority(self): # if file streamed has reached end, unblacklist all prior pieces if self.do_shutdown: return @@ -456,13 +471,13 @@ class Torrent(object): 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 + 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 + if f.file_requested: # we expect a piece head to be at start heads.add(f.first_piece) waiting_for_pieces = set() @@ -472,7 +487,7 @@ class Torrent(object): waiting_for_pieces.add(tfr.waiting_for_piece) piece = max(tfr.waiting_for_piece, tfr.current_piece) - if piece is not None: + if piece is not None: heads.add(piece) if not heads: @@ -483,7 +498,6 @@ class Torrent(object): 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 @@ -539,6 +553,7 @@ class Torrent(object): reactor.callLater(1, self.update_piece_priority) + class TorrentHandler(object): def __init__(self, config): self.torrents = {} @@ -576,6 +591,7 @@ class TorrentHandler(object): for torrent in self.torrents.values(): torrent.shutdown() + class Core(CorePluginBase): listening = None base_url = None @@ -610,7 +626,7 @@ class Core(CorePluginBase): 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['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"}) @@ -634,7 +650,7 @@ class Core(CorePluginBase): port = self.config['port'] ip = self.config['ip'] - elif self.config['serve_method'] == 'webui' and self.check_webui(): # this webserver is fubar + elif self.config['serve_method'] == 'webui' and self.check_webui(): # this webserver is fubar plugin_manager = component.get("CorePluginManager") webui_plugin = plugin_manager['WebUi'].plugin @@ -754,4 +770,4 @@ class Core(CorePluginBase): '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(filename)) - }) \ No newline at end of file + }) diff --git a/streaming/filelike.py b/streaming/filelike.py index 651c92d..12069dc 100644 --- a/streaming/filelike.py +++ b/streaming/filelike.py @@ -2,22 +2,18 @@ from twisted.internet import defer from twisted.python import log from twisted.web import http, resource, server, static -# NOTICE! -# All these producers are taken directly from the Twisted Project. -# This is because i needed to make them accept defers. -# /NOTICE! class NoRangeStaticProducer(static.NoRangeStaticProducer): @defer.inlineCallbacks def resumeProducing(self): if not self.request: return - + data = yield defer.maybeDeferred(self.fileObject.read, self.bufferSize) - + if not self.request: return - + if data: # this .write will spin the reactor, calling .doWrite and then # .resumeProducing again, so be prepared for a re-entrant call @@ -27,35 +23,36 @@ class NoRangeStaticProducer(static.NoRangeStaticProducer): self.request.finish() self.stopProducing() + class SingleRangeStaticProducer(static.SingleRangeStaticProducer): @defer.inlineCallbacks def resumeProducing(self): if not self.request: return - - data = yield defer.maybeDeferred(self.fileObject.read, - min(self.bufferSize, self.size - self.bytesWritten)) - + + data = yield defer.maybeDeferred(self.fileObject.read, min(self.bufferSize, self.size - self.bytesWritten)) + if not self.request: return - + if data: self.bytesWritten += len(data) # this .write will spin the reactor, calling .doWrite and then # .resumeProducing again, so be prepared for a re-entrant call self.request.write(data) - + if self.request and self.bytesWritten == self.size: self.request.unregisterProducer() self.request.finish() self.stopProducing() + class MultipleRangeStaticProducer(static.MultipleRangeStaticProducer): @defer.inlineCallbacks def resumeProducing(self): if not self.request: return - + data = [] dataLength = 0 done = False @@ -64,9 +61,7 @@ class MultipleRangeStaticProducer(static.MultipleRangeStaticProducer): dataLength += len(self.partBoundary) data.append(self.partBoundary) self.partBoundary = None - p = yield defer.maybeDeferred(self.fileObject.read, - min(self.bufferSize - dataLength, - self._partSize - self._partBytesWritten)) + p = yield defer.maybeDeferred(self.fileObject.read, min(self.bufferSize - dataLength, self._partSize - self._partBytesWritten)) self._partBytesWritten += len(p) dataLength += len(p) data.append(p) @@ -76,18 +71,19 @@ class MultipleRangeStaticProducer(static.MultipleRangeStaticProducer): except StopIteration: done = True break - + if not self.request: return - + self.request.write(''.join(data)) - + if done: self.request.unregisterProducer() self.request.finish() self.stopProducing() self.request = None + class FilelikeObjectResource(static.File): isLeaf = True contentType = None diff --git a/streaming/gtkui.py b/streaming/gtkui.py index 7daa6ce..ee5ab74 100644 --- a/streaming/gtkui.py +++ b/streaming/gtkui.py @@ -37,7 +37,6 @@ # statement from all source files in the program, then also delete it here. # -import json import gtk import os import subprocess @@ -50,8 +49,7 @@ from deluge.plugins.pluginbase import GtkPluginBase import deluge.component as component import deluge.common -from twisted.internet import reactor, defer, threads -from twisted.web import server, resource +from twisted.internet import defer, threads from common import get_resource @@ -69,6 +67,13 @@ def execute_url(url): class GtkUI(GtkPluginBase): + def get_widget(self, widget_name): + main_window = component.get("MainWindow") + if hasattr(main_window, 'main_glade'): + return main_window.main_glade.get_widget(widget_name) + else: + return main_window.main_builder.get_object(widget_name) + def enable(self): self.glade = gtk.glade.XML(get_resource("config.glade")) @@ -76,7 +81,7 @@ class GtkUI(GtkPluginBase): 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') + file_menu = self.get_widget('menu_file_tab') self.sep = gtk.SeparatorMenuItem() self.item = gtk.MenuItem(_("_Stream this file")) @@ -105,7 +110,7 @@ class GtkUI(GtkPluginBase): 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 = self.get_widget('menu_file_tab') file_menu.remove(self.item) file_menu.remove(self.sep) diff --git a/streaming/resource.py b/streaming/resource.py index 91d7ed2..8eb9792 100644 --- a/streaming/resource.py +++ b/streaming/resource.py @@ -1,22 +1,22 @@ from twisted.web.resource import Resource as TwistedResource, _computeAllowedMethods -from twisted.web import server, error +from twisted.web import server from twisted.internet import defer class Resource(TwistedResource): content_type = 'application/json' - + def __init__(self, username=None, password=None, *args, **kwargs): self.username = username self.password = password TwistedResource.__init__(self, *args, **kwargs) - - def render(self, request): # Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== + + def render(self, request): # Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== """ Adds support for deferred render methods """ auth_header = request.getHeader('Authorization') - + if self.username or self.password: authenticated = False if auth_header: @@ -27,14 +27,13 @@ class Resource(TwistedResource): username, password = userpass if self.username == username and self.password == password: authenticated = True - - + if not authenticated: print auth_header print self.username, self.password request.setResponseCode(401) return 'Unauthorized' - + m = getattr(self, 'render_' + request.method, None) if not m: # This needs to be here until the deprecated subclasses of the @@ -43,18 +42,18 @@ class Resource(TwistedResource): allowedMethods = (getattr(self, 'allowedMethods', 0) or _computeAllowedMethods(self)) raise UnsupportedMethod(allowedMethods) - + result = defer.maybeDeferred(m, request) - + def write_rest(defer_result, request): request.write(defer_result) request.finish() - + def err_rest(defer_result=None): defer_result.printTraceback() request.finish() - + result.addCallback(write_rest, request) result.addErrback(err_rest) - - return server.NOT_DONE_YET \ No newline at end of file + + return server.NOT_DONE_YET diff --git a/streaming/webui.py b/streaming/webui.py index 6017652..f3e6d98 100644 --- a/streaming/webui.py +++ b/streaming/webui.py @@ -44,6 +44,6 @@ from deluge.plugins.pluginbase import WebPluginBase from common import get_resource -class WebUI(WebPluginBase): - scripts = [get_resource("streaming.js")] \ No newline at end of file +class WebUI(WebPluginBase): + scripts = [get_resource("streaming.js")]