commit 85dc70aae086462ed7efa28d10cd0d4a8f37fdef Author: j <0x006A@0x2620.org> Date: Thu Sep 6 12:08:30 2012 +0200 0xcd diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..2a1b841 --- /dev/null +++ b/README.txt @@ -0,0 +1,3 @@ +on os x run: ./bin/0xcd +run ./bin/0xcd -h for a list of options + diff --git a/bin/0xcd b/bin/0xcd new file mode 100755 index 0000000..eed319b --- /dev/null +++ b/bin/0xcd @@ -0,0 +1,24 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# vi:si:et:sw=4:sts=4:ts=4 + +import os +import sys +from optparse import OptionParser + +root = os.path.join(os.path.abspath(os.path.dirname(__file__)), '..') +if os.path.exists(os.path.join(root, 'oxcd')): + sys.path.insert(0, root) + +import oxcd + +if __name__ == '__main__': + parser = OptionParser() + parser.add_option('-p', '--port', dest='port', help='port', default=2681) + parser.add_option('-i', '--itunes', dest='itunes', help='iTunes xml', default=oxcd.itunes_path()) + (opts, args) = parser.parse_args() + + if None in (opts.port, opts.itunes): + parser.print_help() + sys.exit() + oxcd.main(opts.port, opts.itunes) diff --git a/oxcd/__init__.py b/oxcd/__init__.py new file mode 100644 index 0000000..d8af74d --- /dev/null +++ b/oxcd/__init__.py @@ -0,0 +1,35 @@ +# encoding: utf-8 +# vi:si:et:sw=4:sts=4:ts=4 +import os +import sys +import webbrowser + +from twisted.internet import glib2reactor + +from twisted.web.server import Site +from twisted.internet import reactor + +from itunes import iTunes +from server import Server +import api + +from version import __version__ + +def itunes_path(): + if sys.platform == 'darwin': + path = os.path.expanduser('~/Music/iTunes/iTunes Library.xml') + elif sys.platform == 'win32': + path = os.path.expanduser('~\\Music\\iTunes\\iTunes Library.xml') + else: + path = None + return path + +def main(port, itunes): + base = os.path.abspath(os.path.dirname(__file__)) + backend = iTunes(itunes) + root = Server(base, backend) + site = Site(root) + reactor.listenTCP(port, site) + reactor.callLater(1, lambda: webbrowser.open_new_tab('http://127.0.0.1:%s/' % port)) + print 'opening browser at http://127.0.0.1:%s/ ...' % port + reactor.run() diff --git a/oxcd/api.py b/oxcd/api.py new file mode 100644 index 0000000..dcb172c --- /dev/null +++ b/oxcd/api.py @@ -0,0 +1,15 @@ +# encoding: utf-8 +# vi:si:et:sw=4:sts=4:ts=4 +import os + +from server import actions, json_response + +def init(backend, site, data): + response = {} + return json_response(response) +actions.register(init, cache=False) + +def library(backend, site, data): + response = backend.library + return json_response(response) +actions.register(library, cache=True) diff --git a/oxcd/itunes.py b/oxcd/itunes.py new file mode 100644 index 0000000..538e9da --- /dev/null +++ b/oxcd/itunes.py @@ -0,0 +1,23 @@ +# encoding: utf-8 +# vi:si:et:sw=4:sts=4:ts=4 +from __future__ import with_statement, division + +import os +from urllib import unquote + +from plistlib import readPlist + +class iTunes(object): + def __init__(self, xml): + self.library = readPlist(xml) + for id in self.library['Tracks']: + self.library['Tracks'][id]['Location'] = unquote(self.library['Tracks'][id]['Location'].replace('file://localhost/', '/')) + if self.library['Tracks'][id]['Location'].startswith('//'): + self.library['Tracks'][id]['Location'] = self.library['Tracks'][id]['Location'][1:] + + self.library['Music Folder'] = unquote(self.library['Music Folder'].replace('file://localhost/', '/')) + if self.library['Music Folder'].startswith('//'): + self.library['Music Folder'] = self.library['Music Folder'][1:] + self.xml = xml + self.root = self.library['Music Folder'] + diff --git a/oxcd/plistlib.py b/oxcd/plistlib.py new file mode 100644 index 0000000..0cbcb20 --- /dev/null +++ b/oxcd/plistlib.py @@ -0,0 +1,454 @@ +r"""plistlib.py -- a tool to generate and parse MacOSX .plist files. + +The property list (.plist) file format is a simple XML pickle supporting +basic object types, like dictionaries, lists, numbers and strings. +Usually the top level object is a dictionary. + +To write out a plist file, use the writePlist(rootObject, pathOrFile) +function. 'rootObject' is the top level object, 'pathOrFile' is a +filename or a (writable) file object. + +To parse a plist from a file, use the readPlist(pathOrFile) function, +with a file name or a (readable) file object as the only argument. It +returns the top level object (again, usually a dictionary). + +To work with plist data in bytes objects, you can use readPlistFromBytes() +and writePlistToBytes(). + +Values can be strings, integers, floats, booleans, tuples, lists, +dictionaries (but only with string keys), Data or datetime.datetime objects. +String values (including dictionary keys) have to be unicode strings -- they +will be written out as UTF-8. + +The plist type is supported through the Data class. This is a +thin wrapper around a Python bytes object. Use 'Data' if your strings +contain control characters. + +Generate Plist example: + + pl = dict( + aString = "Doodah", + aList = ["A", "B", 12, 32.1, [1, 2, 3]], + aFloat = 0.1, + anInt = 728, + aDict = dict( + anotherString = "", + aUnicodeValue = "M\xe4ssig, Ma\xdf", + aTrueValue = True, + aFalseValue = False, + ), + someData = Data(b""), + someMoreData = Data(b"" * 10), + aDate = datetime.datetime.fromtimestamp(time.mktime(time.gmtime())), + ) + writePlist(pl, fileName) + +Parse Plist example: + + pl = readPlist(pathOrFile) + print pl["aKey"] +""" + + +__all__ = [ + "readPlist", "writePlist", "readPlistFromBytes", "writePlistToBytes", + "Plist", "Data", "Dict" +] +# Note: the Plist and Dict classes have been deprecated. + +import binascii +import datetime +from io import BytesIO +import re + + +def readPlist(pathOrFile): + """Read a .plist file. 'pathOrFile' may either be a file name or a + (readable) file object. Return the unpacked root object (which + usually is a dictionary). + """ + didOpen = False + try: + if isinstance(pathOrFile, str): + pathOrFile = open(pathOrFile, 'rb') + didOpen = True + p = PlistParser() + rootObject = p.parse(pathOrFile) + finally: + if didOpen: + pathOrFile.close() + return rootObject + + +def writePlist(rootObject, pathOrFile): + """Write 'rootObject' to a .plist file. 'pathOrFile' may either be a + file name or a (writable) file object. + """ + didOpen = False + try: + if isinstance(pathOrFile, str): + pathOrFile = open(pathOrFile, 'wb') + didOpen = True + writer = PlistWriter(pathOrFile) + writer.writeln("") + writer.writeValue(rootObject) + writer.writeln("") + finally: + if didOpen: + pathOrFile.close() + + +def readPlistFromBytes(data): + """Read a plist data from a bytes object. Return the root object. + """ + return readPlist(BytesIO(data)) + + +def writePlistToBytes(rootObject): + """Return 'rootObject' as a plist-formatted bytes object. + """ + f = BytesIO() + writePlist(rootObject, f) + return f.getvalue() + + +class DumbXMLWriter: + def __init__(self, file, indentLevel=0, indent="\t"): + self.file = file + self.stack = [] + self.indentLevel = indentLevel + self.indent = indent + + def beginElement(self, element): + self.stack.append(element) + self.writeln("<%s>" % element) + self.indentLevel += 1 + + def endElement(self, element): + assert self.indentLevel > 0 + assert self.stack.pop() == element + self.indentLevel -= 1 + self.writeln("" % element) + + def simpleElement(self, element, value=None): + if value is not None: + value = _escape(value) + self.writeln("<%s>%s" % (element, value, element)) + else: + self.writeln("<%s/>" % element) + + def writeln(self, line): + if line: + # plist has fixed encoding of utf-8 + if isinstance(line, str): + line = line.encode('utf-8') + self.file.write(self.indentLevel * self.indent) + self.file.write(line) + self.file.write(b'\n') + + +# Contents should conform to a subset of ISO 8601 +# (in particular, YYYY '-' MM '-' DD 'T' HH ':' MM ':' SS 'Z'. Smaller units may be omitted with +# a loss of precision) +_dateParser = re.compile(r"(?P\d\d\d\d)(?:-(?P\d\d)(?:-(?P\d\d)(?:T(?P\d\d)(?::(?P\d\d)(?::(?P\d\d))?)?)?)?)?Z") + +def _dateFromString(s): + order = ('year', 'month', 'day', 'hour', 'minute', 'second') + gd = _dateParser.match(s).groupdict() + lst = [] + for key in order: + val = gd[key] + if val is None: + break + lst.append(int(val)) + return datetime.datetime(*lst) + +def _dateToString(d): + return '%04d-%02d-%02dT%02d:%02d:%02dZ' % ( + d.year, d.month, d.day, + d.hour, d.minute, d.second + ) + + +# Regex to find any control chars, except for \t \n and \r +_controlCharPat = re.compile( + r"[\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0e\x0f" + r"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f]") + +def _escape(text): + m = _controlCharPat.search(text) + if m is not None: + raise ValueError("strings can't contains control characters; " + "use plistlib.Data instead") + text = text.replace("\r\n", "\n") # convert DOS line endings + text = text.replace("\r", "\n") # convert Mac line endings + text = text.replace("&", "&") # escape '&' + text = text.replace("<", "<") # escape '<' + text = text.replace(">", ">") # escape '>' + return text + + +PLISTHEADER = b"""\ + + +""" + +class PlistWriter(DumbXMLWriter): + + def __init__(self, file, indentLevel=0, indent=b"\t", writeHeader=1): + if writeHeader: + file.write(PLISTHEADER) + DumbXMLWriter.__init__(self, file, indentLevel, indent) + + def writeValue(self, value): + if isinstance(value, str): + self.simpleElement("string", value) + elif isinstance(value, bool): + # must switch for bool before int, as bool is a + # subclass of int... + if value: + self.simpleElement("true") + else: + self.simpleElement("false") + elif isinstance(value, int): + self.simpleElement("integer", "%d" % value) + elif isinstance(value, float): + self.simpleElement("real", repr(value)) + elif isinstance(value, dict): + self.writeDict(value) + elif isinstance(value, Data): + self.writeData(value) + elif isinstance(value, datetime.datetime): + self.simpleElement("date", _dateToString(value)) + elif isinstance(value, (tuple, list)): + self.writeArray(value) + else: + raise TypeError("unsupported type: %s" % type(value)) + + def writeData(self, data): + self.beginElement("data") + self.indentLevel -= 1 + maxlinelength = 76 - len(self.indent.replace(b"\t", b" " * 8) * + self.indentLevel) + for line in data.asBase64(maxlinelength).split(b"\n"): + if line: + self.writeln(line) + self.indentLevel += 1 + self.endElement("data") + + def writeDict(self, d): + if d: + self.beginElement("dict") + items = sorted(d.items()) + for key, value in items: + if not isinstance(key, str): + raise TypeError("keys must be strings") + self.simpleElement("key", key) + self.writeValue(value) + self.endElement("dict") + else: + self.simpleElement("dict") + + def writeArray(self, array): + if array: + self.beginElement("array") + for value in array: + self.writeValue(value) + self.endElement("array") + else: + self.simpleElement("array") + + +class _InternalDict(dict): + + # This class is needed while Dict is scheduled for deprecation: + # we only need to warn when a *user* instantiates Dict or when + # the "attribute notation for dict keys" is used. + + def __getattr__(self, attr): + try: + value = self[attr] + except KeyError: + raise AttributeError(attr) + from warnings import warn + warn("Attribute access from plist dicts is deprecated, use d[key] " + "notation instead", DeprecationWarning, 2) + return value + + def __setattr__(self, attr, value): + from warnings import warn + warn("Attribute access from plist dicts is deprecated, use d[key] " + "notation instead", DeprecationWarning, 2) + self[attr] = value + + def __delattr__(self, attr): + try: + del self[attr] + except KeyError: + raise AttributeError(attr) + from warnings import warn + warn("Attribute access from plist dicts is deprecated, use d[key] " + "notation instead", DeprecationWarning, 2) + +class Dict(_InternalDict): + + def __init__(self, **kwargs): + from warnings import warn + warn("The plistlib.Dict class is deprecated, use builtin dict instead", + DeprecationWarning, 2) + super().__init__(**kwargs) + + +class Plist(_InternalDict): + + """This class has been deprecated. Use readPlist() and writePlist() + functions instead, together with regular dict objects. + """ + + def __init__(self, **kwargs): + from warnings import warn + warn("The Plist class is deprecated, use the readPlist() and " + "writePlist() functions instead", DeprecationWarning, 2) + super().__init__(**kwargs) + + def fromFile(cls, pathOrFile): + """Deprecated. Use the readPlist() function instead.""" + rootObject = readPlist(pathOrFile) + plist = cls() + plist.update(rootObject) + return plist + fromFile = classmethod(fromFile) + + def write(self, pathOrFile): + """Deprecated. Use the writePlist() function instead.""" + writePlist(self, pathOrFile) + + +def _encodeBase64(s, maxlinelength=76): + # copied from base64.encodebytes(), with added maxlinelength argument + maxbinsize = (maxlinelength//4)*3 + pieces = [] + for i in range(0, len(s), maxbinsize): + chunk = s[i : i + maxbinsize] + pieces.append(binascii.b2a_base64(chunk)) + return b''.join(pieces) + +class Data: + + """Wrapper for binary data.""" + + def __init__(self, data): + if not isinstance(data, bytes): + raise TypeError("data must be as bytes") + self.data = data + + @classmethod + def fromBase64(cls, data): + # base64.decodebytes just calls binascii.a2b_base64; + # it seems overkill to use both base64 and binascii. + return cls(binascii.a2b_base64(data)) + + def asBase64(self, maxlinelength=76): + return _encodeBase64(self.data, maxlinelength) + + def __eq__(self, other): + if isinstance(other, self.__class__): + return self.data == other.data + elif isinstance(other, str): + return self.data == other + else: + return id(self) == id(other) + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, repr(self.data)) + +class PlistParser: + + def __init__(self): + self.stack = [] + self.currentKey = None + self.root = None + + def parse(self, fileobj): + from xml.parsers.expat import ParserCreate + self.parser = ParserCreate() + self.parser.StartElementHandler = self.handleBeginElement + self.parser.EndElementHandler = self.handleEndElement + self.parser.CharacterDataHandler = self.handleData + self.parser.ParseFile(fileobj) + return self.root + + def handleBeginElement(self, element, attrs): + self.data = [] + handler = getattr(self, "begin_" + element, None) + if handler is not None: + handler(attrs) + + def handleEndElement(self, element): + handler = getattr(self, "end_" + element, None) + if handler is not None: + handler() + + def handleData(self, data): + self.data.append(data) + + def addObject(self, value): + if self.currentKey is not None: + if not isinstance(self.stack[-1], type({})): + raise ValueError("unexpected element at line %d" % + self.parser.CurrentLineNumber) + self.stack[-1][self.currentKey] = value + self.currentKey = None + elif not self.stack: + # this is the root object + self.root = value + else: + if not isinstance(self.stack[-1], type([])): + raise ValueError("unexpected element at line %d" % + self.parser.CurrentLineNumber) + self.stack[-1].append(value) + + def getData(self): + data = ''.join(self.data) + self.data = [] + return data + + # element handlers + + def begin_dict(self, attrs): + d = _InternalDict() + self.addObject(d) + self.stack.append(d) + def end_dict(self): + if self.currentKey: + raise ValueError("missing value for key '%s' at line %d" % + (self.currentKey,self.parser.CurrentLineNumber)) + self.stack.pop() + + def end_key(self): + if self.currentKey or not isinstance(self.stack[-1], type({})): + raise ValueError("unexpected key at line %d" % + self.parser.CurrentLineNumber) + self.currentKey = self.getData() + + def begin_array(self, attrs): + a = [] + self.addObject(a) + self.stack.append(a) + def end_array(self): + self.stack.pop() + + def end_true(self): + self.addObject(True) + def end_false(self): + self.addObject(False) + def end_integer(self): + self.addObject(int(self.getData())) + def end_real(self): + self.addObject(float(self.getData())) + def end_string(self): + self.addObject(self.getData()) + def end_data(self): + self.addObject(Data.fromBase64(self.getData().encode("utf-8"))) + def end_date(self): + self.addObject(_dateFromString(self.getData())) diff --git a/oxcd/server.py b/oxcd/server.py new file mode 100644 index 0000000..4e6295c --- /dev/null +++ b/oxcd/server.py @@ -0,0 +1,202 @@ +# encoding: utf-8 +# vi:si:et:sw=4:sts=4:ts=4 +from __future__ import with_statement, division + +import sys +import inspect +import os +import json +from urlparse import urlparse +import datetime + +from twisted.web.resource import Resource +from twisted.web.static import File +from twisted.web.util import Redirect +from twisted.web.error import NoResource + +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 _to_json(python_object): + if isinstance(python_object, datetime.datetime): + return python_object.strftime('%Y-%m-%dT%H:%M:%SZ') + raise TypeError(u'%s %s is not JSON serializable' % (repr(python_object), type(python_object))) + +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(backend, 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, default=_to_json) + +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): + 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.startswith('/track/'): + track_id = request.path.split('/')[-1].split('.')[0] + track = self.backend.library['Tracks'].get(track_id) + if track: + path = track['Location'] + if os.path.exists(path): + request.headers['Access-Control-Allow-Origin'] = '*' + f = File(path, 'audio/mpeg') + f.isLeaf = True + return f + return NoResource() + 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): + request.headers['Server'] = 'oxcd/%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): + request.headers['Server'] = 'oxcd/%s' % __version__ + if request.path.startswith('/api'): + path = self.static_path('api.html') + else: + path = self.static_path('index.html') + with open(path) as f: + data = f.read() + request.headers['Content-Type'] = 'text/html' + site = self.get_site(request) + data = data.replace('$name', site) + return data diff --git a/oxcd/static/api.html b/oxcd/static/api.html new file mode 100644 index 0000000..e2fbc29 --- /dev/null +++ b/oxcd/static/api.html @@ -0,0 +1,12 @@ + + + + + 0xcd API + + + + + + + diff --git a/oxcd/static/index.html b/oxcd/static/index.html new file mode 100644 index 0000000..8b26615 --- /dev/null +++ b/oxcd/static/index.html @@ -0,0 +1,12 @@ + + + + + 0xcd + + + + + + + diff --git a/oxcd/static/js/api/api.js b/oxcd/static/js/api/api.js new file mode 100755 index 0000000..cbf4536 --- /dev/null +++ b/oxcd/static/js/api/api.js @@ -0,0 +1,170 @@ +/*** + Pandora API +***/ +Ox.load('UI', { + hideScreen: false, + showScreen: true, + theme: 'classic' +}, function() { + +var app = new Ox.App({ + url: '/api/', + init: 'init', +}).bindEvent('load', function(data) { + app.site = data.site; + app.user = data.user; + app.site = app.site || {site: {name: ''}}; + app.site.default_info = '

