20 Commits

Author SHA1 Message Date
Anders Jensen
123962e123 Merge tag '0.12.2' into develop
-
2020-07-31 16:18:56 +02:00
Anders Jensen
435707b379 Merge branch 'release/0.12.2' 2020-07-31 16:18:54 +02:00
Anders Jensen
034f0ef331 updated readmes 2020-07-31 16:18:42 +02:00
Anders Jensen
d0e9780d76 added support for new ssl version 2020-07-31 16:16:57 +02:00
Anders Jensen
bffef417f8 Merge tag '0.12.1' into develop
-
2020-04-22 12:34:42 +02:00
Anders Jensen
30340a25a2 Merge branch 'release/0.12.1' 2020-04-22 12:34:39 +02:00
Anders Jensen
287efadb3e fixed breaking bug 2020-04-22 12:34:25 +02:00
Anders Jensen
d6e7fc83b8 Merge tag '0.12.0' into develop
-
2020-04-21 21:49:58 +02:00
Anders Jensen
b159bc2be5 Merge branch 'release/0.12.0' 2020-04-21 21:49:54 +02:00
Anders Jensen
e8f24b34bb moved to using read piece instead of reading from disk 2020-04-21 21:49:31 +02:00
Anders Jensen
a7fff07379 Merge tag '0.11.0' into develop
Added Deluge 2 support
2019-11-03 09:47:28 +01:00
Anders Jensen
6846f4fc52 Merge branch 'release/0.11.0' 2019-11-03 09:47:24 +01:00
Anders Jensen
b8183d01dd bumped version 2019-11-03 09:47:07 +01:00
Anders Jensen
da05a6de1b added 2/3 egg create script and updated readme 2019-11-03 09:33:01 +01:00
Anders Jensen
d058d1d914 Initial Deluge v2 #22 support and functionality to hopefully fix #23 2019-11-02 21:55:23 +01:00
Anders Jensen
934048dd46 Merge tag '0.10.5' into develop
-
2019-02-17 15:03:40 +01:00
Anders Jensen
fae5c777fc Merge branch 'release/0.10.5' 2019-02-17 15:03:38 +01:00
Anders Jensen
490a083b6e bumped version 2019-02-17 15:03:27 +01:00
Anders Jensen
c314e9381b added support for opening files directly in the browser 2019-02-17 14:59:50 +01:00
Anders Jensen
fb739cedbe Merge tag '0.10.4' into develop
-
2018-09-21 21:43:27 +02:00
16 changed files with 1299 additions and 114 deletions

2
.gitignore vendored
View File

@@ -24,7 +24,7 @@ _trial_temp
*.komodoproject *.komodoproject
docs/_build* docs/_build*
.env* .env*
EGG-INFO
# for bundling # for bundling
thomas thomas

View File

@@ -1,7 +1,7 @@
# Streaming Plugin # Streaming Plugin
https://github.com/JohnDoee/deluge-streaming https://github.com/JohnDoee/deluge-streaming
(c)2016 by Anders Jensen <johndoee@tidalstream.org> (c)2020 by Anders Jensen <johndoee@tridentstream.org>
## Description ## Description
@@ -44,12 +44,17 @@ The _allow remote_ option is to allow remote add and stream of torrents.
## Todo ## Todo
* [x] Add RAR streaming support
* [ ] Better feedback in interface about streams * [ ] Better feedback in interface about streams
* [ ] Better feedback when using API * [ ] Better feedback when using API
* [x] Reverse proxy improvement (e.g. port different than bind port)
* [ ] Fix problems when removing torrent from Deluge (sea of errors) * [ ] Fix problems when removing torrent from Deluge (sea of errors)
# Important Deluge 2 information
While developing the Deluge 2 version of this plugin I hit a few problems that might be visible for you too.
* When shutting down Deluge an exception / error happens every time, this bug is reported.
* Sometimes the Web UI does not load plugins correctly, try restarting Deluge and refresh your browser if this happens.
# HTTP API Usage # HTTP API Usage
## Prerequisite ## Prerequisite
@@ -102,6 +107,27 @@ List of URL GET Arguments
# Version Info # Version Info
## Version 0.12.2
* Added support for TLS 1.2
## Version 0.12.1
* Fixed small breaking bug
## Version 0.12.0
* Moved to reading pieces through Deluge to avoid unflushed data
* Fixed Deluge 2 / libtorrent related bug
## Version 0.11.0
* Initial support for Deluge 2 / Python 3
* Added support for aggressive piece prioritization when it should not be necessary.
* Fixed bug related to paused torrent with no data downloaded.
## Version 0.10.5
* Added support for serving files inline
## Version 0.10.4 ## Version 0.10.4
* Trying to set max priority less as it destroys performance * Trying to set max priority less as it destroys performance

View File

@@ -1,9 +0,0 @@
virtualenv .env-egg
.env-egg/bin/pip install -U thomas
ln -s .env-egg/lib/python2.7/site-packages/thomas .
ln -s .env-egg/lib/python2.7/site-packages/rarfile.py .
ln -s .env-egg/lib/python2.7/site-packages/six.py .
ln -s .env-egg/lib/python2.7/site-packages/rfc6266.py .
ln -s .env-egg/lib/python2.7/site-packages/lepl .
ln -s .env-egg/lib/python2.7/site-packages/pytz .
.env-egg/bin/python setup.py bdist_egg

9
create-egg2.sh Executable file
View File

@@ -0,0 +1,9 @@
virtualenv .env-egg2
.env-egg2/bin/pip install -U thomas
ln -s .env-egg2/lib/python*/site-packages/thomas .
ln -s .env-egg2/lib/python*/site-packages/rarfile.py .
ln -s .env-egg2/lib/python*/site-packages/six.py .
ln -s .env-egg2/lib/python*/site-packages/rfc6266.py .
ln -s .env-egg2/lib/python*/site-packages/lepl .
ln -s .env-egg2/lib/python*/site-packages/pytz .
.env-egg2/bin/python setup.py bdist_egg

9
create-egg3.sh Executable file
View File

@@ -0,0 +1,9 @@
python3 -m venv .env-egg3
.env-egg3/bin/pip install -U thomas
ln -s .env-egg3/lib/python*/site-packages/thomas .
ln -s .env-egg3/lib/python*/site-packages/rarfile.py .
ln -s .env-egg3/lib/python*/site-packages/six.py .
ln -s .env-egg3/lib/python*/site-packages/rfc6266.py .
ln -s .env-egg3/lib/python*/site-packages/lepl .
ln -s .env-egg3/lib/python*/site-packages/pytz .
.env-egg3/bin/python setup.py bdist_egg

View File

