mirror of
https://github.com/JohnDoee/deluge-streaming/
synced 2026-07-01 07:31:17 -07:00
Merge branch 'release/0.9.0'
This commit is contained in:
@@ -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
|
||||
|
||||
4
setup.py
4
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."
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
return server.NOT_DONE_YET
|
||||
|
||||
@@ -44,6 +44,6 @@ from deluge.plugins.pluginbase import WebPluginBase
|
||||
|
||||
from common import get_resource
|
||||
|
||||
class WebUI(WebPluginBase):
|
||||
|
||||
scripts = [get_resource("streaming.js")]
|
||||
class WebUI(WebPluginBase):
|
||||
scripts = [get_resource("streaming.js")]
|
||||
|
||||
Reference in New Issue
Block a user