Pan.do/ra API Overview

use this api in the browser with Ox.app or use pandora_client it in python. Further description of the api can be found on the wiki
'; + app.$body = $('body'); + app.$document = $(document); + app.$window = $(window); + //app.$body.html(''); + Ox.UI.hideLoadingScreen(); + + app.$ui = {}; + app.$ui.actionList = constructList(); + app.$ui.actionInfo = Ox.Container().css({padding: '16px'}).html(app.site.default_info); + + app.api.api({docs: true, code: true}, function(results) { + app.actions = results.data.actions; + if (document.location.hash) { + app.$ui.actionList.triggerEvent('select', { + ids: document.location.hash.substring(1).split(',') + }); + } + }); + + var $left = new Ox.SplitPanel({ + elements: [ + { + element: new Ox.Element().append(new Ox.Element() + .html(app.site.site.name + ' API').css({ + 'padding': '4px', + })).css({ + 'background-color': '#ddd', + 'font-weight': 'bold', + }), + size: 24 + }, + { + element: app.$ui.actionList + } + ], + orientation: 'vertical' + }); + var $main = new Ox.SplitPanel({ + elements: [ + { + element: $left, + size: 160 + }, + { + element: app.$ui.actionInfo, + } + ], + orientation: 'horizontal' + }); + + $main.appendTo(app.$body); +}); + +function constructList() { + return new Ox.TableList({ + columns: [ + { + align: "left", + id: "name", + operator: "+", + title: "Name", + visible: true, + width: 140 + }, + ], + columnsMovable: false, + columnsRemovable: false, + id: 'actionList', + items: function(data, callback) { + function _sort(a, b) { + return a.name > b.name ? 1 : a.name == b.name ? 0 : -1; + } + if (!data.keys) { + app.api.api(function(results) { + var items = []; + Ox.forEach(results.data.actions, function(v, k) { + items.push({'name': k}) + }); + items.sort(_sort); + var result = {'data': {'items': items.length}}; + callback(result); + }); + } else { + app.api.api(function(results) { + var items = []; + Ox.forEach(results.data.actions, function(v, k) { + items.push({'name': k}) + }); + items.sort(_sort); + var result = {'data': {'items': items}}; + callback(result); + }); + } + }, + scrollbarVisible: true, + sort: [{key: "name", operator: "+"}], + unique: 'name' + }).bindEvent({ + select: function(data) { + var info = $('
').addClass('OxSelectable'), + hash = '#'; + if (data.ids.length) + data.ids.forEach(function(id) { + info.append( + $('