@@ -1,3 +1,5 @@
import argparse
import requests import requests
class FailedToStreamException(Exception): class FailedToStreamException(Exception):
@@ -44,6 +46,8 @@ def stream_torrent(remote_control_url, infohash=None, path=None, wait_for_end_pi
data = r.json() data = r.json()
if data['status'] == 'success': if data['status'] == 'success':
return data['url'] return data['url']
else:
raise FailedToStreamException('Request failed: %r' % (data, ))
if torrent_body: if torrent_body:
r = requests.post(url, auth=(username, password), params=params, data=torrent_body) r = requests.post(url, auth=(username, password), params=params, data=torrent_body)
@@ -53,5 +57,40 @@ def stream_torrent(remote_control_url, infohash=None, path=None, wait_for_end_pi
data = r.json() data = r.json()
if data['status'] == 'success': if data['status'] == 'success':
return data['url'] return data['url']
else:
raise FailedToStreamException('Request failed: %r' % (data, ))
raise FailedToStreamException('Streaming was never successful') raise FailedToStreamException('Streaming was never successful')
if __name__ == '__main__':
parser = argparse.ArgumentParser(description="Stream some torrents")
parser.add_argument('url', help="Full API Url including auth info")
parser.add_argument('--infohash', nargs='?', help="Infohash of torrent to stream")
parser.add_argument('--path', nargs='?', help="Path to file within the torrent to stream")
parser.add_argument('--label', nargs='?', help="Label to add the torrent with")
parser.add_argument('--torrent', nargs='?', help="Path to the torrent to stream", type=argparse.FileType(mode='rb'))
parser.add_argument('--skip_wait_for_end_pieces', help="Wait until client downloaded the first and last piece of the torrent", action='store_false')
args = parser.parse_args()
kwargs = {
'remote_control_url': args.url,
'wait_for_end_pieces': args.skip_wait_for_end_pieces
}
if args.infohash:
kwargs['infohash'] = args.infohash
if args.path:
kwargs['path'] = args.path
if args.label:
kwargs['label'] = args.label
if args.torrent:
kwargs['torrent_body'] = args.torrent.read()
result = stream_torrent(**kwargs)
print('URL %s' % (result, ))

View File

@@ -41,8 +41,8 @@ from setuptools import setup, find_packages
__plugin_name__ = "Streaming" __plugin_name__ = "Streaming"
__author__ = "Anders Jensen" __author__ = "Anders Jensen"
__author_email__ = "johndoee@tidalstream.org" __author_email__ = "johndoee@tridentstream.org"
__version__ = "0.10.4" __version__ = "0.12.2"
__url__ = "https://github.com/JohnDoee/deluge-streaming" __url__ = "https://github.com/JohnDoee/deluge-streaming"
__license__ = "GPLv3" __license__ = "GPLv3"
__description__ = "Enables streaming of files while downloading them." __description__ = "Enables streaming of files while downloading them."
@@ -71,7 +71,6 @@ REQUIREMENTS_PACKAGES = [
] ]
REQUIREMENTS_MODULES = [ REQUIREMENTS_MODULES = [
'six',
'rarfile', 'rarfile',
'rfc6266', 'rfc6266',
] ]
@@ -96,7 +95,9 @@ setup(
%s = %s:CorePlugin %s = %s:CorePlugin
[deluge.plugin.gtkui] [deluge.plugin.gtkui]
%s = %s:GtkUIPlugin %s = %s:GtkUIPlugin
[deluge.plugin.gtk3ui]
%s = %s:Gtk3UIPlugin
[deluge.plugin.web] [deluge.plugin.web]
%s = %s:WebUIPlugin %s = %s:WebUIPlugin
""" % ((__plugin_name__, __plugin_name__.lower())*3) """ % ((__plugin_name__, __plugin_name__.lower())*4)
) )

View File

@@ -42,20 +42,27 @@ from deluge.plugins.init import PluginInitBase
class CorePlugin(PluginInitBase): class CorePlugin(PluginInitBase):
def __init__(self, plugin_name): def __init__(self, plugin_name):
from core import Core as _plugin_cls from .core import Core as _plugin_cls
self._plugin_cls = _plugin_cls self._plugin_cls = _plugin_cls
super(CorePlugin, self).__init__(plugin_name) super(CorePlugin, self).__init__(plugin_name)
class GtkUIPlugin(PluginInitBase): class GtkUIPlugin(PluginInitBase):
def __init__(self, plugin_name): def __init__(self, plugin_name):
from gtkui import GtkUI as _plugin_cls from .gtkui import GtkUI as _plugin_cls
self._plugin_cls = _plugin_cls self._plugin_cls = _plugin_cls
super(GtkUIPlugin, self).__init__(plugin_name) super(GtkUIPlugin, self).__init__(plugin_name)
class Gtk3UIPlugin(PluginInitBase):
def __init__(self, plugin_name):
from .gtk3ui import Gtk3UI as PluginClass
self._plugin_cls = PluginClass
super(Gtk3UIPlugin, self).__init__(plugin_name)
class WebUIPlugin(PluginInitBase): class WebUIPlugin(PluginInitBase):
def __init__(self, plugin_name): def __init__(self, plugin_name):
from webui import WebUI as _plugin_cls from .webui import WebUI as _plugin_cls
self._plugin_cls = _plugin_cls self._plugin_cls = _plugin_cls
super(WebUIPlugin, self).__init__(plugin_name) super(WebUIPlugin, self).__init__(plugin_name)

View File

