mirror of
https://github.com/JohnDoee/deluge-streaming/
synced 2026-07-01 07:31:17 -07:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ab94d22b9 | ||
|
|
4b1d3799b5 | ||
|
|
1398e5042b | ||
|
|
de54ed067a | ||
|
|
7cfdfc79fe | ||
|
|
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
|
||||
85
README.md
85
README.md
@@ -38,12 +38,95 @@ By using a small tool it is possible to it's possible to open streams directly i
|
||||
## Motivation
|
||||
|
||||
The plugin is not meant to be used as a right-click to stream thing. The idea is to
|
||||
make Deluge an abstraction layer for the [TidalStream](http://www.tidalstream.org/) project, i.e. torrents to http on demand.
|
||||
make Deluge an abstraction layer for the [Tidalstream](http://www.tidalstream.org/) project, i.e. torrents to http on demand.
|
||||
|
||||
The _allow remote_ option is to allow remote add and stream of torrents.
|
||||
|
||||
## Todo
|
||||
|
||||
* [x] Add RAR streaming support
|
||||
* [ ] Better feedback in interface about streams
|
||||
* [ ] Better feedback when using API
|
||||
* [x] Reverse proxy improvement (e.g. port different than bind port)
|
||||
* [ ] Fix problems when removing torrent from Deluge (sea of errors)
|
||||
|
||||
# HTTP API Usage
|
||||
|
||||
## Prerequisite
|
||||
|
||||
Install and enable the plugin. Afterwards, head into Streaming settings and enable "Allow remote control".
|
||||
The URL found in the "Remote control url" field is where the API can be reached. The auth used is Basic Auth.
|
||||
|
||||
## Usage
|
||||
|
||||
There is only one end-point and that is where a torrent stream can be requested.
|
||||
|
||||
Both return the same responses and all responses are JSON encoded.
|
||||
All successfully authenticated responses have status code 200.
|
||||
|
||||
## POST /streaming/stream
|
||||
|
||||
POST body must be the raw torrent you want to stream. No form formatting or anything can be used.
|
||||
|
||||
List of URL GET Arguments
|
||||
|
||||
* **path**: Path inside the torrent file to either a folder or a file you want to stream. The plugin will try to guess the best one. **Optional**. **Default**: '' (i.e. find the best file in the whole torrent)
|
||||
* **infohash**: Infohash of the torrent you want to stream, can make it a bit faster as it can avoid reading POST body. **Optional**.
|
||||
* **label**: If label plugin is enabled and the torrent is actually added then give the torrent this label. **Optional**. **Default**: ''
|
||||
* **wait_for_end_pieces**: Wait for the first and last piece in the streamed file to be fully downloaded. Can be necessary for some video players. It also enforces that the torrent can be actually downloaded. If the key exist with any (even empty) value, the feature is enabled. **Optional**. **Default**: false
|
||||
|
||||
## GET /streaming/stream
|
||||
|
||||
* **infohash**: Does the same as when POSTed. **Mandatory**.
|
||||
* **path**: Does the same as when POSTed. **Optional**.
|
||||
* **wait_for_end_pieces**: Does the same as when POSTed. **Optional**.
|
||||
|
||||
## Success Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success", # Always equals this
|
||||
"filename" "horse.mkv", # Filename of the streamed torrent
|
||||
"url": "http://example.com/" # URL where the file can be reached by e.g. a media player
|
||||
}
|
||||
```
|
||||
|
||||
## Error Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "error", # Always equals this
|
||||
"message" "Torrent failed" # description for why it failed
|
||||
}
|
||||
```
|
||||
|
||||
# Version Info
|
||||
|
||||
## Version 0.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
|
||||
* 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.4"
|
||||
__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
|
||||
|
||||
1030
streaming/core.py
1030
streaming/core.py
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