42 Commits

Author SHA1 Message Date
Anders Jensen
123962e123 Merge tag '0.12.2' into develop
-
2020-07-31 16:18:56 +02:00
Anders Jensen
435707b379 Merge branch 'release/0.12.2' 2020-07-31 16:18:54 +02:00
Anders Jensen
034f0ef331 updated readmes 2020-07-31 16:18:42 +02:00
Anders Jensen
d0e9780d76 added support for new ssl version 2020-07-31 16:16:57 +02:00
Anders Jensen
bffef417f8 Merge tag '0.12.1' into develop
-
2020-04-22 12:34:42 +02:00
Anders Jensen
30340a25a2 Merge branch 'release/0.12.1' 2020-04-22 12:34:39 +02:00
Anders Jensen
287efadb3e fixed breaking bug 2020-04-22 12:34:25 +02:00
Anders Jensen
d6e7fc83b8 Merge tag '0.12.0' into develop
-
2020-04-21 21:49:58 +02:00
Anders Jensen
b159bc2be5 Merge branch 'release/0.12.0' 2020-04-21 21:49:54 +02:00
Anders Jensen
e8f24b34bb moved to using read piece instead of reading from disk 2020-04-21 21:49:31 +02:00
Anders Jensen
a7fff07379 Merge tag '0.11.0' into develop
Added Deluge 2 support
2019-11-03 09:47:28 +01:00
Anders Jensen
6846f4fc52 Merge branch 'release/0.11.0' 2019-11-03 09:47:24 +01:00
Anders Jensen
b8183d01dd bumped version 2019-11-03 09:47:07 +01:00
Anders Jensen
da05a6de1b added 2/3 egg create script and updated readme 2019-11-03 09:33:01 +01:00
Anders Jensen
d058d1d914 Initial Deluge v2 #22 support and functionality to hopefully fix #23 2019-11-02 21:55:23 +01:00
Anders Jensen
934048dd46 Merge tag '0.10.5' into develop
-
2019-02-17 15:03:40 +01:00
Anders Jensen
fae5c777fc Merge branch 'release/0.10.5' 2019-02-17 15:03:38 +01:00
Anders Jensen
490a083b6e bumped version 2019-02-17 15:03:27 +01:00
Anders Jensen
c314e9381b added support for opening files directly in the browser 2019-02-17 14:59:50 +01:00
Anders Jensen
fb739cedbe Merge tag '0.10.4' into develop
-
2018-09-21 21:43:27 +02:00
Anders Jensen
8ab94d22b9 Merge branch 'release/0.10.4' 2018-09-21 21:43:25 +02:00
Anders Jensen
4b1d3799b5 trying to fix bug with setting priority too often 2018-09-21 21:43:12 +02:00
Anders Jensen
1398e5042b Highlighting, added examples, fixed small title stuff 2018-09-17 21:50:23 +02:00
Anders Jensen
de54ed067a Updated readme to fix #20 2018-09-17 19:00:05 +02:00
Anders Jensen
7cfdfc79fe Merge tag '0.10.3' into develop
-
2018-09-11 18:39:00 +02:00
Anders Jensen
0c88dfd2e1 Merge branch 'release/0.10.3' 2018-09-11 18:38:58 +02:00
Anders Jensen
706c0f71d3 fixed bug with internal state stale and bumped version 2018-09-11 18:38:44 +02:00
Anders Jensen
d90549e60a Added example on how to use HTTP API 2018-09-02 10:47:19 +02:00
Anders Jensen
9c4c6f5db2 added label support 2018-09-02 10:42:28 +02:00
Anders Jensen
a20e623223 trying to make path resolution smarter 2018-08-26 11:13:09 +02:00
Anders Jensen
119cac1a56 Merge tag '0.10.2' into develop
-
2018-08-25 18:05:26 +02:00
Anders Jensen
86c6b0db00 Merge branch 'release/0.10.2' 2018-08-25 18:05:25 +02:00
Anders Jensen
73ddeb021c bumped version 2018-08-25 18:05:12 +02:00
Anders Jensen
21f1d77568 ensuring torrent is added fully before streaming 2018-08-25 18:03:30 +02:00
Anders Jensen
015a7cbc7a added buffer buster and some wrong math 2018-08-25 17:53:55 +02:00
Anders Jensen
3417b109ec Added example 2018-08-25 10:20:20 +02:00
Anders Jensen
3a3c90ed8b Merge tag '0.10.1' into develop
-
2018-08-25 09:55:32 +02:00
Anders Jensen
bfe0f9f49c Merge branch 'release/0.10.1' 2018-08-25 09:55:31 +02:00
Anders Jensen
9e38de34f2 updated readme and version 2018-08-25 09:55:11 +02:00
Anders Jensen
ec02a2e61d ensure priorities arent overwritten 2018-08-25 09:53:04 +02:00
Anders Jensen
0e63ed4ebc fixed small bug with file priority overwriting manual priority management 2018-08-25 09:16:07 +02:00
Anders Jensen
ba6c689d98 Merge tag '0.10.0' into develop
-
2018-08-24 21:23:40 +02:00
19 changed files with 1603 additions and 142 deletions