@@ -37,6 +37,7 @@
# statement from all source files in the program, then also delete it here. # statement from all source files in the program, then also delete it here.
# #
import base64
import json import json
import logging import logging
import os import os
@@ -55,15 +56,15 @@ from deluge._libtorrent import lt
from deluge.core.rpcserver import export from deluge.core.rpcserver import export
from deluge.plugins.pluginbase import CorePluginBase from deluge.plugins.pluginbase import CorePluginBase
from twisted.internet import reactor, defer, task from twisted.internet import reactor, defer, task, error
from twisted.web import server, client from twisted.web import server, client
from twisted.web.resource import Resource as TwistedResource
from thomas import router, Item, OutputBase from thomas import router, Item, OutputBase
from .resource import Resource from .resource import Resource
from .torrentfile import DelugeTorrentInput from .torrentfile import DelugeTorrentInput
defer.setDebugging(True)
router.register_handler(DelugeTorrentInput.plugin_name, DelugeTorrentInput, True, False, False) router.register_handler(DelugeTorrentInput.plugin_name, DelugeTorrentInput, True, False, False)
VIDEO_STREAMABLE_EXTENSIONS = ['mkv', 'mp4', 'iso', 'ogg', 'ogm', 'm4v'] VIDEO_STREAMABLE_EXTENSIONS = ['mkv', 'mp4', 'iso', 'ogg', 'ogm', 'm4v']
@@ -94,6 +95,7 @@ DEFAULT_PREFS = {
'ssl_source': 'daemon', 'ssl_source': 'daemon',
'ssl_priv_key_path': '', 'ssl_priv_key_path': '',
'ssl_cert_path': '', 'ssl_cert_path': '',
'aggressive_prioritizing': False,
} }
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -116,7 +118,7 @@ def get_torrent(infohash):
# Ensure file_priorities option is populated. # Ensure file_priorities option is populated.
self.set_file_priorities([]) self.set_file_priorities([])
return self.options["file_priorities"] return list(self.options["file_priorities"])
torrent = component.get("TorrentManager").torrents.get(infohash, None) torrent = component.get("TorrentManager").torrents.get(infohash, None)
if torrent and not hasattr(torrent, 'get_file_priorities'): if torrent and not hasattr(torrent, 'get_file_priorities'):
@@ -126,9 +128,10 @@ def get_torrent(infohash):
class Torrent(object): class Torrent(object):
def __init__(self, torrent_handler, infohash): def __init__(self, torrent_handler, infohash, aggressive_prioritizing=False):
self.torrent_handler = torrent_handler self.torrent_handler = torrent_handler
self.infohash = infohash self.infohash = infohash
self.aggressive_prioritizing = aggressive_prioritizing
self.filesets = {} self.filesets = {}
self.readers = {} self.readers = {}
@@ -176,10 +179,11 @@ class Torrent(object):
if file_piece_count <= MIN_PIECE_COUNT_FOR_CHAIN_CONSIDERATION: if file_piece_count <= MIN_PIECE_COUNT_FOR_CHAIN_CONSIDERATION:
is_next_in_chain = True is_next_in_chain = True
else: elif self.readers:
best_reader_from_byte = max(reader[1] for reader in self.readers.values() if reader[1] <= from_byte) best_reader_from_byte = max(reader[1] for reader in self.readers.values() if reader[1] <= from_byte)
best_reader_piece = best_reader_from_byte // self.piece_length best_reader_piece = best_reader_from_byte // self.piece_length
downloading_pieces = self.get_currently_downloading() downloading_pieces = self.get_currently_downloading()
# TODO: unfinished_piece can be None
for unfinished_piece, status in enumerate(self.torrent.status.pieces[best_reader_piece:], best_reader_piece): for unfinished_piece, status in enumerate(self.torrent.status.pieces[best_reader_piece:], best_reader_piece):
if not status and unfinished_piece not in downloading_pieces: if not status and unfinished_piece not in downloading_pieces:
break break
@@ -187,13 +191,15 @@ class Torrent(object):
piece_diff = best_reader_piece - unfinished_piece - 1 piece_diff = best_reader_piece - unfinished_piece - 1
if unfinished_piece >= best_reader_piece or piece_diff / file_piece_count <= WITHIN_CHAIN_PERCENTAGE: if unfinished_piece >= best_reader_piece or piece_diff / file_piece_count <= WITHIN_CHAIN_PERCENTAGE:
is_next_in_chain = True is_next_in_chain = True
else:
is_next_in_chain = True
if not is_next_in_chain: if not is_next_in_chain or self.aggressive_prioritizing:
logger.debug('Not a next-in-chain piece, setting priority now') logger.debug('Not a next-in-chain piece or aggressive prioritization enabled, setting priority now')
self.torrent.handle.set_piece_deadline(needed_piece, 0) self.torrent.handle.set_piece_deadline(needed_piece, 0)
self.torrent.handle.piece_priority(needed_piece, MAX_PIECE_PRIORITY) self.torrent.handle.piece_priority(needed_piece, MAX_PIECE_PRIORITY)
file_priorities = self.torrent.get_file_priorities() file_priorities = list(self.torrent.get_file_priorities())
if file_priorities[f['index']] != MAX_FILE_PRIORITY: if file_priorities[f['index']] != MAX_FILE_PRIORITY:
logger.debug('Also setting file to max %r' % (f, )) logger.debug('Also setting file to max %r' % (f, ))
file_priorities[f['index']] = MAX_FILE_PRIORITY file_priorities[f['index']] = MAX_FILE_PRIORITY
@@ -217,7 +223,8 @@ class Torrent(object):
logger.debug('Calling read again to get the real number') logger.debug('Calling read again to get the real number')
return self.can_read(from_byte) return self.can_read(from_byte)
else: else:
return ((last_available_piece - needed_piece) * self.piece_length) + self.piece_length - rest logger.debug('Really last available piece is %s' % (last_available_piece, ))
return ((last_available_piece - needed_piece) * self.piece_length) + self.piece_length - rest, last_available_piece
def is_idle(self): def is_idle(self):
return not self.readers and self.last_activity + TORRENT_CLEANUP_INTERVAL < datetime.now() return not self.readers and self.last_activity + TORRENT_CLEANUP_INTERVAL < datetime.now()
@@ -268,7 +275,7 @@ class Torrent(object):
logger.debug('We had a fileset not started, must_whitelist:%r first_files:%r cannot_blacklist:%r' % (must_whitelist, first_files, cannot_blacklist)) logger.debug('We had a fileset not started, must_whitelist:%r first_files:%r cannot_blacklist:%r' % (must_whitelist, first_files, cannot_blacklist))
status = self.torrent.get_status(['files', 'file_progress']) status = self.torrent.get_status(['files', 'file_progress'])
file_priorities = self.torrent.get_file_priorities() file_priorities = list(self.torrent.get_file_priorities())
for f, progress in zip(status['files'], status['file_progress']): for f, progress in zip(status['files'], status['file_progress']):
i = f['index'] i = f['index']
if progress == 1.0: if progress == 1.0:
@@ -307,7 +314,7 @@ class Torrent(object):
else: else:
fileset_ranges[fileset_hash] = fileset['files'].index(path) fileset_ranges[fileset_hash] = fileset['files'].index(path)
file_priorities = self.torrent.get_file_priorities() file_priorities = list(self.torrent.get_file_priorities())
logger.debug('Fileset heads: %r' % (fileset_ranges, )) logger.debug('Fileset heads: %r' % (fileset_ranges, ))
for fileset_hash, first_file in fileset_ranges.items(): for fileset_hash, first_file in fileset_ranges.items():
fileset = self.filesets[fileset_hash] fileset = self.filesets[fileset_hash]
@@ -380,15 +387,25 @@ class Torrent(object):
if fileset_hash not in self.filesets: if fileset_hash not in self.filesets:
self.filesets[fileset_hash] = {'started': False, 'files': files} self.filesets[fileset_hash] = {'started': False, 'files': files}
def request_piece(self, piece):
self.torrent.handle.read_piece(piece)
def new_piece_available(self, piece, data):
logger.debug("New pice available: %s" % (piece, ))
for reader in self.readers.keys():
reader.new_piece_available(piece, data)
class TorrentHandler(object): class TorrentHandler(object):
def __init__(self, reset_priorities_on_finish): def __init__(self, reset_priorities_on_finish, aggressive_prioritizing=False):
self.torrents = {} self.torrents = {}
self.reset_priorities_on_finish = reset_priorities_on_finish self.reset_priorities_on_finish = reset_priorities_on_finish
self.aggressive_prioritizing = aggressive_prioritizing
self.alerts = component.get("AlertManager") self.alerts = component.get("AlertManager")
self.alerts.register_handler("torrent_removed_alert", self.on_alert_torrent_removed) self.alerts.register_handler("torrent_removed_alert", self.on_alert_torrent_removed)
self.alerts.register_handler("torrent_finished_alert", self.on_alert_torrent_finished) self.alerts.register_handler("torrent_finished_alert", self.on_alert_torrent_finished)
self.alerts.register_handler("read_piece_alert", self.on_alert_read_piece)
self.cleanup_looping_call = task.LoopingCall(self.cleanup) self.cleanup_looping_call = task.LoopingCall(self.cleanup)
self.cleanup_looping_call.start(60) self.cleanup_looping_call.start(60)
@@ -419,6 +436,18 @@ class TorrentHandler(object):
if self.reset_priorities_on_finish: if self.reset_priorities_on_finish:
self.torrents[infohash].reset_priorities() self.torrents[infohash].reset_priorities()
def on_alert_read_piece(self, alert):
try:
infohash = str(alert.handle.info_hash())
except (RuntimeError, KeyError):
logger.warning('Failed to handle on read piece alert')
return
if infohash not in self.torrents:
return
self.torrents[infohash].new_piece_available(alert.piece, alert.buffer)
def shutdown(self): def shutdown(self):
for torrent in self.torrents.values(): for torrent in self.torrents.values():
if self.reset_priorities_on_finish: if self.reset_priorities_on_finish:
@@ -487,7 +516,7 @@ class TorrentHandler(object):
def get_torrent(self, infohash): def get_torrent(self, infohash):
if infohash not in self.torrents: if infohash not in self.torrents:
self.torrents[infohash] = Torrent(self, infohash) self.torrents[infohash] = Torrent(self, infohash, self.aggressive_prioritizing)
return self.torrents[infohash] return self.torrents[infohash]
@defer.inlineCallbacks @defer.inlineCallbacks
@@ -591,9 +620,11 @@ class ServerContextFactory(object):
def getContext(self): def getContext(self):
from OpenSSL import SSL from OpenSSL import SSL
method = getattr(SSL, 'TLSv1_1_METHOD', None) methods_names = ['TLSv1_2_METHOD', 'TLSv1_1_METHOD', 'SSLv23_METHOD']
if method is None: for method_name in methods_names:
method = getattr(SSL, 'SSLv23_METHOD', None) method = getattr(SSL, method_name, None)
if method is not None:
break
ctx = SSL.Context(method) ctx = SSL.Context(method)
ctx.use_certificate_file(self._cert_file) ctx.use_certificate_file(self._cert_file)
@@ -611,58 +642,61 @@ class StreamResource(Resource):
@defer.inlineCallbacks @defer.inlineCallbacks
def render_POST(self, request): def render_POST(self, request):
infohash = request.args.get('infohash') infohash = request.args.get(b'infohash')
path = request.args.get('path') path = request.args.get(b'path')
wait_for_end_pieces = bool(request.args.get('wait_for_end_pieces')) wait_for_end_pieces = bool(request.args.get(b'wait_for_end_pieces'))
label = request.args.get('label') label = request.args.get(b'label')
if path: if path:
path = path[0] path = path[0].decode('utf-8')
else: else:
path = None path = None
if infohash: if infohash:
infohash = infohash[0] infohash = infohash[0].decode('utf-8')
else: else:
infohash = infohash infohash = None
if label: if label:
label = label[0] label = label[0].decode('utf-8')
else: else:
label = None label = None
payload = request.content.read() payload = request.content.read()
if not payload: if not payload:
defer.returnValue(json.dumps({'status': 'error', 'message': 'invalid torrent'})) defer.returnValue(json.dumps({'status': 'error', 'message': 'invalid torrent'}).encode('utf-8'))
result = yield self.client.stream_torrent(infohash=infohash, filedump=payload, filepath_or_index=path, wait_for_end_pieces=wait_for_end_pieces, label=label) result = yield self.client.stream_torrent(infohash=infohash, filedump=payload, filepath_or_index=path, wait_for_end_pieces=wait_for_end_pieces, label=label)
defer.returnValue(json.dumps(result)) defer.returnValue(json.dumps(result).encode('utf-8'))
@defer.inlineCallbacks @defer.inlineCallbacks
def render_GET(self, request): def render_GET(self, request):
infohash = request.args.get('infohash') infohash = request.args.get(b'infohash')
path = request.args.get('path') path = request.args.get(b'path')
wait_for_end_pieces = bool(request.args.get('wait_for_end_pieces')) wait_for_end_pieces = bool(request.args.get(b'wait_for_end_pieces'))
if not infohash: if not infohash:
defer.returnValue(json.dumps({'status': 'error', 'message': 'missing infohash'})) defer.returnValue(json.dumps({'status': 'error', 'message': 'missing infohash'}).encode('utf-8'))
infohash = infohash[0] infohash = infohash[0].decode('utf-8')
if path: if path:
path = path[0] path = path[0].decode('utf-8')
else: else:
path = None path = None
result = yield self.client.stream_torrent(infohash=infohash, filepath_or_index=path, wait_for_end_pieces=wait_for_end_pieces) 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)) defer.returnValue(json.dumps(result).encode('utf-8'))
class Core(CorePluginBase): class Core(CorePluginBase):
listening = None listening = None
base_url = None base_url = None
_is_enabled = False
def enable(self): def enable(self):
self._is_enabled = True
self.config = deluge.configmanager.ConfigManager("streaming.conf", DEFAULT_PREFS) self.config = deluge.configmanager.ConfigManager("streaming.conf", DEFAULT_PREFS)
try: try:
@@ -679,18 +713,18 @@ class Core(CorePluginBase):
self.thomas_http_output = http_output self.thomas_http_output = http_output
resource = Resource() resource = TwistedResource()
resource.putChild('file', http_output.resource) resource.putChild(b'file', http_output.resource)
if self.config['allow_remote']: if self.config['allow_remote']:
resource.putChild('stream', StreamResource(username=self.config['remote_username'], resource.putChild(b'stream', StreamResource(username=self.config['remote_username'],
password=self.config['remote_password'], password=self.config['remote_password'],
client=self)) client=self))
base_resource = Resource() base_resource = TwistedResource()
base_resource.putChild('streaming', resource) base_resource.putChild(b'streaming', resource)
self.site = server.Site(base_resource) self.site = server.Site(base_resource)
self.torrent_handler = TorrentHandler(self.config['download_only_streamed'] == False) self.torrent_handler = TorrentHandler(self.config['download_only_streamed'] == False, self.config['aggressive_prioritizing'])
plugin_manager = component.get("CorePluginManager") plugin_manager = component.get("CorePluginManager")
logger.warning('plugins %s' % (plugin_manager.get_enabled_plugins(), )) logger.warning('plugins %s' % (plugin_manager.get_enabled_plugins(), ))
@@ -711,13 +745,19 @@ class Core(CorePluginBase):
try: try:
self.listening = reactor.listenSSL(self.config['port'], self.site, context, interface=self.config['ip']) self.listening = reactor.listenSSL(self.config['port'], self.site, context, interface=self.config['ip'])
except: except:
self.listening = reactor.listenSSL(self.config['port'], self.site, context, interface='0.0.0.0') try:
self.listening = reactor.listenSSL(self.config['port'], self.site, context, interface='0.0.0.0')
except error.CannotListenError:
logger.warning("Unable to listen to anything")
self.base_url += 's' self.base_url += 's'
else: else:
try: try:
self.listening = reactor.listenTCP(self.config['port'], self.site, interface=self.config['ip']) self.listening = reactor.listenTCP(self.config['port'], self.site, interface=self.config['ip'])
except: except:
self.listening = reactor.listenTCP(self.config['port'], self.site, interface='0.0.0.0') try:
self.listening = reactor.listenTCP(self.config['port'], self.site, interface='0.0.0.0')
except error.CannotListenError:
logger.warning("Unable to listen to anything")
port = self.config['port'] port = self.config['port']
ip = self.config['ip'] ip = self.config['ip']
@@ -747,6 +787,11 @@ class Core(CorePluginBase):
@defer.inlineCallbacks @defer.inlineCallbacks
def disable(self): def disable(self):
if not self._is_enabled:
defer.returnValue(None)
self._is_enabled = False
self.site.stopFactory() self.site.stopFactory()
self.torrent_handler.shutdown() self.torrent_handler.shutdown()
self.thomas_http_output.stop() self.thomas_http_output.stop()
@@ -783,9 +828,6 @@ class Core(CorePluginBase):
plugin_manager = component.get("CorePluginManager") plugin_manager = component.get("CorePluginManager")
return 'WebUi' in plugin_manager.get_enabled_plugins() return 'WebUi' in plugin_manager.get_enabled_plugins()
def check_config(self):
pass
@export @export
@defer.inlineCallbacks @defer.inlineCallbacks
def set_config(self, config): def set_config(self, config):
@@ -809,7 +851,7 @@ class Core(CorePluginBase):
@export @export
@defer.inlineCallbacks @defer.inlineCallbacks
def stream_torrent(self, infohash=None, url=None, filedump=None, filepath_or_index=None, includes_name=False, wait_for_end_pieces=False, label=None): def stream_torrent(self, infohash=None, url=None, filedump=None, filepath_or_index=None, includes_name=False, wait_for_end_pieces=False, label=None, as_inline=False):
logger.debug('Trying to stream infohash:%s, url:%s, filepath_or_index:%s' % (infohash, url, filepath_or_index)) logger.debug('Trying to stream infohash:%s, url:%s, filepath_or_index:%s' % (infohash, url, filepath_or_index))
torrent = get_torrent(infohash) torrent = get_torrent(infohash)
@@ -827,7 +869,7 @@ class Core(CorePluginBase):
core = component.get("Core") core = component.get("Core")
try: try:
yield core.add_torrent_file('file.torrent', filedump.encode('base64'), {'add_paused': True}) yield core.add_torrent_file('file.torrent', base64.b64encode(filedump), {'add_paused': True})
if label and 'Label' in component.get('CorePluginManager').get_enabled_plugins(): if label and 'Label' in component.get('CorePluginManager').get_enabled_plugins():
label_plugin = component.get('CorePlugin.Label') label_plugin = component.get('CorePlugin.Label')
if label not in label_plugin.get_labels(): if label not in label_plugin.get_labels():
@@ -851,7 +893,7 @@ class Core(CorePluginBase):
try: try:
stream_or_item = yield defer.maybeDeferred(self.torrent_handler.stream, infohash, fn, wait_for_end_pieces=wait_for_end_pieces) stream_or_item = yield defer.maybeDeferred(self.torrent_handler.stream, infohash, fn, wait_for_end_pieces=wait_for_end_pieces)
stream_url = self.thomas_http_output.serve_item(stream_or_item) stream_url = self.thomas_http_output.serve_item(stream_or_item, as_inline=as_inline)
except: except:
logger.exception('Failed to stream torrent') logger.exception('Failed to stream torrent')
defer.returnValue({'status': 'error', 'message': 'failed to stream torrent'}) defer.returnValue({'status': 'error', 'message': 'failed to stream torrent'})

