Use generic response renderer
This commit is contained in:
parent
73f0ff1201
commit
b44b6fb022
460
pysonic/api.py
460
pysonic/api.py
|
@ -1,3 +1,4 @@
|
||||||
|
import re
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import subprocess
|
import subprocess
|
||||||
|
@ -11,55 +12,17 @@ from pysonic.library import LETTER_GROUPS
|
||||||
from pysonic.types import MUSIC_TYPES
|
from pysonic.types import MUSIC_TYPES
|
||||||
|
|
||||||
|
|
||||||
|
CALLBACK_RE = re.compile(r'^[a-zA-Z0-9_]+$')
|
||||||
logging = logging.getLogger("api")
|
logging = logging.getLogger("api")
|
||||||
|
|
||||||
|
|
||||||
class ApiResponse(object):
|
|
||||||
def __init__(self, status="ok", version="1.15.0", top=None, **kwargs):
|
|
||||||
self.status = status
|
|
||||||
self.version = version
|
|
||||||
self.data = {}
|
|
||||||
self.top = top
|
|
||||||
if self.top:
|
|
||||||
self.data[self.top] = kwargs ## kwargs unused TODO
|
|
||||||
|
|
||||||
def add_child(self, _type, **kwargs):
|
|
||||||
if not self.top:
|
|
||||||
raise Exception("You can't do this?")
|
|
||||||
if _type not in self.data[self.top]:
|
|
||||||
self.data[self.top][_type] = []
|
|
||||||
self.data[self.top][_type].append(kwargs)
|
|
||||||
|
|
||||||
def render_json(self):
|
|
||||||
return json.dumps({"subsonic-response": dict(status=self.status, version="1.15.0", **self.data)}, indent=4)
|
|
||||||
|
|
||||||
def render_xml(self):
|
|
||||||
doc = BeautifulSoup('', features='lxml-xml')
|
|
||||||
root = doc.new_tag("subsonic-response", xmlns="http://subsonic.org/restapi",
|
|
||||||
status=self.status,
|
|
||||||
version=self.version)
|
|
||||||
doc.append(root)
|
|
||||||
|
|
||||||
if self.top:
|
|
||||||
top = doc.new_tag(self.top)
|
|
||||||
root.append(top)
|
|
||||||
# TODO top_attrs ?
|
|
||||||
for top_child_type, top_child_instances in self.data[self.top].items():
|
|
||||||
for top_child_attrs in top_child_instances:
|
|
||||||
child = doc.new_tag(top_child_type)
|
|
||||||
child.attrs.update(top_child_attrs)
|
|
||||||
top.append(child)
|
|
||||||
|
|
||||||
albumlist = doc.new_tag("albumList")
|
|
||||||
doc.append(albumlist)
|
|
||||||
return doc.prettify()
|
|
||||||
|
|
||||||
|
|
||||||
response_formats = defaultdict(lambda: "render_xml")
|
response_formats = defaultdict(lambda: "render_xml")
|
||||||
response_formats["json"] = "render_json"
|
response_formats["json"] = "render_json"
|
||||||
|
response_formats["jsonp"] = "render_jsonp"
|
||||||
|
|
||||||
response_headers = defaultdict(lambda: "text/xml; charset=utf-8")
|
response_headers = defaultdict(lambda: "text/xml; charset=utf-8")
|
||||||
response_headers["json"] = "text/json" #TODO is this right?
|
response_headers["json"] = "application/json; charset=utf-8"
|
||||||
|
response_headers["jsonp"] = "text/javascript; charset=utf-8"
|
||||||
|
|
||||||
|
|
||||||
def formatresponse(func):
|
def formatresponse(func):
|
||||||
|
@ -68,84 +31,169 @@ def formatresponse(func):
|
||||||
"""
|
"""
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
response = func(*args, **kwargs)
|
response = func(*args, **kwargs)
|
||||||
cherrypy.response.headers['Content-Type'] = response_headers[kwargs.get("f", "xml")]
|
response_format = kwargs.get("f", "xml")
|
||||||
return getattr(response, response_formats[kwargs.get("f", "xml")])()
|
callback = kwargs.get("callback", None)
|
||||||
|
cherrypy.response.headers['Content-Type'] = response_headers[response_format]
|
||||||
|
renderer = getattr(response, response_formats[response_format])
|
||||||
|
if response_format == "jsonp":
|
||||||
|
if callback is None:
|
||||||
|
return response.render_xml().encode('UTF-8') # copy original subsonic behavior
|
||||||
|
else:
|
||||||
|
return renderer(callback).encode('UTF-8')
|
||||||
|
return renderer().encode('UTF-8')
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
class ApiResponse(object):
|
||||||
|
def __init__(self, status="ok", version="1.15.0"):
|
||||||
|
"""
|
||||||
|
ApiResponses are python data structures that can be converted to other formats. The response has a status and a
|
||||||
|
version. The response data structure is stored in self.data and follows these rules:
|
||||||
|
- self.data is a dict
|
||||||
|
- the dict's values become either child nodes or attributes, named by the key
|
||||||
|
- lists become many oner one child
|
||||||
|
- dict values are not allowed
|
||||||
|
- all other types (str, int, NoneType) are attributes
|
||||||
|
:param status:
|
||||||
|
:param version:
|
||||||
|
"""
|
||||||
|
self.status = status
|
||||||
|
self.version = version
|
||||||
|
self.data = defaultdict(lambda: list())
|
||||||
|
|
||||||
|
def add_child(self, _type, _parent="", _real_parent=None, **kwargs):
|
||||||
|
parent = _real_parent if _real_parent else self.get_child(_parent)
|
||||||
|
m = defaultdict(lambda: list())
|
||||||
|
m.update(dict(kwargs))
|
||||||
|
parent[_type].append(m)
|
||||||
|
return m
|
||||||
|
|
||||||
|
def get_child(self, _path):
|
||||||
|
parent_path = _path.split(".")
|
||||||
|
parent = self.data
|
||||||
|
for item in parent_path:
|
||||||
|
if not item:
|
||||||
|
continue
|
||||||
|
parent = parent.get(item)[0]
|
||||||
|
return parent
|
||||||
|
|
||||||
|
def set_attrs(self, _path, **attrs):
|
||||||
|
parent = self.get_child(_path)
|
||||||
|
if type(parent) not in (dict, defaultdict):
|
||||||
|
raise Exception("wot")
|
||||||
|
parent.update(attrs)
|
||||||
|
|
||||||
|
def render_json(self):
|
||||||
|
def _flatten_json(item):
|
||||||
|
"""
|
||||||
|
Convert defaultdicts to dicts and remove lists where node has 1 or no child
|
||||||
|
"""
|
||||||
|
listed_attrs = ["folder"]
|
||||||
|
d = {}
|
||||||
|
for k, v in item.items():
|
||||||
|
if type(v) is list:
|
||||||
|
if len(v) > 1:
|
||||||
|
d[k] = []
|
||||||
|
for subitem in v:
|
||||||
|
d[k].append(_flatten_json(subitem))
|
||||||
|
elif len(v) == 1:
|
||||||
|
d[k] = _flatten_json(v[0])
|
||||||
|
else:
|
||||||
|
d[k] = {}
|
||||||
|
else:
|
||||||
|
d[k] = [v] if k in listed_attrs else v
|
||||||
|
return d
|
||||||
|
|
||||||
|
data = _flatten_json(self.data)
|
||||||
|
return json.dumps({"subsonic-response": dict(status=self.status, version=self.version, **data)}, indent=4)
|
||||||
|
|
||||||
|
def render_jsonp(self, callback):
|
||||||
|
assert CALLBACK_RE.match(callback), "Invalid callback"
|
||||||
|
return "{}({});".format(callback, self.render_json())
|
||||||
|
|
||||||
|
def render_xml(self):
|
||||||
|
text_attrs = ['largeImageUrl', 'musicBrainzId', 'smallImageUrl', 'mediumImageUrl', 'lastFmUrl', 'biography',
|
||||||
|
'folder']
|
||||||
|
# These attributes will be placed in <hello>{{ value }}</hello> tags instead of hello="{{ value }}" on parent
|
||||||
|
doc = BeautifulSoup('', features='lxml-xml')
|
||||||
|
root = doc.new_tag("subsonic-response", xmlns="http://subsonic.org/restapi",
|
||||||
|
status=self.status,
|
||||||
|
version=self.version)
|
||||||
|
doc.append(root)
|
||||||
|
|
||||||
|
def _render_xml(node, parent):
|
||||||
|
"""
|
||||||
|
For every key in the node dict, the parent gets a new child tag with name == key
|
||||||
|
If the value is a dict, it becomes the new tag's attrs
|
||||||
|
If the value is a list, the parent gets many new tags with each dict as attrs
|
||||||
|
If the value is str int etc, parent gets attrs
|
||||||
|
"""
|
||||||
|
for key, value in node.items():
|
||||||
|
if type(value) in (dict, defaultdict):
|
||||||
|
tag = doc.new_tag(key)
|
||||||
|
parent.append(tag)
|
||||||
|
tag.attrs.update(value)
|
||||||
|
elif type(value) is list:
|
||||||
|
for item in value:
|
||||||
|
tag = doc.new_tag(key)
|
||||||
|
parent.append(tag)
|
||||||
|
_render_xml(item, tag)
|
||||||
|
else:
|
||||||
|
if key in text_attrs:
|
||||||
|
tag = doc.new_tag(key)
|
||||||
|
parent.append(tag)
|
||||||
|
tag.append(str(value))
|
||||||
|
else:
|
||||||
|
parent.attrs[key] = value
|
||||||
|
_render_xml(self.data, root)
|
||||||
|
return doc.prettify()
|
||||||
|
|
||||||
|
|
||||||
class PysonicApi(object):
|
class PysonicApi(object):
|
||||||
def __init__(self, db, library, options):
|
def __init__(self, db, library, options):
|
||||||
self.db = db
|
self.db = db
|
||||||
self.library = library
|
self.library = library
|
||||||
self.options = options
|
self.options = options
|
||||||
|
|
||||||
def response(self, status="ok"):
|
|
||||||
doc = BeautifulSoup('', features='lxml-xml')
|
|
||||||
root = doc.new_tag("subsonic-response", xmlns="http://subsonic.org/restapi", status=status, version="1.15.0")
|
|
||||||
doc.append(root)
|
|
||||||
return doc, root
|
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
|
@formatresponse
|
||||||
def ping_view(self, **kwargs):
|
def ping_view(self, **kwargs):
|
||||||
# Called when the app hits the "test connection" server option
|
# Called when the app hits the "test connection" server option
|
||||||
cherrypy.response.headers['Content-Type'] = 'text/xml; charset=utf-8'
|
return ApiResponse()
|
||||||
doc, root = self.response()
|
|
||||||
yield doc.prettify()
|
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
|
@formatresponse
|
||||||
def getLicense_view(self, **kwargs):
|
def getLicense_view(self, **kwargs):
|
||||||
# Called after ping.view
|
# Called after ping.view
|
||||||
cherrypy.response.headers['Content-Type'] = 'text/xml; charset=utf-8'
|
response = ApiResponse()
|
||||||
doc, root = self.response()
|
response.add_child("license",
|
||||||
root.append(doc.new_tag("license",
|
valid="true",
|
||||||
valid="true",
|
email="admin@localhost",
|
||||||
email="admin@localhost",
|
licenseExpires="2100-01-01T00:00:00.000Z",
|
||||||
licenseExpires="2100-01-01T00:00:00.000Z",
|
trialExpires="2100-01-01T01:01:00.000Z")
|
||||||
trialExpires="2100-01-01T01:01:00.000Z"))
|
return response
|
||||||
yield doc.prettify()
|
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
|
@formatresponse
|
||||||
def getMusicFolders_view(self, **kwargs):
|
def getMusicFolders_view(self, **kwargs):
|
||||||
# Get list of configured dirs
|
response = ApiResponse()
|
||||||
# {'c': 'DSub', 's': 'bfk9mir8is02u3m5as8ucsehn0', 'v': '1.2.0',
|
response.add_child("musicFolders")
|
||||||
# 't': 'e2b09fb9233d1bfac9abe3dc73017f1e', 'u': 'dave'}
|
|
||||||
# Access-Control-Allow-Origin:*
|
|
||||||
# Content-Encoding:gzip
|
|
||||||
# Content-Type:text/xml; charset=utf-8
|
|
||||||
# Server:Jetty(6.1.x)
|
|
||||||
# Transfer-Encoding:chunked
|
|
||||||
cherrypy.response.headers['Content-Type'] = 'text/xml; charset=utf-8'
|
|
||||||
|
|
||||||
doc, root = self.response()
|
|
||||||
folder_list = doc.new_tag("musicFolders")
|
|
||||||
root.append(folder_list)
|
|
||||||
|
|
||||||
for folder in self.library.get_libraries():
|
for folder in self.library.get_libraries():
|
||||||
entry = doc.new_tag("musicFolder", id=folder["id"])
|
response.add_child("musicFolder", _parent="musicFolders", id=folder["id"], name=folder["name"])
|
||||||
entry.attrs["name"] = folder["name"]
|
return response
|
||||||
folder_list.append(entry)
|
|
||||||
yield doc.prettify()
|
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
|
@formatresponse
|
||||||
def getIndexes_view(self, **kwargs):
|
def getIndexes_view(self, **kwargs):
|
||||||
# Get listing of top-level dir
|
# Get listing of top-level dir
|
||||||
# /rest/getIndexes.view?u=dave&s=bfk9mir8is02u3m5as8ucsehn0
|
response = ApiResponse()
|
||||||
# &t=e2b09fb9233d1bfac9abe3dc73017f1e&v=1.2.0&c=DSub HTTP/1.1
|
response.add_child("indexes", lastModified="1502310831000", ignoredArticles="The El La Los Las Le Les")
|
||||||
cherrypy.response.headers['Content-Type'] = 'text/xml; charset=utf-8'
|
|
||||||
doc, root = self.response()
|
|
||||||
indexes = doc.new_tag("indexes", lastModified="1502310831000", ignoredArticles="The El La Los Las Le Les")
|
|
||||||
doc.append(indexes)
|
|
||||||
|
|
||||||
for letter in LETTER_GROUPS:
|
for letter in LETTER_GROUPS:
|
||||||
index = doc.new_tag("index")
|
index = response.add_child("index", _parent="indexes", name=letter.upper())
|
||||||
index.attrs["name"] = letter.upper()
|
|
||||||
indexes.append(index)
|
|
||||||
for artist in self.library.get_artists():
|
for artist in self.library.get_artists():
|
||||||
if artist["name"][0].lower() in letter:
|
if artist["name"][0].lower() in letter:
|
||||||
artist_tag = doc.new_tag("artist")
|
response.add_child("artist", _real_parent=index, id=artist["id"], name=artist["name"])
|
||||||
artist_tag.attrs.update({"id": artist["id"], "name": artist["name"]})
|
return response
|
||||||
index.append(artist_tag)
|
|
||||||
yield doc.prettify()
|
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
def savePlayQueue_view(self, id, current, position, **kwargs):
|
def savePlayQueue_view(self, id, current, position, **kwargs):
|
||||||
|
@ -174,7 +222,9 @@ class PysonicApi(object):
|
||||||
raise NotImplemented()
|
raise NotImplemented()
|
||||||
albumset = albums[0 + int(offset):int(size) + int(offset)]
|
albumset = albums[0 + int(offset):int(size) + int(offset)]
|
||||||
|
|
||||||
response = ApiResponse(top="albumList")
|
response = ApiResponse()
|
||||||
|
|
||||||
|
response.add_child("albumList")
|
||||||
|
|
||||||
for album in albumset:
|
for album in albumset:
|
||||||
album_meta = album['metadata']
|
album_meta = album['metadata']
|
||||||
|
@ -184,8 +234,6 @@ class PysonicApi(object):
|
||||||
title=album_meta.get("id3_title", album["name"]), #TODO these cant be blank or dsub gets mad
|
title=album_meta.get("id3_title", album["name"]), #TODO these cant be blank or dsub gets mad
|
||||||
album=album_meta.get("id3_album", album["album"]),
|
album=album_meta.get("id3_album", album["album"]),
|
||||||
artist=album_meta.get("id3_artist", album["artist"]),
|
artist=album_meta.get("id3_artist", album["artist"]),
|
||||||
# X year="2014"
|
|
||||||
# X coverArt="3228"
|
|
||||||
# playCount="0"
|
# playCount="0"
|
||||||
# created="2016-05-08T05:31:31.000Z"/>)
|
# created="2016-05-08T05:31:31.000Z"/>)
|
||||||
)
|
)
|
||||||
|
@ -193,10 +241,11 @@ class PysonicApi(object):
|
||||||
album_kw["coverArt"] = album_meta["cover"]
|
album_kw["coverArt"] = album_meta["cover"]
|
||||||
if 'id3_year' in album_meta:
|
if 'id3_year' in album_meta:
|
||||||
album_kw["year"] = album_meta['id3_year']
|
album_kw["year"] = album_meta['id3_year']
|
||||||
response.add_child("album", **album_kw)
|
response.add_child("album", _parent="albumList", **album_kw)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
|
@formatresponse
|
||||||
def getMusicDirectory_view(self, id, **kwargs):
|
def getMusicDirectory_view(self, id, **kwargs):
|
||||||
"""
|
"""
|
||||||
List an artist dir
|
List an artist dir
|
||||||
|
@ -204,66 +253,67 @@ class PysonicApi(object):
|
||||||
dir_id = int(id)
|
dir_id = int(id)
|
||||||
|
|
||||||
cherrypy.response.headers['Content-Type'] = 'text/xml; charset=utf-8'
|
cherrypy.response.headers['Content-Type'] = 'text/xml; charset=utf-8'
|
||||||
doc, root = self.response()
|
|
||||||
|
|
||||||
dirtag = doc.new_tag("directory")
|
response = ApiResponse()
|
||||||
|
response.add_child("directory")
|
||||||
|
|
||||||
directory = self.library.get_dir(dir_id)
|
directory = self.library.get_dir(dir_id)
|
||||||
dir_meta = directory["metadata"]
|
dir_meta = directory["metadata"]
|
||||||
children = self.library.get_dir_children(dir_id)
|
children = self.library.get_dir_children(dir_id)
|
||||||
dirtag.attrs.update(name=directory['name'], id=directory['id'],
|
response.set_attrs(_path="directory", name=directory['name'], id=directory['id'],
|
||||||
parent=directory['parent'], playCount=10)
|
parent=directory['parent'], playCount=10)
|
||||||
root.append(dirtag)
|
|
||||||
|
|
||||||
for item in children:
|
for item in children:
|
||||||
# omit not dirs and media in browser
|
# omit not dirs and media in browser
|
||||||
if not item["isdir"] and item["type"] not in MUSIC_TYPES:
|
if not item["isdir"] and item["type"] not in MUSIC_TYPES:
|
||||||
continue
|
continue
|
||||||
item_meta = item['metadata']
|
item_meta = item['metadata']
|
||||||
dirtag.append(self.render_node(doc, item, item_meta, directory, dir_meta))
|
response.add_child("child", _parent="directory", **self.render_node2(item, item_meta, directory, dir_meta))
|
||||||
yield doc.prettify()
|
|
||||||
|
|
||||||
def render_node(self, doc, item, item_meta, directory, dir_meta, tagname="child"):
|
return response
|
||||||
child = doc.new_tag(tagname,
|
|
||||||
id=item["id"],
|
def render_node2(self, item, item_meta, directory, dir_meta):
|
||||||
parent=item["id"],
|
"""
|
||||||
isDir="true" if item['isdir'] else "false",
|
Given a node and it's parent directory, and meta, return a dict with the keys formatted how the subsonic clients
|
||||||
title=item_meta.get("id3_title", item["name"]),
|
expect them to be
|
||||||
album=item_meta.get("id3_album", item["album"]),
|
:param item:
|
||||||
artist=item_meta.get("id3_artist", item["artist"]),
|
:param item_meta:
|
||||||
# playCount="5",
|
:param directory:
|
||||||
# created="2016-04-25T07:31:33.000Z"
|
:param dir_meta:
|
||||||
# X track="3",
|
"""
|
||||||
# X year="2012",
|
child = dict(id=item["id"],
|
||||||
# X coverArt="12835",
|
parent=item["id"],
|
||||||
# X contentType="audio/mpeg"
|
isDir="true" if item['isdir'] else "false",
|
||||||
# X suffix="mp3"
|
title=item_meta.get("id3_title", item["name"]),
|
||||||
# genre="Other",
|
album=item_meta.get("id3_album", item["album"]),
|
||||||
# size="15838864"
|
artist=item_meta.get("id3_artist", item["artist"]),
|
||||||
# duration="395"
|
# playCount="5",
|
||||||
# bitRate="320"
|
# created="2016-04-25T07:31:33.000Z"
|
||||||
# path="Cosmic Gate/Sign Of The Times/03 Flatline (featuring Kyler England).mp3"
|
# genre="Other",
|
||||||
type="music")
|
# path="Cosmic Gate/Sign Of The Times/03 Flatline (featuring Kyler England).mp3"
|
||||||
|
type="music")
|
||||||
|
if 'kbitrate' in item_meta:
|
||||||
|
child["bitrate"] = item_meta["kbitrate"]
|
||||||
if item["size"] != -1:
|
if item["size"] != -1:
|
||||||
child.attrs["size"] = item["size"]
|
child["size"] = item["size"]
|
||||||
if "media_length" in item_meta:
|
if "media_length" in item_meta:
|
||||||
child.attrs["duration"] = item_meta["media_length"]
|
child["duration"] = item_meta["media_length"]
|
||||||
if "albumId" in directory:
|
if "albumId" in directory:
|
||||||
child.attrs["albumId"] = directory["id"]
|
child["albumId"] = directory["id"]
|
||||||
if "artistId" in directory:
|
if "artistId" in directory:
|
||||||
child.attrs["artistId"] = directory["parent"]
|
child["artistId"] = directory["parent"]
|
||||||
if "." in item["name"]:
|
if "." in item["name"]:
|
||||||
child.attrs["suffix"] = item["name"].split(".")[-1]
|
child["suffix"] = item["name"].split(".")[-1]
|
||||||
if item["type"]:
|
if item["type"]:
|
||||||
child.attrs["contentType"] = item["type"]
|
child["contentType"] = item["type"]
|
||||||
if 'cover' in item_meta:
|
if 'cover' in item_meta:
|
||||||
child.attrs["coverArt"] = item_meta["cover"]
|
child["coverArt"] = item_meta["cover"]
|
||||||
elif 'cover' in dir_meta:
|
elif 'cover' in dir_meta:
|
||||||
child.attrs["coverArt"] = dir_meta["cover"]
|
child["coverArt"] = dir_meta["cover"]
|
||||||
if 'track' in item_meta:
|
if 'track' in item_meta:
|
||||||
child.attrs["track"] = item_meta['track']
|
child["track"] = item_meta['track']
|
||||||
if 'id3_year' in item_meta:
|
if 'id3_year' in item_meta:
|
||||||
child.attrs["year"] = item_meta['id3_year']
|
child["year"] = item_meta['id3_year']
|
||||||
return child
|
return child
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
|
@ -341,7 +391,8 @@ class PysonicApi(object):
|
||||||
fpath = self.library.get_filepath(id)
|
fpath = self.library.get_filepath(id)
|
||||||
type2ct = {
|
type2ct = {
|
||||||
'jpg': 'image/jpeg',
|
'jpg': 'image/jpeg',
|
||||||
'png': 'image/png'
|
'png': 'image/png',
|
||||||
|
'gif': 'image/gif'
|
||||||
}
|
}
|
||||||
cherrypy.response.headers['Content-Type'] = type2ct[fpath[-3:]]
|
cherrypy.response.headers['Content-Type'] = type2ct[fpath[-3:]]
|
||||||
|
|
||||||
|
@ -359,7 +410,52 @@ class PysonicApi(object):
|
||||||
|
|
||||||
getCoverArt_view._cp_config = {'response.stream': True}
|
getCoverArt_view._cp_config = {'response.stream': True}
|
||||||
|
|
||||||
|
def response(self, status="ok"):
|
||||||
|
doc = BeautifulSoup('', features='lxml-xml')
|
||||||
|
root = doc.new_tag("subsonic-response", xmlns="http://subsonic.org/restapi", status=status, version="1.15.0")
|
||||||
|
doc.append(root)
|
||||||
|
return doc, root
|
||||||
|
|
||||||
|
def render_node(self, doc, item, item_meta, directory, dir_meta, tagname="child"):
|
||||||
|
child = doc.new_tag(tagname,
|
||||||
|
id=item["id"],
|
||||||
|
parent=item["id"],
|
||||||
|
isDir="true" if item['isdir'] else "false",
|
||||||
|
title=item_meta.get("id3_title", item["name"]),
|
||||||
|
album=item_meta.get("id3_album", item["album"]),
|
||||||
|
artist=item_meta.get("id3_artist", item["artist"]),
|
||||||
|
# playCount="5",
|
||||||
|
# created="2016-04-25T07:31:33.000Z"
|
||||||
|
# genre="Other",
|
||||||
|
# path="Cosmic Gate/Sign Of The Times/03 Flatline (featuring Kyler England).mp3"
|
||||||
|
type="music")
|
||||||
|
if 'kbitrate' in item_meta:
|
||||||
|
child.attrs["bitrate"] = item_meta["kbitrate"]
|
||||||
|
if item["size"] != -1:
|
||||||
|
child.attrs["size"] = item["size"]
|
||||||
|
if "media_length" in item_meta:
|
||||||
|
child.attrs["duration"] = item_meta["media_length"]
|
||||||
|
if "albumId" in directory:
|
||||||
|
child.attrs["albumId"] = directory["id"]
|
||||||
|
if "artistId" in directory:
|
||||||
|
child.attrs["artistId"] = directory["parent"]
|
||||||
|
if "." in item["name"]:
|
||||||
|
child.attrs["suffix"] = item["name"].split(".")[-1]
|
||||||
|
if item["type"]:
|
||||||
|
child.attrs["contentType"] = item["type"]
|
||||||
|
if 'cover' in item_meta:
|
||||||
|
child.attrs["coverArt"] = item_meta["cover"]
|
||||||
|
elif 'cover' in dir_meta:
|
||||||
|
child.attrs["coverArt"] = dir_meta["cover"]
|
||||||
|
if 'track' in item_meta:
|
||||||
|
child.attrs["track"] = item_meta['track']
|
||||||
|
if 'id3_year' in item_meta:
|
||||||
|
child.attrs["year"] = item_meta['id3_year']
|
||||||
|
return child
|
||||||
|
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
|
@formatresponse
|
||||||
def getArtistInfo_view(self, id, includeNotPresent="true", **kwargs):
|
def getArtistInfo_view(self, id, includeNotPresent="true", **kwargs):
|
||||||
# /rest/getArtistInfo.view?
|
# /rest/getArtistInfo.view?
|
||||||
# u=dave
|
# u=dave
|
||||||
|
@ -370,83 +466,70 @@ class PysonicApi(object):
|
||||||
# id=7
|
# id=7
|
||||||
# includeNotPresent=true
|
# includeNotPresent=true
|
||||||
info = self.library.get_artist_info(id)
|
info = self.library.get_artist_info(id)
|
||||||
cherrypy.response.headers['Content-Type'] = 'text/xml; charset=utf-8'
|
|
||||||
doc, root = self.response()
|
|
||||||
|
|
||||||
dirtag = doc.new_tag("artistInfo")
|
response = ApiResponse()
|
||||||
root.append(dirtag)
|
response.add_child("artistInfo")
|
||||||
|
response.set_attrs("artistInfo", **info)
|
||||||
|
|
||||||
for key, value in info.items():
|
return response
|
||||||
if key == "similarArtists":
|
|
||||||
continue
|
|
||||||
tag = doc.new_tag(key)
|
|
||||||
tag.append(str(value))
|
|
||||||
dirtag.append(tag)
|
|
||||||
yield doc.prettify()
|
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
|
@formatresponse
|
||||||
def getUser_view(self, u, username, **kwargs):
|
def getUser_view(self, u, username, **kwargs):
|
||||||
cherrypy.response.headers['Content-Type'] = 'text/xml; charset=utf-8'
|
|
||||||
doc, root = self.response()
|
|
||||||
|
|
||||||
user = {} if self.options.disable_auth else self.library.db.get_user(cherrypy.request.login)
|
user = {} if self.options.disable_auth else self.library.db.get_user(cherrypy.request.login)
|
||||||
tag = doc.new_tag("user",
|
response = ApiResponse()
|
||||||
username=user["username"],
|
response.add_child("user",
|
||||||
email=user["email"],
|
username=user["username"],
|
||||||
scrobblingEnabled="false",
|
email=user["email"],
|
||||||
adminRole="true" if user["admin"] else "false",
|
scrobblingEnabled="false",
|
||||||
settingsRole="false",
|
adminRole="true" if user["admin"] else "false",
|
||||||
downloadRole="true",
|
settingsRole="false",
|
||||||
uploadRole="false",
|
downloadRole="true",
|
||||||
playlistRole="true",
|
uploadRole="false",
|
||||||
coverArtRole="false",
|
playlistRole="true",
|
||||||
commentRole="false",
|
coverArtRole="false",
|
||||||
podcastRole="false",
|
commentRole="false",
|
||||||
streamRole="true",
|
podcastRole="false",
|
||||||
jukeboxRole="false",
|
streamRole="true",
|
||||||
shareRole="true",
|
jukeboxRole="false",
|
||||||
videoConversionRole="false",
|
shareRole="true",
|
||||||
avatarLastChanged="2017-08-07T20:16:24.596Z")
|
videoConversionRole="false",
|
||||||
root.append(tag)
|
avatarLastChanged="2017-08-07T20:16:24.596Z",
|
||||||
folder = doc.new_tag("folder")
|
folder=0)
|
||||||
folder.append("0")
|
return response
|
||||||
tag.append(folder)
|
|
||||||
yield doc.prettify()
|
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
|
@formatresponse
|
||||||
def star_view(self, id, **kwargs):
|
def star_view(self, id, **kwargs):
|
||||||
self.library.set_starred(cherrypy.request.login, int(id), starred=True)
|
self.library.set_starred(cherrypy.request.login, int(id), starred=True)
|
||||||
yield self.response()[0].prettify()
|
return ApiResponse()
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
|
@formatresponse
|
||||||
def unstar_view(self, id, **kwargs):
|
def unstar_view(self, id, **kwargs):
|
||||||
self.library.set_starred(cherrypy.request.login, int(id), starred=False)
|
self.library.set_starred(cherrypy.request.login, int(id), starred=False)
|
||||||
yield self.response()[0].prettify()
|
return ApiResponse()
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
|
@formatresponse
|
||||||
def getStarred_view(self, **kwargs):
|
def getStarred_view(self, **kwargs):
|
||||||
cherrypy.response.headers['Content-Type'] = 'text/xml; charset=utf-8'
|
|
||||||
doc, root = self.response()
|
|
||||||
tag = doc.new_tag("starred")
|
|
||||||
root.append(tag)
|
|
||||||
|
|
||||||
children = self.library.get_starred(cherrypy.request.login)
|
children = self.library.get_starred(cherrypy.request.login)
|
||||||
|
response = ApiResponse()
|
||||||
|
response.add_child("starred")
|
||||||
for item in children:
|
for item in children:
|
||||||
# omit not dirs and media in browser
|
# omit not dirs and media in browser
|
||||||
if not item["isdir"] and item["type"] not in MUSIC_TYPES:
|
if not item["isdir"] and item["type"] not in MUSIC_TYPES:
|
||||||
continue
|
continue
|
||||||
item_meta = item['metadata']
|
item_meta = item['metadata']
|
||||||
itemtype = "song" if item["type"] in MUSIC_TYPES else "album"
|
itemtype = "song" if item["type"] in MUSIC_TYPES else "album"
|
||||||
tag.append(self.render_node(doc, item, item_meta, {}, {}, tagname=itemtype))
|
response.add_child(itemtype, _parent="starred", **self.render_node2(item, item_meta, {}, {}))
|
||||||
yield doc.prettify()
|
return response
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
|
@formatresponse
|
||||||
def getRandomSongs_view(self, size=50, genre=None, fromYear=0, toYear=0, **kwargs):
|
def getRandomSongs_view(self, size=50, genre=None, fromYear=0, toYear=0, **kwargs):
|
||||||
cherrypy.response.headers['Content-Type'] = 'text/xml; charset=utf-8'
|
response = ApiResponse()
|
||||||
doc, root = self.response()
|
response.add_child("randomSongs")
|
||||||
tag = doc.new_tag("randomSongs")
|
|
||||||
root.append(tag)
|
|
||||||
|
|
||||||
children = self.library.get_songs(size, shuffle=True)
|
children = self.library.get_songs(size, shuffle=True)
|
||||||
for item in children:
|
for item in children:
|
||||||
# omit not dirs and media in browser
|
# omit not dirs and media in browser
|
||||||
|
@ -454,5 +537,6 @@ class PysonicApi(object):
|
||||||
continue
|
continue
|
||||||
item_meta = item['metadata']
|
item_meta = item['metadata']
|
||||||
itemtype = "song" if item["type"] in MUSIC_TYPES else "album"
|
itemtype = "song" if item["type"] in MUSIC_TYPES else "album"
|
||||||
tag.append(self.render_node(doc, item, item_meta, {}, self.db.getnode(item["parent"])["metadata"], tagname=itemtype))
|
response.add_child(itemtype, _parent="randomSongs",
|
||||||
yield doc.prettify()
|
**self.render_node2(item, item_meta, {}, self.db.getnode(item["parent"])["metadata"]))
|
||||||
|
return response
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
import cherrypy
|
import cherrypy
|
||||||
|
from sqlite3 import IntegrityError
|
||||||
from pysonic.api import PysonicApi
|
from pysonic.api import PysonicApi
|
||||||
from pysonic.library import PysonicLibrary, DuplicateRootException
|
from pysonic.library import PysonicLibrary, DuplicateRootException
|
||||||
from pysonic.database import PysonicDatabase
|
from pysonic.database import PysonicDatabase
|
||||||
|
@ -43,7 +44,10 @@ def main():
|
||||||
library.update()
|
library.update()
|
||||||
|
|
||||||
for username, password in args.user:
|
for username, password in args.user:
|
||||||
db.add_user(username, password)
|
try:
|
||||||
|
db.add_user(username, password)
|
||||||
|
except IntegrityError:
|
||||||
|
db.update_user(username, password)
|
||||||
|
|
||||||
logging.warning("Libraries: {}".format([i["name"] for i in library.get_libraries()]))
|
logging.warning("Libraries: {}".format([i["name"] for i in library.get_libraries()]))
|
||||||
logging.warning("Artists: {}".format([i["name"] for i in library.get_artists()]))
|
logging.warning("Artists: {}".format([i["name"] for i in library.get_artists()]))
|
||||||
|
|
|
@ -209,8 +209,13 @@ class PysonicDatabase(object):
|
||||||
|
|
||||||
def add_user(self, username, password, is_admin=False):
|
def add_user(self, username, password, is_admin=False):
|
||||||
with closing(self.db.cursor()) as cursor:
|
with closing(self.db.cursor()) as cursor:
|
||||||
cursor.execute("REPLACE INTO users (username, password, admin) VALUES (?, ?, ?)",
|
cursor.execute("INSERT INTO users (username, password, admin) VALUES (?, ?, ?)",
|
||||||
(username, self.hashit(password), is_admin)).fetchall()
|
(username, self.hashit(password), is_admin))
|
||||||
|
|
||||||
|
def update_user(self, username, password, is_admin=False):
|
||||||
|
with closing(self.db.cursor()) as cursor:
|
||||||
|
cursor.execute("UPDATE users SET password=?, admin=? WHERE username=?;",
|
||||||
|
(self.hashit(password), is_admin, username))
|
||||||
|
|
||||||
def get_user(self, user):
|
def get_user(self, user):
|
||||||
with closing(self.db.cursor()) as cursor:
|
with closing(self.db.cursor()) as cursor:
|
||||||
|
|
|
@ -4,4 +4,4 @@ MUSIC_TYPES = ["audio/mpeg", "audio/flac", "audio/x-wav"]
|
||||||
MPX_TYPES = ["audio/mpeg"]
|
MPX_TYPES = ["audio/mpeg"]
|
||||||
FLAC_TYPES = ["audio/flac"]
|
FLAC_TYPES = ["audio/flac"]
|
||||||
WAV_TYPES = ["audio/x-wav"]
|
WAV_TYPES = ["audio/x-wav"]
|
||||||
IMAGE_TYPES = ["image/jpeg", "image/png"]
|
IMAGE_TYPES = ["image/jpeg", "image/png", "image/gif"]
|
||||||
|
|
Loading…
Reference in New Issue