') + .html(id) + .css({ + marginBottom: '8px' + }) + ); + var code = app.actions[id].code[1], + f = app.actions[id].code[0], + line = Math.round(Ox.last(f.split(':')) || 0), + doc = app.actions[id].doc.replace('/\n/
\n/g'), + $code, $doc; + + $doc = Ox.SyntaxHighlighter({ + source: doc, + }) + .appendTo(info); + + Ox.Button({ + title: 'View Source (' + f + ')', + }).bindEvent({ + click: function() { + $code.toggle(); + } + }) + .css({ + margin: '4px' + }) + .appendTo(info); + $code = Ox.SyntaxHighlighter({ + showLineNumbers: true, + source: code, + offset: line + }) + .css({ + borderWidth: '1px', + }).appendTo(info).hide(); + Ox.print(code); + hash += id + ',' + }); + else + info.html(app.site.default_info); + + document.location.hash = hash.slice(0, -1); + app.$ui.actionInfo.html(info); + } + }); +} +}); + diff --git a/oxcd/static/js/site.js b/oxcd/static/js/site.js new file mode 100644 index 0000000..6cebc6c --- /dev/null +++ b/oxcd/static/js/site.js @@ -0,0 +1,18 @@ +Ox.load('UI', function() { + window.oxcd = Ox.App({ + name: 'oxcd', + url: '/api/' + }).bindEvent({ + load: function(data) { + oxcd.api.library(function(result) { + var items = Ox.values(result.data.Tracks), + element = Ox.Element(); + items.forEach(function(item) { + element.append($('
').html(''+item['Track ID'] + ': '+item.Name+'')) + console.log(''+item['Track ID'] + ':'+item.Name+''); + }); + Ox.UI.$body.append(element); + }) + } + }); +}); diff --git a/oxcd/static/png/icon16.png b/oxcd/static/png/icon16.png new file mode 100644 index 0000000..23e2830 Binary files /dev/null and b/oxcd/static/png/icon16.png differ diff --git a/oxcd/version.py b/oxcd/version.py new file mode 100644 index 0000000..1c6be3a --- /dev/null +++ b/oxcd/version.py @@ -0,0 +1 @@ +__version__ = 0.1 diff --git a/update.sh b/update.sh new file mode 100755 index 0000000..126325d --- /dev/null +++ b/update.sh @@ -0,0 +1,28 @@ +#!/bin/bash +oxcd_repos=http://code.0x2620.org/oxcd/ +oxjs_repos=http://code.0x2620.org/oxjs/ +cd `dirname $0` +base=`pwd` +current=`bzr revno` +bzr pull $pandora_repos +new=`bzr revno` +cd $base +if [ -e oxcd/static/oxjs ]; then + cd oxcd/static/oxjs + current=$current`bzr revno` + bzr pull $oxjs_repos + new=$new`bzr revno` +else + cd oxcd/static + bzr branch $oxjs_repos + cd oxjs + new=$new`bzr revno` +fi +if [ $current -ne $new ]; then + cd $base/oxcd/static/oxjs + if [ -e build/Ox.Geo/json/Ox.Geo.json ]; then + ./tools/build/build.py -nogeo >/dev/null + else + ./tools/build/build.py >/dev/null + fi +fi