mirror of
https://github.com/JohnDoee/deluge-streaming/
synced 2026-07-01 07:31:17 -07:00
added authentication to remote api and reset when streamed file is complete
This commit is contained in:
@@ -39,5 +39,14 @@ The _allow remote_ option is to allow remote add and stream of torrents.
|
||||
|
||||
# Version Info
|
||||
|
||||
## Version 0.3
|
||||
* Fixed bug when streaming multiple files.
|
||||
* Changed to try to prioritize end pieces more aggressively to not leave them hanging.
|
||||
* Added option to download rest of torrent when finished downloading the streamed torrent.
|
||||
* Added authentication to remote API.
|
||||
|
||||
## Version 0.2
|
||||
* Improved buffering algorithm, not using only deadline anymore.
|
||||
|
||||
## Version 0.1
|
||||
* Initial working release
|
||||
@@ -66,6 +66,9 @@ DEFAULT_PREFS = {
|
||||
'ip': '127.0.0.1',
|
||||
'port': 46123,
|
||||
'allow_remote': False,
|
||||
'reset_complete': True,
|
||||
'remote_username': 'username',
|
||||
'remote_password': 'password',
|
||||
}
|
||||
|
||||
from .filelike import FilelikeObjectResource
|
||||
@@ -109,9 +112,9 @@ class FileServeResource(resource.Resource):
|
||||
class AddTorrentResource(Resource):
|
||||
isLeaf = True
|
||||
|
||||
def __init__(self, client):
|
||||
def __init__(self, client, *args, **kwargs):
|
||||
self.client = client
|
||||
Resource.__init__(self)
|
||||
Resource.__init__(self, *args, **kwargs)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def render_POST(self, request):
|
||||
@@ -131,9 +134,9 @@ class AddTorrentResource(Resource):
|
||||
class StreamResource(Resource):
|
||||
isLeaf = True
|
||||
|
||||
def __init__(self, client):
|
||||
def __init__(self, client, *args, **kwargs):
|
||||
self.client = client
|
||||
Resource.__init__(self)
|
||||
Resource.__init__(self, *args, **kwargs)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def render_GET(self, request):
|
||||
@@ -151,7 +154,7 @@ class StreamResource(Resource):
|
||||
|
||||
class TorrentFile(object):
|
||||
file_handler = None
|
||||
def __init__(self, torrent_handler, file_path, torrent_file_path, size, chunk_size, offset):
|
||||
def __init__(self, torrent_handler, file_path, torrent_file_path, size, chunk_size, offset, file_index):
|
||||
self.torrent_handler = torrent_handler
|
||||
self.file_path = file_path
|
||||
self.torrent_file_path = torrent_file_path
|
||||
@@ -159,6 +162,7 @@ class TorrentFile(object):
|
||||
self.last_chunk = (offset + size) / chunk_size
|
||||
self.chunk_size = chunk_size
|
||||
self.offset = offset
|
||||
self.file_index = file_index
|
||||
self.size = size
|
||||
self.last_requested_chunk = self.first_chunk
|
||||
self.is_closed = False
|
||||
@@ -247,7 +251,8 @@ class TorrentFile(object):
|
||||
return self.file_handler.close()
|
||||
|
||||
def copy(self):
|
||||
tf = TorrentFile(self.torrent_handler, self.file_path, self.torrent_file_path, self.size, self.chunk_size, self.offset)
|
||||
tf = TorrentFile(self.torrent_handler, self.file_path, self.torrent_file_path,
|
||||
self.size, self.chunk_size, self.offset, self.file_index)
|
||||
return tf
|
||||
|
||||
class TorrentHandler(object):
|
||||
@@ -256,7 +261,7 @@ class TorrentHandler(object):
|
||||
self.torrent_handle = torrent.handle
|
||||
self.torrent_id = torrent_id
|
||||
self.core = core
|
||||
self.priorities = [0] * len(torrent.get_status(['files'])['files']) # TODO: get current priorities and use those instead
|
||||
self.priorities = [0] * len(torrent.get_status(['files'])['files'])
|
||||
self.torrent_files = defaultdict(list)
|
||||
self.priorities_increased = {}
|
||||
# need to blackhole all pieces not downloaded yet
|
||||
@@ -340,14 +345,30 @@ class TorrentHandler(object):
|
||||
|
||||
return bool(handled_heads)
|
||||
|
||||
def update_chunk_priorities(self):
|
||||
def update_chunk_priorities(self): # TODO: check if torrent still exists
|
||||
file_progress = self.torrent.get_status(['file_progress'])['file_progress']
|
||||
incomplete_files = False
|
||||
|
||||
for torrent_file_path, tfs in self.torrent_files.items():
|
||||
if not tfs:
|
||||
logger.debug('No heads left for %r' % torrent_file_path)
|
||||
del self.torrent_files[torrent_file_path]
|
||||
continue
|
||||
|
||||
tf = tfs[0]
|
||||
if file_progress[tf.file_index] == 1.0:
|
||||
logger.info('%s is already complete, skipping' % torrent_file_path)
|
||||
continue
|
||||
|
||||
if self.update_chunk_priority(tfs):
|
||||
self.last_activity = datetime.now()
|
||||
|
||||
incomplete_files = True
|
||||
|
||||
if not incomplete_files and self.core.config['reset_complete'] and not all(self.priorities):
|
||||
logger.info('We are not doing any file streamings, but not downloading all files, changing that.')
|
||||
self.priorities = [1] * len(self.priorities)
|
||||
self.update_priorities()
|
||||
|
||||
if datetime.now() - self.last_activity > HANDLERS_TIMEOUT:
|
||||
logger.debug('Torrent handler idle, killing myself.')
|
||||
@@ -385,8 +406,23 @@ class TorrentHandler(object):
|
||||
if progress == 1: # file is complete, no need to fire up all the torrent jazz
|
||||
defer.returnValue(static.File(fp))
|
||||
|
||||
self.priorities = [0] * len(self.priorities)
|
||||
self.priorities[f['index']] = 3
|
||||
current_tfs = []
|
||||
for tfs in self.torrent_files.values():
|
||||
if not tfs:
|
||||
continue
|
||||
tf = tfs[0]
|
||||
self.priorities[tf.file_index] = 3
|
||||
current_tfs.append((tf, self.torrent_handle.piece_priorities()[tf.first_chunk:tf.last_chunk+1]))
|
||||
|
||||
self.update_priorities()
|
||||
|
||||
for tf, chunk_status in current_tfs:
|
||||
logger.info('Setting chunks to old status')
|
||||
for i, chunk in enumerate(chunk_status, tf.first_chunk):
|
||||
logger.debug('Setting status on chunk %s back to %s' % (i, chunk))
|
||||
self.torrent_handle.piece_priority(i, chunk)
|
||||
|
||||
self.torrent.resume()
|
||||
|
||||
@@ -394,7 +430,7 @@ class TorrentHandler(object):
|
||||
size_pieces = int(min(math.ceil((EXPECTED_SIZE * 1.0) / piece_length), f['pieces']))
|
||||
expected_pieces = max(percent_pieces, size_pieces) # we need to download either 5% or 5MB of the file before allowing stream.
|
||||
|
||||
tf = TorrentFile(self, fp, f['path'], f['size'], status['piece_length'], f['offset'])
|
||||
tf = TorrentFile(self, fp, f['path'], f['size'], status['piece_length'], f['offset'], f['index'])
|
||||
|
||||
if len(self.torrent_files[f['path']]) == 1:
|
||||
self.blackhole_all_pieces(tf.first_chunk, tf.last_chunk)
|
||||
@@ -422,11 +458,14 @@ class Core(CorePluginBase):
|
||||
self.resource = Resource()
|
||||
self.resource.putChild('file', self.fsr)
|
||||
if self.config['allow_remote']:
|
||||
self.resource.putChild('add_torrent', AddTorrentResource(self))
|
||||
self.resource.putChild('stream', StreamResource(self))
|
||||
self.resource.putChild('add_torrent', AddTorrentResource(username=self.config['remote_username'],
|
||||
password=self.config['remote_password'],
|
||||
client=self))
|
||||
self.resource.putChild('stream', StreamResource(username=self.config['remote_username'],
|
||||
password=self.config['remote_password'],
|
||||
client=self))
|
||||
|
||||
self.site = server.Site(self.resource)
|
||||
self.listening = reactor.listenTCP(self.config.config['port'], self.site, interface=self.config.config['ip'])
|
||||
|
||||
session = component.get("Core").session
|
||||
settings = session.get_settings()
|
||||
@@ -434,6 +473,11 @@ class Core(CorePluginBase):
|
||||
session.set_settings(settings)
|
||||
|
||||
self.torrent_handlers = {}
|
||||
|
||||
try:
|
||||
self.listening = reactor.listenTCP(self.config['port'], self.site, interface=self.config['ip'])
|
||||
except:
|
||||
self.listening = reactor.listenTCP(self.config['port'], self.site, interface='127.0.0.1')
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def disable(self):
|
||||
@@ -500,5 +544,5 @@ class Core(CorePluginBase):
|
||||
defer.returnValue({
|
||||
'status': 'success',
|
||||
'url': 'http://%s:%s/file/%s/%s' % (self.config.config['ip'], self.config.config['port'],
|
||||
self.fsr.add_file(tf), os.path.basename(tf.file_path))
|
||||
self.fsr.add_file(tf), urllib.quote_plus(os.path.basename(tf.file_path)))
|
||||
})
|
||||
@@ -10,6 +10,7 @@
|
||||
<widget class="GtkVBox" id="label_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="orientation">vertical</property>
|
||||
|
||||
<child>
|
||||
<widget class="GtkLabel" id="label_ip">
|
||||
<property name="visible">True</property>
|
||||
@@ -20,6 +21,7 @@
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
|
||||
<child>
|
||||
<widget class="GtkLabel" id="label_port">
|
||||
<property name="visible">True</property>
|
||||
@@ -30,6 +32,18 @@
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
|
||||
<child>
|
||||
<widget class="GtkLabel" id="label_reset_complete">
|
||||
<property name="visible">True</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="label" translatable="yes">Reset "do not download" when streamed file is complete:</property>
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
|
||||
<child>
|
||||
<widget class="GtkLabel" id="label_allow_remote">
|
||||
<property name="visible">True</property>
|
||||
@@ -37,9 +51,32 @@
|
||||
<property name="label" translatable="yes">Allow remote control:</property>
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="position">2</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
|
||||
<child>
|
||||
<widget class="GtkLabel" id="label_remote_username">
|
||||
<property name="visible">True</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="label" translatable="yes">Remote username:</property>
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
|
||||
<child>
|
||||
<widget class="GtkLabel" id="label_remote_password">
|
||||
<property name="visible">True</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="label" translatable="yes">Remote password:</property>
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="position">5</property>
|
||||
</packing>
|
||||
</child>
|
||||
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="position">0</property>
|
||||
@@ -49,6 +86,7 @@
|
||||
<widget class="GtkVBox" id="input_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="orientation">vertical</property>
|
||||
|
||||
<child>
|
||||
<widget class="GtkEntry" id="input_ip">
|
||||
<property name="visible">True</property>
|
||||
@@ -58,6 +96,7 @@
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
|
||||
<child>
|
||||
<widget class="GtkEntry" id="input_port">
|
||||
<property name="visible">True</property>
|
||||
@@ -67,8 +106,9 @@
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
|
||||
<child>
|
||||
<widget class="GtkCheckButton" id="input_allow_remote">
|
||||
<widget class="GtkCheckButton" id="input_reset_complete">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
</widget>
|
||||
@@ -76,6 +116,38 @@
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
|
||||
<child>
|
||||
<widget class="GtkCheckButton" id="input_allow_remote">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
|
||||
<child>
|
||||
<widget class="GtkEntry" id="input_remote_username">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
|
||||
<child>
|
||||
<widget class="GtkEntry" id="input_remote_password">
|
||||
<property name="visible">True</property>
|
||||
<property name="visibility">False</property>
|
||||
<property name="can_focus">True</property>
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="position">5</property>
|
||||
</packing>
|
||||
</child>
|
||||
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="position">1</property>
|
||||
|
||||
@@ -84,7 +84,11 @@ class GtkUI(GtkPluginBase):
|
||||
"ip": self.glade.get_widget("input_ip").get_text(),
|
||||
"port": int(self.glade.get_widget("input_port").get_text()),
|
||||
"allow_remote": self.glade.get_widget("input_allow_remote").get_active(),
|
||||
"reset_complete": self.glade.get_widget("input_reset_complete").get_active(),
|
||||
"remote_username": self.glade.get_widget("input_remote_username").get_text(),
|
||||
"remote_password": self.glade.get_widget("input_remote_password").get_text(),
|
||||
}
|
||||
|
||||
client.streaming.set_config(config)
|
||||
|
||||
def on_show_prefs(self):
|
||||
@@ -95,6 +99,9 @@ class GtkUI(GtkPluginBase):
|
||||
self.glade.get_widget("input_ip").set_text(config["ip"])
|
||||
self.glade.get_widget("input_port").set_text(str(config["port"]))
|
||||
self.glade.get_widget("input_allow_remote").set_active(config["allow_remote"])
|
||||
self.glade.get_widget("input_reset_complete").set_active(config["reset_complete"])
|
||||
self.glade.get_widget("input_remote_username").set_text(config["remote_username"])
|
||||
self.glade.get_widget("input_remote_password").set_text(config["remote_password"])
|
||||
|
||||
def on_menuitem_stream(self, data=None):
|
||||
torrent_id = component.get("TorrentView").get_selected_torrents()[0]
|
||||
|
||||
@@ -1,16 +1,40 @@
|
||||
from twisted.web.resource import Resource as TwistedResource, _computeAllowedMethods
|
||||
from twisted.web import server
|
||||
from twisted.web import server, error
|
||||
from twisted.internet import defer
|
||||
|
||||
|
||||
class Resource(TwistedResource):
|
||||
content_type = 'application/json'
|
||||
|
||||
|
||||
def render(self, request):
|
||||
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==
|
||||
"""
|
||||
Adds support for deferred render methods
|
||||
"""
|
||||
auth_header = request.getHeader('Authorization')
|
||||
|
||||
if self.username or self.password:
|
||||
authenticated = False
|
||||
if auth_header:
|
||||
auth_header = auth_header.split(' ')
|
||||
if len(auth_header) > 1 and auth_header[0] == 'Basic':
|
||||
userpass = auth_header[1].decode('base64').split(':')
|
||||
if len(userpass) == 2:
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user