initial version

This commit is contained in:
JohnDoee
2015-01-22 19:41:44 +01:00
commit a9854e80ea
11 changed files with 1062 additions and 0 deletions

58
streaming/__init__.py Normal file
View 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
View 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
View 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]))
})

View 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>

View 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
View 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
View 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
View 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
View 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