mirror of
https://github.com/JohnDoee/deluge-streaming/
synced 2026-07-01 07:31:17 -07:00
initial version
This commit is contained in:
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
*.mo
|
||||||
|
*.egg-info
|
||||||
|
*.egg
|
||||||
|
*.EGG
|
||||||
|
*.EGG-INFO
|
||||||
|
bin
|
||||||
|
build
|
||||||
|
develop-eggs
|
||||||
|
downloads
|
||||||
|
eggs
|
||||||
|
fake-eggs
|
||||||
|
parts
|
||||||
|
dist
|
||||||
|
.installed.cfg
|
||||||
|
.mr.developer.cfg
|
||||||
|
.hg
|
||||||
|
.bzr
|
||||||
|
.svn
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.tmp*
|
||||||
|
dropin.cache
|
||||||
|
_trial_temp
|
||||||
|
*.komodoproject
|
||||||
|
docs/_build*
|
||||||
|
apiserver/metadata/imdbhandler.py
|
||||||
|
apiserver/metadata/malhandler.py
|
||||||
|
apiserver/services/search.py
|
||||||
|
apiserver/services/control.py
|
||||||
|
apiserver/services/files.py
|
||||||
73
setup.py
Normal file
73
setup.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
#
|
||||||
|
# setup.py
|
||||||
|
#
|
||||||
|
# Copyright (C) 2015 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 setuptools import setup
|
||||||
|
|
||||||
|
__plugin_name__ = "Streaming"
|
||||||
|
__author__ = "John Doee"
|
||||||
|
__author_email__ = "johndoee@tidalstream.org"
|
||||||
|
__version__ = "0.1"
|
||||||
|
__url__ = "https://github.com/JohnDoee/deluge-streaming"
|
||||||
|
__license__ = "GPLv3"
|
||||||
|
__description__ = ""
|
||||||
|
__long_description__ = """"""
|
||||||
|
__pkg_data__ = {__plugin_name__.lower(): ["template/*", "data/*"]}
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name=__plugin_name__,
|
||||||
|
version=__version__,
|
||||||
|
description=__description__,
|
||||||
|
author=__author__,
|
||||||
|
author_email=__author_email__,
|
||||||
|
url=__url__,
|
||||||
|
license=__license__,
|
||||||
|
long_description=__long_description__ if __long_description__ else __description__,
|
||||||
|
|
||||||
|
packages=[__plugin_name__.lower()],
|
||||||
|
package_data = __pkg_data__,
|
||||||
|
|
||||||
|
entry_points="""
|
||||||
|
[deluge.plugin.core]
|
||||||
|
%s = %s:CorePlugin
|
||||||
|
[deluge.plugin.gtkui]
|
||||||
|
%s = %s:GtkUIPlugin
|
||||||
|
[deluge.plugin.web]
|
||||||
|
%s = %s:WebUIPlugin
|
||||||
|
""" % ((__plugin_name__, __plugin_name__.lower())*3)
|
||||||
|
)
|
||||||
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