708
streaming/data/config.ui Normal file
View File

@@ -0,0 +1,708 @@
<?xml version="1.0"?>
<interface>
<!-- interface-requires gtk+ 2.16 -->
<!-- interface-naming-policy toplevel-contextual -->
<object class="GtkWindow" id="window1">
<property name="can_focus">False</property>
<child>
<object class="GtkVBox" id="prefs_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkFrame" id="settings_frame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<property name="shadow_type">none</property>
<child>
<object class="GtkAlignment" id="settings_alignment">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="top_padding">10</property>
<property name="left_padding">12</property>
<child>
<object class="GtkVBox" id="settings_vbox1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
<child>
<object class="GtkVBox" id="settings_vbox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
<child>
<object class="GtkCheckButton" id="input_download_only_streamed">
<property name="label" translatable="yes">Download only streamed files, skip the other files</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="settings_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">&lt;b&gt;Settings&lt;/b&gt;</property>
<property name="use_markup">True</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="serving_frame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<property name="shadow_type">none</property>
<child>
<object class="GtkAlignment" id="settings_alignment1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="top_padding">10</property>
<property name="left_padding">12</property>
<child>
<object class="GtkVBox" id="settings_vbox2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
<child>
<object class="GtkHBox" id="hbox1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkLabel" id="remote_username_label2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Hostname: </property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="input_ip">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="invisible_char">&#x2022;</property>
<property name="invisible_char_set">True</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="primary_icon_sensitive">True</property>
<property name="secondary_icon_sensitive">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkHBox" id="hbox2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkLabel" id="remote_username_label3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Port: </property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="input_port">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="invisible_char">&#x2022;</property>
<property name="invisible_char_set">True</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="primary_icon_sensitive">True</property>
<property name="secondary_icon_sensitive">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkHBox" id="hbox54">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkCheckButton" id="input_reverse_proxy_enabled">
<property name="label" translatable="yes">Enable Reverse Proxy</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkHBox" id="hbox55">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkLabel" id="reverse_proxy_base_url_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Reverse Proxy Base Url: </property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="input_reverse_proxy_base_url">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="primary_icon_sensitive">True</property>
<property name="secondary_icon_sensitive">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<!-- <child>
<object class="GtkRadioButton" id="input_serve_webui">
<property name="label" translatable="yes">Serve files via WebUI</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child> -->
<child>
<object class="GtkVBox" id="settings_vbox3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
<!-- <child>
<object class="GtkRadioButton" id="input_serve_standalone">
<property name="label" translatable="yes">Serve files via standalone</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="draw_indicator">True</property>
<property name="group">input_serve_webui</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child> -->
<child>
<object class="GtkAlignment" id="remote_alignment1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<!-- <property name="left_padding">20</property> -->
<child>
<object class="GtkVBox" id="remote_vbox1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
<child>
<object class="GtkCheckButton" id="input_use_ssl">
<property name="label" translatable="yes">Use SSL</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkAlignment" id="alignment1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="left_padding">20</property>
<child>
<object class="GtkVBox" id="vbox1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkRadioButton" id="input_ssl_cert_daemon">
<property name="label" translatable="yes">Use Daemon/WebUI Certificate</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="input_ssl_cert_custom">
<property name="label" translatable="yes">Custom Certificate</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
<property name="group">input_ssl_cert_daemon</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkAlignment" id="alignment2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="left_padding">20</property>
<child>
<object class="GtkVBox" id="vbox2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkLabel" id="label1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Private key file path</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="input_ssl_priv_key_path">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="invisible_char">&#x2022;</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="primary_icon_sensitive">True</property>
<property name="secondary_icon_sensitive">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Certificate and chains file path</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="input_ssl_cert_path">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="invisible_char">&#x2022;</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="primary_icon_sensitive">True</property>
<property name="secondary_icon_sensitive">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">5</property>
</packing>
</child>
</object>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="serving_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">&lt;b&gt;File Serving Settings&lt;/b&gt;</property>
<property name="use_markup">True</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="settings_frame1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<property name="shadow_type">none</property>
<child>
<object class="GtkAlignment" id="settings_alignment2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="top_padding">10</property>
<property name="left_padding">12</property>
<child>
<object class="GtkVBox" id="settings_vbox4">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
<child>
<object class="GtkVBox" id="settings_vbox5">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
<child>
<object class="GtkCheckButton" id="input_allow_remote">
<property name="label" translatable="yes">Allow remote control</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkAlignment" id="remote_alignment2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="left_padding">32</property>
<child>
<object class="GtkVBox" id="remote_vbox2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
<!-- <child>
<widget class="GtkHBox" id="remote_username_hbox1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
<child>
<widget class="GtkLabel" id="remote_username_label1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Remote control username:</property>
</widget>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<widget class="GtkEntry" id="input_remote_username">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="invisible_char">•</property>
<property name="invisible_char_set">True</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="primary_icon_sensitive">True</property>
<property name="secondary_icon_sensitive">True</property>
</widget>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</widget>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child> -->
<child>
<object class="GtkHBox" id="remote_password_hbox1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel" id="remote_password_label1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Remote control password:</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="input_remote_password">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="visibility">True</property>
<property name="invisible_char">&#x2022;</property>
<property name="invisible_char_set">True</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="primary_icon_sensitive">True</property>
<property name="secondary_icon_sensitive">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkHBox" id="remote_url_hbox1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel" id="remote_url_label1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Remote control url:</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="output_remote_url">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="invisible_char">&#x2022;</property>
<property name="invisible_char_set">True</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="primary_icon_sensitive">True</property>
<property name="secondary_icon_sensitive">True</property>
<property name="editable">False</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="input_use_stream_urls">
<property name="label" translatable="yes">Use stream protocol urls</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="input_auto_open_stream_urls">
<property name="label" translatable="yes">Auto-open stream protocol urls</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="input_aggressive_prioritizing">
<property name="label" translatable="yes">Aggressive prioritizing</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">3</property>
</packing>
</child>
</object>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="settings_label1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">&lt;b&gt;Advanced Settings&lt;/b&gt;</property>
<property name="use_markup">True</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
</interface>

