From 47182a28d6e55e712cdc2dacf3c54ffee768fe9f Mon Sep 17 00:00:00 2001 From: j <0x006A@0x2620.org> Date: Thu, 22 Dec 2011 17:50:47 +0530 Subject: [PATCH] pandoralocal --- README | 19 +++ bin/pandoralocal | 25 ++++ pandoralocal/__init__.py | 22 ++++ pandoralocal/api.py | 12 ++ pandoralocal/backend.py | 15 +++ pandoralocal/server.py | 198 +++++++++++++++++++++++++++++ pandoralocal/static/index.html | 2 + pandoralocal/static/png/icon16.png | Bin 0 -> 3464 bytes pandoralocal/version.py | 1 + setup.py | 38 ++++++ 10 files changed, 332 insertions(+) create mode 100644 README create mode 100755 bin/pandoralocal create mode 100644 pandoralocal/__init__.py create mode 100644 pandoralocal/api.py create mode 100644 pandoralocal/backend.py create mode 100644 pandoralocal/server.py create mode 100644 pandoralocal/static/index.html create mode 100644 pandoralocal/static/png/icon16.png create mode 100644 pandoralocal/version.py create mode 100644 setup.py diff --git a/README b/README new file mode 100644 index 0000000..38791ec --- /dev/null +++ b/README @@ -0,0 +1,19 @@ +pandoralocal runs a webserver on http://localhost:2620 +that provides services that are integrated with a pandora instance + +- there should be a way to start pandoralocal with a user session for osx, win32, linux +- possibly some gui to start/stop and enable/disable session startup + +once running the configuration is done via web interface +there are 2 modes + +a) integration with a pandora instance, this happens via trying to connect to + http://local.pad.ma:2620/api/ and if successfull use the local api in the pandora + site to manage local volumes(upload/sync), cached videos(playback, download for later playback) + +b) offline annotations(something like speedtrans) with ability to sync/upload to pandora later + this uses the same cache for videos but runs on http://127.0.0.1:2620/pad.ma/ + since there might be no dns lookup for local.pad.ma possible + + + diff --git a/bin/pandoralocal b/bin/pandoralocal new file mode 100755 index 0000000..f5e3e26 --- /dev/null +++ b/bin/pandoralocal @@ -0,0 +1,25 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# vi:si:et:sw=4:sts=4:ts=4 +# GPL 2008 + +import os +import sys +from glob import glob +from optparse import OptionParser + +root = os.path.join(os.path.abspath(os.path.dirname(__file__)), '..') +if os.path.exists(os.path.join(root, 'pandoralocal')): + sys.path.insert(0, root) + +import pandoralocal + +if __name__ == '__main__': + parser = OptionParser() + parser.add_option('-c', '--config', dest='config', help='config file', default='config.json') + (opts, args) = parser.parse_args() + + if None in (opts.config, ): + parser.print_help() + sys.exit() + pandoralocal.main(opts.config) diff --git a/pandoralocal/__init__.py b/pandoralocal/__init__.py new file mode 100644 index 0000000..e3e4c63 --- /dev/null +++ b/pandoralocal/__init__.py @@ -0,0 +1,22 @@ +# encoding: utf-8 +# vi:si:et:sw=4:sts=4:ts=4 +import os + +from twisted.web.server import Site +from twisted.internet import reactor + +from backend import Backend +from server import Server + +from version import __version__ + + +def main(config): + base = os.path.abspath(os.path.dirname(__file__)) + backend = Backend(config) + root = Server(base, backend) + site = Site(root) + port = 2620 + interface = '127.0.0.1' + reactor.listenTCP(port, site, interface=interface) + reactor.run() diff --git a/pandoralocal/api.py b/pandoralocal/api.py new file mode 100644 index 0000000..6721a85 --- /dev/null +++ b/pandoralocal/api.py @@ -0,0 +1,12 @@ +# encoding: utf-8 +# vi:si:et:sw=4:sts=4:ts=4 +from server import actions, json_response + + +def echo(backend, site, data): + return json_response(data) +actions.register(echo, cache=False) + +def site(backend, site, data): + return json_response({'site': site}) +actions.register(site, cache=False) diff --git a/pandoralocal/backend.py b/pandoralocal/backend.py new file mode 100644 index 0000000..563f213 --- /dev/null +++ b/pandoralocal/backend.py @@ -0,0 +1,15 @@ +# encoding: utf-8 +# vi:si:et:sw=4:sts=4:ts=4 + +class Backend: + def __init__(self, config): + self.config = config + + def get_file(self, site, itemId, filename): + filename, ext = filename.split('.') + resolution, part = filename.split('p') + print site, itemId, resolution, part, ext + path = '' + if resolution == '480' and ext == 'webm': + path = '/home/j/.ox/media/44/c4/b1/11a888e96a/480p.webm' + return path diff --git a/pandoralocal/server.py b/pandoralocal/server.py new file mode 100644 index 0000000..d26030c --- /dev/null +++ b/pandoralocal/server.py @@ -0,0 +1,198 @@ +# encoding: utf-8 +# vi:si:et:sw=4:sts=4:ts=4 +import sys +import inspect +import os +import json +from urlparse import urlparse + +from twisted.web.resource import Resource +from twisted.web.static import File +from twisted.web.util import Redirect + +from version import __version__ + +def trim(docstring): + if not docstring: + return '' + # Convert tabs to spaces (following the normal Python rules) + # and split into a list of lines: + lines = docstring.expandtabs().splitlines() + # Determine minimum indentation (first line doesn't count): + indent = sys.maxint + for line in lines[1:]: + stripped = line.lstrip() + if stripped: + indent = min(indent, len(line) - len(stripped)) + # Remove indentation (first line is special): + trimmed = [lines[0].strip()] + if indent < sys.maxint: + for line in lines[1:]: + trimmed.append(line[indent:].rstrip()) + # Strip off trailing and leading blank lines: + while trimmed and not trimmed[-1]: + trimmed.pop() + while trimmed and not trimmed[0]: + trimmed.pop(0) + # Return a single string: + return '\n'.join(trimmed) + +def json_response(data=None, status=200, text='ok'): + if not data: + data = {} + return {'status': {'code': status, 'text': text}, 'data': data} + +class ApiActions(dict): + properties = {} + def __init__(self): + + def api(site, data): + ''' + returns list of all known api actions + param data { + docs: bool + } + if docs is true, action properties contain docstrings + return { + status: {'code': int, 'text': string}, + data: { + actions: { + 'api': { + cache: true, + doc: 'recursion' + }, + 'hello': { + cache: true, + .. + } + ... + } + } + } + ''' + docs = data.get('docs', False) + code = data.get('code', False) + _actions = self.keys() + _actions.sort() + actions = {} + for a in _actions: + actions[a] = self.properties[a] + if docs: + actions[a]['doc'] = self.doc(a) + if code: + actions[a]['code'] = self.code(a) + response = json_response({'actions': actions}) + return response + self.register(api) + + def doc(self, f): + return trim(self[f].__doc__) + + def code(self, name): + f = self[name] + if name != 'api' and hasattr(f, 'func_closure') and f.func_closure: + f = f.func_closure[0].cell_contents + info = f.func_code.co_firstlineno + return info, trim(inspect.getsource(f)) + + def register(self, method, action=None, cache=True): + if not action: + action = method.func_name + self[action] = method + self.properties[action] = {'cache': cache} + + def unregister(self, action): + if action in self: + del self[action] + + def render(self, backend, site, action, data): + if action in self: + result = self[action](backend, site, data) + else: + result = json_response(status=404, text='not found') + print result + return json.dumps(result) + +actions = ApiActions() + + +class Server(Resource): + + def __init__(self, base, backend): + self.base = base + self.backend = backend + Resource.__init__(self) + + def static_path(self, path): + return os.path.join(self.base, 'static', path) + + def get_site(self, request): + headers = request.getAllHeaders() + #print headers + if 'origin' in headers: + request.headers['Access-Control-Allow-Origin'] = headers['origin'] + site = headers['origin'] + elif 'referer' in headers: + u = urlparse(headers['referer']) + site = u.scheme + '://' + u.hostname + else: + site = 'http://' + headers['host'] + return site + + def getChild(self, name, request): + print request + if name in ('icon.png', 'favicon.ico'): + f = File(self.static_path('png/icon16.png')) + f.isLeaf = True + return f + if request.path == '/api/': + return self + if request.path.endswith('.webm'): + video = request.path + site = self.get_site(request) + itemId, filename = video[1:].split('/') + path = self.backend.get_file(site, itemId, filename) + if path: + print itemId, filename, 'use', path + request.headers['Access-Control-Allow-Origin'] = '*' + f = File(path) + f.isLeaf = True + return f + else: + url = site + video + #url = 'http://padmo.local/B/240p.webm' + print "redirect", url + return Redirect(url) + path = request.path + path = path[1:] + if not path: + path = 'index.html' + path = self.static_path(path) + f = File(path) + if not os.path.isdir(path): + f.isLeaf = True + return f + + def render_POST(self, request): + print 'render_POST' + request.headers['Server'] = 'pandoralocal/%s' % __version__ + site = self.get_site(request) + print "POST", request.args + if 'action' in request.args: + if 'data' in request.args: + data = json.loads(request.args['data'][0]) + else: + data = {} + action = request.args['action'][0] + return actions.render(self.backend, site, action, data) + + def render_GET(self, request): + print 'render_GET' + request.headers['Server'] = 'pandoralocal/%s' % __version__ + f = open('static/index.html') + data = f.read() + f.close() + request.headers['Content-Type'] = 'text/html' + site = self.get_site(request) + data = data.replace('$name', site) + return data diff --git a/pandoralocal/static/index.html b/pandoralocal/static/index.html new file mode 100644 index 0000000..845867c --- /dev/null +++ b/pandoralocal/static/index.html @@ -0,0 +1,2 @@ +only robots may pass this point +$name diff --git a/pandoralocal/static/png/icon16.png b/pandoralocal/static/png/icon16.png new file mode 100644 index 0000000000000000000000000000000000000000..23e2830ad0919be25de8656bda539a78227df138 GIT binary patch literal 3464 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