From 85dc70aae086462ed7efa28d10cd0d4a8f37fdef Mon Sep 17 00:00:00 2001 From: j <0x006A@0x2620.org> Date: Thu, 6 Sep 2012 12:08:30 +0200 Subject: [PATCH] 0xcd --- README.txt | 3 + bin/0xcd | 24 ++ oxcd/__init__.py | 35 +++ oxcd/api.py | 15 ++ oxcd/itunes.py | 23 ++ oxcd/plistlib.py | 454 +++++++++++++++++++++++++++++++++++++ oxcd/server.py | 202 +++++++++++++++++ oxcd/static/api.html | 12 + oxcd/static/index.html | 12 + oxcd/static/js/api/api.js | 170 ++++++++++++++ oxcd/static/js/site.js | 18 ++ oxcd/static/png/icon16.png | Bin 0 -> 3464 bytes oxcd/version.py | 1 + update.sh | 28 +++ 14 files changed, 997 insertions(+) create mode 100644 README.txt create mode 100755 bin/0xcd create mode 100644 oxcd/__init__.py create mode 100644 oxcd/api.py create mode 100644 oxcd/itunes.py create mode 100644 oxcd/plistlib.py create mode 100644 oxcd/server.py create mode 100644 oxcd/static/api.html create mode 100644 oxcd/static/index.html create mode 100755 oxcd/static/js/api/api.js create mode 100644 oxcd/static/js/site.js create mode 100644 oxcd/static/png/icon16.png create mode 100644 oxcd/version.py create mode 100755 update.sh 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 0000000000000000000000000000000000000000..23e2830ad0919be25de8656bda539a78227df138 GIT binary patch literal 3464 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/dev/null + else + ./tools/build/build.py >/dev/null + fi +fi