View File

@@ -270,6 +270,13 @@ PreferencePage = Ext.extend(Ext.Panel, {
boxLabel: 'Auto-open stream protocol urls', boxLabel: 'Auto-open stream protocol urls',
style: 'margin-left: 12px;' style: 'margin-left: 12px;'
})); }));
om.bind('aggressive_prioritizing', fieldset.add({
xtype: 'checkbox',
name: 'aggressive_prioritizing',
boxLabel: 'Aggressive prioritizing',
style: 'margin-left: 12px;'
}));
}, },
onApply: function() { onApply: function() {
@@ -349,20 +356,23 @@ StreamingPlugin = Ext.extend(Deluge.Plugin, {
deluge.preferences.addPage(this.prefsPage); deluge.preferences.addPage(this.prefsPage);
console.log('Streaming plugin loaded'); console.log('Streaming plugin loaded');
var doStream = function (tid, fileIndex) { var doStream = function (tid, fileIndex, asInline) {
deluge.client.streaming.stream_torrent(tid, null, null, fileIndex, true, { deluge.client.streaming.stream_torrent(tid, null, null, fileIndex, true, false, null, asInline, {
success: function (result) { success: function (result) {
console.log('Got result', result);
if (result.status == 'success') { if (result.status == 'success') {
var url = result.url; if (asInline) {
if (result.use_stream_urls) { window.open(result.url, '_blank');
url = 'stream+' + url; } else {
if (result.auto_open_stream_urls) { var url = result.url;
window.location.assign(url); if (result.use_stream_urls) {
return; url = 'stream+' + url;
if (result.auto_open_stream_urls) {
window.location.assign(url);
return;
}
} }
Ext.Msg.alert('Stream ready', 'URL for stream: <a target="_blank" href="' + url + '">' + url + '</a>');
} }
Ext.Msg.alert('Stream ready', 'URL for stream: <a target="_blank" href="' + url + '">' + url + '</a>');
} else { } else {
Ext.Msg.alert('Stream failed', 'Error message: ' + result.message); Ext.Msg.alert('Stream failed', 'Error message: ' + result.message);
} }
@@ -370,6 +380,28 @@ StreamingPlugin = Ext.extend(Deluge.Plugin, {
}) })
} }
var triggerStreamFile = function (asInline) {
var files = deluge.details.items.items[2];
var nodes = files.getSelectionModel().getSelectedNodes();
if (nodes) {
var fileIndex = nodes[0].attributes.fileIndex;
var tid = files.torrentId;
if (fileIndex >= 0) {
doStream(tid, fileIndex, asInline);
}
}
}
deluge.menus.filePriorities.addMenuItem({
id: 'playthis',
text: 'Play in browser',
iconCls: 'icon-resume',
handler: function (item, event) {
deluge.menus.filePriorities.hide();
triggerStreamFile(true);
return false;
}
});
deluge.menus.filePriorities.addMenuItem({ deluge.menus.filePriorities.addMenuItem({
id: 'streamthis', id: 'streamthis',
@@ -377,15 +409,26 @@ StreamingPlugin = Ext.extend(Deluge.Plugin, {
iconCls: 'icon-down', iconCls: 'icon-down',
handler: function (item, event) { handler: function (item, event) {
deluge.menus.filePriorities.hide(); deluge.menus.filePriorities.hide();
var files = deluge.details.items.items[2]; triggerStreamFile(false);
var nodes = files.getSelectionModel().getSelectedNodes(); return false;
if (nodes) { }
var fileIndex = nodes[0].attributes.fileIndex; });
var tid = files.torrentId;
if (fileIndex >= 0) {
doStream(tid, fileIndex); var triggerStreamTorrent = function (asInline) {
} var ids = deluge.torrents.getSelectedIds();
} if (ids) {
doStream(ids[0], null, asInline);
}
}
deluge.menus.torrent.addMenuItem({
id: 'playthistorrent',
text: 'Play in browser',
iconCls: 'icon-resume',
handler: function (item, event) {
deluge.menus.torrent.hide();
triggerStreamTorrent(true);
return false; return false;
} }
}); });
@@ -396,10 +439,7 @@ StreamingPlugin = Ext.extend(Deluge.Plugin, {
iconCls: 'icon-down', iconCls: 'icon-down',
handler: function (item, event) { handler: function (item, event) {
deluge.menus.torrent.hide(); deluge.menus.torrent.hide();
var ids = deluge.torrents.getSelectedIds(); triggerStreamTorrent(false);
if (ids) {
doStream(ids[0]);
}
return false; return false;
} }
}); });

234
streaming/gtk3ui.py Normal file
View File

@@ -0,0 +1,234 @@
#
# gtkui.py
#
# Copyright (C) 2009 John Doee <johndoee@tidalstream.org>
#
# Basic plugin template created by:
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
#
# Deluge is free software.
#
# You may redistribute it and/or modify it under the terms of the
# GNU General Public License, as published by the Free Software
# Foundation; either version 3 of the License, or (at your option)
# any later version.
#
# deluge is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with deluge. If not, write to:
# The Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor
# Boston, MA 02110-1301, USA.
#
# In addition, as a special exception, the copyright holders give
# permission to link the code of portions of this program with the OpenSSL
# library.
# You must obey the GNU General Public License in all respects for all of
# the code used other than OpenSSL. If you modify file(s) with this
# exception, you may extend this exception to your version of the file(s),
# but you are not obligated to do so. If you do not wish to do so, delete
# this exception statement from your version. If you delete this exception
# statement from all source files in the program, then also delete it here.
#
import logging
import os
import sys
import subprocess
from gi.repository import Gtk
from gi.repository.Gtk import MenuItem, SeparatorMenuItem
import deluge.component as component
from deluge.plugins.pluginbase import Gtk3PluginBase
from deluge.ui.client import client
from deluge.ui.gtk3 import dialogs
from twisted.internet import defer, threads
from .common import get_resource
log = logging.getLogger(__name__)
def execute_url(url):
if sys.platform == 'win32':
os.startfile(url)
elif sys.platform == 'darwin':
subprocess.Popen(['open', url])
else:
try:
subprocess.Popen(['xdg-open', url])
except OSError:
print('Unable to open URL %s' % (url, ))
class Gtk3UI(Gtk3PluginBase):
def enable(self):
self.builder = Gtk.Builder()
self.builder.add_from_file(get_resource('config.ui'))
component.get('Preferences').add_page('Streaming', self.builder.get_object('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 = self.get_widget('menu_file_tab')
self.sep = SeparatorMenuItem()
self.item = 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 = SeparatorMenuItem()
self.item_torrentmenu = 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()
def disable(self):
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 = self.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)
def get_widget(self, widget_name):
main_builder = component.get('MainWindow').get_builder()
return main_builder.get_object(widget_name)
@defer.inlineCallbacks
def on_apply_prefs(self):
log.debug("applying prefs for Streaming")
serve_method = 'standalone'
# if self.builder.get_object("input_serve_standalone").get_active():
# serve_method = 'standalone'
# elif self.builder.get_object("input_serve_webui").get_active():
# serve_method = 'webui'
if self.builder.get_object("input_ssl_cert_daemon").get_active():
ssl_source = 'daemon'
elif self.builder.get_object("input_ssl_cert_custom").get_active():
ssl_source = 'custom'
config = {
"ip": self.builder.get_object("input_ip").get_text(),
"port": int(self.builder.get_object("input_port").get_text()),
"use_stream_urls": self.builder.get_object("input_use_stream_urls").get_active(),
"auto_open_stream_urls": self.builder.get_object("input_auto_open_stream_urls").get_active(),
"aggressive_prioritizing": self.builder.get_object("input_aggressive_prioritizing").get_active(),
"allow_remote": self.builder.get_object("input_allow_remote").get_active(),
"download_only_streamed": self.builder.get_object("input_download_only_streamed").get_active(),
"reverse_proxy_enabled": self.builder.get_object("input_reverse_proxy_enabled").get_active(),
# "download_in_order": self.builder.get_object("input_download_in_order").get_active(),
"use_ssl": self.builder.get_object("input_use_ssl").get_active(),
# "remote_username": self.builder.get_object("input_remote_username").get_text(),
"reverse_proxy_base_url": self.builder.get_object("input_reverse_proxy_base_url").get_text(),
"remote_password": self.builder.get_object("input_remote_password").get_text(),
"ssl_priv_key_path": self.builder.get_object("input_ssl_priv_key_path").get_text(),
"ssl_cert_path": self.builder.get_object("input_ssl_cert_path").get_text(),
"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):
client.streaming.get_config().addCallback(self.cb_get_config)
def cb_get_config(self, config):
"""callback for on show_prefs"""
self.builder.get_object("input_ip").set_text(config["ip"])
self.builder.get_object("input_port").set_text(str(config["port"]))
self.builder.get_object("input_use_stream_urls").set_active(config["use_stream_urls"])
self.builder.get_object("input_auto_open_stream_urls").set_active(config["auto_open_stream_urls"])
self.builder.get_object("input_aggressive_prioritizing").set_active(config["aggressive_prioritizing"])
self.builder.get_object("input_allow_remote").set_active(config["allow_remote"])
self.builder.get_object("input_use_ssl").set_active(config["use_ssl"])
self.builder.get_object("input_download_only_streamed").set_active(config["download_only_streamed"])
self.builder.get_object("input_reverse_proxy_enabled").set_active(config["reverse_proxy_enabled"])
# self.builder.get_object("input_download_in_order").set_active(config["download_in_order"])
# self.builder.get_object("input_download_everything").set_active(not config["download_in_order"] and not config["download_only_streamed"])
# self.builder.get_object("input_remote_username").set_text(config["remote_username"])
self.builder.get_object("input_reverse_proxy_base_url").set_text(config["reverse_proxy_base_url"])
self.builder.get_object("input_remote_password").set_text(config["remote_password"])
self.builder.get_object("input_ssl_priv_key_path").set_text(config["ssl_priv_key_path"])
self.builder.get_object("input_ssl_cert_path").set_text(config["ssl_cert_path"])
# self.builder.get_object("input_serve_standalone").set_active(config["serve_method"] == "standalone")
# self.builder.get_object("input_serve_webui").set_active(config["serve_method"] == "webui")
self.builder.get_object("input_ssl_cert_daemon").set_active(config["ssl_source"] == "daemon")
self.builder.get_object("input_ssl_cert_custom").set_active(config["ssl_source"] == "custom")
api_url = 'http%s://%s:%s@%s:%s/streaming/stream' % (('s' if config["use_ssl"] else ''), config["remote_username"], config["remote_password"], config["ip"], config["port"])
self.builder.get_object("output_remote_url").set_text(api_url)
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)
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 stream_ready(self, result):
if result['status'] == 'success':
if result.get('use_stream_urls', False):
url = "stream+%s" % result['url']
if result.get('auto_open_stream_urls', False):
threads.deferToThread(execute_url, url)
else:
def on_dialog_callback(response):
if response == Gtk.ResponseType.YES:
threads.deferToThread(execute_url, url)
dialogs.YesNoDialog('Stream ready', 'Do you want to play the video?').run().addCallback(on_dialog_callback)
else:
dialogs.ErrorDialog('Stream ready', 'Copy the link into a media player', details=result['url']).run()
else:
dialogs.ErrorDialog('Stream failed', 'Was unable to prepare the stream', details=result).run()

View File

@@ -63,7 +63,7 @@ def execute_url(url):
try: try:
subprocess.Popen(['xdg-open', url]) subprocess.Popen(['xdg-open', url])
except OSError: except OSError:
print 'Unable to open URL %s' % (url, ) print('Unable to open URL %s' % (url, ))
class GtkUI(GtkPluginBase): class GtkUI(GtkPluginBase):

View File

@@ -1,3 +1,5 @@
import base64
from twisted.web.resource import Resource as TwistedResource, _computeAllowedMethods from twisted.web.resource import Resource as TwistedResource, _computeAllowedMethods
from twisted.web import server from twisted.web import server
from twisted.internet import defer from twisted.internet import defer
@@ -22,7 +24,7 @@ class Resource(TwistedResource):
if auth_header: if auth_header:
auth_header = auth_header.split(' ') auth_header = auth_header.split(' ')
if len(auth_header) > 1 and auth_header[0] == 'Basic': if len(auth_header) > 1 and auth_header[0] == 'Basic':
userpass = auth_header[1].decode('base64').split(':') userpass = base64.b64decode(auth_header[1].encode('utf-8')).decode('utf-8').split(':')
if len(userpass) == 2: if len(userpass) == 2:
username, password = userpass username, password = userpass
if self.username == username and self.password == password: if self.username == username and self.password == password:
@@ -30,9 +32,9 @@ class Resource(TwistedResource):
if not authenticated: if not authenticated:
request.setResponseCode(401) request.setResponseCode(401)
return 'Unauthorized' return b'Unauthorized'
m = getattr(self, 'render_' + request.method, None) m = getattr(self, 'render_' + request.method.decode('utf-8'), None)
if not m: if not m:
# This needs to be here until the deprecated subclasses of the # This needs to be here until the deprecated subclasses of the
# below three error resources in twisted.web.error are removed. # below three error resources in twisted.web.error are removed.

View File

@@ -1,17 +1,27 @@
import logging import logging
import mimetypes import mimetypes
import os import os
import time
import threading
from io import BytesIO
from thomas import InputBase from thomas import InputBase
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
PIECE_REQUEST_HISTORY_TIME = 10
MAX_PIECE_REQUEST_COUNT = 20
class DelugeTorrentInput(InputBase.find_plugin('file')): class DelugeTorrentInput(InputBase):
plugin_name = 'torrent_file' plugin_name = 'torrent_file'
protocols = [] protocols = []
current_piece_data = None
can_read_to = None can_read_to = None
last_available_piece = None
_pos = None
_closed = False
def __init__(self, item, torrent_handler, infohash, offset, path): def __init__(self, item, torrent_handler, infohash, offset, path):
self.item = item self.item = item
@@ -20,6 +30,9 @@ class DelugeTorrentInput(InputBase.find_plugin('file')):
self.infohash = infohash self.infohash = infohash
self.offset = offset self.offset = offset
self.path = path self.path = path
self.piece_buffer = {}
self.requested_pieces = {}
self.piece_consumption_time = []
self.size, self.filename, self.content_type = self.get_info() self.size, self.filename, self.content_type = self.get_info()
def get_info(self): def get_info(self):
@@ -33,36 +46,94 @@ class DelugeTorrentInput(InputBase.find_plugin('file')):
if not os.path.exists(self.path): if not os.path.exists(self.path):
self.torrent.can_read(self.offset) self.torrent.can_read(self.offset)
def tell(self):
return self._pos
def seek(self, pos): def seek(self, pos):
self.ensure_exists() self.ensure_exists()
super(DelugeTorrentInput, self).seek(pos) self._pos = pos
logger.debug('Seeking at %s torrentfile_id %r' % (self.tell(), id(self))) logger.debug('Seeking at %s torrentfile_id %r' % (self.tell(), id(self)))
self.torrent.add_reader(self, self.item.path, self.offset + self.tell(), self.offset + self.size) self.torrent.add_reader(self, self.item.path, self.offset + self.tell(), self.offset + self.size)
def _read(self, num):
data = self.current_piece_data.read(num)
self._pos += len(data)
return data
def read(self, num): def read(self, num):
if self.current_piece_data:
data = self._read(num)
if data:
return data
self.ensure_exists() self.ensure_exists()
if not self._open_file: if self._pos is None:
self.seek(0) self.seek(0)
#logger.debug('Trying to read %s from %i torrentfile_id %r' % (self.path, self.tell(), id(self))) logger.debug('Trying to read %s from %i torrentfile_id %r' % (self.path, self.tell(), id(self)))
tell = self.tell() tell = self.tell()
if self.can_read_to <= tell or self.can_read_to is None: if self.can_read_to is None or self.can_read_to <= tell:
self.can_read_to = self.torrent.can_read(self.offset + tell) + tell can_read_result = self.torrent.can_read(self.offset + tell)
self.last_available_piece = can_read_result[1]
self.can_read_to = can_read_result[0] + tell
if self._open_file: current_piece, rest = self.current_piece
self._open_file.seek(tell) logger.debug('Calculated last available piece is %s offset %s can_read_to %s piece_length %s' % (self.last_available_piece, self.offset, self.can_read_to, self.torrent.piece_length))
real_num = min(num, self.can_read_to - tell) while self.piece_consumption_time and self.piece_consumption_time[0] < time.time() - PIECE_REQUEST_HISTORY_TIME:
if num != real_num: self.piece_consumption_time.pop(0)
logger.info('The real number we can read to is %s and not %s at position %s' % (real_num, num, tell))
if not self._open_file: # the file was closed while we waited max_piece_count = (self.last_available_piece - current_piece) + 1
pieces_to_request = min(min(max(2, len(self.piece_consumption_time)), max_piece_count), MAX_PIECE_REQUEST_COUNT)
logger.debug('New piece request status pieces_to_request: %s piece_consumption_time: %s max_piece_count: %s' % (pieces_to_request, len(self.piece_consumption_time), max_piece_count, ))
logger.debug('Requested pieces: %r' % (self.requested_pieces.items()))
logger.debug('Piece buffer: %r' % (self.piece_buffer.keys()))
for piece in range(current_piece, current_piece + pieces_to_request):
if piece in self.requested_pieces:
continue
logger.debug('Requesting piece %s' % (piece, ))
self.requested_pieces[piece] = threading.Event()
self.torrent.request_piece(piece)
for _ in range(1000):
if self.requested_pieces[current_piece].wait(1):
break
if self._closed:
return b''
else:
return b'' return b''
data = super(DelugeTorrentInput, self).read(real_num) for delete_piece in [p for p in self.piece_buffer.keys() if p < current_piece]:
return data del self.piece_buffer[delete_piece]
for delete_piece in [p for p in self.requested_pieces.keys() if p < current_piece]:
del self.requested_pieces[delete_piece]
self.current_piece_data = self.piece_buffer[current_piece]
self.current_piece_data.seek(rest)
self.piece_consumption_time.append(time.time())
logger.debug('Returning %s bytes' % (num, ))
return self._read(num)
@property
def current_piece(self):
from_byte = self.offset + self.tell()
piece_length = self.torrent.piece_length
piece, rest = divmod(from_byte, piece_length)
return piece, rest
def new_piece_available(self, piece, data):
if piece not in self.requested_pieces or self.requested_pieces[piece].is_set():
return
logger.debug("Setting data for piece %s" % (piece, ))
self.piece_buffer[piece] = BytesIO(data)
self.requested_pieces[piece].set()
def close(self): def close(self):
self.torrent.remove_reader(self) self.torrent.remove_reader(self)
super(DelugeTorrentInput, self).close() self._closed = True

View File

@@ -36,14 +36,20 @@
# this exception statement from your version. If you delete this exception # this exception statement from your version. If you delete this exception
# statement from all source files in the program, then also delete it here. # statement from all source files in the program, then also delete it here.
# #
import logging
from deluge.log import LOG as log
from deluge.ui.client import client
from deluge import component
from deluge.plugins.pluginbase import WebPluginBase from deluge.plugins.pluginbase import WebPluginBase
from common import get_resource from .common import get_resource
log = logging.getLogger(__name__)
class WebUI(WebPluginBase): class WebUI(WebPluginBase):
scripts = [get_resource("streaming.js")] scripts = [get_resource("streaming.js")]
def enable(self):
pass
def disable(self):
pass