15 Commits

Author SHA1 Message Date
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
6 changed files with 281 additions and 40 deletions

View File

@@ -38,19 +38,79 @@ 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.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

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

View File

@@ -42,7 +42,7 @@ from setuptools import setup, find_packages
__plugin_name__ = "Streaming"
__author__ = "Anders Jensen"
__author_email__ = "johndoee@tidalstream.org"
__version__ = "0.10.2"
__version__ = "0.10.5"
__url__ = "https://github.com/JohnDoee/deluge-streaming"
__license__ = "GPLv3"
__description__ = "Enables streaming of files while downloading them."

View File

@@ -71,6 +71,12 @@ AUDIO_STREAMABLE_EXTENSIONS = ['flac', 'mp3', 'oga']
STREAMABLE_EXTENSIONS = set(VIDEO_STREAMABLE_EXTENSIONS + AUDIO_STREAMABLE_EXTENSIONS)
TORRENT_CLEANUP_INTERVAL = timedelta(minutes=30)
MAX_FILE_PRIORITY = 2
MAX_PIECE_PRIORITY = 7
MIN_WAIT_PIECE_PRIORITY_DELAY = timedelta(seconds=5)
WITHIN_CHAIN_PERCENTAGE = 0.10
MIN_PIECE_COUNT_FOR_CHAIN_CONSIDERATION = 40
MIN_CHAIN_WAIT_DELAY = timedelta(seconds=8)
DEFAULT_PREFS = {
'ip': '127.0.0.1',
@@ -128,6 +134,7 @@ class Torrent(object):
self.readers = {}
self.cycle_lock = defer.DeferredLock()
self.last_activity = datetime.now()
self.waited_pieces = set()
self.torrent = get_torrent(infohash)
status = self.torrent.get_status(['piece_length'])
@@ -152,6 +159,7 @@ class Torrent(object):
def can_read(self, from_byte):
self.ensure_started()
status = self.torrent.get_status(['pieces'])
needed_piece, rest = divmod(from_byte, self.piece_length)
last_available_piece = None
for piece, status in enumerate(self.torrent.status.pieces[needed_piece:], needed_piece):
@@ -160,11 +168,31 @@ class Torrent(object):
last_available_piece = piece
if last_available_piece is None:
logger.debug('Since we are waiting for a piece, setting priority for %s to max' % (needed_piece, ))
self.torrent.handle.set_piece_deadline(needed_piece, 0)
self.torrent.handle.piece_priority(needed_piece, 7)
logger.debug('Since we are waiting for a piece, we need to check if we should set piece %s to max' % (needed_piece, ))
is_next_in_chain = False
f = self.get_file_from_offset(from_byte)
file_piece_count = (f['size'] // self.piece_length) + 1
if file_piece_count <= MIN_PIECE_COUNT_FOR_CHAIN_CONSIDERATION:
is_next_in_chain = True
else:
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
if not is_next_in_chain:
logger.debug('Not a next-in-chain piece, setting priority now')
self.torrent.handle.set_piece_deadline(needed_piece, 0)
self.torrent.handle.piece_priority(needed_piece, MAX_PIECE_PRIORITY)
file_priorities = self.torrent.get_file_priorities()
if file_priorities[f['index']] != MAX_FILE_PRIORITY:
@@ -172,12 +200,20 @@ class Torrent(object):
file_priorities[f['index']] = MAX_FILE_PRIORITY
self.torrent.set_file_priorities(file_priorities)
for _ in range(300):
for i in range(300):
if self.torrent.status.pieces[needed_piece]:
break
if not reactor.running:
return
if is_next_in_chain and i == MIN_CHAIN_WAIT_DELAY.total_seconds() * 5 and needed_piece not in self.get_currently_downloading():
logger.debug('Next in chain waiting failed, setting priority')
self.torrent.handle.set_piece_deadline(needed_piece, 0)
self.torrent.handle.piece_priority(needed_piece, MAX_PIECE_PRIORITY)
time.sleep(0.2)
status = self.torrent.get_status(['pieces'])
logger.debug('Calling read again to get the real number')
return self.can_read(from_byte)
@@ -251,7 +287,7 @@ class Torrent(object):
self.torrent.set_file_priorities(file_priorities)
if self.readers:
status = self.torrent.get_status(['files', 'file_progress'])
status = self.torrent.get_status(['files', 'file_progress', 'pieces'])
file_ranges = {}
fileset_ranges = {}
for path, from_byte, to_byte in self.readers.values():
@@ -261,6 +297,10 @@ class Torrent(object):
else:
file_ranges[path] = from_byte
reader_piece = from_byte // self.piece_length
self.torrent.handle.set_piece_deadline(reader_piece, 0)
self.torrent.handle.piece_priority(reader_piece, MAX_PIECE_PRIORITY)
for fileset_hash, fileset in self.filesets.items():
if path in fileset['files']:
if fileset_hash in fileset_ranges:
@@ -311,8 +351,6 @@ class Torrent(object):
if piece < current_piece:
self.torrent.handle.piece_priority(piece, 0)
elif piece == current_piece:
self.torrent.handle.piece_priority(piece, 7)
else:
self.torrent.handle.piece_priority(piece, 1)
@@ -325,6 +363,7 @@ class Torrent(object):
return currently_downloading
def reset_priorities(self):
status = self.torrent.get_status(['pieces'])
for piece in range(len(self.torrent.status.pieces)):
self.torrent.handle.piece_priority(piece, 1)
@@ -468,6 +507,8 @@ class TorrentHandler(object):
filesystem = self.get_filesystem(infohash)
if path:
stream_item = filesystem.get_item_from_path(path)
if stream_item and filesystem == stream_item and path != stream_item.id:
stream_item = filesystem.get_item_from_path('%s/%s' % (filesystem.id, path))
else:
stream_item = filesystem
@@ -518,11 +559,13 @@ class TorrentHandler(object):
wait_for_pieces.append(piece - 1)
logger.debug('We want first and last piece first, these are the pieces: %r' % (wait_for_pieces, ))
for piece in wait_for_pieces:
torrent.handle.set_piece_deadline(piece, 0)
torrent.handle.piece_priority(piece, 7)
if wait_for_pieces:
for piece in wait_for_pieces:
torrent.handle.set_piece_deadline(piece, 0)
torrent.handle.piece_priority(piece, MAX_PIECE_PRIORITY)
for _ in range(220):
status = torrent.get_status(['pieces'])
for piece in wait_for_pieces:
if not torrent.status.pieces[piece]:
break
@@ -572,6 +615,7 @@ class StreamResource(Resource):
infohash = request.args.get('infohash')
path = request.args.get('path')
wait_for_end_pieces = bool(request.args.get('wait_for_end_pieces'))
label = request.args.get('label')
if path:
path = path[0]
@@ -583,11 +627,16 @@ class StreamResource(Resource):
else:
infohash = infohash
if label:
label = label[0]
else:
label = None
payload = request.content.read()
if not payload:
defer.returnValue(json.dumps({'status': 'error', 'message': 'invalid torrent'}))
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.inlineCallbacks
@@ -761,7 +810,7 @@ class Core(CorePluginBase):
@export
@defer.inlineCallbacks
def stream_torrent(self, infohash=None, url=None, filedump=None, filepath_or_index=None, includes_name=False, wait_for_end_pieces=False):
def stream_torrent(self, infohash=None, url=None, filedump=None, filepath_or_index=None, includes_name=False, wait_for_end_pieces=False, label=None, as_inline=False):
logger.debug('Trying to stream infohash:%s, url:%s, filepath_or_index:%s' % (infohash, url, filepath_or_index))
torrent = get_torrent(infohash)
@@ -780,6 +829,15 @@ class Core(CorePluginBase):
core = component.get("Core")
try:
yield core.add_torrent_file('file.torrent', filedump.encode('base64'), {'add_paused': True})
if label and 'Label' in component.get('CorePluginManager').get_enabled_plugins():
label_plugin = component.get('CorePlugin.Label')
if label not in label_plugin.get_labels():
label_plugin.add(label)
try:
label_plugin.set_torrent(infohash, label)
except:
logger.exception('Failed to set label')
except:
logger.exception('Failed to add torrent')
defer.returnValue({'status': 'error', 'message': 'failed to add torrent'})
@@ -794,7 +852,7 @@ class Core(CorePluginBase):
try:
stream_or_item = yield defer.maybeDeferred(self.torrent_handler.stream, infohash, fn, wait_for_end_pieces=wait_for_end_pieces)
stream_url = self.thomas_http_output.serve_item(stream_or_item)
stream_url = self.thomas_http_output.serve_item(stream_or_item, as_inline=as_inline)
except:
logger.exception('Failed to stream torrent')
defer.returnValue({'status': 'error', 'message': 'failed to stream torrent'})

View File

@@ -349,20 +349,23 @@ StreamingPlugin = Ext.extend(Deluge.Plugin, {
deluge.preferences.addPage(this.prefsPage);
console.log('Streaming plugin loaded');
var doStream = function (tid, fileIndex) {
deluge.client.streaming.stream_torrent(tid, null, null, fileIndex, true, {
var doStream = function (tid, fileIndex, asInline) {
deluge.client.streaming.stream_torrent(tid, null, null, fileIndex, true, false, null, asInline, {
success: function (result) {
console.log('Got result', result);
if (result.status == 'success') {
var url = result.url;
if (result.use_stream_urls) {
url = 'stream+' + url;
if (result.auto_open_stream_urls) {
window.location.assign(url);
return;
if (asInline) {
window.open(result.url, '_blank');
} else {
var url = result.url;
if (result.use_stream_urls) {
url = 'stream+' + url;
if (result.auto_open_stream_urls) {
window.location.assign(url);
return;
}
}
Ext.Msg.alert('Stream ready', 'URL for stream: <a target="_blank" href="' + url + '">' + url + '</a>');
}
Ext.Msg.alert('Stream ready', 'URL for stream: <a target="_blank" href="' + url + '">' + url + '</a>');
} else {
Ext.Msg.alert('Stream failed', 'Error message: ' + result.message);
}
@@ -370,6 +373,28 @@ StreamingPlugin = Ext.extend(Deluge.Plugin, {
})
}
var triggerStreamFile = function (asInline) {
var files = deluge.details.items.items[2];
var nodes = files.getSelectionModel().getSelectedNodes();
if (nodes) {
var fileIndex = nodes[0].attributes.fileIndex;
var tid = files.torrentId;
if (fileIndex >= 0) {
doStream(tid, fileIndex, asInline);
}
}
}
deluge.menus.filePriorities.addMenuItem({
id: 'playthis',
text: 'Play in browser',
iconCls: 'icon-resume',
handler: function (item, event) {
deluge.menus.filePriorities.hide();
triggerStreamFile(true);
return false;
}
});
deluge.menus.filePriorities.addMenuItem({
id: 'streamthis',
@@ -377,15 +402,26 @@ StreamingPlugin = Ext.extend(Deluge.Plugin, {
iconCls: 'icon-down',
handler: function (item, event) {
deluge.menus.filePriorities.hide();
var files = deluge.details.items.items[2];
var nodes = files.getSelectionModel().getSelectedNodes();
if (nodes) {
var fileIndex = nodes[0].attributes.fileIndex;
var tid = files.torrentId;
if (fileIndex >= 0) {
doStream(tid, fileIndex);
}
}
triggerStreamFile(false);
return false;
}
});
var triggerStreamTorrent = function (asInline) {
var ids = deluge.torrents.getSelectedIds();
if (ids) {
doStream(ids[0], null, asInline);
}
}
deluge.menus.torrent.addMenuItem({
id: 'playthistorrent',
text: 'Play in browser',
iconCls: 'icon-resume',
handler: function (item, event) {
deluge.menus.torrent.hide();
triggerStreamTorrent(true);
return false;
}
});
@@ -396,10 +432,7 @@ StreamingPlugin = Ext.extend(Deluge.Plugin, {
iconCls: 'icon-down',
handler: function (item, event) {
deluge.menus.torrent.hide();
var ids = deluge.torrents.getSelectedIds();
if (ids) {
doStream(ids[0]);
}
triggerStreamTorrent(false);
return false;
}
});