mirror of
https://github.com/JohnDoee/deluge-streaming/
synced 2026-07-01 07:31:17 -07:00
initial version
This commit is contained in:
58
streaming/__init__.py
Normal file
58
streaming/__init__.py
Normal file
@@ -0,0 +1,58 @@
|
||||
#
|
||||
# __init__.py
|
||||
#
|
||||
# Copyright (C) 2009 John Doee <johndoee@tidalstream.org>
|
||||
#
|
||||
# Basic plugin template created by:
|
||||
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
||||
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
|
||||
# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
|
||||
#
|
||||
# Deluge is free software.
|
||||
#
|
||||
# You may redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License, as published by the Free Software
|
||||
# Foundation; either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# deluge is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with deluge. If not, write to:
|
||||
# The Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor
|
||||
# Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# In addition, as a special exception, the copyright holders give
|
||||
# permission to link the code of portions of this program with the OpenSSL
|
||||
# library.
|
||||
# You must obey the GNU General Public License in all respects for all of
|
||||
# the code used other than OpenSSL. If you modify file(s) with this
|
||||
# exception, you may extend this exception to your version of the file(s),
|
||||
# but you are not obligated to do so. If you do not wish to do so, delete
|
||||
# this exception statement from your version. If you delete this exception
|
||||
# statement from all source files in the program, then also delete it here.
|
||||
#
|
||||
|
||||
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
|
||||
self._plugin_cls = _plugin_cls
|
||||
super(WebUIPlugin, self).__init__(plugin_name)
|
||||
42
streaming/common.py
Normal file
42
streaming/common.py
Normal file
@@ -0,0 +1,42 @@
|
||||
#
|
||||
# common.py
|
||||
#
|
||||
# Copyright (C) 2009 John Doee <johndoee@tidalstream.org>
|
||||
#
|
||||
# Basic plugin template created by:
|
||||
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
||||
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
|
||||
# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
|
||||
#
|
||||
# Deluge is free software.
|
||||
#
|
||||
# You may redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License, as published by the Free Software
|
||||
# Foundation; either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# deluge is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with deluge. If not, write to:
|
||||
# The Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor
|
||||
# Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# In addition, as a special exception, the copyright holders give
|
||||
# permission to link the code of portions of this program with the OpenSSL
|
||||
# library.
|
||||
# You must obey the GNU General Public License in all respects for all of
|
||||
# the code used other than OpenSSL. If you modify file(s) with this
|
||||
# exception, you may extend this exception to your version of the file(s),
|
||||
# but you are not obligated to do so. If you do not wish to do so, delete
|
||||
# this exception statement from your version. If you delete this exception
|
||||
# statement from all source files in the program, then also delete it here.
|
||||
#
|
||||
|
||||
def get_resource(filename):
|
||||
import pkg_resources, os
|
||||
return pkg_resources.resource_filename("streaming", os.path.join("data", filename))
|
||||
364
streaming/core.py
Normal file
364
streaming/core.py
Normal file
@@ -0,0 +1,364 @@
|
||||
#
|
||||
# core.py
|
||||
#
|
||||
# Copyright (C) 2009 John Doee <johndoee@tidalstream.org>
|
||||
#
|
||||
# Basic plugin template created by:
|
||||
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
||||
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
|
||||
# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
|
||||
#
|
||||
# Deluge is free software.
|
||||
#
|
||||
# You may redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License, as published by the Free Software
|
||||
# Foundation; either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# deluge is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with deluge. If not, write to:
|
||||
# The Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor
|
||||
# Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# In addition, as a special exception, the copyright holders give
|
||||
# permission to link the code of portions of this program with the OpenSSL
|
||||
# library.
|
||||
# You must obey the GNU General Public License in all respects for all of
|
||||
# the code used other than OpenSSL. If you modify file(s) with this
|
||||
# exception, you may extend this exception to your version of the file(s),
|
||||
# but you are not obligated to do so. If you do not wish to do so, delete
|
||||
# this exception statement from your version. If you delete this exception
|
||||
# statement from all source files in the program, then also delete it here.
|
||||
#
|
||||
|
||||
import base64
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import urllib
|
||||
|
||||
from twisted.internet import reactor, defer, task
|
||||
from twisted.python import randbytes
|
||||
from twisted.web import server, resource, static, http
|
||||
from twisted.web.static import StaticProducer
|
||||
|
||||
import deluge.component as component
|
||||
import deluge.configmanager
|
||||
from deluge.core.rpcserver import export
|
||||
from deluge.log import LOG as log
|
||||
from deluge.plugins.pluginbase import CorePluginBase
|
||||
|
||||
from .resource import Resource
|
||||
|
||||
DEFAULT_PREFS = {
|
||||
'ip': '127.0.0.1',
|
||||
'port': 46123,
|
||||
'allow_remote': False,
|
||||
}
|
||||
|
||||
from .filelike import FilelikeObjectResource
|
||||
|
||||
MAX_QUEUE_CHUNKS = 12
|
||||
|
||||
class FileServeResource(resource.Resource):
|
||||
isLeaf = True
|
||||
|
||||
def __init__(self):
|
||||
self.file_mapping = {}
|
||||
resource.Resource.__init__(self)
|
||||
|
||||
def generate_secure_token(self):
|
||||
return base64.urlsafe_b64encode(randbytes.RandomFactory().secureRandom(21, True))
|
||||
|
||||
def add_file(self, path):
|
||||
token = self.generate_secure_token()
|
||||
self.file_mapping[token] = path
|
||||
|
||||
return token
|
||||
|
||||
def render_GET(self, request):
|
||||
key = request.path.split('/')[2]
|
||||
if key not in self.file_mapping:
|
||||
return resource.NoResource().render()
|
||||
|
||||
tf = self.file_mapping[key].copy()
|
||||
tf.open()
|
||||
return FilelikeObjectResource(tf, tf.size).render_GET(request)
|
||||
|
||||
class AddTorrentResource(Resource):
|
||||
isLeaf = True
|
||||
|
||||
def __init__(self, client):
|
||||
self.client = client
|
||||
Resource.__init__(self)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def render_POST(self, request):
|
||||
torrent_data = request.args.get('torrent_data', None)
|
||||
if not torrent_data:
|
||||
defer.returnValue(json.dumps({'status': 'error', 'message': 'missing torrent_data in request'}))
|
||||
|
||||
torrent_data = torrent_data[0].encode('base64')
|
||||
|
||||
torrent_id = yield self.client.add_torrent(torrent_data)
|
||||
|
||||
if torrent_id is None:
|
||||
defer.returnValue(json.dumps({'status': 'error', 'message': 'failed to add torrent'}))
|
||||
|
||||
defer.returnValue(json.dumps({'status': 'success', 'infohash': torrent_id, 'message': 'torrent added successfully'}))
|
||||
|
||||
class StreamResource(Resource):
|
||||
isLeaf = True
|
||||
|
||||
def __init__(self, client):
|
||||
self.client = client
|
||||
Resource.__init__(self)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def render_GET(self, request):
|
||||
infohash = request.args.get('infohash', None)
|
||||
path = request.args.get('path', None)
|
||||
|
||||
if infohash is None:
|
||||
defer.returnValue(json.dumps({'status': 'error', 'message': 'missing infohash'}))
|
||||
|
||||
if path is None:
|
||||
defer.returnValue(json.dumps({'status': 'error', 'message': 'missing path'}))
|
||||
|
||||
result = yield self.client.stream_torrent(infohash[0], path[0])
|
||||
defer.returnValue(json.dumps(result))
|
||||
|
||||
class TorrentFile(object):
|
||||
def __init__(self, torrent, file_path, size, chunk_size, offset):
|
||||
self.torrent = torrent
|
||||
self.torrent_handle = torrent.handle
|
||||
self.file_path = file_path
|
||||
self.first_chunk = offset / chunk_size
|
||||
self.last_chunk = (offset + size) / chunk_size
|
||||
self.chunk_size = chunk_size
|
||||
self.offset = offset
|
||||
self.size = size
|
||||
self.last_requested_chunk = self.first_chunk
|
||||
self.is_closed = False
|
||||
|
||||
self.priorities_increased = {}
|
||||
|
||||
self.first_chunk_end = self.chunk_size * (self.first_chunk + 1) - offset
|
||||
|
||||
def open(self):
|
||||
self.update_chunk_priority()
|
||||
self.file_handler = open(self.file_path, 'rb')
|
||||
|
||||
def get_chunk(self, tell):
|
||||
i = (tell + 1) - self.first_chunk_end
|
||||
if i <= 0:
|
||||
offset = 0
|
||||
else:
|
||||
offset = (i / self.chunk_size) + 1
|
||||
|
||||
return self.first_chunk + offset, self.first_chunk_end + (offset * self.chunk_size)
|
||||
|
||||
def wait_chunk_complete(self, chunk):
|
||||
d = defer.Deferred()
|
||||
|
||||
def check_if_done():
|
||||
if self.torrent.status.pieces[chunk]:
|
||||
return d.callback(True)
|
||||
|
||||
self.set_prio(chunk, 7)
|
||||
|
||||
if self.is_closed:
|
||||
return d.errback(None)
|
||||
|
||||
reactor.callLater(1.0, check_if_done)
|
||||
|
||||
check_if_done()
|
||||
|
||||
return d
|
||||
|
||||
def set_prio(self, chunk, prio):
|
||||
if self.priorities_increased.get(chunk, 0) < prio:
|
||||
self.torrent_handle.piece_priority(chunk, prio)
|
||||
self.priorities_increased[chunk] = prio
|
||||
|
||||
if prio == 7:
|
||||
self.torrent_handle.set_piece_deadline(chunk, 100)
|
||||
|
||||
def prepare_torrent(self, buffer_pieces):
|
||||
self.set_prio(self.first_chunk, 7)
|
||||
self.set_prio(self.last_chunk, 7)
|
||||
|
||||
for chunk, chunk_status in enumerate(self.torrent.status.pieces[self.first_chunk:self.first_chunk+buffer_pieces+1], self.first_chunk):
|
||||
self.set_prio(chunk, 7)
|
||||
|
||||
self.update_chunk_priority()
|
||||
|
||||
def is_buffered(self, expected_pieces):
|
||||
if [x for x in self.torrent.status.pieces[self.first_chunk:self.first_chunk+expected_pieces+1] if not x]:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def update_chunk_priority(self): # no need to do this when the file is complete
|
||||
if self.is_closed:
|
||||
return
|
||||
|
||||
if self.last_requested_chunk is not None:
|
||||
offset = self.last_requested_chunk + 1
|
||||
|
||||
status_increase_count = 0
|
||||
for chunk, chunk_status in enumerate(self.torrent.status.pieces[offset:self.last_chunk+1], offset):
|
||||
if not chunk_status:
|
||||
self.set_prio(chunk, 7)
|
||||
status_increase_count += 1
|
||||
|
||||
if status_increase_count > MAX_QUEUE_CHUNKS:
|
||||
break
|
||||
|
||||
reactor.callLater(4, self.update_chunk_priority)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def read(self, size=1024):
|
||||
tell = self.tell()
|
||||
chunk, end_of_chunk = self.get_chunk(tell)
|
||||
self.last_requested_chunk = chunk
|
||||
print 'waiting for chunk', chunk, size, tell
|
||||
yield self.wait_chunk_complete(chunk)
|
||||
print 'done waiting', chunk, size, tell
|
||||
defer.returnValue(self.file_handler.read(min(end_of_chunk-tell, size)))
|
||||
|
||||
def seek(self, offset, whence=os.SEEK_SET):
|
||||
return self.file_handler.seek(offset, whence)
|
||||
|
||||
def tell(self):
|
||||
return self.file_handler.tell()
|
||||
|
||||
def close(self):
|
||||
self.is_closed = True
|
||||
return self.file_handler.close()
|
||||
|
||||
def copy(self):
|
||||
tf = TorrentFile(self.torrent, self.file_path, self.size, self.chunk_size, self.offset)
|
||||
tf.priorities_increased = self.priorities_increased
|
||||
|
||||
return tf
|
||||
|
||||
def sleep(seconds):
|
||||
d = defer.Deferred()
|
||||
reactor.callLater(seconds, d.callback, seconds)
|
||||
return d
|
||||
|
||||
class Core(CorePluginBase):
|
||||
def enable(self):
|
||||
self.config = deluge.configmanager.ConfigManager("streaming.conf", DEFAULT_PREFS)
|
||||
self.fsr = FileServeResource()
|
||||
|
||||
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.site = server.Site(self.resource)
|
||||
self.listening = reactor.listenTCP(self.config.config['port'], self.site, interface=self.config.config['ip'])
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def disable(self):
|
||||
self.site.stopFactory()
|
||||
yield self.listening.stopListening()
|
||||
|
||||
def update(self):
|
||||
pass
|
||||
|
||||
@export
|
||||
@defer.inlineCallbacks
|
||||
def set_config(self, config):
|
||||
"""Sets the config dictionary"""
|
||||
do_reload = False
|
||||
for key in config.keys():
|
||||
self.config[key] = config[key]
|
||||
self.config.save()
|
||||
|
||||
yield self.disable()
|
||||
self.enable()
|
||||
|
||||
@export
|
||||
def get_config(self):
|
||||
"""Returns the config dictionary"""
|
||||
return self.config.config
|
||||
|
||||
@export
|
||||
@defer.inlineCallbacks
|
||||
def add_torrent(self, torrent_data):
|
||||
core = component.get("Core")
|
||||
tid = yield core.add_torrent_file('file.torrent', torrent_data, {'add_paused': True})
|
||||
|
||||
tor = component.get("TorrentManager").torrents.get(tid, None)
|
||||
|
||||
state = tor.get_status(['files'])
|
||||
tor.set_file_priorities([0] * len(state['files']))
|
||||
|
||||
defer.returnValue(tid)
|
||||
|
||||
@export
|
||||
@defer.inlineCallbacks
|
||||
def stream_torrent(self, tid, filepath):
|
||||
tor = component.get("TorrentManager").torrents.get(tid, None)
|
||||
|
||||
if tor is None: # torrent isn't downloaded yet
|
||||
defer.returnValue({'status': 'error', 'message': 'torrent_not_found'})
|
||||
|
||||
status = tor.get_status(['piece_length', 'files', 'file_priorities', 'file_progress', 'state', 'save_path'])
|
||||
pieces = tor.status.pieces
|
||||
piece_length = status['piece_length']
|
||||
files = status['files']
|
||||
|
||||
for f, priority, progress in zip(files, status['file_priorities'], status['file_progress']):
|
||||
f['first_piece'] = f['offset'] / piece_length
|
||||
f['last_piece'] = (f['offset'] + f['size']) / piece_length
|
||||
f['pieces'] = pieces[f['first_piece']:f['last_piece']+1]
|
||||
f['priority'] = priority
|
||||
f['progress'] = progress
|
||||
|
||||
f = [f for f in files if f['path'] == filepath]
|
||||
|
||||
if not f: # file not found in torrent
|
||||
defer.returnValue({'status': 'error', 'message': 'file_not_found'})
|
||||
f = f[0]
|
||||
|
||||
priorities = [0] * len(tor.get_status(['files'])['files'])
|
||||
priorities[f['index']] = 1
|
||||
tor.set_file_priorities(priorities)
|
||||
|
||||
tor.resume()
|
||||
|
||||
EXPECTED_PERCENT = 5.0
|
||||
EXPECTED_SIZE = 5*1024*1024
|
||||
|
||||
percent_pieces = int(math.ceil((len(f['pieces']) / 100.0) * EXPECTED_PERCENT))
|
||||
size_pieces = int(min(math.ceil((EXPECTED_SIZE * 1.0) / piece_length), f['pieces']))
|
||||
expected_pieces = max(percent_pieces, size_pieces)
|
||||
|
||||
fp = os.path.join(status['save_path'], f['path'])
|
||||
|
||||
tf = TorrentFile(tor, fp, f['size'], status['piece_length'], f['offset'])
|
||||
tf.prepare_torrent(expected_pieces)
|
||||
|
||||
for _ in range(300):
|
||||
if os.path.isfile(fp) and tf.is_buffered(expected_pieces):
|
||||
break
|
||||
|
||||
yield sleep(1)
|
||||
|
||||
defer.returnValue({
|
||||
'status': 'success',
|
||||
'url': 'http://%s:%s/file/%s/%s' % (self.config.config['ip'], self.config.config['port'],
|
||||
self.fsr.add_file(tf),
|
||||
urllib.quote(f['path'].split('/')[-1]))
|
||||
})
|
||||
87
streaming/data/config.glade
Normal file
87
streaming/data/config.glade
Normal file
@@ -0,0 +1,87 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd">
|
||||
<!--Generated with glade3 3.4.5 on Fri Aug 8 23:34:44 2008 -->
|
||||
<glade-interface>
|
||||
<widget class="GtkWindow" id="window1">
|
||||
<child>
|
||||
<widget class="GtkHBox" id="prefs_box">
|
||||
<property name="visible">True</property>
|
||||
<child>
|
||||
<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>
|
||||
<property name="xalign">0</property>
|
||||
<property name="label" translatable="yes">IP:</property>
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<widget class="GtkLabel" id="label_port">
|
||||
<property name="visible">True</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="label" translatable="yes">Port:</property>
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<widget class="GtkLabel" id="label_allow_remote">
|
||||
<property name="visible">True</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="label" translatable="yes">Allow remote control:</property>
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<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>
|
||||
<property name="can_focus">True</property>
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<widget class="GtkEntry" id="input_port">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="position">1</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">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</widget>
|
||||
</child>
|
||||
</widget>
|
||||
</glade-interface>
|
||||
50
streaming/data/streaming.js
Normal file
50
streaming/data/streaming.js
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
Script: streaming.js
|
||||
The client-side javascript code for the Streaming plugin.
|
||||
|
||||
Copyright:
|
||||
(C) John Doee 2009 <johndoee@tidalstream.org>
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 3, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, write to:
|
||||
The Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor
|
||||
Boston, MA 02110-1301, USA.
|
||||
|
||||
In addition, as a special exception, the copyright holders give
|
||||
permission to link the code of portions of this program with the OpenSSL
|
||||
library.
|
||||
You must obey the GNU General Public License in all respects for all of
|
||||
the code used other than OpenSSL. If you modify file(s) with this
|
||||
exception, you may extend this exception to your version of the file(s),
|
||||
but you are not obligated to do so. If you do not wish to do so, delete
|
||||
this exception statement from your version. If you delete this exception
|
||||
statement from all source files in the program, then also delete it here.
|
||||
*/
|
||||
|
||||
StreamingPlugin = Ext.extend(Deluge.Plugin, {
|
||||
constructor: function(config) {
|
||||
config = Ext.apply({
|
||||
name: "Streaming"
|
||||
}, config);
|
||||
StreamingPlugin.superclass.constructor.call(this, config);
|
||||
},
|
||||
|
||||
onDisable: function() {
|
||||
|
||||
},
|
||||
|
||||
onEnable: function() {
|
||||
|
||||
}
|
||||
});
|
||||
new StreamingPlugin();
|
||||
149
streaming/filelike.py
Normal file
149
streaming/filelike.py
Normal file
@@ -0,0 +1,149 @@
|
||||
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 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)
|
||||
else:
|
||||
self.request.unregisterProducer()
|
||||
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))
|
||||
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
|
||||
while dataLength < self.bufferSize:
|
||||
if self.partBoundary:
|
||||
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))
|
||||
self._partBytesWritten += len(p)
|
||||
dataLength += len(p)
|
||||
data.append(p)
|
||||
if self.request and self._partBytesWritten == self._partSize:
|
||||
try:
|
||||
self._nextRange()
|
||||
except StopIteration:
|
||||
done = True
|
||||
break
|
||||
self.request.write(''.join(data))
|
||||
if done:
|
||||
self.request.unregisterProducer()
|
||||
self.request.finish()
|
||||
self.request = None
|
||||
|
||||
class FilelikeObjectResource(static.File):
|
||||
isLeaf = True
|
||||
contentType = None
|
||||
fileObject = None
|
||||
encoding = 'bytes'
|
||||
|
||||
def __init__(self, fileObject, size, contentType='bytes'):
|
||||
self.contentType = contentType
|
||||
self.fileObject = fileObject
|
||||
self.fileSize = size
|
||||
resource.Resource.__init__(self)
|
||||
|
||||
def _setContentHeaders(self, request, size=None):
|
||||
if size is None:
|
||||
size = self.getFileSize()
|
||||
|
||||
if size:
|
||||
request.setHeader('content-length', str(size))
|
||||
if self.contentType:
|
||||
request.setHeader('content-type', self.contentType)
|
||||
if self.encoding:
|
||||
request.setHeader('content-encoding', self.encoding)
|
||||
|
||||
def makeProducer(self, request, fileForReading):
|
||||
"""
|
||||
Make a L{StaticProducer} that will produce the body of this response.
|
||||
|
||||
This method will also set the response code and Content-* headers.
|
||||
|
||||
@param request: The L{Request} object.
|
||||
@param fileForReading: The file object containing the resource.
|
||||
@return: A L{StaticProducer}. Calling C{.start()} on this will begin
|
||||
producing the response.
|
||||
"""
|
||||
byteRange = request.getHeader('range')
|
||||
if byteRange is None or not self.getFileSize():
|
||||
self._setContentHeaders(request)
|
||||
request.setResponseCode(http.OK)
|
||||
return NoRangeStaticProducer(request, fileForReading)
|
||||
try:
|
||||
parsedRanges = self._parseRangeHeader(byteRange)
|
||||
except ValueError:
|
||||
log.msg("Ignoring malformed Range header %r" % (byteRange,))
|
||||
self._setContentHeaders(request)
|
||||
request.setResponseCode(http.OK)
|
||||
return NoRangeStaticProducer(request, fileForReading)
|
||||
|
||||
if len(parsedRanges) == 1:
|
||||
offset, size = self._doSingleRangeRequest(
|
||||
request, parsedRanges[0])
|
||||
self._setContentHeaders(request, size)
|
||||
return SingleRangeStaticProducer(
|
||||
request, fileForReading, offset, size)
|
||||
else:
|
||||
rangeInfo = self._doMultipleRangeRequest(request, parsedRanges)
|
||||
return MultipleRangeStaticProducer(
|
||||
request, fileForReading, rangeInfo)
|
||||
|
||||
def getFileSize(self):
|
||||
return self.fileSize
|
||||
|
||||
def render_GET(self, request):
|
||||
"""
|
||||
Begin sending the contents of this L{File} (or a subset of the
|
||||
contents, based on the 'range' header) to the given request.
|
||||
"""
|
||||
request.setHeader('accept-ranges', 'bytes')
|
||||
|
||||
producer = self.makeProducer(request, self.fileObject)
|
||||
|
||||
if request.method == 'HEAD':
|
||||
return ''
|
||||
|
||||
producer.start()
|
||||
# and make sure the connection doesn't get closed
|
||||
return server.NOT_DONE_YET
|
||||
render_HEAD = render_GET
|
||||
118
streaming/gtkui.py
Normal file
118
streaming/gtkui.py
Normal file
@@ -0,0 +1,118 @@
|
||||
#
|
||||
# gtkui.py
|
||||
#
|
||||
# Copyright (C) 2009 John Doee <johndoee@tidalstream.org>
|
||||
#
|
||||
# Basic plugin template created by:
|
||||
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
||||
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
|
||||
# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
|
||||
#
|
||||
# Deluge is free software.
|
||||
#
|
||||
# You may redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License, as published by the Free Software
|
||||
# Foundation; either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# deluge is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with deluge. If not, write to:
|
||||
# The Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor
|
||||
# Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# In addition, as a special exception, the copyright holders give
|
||||
# permission to link the code of portions of this program with the OpenSSL
|
||||
# library.
|
||||
# You must obey the GNU General Public License in all respects for all of
|
||||
# the code used other than OpenSSL. If you modify file(s) with this
|
||||
# exception, you may extend this exception to your version of the file(s),
|
||||
# but you are not obligated to do so. If you do not wish to do so, delete
|
||||
# this exception statement from your version. If you delete this exception
|
||||
# statement from all source files in the program, then also delete it here.
|
||||
#
|
||||
|
||||
import gtk
|
||||
|
||||
from deluge.log import LOG as log
|
||||
from deluge.ui.client import client
|
||||
from deluge.ui.gtkui import dialogs
|
||||
from deluge.plugins.pluginbase import GtkPluginBase
|
||||
import deluge.component as component
|
||||
import deluge.common
|
||||
|
||||
from common import get_resource
|
||||
|
||||
class GtkUI(GtkPluginBase):
|
||||
def enable(self):
|
||||
self.glade = gtk.glade.XML(get_resource("config.glade"))
|
||||
|
||||
component.get("Preferences").add_page("Streaming", self.glade.get_widget("prefs_box"))
|
||||
component.get("PluginManager").register_hook("on_apply_prefs", self.on_apply_prefs)
|
||||
component.get("PluginManager").register_hook("on_show_prefs", self.on_show_prefs)
|
||||
|
||||
file_menu = component.get("MainWindow").main_glade.get_widget('menu_file_tab')
|
||||
|
||||
self.sep = gtk.SeparatorMenuItem()
|
||||
self.item = gtk.MenuItem(_("_Stream this file"))
|
||||
self.item.connect("activate", self.on_menuitem_stream)
|
||||
|
||||
file_menu.append(self.sep)
|
||||
file_menu.append(self.item)
|
||||
|
||||
self.sep.show()
|
||||
self.item.show()
|
||||
|
||||
def disable(self):
|
||||
component.get("Preferences").remove_page("Streaming")
|
||||
component.get("PluginManager").deregister_hook("on_apply_prefs", self.on_apply_prefs)
|
||||
component.get("PluginManager").deregister_hook("on_show_prefs", self.on_show_prefs)
|
||||
|
||||
file_menu = component.get("MainWindow").main_glade.get_widget('menu_file_tab')
|
||||
|
||||
file_menu.remove(self.item)
|
||||
file_menu.remove(self.sep)
|
||||
|
||||
def on_apply_prefs(self):
|
||||
log.debug("applying prefs for Streaming")
|
||||
config = {
|
||||
"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(),
|
||||
}
|
||||
client.streaming.set_config(config)
|
||||
|
||||
def on_show_prefs(self):
|
||||
client.streaming.get_config().addCallback(self.cb_get_config)
|
||||
|
||||
def cb_get_config(self, config):
|
||||
"callback for on show_prefs"
|
||||
self.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"])
|
||||
|
||||
def on_menuitem_stream(self, data=None):
|
||||
torrent_id = component.get("TorrentView").get_selected_torrents()[0]
|
||||
|
||||
ft = component.get("TorrentDetails").tabs['Files']
|
||||
paths = ft.listview.get_selection().get_selected_rows()[1]
|
||||
|
||||
selected = []
|
||||
for path in paths:
|
||||
selected.append(ft.treestore.get_iter(path))
|
||||
|
||||
def stream_ready(result):
|
||||
if result['status'] == 'success':
|
||||
dialogs.ErrorDialog('Stream ready', 'Copy the link into a media player', details=result['url']).run()
|
||||
else:
|
||||
dialogs.ErrorDialog('Stream failed', 'Was unable to prepare the stream', details=result).run()
|
||||
|
||||
for select in selected:
|
||||
path = ft.get_file_path(select)
|
||||
client.streaming.stream_torrent(torrent_id, path).addCallback(stream_ready)
|
||||
break
|
||||
36
streaming/resource.py
Normal file
36
streaming/resource.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from twisted.web.resource import Resource as TwistedResource, _computeAllowedMethods
|
||||
from twisted.web import server, guard
|
||||
from twisted.internet import defer
|
||||
|
||||
|
||||
class Resource(TwistedResource):
|
||||
content_type = 'application/json'
|
||||
|
||||
|
||||
def render(self, request):
|
||||
"""
|
||||
Adds support for deferred render methods
|
||||
"""
|
||||
m = getattr(self, 'render_' + request.method, None)
|
||||
if not m:
|
||||
# This needs to be here until the deprecated subclasses of the
|
||||
# below three error resources in twisted.web.error are removed.
|
||||
from twisted.web.error import UnsupportedMethod
|
||||
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
|
||||
55
streaming/webui.py
Normal file
55
streaming/webui.py
Normal file
@@ -0,0 +1,55 @@
|
||||
#
|
||||
# webui.py
|
||||
#
|
||||
# Copyright (C) 2009 John Doee <johndoee@tidalstream.org>
|
||||
#
|
||||
# Basic plugin template created by:
|
||||
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
||||
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
|
||||
# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
|
||||
#
|
||||
# Deluge is free software.
|
||||
#
|
||||
# You may redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License, as published by the Free Software
|
||||
# Foundation; either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# deluge is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with deluge. If not, write to:
|
||||
# The Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor
|
||||
# Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# In addition, as a special exception, the copyright holders give
|
||||
# permission to link the code of portions of this program with the OpenSSL
|
||||
# library.
|
||||
# You must obey the GNU General Public License in all respects for all of
|
||||
# the code used other than OpenSSL. If you modify file(s) with this
|
||||
# exception, you may extend this exception to your version of the file(s),
|
||||
# but you are not obligated to do so. If you do not wish to do so, delete
|
||||
# this exception statement from your version. If you delete this exception
|
||||
# statement from all source files in the program, then also delete it here.
|
||||
#
|
||||
|
||||
from deluge.log import LOG as log
|
||||
from deluge.ui.client import client
|
||||
from deluge import component
|
||||
from deluge.plugins.pluginbase import WebPluginBase
|
||||
|
||||
from common import get_resource
|
||||
|
||||
class WebUI(WebPluginBase):
|
||||
|
||||
scripts = [get_resource("streaming.js")]
|
||||
|
||||
def enable(self):
|
||||
pass
|
||||
|
||||
def disable(self):
|
||||
pass
|
||||
Reference in New Issue
Block a user