2
.gitignore vendored
View File

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

100
README.md
View File

@@ -1,7 +1,7 @@
# Streaming Plugin # Streaming Plugin
https://github.com/JohnDoee/deluge-streaming https://github.com/JohnDoee/deluge-streaming
(c)2016 by Anders Jensen <johndoee@tidalstream.org> (c)2020 by Anders Jensen <johndoee@tridentstream.org>
## Description ## Description
@@ -38,19 +38,111 @@ By using a small tool it is possible to it's possible to open streams directly i
## Motivation ## Motivation
The plugin is not meant to be used as a right-click to stream thing. The idea is to 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. 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)
# 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 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
## Version 0.10.1
* Small bugfixes related to priorities, should actually make sequential download work.
## Version 0.10.0 ## Version 0.10.0
* Rewrote large parts of the code * Rewrote large parts of the code
* Now using [thomas](https://github.com/JohnDoee/thomas) as file-reading core - this adds support for multi-rar streaming. * Now using [thomas](https://github.com/JohnDoee/thomas) as file-reading core - this adds support for multi-rar streaming.

View File

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

9
create-egg2.sh Executable file
View File

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

9
create-egg3.sh Executable file
View File

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

View File

@@ -0,0 +1,23 @@
# Commandline Tool to stream
Stream from the commandline.
## Requirements
* Python
* deluge_client python package
## Installation example
```bash
virtualenv cli-example
cli-example/bin/pip install deluge_client
```
## Usage
Open a torrent directly in VLC on Linux or OSX.
```bash
vlc `cli-example/bin/python stream-cli.py username password my_video.torrent`
```

View File

@@ -0,0 +1,28 @@
import argparse
import urllib
from deluge_client import DelugeRPCClient
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Stream something.')
parser.add_argument('username', type=str, help='Deluge username')
parser.add_argument('password', type=str, help='Deluge password')
parser.add_argument('path_or_url', type=str, help='Path or URL to torrent')
parser.add_argument('--hostname', '-o', type=str, default='localhost', help='Deluge daemon hostname or ip')
parser.add_argument('--port', '-p', type=int, default=58846, help='Deluge daemon port')
args = parser.parse_args()
if args.path_or_url.startswith('http'):
filedata = urllib.urlopen(args.path_or_url).read()
else:
with open(args.path_or_url, 'rb') as f:
filedata = f.read()
client = DelugeRPCClient(args.hostname, args.port, args.username, args.password)
client.connect()
result = client.streaming.stream_torrent(None, None, filedata, None, None, True)
print(result['url'])

View 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, ))
```

View 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, ))

View File

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

View File

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

View File

@@ -37,6 +37,7 @@
# statement from all source files in the program, then also delete it here. # statement from all source files in the program, then also delete it here.
# #
import base64
import json import json
import logging import logging
import os import os
@@ -55,21 +56,28 @@ 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']
AUDIO_STREAMABLE_EXTENSIONS = ['flac', 'mp3', 'oga'] AUDIO_STREAMABLE_EXTENSIONS = ['flac', 'mp3', 'oga']
STREAMABLE_EXTENSIONS = set(VIDEO_STREAMABLE_EXTENSIONS + AUDIO_STREAMABLE_EXTENSIONS) STREAMABLE_EXTENSIONS = set(VIDEO_STREAMABLE_EXTENSIONS + AUDIO_STREAMABLE_EXTENSIONS)
TORRENT_CLEANUP_INTERVAL = timedelta(minutes=30) 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 = { DEFAULT_PREFS = {
'ip': '127.0.0.1', 'ip': '127.0.0.1',
@@ -87,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__)
@@ -109,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'):
@@ -119,14 +128,16 @@ 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 = {}
self.cycle_lock = defer.DeferredLock() self.cycle_lock = defer.DeferredLock()
self.last_activity = datetime.now() self.last_activity = datetime.now()
self.waited_pieces = set()
self.torrent = get_torrent(infohash) self.torrent = get_torrent(infohash)
status = self.torrent.get_status(['piece_length']) status = self.torrent.get_status(['piece_length'])
@@ -151,6 +162,7 @@ class Torrent(object):
def can_read(self, from_byte): def can_read(self, from_byte):
self.ensure_started() self.ensure_started()
status = self.torrent.get_status(['pieces'])
needed_piece, rest = divmod(from_byte, self.piece_length) needed_piece, rest = divmod(from_byte, self.piece_length)
last_available_piece = None last_available_piece = None
for piece, status in enumerate(self.torrent.status.pieces[needed_piece:], needed_piece): for piece, status in enumerate(self.torrent.status.pieces[needed_piece:], needed_piece):
@@ -159,27 +171,60 @@ class Torrent(object):
last_available_piece = piece last_available_piece = piece
if last_available_piece is None: if last_available_piece is None:
logger.debug('Since we are waiting for a piece, setting priority for %s to max' % (needed_piece, )) logger.debug('Since we are waiting for a piece, we need to check if we should set piece %s to max' % (needed_piece, ))
self.torrent.handle.set_piece_deadline(needed_piece, 0)
self.torrent.handle.piece_priority(needed_piece, 7)
is_next_in_chain = False
f = self.get_file_from_offset(from_byte) f = self.get_file_from_offset(from_byte)
logger.debug('Also setting file to max %r' % (f, )) file_piece_count = (f['size'] // self.piece_length) + 1
file_priorities = self.torrent.get_file_priorities()
file_priorities[f['index']] = 7
self.torrent.set_file_priorities(file_priorities)
for _ in range(300): 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 i in range(300):
if self.torrent.status.pieces[needed_piece]: if self.torrent.status.pieces[needed_piece]:
break break
if not reactor.running: if not reactor.running:
return 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) time.sleep(0.2)
status = self.torrent.get_status(['pieces'])
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) + rest + self.piece_length 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()
@@ -230,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:
@@ -239,7 +284,7 @@ class Torrent(object):
if f['path'] in must_whitelist: if f['path'] in must_whitelist:
if f['path'] in first_files: if f['path'] in first_files:
file_priorities[i] = 7 file_priorities[i] = MAX_FILE_PRIORITY
else: else:
file_priorities[i] = 1 file_priorities[i] = 1
elif f['path'] not in cannot_blacklist: elif f['path'] not in cannot_blacklist:
@@ -248,7 +293,7 @@ class Torrent(object):
self.torrent.set_file_priorities(file_priorities) self.torrent.set_file_priorities(file_priorities)
if self.readers: if self.readers:
status = self.torrent.get_status(['files', 'file_progress']) status = self.torrent.get_status(['files', 'file_progress', 'pieces'])
file_ranges = {} file_ranges = {}
fileset_ranges = {} fileset_ranges = {}
for path, from_byte, to_byte in self.readers.values(): for path, from_byte, to_byte in self.readers.values():
@@ -258,6 +303,10 @@ class Torrent(object):
else: else:
file_ranges[path] = from_byte 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(): for fileset_hash, fileset in self.filesets.items():
if path in fileset['files']: if path in fileset['files']:
if fileset_hash in fileset_ranges: if fileset_hash in fileset_ranges:
@@ -265,6 +314,23 @@ class Torrent(object):
else: else:
fileset_ranges[fileset_hash] = fileset['files'].index(path) fileset_ranges[fileset_hash] = fileset['files'].index(path)
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]
logger.debug('From index %s' % (first_file, ))
file_mapping = {f['path']: f['index'] for f in status['files']}
for i, f in enumerate(fileset['files']):
index = file_mapping[f]
if i < first_file:
file_priorities[index] = 0
elif i == first_file:
file_priorities[index] = MAX_FILE_PRIORITY
else:
file_priorities[index] = 1
self.torrent.set_file_priorities(file_priorities)
currently_downloading = self.get_currently_downloading() currently_downloading = self.get_currently_downloading()
logger.debug('File heads: %r' % (file_ranges, )) logger.debug('File heads: %r' % (file_ranges, ))
for f, progress in zip(status['files'], status['file_progress']): for f, progress in zip(status['files'], status['file_progress']):
@@ -291,28 +357,9 @@ class Torrent(object):
if piece < current_piece: if piece < current_piece:
self.torrent.handle.piece_priority(piece, 0) self.torrent.handle.piece_priority(piece, 0)
elif piece == current_piece:
self.torrent.handle.piece_priority(piece, 7)
else: else:
self.torrent.handle.piece_priority(piece, 1) self.torrent.handle.piece_priority(piece, 1)
file_priorities = 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]
logger.debug('From index %s' % (first_file, ))
file_mapping = {f['path']: f['index'] for f in status['files']}
for i, f in enumerate(fileset['files']):
index = file_mapping[f]
if i < first_file:
file_priorities[index] = 0
elif i == first_file:
file_priorities[index] = 7
else:
file_priorities[index] = 1
self.torrent.set_file_priorities(file_priorities)
def get_currently_downloading(self): def get_currently_downloading(self):
currently_downloading = set() currently_downloading = set()
for peer in self.torrent.handle.get_peer_info(): for peer in self.torrent.handle.get_peer_info():
@@ -322,6 +369,7 @@ class Torrent(object):
return currently_downloading return currently_downloading
def reset_priorities(self): def reset_priorities(self):
status = self.torrent.get_status(['pieces'])
for piece in range(len(self.torrent.status.pieces)): for piece in range(len(self.torrent.status.pieces)):
self.torrent.handle.piece_priority(piece, 1) self.torrent.handle.piece_priority(piece, 1)
@@ -339,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)
@@ -378,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:
@@ -388,8 +458,7 @@ class TorrentHandler(object):
def get_filesystem(self, infohash): def get_filesystem(self, infohash):
torrent = get_torrent(infohash) torrent = get_torrent(infohash)
status = torrent.get_status(['piece_length', 'files', 'file_progress', 'save_path']) status = torrent.get_status(['files', 'file_progress', 'save_path'])
self.piece_length = status['piece_length']
save_path = status['save_path'] save_path = status['save_path']
found_rar = False found_rar = False
@@ -447,18 +516,27 @@ 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
def stream(self, infohash, path, wait_for_end_pieces=False): def stream(self, infohash, path, wait_for_end_pieces=False):
logger.debug('Trying to get path:%s from infohash:%s' % (path, infohash)) logger.debug('Trying to get path:%s from infohash:%s' % (path, infohash))
local_torrent = self.get_torrent(infohash)
torrent = get_torrent(infohash) torrent = get_torrent(infohash)
for _ in range(10):
status = torrent.get_status(['piece_length'])
if status['piece_length'] > 0:
break
yield sleep(0.2)
local_torrent = self.get_torrent(infohash)
filesystem = self.get_filesystem(infohash) filesystem = self.get_filesystem(infohash)
if path: if path:
stream_item = filesystem.get_item_from_path(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: else:
stream_item = filesystem stream_item = filesystem
@@ -509,11 +587,13 @@ class TorrentHandler(object):
wait_for_pieces.append(piece - 1) wait_for_pieces.append(piece - 1)
logger.debug('We want first and last piece first, these are the pieces: %r' % (wait_for_pieces, )) logger.debug('We want first and last piece first, these are the pieces: %r' % (wait_for_pieces, ))
for piece in wait_for_pieces: if wait_for_pieces:
torrent.handle.set_piece_deadline(piece, 0) for piece in wait_for_pieces:
torrent.handle.piece_priority(piece, 7) torrent.handle.set_piece_deadline(piece, 0)
torrent.handle.piece_priority(piece, MAX_PIECE_PRIORITY)
for _ in range(220): for _ in range(220):
status = torrent.get_status(['pieces'])
for piece in wait_for_pieces: for piece in wait_for_pieces:
if not torrent.status.pieces[piece]: if not torrent.status.pieces[piece]:
break break
@@ -540,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)
@@ -560,52 +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(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:
label = label[0].decode('utf-8')
else:
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) 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:
@@ -622,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(), ))
@@ -654,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']
@@ -690,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()
@@ -726,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):
@@ -752,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): 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)
@@ -770,7 +869,16 @@ 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():
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: except:
logger.exception('Failed to add torrent') logger.exception('Failed to add torrent')
defer.returnValue({'status': 'error', 'message': 'failed to add torrent'}) defer.returnValue({'status': 'error', 'message': 'failed to add torrent'})
@@ -785,7 +893,7 @@ class Core(CorePluginBase):
try: try:
stream_or_item = yield defer.maybeDeferred(self.torrent_handler.stream, infohash, fn, wait_for_end_pieces=wait_for_end_pieces) stream_or_item = yield defer.maybeDeferred(self.torrent_handler.stream, infohash, fn, wait_for_end_pieces=wait_for_end_pieces)
stream_url = self.thomas_http_output.serve_item(stream_or_item) stream_url = self.thomas_http_output.serve_item(stream_or_item, as_inline=as_inline)
except: except:
logger.exception('Failed to stream torrent') logger.exception('Failed to stream torrent')
defer.returnValue({'status': 'error', 'message': 'failed to stream torrent'}) defer.returnValue({'status': 'error', 'message': 'failed to stream torrent'})

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

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

View File

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

234
streaming/gtk3ui.py Normal file
View File

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

View File

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

View File

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

View File

@@ -1,17 +1,27 @@
import logging import logging
import mimetypes import mimetypes
import os import os
import time
import threading
from io import BytesIO
from thomas import InputBase from thomas import InputBase
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
PIECE_REQUEST_HISTORY_TIME = 10
MAX_PIECE_REQUEST_COUNT = 20
class DelugeTorrentInput(InputBase.find_plugin('file')): class DelugeTorrentInput(InputBase):
plugin_name = 'torrent_file' plugin_name = 'torrent_file'
protocols = [] protocols = []
current_piece_data = None
can_read_to = None can_read_to = None
last_available_piece = None
_pos = None
_closed = False
def __init__(self, item, torrent_handler, infohash, offset, path): def __init__(self, item, torrent_handler, infohash, offset, path):
self.item = item self.item = item
@@ -20,6 +30,9 @@ class DelugeTorrentInput(InputBase.find_plugin('file')):
self.infohash = infohash self.infohash = infohash
self.offset = offset self.offset = offset
self.path = path self.path = path
self.piece_buffer = {}
self.requested_pieces = {}
self.piece_consumption_time = []
self.size, self.filename, self.content_type = self.get_info() self.size, self.filename, self.content_type = self.get_info()
def get_info(self): def get_info(self):
@@ -33,33 +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
real_num = min(num, self.can_read_to - tell) current_piece, rest = self.current_piece
if num != real_num: 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))
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 while self.piece_consumption_time and self.piece_consumption_time[0] < time.time() - PIECE_REQUEST_HISTORY_TIME:
self.piece_consumption_time.pop(0)
max_piece_count = (self.last_available_piece - current_piece) + 1
pieces_to_request = min(min(max(2, len(self.piece_consumption_time)), max_piece_count), MAX_PIECE_REQUEST_COUNT)
logger.debug('New piece request status pieces_to_request: %s piece_consumption_time: %s max_piece_count: %s' % (pieces_to_request, len(self.piece_consumption_time), max_piece_count, ))
logger.debug('Requested pieces: %r' % (self.requested_pieces.items()))
logger.debug('Piece buffer: %r' % (self.piece_buffer.keys()))
for piece in range(current_piece, current_piece + pieces_to_request):
if piece in self.requested_pieces:
continue
logger.debug('Requesting piece %s' % (piece, ))
self.requested_pieces[piece] = threading.Event()
self.torrent.request_piece(piece)
for _ in range(1000):
if self.requested_pieces[current_piece].wait(1):
break
if self._closed:
return b''
else:
return b'' return b''
data = super(DelugeTorrentInput, self).read(real_num) for delete_piece in [p for p in self.piece_buffer.keys() if p < current_piece]:
return data del self.piece_buffer[delete_piece]
for delete_piece in [p for p in self.requested_pieces.keys() if p < current_piece]:
del self.requested_pieces[delete_piece]
self.current_piece_data = self.piece_buffer[current_piece]
self.current_piece_data.seek(rest)
self.piece_consumption_time.append(time.time())
logger.debug('Returning %s bytes' % (num, ))
return self._read(num)
@property
def current_piece(self):
from_byte = self.offset + self.tell()
piece_length = self.torrent.piece_length
piece, rest = divmod(from_byte, piece_length)
return piece, rest
def new_piece_available(self, piece, data):
if piece not in self.requested_pieces or self.requested_pieces[piece].is_set():
return
logger.debug("Setting data for piece %s" % (piece, ))
self.piece_buffer[piece] = BytesIO(data)
self.requested_pieces[piece].set()
def close(self): def close(self):
self.torrent.remove_reader(self) self.torrent.remove_reader(self)
super(DelugeTorrentInput, self).close() self._closed = True

View File

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