mirror of
https://github.com/JohnDoee/deluge-streaming/
synced 2026-07-01 07:31:17 -07:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c88dfd2e1 | ||
|
|
706c0f71d3 | ||
|
|
d90549e60a | ||
|
|
9c4c6f5db2 | ||
|
|
a20e623223 | ||
|
|
119cac1a56 | ||
|
|
86c6b0db00 | ||
|
|
73ddeb021c | ||
|
|
21f1d77568 | ||
|
|
015a7cbc7a | ||
|
|
3417b109ec | ||
|
|
3a3c90ed8b | ||
|
|
bfe0f9f49c | ||
|
|
9e38de34f2 | ||
|
|
ec02a2e61d | ||
|
|
0e63ed4ebc | ||
|
|
ba6c689d98 | ||
|
|
ce7f6efd6d | ||
|
|
7aed811b78 | ||
|
|
37753a23e4 | ||
|
|
cdf2a5515a | ||
|
|
14c23065b5 | ||
|
|
3af5c420f8 | ||
|
|
cc13b032ea | ||
|
|
1aed78f389 | ||
|
|
e1db68012b | ||
|
|
3c30163582 | ||
|
|
e6948fa90f | ||
|
|
84dd3c4fe7 | ||
|
|
ea96d0f739 |
15
.gitignore
vendored
15
.gitignore
vendored
@@ -23,8 +23,13 @@ dropin.cache
|
||||
_trial_temp
|
||||
*.komodoproject
|
||||
docs/_build*
|
||||
apiserver/metadata/imdbhandler.py
|
||||
apiserver/metadata/malhandler.py
|
||||
apiserver/services/search.py
|
||||
apiserver/services/control.py
|
||||
apiserver/services/files.py
|
||||
.env*
|
||||
|
||||
|
||||
# for bundling
|
||||
thomas
|
||||
six.py
|
||||
rarfile.py
|
||||
rfc6266.py
|
||||
lepl
|
||||
pytz
|
||||
31
README.md
31
README.md
@@ -38,12 +38,41 @@ 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)
|
||||
|
||||
# Version Info
|
||||
|
||||
## Version Unreleased
|
||||
* 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
|
||||
* 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.
|
||||
* Faster streaming by reading directly from disk
|
||||
* Reverse proxy mode
|
||||
|
||||
## Version 0.9.0
|
||||
* Few bugfixes
|
||||
* Added support for Deluge 2
|
||||
|
||||
## Version 0.8.1
|
||||
* Fixed some small problems and bugs
|
||||
* better URL execution with GTKUI
|
||||
|
||||
9
create-egg.sh
Normal file
9
create-egg.sh
Normal file
@@ -0,0 +1,9 @@
|
||||
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
|
||||
23
examples/cli-stream/README.md
Normal file
23
examples/cli-stream/README.md
Normal 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`
|
||||
```
|
||||
28
examples/cli-stream/stream-cli.py
Normal file
28
examples/cli-stream/stream-cli.py
Normal 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'])
|
||||
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, ))
|
||||
```
|
||||
|
||||
57
examples/http-api/streamtorrent.py
Normal file
57
examples/http-api/streamtorrent.py
Normal file
@@ -0,0 +1,57 @@
|
||||
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']
|
||||
|
||||
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']
|
||||
|
||||
raise FailedToStreamException('Streaming was never successful')
|
||||
22
setup.py
22
setup.py
@@ -37,12 +37,12 @@
|
||||
# statement from all source files in the program, then also delete it here.
|
||||
#
|
||||
|
||||
from setuptools import setup
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
__plugin_name__ = "Streaming"
|
||||
__author__ = "John Doee"
|
||||
__author__ = "Anders Jensen"
|
||||
__author_email__ = "johndoee@tidalstream.org"
|
||||
__version__ = "0.8.1"
|
||||
__version__ = "0.10.3"
|
||||
__url__ = "https://github.com/JohnDoee/deluge-streaming"
|
||||
__license__ = "GPLv3"
|
||||
__description__ = "Enables streaming of files while downloading them."
|
||||
@@ -64,6 +64,18 @@ downloads ahead, this enables seeking in video files.
|
||||
If you want to stream from a non-local computer, e.g. your seedbox, you will need to change the IP in option to the external server ip."""
|
||||
__pkg_data__ = {__plugin_name__.lower(): ["template/*", "data/*"]}
|
||||
|
||||
REQUIREMENTS_PACKAGES = [
|
||||
'thomas',
|
||||
'lepl',
|
||||
'pytz',
|
||||
]
|
||||
|
||||
REQUIREMENTS_MODULES = [
|
||||
'six',
|
||||
'rarfile',
|
||||
'rfc6266',
|
||||
]
|
||||
|
||||
setup(
|
||||
name=__plugin_name__,
|
||||
version=__version__,
|
||||
@@ -73,8 +85,10 @@ setup(
|
||||
url=__url__,
|
||||
license=__license__,
|
||||
long_description=__long_description__ if __long_description__ else __description__,
|
||||
# install_requires=REQUIREMENTS_PACKAGES,
|
||||
|
||||
packages=[__plugin_name__.lower()],
|
||||
packages=[__plugin_name__.lower()] + ['%s.%s' % (x, y) for x in REQUIREMENTS_PACKAGES for y in find_packages(x)] + REQUIREMENTS_PACKAGES,
|
||||
py_modules=REQUIREMENTS_MODULES,
|
||||
package_data = __pkg_data__,
|
||||
|
||||
entry_points="""
|
||||
|
||||
@@ -39,18 +39,21 @@
|
||||
|
||||
from deluge.plugins.init import PluginInitBase
|
||||
|
||||
|
||||
class CorePlugin(PluginInitBase):
|
||||
def __init__(self, plugin_name):
|
||||
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
|
||||
self._plugin_cls = _plugin_cls
|
||||
super(GtkUIPlugin, self).__init__(plugin_name)
|
||||
|
||||
|
||||
class WebUIPlugin(PluginInitBase):
|
||||
def __init__(self, plugin_name):
|
||||
from webui import WebUI as _plugin_cls
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -26,19 +26,26 @@
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<widget class="GtkCheckButton" id="input_download_only_streamed">
|
||||
<property name="label" translatable="yes">Download only streamed files, skip the other files</property>
|
||||
<widget class="GtkVBox" id="settings_vbox">
|
||||
<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="can_focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<widget 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>
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</widget>
|
||||
</child>
|
||||
@@ -160,9 +167,73 @@
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<widget class="GtkHBox" id="hbox54">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<widget 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>
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<widget class="GtkHBox" id="hbox55">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<widget 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>
|
||||
</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_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>
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<!-- <child>
|
||||
<widget class="GtkRadioButton" id="input_serve_webui">
|
||||
<property name="label" translatable="yes">Serve files via WebUI</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="visible">False</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
@@ -175,16 +246,16 @@
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</child> -->
|
||||
<child>
|
||||
<widget class="GtkVBox" id="settings_vbox3">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<!-- <child>
|
||||
<widget class="GtkRadioButton" id="input_serve_standalone">
|
||||
<property name="label" translatable="yes">Serve files via standalone</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="visible">False</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
@@ -198,12 +269,12 @@
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</child> -->
|
||||
<child>
|
||||
<widget class="GtkAlignment" id="remote_alignment1">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="left_padding">20</property>
|
||||
<!-- <property name="left_padding">20</property> -->
|
||||
<child>
|
||||
<widget class="GtkVBox" id="remote_vbox1">
|
||||
<property name="visible">True</property>
|
||||
@@ -364,7 +435,7 @@
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">3</property>
|
||||
<property name="position">5</property>
|
||||
</packing>
|
||||
</child>
|
||||
</widget>
|
||||
@@ -436,7 +507,7 @@
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<!-- <child>
|
||||
<widget class="GtkHBox" id="remote_username_hbox1">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
@@ -476,7 +547,7 @@
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</child> -->
|
||||
<child>
|
||||
<widget class="GtkHBox" id="remote_password_hbox1">
|
||||
<property name="visible">True</property>
|
||||
@@ -498,7 +569,7 @@
|
||||
<widget class="GtkEntry" id="input_remote_password">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="visibility">False</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>
|
||||
|
||||
@@ -35,7 +35,10 @@ PreferencePage = Ext.extend(Ext.Panel, {
|
||||
title: 'Streaming',
|
||||
border: false,
|
||||
layout: 'form',
|
||||
header: false,
|
||||
autoScroll: true,
|
||||
autoHeight: true,
|
||||
width: 320,
|
||||
_fields: {},
|
||||
|
||||
initComponent: function() {
|
||||
@@ -50,10 +53,12 @@ PreferencePage = Ext.extend(Ext.Panel, {
|
||||
title: 'Settings',
|
||||
style: 'margin-bottom: 0px; padding-bottom: 0px; padding-top: 5px',
|
||||
autoHeight: true,
|
||||
labelWidth: 1,
|
||||
labelAlign: 'top',
|
||||
labelWidth: 150,
|
||||
width: 300,
|
||||
defaultType: 'textfield',
|
||||
defaults: {
|
||||
width: 180,
|
||||
width: 280,
|
||||
}
|
||||
});
|
||||
|
||||
@@ -69,10 +74,12 @@ PreferencePage = Ext.extend(Ext.Panel, {
|
||||
title: 'File Serving Settings',
|
||||
style: 'margin-bottom: 0px; padding-bottom: 0px; padding-top: 5px',
|
||||
autoHeight: true,
|
||||
labelWidth: 110,
|
||||
labelAlign: 'top',
|
||||
labelWidth: 150,
|
||||
width: 280,
|
||||
defaultType: 'textfield',
|
||||
defaults: {
|
||||
width: 180,
|
||||
width: 260,
|
||||
}
|
||||
});
|
||||
|
||||
@@ -89,13 +96,22 @@ PreferencePage = Ext.extend(Ext.Panel, {
|
||||
maxValue: 99999,
|
||||
}));
|
||||
|
||||
var field = fieldset.add({
|
||||
xtype: 'togglefield',
|
||||
name: 'reverse_proxy_base_url',
|
||||
fieldLabel: 'Reverse Proxy Config',
|
||||
});
|
||||
|
||||
om.bind('reverse_proxy_enabled', field.toggle);
|
||||
om.bind('reverse_proxy_base_url', field.input);
|
||||
|
||||
fieldset = this.add({
|
||||
xtype: 'fieldset',
|
||||
border: false,
|
||||
autoHeight: true,
|
||||
defaultType: 'radio',
|
||||
style: 'margin-bottom: 5px; margin-top: 0; padding-bottom: 5px; padding-top: 0;',
|
||||
width: 240,
|
||||
width: 280,
|
||||
labelWidth: 1
|
||||
});
|
||||
|
||||
@@ -130,7 +146,7 @@ PreferencePage = Ext.extend(Ext.Panel, {
|
||||
autoHeight: true,
|
||||
defaultType: 'radio',
|
||||
style: 'margin-left: 24px; margin-bottom: 5px; margin-top: 0; padding-bottom: 5px; padding-top: 0;',
|
||||
width: 240,
|
||||
width: 280,
|
||||
labelWidth: 1
|
||||
});
|
||||
|
||||
@@ -164,12 +180,12 @@ PreferencePage = Ext.extend(Ext.Panel, {
|
||||
|
||||
om.bind('ssl_priv_key_path', fieldset.add({
|
||||
name: 'ssl_priv_key_path',
|
||||
fieldLabel: 'Private key file path'
|
||||
fieldLabel: 'Private key file path',
|
||||
}));
|
||||
|
||||
om.bind('ssl_cert_path', fieldset.add({
|
||||
name: 'ssl_cert_path',
|
||||
fieldLabel: 'Certificate and chains file path'
|
||||
fieldLabel: 'Certificate and chains file path',
|
||||
}));
|
||||
|
||||
fieldset = this.add({
|
||||
@@ -178,10 +194,12 @@ PreferencePage = Ext.extend(Ext.Panel, {
|
||||
title: 'Advanced settings',
|
||||
style: 'margin-bottom: 0px; padding-bottom: 0px; padding-top: 5px',
|
||||
autoHeight: true,
|
||||
labelWidth: 1,
|
||||
labelAlign: 'top',
|
||||
labelWidth: 150,
|
||||
width: 280,
|
||||
defaultType: 'textfield',
|
||||
defaults: {
|
||||
width: 180,
|
||||
width: 260,
|
||||
}
|
||||
});
|
||||
|
||||
@@ -189,7 +207,8 @@ PreferencePage = Ext.extend(Ext.Panel, {
|
||||
xtype: 'checkbox',
|
||||
name: 'allow_remote',
|
||||
boxLabel: 'Allow remote control',
|
||||
style: 'margin-left: 12px;'
|
||||
style: 'margin-left: 12px;',
|
||||
width: 150
|
||||
}));
|
||||
|
||||
fieldset = this.add({
|
||||
@@ -197,22 +216,23 @@ PreferencePage = Ext.extend(Ext.Panel, {
|
||||
border: false,
|
||||
style: 'margin-bottom: 0px; padding-bottom: 0px; padding-top: 5px',
|
||||
autoHeight: true,
|
||||
labelWidth: 110,
|
||||
labelAlign: 'top',
|
||||
labelWidth: 150,
|
||||
width: 260,
|
||||
defaultType: 'textfield',
|
||||
defaults: {
|
||||
width: 180,
|
||||
width: 240,
|
||||
}
|
||||
});
|
||||
|
||||
om.bind('remote_username', fieldset.add({
|
||||
xtype: 'textfield',
|
||||
name: 'remote_username',
|
||||
fieldLabel: 'Remote control username'
|
||||
}));
|
||||
// om.bind('remote_username', fieldset.add({
|
||||
// xtype: 'textfield',
|
||||
// name: 'remote_username',
|
||||
// fieldLabel: 'Remote control username'
|
||||
// }));
|
||||
|
||||
om.bind('remote_password', fieldset.add({
|
||||
xtype: 'textfield',
|
||||
inputType: 'password',
|
||||
name: 'remote_password',
|
||||
fieldLabel: 'Remote control password'
|
||||
}));
|
||||
@@ -233,7 +253,7 @@ PreferencePage = Ext.extend(Ext.Panel, {
|
||||
labelWidth: 1,
|
||||
defaultType: 'textfield',
|
||||
defaults: {
|
||||
width: 180,
|
||||
width: 200,
|
||||
}
|
||||
});
|
||||
|
||||
@@ -308,7 +328,7 @@ PreferencePage = Ext.extend(Ext.Panel, {
|
||||
var apiUrl = 'http';
|
||||
if (optionsManager.get('use_ssl'))
|
||||
apiUrl += 's';
|
||||
apiUrl += '://' + optionsManager.get('ip') + ':' + optionsManager.get('port') + '/streaming/stream';
|
||||
apiUrl += '://' + optionsManager.get('remote_username') + ':' + optionsManager.get('remote_password') + '@' + optionsManager.get('ip') + ':' + optionsManager.get('port') + '/streaming/stream';
|
||||
Ext.getCmp('remote_url').setValue(apiUrl);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,22 +2,18 @@ from twisted.internet import defer
|
||||
from twisted.python import log
|
||||
from twisted.web import http, resource, server, static
|
||||
|
||||
# NOTICE!
|
||||
# All these producers are taken directly from the Twisted Project.
|
||||
# This is because i needed to make them accept defers.
|
||||
# /NOTICE!
|
||||
|
||||
class NoRangeStaticProducer(static.NoRangeStaticProducer):
|
||||
@defer.inlineCallbacks
|
||||
def resumeProducing(self):
|
||||
if not self.request:
|
||||
return
|
||||
|
||||
|
||||
data = yield defer.maybeDeferred(self.fileObject.read, self.bufferSize)
|
||||
|
||||
|
||||
if not self.request:
|
||||
return
|
||||
|
||||
|
||||
if data:
|
||||
# this .write will spin the reactor, calling .doWrite and then
|
||||
# .resumeProducing again, so be prepared for a re-entrant call
|
||||
@@ -27,35 +23,36 @@ class NoRangeStaticProducer(static.NoRangeStaticProducer):
|
||||
self.request.finish()
|
||||
self.stopProducing()
|
||||
|
||||
|
||||
class SingleRangeStaticProducer(static.SingleRangeStaticProducer):
|
||||
@defer.inlineCallbacks
|
||||
def resumeProducing(self):
|
||||
if not self.request:
|
||||
return
|
||||
|
||||
data = yield defer.maybeDeferred(self.fileObject.read,
|
||||
min(self.bufferSize, self.size - self.bytesWritten))
|
||||
|
||||
|
||||
data = yield defer.maybeDeferred(self.fileObject.read, min(self.bufferSize, self.size - self.bytesWritten))
|
||||
|
||||
if not self.request:
|
||||
return
|
||||
|
||||
|
||||
if data:
|
||||
self.bytesWritten += len(data)
|
||||
# this .write will spin the reactor, calling .doWrite and then
|
||||
# .resumeProducing again, so be prepared for a re-entrant call
|
||||
self.request.write(data)
|
||||
|
||||
|
||||
if self.request and self.bytesWritten == self.size:
|
||||
self.request.unregisterProducer()
|
||||
self.request.finish()
|
||||
self.stopProducing()
|
||||
|
||||
|
||||
class MultipleRangeStaticProducer(static.MultipleRangeStaticProducer):
|
||||
@defer.inlineCallbacks
|
||||
def resumeProducing(self):
|
||||
if not self.request:
|
||||
return
|
||||
|
||||
|
||||
data = []
|
||||
dataLength = 0
|
||||
done = False
|
||||
@@ -64,9 +61,7 @@ class MultipleRangeStaticProducer(static.MultipleRangeStaticProducer):
|
||||
dataLength += len(self.partBoundary)
|
||||
data.append(self.partBoundary)
|
||||
self.partBoundary = None
|
||||
p = yield defer.maybeDeferred(self.fileObject.read,
|
||||
min(self.bufferSize - dataLength,
|
||||
self._partSize - self._partBytesWritten))
|
||||
p = yield defer.maybeDeferred(self.fileObject.read, min(self.bufferSize - dataLength, self._partSize - self._partBytesWritten))
|
||||
self._partBytesWritten += len(p)
|
||||
dataLength += len(p)
|
||||
data.append(p)
|
||||
@@ -76,18 +71,19 @@ class MultipleRangeStaticProducer(static.MultipleRangeStaticProducer):
|
||||
except StopIteration:
|
||||
done = True
|
||||
break
|
||||
|
||||
|
||||
if not self.request:
|
||||
return
|
||||
|
||||
|
||||
self.request.write(''.join(data))
|
||||
|
||||
|
||||
if done:
|
||||
self.request.unregisterProducer()
|
||||
self.request.finish()
|
||||
self.stopProducing()
|
||||
self.request = None
|
||||
|
||||
|
||||
class FilelikeObjectResource(static.File):
|
||||
isLeaf = True
|
||||
contentType = None
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
# statement from all source files in the program, then also delete it here.
|
||||
#
|
||||
|
||||
import json
|
||||
import gtk
|
||||
import os
|
||||
import subprocess
|
||||
@@ -50,8 +49,7 @@ from deluge.plugins.pluginbase import GtkPluginBase
|
||||
import deluge.component as component
|
||||
import deluge.common
|
||||
|
||||
from twisted.internet import reactor, defer, threads
|
||||
from twisted.web import server, resource
|
||||
from twisted.internet import defer, threads
|
||||
|
||||
from common import get_resource
|
||||
|
||||
@@ -69,6 +67,13 @@ def execute_url(url):
|
||||
|
||||
|
||||
class GtkUI(GtkPluginBase):
|
||||
def get_widget(self, widget_name):
|
||||
main_window = component.get("MainWindow")
|
||||
if hasattr(main_window, 'main_glade'):
|
||||
return main_window.main_glade.get_widget(widget_name)
|
||||
else:
|
||||
return main_window.main_builder.get_object(widget_name)
|
||||
|
||||
def enable(self):
|
||||
self.glade = gtk.glade.XML(get_resource("config.glade"))
|
||||
|
||||
@@ -76,7 +81,7 @@ class GtkUI(GtkPluginBase):
|
||||
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 = component.get("MainWindow").main_glade.get_widget('menu_file_tab')
|
||||
file_menu = self.get_widget('menu_file_tab')
|
||||
|
||||
self.sep = gtk.SeparatorMenuItem()
|
||||
self.item = gtk.MenuItem(_("_Stream this file"))
|
||||
@@ -105,7 +110,7 @@ class GtkUI(GtkPluginBase):
|
||||
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 = component.get("MainWindow").main_glade.get_widget('menu_file_tab')
|
||||
file_menu = self.get_widget('menu_file_tab')
|
||||
|
||||
file_menu.remove(self.item)
|
||||
file_menu.remove(self.sep)
|
||||
@@ -119,10 +124,11 @@ class GtkUI(GtkPluginBase):
|
||||
def on_apply_prefs(self):
|
||||
log.debug("applying prefs for Streaming")
|
||||
|
||||
if self.glade.get_widget("input_serve_standalone").get_active():
|
||||
serve_method = 'standalone'
|
||||
elif self.glade.get_widget("input_serve_webui").get_active():
|
||||
serve_method = 'webui'
|
||||
serve_method = 'standalone'
|
||||
# if self.glade.get_widget("input_serve_standalone").get_active():
|
||||
# serve_method = 'standalone'
|
||||
# elif self.glade.get_widget("input_serve_webui").get_active():
|
||||
# serve_method = 'webui'
|
||||
|
||||
if self.glade.get_widget("input_ssl_cert_daemon").get_active():
|
||||
ssl_source = 'daemon'
|
||||
@@ -136,8 +142,11 @@ class GtkUI(GtkPluginBase):
|
||||
"auto_open_stream_urls": self.glade.get_widget("input_auto_open_stream_urls").get_active(),
|
||||
"allow_remote": self.glade.get_widget("input_allow_remote").get_active(),
|
||||
"download_only_streamed": self.glade.get_widget("input_download_only_streamed").get_active(),
|
||||
"reverse_proxy_enabled": self.glade.get_widget("input_reverse_proxy_enabled").get_active(),
|
||||
# "download_in_order": self.glade.get_widget("input_download_in_order").get_active(),
|
||||
"use_ssl": self.glade.get_widget("input_use_ssl").get_active(),
|
||||
"remote_username": self.glade.get_widget("input_remote_username").get_text(),
|
||||
# "remote_username": self.glade.get_widget("input_remote_username").get_text(),
|
||||
"reverse_proxy_base_url": self.glade.get_widget("input_reverse_proxy_base_url").get_text(),
|
||||
"remote_password": self.glade.get_widget("input_remote_password").get_text(),
|
||||
"ssl_priv_key_path": self.glade.get_widget("input_ssl_priv_key_path").get_text(),
|
||||
"ssl_cert_path": self.glade.get_widget("input_ssl_cert_path").get_text(),
|
||||
@@ -168,18 +177,22 @@ class GtkUI(GtkPluginBase):
|
||||
self.glade.get_widget("input_allow_remote").set_active(config["allow_remote"])
|
||||
self.glade.get_widget("input_use_ssl").set_active(config["use_ssl"])
|
||||
self.glade.get_widget("input_download_only_streamed").set_active(config["download_only_streamed"])
|
||||
self.glade.get_widget("input_remote_username").set_text(config["remote_username"])
|
||||
self.glade.get_widget("input_reverse_proxy_enabled").set_active(config["reverse_proxy_enabled"])
|
||||
# self.glade.get_widget("input_download_in_order").set_active(config["download_in_order"])
|
||||
# self.glade.get_widget("input_download_everything").set_active(not config["download_in_order"] and not config["download_only_streamed"])
|
||||
# self.glade.get_widget("input_remote_username").set_text(config["remote_username"])
|
||||
self.glade.get_widget("input_reverse_proxy_base_url").set_text(config["reverse_proxy_base_url"])
|
||||
self.glade.get_widget("input_remote_password").set_text(config["remote_password"])
|
||||
self.glade.get_widget("input_ssl_priv_key_path").set_text(config["ssl_priv_key_path"])
|
||||
self.glade.get_widget("input_ssl_cert_path").set_text(config["ssl_cert_path"])
|
||||
|
||||
self.glade.get_widget("input_serve_standalone").set_active(config["serve_method"] == "standalone")
|
||||
self.glade.get_widget("input_serve_webui").set_active(config["serve_method"] == "webui")
|
||||
# self.glade.get_widget("input_serve_standalone").set_active(config["serve_method"] == "standalone")
|
||||
# self.glade.get_widget("input_serve_webui").set_active(config["serve_method"] == "webui")
|
||||
|
||||
self.glade.get_widget("input_ssl_cert_daemon").set_active(config["ssl_source"] == "daemon")
|
||||
self.glade.get_widget("input_ssl_cert_custom").set_active(config["ssl_source"] == "custom")
|
||||
|
||||
api_url = 'http%s://%s:%s/streaming/stream' % (('s' if config["use_ssl"] else ''), config["ip"], config["port"])
|
||||
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.glade.get_widget("output_remote_url").set_text(api_url)
|
||||
|
||||
def stream_ready(self, result):
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
from twisted.web.resource import Resource as TwistedResource, _computeAllowedMethods
|
||||
from twisted.web import server, error
|
||||
from twisted.web import server
|
||||
from twisted.internet import defer
|
||||
|
||||
|
||||
class Resource(TwistedResource):
|
||||
content_type = 'application/json'
|
||||
|
||||
|
||||
def __init__(self, username=None, password=None, *args, **kwargs):
|
||||
self.username = username
|
||||
self.password = password
|
||||
TwistedResource.__init__(self, *args, **kwargs)
|
||||
|
||||
def render(self, request): # Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
|
||||
|
||||
def render(self, request): # Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
|
||||
"""
|
||||
Adds support for deferred render methods
|
||||
"""
|
||||
auth_header = request.getHeader('Authorization')
|
||||
|
||||
|
||||
if self.username or self.password:
|
||||
authenticated = False
|
||||
if auth_header:
|
||||
@@ -27,14 +27,11 @@ class Resource(TwistedResource):
|
||||
username, password = userpass
|
||||
if self.username == username and self.password == password:
|
||||
authenticated = True
|
||||
|
||||
|
||||
|
||||
if not authenticated:
|
||||
print auth_header
|
||||
print self.username, self.password
|
||||
request.setResponseCode(401)
|
||||
return 'Unauthorized'
|
||||
|
||||
|
||||
m = getattr(self, 'render_' + request.method, None)
|
||||
if not m:
|
||||
# This needs to be here until the deprecated subclasses of the
|
||||
@@ -43,18 +40,18 @@ class Resource(TwistedResource):
|
||||
allowedMethods = (getattr(self, 'allowedMethods', 0) or
|
||||
_computeAllowedMethods(self))
|
||||
raise UnsupportedMethod(allowedMethods)
|
||||
|
||||
|
||||
result = defer.maybeDeferred(m, request)
|
||||
|
||||
|
||||
def write_rest(defer_result, request):
|
||||
request.write(defer_result)
|
||||
request.finish()
|
||||
|
||||
|
||||
def err_rest(defer_result=None):
|
||||
defer_result.printTraceback()
|
||||
request.finish()
|
||||
|
||||
|
||||
result.addCallback(write_rest, request)
|
||||
result.addErrback(err_rest)
|
||||
|
||||
return server.NOT_DONE_YET
|
||||
|
||||
return server.NOT_DONE_YET
|
||||
|
||||
68
streaming/torrentfile.py
Normal file
68
streaming/torrentfile.py
Normal file
@@ -0,0 +1,68 @@
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
|
||||
from thomas import InputBase
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DelugeTorrentInput(InputBase.find_plugin('file')):
|
||||
plugin_name = 'torrent_file'
|
||||
protocols = []
|
||||
|
||||
can_read_to = None
|
||||
|
||||
def __init__(self, item, torrent_handler, infohash, offset, path):
|
||||
self.item = item
|
||||
self.torrent_handler = torrent_handler
|
||||
self.torrent = torrent_handler.get_torrent(infohash)
|
||||
self.infohash = infohash
|
||||
self.offset = offset
|
||||
self.path = path
|
||||
self.size, self.filename, self.content_type = self.get_info()
|
||||
|
||||
def get_info(self):
|
||||
logger.info('Getting info about %r' % (self.path, ))
|
||||
|
||||
content_type = mimetypes.guess_type(self.path)[0] or 'bytes'
|
||||
|
||||
return self.item['size'], os.path.basename(self.path), content_type
|
||||
|
||||
def ensure_exists(self):
|
||||
if not os.path.exists(self.path):
|
||||
self.torrent.can_read(self.offset)
|
||||
|
||||
def seek(self, pos):
|
||||
self.ensure_exists()
|
||||
super(DelugeTorrentInput, self).seek(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):
|
||||
self.ensure_exists()
|
||||
|
||||
if not self._open_file:
|
||||
self.seek(0)
|
||||
|
||||
#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._open_file:
|
||||
self._open_file.seek(tell)
|
||||
|
||||
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))
|
||||
|
||||
if not self._open_file: # the file was closed while we waited
|
||||
return b''
|
||||
|
||||
data = super(DelugeTorrentInput, self).read(real_num)
|
||||
return data
|
||||
|
||||
def close(self):
|
||||
self.torrent.remove_reader(self)
|
||||
super(DelugeTorrentInput, self).close()
|
||||
@@ -44,6 +44,6 @@ from deluge.plugins.pluginbase import WebPluginBase
|
||||
|
||||
from common import get_resource
|
||||
|
||||
class WebUI(WebPluginBase):
|
||||
|
||||
scripts = [get_resource("streaming.js")]
|
||||
class WebUI(WebPluginBase):
|
||||
scripts = [get_resource("streaming.js")]
|
||||
|
||||
Reference in New Issue
Block a user