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