commit a9854e80ea1fbc45ccaad91d21ba79a7551029f8 Author: JohnDoee Date: Thu Jan 22 19:41:44 2015 +0100 initial version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..27da99e --- /dev/null +++ b/.gitignore @@ -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 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..ecef599 --- /dev/null +++ b/setup.py @@ -0,0 +1,73 @@ +# +# setup.py +# +# Copyright (C) 2015 John Doee +# +# Basic plugin template created by: +# Copyright (C) 2008 Martijn Voncken +# Copyright (C) 2007-2009 Andrew Resch +# Copyright (C) 2009 Damien Churchill +# +# 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) +) diff --git a/streaming/__init__.py b/streaming/__init__.py new file mode 100644 index 0000000..f29035c --- /dev/null +++ b/streaming/__init__.py @@ -0,0 +1,58 @@ +# +# __init__.py +# +# Copyright (C) 2009 John Doee +# +# Basic plugin template created by: +# Copyright (C) 2008 Martijn Voncken +# Copyright (C) 2007-2009 Andrew Resch +# Copyright (C) 2009 Damien Churchill +# +# 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) diff --git a/streaming/common.py b/streaming/common.py new file mode 100644 index 0000000..1aea03b --- /dev/null +++ b/streaming/common.py @@ -0,0 +1,42 @@ +# +# common.py +# +# Copyright (C) 2009 John Doee +# +# Basic plugin template created by: +# Copyright (C) 2008 Martijn Voncken +# Copyright (C) 2007-2009 Andrew Resch +# Copyright (C) 2009 Damien Churchill +# +# 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)) diff --git a/streaming/core.py b/streaming/core.py new file mode 100644 index 0000000..18174f7 --- /dev/null +++ b/streaming/core.py @@ -0,0 +1,364 @@ +# +# core.py +# +# Copyright (C) 2009 John Doee +# +# Basic plugin template created by: +# Copyright (C) 2008 Martijn Voncken +# Copyright (C) 2007-2009 Andrew Resch +# Copyright (C) 2009 Damien Churchill +# +# 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])) + }) \ No newline at end of file diff --git a/streaming/data/config.glade b/streaming/data/config.glade new file mode 100644 index 0000000..f0ec6dd --- /dev/null +++ b/streaming/data/config.glade @@ -0,0 +1,87 @@ + + + + + + + + True + + + True + vertical + + + True + 0 + IP: + + + 0 + + + + + True + 0 + Port: + + + 1 + + + + + True + 0 + Allow remote control: + + + 2 + + + + + 0 + + + + + True + vertical + + + True + True + + + 0 + + + + + True + True + + + 1 + + + + + True + True + + + 2 + + + + + 1 + + + + + + diff --git a/streaming/data/streaming.js b/streaming/data/streaming.js new file mode 100644 index 0000000..2567a56 --- /dev/null +++ b/streaming/data/streaming.js @@ -0,0 +1,50 @@ +/* +Script: streaming.js + The client-side javascript code for the Streaming plugin. + +Copyright: + (C) John Doee 2009 + 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(); diff --git a/streaming/filelike.py b/streaming/filelike.py new file mode 100644 index 0000000..acef851 --- /dev/null +++ b/streaming/filelike.py @@ -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 diff --git a/streaming/gtkui.py b/streaming/gtkui.py new file mode 100644 index 0000000..d5bb3f5 --- /dev/null +++ b/streaming/gtkui.py @@ -0,0 +1,118 @@ +# +# gtkui.py +# +# Copyright (C) 2009 John Doee +# +# Basic plugin template created by: +# Copyright (C) 2008 Martijn Voncken +# Copyright (C) 2007-2009 Andrew Resch +# Copyright (C) 2009 Damien Churchill +# +# 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 \ No newline at end of file diff --git a/streaming/resource.py b/streaming/resource.py new file mode 100644 index 0000000..0d66e06 --- /dev/null +++ b/streaming/resource.py @@ -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 \ No newline at end of file diff --git a/streaming/webui.py b/streaming/webui.py new file mode 100644 index 0000000..4cb20b3 --- /dev/null +++ b/streaming/webui.py @@ -0,0 +1,55 @@ +# +# webui.py +# +# Copyright (C) 2009 John Doee +# +# Basic plugin template created by: +# Copyright (C) 2008 Martijn Voncken +# Copyright (C) 2007-2009 Andrew Resch +# Copyright (C) 2009 Damien Churchill +# +# 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