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
docs/_build*
.env*
EGG-INFO
# for bundling
thomas

View File

@@ -1,7 +1,7 @@
# Streaming Plugin
https://github.com/JohnDoee/deluge-streaming
(c)2016 by Anders Jensen <johndoee@tidalstream.org>
(c)2020 by Anders Jensen <johndoee@tridentstream.org>
## Description
@@ -44,12 +44,17 @@ The _allow remote_ option is to allow remote add and stream of torrents.
## Todo
* [x] Add RAR streaming support
* [ ] Better feedback in interface about streams
* [ ] 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)
# 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
## Prerequisite
@@ -102,6 +107,27 @@ List of URL GET Arguments
# 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
* 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
class FailedToStreamException(Exception):
@@ -44,6 +46,8 @@ def stream_torrent(remote_control_url, infohash=None, path=None, wait_for_end_pi
data = r.json()
if data['status'] == 'success':
return data['url']
else:
raise FailedToStreamException('Request failed: %r' % (data, ))
if 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()
if data['status'] == 'success':
return data['url']
else:
raise FailedToStreamException('Request failed: %r' % (data, ))
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"
__author__ = "Anders Jensen"
__author_email__ = "johndoee@tidalstream.org"
__version__ = "0.10.4"
__author_email__ = "johndoee@tridentstream.org"
__version__ = "0.12.2"
__url__ = "https://github.com/JohnDoee/deluge-streaming"
__license__ = "GPLv3"
__description__ = "Enables streaming of files while downloading them."
@@ -71,7 +71,6 @@ REQUIREMENTS_PACKAGES = [
]
REQUIREMENTS_MODULES = [
'six',
'rarfile',
'rfc6266',
]
@@ -96,7 +95,9 @@ setup(
%s = %s:CorePlugin
[deluge.plugin.gtkui]
%s = %s:GtkUIPlugin
[deluge.plugin.gtk3ui]
%s = %s:Gtk3UIPlugin
[deluge.plugin.web]
%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):
def __init__(self, plugin_name):
from core import Core as _plugin_cls
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
from .gtkui import GtkUI as _plugin_cls
self._plugin_cls = _plugin_cls
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):
def __init__(self, plugin_name):
from webui import WebUI as _plugin_cls
from .webui import WebUI as _plugin_cls
self._plugin_cls = _plugin_cls
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.
#
import base64
import json
import logging
import os
@@ -55,15 +56,15 @@ from deluge._libtorrent import lt
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, task, error
from twisted.web import server, client
from twisted.web.resource import Resource as TwistedResource
from thomas import router, Item, OutputBase
from .resource import Resource
from .torrentfile import DelugeTorrentInput
defer.setDebugging(True)
router.register_handler(DelugeTorrentInput.plugin_name, DelugeTorrentInput, True, False, False)
VIDEO_STREAMABLE_EXTENSIONS = ['mkv', 'mp4', 'iso', 'ogg', 'ogm', 'm4v']
@@ -94,6 +95,7 @@ DEFAULT_PREFS = {
'ssl_source': 'daemon',
'ssl_priv_key_path': '',
'ssl_cert_path': '',
'aggressive_prioritizing': False,
}
logger = logging.getLogger(__name__)
@@ -116,7 +118,7 @@ def get_torrent(infohash):
# Ensure file_priorities option is populated.
self.set_file_priorities([])
return self.options["file_priorities"]
return list(self.options["file_priorities"])
torrent = component.get("TorrentManager").torrents.get(infohash, None)
if torrent and not hasattr(torrent, 'get_file_priorities'):
@@ -126,9 +128,10 @@ def get_torrent(infohash):
class Torrent(object):
def __init__(self, torrent_handler, infohash):
def __init__(self, torrent_handler, infohash, aggressive_prioritizing=False):
self.torrent_handler = torrent_handler
self.infohash = infohash
self.aggressive_prioritizing = aggressive_prioritizing
self.filesets = {}
self.readers = {}
@@ -176,10 +179,11 @@ class Torrent(object):
if file_piece_count <= MIN_PIECE_COUNT_FOR_CHAIN_CONSIDERATION:
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_piece = best_reader_from_byte // self.piece_length
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):
if not status and unfinished_piece not in downloading_pieces:
break
@@ -187,13 +191,15 @@ class Torrent(object):
piece_diff = best_reader_piece - unfinished_piece - 1
if unfinished_piece >= best_reader_piece or piece_diff / file_piece_count <= WITHIN_CHAIN_PERCENTAGE:
is_next_in_chain = True
else:
is_next_in_chain = True
if not is_next_in_chain:
logger.debug('Not a next-in-chain piece, setting priority now')
if not is_next_in_chain or self.aggressive_prioritizing:
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.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:
logger.debug('Also setting file to max %r' % (f, ))
file_priorities[f['index']] = MAX_FILE_PRIORITY
@@ -217,7 +223,8 @@ class Torrent(object):
logger.debug('Calling read again to get the real number')
return self.can_read(from_byte)
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):
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))
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']):
i = f['index']
if progress == 1.0:
@@ -307,7 +314,7 @@ class Torrent(object):
else:
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, ))
for fileset_hash, first_file in fileset_ranges.items():
fileset = self.filesets[fileset_hash]
@@ -380,15 +387,25 @@ class Torrent(object):
if fileset_hash not in self.filesets:
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):
def __init__(self, reset_priorities_on_finish):
def __init__(self, reset_priorities_on_finish, aggressive_prioritizing=False):
self.torrents = {}
self.reset_priorities_on_finish = reset_priorities_on_finish
self.aggressive_prioritizing = aggressive_prioritizing
self.alerts = component.get("AlertManager")
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("read_piece_alert", self.on_alert_read_piece)
self.cleanup_looping_call = task.LoopingCall(self.cleanup)
self.cleanup_looping_call.start(60)
@@ -419,6 +436,18 @@ class TorrentHandler(object):
if self.reset_priorities_on_finish:
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):
for torrent in self.torrents.values():
if self.reset_priorities_on_finish:
@@ -487,7 +516,7 @@ class TorrentHandler(object):
def get_torrent(self, infohash):
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]
@defer.inlineCallbacks
@@ -591,9 +620,11 @@ class ServerContextFactory(object):
def getContext(self):
from OpenSSL import SSL
method = getattr(SSL, 'TLSv1_1_METHOD', None)
if method is None:
method = getattr(SSL, 'SSLv23_METHOD', None)
methods_names = ['TLSv1_2_METHOD', 'TLSv1_1_METHOD', 'SSLv23_METHOD']
for method_name in methods_names:
method = getattr(SSL, method_name, None)
if method is not None:
break
ctx = SSL.Context(method)
ctx.use_certificate_file(self._cert_file)
@@ -611,58 +642,61 @@ class StreamResource(Resource):
@defer.inlineCallbacks
def render_POST(self, request):
infohash = request.args.get('infohash')
path = request.args.get('path')
wait_for_end_pieces = bool(request.args.get('wait_for_end_pieces'))
label = request.args.get('label')
infohash = request.args.get(b'infohash')
path = request.args.get(b'path')
wait_for_end_pieces = bool(request.args.get(b'wait_for_end_pieces'))
label = request.args.get(b'label')
if path:
path = path[0]
path = path[0].decode('utf-8')
else:
path = None
if infohash:
infohash = infohash[0]
infohash = infohash[0].decode('utf-8')
else:
infohash = infohash
infohash = None
if label:
label = label[0]
label = label[0].decode('utf-8')
else:
label = None
payload = request.content.read()
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)
defer.returnValue(json.dumps(result))
defer.returnValue(json.dumps(result).encode('utf-8'))
@defer.inlineCallbacks
def render_GET(self, request):
infohash = request.args.get('infohash')
path = request.args.get('path')
wait_for_end_pieces = bool(request.args.get('wait_for_end_pieces'))
infohash = request.args.get(b'infohash')
path = request.args.get(b'path')
wait_for_end_pieces = bool(request.args.get(b'wait_for_end_pieces'))
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:
path = path[0]
path = path[0].decode('utf-8')
else:
path = None
result = yield self.client.stream_torrent(infohash=infohash, filepath_or_index=path, wait_for_end_pieces=wait_for_end_pieces)
defer.returnValue(json.dumps(result))
defer.returnValue(json.dumps(result).encode('utf-8'))
class Core(CorePluginBase):
listening = None
base_url = None
_is_enabled = False
def enable(self):
self._is_enabled = True
self.config = deluge.configmanager.ConfigManager("streaming.conf", DEFAULT_PREFS)
try:
@@ -679,18 +713,18 @@ class Core(CorePluginBase):
self.thomas_http_output = http_output
resource = Resource()
resource.putChild('file', http_output.resource)
resource = TwistedResource()
resource.putChild(b'file', http_output.resource)
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'],
client=self))
base_resource = Resource()
base_resource.putChild('streaming', resource)
base_resource = TwistedResource()
base_resource.putChild(b'streaming', 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")
logger.warning('plugins %s' % (plugin_manager.get_enabled_plugins(), ))
@@ -711,13 +745,19 @@ class Core(CorePluginBase):
try:
self.listening = reactor.listenSSL(self.config['port'], self.site, context, interface=self.config['ip'])
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'
else:
try:
self.listening = reactor.listenTCP(self.config['port'], self.site, interface=self.config['ip'])
except:
self.listening = reactor.listenTCP(self.config['port'], self.site, interface='0.0.0.0')
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']
ip = self.config['ip']
@@ -747,6 +787,11 @@ class Core(CorePluginBase):
@defer.inlineCallbacks
def disable(self):
if not self._is_enabled:
defer.returnValue(None)
self._is_enabled = False
self.site.stopFactory()
self.torrent_handler.shutdown()
self.thomas_http_output.stop()
@@ -783,9 +828,6 @@ class Core(CorePluginBase):
plugin_manager = component.get("CorePluginManager")
return 'WebUi' in plugin_manager.get_enabled_plugins()
def check_config(self):
pass
@export
@defer.inlineCallbacks
def set_config(self, config):
@@ -809,7 +851,7 @@ class Core(CorePluginBase):
@export
@defer.inlineCallbacks
def stream_torrent(self, infohash=None, url=None, filedump=None, filepath_or_index=None, includes_name=False, wait_for_end_pieces=False, 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))
torrent = get_torrent(infohash)
@@ -827,7 +869,7 @@ class Core(CorePluginBase):
core = component.get("Core")
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():
label_plugin = component.get('CorePlugin.Label')
if label not in label_plugin.get_labels():
@@ -851,7 +893,7 @@ class Core(CorePluginBase):
try:
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:
logger.exception('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',
style: 'margin-left: 12px;'
}));
om.bind('aggressive_prioritizing', fieldset.add({
xtype: 'checkbox',
name: 'aggressive_prioritizing',
boxLabel: 'Aggressive prioritizing',
style: 'margin-left: 12px;'
}));
},
onApply: function() {
@@ -349,20 +356,23 @@ StreamingPlugin = Ext.extend(Deluge.Plugin, {
deluge.preferences.addPage(this.prefsPage);
console.log('Streaming plugin loaded');
var doStream = function (tid, fileIndex) {
deluge.client.streaming.stream_torrent(tid, null, null, fileIndex, true, {
var doStream = function (tid, fileIndex, asInline) {
deluge.client.streaming.stream_torrent(tid, null, null, fileIndex, true, false, null, asInline, {
success: function (result) {
console.log('Got result', result);
if (result.status == 'success') {
var url = result.url;
if (result.use_stream_urls) {
url = 'stream+' + url;
if (result.auto_open_stream_urls) {
window.location.assign(url);
return;
if (asInline) {
window.open(result.url, '_blank');
} else {
var url = result.url;
if (result.use_stream_urls) {
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 {
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({
id: 'streamthis',
@@ -377,15 +409,26 @@ StreamingPlugin = Ext.extend(Deluge.Plugin, {
iconCls: 'icon-down',
handler: function (item, event) {
deluge.menus.filePriorities.hide();
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);
}
}
triggerStreamFile(false);
return false;
}
});
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;
}
});
@@ -396,10 +439,7 @@ StreamingPlugin = Ext.extend(Deluge.Plugin, {
iconCls: 'icon-down',
handler: function (item, event) {
deluge.menus.torrent.hide();
var ids = deluge.torrents.getSelectedIds();
if (ids) {
doStream(ids[0]);
}
triggerStreamTorrent(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:
subprocess.Popen(['xdg-open', url])
except OSError:
print 'Unable to open URL %s' % (url, )
print('Unable to open URL %s' % (url, ))
class GtkUI(GtkPluginBase):

View File

@@ -1,3 +1,5 @@
import base64
from twisted.web.resource import Resource as TwistedResource, _computeAllowedMethods
from twisted.web import server
from twisted.internet import defer
@@ -22,7 +24,7 @@ class Resource(TwistedResource):
if auth_header:
auth_header = auth_header.split(' ')
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:
username, password = userpass
if self.username == username and self.password == password:
@@ -30,9 +32,9 @@ class Resource(TwistedResource):
if not authenticated:
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:
# This needs to be here until the deprecated subclasses of the
# below three error resources in twisted.web.error are removed.

View File

@@ -1,17 +1,27 @@
import logging
import mimetypes
import os
import time
import threading
from io import BytesIO
from thomas import InputBase
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'
protocols = []
current_piece_data = None
can_read_to = None
last_available_piece = None
_pos = None
_closed = False
def __init__(self, item, torrent_handler, infohash, offset, path):
self.item = item
@@ -20,6 +30,9 @@ class DelugeTorrentInput(InputBase.find_plugin('file')):
self.infohash = infohash
self.offset = offset
self.path = path
self.piece_buffer = {}
self.requested_pieces = {}
self.piece_consumption_time = []
self.size, self.filename, self.content_type = self.get_info()
def get_info(self):
@@ -33,36 +46,94 @@ class DelugeTorrentInput(InputBase.find_plugin('file')):
if not os.path.exists(self.path):
self.torrent.can_read(self.offset)
def tell(self):
return self._pos
def seek(self, pos):
self.ensure_exists()
super(DelugeTorrentInput, self).seek(pos)
self._pos = pos
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)
def _read(self, num):
data = self.current_piece_data.read(num)
self._pos += len(data)
return data
def read(self, num):
if self.current_piece_data:
data = self._read(num)
if data:
return data
self.ensure_exists()
if not self._open_file:
if self._pos is None:
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()
if self.can_read_to <= tell or self.can_read_to is None:
self.can_read_to = self.torrent.can_read(self.offset + tell) + tell
if self.can_read_to is None or self.can_read_to <= 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:
self._open_file.seek(tell)
current_piece, rest = self.current_piece
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)
if num != real_num:
logger.info('The real number we can read to is %s and not %s at position %s' % (real_num, num, tell))
while self.piece_consumption_time and self.piece_consumption_time[0] < time.time() - PIECE_REQUEST_HISTORY_TIME:
self.piece_consumption_time.pop(0)
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''
data = super(DelugeTorrentInput, self).read(real_num)
return data
for delete_piece in [p for p in self.piece_buffer.keys() if p < current_piece]:
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):
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
# 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 common import get_resource
from .common import get_resource
log = logging.getLogger(__name__)
class WebUI(WebPluginBase):
scripts = [get_resource("streaming.js")]
def enable(self):
pass
def disable(self):
pass