mirror of
https://github.com/JohnDoee/deluge-streaming/
synced 2026-07-01 07:31:17 -07:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
435707b379 | ||
|
|
034f0ef331 | ||
|
|
d0e9780d76 | ||
|
|
bffef417f8 | ||
|
|
30340a25a2 | ||
|
|
287efadb3e | ||
|
|
d6e7fc83b8 | ||
|
|
b159bc2be5 | ||
|
|
e8f24b34bb | ||
|
|
a7fff07379 | ||
|
|
6846f4fc52 | ||
|
|
b8183d01dd | ||
|
|
da05a6de1b | ||
|
|
d058d1d914 | ||
|
|
934048dd46 | ||
|
|
fae5c777fc | ||
|
|
490a083b6e | ||
|
|
c314e9381b | ||
|
|
fb739cedbe | ||
|
|
8ab94d22b9 | ||
|
|
4b1d3799b5 | ||
|
|
1398e5042b | ||
|
|
de54ed067a | ||
|
|
7cfdfc79fe | ||
|
|
0c88dfd2e1 | ||
|
|
706c0f71d3 | ||
|
|
d90549e60a | ||
|
|
9c4c6f5db2 | ||
|
|
a20e623223 | ||
|
|
119cac1a56 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -24,7 +24,7 @@ _trial_temp
|
||||
*.komodoproject
|
||||
docs/_build*
|
||||
.env*
|
||||
|
||||
EGG-INFO
|
||||
|
||||
# for bundling
|
||||
thomas
|
||||
@@ -32,4 +32,4 @@ six.py
|
||||
rarfile.py
|
||||
rfc6266.py
|
||||
lepl
|
||||
pytz
|
||||
pytz
|
||||
|
||||
93
README.md
93
README.md
@@ -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
|
||||
|
||||
@@ -38,19 +38,104 @@ By using a small tool it is possible to it's possible to open streams directly i
|
||||
## Motivation
|
||||
|
||||
The plugin is not meant to be used as a right-click to stream thing. The idea is to
|
||||
make Deluge an abstraction layer for the [TidalStream](http://www.tidalstream.org/) project, i.e. torrents to http on demand.
|
||||
make Deluge an abstraction layer for the [Tidalstream](http://www.tidalstream.org/) project, i.e. torrents to http on demand.
|
||||
|
||||
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
|
||||
|
||||
Install and enable the plugin. Afterwards, head into Streaming settings and enable "Allow remote control".
|
||||
The URL found in the "Remote control url" field is where the API can be reached. The auth used is Basic Auth.
|
||||
|
||||
## Usage
|
||||
|
||||
There is only one end-point and that is where a torrent stream can be requested.
|
||||
|
||||
Both return the same responses and all responses are JSON encoded.
|
||||
All successfully authenticated responses have status code 200.
|
||||
|
||||
## POST /streaming/stream
|
||||
|
||||
POST body must be the raw torrent you want to stream. No form formatting or anything can be used.
|
||||
|
||||
List of URL GET Arguments
|
||||
|
||||
* **path**: Path inside the torrent file to either a folder or a file you want to stream. The plugin will try to guess the best one. **Optional**. **Default**: '' (i.e. find the best file in the whole torrent)
|
||||
* **infohash**: Infohash of the torrent you want to stream, can make it a bit faster as it can avoid reading POST body. **Optional**.
|
||||
* **label**: If label plugin is enabled and the torrent is actually added then give the torrent this label. **Optional**. **Default**: ''
|
||||
* **wait_for_end_pieces**: Wait for the first and last piece in the streamed file to be fully downloaded. Can be necessary for some video players. It also enforces that the torrent can be actually downloaded. If the key exist with any (even empty) value, the feature is enabled. **Optional**. **Default**: false
|
||||
|
||||
## GET /streaming/stream
|
||||
|
||||
* **infohash**: Does the same as when POSTed. **Mandatory**.
|
||||
* **path**: Does the same as when POSTed. **Optional**.
|
||||
* **wait_for_end_pieces**: Does the same as when POSTed. **Optional**.
|
||||
|
||||
## Success Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success", # Always equals this
|
||||
"filename" "horse.mkv", # Filename of the streamed torrent
|
||||
"url": "http://example.com/" # URL where the file can be reached by e.g. a media player
|
||||
}
|
||||
```
|
||||
|
||||
## Error Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "error", # Always equals this
|
||||
"message" "Torrent failed" # description for why it failed
|
||||
}
|
||||
```
|
||||
|
||||
# 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
|
||||
|
||||
## Version 0.10.3
|
||||
* Added label support
|
||||
* Reverse proxy config / replace URL config
|
||||
* Ensure internal Deluge state is updated before trying to use it
|
||||
|
||||
## Version 0.10.2
|
||||
* Busting cache when waiting for piece
|
||||
* Math error in calculating size of readable bytes
|
||||
|
||||
@@ -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
|
||||
33
examples/http-api/README.md
Normal file
33
examples/http-api/README.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# HTTP API
|
||||
|
||||
Stream using the HTTP API built into Deluge Streaming.
|
||||
|
||||
## Requirements
|
||||
|
||||
* Python
|
||||
* requests package
|
||||
|
||||
## Config
|
||||
|
||||
You need to enable HTTP API in Deluge Streaming config.
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
from streamtorrent import stream_torrent
|
||||
|
||||
if __name__ == '__main__':
|
||||
with open('TPB.AFK.2013.1080p.h264-SimonKlose', 'rb') as f:
|
||||
torrent_data = f.read()
|
||||
|
||||
# Stream 1080p TPB AFK using infohash to avoid posting the torrent
|
||||
# if it already exist.
|
||||
url = stream_torrent(
|
||||
'http://stream:password@127.0.0.1:46123/streaming/stream',
|
||||
infohash='411a7a164505636ab1a8276395b375a3a30bff32',
|
||||
torrent_body=torrent_data,
|
||||
label='tpbafk'
|
||||
)
|
||||
print('we can stream %s' % (url, ))
|
||||
```
|
||||
|
||||
96
examples/http-api/streamtorrent.py
Normal file
96
examples/http-api/streamtorrent.py
Normal file
@@ -0,0 +1,96 @@
|
||||
import argparse
|
||||
|
||||
import requests
|
||||
|
||||
class FailedToStreamException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def stream_torrent(remote_control_url, infohash=None, path=None, wait_for_end_pieces=True, label=None, torrent_body=None):
|
||||
"""
|
||||
Add a torrent to deluge, stream it and return a URL to where it can be watched.
|
||||
|
||||
All optional parameters are optional but you will need to at least provide an infohash (if the torrent is already added)
|
||||
or a torrent_body (if you want the torrent added).
|
||||
|
||||
remote_control_url - The URL found in Deluge Streaming config
|
||||
infohash - Torrent infohash, makes it faster if the torrent is already added
|
||||
path - path inside the torrent you want to stream
|
||||
wait_for_end_pieces - make sure the first and last piece are downloaded before returning url.
|
||||
This might be necessary for some players
|
||||
label - Label to set in deluge
|
||||
torrent_body - The content of the .torrent file you want to stream
|
||||
"""
|
||||
first_part, second_part = remote_control_url.split('@')
|
||||
username, password = first_part.split('/')[2].split(':')
|
||||
url = '/'.join(first_part.split('/')[:2]) + '/' + second_part
|
||||
|
||||
params = {}
|
||||
if infohash:
|
||||
params['infohash'] = infohash
|
||||
|
||||
if wait_for_end_pieces:
|
||||
params['wait_for_end_pieces'] = wait_for_end_pieces
|
||||
|
||||
if path:
|
||||
params['path'] = path
|
||||
|
||||
if label:
|
||||
params['label'] = label
|
||||
|
||||
if infohash: # try to stream it without posting torrent body first
|
||||
r = requests.get(url, auth=(username, password), params=params)
|
||||
if r.status_code != 200:
|
||||
raise FailedToStreamException('Got non-200 error code from Deluge')
|
||||
|
||||
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)
|
||||
if r.status_code != 200:
|
||||
raise FailedToStreamException('Got non-200 error code from Deluge')
|
||||
|
||||
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, ))
|
||||
9
setup.py
9
setup.py
@@ -41,8 +41,8 @@ from setuptools import setup, find_packages
|
||||
|
||||
__plugin_name__ = "Streaming"
|
||||
__author__ = "Anders Jensen"
|
||||
__author_email__ = "johndoee@tidalstream.org"
|
||||
__version__ = "0.10.2"
|
||||
__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)
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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']
|
||||
@@ -71,6 +72,12 @@ AUDIO_STREAMABLE_EXTENSIONS = ['flac', 'mp3', 'oga']
|
||||
STREAMABLE_EXTENSIONS = set(VIDEO_STREAMABLE_EXTENSIONS + AUDIO_STREAMABLE_EXTENSIONS)
|
||||
TORRENT_CLEANUP_INTERVAL = timedelta(minutes=30)
|
||||
MAX_FILE_PRIORITY = 2
|
||||
MAX_PIECE_PRIORITY = 7
|
||||
MIN_WAIT_PIECE_PRIORITY_DELAY = timedelta(seconds=5)
|
||||
WITHIN_CHAIN_PERCENTAGE = 0.10
|
||||
MIN_PIECE_COUNT_FOR_CHAIN_CONSIDERATION = 40
|
||||
MIN_CHAIN_WAIT_DELAY = timedelta(seconds=8)
|
||||
|
||||
|
||||
DEFAULT_PREFS = {
|
||||
'ip': '127.0.0.1',
|
||||
@@ -88,6 +95,7 @@ DEFAULT_PREFS = {
|
||||
'ssl_source': 'daemon',
|
||||
'ssl_priv_key_path': '',
|
||||
'ssl_cert_path': '',
|
||||
'aggressive_prioritizing': False,
|
||||
}
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -110,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'):
|
||||
@@ -120,14 +128,16 @@ 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 = {}
|
||||
self.cycle_lock = defer.DeferredLock()
|
||||
self.last_activity = datetime.now()
|
||||
self.waited_pieces = set()
|
||||
|
||||
self.torrent = get_torrent(infohash)
|
||||
status = self.torrent.get_status(['piece_length'])
|
||||
@@ -152,6 +162,7 @@ class Torrent(object):
|
||||
def can_read(self, from_byte):
|
||||
self.ensure_started()
|
||||
|
||||
status = self.torrent.get_status(['pieces'])
|
||||
needed_piece, rest = divmod(from_byte, self.piece_length)
|
||||
last_available_piece = None
|
||||
for piece, status in enumerate(self.torrent.status.pieces[needed_piece:], needed_piece):
|
||||
@@ -160,29 +171,60 @@ class Torrent(object):
|
||||
last_available_piece = piece
|
||||
|
||||
if last_available_piece is None:
|
||||
logger.debug('Since we are waiting for a piece, setting priority for %s to max' % (needed_piece, ))
|
||||
self.torrent.handle.set_piece_deadline(needed_piece, 0)
|
||||
self.torrent.handle.piece_priority(needed_piece, 7)
|
||||
logger.debug('Since we are waiting for a piece, we need to check if we should set piece %s to max' % (needed_piece, ))
|
||||
|
||||
is_next_in_chain = False
|
||||
f = self.get_file_from_offset(from_byte)
|
||||
file_piece_count = (f['size'] // self.piece_length) + 1
|
||||
|
||||
file_priorities = self.torrent.get_file_priorities()
|
||||
if file_piece_count <= MIN_PIECE_COUNT_FOR_CHAIN_CONSIDERATION:
|
||||
is_next_in_chain = True
|
||||
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
|
||||
|
||||
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 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 = 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
|
||||
self.torrent.set_file_priorities(file_priorities)
|
||||
|
||||
for _ in range(300):
|
||||
for i in range(300):
|
||||
if self.torrent.status.pieces[needed_piece]:
|
||||
break
|
||||
|
||||
if not reactor.running:
|
||||
return
|
||||
|
||||
if is_next_in_chain and i == MIN_CHAIN_WAIT_DELAY.total_seconds() * 5 and needed_piece not in self.get_currently_downloading():
|
||||
logger.debug('Next in chain waiting failed, setting priority')
|
||||
self.torrent.handle.set_piece_deadline(needed_piece, 0)
|
||||
self.torrent.handle.piece_priority(needed_piece, MAX_PIECE_PRIORITY)
|
||||
|
||||
time.sleep(0.2)
|
||||
status = self.torrent.get_status(['pieces'])
|
||||
|
||||
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()
|
||||
@@ -233,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:
|
||||
@@ -251,7 +293,7 @@ class Torrent(object):
|
||||
self.torrent.set_file_priorities(file_priorities)
|
||||
|
||||
if self.readers:
|
||||
status = self.torrent.get_status(['files', 'file_progress'])
|
||||
status = self.torrent.get_status(['files', 'file_progress', 'pieces'])
|
||||
file_ranges = {}
|
||||
fileset_ranges = {}
|
||||
for path, from_byte, to_byte in self.readers.values():
|
||||
@@ -261,6 +303,10 @@ class Torrent(object):
|
||||
else:
|
||||
file_ranges[path] = from_byte
|
||||
|
||||
reader_piece = from_byte // self.piece_length
|
||||
self.torrent.handle.set_piece_deadline(reader_piece, 0)
|
||||
self.torrent.handle.piece_priority(reader_piece, MAX_PIECE_PRIORITY)
|
||||
|
||||
for fileset_hash, fileset in self.filesets.items():
|
||||
if path in fileset['files']:
|
||||
if fileset_hash in fileset_ranges:
|
||||
@@ -268,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]
|
||||
@@ -311,8 +357,6 @@ class Torrent(object):
|
||||
|
||||
if piece < current_piece:
|
||||
self.torrent.handle.piece_priority(piece, 0)
|
||||
elif piece == current_piece:
|
||||
self.torrent.handle.piece_priority(piece, 7)
|
||||
else:
|
||||
self.torrent.handle.piece_priority(piece, 1)
|
||||
|
||||
@@ -325,6 +369,7 @@ class Torrent(object):
|
||||
return currently_downloading
|
||||
|
||||
def reset_priorities(self):
|
||||
status = self.torrent.get_status(['pieces'])
|
||||
for piece in range(len(self.torrent.status.pieces)):
|
||||
self.torrent.handle.piece_priority(piece, 1)
|
||||
|
||||
@@ -342,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)
|
||||
@@ -381,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:
|
||||
@@ -449,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
|
||||
@@ -468,6 +535,8 @@ class TorrentHandler(object):
|
||||
filesystem = self.get_filesystem(infohash)
|
||||
if path:
|
||||
stream_item = filesystem.get_item_from_path(path)
|
||||
if stream_item and filesystem == stream_item and path != stream_item.id:
|
||||
stream_item = filesystem.get_item_from_path('%s/%s' % (filesystem.id, path))
|
||||
else:
|
||||
stream_item = filesystem
|
||||
|
||||
@@ -518,11 +587,13 @@ class TorrentHandler(object):
|
||||
wait_for_pieces.append(piece - 1)
|
||||
|
||||
logger.debug('We want first and last piece first, these are the pieces: %r' % (wait_for_pieces, ))
|
||||
for piece in wait_for_pieces:
|
||||
torrent.handle.set_piece_deadline(piece, 0)
|
||||
torrent.handle.piece_priority(piece, 7)
|
||||
if wait_for_pieces:
|
||||
for piece in wait_for_pieces:
|
||||
torrent.handle.set_piece_deadline(piece, 0)
|
||||
torrent.handle.piece_priority(piece, MAX_PIECE_PRIORITY)
|
||||
|
||||
for _ in range(220):
|
||||
status = torrent.get_status(['pieces'])
|
||||
for piece in wait_for_pieces:
|
||||
if not torrent.status.pieces[piece]:
|
||||
break
|
||||
@@ -549,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)
|
||||
@@ -569,52 +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'))
|
||||
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].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)
|
||||
defer.returnValue(json.dumps(result))
|
||||
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).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:
|
||||
@@ -631,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(), ))
|
||||
@@ -663,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']
|
||||
@@ -699,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()
|
||||
@@ -735,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):
|
||||
@@ -761,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):
|
||||
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)
|
||||
|
||||
@@ -779,7 +869,16 @@ 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():
|
||||
label_plugin.add(label)
|
||||
|
||||
try:
|
||||
label_plugin.set_torrent(infohash, label)
|
||||
except:
|
||||
logger.exception('Failed to set label')
|
||||
except:
|
||||
logger.exception('Failed to add torrent')
|
||||
defer.returnValue({'status': 'error', 'message': 'failed to add torrent'})
|
||||
@@ -794,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
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',
|
||||
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
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:
|
||||
subprocess.Popen(['xdg-open', url])
|
||||
except OSError:
|
||||
print 'Unable to open URL %s' % (url, )
|
||||
print('Unable to open URL %s' % (url, ))
|
||||
|
||||
|
||||
class GtkUI(GtkPluginBase):
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user