0xcd
This commit is contained in:
commit
85dc70aae0
14 changed files with 997 additions and 0 deletions
3
README.txt
Normal file
3
README.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
on os x run: ./bin/0xcd
|
||||
run ./bin/0xcd -h for a list of options
|
||||
|
24
bin/0xcd
Executable file
24
bin/0xcd
Executable file
|
@ -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)
|
35
oxcd/__init__.py
Normal file
35
oxcd/__init__.py
Normal file
|
@ -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()
|
15
oxcd/api.py
Normal file
15
oxcd/api.py
Normal file
|
@ -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)
|
23
oxcd/itunes.py
Normal file
23
oxcd/itunes.py
Normal file
|
@ -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']
|
||||
|
454
oxcd/plistlib.py
Normal file
454
oxcd/plistlib.py
Normal file
|
@ -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 <data> 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 = "<hello & hi there!>",
|
||||
aUnicodeValue = "M\xe4ssig, Ma\xdf",
|
||||
aTrueValue = True,
|
||||
aFalseValue = False,
|
||||
),
|
||||
someData = Data(b"<binary gunk>"),
|
||||
someMoreData = Data(b"<lots of binary gunk>" * 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("<plist version=\"1.0\">")
|
||||
writer.writeValue(rootObject)
|
||||
writer.writeln("</plist>")
|
||||
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("</%s>" % element)
|
||||
|
||||
def simpleElement(self, element, value=None):
|
||||
if value is not None:
|
||||
value = _escape(value)
|
||||
self.writeln("<%s>%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<year>\d\d\d\d)(?:-(?P<month>\d\d)(?:-(?P<day>\d\d)(?:T(?P<hour>\d\d)(?::(?P<minute>\d\d)(?::(?P<second>\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"""\
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
"""
|
||||
|
||||
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()))
|
202
oxcd/server.py
Normal file
202
oxcd/server.py
Normal file
|
@ -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
|
12
oxcd/static/api.html
Normal file
12
oxcd/static/api.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>0xcd API</title>
|
||||
<link rel="shortcut icon" type="image/png" href="/png/icon16.png"/>
|
||||
<script type="text/javascript" src="/oxjs/build/Ox.js"></script>
|
||||
<script type="text/javascript" src="/js/api/api.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
12
oxcd/static/index.html
Normal file
12
oxcd/static/index.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>0xcd</title>
|
||||
<link rel="shortcut icon" type="image/png" href="/png/icon16.png"/>
|
||||
<script type="text/javascript" src="/oxjs/build/Ox.js"></script>
|
||||
<script type="text/javascript" src="/js/site.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
170
oxcd/static/js/api/api.js
Executable file
170
oxcd/static/js/api/api.js
Executable file
|
@ -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 = '<div class="OxSelectable"><h2>Pan.do/ra API Overview</h2>use this api in the browser with <a href="/static/oxjs/demos/doc2/index.html#Ox.App">Ox.app</a> or use <a href="http://code.0x2620.org/pandora_client">pandora_client</a> it in python. Further description of the api can be found <a href="https://wiki.0x2620.org/wiki/pandora/API">on the wiki</a></div>';
|
||||
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 = $('<div>').addClass('OxSelectable'),
|
||||
hash = '#';
|
||||
if (data.ids.length)
|
||||
data.ids.forEach(function(id) {
|
||||
info.append(
|
||||
$('<h2>')
|
||||
.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/<br>\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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
18
oxcd/static/js/site.js
Normal file
18
oxcd/static/js/site.js
Normal file
|
@ -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($('<div>').html('<a href="/track/'+item['Track ID']+'.mp3">'+item['Track ID'] + ': '+item.Name+'</a>'))
|
||||
console.log('<a href="/track/'+item['Track ID']+'.mp3">'+item['Track ID'] + ':'+item.Name+'</a>');
|
||||
});
|
||||
Ox.UI.$body.append(element);
|
||||
})
|
||||
}
|
||||
});
|
||||
});
|
BIN
oxcd/static/png/icon16.png
Normal file
BIN
oxcd/static/png/icon16.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.4 KiB |
1
oxcd/version.py
Normal file
1
oxcd/version.py
Normal file
|
@ -0,0 +1 @@
|
|||
__version__ = 0.1
|
28
update.sh
Executable file
28
update.sh
Executable file
|
@ -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
|
Loading…
Reference in a new issue