mirror of
https://github.com/JohnDoee/deluge-streaming/
synced 2026-07-01 07:31:17 -07:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
123962e123 | ||
|
|
435707b379 | ||
|
|
034f0ef331 | ||
|
|
d0e9780d76 | ||
|
|
bffef417f8 | ||
|
|
30340a25a2 | ||
|
|
287efadb3e | ||
|
|
d6e7fc83b8 | ||
|
|
b159bc2be5 | ||
|
|
e8f24b34bb | ||
|
|
a7fff07379 | ||
|
|
6846f4fc52 | ||
|
|
b8183d01dd | ||
|
|
da05a6de1b | ||
|
|
d058d1d914 | ||
|
|
934048dd46 | ||
|
|
fae5c777fc | ||
|
|
490a083b6e | ||
|
|
c314e9381b | ||
|
|
fb739cedbe |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -24,7 +24,7 @@ _trial_temp
|
|||||||
*.komodoproject
|
*.komodoproject
|
||||||
docs/_build*
|
docs/_build*
|
||||||
.env*
|
.env*
|
||||||
|
EGG-INFO
|
||||||
|
|
||||||
# for bundling
|
# for bundling
|
||||||
thomas
|
thomas
|
||||||
|
|||||||
32
README.md
32
README.md
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
9
create-egg2.sh
Executable 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
9
create-egg3.sh
Executable 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
|
||||||
@@ -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, ))
|
||||||
|
|||||||
9
setup.py
9
setup.py
@@ -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)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
708
streaming/data/config.ui
Normal 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"><b>Settings</b></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">•</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">•</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">•</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">•</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"><b>File Serving Settings</b></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">•</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">•</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"><b>Advanced Settings</b></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>
|
||||||
@@ -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
234
streaming/gtk3ui.py
Normal 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()
|
||||||
@@ -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):
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user